Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!');
}

```

<a name="pkcs8" />
Expand Down
203 changes: 199 additions & 4 deletions lib/pkcs7.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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 || {});
},

/**
Expand Down Expand Up @@ -537,6 +548,142 @@ p7.createSignedData = function() {
// add signer info
msg.signerInfos = _signersToAsn1(msg.signers);
}

function verifySignerInfos(mds, caStore, options) {
Copy link
Contributor

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 with util.promisify implementations 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 with callback(err); instead.

Do you think it would be simpler to eliminate the callback functionality?

Copy link
Author

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.

Copy link
Author

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.

Copy link
Contributor

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.nextTick or something to defer execution, I believe this code effectively runs synchronously with or without the callback.

Copy link
Author

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?

Copy link
Contributor

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 a callback as a function that is called when the called function completes/errors. This is reflected in the util.promisify API 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.onSignatureVerificationComplete handler 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.

Copy link
Author

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.

Copy link
Author

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.

Copy link
Member

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.

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();
Copy link
Member

Choose a reason for hiding this comment

The 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;
}
};

/**
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}

Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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
Expand Down
19 changes: 17 additions & 2 deletions lib/pkcs7asn1.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -311,6 +325,7 @@ var signerValidator = {
capture: 'unauthenticatedAttributes'
}]
};
p7v.signerValidator = signerValidator;

p7v.signedDataValidator = {
name: 'SignedData',
Expand Down
31 changes: 31 additions & 0 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
24 changes: 1 addition & 23 deletions lib/x509.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};

/**
Expand Down
Loading