.NET Security Workshop
Home About Workshops Articles Writing Talks Books Contact

14. PKCS and CMS

Earlier in this tutorial you saw how certificates can be used to sign data and can be used to encrypt data (well, in the key exchange needed for encryption with symmetric algorithms). A fair amount of code had to be written, but more importantly, you have to write the code that will provide the signature and encrypted data, and the code that consumes it. What is needed here is a standard.

Public Key Cryptography Standard (PKCS) #7 is a standard for signed data and enveloped data. PKCS#7 is defined by RFC 2315. Cryptographic Message Standard (CMS) is a superset of PKCS#7 and it supports digital signatures, message authentication codes, and encryption. CMS is defined by RFC 2630. The framework library in .NET 3.0/2.0 supports PKCS#7 and CMS in the System.Security.Cryptography.Pkcs namespace and the classes there are essentially wrappers around the Windows CryptoAPI.

14.1 Architecture

A digital envelope is a service that encrypts data so that it is can only be read by the holder of the appropriate key. Message authentication and integrity comes from using message signatures, and CMS enables one or more entities to sign a message. These actions are not mutually exclusive. A message can have a signature and be encrypted, that is, be signed and be delivered in an envelope. Usually the order is to sign first and then envelope the signed data, but it is not mandatory because both actions (signing and enveloping) act upon a blob of data and it is not relevant what the blob is. A message can also have attributes which are additional services applied to the message (for example the signing time). Attributes are defined by another standard, PKCS#9, defined by RFC 2985 and .NET provides classes for them too.

All PKCS/CMS messages have content, that is, data that is either signed or encrypted. Content is stored in the ASN.1 formatted type ContentInfo and .NET provides a class with the same name that performs this action. The ContentInfo class essentially contains the data in a byte array (the Contents property) and a description of the data with an Oid number (the ContentType property). The class also has a static method called GetContentType which will return the OID of a CMS message passed in as a byte array. The SignedCms class is used to sign a message in a ContentInfo object and the EnvelopedCms class is used to encrypt a message. Both of these classes have a ContentInfo property that contains the contents of the object. These classes have methods (Encode and Decode) that will will encode the contents or decode the contents as ASN.1.

CMS has a concept of subject. The subject can one (or several) of many roles involved in producing or consuming the message, a subject is identified by a certificate. The .NET framework has two subject classes: CmsSigner, indicating that the subject will sign data; and CmsRecipient, indicating that the subject consumes a CMS message. More information about a recipient, specifically when concerned with key exchange, is obtained through the classes derived from RecipientInfo: KeyAgreeRecipientInfo and KeyTransRecipientInfo. Instances of these classes are returned through the collection property EnvelopedCms.RecipientInfos. The KeyAggreeRecipientInfo class gives information about key agreement information (in particular Diffie-Hellman key agreement algorithm) and the KeyTransRecipientInfo class gives information about the recipient to which a key is transported. Key agreement and key transport are different in that in the former both parties come to an agreement about a key, whereas in the later case the sender determines the key and informs the recipient.

14.2 Signed Messages

The first thing to do is to create a certificate and put it in the store. Use makecert to do this:

makecert -n "CN=Richard Grimes" -ss MY

Clearly use your own name as the X509 name in the certificate. This will create a signature certificate. Now create a process (signer.cs) that will sign or verify data using the certificate:

using System;
using System.Security.Cryptography.Pkcs;
using System.Security.Cryptography.X509Certificates;
using System.IO;
using System.Text;

class App
{
   static void Main(string[] args)
   {
      if (args.Length < 2) return;
      bool sign = true;
      sign = (args[0].ToLower()[0] == 's');
      string infile = args[1];
      if (!File.Exists(infile)) return;

      if (sign)
      {
         string certname = args[2];
         X509Certificate2 cert = GetCertificate(certname);
         if (cert == null) return;
         string outfile = null;
         outfile = Path.GetFileNameWithoutExtension(infile) + ".p7s";
         if (File.Exists(outfile)) File.Delete(outfile);
         SignMsg(infile, outfile, cert);
      }
      else
      {
         if (VerifyMsg(infile))
         {
            Console.WriteLine("\nMessage verified");
         }
         else
         {
            Console.WriteLine("\nMessage failed to verify");
         }
      }
   }
   static X509Certificate2 GetCertificate(string certname)
   {
      return null;
   }
   static void SignMsg(string infile, string outfile, X509Certificate2 cert)
   {
   }
   static bool VerifyMsg(string infile)
   {
      return true;
   }
}

