Skip to content
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

pkcs7 PublicKey + RSASSA-PSS support + Secret Key Encryption for recipient #1063

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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
219 changes: 171 additions & 48 deletions lib/pkcs7.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -241,12 +239,15 @@ 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
* 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) {
Expand All @@ -269,18 +270,20 @@ 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;

// if authenticatedAttributes is present, then the attributes
// must contain at least PKCS #9 content-type and message-digest
var authenticatedAttributes = signer.authenticatedAttributes || [];
Expand Down Expand Up @@ -315,11 +318,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: []
Expand Down Expand Up @@ -374,7 +378,7 @@ p7.createSignedData = function() {
var mds = addDigestAlgorithmIds();

// generate signerInfos
addSignerInfos(mds);
addSignerInfos(mds, options);
},

verify: function() {
Expand Down Expand Up @@ -443,7 +447,7 @@ p7.createSignedData = function() {
return mds;
}

function addSignerInfos(mds) {
function addSignerInfos(mds, options) {
var content;

if (msg.detachedContent) {
Expand Down Expand Up @@ -531,7 +535,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
Expand Down Expand Up @@ -711,6 +715,47 @@ p7.createEnvelopedData = function() {
});
},

/**
* Add (another) entity to list of recipients.
*
* @param publicKey
* @param subjectKeyIdentifier identifier for the public key
*/
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
}
});
},

/**
* 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.
*
Expand All @@ -725,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;
Expand Down Expand Up @@ -787,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];

Expand All @@ -802,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);
}
}
}
}
Expand Down Expand Up @@ -844,39 +892,77 @@ function _recipientFromAsn1(obj) {
}

/**
* Converts a single recipient object to an ASN.1 object.
*
* @param obj the recipient object.
*
* @return the ASN.1 RecipientInfo.
*/
function _recipientToAsn1(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, [
// name
forge.pki.distinguishedNameToAsn1({attributes: obj.issuer}),
// serial
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.INTEGER, false,
forge.util.hexToBytes(obj.serialNumber))
]);
}
}

function _createAsn1RecipientInfo(obj) {
return asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
// 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
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,
obj.encryptedContent.content)
]);
}

/**
* 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.
*
Expand Down Expand Up @@ -943,6 +1029,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.
*
Expand All @@ -956,14 +1087,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
Expand All @@ -985,8 +1109,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
Expand Down
23 changes: 23 additions & 0 deletions lib/pki.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};