EncryptTo/DecryptTo: Encryption in .NET with CryptoAPI Certificate Stores

 

Michel I. Gallant, Ph.D.
JavaScience Consulting

November 2003

Applies to:
    Microsoft® .NET Framework
   Microsoft® Windows® security

Summary: Learn about CryptoAPI certificate store access and use from .NET managed code. (12 printed pages)

Download the EncrypTo.EXE code sample.

Contents

Introduction
Asymmetric Meets Symmetric
Code Details: EncryptTo.cs
Code Details: DecryptTo.cs
Conclusion

Introduction

In a previous article (Extending .NET Cryptography with CAPICOM and P/Invoke), we discussed how to supplement the cryptographic capabilities currently shipping with the Microsoft® .NET Framework version 1.1 through the use of COM interop and P/Invoke to CryptoAPI native libraries. That article discussed general techniques and motivations for using unmanaged code from managed code to implement cryptographic functionality. This article delves more deeply into CryptoAPI certificate store access and use from .NET managed code. The code samples demonstrate how to:

  • Extract all RSA public key parameters from CryptAPI store certificates or X509 certificate files.
  • Use the certificate public key data to instantiate an RSACryptoServiceProvider for RSA encryption.
  • Find the CryptoAPI key container holding the protected private key associated with a store certificate.
  • Use the key container private key to instantiate an RSACryptoServiceProvider for RSA decryption.

As discussed in the previous article, the .NET Framework 1.0/1.1 lacks good support for using CryptoAPI certificate stores easily with classes from the System.Security.Cryptography and System.Security.Cryptography.X509Certificates namespaces. For example, it is not possible with the current Framework Class Library (FCL) to directly use the public key of a certificate within a CryptoAPI certificate store for recipient-based envelope encryption, or to use the private key of a personal certificate in the MY store to decrypt a secret symmetric key. CAPICOM 2 does provide good support for standard CMS/PKCS #7 Enveloped Data structures, but there are some outstanding .NET<-->COM interop marshaling problems associated with using binary data. The following article demonstrates how to use .NET and P/Invoke to CryptoAPI to bridge functionality between the shipping .NET Cryptography classes and CryptoAPI certificate store functionality. There are several published examples of using derived symmetric keys based on passwords using the PasswordDeriveBytes class to encrypt data. However in most cases, users tend to use poor passwords, making the encrypted content vulnerable to brute-force attack. The advantage of using a public key asymmetric encryption to protect a secret symmetric key is that the symmetric key is generated with the full randomness available to the instantiated key size, and is thus better protected. Of course the private RSA asymmetric key must be carefully protected.

Asymmetric Meets Symmetric

It is important to understand the main differences between asymmetric and symmetric key encryption. Symmetric key encryption algorithms are computationally fast compared to asymmetric encryption algorithms (like RSA). However, since the same secret key is used for symmetric encryption and decryption, we have the difficult problem of securely distributing that secret key. Conversely, asymmetric key infrastructure in PKI does not rely on distribution of any private key. However, the common asymmetric algorithms are too slow to be used for bulk encryption with current computation capability. In fact, the .NET RSA encryption classes limit the amount of data that can be encrypted to slightly less than the public key modulus size (1024 bit/128 byte). Enveloping data helps manage efficient bulk encryption and secure distribution by using a randomly generated secret symmetric key for the bulk encryption, combined with asymmetric encryption of the relatively small secret symmetric key (for example, 192 bit/24 byte 3DES key) for secure distribution to a recipient.

Let's explore how this capability can be achieved in .NET using P/Invoke.

We will use two matching C# utilities, EncryptTo.cs and DecryptTo.cs as a rudimentary demonstration of how RSA asymmetric key enveloped encryption/decryption functionality can be achieved in .NET 1.1 using P/Invoke.

Let's start with a general discussion about encryption/decryption functionality, followed by the more important details in the code samples.

EncryptTo is a .NET assembly console utility which:

  • Reads a local X509 v3 certificate file (binary DER or b64 encoded) or searches CryptoAPI certificate store(s) for a certificate matching a SubjectName substring.
  • Extracts the RSA public-key modulus and exponent from the certificate.
  • Generates a random 3DES secret symmetric encryption Key and Initialization Vector (IV).
  • Encrypts a user-provided file with the 3DES secret key and saves to user-specified file.
  • Encrypts the 3DES Key and IV using the certificate RSA public key and saves to user-specified files.

For a good basic description of symmetric block ciphers, modes of usage (such as CBC Cipher Block Chaining used here) and how Initialization Vectors help protect encrypted data, see MSDN's Cryptography Overview.

