Skip to content

crypto: support Ed448 and ML-DSA context in node:crypto and Web Cryptography #59570

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions deps/ncrypto/ncrypto.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4256,6 +4256,56 @@ std::optional<EVP_PKEY_CTX*> EVPMDCtxPointer::verifyInit(
return ctx;
}

std::optional<EVP_PKEY_CTX*> EVPMDCtxPointer::signInitWithContext(
const EVPKeyPointer& key,
const Digest& digest,
const Buffer<const unsigned char>& context_string) {
#ifdef OSSL_SIGNATURE_PARAM_CONTEXT_STRING
EVP_PKEY_CTX* ctx = nullptr;

const OSSL_PARAM params[] = {
OSSL_PARAM_construct_octet_string(
OSSL_SIGNATURE_PARAM_CONTEXT_STRING,
const_cast<unsigned char*>(context_string.data),
context_string.len),
OSSL_PARAM_END};

const char* digest_name = digest ? EVP_MD_get0_name(digest) : nullptr;
if (!EVP_DigestSignInit_ex(
ctx_.get(), &ctx, digest_name, nullptr, nullptr, key.get(), params)) {
return std::nullopt;
}
return ctx;
#else
return std::nullopt;
#endif
}

std::optional<EVP_PKEY_CTX*> EVPMDCtxPointer::verifyInitWithContext(
const EVPKeyPointer& key,
const Digest& digest,
const Buffer<const unsigned char>& context_string) {
#ifdef OSSL_SIGNATURE_PARAM_CONTEXT_STRING
EVP_PKEY_CTX* ctx = nullptr;

const OSSL_PARAM params[] = {
OSSL_PARAM_construct_octet_string(
OSSL_SIGNATURE_PARAM_CONTEXT_STRING,
const_cast<unsigned char*>(context_string.data),
context_string.len),
OSSL_PARAM_END};

const char* digest_name = digest ? EVP_MD_get0_name(digest) : nullptr;
if (!EVP_DigestVerifyInit_ex(
ctx_.get(), &ctx, digest_name, nullptr, nullptr, key.get(), params)) {
return std::nullopt;
}
return ctx;
#else
return std::nullopt;
#endif
}

