diff --git a/README.md b/README.md index 6f3279efb..da7b4c15d 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![Build Status](https://github.com/digitalbazaar/forge/workflows/Main%20Checks/badge.svg)](https://github.com/digitalbazaar/forge/actions?query=workflow%3A%22Main+Checks%22) A native implementation of [TLS][] (and various other cryptographic tools) in -[JavaScript][]. +[JavaScript][], with the addition of cms remote-signing capability (client-side signature, server-side cms generation). Introduction ------------ @@ -1367,6 +1367,39 @@ var pem = forge.pkcs7.messageToPem(p7); // Includes the signature and certificate without the signed data. p7.sign({detached: true}); +// create PKCS#7 signed data structure with authenticatedAttributes +// attributes include: PKCS#9 content-type, message-digest, and signing-time +var p7 = forge.pkcs7.createSignedData(); +p7.content = forge.util.createBuffer('Some content to be signed.', 'utf8'); +p7.addCertificate(certOrCertPem); +p7.addSignerTemplate({ + certificate: certOrCertPem, + digestAlgorithm: forge.pki.oids.sha256, + authenticatedAttributes: [{ + type: forge.pki.oids.contentType, + value: forge.pki.oids.data + }, { + type: forge.pki.oids.messageDigest + // value will be auto-populated at signing time + }, { + type: forge.pki.oids.signingTime, + // value can also be auto-populated at signing time + value: new Date() + }] +}); + +p7.prepare(); + +// DER-serialized of ASN.1's digestInfo as forge's ByteBuffer object +const dtbs = p7.getDigestToBeSigned({ signerSerialNumber: cert.serialNumber }) + +// Simulate client-side signature (RSASSA-PKCS1-V1_5) using crypto library +const signature = crypto.privateEncrypt(privateKeyAssociatedWithCert, Buffer.from(d.toHex(), 'hex')) + +// Add available signature into cms structure +p7.addSignature({ signerSerialNumber: cert.serialNumber, signature.toString('binary') }) + +var pem = forge.pkcs7.messageToPem(p7); ``` diff --git a/lib/pkcs7.js b/lib/pkcs7.js index 3a5d845c5..6bc83b974 100644 --- a/lib/pkcs7.js +++ b/lib/pkcs7.js @@ -133,7 +133,7 @@ p7.createSignedData = function() { contentInfo: null, signerInfos: [], - fromAsn1: function(obj) { + fromAsn1: function (obj) { // validate SignedData content block and capture data. _fromAsn1(msg, obj, p7.asn1.signedDataValidator); msg.certificates = []; @@ -142,9 +142,9 @@ p7.createSignedData = function() { msg.contentInfo = null; msg.signerInfos = []; - if(msg.rawCapture.certificates) { + if (msg.rawCapture.certificates) { var certs = msg.rawCapture.certificates.value; - for(var i = 0; i < certs.length; ++i) { + for (var i = 0; i < certs.length; ++i) { msg.certificates.push(forge.pki.certificateFromAsn1(certs[i])); } } @@ -152,14 +152,14 @@ p7.createSignedData = function() { // TODO: parse crls }, - toAsn1: function() { + toAsn1: function () { // degenerate case with no content - if(!msg.contentInfo) { + if (!msg.contentInfo) { msg.sign(); } var certs = []; - for(var i = 0; i < msg.certificates.length; ++i) { + for (var i = 0; i < msg.certificates.length; ++i) { certs.push(forge.pki.certificateToAsn1(msg.certificates[i])); } @@ -180,12 +180,12 @@ p7.createSignedData = function() { msg.contentInfo ]) ]); - if(certs.length > 0) { + if (certs.length > 0) { // [0] IMPLICIT ExtendedCertificatesAndCertificates OPTIONAL signedData.value[0].value.push( asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, certs)); } - if(crls.length > 0) { + if (crls.length > 0) { // [1] IMPLICIT CertificateRevocationLists OPTIONAL signedData.value[0].value.push( asn1.create(asn1.Class.CONTEXT_SPECIFIC, 1, true, crls)); @@ -246,66 +246,142 @@ p7.createSignedData = function() { * [authenticatedAttributes] an optional array of attributes * to also sign along with the content. */ - addSigner: function(signer) { + addSigner: function (signer) { var issuer = signer.issuer; var serialNumber = signer.serialNumber; - if(signer.certificate) { + if (signer.certificate) { var cert = signer.certificate; - if(typeof cert === 'string') { + if (typeof cert === 'string') { cert = forge.pki.certificateFromPem(cert); } issuer = cert.issuer.attributes; serialNumber = cert.serialNumber; } var key = signer.key; - if(!key) { + if (!key) { throw new Error( 'Could not add PKCS#7 signer; no private key specified.'); } - if(typeof key === 'string') { + if (typeof key === 'string') { key = forge.pki.privateKeyFromPem(key); } // ensure OID known for digest algorithm var digestAlgorithm = signer.digestAlgorithm || forge.pki.oids.sha1; - switch(digestAlgorithm) { - case forge.pki.oids.sha1: - case forge.pki.oids.sha256: - case forge.pki.oids.sha384: - case forge.pki.oids.sha512: - case forge.pki.oids.md5: - break; - default: - throw new Error( - 'Could not add PKCS#7 signer; unknown message digest algorithm: ' + - digestAlgorithm); + switch (digestAlgorithm) { + case forge.pki.oids.sha1: + case forge.pki.oids.sha256: + case forge.pki.oids.sha384: + case forge.pki.oids.sha512: + case forge.pki.oids.md5: + break; + default: + throw new Error( + 'Could not add PKCS#7 signer; unknown message digest algorithm: ' + + digestAlgorithm); } // if authenticatedAttributes is present, then the attributes // must contain at least PKCS #9 content-type and message-digest var authenticatedAttributes = signer.authenticatedAttributes || []; - if(authenticatedAttributes.length > 0) { + if (authenticatedAttributes.length > 0) { var contentType = false; var messageDigest = false; - for(var i = 0; i < authenticatedAttributes.length; ++i) { + for (var i = 0; i < authenticatedAttributes.length; ++i) { var attr = authenticatedAttributes[i]; - if(!contentType && attr.type === forge.pki.oids.contentType) { + if (!contentType && attr.type === forge.pki.oids.contentType) { contentType = true; - if(messageDigest) { + if (messageDigest) { break; } continue; } - if(!messageDigest && attr.type === forge.pki.oids.messageDigest) { + if (!messageDigest && attr.type === forge.pki.oids.messageDigest) { messageDigest = true; - if(contentType) { + if (contentType) { break; } continue; } } - if(!contentType || !messageDigest) { + if (!contentType || !messageDigest) { + throw new Error('Invalid signer.authenticatedAttributes. If ' + + 'signer.authenticatedAttributes is specified, then it must ' + + 'contain at least two attributes, PKCS #9 content-type and ' + + 'PKCS #9 message-digest.'); + } + } + + msg.signers.push({ + key: key, + version: 1, + issuer: issuer, + serialNumber: serialNumber, + digestAlgorithm: digestAlgorithm, + signatureAlgorithm: forge.pki.oids.rsaEncryption, + signature: null, + authenticatedAttributes: authenticatedAttributes, + unauthenticatedAttributes: [] + }); + }, + + addSignerTemplate(signer) { // Same as addSigner but doesn't require key + var issuer = signer.issuer; + var serialNumber = signer.serialNumber; + if (signer.certificate) { + var cert = signer.certificate; + if (typeof cert === 'string') { + cert = forge.pki.certificateFromPem(cert); + } + issuer = cert.issuer.attributes; + serialNumber = cert.serialNumber; + } + var key = signer.key; + if (key && (typeof key === 'string')) { + key = forge.pki.privateKeyFromPem(key); + } + + // ensure OID known for digest algorithm + var digestAlgorithm = signer.digestAlgorithm || forge.pki.oids.sha1; + switch (digestAlgorithm) { + case forge.pki.oids.sha1: + case forge.pki.oids.sha256: + case forge.pki.oids.sha384: + case forge.pki.oids.sha512: + case forge.pki.oids.md5: + break; + default: + throw new Error( + 'Could not add PKCS#7 signer; unknown message digest algorithm: ' + + digestAlgorithm); + } + + // if authenticatedAttributes is present, then the attributes + // must contain at least PKCS #9 content-type and message-digest + var authenticatedAttributes = signer.authenticatedAttributes || []; + if (authenticatedAttributes.length > 0) { + var contentType = false; + var messageDigest = false; + for (var i = 0; i < authenticatedAttributes.length; ++i) { + var attr = authenticatedAttributes[i]; + if (!contentType && attr.type === forge.pki.oids.contentType) { + contentType = true; + if (messageDigest) { + break; + } + continue; + } + if (!messageDigest && attr.type === forge.pki.oids.messageDigest) { + messageDigest = true; + if (contentType) { + break; + } + continue; + } + } + + if (!contentType || !messageDigest) { throw new Error('Invalid signer.authenticatedAttributes. If ' + 'signer.authenticatedAttributes is specified, then it must ' + 'contain at least two attributes, PKCS #9 content-type and ' + @@ -331,10 +407,10 @@ p7.createSignedData = function() { * @param options Options to apply when signing: * [detached] boolean. If signing should be done in detached mode. Defaults to false. */ - sign: function(options) { + sign: function (options) { options = options || {}; // auto-generate content info - if(typeof msg.content !== 'object' || msg.contentInfo === null) { + if (typeof msg.content !== 'object' || msg.contentInfo === null) { // use Data ContentInfo msg.contentInfo = asn1.create( asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ @@ -344,11 +420,11 @@ p7.createSignedData = function() { ]); // add actual content, if present - if('content' in msg) { + if ('content' in msg) { var content; - if(msg.content instanceof forge.util.ByteBuffer) { + if (msg.content instanceof forge.util.ByteBuffer) { content = msg.content.bytes(); - } else if(typeof msg.content === 'string') { + } else if (typeof msg.content === 'string') { content = forge.util.encodeUtf8(msg.content); } @@ -366,7 +442,7 @@ p7.createSignedData = function() { } // no signers, return early (degenerate case for certificate container) - if(msg.signers.length === 0) { + if (msg.signers.length === 0) { return; } @@ -376,8 +452,77 @@ p7.createSignedData = function() { // generate signerInfos addSignerInfos(mds); }, + prepare: function (options) { // proceeds with the same logic as sign method but calls addSignerInfosNoKey + options = options || {}; + // auto-generate content info + if (typeof msg.content !== 'object' || msg.contentInfo === null) { + // use Data ContentInfo + msg.contentInfo = asn1.create( + asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ + // ContentType + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, + asn1.oidToDer(forge.pki.oids.data).getBytes()) + ]); + + // add actual content, if present + if ('content' in msg) { + var content; + if (msg.content instanceof forge.util.ByteBuffer) { + content = msg.content.bytes(); + } else if (typeof msg.content === 'string') { + content = forge.util.encodeUtf8(msg.content); + } - verify: function() { + if (options.detached) { + msg.detachedContent = asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, content); + } else { + msg.contentInfo.value.push( + // [0] EXPLICIT content + asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [ + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, + content) + ])); + } + } + } + + // no signers, return early (degenerate case for certificate container) + if (msg.signers.length === 0) { + return; + } + + // generate digest algorithm identifiers + var mds = addDigestAlgorithmIds(); + + // generate signerInfos + addSignerInfosNoKey(mds); + }, + + /** + * Allows signature to be added into previously made Signer template + * @param signerSerialNumber The serial number of the signer into which the signature will be inserted + * @param signature The signature to insert, should be a binary string + */ + addSignature({ signerSerialNumber, signature }) { + var signer = this.signers.find(signer => signer.serialNumber === signerSerialNumber) + signer.signature = signature + this.signerInfos = _signersToAsn1(this.signers) + return this + }, + + /** + * returns the Digest that needs to be signed, usually the result of concatenation and transformation + * of signed attributes and content + * @param signerSerialNumber The serial number of the signer whos digest-to-sign is required + * @returns {*} A ByteStringBuffer containing the binary string + */ + getDigestToBeSigned({ signerSerialNumber }) { + var signer = this.signers.find(signer => signer.serialNumber === signerSerialNumber) + var d = forge.pki.rsa.emsaPkcs1v15encode(signer.md) + return forge.util.createBuffer(d) + }, + + verify: function () { throw new Error('PKCS#7 signature verification not yet implemented.'); }, @@ -386,9 +531,9 @@ p7.createSignedData = function() { * * @param cert the certificate to add. */ - addCertificate: function(cert) { + addCertificate: function (cert) { // convert from PEM - if(typeof cert === 'string') { + if (typeof cert === 'string') { cert = forge.pki.certificateFromPem(cert); } msg.certificates.push(cert); @@ -399,7 +544,7 @@ p7.createSignedData = function() { * * @param crl the certificate revokation list to add. */ - addCertificateRevokationList: function(crl) { + addCertificateRevokationList: function (crl) { throw new Error('PKCS#7 CRL support not yet implemented.'); } }; @@ -408,14 +553,14 @@ p7.createSignedData = function() { function addDigestAlgorithmIds() { var mds = {}; - for(var i = 0; i < msg.signers.length; ++i) { + for (var i = 0; i < msg.signers.length; ++i) { var signer = msg.signers[i]; var oid = signer.digestAlgorithm; - if(!(oid in mds)) { + if (!(oid in mds)) { // content digest mds[oid] = forge.md[forge.pki.oids[oid]].create(); } - if(signer.authenticatedAttributes.length === 0) { + if (signer.authenticatedAttributes.length === 0) { // no custom attributes to digest; use content message digest signer.md = mds[oid]; } else { @@ -428,7 +573,7 @@ p7.createSignedData = function() { // add unique digest algorithm identifiers msg.digestAlgorithmIdentifiers = []; - for(var oid in mds) { + for (var oid in mds) { msg.digestAlgorithmIdentifiers.push( // AlgorithmIdentifier asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ @@ -459,7 +604,7 @@ p7.createSignedData = function() { content = content.value[0]; } - if(!content) { + if (!content) { throw new Error( 'Could not sign PKCS#7 message; there is no content to sign.'); } @@ -478,19 +623,19 @@ p7.createSignedData = function() { bytes = bytes.getBytes(); // digest content DER value bytes - for(var oid in mds) { + for (var oid in mds) { mds[oid].start().update(bytes); } // sign content var signingTime = new Date(); - for(var i = 0; i < msg.signers.length; ++i) { + for (var i = 0; i < msg.signers.length; ++i) { var signer = msg.signers[i]; - if(signer.authenticatedAttributes.length === 0) { + 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) { + 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.'); @@ -506,14 +651,14 @@ p7.createSignedData = function() { var attrsAsn1 = asn1.create( asn1.Class.UNIVERSAL, asn1.Type.SET, true, []); - for(var ai = 0; ai < signer.authenticatedAttributes.length; ++ai) { + for (var ai = 0; ai < signer.authenticatedAttributes.length; ++ai) { var attr = signer.authenticatedAttributes[ai]; - if(attr.type === forge.pki.oids.messageDigest) { + if (attr.type === forge.pki.oids.messageDigest) { // use content message digest as value attr.value = mds[signer.digestAlgorithm].digest(); - } else if(attr.type === forge.pki.oids.signingTime) { + } else if (attr.type === forge.pki.oids.signingTime) { // auto-populate signing time if not already set - if(!attr.value) { + if (!attr.value) { attr.value = signingTime; } } @@ -537,8 +682,103 @@ p7.createSignedData = function() { // add signer info msg.signerInfos = _signersToAsn1(msg.signers); } + + /** + * Proceeds with the same logic as addSignerInfo but does not calculate the signature + * @param mds + */ + function addSignerInfosNoKey(mds) { + + var content + + if (msg.detachedContent) { + // Signature has been made in detached mode. + content = msg.detachedContent + } else { + // 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] + } + + if (!content) { + throw new Error( + 'Could not sign PKCS#7 message; there is no content to sign.') + } + + // 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) + } + + // sign content + var signingTime = new Date() + for (var i = 0; i < msg.signers.length; ++i) { + var signer = msg.signers[i] + + 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 { + // process authenticated attributes + // [0] IMPLICIT + signer.authenticatedAttributesAsn1 = asn1.create( + asn1.Class.CONTEXT_SPECIFIC, 0, true, []) + + // 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, []) + + for (var ai = 0; ai < signer.authenticatedAttributes.length; ++ai) { + var attr = signer.authenticatedAttributes[ai] + if (attr.type === forge.pki.oids.messageDigest) { + // use content message digest as value + attr.value = mds[signer.digestAlgorithm].digest() + } else if (attr.type === forge.pki.oids.signingTime) { + // auto-populate signing time if not already set + if (!attr.value) { + attr.value = signingTime + } + } + + // convert to ASN.1 and push onto Attributes SET (for signing) and + // onto authenticatedAttributesAsn1 to complete SignedData ASN.1 + // TODO: optimize away duplication + attrsAsn1.value.push(_attributeToAsn1(attr)) + signer.authenticatedAttributesAsn1.value.push(_attributeToAsn1(attr)) + } + + // DER-serialize and digest SET OF attributes only + bytes = asn1.toDer(attrsAsn1).getBytes() + signer.md.start().update(bytes) + } + } + } }; + /** * Creates an empty PKCS#7 message of type EncryptedData. * diff --git a/lib/rsa.js b/lib/rsa.js index 5c73209f9..6d8b892e3 100644 --- a/lib/rsa.js +++ b/lib/rsa.js @@ -318,7 +318,7 @@ var digestInfoValidator = { * * @return the encoded message (ready for RSA encrytion) */ -var emsaPkcs1v15encode = function(md) { +var emsaPkcs1v15encode = pki.rsa.emsaPkcs1v15encode = function(md) { // get the oid for the algorithm var oid; if(md.algorithm in pki.oids) { diff --git a/lib/x509.js b/lib/x509.js index 2877810c1..814a19c9b 100644 --- a/lib/x509.js +++ b/lib/x509.js @@ -1810,6 +1810,33 @@ pki.createCertificationRequest = function() { csr.signature = key.sign(csr.md); }; + /** + * Get the digest that should be signed + * + * @param md the message digest object to use (defaults to forge.md.sha1). + */ + csr.getDigestToBeSigned = function(md) { + // TODO: get signature OID from private key + csr.md = md || forge.md.sha1.create(); + var algorithmOid = oids[csr.md.algorithm + 'WithRSAEncryption']; + if(!algorithmOid) { + var error = new Error('Could not compute certification request digest. ' + + 'Unknown message digest algorithm OID.'); + error.algorithm = csr.md.algorithm; + throw error; + } + csr.signatureOid = csr.siginfo.algorithmOid = algorithmOid; + + // get CertificationRequestInfo, convert to DER + csr.certificationRequestInfo = pki.getCertificationRequestInfo(csr); + var bytes = asn1.toDer(csr.certificationRequestInfo); + + // digest and sign + csr.md.update(bytes.getBytes()); + var d = forge.rsa.emsaPkcs1v15encode(csr.md, csr.publicKey.n.bitLength()); + return d + }; + /** * Attempts verify the signature on the passed certification request using * its public key.