Security

Applying Cryptography Using The CNG API In Windows Vista

Kenny Kerr

This article discusses:

  • Differences between CryptoAPI and CNG
  • Algorithm providers
  • Random numbers, encryption, signatures, and verification
  • Interop with .NET applications
This article uses the following technologies:
Windows Vista

Code download available at: CNG 2007_07.exe(158 KB)

Contents

Algorithm Providers
Random Number Generation
Hash Functions
Symmetric Encryption
Asymmetric Encryption
Signatures and Verification
Interop with .NET
Looking Forward

Windows Vista™ introduces a new cryptography API to replace the old CryptoAPI, which has its roots in the early versions of Windows® NT and Windows 95. Cryptography Next Generation (CNG) is meant to be a long-term replacement for the CryptoAPI, providing substitutes for all of the cryptographic primitives it offered. CNG supports all of the algorithms provided by the CryptoAPI, but goes much further and includes many new algorithms and a much more flexible design, providing developers with greater control over how cryptographic operations are performed and how algorithms work together to perform various operations.

Figure 1 illustrates the design of CNG at a high level. This article focuses on BCrypt, the subset of CNG that provides the cryptographic primitives such as random number generation, hash functions, signatures, and encryption keys. In contrast, NCrypt is the subset of CNG that provides key storage facilities to support persisting asymmetric keys and hardware such as smart cards. Don’t read too much into the naming of BCrypt and NCrypt. BCrypt is simply the name of the header file and DLL that provides the base services for CNG. "B" stands for "base" in this case. Similarly, NCrypt is simply the name of the header file and DLL that provides the higher-level key storage functionality. "N" stands for "new."

Figure 1 CNG Architecture

Figure 1** CNG Architecture **

The cryptographic primitives provided by BCrypt are available directly in kernel mode, providing for the first time a common cryptographic framework for both user-mode and kernel-mode applications. The key storage facilities provided by NCrypt, however, are only available to user-mode applications.

There are two main ways to view the cryptographic primitives provided by CNG. The first is as a set of logical objects that provide methods you may call and properties you may query and sometimes modify. These objects include algorithm providers, hash functions, keys, and secret agreements. Where do random number generators, signatures, and different types of keys come in? As it turns out, random number generation is handled directly by algorithm providers, and keys provide almost everything else. They can represent symmetric or asymmetric keys, and they are used to sign and verify hash signatures. Hash and key objects are derived from algorithm providers and secret agreements are derived from a pair of key objects—the public key of a principal and the private key of another principal wishing to communicate securely.

The other way to look at CNG is as a software router or intermediary for cryptographic operations. The CNG API is built on a set of logical cryptographic interfaces. As an example, you can use the hash interface without hard-coding a particular hash algorithm implementation. Most cryptography APIs take an algorithm-centric approach, whereas CNG takes an interface-centric approach. This provides greater agility both for the developer and for the application administrator who might need to replace an algorithm used by an application that is found to be flawed after that application has shipped. Figure 2 illustrates this interface-centric view of CNG.

Figure 2 CNG Interfaces

Figure 2** CNG Interfaces **(Click the image for a larger view)

The CNG configuration system deserves an article of its own, but for now it is sufficient to know that CNG allows developers to request algorithms without having to specify algorithm providers. The providers are responsible for the algorithm implementation, and it is this aspect of CNG that allows administrators to reconfigure applications to use different implementations either declaratively in an application or through system policy.

Algorithm Providers

With that introduction out of the way, let’s see how we can use CNG to perform various common cryptographic operations. The first thing you need is an algorithm provider. All CNG objects defined by BCrypt are identified by a BCRYPT_HANDLE, and algorithm providers are no exception. The BCryptOpenAlgorithmProvider function loads an algorithm provider based on your choice of algorithm and optional implementation and returns a handle for use in subsequent calls to CNG functions. BCrypt also adopts the NTSTATUS type from the Windows Driver Kit (WDK) for indicating error information regardless of whether you are coding in user mode or kernel mode. Here’s how you load an algorithm provider into memory:

BCRYPT_HANDLE algorithmProvider = 0;

NTSTATUS status = ::BCryptOpenAlgorithmProvider(
                      &algorithmProvider, algorithmName,
                      implementation, flags);

if (NT_SUCCESS(status))
{
    // Use algorithm provider
}