This just processes the command line that should look like this:

signer s|v infile certname

Where you use s for the first parameter to sign data and v to verify signed data. infile is the file that contains the data to be signed, or the signed data to be verified. If you are signing data then you need to provide the third parameter, certname, which is the X509 name used to identify the certificate; in this case the signed data will be written to a file with the extension .p7s. The three methods are: GetCertificate, which will return the certificate with the specified X509 name in the subject; SignMsg, which will sign the data in infile and VerifyMsg, which will verify the signed data. You should be able to compile this code (csc signer.cs), but it will do nothing when you run it.

The GetCertificate is straightforward, add the following code:

static X509Certificate2 GetCertificate(string certname)
{
   X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
   store.Open(OpenFlags.ReadOnly);
   X509Certificate2Collection certs =
      store.Certificates.Find(X509FindType.FindBySubjectName, certname, false);
   store.Close();
   if (certs.Count == 0) return null;
   return certs[0];
}

This calls the Find method to locate all the certificates that have the name you supply in the subject name. This search may return more than one certificate and so only the first certificate is returned. Because of this, it is important that you give the certificate's subject name a unique value, so that the wrong certificate is not returned by mistake.

When you run the code later on and you get an exception about the certificate (Keyset does not exist) it is likely that there is more than one certificate with the same name in the subject and the wrong one is returned. In this case, run certmgr and inspect the certificates in your personal store and then create a new certificate with a name that cannot be confused with any of the existing certificates.

Next, add the code to sign the data:

static void SignMsg(string infile, string outfile, X509Certificate2 cert)
{
   ContentInfo contentInfo = null;
   using (FileStream fsIn = File.OpenRead(infile))
   {
      int count = (int)fsIn.Length;
      byte[] data = new byte[count];
      int read = 0;
      int offset = 0;
      while (count > 0)
      {
         read = fsIn.Read(data, offset, count);
         if (read == 0) break;
         offset += read;
         count -= read;
      }
      contentInfo = new ContentInfo(data);
   }

   SignedCms signedCms = new SignedCms(contentInfo);
   CmsSigner cmsSigner = new CmsSigner(cert);
   signedCms.ComputeSignature(cmsSigner);
   byte[] enc = signedCms.Encode();

   using (FileStream fsOut = File.OpenWrite(outfile))
   {
      fsOut.Write(enc, 0, enc.Length);
   }
}

The first part of this code reads the contents of the file into a byte array and uses this to initialize a ContentInfo object; similarly the last part of the code writes the data and the signature in the enc array to a file. Such code is straight forward, the interesting code is between these sections:

SignedCms signedCms = new SignedCms(contentInfo);
CmsSigner cmsSigner = new CmsSigner(cert);
signedCms.ComputeSignature(cmsSigner);
byte[] enc = signedCms.Encode();

These few lines do a lot! The SignedCms object is the CMS signed object. The first line initializes this object with the contents of the input file. There are six constructors for this object, and the one that you've used here is one of the simplest, using default values for most of the data in the object. Here's the constructor with the most parameters:

SignedCms(SubjectIdentifierType type, ContentInfo info, bool detached);

The SubjectIdentifierType enumeration indicates how the subject is identified, the default for SignedCms is IssuerAndSerialNumber which means that the subject is identified by the issuer of the certificate and the serial number of the certificate. Other values indicate that signing is not performed, but instead a hash is created and this can be verified (NoSignature); or the subject's public key is hashed (SubjectKeyIdentifier). In our example, the default is used which means that the issuer of the certificate, and the certificate serial number (the combination which uniquely identify the certificate and hence the keys in the certificate) will be provided as part of the signature.

