From 1b22dc86caf3d298134fbdfd27fc38d9bb1ae62e Mon Sep 17 00:00:00 2001 From: roham Date: Wed, 31 May 2023 00:29:21 +0330 Subject: [PATCH 1/4] add early experimental changes which allow adding signer without specifying key, used to generate the asn1 and der formats to enable helped cms creation on PKCS7-disabled platforms (e.g. iOS). The actual digest that needs to be signed will be generated and extractable, and allows simple privateEncrypted messages (signatures) to be added to the structure and the final PEM be generated. --- lib/pkcs7.js | 347 +++++++++++++++++++++++++++++++++++++++++++-------- lib/rsa.js | 2 +- 2 files changed, 294 insertions(+), 55 deletions(-) diff --git a/lib/pkcs7.js b/lib/pkcs7.js index 3a5d845c5..faaea4afd 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,76 @@ 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); + }, + + /** + * 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 +530,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 +543,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 +552,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 +572,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 +603,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 +622,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 +650,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 +681,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) { From effaa0bf8ac047b09255f1a2c13e0b42c9dd24f9 Mon Sep 17 00:00:00 2001 From: Roham Hayedi <58424056+Roham-H@users.noreply.github.com> Date: Fri, 16 Jun 2023 12:47:44 +0330 Subject: [PATCH 2/4] Update README.md Add sample usage for added methods --- README.md | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) 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); ``` From edc11c49744f45428d9a3b7ea2870490d4d64957 Mon Sep 17 00:00:00 2001 From: roham Date: Mon, 26 Jun 2023 10:58:16 +0330 Subject: [PATCH 3/4] return this for object chaining --- lib/pkcs7.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/pkcs7.js b/lib/pkcs7.js index faaea4afd..6bc83b974 100644 --- a/lib/pkcs7.js +++ b/lib/pkcs7.js @@ -506,7 +506,8 @@ p7.createSignedData = function() { addSignature({ signerSerialNumber, signature }) { var signer = this.signers.find(signer => signer.serialNumber === signerSerialNumber) signer.signature = signature - this.signerInfos = _signersToAsn1(this.signers); + this.signerInfos = _signersToAsn1(this.signers) + return this }, /** From 6b2fd15ac907ec0589299f82e5f7b67a7572ff8f Mon Sep 17 00:00:00 2001 From: roham Date: Wed, 11 Oct 2023 23:26:31 +0330 Subject: [PATCH 4/4] Add getDigestToBeSigned for csr --- lib/x509.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) 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.