In most cases you will pass zero for both the implementation and flags parameters. Passing zero for implementation indicates that the default algorithm provider should be loaded for the particular algorithm identified by the algorithmName parameter. The NT_SUCCESS macro should be familiar to those who have used the WDK in the past. It is a macro similar to SUCCEEDED used by COM developers and indicates whether the status value represents success or failure.

When you are finished with the algorithm provider, you must unload it by passing the handle returned by BCryptOpenAlgorithmProvider to the BCryptCloseAlgorithmProvider function as you see here:

  status = ::BCryptCloseAlgorithmProvider(
    algorithmProvider, flags);
  ASSERT(NT_SUCCESS(status));

No flags are currently defined for this function so you must pass zero for the flags parameter. Naturally, the assertion is not required, but is useful in weeding out bugs in your application.

The remainder of this article uses the NT_VERIFY macro to verify the result of CNG functions. This is just a placeholder to remind you that you need to check the status code that gets returned. If you are following along you can define the macros as follows:

#define NT_VERIFY(x) ASSERT(NT_SUCCESS(x))

Just be sure to use error handling that conforms to your project’s error handling strategy in production code.

Loading algorithm providers can be an expensive operation, so it is wise to consider the usage pattern of the providers in your application and reuse them as much as is reasonable. The remainder of this article makes use of algorithm providers to achieve various goals, so hold on to your handles!

Random Number Generation

Random number generation is provided courtesy of the BCryptGenRandom function. It could easily have been called random buffer generation as it allows you to fill any buffer with a random value. Let’s say you need to generate 10 random numbers. Figure 3 shows how you might accomplish that.

Figure 3 Random Numbers

BCRYPT_HANDLE algorithmProvider = 0;

NT_VERIFY(::BCryptOpenAlgorithmProvider(
    &algorithmProvider, 
    BCRYPT_RNG_ALGORITHM,
    0,                     // implementation,
    0));                   // flags

for (int i = 0; i < 10; ++i)
{
    UINT random = 0;

    NT_VERIFY(::BCryptGenRandom(
        algorithmProvider, 
        reinterpret_cast<PUCHAR>(&random),
        sizeof(UINT), 
        0));

    cout << random << endl;
}

In this example, I requested the default random number generation algorithm by specifying the BCRYPT_RNG_ALGORITHM algorithm identifier. The BCRYPT_RNG_FIPS186_DSA_ALGORITHM algorithm identifier is also available should you need an algorithm that meets Federal Information Processing Standards (FIPS) for use with the Digital Signature Algorithm (DSA).

As you can see from the example in Figure 3, the BCryptGenRandom function expects a pointer to a UCHAR to identify the buffer instead of the more practical pointer to void (any type). Fortunately, this is easily solved in a type-safe way with a little help from C++. Take a look at the following function template:

template <typename T>
__checkReturn
NTSTATUS GenRandom(BCRYPT_HANDLE algorithmProvider,
                   T& buffer, ULONG flags)
{
    ASSERT(0 != algorithmProvider);

    return ::BCryptGenRandom(algorithmProvider,
                             reinterpret_cast<PUCHAR>(&buffer),
                             sizeof(T), flags);
}

If you’re not familiar with Standard Annotation Language (SAL), the __checkReturn macro is simply a hint to analysis tools indicating that callers of the function should be checking the result of the function.

Although this function template is not suitable in all cases (such as if you wish to populate a buffer wrapped in a collection class), it does make generating a random number much more convenient in most cases as the casting is isolated within the function template. The template will also determine the size of the buffer automatically. Consider the following example of generating 10 random GUID values:

for (int i = 0; i < 10; ++i)
{
    GUID random = { 0 };

    NT_VERIFY(GenRandom(algorithmProvider, random, 0));

    // Use ‘random’ value
}

The BCryptGenRandom function supports an optional flag that allows you to provide additional entropy for the random number generation algorithm. The BCRYPT_RNG_USE_ENTROPY_IN_BUFFER flag indicates that the algorithm should use the value in the buffer passed to the function as additional entropy in calculating the random number that is then returned in the same buffer. Here is an example:

UINT random = 0;

for (int i = 0; i < 10; ++i)
{
    NT_VERIFY(GenRandom(algorithmProvider, random,
                        BCRYPT_RNG_USE_ENTROPY_IN_BUFFER));

    // Use ‘random’ value
}

