Skip to content

Commit 38b4d03

Browse files
committed
crypto: add signDigest/verifyDigest and Ed25519ctx support
Resolves: #60263
1 parent abff716 commit 38b4d03

File tree

10 files changed

+1352
-76
lines changed

10 files changed

+1352
-76
lines changed

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: 168 additions & 8 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/62345
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.
@@ -5786,7 +5789,12 @@ algorithm. If `algorithm` is `null` or `undefined`, then the algorithm is
57865789
dependent upon the key type.
57875790

57885791
`algorithm` is required to be `null` or `undefined` for Ed25519, Ed448, and
5789-
ML-DSA.
5792+
ML-DSA. For Ed25519 and Ed448, this function uses the pure signature schemes
5793+
from [RFC 8032][] (or Ed25519ctx when a non-empty `context` is provided;
5794+
an empty or absent context uses pure Ed25519). Ed25519 and
5795+
Ed448 signatures produced by this function cannot be verified with
5796+
[`crypto.verifyDigest()`][] because it uses the Ed25519ph and Ed448ph prehash
5797+
variants which have different domain separation.
57905798

57915799
If `key` is not a [`KeyObject`][], this function behaves as if `key` had been
57925800
passed to [`crypto.createPrivateKey()`][]. If it is an object, the following
@@ -5808,9 +5816,75 @@ additional properties can be passed:
58085816
`crypto.constants.RSA_PSS_SALTLEN_DIGEST` sets the salt length to the digest
58095817
size, `crypto.constants.RSA_PSS_SALTLEN_MAX_SIGN` (default) sets it to the
58105818
maximum permissible value.
5811-
* `context` {ArrayBuffer|Buffer|TypedArray|DataView} For Ed448, ML-DSA, and SLH-DSA,
5812-
this option specifies the optional context to differentiate signatures generated
5813-
for different purposes with the same key.
5819+
* `context` {ArrayBuffer|Buffer|TypedArray|DataView} For Ed25519
5820+
(using Ed25519ctx from [RFC 8032][]), Ed448, ML-DSA, and SLH-DSA,
5821+
this option specifies the optional context to differentiate signatures
5822+
generated for different purposes with the same key.
5823+
5824+
If the `callback` function is provided this function uses libuv's threadpool.
5825+
5826+
### `crypto.signDigest(algorithm, digest, key[, callback])`
5827+
5828+
<!-- YAML
5829+
added: REPLACEME
5830+
-->
5831+
5832+
<!--lint disable maximum-line-length remark-lint-->
5833+
5834+
* `algorithm` {string | null | undefined}
5835+
* `digest` {ArrayBuffer|Buffer|TypedArray|DataView}
5836+
* `key` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject|CryptoKey}
5837+
* `callback` {Function}
5838+
* `err` {Error}
5839+
* `signature` {Buffer}
5840+
* Returns: {Buffer} if the `callback` function is not provided.
5841+
5842+
<!--lint enable maximum-line-length remark-lint-->
5843+
5844+
Calculates and returns the signature for `digest` using the given private key
5845+
and algorithm. Unlike [`crypto.sign()`][], this function does not hash the data
5846+
internally — `digest` is expected to be a pre-computed hash digest.
5847+
5848+
The interpretation of `algorithm` and `digest` depends on the key type:
5849+
5850+
* RSA, ECDSA, DSA: `algorithm` identifies the hash function used to create
5851+
`digest`. The resulting signatures are compatible with [`crypto.verify()`][]
5852+
and signatures produced by [`crypto.sign()`][] can be verified with
5853+
[`crypto.verifyDigest()`][].
5854+
5855+
* Ed25519, Ed448: `algorithm` must be `null` or `undefined`. These keys
5856+
use the Ed25519ph and Ed448ph prehash variants from [RFC 8032][]
5857+
respectively. `digest` must be the output of the appropriate prehash
5858+
function (SHA-512 for Ed25519ph, SHAKE256 with 64-byte output for
5859+
Ed448ph). The resulting signatures can only be verified with
5860+
[`crypto.verifyDigest()`][], not with [`crypto.verify()`][], because
5861+
the prehash variants have different domain separation from the pure
5862+
Ed25519/Ed448 (or Ed25519ctx with context) variants used by
5863+
[`crypto.sign()`][] and [`crypto.verify()`][].
5864+
5865+
If `key` is not a [`KeyObject`][], this function behaves as if `key` had been
5866+
passed to [`crypto.createPrivateKey()`][]. If it is an object, the following
5867+
additional properties can be passed:
5868+
5869+
* `dsaEncoding` {string} For DSA and ECDSA, this option specifies the
5870+
format of the generated signature. It can be one of the following:
5871+
* `'der'` (default): DER-encoded ASN.1 signature structure encoding `(r, s)`.
5872+
* `'ieee-p1363'`: Signature format `r || s` as proposed in IEEE-P1363.
5873+
* `padding` {integer} Optional padding value for RSA, one of the following:
5874+
5875+
* `crypto.constants.RSA_PKCS1_PADDING` (default)
5876+
* `crypto.constants.RSA_PKCS1_PSS_PADDING`
5877+
5878+
`RSA_PKCS1_PSS_PADDING` will use MGF1 with the same hash function
5879+
used to create the digest as specified in section 3.1 of [RFC 4055][].
5880+
* `saltLength` {integer} Salt length for when padding is
5881+
`RSA_PKCS1_PSS_PADDING`. The special value
5882+
`crypto.constants.RSA_PSS_SALTLEN_DIGEST` sets the salt length to the digest
5883+
size, `crypto.constants.RSA_PSS_SALTLEN_MAX_SIGN` (default) sets it to the
5884+
maximum permissible value.
5885+
* `context` {ArrayBuffer|Buffer|TypedArray|DataView} For Ed25519ph and Ed448ph,
5886+
this option specifies the optional context to differentiate signatures
5887+
generated for different purposes with the same key.
58145888

