-
Notifications
You must be signed in to change notification settings - Fork 833
Implement PKCS#7 signature verification. #706
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
roysjosh
wants to merge
7
commits into
digitalbazaar:main
Choose a base branch
from
roysjosh:ImplementPkcs7Verify
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+342
−29
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
cfff1d7
Implement PKCS#7 signature verification.
roysks c1f5084
Add certificate chain verification.
roysks fd656a1
Add callback for each PKCS#7 signature verification.
roysks 43ff714
Support arbitrary attributes (e.g. S/MIME caps)
roysks 25c7b12
pkcs7: allow for null validityCheckDate for consistency with x509
fergusean b4385e2
Add example of PKCS#7 verification to README
roysks fce10da
Add util.compareDN documentation and update variable names
roysks File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does any deeper validation need to happen here? Or if the stream is bogus will the digests just not match so that's fine? |
||
|
|
||
| // 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 | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see you're providing for
options.callback. This seems to me to be an unconventional way of implementing a callback which would not be compatible withutil.promisifyimplementations which expect the last param to be a callback function.That said, can you say more about the use case for callbacks here? Given that all the code here is synchronous, does the callback add value?
If the callback is going to remain part of this API, it appears to me that it is not being called in a number of situations. For example, if the conditional on L556 fails we end up at L677
retrurn rval;and the callback is never called. Also, for the most part this function is written to throw errors at various times when it may be appropriate to call the callback withcallback(err);instead.Do you think it would be simpler to eliminate the callback functionality?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a callback so that as a part of the verification process you would get information on good signatures to display to the user. I would like to keep it for that reason. I'll clean it up and make it promisifyable and we'll see how it looks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pushed, let me know what you think. Double check the code flow? L556-573 is a contained if/else block.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hello, are you saying you implemented the callback in order to return different values from a synchronous call vs a callback call? It does appear that is what is happening? I think we need the sync/callback versions to return the same data structure, is there something preventing consistency here? And if the synchronous version can return the same data structure as the callback, do we still need the callback? https://github.com/digitalbazaar/forge/pull/706/files#diff-d4c741d422d58aea2d7ffefe1687abd2R678
Also, without a
process.nextTickor something to defer execution, I believe this code effectively runs synchronously with or without the callback.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The purpose of the callback isn't to get a result so much as status updates on each signature processed. The verify function could return an array of results, yes. At that point I would just eliminate the callback. Is that what you would prefer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand now that my confusion has been caused by referring to this passed in function as a
callback. I think of acallbackas a function that is called when the called function completes/errors. This is reflected in theutil.promisifyAPI such that the Promise is resolved when the callback is invoked, which is not what you are attempting to do here.If this were a long running asynchronous process, a solution involving event emitters might be appropriate.
Now that I understand what you're trying to do and
options.onSignatureVerificationCompletehandler might be appropriate.Do you have some use case that requires there to be status updates on each signature? If not, then I think removing the status callback function would be best.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I modify the return to contain the same info as the status callback/emitter then no, I don't need it. I'll do that now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ended up going with the per-signature event emitter vs a return of an array of results. Let me know what you think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should be a common pattern in the whole lib for events and more detailed results. That's a bigger project. This does do the common return true/false and that will work for now.