Notice that I moved the definition of the random variable outside of the loop so that the value of the previous random number provides additional entropy for the next generated number.

Hash Functions

Like random number generation, hash functions play a critical role in enabling security measures and features in many aspects of modern computing. Hash functions are exposed as objects in CNG, but because the API needs to be accessible from kernel-mode code (which is mostly written in C), it takes a bit of up-front work to put the object back together. As I mentioned in the introduction, algorithm providers and hash functions are represented by objects in CNG. Since these are part of BCrypt, you can employ the BCryptGetProperty and BCryptSetProperty functions to query and set named properties for a particular object. Matching functions are provided for handling object properties with NCrypt.

The BCryptCreateHash function creates a new hash object. Before you call it, however, you need to have allocated a buffer that the hash function will use for processing. Here you can see one of the side effects of supporting both user mode and kernel mode in a single API—kernel mode is very sensitive regarding where memory is allocated and in particular whether the allocation is from paged or non-paged memory. Aside from the buffer, you are responsible for managing the handle and hash table resources for the hash object.

The size of the buffer required to create the hash object is a property of the algorithm provider and the BCRYPT_OBJECT_LENGTH identifier gives you the name of the property to query. Figure 4 shows how you might load the algorithm provider for the SHA-256 hashing algorithm and query the hash buffer size.

Figure 4 Using an Algorithm Provider

BCRYPT_HANDLE algorithmProvider = 0;

NT_VERIFY(::BCryptOpenAlgorithmProvider(
    &algorithmProvider, 
    BCRYPT_SHA256_ALGORITHM,
    0,                      // implementation,
    0));                    // flags

ULONG hashBufferSize = 0;
ULONG bytesCopied = 0;

NT_VERIFY(::BCryptGetProperty(
    algorithmProvider, 
    BCRYPT_OBJECT_LENGTH,
    reinterpret_cast<PUCHAR>(&hashBufferSize),
    sizeof(ULONG), 
    &bytesCopied, 0));

ASSERT(sizeof(ULONG) == bytesCopied);

BCryptGetProperty’s first parameter indicates the object to query. The second parameter indicates the name of the property. The third and fourth identify the destination buffer where the property value is stored as well as the size of the buffer. The bytesCopied parameter is useful in cases where you do not know beforehand what the expected buffer size is. No flags are currently defined for this function so you must pass zero for the flags parameter.

Having determined the size of the hash buffer, you need to commit a chunk of memory for the hash object. With the exception of kernel mode, it does not matter too much where you allocate this buffer, and you are free to use whatever storage is most appropriate as long as it is stable (in other words, you cannot use an unpinned managed byte array). Figure 5 provides a straightforward Buffer class that you can use for simple user-mode buffer allocations. This class will be used again when creating encryption key objects so it is worthwhile to have a Buffer class of some kind on hand.

With all of the prerequisites out of the way, you can now finally create the hash object with the BCryptCreateHash function as shown here:

Buffer hashBuffer;
NT_VERIFY(hashBuffer.Create(hashBufferSize));

BCRYPT_HANDLE hash = 0;

NT_VERIFY(::BCryptCreateHash(algorithmProvider, &hash,
                             hashBuffer.GetData(),
                             hashBuffer.GetSize(),
                             0,   // secret
                             0,   // secret size
                             0)); // flags

BCryptCreateHash’s first parameter indicates the algorithm provider that implements the hash interface. The second parameter receives the handle to the hash object. The third and fourth parameters identify the hash buffer and its size. Keyed hash algorithms use the secret parameters to identify the secret key. No flags are currently defined for this function so you must pass zero for the flags parameter. When you are finished with the hash object, you must destroy it using the BCryptDestroyHash function and then free the hash object buffer.

Now that you’ve seen how to create and destroy hash objects, let’s actually do something useful with them. After a hash object is created, you can call the BCryptHashData function to perform a one-way hash of a buffer. This function can be called repeatedly to combine additional buffers into the hash. Keep in mind that the size of a hash value is fixed and determined by the algorithm provider so no matter how many times you call BCryptHashData, the resulting hash value will always be the same size.

This example illustrates how to hash data read from a stream using the COM IStream interface:

BYTE buffer[256] = { 0 };
ULONG bytesRead = 0;