The Boolean parameter determines if the data is detached, that is, when the signature is encoded the result will just be the signature, and it will not contain the data that is signed. The default for SignedCms is for the data not to be detached (this parameter is false) which means that the encoded data contains the signature and the original data.

Clearly you want to sign the data in the SignedCms object and thus you need to provide a certificate with the key to perform the signing. If you take a look at the SignedCms class you'll see that there is a Certificates property that is a collection of certificates (X509Certificate2Collection). This property is read-only, meaning that you cannot change the actual object used for the collection, but you can add new objects into the collection. However, you should not do this because any values you add will be ignored because this property contains the certificates that have been used rather than will be used to sign the data. To provide the certificate information you have to provide a CmsSigner object.

The CmsSigner object contains information about how the signing will be performed as well as the key used to do the signing. In this example a certificate is used to initialize the object which will supply the private key to be used to sign the data. The hash algorithm used for the signing process is SHA1 (the default) but you can change this to another algorithm through the DigestAlgorithm property which is an Oid and so you can initialize it with either the OID string or the friendly name. Of course, when it comes to verify the signature, the verification can only be valid if the key is valid, and that depends on checking the certificate (but more of that later). You can determine how much of the certificate is placed in the signature through the IncludeOption property which is an enumeration of type X509IncludeOption and allows you to specify that none of the certificate chain is included, only the end certificate, all the chain except for the root certificate, or all of the chain is included. The default is to exclude the root except when only a hash is created, in which case no chain is included. Finally, you can use the CmsSigner object to indicate if there are co-signers, and to give attributes, both topics will be covered later.

The signature is generated in a two-step process. The first step is to create the signature by passing the CmsSigner object to the ComputeSignature method. The second step is to create the PKCS#7 encoded data by calling the Encode method which returns a byte array.

You can compile this code (csc signer.cs) and then run it to sign some data:

signer s signer.cs "Richard Grimes"

This will create a file called signer.p7s which contains the data, signature and information about the certificate.

Now you need to verify the signature. Add the following code:

static bool VerifyMsg(string infile)
{
   byte[] data = null;
   using (FileStream fsIn = File.OpenRead(infile))
   {
      int count = (int)fsIn.Length;
      data = new byte[count];
      int read = 0;
      int offset = 0;
      while (count > 0)
      {
         read = fsIn.Read(data, offset, count);
         if (read == 0) break;
         offset += read;
         count -= read;
      }
   }

   SignedCms signedCms = new SignedCms();
   signedCms.Decode(data);

   try
   {
      signedCms.CheckSignature(true);
   }
   catch (System.Security.Cryptography.CryptographicException)
   {
      return false;
   }

   return true;
}

The first half of this method loads the .p7s file into a byte array. The second half of the method creates a SignedCms object and then decodes the PKCS#7 format into binary so that it can be processed. The signature is then checked by calling CheckSignature. The parameter is true to indicate that only the signature is checked, if you pass false then the certificate is validated as well. This method will throw an exception if the data cannot be verified, which is why the code uses a exception block to catch the CryptographicException exception.

You should compile this code (csc signer.cs) and then run it to verify the data that you have just signed:

C:\security\14.2>signer v signer.p7s

Message verified

The program should indicate that the message has been verified, that is, the data has not been tampered. Once you have called Decode the ContentsInfo property will contain the original data (which you can access through the Contents property) and so you can use this to get access to the data in the .p7s file. For example, add the following to the end of the VerifyMsg method:

   SignedCms signedCms = new SignedCms();
   signedCms.Decode(data);

   try
   {
      signedCms.CheckSignature(true);
   }
   catch (System.Security.Cryptography.CryptographicException)
   {
      return false;
   }

   Console.WriteLine(
      Encoding.ASCII.GetString(signedCms.ContentInfo.Content));

   return true;
}

This will print out the data that you signed.

In this example, the data is signed by just one certificate, however, it is possible to sign the data by more than one certificate. Remember that a CMS message contains content which is a blob of data, and each time you perform some action on the blob you get a new blob which replaces the existing blob. So if you have a collection of certificates all you need to do is apply ComputeSignature on the blob for each certificate:

