Verify PKCS#7 (PEM) signature / unpack data in node.js

Guard picture Guard · Apr 12, 2013 · Viewed 7.4k times · Source

I get a PKCS#7 crypto package from a 3rd party system. The package is not compressed and not encrypted, PEM-encoded, signed with X.509 certificate. I also have a PEM cert file from the provider.

The data inside is XML

I need to do the following in Node.JS:

  • extract the data
  • verify the signature

A sample package (no sensitive info, data refers to our qa system) http://pastebin.com/7ay7F99e

Answer

Guard picture Guard · Apr 22, 2013

OK, finally got it.

First of all, PKCS messages are complex structures binary-encoded using ASN1.

Second, they can be serialized to binary files (DER encoding) or text PEM files using Base64 encoding.

Third, PKCS#7 format specifies several package types from which my is called Signed Data. These formats are distinguished by OBJECT IDENTIFIER value in the beginning of the ASN1 object (1st element of the wrapper sequence) — you can go to http://lapo.it/asn1js/ and paste the package text for the fully parsed structure.

Next, we need to parse the package (Base64 -> ASN1 -> some object representation). Unfortunately, there's no npm package for that. I found quite a good project forge that is not published to npm registry (though npm-compatible). It parsed PEM format but the resulting tree is quite an unpleasant thing to traverse. Based on their Encrypted Data and Enveloped Data implementations I created partial implementation of Signed Data in my own fork. UPD: my pull request was later merged to the forge project.

Now finally we have the whole thing parsed. At that point I found a great (and probably the only on the whole web) explanative article on signed PKCS#7 verification: http://qistoph.blogspot.com/2012/01/manual-verify-pkcs7-signed-data-with.html

I was able to extract and successfully decode the signature from the file, but the hash inside was different from the data's hash. God bless Chris who explained what actually happens.

The data signing process is 2-step:

  1. original content's hash is calculated
  2. a set of "Authorized Attributes" is constructed including: type of the data singed, signing time and data hash

Then the set from step 2 is signed using the signer's private key.

Due to PKCS#7 specifics this set of attributes is stored inside of the context-specific constructed type (class=0x80, type=0) but should be signed and validated as normal SET (class=0, type=17).

As Chris mentions (https://stackoverflow.com/a/16154756/108533) this only verifies that the attributes in the package are valid. We should also validate the actual data hash against the digest attribute.

So finally here's a code doing validation (cert.pem is a certificate file that the provider sent me, package is a PEM-encoded message I got from them over HTTP POST):

var fs = require('fs');
var crypto = require('crypto');
var forge = require('forge');
var pkcs7 = forge.pkcs7; 
var asn1 = forge.asn1;
var oids = forge.pki.oids;

var folder = '/a/path/to/files/';
var pkg = fs.readFileSync(folder + 'package').toString();
var cert = fs.readFileSync(folder + 'cert.pem').toString();


var res = true;

try {
    var msg = pkcs7.messageFromPem(pkg);
    var attrs = msg.rawCapture.authenticatedAttributes;
    var set = asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SET, true, attrs);
    var buf = Buffer.from(asn1.toDer(set).data, 'binary');

    var sig = msg.rawCapture.signature;

    var v = crypto.createVerify('RSA-SHA1');
    v.update(buf);
    if (!v.verify(cert, sig)) {
        console.log('Wrong authorized attributes!');
        res = false;
    }

    var h = crypto.createHash('SHA1');
    var data = msg.rawCapture.content.value[0].value[0].value;
    h.update(data);

    var attrDigest = null;
    for (var i = 0, l = attrs.length; i < l; ++i) {
        if (asn1.derToOid(attrs[i].value[0].value) === oids.messageDigest) {
            attrDigest = attrs[i].value[1].value[0].value;
        }
    }

    var dataDigest = h.digest();
    if (dataDigest !== attrDigest) {
        console.log('Wrong content digest');
        res = false;
    }

}
catch (_e) {
    console.dir(_e);
    res = false;
}

if (res) {
    console.log("It's OK");
}