while (SUCCEEDED(stream->Read(buffer, _countof(buffer),
                              &bytesRead)) && 0 < bytesRead)
{
    NT_VERIFY(::BCryptHashData(hash, buffer, bytesRead, 0));
}

BCryptHashData’s first parameter is the handle returned from the BCryptCreateHash function. The second and third parameters indicate the buffer containing the data to hash as well as the size of the buffer. Notice that I am careful to pass bytesRead for the size of the buffer to ensure that I include only data actually read from the stream. Finally, no flags are yet defined for BCryptHashData so I must pass zero for the last parameter.

Once you pass all the data to the hash function, you can retrieve the resulting hash value by calling the BCryptFinishHash function. The size of the resulting hash value is dependent on the particular hash algorithm in use. If you do not know this size, you can query the hash object with the BCRYPT_HASH_LENGTH identifier. In the following example, I query the hash value size, create a buffer for it, and finally copy the hash value into the buffer using BCryptFinishHash:

ULONG hashValueSize = 0;
NT_VERIFY(::BCryptGetProperty(hash, BCRYPT_HASH_LENGTH, ...))

Buffer hashValue;
NT_VERIFY(hashValue.Create(hashValueSize));

NT_VERIFY(::BCryptFinishHash(hash,
                             hashValue.GetData(),
                             hashValue.GetSize(),
                             0)); // flags

The pattern is probably becoming pretty familiar to you. BCryptFinishHash’s first parameter is the hash object handle. The second and third parameters indicate the buffer that will receive the hash value as well as the buffer’s size. The last parameter indicates the flags, of which there are none at present.

Finally, hash objects can be duplicated. This is useful in the case where you want to produce two or more hash values based on some common data. You can start by creating a single hash object to hash the common data and then duplicate it one or more times, adding any deltas to the duplicates. Once a hash object has been duplicated, the two hash objects contain the same state but have no connection to one another and you are free to add unique data to each one, produce a hash value, or destroy one of the hash objects without affecting the other in any way.

The BCryptDuplicateHash function takes care of duplicating a hash object. You need to simply provide a handle to the hash object to duplicate as well as a new buffer that it will use for processing:

Buffer newHashBuffer;
NT_VERIFY(newHashBuffer.Create(hashBufferSize));
BCRYPT_HANDLE newHash = 0;

NT_VERIFY(::BCryptDuplicateHash(hash,
                                &newHash,
                                newHashBuffer.GetData(),
                                newHashBuffer.GetSize(),
                                0));

Symmetric Encryption

Symmetric encryption begins in much the same way as hash functions in that you need to create and prepare the symmetric key and its object buffer. The steps should be familiar: create an algorithm provider, determine the size of the key object buffer by querying the provider’s BCRYPT_OBJECT_LENGTH property, and allocate a buffer of that size. A call to the BCryptGenerateSymmetricKey function with a secret to initialize it creates the symmetric key. Figure 6 illustrates this process.

Figure 6 Creating a Symmetric Key Object

Figure 6** Creating a Symmetric Key Object **

As I’ve already covered opening algorithm providers and creating buffers in previous sections, I’ll skip ahead and focus here on generating the symmetric key. This code illustrates a typical call to BCryptGenerateSymmetricKey:

BCRYPT_KEY_HANDLE key = 0;

NT_VERIFY(::BCryptGenerateSymmetricKey(algorithmProvider,
                                       &key,
                                       keyBuffer.GetData(),
                                       keyBuffer.GetSize(),
                                       secret.GetData(),
                                       secret.GetSize(),
                                       0)); // flags

The first parameter indicates the algorithm provider that implements a symmetric encryption algorithm. The second parameter receives the handle to the key object. The third and fourth parameters identify the key buffer and its size. The next two parameters indicate a buffer containing the secret key shared by the sender and receiver. This can be any byte array—and can even be empty—but is typically a hash of a password of some kind. No flags are currently defined for this function so you must pass zero for the flags parameter.

When you are finished with the key object, you must destroy it using the BCryptDestroyKey function and then free the key object buffer.

When you’ve created a key object, chances are you want to actually encrypt and decrypt some data, so let’s get to it. The aptly named BCryptEncrypt and BCryptDecrypt functions are used to encrypt and decrypt data for both symmetric and asymmetric keys. With the exception of padding schemes and initialization vectors, the functions are used in the same way regardless of the type of key in use. These functions may seem intimidating at first due to their long parameter list, but just keep in mind that they accept a set of common parameters that must match in order for data to be successfully decrypted. The sender and receiver in a symmetric encryption operation must share a few common properties. They need a key created with the same secret and matching property values. They need initialization vectors that are equal. And they need the same padding.