byte[] SignBlob(byte[] blob, X509Certificate2Collection certs)
{
   SignedCms msg = new SignedCms(new ContentInfo(blob));
   foreach(X509Certificate2 cert in certs)
   {
      msg.ComputSignature(new CmsSigner(cert));
   }
   return msg.Encode();
}

this method takes a non-encoded blob and signs it with each certificate in the collection, then it returns an encoded blob.

When you sign a message information about a signer is added to a collection property of SignedCms called SignerInfos, each item is a SignerInfo object. SignerInfo contains information about the certificate used to perform the signature, the algorithm used (an Oid property called DigestAlgorithm) and attributes about the signing (SignedAttributes and UnsignedAttributes). In addition there is a SubjectIdentifier property called SignerIdentifier that indicates if the subject is a signer or recipient. Each SignerInfo object also has a method called CheckSignature which can be called to check the signature for that specific signer. Thus to verify a message signed by many signers you just need to initialize a single SignedCms object and call CheckSignature once. This method will obtain the SignerInfos collection and call CheckSignature on each.

The SignerInfo class also has a method called ComputeCounterSignature (and a corresponding RemoveCounterSignature). A counter signature is the signature of a signature. Contrast this with multiple signatures: when you sign with multiple signatures each signature signs the entire message, whereas a countersignature signs just a signature. Each SignerInfo object has a CounterSignerInfos property that is a collection of SignerInfo objects, clearly this collection is available to the SignedCms class and hence SignedCms.CheckSignature will also check the countersignatures.

To clean up this example remove the certificate that you added to the store. The simples way to do this is to type certmgr on the command line to get the GUI version of the tool and then select the certificate you added (in my case I will look for a certificate with the name Richard Grimes in the Issued To column) and then click the Remove button. You will get a warning dialog with the rather garbled message: You cannot decrypt data encrypted using the certificates. Do you want to delete the certificates? This should say If you remove these certificates you will not be able to decrypt data that has been encrypted with the certificates. To remove the certificate, click on the Yes button and then close certmgr.

14.3 Enveloped Messages

An enveloped message is encrypted with a public key, this means that the message is intended for a recipient that has the corresponding private key. Since enveloping uses a public key, you are most likely to use a certificate in the AddressBook certificate store, because the certificate will usually be someone else's certificate. When you decrypt an enveloped message you are most likely to use a certificate in the My certificate store because only a private key can decrypt the message and therefore if you wish to decrypt the message it means that you must be the recipient.

In the last example you created a certificate and put it in the My store. This means that the stored certificate will have both a public and a private key which at first sight appears to be suitable for the current example. However, the default action of makecert (which was used in the last example) is to create a signature key and in this example we will be performing key exchange and so we have to use an exchange key. To do this type the following on the command line:

makecert -pe -n "CN=Richard Grimes" -sky exchange -ss My

(Obviously use your own name instead of Richard Grimes.) You will not export the private key, but if you do not use the -pe option then you will not be able to create the exchange key. The public key must be placed in the AddressBook store, so the first action is to export the public key. Type the following on the command line:

certmgr -s my -c -put pub.cer

The tool will list all the certificates in the My store and ask you to indicate which certificate you want to export:

==============Certificate # 1 ==========
<data>
==============Certificate # 2 ==========
<data>
==============Certificate # 3 ==========
<data>
Enter cert # from the above list to put-->2
CertMgr Succeeded

Identify the certificate that you just added and type its identification number when you are prompted. This will export just the public key to the specified file. Now add the public key to the address book:

certmgr -add pub.cer -s addressbook

Now create a file (envelope.cs) for a process to envelope a message:

using System;
using System.Security.Cryptography.Pkcs;
using System.Security.Cryptography.X509Certificates;
using System.IO;
using System.Text;