58155889
If the `callback` function is provided this function uses libuv's threadpool.
58165890

@@ -5870,6 +5944,9 @@ not introduce timing vulnerabilities.
58705944
<!-- YAML
58715945
added: v12.0.0
58725946
changes:
5947+
- version: REPLACEME
5948+
pr-url: https://github.com/nodejs/node/pull/62345
5949+
description: Add support for Ed25519 context parameter.
58735950
- version: v24.8.0
58745951
pr-url: https://github.com/nodejs/node/pull/59570
58755952
description: Add support for ML-DSA, Ed448, and SLH-DSA context parameter.
@@ -5917,7 +5994,12 @@ Verifies the given signature for `data` using the given key and algorithm. If
59175994
key type.
59185995

59195996
`algorithm` is required to be `null` or `undefined` for Ed25519, Ed448, and
5920-
ML-DSA.
5997+
ML-DSA. For Ed25519 and Ed448, this function uses the pure signature schemes
5998+
from [RFC 8032][] (or Ed25519ctx when a non-empty `context` is provided;
5999+
an empty or absent context uses pure Ed25519). Ed25519 and
6000+
Ed448 signatures produced by [`crypto.signDigest()`][] cannot be verified with
6001+
this function because they use the Ed25519ph and Ed448ph prehash variants which
6002+
have different domain separation.
59216003

59226004
If `key` is not a [`KeyObject`][], this function behaves as if `key` had been
59236005
passed to [`crypto.createPublicKey()`][]. If it is an object, the following
@@ -5939,9 +6021,10 @@ additional properties can be passed:
59396021
`crypto.constants.RSA_PSS_SALTLEN_DIGEST` sets the salt length to the digest
59406022
size, `crypto.constants.RSA_PSS_SALTLEN_MAX_SIGN` (default) sets it to the
59416023
maximum permissible value.
5942-
* `context` {ArrayBuffer|Buffer|TypedArray|DataView} For Ed448, ML-DSA, and SLH-DSA,
5943-
this option specifies the optional context to differentiate signatures generated
5944-
for different purposes with the same key.
6024+
* `context` {ArrayBuffer|Buffer|TypedArray|DataView} For Ed25519
6025+
(using Ed25519ctx from [RFC 8032][]), Ed448, ML-DSA, and SLH-DSA,
6026+
this option specifies the optional context to differentiate signatures
6027+
generated for different purposes with the same key.
59456028

59466029
The `signature` argument is the previously calculated signature for the `data`.
59476030

@@ -5950,6 +6033,78 @@ key may be passed for `key`.
59506033

59516034
If the `callback` function is provided this function uses libuv's threadpool.
59526035