DataPointer EVPMDCtxPointer::signOneShot(
const Buffer<const unsigned char>& buf) const {
if (!ctx_) return {};
Expand Down
9 changes: 9 additions & 0 deletions deps/ncrypto/ncrypto.h
Original file line number Diff line number Diff line change
Expand Up @@ -1403,6 +1403,15 @@ class EVPMDCtxPointer final {
std::optional<EVP_PKEY_CTX*> verifyInit(const EVPKeyPointer& key,
const Digest& digest);

std::optional<EVP_PKEY_CTX*> signInitWithContext(
const EVPKeyPointer& key,
const Digest& digest,
const Buffer<const unsigned char>& context_string);
std::optional<EVP_PKEY_CTX*> verifyInitWithContext(
const EVPKeyPointer& key,
const Digest& digest,
const Buffer<const unsigned char>& context_string);

DataPointer signOneShot(const Buffer<const unsigned char>& buf) const;
DataPointer sign(const Buffer<const unsigned char>& buf) const;
bool verify(const Buffer<const unsigned char>& buf,
Expand Down
12 changes: 12 additions & 0 deletions doc/api/crypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -5691,6 +5691,9 @@ Throws an error if FIPS mode is not available.
<!-- YAML
added: v12.0.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/59570
description: Add support for ML-DSA and Ed448 context parameter.
- version: v24.6.0
pr-url: https://github.com/nodejs/node/pull/59259
description: Add support for ML-DSA signing.
Expand Down Expand Up @@ -5748,6 +5751,9 @@ additional properties can be passed:
`crypto.constants.RSA_PSS_SALTLEN_DIGEST` sets the salt length to the digest
size, `crypto.constants.RSA_PSS_SALTLEN_MAX_SIGN` (default) sets it to the
maximum permissible value.
* `context` {ArrayBuffer|Buffer|TypedArray|DataView} For Ed448 and ML-DSA, this
option specifies the optional context to differentiate signatures generated
for different purposes with the same key.

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

Expand Down Expand Up @@ -5807,6 +5813,9 @@ not introduce timing vulnerabilities.
<!-- YAML
added: v12.0.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/59570
description: Add support for ML-DSA and Ed448 context parameter.
- version: v24.6.0
pr-url: https://github.com/nodejs/node/pull/59259
description: Add support for ML-DSA signature verification.
Expand Down Expand Up @@ -5870,6 +5879,9 @@ additional properties can be passed:
`crypto.constants.RSA_PSS_SALTLEN_DIGEST` sets the salt length to the digest
size, `crypto.constants.RSA_PSS_SALTLEN_MAX_SIGN` (default) sets it to the
maximum permissible value.
* `context` {ArrayBuffer|Buffer|TypedArray|DataView} For Ed448 and ML-DSA, this
option specifies the optional context to differentiate signatures generated
for different purposes with the same key.

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

Expand Down
44 changes: 8 additions & 36 deletions doc/api/webcrypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -1265,7 +1265,7 @@ changes:

<!--lint disable maximum-line-length remark-lint-->

* `algorithm` {string|Algorithm|RsaPssParams|EcdsaParams|Ed448Params|ContextParams}
* `algorithm` {string|Algorithm|RsaPssParams|EcdsaParams|ContextParams}
* `key` {CryptoKey}
* `data` {ArrayBuffer|TypedArray|DataView|Buffer}
* Returns: {Promise} Fulfills with an {ArrayBuffer} upon success.
Expand Down Expand Up @@ -1374,7 +1374,7 @@ changes:

<!--lint disable maximum-line-length remark-lint-->

* `algorithm` {string|Algorithm|RsaPssParams|EcdsaParams|Ed448Params|ContextParams}
* `algorithm` {string|Algorithm|RsaPssParams|EcdsaParams|ContextParams}
* `key` {CryptoKey}
* `signature` {ArrayBuffer|TypedArray|DataView|Buffer}
* `data` {ArrayBuffer|TypedArray|DataView|Buffer}
Expand Down Expand Up @@ -1652,20 +1652,23 @@ added: REPLACEME
added: REPLACEME
-->

* Type: {string} Must be `'ML-DSA-44'`[^modern-algos], `'ML-DSA-65'`[^modern-algos], or `'ML-DSA-87'`[^modern-algos].
* Type: {string} Must be `Ed448`[^secure-curves], `'ML-DSA-44'`[^modern-algos],
`'ML-DSA-65'`[^modern-algos], or `'ML-DSA-87'`[^modern-algos].

#### `contextParams.context`

<!-- YAML
added: REPLACEME
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/59570
description: Non-empty context is now supported.
-->

* Type: {ArrayBuffer|TypedArray|DataView|Buffer|undefined}

The `context` member represents the optional context data to associate with
the message.
The Node.js Web Crypto API implementation only supports zero-length context
which is equivalent to not providing context at all.

### Class: `CShakeParams`

Expand Down Expand Up @@ -1846,37 +1849,6 @@ added: v15.0.0

* Type: {string} Must be one of `'P-256'`, `'P-384'`, `'P-521'`.

### Class: `Ed448Params`

<!-- YAML
added: v15.0.0
-->

#### `ed448Params.name`

<!-- YAML
added:
- v18.4.0
- v16.17.0
-->

* Type: {string} Must be `'Ed448'`[^secure-curves].

#### `ed448Params.context`

<!-- YAML
added:
- v18.4.0
- v16.17.0
-->

* Type: {ArrayBuffer|TypedArray|DataView|Buffer|undefined}

The `context` member represents the optional context data to associate with
the message.
The Node.js Web Crypto API implementation only supports zero-length context
which is equivalent to not providing context at all.

### Class: `EncapsulatedBits`

<!-- YAML
Expand Down
1 change: 1 addition & 0 deletions lib/internal/crypto/cfrg.js
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ function eddsaSignVerify(key, data, algorithm, signature) {
undefined,
undefined,
undefined,
algorithm.name === 'Ed448' ? algorithm.context : undefined,
signature));
}

Expand Down
1 change: 1 addition & 0 deletions lib/internal/crypto/ec.js
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ function ecdsaSignVerify(key, data, { name, hash }, signature) {
undefined, // Salt length, not used with ECDSA
undefined, // PSS Padding, not used with ECDSA
kSigEncP1363,
undefined,
signature));
}

Expand Down
1 change: 1 addition & 0 deletions lib/internal/crypto/ml_dsa.js
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@ function mlDsaSignVerify(key, data, algorithm, signature) {
undefined,
undefined,
undefined,
algorithm.context,
signature));
}

Expand Down
1 change: 1 addition & 0 deletions lib/internal/crypto/rsa.js
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ function rsaSignVerify(key, data, { saltLength }, signature) {
saltLength,
key[kAlgorithm].name === 'RSA-PSS' ? RSA_PKCS1_PSS_PADDING : undefined,
undefined,
undefined,
signature);
});
}
Expand Down
24 changes: 23 additions & 1 deletion lib/internal/crypto/sig.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,19 @@ function getDSASignatureEncoding(options) {
return kSigEncDER;
}