class App
{
   // Command line params: e|d infile outfile [certname]
   static void Main(string[] args)
   {
      if (args.Length < 3) return;

      bool encrypt = true;
      encrypt = (args[0].ToLower()[0] == 'e');

      string infile = args[1];
      if (!File.Exists(infile)) return;

      string outfile = args[2];
      if (File.Exists(outfile))
      {
         File.Delete(outfile);
      }

      if (encrypt)
      {
         string certname = args[3];
         X509Certificate2 cert = GetCertificate(certname);
         if (cert == null) return;
         EncryptMsg(infile, outfile, cert);
      }
      else
      {
         DecryptMsg(infile, outfile);
      }
   }

   static X509Certificate2 GetCertificate(string certname)
   {
      return null;
   }

   static void EncryptMsg(string infile, string outfile, X509Certificate2 cert)
   {
   }

   static void DecryptMsg(string infile, string outfile)
   {
   }
}

This tool takes four parameters:

envelope e|d infile oldfile [certname]

If the first parameter is e then the message is encrypted, if it is d then the message is decrypted. The second parameter is the name of the file with the message to encrypt or decrypt and the third parameter is the name of the file with the data after it has been encrypted or decrypted. The final parameter is the X509 name in the subject of the certificate which will be in the AddressBook store for encryption. Information about this certificate will be put in the enveloped message and during decryption the certificate with the same issuer and serial number will be accessed from the My store. You can compile this code if you wish (csc envelope.cs) but the process will do very little.

The GetCertificate method is a modified version of the method in the signer example above in that it provides the X509 name of the subject and the certificate store, add the following:

static X509Certificate2 GetCertificate(string certname)
{
   X509Store store = new X509Store(StoreName.AddressBook, StoreLocation.CurrentUser);
   store.Open(OpenFlags.ReadOnly);

   X509Certificate2Collection certs =
      store.Certificates.Find(X509FindType.FindBySubjectName, certname, false);
   store.Close();
   if (certs.Count == 0) return null;

   return certs[0];
}

The only difference to the last example is that the name of the store is the AddressBook. Again, if you choose to you can compile this, but again, it will do very little. The code gets more interesting with the EncryptMsg method, add the following:

static void EncryptMsg(string infile, string outfile, X509Certificate2 cert)
{
   byte[] data = null;
   using (FileStream fsIn = File.OpenRead(infile))
   {
      int count = (int)fsIn.Length;
      data = new byte[count];
      int read = 0;
      int offset = 0;
      while (count > 0)
      {
         read = fsIn.Read(data, offset, count);
         if (read == 0) break;
         offset += read;
         count -= read;
      }
   }

   ContentInfo contentInfo = new ContentInfo(data);
   EnvelopedCms envCms = new EnvelopedCms(contentInfo);
   CmsRecipient cmsRecipient = new CmsRecipient(SubjectIdentifierType.IssuerAndSerialNumber, cert);
   envCms.Encrypt(cmsRecipient);
   byte[] enc = envCms.Encode();

   using (FileStream fsOut = File.OpenWrite(outfile))
   {
      fsOut.Write(enc, 0, enc.Length);
   }
}

The first part of this reads the message into a byte array and then uses it to initialize a ContentInfo object. The last part writes the encoded message to the output file. The code in the centre does the work of encrypting the data. It does this by initialising an instance of EnvelopedCms with the content and then calling the Encrypt method. This method needs a certificate and this is done by passing a CmsRecipient object that has been initialized with the certificate of the recipient taken from the AddressBook store. The EnvelopedCms has several constructors and here I have used the simplest. There are constructors that allow you to determine the parts of the certificate that will be used to identify the subject (a SubjectIdentifierType  enumeration, the default is the issuer and the certificate serial number) and the symmetric algorithm used to encrypt the data (an instance of AlgorithmIdentifier that contains an OID, the default is 3DES).

Compile this code and then run it:

envelope e envelope.cs envelope.dat "Richard Grimes"

This will create a file called envelope.dat that has the encrypted data. Now you need to implement the code to decrypt the data. Add the following:

