diff --git a/README.md b/README.md index 40bf29561..c4ca80f6f 100644 --- a/README.md +++ b/README.md @@ -1380,6 +1380,17 @@ var pem = forge.pkcs7.messageToPem(p7); // Includes the signature and certificate without the signed data. p7.sign({detached: true}); +// Verify a PKCS#7 signature +var caStore = forge.pki.createCaStore(); +caStore.addCertificate(caPem); +var p7 = forge.pkcs7.messageFromPem(pem); +// if the signature was detached, reattach it +p7.content = forge.util.createBuffer('Some content to be signed.', 'utf8'); +// return is true IFF all signatures are valid and chain up to a provided CA +if(!p7.verify(caStore)) { + throw new Error('invalid signature!'); +} + ``` diff --git a/lib/pkcs7.js b/lib/pkcs7.js index bb87de363..06d8096ab 100644 --- a/lib/pkcs7.js +++ b/lib/pkcs7.js @@ -150,6 +150,13 @@ p7.createSignedData = function() { } // TODO: parse crls + + msg.contentInfo = msg.rawCapture.contentInfo; + msg.signerInfos = msg.rawCapture.signerInfos; + + if(msg.signerInfos) { + msg.signers = _signersFromAsn1(msg.signerInfos); + } }, toAsn1: function() { @@ -377,8 +384,12 @@ p7.createSignedData = function() { addSignerInfos(mds); }, - verify: function() { - throw new Error('PKCS#7 signature verification not yet implemented.'); + verify: function(caStore, options) { + if(!caStore) { + throw new Error('You must provide a CA store for PKCS#7 verification.'); + } + var mds = addDigestAlgorithmIds(); + return verifySignerInfos(mds, caStore, options || {}); }, /** @@ -537,6 +548,142 @@ p7.createSignedData = function() { // add signer info msg.signerInfos = _signersToAsn1(msg.signers); } + + function verifySignerInfos(mds, caStore, options) { + var content; + var rval = true; + var svEvent = options.onSignatureVerificationComplete; + + if(msg.contentInfo !== null && msg.contentInfo.value[1]) { + // Note: ContentInfo is a SEQUENCE with 2 values, second value is + // the content field and is optional for a ContentInfo but required here + // since signers are present + // get ContentInfo content + content = msg.contentInfo.value[1]; + // skip [0] EXPLICIT content wrapper + content = content.value[0]; + } else if('content' in msg) { + // signature was likely made in detached mode + // caller must set p7.content before attempting to verify + if(msg.content instanceof forge.util.ByteBuffer) { + content = msg.content.bytes(); + } else if(typeof msg.content === 'string') { + content = forge.util.encodeUtf8(msg.content); + } + content = asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, content); + } + + if(!content) { + svEvent && svEvent(new Error('Could not verify PKCS#7 message; there is no content to verify.'), null); + return false; + } + + if(msg.signers.length === 0) { + svEvent && svEvent(new Error('There are no signatures to verify.'), null); + return false; + } + + // get ContentInfo content type + var contentType = asn1.derToOid(msg.contentInfo.value[0].value); + + // serialize content + var bytes = asn1.toDer(content); + + // skip identifier and length per RFC 2315 9.3 + // skip identifier (1 byte) + bytes.getByte(); + // read and discard length bytes + asn1.getBerValueLength(bytes); + bytes = bytes.getBytes(); + + // digest content DER value bytes + for(var oid in mds) { + mds[oid].start().update(bytes); + } + + // verify content + for(var i = 0; i < msg.signers.length; ++i) { + var signer = msg.signers[i]; + + // find certificate + var signerCert = null; + for(var j = 0; j < msg.certificates.length; ++j) { + var cert = msg.certificates[j]; + if(forge.util.compareDN({attributes: signer.issuer}, cert.issuer) && + signer.serialNumber === cert.serialNumber) { + signerCert = cert; + } + } + if(signerCert === null) { + svEvent && svEvent(new Error('Unable to find signing certificate.'), null); + rval = false; + continue; + } + + var verifyOpts = {}; + if(typeof options.validityCheckDate !== 'undefined') { + verifyOpts.validityCheckDate = options.validityCheckDate; + } + + if(signer.authenticatedAttributes.length === 0) { + // if ContentInfo content type is not "Data", then + // authenticatedAttributes must be present per RFC 2315 + if(contentType !== forge.pki.oids.data) { + svEvent && svEvent(new Error( + 'Invalid signer; authenticatedAttributes must be present ' + + 'when the ContentInfo content type is not PKCS#7 Data.'), + null + ); + rval = false; + continue; + } + } else { + // per RFC 2315, attributes are to be digested using a SET container + // not the above [0] IMPLICIT container + var attrsAsn1 = asn1.create( + asn1.Class.UNIVERSAL, asn1.Type.SET, true, []); + + // if you have authenticated attributes, one of them must be the digest as this is how the content is verified + var foundDigest = false; + for(var ai in signer.authenticatedAttributes) { + switch(signer.authenticatedAttributes[ai].type) { + case forge.pki.oids.signingTime: + if(typeof verifyOpts.validityCheckDate === 'undefined') { + verifyOpts.validityCheckDate = signer.authenticatedAttributes[ai].value; + } + break; + case forge.pki.oids.messageDigest: + foundDigest = true; + signer.authenticatedAttributes[ai].value = mds[signer.digestAlgorithm].digest(); + break; + default: + break; + } + + attrsAsn1.value.push(_attributeToAsn1(signer.authenticatedAttributes[ai])); + } + if(!foundDigest) { + svEvent && svEvent(new Error('Authenticated attributes missing digest! Unable to verify signature.'), null); + rval = false; + continue; + } + + // DER-serialize and digest SET OF attributes only + bytes = asn1.toDer(attrsAsn1).getBytes(); + signer.md.start().update(bytes); + } + forge.pki.verifyCertificateChain(caStore, msg.certificates, verifyOpts); + + // verify digest + var verified = signerCert.publicKey.verify(signer.md.digest().bytes(), signer.signature, 'RSASSA-PKCS1-V1_5'); + rval = rval && verified; + + // emit the final status of this signature + svEvent && svEvent(null, {verified: verified, signer: signerCert}); + } + + return rval; + } }; /** @@ -918,7 +1065,7 @@ function _signerFromAsn1(obj) { // validate EnvelopedData content block and capture data var capture = {}; var errors = []; - if(!asn1.validate(obj, p7.asn1.signerInfoValidator, capture, errors)) { + if(!asn1.validate(obj, p7.asn1.signerValidator, capture, errors)) { var error = new Error('Cannot read PKCS#7 SignerInfo. ' + 'ASN.1 object is not an PKCS#7 SignerInfo.'); error.errors = errors; @@ -936,10 +1083,16 @@ function _signerFromAsn1(obj) { unauthenticatedAttributes: [] }; - // TODO: convert attributes var authenticatedAttributes = capture.authenticatedAttributes || []; var unauthenticatedAttributes = capture.unauthenticatedAttributes || []; + for(var i in authenticatedAttributes) { + rval.authenticatedAttributes.push(_attributeFromAsn1(authenticatedAttributes[i])); + } + for(var j in unauthenticatedAttributes) { + rval.unauthenticatedAttributes.push(_attributeFromAsn1(unauthenticatedAttributes[j])); + } + return rval; } @@ -1037,6 +1190,46 @@ function _signersToAsn1(signers) { return ret; } +/** + * Convert an attribute object from an ASN.1 Attribute. + * + * @param attr the ASN.1 Attribute. + * + * @return the attribute object. + */ +function _attributeFromAsn1(attr) { + var rval = {}; + var type; + var value; + + type = asn1.derToOid(attr.value[0].value); + rval.type = type; + + if(type === forge.pki.oids.contentType) { + value = asn1.derToOid(attr.value[1].value[0].value); + } else if(type === forge.pki.oids.messageDigest) { + value = forge.util.createBuffer(attr.value[1].value[0].value); + } else if(type === forge.pki.oids.signingTime) { + /* Note per RFC 2985: Dates between 1 January 1950 and 31 December 2049 + (inclusive) MUST be encoded as UTCTime. Any dates with year values + before 1950 or after 2049 MUST be encoded as GeneralizedTime. [Further,] + UTCTime values MUST be expressed in Greenwich Mean Time (Zulu) and MUST + include seconds (i.e., times are YYMMDDHHMMSSZ), even where the + number of seconds is zero. Midnight (GMT) must be represented as + "YYMMDD000000Z". */ + if(attr.value[1].value[0].type === asn1.Type.UTCTIME) { + value = asn1.utcTimeToDate(attr.value[1].value[0].value); + } else if(attr.value[1].value[0].type === asn1.Type.GENERALIZEDTIME) { + value = asn1.generalizedTimeToDate(attr.value[1].value[0].value); + } + } else { + value = attr.value[1].value[0]; + } + rval.value = value; + + return rval; +} + /** * Convert an attribute object to an ASN.1 Attribute. * @@ -1089,6 +1282,8 @@ function _attributeToAsn1(attr) { asn1.Class.UNIVERSAL, asn1.Type.GENERALIZEDTIME, false, asn1.dateToGeneralizedTime(date)); } + } else { + value = attr.value; } // TODO: expose as common API call diff --git a/lib/pkcs7asn1.js b/lib/pkcs7asn1.js index a2ac01f85..66088108e 100644 --- a/lib/pkcs7asn1.js +++ b/lib/pkcs7asn1.js @@ -124,6 +124,7 @@ var contentInfoValidator = { tagClass: asn1.Class.UNIVERSAL, type: asn1.Type.SEQUENCE, constructed: true, + captureAsn1: 'contentInfo', value: [{ name: 'ContentInfo.ContentType', tagClass: asn1.Class.UNIVERSAL, @@ -246,7 +247,8 @@ var signerValidator = { name: 'SignerInfo.version', tagClass: asn1.Class.UNIVERSAL, type: asn1.Type.INTEGER, - constructed: false + constructed: false, + capture: 'version' }, { name: 'SignerInfo.issuerAndSerialNumber', tagClass: asn1.Class.UNIVERSAL, @@ -295,7 +297,19 @@ var signerValidator = { tagClass: asn1.Class.UNIVERSAL, type: asn1.Type.SEQUENCE, constructed: true, - capture: 'signatureAlgorithm' + value: [{ + name: 'SignerInfo.digestEncryptionAlgorithm.algorithm', + tagClass: asn1.Class.UNIVERSAL, + type: asn1.Type.OID, + constructed: false, + capture: 'signatureAlgorithm' + }, { + name: 'SignerInfo.digestEncryptionAlgorithm.parameter', + tagClass: asn1.Class.UNIVERSAL, + constructed: false, + captureAsn1: 'signatureParameter', + optional: true + }] }, { name: 'SignerInfo.encryptedDigest', tagClass: asn1.Class.UNIVERSAL, @@ -311,6 +325,7 @@ var signerValidator = { capture: 'unauthenticatedAttributes' }] }; +p7v.signerValidator = signerValidator; p7v.signedDataValidator = { name: 'SignedData', diff --git a/lib/util.js b/lib/util.js index a86609284..1c47eab9b 100644 --- a/lib/util.js +++ b/lib/util.js @@ -2998,3 +2998,34 @@ util.estimateCores = function(options, callback) { }, 0); } }; + +/** + * Compare two DNs for equality. + * + * @param dn1 a distinguished name object + * @param dn2 a distinguished name object + * + * @return true if the DN objects are equal, false otherwise. + */ +util.compareDN = function(dn1, dn2) { + var rval = false; + + // compare hashes if present + if(dn1.hash && dn2.hash) { + rval = (dn1.hash === dn2.hash); + } else if(dn1.attributes.length === dn2.attributes.length) { + // all attributes are the same so issuer matches subject + rval = true; + var dn1attr, dn2attr; + for(var n = 0; rval && n < dn1.attributes.length; ++n) { + dn1attr = dn1.attributes[n]; + dn2attr = dn2.attributes[n]; + if(dn1attr.type !== dn2attr.type || dn1attr.value !== dn2attr.value) { + // attribute mismatch + rval = false; + } + } + } + + return rval; +}; diff --git a/lib/x509.js b/lib/x509.js index 95dbc2946..1d82c0fd7 100644 --- a/lib/x509.js +++ b/lib/x509.js @@ -1175,29 +1175,7 @@ pki.createCertificate = function() { * subject. */ cert.isIssuer = function(parent) { - var rval = false; - - var i = cert.issuer; - var s = parent.subject; - - // compare hashes if present - if(i.hash && s.hash) { - rval = (i.hash === s.hash); - } else if(i.attributes.length === s.attributes.length) { - // all attributes are the same so issuer matches subject - rval = true; - var iattr, sattr; - for(var n = 0; rval && n < i.attributes.length; ++n) { - iattr = i.attributes[n]; - sattr = s.attributes[n]; - if(iattr.type !== sattr.type || iattr.value !== sattr.value) { - // attribute mismatch - rval = false; - } - } - } - - return rval; + return forge.util.compareDN(cert.issuer, parent.subject); }; /** diff --git a/tests/unit/pkcs7.js b/tests/unit/pkcs7.js index e99c13867..7fba5598a 100644 --- a/tests/unit/pkcs7.js +++ b/tests/unit/pkcs7.js @@ -172,6 +172,41 @@ var UTIL = require('../../lib/util'); '0pRXsBgGNbe1FClekomqKBeeuTfBgyKd+HhabcCNc6Q7kZBfBU9T0JUFhPj5ut39\r\n' + 'JYiOgKdXRs1MdQqnl0Q=\r\n' + '-----END PKCS7-----\r\n', + signedDataNoAttrsBadSig: + '-----BEGIN PKCS7-----\r\n' + + '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\r\n' + + 'CkViDnIVfo8EvGUdqt8=\r\n' + + '-----END PKCS7-----\r\n', signedDataWithAttrs1949GeneralizedTime: '-----BEGIN PKCS7-----\r\n' + 'MIIGRwYJKoZIhvcNAQcCoIIGODCCBjQCAQExDzANBglghkgBZQMEAgEFADAcBgkq\r\n' + @@ -701,6 +736,54 @@ var UTIL = require('../../lib/util'); ASSERT.equal(pem, _pem.detachedSignature); }); + it('should verify PKCS#7 signature w/o attributes', function() { + var p7 = PKCS7.messageFromPem(_pem.signedDataNoAttrs); + var verified = p7.verify(PKI.createCaStore([_pem.certificate]), { validityCheckDate: new Date('2012-12-25T00:00:00Z') }); + ASSERT.equal(verified, true); + }); + + it('should fail to verify bad PKCS#7 signature w/o attributes', function() { + var p7 = PKCS7.messageFromPem(_pem.signedDataNoAttrsBadSig); + var verified = p7.verify(PKI.createCaStore([_pem.certificate]), { validityCheckDate: new Date('2012-12-25T00:00:00Z') }); + ASSERT.equal(verified, false); + }); + + it('should verify PKCS#7 signature w/attributes', function() { + var p7 = PKCS7.messageFromPem(_pem.signedDataWithAttrs1950UTCTime); + var verified = p7.verify(PKI.createCaStore([_pem.certificate]), { validityCheckDate: new Date('2012-12-25T00:00:00Z') }); + ASSERT.equal(verified, true); + }); + + it('should verify PKCS#7 detached signature', function() { + var p7 = PKCS7.messageFromPem(_pem.detachedSignature); + p7.content = UTIL.createBuffer('To be signed.', 'utf8'); + var verified = p7.verify(PKI.createCaStore([_pem.certificate]), { validityCheckDate: new Date('2012-12-25T00:00:00Z') }); + ASSERT.equal(verified, true); + }); + + it('should fail to verify bad PKCS#7 detached signature', function() { + var p7 = PKCS7.messageFromPem(_pem.detachedSignature); + p7.content = UTIL.createBuffer('To be verified.', 'utf8'); + var verified = p7.verify(PKI.createCaStore([_pem.certificate]), { validityCheckDate: new Date('2012-12-25T00:00:00Z') }); + ASSERT.equal(verified, false); + }); + + it('should callback with a status and certificate', function() { + var p7 = PKCS7.messageFromPem(_pem.detachedSignature); + p7.content = UTIL.createBuffer('To be signed.', 'utf8'); + var callback = (err, res) => { + ASSERT.equal(err, null); + ASSERT.equal(res.verified, true); + ASSERT.equal(res.signer.serialNumber, '00d4541c40d835e2f3'); + }; + var options = { + onSignatureVerificationComplete: callback, + validityCheckDate: new Date('2012-12-25T00:00:00Z'), + }; + var verified = p7.verify(PKI.createCaStore([_pem.certificate]), options); + ASSERT.equal(verified, true); + }); + it('should create PKCS#7 SignedData with content-type, message-digest, ' + 'and signing-time attributes using UTCTime (2049)', function() { // verify with: