From 2427a93eae047e995bf957bcc29e0ef0cb5ea245 Mon Sep 17 00:00:00 2001 From: Frederik Libert Date: Thu, 14 Dec 2023 11:23:13 +0100 Subject: [PATCH 1/2] pkcs7 PublicKey + RSASSA-PSS support Supports defining following elements in pkcs signing and encryption: * SubjectKeyIdentifier as SignerIdentifier (alternative choice for IssuerAndSerialNumber) * SubjectKeyIdentifier as RecipientIdentifier (alternative choice for IssuerAndSerialNumber) * support for RSASSA-PSS as scheme (defaults to RSASSA-PKCS1-V1_5, which was fixed value) --- lib/pkcs7.js | 119 +++++++++++++++++++++++++++++++++++++++++---------- lib/pki.js | 23 ++++++++++ 2 files changed, 119 insertions(+), 23 deletions(-) diff --git a/lib/pkcs7.js b/lib/pkcs7.js index 3a5d845c5..551e6a815 100644 --- a/lib/pkcs7.js +++ b/lib/pkcs7.js @@ -247,6 +247,8 @@ p7.createSignedData = function() { * to also sign along with the content. */ addSigner: function(signer) { + var version = (signer.subjectKeyIdentifier) ? 3 : 1; + var subjectKeyIdentifier = signer.subjectKeyIdentifier; var issuer = signer.issuer; var serialNumber = signer.serialNumber; if(signer.certificate) { @@ -281,6 +283,8 @@ p7.createSignedData = function() { digestAlgorithm); } + let signatureAlgorithm = (signer.signatureAlgorithm) ? signer.signatureAlgorithm : forge.pki.oids.rsaEncryption; + // if authenticatedAttributes is present, then the attributes // must contain at least PKCS #9 content-type and message-digest var authenticatedAttributes = signer.authenticatedAttributes || []; @@ -315,11 +319,12 @@ p7.createSignedData = function() { msg.signers.push({ key: key, - version: 1, + version: version, issuer: issuer, serialNumber: serialNumber, + subjectKeyIdentifier: subjectKeyIdentifier, digestAlgorithm: digestAlgorithm, - signatureAlgorithm: forge.pki.oids.rsaEncryption, + signatureAlgorithm: signatureAlgorithm, signature: null, authenticatedAttributes: authenticatedAttributes, unauthenticatedAttributes: [] @@ -374,7 +379,7 @@ p7.createSignedData = function() { var mds = addDigestAlgorithmIds(); // generate signerInfos - addSignerInfos(mds); + addSignerInfos(mds, options); }, verify: function() { @@ -443,7 +448,7 @@ p7.createSignedData = function() { return mds; } - function addSignerInfos(mds) { + function addSignerInfos(mds, options) { var content; if (msg.detachedContent) { @@ -531,7 +536,7 @@ p7.createSignedData = function() { } // sign digest - signer.signature = signer.key.sign(signer.md, 'RSASSA-PKCS1-V1_5'); + signer.signature = signer.key.sign(signer.md, (options.scheme) ? options.scheme : 'RSASSA-PKCS1-V1_5'); } // add signer info @@ -711,6 +716,27 @@ p7.createEnvelopedData = function() { }); }, + /** + * Add (another) entity to list of recipients. + * + * @param publicKey + * @param subjectKeyIdentifier + */ + addRecipientPublicKey: function(publicKey, subjectKeyIdentifier) { + let key = (typeof publicKey === 'string') ? forge.pki.publicKeyFromPem(publicKey) : publicKey; + msg.recipients.push({ + version: 2, + subjectKeyIdentifier: subjectKeyIdentifier, + encryptedContent: { + // We simply assume rsaEncryption here, since forge.pki only + // supports RSA so far. If the PKI module supports other + // ciphers one day, we need to modify this one as well. + algorithm: forge.pki.oids.rsaEncryption, + key: key + } + }); + }, + /** * Encrypt enveloped content. * @@ -843,6 +869,23 @@ function _recipientFromAsn1(obj) { }; } +function _createAsn1RecipientIdentifier(obj) { + if (obj.version === 2) { + // subjectKeyIdentifier + return asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, + obj.subjectKeyIdentifier); + } else { + // issuerAndSerialNumber + return asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ + // name + forge.pki.distinguishedNameToAsn1({attributes: obj.issuer}), + // serial + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.INTEGER, false, + forge.util.hexToBytes(obj.serialNumber)) + ]); + } +} + /** * Converts a single recipient object to an ASN.1 object. * @@ -855,14 +898,7 @@ function _recipientToAsn1(obj) { // Version asn1.create(asn1.Class.UNIVERSAL, asn1.Type.INTEGER, false, asn1.integerToDer(obj.version).getBytes()), - // IssuerAndSerialNumber - asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ - // Name - forge.pki.distinguishedNameToAsn1({attributes: obj.issuer}), - // Serial - asn1.create(asn1.Class.UNIVERSAL, asn1.Type.INTEGER, false, - forge.util.hexToBytes(obj.serialNumber)) - ]), + _createAsn1RecipientIdentifier(obj), // KeyEncryptionAlgorithmIdentifier asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ // Algorithm @@ -943,6 +979,51 @@ function _signerFromAsn1(obj) { return rval; } +function _createAsn1SignerIdentifier(obj) { + if (obj.version === 3) { + // subjectKeyIdentifier + return asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, + obj.subjectKeyIdentifier); + } else { + // issuerAndSerialNumber + return asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ + // name + forge.pki.distinguishedNameToAsn1({attributes: obj.issuer}), + // serial + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.INTEGER, false, + forge.util.hexToBytes(obj.serialNumber)) + ]); + } +} + +function _createDigestEncryptionAlgorithmParameters(obj) { + if (obj.signatureAlgorithm === forge.pki.oids['RSASSA-PSS']) { + // Parameters: hashAlgorithm, maskGenAlgorithm (MGF1 + hash algorithm), saltLength (expects length of hash as recommended) + return asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ + asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [ + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, asn1.oidToDer(obj.digestAlgorithm).getBytes()), + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.NULL, false, ''), + ]) + ]), + asn1.create(asn1.Class.CONTEXT_SPECIFIC, 1, true, [ + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, asn1.oidToDer(forge.oids.mgf1).getBytes()), + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, asn1.oidToDer(obj.digestAlgorithm).getBytes()), + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.NULL, false, '') + ]) + ]), + ]), + asn1.create(asn1.Class.CONTEXT_SPECIFIC, 2, true, [ + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.INTEGER, false, asn1.integerToDer(obj.signature.length / 8).getBytes()) + ]), + ]); + } + // parameters (null) + return asn1.create(asn1.Class.UNIVERSAL, asn1.Type.NULL, false, ''); +} + /** * Converts a single signerInfo object to an ASN.1 object. * @@ -956,14 +1037,7 @@ function _signerToAsn1(obj) { // version asn1.create(asn1.Class.UNIVERSAL, asn1.Type.INTEGER, false, asn1.integerToDer(obj.version).getBytes()), - // issuerAndSerialNumber - asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ - // name - forge.pki.distinguishedNameToAsn1({attributes: obj.issuer}), - // serial - asn1.create(asn1.Class.UNIVERSAL, asn1.Type.INTEGER, false, - forge.util.hexToBytes(obj.serialNumber)) - ]), + _createAsn1SignerIdentifier(obj), // digestAlgorithm asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ // algorithm @@ -985,8 +1059,7 @@ function _signerToAsn1(obj) { // algorithm asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, asn1.oidToDer(obj.signatureAlgorithm).getBytes()), - // parameters (null) - asn1.create(asn1.Class.UNIVERSAL, asn1.Type.NULL, false, '') + _createDigestEncryptionAlgorithmParameters(obj) ])); // encryptedDigest diff --git a/lib/pki.js b/lib/pki.js index ee82ff1ce..bcc49f660 100644 --- a/lib/pki.js +++ b/lib/pki.js @@ -100,3 +100,26 @@ pki.privateKeyInfoToPem = function(pki, maxline) { }; return forge.pem.encode(msg, {maxline: maxline}); }; + +/** + * Converts an RSA private key from PEM format. + * + * @param pem the PEM-formatted private key. + * + * @return the private key. + */ +pki.publicKeyFromPem = function(pem) { + var msg = forge.pem.decode(pem)[0]; + + if(msg.type !== 'PUBLIC KEY' && msg.type !== 'RSA PUBLIC KEY') { + var error = new Error('Could not convert public key from PEM; PEM ' + + 'header type is not "PUBLIC KEY" or "RSA PUBLIC KEY".'); + error.headerType = msg.type; + throw error; + } + + // convert DER to ASN.1 object + var obj = asn1.fromDer(msg.body); + + return pki.publicKeyFromAsn1(obj); +}; From 0551e024fae1afe34a5c14b5917328e369fc946b Mon Sep 17 00:00:00 2001 From: Frederik Libert Date: Fri, 22 Dec 2023 11:05:01 +0100 Subject: [PATCH 2/2] Add support for pkcs encryption with secret key for recipient #1067 --- lib/pkcs7.js | 106 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 78 insertions(+), 28 deletions(-) diff --git a/lib/pkcs7.js b/lib/pkcs7.js index 551e6a815..847547210 100644 --- a/lib/pkcs7.js +++ b/lib/pkcs7.js @@ -230,8 +230,6 @@ p7.createSignedData = function() { * }] * }); * - * TODO: Support [subjectKeyIdentifier] as signer's ID. - * * @param signer the signer information: * key the signer's private key. * [certificate] a certificate containing the public key @@ -241,6 +239,7 @@ p7.createSignedData = function() { * [issuer] the issuer attributes (eg: cert.issuer.attributes). * [serialNumber] the signer's certificate's serial number in * hexadecimal (eg: cert.serialNumber). + * [subjectKeyIdentifier] use this as alternative for certificate or issuer and serialNumber in case you use a keypair without a certificate * [digestAlgorithm] the message digest OID, as a string, to use * (eg: forge.pki.oids.sha1). * [authenticatedAttributes] an optional array of attributes @@ -271,16 +270,16 @@ p7.createSignedData = function() { // 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); + 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); } let signatureAlgorithm = (signer.signatureAlgorithm) ? signer.signatureAlgorithm : forge.pki.oids.rsaEncryption; @@ -720,7 +719,7 @@ p7.createEnvelopedData = function() { * Add (another) entity to list of recipients. * * @param publicKey - * @param subjectKeyIdentifier + * @param subjectKeyIdentifier identifier for the public key */ addRecipientPublicKey: function(publicKey, subjectKeyIdentifier) { let key = (typeof publicKey === 'string') ? forge.pki.publicKeyFromPem(publicKey) : publicKey; @@ -737,6 +736,26 @@ p7.createEnvelopedData = function() { }); }, + /** + * Add (another) entity to list of recipients. + * + * @param secretKey symmetric key to use for this recipient (kek) + * @param kekIdentifier identifier for this key (kek) + * @param keyEncryptionAlgorithm algorithm to use to protect the content encryption key + * @param keyEncryptionFunction function that implements the key encryption algorithm + */ + addRecipientSecretKey: function(secretKey, kekIdentifier, keyEncryptionAlgorithm, keyEncryptionFunction) { + msg.recipients.push({ + version: 4, + kekIdentifier: kekIdentifier, + encryptedContent: { + algorithm: keyEncryptionAlgorithm, + key: secretKey, + keyEncryptionFunction: keyEncryptionFunction + } + }); + }, + /** * Encrypt enveloped content. * @@ -751,7 +770,7 @@ p7.createEnvelopedData = function() { * @param [cipher] The OID of the symmetric cipher to use. */ encrypt: function(key, cipher) { - // Part 1: Symmetric encryption + // Part 1: Content encryption with symmetric key if(msg.encryptedContent.content === undefined) { cipher = cipher || msg.encryptedContent.algorithm; key = key || msg.encryptedContent.key; @@ -813,7 +832,7 @@ p7.createEnvelopedData = function() { msg.encryptedContent.content = ciph.output; } - // Part 2: asymmetric encryption for each recipient + // Part 2: content encryption key encryption for each recipient for(var i = 0; i < msg.recipients.length; ++i) { var recipient = msg.recipients[i]; @@ -828,10 +847,13 @@ p7.createEnvelopedData = function() { recipient.encryptedContent.key.encrypt( msg.encryptedContent.key.data); break; - default: - throw new Error('Unsupported asymmetric cipher, OID ' + - recipient.encryptedContent.algorithm); + if (recipient.encryptedContent.keyEncryptionFunction) { + // caller specified function to support alternative encryption so call it. + recipient.encryptedContent.content = recipient.encryptedContent.keyEncryptionFunction(msg.encryptedContent.key, recipient.encryptedContent.key); + } else { + throw new Error('Unsupported key encryption algorithm: OID ' + recipient.encryptedContent.algorithm); + } } } } @@ -869,11 +891,25 @@ function _recipientFromAsn1(obj) { }; } +/** + * Supports SubjectKeyIdentifier, KEKIdentifier, IssuerAndSerialNumber + **/ function _createAsn1RecipientIdentifier(obj) { if (obj.version === 2) { // subjectKeyIdentifier return asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, obj.subjectKeyIdentifier); + } else if (obj.version === 4) { + // KEKIdentifier + return asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ + // keyIdentifier + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, + obj.kekIdentifier), + // date + // asn1.create(asn1.Class.UNIVERSAL, asn1.Type.NULL, false, ''), + // other + // asn1.create(asn1.Class.UNIVERSAL, asn1.Type.NULL, false, '') + ]); } else { // issuerAndSerialNumber return asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ @@ -886,14 +922,7 @@ function _createAsn1RecipientIdentifier(obj) { } } -/** - * Converts a single recipient object to an ASN.1 object. - * - * @param obj the recipient object. - * - * @return the ASN.1 RecipientInfo. - */ -function _recipientToAsn1(obj) { +function _createAsn1RecipientInfo(obj) { return asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ // Version asn1.create(asn1.Class.UNIVERSAL, asn1.Type.INTEGER, false, @@ -905,7 +934,7 @@ function _recipientToAsn1(obj) { asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, asn1.oidToDer(obj.encryptedContent.algorithm).getBytes()), // Parameter, force NULL, only RSA supported for now. - asn1.create(asn1.Class.UNIVERSAL, asn1.Type.NULL, false, '') + // asn1.create(asn1.Class.UNIVERSAL, asn1.Type.NULL, false, '') ]), // EncryptedKey asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, @@ -913,6 +942,27 @@ function _recipientToAsn1(obj) { ]); } +/** + * Converts a single recipient object to an ASN.1 object. + * Supports KEKRecipientInfo, KetTransRecipientInfo. + * + * @param obj the recipient object. + * + * @return the ASN.1 RecipientInfo. + */ +function _recipientToAsn1(obj) { + if (obj.version === 4) { + // kekri [2] KEKRecipientInfo + return asn1.create(asn1.Class.CONTEXT_SPECIFIC, 2, true, [ + _createAsn1RecipientInfo(obj) + ]); + } else { + // ktri KeyTransRecipientInfo + return _createAsn1RecipientInfo(obj); + } + +} + /** * Map a set of RecipientInfo ASN.1 objects to recipient objects. * @@ -1059,7 +1109,7 @@ function _signerToAsn1(obj) { // algorithm asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, asn1.oidToDer(obj.signatureAlgorithm).getBytes()), - _createDigestEncryptionAlgorithmParameters(obj) + _createDigestEncryptionAlgorithmParameters(obj) ])); // encryptedDigest