Symmetric encryption is surprisingly simple to use—the trouble comes when you consider sharing all these properties between parties. The first piece of information you need to determine is the size of a block of data for the algorithm:

ULONG blockSize = 0;
NT_VERIFY(::BCryptGetProperty(key, BCRYPT_BLOCK_LENGTH, ...))

The block size is useful for a number of reasons when working with block cipher algorithms, the most common form of symmetric algorithm. Block ciphers encrypt a fixed sized block of plaintext into a block of ciphertext of the same size. The block size also indicates the size of the initialization vector if used.

Once you have prepared the message to encrypt as well as the initialization vector, you can call the BCryptEncrypt function to encrypt the plaintext message. Of course you will need to determine the size of the buffer that will receive the ciphertext. To accomplish this you simply call BCryptEncrypt and pass zero for both the output and output size parameters. BCryptEncrypt will then return the necessary size.

ULONG ciphertextSize = 0;

NT_VERIFY(::BCryptEncrypt(key,
                          message.GetData(),
                          message.GetSize(),
                          0,   // padding info
                          iv.GetData(),
                          iv.GetSize(),
                          0,   // output
                          0,   // output size,
                          &ciphertextSize,
                          0)); // flags

The first parameter indicates the key to use for encryption. The second and third parameters identify the message to encrypt. The fourth parameter provides additional padding information for asymmetric encryption. It is not used by symmetric algorithms so you must pass zero for this parameter. The next two parameters indicate a buffer containing the initialization vector to use. The second-to-last parameter receives the size of the expected ciphertext and the last parameter accepts optional parameters. Only the BCRYPT_BLOCK_PADDING flag is used with symmetric algorithms and is handy when you don’t want to worry about padding a particular message to a multiple of the block size.

You can then create the ciphertext buffer and call BCryptEncrypt again, this time providing the buffer to receive the ciphertext:

Buffer ciphertext;
NT_VERIFY(ciphertext.Create(ciphertextSize));

NT_VERIFY(::BCryptEncrypt(key,
                          message.GetData(),
                          message.GetSize(),
                          0,   // padding info
                          iv.GetData(),
                          iv.GetSize(),
                          ciphertext.GetData(),
                          ciphertext.GetSize(),
                          &ciphertextSize,
                          0)); // flags

The decryption process is much the same. If you don’t know the size of the plaintext, you call the BCryptDecrypt function in much the same way as illustrated previously for determining the size of the ciphertext. Finally, you call BCryptDecrypt with the ciphertext, initialization vector, and the plaintext buffer to receive the resulting decrypted message:

Buffer plaintext;
NT_VERIFY(plaintext.Create(plaintextSize));

NT_VERIFY(::BCryptDecrypt(newKey,
                          ciphertext.GetData(),
                          ciphertext.GetSize(),
                          0,   // padding info
                          iv.GetData(),
                          iv.GetSize(),
                          plaintext.GetData(),
                          plaintext.GetSize(),
                          &plaintextSize,
                          0)); // flags

Be sure to specify the same flags when decrypting a message as you did when encrypting it.

Asymmetric Encryption

Asymmetric encryption solves the problems related to sharing secrets and initialization vectors by taking a public-key approach whereby both parties maintain a private and public key. The public keys are made available to anyone. The keys are related such that only the private key can be used to decipher data encrypted with the public key. Naturally, this additional power comes at a cost and asymmetric encryption turns out to be considerably more computationally expensive when compared to symmetric encryption. It is, nevertheless, invaluable if only to establish communication by providing a mechanism for parties to securely share symmetric encryption key information. Figure 7 illustrates the process by which asymmetric keys are created.

Figure 7 Creating an Asymmetric Key

Figure 7** Creating an Asymmetric Key **

The following example illustrates calling the BCryptGenerateKeyPair function to generate a public and private key pair:

NT_VERIFY(::BCryptGenerateKeyPair(algorithmProvider,
                                  &key, keySize,
                                  0)); // flags

