Enveloped PKCS #7 Signatures

One of the new cryptography features in the v2.0 framework is the ability to work with PKCS #7 formatted messages.  The PKCS features live in the new System.Security.Cryptography.Pkcs namespace in System.Security.dll, and are thin wrappers around the CAPI PKCS #7 implementation.  In fact, the actual code was provided to the CLR team by the Windows team.

These classes make it quick and easy to sign some arbitrary data, enveloping it in a p7s signature file.  On the other end, you can load up a p7s signature, and verify that the data is still valid, then extract the enveloped data.  These operations are centered around the SignedCms class.

Here's how a simple enveloped signature would be created:

public static byte[] Sign(byte[] data, X509Certificate2 certificate)
{
    if(data == null)
        throw new ArgumentNullException("data");
    if(certificate == null)
        throw new ArgumentNullException("certificate");
        
    // setup the data to sign
    ContentInfo content = new ContentInfo(data);
    SignedCms signedCms = new SignedCms(content, false);
    CmsSigner signer = new CmsSigner(SubjectIdentifierType.IssuerAndSerialNumber, certificate);
        
    // create the signature
    signedCms.ComputeSignature(signer);
    return signedCms.Encode();
}

First we wrap the data in a ContentInfo object.  Then we create a SignedCms object to create the signature, passing in the content to sign and indicating that we want an enveloped signature by passing false for the detached parameter.  The last setup step is to create a CmsSigner with the certificate which has the private key that will be used to create the signature.  We'll record this certificate by making note of its serial number and who issued it.  After the setup is done, all that's left to do is to compute the signature and encode the p7s data.

On the other end, verifying the signature is even easier:

public static bool Verify(byte[] signature, X509Certificate2 certificate)
{
    if(signature == null)
        throw new ArgumentNullException("signature");
    if(certificate == null)
        throw new ArgumentNullException("certificate");
        
    // decode the signature
    SignedCms verifyCms = new SignedCms();
    verifyCms.Decode(signature);

    // verify it
    try
    {
        verifyCms.CheckSignature(new X509Certificate2Collection(certificate), false);
        return true;
    }
    catch(CryptographicException)
    {
        return false;
    }
}

First we create a SignedCms to do the verification.  Then the p7s data is decoded, and we check the signature passing in the certificate we expect it to be signed with.  By passing false as the last parameter to CheckSignature, we're telling the CLR to also reject a signature which is valid but was created by an invalid X.509 certificate.

CheckSignature works differently from other signature verification code that ships with the runtime in that it will do nothing if the signature is valid, but throw a CryptographicException if the signature is invalid.  In order to convert this to a boolean result, we just catch the exception and return false.  The lack of an exception means we can return true.

Another operation we might want to do once we've verified an enveloped signature is to extract the data which was signed.  The SignedCms class also makes this a simple operation:

public static byte[] ExtractEnvelopedData(byte[] signature)
{
    if(signature == null)
        throw new ArgumentNullException("signature");
        
    // decode the signature
    SignedCms cms = new SignedCms();
    cms.Decode(signature);

    if(cms.Detached)
        throw new InvalidOperationException("Cannot extract enveloped content from a detached signature.");
        
    return cms.ContentInfo.Content;
}

Again, we simply decode the signature, and assuming that it was an enveloped signature return the content from within.