Skip to content
Merged
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
9 changes: 5 additions & 4 deletions benchmark/crypto/create-keyobject.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@ function readKeyPair(publicKeyName, privateKeyName) {
}

const keyFixtures = {
ec: readKeyPair('ec_p256_public', 'ec_p256_private'),
rsa: readKeyPair('rsa_public_2048', 'rsa_private_2048'),
ed25519: readKeyPair('ed25519_public', 'ed25519_private'),
'ec': readKeyPair('ec_p256_public', 'ec_p256_private'),
'rsa': readKeyPair('rsa_public_2048', 'rsa_private_2048'),
'ed25519': readKeyPair('ed25519_public', 'ed25519_private'),
'ml-dsa-44': readKeyPair('ml_dsa_44_public', 'ml_dsa_44_private'),
};

const bench = common.createBenchmark(main, {
keyType: ['rsa', 'ec', 'ed25519'],
keyType: ['rsa', 'ec', 'ed25519', 'ml-dsa-44'],
keyFormat: ['pkcs8', 'spki', 'der-pkcs8', 'der-spki', 'jwk-public', 'jwk-private'],
n: [1e3],
});
Expand Down
14 changes: 10 additions & 4 deletions benchmark/crypto/oneshot-sign.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@ const fs = require('fs');
const path = require('path');
const fixtures_keydir = path.resolve(__dirname, '../../test/fixtures/keys/');

function readKey(name) {
return fs.readFileSync(`${fixtures_keydir}/${name}.pem`, 'utf8');
}

const keyFixtures = {
ec: fs.readFileSync(`${fixtures_keydir}/ec_p256_private.pem`, 'utf-8'),
rsa: fs.readFileSync(`${fixtures_keydir}/rsa_private_2048.pem`, 'utf-8'),
ed25519: fs.readFileSync(`${fixtures_keydir}/ed25519_private.pem`, 'utf-8'),
'ec': readKey('ec_p256_private'),
'rsa': readKey('rsa_private_2048'),
'ed25519': readKey('ed25519_private'),
'ml-dsa-44': readKey('ml_dsa_44_private'),
};

const data = crypto.randomBytes(256);
Expand All @@ -18,7 +23,7 @@ let pems;
let keyObjects;

