crypto: support ML-KEM in Web Cryptography

PR-URL: https://github.com/nodejs/node/pull/59569
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
Filip Skokan 2025-08-21 11:09:06 +02:00
parent 19d2cee62c
commit 589ef79bf8
No known key found for this signature in database
18 changed files with 1853 additions and 101 deletions

View File

@ -2162,21 +2162,34 @@ DataPointer EVPKeyPointer::rawPublicKey() const {
#if OPENSSL_WITH_PQC
DataPointer EVPKeyPointer::rawSeed() const {
if (!pkey_) return {};
// Determine seed length and parameter name based on key type
size_t seed_len;
const char* param_name;
switch (id()) {
case EVP_PKEY_ML_DSA_44:
case EVP_PKEY_ML_DSA_65:
case EVP_PKEY_ML_DSA_87:
seed_len = 32; // ML-DSA uses 32-byte seeds
param_name = OSSL_PKEY_PARAM_ML_DSA_SEED;
break;
case EVP_PKEY_ML_KEM_512:
case EVP_PKEY_ML_KEM_768:
case EVP_PKEY_ML_KEM_1024:
seed_len = 64; // ML-KEM uses 64-byte seeds
param_name = OSSL_PKEY_PARAM_ML_KEM_SEED;
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)
get(), param_name, buf.data, len, &seed_len) != 1)
return {};
return data;
}

View File