The first parameter indicates the algorithm provider that implements the asymmetric encryption algorithm. The second parameter receives the handle to the key object. The third parameter indicates the key size the algorithm should use. This value is expressed in bits, not bytes, and varies from one algorithm to another. For example, the RSA algorithm supports key sizes that are multiples of 64 between 512 and 16384 bits, inclusive. Generally speaking, the size of the key directly affects the performance of the algorithm, but more importantly affects the cost of defeating the encryption using a brute force attack. The key size is also important for another, less ominous reason: it indicates the block size that the algorithm will use. To determine the block size, simply divide the key size by 8.

Once you have generated the key pair with BCryptGenerateKeyPair, you may need to set various algorithm-specific key properties using the BCryptSetProperty function. Before you can use the key pair, however, you need to call the BCryptFinalizeKeyPair function to finalize the creation of the key object, as follows:

   NT_VERIFY(::BCryptFinalizeKeyPair(key, 0));

At this point, you can happily encrypt and decrypt data using the BCryptEncrypt and BCryptDecrypt functions discussed previously. About the only thing that’s slightly different in this case is that asymmetric keys ignore the initialization vector, and the padding scheme may be a bit more complex depending on which flags you choose. Assuming the message to be encrypted is provided in a buffer that is a multiple of the block size, you simply specify the BCRYPT_PAD_NONE flag as no padding is necessary. On the other hand, if you want to encrypt a message without having to first pad it to a multiple of the block size, then you have a few options. The simplest option is to specify the BCRYPT_PAD_PKCS1 flag, which tells the algorithm provider to pad the input buffer to a multiple of the block size using a random number based on the PKCS-1 standard.

Of course, the usefulness of asymmetric encryption is largely defeated if you can’t share the public portion of a key pair, so let’s take a look at how to do this. The BCryptExportKey and BCryptImportKeyPair functions can be used to export and import keys. Incidentally, BCryptExportKey can also be used to export symmetric keys, but you need to employ the BCryptImportKey function to import them since it needs to associate a buffer with the key object. BCryptExportKey and BCryptImportKeyPair can be used to export either the complete public and private key pair or just the public key. You might, for example, export the key pair and persist it for your own use at a later time, and export the public key separately to offer to those wanting to communicate with you.

The code in Figure 8 illustrates how to create a binary blob containing only the public key. As you can see, BCryptExportKey follows the same pattern we’ve seen before in that you first call it to determine the size of the blob and then you call it again to actually receive the public key information. The function’s third parameter is notable since it indicates both the algorithm and what exactly will be exported.

Figure 8 Creating a Public Key Binary Blob

ULONG publicKeyBlobSize = 0;

NT_VERIFY(::BCryptExportKey(key,
                            0,                      // reserved
                            BCRYPT_RSAPUBLIC_BLOB,
                            0,                      // output,
                            0,                      // output size
                            &publicKeyBlobSize,
                            0));                    // flags

Buffer publicKeyBlob;
NT_VERIFY(publicKeyBlob.Create(publicKeyBlobSize));

NT_VERIFY(::BCryptExportKey(key,
                            0,                      // reserved
                            BCRYPT_RSAPUBLIC_BLOB,
                            publicKeyBlob.GetData(),
                            publicKeyBlob.GetSize(),
                            &publicKeyBlobSize,
                            0));                    // flags

In the example, I used BCRYPT_RSAPUBLIC_BLOB to indicate that I want to export only the public key portion of an RSA key pair. If you wanted to export both public and private keys, you simply use BCRYPT_RSAPRIVATE_BLOB instead. Don’t let the name fool you—it contains both the private and the public key. This is important to grasp since only the public key is used for encryption, so if you exported the private key and then imported it, you’d be able to encrypt a message, but it would use the public key and not the private key for encryption. The private key is only used for decryption.

Importing a key from a blob is straightforward:

BCRYPT_HANDLE publicKey = 0;

NT_VERIFY(::BCryptImportKeyPair(algorithmProvider,
                                0,   // reserved
                                BCRYPT_RSAPUBLIC_BLOB,
                                &publicKey,
                                publicKeyBlob.GetData(),
                                publicKeyBlob.GetSize(),
                                0)); // flags

The third parameter is again notable as it indicates the anatomy of the blob that is being imported. Just make sure the algorithm provider implements the algorithm being imported and it should work just fine. The resulting public key handle can then safely be used to encrypt messages that only the holder of the private key can decipher.

