Skip to content

Commit 7099e0f

Browse files
committed
fixup! crypto: add signDigest/verifyDigest and Ed25519ctx support
1 parent c1342a1 commit 7099e0f

File tree

5 files changed

+18
-195
lines changed

5 files changed

+18
-195
lines changed

deps/ncrypto/ncrypto.cc

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3800,18 +3800,6 @@ int EVPKeyCtxPointer::initForVerifyEx(const OSSL_PARAM params[]) {
38003800
}
38013801
#endif
38023802

3803-
#ifdef OSSL_SIGNATURE_PARAM_MU
3804-
int EVPKeyCtxPointer::initForSignMessage(const OSSL_PARAM params[]) {
3805-
if (!ctx_) return 0;
3806-
return EVP_PKEY_sign_message_init(ctx_.get(), nullptr, params);
3807-
}
3808-
3809-
int EVPKeyCtxPointer::initForVerifyMessage(const OSSL_PARAM params[]) {
3810-
if (!ctx_) return 0;
3811-
return EVP_PKEY_verify_message_init(ctx_.get(), nullptr, params);
3812-
}
3813-
#endif
3814-
38153803
bool EVPKeyCtxPointer::initForEncrypt() {
38163804
if (!ctx_) return false;
38173805
return EVP_PKEY_encrypt_init(ctx_.get()) == 1;

deps/ncrypto/ncrypto.h

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -826,10 +826,6 @@ class EVPKeyCtxPointer final {
826826
int initForVerifyEx(const OSSL_PARAM params[]);
827827
int initForSignEx(const OSSL_PARAM params[]);
828828
#endif
829-
#ifdef OSSL_SIGNATURE_PARAM_MU
830-
int initForSignMessage(const OSSL_PARAM params[]);
831-
int initForVerifyMessage(const OSSL_PARAM params[]);
832-
#endif
833829

834830
static EVPKeyCtxPointer New(const EVPKeyPointer& key);
835831
static EVPKeyCtxPointer NewFromID(int id);

doc/api/crypto.md

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5860,11 +5860,6 @@ The interpretation of `algorithm` and `digest` depends on the key type:
58605860
the prehash variants have different domain separation from the pure
58615861
Ed25519/Ed448 (or Ed25519ctx with context) variants used by
58625862
[`crypto.sign()`][] and [`crypto.verify()`][].
5863-
* ML-DSA: `algorithm` must be `null` or `undefined`. `digest` must be the
5864-
64-byte external mu value per FIPS 204. The resulting signatures are
5865-
compatible with [`crypto.verify()`][] when the mu value is correctly computed
5866-
from the message per FIPS 204.
5867-
58685863
If `key` is not a [`KeyObject`][], this function behaves as if `key` had been
58695864
passed to [`crypto.createPrivateKey()`][]. If it is an object, the following
58705865
additional properties can be passed:
@@ -5887,8 +5882,7 @@ additional properties can be passed:
58875882
maximum permissible value.
58885883
* `context` {ArrayBuffer|Buffer|TypedArray|DataView} For Ed25519ph and Ed448ph,
58895884
this option specifies the optional context to differentiate signatures
5890-
generated for different purposes with the same key. Not supported for ML-DSA
5891-
keys because the context is already encoded into the mu value.
5885+
generated for different purposes with the same key.
58925886

58935887
If the `callback` function is provided this function uses libuv's threadpool.
58945888

@@ -6077,11 +6071,6 @@ The interpretation of `algorithm` and `digest` depends on the key type:
60776071
the prehash variants have different domain separation from the pure
60786072
Ed25519/Ed448 (or Ed25519ctx with context) variants used by
60796073
[`crypto.sign()`][] and [`crypto.verify()`][].
6080-
* ML-DSA: `algorithm` must be `null` or `undefined`. `digest` must be the
6081-
64-byte external mu value per FIPS 204. Signatures produced by
6082-
[`crypto.sign()`][] can be verified with this function when the mu value is
6083-
correctly computed from the message per FIPS 204.
6084-
60856074
If `key` is not a [`KeyObject`][], this function behaves as if `key` had been
60866075
passed to [`crypto.createPublicKey()`][]. If it is an object, the following
60876076
additional properties can be passed:
@@ -6104,8 +6093,7 @@ additional properties can be passed:
61046093
maximum permissible value.
61056094
* `context` {ArrayBuffer|Buffer|TypedArray|DataView} For Ed25519ph and Ed448ph,
61066095
this option specifies the optional context to differentiate signatures
6107-
generated for different purposes with the same key. Not supported for ML-DSA
6108-
keys because the context is already encoded into the mu value.
6096+
generated for different purposes with the same key.
61096097

61106098
The `signature` argument is the previously calculated signature for the `digest`.
61116099

src/crypto/crypto_sig.cc

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -756,35 +756,6 @@ bool SignTraits::DeriveBits(Environment* env,
756756
#endif // OSSL_SIGNATURE_PARAM_INSTANCE
757757
}
758758

759-
#if OPENSSL_WITH_PQC
760-
case EVP_PKEY_ML_DSA_44:
761-
case EVP_PKEY_ML_DSA_65:
762-
case EVP_PKEY_ML_DSA_87: {
763-
// Context must already be part of the externally computed mu value.
764-
if (has_context) {
765-
if (can_throw)
766-
crypto::CheckThrow(env, SignBase::Error::ContextUnsupported);
767-
return false;
768-
}
769-
770-
#ifdef OSSL_SIGNATURE_PARAM_MU
771-
int mu_flag = 1;
772-
std::vector<OSSL_PARAM> ossl_params;
773-
ossl_params.push_back(
774-
OSSL_PARAM_construct_int(OSSL_SIGNATURE_PARAM_MU, &mu_flag));
775-
ossl_params.push_back(OSSL_PARAM_END);
776-
777-
init_ret = is_sign ? pkctx.initForSignMessage(ossl_params.data())
778-
: pkctx.initForVerifyMessage(ossl_params.data());
779-
break;
780-
#else
781-
if (can_throw)
782-
crypto::CheckThrow(env, SignBase::Error::PrehashUnsupported);
783-
return false;
784-
#endif // OSSL_SIGNATURE_PARAM_MU
785-
}
786-
#endif // OPENSSL_WITH_PQC
787-
788759
default:
789760
if (key.isOneShotVariant()) {
790761
if (can_throw)

test/parallel/test-crypto-sign-verify-digest.js

Lines changed: 16 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -345,35 +345,6 @@ if (hasOpenSSL(3, 2)) {
345345
}
346346
}
347347

348-
// --- ML-DSA external mu (prehashed) ---
349-
if (hasOpenSSL(3, 5)) {
350-
// ML-DSA signDigest/verifyDigest treats input as external mu.
351-
// mu is the 64-byte SHAKE-256(tr || M') value that the caller computes.
352-
const variants = [
353-
{ alg: 'ml_dsa_44' },
354-
{ alg: 'ml_dsa_65' },
355-
{ alg: 'ml_dsa_87' },
356-
];
357-
358-
for (const { alg } of variants) {
359-
const privKey = fixtures.readKey(`${alg}_private.pem`, 'ascii');
360-
const pubKey = fixtures.readKey(`${alg}_public.pem`, 'ascii');
361-
362-
const mu = crypto.randomBytes(64);
363-
364-
const sig = crypto.signDigest(null, mu, privKey);
365-
assert(Buffer.isBuffer(sig));
366-
assert(sig.length > 0);
367-
368-
// Verify with same mu succeeds
369-
assert.strictEqual(crypto.verifyDigest(null, mu, pubKey, sig), true);
370-
371-
// Verify with wrong mu fails
372-
const wrongMu = crypto.randomBytes(64);
373-
assert.strictEqual(crypto.verifyDigest(null, wrongMu, pubKey, sig), false);
374-
}
375-
}
376-
377348
// --- Async (callback) mode ---
378349
{
379350
const privKey = fixtures.readKey('rsa_private_2048.pem', 'ascii');
@@ -409,115 +380,8 @@ if (hasOpenSSL(3, 2)) {
409380
}));
410381
}
411382