@ -2,6 +2,9 @@
<!-- YAML
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/59569
description: ML-KEM algorithms are now supported.
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/59365
description: ChaCha20-Poly1305 algorithm is now supported.
@ -107,6 +110,9 @@ Algorithms:
* `'ML-DSA-44'`[^openssl35]
* `'ML-DSA-65'`[^openssl35]
* `'ML-DSA-87'`[^openssl35]
* `'ML-KEM-1024'`[^openssl35]
* `'ML-KEM-512'`[^openssl35]
* `'ML-KEM-768'`[^openssl35]
* `'SHA3-256'`
* `'SHA3-384'`
* `'SHA3-512'`
@ -119,6 +125,10 @@ Key Formats:
Methods:
* [`subtle.decapsulateBits()`][]
* [`subtle.decapsulateKey()`][]
* [`subtle.encapsulateBits()`][]
* [`subtle.encapsulateKey()`][]
* [`subtle.getPublicKey()`][]
* [`SubtleCrypto.supports()`][]
@ -480,40 +490,83 @@ const decrypted = new TextDecoder().decode(await crypto.subtle.decrypt(
## Algorithm matrix
The table details the algorithms supported by the Node.js Web Crypto API
The tables details the algorithms supported by the Node.js Web Crypto API
implementation and the APIs supported for each:
| Algorithm | `generateKey` | `exportKey` | `importKey` | `encrypt/decrypt` | `wrapKey/unwrapKey` | `deriveBits/deriveKey` | `sign/verify` | `digest` | `getPublicKey` |
| ------------------------------------ | ------------- | ----------- | ----------- | ----------------- | ------------------- | ---------------------- | ------------- | -------- | -------------- |
| `'AES-CBC'` | ✔ | ✔ | ✔ | ✔ | ✔ | | | | |
| `'AES-CTR'` | ✔ | ✔ | ✔ | ✔ | ✔ | | | | |
| `'AES-GCM'` | ✔ | ✔ | ✔ | ✔ | ✔ | | | | |
| `'AES-KW'` | ✔ | ✔ | ✔ | | ✔ | | | | |
| `'ChaCha20-Poly1305'`[^modern-algos] | ✔ | ✔ | ✔ | ✔ | ✔ | | | | |
| `'cSHAKE128'`[^modern-algos] | | | | | | | | ✔ | |
| `'cSHAKE256'`[^modern-algos] | | | | | | | | ✔ | |
| `'ECDH'` | ✔ | ✔ | ✔ | | | ✔ | | | ✔ |
| `'ECDSA'` | ✔ | ✔ | ✔ | | | | ✔ | | ✔ |
| `'Ed25519'` | ✔ | ✔ | ✔ | | | | ✔ | | ✔ |
| `'Ed448'`[^secure-curves] | ✔ | ✔ | ✔ | | | | ✔ | | ✔ |
| `'HKDF'` | | | ✔ | | | ✔ | | | |
| `'HMAC'` | ✔ | ✔ | ✔ | | | | ✔ | | |
| `'ML-DSA-44'`[^modern-algos] | ✔ | ✔ | ✔ | | | | ✔ | | ✔ |
| `'ML-DSA-65'`[^modern-algos] | ✔ | ✔ | ✔ | | | | ✔ | | ✔ |
| `'ML-DSA-87'`[^modern-algos] | ✔ | ✔ | ✔ | | | | ✔ | | ✔ |
| `'PBKDF2'` | | | ✔ | | | ✔ | | | |
| `'RSA-OAEP'` | ✔ | ✔ | ✔ | ✔ | ✔ | | | | ✔ |
| `'RSA-PSS'` | ✔ | ✔ | ✔ | | | | ✔ | | ✔ |
| `'RSASSA-PKCS1-v1_5'` | ✔ | ✔ | ✔ | | | | ✔ | | ✔ |
| `'SHA-1'` | | | | | | | | ✔ | |
| `'SHA-256'` | | | | | | | | ✔ | |
| `'SHA-384'` | | | | | | | | ✔ | |
| `'SHA-512'` | | | | | | | | ✔ | |
| `'SHA3-256'`[^modern-algos] | | | | | | | | ✔ | |
| `'SHA3-384'`[^modern-algos] | | | | | | | | ✔ | |
| `'SHA3-512'`[^modern-algos] | | | | | | | | ✔ | |
| `'X25519'` | ✔ | ✔ | ✔ | | | ✔ | | | ✔ |
| `'X448'`[^secure-curves] | ✔ | ✔ | ✔ | | | ✔ | | | ✔ |
### Key Management APIs
| Algorithm | [`subtle.generateKey()`][] | [`subtle.exportKey()`][] | [`subtle.importKey()`][] | [`subtle.getPublicKey()`][] |
| ------------------------------------ | -------------------------- | ------------------------ | ------------------------ | --------------------------- |
| `'AES-CBC'` | ✔ | ✔ | ✔ | |
| `'AES-CTR'` | ✔ | ✔ | ✔ | |
| `'AES-GCM'` | ✔ | ✔ | ✔ | |
| `'AES-KW'` | ✔ | ✔ | ✔ | |
| `'ChaCha20-Poly1305'`[^modern-algos] | ✔ | ✔ | ✔ | |
| `'ECDH'` | ✔ | ✔ | ✔ | ✔ |
| `'ECDSA'` | ✔ | ✔ | ✔ | ✔ |
| `'Ed25519'` | ✔ | ✔ | ✔ | ✔ |
| `'Ed448'`[^secure-curves] | ✔ | ✔ | ✔ | ✔ |
| `'HKDF'` | | | ✔ | |
| `'HMAC'` | ✔ | ✔ | ✔ | |
| `'ML-DSA-44'`[^modern-algos] | ✔ | ✔ | ✔ | ✔ |
| `'ML-DSA-65'`[^modern-algos] | ✔ | ✔ | ✔ | ✔ |
| `'ML-DSA-87'`[^modern-algos] | ✔ | ✔ | ✔ | ✔ |
| `'ML-KEM-512'`[^modern-algos] | ✔ | ✔ | ✔ | ✔ |
| `'ML-KEM-768'`[^modern-algos] | ✔ | ✔ | ✔ | ✔ |
| `'ML-KEM-1024'`[^modern-algos] | ✔ | ✔ | ✔ | ✔ |
| `'PBKDF2'` | | | ✔ | |
| `'RSA-OAEP'` | ✔ | ✔ | ✔ | ✔ |
| `'RSA-PSS'` | ✔ | ✔ | ✔ | ✔ |
| `'RSASSA-PKCS1-v1_5'` | ✔ | ✔ | ✔ | ✔ |
| `'X25519'` | ✔ | ✔ | ✔ | ✔ |
| `'X448'`[^secure-curves] | ✔ | ✔ | ✔ | ✔ |
### Crypto Operation APIs
**Column Legend:**
* **Encryption**: [`subtle.encrypt()`][] / [`subtle.decrypt()`][]
* **Signatures and MAC**: [`subtle.sign()`][] / [`subtle.verify()`][]
* **Key or Bits Derivation**: [`subtle.deriveBits()`][] / [`subtle.deriveKey()`][]
* **Key Wrapping**: [`subtle.wrapKey()`][] / [`subtle.unwrapKey()`][]
* **Key Encapsulation**: [`subtle.encapsulateBits()`][] / [`subtle.decapsulateBits()`][] /
[`subtle.encapsulateKey()`][] / [`subtle.decapsulateKey()`][]
* **Digest**: [`subtle.digest()`][]
| Algorithm | Encryption | Signatures and MAC | Key or Bits Derivation | Key Wrapping | Key Encapsulation | Digest |
| ------------------------------------ | ---------- | ------------------ | ---------------------- | ------------ | ----------------- | ------ |
| `'AES-CBC'` | ✔ | | | ✔ | | |
| `'AES-CTR'` | ✔ | | | ✔ | | |
| `'AES-GCM'` | ✔ | | | ✔ | | |
| `'AES-KW'` | | | | ✔ | | |
| `'ChaCha20-Poly1305'`[^modern-algos] | ✔ | | | ✔ | | |
| `'cSHAKE128'`[^modern-algos] | | | | | | ✔ |
| `'cSHAKE256'`[^modern-algos] | | | | | | ✔ |
| `'ECDH'` | | | ✔ | | | |
| `'ECDSA'` | | ✔ | | | | |
| `'Ed25519'` | | ✔ | | | | |
| `'Ed448'`[^secure-curves] | | ✔ | | | | |
| `'HKDF'` | | | ✔ | | | |
| `'HMAC'` | | ✔ | | | | |
| `'ML-DSA-44'`[^modern-algos] | | ✔ | | | | |
| `'ML-DSA-65'`[^modern-algos] | | ✔ | | | | |
| `'ML-DSA-87'`[^modern-algos] | | ✔ | | | | |
| `'ML-KEM-512'`[^modern-algos] | | | | | ✔ | |
| `'ML-KEM-768'`[^modern-algos] | | | | | ✔ | |
| `'ML-KEM-1024'`[^modern-algos] | | | | | ✔ | |
| `'PBKDF2'` | | | ✔ | | | |
| `'RSA-OAEP'` | ✔ | | | ✔ | | |
| `'RSA-PSS'` | | ✔ | | | | |
| `'RSASSA-PKCS1-v1_5'` | | ✔ | | | | |
| `'SHA-1'` | | | | | | ✔ |
| `'SHA-256'` | | | | | | ✔ |
| `'SHA-384'` | | | | | | ✔ |
| `'SHA-512'` | | | | | | ✔ |
| `'SHA3-256'`[^modern-algos] | | | | | | ✔ |
| `'SHA3-384'`[^modern-algos] | | | | | | ✔ |
| `'SHA3-512'`[^modern-algos] | | | | | | ✔ |
| `'X25519'` | | | ✔ | | | |
| `'X448'`[^secure-curves] | | | ✔ | | | |
## Class: `Crypto`
@ -623,40 +676,56 @@ key may be used.
The possible usages are:
* `'encrypt'` - The key may be used to encrypt data.
* `'decrypt'` - The key may be used to decrypt data.
* `'sign'` - The key may be used to generate digital signatures.
* `'verify'` - The key may be used to verify digital signatures.
* `'deriveKey'` - The key may be used to derive a new key.
* `'deriveBits'` - The key may be used to derive bits.
* `'wrapKey'` - The key may be used to wrap another key.
* `'unwrapKey'` - The key may be used to unwrap another key.
* `'encrypt'` - Enable using the key with [`subtle.encrypt()`][]
* `'decrypt'` - Enable using the key with [`subtle.decrypt()`][]
* `'sign'` - Enable using the key with [`subtle.sign()`][]
* `'verify'` - Enable using the key with [`subtle.verify()`][]
* `'deriveKey'` - Enable using the key with [`subtle.deriveKey()`][]
* `'deriveBits'` - Enable using the key with [`subtle.deriveBits()`][]
* `'encapsulateBits'` - Enable using the key with [`subtle.encapsulateBits()`][]
* `'decapsulateBits'` - Enable using the key with [`subtle.decapsulateBits()`][]
* `'encapsulateKey'` - Enable using the key with [`subtle.encapsulateKey()`][]
* `'decapsulateKey'` - Enable using the key with [`subtle.decapsulateKey()`][]
* `'wrapKey'` - Enable using the key with [`subtle.wrapKey()`][]
* `'unwrapKey'` - Enable using the key with [`subtle.unwrapKey()`][]
Valid key usages depend on the key algorithm (identified by
`cryptokey.algorithm.name`).
| Supported Key Algorithm | `'encrypt'` | `'decrypt'` | `'sign'` | `'verify'` | `'deriveKey'` | `'deriveBits'` | `'wrapKey'` | `'unwrapKey'` |
| ------------------------------------ | ----------- | ----------- | -------- | ---------- | ------------- | -------------- | ----------- | ------------- |
| `'AES-CBC'` | ✔ | ✔ | | | | | ✔ | ✔ |
| `'AES-CTR'` | ✔ | ✔ | | | | | ✔ | ✔ |
| `'AES-GCM'` | ✔ | ✔ | | | | | ✔ | ✔ |
| `'AES-KW'` | | | | | | | ✔ | ✔ |
| `'ChaCha20-Poly1305'`[^modern-algos] | ✔ | ✔ | | | | | ✔ | ✔ |
| `'ECDH'` | | | | | ✔ | ✔ | | |
| `'ECDSA'` | | | ✔ | ✔ | | | | |
| `'Ed25519'` | | | ✔ | ✔ | | | | |
| `'Ed448'`[^secure-curves] | | | ✔ | ✔ | | | | |
| `'HDKF'` | | | | | ✔ | ✔ | | |
| `'HMAC'` | | | ✔ | ✔ | | | | |
| `'ML-DSA-44'`[^modern-algos] | | | ✔ | ✔ | | | | |
| `'ML-DSA-65'`[^modern-algos] | | | ✔ | ✔ | | | | |
| `'ML-DSA-87'`[^modern-algos] | | | ✔ | ✔ | | | | |
| `'PBKDF2'` | | | | | ✔ | ✔ | | |
| `'RSA-OAEP'` | ✔ | ✔ | | | | | ✔ | ✔ |
| `'RSA-PSS'` | | | ✔ | ✔ | | | | |
| `'RSASSA-PKCS1-v1_5'` | | | ✔ | ✔ | | | | |
| `'X25519'` | | | | | ✔ | ✔ | | |
| `'X448'`[^secure-curves] | | | | | ✔ | ✔ | | |
**Column Legend:**
* **Encryption**: [`subtle.encrypt()`][] / [`subtle.decrypt()`][]
* **Signatures and MAC**: [`subtle.sign()`][] / [`subtle.verify()`][]
* **Key or Bits Derivation**: [`subtle.deriveBits()`][] / [`subtle.deriveKey()`][]
* **Key Wrapping**: [`subtle.wrapKey()`][] / [`subtle.unwrapKey()`][]
* **Key Encapsulation**: [`subtle.encapsulateBits()`][] / [`subtle.decapsulateBits()`][] /
[`subtle.encapsulateKey()`][] / [`subtle.decapsulateKey()`][]
| Supported Key Algorithm | Encryption | Signatures and MAC | Key or Bits Derivation | Key Wrapping | Key Encapsulation |
| ------------------------------------ | ---------- | ------------------ | ---------------------- | ------------ | ----------------- |
| `'AES-CBC'` | ✔ | | | ✔ | |
| `'AES-CTR'` | ✔ | | | ✔ | |
| `'AES-GCM'` | ✔ | | | ✔ | |
| `'AES-KW'` | | | | ✔ | |
| `'ChaCha20-Poly1305'`[^modern-algos] | ✔ | | | ✔ | |
| `'ECDH'` | | | ✔ | | |
| `'ECDSA'` | | ✔ | | | |
| `'Ed25519'` | | ✔ | | | |
| `'Ed448'`[^secure-curves] | | ✔ | | | |
| `'HDKF'` | | | ✔ | | |
| `'HMAC'` | | ✔ | | | |
| `'ML-DSA-44'`[^modern-algos] | | ✔ | | | |
| `'ML-DSA-65'`[^modern-algos] | | ✔ | | | |
| `'ML-DSA-87'`[^modern-algos] | | ✔ | | | |
| `'ML-KEM-512'`[^modern-algos] | | | | | ✔ |
| `'ML-KEM-768'`[^modern-algos] | | | | | ✔ |
| `'ML-KEM-1024'`[^modern-algos] | | | | | ✔ |
| `'PBKDF2'` | | | ✔ | | |
| `'RSA-OAEP'` | ✔ | | | ✔ | |
| `'RSA-PSS'` | | ✔ | | | |
| `'RSASSA-PKCS1-v1_5'` | | ✔ | | | |
| `'X25519'` | | | ✔ | | |
| `'X448'`[^secure-curves] | | | ✔ | | |
## Class: `CryptoKeyPair`
@ -710,6 +779,47 @@ Allows feature detection in Web Crypto API,
which can be used to detect whether a given algorithm identifier
(including its parameters) is supported for the given operation.
### `subtle.decapsulateBits(decapsulationAlgorithm, decapsulationKey, ciphertext)`
<!-- YAML
added: REPLACEME
-->
> Stability: 1.1 - Active development
* `decapsulationAlgorithm` {string|Algorithm}
* `decapsulationKey` {CryptoKey}
* `ciphertext` {ArrayBuffer|TypedArray|DataView|Buffer}
* Returns: {Promise} Fulfills with {ArrayBuffer} upon success.
The algorithms currently supported include:
* `'ML-KEM-512'`[^modern-algos]
* `'ML-KEM-768'`[^modern-algos]
* `'ML-KEM-1024'`[^modern-algos]
### `subtle.decapsulateKey(decapsulationAlgorithm, decapsulationKey, ciphertext, sharedKeyAlgorithm, extractable, usages)`
<!-- YAML
added: REPLACEME
-->
> Stability: 1.1 - Active development
* `decapsulationAlgorithm` {string|Algorithm}
* `decapsulationKey` {CryptoKey}
* `ciphertext` {ArrayBuffer|TypedArray|DataView|Buffer}
* `sharedKeyAlgorithm` {string|Algorithm|HmacImportParams|AesDerivedKeyParams}
* `extractable` {boolean}
* `usages` {string\[]} See [Key usages][].
* Returns: {Promise} Fulfills with {CryptoKey} upon success.
The algorithms currently supported include:
* `'ML-KEM-512'`[^modern-algos]
* `'ML-KEM-768'`[^modern-algos]
* `'ML-KEM-1024'`[^modern-algos]
### `subtle.decrypt(algorithm, key, data)`
<!-- YAML
@ -861,6 +971,45 @@ If `algorithm` is provided as a {string}, it must be one of:
If `algorithm` is provided as an {Object}, it must have a `name` property
whose value is one of the above.
### `subtle.encapsulateBits(encapsulationAlgorithm, encapsulationKey)`
<!-- YAML
added: REPLACEME
-->
> Stability: 1.1 - Active development
* `encapsulationAlgorithm` {string|Algorithm}
* `encapsulationKey` {CryptoKey}
* Returns: {Promise} Fulfills with {EncapsulatedBits} upon success.
The algorithms currently supported include:
* `'ML-KEM-512'`[^modern-algos]
* `'ML-KEM-768'`[^modern-algos]
* `'ML-KEM-1024'`[^modern-algos]
### `subtle.encapsulateKey(encapsulationAlgorithm, encapsulationKey, sharedKeyAlgorithm, extractable, usages)`
<!-- YAML
added: REPLACEME
-->
> Stability: 1.1 - Active development
* `encapsulationAlgorithm` {string|Algorithm}
* `encapsulationKey` {CryptoKey}
* `sharedKeyAlgorithm` {string|Algorithm|HmacImportParams|AesDerivedKeyParams}
* `extractable` {boolean}
* `usages` {string\[]} See [Key usages][].
* Returns: {Promise} Fulfills with {EncapsulatedKey} upon success.
The algorithms currently supported include:
* `'ML-KEM-512'`[^modern-algos]
* `'ML-KEM-768'`[^modern-algos]
* `'ML-KEM-1024'`[^modern-algos]
### `subtle.encrypt(algorithm, key, data)`
<!-- YAML
@ -894,6 +1043,9 @@ The algorithms currently supported include:
<!-- YAML
added: v15.0.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/59569
description: ML-KEM algorithms are now supported.
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/59365
description: ChaCha20-Poly1305 algorithm is now supported.
@ -943,6 +1095,9 @@ specification.
| `'ML-DSA-44'`[^modern-algos] | ✔ | ✔ | ✔ | | | ✔ | ✔ |
| `'ML-DSA-65'`[^modern-algos] | ✔ | ✔ | ✔ | | | ✔ | ✔ |
| `'ML-DSA-87'`[^modern-algos] | ✔ | ✔ | ✔ | | | ✔ | ✔ |
| `'ML-KEM-512'`[^modern-algos] | ✔ | ✔ | | | | ✔ | ✔ |
| `'ML-KEM-768'`[^modern-algos] | ✔ | ✔ | | | | ✔ | ✔ |
| `'ML-KEM-1024'`[^modern-algos] | ✔ | ✔ | | | | ✔ | ✔ |
| `'RSA-OAEP'` | ✔ | ✔ | ✔ | | | | |
| `'RSA-PSS'` | ✔ | ✔ | ✔ | | | | |
| `'RSASSA-PKCS1-v1_5'` | ✔ | ✔ | ✔ | | | | |
@ -966,6 +1121,9 @@ Derives the public key from a given private key.
<!-- YAML
added: v15.0.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/59569
description: ML-KEM algorithms are now supported.
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/59365
description: ChaCha20-Poly1305 algorithm is now supported.
@ -998,6 +1156,9 @@ include:
* `'ML-DSA-44'`[^modern-algos]
* `'ML-DSA-65'`[^modern-algos]
* `'ML-DSA-87'`[^modern-algos]
* `'ML-KEM-512'`[^modern-algos]
* `'ML-KEM-768'`[^modern-algos]
* `'ML-KEM-1024'`[^modern-algos]
* `'RSA-OAEP'`
* `'RSA-PSS'`
* `'RSASSA-PKCS1-v1_5'`
@ -1018,6 +1179,9 @@ The {CryptoKey} (secret key) generating algorithms supported include:
<!-- YAML
added: v15.0.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/59569
description: ML-KEM algorithms are now supported.
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/59365
description: ChaCha20-Poly1305 algorithm is now supported.
@ -1074,6 +1238,9 @@ The algorithms currently supported include:
| `'ML-DSA-44'`[^modern-algos] | ✔ | ✔ | ✔ | | | ✔ | ✔ |
| `'ML-DSA-65'`[^modern-algos] | ✔ | ✔ | ✔ | | | ✔ | ✔ |
| `'ML-DSA-87'`[^modern-algos] | ✔ | ✔ | ✔ | | | ✔ | ✔ |
| `'ML-KEM-512'`[^modern-algos] | ✔ | ✔ | | | | ✔ | ✔ |
| `'ML-KEM-768'`[^modern-algos] | ✔ | ✔ | | | | ✔ | ✔ |
| `'ML-KEM-1024'`[^modern-algos] | ✔ | ✔ | | | | ✔ | ✔ |
| `'PBKDF2'` | | | | ✔ | ✔ | | |
| `'RSA-OAEP'` | ✔ | ✔ | ✔ | | | | |
| `'RSA-PSS'` | ✔ | ✔ | ✔ | | | | |
@ -1181,6 +1348,9 @@ The unwrapped key algorithms supported include:
* `'ML-DSA-44'`[^modern-algos]
* `'ML-DSA-65'`[^modern-algos]
* `'ML-DSA-87'`[^modern-algos]
* `'ML-KEM-512'`[^modern-algos]
* `'ML-KEM-768'`[^modern-algos]
* `'ML-KEM-1024'`[^modern-algos]v
* `'RSA-OAEP'`
* `'RSA-PSS'`
* `'RSASSA-PKCS1-v1_5'`
@ -1707,6 +1877,50 @@ 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
added: REPLACEME
-->
#### `encapsulatedBits.ciphertext`
<!-- YAML
added: REPLACEME
-->
* Type: {ArrayBuffer}
#### `encapsulatedBits.sharedKey`
<!-- YAML
added: REPLACEME
-->
* Type: {ArrayBuffer}
### Class: `EncapsulatedKey`
<!-- YAML
added: REPLACEME
-->
#### `encapsulatedKey.ciphertext`
<!-- YAML
added: REPLACEME
-->
* Type: {ArrayBuffer}
#### `encapsulatedKey.sharedKey`
<!-- YAML
added: REPLACEME
-->
* Type: {CryptoKey}
### Class: `HkdfParams`
<!-- YAML
@ -2187,4 +2401,20 @@ The length (in bytes) of the random salt to use.
[Secure Curves in the Web Cryptography API]: #secure-curves-in-the-web-cryptography-api
[Web Crypto API]: https://www.w3.org/TR/WebCryptoAPI/
[`SubtleCrypto.supports()`]: #static-method-subtlecryptosupportsoperation-algorithm-lengthoradditionalalgorithm
[`subtle.decapsulateBits()`]: #subtledecapsulatebitsdecapsulationalgorithm-decapsulationkey-ciphertext
[`subtle.decapsulateKey()`]: #subtledecapsulatekeydecapsulationalgorithm-decapsulationkey-ciphertext-sharedkeyalgorithm-extractable-usages
[`subtle.decrypt()`]: #subtledecryptalgorithm-key-data
[`subtle.deriveBits()`]: #subtlederivebitsalgorithm-basekey-length
[`subtle.deriveKey()`]: #subtlederivekeyalgorithm-basekey-derivedkeyalgorithm-extractable-keyusages
[`subtle.digest()`]: #subtledigestalgorithm-data
[`subtle.encapsulateBits()`]: #subtleencapsulatebitsencapsulationalgorithm-encapsulationkey
[`subtle.encapsulateKey()`]: #subtleencapsulatekeyencapsulationalgorithm-encapsulationkey-sharedkeyalgorithm-extractable-usages
[`subtle.encrypt()`]: #subtleencryptalgorithm-key-data
[`subtle.exportKey()`]: #subtleexportkeyformat-key
[`subtle.generateKey()`]: #subtlegeneratekeyalgorithm-extractable-keyusages
[`subtle.getPublicKey()`]: #subtlegetpublickeykey-keyusages
[`subtle.importKey()`]: #subtleimportkeyformat-keydata-algorithm-extractable-keyusages
[`subtle.sign()`]: #subtlesignalgorithm-key-data
[`subtle.unwrapKey()`]: #subtleunwrapkeyformat-wrappedkey-unwrappingkey-unwrapalgo-unwrappedkeyalgo-extractable-keyusages
[`subtle.verify()`]: #subtleverifyalgorithm-key-signature-data
[`subtle.wrapKey()`]: #subtlewrapkeyformat-key-wrappingkey-wrapalgo

View File

@ -308,6 +308,14 @@ const {
result = require('internal/crypto/ml_dsa')
.mlDsaImportKey('KeyObject', this, algorithm, extractable, keyUsages);
break;
case 'ML-KEM-512':
// Fall through
case 'ML-KEM-768':
// Fall through
case 'ML-KEM-1024':
result = require('internal/crypto/ml_kem')
.mlKemImportKey('KeyObject', this, algorithm, extractable, keyUsages);
break;
default:
throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError');
}
@ -568,7 +576,7 @@ function getKeyObjectHandleFromJwk(key, ctx) {
const handle = new KeyObjectHandle();
const keyType = isPublic ? kKeyTypePublic : kKeyTypePrivate;
if (!handle.initMlDsaRaw(key.alg, keyData, keyType)) {
if (!handle.initPqcRaw(key.alg, keyData, keyType)) {
throw new ERR_CRYPTO_INVALID_JWK();
}

View File

@ -69,7 +69,7 @@ function verifyAcceptableMlDsaKeyUse(name, isPublic, usages) {
function createMlDsaRawKey(name, keyData, isPublic) {
const handle = new KeyObjectHandle();
const keyType = isPublic ? kKeyTypePublic : kKeyTypePrivate;
if (!handle.initMlDsaRaw(name, keyData, keyType)) {
if (!handle.initPqcRaw(name, keyData, keyType)) {
throw lazyDOMException('Invalid keyData', 'DataError');
}
@ -119,19 +119,16 @@ function mlDsaExportKey(key, format) {
switch (format) {
case kWebCryptoKeyFormatRaw: {
if (key[kKeyType] === 'private') {
const { priv } = key[kKeyObject][kHandle].exportJwk({}, false);
return Buffer.alloc(32, priv, 'base64url').buffer;
return key[kKeyObject][kHandle].rawSeed().buffer;
}
const { pub } = key[kKeyObject][kHandle].exportJwk({}, false);
return Buffer.alloc(Buffer.byteLength(pub, 'base64url'), pub, 'base64url').buffer;
return key[kKeyObject][kHandle].rawPublicKey().buffer;
}
case kWebCryptoKeyFormatSPKI: {
return key[kKeyObject][kHandle].export(kKeyFormatDER, kWebCryptoKeyFormatSPKI).buffer;
}
case kWebCryptoKeyFormatPKCS8: {
const { priv } = key[kKeyObject][kHandle].exportJwk({}, false);
const seed = Buffer.alloc(32, priv, 'base64url');
const seed = key[kKeyObject][kHandle].rawSeed();
const buffer = new Uint8Array(54);
buffer.set([
0x30, 0x34, 0x02, 0x01, 0x00, 0x30, 0x0B, 0x06,

View File

@ -0,0 +1,287 @@
'use strict';
const {
PromiseWithResolvers,
SafeSet,
Uint8Array,
} = primordials;
const {
kCryptoJobAsync,
KEMDecapsulateJob,
KEMEncapsulateJob,
KeyObjectHandle,
kKeyFormatDER,
kKeyTypePrivate,
kKeyTypePublic,
kWebCryptoKeyFormatPKCS8,
kWebCryptoKeyFormatRaw,
kWebCryptoKeyFormatSPKI,
} = internalBinding('crypto');
const {
getUsagesUnion,
hasAnyNotIn,
kHandle,
kKeyObject,
} = require('internal/crypto/util');
const {
lazyDOMException,
promisify,
} = require('internal/util');
const {
generateKeyPair: _generateKeyPair,
} = require('internal/crypto/keygen');
const {
InternalCryptoKey,
PrivateKeyObject,
PublicKeyObject,
createPrivateKey,
createPublicKey,
kAlgorithm,
kKeyType,
} = require('internal/crypto/keys');
const generateKeyPair = promisify(_generateKeyPair);
async function mlKemGenerateKey(algorithm, extractable, keyUsages) {
const { name } = algorithm;
const usageSet = new SafeSet(keyUsages);
if (hasAnyNotIn(usageSet, ['encapsulateKey', 'encapsulateBits', 'decapsulateKey', 'decapsulateBits'])) {
throw lazyDOMException(
`Unsupported key usage for an ${name} key`,
'SyntaxError');
}
const keyPair = await generateKeyPair(name.toLowerCase()).catch((err) => {
throw lazyDOMException(
'The operation failed for an operation-specific reason',
{ name: 'OperationError', cause: err });
});
const publicUsages = getUsagesUnion(usageSet, 'encapsulateBits', 'encapsulateKey');
const privateUsages = getUsagesUnion(usageSet, 'decapsulateBits', 'decapsulateKey');
const keyAlgorithm = { name };
const publicKey =
new InternalCryptoKey(
keyPair.publicKey,
keyAlgorithm,
publicUsages,
true);
const privateKey =
new InternalCryptoKey(
keyPair.privateKey,
keyAlgorithm,
privateUsages,
extractable);
return { __proto__: null, privateKey, publicKey };
}
function mlKemExportKey(key, format) {
try {
switch (format) {
case kWebCryptoKeyFormatRaw: {
if (key[kKeyType] === 'private') {
return key[kKeyObject][kHandle].rawSeed().buffer;
}
return key[kKeyObject][kHandle].rawPublicKey().buffer;
}
case kWebCryptoKeyFormatSPKI: {
return key[kKeyObject][kHandle].export(kKeyFormatDER, kWebCryptoKeyFormatSPKI).buffer;
}
case kWebCryptoKeyFormatPKCS8: {
const seed = key[kKeyObject][kHandle].rawSeed();
const buffer = new Uint8Array(86);
buffer.set([
0x30, 0x54, 0x02, 0x01, 0x00, 0x30, 0x0B, 0x06,
0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04,
0x04, 0x00, 0x04, 0x42, 0x80, 0x40,
], 0);
switch (key[kAlgorithm].name) {
case 'ML-KEM-512':
buffer.set([0x01], 17);
break;
case 'ML-KEM-768':
buffer.set([0x02], 17);
break;
case 'ML-KEM-1024':
buffer.set([0x03], 17);
break;
}
buffer.set(seed, 22);
return buffer.buffer;
}
default:
return undefined;
}
} catch (err) {
throw lazyDOMException(
'The operation failed for an operation-specific reason',
{ name: 'OperationError', cause: err });
}
}
function verifyAcceptableMlKemKeyUse(name, isPublic, usages) {
const checkSet = isPublic ? ['encapsulateKey', 'encapsulateBits'] : ['decapsulateKey', 'decapsulateBits'];
if (hasAnyNotIn(usages, checkSet)) {
throw lazyDOMException(
`Unsupported key usage for a ${name} key`,
'SyntaxError');
}
}
function createMlKemRawKey(name, keyData, isPublic) {
const handle = new KeyObjectHandle();
const keyType = isPublic ? kKeyTypePublic : kKeyTypePrivate;
if (!handle.initPqcRaw(name, keyData, keyType)) {
throw lazyDOMException('Invalid keyData', 'DataError');
}
return isPublic ? new PublicKeyObject(handle) : new PrivateKeyObject(handle);
}
function mlKemImportKey(
format,
keyData,
algorithm,
extractable,
keyUsages) {
const { name } = algorithm;
let keyObject;
const usagesSet = new SafeSet(keyUsages);
switch (format) {
case 'KeyObject': {
verifyAcceptableMlKemKeyUse(name, keyData.type === 'public', usagesSet);
keyObject = keyData;
break;
}
case 'spki': {
verifyAcceptableMlKemKeyUse(name, true, usagesSet);
try {
keyObject = createPublicKey({
key: keyData,
format: 'der',
type: 'spki',
});
} catch (err) {
throw lazyDOMException(
'Invalid keyData', { name: 'DataError', cause: err });
}
break;
}
case 'pkcs8': {
verifyAcceptableMlKemKeyUse(name, false, usagesSet);
try {
keyObject = createPrivateKey({
key: keyData,
format: 'der',
type: 'pkcs8',
});
} catch (err) {
throw lazyDOMException(
'Invalid keyData', { name: 'DataError', cause: err });
}
break;
}
case 'raw-public':
case 'raw-seed': {
const isPublic = format === 'raw-public';
verifyAcceptableMlKemKeyUse(name, isPublic, usagesSet);
try {
keyObject = createMlKemRawKey(name, keyData, isPublic);
} catch (err) {
throw lazyDOMException('Invalid keyData', { name: 'DataError', cause: err });
}
break;
}
default:
return undefined;
}
if (keyObject.asymmetricKeyType !== name.toLowerCase()) {
throw lazyDOMException('Invalid key type', 'DataError');
}
return new InternalCryptoKey(
keyObject,
{ name },
keyUsages,
extractable);
}
function mlKemEncapsulate(encapsulationKey) {
if (encapsulationKey[kKeyType] !== 'public') {
throw lazyDOMException(`Key must be a public key`, 'InvalidAccessError');
}
const { promise, resolve, reject } = PromiseWithResolvers();
const job = new KEMEncapsulateJob(
kCryptoJobAsync,
encapsulationKey[kKeyObject][kHandle],
undefined,
undefined,
undefined);
job.ondone = (error, result) => {
if (error) {
reject(lazyDOMException(
'The operation failed for an operation-specific reason',
{ name: 'OperationError', cause: error }));
} else {
const { 0: sharedKey, 1: ciphertext } = result;
resolve({ sharedKey: sharedKey.buffer, ciphertext: ciphertext.buffer });
}
};
job.run();
return promise;
}
function mlKemDecapsulate(decapsulationKey, ciphertext) {
if (decapsulationKey[kKeyType] !== 'private') {
throw lazyDOMException(`Key must be a private key`, 'InvalidAccessError');
}
const { promise, resolve, reject } = PromiseWithResolvers();
const job = new KEMDecapsulateJob(
kCryptoJobAsync,
decapsulationKey[kKeyObject][kHandle],
undefined,
undefined,
undefined,
ciphertext);
job.ondone = (error, result) => {
if (error) {
reject(lazyDOMException(
'The operation failed for an operation-specific reason',
{ name: 'OperationError', cause: error }));
} else {
resolve(result.buffer);
}
};
job.run();
return promise;
}
module.exports = {
mlKemExportKey,
mlKemImportKey,
mlKemEncapsulate,
mlKemDecapsulate,
mlKemGenerateKey,
};

View File

@ -36,6 +36,9 @@ const {
EVP_PKEY_ML_DSA_44,
EVP_PKEY_ML_DSA_65,
EVP_PKEY_ML_DSA_87,
EVP_PKEY_ML_KEM_512,
EVP_PKEY_ML_KEM_768,
EVP_PKEY_ML_KEM_1024,
} = internalBinding('crypto');
const { getOptionValue } = require('internal/options');
@ -276,6 +279,27 @@ const kAlgorithmDefinitions = {
'sign': 'ContextParams',
'verify': 'ContextParams',
},
'ML-KEM-512': {
'generateKey': null,
'exportKey': null,
'importKey': null,
'encapsulate': null,
'decapsulate': null,
},
'ML-KEM-768': {
'generateKey': null,
'exportKey': null,
'importKey': null,
'encapsulate': null,
'decapsulate': null,
},
'ML-KEM-1024': {
'generateKey': null,
'exportKey': null,
'importKey': null,
'encapsulate': null,
'decapsulate': null,
},
'PBKDF2': {
'importKey': null,
'deriveBits': 'Pbkdf2Params',
@ -336,6 +360,9 @@ const conditionalAlgorithms = {
'ML-DSA-44': !!EVP_PKEY_ML_DSA_44,
'ML-DSA-65': !!EVP_PKEY_ML_DSA_65,
'ML-DSA-87': !!EVP_PKEY_ML_DSA_87,
'ML-KEM-512': !!EVP_PKEY_ML_KEM_512,
'ML-KEM-768': !!EVP_PKEY_ML_KEM_768,
'ML-KEM-1024': !!EVP_PKEY_ML_KEM_1024,
'SHA3-256': !process.features.openssl_is_boringssl ||
ArrayPrototypeIncludes(getHashes(), 'sha3-256'),
'SHA3-384': !process.features.openssl_is_boringssl ||
@ -354,6 +381,9 @@ const experimentalAlgorithms = [
'ML-DSA-44',
'ML-DSA-65',
'ML-DSA-87',
'ML-KEM-512',
'ML-KEM-768',
'ML-KEM-1024',
'SHA3-256',
'SHA3-384',
'SHA3-512',

View File

@ -174,6 +174,15 @@ async function generateKey(
result = await require('internal/crypto/ml_dsa')
.mlDsaGenerateKey(algorithm, extractable, keyUsages);
break;
case 'ML-KEM-512':
// Fall through
case 'ML-KEM-768':
// Fall through
case 'ML-KEM-1024':
resultType = 'CryptoKeyPair';
result = await require('internal/crypto/ml_kem')
.mlKemGenerateKey(algorithm, extractable, keyUsages);
break;
default:
throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError');
}
@ -366,10 +375,16 @@ async function exportKeySpki(key) {
// Fall through
case 'ML-DSA-65':
// Fall through
case 'ML-DSA-87': {
case 'ML-DSA-87':
return require('internal/crypto/ml_dsa')
.mlDsaExportKey(key, kWebCryptoKeyFormatSPKI);
}
case 'ML-KEM-512':
// Fall through
case 'ML-KEM-768':
// Fall through
case 'ML-KEM-1024':
return require('internal/crypto/ml_kem')
.mlKemExportKey(key, kWebCryptoKeyFormatSPKI);
default:
return undefined;
}
@ -402,10 +417,16 @@ async function exportKeyPkcs8(key) {
// Fall through
case 'ML-DSA-65':
// Fall through
case 'ML-DSA-87': {
case 'ML-DSA-87':
return require('internal/crypto/ml_dsa')
.mlDsaExportKey(key, kWebCryptoKeyFormatPKCS8);
}
case 'ML-KEM-512':
// Fall through
case 'ML-KEM-768':
// Fall through
case 'ML-KEM-1024':
return require('internal/crypto/ml_kem')
.mlKemExportKey(key, kWebCryptoKeyFormatPKCS8);
default:
return undefined;
}
@ -439,6 +460,18 @@ async function exportKeyRawPublic(key, format) {
return require('internal/crypto/ml_dsa')
.mlDsaExportKey(key, kWebCryptoKeyFormatRaw);
}
case 'ML-KEM-512':
// Fall through
case 'ML-KEM-768':
// Fall through
case 'ML-KEM-1024': {
// ML-KEM keys don't recognize "raw"
if (format !== 'raw-public') {
return undefined;
}
return require('internal/crypto/ml_kem')
.mlKemExportKey(key, kWebCryptoKeyFormatRaw);
}
default:
return undefined;
}
@ -450,10 +483,16 @@ async function exportKeyRawSeed(key) {
// Fall through
case 'ML-DSA-65':
// Fall through
case 'ML-DSA-87': {
case 'ML-DSA-87':
return require('internal/crypto/ml_dsa')
.mlDsaExportKey(key, kWebCryptoKeyFormatRaw);
}
case 'ML-KEM-512':
// Fall through
case 'ML-KEM-768':
// Fall through
case 'ML-KEM-1024':
return require('internal/crypto/ml_kem')
.mlKemExportKey(key, kWebCryptoKeyFormatRaw);
default:
return undefined;
}
@ -746,6 +785,14 @@ async function importKey(
result = require('internal/crypto/ml_dsa')
.mlDsaImportKey(format, keyData, algorithm, extractable, keyUsages);
break;
case 'ML-KEM-512':
// Fall through
case 'ML-KEM-768':
// Fall through
case 'ML-KEM-1024':
result = require('internal/crypto/ml_kem')
.mlKemImportKey(format, keyData, algorithm, extractable, keyUsages);
break;
}
if (!result) {
@ -1099,6 +1146,229 @@ async function getPublicKey(key, keyUsages) {
return keyObject.toCryptoKey(key[kAlgorithm], true, keyUsages);
}
async function encapsulateBits(encapsulationAlgorithm, encapsulationKey) {
emitExperimentalWarning('The encapsulateBits Web Crypto API method');
if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto');
webidl ??= require('internal/crypto/webidl');
const prefix = "Failed to execute 'encapsulateBits' on 'SubtleCrypto'";
webidl.requiredArguments(arguments.length, 2, { prefix });
encapsulationAlgorithm = webidl.converters.AlgorithmIdentifier(encapsulationAlgorithm, {
prefix,
context: '1st argument',
});
encapsulationKey = webidl.converters.CryptoKey(encapsulationKey, {
prefix,
context: '2nd argument',
});
const normalizedEncapsulationAlgorithm = normalizeAlgorithm(encapsulationAlgorithm, 'encapsulate');
if (normalizedEncapsulationAlgorithm.name !== encapsulationKey[kAlgorithm].name) {
throw lazyDOMException(
'key algorithm mismatch',
'InvalidAccessError');
}
if (!ArrayPrototypeIncludes(encapsulationKey[kKeyUsages], 'encapsulateBits')) {
throw lazyDOMException(
'encapsulationKey does not have encapsulateBits usage',
'InvalidAccessError');
}
switch (encapsulationKey[kAlgorithm].name) {
case 'ML-KEM-512':
case 'ML-KEM-768':
case 'ML-KEM-1024':
return require('internal/crypto/ml_kem')
.mlKemEncapsulate(encapsulationKey);
}
throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError');
}
async function encapsulateKey(encapsulationAlgorithm, encapsulationKey, sharedKeyAlgorithm, extractable, usages) {
emitExperimentalWarning('The encapsulateKey Web Crypto API method');
if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto');
webidl ??= require('internal/crypto/webidl');
const prefix = "Failed to execute 'encapsulateKey' on 'SubtleCrypto'";
webidl.requiredArguments(arguments.length, 5, { prefix });
encapsulationAlgorithm = webidl.converters.AlgorithmIdentifier(encapsulationAlgorithm, {
prefix,
context: '1st argument',
});
encapsulationKey = webidl.converters.CryptoKey(encapsulationKey, {
prefix,
context: '2nd argument',
});
sharedKeyAlgorithm = webidl.converters.AlgorithmIdentifier(sharedKeyAlgorithm, {
prefix,
context: '3rd argument',
});
extractable = webidl.converters.boolean(extractable, {
prefix,
context: '4th argument',
});
usages = webidl.converters['sequence<KeyUsage>'](usages, {
prefix,
context: '5th argument',
});
const normalizedEncapsulationAlgorithm = normalizeAlgorithm(encapsulationAlgorithm, 'encapsulate');
const normalizedSharedKeyAlgorithm = normalizeAlgorithm(sharedKeyAlgorithm, 'importKey');
if (normalizedEncapsulationAlgorithm.name !== encapsulationKey[kAlgorithm].name) {
throw lazyDOMException(
'key algorithm mismatch',
'InvalidAccessError');
}
if (!ArrayPrototypeIncludes(encapsulationKey[kKeyUsages], 'encapsulateKey')) {
throw lazyDOMException(
'encapsulationKey does not have encapsulateKey usage',
'InvalidAccessError');
}
let encapsulateBits;
switch (encapsulationKey[kAlgorithm].name) {
case 'ML-KEM-512':
case 'ML-KEM-768':
case 'ML-KEM-1024':
encapsulateBits = await require('internal/crypto/ml_kem')
.mlKemEncapsulate(encapsulationKey);
break;
default:
throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError');
}
const sharedKey = await ReflectApply(
importKey,
this,
['raw-secret', encapsulateBits.sharedKey, normalizedSharedKeyAlgorithm, extractable, usages],
);
const encapsulatedKey = {
ciphertext: encapsulateBits.ciphertext,
sharedKey,
};
return encapsulatedKey;
}
async function decapsulateBits(decapsulationAlgorithm, decapsulationKey, ciphertext) {
emitExperimentalWarning('The decapsulateBits Web Crypto API method');
if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto');
webidl ??= require('internal/crypto/webidl');
const prefix = "Failed to execute 'decapsulateBits' on 'SubtleCrypto'";
webidl.requiredArguments(arguments.length, 3, { prefix });
decapsulationAlgorithm = webidl.converters.AlgorithmIdentifier(decapsulationAlgorithm, {
prefix,
context: '1st argument',
});
decapsulationKey = webidl.converters.CryptoKey(decapsulationKey, {
prefix,
context: '2nd argument',
});
ciphertext = webidl.converters.BufferSource(ciphertext, {
prefix,
context: '3rd argument',
});
const normalizedDecapsulationAlgorithm = normalizeAlgorithm(decapsulationAlgorithm, 'decapsulate');
if (normalizedDecapsulationAlgorithm.name !== decapsulationKey[kAlgorithm].name) {
throw lazyDOMException(
'key algorithm mismatch',
'InvalidAccessError');
}
if (!ArrayPrototypeIncludes(decapsulationKey[kKeyUsages], 'decapsulateBits')) {
throw lazyDOMException(
'decapsulationKey does not have decapsulateBits usage',
'InvalidAccessError');
}
switch (decapsulationKey[kAlgorithm].name) {
case 'ML-KEM-512':
case 'ML-KEM-768':
case 'ML-KEM-1024':
return require('internal/crypto/ml_kem')
.mlKemDecapsulate(decapsulationKey, ciphertext);
}
throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError');
}
async function decapsulateKey(
decapsulationAlgorithm, decapsulationKey, ciphertext, sharedKeyAlgorithm, extractable, usages,
) {
emitExperimentalWarning('The decapsulateKey Web Crypto API method');
if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto');
webidl ??= require('internal/crypto/webidl');
const prefix = "Failed to execute 'decapsulateKey' on 'SubtleCrypto'";
webidl.requiredArguments(arguments.length, 6, { prefix });
decapsulationAlgorithm = webidl.converters.AlgorithmIdentifier(decapsulationAlgorithm, {
prefix,
context: '1st argument',
});
decapsulationKey = webidl.converters.CryptoKey(decapsulationKey, {
prefix,
context: '2nd argument',
});
ciphertext = webidl.converters.BufferSource(ciphertext, {
prefix,
context: '3rd argument',
});
sharedKeyAlgorithm = webidl.converters.AlgorithmIdentifier(sharedKeyAlgorithm, {
prefix,
context: '4th argument',
});
extractable = webidl.converters.boolean(extractable, {
prefix,
context: '5th argument',
});
usages = webidl.converters['sequence<KeyUsage>'](usages, {
prefix,
context: '6th argument',
});
const normalizedDecapsulationAlgorithm = normalizeAlgorithm(decapsulationAlgorithm, 'decapsulate');
const normalizedSharedKeyAlgorithm = normalizeAlgorithm(sharedKeyAlgorithm, 'importKey');
if (normalizedDecapsulationAlgorithm.name !== decapsulationKey[kAlgorithm].name) {
throw lazyDOMException(
'key algorithm mismatch',
'InvalidAccessError');
}
if (!ArrayPrototypeIncludes(decapsulationKey[kKeyUsages], 'decapsulateKey')) {
throw lazyDOMException(
'decapsulationKey does not have decapsulateKey usage',
'InvalidAccessError');
}
let decapsulatedBits;
switch (decapsulationKey[kAlgorithm].name) {
case 'ML-KEM-512':
case 'ML-KEM-768':
case 'ML-KEM-1024':
decapsulatedBits = await require('internal/crypto/ml_kem')
.mlKemDecapsulate(decapsulationKey, ciphertext);
break;
default:
throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError');
}
return ReflectApply(
importKey,
this,
['raw-secret', decapsulatedBits, normalizedSharedKeyAlgorithm, extractable, usages],
);
}
// The SubtleCrypto and Crypto classes are defined as part of the
// Web Crypto API standard: https://www.w3.org/TR/WebCryptoAPI/
@ -1125,19 +1395,23 @@ class SubtleCrypto {
});
switch (operation) {
case 'encrypt':
case 'decapsulateBits':
case 'decapsulateKey':
case 'decrypt':
case 'sign':
case 'verify':
case 'digest':
case 'generateKey':
case 'deriveKey':
case 'deriveBits':
case 'importKey':
case 'deriveKey':
case 'digest':
case 'encapsulateBits':
case 'encapsulateKey':
case 'encrypt':
case 'exportKey':
case 'wrapKey':
case 'unwrapKey':
case 'generateKey':
case 'getPublicKey':
case 'importKey':
case 'sign':
case 'unwrapKey':
case 'verify':
case 'wrapKey':
break;
default:
return false;
@ -1208,6 +1482,42 @@ class SubtleCrypto {
default:
return false;
}
} else if (operation === 'encapsulateKey' || operation === 'decapsulateKey') {
additionalAlgorithm = webidl.converters.AlgorithmIdentifier(lengthOrAdditionalAlgorithm, {
prefix,
context: '3rd argument',
});
let normalizedAdditionalAlgorithm;
try {
normalizedAdditionalAlgorithm = normalizeAlgorithm(additionalAlgorithm, 'importKey');
} catch {
return false;
}
switch (normalizedAdditionalAlgorithm.name) {
case 'AES-OCB':
case 'AES-KW':
case 'AES-GCM':
case 'AES-CTR':
case 'AES-CBC':
case 'ChaCha20-Poly1305':
case 'HKDF':
case 'PBKDF2':
case 'Argon2i':
case 'Argon2d':
case 'Argon2id':
break;
case 'HMAC':
case 'KMAC128':
case 'KMAC256':
if (normalizedAdditionalAlgorithm.length === undefined || normalizedAdditionalAlgorithm.length === 256) {
break;
}
return false;
default:
return false;
}
}
return check(operation, algorithm, length);
@ -1215,6 +1525,14 @@ class SubtleCrypto {
}
function check(op, alg, length) {
if (op === 'encapsulateBits' || op === 'encapsulateKey') {
op = 'encapsulate';
}
if (op === 'decapsulateBits' || op === 'decapsulateKey') {
op = 'decapsulate';
}
let normalizedAlgorithm;
try {
normalizedAlgorithm = normalizeAlgorithm(alg, op);
@ -1231,15 +1549,17 @@ function check(op, alg, length) {
}
switch (op) {
case 'encrypt':
case 'decapsulate':
case 'decrypt':
case 'sign':
case 'verify':
case 'digest':
case 'importKey':
case 'encapsulate':
case 'encrypt':
case 'exportKey':
case 'wrapKey':
case 'importKey':
case 'sign':
case 'unwrapKey':
case 'verify':
case 'wrapKey':
return true;
case 'deriveBits': {
if (normalizedAlgorithm.name === 'HKDF') {
@ -1428,6 +1748,34 @@ ObjectDefineProperties(
writable: true,
value: getPublicKey,
},
encapsulateBits: {
__proto__: null,
enumerable: true,
configurable: true,
writable: true,
value: encapsulateBits,
},
encapsulateKey: {
__proto__: null,
enumerable: true,
configurable: true,
writable: true,
value: encapsulateKey,
},
decapsulateBits: {
__proto__: null,
enumerable: true,
configurable: true,
writable: true,
value: decapsulateBits,
},
decapsulateKey: {
__proto__: null,
enumerable: true,
configurable: true,
writable: true,
value: decapsulateKey,
},
});
module.exports = {

View File

@ -362,6 +362,10 @@ converters.KeyUsage = createEnumConverter('KeyUsage', [
'deriveBits',
'wrapKey',
'unwrapKey',
'encapsulateBits',
'decapsulateBits',
'encapsulateKey',
'decapsulateKey',
]);
converters['sequence<KeyUsage>'] = createSequenceConverter(converters.KeyUsage);

View File

@ -287,6 +287,12 @@ int GetNidFromName(const char* name) {
nid = EVP_PKEY_ML_DSA_65;
} else if (strcmp(name, "ML-DSA-87") == 0) {
nid = EVP_PKEY_ML_DSA_87;
} else if (strcmp(name, "ML-KEM-512") == 0) {
nid = EVP_PKEY_ML_KEM_512;
} else if (strcmp(name, "ML-KEM-768") == 0) {
nid = EVP_PKEY_ML_KEM_768;
} else if (strcmp(name, "ML-KEM-1024") == 0) {
nid = EVP_PKEY_ML_KEM_1024;
#endif
} else {
nid = NID_undef;
@ -621,7 +627,9 @@ Local<Function> KeyObjectHandle::Initialize(Environment* env) {
SetProtoMethod(isolate, templ, "initECRaw", InitECRaw);
SetProtoMethod(isolate, templ, "initEDRaw", InitEDRaw);
#if OPENSSL_WITH_PQC
SetProtoMethod(isolate, templ, "initMlDsaRaw", InitMlDsaRaw);
SetProtoMethod(isolate, templ, "initPqcRaw", InitPqcRaw);
SetProtoMethodNoSideEffect(isolate, templ, "rawPublicKey", RawPublicKey);
SetProtoMethodNoSideEffect(isolate, templ, "rawSeed", RawSeed);
#endif
SetProtoMethod(isolate, templ, "initJwk", InitJWK);
SetProtoMethod(isolate, templ, "keyDetail", GetKeyDetail);
@ -644,7 +652,9 @@ void KeyObjectHandle::RegisterExternalReferences(
registry->Register(InitECRaw);
registry->Register(InitEDRaw);
#if OPENSSL_WITH_PQC
registry->Register(InitMlDsaRaw);
registry->Register(InitPqcRaw);
registry->Register(RawPublicKey);
registry->Register(RawSeed);
#endif
registry->Register(InitJWK);
registry->Register(GetKeyDetail);
@ -839,7 +849,7 @@ void KeyObjectHandle::InitEDRaw(const FunctionCallbackInfo<Value>& args) {
}
#if OPENSSL_WITH_PQC
void KeyObjectHandle::InitMlDsaRaw(const FunctionCallbackInfo<Value>& args) {
void KeyObjectHandle::InitPqcRaw(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
KeyObjectHandle* key;
ASSIGN_OR_RETURN_UNWRAP(&key, args.This());
@ -862,7 +872,10 @@ void KeyObjectHandle::InitMlDsaRaw(const FunctionCallbackInfo<Value>& args) {
switch (id) {
case EVP_PKEY_ML_DSA_44:
case EVP_PKEY_ML_DSA_65:
case EVP_PKEY_ML_DSA_87: {
case EVP_PKEY_ML_DSA_87:
case EVP_PKEY_ML_KEM_512:
case EVP_PKEY_ML_KEM_768:
case EVP_PKEY_ML_KEM_1024: {
auto pkey = fn(id,
ncrypto::Buffer<const unsigned char>{
.data = key_data.data(),
@ -1083,6 +1096,50 @@ MaybeLocal<Value> KeyObjectHandle::ExportPrivateKey(
return WritePrivateKey(env(), data_.GetAsymmetricKey(), config);
}
#if OPENSSL_WITH_PQC
void KeyObjectHandle::RawPublicKey(
const v8::FunctionCallbackInfo<v8::Value>& args) {
Environment* env = Environment::GetCurrent(args);
KeyObjectHandle* key;
ASSIGN_OR_RETURN_UNWRAP(&key, args.This());
const KeyObjectData& data = key->Data();
CHECK_NE(data.GetKeyType(), kKeyTypeSecret);
Mutex::ScopedLock lock(data.mutex());
auto raw_data = data.GetAsymmetricKey().rawPublicKey();
if (!raw_data) {
return THROW_ERR_CRYPTO_OPERATION_FAILED(env,
"Failed to get raw public key");
}
args.GetReturnValue().Set(
Buffer::Copy(
env, reinterpret_cast<const char*>(raw_data.get()), raw_data.size())
.FromMaybe(Local<Value>()));
}
void KeyObjectHandle::RawSeed(const v8::FunctionCallbackInfo<v8::Value>& args) {
Environment* env = Environment::GetCurrent(args);
KeyObjectHandle* key;
ASSIGN_OR_RETURN_UNWRAP(&key, args.This());
const KeyObjectData& data = key->Data();
CHECK_EQ(data.GetKeyType(), kKeyTypePrivate);
Mutex::ScopedLock lock(data.mutex());
auto raw_data = data.GetAsymmetricKey().rawSeed();
if (!raw_data) {
return THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to get raw seed");
}
args.GetReturnValue().Set(
Buffer::Copy(
env, reinterpret_cast<const char*>(raw_data.get()), raw_data.size())
.FromMaybe(Local<Value>()));
}
#endif
void KeyObjectHandle::ExportJWK(
const v8::FunctionCallbackInfo<v8::Value>& args) {
Environment* env = Environment::GetCurrent(args);

View File

@ -152,9 +152,6 @@ class KeyObjectHandle : public BaseObject {
static void Init(const v8::FunctionCallbackInfo<v8::Value>& args);
static void InitECRaw(const v8::FunctionCallbackInfo<v8::Value>& args);
static void InitEDRaw(const v8::FunctionCallbackInfo<v8::Value>& args);
#if OPENSSL_WITH_PQC
static void InitMlDsaRaw(const v8::FunctionCallbackInfo<v8::Value>& args);
#endif
static void InitJWK(const v8::FunctionCallbackInfo<v8::Value>& args);
static void GetKeyDetail(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Equals(const v8::FunctionCallbackInfo<v8::Value>& args);
@ -173,6 +170,12 @@ class KeyObjectHandle : public BaseObject {
static void Export(const v8::FunctionCallbackInfo<v8::Value>& args);
#if OPENSSL_WITH_PQC
static void InitPqcRaw(const v8::FunctionCallbackInfo<v8::Value>& args);
static void RawPublicKey(const v8::FunctionCallbackInfo<v8::Value>& args);
static void RawSeed(const v8::FunctionCallbackInfo<v8::Value>& args);
#endif
v8::MaybeLocal<v8::Value> ExportSecretKey() const;
v8::MaybeLocal<v8::Value> ExportPublicKey(
const ncrypto::EVPKeyPointer::PublicKeyEncodingConfig& config) const;

49
test/fixtures/crypto/ml-kem.js vendored Normal file
View File

@ -0,0 +1,49 @@
'use strict';
const fixtures = require('../../common/fixtures');
function getKeyFileName(type, suffix) {
return `${type.replaceAll('-', '_')}_${suffix}.pem`;
}
module.exports = function() {
const pkcs8 = {
'ML-KEM-512': fixtures.readKey(getKeyFileName('ml-kem-512', 'private_seed_only'), 'ascii'),
'ML-KEM-768': fixtures.readKey(getKeyFileName('ml-kem-768', 'private_seed_only'), 'ascii'),
'ML-KEM-1024': fixtures.readKey(getKeyFileName('ml-kem-1024', 'private_seed_only'), 'ascii'),
}
const spki = {
'ML-KEM-512': fixtures.readKey(getKeyFileName('ml-kem-512', 'public'), 'ascii'),
'ML-KEM-768': fixtures.readKey(getKeyFileName('ml-kem-768', 'public'), 'ascii'),
'ML-KEM-1024': fixtures.readKey(getKeyFileName('ml-kem-1024', 'public'), 'ascii'),
}
/* eslint-disable @stylistic/js/max-len */
const results = {
'ML-KEM-512': {
ciphertext: Buffer.from('3b7ac92f6140c9fa0348f112ee8211ee668ce657b8bb6352a076880dd7ff4ca7c0babf635f1d36cab5106b8287504d572fab1d0fa3e086310564bd853bdd96cff460eeaa49be316eb95e47c477eeeec7276422abb44ce349016d80eb28f3519a64f1e1f7df63730a1ae83f6cd88af463fae7552103f1eff1097d2b80fa7539b10355136753d725dafd9311fca3d3b5353e2af9ac7f514c420dd7cf8a7e95eec82a37b39f29806db0c6469e884787054c8e48d1416e28fc5c809e42e0abe4547ba9f183f4ab3ce27da63d858b0a2c0970dce72674b5f2c62d6016fc9557b788685cb0b1c7ed3dbe5263463618edb729823f6afb349c3efd229bd03d94393880e48e9d5c80905702fd8bbaf19050dd67029f77df4d37a053fce22a41bde0a464f3940acada39d96a7e20dfef6f02d8317e40cbbe584fde7eeccd02db0b6e1b16c42eb2895afaafebeba0f628907af2a7de9da22afecf77ae1e3abd9593ed545736c766aafe7b223e3b8ab96084a8e14b81bdeff5b5d6c34a107f5108d99d76940535b0a0d3ffcd7e1085ef3e1c1fb54667af235d12f1b8a153faa5fc033676c81a4f39835c29a5ccc1ebb170c8f1717a9f0660ad353e5c18277e3c5e1c999431e51601f3d0a30b027b529489c83ab8da3e00faefc83c5df551c32446280db37940e347f283ac504b2f6a4ca3de69598dd5b73cbf64a2efac948f4793576ba59aafebff69cbfa53a9833d8a2af9b43bbb31c5c18248ddd3c869f7c4d701bd8050345c33bf87ae52cd97246930d84736723b777ab25d08233501a116ded57c28007944c7f3bfb469f0e7d54e643deb543014b6ce9ca4dce6f56a11af37ec0846bf8023efc5127af080e2ecf1c568626cd19842386f3f722d11f153c0167951692dab5468e5f8bc978fc21cca1fdbb6e1d2b27440bb132014da61536ec9c0175d4a7f6ab07bb57004b929a9efb452c718fbf754afb5433df64ae6b95850a1cdbe1e9d743b216f9e1465e488c3fe813a342138134ff73323210eea5950019b35940f8ab340a42054e4143dad43ebc4e9a363e610b5bef73f2de70642e3aa4a631f07b4', 'hex'),
sharedKey: Buffer.from('f6d03353f51c73a55611f307f15eaa2fc8527213667370df2ccab580a0b50da3', 'hex'),
},
'ML-KEM-768': {
ciphertext: Buffer.from('90a9c1f4d9a9ab9b90acd011a9b881c18e692e2019d452c11d81300259d3f2d1df10ea4d352574a4e7f84ba78779631d927d8172dea8118e3a9b16efc28d92f15e75a2a223f68155ad39a8f94e95eeda8e5fe14257f7e3158bb8f12927d48e616318569b8cedaa9d71e5a6c883e6344e2ae5cf55ef0cc484b02ecb1dffcb8146971d6bf043a6b772b2a8aeaf6c2549c7695d389e3e6e106daa4b40a4cb429aff76f04e2524b635d4f4e7e820381da770c9b7b53e37cfb84f3744ca8a37a2e150104b3fe82f2c93d5fbbf77b55ccf2c9ea209cd3a950c3e986ee109d10cb492b1e6e1d39672d4c56ce986fb29c8687c5f8efa8be5cb96d0e11162976f6bd04404cad93598187764b38acde20674ea797e1df86869de406cfb863c7d95c94aa419c32f78caae4c86114906edf4bcc41331fac7ff92c2c695452952b7abf7f726f46d22653ce9e77eedf08696ebcd29b6f9a129f8c66ebf6d41c5066f652d79f1cc0b1d3098e20e34917308b1c3a62a10040bf5d6b761fe78be0502f05f5f11a3c443458d185820e132edbe7effb8247d4e397c832d073b822a611b26830b3d3225ee9c56d00348ed234b71bda05c39f1743573b7252081acc36cdb5966349da9faebb1cc269f9584c6cedc412bdb53e70e27cd1dada840fb5e972a0004edbf58b4828e434b7f1fc87814bbe77a8b36bf42fc8d47a5a09dfc1f1e551e47114fdb688dd3c7dd85ae582f169720e7df0b96896e96df39881e83d7e58f5b55c7ecc2fdd0e20e384942db0bbd10edf28c76a84d29607b394e774b7f528874722c57088fe6ffe41428f6c3618578031632e99b7bb9145f035eadb3e621c39b2a4eae17d4fdfb12eaef29e38d915474866911e6b0a963e25b16cf5b0baab3dd07f46dd5e8bd07e5f8b20c346200b3eba57d01d7e365c9309c73ea8c0a1aecb154bf810e08612220bd03733f7750ff291c07b8c7b8dfc3cb889dcbe268832e80e9637d32a38dfbcccc8600f3f1356718708b7d593a90988f584755e16738b2ca0465a2c5920e7be7f180db854a7b59fedbd5252e435e04f5d800b603e04a844a073ead6c43ffd7aa85633fe54be40e371fdcc19e9596292e3745b5bd5a1bc92948489a558c9089c17ff9c05c93dffbc28ac8f4fbd53a54f2017e68db6609d7a7a20f7ab858ce6a204d1f43aeb4599e66d9e0dea307d253b7882da6a6d18dd87a285c6c1ed31c88193e1a103ee175380a89ca8f0fb91fad3e383c88bcf42c966c36c676a7cad83844360ee4ca4c391569aebf2a0df09274116ee03fdfeb6ef4307928e7aae14b311d450fa2b073afac336a85ee3b1f734534feb75b8eec929175fdaa2a7b96dea45b14f897f15b8085dc126ea49df13edb4cbbbd10aeeb688de0e2809faf3e912a4aadce23df8655326079701f035b6acbb8b22fb2b25649b77249eb9c829405aaedcf470f07f7b3ba5c633c202265018177d2991a5d39a23652fb9fa5cc9f9b6394c788c2a529d706f14ef1c7592066aae6b2369c70622bdf345c5692a8f4', 'hex'),
sharedKey: Buffer.from('e9888adf3af812be74eb1b52a49d37ec1ca0e06c04d47d8ceb05f64f0c979b5f', 'hex'),
},
'ML-KEM-1024': {
ciphertext: Buffer.from('3ea6d152dd8883f588a33bc88ccbcf7db6e2ee0278b368eb55c81f5e11409509ce0be7932bf60e7b8c463f6f7763fdc11094480dbb94d5cdbb8eb08445f5404f4b37eb1b7a57533269e32c5dc1c673d4b45ee61e7cac186e4b699615ab92591225a2e4ff419aa3e57488a92f75af6945134f3d77e84dcc38bbc996bbb524e1c2afab4e3bc4812de74309ee4a0011299b5097c7bf468024e34552b3be56a63dec078e0083474a31347f7ebabcbcb261c5c2a8e2b14c61c622016e117b6dd8fb7008d64cee91085c4f3a7a90fc6a76af1214e265ca75bb218d13f9e7142fcb5cbc35f3afdb855eb14ab53738ed6e670473e5480f949b59db466affe2b95c002bdc31e901c9ada3bb969e71e1ca95d816d9adaa7fb9e696b7549ede59defce525356b3bc38afe854e5df16771f7a40bcf0c0eebbb051760ab102a18ecc537d44e4cce2a1827d2c863c3b4341dedc7cdb4881bfa5a228ddf21c615d5c29e9404a08aaa61481fff2665c0b057264a65ab355bfc0c407f36546aa69e0e71563bb9ffea45fdc40a6c91a5b1c58901b8b72ca85f39ae159638bf7cf2cd3c9cf344333ac3caa8ba5b900f3c3fac4cfffcd6767f1f347f0de4d0414d18e70e06beb7293e14e3ae49b01a7a75c6c3bb2458b37c68dc583e0742e80fdb2136c01b47173c33759d13753bd9a75d853422006ee05429a644c62e932343f7bab875b635db94af63f9b64a1b44eacf5b8fcc663f4f54078cfe80438156696a5985548e28ea512ac12c267c9b38c73139a91f5ad3034a7edd76ce5e794cdd2775aaeb90c6b991764874a0729aeb66033db5c22c7de93da7e3f068a16b98a0f0a33968f9778ebde1c0d60fb756c150d17220829b41bf393fd96c58336d6ec73844debee343acad5fdd43fd69bacb11200413dd98d3fc72416a20270081dad8f72ab909c2bb2b5aa4f0fb40d0fd738fde65dcd1acb580813f721048114d32ba1b90b514b54a5be56bd914eecf36265bfc94939e2fd86b71344ffc086258667b7c07c915020e5ac93f1e308377361b3cde1e9f1d9b879e43f7f74d4a565db2f9e6a920f2137568651f68d112be4cb13890e9ca88370df569199624bb0658d9b36729d3fd7a55a7a9c52f8c6c261e7cbb54436c8ab9f3a816ac9456e9ce426012defcb7f23a2ac1178e9cf006ba36bb71d091582b8aac3922dd5bdc5badb6682b1e907bf405d5d8ae3e0418e8b2764266437261b80489186cd88d12f8587782b6edd88c5f063be492d5c3c72cd699d36558415c3fc53ef43ded7916ba91018381e4f4ac504098b5ea3935d6eaf458e3032ee1c263d603ab1b6198738874b8c8c8a463053e8b11b381d48117526031f8f5d97501edbae61b8a461f53004323fe55329513720255f5f65c2f450bcb389ff6af8719388e6c2e6d835b59187de474c27ddd8720e51d8a04c565e1537dc2b95f591e0984303e4c8ffc57233cf4943478835b7b1e84407561e8608719a2c26a81c5d8be381ef4e127d3cbae07c4a4b254dee58f17a18f5d09cdf8d3f9bec22fa0b862993d0254e288000699b2cf86e55c4def365c1ac4481e1db3e3e77c9facc0322689f7064d35fbad128cedd83431b97a9463c0168f8eba1c1262f2aaf7ba01f2a2d25f236b0c10646e66b619bcc1d5b2e2f25ffe59ec87ba15878049cb3c2fa707e608229cf2a558e2e770bef5ce3e716c133cc31b60efac524ed8191f4e8eafe9d7f4760e1fa136dc67d9a34a383387b69d60eb81da221cbb13466e9cd89bbfcb4baf1eed89cd9675a3deefa1fa63bcd70ebe138aa07fcebb3a271683508994fa2fd593e6a98b124372b324610ef1f0dc0ec03dca70f15c9cabae4909be7b0e3f844d7023a58194a1745b6ec3e43d05dce405fab650447fc786e53ae57d65e516985946e6d69a090a0f2351f926aa8e5ecb034843135cf15ae2e3f7336158e0ae5afcd7a8f3f787f3cf2fada0253137b69b93df6e43125e388e76327bde676b0e1827e02c31a5c01f63d8110986d8f5e3d1d6038699cb83d9ab3e05460a59095a011f454bf15a8e5718fe57afe2753443712af3b8d6b967a6fb90e5a5f83b63a67b96df1472fac3930dba1a317882728c14e51ff9437c1a878755ed6a6ccfc5e0631c71062a37390b5de369d8363ec768499f64854d4ddcaecdae560cecb0a4798440c71c9303549a908d10cab296d53aa1978b205277cae0', 'hex'),
sharedKey: Buffer.from('345cee699a756befaf05c60a35591c6df4f91a97a004356dd823fa4053276405', 'hex'),
},
};
/* eslint-enable @stylistic/js/max-len */
const algorithms = ['ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024'];
const vectors = algorithms.map((algorithm) => ({
publicKeyPem: spki[algorithm],
privateKeyPem: pkcs8[algorithm],
name: algorithm,
results: results[algorithm],
}));
return vectors;
}

View File

@ -31,18 +31,27 @@ export const vectors = {
[pqc, 'ML-DSA-44'],
[pqc, 'ML-DSA-65'],
[pqc, 'ML-DSA-87'],
[pqc, 'ML-KEM-512'],
[pqc, 'ML-KEM-768'],
[pqc, 'ML-KEM-1024'],
[chacha, 'ChaCha20-Poly1305'],
],
'importKey': [
[pqc, 'ML-DSA-44'],
[pqc, 'ML-DSA-65'],
[pqc, 'ML-DSA-87'],
[pqc, 'ML-KEM-512'],
[pqc, 'ML-KEM-768'],
[pqc, 'ML-KEM-1024'],
[chacha, 'ChaCha20-Poly1305'],
],
'exportKey': [
[pqc, 'ML-DSA-44'],
[pqc, 'ML-DSA-65'],
[pqc, 'ML-DSA-87'],
[pqc, 'ML-KEM-512'],
[pqc, 'ML-KEM-768'],
[pqc, 'ML-KEM-1024'],
[chacha, 'ChaCha20-Poly1305'],
],
'getPublicKey': [
@ -56,6 +65,11 @@ export const vectors = {
[true, 'ECDH'],
[true, 'ECDSA'],
[pqc, 'ML-DSA-44'],
[pqc, 'ML-DSA-65'],
[pqc, 'ML-DSA-87'],
[pqc, 'ML-KEM-512'],
[pqc, 'ML-KEM-768'],
[pqc, 'ML-KEM-1024'],
[false, 'AES-CTR'],
[false, 'AES-CBC'],
[false, 'AES-GCM'],
@ -68,5 +82,39 @@ export const vectors = {
[chacha, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(12), tagLength: 128 }],
[false, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(12), tagLength: 64 }],
[false, 'ChaCha20-Poly1305'],
]
],
'encapsulateBits': [
[pqc, 'ML-KEM-512'],
[pqc, 'ML-KEM-768'],
[pqc, 'ML-KEM-1024'],
],
'encapsulateKey': [
[pqc, 'ML-KEM-512', 'AES-KW'],
[pqc, 'ML-KEM-512', 'AES-GCM'],
[pqc, 'ML-KEM-512', 'AES-CTR'],
[pqc, 'ML-KEM-512', 'AES-CBC'],
[pqc, 'ML-KEM-512', 'ChaCha20-Poly1305'],
[pqc, 'ML-KEM-512', 'HKDF'],
[pqc, 'ML-KEM-512', 'PBKDF2'],
[pqc, 'ML-KEM-512', { name: 'HMAC', hash: 'SHA-256' }],
[pqc, 'ML-KEM-512', { name: 'HMAC', hash: 'SHA-256', length: 256 }],
[false, 'ML-KEM-512', { name: 'HMAC', hash: 'SHA-256', length: 128 }],
],
'decapsulateBits': [
[pqc, 'ML-KEM-512'],
[pqc, 'ML-KEM-768'],
[pqc, 'ML-KEM-1024'],
],
'decapsulateKey': [
[pqc, 'ML-KEM-512', 'AES-KW'],
[pqc, 'ML-KEM-512', 'AES-GCM'],
[pqc, 'ML-KEM-512', 'AES-CTR'],
[pqc, 'ML-KEM-512', 'AES-CBC'],
[pqc, 'ML-KEM-512', 'ChaCha20-Poly1305'],
[pqc, 'ML-KEM-512', 'HKDF'],
[pqc, 'ML-KEM-512', 'PBKDF2'],
[pqc, 'ML-KEM-512', { name: 'HMAC', hash: 'SHA-256' }],
[pqc, 'ML-KEM-512', { name: 'HMAC', hash: 'SHA-256', length: 256 }],
[false, 'ML-KEM-512', { name: 'HMAC', hash: 'SHA-256', length: 128 }],
],
};

View File

@ -0,0 +1,264 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const { hasOpenSSL } = require('../common/crypto');
if (!hasOpenSSL(3, 5))
common.skip('requires OpenSSL >= 3.5');
const assert = require('assert');
const crypto = require('crypto');
const { KeyObject } = crypto;
const { subtle } = globalThis.crypto;
const vectors = require('../fixtures/crypto/ml-kem')();
async function testEncapsulateKey({ name, publicKeyPem, privateKeyPem, results }) {
const [
publicKey,
noEncapsulatePublicKey,
privateKey,
] = await Promise.all([
crypto.createPublicKey(publicKeyPem)
.toCryptoKey(name, false, ['encapsulateKey']),
crypto.createPublicKey(publicKeyPem)
.toCryptoKey(name, false, ['encapsulateBits']),
crypto.createPrivateKey(privateKeyPem)
.toCryptoKey(name, false, ['decapsulateKey']),
]);
// Test successful encapsulation
const encapsulated = await subtle.encapsulateKey(
{ name },
publicKey,
'HKDF',
false,
['deriveBits']
);
assert(encapsulated.sharedKey instanceof CryptoKey);
assert(encapsulated.ciphertext instanceof ArrayBuffer);
assert.strictEqual(encapsulated.sharedKey.type, 'secret');
assert.strictEqual(encapsulated.sharedKey.algorithm.name, 'HKDF');
assert.strictEqual(encapsulated.sharedKey.extractable, false);
assert.deepStrictEqual(encapsulated.sharedKey.usages, ['deriveBits']);
// Verify ciphertext length matches expected for algorithm
assert.strictEqual(encapsulated.ciphertext.byteLength, results.ciphertext.byteLength);
// Test with different shared key algorithm
const encapsulated2 = await subtle.encapsulateKey(
{ name },
publicKey,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign', 'verify']
);
assert(encapsulated2.sharedKey instanceof CryptoKey);
assert.strictEqual(encapsulated2.sharedKey.algorithm.name, 'HMAC');
assert.strictEqual(encapsulated2.sharedKey.extractable, false);
// Test failure when using wrong key type
await assert.rejects(
subtle.encapsulateKey({ name }, privateKey, 'HKDF', false, ['deriveBits']), {
name: 'InvalidAccessError',
});
// Test failure when using key without proper usage
await assert.rejects(
subtle.encapsulateKey({ name }, noEncapsulatePublicKey, 'HKDF', false, ['deriveBits']), {
name: 'InvalidAccessError',
});
}
async function testEncapsulateBits({ name, publicKeyPem, privateKeyPem, results }) {
const [
publicKey,
noEncapsulatePublicKey,
privateKey,
] = await Promise.all([
crypto.createPublicKey(publicKeyPem)
.toCryptoKey(name, false, ['encapsulateBits']),
crypto.createPublicKey(publicKeyPem)
.toCryptoKey(name, false, ['encapsulateKey']),
crypto.createPrivateKey(privateKeyPem)
.toCryptoKey(name, false, ['decapsulateBits']),
]);
// Test successful encapsulation
const encapsulated = await subtle.encapsulateBits({ name }, publicKey);
assert(encapsulated.sharedKey instanceof ArrayBuffer);
assert(encapsulated.ciphertext instanceof ArrayBuffer);
assert.strictEqual(encapsulated.sharedKey.byteLength, 32); // ML-KEM shared secret is 32 bytes
// Verify ciphertext length matches expected for algorithm
assert.strictEqual(encapsulated.ciphertext.byteLength, results.ciphertext.byteLength);
// Test failure when using wrong key type
await assert.rejects(
subtle.encapsulateBits({ name }, privateKey), {
name: 'InvalidAccessError',
});
// Test failure when using key without proper usage
await assert.rejects(
subtle.encapsulateBits({ name }, noEncapsulatePublicKey), {
name: 'InvalidAccessError',
});
}
async function testDecapsulateKey({ name, publicKeyPem, privateKeyPem, results }) {
const [
publicKey,
privateKey,
noDecapsulatePrivateKey,
] = await Promise.all([
crypto.createPublicKey(publicKeyPem)
.toCryptoKey(name, false, ['encapsulateKey']),
crypto.createPrivateKey(privateKeyPem)
.toCryptoKey(name, false, ['decapsulateKey']),
crypto.createPrivateKey(privateKeyPem)
.toCryptoKey(name, false, ['decapsulateBits']),
]);
// Test successful round-trip: encapsulate then decapsulate
const encapsulated = await subtle.encapsulateKey(
{ name },
publicKey,
'HKDF',
false,
['deriveBits']
);
const decapsulatedKey = await subtle.decapsulateKey(
{ name },
privateKey,
encapsulated.ciphertext,
'HKDF',
false,
['deriveBits']
);
assert(decapsulatedKey instanceof CryptoKey);
assert.strictEqual(decapsulatedKey.type, 'secret');
assert.strictEqual(decapsulatedKey.algorithm.name, 'HKDF');
assert.strictEqual(decapsulatedKey.extractable, false);
assert.deepStrictEqual(decapsulatedKey.usages, ['deriveBits']);
// Verify the keys are the same by using KeyObject.from() and comparing
const originalKeyData = KeyObject.from(encapsulated.sharedKey).export();
const decapsulatedKeyData = KeyObject.from(decapsulatedKey).export();
assert(originalKeyData.equals(decapsulatedKeyData));
// Test with test vector ciphertext and expected shared key
const vectorDecapsulatedKey = await subtle.decapsulateKey(
{ name },
privateKey,
results.ciphertext,
'HKDF',
false,
['deriveBits']
);
const vectorKeyData = KeyObject.from(vectorDecapsulatedKey).export();
assert(vectorKeyData.equals(results.sharedKey));
// Test failure when using wrong key type
await assert.rejects(
subtle.decapsulateKey({ name }, publicKey, encapsulated.ciphertext,
'HKDF', false, ['deriveKey']), {
name: 'InvalidAccessError'
});
// Test failure when using key without proper usage
await assert.rejects(
subtle.decapsulateKey({ name }, noDecapsulatePrivateKey, encapsulated.ciphertext,
'HKDF', false, ['deriveKey']), {
name: 'InvalidAccessError'
});
// Test failure with wrong ciphertext length
const wrongLengthCiphertext = new Uint8Array(encapsulated.ciphertext.byteLength - 1);
await assert.rejects(
subtle.decapsulateKey({ name }, privateKey, wrongLengthCiphertext,
'HKDF', false, ['deriveKey']), {
name: 'OperationError',
});
}
async function testDecapsulateBits({ name, publicKeyPem, privateKeyPem, results }) {
const [
publicKey,
privateKey,
noDecapsulatePrivateKey,
] = await Promise.all([
crypto.createPublicKey(publicKeyPem)
.toCryptoKey(name, false, ['encapsulateBits']),
crypto.createPrivateKey(privateKeyPem)
.toCryptoKey(name, false, ['decapsulateBits']),
crypto.createPrivateKey(privateKeyPem)
.toCryptoKey(name, false, ['decapsulateKey']),
]);
// Test successful round-trip: encapsulate then decapsulate
const encapsulated = await subtle.encapsulateBits({ name }, publicKey);
const decapsulatedBits = await subtle.decapsulateBits(
{ name },
privateKey,
encapsulated.ciphertext
);
assert(decapsulatedBits instanceof ArrayBuffer);
assert.strictEqual(decapsulatedBits.byteLength, 32); // ML-KEM shared secret is 32 bytes
// Verify the shared secrets are the same
assert(Buffer.from(encapsulated.sharedKey).equals(Buffer.from(decapsulatedBits)));
// Test with test vector ciphertext and expected shared key
const vectorDecapsulatedBits = await subtle.decapsulateBits(
{ name },
privateKey,
results.ciphertext
);
assert(Buffer.from(vectorDecapsulatedBits).equals(results.sharedKey));
// Test failure when using wrong key type
await assert.rejects(
subtle.decapsulateBits({ name }, publicKey, encapsulated.ciphertext), {
name: 'InvalidAccessError'
});
// Test failure when using key without proper usage
await assert.rejects(
subtle.decapsulateBits({ name }, noDecapsulatePrivateKey, encapsulated.ciphertext), {
name: 'InvalidAccessError'
});
// Test failure with wrong ciphertext length
const wrongLengthCiphertext = new Uint8Array(encapsulated.ciphertext.byteLength - 1);
await assert.rejects(
subtle.decapsulateBits({ name }, privateKey, wrongLengthCiphertext), {
name: 'OperationError',
});
}
(async function() {
const variations = [];
vectors.forEach((vector) => {
variations.push(testEncapsulateKey(vector));
variations.push(testEncapsulateBits(vector));
variations.push(testDecapsulateKey(vector));
variations.push(testDecapsulateBits(vector));
});
await Promise.all(variations);
})().then(common.mustCall());

View File

@ -213,7 +213,7 @@ async function testImportPkcs8PrivOnly({ name, privateUsages }, extractable) {
await assert.rejects(subtle.exportKey('pkcs8', key), (err) => {
assert.strictEqual(err.name, 'OperationError');
assert.strictEqual(err.cause.code, 'ERR_CRYPTO_OPERATION_FAILED');
assert.strictEqual(err.cause.message, 'key does not have an available seed');
assert.strictEqual(err.cause.message, 'Failed to get raw seed');
return true;
});
} else {

View File

@ -0,0 +1,315 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const { hasOpenSSL } = require('../common/crypto');
if (!hasOpenSSL(3, 5))
common.skip('requires OpenSSL >= 3.5');
const assert = require('assert');
const { subtle } = globalThis.crypto;
const fixtures = require('../common/fixtures');
function getKeyFileName(type, suffix) {
return `${type.replaceAll('-', '_')}_${suffix}.pem`;
}
function toDer(pem) {
const der = pem.replace(/(?:-----(?:BEGIN|END) (?:PRIVATE|PUBLIC) KEY-----|\s)/g, '');
return Buffer.alloc(Buffer.byteLength(der, 'base64'), der, 'base64');
}
const keyData = {
'ML-KEM-512': {
pkcs8_seed_only: toDer(fixtures.readKey(getKeyFileName('ml-kem-512', 'private_seed_only'), 'ascii')),
pkcs8: toDer(fixtures.readKey(getKeyFileName('ml-kem-512', 'private'), 'ascii')),
pkcs8_priv_only: toDer(fixtures.readKey(getKeyFileName('ml-kem-512', 'private_priv_only'), 'ascii')),
spki: toDer(fixtures.readKey(getKeyFileName('ml-kem-512', 'public'), 'ascii')),
pub_len: 800,
},
'ML-KEM-768': {
pkcs8_seed_only: toDer(fixtures.readKey(getKeyFileName('ml-kem-768', 'private_seed_only'), 'ascii')),
pkcs8: toDer(fixtures.readKey(getKeyFileName('ml-kem-768', 'private'), 'ascii')),
pkcs8_priv_only: toDer(fixtures.readKey(getKeyFileName('ml-kem-768', 'private_priv_only'), 'ascii')),
spki: toDer(fixtures.readKey(getKeyFileName('ml-kem-768', 'public'), 'ascii')),
pub_len: 1184,
},
'ML-KEM-1024': {
pkcs8_seed_only: toDer(fixtures.readKey(getKeyFileName('ml-kem-1024', 'private_seed_only'), 'ascii')),
pkcs8: toDer(fixtures.readKey(getKeyFileName('ml-kem-1024', 'private'), 'ascii')),
pkcs8_priv_only: toDer(fixtures.readKey(getKeyFileName('ml-kem-1024', 'private_priv_only'), 'ascii')),
spki: toDer(fixtures.readKey(getKeyFileName('ml-kem-1024', 'public'), 'ascii')),
pub_len: 1568,
},
};
const testVectors = [
{
name: 'ML-KEM-512',
privateUsages: ['decapsulateKey', 'decapsulateBits'],
publicUsages: ['encapsulateKey', 'encapsulateBits']
},
{
name: 'ML-KEM-768',
privateUsages: ['decapsulateKey', 'decapsulateBits'],
publicUsages: ['encapsulateKey', 'encapsulateBits']
},
{
name: 'ML-KEM-1024',
privateUsages: ['decapsulateKey', 'decapsulateBits'],
publicUsages: ['encapsulateKey', 'encapsulateBits']
},
];
async function testImportSpki({ name, publicUsages }, extractable) {
const key = await subtle.importKey(
'spki',
keyData[name].spki,
{ name },
extractable,
publicUsages);
assert.strictEqual(key.type, 'public');
assert.strictEqual(key.extractable, extractable);
assert.deepStrictEqual(key.usages, publicUsages);
assert.deepStrictEqual(key.algorithm.name, name);
assert.strictEqual(key.algorithm, key.algorithm);
assert.strictEqual(key.usages, key.usages);
if (extractable) {
// Test the roundtrip
const spki = await subtle.exportKey('spki', key);
assert.strictEqual(
Buffer.from(spki).toString('hex'),
keyData[name].spki.toString('hex'));
} else {
await assert.rejects(
subtle.exportKey('spki', key), {
message: /key is not extractable/
});
}
// Bad usage
await assert.rejects(
subtle.importKey(
'spki',
keyData[name].spki,
{ name },
extractable,
['wrapKey']),
{ message: /Unsupported key usage/ });
}
async function testImportPkcs8({ name, privateUsages }, extractable) {
const key = await subtle.importKey(
'pkcs8',
keyData[name].pkcs8,
{ name },
extractable,
privateUsages);
assert.strictEqual(key.type, 'private');
assert.strictEqual(key.extractable, extractable);
assert.deepStrictEqual(key.usages, privateUsages);
assert.deepStrictEqual(key.algorithm.name, name);
assert.strictEqual(key.algorithm, key.algorithm);
assert.strictEqual(key.usages, key.usages);
if (extractable) {
// Test the roundtrip
const pkcs8 = await subtle.exportKey('pkcs8', key);
assert.strictEqual(
Buffer.from(pkcs8).toString('hex'),
keyData[name].pkcs8_seed_only.toString('hex'));
} else {
await assert.rejects(
subtle.exportKey('pkcs8', key), {
message: /key is not extractable/
});
}
await assert.rejects(
subtle.importKey(
'pkcs8',
keyData[name].pkcs8,
{ name },
extractable,
[/* empty usages */]),
{ name: 'SyntaxError', message: 'Usages cannot be empty when importing a private key.' });
}
async function testImportPkcs8SeedOnly({ name, privateUsages }, extractable) {
const key = await subtle.importKey(
'pkcs8',
keyData[name].pkcs8_seed_only,
{ name },
extractable,
privateUsages);
assert.strictEqual(key.type, 'private');
assert.strictEqual(key.extractable, extractable);
assert.deepStrictEqual(key.usages, privateUsages);
assert.deepStrictEqual(key.algorithm.name, name);
assert.strictEqual(key.algorithm, key.algorithm);
assert.strictEqual(key.usages, key.usages);
if (extractable) {
// Test the roundtrip
const pkcs8 = await subtle.exportKey('pkcs8', key);
assert.strictEqual(
Buffer.from(pkcs8).toString('hex'),
keyData[name].pkcs8_seed_only.toString('hex'));
} else {
await assert.rejects(
subtle.exportKey('pkcs8', key), {
message: /key is not extractable/
});
}
await assert.rejects(
subtle.importKey(
'pkcs8',
keyData[name].pkcs8_seed_only,
{ name },
extractable,
[/* empty usages */]),
{ name: 'SyntaxError', message: 'Usages cannot be empty when importing a private key.' });
}
async function testImportPkcs8PrivOnly({ name, privateUsages }, extractable) {
const key = await subtle.importKey(
'pkcs8',
keyData[name].pkcs8_priv_only,
{ name },
extractable,
privateUsages);
assert.strictEqual(key.type, 'private');
assert.strictEqual(key.extractable, extractable);
assert.deepStrictEqual(key.usages, privateUsages);
assert.deepStrictEqual(key.algorithm.name, name);
assert.strictEqual(key.algorithm, key.algorithm);
assert.strictEqual(key.usages, key.usages);
if (extractable) {
await assert.rejects(subtle.exportKey('pkcs8', key), (err) => {
assert.strictEqual(err.name, 'OperationError');
assert.strictEqual(err.cause.code, 'ERR_CRYPTO_OPERATION_FAILED');
assert.strictEqual(err.cause.message, 'Failed to get raw seed');
return true;
});
} else {
await assert.rejects(
subtle.exportKey('pkcs8', key), {
message: /key is not extractable/
});
}
await assert.rejects(
subtle.importKey(
'pkcs8',
keyData[name].pkcs8_seed_only,
{ name },
extractable,
[/* empty usages */]),
{ name: 'SyntaxError', message: 'Usages cannot be empty when importing a private key.' });
}
async function testImportRawPublic({ name, publicUsages }, extractable) {
const pub = keyData[name].spki.subarray(-keyData[name].pub_len);
const publicKey = await subtle.importKey(
'raw-public',
pub,
{ name },
extractable, publicUsages);
assert.strictEqual(publicKey.type, 'public');
assert.deepStrictEqual(publicKey.usages, publicUsages);
assert.strictEqual(publicKey.algorithm.name, name);
assert.strictEqual(publicKey.algorithm, publicKey.algorithm);
assert.strictEqual(publicKey.usages, publicKey.usages);
assert.strictEqual(publicKey.extractable, extractable);
if (extractable) {
const value = await subtle.exportKey('raw-public', publicKey);
assert.deepStrictEqual(Buffer.from(value), pub);
await assert.rejects(subtle.exportKey('raw', publicKey), {
name: 'NotSupportedError',
message: `Unable to export ${publicKey.algorithm.name} public key using raw format`,
});
}
await assert.rejects(
subtle.importKey(
'raw-public',
pub.subarray(0, pub.byteLength - 1),
{ name },
extractable, publicUsages),
{ message: 'Invalid keyData' });
await assert.rejects(
subtle.importKey(
'raw-public',
pub,
{ name: name === 'ML-KEM-512' ? 'ML-KEM-768' : 'ML-KEM-512' },
extractable, publicUsages),
{ message: 'Invalid keyData' });
}
async function testImportRawSeed({ name, privateUsages }, extractable) {
const seed = keyData[name].pkcs8_seed_only.subarray(-64);
const privateKey = await subtle.importKey(
'raw-seed',
seed,
{ name },
extractable, privateUsages);
assert.strictEqual(privateKey.type, 'private');
assert.deepStrictEqual(privateKey.usages, privateUsages);
assert.strictEqual(privateKey.algorithm.name, name);
assert.strictEqual(privateKey.algorithm, privateKey.algorithm);
assert.strictEqual(privateKey.usages, privateKey.usages);
assert.strictEqual(privateKey.extractable, extractable);
if (extractable) {
const value = await subtle.exportKey('raw-seed', privateKey);
assert.deepStrictEqual(Buffer.from(value), seed);
}
await assert.rejects(
subtle.importKey(
'raw-seed',
seed.subarray(0, 30),
{ name },
extractable,
privateUsages),
{ message: 'Invalid keyData' });
}
(async function() {
const tests = [];
for (const vector of testVectors) {
for (const extractable of [true, false]) {
tests.push(testImportSpki(vector, extractable));
tests.push(testImportPkcs8(vector, extractable));
tests.push(testImportPkcs8SeedOnly(vector, extractable));
tests.push(testImportPkcs8PrivOnly(vector, extractable));
tests.push(testImportRawSeed(vector, extractable));
tests.push(testImportRawPublic(vector, extractable));
}
}
await Promise.all(tests);
})().then(common.mustCall());
(async function() {
const alg = 'ML-KEM-512';
const pub = keyData[alg].spki.subarray(-keyData[alg].pub_len);
await assert.rejects(subtle.importKey('raw', pub, alg, false, []), {
name: 'NotSupportedError',
message: 'Unable to import ML-KEM-512 using raw format',
});
})().then(common.mustCall());

View File

@ -790,3 +790,48 @@ if (hasOpenSSL(3, 5)) {
Promise.all(tests).then(common.mustCall());
}
// Test ML-KEM Key Generation
if (hasOpenSSL(3, 5)) {
async function test(
name,
privateUsages,
publicUsages = privateUsages) {
let usages = privateUsages;
if (publicUsages !== privateUsages)
usages = usages.concat(publicUsages);
const { publicKey, privateKey } = await subtle.generateKey({
name,
}, true, usages);
assert(publicKey);
assert(privateKey);
assert(isCryptoKey(publicKey));
assert(isCryptoKey(privateKey));
assert.strictEqual(publicKey.type, 'public');
assert.strictEqual(privateKey.type, 'private');
assert.strictEqual(publicKey.toString(), '[object CryptoKey]');
assert.strictEqual(privateKey.toString(), '[object CryptoKey]');
assert.strictEqual(publicKey.extractable, true);
assert.strictEqual(privateKey.extractable, true);
assert.deepStrictEqual(publicKey.usages, publicUsages);
assert.deepStrictEqual(privateKey.usages, privateUsages);
assert.strictEqual(publicKey.algorithm.name, name);
assert.strictEqual(privateKey.algorithm.name, name);
assert.strictEqual(privateKey.algorithm, privateKey.algorithm);
assert.strictEqual(privateKey.usages, privateKey.usages);
assert.strictEqual(publicKey.algorithm, publicKey.algorithm);
assert.strictEqual(publicKey.usages, publicKey.usages);
}
const kTests = ['ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024'];
const tests = kTests.map((name) => test(name,
['decapsulateBits', 'decapsulateKey'],
['encapsulateBits', 'encapsulateKey']));
Promise.all(tests).then(common.mustCall());
}

View File

@ -24,6 +24,7 @@ for await (const mod of sources) {
vectors.verify = vectors.sign;
vectors.decrypt = vectors.encrypt;
vectors.decapsulateBits = vectors.encapsulateBits;
for (const enc of vectors.encrypt) {
for (const exp of vectors.exportKey) {
@ -41,6 +42,57 @@ for (const exportKey of vectors.exportKey) {
if (!exportKey[0]) vectors.getPublicKey.push(exportKey);
}
function supportsRawSecret(alg) {
if (typeof alg === 'string') {
alg = alg.toLowerCase();
return alg.startsWith('aes') ||
alg.startsWith('argon2') ||
alg.startsWith('kmac') ||
alg === 'chacha20-poly1305' ||
alg === 'pbkdf2' ||
alg === 'hkdf' ||
alg === 'hmac';
}
if (typeof alg?.name === 'string') {
return supportsRawSecret(alg.name);
}
return false;
}
function supports256RawSecret(alg) {
if (!supportsRawSecret(alg)) return false;
switch (alg?.name?.toLowerCase?.()) {
case 'hmac':
case 'kmac128':
case 'kmac256':
return typeof alg.length !== 'number' || alg.length === 256;
default:
return true;
}
}
for (const encap of vectors.encapsulateBits) {
for (const imp of vectors.importKey) {
if (supports256RawSecret(imp[1])) {
vectors.encapsulateKey.push([encap[0] && imp[0], encap[1], imp[1]]);
} else {
vectors.encapsulateKey.push([false, encap[1], imp[1]]);
}
}
}
for (const decap of vectors.decapsulateBits) {
for (const imp of vectors.importKey) {
if (supports256RawSecret(imp[1])) {
vectors.decapsulateKey.push([decap[0] && imp[0], decap[1], imp[1]]);
} else {
vectors.decapsulateKey.push([false, decap[1], imp[1]]);
}
}
}
for (const operation of Object.keys(vectors)) {
for (const [expectation, ...args] of vectors[operation]) {
assert.strictEqual(

View File

@ -93,6 +93,8 @@ const customTypesMap = {
'CryptoKey': 'webcrypto.html#class-cryptokey',
'CryptoKeyPair': 'webcrypto.html#class-cryptokeypair',
'Crypto': 'webcrypto.html#class-crypto',
'EncapsulatedBits': 'webcrypto.html#class-encapsulatedbits',
'EncapsulatedKey': 'webcrypto.html#class-encapsulatedkey',
'SubtleCrypto': 'webcrypto.html#class-subtlecrypto',
'RsaOaepParams': 'webcrypto.html#class-rsaoaepparams',
'AesCtrParams': 'webcrypto.html#class-aesctrparams',