Signatures and Verification

A common use of asymmetric cryptography is the creation of digital signatures. They are used heavily by Windows, as well as by the Microsoft® .NET Framework, to certify the authenticity of messages, executables, and assemblies.

Unlike the asymmetric encryption and decryption process described already, signatures are created using a private key and verified using a public key. This allows someone holding a public key to verify that a given signature was generated by the holder of a private key without needing access to that private key.

The process of creating or verifying a signature is pretty straightforward given the groundwork already covered. Start by identifying the data you want to sign and then hash the data as described in the section on hash functions. The signature is then calculated by the BCryptSignHash function based on the hash value as well as a private key for a digital signature algorithm. Figure 9 provides a simple example.

Figure 9 Calculating a Signature

ULONG signatureSize = 0;

NT_VERIFY(::BCryptSignHash(keyPair,
                           0,                  // padding info
                           hashValue.GetData(),
                           hashValue.GetSize(),
                           0,                  // output
                           0,                  // output size
                           &signatureSize,
                           0));                // flags

Buffer signature;
NT_VERIFY(signature.Create(signatureSize));

NT_VERIFY(::BCryptSignHash(keyPair,
                           0,                  // padding info
                           hashValue.GetData(),
                           hashValue.GetSize(),
                           signature.GetData(),
                           signature.GetSize(),
                           &signatureSize,
                           0));                // flags

As usual, the first call calculates the size of the resulting signature and the second actually computes the signature. Depending on the size of the hash value and the algorithm being used, you may need to provide additional padding information.

Verifying the signature is even simpler. The idea is to compute the hash independently and then pass this hash value, the public key of the signer, and the signature you received to the BCryptVerifySignature function for verification. BCryptVerifySignature returns STATUS_SUCCESS if the signature matches the hash value and STATUS_INVALID_SIGNATURE if it does not. Figure 10 gives you an idea what it might look like.

Figure 10 Verifying a Signature

NTSTATUS status = ::BCryptVerifySignature(publicKey,
                                          0,              // padding info
                                          hashValue.GetData(),
                                          hashValue.GetSize(),
                                          signature.GetData(),
                                          signature.GetSize(),
                                          0);             // flags
switch (status)
{
    case STATUS_SUCCESS:
    {
        // The signature matches the hash.
        break;
    }
    case STATUS_INVALID_SIGNATURE:
    {
        // The signature does not match the hash.
        break;
    }
    default:
    {
        // An error occurred.
    }
}

Interop with .NET

The cryptography libraries included with the .NET Framework 2.0 and 3.0 provide a variety of managed algorithm implementations as well as wrappers around native implementations from the CryptoAPI. In many cases you will be able to interoperate with CNG data without any trouble, though in some cases the .NET Framework cryptography classes use inappropriate default algorithm properties that may force you to do some troubleshooting. The trick is to match every single key property between CNG and its managed equivalent, and then it should work just fine.

In some ways, working with CNG is quite a bit simpler than working with the managed cryptography classes as there are fewer abstractions and inherited defaults to think about. Effective cryptography relies on programmers being explicit about every single detail.

For symmetric encryption, keep the following properties of the .NET Framework SymmetricAlgorithm class in mind as you prepare to exchange ciphertext between CNG and the .NET Framework: BlockSize, IV, Key, KeySize, Mode, and Padding. Getting just one of these slightly wrong will lead to failures. Although the defaults used by CNG and the .NET Framework are not always the same, you can usually find common values for things like block sizes and padding schemes that are compatible across implementations.

Getting asymmetric encryption working across implementations is a bit more work as different algorithms employ vastly different properties, so finding a common list of properties to match up is not going to do it. Instead, you need to focus on how the different implementations import and export key information and provide the necessary translation yourself. Let’s look at a concrete example to illustrate the process of communicating a public key across implementations.

The RSA algorithm accepts the following general parameters: a public and private exponent, a modulus, a pair of prime numbers, another pair of exponents, and a coefficient. Although this may be interesting to cryptographers and mathematicians, most developers just want to know how to share the public portion of the key pair and, in the case of the RSA algorithm, you simply need to share the public exponent and the modulus values.