Note that while any valid public key can be used in the encryption process, CryptoAPI only supports RSA decryption with private keys of type AT_KEYEXCHANGE. This limitation is historically associated with previously more stringent US export controls on encryption (when it was important to separate signature and encryption keys because no size limits existed on signature keys). Most commercially issued certificates have keys that have been generated with this type, with the exception of some code-signing certificates. In most cases, AT_KEYEXCHANGE key can be used for both signature generation and encryption. One notable exception is .NET Strong Name signing, which in .NET Framework 1.1 is limited to AT_SIGNATURE keys. In fact, the Strong Name tool sn.exe can only generate AT_SIGNATURE keys. Note also that certificates with KeyUsage extensions that don't have the keyEncipherment bit set should not be used to encrypt symmetric keys. This check should be implemented in any production code.

EncryptTo is similar in spirit to part of the CryptoAPI C sample in the Platform SDK Encrypting a File. However, the sample here uses the public key material associated with any X509 certificate, while the PSDK C sample uses the exchange public key from the default key container. Also, the sample here simply writes separate raw encrypted content, encrypted symmetric key, and IV local files, whereas the PSDK C sample writes a single file containing the encrypted content and a CryptoAPI structured SIMPLEBLOB, which contains identifiers for both the symmetric key algorithm and the asymmetric algorithm used to encrypt it. It is easy to modify the sample here to merge the raw encrypted file with the encrypted symmetric key and IV into a single file, perhaps with extra descriptive information. You might even consider custom packaging for these files. For example, a cab archive or managed resources in a signed executable wrapper assembly would clearly be more convenient for distribution in a real implementation. However, if you are concerned about standards and cross-platform interoperability, it is best to use the standard CMS format as noted later in this article. The emphasis in this article is on showing the components in detail.

ms867080.encryptdecrypt2a_figure1(en-us,MSDN.10).gif

Fig. 1 Simplified RSA enveloping. Compare this with the standard EnvelopedData content encapsulation.

EncryptTo.cs can be compiled, with no other dependencies, with .NET Framework SDK 1.0+ or Microsoft Visual Studio® .NET 2002+.

EncryptTo.exe is invoked as:

EncryptTo.exe [inContentFile] [outEncryptedFile] [outKeyfile] [outIVfile]

outKeyfile and outIVfile are the RSA-encrypted 3DES Key and IV respectively in PKCS#1 encoded form. The outEncryptedFile represents the inContentFile, encrypted using the 3DES cipher in CBC mode. Of course, the code can be modified to make use of any supported symmetric cipher available (RC2, Rijndael/AES and so on).

Typically, recipient certificates are stored in the "ADDRESSBOOK" (also called "Other People") store, although EncryptTo searches the "MY" store as well for a matching certificate. EncryptTo only encrypts to a single recipient. To encrypt for yourself, you must possess a valid X509 certificate within the "Personal /MY" certificate stores.

Only the RSA public key of the recipient certificate is required for encrypting the secret 3DES Key. Note that new 3DES Key and IV values are generated each time EncryptTo is executed. There is no reuse of the Key or IV (as is common in password-based key derivations). It is not strictly necessary to encrypt the IV, but both Key and IV are encrypted here. EncryptTo does not create a formal "Enveloped Data" content type structure, as defined by the RSA PKCS #7 standard. However, EncryptTo demonstrates some of the basic components that would be used, with appropriate ASN.1/DER encoding, in creating a full self-contained Enveloped Data structure.

DecryptTo is a .NET assembly console utility, designed to be used with EncryptTo, which:

  • Searches CryptoAPI Current User MY store for a certificate matching a SubjectName substring.
  • Returns the first certificate which matches and which has an associated private key container.
  • Attempts to decrypt the user-specified encrypted Key and IV files using the RSA private key.
  • If the secret 3DES Key and IV can be decrypted, the Key and IV are used to decrypt the specified encrypted content file.
  • Saves the content file to a user-specified file.

DecryptTo.cs can be compiled, with no other dependencies, with .NET Framework SDK 1.0+ or Visual Studio .NET 2002+.

DecryptTo.exe is invoked as:

DecryptTo.exe [inEncryptedFile] [outContentFile] [inKeyfile] [inIVfile]

Note that to successfully decrypt the encrypted file, you must specify the same encrypted Key and IV files used to encrypt the content. You must also have access to the private key (key type must be AT_KEYEXCHANGE) associated with the public key of the certificate you've selected to "envelope" the 3DES Key and IV data with. That private RSA key must be associated with a certificate in the current user MY certificate store. If the private key has strong protection, as determined when the key was installed, the decryption process will prompt the user with a native password dialog to enable authorized access to the protected private RSA key.