6036+
### `crypto.verifyDigest(algorithm, digest, key, signature[, callback])`
6037+
6038+
<!-- YAML
6039+
added: REPLACEME
6040+
-->
6041+
6042+
<!--lint disable maximum-line-length remark-lint-->
6043+
6044+
* `algorithm` {string|null|undefined}
6045+
* `digest` {ArrayBuffer|Buffer|TypedArray|DataView}
6046+
* `key` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject|CryptoKey}
6047+
* `signature` {ArrayBuffer|Buffer|TypedArray|DataView}
6048+
* `callback` {Function}
6049+
* `err` {Error}
6050+
* `result` {boolean}
6051+
* Returns: {boolean} `true` or `false` depending on the validity of the
6052+
signature for the digest and public key if the `callback` function is not
6053+
provided.
6054+
6055+
<!--lint enable maximum-line-length remark-lint-->
6056+
6057+
Verifies the given signature for `digest` using the given key and algorithm.
6058+
Unlike [`crypto.verify()`][], this function does not hash the data
6059+
internally — `digest` is expected to be a pre-computed hash digest.
6060+
6061+
The interpretation of `algorithm` and `digest` depends on the key type:
6062+
6063+
* RSA, ECDSA, DSA: `algorithm` identifies the hash function used to create
6064+
`digest`. Signatures produced by [`crypto.sign()`][] can be verified with
6065+
this function, and signatures produced by [`crypto.signDigest()`][] can be
6066+
verified with [`crypto.verify()`][].
6067+
* Ed25519, Ed448: `algorithm` must be `null` or `undefined`. These keys
6068+
use the Ed25519ph and Ed448ph prehash variants from [RFC 8032][]
6069+
respectively. `digest` must be the output of the appropriate prehash
6070+
function (SHA-512 for Ed25519ph, SHAKE256 with 64-byte output for
6071+
Ed448ph). Only signatures produced by [`crypto.signDigest()`][] can be
6072+
verified with this function, not those from [`crypto.sign()`][], because
6073+
the prehash variants have different domain separation from the pure
6074+
Ed25519/Ed448 (or Ed25519ctx with context) variants used by
6075+
[`crypto.sign()`][] and [`crypto.verify()`][].
6076+
6077+
If `key` is not a [`KeyObject`][], this function behaves as if `key` had been
6078+
passed to [`crypto.createPublicKey()`][]. If it is an object, the following
6079+
additional properties can be passed:
6080+
6081+
* `dsaEncoding` {string} For DSA and ECDSA, this option specifies the
6082+
format of the signature. It can be one of the following:
6083+
* `'der'` (default): DER-encoded ASN.1 signature structure encoding `(r, s)`.
6084+
* `'ieee-p1363'`: Signature format `r || s` as proposed in IEEE-P1363.
6085+
* `padding` {integer} Optional padding value for RSA, one of the following:
6086+
6087+
* `crypto.constants.RSA_PKCS1_PADDING` (default)
6088+
* `crypto.constants.RSA_PKCS1_PSS_PADDING`
6089+
6090+
`RSA_PKCS1_PSS_PADDING` will use MGF1 with the same hash function
6091+
used to create the digest as specified in section 3.1 of [RFC 4055][].
6092+
* `saltLength` {integer} Salt length for when padding is
6093+
`RSA_PKCS1_PSS_PADDING`. The special value
6094+
`crypto.constants.RSA_PSS_SALTLEN_DIGEST` sets the salt length to the digest
6095+
size, `crypto.constants.RSA_PSS_SALTLEN_MAX_SIGN` (default) sets it to the
6096+
maximum permissible value.
6097+
* `context` {ArrayBuffer|Buffer|TypedArray|DataView} For Ed25519ph and Ed448ph,
6098+
this option specifies the optional context to differentiate signatures
6099+
generated for different purposes with the same key.
6100+
6101+
The `signature` argument is the previously calculated signature for the `digest`.
6102+
6103+
Because public keys can be derived from private keys, a private key or a public
6104+
key may be passed for `key`.
6105+
6106+
If the `callback` function is provided this function uses libuv's threadpool.
6107+
59536108
### `crypto.webcrypto`
59546109

59556110
<!-- YAML
@@ -6539,6 +6694,7 @@ See the [list of SSL OP Flags][] for details.
65396694
[RFC 4122]: https://www.rfc-editor.org/rfc/rfc4122.txt
65406695
[RFC 5208]: https://www.rfc-editor.org/rfc/rfc5208.txt
65416696
[RFC 5280]: https://www.rfc-editor.org/rfc/rfc5280.txt
6697+
[RFC 8032]: https://www.rfc-editor.org/rfc/rfc8032.txt
65426698
[Web Crypto API documentation]: webcrypto.md
65436699
[`BN_is_prime_ex`]: https://www.openssl.org/docs/man1.1.1/man3/BN_is_prime_ex.html
65446700
[`Buffer`]: buffer.md
@@ -6572,6 +6728,10 @@ See the [list of SSL OP Flags][] for details.
65726728
[`crypto.publicEncrypt()`]: #cryptopublicencryptkey-buffer
65736729
[`crypto.randomBytes()`]: #cryptorandombytessize-callback
65746730
[`crypto.randomFill()`]: #cryptorandomfillbuffer-offset-size-callback
6731+
[`crypto.sign()`]: #cryptosignalgorithm-data-key-callback
6732+
[`crypto.signDigest()`]: #cryptosigndigestalgorithm-digest-key-callback
6733+
[`crypto.verify()`]: #cryptoverifyalgorithm-data-key-signature-callback
6734+
[`crypto.verifyDigest()`]: #cryptoverifydigestalgorithm-digest-key-signature-callback
65756735
[`crypto.webcrypto.getRandomValues()`]: webcrypto.md#cryptogetrandomvaluestypedarray
65766736
[`crypto.webcrypto.subtle`]: webcrypto.md#class-subtlecrypto
65776737
[`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,

0 commit comments

Comments
 (0)