CNG provides the BCryptExportKey function to assist in exporting an encryption key to a binary blob that can be persisted or shared as necessary. Note that it follows the same pattern you’ve seen over and over where you need to call it a first time to determine the size of the blob and then again with the newly allocated buffer ready to receive the exported key information. The only thing that is worth mentioning is the third parameter, shown here:

NT_VERIFY(::BCryptExportKey(key,
                            0,   // reserved
                            BCRYPT_RSAPUBLIC_BLOB,
                            blob.GetData(),
                            blob.GetSize(),
                            &blobSize,
                            0)); // flags

This parameter indicates the blob type to produce. In this example, the BCRYPT_RSAPUBLIC_BLOB identifier indicates that the function should export only the public portion of the RSA key.

Now comes the fun part. You need to crack open that blob to find out what’s inside of it since the .NET Framework RSA classes use a different format for importing key information. The blob is actually a simple format starting with the BCRYPT_RSAKEY_BLOB structure as a header. This header indicates the type of blob as well as the extents of the various key parameters. Figure 11 illustrates the memory layout of a public key blob.

Figure 11 Memory Layout for Public Key Blob

Figure 11** Memory Layout for Public Key Blob **

With this information in hand, it’s simply a matter of copying the necessary memory ranges and populating an RSAParameters structure, which is what the .NET Framework RSA classes expect when importing key information.

As you can see in Figure 12, I have used C++/CLI to directly convert a native RSA key blob into a managed RSAParameters structure. The structure’s Exponent and Modulus fields are assigned newly created managed byte arrays and then the values are copied directly into those byte arrays by first pinning them and then using the memcpy_s function. Suppose you had received the binary blob in C#, for example, you could have achieved the same results using the .NET Framework BinaryReader class. The rest of the process is straightforward:

Figure 12 Converting RSA Key Blob to .NET Structure

BCRYPT_RSAKEY_BLOB* rsaBlob = 
    reinterpret_cast<BCRYPT_RSAKEY_BLOB*>(blob.GetData());
RSAParameters rsaParams;

rsaParams.Exponent = gcnew array<BYTE>(rsaBlob->cbPublicExp);
{
    pin_ptr<BYTE> destination = &rsaParams.Exponent[0];
    const BYTE* source = reinterpret_cast<BYTE*>(rsaBlob);

    memcpy_s(destination,
             rsaParams.Exponent->Length,
             source + sizeof(BCRYPT_RSAKEY_BLOB),
             rsaBlob->cbPublicExp);
}

rsaParams.Modulus = gcnew array<BYTE>(rsaBlob->cbModulus);
{
    pin_ptr<BYTE> destination = &rsaParams.Modulus[0];
    const BYTE* source = reinterpret_cast<BYTE*>(rsaBlob);

    memcpy_s(destination,
             rsaParams.Modulus->Length,
             source + sizeof(BCRYPT_RSAKEY_BLOB) + rsaBlob->cbPublicExp,
             rsaBlob->cbModulus);
}

RSACryptoServiceProvider rsa;
rsa.ImportParameters(rsaParams);
array<BYTE>^ ciphertext = rsa.Encrypt(plaintext, false);

The RSACryptoServiceProvider class is the .NET Framework RSA algorithm implementation that is based on the CryptoAPI. The ImportParameters method imports the RSAParameters structure we prepared and the Encrypt method goes ahead and uses the public key to encrypt a message that can be decrypted only by using the private key.

Looking Forward

As I write this, Microsoft is hard at work on the next version of Visual Studio, code-named "Orcas," and along with that release we will see a new version of the .NET Framework. The .NET Framework 3.5 introduces a number of new algorithm implementations that are based on CNG. Once the .NET Framework 3.5 has been released, you can expect even greater interoperability between native and managed code, especially in the area of cryptography. These new additions to the framework provide drop-in replacements for many of the existing implementations as CNG becomes the core cryptographic provider for the Windows platform. Some brand-new algorithm implementations never before seen in the .NET Framework are also being introduced; they cover some of the elliptical curve algorithms provided through CNG’s key storage functions. These implementations fall under the subset of CNG known as NCrypt that I alluded to in the introduction to this article. For more information on all of this, see the "Cryptographic Next Generation Resources" sidebar.

Cryptographic Next Generation Resources

Kenny Kerr is a software craftsman specializing in software development for Windows. He has a passion for writing and teaching developers about programming and software design. Reach Kenny at weblogs.asp.net/kennykerr.