Skip to content

Commit 443c5b2

Browse files
committed
crypto: add signDigest/verifyDigest and Ed25519ctx support
Resolves: #60263
1 parent b328bf7 commit 443c5b2

9 files changed

Lines changed: 791 additions & 14 deletions

File tree

deps/ncrypto/ncrypto.cc

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3783,6 +3783,23 @@ bool EVPKeyCtxPointer::setSignatureMd(const EVPMDCtxPointer& md) {
37833783
1;
37843784
}
37853785

3786+
bool EVPKeyCtxPointer::setSignatureMd(const Digest& md) {
3787+
if (!ctx_ || !md) return false;
3788+
return EVP_PKEY_CTX_set_signature_md(ctx_.get(), md.get()) == 1;
3789+
}
3790+
3791+
#if OPENSSL_VERSION_MAJOR >= 3
3792+
int EVPKeyCtxPointer::initForSignEx(const OSSL_PARAM params[]) {
3793+
if (!ctx_) return 0;
3794+
return EVP_PKEY_sign_init_ex(ctx_.get(), params);
3795+
}
3796+
3797+
int EVPKeyCtxPointer::initForVerifyEx(const OSSL_PARAM params[]) {
3798+
if (!ctx_) return 0;
3799+
return EVP_PKEY_verify_init_ex(ctx_.get(), params);
3800+
}
3801+
#endif
3802+
37863803
bool EVPKeyCtxPointer::initForEncrypt() {
37873804
if (!ctx_) return false;
37883805
return EVP_PKEY_encrypt_init(ctx_.get()) == 1;
@@ -4321,6 +4338,27 @@ std::optional<EVP_PKEY_CTX*> EVPMDCtxPointer::signInitWithContext(
43214338
#ifdef OSSL_SIGNATURE_PARAM_CONTEXT_STRING
43224339
EVP_PKEY_CTX* ctx = nullptr;
43234340

4341+
#ifdef OSSL_SIGNATURE_PARAM_INSTANCE
4342+
// Ed25519ctx requires the INSTANCE param to enable context string support.
4343+
// Ed25519 pure mode ignores context strings without this.
4344+
if (key.id() == EVP_PKEY_ED25519) {
4345+
const OSSL_PARAM params[] = {
4346+
OSSL_PARAM_construct_utf8_string(
4347+
OSSL_SIGNATURE_PARAM_INSTANCE, const_cast<char*>("Ed25519ctx"), 0),
4348+
OSSL_PARAM_construct_octet_string(
4349+
OSSL_SIGNATURE_PARAM_CONTEXT_STRING,
4350+
const_cast<unsigned char*>(context_string.data),
4351+
context_string.len),
4352+
OSSL_PARAM_END};
4353+
4354+
if (!EVP_DigestSignInit_ex(
4355+
ctx_.get(), &ctx, nullptr, nullptr, nullptr, key.get(), params)) {
4356+
return std::nullopt;
4357+
}
4358+
return ctx;
4359+
}
4360+
#endif // OSSL_SIGNATURE_PARAM_INSTANCE
4361+
43244362
const OSSL_PARAM params[] = {
43254363
OSSL_PARAM_construct_octet_string(
43264364
OSSL_SIGNATURE_PARAM_CONTEXT_STRING,
@@ -4345,6 +4383,27 @@ std::optional<EVP_PKEY_CTX*> EVPMDCtxPointer::verifyInitWithContext(
43454383
#ifdef OSSL_SIGNATURE_PARAM_CONTEXT_STRING
43464384
EVP_PKEY_CTX* ctx = nullptr;
43474385

4386+
#ifdef OSSL_SIGNATURE_PARAM_INSTANCE
4387+
// Ed25519ctx requires the INSTANCE param to enable context string support.
4388+
// Ed25519 pure mode ignores context strings without this.
4389+
if (key.id() == EVP_PKEY_ED25519) {
4390+
const OSSL_PARAM params[] = {
4391+
OSSL_PARAM_construct_utf8_string(
4392+
OSSL_SIGNATURE_PARAM_INSTANCE, const_cast<char*>("Ed25519ctx"), 0),
4393+
OSSL_PARAM_construct_octet_string(
4394+
OSSL_SIGNATURE_PARAM_CONTEXT_STRING,
4395+
const_cast<unsigned char*>(context_string.data),
4396+
context_string.len),
4397+
OSSL_PARAM_END};
4398+
4399+
if (!EVP_DigestVerifyInit_ex(
4400+
ctx_.get(), &ctx, nullptr, nullptr, nullptr, key.get(), params)) {
4401+
return std::nullopt;
4402+
}
4403+
return ctx;
4404+
}
4405+
#endif // OSSL_SIGNATURE_PARAM_INSTANCE
4406+
43484407
const OSSL_PARAM params[] = {
43494408
OSSL_PARAM_construct_octet_string(
43504409
OSSL_SIGNATURE_PARAM_CONTEXT_STRING,

deps/ncrypto/ncrypto.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -798,6 +798,7 @@ class EVPKeyCtxPointer final {
798798
bool setRsaOaepLabel(DataPointer&& data);
799799

800800
bool setSignatureMd(const EVPMDCtxPointer& md);
801+
bool setSignatureMd(const Digest& md);
801802

802803
bool publicCheck() const;
803804
bool privateCheck() const;
@@ -821,6 +822,10 @@ class EVPKeyCtxPointer final {
821822
bool initForKeygen();
822823
int initForVerify();
823824
int initForSign();
825+
#if OPENSSL_VERSION_MAJOR >= 3
826+
int initForVerifyEx(const OSSL_PARAM params[]);
827+
int initForSignEx(const OSSL_PARAM params[]);
828+
#endif
824829

825830
static EVPKeyCtxPointer New(const EVPKeyPointer& key);
826831
static EVPKeyCtxPointer NewFromID(int id);

doc/api/crypto.md

Lines changed: 130 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5745,6 +5745,9 @@ Throws an error if FIPS mode is not available.
57455745
<!-- YAML
57465746
added: v12.0.0
57475747
changes:
5748+
- version: REPLACEME
5749+
pr-url: https://github.com/nodejs/node/pull/XXXXX
5750+
description: Add support for Ed25519 context parameter.
57485751
- version: v24.8.0
57495752
pr-url: https://github.com/nodejs/node/pull/59570
57505753
description: Add support for ML-DSA, Ed448, and SLH-DSA context parameter.
@@ -5808,12 +5811,68 @@ additional properties can be passed:
58085811
`crypto.constants.RSA_PSS_SALTLEN_DIGEST` sets the salt length to the digest
58095812
size, `crypto.constants.RSA_PSS_SALTLEN_MAX_SIGN` (default) sets it to the
58105813
maximum permissible value.
5811-
* `context` {ArrayBuffer|Buffer|TypedArray|DataView} For Ed448, ML-DSA, and SLH-DSA,
5814+
* `context` {ArrayBuffer|Buffer|TypedArray|DataView} For Ed25519, Ed448, ML-DSA, and SLH-DSA,
58125815
this option specifies the optional context to differentiate signatures generated
58135816
for different purposes with the same key.
58145817

58155818
If the `callback` function is provided this function uses libuv's threadpool.
58165819

5820+
### `crypto.signDigest(algorithm, digest, key[, callback])`
5821+
5822+
<!-- YAML
5823+
added: REPLACEME
5824+
-->
5825+
5826+
<!--lint disable maximum-line-length remark-lint-->
5827+
5828+
* `algorithm` {string | null | undefined}
5829+
* `digest` {ArrayBuffer|Buffer|TypedArray|DataView}
5830+
* `key` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject|CryptoKey}
5831+
* `callback` {Function}
5832+
* `err` {Error}
5833+
* `signature` {Buffer}
5834+
* Returns: {Buffer} if the `callback` function is not provided.
5835+
5836+
<!--lint enable maximum-line-length remark-lint-->
5837+
5838+
Calculates and returns the signature for `digest` using the given private key
5839+
and algorithm. Unlike [`crypto.sign()`][], this function does not hash the data
5840+
internally — `digest` is expected to be a pre-computed hash digest.
5841+
5842+
For RSA, ECDSA, and DSA keys, `algorithm` identifies the hash function that was
5843+
used to create `digest`. For Ed25519 and Ed448 keys, `algorithm` must be `null`
5844+
or `undefined`, and `digest` must be the output of the appropriate prehash
5845+
function (SHA-512 for Ed25519ph, SHAKE256 with 64-byte output for Ed448ph).
5846+
5847+
If `key` is not a [`KeyObject`][], this function behaves as if `key` had been
5848+
passed to [`crypto.createPrivateKey()`][]. If it is an object, the following
5849+
additional properties can be passed:
5850+
5851+
* `dsaEncoding` {string} For DSA and ECDSA, this option specifies the
5852+
format of the generated signature. It can be one of the following:
5853+
* `'der'` (default): DER-encoded ASN.1 signature structure encoding `(r, s)`.
5854+
* `'ieee-p1363'`: Signature format `r || s` as proposed in IEEE-P1363.
5855+
* `padding` {integer} Optional padding value for RSA, one of the following:
5856+
5857+
* `crypto.constants.RSA_PKCS1_PADDING` (default)
5858+
* `crypto.constants.RSA_PKCS1_PSS_PADDING`
5859+
5860+
`RSA_PKCS1_PSS_PADDING` will use MGF1 with the same hash function
5861+
used to create the digest as specified in section 3.1 of [RFC 4055][].
5862+
* `saltLength` {integer} Salt length for when padding is
5863+
`RSA_PKCS1_PSS_PADDING`. The special value
5864+
`crypto.constants.RSA_PSS_SALTLEN_DIGEST` sets the salt length to the digest
5865+
size, `crypto.constants.RSA_PSS_SALTLEN_MAX_SIGN` (default) sets it to the
5866+
maximum permissible value.
5867+
* `context` {ArrayBuffer|Buffer|TypedArray|DataView} For Ed25519ph and Ed448ph,
5868+
this option specifies the optional context to differentiate signatures
5869+
generated for different purposes with the same key.
5870+
5871+
This function does not support key types that require one-shot signing without
5872+
prehash variants, such as ML-DSA and SLH-DSA.
5873+
5874+
If the `callback` function is provided this function uses libuv's threadpool.
5875+
58175876
### `crypto.subtle`
58185877

58195878
<!-- YAML
@@ -5870,6 +5929,9 @@ not introduce timing vulnerabilities.
58705929
<!-- YAML
58715930
added: v12.0.0
58725931
changes:
5932+
- version: REPLACEME
5933+
pr-url: https://github.com/nodejs/node/pull/XXXXX
5934+
description: Add support for Ed25519 context parameter.
58735935
- version: v24.8.0
58745936
pr-url: https://github.com/nodejs/node/pull/59570
58755937
description: Add support for ML-DSA, Ed448, and SLH-DSA context parameter.
@@ -5939,7 +6001,7 @@ additional properties can be passed:
59396001
`crypto.constants.RSA_PSS_SALTLEN_DIGEST` sets the salt length to the digest
59406002
size, `crypto.constants.RSA_PSS_SALTLEN_MAX_SIGN` (default) sets it to the
59416003
maximum permissible value.
5942-
* `context` {ArrayBuffer|Buffer|TypedArray|DataView} For Ed448, ML-DSA, and SLH-DSA,
6004+
* `context` {ArrayBuffer|Buffer|TypedArray|DataView} For Ed25519, Ed448, ML-DSA, and SLH-DSA,
59436005
this option specifies the optional context to differentiate signatures generated
59446006
for different purposes with the same key.
59456007

@@ -5950,6 +6012,70 @@ key may be passed for `key`.
59506012

59516013
If the `callback` function is provided this function uses libuv's threadpool.
59526014

6015+
### `crypto.verifyDigest(algorithm, digest, key, signature[, callback])`
6016+
6017+
<!-- YAML
6018+
added: REPLACEME
6019+
-->
6020+
6021+
<!--lint disable maximum-line-length remark-lint-->
6022+
6023+
* `algorithm` {string|null|undefined}
6024+
* `digest` {ArrayBuffer|Buffer|TypedArray|DataView}
6025+
* `key` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject|CryptoKey}
6026+
* `signature` {ArrayBuffer|Buffer|TypedArray|DataView}
6027+
* `callback` {Function}
6028+
* `err` {Error}
6029+
* `result` {boolean}
6030+
* Returns: {boolean} `true` or `false` depending on the validity of the
6031+
signature for the digest and public key if the `callback` function is not
6032+
provided.
6033+
6034+
<!--lint enable maximum-line-length remark-lint-->
6035+
6036+
Verifies the given signature for `digest` using the given key and algorithm.
6037+
Unlike [`crypto.verify()`][], this function does not hash the data
6038+
internally — `digest` is expected to be a pre-computed hash digest.
6039+
6040+
For RSA, ECDSA, and DSA keys, `algorithm` identifies the hash function that was
6041+
used to create `digest`. For Ed25519 and Ed448 keys, `algorithm` must be `null`
6042+
or `undefined`, and `digest` must be the output of the appropriate prehash
6043+
function (SHA-512 for Ed25519ph, SHAKE256 with 64-byte output for Ed448ph).
6044+
6045+
If `key` is not a [`KeyObject`][], this function behaves as if `key` had been
6046+
passed to [`crypto.createPublicKey()`][]. If it is an object, the following
6047+
additional properties can be passed:
6048+
6049+
* `dsaEncoding` {string} For DSA and ECDSA, this option specifies the
6050+
format of the signature. It can be one of the following:
6051+
* `'der'` (default): DER-encoded ASN.1 signature structure encoding `(r, s)`.
6052+
* `'ieee-p1363'`: Signature format `r || s` as proposed in IEEE-P1363.
6053+
* `padding` {integer} Optional padding value for RSA, one of the following:
6054+
6055+
* `crypto.constants.RSA_PKCS1_PADDING` (default)
6056+
* `crypto.constants.RSA_PKCS1_PSS_PADDING`
6057+
6058+
`RSA_PKCS1_PSS_PADDING` will use MGF1 with the same hash function
6059+
used to create the digest as specified in section 3.1 of [RFC 4055][].
6060+
* `saltLength` {integer} Salt length for when padding is
6061+
`RSA_PKCS1_PSS_PADDING`. The special value
6062+
`crypto.constants.RSA_PSS_SALTLEN_DIGEST` sets the salt length to the digest
6063+
size, `crypto.constants.RSA_PSS_SALTLEN_MAX_SIGN` (default) sets it to the
6064+
maximum permissible value.
6065+
* `context` {ArrayBuffer|Buffer|TypedArray|DataView} For Ed25519ph and Ed448ph,
6066+
this option specifies the optional context to differentiate signatures
6067+
generated for different purposes with the same key.
6068+
6069+
The `signature` argument is the previously calculated signature for the `digest`.
6070+
6071+
Because public keys can be derived from private keys, a private key or a public
6072+
key may be passed for `key`.
6073+
6074+
This function does not support key types that require one-shot verification
6075+
without prehash variants, such as ML-DSA and SLH-DSA.
6076+
6077+
If the `callback` function is provided this function uses libuv's threadpool.
6078+
59536079
### `crypto.webcrypto`
59546080

59556081
<!-- YAML
@@ -6572,6 +6698,8 @@ See the [list of SSL OP Flags][] for details.
65726698
[`crypto.publicEncrypt()`]: #cryptopublicencryptkey-buffer
65736699
[`crypto.randomBytes()`]: #cryptorandombytessize-callback
65746700
[`crypto.randomFill()`]: #cryptorandomfillbuffer-offset-size-callback
6701+
[`crypto.sign()`]: #cryptosignalgorithm-data-key-callback
6702+
[`crypto.verify()`]: #cryptoverifyalgorithm-data-key-signature-callback
65756703
[`crypto.webcrypto.getRandomValues()`]: webcrypto.md#cryptogetrandomvaluestypedarray
65766704
[`crypto.webcrypto.subtle`]: webcrypto.md#class-subtlecrypto
65776705
[`decipher.final()`]: #decipherfinaloutputencoding

lib/crypto.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,10 @@ const {
103103
const {
104104
Sign,
105105
signOneShot,
106+
signDigestOneShot,
106107
Verify,
107108
verifyOneShot,
109+
verifyDigestOneShot,
108110
} = require('internal/crypto/sig');
109111
const {
110112
Hash,
@@ -223,11 +225,13 @@ module.exports = {
223225
scrypt,
224226
scryptSync,
225227
sign: signOneShot,
228+
signDigest: signDigestOneShot,
226229
setEngine,
227230
timingSafeEqual,
228231
getFips,
229232
setFips,
230233
verify: verifyOneShot,
234+
verifyDigest: verifyDigestOneShot,
231235
hash,
232236
encapsulate,
233237
decapsulate,

lib/internal/crypto/sig.js

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -152,14 +152,14 @@ Sign.prototype.sign = function sign(options, encoding) {
152152
return ret;
153153
};
154154

155-
function signOneShot(algorithm, data, key, callback) {
155+
function signOneShotImpl(algorithm, data, key, callback, prehashed) {
156156
if (algorithm != null)
157157
validateString(algorithm, 'algorithm');
158158

159159
if (callback !== undefined)
160160
validateFunction(callback, 'callback');
161161

162-
data = getArrayBufferOrView(data, 'data');
162+
data = getArrayBufferOrView(data, prehashed ? 'digest' : 'data');
163163

164164
if (!key)
165165
throw new ERR_CRYPTO_SIGN_KEY_REQUIRED();
@@ -194,7 +194,8 @@ function signOneShot(algorithm, data, key, callback) {
194194
rsaPadding,
195195
dsaSigEnc,
196196
context,
197-
undefined);
197+
undefined,
198+
prehashed);
198199

199200
if (!callback) {
200201
const { 0: err, 1: signature } = job.run();
@@ -211,6 +212,10 @@ function signOneShot(algorithm, data, key, callback) {
211212
job.run();
212213
}
213214

215+
function signOneShot(algorithm, data, key, callback) {
216+
return signOneShotImpl(algorithm, data, key, callback, false);
217+
}
218+
214219
function Verify(algorithm, options) {
215220
if (!(this instanceof Verify))
216221
return new Verify(algorithm, options);
@@ -248,18 +253,19 @@ Verify.prototype.verify = function verify(options, signature, sigEncoding) {
248253
rsaPadding, pssSaltLength, dsaSigEnc);
249254
};
250255

251-
function verifyOneShot(algorithm, data, key, signature, callback) {
256+
function verifyOneShotImpl(algorithm, data, key, signature, callback, prehashed) {
252257
if (algorithm != null)
253258
validateString(algorithm, 'algorithm');
254259

255260
if (callback !== undefined)
256261
validateFunction(callback, 'callback');
257262

258-
data = getArrayBufferOrView(data, 'data');
263+
const dataName = prehashed ? 'digest' : 'data';
264+
data = getArrayBufferOrView(data, dataName);
259265

260266
if (!isArrayBufferView(data)) {
261267
throw new ERR_INVALID_ARG_TYPE(
262-
'data',
268+
dataName,
263269
['Buffer', 'TypedArray', 'DataView'],
264270
data,
265271
);
@@ -303,7 +309,8 @@ function verifyOneShot(algorithm, data, key, signature, callback) {
303309
rsaPadding,
304310
dsaSigEnc,
305311
context,
306-
signature);
312+
signature,
313+
prehashed);
307314

308315
if (!callback) {
309316
const { 0: err, 1: result } = job.run();
@@ -320,9 +327,23 @@ function verifyOneShot(algorithm, data, key, signature, callback) {
320327
job.run();
321328
}
322329

330+
function verifyOneShot(algorithm, data, key, signature, callback) {
331+
return verifyOneShotImpl(algorithm, data, key, signature, callback, false);
332+
}
333+
334+
function signDigestOneShot(algorithm, digest, key, callback) {
335+
return signOneShotImpl(algorithm, digest, key, callback, true);
336+
}
337+
338+
function verifyDigestOneShot(algorithm, digest, key, signature, callback) {
339+
return verifyOneShotImpl(algorithm, digest, key, signature, callback, true);
340+
}
341+
323342
module.exports = {
324343
Sign,
325344
signOneShot,
345+
signDigestOneShot,
326346
Verify,
327347
verifyOneShot,
348+
verifyDigestOneShot,
328349
};

0 commit comments

Comments
 (0)