function getContext(options) {
if (options?.context === undefined) {
return undefined;
}

if (!isArrayBufferView(options.context)) {
throw new ERR_INVALID_ARG_TYPE(
'options.context', ['Buffer', 'TypedArray', 'DataView'], options.context);
}

return options.context;
}

function getIntOption(name, options) {
const value = options[name];
if (value !== undefined) {
Expand Down Expand Up @@ -153,6 +166,9 @@ function signOneShot(algorithm, data, key, callback) {
// Options specific to (EC)DSA
const dsaSigEnc = getDSASignatureEncoding(key);

// Options specific to Ed448 and ML-DSA
const context = getContext(key);

const {
data: keyData,
format: keyFormat,
Expand All @@ -171,7 +187,9 @@ function signOneShot(algorithm, data, key, callback) {
algorithm,
pssSaltLength,
rsaPadding,
dsaSigEnc);
dsaSigEnc,
context,
undefined);

if (!callback) {
const { 0: err, 1: signature } = job.run();
Expand Down Expand Up @@ -249,6 +267,9 @@ function verifyOneShot(algorithm, data, key, signature, callback) {
// Options specific to (EC)DSA
const dsaSigEnc = getDSASignatureEncoding(key);

// Options specific to Ed448 and ML-DSA
const context = getContext(key);

if (!isArrayBufferView(signature)) {
throw new ERR_INVALID_ARG_TYPE(
'signature',
Expand Down Expand Up @@ -276,6 +297,7 @@ function verifyOneShot(algorithm, data, key, signature, callback) {
pssSaltLength,
rsaPadding,
dsaSigEnc,
context,
signature);

if (!callback) {
Expand Down
5 changes: 2 additions & 3 deletions lib/internal/crypto/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -242,8 +242,8 @@ const kAlgorithmDefinitions = {
'generateKey': null,
'exportKey': null,
'importKey': null,
'sign': 'Ed448Params',
'verify': 'Ed448Params',
'sign': 'ContextParams',
'verify': 'ContextParams',
},
'HKDF': {
'importKey': null,
Expand Down Expand Up @@ -440,7 +440,6 @@ const simpleAlgorithmDictionaries = {
salt: 'BufferSource',
info: 'BufferSource',
},
Ed448Params: { context: 'BufferSource' },
ContextParams: { context: 'BufferSource' },
Pbkdf2Params: { hash: 'HashAlgorithmIdentifier', salt: 'BufferSource' },
RsaOaepParams: { label: 'BufferSource' },
Expand Down
34 changes: 21 additions & 13 deletions lib/internal/crypto/webidl.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const {
MathTrunc,
Number,
NumberIsFinite,
NumberParseInt,
ObjectPrototypeHasOwnProperty,
ObjectPrototypeIsPrototypeOf,
SafeArrayIterator,
Expand Down Expand Up @@ -304,13 +305,12 @@ function createDictionaryConverter(name, dictionaries) {
const context = `'${key}' of '${name}'${
opts.context ? ` (${opts.context})` : ''
}`;
const { converter, validator } = member;
const idlMemberValue = converter(esMemberValue, {
const idlMemberValue = member.converter(esMemberValue, {
__proto__: null,
...opts,
context,
});
validator?.(idlMemberValue, esDict);
member.validator?.(idlMemberValue, esDict);
setOwnProperty(idlDict, key, idlMemberValue);
} else if (member.required) {
throw makeException(
Expand Down Expand Up @@ -779,17 +779,25 @@ converters.EcdhKeyDeriveParams = createDictionaryConverter(
},
]);

for (const name of ['Ed448Params', 'ContextParams']) {
converters[name] = createDictionaryConverter(
name, [
...new SafeArrayIterator(dictAlgorithm),
{
key: 'context',
converter: converters.BufferSource,
validator: validateZeroLength(`${name}.context`),
converters.ContextParams = createDictionaryConverter(
'ContextParams', [
...new SafeArrayIterator(dictAlgorithm),
{
key: 'context',
converter: converters.BufferSource,
validator(V, dict) {
let { 0: major, 1: minor } = process.versions.openssl.split('.');
major = NumberParseInt(major, 10);
minor = NumberParseInt(minor, 10);
if (major > 3 || (major === 3 && minor >= 2)) {
this.validator = undefined;
} else {
this.validator = validateZeroLength('ContextParams.context');
this.validator(V, dict);
}
},
]);
}
},
]);

module.exports = {
converters,
Expand Down
Loading
Loading