412-
if (hasOpenSSL(3, 5)) {
413-
// ML-DSA async sign+verify with external mu (64-byte pre-computed value)
414-
const mldsaPrivKey = fixtures.readKey('ml_dsa_44_private.pem', 'ascii');
415-
const mldsaPubKey = fixtures.readKey('ml_dsa_44_public.pem', 'ascii');
416-
const mu = crypto.randomBytes(64);
417-
crypto.signDigest(null, mu, mldsaPrivKey, common.mustSucceed((sig) => {
418-
assert(sig.length > 0);
419-
crypto.verifyDigest(null, mu, mldsaPubKey, sig, common.mustSucceed((ok) => {
420-
assert.strictEqual(ok, true);
421-
}));
422-
}));
423-
424-
// Wrong mu length (32 bytes) is rejected asynchronously
425-
crypto.signDigest(null, Buffer.alloc(32), mldsaPrivKey, common.mustCall((err) => {
426-
assert(err);
427-
assert.match(err.message, /provider signature failure/);
428-
}));
429-
}
430-
431383
// --- Error: unsupported key type for prehashed signing ---
432384
{
433-
// ML-DSA rejects wrong mu length (must be exactly 64 bytes).
434-
if (hasOpenSSL(3, 5)) {
435-
const privKey = fixtures.readKey('ml_dsa_44_private.pem', 'ascii');
436-
const pubKey = fixtures.readKey('ml_dsa_44_public.pem', 'ascii');
437-
438-
assert.throws(() => {
439-
crypto.signDigest(null, Buffer.alloc(32), privKey);
440-
}, /provider signature failure/);
441-
442-
assert.throws(() => {
443-
crypto.signDigest(null, Buffer.alloc(128), privKey);
444-
}, /provider signature failure/);
445-
446-
// verifyDigest returns false for wrong mu length (not a throw)
447-
assert.strictEqual(
448-
crypto.verifyDigest(null, Buffer.alloc(32), pubKey, Buffer.alloc(2420)),
449-
false,
450-
);
451-
452-
// Context string is not supported with signDigest/verifyDigest for ML-DSA
453-
// since context must already be incorporated into the externally computed mu.
454-
assert.throws(() => {
455-
crypto.signDigest(null, Buffer.alloc(64), { key: privKey, context: Buffer.from('ctx') });
456-
}, { code: 'ERR_CRYPTO_OPERATION_FAILED', message: /Context parameter is unsupported/ });
457-
assert.throws(() => {
458-
crypto.verifyDigest(null, Buffer.alloc(64), { key: pubKey, context: Buffer.from('ctx') },
459-
Buffer.alloc(2420));
460-
}, { code: 'ERR_CRYPTO_OPERATION_FAILED', message: /Context parameter is unsupported/ });
461-
}
462-
463-
// ML-DSA external mu cross-verification with crypto.sign/crypto.verify.
464-
// Computes mu = SHAKE-256(tr || M', 64) per FIPS 204, where
465-
// tr = SHAKE-256(pk, 64) and M' encodes the context.
466-
if (hasOpenSSL(3, 5)) {
467-
const variants = [
468-
{ alg: 'ml_dsa_44', sigLen: 2420 },
469-
{ alg: 'ml_dsa_65', sigLen: 3309 },
470-
{ alg: 'ml_dsa_87', sigLen: 4627 },
471-
];
472-
473-
for (const { alg } of variants) {
474-
const privKey = fixtures.readKey(`${alg}_private.pem`, 'ascii');
475-
const pubKey = fixtures.readKey(`${alg}_public.pem`, 'ascii');
476-
477-
// Get raw public key bytes for tr computation via JWK export.
478-
const pubKeyObj = crypto.createPublicKey(pubKey);
479-
const pkBytes = Buffer.from(pubKeyObj.export({ format: 'jwk' }).pub, 'base64url');
480-
const tr = crypto.createHash('shake256', { outputLength: 64 }).update(pkBytes).digest();
481-
482-
const msg = Buffer.from('ML-DSA cross-verify test message');
483-
484-
// Without context: M' = 0x00 || 0x00 || M
485-
{
486-
const mPrime = Buffer.concat([Buffer.from([0x00, 0x00]), msg]);
487-
const mu = crypto.createHash('shake256', { outputLength: 64 })
488-
.update(tr).update(mPrime).digest();
489-
490-
const sig = crypto.signDigest(null, mu, privKey);
491-
assert.strictEqual(crypto.verify(null, msg, pubKey, sig), true);
492-
493-
const sig2 = crypto.sign(null, msg, privKey);
494-
assert.strictEqual(crypto.verifyDigest(null, mu, pubKey, sig2), true);
495-
}
496-
497-
// With context: M' = 0x00 || len(ctx) || ctx || M
498-
{
499-
const ctx = Buffer.from('test context string');
500-
const mPrime = Buffer.concat([Buffer.from([0x00, ctx.length]), ctx, msg]);
501-
const mu = crypto.createHash('shake256', { outputLength: 64 })
502-
.update(tr).update(mPrime).digest();
503-
504-
const sig = crypto.signDigest(null, mu, privKey);
505-
assert.strictEqual(
506-
crypto.verify(null, msg, { key: pubKey, context: ctx }, sig), true);
507-
508-
const sig2 = crypto.sign(null, msg, { key: privKey, context: ctx });
509-
assert.strictEqual(crypto.verifyDigest(null, mu, pubKey, sig2), true);
510-
511-
// Mismatched context: signDigest with context mu, verify without context
512-
assert.strictEqual(crypto.verify(null, msg, pubKey, sig), false);
513-
514-
// Mismatched context: sign without context, verifyDigest with context mu
515-
const sig3 = crypto.sign(null, msg, privKey);
516-
assert.strictEqual(crypto.verifyDigest(null, mu, pubKey, sig3), false);
517-
}
518-
}
519-
}
520-
521385
// Ed25519ph/Ed448ph require OpenSSL >= 3.2. On older versions, they
522386
// should throw PrehashUnsupported.
523387
if (!hasOpenSSL(3, 2)) {
@@ -537,6 +401,22 @@ if (hasOpenSSL(3, 5)) {
537401
assert.throws(() => {
538402
crypto.signDigest(123, Buffer.alloc(32), fixtures.readKey('rsa_private_2048.pem', 'ascii'));
539403
}, { code: 'ERR_INVALID_ARG_TYPE' });
404+
405+
// ML-DSA keys are not supported with signDigest/verifyDigest.
406+
if (hasOpenSSL(3, 5)) {
407+
for (const alg of ['ml_dsa_44', 'ml_dsa_65', 'ml_dsa_87']) {
408+
const privKey = fixtures.readKey(`${alg}_private.pem`, 'ascii');
409+
const pubKey = fixtures.readKey(`${alg}_public.pem`, 'ascii');
410+
411+
assert.throws(() => {
412+
crypto.signDigest(null, Buffer.alloc(64), privKey);
413+
}, { code: 'ERR_CRYPTO_OPERATION_FAILED', message: /Prehashed signing is not supported/ });
414+
415+
assert.throws(() => {
416+
crypto.verifyDigest(null, Buffer.alloc(64), pubKey, Buffer.alloc(64));
417+
}, { code: 'ERR_CRYPTO_OPERATION_FAILED', message: /Prehashed signing is not supported/ });
418+
}
419+
}
540420
}
541421

542422
// --- Error: non-signing key types (X25519, X448) ---

0 commit comments

Comments
 (0)