static void DecryptMsg(string infile, string outfile)
{
   byte[] data = null;
   using (FileStream fsIn = File.OpenRead(infile))
   {
      int count = (int)fsIn.Length;
      data = new byte[count];
      int read = 0;
      int offset = 0;
      while (count > 0)
      {
         read = fsIn.Read(data, offset, count);
         if (read == 0) break;
         offset += read;
         count -= read;
      }
   }

   EnvelopedCms envCms = new EnvelopedCms();
   envCms.Decode(data);
   envCms.Decrypt();

   using (FileStream fsOut = File.OpenWrite(outfile))
   {
      fsOut.Write(envCms.ContentInfo.Content, 0, envCms.ContentInfo.Content.Length);
   }
}

Yet again, the first part reads the encrypted data from the file and the last part writes the decrypted data to a file. The code in the centre does the work. The first thing is to convert the PKCS#7 encoded data to raw data using the Decode method which will copy the decoded data into the ContentInfo member of the EnvelopedCms object. Finally the data is decrypted with the Decrypt method. This will decrypt the key embedded in the message using the certificate identified by the message. This information is held in the RecipientInfo property of the EnvelopedCms object. Each entry in this collection is a class derived from RecipientInfo, for key exchange (used in this example) each item will be a KeyTransRecipientInfo object. This class has information about the algorithm used to do the encryption (the KeyEncryptionAlgorithm property of type AlgorithmIdentifier); the key itself (EncryptedKey a byte array) and information about the type of the recipient (the RecipientIdentifier property, of type SubjectIdentifier).

Compile this code and then run it to decrypt the data you just created:

envelope d envelope.dat envelope.txt "Richard Grimes"

The envelope.txt file should contain the cleartext of the file that you encrypted earlier, to confirm this type the file to the command line (type envelope.txt).

You can envelope a message with more than one certificate. To do this create a X509Certificate2Collection containing the certificates and use this to initialize a CmsRecipientCollection object which you then pass to an overload of EnvelopedCms.Encrypt. Decryption is the same as before.

To clean up this example remove the certificates from the My and AddressBook stores:

C:\security\14.3> certmgr -del -c -s my
==============Certificate # 1 ==========
<data>
==============Certificate # 2 ==========
<data>
==============Certificate # 3 ==========
Subject::
[0,0] 2.5.4.3 (CN) Richard Grimes
<data>

Enter cert # from the above list to delete-->3
C:\security\14.3> certmgr -del -c -s addressbook
==============Certificate # 1 ==========
<data>
==============Certificate # 2 ==========
<data>
==============Certificate # 3 ==========
Subject::
[0,0] 2.5.4.3 (CN) Richard Grimes
<data>

==============Certificate # 4 ==========
<data>
Enter cert # from the above list to delete-->3

The bold lines are the commands you should type and the other lines are sample data. Clearly you should identify the certificate that you inserted into the store and type the appropriate number when prompted.

14.4 Message Attributes

The PKCS#9 standard defines attributes that can be added to a message. Both signed and enveloped messages can have attributes. The following table shows the classes provided for PKCS attributes in the .NET framework.

Class Description
Pkcs9AttributeObject Base class for the attribute classes
Pkcs9ContentType Indicates the type of data in the message in the object ID property ContentType
Pkcs9DocumentDescription Contains a text description of the contents of the message in the DocumentDescription property
Pkcs9DocumentName Contains a text name of the message in the DocumentName property
Pkcs9MessageDigest Contains the message digest in the MessageDigest property
Pkcs9SigningTime Contains a DateTime property called SigningTime that has the time and date when the message was signed

To use an attribute you create an instance of the appropriate attribute object. For some attributes (document name and description) you provide initialization information. Then you put the attribute into the attribute collection of the message you are working with. For a signed message you add the attribute object to the CmsSigner object's SignedAttributes or UnsignedAttributes collection; for enveloped messages you add the attribute object to the UnprotectedAttributes collection of the EnvelopedCms object. Some attribute objects are initialized when the message is created, for example, the message digest, signing time and content type objects will be initialized when the message is signed.

I hope that you enjoy this tutorial and value the knowledge that you will gain from it. I am always pleased to hear from people who use this tutorial (contact me). If you find this tutorial useful then please also email your comments to mvpga@microsoft.com.

Errata

If you see an error on this page, please contact me and I will fix the problem.

Page Fifteen

This page is (c) 2007 Richard Grimes, all rights reserved