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:
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.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:
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:
{
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:
{
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:
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:
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:
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:
{
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:
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:
{
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:
(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:
The tool will list all the certificates in the My store and ask
you to indicate which certificate you want to export:
<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:
Now create a file (envelope.cs)
for a process to envelope a message:
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:
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:
{
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:
{
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:
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:
{
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:
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:
==============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.
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.