const bench = common.createBenchmark(main, {
keyType: ['rsa', 'ec', 'ed25519'],
keyType: ['rsa', 'ec', 'ed25519', 'ml-dsa-44'],
mode: ['sync', 'async', 'async-parallel'],
keyFormat: ['pem', 'der', 'jwk', 'keyObject', 'keyObject.unique'],
n: [1e3],
Expand Down Expand Up @@ -90,6 +95,7 @@ function main({ n, mode, keyFormat, keyType }) {
digest = 'sha256';
break;
case 'ed25519':
case 'ml-dsa-44':
break;
default:
throw new Error('not implemented');
Expand Down
10 changes: 6 additions & 4 deletions benchmark/crypto/oneshot-verify.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ function readKeyPair(publicKeyName, privateKeyName) {
}

const keyFixtures = {
ec: readKeyPair('ec_p256_public', 'ec_p256_private'),
rsa: readKeyPair('rsa_public_2048', 'rsa_private_2048'),
ed25519: readKeyPair('ed25519_public', 'ed25519_private'),
'ec': readKeyPair('ec_p256_public', 'ec_p256_private'),
'rsa': readKeyPair('rsa_public_2048', 'rsa_private_2048'),
'ed25519': readKeyPair('ed25519_public', 'ed25519_private'),
'ml-dsa-44': readKeyPair('ml_dsa_44_public', 'ml_dsa_44_private'),
};

const data = crypto.randomBytes(256);
Expand All @@ -29,7 +30,7 @@ let pems;
let keyObjects;

const bench = common.createBenchmark(main, {
keyType: ['rsa', 'ec', 'ed25519'],
keyType: ['rsa', 'ec', 'ed25519', 'ml-dsa-44'],
mode: ['sync', 'async', 'async-parallel'],
keyFormat: ['pem', 'der', 'jwk', 'keyObject', 'keyObject.unique'],
n: [1e3],
Expand Down Expand Up @@ -104,6 +105,7 @@ function main({ n, mode, keyFormat, keyType }) {
digest = 'sha256';
break;
case 'ed25519':
case 'ml-dsa-44':
break;
default:
throw new Error('not implemented');
Expand Down
74 changes: 72 additions & 2 deletions deps/ncrypto/ncrypto.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1897,6 +1897,31 @@ EVPKeyPointer EVPKeyPointer::NewRawPrivate(
EVP_PKEY_new_raw_private_key(id, nullptr, data.data, data.len));
}

#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5
EVPKeyPointer EVPKeyPointer::NewRawSeed(
int id, const Buffer<const unsigned char>& data) {
if (id == 0) return {};

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

EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(id, nullptr);
if (ctx == nullptr) return {};

EVP_PKEY* pkey = nullptr;
if (ctx == nullptr || EVP_PKEY_fromdata_init(ctx) <= 0 ||
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (ctx == nullptr || EVP_PKEY_fromdata_init(ctx) <= 0 ||
if (EVP_PKEY_fromdata_init(ctx) <= 0 ||

Based on line 1912 this part of the condition will always be false, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'll be doing a pass on this in a follow up unless a blocking review comes in.

EVP_PKEY_fromdata(ctx, &pkey, EVP_PKEY_KEYPAIR, params) <= 0) {
EVP_PKEY_CTX_free(ctx);
return {};
}

return EVPKeyPointer(pkey);
}
#endif

EVPKeyPointer EVPKeyPointer::NewDH(DHPointer&& dh) {
if (!dh) return {};
auto key = New();
Expand Down Expand Up @@ -1942,7 +1967,16 @@ EVP_PKEY* EVPKeyPointer::release() {

int EVPKeyPointer::id(const EVP_PKEY* key) {
if (key == nullptr) return 0;
return EVP_PKEY_id(key);
int type = EVP_PKEY_id(key);
#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a blocker but, does this also need to have a OPENSSL_IS_BORING guard? I doubt boring would end up duplicating these version values but just want to be cautious.

/cc @codebytere

// https://github.com/openssl/openssl/issues/27738#issuecomment-3013215870
if (type == -1) {
if (EVP_PKEY_is_a(key, "ML-DSA-44")) return EVP_PKEY_ML_DSA_44;
if (EVP_PKEY_is_a(key, "ML-DSA-65")) return EVP_PKEY_ML_DSA_65;
if (EVP_PKEY_is_a(key, "ML-DSA-87")) return EVP_PKEY_ML_DSA_87;
}
#endif
return type;
}

int EVPKeyPointer::base_id(const EVP_PKEY* key) {
Expand Down Expand Up @@ -1998,6 +2032,31 @@ DataPointer EVPKeyPointer::rawPublicKey() const {
return {};
}

#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5
DataPointer EVPKeyPointer::rawSeed() const {
if (!pkey_) return {};
switch (id()) {
case EVP_PKEY_ML_DSA_44:
case EVP_PKEY_ML_DSA_65:
case EVP_PKEY_ML_DSA_87:
break;
default:
unreachable();
}

size_t seed_len = 32;
if (auto data = DataPointer::Alloc(seed_len)) {
const Buffer<unsigned char> buf = data;
size_t len = data.size();
if (EVP_PKEY_get_octet_string_param(
get(), OSSL_PKEY_PARAM_ML_DSA_SEED, buf.data, len, &seed_len) != 1)
return {};
return data;
}
return {};
}
#endif

DataPointer EVPKeyPointer::rawPrivateKey() const {
if (!pkey_) return {};
if (auto data = DataPointer::Alloc(rawPrivateKeySize())) {
Expand Down Expand Up @@ -2453,7 +2512,18 @@ bool EVPKeyPointer::isRsaVariant() const {
bool EVPKeyPointer::isOneShotVariant() const {
if (!pkey_) return false;
int type = id();
return type == EVP_PKEY_ED25519 || type == EVP_PKEY_ED448;
switch (type) {
case EVP_PKEY_ED25519:
case EVP_PKEY_ED448:
#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5
case EVP_PKEY_ML_DSA_44:
case EVP_PKEY_ML_DSA_65:
case EVP_PKEY_ML_DSA_87:
#endif
return true;
default:
return false;
}
}

bool EVPKeyPointer::isSigVariant() const {
Expand Down
11 changes: 11 additions & 0 deletions deps/ncrypto/ncrypto.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@

#if OPENSSL_VERSION_MAJOR >= 3
#define OSSL3_CONST const
#if OPENSSL_VERSION_MINOR >= 5
#include <openssl/core_names.h>
#endif
#else
#define OSSL3_CONST
#endif
Expand Down Expand Up @@ -817,6 +820,10 @@ class EVPKeyPointer final {
const Buffer<const unsigned char>& data);
static EVPKeyPointer NewRawPrivate(int id,
const Buffer<const unsigned char>& data);
#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5
static EVPKeyPointer NewRawSeed(int id,
const Buffer<const unsigned char>& data);
#endif
static EVPKeyPointer NewDH(DHPointer&& dh);
static EVPKeyPointer NewRSA(RSAPointer&& rsa);

Expand Down Expand Up @@ -910,6 +917,10 @@ class EVPKeyPointer final {
DataPointer rawPrivateKey() const;
BIOPointer derPublicKey() const;

#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5
DataPointer rawSeed() const;
#endif

Result<BIOPointer, bool> writePrivateKey(
const PrivateKeyEncodingConfig& config) const;
Result<BIOPointer, bool> writePublicKey(
Expand Down
47 changes: 42 additions & 5 deletions doc/api/crypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -1916,6 +1916,9 @@ This can be called many times with new data as it is streamed.
<!-- YAML
added: v11.6.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/59259
description: Add support for ML-DSA keys.
- version:
- v14.5.0
- v12.19.0
Expand Down Expand Up @@ -2021,6 +2024,9 @@ Other key details might be exposed via this API using additional attributes.
<!-- YAML
added: v11.6.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/59259
description: Add support for ML-DSA keys.
- version:
- v13.9.0
- v12.17.0
Expand Down Expand Up @@ -2055,6 +2061,9 @@ types are:
* `'ed25519'` (OID 1.3.101.112)
* `'ed448'` (OID 1.3.101.113)
* `'dh'` (OID 1.2.840.113549.1.3.1)
* `'ml-dsa-44'`[^openssl35] (OID 2.16.840.1.101.3.4.3.17)
* `'ml-dsa-65'`[^openssl35] (OID 2.16.840.1.101.3.4.3.18)
* `'ml-dsa-87'`[^openssl35] (OID 2.16.840.1.101.3.4.3.19)

This property is `undefined` for unrecognized `KeyObject` types and symmetric
keys.
Expand Down Expand Up @@ -3403,6 +3412,9 @@ input.on('readable', () => {
<!-- YAML
added: v11.6.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/59259
description: Add support for ML-DSA keys.
- version: v15.12.0
pr-url: https://github.com/nodejs/node/pull/37254
description: The key can also be a JWK object.
Expand Down Expand Up @@ -3439,6 +3451,9 @@ of the passphrase is limited to 1024 bytes.
<!-- YAML
added: v11.6.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/59259
description: Add support for ML-DSA keys.
- version: v15.12.0
pr-url: https://github.com/nodejs/node/pull/37254
description: The key can also be a JWK object.
Expand Down Expand Up @@ -3648,6 +3663,9 @@ underlying hash function. See [`crypto.createHmac()`][] for more information.
<!-- YAML
added: v10.12.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/59259
description: Add support for ML-DSA key pairs.
- version: v18.0.0
pr-url: https://github.com/nodejs/node/pull/41678
description: Passing an invalid callback to the `callback` argument
Expand Down Expand Up @@ -3678,7 +3696,8 @@ changes:
-->

* `type` {string} Must be `'rsa'`, `'rsa-pss'`, `'dsa'`, `'ec'`, `'ed25519'`,
`'ed448'`, `'x25519'`, `'x448'`, or `'dh'`.
`'ed448'`, `'x25519'`, `'x448'`, `'dh'`, `'ml-dsa-44'`[^openssl35],
`'ml-dsa-65'`[^openssl35], or `'ml-dsa-87'`[^openssl35].
* `options` {Object}
* `modulusLength` {number} Key size in bits (RSA, DSA).
* `publicExponent` {number} Public exponent (RSA). **Default:** `0x10001`.
Expand Down Expand Up @@ -3767,6 +3786,9 @@ a `Promise` for an `Object` with `publicKey` and `privateKey` properties.
<!-- YAML
added: v10.12.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/59259
description: Add support for ML-DSA key pairs.
- version: v16.10.0
pr-url: https://github.com/nodejs/node/pull/39927
description: Add ability to define `RSASSA-PSS-params` sequence parameters
Expand All @@ -3792,7 +3814,8 @@ changes:
-->

* `type` {string} Must be `'rsa'`, `'rsa-pss'`, `'dsa'`, `'ec'`, `'ed25519'`,
`'ed448'`, `'x25519'`, `'x448'`, or `'dh'`.
`'ed448'`, `'x25519'`, `'x448'`, `'dh'`, `'ml-dsa-44'`[^openssl35],
`'ml-dsa-65'`[^openssl35], or `'ml-dsa-87'`[^openssl35].
* `options` {Object}
* `modulusLength` {number} Key size in bits (RSA, DSA).
* `publicExponent` {number} Public exponent (RSA). **Default:** `0x10001`.
Expand All @@ -3816,7 +3839,7 @@ changes:
* `privateKey` {string | Buffer | KeyObject}

Generates a new asymmetric key pair of the given `type`. RSA, RSA-PSS, DSA, EC,
Ed25519, Ed448, X25519, X448, and DH are currently supported.
Ed25519, Ed448, X25519, X448, DH, and ML-DSA[^openssl35] are currently supported.

If a `publicKeyEncoding` or `privateKeyEncoding` was specified, this function
behaves as if [`keyObject.export()`][] had been called on its result. Otherwise,
Expand Down Expand Up @@ -5416,6 +5439,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/59259
description: Add support for ML-DSA signing.
- version: v18.0.0
pr-url: https://github.com/nodejs/node/pull/41678
description: Passing an invalid callback to the `callback` argument
Expand Down Expand Up @@ -5445,7 +5471,10 @@ changes:

Calculates and returns the signature for `data` using the given private key and
algorithm. If `algorithm` is `null` or `undefined`, then the algorithm is
dependent upon the key type (especially Ed25519 and Ed448).
dependent upon the key type.

`algorithm` is required to be `null` or `undefined` for Ed25519, Ed448, and
ML-DSA.

If `key` is not a [`KeyObject`][], this function behaves as if `key` had been
passed to [`crypto.createPrivateKey()`][]. If it is an object, the following
Expand Down Expand Up @@ -5526,6 +5555,9 @@ not introduce timing vulnerabilities.
<!-- YAML
added: v12.0.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/59259
description: Add support for ML-DSA signature verification.
- version: v18.0.0
pr-url: https://github.com/nodejs/node/pull/41678
description: Passing an invalid callback to the `callback` argument
Expand Down Expand Up @@ -5561,7 +5593,10 @@ changes:

Verifies the given signature for `data` using the given key and algorithm. If
`algorithm` is `null` or `undefined`, then the algorithm is dependent upon the
key type (especially Ed25519 and Ed448).
key type.

`algorithm` is required to be `null` or `undefined` for Ed25519, Ed448, and
ML-DSA.

If `key` is not a [`KeyObject`][], this function behaves as if `key` had been
passed to [`crypto.createPublicKey()`][]. If it is an object, the following
Expand Down Expand Up @@ -6150,6 +6185,8 @@ See the [list of SSL OP Flags][] for details.
</tr>
</table>

[^openssl35]: Requires OpenSSL >= 3.5

[AEAD algorithms]: https://en.wikipedia.org/wiki/Authenticated_encryption
[CCM mode]: #ccm-mode
[CVE-2021-44532]: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-44532
Expand Down
Loading
Loading