From d3e75de23d8dce61d44e6f155303779007810901 Mon Sep 17 00:00:00 2001 From: larabr <7375870+larabr@users.noreply.github.com> Date: Tue, 29 Oct 2024 18:02:56 +0100 Subject: [PATCH 1/2] `openpgp.encrypt`: use `encryptionKeys` to determine preferred hash algo when signing In `openpgp.sign`, the signing key preferences are considered instead, since no "recipient keys" are available. The hash algo selection logic has been reworked as follows: if `config.preferredHashAlgo` appears in the prefs of all recipients, we pick it; otherwise, we use the strongest supported algo (note: SHA256 is always implicitly supported by all keys), as long as it is compatible with the signing key (e.g. ECC keys require minimum digest sizes). Previously, only the preferences of the signing key were used to determine the hash algo to use, but this is in contrast to the RFC: https://www.rfc-editor.org/rfc/rfc9580.html#section-5.2.3.16-2 . Also, an algo stronger than `config.preferredHashAlgo` would be used, if the signing key declared it as first preference. With this change, `config.preferredHashAlgo` is picked even if it's weaker than the preferences of the recipient keys. --- src/cleartext.js | 10 +- src/crypto/public_key/elliptic/eddsa.js | 6 + .../public_key/elliptic/eddsa_legacy.js | 5 + src/key/factory.js | 6 +- src/key/helper.js | 106 ++++++-- src/key/private_key.js | 2 +- src/key/subkey.js | 2 +- src/key/user.js | 4 +- src/message.js | 28 +- src/openpgp.js | 6 +- test/general/key.js | 247 ++++++++++-------- test/general/openpgp.js | 14 +- 12 files changed, 278 insertions(+), 158 deletions(-) diff --git a/src/cleartext.js b/src/cleartext.js index a24f4a3e..61136c28 100644 --- a/src/cleartext.js +++ b/src/cleartext.js @@ -59,20 +59,22 @@ export class CleartextMessage { /** * Sign the cleartext message - * @param {Array} privateKeys - private keys with decrypted secret key data for signing + * @param {Array} signingKeys - private keys with decrypted secret key data for signing + * @param {Array} recipientKeys - recipient keys to get the signing preferences from * @param {Signature} [signature] - Any existing detached signature * @param {Array} [signingKeyIDs] - Array of key IDs to use for signing. Each signingKeyIDs[i] corresponds to privateKeys[i] * @param {Date} [date] - The creation time of the signature that should be created - * @param {Array} [userIDs] - User IDs to sign with, e.g. [{ name:'Steve Sender', email:'steve@openpgp.org' }] + * @param {Array} [signingKeyIDs] - User IDs to sign with, e.g. [{ name:'Steve Sender', email:'steve@openpgp.org' }] + * @param {Array} [recipientUserIDs] - User IDs associated with `recipientKeys` to get the signing preferences from * @param {Array} [notations] - Notation Data to add to the signatures, e.g. [{ name: 'test@example.org', value: new TextEncoder().encode('test'), humanReadable: true, critical: false }] * @param {Object} [config] - Full configuration, defaults to openpgp.config * @returns {Promise} New cleartext message with signed content. * @async */ - async sign(privateKeys, signature = null, signingKeyIDs = [], date = new Date(), userIDs = [], notations = [], config = defaultConfig) { + async sign(signingKeys, recipientKeys = [], signature = null, signingKeyIDs = [], date = new Date(), signingUserIDs = [], recipientUserIDs = [], notations = [], config = defaultConfig) { const literalDataPacket = new LiteralDataPacket(); literalDataPacket.setText(this.text); - const newSignature = new Signature(await createSignaturePackets(literalDataPacket, privateKeys, signature, signingKeyIDs, date, userIDs, notations, true, config)); + const newSignature = new Signature(await createSignaturePackets(literalDataPacket, signingKeys, recipientKeys, signature, signingKeyIDs, date, signingUserIDs, recipientUserIDs, notations, true, config)); return new CleartextMessage(this.text, newSignature); } diff --git a/src/crypto/public_key/elliptic/eddsa.js b/src/crypto/public_key/elliptic/eddsa.js index d22cd8bd..7b41eb23 100644 --- a/src/crypto/public_key/elliptic/eddsa.js +++ b/src/crypto/public_key/elliptic/eddsa.js @@ -82,6 +82,9 @@ export async function generate(algo) { */ export async function sign(algo, hashAlgo, message, publicKey, privateKey, hashed) { if (hash.getHashByteLength(hashAlgo) < hash.getHashByteLength(getPreferredHashAlgo(algo))) { + // Enforce digest sizes: + // - Ed25519: https://www.rfc-editor.org/rfc/rfc9580.html#section-5.2.3.4-4 + // - Ed448: https://www.rfc-editor.org/rfc/rfc9580.html#section-5.2.3.5-4 throw new Error('Hash algorithm too weak for EdDSA.'); } switch (algo) { @@ -129,6 +132,9 @@ export async function sign(algo, hashAlgo, message, publicKey, privateKey, hashe */ export async function verify(algo, hashAlgo, { RS }, m, publicKey, hashed) { if (hash.getHashByteLength(hashAlgo) < hash.getHashByteLength(getPreferredHashAlgo(algo))) { + // Enforce digest sizes: + // - Ed25519: https://www.rfc-editor.org/rfc/rfc9580.html#section-5.2.3.4-4 + // - Ed448: https://www.rfc-editor.org/rfc/rfc9580.html#section-5.2.3.5-4 throw new Error('Hash algorithm too weak for EdDSA.'); } switch (algo) { diff --git a/src/crypto/public_key/elliptic/eddsa_legacy.js b/src/crypto/public_key/elliptic/eddsa_legacy.js index 28c72d4e..a88e67b4 100644 --- a/src/crypto/public_key/elliptic/eddsa_legacy.js +++ b/src/crypto/public_key/elliptic/eddsa_legacy.js @@ -46,7 +46,9 @@ export async function sign(oid, hashAlgo, message, publicKey, privateKey, hashed const curve = new CurveWithOID(oid); checkPublicPointEnconding(curve, publicKey); if (hash.getHashByteLength(hashAlgo) < hash.getHashByteLength(enums.hash.sha256)) { + // Enforce digest sizes, since the constraint was already present in RFC4880bis: // see https://tools.ietf.org/id/draft-ietf-openpgp-rfc4880bis-10.html#section-15-7.2 + // and https://www.rfc-editor.org/rfc/rfc9580.html#section-5.2.3.3-3 throw new Error('Hash algorithm too weak for EdDSA.'); } const { RS: signature } = await eddsaSign(enums.publicKey.ed25519, hashAlgo, message, publicKey.subarray(1), privateKey, hashed); @@ -73,6 +75,9 @@ export async function verify(oid, hashAlgo, { r, s }, m, publicKey, hashed) { const curve = new CurveWithOID(oid); checkPublicPointEnconding(curve, publicKey); if (hash.getHashByteLength(hashAlgo) < hash.getHashByteLength(enums.hash.sha256)) { + // Enforce digest sizes, since the constraint was already present in RFC4880bis: + // see https://tools.ietf.org/id/draft-ietf-openpgp-rfc4880bis-10.html#section-15-7.2 + // and https://www.rfc-editor.org/rfc/rfc9580.html#section-5.2.3.3-3 throw new Error('Hash algorithm too weak for EdDSA.'); } const RS = util.concatUint8Array([r, s]); diff --git a/src/key/factory.js b/src/key/factory.js index e66eaa10..68cdfb95 100644 --- a/src/key/factory.js +++ b/src/key/factory.js @@ -246,7 +246,7 @@ async function wrapKeyObject(secretKeyPacket, secretSubkeyPackets, options, conf const signatureProperties = getKeySignatureProperties(); signatureProperties.signatureType = enums.signature.key; - const signaturePacket = await helper.createSignaturePacket(dataToSign, null, secretKeyPacket, signatureProperties, options.date, undefined, undefined, undefined, config); + const signaturePacket = await helper.createSignaturePacket(dataToSign, [], secretKeyPacket, signatureProperties, options.date, undefined, undefined, undefined, config); packetlist.push(signaturePacket); } @@ -262,7 +262,7 @@ async function wrapKeyObject(secretKeyPacket, secretSubkeyPackets, options, conf signatureProperties.isPrimaryUserID = true; } - const signaturePacket = await helper.createSignaturePacket(dataToSign, null, secretKeyPacket, signatureProperties, options.date, undefined, undefined, undefined, config); + const signaturePacket = await helper.createSignaturePacket(dataToSign, [], secretKeyPacket, signatureProperties, options.date, undefined, undefined, undefined, config); return { userIDPacket, signaturePacket }; })).then(list => { @@ -286,7 +286,7 @@ async function wrapKeyObject(secretKeyPacket, secretSubkeyPackets, options, conf // Add revocation signature packet for creating a revocation certificate. // This packet should be removed before returning the key. const dataToSign = { key: secretKeyPacket }; - packetlist.push(await helper.createSignaturePacket(dataToSign, null, secretKeyPacket, { + packetlist.push(await helper.createSignaturePacket(dataToSign, [], secretKeyPacket, { signatureType: enums.signature.keyRevocation, reasonForRevocationFlag: enums.reasonForRevocation.noReason, reasonForRevocationString: '' diff --git a/src/key/helper.js b/src/key/helper.js index e803acab..cd1643d5 100644 --- a/src/key/helper.js +++ b/src/key/helper.js @@ -90,7 +90,7 @@ export async function createBindingSignature(subkey, primaryKey, options, config const signatureProperties = { signatureType: enums.signature.subkeyBinding }; if (options.sign) { signatureProperties.keyFlags = [enums.keyFlags.signData]; - signatureProperties.embeddedSignature = await createSignaturePacket(dataToSign, null, subkey, { + signatureProperties.embeddedSignature = await createSignaturePacket(dataToSign, [], subkey, { signatureType: enums.signature.keyBinding }, options.date, undefined, undefined, undefined, config); } else { @@ -100,41 +100,95 @@ export async function createBindingSignature(subkey, primaryKey, options, config signatureProperties.keyExpirationTime = options.keyExpirationTime; signatureProperties.keyNeverExpires = false; } - const subkeySignaturePacket = await createSignaturePacket(dataToSign, null, primaryKey, signatureProperties, options.date, undefined, undefined, undefined, config); + const subkeySignaturePacket = await createSignaturePacket(dataToSign, [], primaryKey, signatureProperties, options.date, undefined, undefined, undefined, config); return subkeySignaturePacket; } /** - * Returns the preferred signature hash algorithm of a key - * @param {Key} [key] - The key to get preferences from - * @param {SecretKeyPacket|SecretSubkeyPacket} keyPacket - key packet used for signing + * Returns the preferred signature hash algorithm for a set of keys. + * @param {Array} [targetKeys] - The keys to get preferences from + * @param {SecretKeyPacket|SecretSubkeyPacket} signingKeyPacket - key packet used for signing * @param {Date} [date] - Use the given date for verification instead of the current time - * @param {Object} [userID] - User ID + * @param {Object} [targetUserID] - User IDs corresponding to `targetKeys` to get preferences from * @param {Object} config - full configuration * @returns {Promise} * @async */ -export async function getPreferredHashAlgo(key, keyPacket, date = new Date(), userID = {}, config) { - let hashAlgo = config.preferredHashAlgorithm; - let prefAlgo = hashAlgo; - if (key) { - const selfCertification = await key.getPrimarySelfSignature(date, userID, config); - if (selfCertification.preferredHashAlgorithms) { - [prefAlgo] = selfCertification.preferredHashAlgorithms; - hashAlgo = crypto.hash.getHashByteLength(hashAlgo) <= crypto.hash.getHashByteLength(prefAlgo) ? - prefAlgo : hashAlgo; +export async function getPreferredHashAlgo(targetKeys, signingKeyPacket, date = new Date(), targetUserIDs = [], config) { + /** + * If `preferredSenderAlgo` appears in the prefs of all recipients, we pick it; otherwise, we use the + * strongest supported algo (`defaultAlgo` is always implicitly supported by all keys). + * if no keys are available, `preferredSenderAlgo` is returned. + * For ECC signing key, the curve preferred hash is taken into account as well (see logic below). + */ + const defaultAlgo = enums.hash.sha256; // MUST implement + const preferredSenderAlgo = config.preferredHashAlgorithm; + + const supportedAlgosPerTarget = await Promise.all(targetKeys.map(async (key, i) => { + const selfCertification = await key.getPrimarySelfSignature(date, targetUserIDs[i], config); + const targetPrefs = selfCertification.preferredHashAlgorithms; + return targetPrefs; + })); + const supportedAlgosMap = new Map(); // use Map over object to preserve numeric keys + for (const supportedAlgos of supportedAlgosPerTarget) { + for (const hashAlgo of supportedAlgos) { + try { + // ensure that `hashAlgo` is recognized/implemented by us, otherwise e.g. `getHashByteLength` will throw later on + const supportedAlgo = enums.write(enums.hash, hashAlgo); + supportedAlgosMap.set( + supportedAlgo, + supportedAlgosMap.has(supportedAlgo) ? supportedAlgosMap.get(supportedAlgo) + 1 : 1 + ); + } catch {} } } - switch (keyPacket.algorithm) { - case enums.publicKey.ecdsa: - case enums.publicKey.eddsaLegacy: - case enums.publicKey.ed25519: - case enums.publicKey.ed448: - prefAlgo = crypto.getPreferredCurveHashAlgo(keyPacket.algorithm, keyPacket.publicParams.oid); + const isSupportedHashAlgo = hashAlgo => targetKeys.length === 0 || supportedAlgosMap.get(hashAlgo) === targetKeys.length || hashAlgo === defaultAlgo; + const getStrongestSupportedHashAlgo = () => { + if (supportedAlgosMap.size === 0) { + return defaultAlgo; + } + const sortedHashAlgos = Array.from(supportedAlgosMap.keys()) + .filter(hashAlgo => isSupportedHashAlgo(hashAlgo)) + .sort((algoA, algoB) => crypto.hash.getHashByteLength(algoA) - crypto.hash.getHashByteLength(algoB)); + const strongestHashAlgo = sortedHashAlgos[0]; + // defaultAlgo is always implicilty supported, and might be stronger than the rest + return crypto.hash.getHashByteLength(strongestHashAlgo) >= crypto.hash.getHashByteLength(defaultAlgo) ? strongestHashAlgo : defaultAlgo; + }; + + const eccAlgos = new Set([ + enums.publicKey.ecdsa, + enums.publicKey.eddsaLegacy, + enums.publicKey.ed25519, + enums.publicKey.ed448 + ]); + + if (eccAlgos.has(signingKeyPacket.algorithm)) { + // For ECC, the returned hash algo MUST be at least as strong as `preferredCurveHashAlgo`, see: + // - ECDSA: https://www.rfc-editor.org/rfc/rfc9580.html#section-5.2.3.2-5 + // - EdDSALegacy: https://www.rfc-editor.org/rfc/rfc9580.html#section-5.2.3.3-3 + // - Ed25519: https://www.rfc-editor.org/rfc/rfc9580.html#section-5.2.3.4-4 + // - Ed448: https://www.rfc-editor.org/rfc/rfc9580.html#section-5.2.3.5-4 + // Hence, we return the `preferredHashAlgo` as long as it's supported and strong enough; + // Otherwise, we look at the strongest supported algo, and ultimately fallback to the curve + // preferred algo, even if not supported by all targets. + const preferredCurveAlgo = crypto.getPreferredCurveHashAlgo(signingKeyPacket.algorithm, signingKeyPacket.publicParams.oid); + + const preferredSenderAlgoIsSupported = isSupportedHashAlgo(preferredSenderAlgo); + const preferredSenderAlgoStrongerThanCurveAlgo = crypto.hash.getHashByteLength(preferredSenderAlgo) >= crypto.hash.getHashByteLength(preferredCurveAlgo); + + if (preferredSenderAlgoIsSupported && preferredSenderAlgoStrongerThanCurveAlgo) { + return preferredSenderAlgo; + } else { + const strongestSupportedAlgo = getStrongestSupportedHashAlgo(); + return crypto.hash.getHashByteLength(strongestSupportedAlgo) >= crypto.hash.getHashByteLength(preferredCurveAlgo) ? + strongestSupportedAlgo : + preferredCurveAlgo; + } } - return crypto.hash.getHashByteLength(hashAlgo) <= crypto.hash.getHashByteLength(prefAlgo) ? - prefAlgo : hashAlgo; + // `preferredSenderAlgo` may be weaker than the default, but we do not guard against this, + // since it was manually set by the sender. + return isSupportedHashAlgo(preferredSenderAlgo) ? preferredSenderAlgo : getStrongestSupportedHashAlgo(); } /** @@ -205,7 +259,7 @@ export async function getPreferredCipherSuite(keys = [], date = new Date(), user /** * Create signature packet * @param {Object} dataToSign - Contains packets to be signed - * @param {PrivateKey} privateKey - key to get preferences from + * @param {Array} recipientKeys - keys to get preferences from * @param {SecretKeyPacket| * SecretSubkeyPacket} signingKeyPacket secret key packet for signing * @param {Object} [signatureProperties] - Properties to write on the signature packet before signing @@ -216,7 +270,7 @@ export async function getPreferredCipherSuite(keys = [], date = new Date(), user * @param {Object} config - full configuration * @returns {Promise} Signature packet. */ -export async function createSignaturePacket(dataToSign, privateKey, signingKeyPacket, signatureProperties, date, userID, notations = [], detached = false, config) { +export async function createSignaturePacket(dataToSign, recipientKeys, signingKeyPacket, signatureProperties, date, recipientUserIDs, notations = [], detached = false, config) { if (signingKeyPacket.isDummy()) { throw new Error('Cannot sign with a gnu-dummy key.'); } @@ -226,7 +280,7 @@ export async function createSignaturePacket(dataToSign, privateKey, signingKeyPa const signaturePacket = new SignaturePacket(); Object.assign(signaturePacket, signatureProperties); signaturePacket.publicKeyAlgorithm = signingKeyPacket.algorithm; - signaturePacket.hashAlgorithm = await getPreferredHashAlgo(privateKey, signingKeyPacket, date, userID, config); + signaturePacket.hashAlgorithm = await getPreferredHashAlgo(recipientKeys, signingKeyPacket, date, recipientUserIDs, config); signaturePacket.rawNotations = [...notations]; await signaturePacket.sign(signingKeyPacket, dataToSign, date, detached, config); return signaturePacket; diff --git a/src/key/private_key.js b/src/key/private_key.js index 37cd4588..e82fa118 100644 --- a/src/key/private_key.js +++ b/src/key/private_key.js @@ -206,7 +206,7 @@ class PrivateKey extends PublicKey { } const dataToSign = { key: this.keyPacket }; const key = this.clone(); - key.revocationSignatures.push(await helper.createSignaturePacket(dataToSign, null, this.keyPacket, { + key.revocationSignatures.push(await helper.createSignaturePacket(dataToSign, [], this.keyPacket, { signatureType: enums.signature.keyRevocation, reasonForRevocationFlag: enums.write(enums.reasonForRevocation, reasonForRevocationFlag), reasonForRevocationString diff --git a/src/key/subkey.js b/src/key/subkey.js index 130155a7..49af01b5 100644 --- a/src/key/subkey.js +++ b/src/key/subkey.js @@ -185,7 +185,7 @@ class Subkey { ) { const dataToSign = { key: primaryKey, bind: this.keyPacket }; const subkey = new Subkey(this.keyPacket, this.mainKey); - subkey.revocationSignatures.push(await helper.createSignaturePacket(dataToSign, null, primaryKey, { + subkey.revocationSignatures.push(await helper.createSignaturePacket(dataToSign, [], primaryKey, { signatureType: enums.signature.subkeyRevocation, reasonForRevocationFlag: enums.write(enums.reasonForRevocation, reasonForRevocationFlag), reasonForRevocationString diff --git a/src/key/user.js b/src/key/user.js index 49159c5a..b401a742 100644 --- a/src/key/user.js +++ b/src/key/user.js @@ -72,7 +72,7 @@ class User { throw new Error("The user's own key can only be used for self-certifications"); } const signingKey = await privateKey.getSigningKey(undefined, date, undefined, config); - return createSignaturePacket(dataToSign, privateKey, signingKey.keyPacket, { + return createSignaturePacket(dataToSign, [privateKey], signingKey.keyPacket, { // Most OpenPGP implementations use generic certification (0x10) signatureType: enums.signature.certGeneric, keyFlags: [enums.keyFlags.certifyKeys | enums.keyFlags.signData] @@ -260,7 +260,7 @@ class User { key: primaryKey }; const user = new User(dataToSign.userID || dataToSign.userAttribute, this.mainKey); - user.revocationSignatures.push(await createSignaturePacket(dataToSign, null, primaryKey, { + user.revocationSignatures.push(await createSignaturePacket(dataToSign, [], primaryKey, { signatureType: enums.signature.certRevocation, reasonForRevocationFlag: enums.write(enums.reasonForRevocation, reasonForRevocationFlag), reasonForRevocationString diff --git a/src/message.js b/src/message.js index fb12e8e4..d90efb58 100644 --- a/src/message.js +++ b/src/message.js @@ -496,16 +496,18 @@ export class Message { /** * Sign the message (the literal data packet of the message) * @param {Array} signingKeys - private keys with decrypted secret key data for signing + * @param {Array} recipientKeys - recipient keys to get the signing preferences from * @param {Signature} [signature] - Any existing detached signature to add to the message * @param {Array} [signingKeyIDs] - Array of key IDs to use for signing. Each signingKeyIDs[i] corresponds to signingKeys[i] * @param {Date} [date] - Override the creation time of the signature - * @param {Array} [userIDs] - User IDs to sign with, e.g. [{ name:'Steve Sender', email:'steve@openpgp.org' }] + * @param {Array} [signingUserIDs] - User IDs to sign with, e.g. [{ name:'Steve Sender', email:'steve@openpgp.org' }] + * @param {Array} [recipientUserIDs] - User IDs associated with `recipientKeys` to get the signing preferences from * @param {Array} [notations] - Notation Data to add to the signatures, e.g. [{ name: 'test@example.org', value: new TextEncoder().encode('test'), humanReadable: true, critical: false }] * @param {Object} [config] - Full configuration, defaults to openpgp.config * @returns {Promise} New message with signed content. * @async */ - async sign(signingKeys = [], signature = null, signingKeyIDs = [], date = new Date(), userIDs = [], notations = [], config = defaultConfig) { + async sign(signingKeys = [], recipientKeys = [], signature = null, signingKeyIDs = [], date = new Date(), signingUserIDs = [], recipientUserIDs = [], notations = [], config = defaultConfig) { const packetlist = new PacketList(); const literalDataPacket = this.packets.findPacket(enums.packet.literalData); @@ -513,7 +515,7 @@ export class Message { throw new Error('No literal data packet to sign.'); } - const signaturePackets = await createSignaturePackets(literalDataPacket, signingKeys, signature, signingKeyIDs, date, userIDs, notations, false, config); // this returns the existing signature packets as well + const signaturePackets = await createSignaturePackets(literalDataPacket, signingKeys, recipientKeys, signature, signingKeyIDs, date, signingUserIDs, recipientUserIDs, notations, false, config); // this returns the existing signature packets as well const onePassSignaturePackets = signaturePackets.map( (signaturePacket, i) => OnePassSignaturePacket.fromSignaturePacket(signaturePacket, i === 0)) .reverse(); // innermost OPS refers to the first signature packet @@ -549,21 +551,23 @@ export class Message { /** * Create a detached signature for the message (the literal data packet of the message) * @param {Array} signingKeys - private keys with decrypted secret key data for signing + * @param {Array} recipientKeys - recipient keys to get the signing preferences from * @param {Signature} [signature] - Any existing detached signature * @param {Array} [signingKeyIDs] - Array of key IDs to use for signing. Each signingKeyIDs[i] corresponds to signingKeys[i] * @param {Date} [date] - Override the creation time of the signature - * @param {Array} [userIDs] - User IDs to sign with, e.g. [{ name:'Steve Sender', email:'steve@openpgp.org' }] + * @param {Array} [signingUserIDs] - User IDs to sign with, e.g. [{ name:'Steve Sender', email:'steve@openpgp.org' }] + * @param {Array} [recipientUserIDs] - User IDs associated with `recipientKeys` to get the signing preferences from * @param {Array} [notations] - Notation Data to add to the signatures, e.g. [{ name: 'test@example.org', value: new TextEncoder().encode('test'), humanReadable: true, critical: false }] * @param {Object} [config] - Full configuration, defaults to openpgp.config * @returns {Promise} New detached signature of message content. * @async */ - async signDetached(signingKeys = [], signature = null, signingKeyIDs = [], date = new Date(), userIDs = [], notations = [], config = defaultConfig) { + async signDetached(signingKeys = [], recipientKeys = [], signature = null, signingKeyIDs = [], recipientKeyIDs = [], date = new Date(), userIDs = [], notations = [], config = defaultConfig) { const literalDataPacket = this.packets.findPacket(enums.packet.literalData); if (!literalDataPacket) { throw new Error('No literal data packet to sign.'); } - return new Signature(await createSignaturePackets(literalDataPacket, signingKeys, signature, signingKeyIDs, date, userIDs, notations, true, config)); + return new Signature(await createSignaturePackets(literalDataPacket, signingKeys, recipientKeys, signature, signingKeyIDs, recipientKeyIDs, date, userIDs, notations, true, config)); } /** @@ -698,10 +702,12 @@ export class Message { * Create signature packets for the message * @param {LiteralDataPacket} literalDataPacket - the literal data packet to sign * @param {Array} [signingKeys] - private keys with decrypted secret key data for signing + * @param {Array} [recipientKeys] - recipient keys to get the signing preferences from * @param {Signature} [signature] - Any existing detached signature to append * @param {Array} [signingKeyIDs] - Array of key IDs to use for signing. Each signingKeyIDs[i] corresponds to signingKeys[i] * @param {Date} [date] - Override the creationtime of the signature - * @param {Array} [userIDs] - User IDs to sign with, e.g. [{ name:'Steve Sender', email:'steve@openpgp.org' }] + * @param {Array} [signingUserIDs] - User IDs to sign to, e.g. [{ name:'Steve Sender', email:'steve@openpgp.org' }] + * @param {Array} [recipientUserIDs] - User IDs associated with `recipientKeys` to get the signing preferences from * @param {Array} [notations] - Notation Data to add to the signatures, e.g. [{ name: 'test@example.org', value: new TextEncoder().encode('test'), humanReadable: true, critical: false }] * @param {Array} [signatureSalts] - A list of signature salts matching the number of signingKeys that should be used for v6 signatures * @param {Boolean} [detached] - Whether to create detached signature packets @@ -710,7 +716,7 @@ export class Message { * @async * @private */ -export async function createSignaturePackets(literalDataPacket, signingKeys, signature = null, signingKeyIDs = [], date = new Date(), userIDs = [], notations = [], detached = false, config = defaultConfig) { +export async function createSignaturePackets(literalDataPacket, signingKeys, recipientKeys = [], signature = null, signingKeyIDs = [], date = new Date(), signingUserIDs = [], recipientUserIDs = [], notations = [], detached = false, config = defaultConfig) { const packetlist = new PacketList(); // If data packet was created from Uint8Array, use binary, otherwise use text @@ -718,12 +724,12 @@ export async function createSignaturePackets(literalDataPacket, signingKeys, sig enums.signature.binary : enums.signature.text; await Promise.all(signingKeys.map(async (primaryKey, i) => { - const userID = userIDs[i]; + const signingUserID = signingUserIDs[i]; if (!primaryKey.isPrivate()) { throw new Error('Need private key for signing'); } - const signingKey = await primaryKey.getSigningKey(signingKeyIDs[i], date, userID, config); - return createSignaturePacket(literalDataPacket, primaryKey, signingKey.keyPacket, { signatureType }, date, userID, notations, detached, config); + const signingKey = await primaryKey.getSigningKey(signingKeyIDs[i], date, signingUserID, config); + return createSignaturePacket(literalDataPacket, recipientKeys.length ? recipientKeys : [primaryKey], signingKey.keyPacket, { signatureType }, date, recipientUserIDs, notations, detached, config); })).then(signatureList => { packetlist.push(...signatureList); }); diff --git a/src/openpgp.js b/src/openpgp.js index 7fd8a938..5237a8f3 100644 --- a/src/openpgp.js +++ b/src/openpgp.js @@ -290,7 +290,7 @@ export async function encrypt({ message, encryptionKeys, signingKeys, passwords, try { if (signingKeys.length || signature) { // sign the message only if signing keys or signature is specified - message = await message.sign(signingKeys, signature, signingKeyIDs, date, signingUserIDs, signatureNotations, config); + message = await message.sign(signingKeys, encryptionKeys, signature, signingKeyIDs, date, signingUserIDs, encryptionKeyIDs, signatureNotations, config); } message = message.compress( await getPreferredCompressionAlgo(encryptionKeys, date, encryptionUserIDs, config), @@ -422,9 +422,9 @@ export async function sign({ message, signingKeys, format = 'armored', detached try { let signature; if (detached) { - signature = await message.signDetached(signingKeys, undefined, signingKeyIDs, date, signingUserIDs, signatureNotations, config); + signature = await message.signDetached(signingKeys, [], undefined, signingKeyIDs, date, signingUserIDs, undefined, signatureNotations, config); } else { - signature = await message.sign(signingKeys, undefined, signingKeyIDs, date, signingUserIDs, signatureNotations, config); + signature = await message.sign(signingKeys, [], undefined, signingKeyIDs, date, signingUserIDs, undefined, signatureNotations, config); } if (format === 'object') return signature; diff --git a/test/general/key.js b/test/general/key.js index 151a7116..ab082354 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -7,7 +7,7 @@ chaiUse(chaiAsPromised); import sinon from 'sinon'; import openpgp from '../initOpenpgp.js'; import util from '../../src/util.js'; -import { getPreferredCipherSuite } from '../../src/key'; +import { getPreferredCipherSuite, getPreferredHashAlgo } from '../../src/key'; import KeyID from '../../src/type/keyid.js'; const priv_key_arm2 = @@ -4136,113 +4136,152 @@ CNa5yq6lyexhsn2Vs8DsX+SOSUyNJiy5FyIJ expect(revKey.armor()).not.to.match(/Comment: This is a revocation certificate/); }); - it('getPreferredCipherSuite - one key', async function() { - const [key1] = await openpgp.readKeys({ armoredKeys: twoKeys }); - const { symmetricAlgo, aeadAlgo } = await getPreferredCipherSuite([key1], undefined, undefined, { - ...openpgp.config, preferredSymmetricAlgorithm: openpgp.enums.symmetric.aes256 + describe('getPreferredCipherSuite()', () => { + it('getPreferredCipherSuite - one key', async function() { + const [key1] = await openpgp.readKeys({ armoredKeys: twoKeys }); + const { symmetricAlgo, aeadAlgo } = await getPreferredCipherSuite([key1], undefined, undefined, { + ...openpgp.config, preferredSymmetricAlgorithm: openpgp.enums.symmetric.aes256 + }); + expect(symmetricAlgo).to.equal(openpgp.enums.symmetric.aes256); + expect(aeadAlgo).to.equal(undefined); + }); + + it('getPreferredCipherSuite - two keys', async function() { + const { aes128, aes192, cast5 } = openpgp.enums.symmetric; + const [key1, key2] = await openpgp.readKeys({ armoredKeys: twoKeys }); + const primaryUser = await key2.getPrimaryUser(); + primaryUser.selfCertification.preferredSymmetricAlgorithms = [6, aes192, cast5]; + const { symmetricAlgo, aeadAlgo } = await getPreferredCipherSuite([key1, key2], undefined, undefined, { + ...openpgp.config, preferredSymmetricAlgorithm: openpgp.enums.symmetric.aes192 + }); + expect(symmetricAlgo).to.equal(aes192); + expect(aeadAlgo).to.equal(undefined); + const { symmetricAlgo: symmetricAlgo2, aeadAlgo: aeadAlgo2 } = await getPreferredCipherSuite([key1, key2], undefined, undefined, { + ...openpgp.config, preferredSymmetricAlgorithm: openpgp.enums.symmetric.aes256 + }); + expect(symmetricAlgo2).to.equal(aes128); + expect(aeadAlgo2).to.equal(undefined); + }); + + it('getPreferredCipherSuite - two keys - one without pref', async function() { + const [key1, key2] = await openpgp.readKeys({ armoredKeys: twoKeys }); + const primaryUser = await key2.getPrimaryUser(); + primaryUser.selfCertification.preferredSymmetricAlgorithms = null; + const { symmetricAlgo, aeadAlgo } = await getPreferredCipherSuite([key1, key2]); + expect(symmetricAlgo).to.equal(openpgp.enums.symmetric.aes128); + expect(aeadAlgo).to.equal(undefined); + }); + + it('getPreferredCipherSuite with AEAD - one key - GCM', async function() { + const [key1] = await openpgp.readKeys({ armoredKeys: twoKeys }); + const primaryUser = await key1.getPrimaryUser(); + primaryUser.selfCertification.features = [9]; // Monkey-patch SEIPDv2 feature flag + primaryUser.selfCertification.preferredCipherSuites = [[9, 3], [9, 2]]; + const { symmetricAlgo, aeadAlgo } = await getPreferredCipherSuite([key1], undefined, undefined, { + ...openpgp.config, + aeadProtect: true, + preferredAEADAlgorithm: openpgp.enums.aead.gcm + }); + expect(symmetricAlgo).to.equal(openpgp.enums.symmetric.aes256); + expect(aeadAlgo).to.equal(openpgp.enums.aead.gcm); + }); + + it('getPreferredCipherSuite with AEAD - one key - AES256-OCB', async function() { + const [key1] = await openpgp.readKeys({ armoredKeys: twoKeys }); + const primaryUser = await key1.getPrimaryUser(); + primaryUser.selfCertification.features = [9]; // Monkey-patch SEIPDv2 feature flag + primaryUser.selfCertification.preferredCipherSuites = [[openpgp.enums.symmetric.aes256, openpgp.enums.aead.ocb]]; + const { symmetricAlgo, aeadAlgo } = await getPreferredCipherSuite([key1], undefined, undefined, { + ...openpgp.config, + aeadProtect: true, + preferredAEADAlgorithm: openpgp.enums.aead.gcm + }); + expect(symmetricAlgo).to.equal(openpgp.enums.symmetric.aes256); + expect(aeadAlgo).to.equal(openpgp.enums.aead.ocb); + }); + + it('getPreferredCipherSuite with AEAD - one key - AES128-GCM', async function() { + const [key1] = await openpgp.readKeys({ armoredKeys: twoKeys }); + const primaryUser = await key1.getPrimaryUser(); + primaryUser.selfCertification.features = [9]; // Monkey-patch SEIPDv2 feature flag + primaryUser.selfCertification.preferredCipherSuites = [[openpgp.enums.symmetric.aes128, openpgp.enums.aead.gcm]]; + const { symmetricAlgo, aeadAlgo } = await getPreferredCipherSuite([key1], undefined, undefined, { + ...openpgp.config, + aeadProtect: true, + preferredAEADAlgorithm: openpgp.enums.aead.gcm + }); + expect(symmetricAlgo).to.equal(openpgp.enums.symmetric.aes128); + expect(aeadAlgo).to.equal(openpgp.enums.aead.gcm); + }); + + it('getPreferredCipherSuite with AEAD - two keys - one without pref', async function() { + const keys = await openpgp.readKeys({ armoredKeys: twoKeys }); + const key1 = keys[0]; + const key2 = keys[1]; + const primaryUser = await key1.getPrimaryUser(); + primaryUser.selfCertification.features = [9]; // Monkey-patch SEIPDv2 feature flag + primaryUser.selfCertification.preferredCipherSuites = [[9, 3], [9, 2]]; + const primaryUser2 = await key2.getPrimaryUser(); + primaryUser2.selfCertification.features = [9]; // Monkey-patch SEIPDv2 feature flag + const { symmetricAlgo, aeadAlgo } = await getPreferredCipherSuite([key1, key2], undefined, undefined, { + ...openpgp.config, + aeadProtect: true + }); + expect(symmetricAlgo).to.equal(openpgp.enums.symmetric.aes128); + expect(aeadAlgo).to.equal(openpgp.enums.aead.ocb); + }); + + it('getPreferredCipherSuite with AEAD - two keys - one with no support', async function() { + const keys = await openpgp.readKeys({ armoredKeys: twoKeys }); + const key1 = keys[0]; + const key2 = keys[1]; + const primaryUser = await key1.getPrimaryUser(); + primaryUser.selfCertification.features = [9]; // Monkey-patch SEIPDv2 feature flag + primaryUser.selfCertification.preferredCipherSuites = [[9, 3], [9, 2]]; + const { symmetricAlgo, aeadAlgo } = await getPreferredCipherSuite([key1, key2], undefined, undefined, { + ...openpgp.config, + aeadProtect: true + }); + expect(symmetricAlgo).to.equal(openpgp.enums.symmetric.aes256); + expect(aeadAlgo).to.equal(undefined); }); - expect(symmetricAlgo).to.equal(openpgp.enums.symmetric.aes256); - expect(aeadAlgo).to.equal(undefined); }); - it('getPreferredCipherSuite - two keys', async function() { - const { aes128, aes192, cast5 } = openpgp.enums.symmetric; - const [key1, key2] = await openpgp.readKeys({ armoredKeys: twoKeys }); - const primaryUser = await key2.getPrimaryUser(); - primaryUser.selfCertification.preferredSymmetricAlgorithms = [6, aes192, cast5]; - const { symmetricAlgo, aeadAlgo } = await getPreferredCipherSuite([key1, key2], undefined, undefined, { - ...openpgp.config, preferredSymmetricAlgorithm: openpgp.enums.symmetric.aes192 - }); - expect(symmetricAlgo).to.equal(aes192); - expect(aeadAlgo).to.equal(undefined); - const { symmetricAlgo: symmetricAlgo2, aeadAlgo: aeadAlgo2 } = await getPreferredCipherSuite([key1, key2], undefined, undefined, { - ...openpgp.config, preferredSymmetricAlgorithm: openpgp.enums.symmetric.aes256 - }); - expect(symmetricAlgo2).to.equal(aes128); - expect(aeadAlgo2).to.equal(undefined); - }); + describe('getPreferredHashAlgo()', () => { + it('getPreferredHashAlgo - it can handle unknown hash algorithms', async function() { + // Preferred hash algo: SHA256 and unknown algo with ID '99' + const signingKeyWithUnknownAlgoPref = await openpgp.readKey({ armoredKey: `-----BEGIN PGP PRIVATE KEY BLOCK----- - it('getPreferredCipherSuite - two keys - one without pref', async function() { - const [key1, key2] = await openpgp.readKeys({ armoredKeys: twoKeys }); - const primaryUser = await key2.getPrimaryUser(); - primaryUser.selfCertification.preferredSymmetricAlgorithms = null; - const { symmetricAlgo, aeadAlgo } = await getPreferredCipherSuite([key1, key2]); - expect(symmetricAlgo).to.equal(openpgp.enums.symmetric.aes128); - expect(aeadAlgo).to.equal(undefined); - }); - - it('getPreferredCipherSuite with AEAD - one key - GCM', async function() { - const [key1] = await openpgp.readKeys({ armoredKeys: twoKeys }); - const primaryUser = await key1.getPrimaryUser(); - primaryUser.selfCertification.features = [9]; // Monkey-patch SEIPDv2 feature flag - primaryUser.selfCertification.preferredCipherSuites = [[9, 3], [9, 2]]; - const { symmetricAlgo, aeadAlgo } = await getPreferredCipherSuite([key1], undefined, undefined, { - ...openpgp.config, - aeadProtect: true, - preferredAEADAlgorithm: openpgp.enums.aead.gcm +xVgEZyJrexYJKwYBBAHaRw8BAQdAJwddYhjAmI6OzqxkW9cAXVBfZdSFxsaZ +0v9YAJA50fQAAQCK5y2PWn5MEoWnMre7WDMCv3HPs92No9r7ZrmXED3ZohDT +zQ48dGVzdEB0ZXN0Lml0PsLAEQQTFgoAgwWCZyJrewMLCQcJkPu0BwaBSfbo +RRQAAAAAABwAIHNhbHRAbm90YXRpb25zLm9wZW5wZ3Bqcy5vcmdJHbHl9Kh1 +AmD2A1I0IgJEsWl12eWrRzU2C5MilKZDXQMVCGMEFgACAQIZAQKbAwIeARYh +BDc1TCI+j6hTVaLvtfu0BwaBSfboAAA3cwEAwA/JVtszZ1PgowLYG2/ok+WL ++AcEbvhPBBoJV6B2gLsA/2S/WIFiNLJd9xVPCsnlsh6GSqjNjEYXZIag0u14 +WoEKx10EZyJrexIKKwYBBAGXVQEFAQEHQEnAXen/dnz9PZ+oJ9BYrDV+N/6y +c5nTJbTmMj01obBBAwEIBwAA/0izDCturSN2513OhRlrHc55biP/GL2CR6LK +e3Zo4XCoEFDCvgQYFgoAcAWCZyJrewmQ+7QHBoFJ9uhFFAAAAAAAHAAgc2Fs +dEBub3RhdGlvbnMub3BlbnBncGpzLm9yZ6bxx8jT55ZC4ZuKBMyd1j0ULyQ4 +PPAbypPTzwI7bN7zApsMFiEENzVMIj6PqFNVou+1+7QHBoFJ9ugAAPRMAP9k +45AQSzIKF8JmS28I8hSUDrPCjSVh1A3Aw01F6sRYLgEA1wq81Sxnmvo6ztxK +EVdFOaJsHYaJ0A23hIaCWML5nAs= +=jJaL +-----END PGP PRIVATE KEY BLOCK----- +` }); + const config = { + ...openpgp.config, + preferredHashAlgorithm: openpgp.enums.hash.sha512 // SHA512 is not in the key prefs + }; + const hashAlgo = await getPreferredHashAlgo( + [signingKeyWithUnknownAlgoPref], + signingKeyWithUnknownAlgoPref, + undefined, + undefined, + config + ); + expect(hashAlgo).to.equal(openpgp.enums.hash.sha256); }); - expect(symmetricAlgo).to.equal(openpgp.enums.symmetric.aes256); - expect(aeadAlgo).to.equal(openpgp.enums.aead.gcm); - }); - - it('getPreferredCipherSuite with AEAD - one key - AES256-OCB', async function() { - const [key1] = await openpgp.readKeys({ armoredKeys: twoKeys }); - const primaryUser = await key1.getPrimaryUser(); - primaryUser.selfCertification.features = [9]; // Monkey-patch SEIPDv2 feature flag - primaryUser.selfCertification.preferredCipherSuites = [[openpgp.enums.symmetric.aes256, openpgp.enums.aead.ocb]]; - const { symmetricAlgo, aeadAlgo } = await getPreferredCipherSuite([key1], undefined, undefined, { - ...openpgp.config, - aeadProtect: true, - preferredAEADAlgorithm: openpgp.enums.aead.gcm - }); - expect(symmetricAlgo).to.equal(openpgp.enums.symmetric.aes256); - expect(aeadAlgo).to.equal(openpgp.enums.aead.ocb); - }); - - it('getPreferredCipherSuite with AEAD - one key - AES128-GCM', async function() { - const [key1] = await openpgp.readKeys({ armoredKeys: twoKeys }); - const primaryUser = await key1.getPrimaryUser(); - primaryUser.selfCertification.features = [9]; // Monkey-patch SEIPDv2 feature flag - primaryUser.selfCertification.preferredCipherSuites = [[openpgp.enums.symmetric.aes128, openpgp.enums.aead.gcm]]; - const { symmetricAlgo, aeadAlgo } = await getPreferredCipherSuite([key1], undefined, undefined, { - ...openpgp.config, - aeadProtect: true, - preferredAEADAlgorithm: openpgp.enums.aead.gcm - }); - expect(symmetricAlgo).to.equal(openpgp.enums.symmetric.aes128); - expect(aeadAlgo).to.equal(openpgp.enums.aead.gcm); - }); - - it('getPreferredCipherSuite with AEAD - two keys - one without pref', async function() { - const keys = await openpgp.readKeys({ armoredKeys: twoKeys }); - const key1 = keys[0]; - const key2 = keys[1]; - const primaryUser = await key1.getPrimaryUser(); - primaryUser.selfCertification.features = [9]; // Monkey-patch SEIPDv2 feature flag - primaryUser.selfCertification.preferredCipherSuites = [[9, 3], [9, 2]]; - const primaryUser2 = await key2.getPrimaryUser(); - primaryUser2.selfCertification.features = [9]; // Monkey-patch SEIPDv2 feature flag - const { symmetricAlgo, aeadAlgo } = await getPreferredCipherSuite([key1, key2], undefined, undefined, { - ...openpgp.config, - aeadProtect: true - }); - expect(symmetricAlgo).to.equal(openpgp.enums.symmetric.aes128); - expect(aeadAlgo).to.equal(openpgp.enums.aead.ocb); - }); - - it('getPreferredCipherSuite with AEAD - two keys - one with no support', async function() { - const keys = await openpgp.readKeys({ armoredKeys: twoKeys }); - const key1 = keys[0]; - const key2 = keys[1]; - const primaryUser = await key1.getPrimaryUser(); - primaryUser.selfCertification.features = [9]; // Monkey-patch SEIPDv2 feature flag - primaryUser.selfCertification.preferredCipherSuites = [[9, 3], [9, 2]]; - const { symmetricAlgo, aeadAlgo } = await getPreferredCipherSuite([key1, key2], undefined, undefined, { - ...openpgp.config, - aeadProtect: true - }); - expect(symmetricAlgo).to.equal(openpgp.enums.symmetric.aes256); - expect(aeadAlgo).to.equal(undefined); }); it('User attribute packet read & write', async function() { @@ -4343,7 +4382,7 @@ VYGdb3eNlV8CfoEC privateKey.users[0].userID = openpgp.UserIDPacket.fromObject({ name: 'Test User', email: 'b@c.com' }); // Set second user to prefer aes128. We will select this user. privateKey.users[1].selfCertifications[0].preferredHashAlgorithms = [openpgp.enums.hash.sha512]; - const config = { minRSABits: 1024 }; + const config = { minRSABits: 1024, preferredHashAlgorithm: openpgp.enums.hash.sha512 }; const signed = await openpgp.sign({ message: await openpgp.createMessage({ text: 'hello' }), signingKeys: privateKey, signingUserIDs: { name: 'Test McTestington', email: 'test@example.com' }, format: 'binary', config }); diff --git a/test/general/openpgp.js b/test/general/openpgp.js index c141206e..1f2d6438 100644 --- a/test/general/openpgp.js +++ b/test/general/openpgp.js @@ -297,7 +297,7 @@ DECl1Qu4QyeXin29uEXWiekMpNlZVsEuc8icCw6ABhIZ =/7PI -----END PGP PRIVATE KEY BLOCK-----`; -const priv_key_sha3 = `-----BEGIN PGP PRIVATE KEY BLOCK----- +const priv_key_sha3_512 = `-----BEGIN PGP PRIVATE KEY BLOCK----- xUsGZN8edBsAAAAgdUMlFMFCVKNo7sdUd6FVBos6NNjpUpSdrodk6BfPb/kA+3bu A2+WY2LwyxlX5o07WR2VSn+wuegC3v28yO0tClHCtwYfGw4AAABIBYJk3x50BAsJ @@ -1957,8 +1957,8 @@ aOU= })).to.be.rejectedWith(/No signing keys provided/); }); - it('Signing with key which uses sha3 should generate a valid sha3 signature', async function() { - const privKey = await openpgp.readKey({ armoredKey: priv_key_sha3 }); + it('Signing with key which uses sha3 should generate a valid sha3 signature if `config.preferredHashAlgorithm` has been set accordingly', async function() { + const privKey = await openpgp.readKey({ armoredKey: priv_key_sha3_512 }); const pubKey = privKey.toPublic(); const text = 'Hello, world.'; const message = await openpgp.createCleartextMessage({ text }); @@ -1968,10 +1968,18 @@ aOU= expect(parsedArmored.signature.packets.filterByTag(openpgp.enums.packet.signature)).to.have.length(1); expect( parsedArmored.signature.packets.filterByTag(openpgp.enums.packet.signature)[0].hashAlgorithm + ).to.equal(openpgp.config.preferredHashAlgorithm); + const cleartextMessageWithSHA3 = await openpgp.sign({ message, signingKeys: privKey, format: 'armored', config: { preferredHashAlgorithm: openpgp.enums.hash.sha3_512 } }); + const parsedArmoredSHA3 = await openpgp.readCleartextMessage({ cleartextMessage: cleartextMessageWithSHA3 }); + expect(parsedArmoredSHA3.signature.packets.filterByTag(openpgp.enums.packet.signature)).to.have.length(1); + expect( + parsedArmoredSHA3.signature.packets.filterByTag(openpgp.enums.packet.signature)[0].hashAlgorithm ).to.equal(openpgp.enums.hash.sha3_512); const verified = await openpgp.verify({ message: parsedArmored, verificationKeys: pubKey, expectSigned: true }); + const verifiedSHA3 = await openpgp.verify({ message: parsedArmoredSHA3, verificationKeys: pubKey, expectSigned: true }); expect(verified.data).to.equal(text); + expect(verifiedSHA3.data).to.equal(text); }); it('should output cleartext message of expected format', async function() { From f9a3e54364437d15c77c2fcca1c2255ad3a2add3 Mon Sep 17 00:00:00 2001 From: larabr <7375870+larabr@users.noreply.github.com> Date: Tue, 29 Oct 2024 18:16:40 +0100 Subject: [PATCH 2/2] `openpgp.sign`: add `recipientKeys` option to get the signing prefs from If given, the signature will be generated using the preferred hash algo from the recipient keys. Otherwise, the signing key preferences are used (this was also the existing behavior). Note: when signing through `openpgp.encrypt`, the `encryptionKeys` are automatically used as recipient keys. --- src/openpgp.js | 10 ++++++---- test/general/openpgp.js | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/openpgp.js b/src/openpgp.js index 5237a8f3..22ac9077 100644 --- a/src/openpgp.js +++ b/src/openpgp.js @@ -392,21 +392,23 @@ export async function decrypt({ message, decryptionKeys, passwords, sessionKeys, * @param {Object} options * @param {CleartextMessage|Message} options.message - (cleartext) message to be signed * @param {PrivateKey|PrivateKey[]} options.signingKeys - Array of keys or single key with decrypted secret key data to sign cleartext + * @param {Key|Key[]} options.recipientKeys - Array of keys or single to get the signing preferences from * @param {'armored'|'binary'|'object'} [options.format='armored'] - Format of the returned message * @param {Boolean} [options.detached=false] - If the return value should contain a detached signature * @param {KeyID|KeyID[]} [options.signingKeyIDs=latest-created valid signing (sub)keys] - Array of key IDs to use for signing. Each signingKeyIDs[i] corresponds to signingKeys[i] * @param {Date} [options.date=current date] - Override the creation date of the signature * @param {Object|Object[]} [options.signingUserIDs=primary user IDs] - Array of user IDs to sign with, one per key in `signingKeys`, e.g. `[{ name: 'Steve Sender', email: 'steve@openpgp.org' }]` + * @param {Object|Object[]} [options.recipientUserIDs=primary user IDs] - Array of user IDs to get the signing preferences from, one per key in `recipientKeys` * @param {Object|Object[]} [options.signatureNotations=[]] - Array of notations to add to the signatures, e.g. `[{ name: 'test@example.org', value: new TextEncoder().encode('test'), humanReadable: true, critical: false }]` * @param {Object} [options.config] - Custom configuration settings to overwrite those in [config]{@link module:config} * @returns {Promise>} Signed message (string if `armor` was true, the default; Uint8Array if `armor` was false). * @async * @static */ -export async function sign({ message, signingKeys, format = 'armored', detached = false, signingKeyIDs = [], date = new Date(), signingUserIDs = [], signatureNotations = [], config, ...rest }) { +export async function sign({ message, signingKeys, recipientKeys = [], format = 'armored', detached = false, signingKeyIDs = [], date = new Date(), signingUserIDs = [], recipientUserIDs = [], signatureNotations = [], config, ...rest }) { config = { ...defaultConfig, ...config }; checkConfig(config); checkCleartextOrMessage(message); checkOutputMessageFormat(format); - signingKeys = toArray(signingKeys); signingKeyIDs = toArray(signingKeyIDs); signingUserIDs = toArray(signingUserIDs); signatureNotations = toArray(signatureNotations); + signingKeys = toArray(signingKeys); signingKeyIDs = toArray(signingKeyIDs); signingUserIDs = toArray(signingUserIDs); recipientKeys = toArray(recipientKeys); recipientUserIDs = toArray(recipientUserIDs); signatureNotations = toArray(signatureNotations); if (rest.privateKeys) throw new Error('The `privateKeys` option has been removed from openpgp.sign, pass `signingKeys` instead'); if (rest.armor !== undefined) throw new Error('The `armor` option has been removed from openpgp.sign, pass `format` instead.'); @@ -422,9 +424,9 @@ export async function sign({ message, signingKeys, format = 'armored', detached try { let signature; if (detached) { - signature = await message.signDetached(signingKeys, [], undefined, signingKeyIDs, date, signingUserIDs, undefined, signatureNotations, config); + signature = await message.signDetached(signingKeys, recipientKeys, undefined, signingKeyIDs, date, signingUserIDs, recipientUserIDs, signatureNotations, config); } else { - signature = await message.sign(signingKeys, [], undefined, signingKeyIDs, date, signingUserIDs, undefined, signatureNotations, config); + signature = await message.sign(signingKeys, recipientKeys, undefined, signingKeyIDs, date, signingUserIDs, recipientUserIDs, signatureNotations, config); } if (format === 'object') return signature; diff --git a/test/general/openpgp.js b/test/general/openpgp.js index 1f2d6438..3d380d92 100644 --- a/test/general/openpgp.js +++ b/test/general/openpgp.js @@ -2162,6 +2162,47 @@ aOU= }); expect(await stream.readToEnd(streamedData)).to.equal(text); }); + + it('should sign using hash algorithm preferred by `recipientKeys` if given', async function() { + const signingKeyWithoutSHA3Pref = await openpgp.readKey({ armoredKey: `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xVgEZyID/RYJKwYBBAHaRw8BAQdAcdaUl/UXEQaT6rKNSEPmyKypikz9rIsf +BlFAQYjtsF8AAQDiW9ls2uBBRa3vA1Odl0NNNguRBolWhR9XGpdXnVBF3w5E +zQ48dGVzdEB0ZXN0Lml0PsLAEQQTFgoAgwWCZyID/QMLCQcJkJuH6wXn78D5 +RRQAAAAAABwAIHNhbHRAbm90YXRpb25zLm9wZW5wZ3Bqcy5vcmcNolfauRaj +NnItFJ0TOsiyZZhd6bMWVR4032v64tYRywMVCAoEFgACAQIZAQKbAwIeARYh +BGsOUiBRfu57iwuxh5uH6wXn78D5AACEQQEAz4YXoEKgOElvxRrIrkglUlpb +ilLZVU6mXqLxRSEtZi0BAK5xooNiLYbjF42eJuCDWUWriXufI9acT/vnruFr +p34Px10EZyID/RIKKwYBBAGXVQEFAQEHQOC8KcmOQ9+qEgoWBzc8xNgPUvoe +IVNw+mHbljD9eFBfAwEIBwAA/3iHMqnBfuM/c9tOIWKI4advW92aMYnjexrU +HdzPS2IoEU3CvgQYFgoAcAWCZyID/QmQm4frBefvwPlFFAAAAAAAHAAgc2Fs +dEBub3RhdGlvbnMub3BlbnBncGpzLm9yZ5M4VuJhTqDkHF/14D0i/wL8GTtM +fm9AIukMoYWXjGSGApsMFiEEaw5SIFF+7nuLC7GHm4frBefvwPkAAL0QAP9Z +oR7Vxyfuje3vAyEbef1gyfMN/RkIVbMKSiwy3A2W9AEA6QcBF5zUvwmHPpA4 ++SkLLMuq/yUGT6WhAq6kASQ8vgM= +=lluz +-----END PGP PRIVATE KEY BLOCK-----` }); + const recipientKeyWithSHA3Pref = await openpgp.readKey({ armoredKey: priv_key_sha3_512 }); + + const text = 'Hello, world.'; + const message = await openpgp.createCleartextMessage({ text }); + + // SHA3-512 is first preference of recipient key, and should be picked, + // even if not declared in the signing key prefs + const cleartextMessage = await openpgp.sign({ + message, + signingKeys: signingKeyWithoutSHA3Pref, + recipientKeys: recipientKeyWithSHA3Pref, + format: 'armored', + // the preferred hash algo is expected to picked when supported by the recipient keys + config: { preferredHashAlgorithm: openpgp.enums.hash.sha3_512 } + }); + const parsedArmored = await openpgp.readCleartextMessage({ cleartextMessage }); + expect(parsedArmored.signature.packets.filterByTag(openpgp.enums.packet.signature)).to.have.length(1); + expect( + parsedArmored.signature.packets.filterByTag(openpgp.enums.packet.signature)[0].hashAlgorithm + ).to.equal(openpgp.enums.hash.sha3_512); + }); }); describe('encrypt - unit tests', function() {