Skip to content

Commit c694e5c

Browse files
committed
PQC signatures: select and require hash digest of at least 256 bits (draft-10)
See https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-10.html#section-9.4 . When signing, the `preferredHashAlgo` is used as long as it's supported and long enough. Otherwise, we look at the strongest supported algo, and ultimately fallback the default algo (SHA-256).
1 parent b9f2038 commit c694e5c

6 files changed

Lines changed: 143 additions & 8 deletions

File tree

src/crypto/public_key/elliptic/eddsa.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,15 @@ export async function generate(algo) {
7777
* @param {Uint8Array} publicKey - Public key
7878
* @param {Uint8Array} privateKey - Private key used to sign the message
7979
* @param {Uint8Array} hashed - The hashed message
80+
* @param {Boolean} disableHashLengthCheck - whether to skip the hash digest check; this is needed when
81+
* the function is called in the context of PQC composite signatures, that have different hash size requirements.
8082
* @returns {Promise<{
8183
* RS: Uint8Array
8284
* }>} Signature of the message
8385
* @async
8486
*/
85-
export async function sign(algo, hashAlgo, message, publicKey, privateKey, hashed) {
86-
if (getHashByteLength(hashAlgo) < getHashByteLength(getPreferredHashAlgo(algo))) {
87+
export async function sign(algo, hashAlgo, message, publicKey, privateKey, hashed, disableHashLengthCheck = false) {
88+
if (!disableHashLengthCheck && getHashByteLength(hashAlgo) < getHashByteLength(getPreferredHashAlgo(algo))) {
8789
// Enforce digest sizes:
8890
// - Ed25519: https://www.rfc-editor.org/rfc/rfc9580.html#section-5.2.3.4-4
8991
// - Ed448: https://www.rfc-editor.org/rfc/rfc9580.html#section-5.2.3.5-4
@@ -129,11 +131,13 @@ export async function sign(algo, hashAlgo, message, publicKey, privateKey, hashe
129131
* @param {Uint8Array} m - Message to verify
130132
* @param {Uint8Array} publicKey - Public key used to verify the message
131133
* @param {Uint8Array} hashed - The hashed message
134+
* @param {Boolean} disableHashLengthCheck - whether to skip the hash digest check; this is needed when
135+
* the function is called in the context of PQC composite signatures, that have different hash size requirements.
132136
* @returns {Boolean}
133137
* @async
134138
*/
135-
export async function verify(algo, hashAlgo, { RS }, m, publicKey, hashed) {
136-
if (getHashByteLength(hashAlgo) < getHashByteLength(getPreferredHashAlgo(algo))) {
139+
export async function verify(algo, hashAlgo, { RS }, m, publicKey, hashed, disableHashLengthCheck = false) {
140+
if (!disableHashLengthCheck && getHashByteLength(hashAlgo) < getHashByteLength(getPreferredHashAlgo(algo))) {
137141
// Enforce digest sizes:
138142
// - Ed25519: https://www.rfc-editor.org/rfc/rfc9580.html#section-5.2.3.4-4
139143
// - Ed448: https://www.rfc-editor.org/rfc/rfc9580.html#section-5.2.3.5-4

src/crypto/public_key/post_quantum/signature/ecc_dsa.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export async function generate(algo) {
1818
export async function sign(signatureAlgo, hashAlgo, eccSecretKey, eccPublicKey, dataDigest) {
1919
switch (signatureAlgo) {
2020
case enums.publicKey.pqc_mldsa_ed25519: {
21-
const { RS: eccSignature } = await eddsa.sign(enums.publicKey.ed25519, hashAlgo, null, eccPublicKey, eccSecretKey, dataDigest);
21+
const { RS: eccSignature } = await eddsa.sign(enums.publicKey.ed25519, hashAlgo, null, eccPublicKey, eccSecretKey, dataDigest, true);
2222

2323
return { eccSignature };
2424
}
@@ -30,7 +30,7 @@ export async function sign(signatureAlgo, hashAlgo, eccSecretKey, eccPublicKey,
3030
export async function verify(signatureAlgo, hashAlgo, eccPublicKey, dataDigest, eccSignature) {
3131
switch (signatureAlgo) {
3232
case enums.publicKey.pqc_mldsa_ed25519:
33-
return eddsa.verify(enums.publicKey.ed25519, hashAlgo, { RS: eccSignature }, null, eccPublicKey, dataDigest);
33+
return eddsa.verify(enums.publicKey.ed25519, hashAlgo, { RS: eccSignature }, null, eccPublicKey, dataDigest, true);
3434
default:
3535
throw new Error('Unsupported signature algorithm');
3636
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
export { generate, sign, verify, validateParams } from './signature';
1+
export { generate, sign, verify, validateParams, isCompatibleHashAlgo } from './signature';
22
export { expandSecretSeed as mldsaExpandSecretSeed } from './ml_dsa';

src/crypto/public_key/post_quantum/signature/signature.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import enums from '../../../../enums';
22
import * as mldsa from './ml_dsa';
33
import * as eccdsa from './ecc_dsa';
4+
import { getHashByteLength } from '../../../hash';
45

56
export async function generate(algo) {
67
switch (algo) {
@@ -15,6 +16,11 @@ export async function generate(algo) {
1516
}
1617

1718
export async function sign(signatureAlgo, hashAlgo, eccSecretKey, eccPublicKey, mldsaSecretKey, dataDigest) {
19+
if (!isCompatibleHashAlgo(signatureAlgo, hashAlgo)) {
20+
// The signature hash algo MUST have digest larger than 256 bits
21+
// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-10.html#section-9.4
22+
throw new Error('Unexpected hash algorithm for PQC signature: digest size too short');
23+
}
1824
switch (signatureAlgo) {
1925
case enums.publicKey.pqc_mldsa_ed25519: {
2026
const { eccSignature } = await eccdsa.sign(signatureAlgo, hashAlgo, eccSecretKey, eccPublicKey, dataDigest);
@@ -28,6 +34,11 @@ export async function sign(signatureAlgo, hashAlgo, eccSecretKey, eccPublicKey,
2834
}
2935

3036
export async function verify(signatureAlgo, hashAlgo, eccPublicKey, mldsaPublicKey, dataDigest, { eccSignature, mldsaSignature }) {
37+
if (!isCompatibleHashAlgo(signatureAlgo, hashAlgo)) {
38+
// The signature hash algo MUST have digest larger than 256 bits
39+
// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-10.html#section-9.4
40+
throw new Error('Unexpected hash algorithm for PQC signature: digest size too short');
41+
}
3142
switch (signatureAlgo) {
3243
case enums.publicKey.pqc_mldsa_ed25519: {
3344
const eccVerifiedPromise = eccdsa.verify(signatureAlgo, hashAlgo, eccPublicKey, dataDigest, eccSignature);
@@ -40,6 +51,17 @@ export async function verify(signatureAlgo, hashAlgo, eccPublicKey, mldsaPublicK
4051
}
4152
}
4253

54+
export function isCompatibleHashAlgo(signatureAlgo, hashAlgo) {
55+
// The signature hash algo MUST have digest larger than 256 bits
56+
// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-10.html#section-9.4
57+
switch (signatureAlgo) {
58+
case enums.publicKey.pqc_mldsa_ed25519:
59+
return getHashByteLength(hashAlgo) >= 32;
60+
default:
61+
throw new Error('Unsupported signature algorithm');
62+
}
63+
}
64+
4365
export async function validateParams(algo, eccPublicKey, eccSecretKey, mldsaPublicKey, mldsaSeed) {
4466
const eccValidationPromise = eccdsa.validateParams(algo, eccPublicKey, eccSecretKey);
4567
const mldsaValidationPromise = mldsa.validateParams(algo, mldsaPublicKey, mldsaSeed);

src/key/helper.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
SignaturePacket
1010
} from '../packet';
1111
import enums from '../enums';
12-
import { getPreferredCurveHashAlgo, getHashByteLength } from '../crypto';
12+
import { getPreferredCurveHashAlgo, getHashByteLength, publicKey } from '../crypto';
1313
import util from '../util';
1414
import defaultConfig from '../config';
1515

@@ -164,6 +164,10 @@ export async function getPreferredHashAlgo(targetKeys, signingKeyPacket, date =
164164
enums.publicKey.ed448
165165
]);
166166

167+
const pqcAlgos = new Set([
168+
enums.publicKey.pqc_mldsa_ed25519
169+
]);
170+
167171
if (eccAlgos.has(signingKeyPacket.algorithm)) {
168172
// For ECC, the returned hash algo MUST be at least as strong as `preferredCurveHashAlgo`, see:
169173
// - ECDSA: https://www.rfc-editor.org/rfc/rfc9580.html#section-5.2.3.2-5
@@ -186,6 +190,21 @@ export async function getPreferredHashAlgo(targetKeys, signingKeyPacket, date =
186190
strongestSupportedAlgo :
187191
preferredCurveAlgo;
188192
}
193+
} else if (pqcAlgos.has(signingKeyPacket.algorithm)) {
194+
// For PQC, the returned hash algo MUST be at least 256 bit long, see:
195+
// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-10.html#section-9.4 .
196+
// Hence, we return the `preferredHashAlgo` as long as it's supported and long enough;
197+
// Otherwise, we look at the strongest supported algo, and ultimately fallback the default algo (SHA-256).
198+
const preferredSenderAlgoIsSupported = isSupportedHashAlgo(preferredSenderAlgo) && publicKey.postQuantum.signature.isCompatibleHashAlgo(signingKeyPacket.algorithm, preferredSenderAlgo);
199+
200+
if (preferredSenderAlgoIsSupported) {
201+
return preferredSenderAlgo;
202+
} else {
203+
const strongestSupportedAlgo = getStrongestSupportedHashAlgo();
204+
return publicKey.postQuantum.signature.isCompatibleHashAlgo(signingKeyPacket.algorithm, strongestSupportedAlgo) ?
205+
strongestSupportedAlgo :
206+
defaultAlgo;
207+
}
189208
}
190209

191210
// `preferredSenderAlgo` may be weaker than the default, but we do not guard against this,

test/crypto/postQuantum.js

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -860,6 +860,96 @@ Fy70NCeqH2b6JeETZ1xFQEiEInk6B9WE558S9Mi6yjeSXdV65yNK2km5
860860
expect(signingKey1.getKeyID().equals(signingKey2.getKeyID())).to.be.true;
861861
});
862862

863+
it('ML-DSA + Ed25519 - throws on unexpected signature algorithm', async () => {
864+
// The signature hash algo MUST have digest larger than 256 bits
865+
// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-10.html#section-9.4
866+
const messageWithInvalidSignature = await openpgp.readMessage({ armoredMessage: `-----BEGIN PGP MESSAGE-----
867+
868+
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
946+
BQsQGR4j
947+
-----END PGP MESSAGE-----` });
948+
const publicKey = await openpgp.readKey({ armoredKey: mldsaEd25519AndMlkemX25519PublicKey });
949+
950+
await expect(openpgp.verify({ message: messageWithInvalidSignature, verificationKeys: publicKey, expectSigned: true })).to.be.rejectedWith(/Unexpected hash algorithm for PQC signature/);
951+
});
952+
863953
it('ML-DSA + ML-KEM - Test vector: decrypt/verify', async function () {
864954
// Test vector from: https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-09.html#appendix-A.3
865955
const privateKey = await openpgp.readKey({ armoredKey: mldsaEd25519AndMlkemX25519PrivateKey });

0 commit comments

Comments
 (0)