From cfff1d741d20aef00753f0123ac2defb07dd070c Mon Sep 17 00:00:00 2001 From: Josh Roys Date: Thu, 22 Aug 2019 10:45:48 -0400 Subject: [PATCH 1/7] Implement PKCS#7 signature verification. --- lib/pkcs7.js | 175 +++++++++++++++++++++++++++++++++++++++++++- lib/pkcs7asn1.js | 19 ++++- lib/util.js | 23 ++++++ lib/x509.js | 24 +----- tests/unit/pkcs7.js | 81 ++++++++++++++++++++ 5 files changed, 294 insertions(+), 28 deletions(-) diff --git a/lib/pkcs7.js b/lib/pkcs7.js index bb87de363..d92073097 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() { @@ -378,7 +385,8 @@ p7.createSignedData = function() { }, verify: function() { - throw new Error('PKCS#7 signature verification not yet implemented.'); + var mds = addDigestAlgorithmIds(); + return verifySignerInfos(mds); }, /** @@ -537,6 +545,123 @@ p7.createSignedData = function() { // add signer info msg.signerInfos = _signersToAsn1(msg.signers); } + + function verifySignerInfos(mds) { + var content; + var rval = true; + + 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) { + throw new Error( + 'Could not verify PKCS#7 message; there is no content to verify.'); + } + + if(msg.signers.length === 0) { + throw new Error('There are no signatures to verify.'); + } + + // 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) { + throw new Error('Unable to find signing certificate.'); + } + + 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) { + throw new Error( + 'Invalid signer; authenticatedAttributes must be present ' + + 'when the ContentInfo content type is not PKCS#7 Data.'); + } + } 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(!verifyOpts.validityCheckDate) { + 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) { + throw new Error('Authenticated attributes missing digest! Unable to verify signature.'); + } + + // DER-serialize and digest SET OF attributes only + bytes = asn1.toDer(attrsAsn1).getBytes(); + signer.md.start().update(bytes); + } + + // verify digest + var verified = signerCert.publicKey.verify(signer.md.digest().bytes(), signer.signature, 'RSASSA-PKCS1-V1_5'); + rval = rval && verified; + } + + return rval; + } }; /** @@ -918,7 +1043,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 +1061,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 +1168,44 @@ 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); + } + } + rval.value = value; + + return rval; +} + /** * Convert an attribute object to an ASN.1 Attribute. * 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..a33b9f10f 100644 --- a/lib/util.js +++ b/lib/util.js @@ -2998,3 +2998,26 @@ util.estimateCores = function(options, callback) { }, 0); } }; + +util.compareDN = function(l, r) { + var rval = false; + + // compare hashes if present + if(l.hash && r.hash) { + rval = (l.hash === r.hash); + } else if(l.attributes.length === r.attributes.length) { + // all attributes are the same so issuer matches subject + rval = true; + var iattr, sattr; + for(var n = 0; rval && n < l.attributes.length; ++n) { + iattr = l.attributes[n]; + sattr = r.attributes[n]; + if(iattr.type !== sattr.type || iattr.value !== sattr.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..5fcf6d326 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,52 @@ 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(); + 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(); + ASSERT.equal(verified, false); + }); + + it('should verify PKCS#7 signature w/attributes', function() { + var p7 = PKCS7.messageFromPem(_pem.signedDataWithAttrs1950UTCTime); + var verified = p7.verify(); + 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 options = { + validityCheckDate: new Date('2012-12-25T00:00:00Z'), + callback: (cbstatus, cert) => { + ASSERT.equal(cbstatus, true); + ASSERT.equal(cert.serialNumber, '00d4541c40d835e2f3'); + }, + }; + 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: From c1f5084eb81c00b0e6810c0767b85216ddaa1da1 Mon Sep 17 00:00:00 2001 From: Josh Roys Date: Thu, 22 Aug 2019 11:24:28 -0400 Subject: [PATCH 2/7] Add certificate chain verification. --- lib/pkcs7.js | 15 ++++++++++++--- tests/unit/pkcs7.js | 6 +++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/lib/pkcs7.js b/lib/pkcs7.js index d92073097..b2f145421 100644 --- a/lib/pkcs7.js +++ b/lib/pkcs7.js @@ -384,9 +384,12 @@ p7.createSignedData = function() { addSignerInfos(mds); }, - verify: function() { + 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); + return verifySignerInfos(mds, caStore, options || {}); }, /** @@ -546,7 +549,7 @@ p7.createSignedData = function() { msg.signerInfos = _signersToAsn1(msg.signers); } - function verifySignerInfos(mds) { + function verifySignerInfos(mds, caStore, options) { var content; var rval = true; @@ -613,6 +616,11 @@ p7.createSignedData = function() { throw new Error('Unable to find signing certificate.'); } + var verifyOpts = {}; + if(options.validityCheckDate) { + verifyOpts.validityCheckDate = options.validityCheckDate; + } + if(signer.authenticatedAttributes.length === 0) { // if ContentInfo content type is not "Data", then // authenticatedAttributes must be present per RFC 2315 @@ -654,6 +662,7 @@ p7.createSignedData = function() { 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'); diff --git a/tests/unit/pkcs7.js b/tests/unit/pkcs7.js index 5fcf6d326..7348d5713 100644 --- a/tests/unit/pkcs7.js +++ b/tests/unit/pkcs7.js @@ -738,19 +738,19 @@ var UTIL = require('../../lib/util'); it('should verify PKCS#7 signature w/o attributes', function() { var p7 = PKCS7.messageFromPem(_pem.signedDataNoAttrs); - var verified = p7.verify(); + 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(); + 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(); + var verified = p7.verify(PKI.createCaStore([_pem.certificate]), { validityCheckDate: new Date('2012-12-25T00:00:00Z') }); ASSERT.equal(verified, true); }); From fd656a11e35a6b8e2f8297e8e7b389d875b6ede9 Mon Sep 17 00:00:00 2001 From: Josh Roys Date: Thu, 22 Aug 2019 11:35:31 -0400 Subject: [PATCH 3/7] Add callback for each PKCS#7 signature verification. --- lib/pkcs7.js | 27 ++++++++++++++++++++------- tests/unit/pkcs7.js | 10 ++++++---- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/lib/pkcs7.js b/lib/pkcs7.js index b2f145421..31eb36efe 100644 --- a/lib/pkcs7.js +++ b/lib/pkcs7.js @@ -552,6 +552,7 @@ p7.createSignedData = function() { 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 @@ -573,12 +574,13 @@ p7.createSignedData = function() { } if(!content) { - throw new Error( - 'Could not verify PKCS#7 message; there is no content to verify.'); + svEvent && svEvent(new Error('Could not verify PKCS#7 message; there is no content to verify.'), null); + return false; } if(msg.signers.length === 0) { - throw new Error('There are no signatures to verify.'); + svEvent && svEvent(new Error('There are no signatures to verify.'), null); + return false; } // get ContentInfo content type @@ -613,7 +615,9 @@ p7.createSignedData = function() { } } if(signerCert === null) { - throw new Error('Unable to find signing certificate.'); + svEvent && svEvent(new Error('Unable to find signing certificate.'), null); + rval = false; + continue; } var verifyOpts = {}; @@ -625,9 +629,13 @@ p7.createSignedData = function() { // if ContentInfo content type is not "Data", then // authenticatedAttributes must be present per RFC 2315 if(contentType !== forge.pki.oids.data) { - throw new Error( + svEvent && svEvent(new Error( 'Invalid signer; authenticatedAttributes must be present ' + - 'when the ContentInfo content type is not PKCS#7 Data.'); + '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 @@ -655,7 +663,9 @@ p7.createSignedData = function() { attrsAsn1.value.push(_attributeToAsn1(signer.authenticatedAttributes[ai])); } if(!foundDigest) { - throw new Error('Authenticated attributes missing digest! Unable to verify signature.'); + svEvent && svEvent(new Error('Authenticated attributes missing digest! Unable to verify signature.'), null); + rval = false; + continue; } // DER-serialize and digest SET OF attributes only @@ -667,6 +677,9 @@ p7.createSignedData = function() { // 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; diff --git a/tests/unit/pkcs7.js b/tests/unit/pkcs7.js index 7348d5713..7fba5598a 100644 --- a/tests/unit/pkcs7.js +++ b/tests/unit/pkcs7.js @@ -771,12 +771,14 @@ var UTIL = require('../../lib/util'); 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'), - callback: (cbstatus, cert) => { - ASSERT.equal(cbstatus, true); - ASSERT.equal(cert.serialNumber, '00d4541c40d835e2f3'); - }, }; var verified = p7.verify(PKI.createCaStore([_pem.certificate]), options); ASSERT.equal(verified, true); From 43ff714a53d927c0099cc5d64e197d0ae19bf7dc Mon Sep 17 00:00:00 2001 From: Josh Roys Date: Tue, 3 Sep 2019 12:39:29 -0400 Subject: [PATCH 4/7] Support arbitrary attributes (e.g. S/MIME caps) --- lib/pkcs7.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/pkcs7.js b/lib/pkcs7.js index 31eb36efe..d13849fa2 100644 --- a/lib/pkcs7.js +++ b/lib/pkcs7.js @@ -1222,6 +1222,8 @@ function _attributeFromAsn1(attr) { } 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; @@ -1280,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 From 25c7b12ed8750324782561c1e3cf1250b33ceb0f Mon Sep 17 00:00:00 2001 From: Sean Ferguson Date: Thu, 10 Oct 2019 18:34:09 -0400 Subject: [PATCH 5/7] pkcs7: allow for null validityCheckDate for consistency with x509 --- lib/pkcs7.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pkcs7.js b/lib/pkcs7.js index d13849fa2..06d8096ab 100644 --- a/lib/pkcs7.js +++ b/lib/pkcs7.js @@ -621,7 +621,7 @@ p7.createSignedData = function() { } var verifyOpts = {}; - if(options.validityCheckDate) { + if(typeof options.validityCheckDate !== 'undefined') { verifyOpts.validityCheckDate = options.validityCheckDate; } @@ -648,7 +648,7 @@ p7.createSignedData = function() { for(var ai in signer.authenticatedAttributes) { switch(signer.authenticatedAttributes[ai].type) { case forge.pki.oids.signingTime: - if(!verifyOpts.validityCheckDate) { + if(typeof verifyOpts.validityCheckDate === 'undefined') { verifyOpts.validityCheckDate = signer.authenticatedAttributes[ai].value; } break; From b4385e2eeca0011dd154c1b784f7e15fba9c12a8 Mon Sep 17 00:00:00 2001 From: Josh Roys Date: Wed, 11 Mar 2020 15:12:40 -0400 Subject: [PATCH 6/7] Add example of PKCS#7 verification to README --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) 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!'); +} + ``` From fce10da82d8ae3228e399bca361ad11ccc1f55c0 Mon Sep 17 00:00:00 2001 From: Josh Roys Date: Wed, 11 Mar 2020 15:29:54 -0400 Subject: [PATCH 7/7] Add util.compareDN documentation and update variable names --- lib/util.js | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/lib/util.js b/lib/util.js index a33b9f10f..1c47eab9b 100644 --- a/lib/util.js +++ b/lib/util.js @@ -2999,20 +2999,28 @@ util.estimateCores = function(options, callback) { } }; -util.compareDN = function(l, r) { +/** + * 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(l.hash && r.hash) { - rval = (l.hash === r.hash); - } else if(l.attributes.length === r.attributes.length) { + 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 iattr, sattr; - for(var n = 0; rval && n < l.attributes.length; ++n) { - iattr = l.attributes[n]; - sattr = r.attributes[n]; - if(iattr.type !== sattr.type || iattr.value !== sattr.value) { + 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; }