|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
IntroductionThe Crypto++ mailing list occasionally receives questions regarding creating
and verifying digital signatures among various libraries. This article will
examine signing and verifying messages between Crypto++, C# and Java. In
addition, the C# sample presents The Digital Signature Algorithm will be used as the test case. There are a few reasons for this choice. First is popularity. Second, as we will see below, different signatures are created for the same key and message due to a per-message random variable. Next, DSA signatures are represented in at least three different formats, which causes necessary conversions. Finally, we will use strings and streams rather than byte arrays, which adds more interoperability issues. Below, we will see that a signed message is the tuple { message, signature }. When we verify a message, we require the message, the signature, and the signer's public key. This brings to light two problem areas. The first issue is keys and their exchange. The second is defining what exactly will be signed and later verified. The first issue was examined in Cryptographic Interoperability: Keys [1]. The key interoperability article discusses importing and exporting public and private keys in Crypto++, Java, and C# in a portable manner using PKCS#8 and X.509. This article will examine the second issue — understanding what will be (or has been) signed. As with the previous article, we examine the details of the process so that when things go wrong, we can understand why and then correct the issue. Topics to be visited in this article are as follows. Though the impact of strings and streams appear early, we visit the topic last.
Our examples will use the Digital Signature Standard specified in FIPS 186-2 [11]. The standard prescribes three approved signature schemes. We will use the Digital Signature (DS) Algorithm as opposed to RSA digital signature algorithm (RSASS) or Elliptic Curve digital signature algorithm (ECDSA). FIPS 186-2 specifies the use of a 1024 bit p, a 160 bit q, and SHA-1 as the hash. FIPS 186-3 [2] uses larger hashes (SHA-2), larger values for p (up to 3072 bits), and larger values for q (up to 256 bits). FIPS 186-3 is currently in draft status. DownloadsThere are three downloads which are available. Each archive is the project for creating and verifying signatures. For those who only want the source code, Table 1 identifies the download of interest.
Digital SignaturesA digital signature is the electronic equivalent of a hand written signature. It uses a public and private key pair for its operations. The signer signs the message using the private key, and the verifier confirms the signature on the message using the public key. The DSA is a special case of the ElGamal signature system [12]. The security of DSA is derived from discrete logarithms. There are actually two instance problems: the first is logarithms in the multiplicative group Zp, for which the index-calculus method applies. The second is the logarithm problem in the cyclic subgroup q, where current methods run in square root time. DSA is a Signature Scheme with Appendix. This means the that the message must be presented to the verifier function. This is in contrast to a Signature Scheme with Recovery. In a recovery system, the message is folded into the signature, so the message does not have to be sent with the signature. The verification routine will extract the message from the signature in a recovery system. Key GenerationA DSA key is generated as follows [12]. Below, the size of q is fixed by FIPS 186 at 160 bits. Though the original FIPS 186 specification [7] specifies p between 512 to 1024 bits inclusive, FIPS 186-2 [11] fixes p at 1024. This means that some libraries enforce a bit size of 1024 at step three.
The public key is (p, q, α, y). The private key is a. We usually encounter the private key specified as x. Message SigningTo sign a document of arbitrary size using an appendix scheme, two steps occur:
In DSA, the details of signing the binary message m (document) of arbitrary length are as follows [12]. Notice that we are signing a binary message (there is no notion of a string at this level), and the message can be any length. Because the message can be any length, the message is digested with a hash function — h(m).
The signature on m is (r, s). Message m and (r, s) should be sent to the verifier. We need to observe that both r and s are 20 bytes, since a modular reduction is being performed (steps 2 and 5) using q, a 160 bit value. This will gain significance later when we begin verifying messages between Crypto++ and C# (which use the IEEE P1363 signature format) and Java (which uses a DER encoding of a signature). Message VerificationTo verify a document of arbitrary size using an appendix scheme, three steps occur:
The short story of the above is we are comparing our calculated hash of the document with the signer's calculated hash of the document after we remove the signer's encryption operation. The DSA details are as follows [12]. Below, recall that (r, s) is the signature on binary message m, with h(m) digesting the arbitrary length message.
The signature is valid if and only if v = r. Signature FormatsFor those of us who have followed Cryptographic Interoperability: Keys [1], we are not yet finished with standards and formats. There are three formats which Crypto++ supports, with IEEE P1363 being native to the library. The remaining two formats are DER encoding and OpenPGP. If we receive a format other than P1363, we would use Crypto++'s DSAConvertSignatureFormat to convert signature (r, s) to the P1363 format. Recall the signature on m is (r, s). From our exploration of Message Signing, recall that q is 160 bits. Both r and s are a residue of a modular reduction using q, so each is 160 bits (20 bytes). IEEE P1363Both Crypto++ and C# use the format described in IEEE P1363 [9]. The P1363 signature is a concatenation of r and s, denoted r || s. The concatenation results in a signature that is exactly 40 bytes in length. DER EncodingJava uses DER encoding of (r, s). According to the Java Cryptography Architecture API Specification & Reference [8], the syntax of the signature is as follows. The Java signature is consistent with DSS-Sig-Value of RFC 3279, Algorithms and Identifiers for the Internet X.509 Public Key Infrastructure [14]. Refer to Section 2.2.2, DSA Signature Algorithm. SEQUENCE ::= {
r INTEGER,
s INTEGER }
It does not appear we can request any other format from Java. This will only be a minor inconvenience in Crypto++, since the Crypto++ library offers a conversion routine. In C#, we will need to convert the format from DER to P1363. To create a
OpenPGPOpenPGP is specified in RFC 2440, "The OpenPGP Message Format" [10]. OpenPGP uses Signature Packets to represent a signature on a message. In the case of DSA, these are the two MPI (multiprecision integers) r and s. Section 5.2.2 specifies the Version 3 Signature Packet Format while Section 5.2.3 specifies the Version 4 Signature Packet Format. Again, the Crypto++ library offers a conversion routine. Generating Keys, Signing, and VerifyingThis section will examine the signing and verification process. Generating keys was visited in key interoperability, so we will focus on what is required for the case of DSA. We will also detail Crypto++ since it is not documented as well as Java and C#. Finally, to achieve interoperability, we will apply the cryptographic transformations to byte arrays of a string encoded using UTF-8. Our message will be the wide string 'Crypto Interop: \u9aa8', which is shown in Figure 1.
Crypto++Key GenerationTo generate a DSA key for signing messages, we perform the following in
Crypto++. Though we can generate the key using the DSA::Signer signer;
PrivateKey& privateKey = signer.AccessPrivateKey();
privateKey.GenerateRandom( prng );
privateKey.Save(FileSink("private.dsa.cpp.key"));
We then construct a verifier object. We do this so we can access the public
key of the pair. Unfortunately, we cannot access it through the private key. We
then save it using the overridden DSA::Verifier verifier( signer );
PublicKey& publicKey = verifier.AccessPublicKey();
publicKey.Save(FileSink("public.dsa.cpp.key"));
Message SigningWe sign our message with Crypto++ as follows. We start with a wide string.
The message is then converted to a UTF-8 string and stored in
// Crypto++ Load Private Key
DSA::Signer signer;
...
// Convert Wide String to UTF-8
wstring wide = L"Crypto Interop: \u9aa8";
string narrow;
WideCharToMultiByte( UTF8, ... );
...
const byte* data = narrow.c_str();
int length = narrow.length();
// Set up for SignMessage
byte* signature = new byte[ signer.MaxSignatureLength() ];
// PGP RandPool
AutoSeededRandomPool prng;
size_t length = signer.SignMessage( prng, data, length, signature );
After we convert the wide string to UTF8 using
Finally, we call // mfs: message filestream
// sfs: signature filestream
ofstream mfs, sfs;
mfs.open("dsa.cpp.msg", ios_base::binary );
sfs.open("dsa.cpp.sig", ios_base::binary );
// Save Message which was Signed
mfs.write( narrow.c_str(), narrow.length() );
// Save Signature on Message
sfs.write( (const char*)signature, length );
In Figure 2, we examine the contents of the message in file out.cpp.msg. We see the regular UTF-8 compression on the string, except for the last Han character which expands to three bytes.
We next examine the results of creating multiple signatures on the same message and the contents of the file dsa.cpp.sig. We run the routine twice using the same private key and compare the results side by side in Figure 3. If we recall the Message Signing process, we were required to select a random per-message value k. Because k is random, the algorithm produces different signatures on the same message.
It is important that we make the distinction that in Figure 2, dsa.cpp.msg is the message that we signed, and not the original string. When Java or C# verifies our Crypto++ message, they will verify the bytes in this file, and then reconstruct the original string. Message Signing (DER Encoded)Should our message and signature require DER encoding for systems such as Java, we perform the following. Below, the process is examined after the signing process and before we write the signature to disk. // Determine size of required buffer
length = DSAConvertSignatureFormat( NULL, 0, DSA_DER,
signature.c_str(),signature.length, DSA_P1363 );
// A buffer for the conversion
byte* buffer = new byte[ length ];
// We are P1363 format. Java desires DER encoding
length = DSAConvertSignatureFormat( buffer, length, DSA_DER,
signature.c_str(), signature.length(), DSA_P1363 );
Message VerificationThe verification process will abandon the standard library's streams in
favor of a Crypto++ // std::string used as a byte array
string message, signature;
FileSource( "dsa.cpp.msg", true, new StringSink( message ) );
FileSource( "dsa.cpp.sig", true, new StringSink( signature ) );
Next we then verify the message. Recall that Crypto++ is bytes in, bytes out
— hence the reason for a bool result = verifier.VerifyMessage(
(const byte*)message.c_str(), message.length(),
(const byte*)signature.c_str(), signature.length() );
And finally, the conversion back to a wide string, the results of which are shown in Figure 4.
Message Verification (DER Encoded)Recall that Java DER encodes the signature (r, s) on m. When we receive a DER encoded signature from Java, we perform the following. FileSource( "dsa.java.msg", true, new StringSink( message ) );
FileSource( "dsa.java.sig", true, new StringSink( signature ) );
// First, a buffer for the conversion
size_t length = verifier.SignatureLength();
byte* buffer = new byte[ length ];
// DER encoded from Java. We desire P1363 format
length = DSAConvertSignatureFormat( buffer, length,
DSA_P1363, signature.c_str(), signature.length(), DSA_DER );
// Reinitialize signature so that it can be used
// in the verifier below with minimal effort
signature = string( (const char*)buffer, length );
delete[] buffer;
// Verify the Signature on the Message
bool result = verifier.VerifyMessage(
(const byte*)message.c_str(), message.length(),
(const byte*)signature.c_str(), signature.length() );
JavaJava enjoys greater popularity with better documentation, so the following is presented for completeness. The Java Cryptography Extension (JCE) Reference Guide [8] answers most questions. Key GenerationOur code to create a DSA key pair in Java is as follows. At the completion
of the routine, we would serialize the keys for future use using
KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
kpg.initialize(1024, new SecureRandom());
KeyPair keys = kpg.generateKeyPair();
PrivateKey privateKey = keys.getPrivate();
PublicKey publicKey = keys.getPublic();
Message SigningTo sign a message using our generated keys, we perform the following. // Retrieve the Private Key
PrivateKey privateKey = LoadPrivateKey("private.dsa.java.key");
// Create the signer object
Signature signer = Signature.getInstance("DSA");
signer.initSign(privateKey, new SecureRandom());
// Prepare the Message
String s = "Crypto Interop: \u9aa8";
// Save the binary of the String which we will sign
byte[] message = s.getBytes("UTF-8");
// Sign the message
signer.update(message);
byte[] signature = signer.sign();
We then save the byte arrays Message VerificationVerifying a message is as follows. Below, we verify a message generated in C#. // Load the public
PublicKey publicKey = LoadPublicKey("public.dsa.cs.key");
// Load the message from file
byte[] message = LoadMessageFile("dsa.cs.msg");
// Load the signature on the message from file
byte[] signature = LoadSignatureFile("dsa.cs.sig");
// Initialize Signature Object
Signature verifier = Signature.getInstance("DSA");
verifier.initVerify(publicKey);
// Load the message into the verifier
verifier.update(message);
// Verify the Signature on the Message
boolean result = verifier.verify(signature);
Unlike Crypto++ and C#, the Java code expects the signature (r, s) on the message m to be in DER encoded format. Attempting to verify a P1363 signature results in an encoding exception. As a workaround, our Crypto++ and C# source code will DER encode the signature for Java. C#Key GenerationCryptographic Interoperability: Keys [1] is a fairly comprehensive treatment of generating, loading and saving keys, so we will only revisit the basics. Below we create a key pair for use in C#. CspParameters csp = new CspParameters();
csp.KeyContainerName = "DSA Test (OK to Delete)";
csp.ProviderType = PROV_DSS_DH; // 13
csp.KeyNumber = AT_SIGNATURE; // 2
DSACryptoServiceProvider dsa = new DSACryptoServiceProvider(1024, csp);
// Keys
DSAParameters privateKey = dsa.ExportParameters(true);
DSAParameters publicKey = dsa.ExportParameters(false);
Since we used the Message SigningTo sign a message, we perform the following. DSASignatureFormatter signer = new DSASignatureFormatter(dsa);
//Set the hash algorithm to SHA1.
signer.SetHashAlgorithm("SHA1");
String m = "Crypto Interop: \u9aa8";
Encoding e = Encoding.GetEncoding("UTF-8");
byte[] message = e.GetBytes(m);
// Hash the Message
SHA1 sha = new SHA1CryptoServiceProvider();
byte[] hash = sha.ComputeHash(message);
// Create the Signature for h(m)
byte[] signature = signer.CreateSignature(hash);
We would then serialize the message m and the signature (r, s) on the message m. Message VerificationFor the details of loading a DSA key, please see Cryptographic
Interoperability: Keys [1]. We reconstruct a public or private key using
AsnKeyParser keyParser = new AsnKeyParser("public.dsa.cs.key");
DSAParameters publicKey = keyParser.ParseDSAPublicKey();
Next we move on to opening the container. In this case, the
CspParameters csp = new CspParameters();
csp.KeyContainerName = "DSA Test (OK to Delete)";
csp.ProviderType = PROV_DSS; // 3
csp.KeyNumber = AT_SIGNATURE; // 2
// Load key into provider
DSACryptoServiceProvider dsa = new DSACryptoServiceProvider(csp);
dsa.ImportParameters(publicKey);
Once the provider accepts our parameters at the call to
Below, we read the byte[] arrays which constitute the message and signature. byte[] message = LoadMessage();
byte[] signature = LoadSignature();
Finally, the code to verify a signature. Interestingly,
SHA1 sha = new SHA1CryptoServiceProvider();
byte[] hash = sha.ComputeHash(message);
// Verifier
DSASignatureDeformatter verifier = new DSASignatureDeformatter(dsa);
verifier.SetHashAlgorithm("SHA1");
bool result = verifier.VerifySignature(hash, signature);
Be aware that C# can throw a
In C#, we will need to convert the format from DER to P1363. To create a
If we receive a cryptographic exception when exiting Main stating 'Keyset
does not exist,' we should explicitly dispose of the container. Multiple
methods in the sample opens a container named 'DSA Test (OK to Delete)', and
each method sets
Strings and StreamsThough strings and streams are a great convenience, they create the most problems for us when signing and verifying messages. This is because strings are simply encodings that ultimately become byte arrays, which then have a cryptographic transformation applied. Inconsistencies are usually introduced in one of two places when we convert from a string to a byte array. The first can be introduced when a stream is allowed to choose an encoding for the conversion of a string. The second is introduced when programmers (one implementing the signer, the other implementing the verifier) select different encoding conversions for the same string. Note that this situation does not arise when we explicitly use byte arrays. Since we will use strings at times, we need to decide what type of encoding to use. To this end, the Unicode Consortium recommends UTF-8 for data exchange [3]. Since nearly every major library supports UTF-8, we will use it through out for consistent results. Our choice of UTF-8 is a compromise between interoperability, compression, and channel efficiency. We should also be aware that there are other Unicode character sets (for example, SCSU and BOCU-1) that are more efficient for storage and data exchange [4,5]. The Consortium also defines how conversion occurs between character sets
such as UTF-7 and UTF-8. The conversion algorithms are implemented in Windows
functions Crypto++Crypto++ is agnostic with respect to strings and streams. For Crypto++, it is bytes in and bytes out. Unlike Java and C#, there is no method which takes a high level string. However, the C++ standard library does effect a string when using a stream. We already know that Visual Studio uses a UTF-16 encoding. In the following, we will explore the effects of a stream on the string in Visual C++. For our first example, consider the program listed below. wstring ws = L"crypto";
wofstream ofs;
ofs.open("out.cpp.bin", ios_base::binary);
ofs << ws;
ofs.close();
When we examine its file output as in Figure 8, we see that a conversion has taken place despite the fact that we are using wide versions of the standard library and specified binary mode.
To investigate further, we specify the Han character for bone which is U+9AA8. We could use a European code point, but we may as well hit the topic hard. Unfortunately, the standard C++ library has completely failed us in this case. In Figure 9 below, Visual Studio IntelliSense correctly displays the character, while the standard library produces an empty file. In fact, removing binary mode still produces the result. This is a known issue with Microsoft's stream class.
To work around this issue, we have two choices. The first workaround involves iterating over the characters of the wide string, while writing them individually to the stream as shown below. The net effect is that we are writing a UTF-16 stream. Depending on how we chose to output the bytes, we achieve either a big endian (UTF-16BE) or little endian (UTF-16LE) stream. The result of the single Asian character is shown in Figure 10. wstring::const_iterator it = ws.begin();
for( ; it != ws.end; it++ )
{
// Little Endian
ofs.put( (*it & 0x00FF) );
ofs.put( (*it & 0xFF00)>>8 )
}
In the second, we use wstring ws = L"\u9aa8";
char* utf = NULL;
// UTF-8 Encode
int nChars = WideCharToMultiByte( CP_UTF8, 0, ws.c_str(), -1, NULL, 0, NULL, FALSE );
utf = new char[ nChars ];
WideCharToMultiByte( CP_UTF8, 0, ws.c_str(), -1, utf, nChars, NULL, FALSE );
ofstream ofs;
ofs << utf;
...
JavaWe will now explore what occurs in Java. We have two cases to examine:
writing the string using the stream's DataOutputStream dos = new DataOutputStream( new FileOutpuStream("out.java.bin"));
String s = "crypto";
dos.writeUTF(s);
In Figure 12, we see that the
Next, we modify the Java program to explore the various byte arrays returned
from DataOutputStream dos = new DataOutputStream( new FileOutputStream("out.java.bin"));
String s = "crypto";
byte[] b = s.getBytes();
dos.write(b, 0, b.length);
Using byte[] b = s.getBytes("UTF16");
In Figure 13, we see that we have a big endian array with a byte order mark. Again, for interoperability, this is probably a poor choice.
When we run the Java program using C#Our first C# example examines the result of using a default encoding when
writing a string with a using (TextWriter writer = new StreamWriter("out.cs.bin"))
{
String s = "crypto";
writer.Write(s);
}
As with Java, we observe the stream's use of a default encoding which is UTF-8. Next we modify our program to use a UTF-16 encoding. using (BinaryWriter writer = new BinaryWriter(
new FileStream("out.cs.bin", FileMode.Create, FileAccess.ReadWrite)))
{
String s = "crypto";
Encoding e = Encoding.GetEncoding("UTF-16");
byte[] b = e.GetBytes(s);
writer.Write(b);
}
The results are shown in Figure 15. We observe that a little endian stream is written. Additionally, unlike Java, there is no byte order mark.
As with Java, we have to contend with byte order when using UTF-16. This again leads us UTF-8, which does not have byte order and byte order mark issues. Acknowledgements
Checksums
References[1] J. Walton, Cryptographic Interoperability: Keys, April 2008, CryptoInteropKeys.aspx. [2] FIPS 186-3 Draft, Digital Signature Standard, http://csrc.nist.gov/publications/drafts/fips_186-3/Draft-FIPS-186-3%20_March2006.pdf. [3] Unicode Consortium, UTF-16 for Processing, Unicode Technical Note #12, http://unicode.org/notes/tn12/. [4] Unicode Consortium, A Survey of Unicode Compression, Unicode Technical Note #14, http://unicode.org/notes/tn14/. [5] Unicode Consortium, Fast Compression Algorithm for Unicode Text, Unicode Technical Note #31, http://unicode.org/notes/tn31/. [6] D. Schmitt, International Programming for Microsoft Windows, Microsoft Press, ISBN 1-5723-1956-9. [7] MSDN, WideCharToMultiByte, http://msdn2.microsoft.com/en-us/library/ms776420(VS.85).aspx. [8] Java Cryptography Architecture(JCA) Reference Guide, http://java.sun.com/javase/6/docs/technotes/guides/security/crypto/CryptoSpec.html. [9] IEEE P1363, Standard Specifications For Public-Key Cryptography. [10] RFC 2440, OpenPGP Message Format, November 1998, http://www.ietf.org/rfc/rfc2440.txt. [11] FIPS 186-2, Digital Signature Standard, January 2007, http://csrc.nist.gov/publications/fips/fips186-2/fips186-2-change1.pdf. [12] A. Menenzes, et al., Handbook of Applied Cryptography, CRC Press, ISBN 0-8493-8523-7, pp. 451-2. [13] RFC 3275, XML-Signature Syntax and Processing, March 2002, http://www.ietf.org/rfc/rfc3275.txt. [14] RFC 3279, Algorithms and Identifiers for the Internet X.509 Public Key Infrastructure, April 2002, http://www.ietf.org/rfc/rfc3279.txt.
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||