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..22ac9077 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), @@ -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, 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, 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/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..3d380d92 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() { @@ -2154,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() {