The CMS/PKCS #7 structured Enveloped Data content type supports multiple recipients, the encrypted content, encrypted symmetric keys, IVs, and various extensions directly embedded into one file. These PKCS #7 structures are used in building S/MIME email message structures. Future releases of .NET Framework class libraries will hopefully include broad support for advanced security structures such as EnvelopedData and SignedData.

Code Details: EncryptTo.cs

Note   Only details of the code not covered in Extending .NET Cryptography with CAPICOM and P/Invoke is discussed in this article. The P/Invoke native function and struct declarations can be viewed in the full source code.

EncryptTo prompts the user for either a certificate SubjectName substring, or a local certificate file name. If a file name was entered, the GetRecipientFileCert() function is called. If X509Certificate.CreateFromCertFile() fails (CreateFromCertFile only supports binary DER format), perhaps a b64 certificate file was specified. In this case, an attempt is made to b64 decode the file and the overloaded X509Certificate(certBytes) constructor is called:

 private X509Certificate GetRecipientFileCert(String certfile) {
    X509Certificate cert   =null;
     try{
         cert = X509Certificate.CreateFromCertFile(certfile);
   }
     catch(System.Security.Cryptography.CryptographicException)
   {
    StreamReader sr = File.OpenText(certfile);
    String filestr = sr.ReadToEnd();
    sr.Close();
    StringBuilder sb = new StringBuilder(filestr) ;
         sb.Replace("-----BEGIN CERTIFICATE-----", "") ;
         sb.Replace("-----END CERTIFICATE-----", "") ;
        //Decode 
   try{        //see if the file is a valid Base64 encoded cert
           byte[] certBytes = Convert.FromBase64String(sb.ToString()) ;
      cert =  new X509Certificate(certBytes);
    }
   catch(System.FormatException) {
   .....

Alternatively, we can decode using P/Invoke with CryptStringToBinary(), which has the advantage of being able to remove the PEM headers. However, the emphasis in this article is to use unmanaged code only when necessary.

If the entered string is not a file, the GetRecipientStoreCert() function is called. CertFindCertificateInStore() is P/Invoked to search for a matching certificate. The first matching certificate is returned to the instance variable X509Certificate recipcert. If a valid certificate was found, GetCertPublicKey() is called. To extract the public key, modulus, and exponent, the certificate ASN.1 encoded public key, returned by X509Certificate.GetPublicKey(), is decoded to a CryptoAPI PUBLICKEYBLOB by P/Invoking CryptDecodeObject() with structure type parameter RSA_CSP_PUBLICKEYBLOB. The PUBLICKEYBLOB is then parsed to obtain all public key parameters. Decoding of the first part of the PUBLICKEYBLOB is facilitated with a custom PUBLICKEYBLOBHEADERS structure:

 [StructLayout(LayoutKind.Sequential)]
  public struct PUBKEYBLOBHEADERS {
   public byte bType;   //BLOBHEADER
   public byte bVersion;   //BLOBHEADER
   public short reserved;   //BLOBHEADER
   public uint aiKeyAlg;   //BLOBHEADER
   public uint magic;   //RSAPUBKEY
   public uint bitlen;   //RSAPUBKEY
   public uint pubexp;   //RSAPUBKEY
 }

The modulus data follows immediately after this structure data in the PUBLICKEYBLOB and is easily extracted by a simple array copy. The modulus, exponent, and key size properties are assigned to instance variables. CryptoAPI structures like PUBLICKEYBLOB usually represent data in little-endian order, but because .NET RSAParameters properties are big-endian ordered, the modulus and exponent arrays are reversed. If the verbose parameter is true, all public key properties are displayed, as well as the complete encoded ASN.1 and decoded CryptoAPI key blobs:

 private bool GetCertPublicKey(X509Certificate cert)
 {
   byte[] publickeyblob ;
   byte[] encodedpubkey = cert.GetPublicKey(); //ASN.1 encoded public 
     key

          uint blobbytes=0;
   if(verbose) {
     Console.WriteLine();
     showBytes("Encoded publickey", encodedpubkey);
     Console.WriteLine();
    }
   if(Win32.CryptDecodeObject(ENCODING_TYPE, RSA_CSP_PUBLICKEYBLOB, 
     encodedpubkey, (uint)encodedpubkey.Length, 0, null, ref 
     blobbytes))
    {
     publickeyblob = new byte[blobbytes];
      if(Win32.CryptDecodeObject(ENCODING_TYPE, RSA_CSP_PUBLICKEYBLOB, 
        encodedpubkey, (uint)encodedpubkey.Length, 0, publickeyblob, 
        ref blobbytes))
      if(verbose)
        showBytes("CryptoAPI publickeyblob", publickeyblob);
    }
   else{
     Console.WriteLine("Couldn't decode publickeyblob from certificate 
       publickey") ;
     return false;
   }

   PUBKEYBLOBHEADERS pkheaders = new PUBKEYBLOBHEADERS() ;
   int headerslength = Marshal.SizeOf(pkheaders);
   IntPtr buffer = Marshal.AllocHGlobal( headerslength);
   Marshal.Copy( publickeyblob, 0, buffer, headerslength );
   pkheaders = (PUBKEYBLOBHEADERS) Marshal.PtrToStructure( buffer, 
     typeof(PUBKEYBLOBHEADERS) );
   Marshal.FreeHGlobal( buffer );

   if(verbose) {
    Console.WriteLine("\n ---- PUBLICKEYBLOB headers ------");
    Console.WriteLine("  btype     {0}", pkheaders.bType);
    Console.WriteLine("  bversion  {0}", pkheaders.bVersion);
    Console.WriteLine("  reserved  {0}", pkheaders.reserved);
    Console.WriteLine("  aiKeyAlg  0x{0:x8}", pkheaders.aiKeyAlg);
    String magicstring = (new 
      ASCIIEncoding()).GetString(BitConverter.GetBytes
      (pkheaders.magic)) ;
    Console.WriteLine("  magic     0x{0:x8}     '{1}'", 
      pkheaders.magic, magicstring);
         Console.WriteLine("  bitlen    {0}", pkheaders.bitlen);
    Console.WriteLine("  pubexp    {0}", pkheaders.pubexp);
    Console.WriteLine(" --------------------------------");
   }
   //-----  Get public key size in bits -------------
   this.certkeysize = pkheaders.bitlen;

   //-----  Get public exponent -------------
   byte[] exponent = BitConverter.GetBytes(pkheaders.pubexp); //little-
     endian ordered
   Array.Reverse(exponent);    //convert to big-endian order
   this.certkeyexponent = exponent;
   if(verbose)
    showBytes("\nPublic key exponent (big-endian order):", exponent);

   //-----  Get modulus  -------------
   int modulusbytes = (int)pkheaders.bitlen/8 ;
   byte[] modulus = new byte[modulusbytes];
   try{
      Array.Copy(publickeyblob, headerslength, modulus, 0, 
        modulusbytes);
      Array.Reverse(modulus);   //convert from little to big-endian 
        ordering.
      this.certkeymodulus = modulus;
      if(verbose)
       showBytes("\nPublic key modulus  (big-endian order):", modulus);
   }
   catch(Exception){
      Console.WriteLine("Problem getting modulus from publickeyblob");
      return false;
   }
   return true;
 }

In either case, the full SubjectName and public key (modulus) size of the retrieved certificate is displayed.

After the public key properties are acquired, the TripleDESEncrypt() function is called to generate a random 3DES key and IV, and, using the default CBC mode, encrypts the content file to an output file:

   FileStream fin  = new FileStream(content, FileMode.Open, 
     FileAccess.Read);
   FileStream fout = new FileStream(encContent, FileMode.OpenOrCreate, 
     FileAccess.Write);
   byte[] buff = new byte[1000]; //encryption buffer.
   int lenread;                
   byte[] encdata ;
   try
   {
    TripleDESCryptoServiceProvider tdes = new 
      TripleDESCryptoServiceProvider();          
    CryptoStream encStream = new CryptoStream(fout, 
      tdes.CreateEncryptor(), CryptoStreamMode.Write);
    //do the encryption ...
    while( (lenread = fin.Read(buff, 0, 1000))>0)
     encStream.Write(buff, 0, lenread);
     encStream.Close();

Finally, an RSAParameters instance is initialized with the key modulus and exponent, an RSACryptoServiceProvider instance is created and initialized with the RSAParameters object, and the 3DES Key and IV are encrypted to output files:

   RSAParameters RSAKeyInfo= new RSAParameters();
   RSAKeyInfo.Modulus   = modulus;
   RSAKeyInfo.Exponent   = exponent;
   //Initialize RSACryptoServiceProvider
   RSACryptoServiceProvider oRSA   = new RSACryptoServiceProvider();
   oRSA.ImportParameters(RSAKeyInfo);
   protectedkey = oRSA.Encrypt(keydata, false);

Code Details: DecryptTo.cs

DecryptTo prompts the user for a MY store certificate SubjectName substring. However, for RSA decryption, we need access to the private key container associated with a matching certificate. (Remember that EncryptTo only required the RSA public key.) CertFindCertificateInStore() is P/Invoked to search for a matching certificate. To determine if a matching certificate has an associated private key available, CertGetCertificateContextProperty() is P/Invoked with the property identifier dwPropId = CERT_KEY_PROV_INFO_PROP_ID. This function returns true if a matching private key container exists. If this case, the instance variable X509Certificate recipcert is set. A CRYPT_KEY_PROV_INFO struct is marshaled back, and the target key container name and RSA key type members are used to set instance fields.

 [StructLayout(LayoutKind.Sequential)]
  public struct CRYPT_KEY_PROV_INFO
  {
   [MarshalAs(UnmanagedType.LPWStr)] public String ContainerName;
   [MarshalAs(UnmanagedType.LPWStr)] public String ProvName;
   public uint ProvType;
   public uint Flags;
   public uint ProvParam;
   public IntPtr rgProvParam;
   public uint KeySpec;
  }

 ....
  if(Win32.CertGetCertificateContextProperty(hCertCntxt, 
    CERT_KEY_PROV_INFO_PROP_ID, pProvInfo, ref provinfosize))
   {
   CRYPT_KEY_PROV_INFO ckinfo = 
     (CRYPT_KEY_PROV_INFO)Marshal.PtrToStructure(pProvInfo,
     typeof(CRYPT_KEY_PROV_INFO));
   Marshal.FreeHGlobal(pProvInfo);
   this.recipcert = new X509Certificate(hCertCntxt);
   this.keycontainer = ckinfo.ContainerName;
   this.RSAkeytype = (int)ckinfo.KeySpec;
   gotpvkprops = true ;  // only way for valid return
   }

The full SubjectName, key container name, and key type of any matching certificate is displayed. If the key type is AT_SIGNATURE, DecryptTo exits since only AT_KEYEXCHANGE keys are supported for RSA decryption in CryptoAPI. Although not actually required for decryption, GetCertPublicKey() is called in order to get related public key information, including the key size which is also displayed. Finally, TripleDESDecrypt() is called, which in turn calls DoRSADecrypt() to decrypt the encrypted 3DES Key and IV files. In this case, the RSACryptoServiceProvider is initialized with CspParameters using the acquired keycontainer and key type (which must be type AT_KEYEXCHANGE or equivalent in .NET KeyNumber==1).

   //Construct RSA with keycontainer associated with certificate found
   CspParameters cp = new CspParameters();
   cp.KeyContainerName = container;
   cp.KeyNumber = keyspec;
   RSACryptoServiceProvider oRSA = new RSACryptoServiceProvider(cp);
   clearkey = oRSA.Decrypt(encdata, false);

Technically it is not necessary here to specify the KeyNumber since the default value is AT_KEYEXCHANGE (1).

Finally the encrypted content file is decrypted with the recovered 3DES Key and IV:

    FileStream fin  = new FileStream(encContent, FileMode.Open,
      FileAccess.Read);
    FileStream fout = new FileStream(content, FileMode.OpenOrCreate,
      FileAccess.Write);
    byte[] buff = new byte[1000]; //decryption buffer.
    int lenread;                
    try
    {
     TripleDESCryptoServiceProvider tdes = new
       TripleDESCryptoServiceProvider();
     CryptoStream decStream = new CryptoStream(fout,
       tdes.CreateDecryptor(key, IV), CryptoStreamMode.Write);
     Console.WriteLine("Decrypting content ...");
     while( (lenread = fin.Read(buff, 0, 1000))>0)
        decStream.Write(buff, 0, lenread);
     decStream.Close();
     return true;
    }

For greater portability and as a useful extension to DecryptTo, you could extend the code functionality to decrypt directly from a password protected PFX (PKCS#12 format) file instead of using CryptoAPI key container credentials. With this capability, you could carry your protected public/private RSA keypair decryption credentials with you on any removable media such as floppy, CD-ROM, memory stick and so on.

Conclusion

Hopefully you will find in the code samples presented here some useful information, or techniques or perhaps even a few lines of code that will help you in developing more capable .NET powered applications. With your creativity hat firmly on, you can surely think of many useful extensions to the techniques presented here.

References

About the Author

Michel I. Gallant, Ph.D., has over 20 years experience in the telecommunications industry. He has worked as a senior photonic designer, and as a security analyst and architect in a major Canadian telecommunications corporation. He has extensive experience in code-signing and applied cryptography. He was awarded an MVP in Security for 2003. Michel lives in Ottawa, Canada and enjoys playing surf music, designing puzzles and designing innovative electronic projects.