diff --git a/benchmark/crypto/create-keyobject.js b/benchmark/crypto/create-keyobject.js index 58b873cde7f27a..30f8213175df69 100644 --- a/benchmark/crypto/create-keyobject.js +++ b/benchmark/crypto/create-keyobject.js @@ -30,8 +30,22 @@ if (hasOpenSSL(3, 5)) { const bench = common.createBenchmark(main, { keyType: Object.keys(keyFixtures), - keyFormat: ['pkcs8', 'spki', 'der-pkcs8', 'der-spki', 'jwk-public', 'jwk-private'], + keyFormat: ['pkcs8', 'spki', 'der-pkcs8', 'der-spki', 'jwk-public', 'jwk-private', + 'raw-public', 'raw-private', 'raw-seed'], n: [1e3], +}, { + combinationFilter(p) { + // raw-private is not supported for rsa and ml-dsa + if (p.keyFormat === 'raw-private') + return p.keyType !== 'rsa' && !p.keyType.startsWith('ml-'); + // raw-public is not supported by rsa + if (p.keyFormat === 'raw-public') + return p.keyType !== 'rsa'; + // raw-seed is only supported for ml-dsa + if (p.keyFormat === 'raw-seed') + return p.keyType.startsWith('ml-'); + return true; + }, }); function measure(n, fn, input) { @@ -82,6 +96,29 @@ function main({ n, keyFormat, keyType }) { fn = crypto.createPrivateKey; break; } + case 'raw-public': { + const exportedKey = keyPair.publicKey.export({ format: 'raw-public' }); + key = { key: exportedKey, format: 'raw-public', asymmetricKeyType: keyType }; + if (keyType === 'ec') key.namedCurve = keyPair.publicKey.asymmetricKeyDetails.namedCurve; + fn = crypto.createPublicKey; + break; + } + case 'raw-private': { + const exportedKey = keyPair.privateKey.export({ format: 'raw-private' }); + key = { key: exportedKey, format: 'raw-private', asymmetricKeyType: keyType }; + if (keyType === 'ec') key.namedCurve = keyPair.privateKey.asymmetricKeyDetails.namedCurve; + fn = crypto.createPrivateKey; + break; + } + case 'raw-seed': { + key = { + key: keyPair.privateKey.export({ format: 'raw-seed' }), + format: 'raw-seed', + asymmetricKeyType: keyType, + }; + fn = crypto.createPrivateKey; + break; + } default: throw new Error('not implemented'); } diff --git a/benchmark/crypto/kem.js b/benchmark/crypto/kem.js index c36e79957a115c..e03ae65f1926ca 100644 --- a/benchmark/crypto/kem.js +++ b/benchmark/crypto/kem.js @@ -11,22 +11,29 @@ function readKey(name) { return fs.readFileSync(`${fixtures_keydir}/${name}.pem`, 'utf8'); } +function readKeyPair(publicKeyName, privateKeyName) { + return { + publicKey: readKey(publicKeyName), + privateKey: readKey(privateKeyName), + }; +} + const keyFixtures = {}; if (hasOpenSSL(3, 5)) { - keyFixtures['ml-kem-512'] = readKey('ml_kem_512_private'); - keyFixtures['ml-kem-768'] = readKey('ml_kem_768_private'); - keyFixtures['ml-kem-1024'] = readKey('ml_kem_1024_private'); + keyFixtures['ml-kem-512'] = readKeyPair('ml_kem_512_public', 'ml_kem_512_private'); + keyFixtures['ml-kem-768'] = readKeyPair('ml_kem_768_public', 'ml_kem_768_private'); + keyFixtures['ml-kem-1024'] = readKeyPair('ml_kem_1024_public', 'ml_kem_1024_private'); } if (hasOpenSSL(3, 2)) { - keyFixtures['p-256'] = readKey('ec_p256_private'); - keyFixtures['p-384'] = readKey('ec_p384_private'); - keyFixtures['p-521'] = readKey('ec_p521_private'); - keyFixtures.x25519 = readKey('x25519_private'); - keyFixtures.x448 = readKey('x448_private'); + keyFixtures['p-256'] = readKeyPair('ec_p256_public', 'ec_p256_private'); + keyFixtures['p-384'] = readKeyPair('ec_p384_public', 'ec_p384_private'); + keyFixtures['p-521'] = readKeyPair('ec_p521_public', 'ec_p521_private'); + keyFixtures.x25519 = readKeyPair('x25519_public', 'x25519_private'); + keyFixtures.x448 = readKeyPair('x448_public', 'x448_private'); } if (hasOpenSSL(3, 0)) { - keyFixtures.rsa = readKey('rsa_private_2048'); + keyFixtures.rsa = readKeyPair('rsa_public_2048', 'rsa_private_2048'); } if (Object.keys(keyFixtures).length === 0) { @@ -37,32 +44,46 @@ if (Object.keys(keyFixtures).length === 0) { const bench = common.createBenchmark(main, { keyType: Object.keys(keyFixtures), mode: ['sync', 'async', 'async-parallel'], - keyFormat: ['keyObject', 'keyObject.unique'], + keyFormat: ['keyObject', 'keyObject.unique', 'pem', 'der', 'jwk', + 'raw-public', 'raw-private', 'raw-seed'], op: ['encapsulate', 'decapsulate'], n: [1e3], }, { combinationFilter(p) { // "keyObject.unique" allows to compare the result with "keyObject" to // assess whether mutexes over the key material impact the operation - return p.keyFormat !== 'keyObject.unique' || - (p.keyFormat === 'keyObject.unique' && p.mode === 'async-parallel'); + if (p.keyFormat === 'keyObject.unique') + return p.mode === 'async-parallel'; + // JWK is not supported for ml-kem for now + if (p.keyFormat === 'jwk') + return !p.keyType.startsWith('ml-'); + // raw-public is only supported for encapsulate, not rsa + if (p.keyFormat === 'raw-public') + return p.keyType !== 'rsa' && p.op === 'encapsulate'; + // raw-private is not supported for rsa and ml-kem, only for decapsulate + if (p.keyFormat === 'raw-private') + return p.keyType !== 'rsa' && !p.keyType.startsWith('ml-') && p.op === 'decapsulate'; + // raw-seed is only supported for ml-kem + if (p.keyFormat === 'raw-seed') + return p.keyType.startsWith('ml-'); + return true; }, }); -function measureSync(n, op, privateKey, keys, ciphertexts) { +function measureSync(n, op, key, keys, ciphertexts) { bench.start(); for (let i = 0; i < n; ++i) { - const key = privateKey || keys[i]; + const k = key || keys[i]; if (op === 'encapsulate') { - crypto.encapsulate(key); + crypto.encapsulate(k); } else { - crypto.decapsulate(key, ciphertexts[i]); + crypto.decapsulate(k, ciphertexts[i]); } } bench.end(n); } -function measureAsync(n, op, privateKey, keys, ciphertexts) { +function measureAsync(n, op, key, keys, ciphertexts) { let remaining = n; function done() { if (--remaining === 0) @@ -72,18 +93,18 @@ function measureAsync(n, op, privateKey, keys, ciphertexts) { } function one() { - const key = privateKey || keys[n - remaining]; + const k = key || keys[n - remaining]; if (op === 'encapsulate') { - crypto.encapsulate(key, done); + crypto.encapsulate(k, done); } else { - crypto.decapsulate(key, ciphertexts[n - remaining], done); + crypto.decapsulate(k, ciphertexts[n - remaining], done); } } bench.start(); one(); } -function measureAsyncParallel(n, op, privateKey, keys, ciphertexts) { +function measureAsyncParallel(n, op, key, keys, ciphertexts) { let remaining = n; function done() { if (--remaining === 0) @@ -91,25 +112,79 @@ function measureAsyncParallel(n, op, privateKey, keys, ciphertexts) { } bench.start(); for (let i = 0; i < n; ++i) { - const key = privateKey || keys[i]; + const k = key || keys[i]; if (op === 'encapsulate') { - crypto.encapsulate(key, done); + crypto.encapsulate(k, done); } else { - crypto.decapsulate(key, ciphertexts[i], done); + crypto.decapsulate(k, ciphertexts[i], done); } } } function main({ n, mode, keyFormat, keyType, op }) { - const pems = [...Buffer.alloc(n)].map(() => keyFixtures[keyType]); - const keyObjects = pems.map(crypto.createPrivateKey); + const isEncapsulate = op === 'encapsulate'; + const pemSource = isEncapsulate ? + keyFixtures[keyType].publicKey : + keyFixtures[keyType].privateKey; + const createKeyFn = isEncapsulate ? crypto.createPublicKey : crypto.createPrivateKey; + const pems = [...Buffer.alloc(n)].map(() => pemSource); + const keyObjects = pems.map(createKeyFn); - let privateKey, keys, ciphertexts; + // Warm up OpenSSL's provider operation cache for each key object + if (isEncapsulate) { + for (const keyObject of keyObjects) { + crypto.encapsulate(keyObject); + } + } else { + const warmupCiphertext = crypto.encapsulate(keyObjects[0]).ciphertext; + for (const keyObject of keyObjects) { + crypto.decapsulate(keyObject, warmupCiphertext); + } + } + + const asymmetricKeyType = keyObjects[0].asymmetricKeyType; + let key, keys, ciphertexts; switch (keyFormat) { case 'keyObject': - privateKey = keyObjects[0]; + key = keyObjects[0]; + break; + case 'pem': + key = pems[0]; break; + case 'jwk': { + key = { key: keyObjects[0].export({ format: 'jwk' }), format: 'jwk' }; + break; + } + case 'der': { + const type = isEncapsulate ? 'spki' : 'pkcs8'; + key = { key: keyObjects[0].export({ format: 'der', type }), format: 'der', type }; + break; + } + case 'raw-public': { + const exportedKey = keyObjects[0].export({ format: 'raw-public' }); + const keyOpts = { key: exportedKey, format: 'raw-public', asymmetricKeyType }; + if (asymmetricKeyType === 'ec') keyOpts.namedCurve = keyObjects[0].asymmetricKeyDetails.namedCurve; + key = keyOpts; + break; + } + case 'raw-private': { + const exportedKey = keyObjects[0].export({ format: 'raw-private' }); + const keyOpts = { key: exportedKey, format: 'raw-private', asymmetricKeyType }; + if (asymmetricKeyType === 'ec') keyOpts.namedCurve = keyObjects[0].asymmetricKeyDetails.namedCurve; + key = keyOpts; + break; + } + case 'raw-seed': { + // raw-seed requires a private key to export from + const privateKeyObject = crypto.createPrivateKey(keyFixtures[keyType].privateKey); + key = { + key: privateKeyObject.export({ format: 'raw-seed' }), + format: 'raw-seed', + asymmetricKeyType, + }; + break; + } case 'keyObject.unique': keys = keyObjects; break; @@ -118,23 +193,25 @@ function main({ n, mode, keyFormat, keyType, op }) { } // Pre-generate ciphertexts for decapsulate operations - if (op === 'decapsulate') { - if (privateKey) { - ciphertexts = [...Buffer.alloc(n)].map(() => crypto.encapsulate(privateKey).ciphertext); + if (!isEncapsulate) { + const encapKey = crypto.createPublicKey( + crypto.createPrivateKey(keyFixtures[keyType].privateKey)); + if (key) { + ciphertexts = [...Buffer.alloc(n)].map(() => crypto.encapsulate(encapKey).ciphertext); } else { - ciphertexts = keys.map((key) => crypto.encapsulate(key).ciphertext); + ciphertexts = keys.map(() => crypto.encapsulate(encapKey).ciphertext); } } switch (mode) { case 'sync': - measureSync(n, op, privateKey, keys, ciphertexts); + measureSync(n, op, key, keys, ciphertexts); break; case 'async': - measureAsync(n, op, privateKey, keys, ciphertexts); + measureAsync(n, op, key, keys, ciphertexts); break; case 'async-parallel': - measureAsyncParallel(n, op, privateKey, keys, ciphertexts); + measureAsyncParallel(n, op, key, keys, ciphertexts); break; } } diff --git a/benchmark/crypto/oneshot-sign.js b/benchmark/crypto/oneshot-sign.js index e1942c347d7508..d0abc7b5412e60 100644 --- a/benchmark/crypto/oneshot-sign.js +++ b/benchmark/crypto/oneshot-sign.js @@ -29,14 +29,21 @@ let keyObjects; const bench = common.createBenchmark(main, { keyType: Object.keys(keyFixtures), mode: ['sync', 'async', 'async-parallel'], - keyFormat: ['pem', 'der', 'jwk', 'keyObject', 'keyObject.unique'], + keyFormat: ['pem', 'der', 'jwk', 'keyObject', 'keyObject.unique', 'raw-private', 'raw-seed'], n: [1e3], }, { combinationFilter(p) { // "keyObject.unique" allows to compare the result with "keyObject" to // assess whether mutexes over the key material impact the operation - return p.keyFormat !== 'keyObject.unique' || - (p.keyFormat === 'keyObject.unique' && p.mode === 'async-parallel'); + if (p.keyFormat === 'keyObject.unique') + return p.mode === 'async-parallel'; + // raw-private is not supported for rsa and ml-dsa + if (p.keyFormat === 'raw-private') + return p.keyType !== 'rsa' && !p.keyType.startsWith('ml-'); + // raw-seed is only supported for ml-dsa + if (p.keyFormat === 'raw-seed') + return p.keyType.startsWith('ml-'); + return true; }, }); @@ -91,6 +98,12 @@ function main({ n, mode, keyFormat, keyType }) { pems ||= [...Buffer.alloc(n)].map(() => keyFixtures[keyType]); keyObjects ||= pems.map(crypto.createPrivateKey); + // Warm up OpenSSL's provider operation cache for each key object + for (const keyObject of keyObjects) { + crypto.sign(keyType === 'rsa' || keyType === 'ec' ? 'sha256' : null, + data, keyObject); + } + let privateKey, keys, digest; switch (keyType) { @@ -120,6 +133,21 @@ function main({ n, mode, keyFormat, keyType }) { privateKey = { key: keyObjects[0].export({ format: 'der', type: 'pkcs8' }), format: 'der', type: 'pkcs8' }; break; } + case 'raw-private': { + const exportedKey = keyObjects[0].export({ format: 'raw-private' }); + const keyOpts = { key: exportedKey, format: 'raw-private', asymmetricKeyType: keyType }; + if (keyType === 'ec') keyOpts.namedCurve = keyObjects[0].asymmetricKeyDetails.namedCurve; + privateKey = keyOpts; + break; + } + case 'raw-seed': { + privateKey = { + key: keyObjects[0].export({ format: 'raw-seed' }), + format: 'raw-seed', + asymmetricKeyType: keyType, + }; + break; + } case 'keyObject.unique': keys = keyObjects; break; diff --git a/benchmark/crypto/oneshot-verify.js b/benchmark/crypto/oneshot-verify.js index e0bbc0ce755f15..c6a24f52126eb2 100644 --- a/benchmark/crypto/oneshot-verify.js +++ b/benchmark/crypto/oneshot-verify.js @@ -36,14 +36,18 @@ let keyObjects; const bench = common.createBenchmark(main, { keyType: Object.keys(keyFixtures), mode: ['sync', 'async', 'async-parallel'], - keyFormat: ['pem', 'der', 'jwk', 'keyObject', 'keyObject.unique'], + keyFormat: ['pem', 'der', 'jwk', 'keyObject', 'keyObject.unique', 'raw-public'], n: [1e3], }, { combinationFilter(p) { // "keyObject.unique" allows to compare the result with "keyObject" to // assess whether mutexes over the key material impact the operation - return p.keyFormat !== 'keyObject.unique' || - (p.keyFormat === 'keyObject.unique' && p.mode === 'async-parallel'); + if (p.keyFormat === 'keyObject.unique') + return p.mode === 'async-parallel'; + // raw-public is not supported by rsa + if (p.keyFormat === 'raw-public') + return p.keyType !== 'rsa'; + return true; }, }); @@ -101,6 +105,13 @@ function main({ n, mode, keyFormat, keyType }) { pems ||= [...Buffer.alloc(n)].map(() => keyFixtures[keyType].publicKey); keyObjects ||= pems.map(crypto.createPublicKey); + // Warm up OpenSSL's provider operation cache for each key object + const warmupDigest = keyType === 'rsa' || keyType === 'ec' ? 'sha256' : null; + const warmupSig = crypto.sign(warmupDigest, data, keyFixtures[keyType].privateKey); + for (const keyObject of keyObjects) { + crypto.verify(warmupDigest, data, keyObject, warmupSig); + } + let publicKey, keys, digest; switch (keyType) { @@ -130,6 +141,13 @@ function main({ n, mode, keyFormat, keyType }) { publicKey = { key: keyObjects[0].export({ format: 'der', type: 'spki' }), format: 'der', type: 'spki' }; break; } + case 'raw-public': { + const exportedKey = keyObjects[0].export({ format: 'raw-public' }); + const keyOpts = { key: exportedKey, format: 'raw-public', asymmetricKeyType: keyType }; + if (keyType === 'ec') keyOpts.namedCurve = keyObjects[0].asymmetricKeyDetails.namedCurve; + publicKey = keyOpts; + break; + } case 'keyObject.unique': keys = keyObjects; break; diff --git a/doc/api/crypto.md b/doc/api/crypto.md index acde45c346c84e..c88e2ee5d56aa0 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -75,37 +75,421 @@ try { ## Asymmetric key types -The following table lists the asymmetric key types recognized by the [`KeyObject`][] API: - -| Key Type | Description | OID | -| ---------------------------------- | ------------------ | ----------------------- | -| `'dh'` | Diffie-Hellman | 1.2.840.113549.1.3.1 | -| `'dsa'` | DSA | 1.2.840.10040.4.1 | -| `'ec'` | Elliptic curve | 1.2.840.10045.2.1 | -| `'ed25519'` | Ed25519 | 1.3.101.112 | -| `'ed448'` | Ed448 | 1.3.101.113 | -| `'ml-dsa-44'`[^openssl35] | ML-DSA-44 | 2.16.840.1.101.3.4.3.17 | -| `'ml-dsa-65'`[^openssl35] | ML-DSA-65 | 2.16.840.1.101.3.4.3.18 | -| `'ml-dsa-87'`[^openssl35] | ML-DSA-87 | 2.16.840.1.101.3.4.3.19 | -| `'ml-kem-512'`[^openssl35] | ML-KEM-512 | 2.16.840.1.101.3.4.4.1 | -| `'ml-kem-768'`[^openssl35] | ML-KEM-768 | 2.16.840.1.101.3.4.4.2 | -| `'ml-kem-1024'`[^openssl35] | ML-KEM-1024 | 2.16.840.1.101.3.4.4.3 | -| `'rsa-pss'` | RSA PSS | 1.2.840.113549.1.1.10 | -| `'rsa'` | RSA | 1.2.840.113549.1.1.1 | -| `'slh-dsa-sha2-128f'`[^openssl35] | SLH-DSA-SHA2-128f | 2.16.840.1.101.3.4.3.21 | -| `'slh-dsa-sha2-128s'`[^openssl35] | SLH-DSA-SHA2-128s | 2.16.840.1.101.3.4.3.20 | -| `'slh-dsa-sha2-192f'`[^openssl35] | SLH-DSA-SHA2-192f | 2.16.840.1.101.3.4.3.23 | -| `'slh-dsa-sha2-192s'`[^openssl35] | SLH-DSA-SHA2-192s | 2.16.840.1.101.3.4.3.22 | -| `'slh-dsa-sha2-256f'`[^openssl35] | SLH-DSA-SHA2-256f | 2.16.840.1.101.3.4.3.25 | -| `'slh-dsa-sha2-256s'`[^openssl35] | SLH-DSA-SHA2-256s | 2.16.840.1.101.3.4.3.24 | -| `'slh-dsa-shake-128f'`[^openssl35] | SLH-DSA-SHAKE-128f | 2.16.840.1.101.3.4.3.27 | -| `'slh-dsa-shake-128s'`[^openssl35] | SLH-DSA-SHAKE-128s | 2.16.840.1.101.3.4.3.26 | -| `'slh-dsa-shake-192f'`[^openssl35] | SLH-DSA-SHAKE-192f | 2.16.840.1.101.3.4.3.29 | -| `'slh-dsa-shake-192s'`[^openssl35] | SLH-DSA-SHAKE-192s | 2.16.840.1.101.3.4.3.28 | -| `'slh-dsa-shake-256f'`[^openssl35] | SLH-DSA-SHAKE-256f | 2.16.840.1.101.3.4.3.31 | -| `'slh-dsa-shake-256s'`[^openssl35] | SLH-DSA-SHAKE-256s | 2.16.840.1.101.3.4.3.30 | -| `'x25519'` | X25519 | 1.3.101.110 | -| `'x448'` | X448 | 1.3.101.111 | +The following table lists the asymmetric key types recognized by the +[`KeyObject`][] API and the export/import formats supported for each key type. + +| Key Type | Description | OID | `'pem'` | `'der'` | `'jwk'` | `'raw-public'` | `'raw-private'` | `'raw-seed'` | +| ---------------------------------- | ------------------ | ----------------------- | ------- | ------- | ------- | -------------- | --------------- | ------------ | +| `'dh'` | Diffie-Hellman | 1.2.840.113549.1.3.1 | ✔ | ✔ | | | | | +| `'dsa'` | DSA | 1.2.840.10040.4.1 | ✔ | ✔ | | | | | +| `'ec'` | Elliptic curve | 1.2.840.10045.2.1 | ✔ | ✔ | ✔ | ✔ | ✔ | | +| `'ed25519'` | Ed25519 | 1.3.101.112 | ✔ | ✔ | ✔ | ✔ | ✔ | | +| `'ed448'` | Ed448 | 1.3.101.113 | ✔ | ✔ | ✔ | ✔ | ✔ | | +| `'ml-dsa-44'`[^openssl35] | ML-DSA-44 | 2.16.840.1.101.3.4.3.17 | ✔ | ✔ | ✔ | ✔ | | ✔ | +| `'ml-dsa-65'`[^openssl35] | ML-DSA-65 | 2.16.840.1.101.3.4.3.18 | ✔ | ✔ | ✔ | ✔ | | ✔ | +| `'ml-dsa-87'`[^openssl35] | ML-DSA-87 | 2.16.840.1.101.3.4.3.19 | ✔ | ✔ | ✔ | ✔ | | ✔ | +| `'ml-kem-512'`[^openssl35] | ML-KEM-512 | 2.16.840.1.101.3.4.4.1 | ✔ | ✔ | | ✔ | | ✔ | +| `'ml-kem-768'`[^openssl35] | ML-KEM-768 | 2.16.840.1.101.3.4.4.2 | ✔ | ✔ | | ✔ | | ✔ | +| `'ml-kem-1024'`[^openssl35] | ML-KEM-1024 | 2.16.840.1.101.3.4.4.3 | ✔ | ✔ | | ✔ | | ✔ | +| `'rsa-pss'` | RSA PSS | 1.2.840.113549.1.1.10 | ✔ | ✔ | | | | | +| `'rsa'` | RSA | 1.2.840.113549.1.1.1 | ✔ | ✔ | ✔ | | | | +| `'slh-dsa-sha2-128f'`[^openssl35] | SLH-DSA-SHA2-128f | 2.16.840.1.101.3.4.3.21 | ✔ | ✔ | | ✔ | ✔ | | +| `'slh-dsa-sha2-128s'`[^openssl35] | SLH-DSA-SHA2-128s | 2.16.840.1.101.3.4.3.20 | ✔ | ✔ | | ✔ | ✔ | | +| `'slh-dsa-sha2-192f'`[^openssl35] | SLH-DSA-SHA2-192f | 2.16.840.1.101.3.4.3.23 | ✔ | ✔ | | ✔ | ✔ | | +| `'slh-dsa-sha2-192s'`[^openssl35] | SLH-DSA-SHA2-192s | 2.16.840.1.101.3.4.3.22 | ✔ | ✔ | | ✔ | ✔ | | +| `'slh-dsa-sha2-256f'`[^openssl35] | SLH-DSA-SHA2-256f | 2.16.840.1.101.3.4.3.25 | ✔ | ✔ | | ✔ | ✔ | | +| `'slh-dsa-sha2-256s'`[^openssl35] | SLH-DSA-SHA2-256s | 2.16.840.1.101.3.4.3.24 | ✔ | ✔ | | ✔ | ✔ | | +| `'slh-dsa-shake-128f'`[^openssl35] | SLH-DSA-SHAKE-128f | 2.16.840.1.101.3.4.3.27 | ✔ | ✔ | | ✔ | ✔ | | +| `'slh-dsa-shake-128s'`[^openssl35] | SLH-DSA-SHAKE-128s | 2.16.840.1.101.3.4.3.26 | ✔ | ✔ | | ✔ | ✔ | | +| `'slh-dsa-shake-192f'`[^openssl35] | SLH-DSA-SHAKE-192f | 2.16.840.1.101.3.4.3.29 | ✔ | ✔ | | ✔ | ✔ | | +| `'slh-dsa-shake-192s'`[^openssl35] | SLH-DSA-SHAKE-192s | 2.16.840.1.101.3.4.3.28 | ✔ | ✔ | | ✔ | ✔ | | +| `'slh-dsa-shake-256f'`[^openssl35] | SLH-DSA-SHAKE-256f | 2.16.840.1.101.3.4.3.31 | ✔ | ✔ | | ✔ | ✔ | | +| `'slh-dsa-shake-256s'`[^openssl35] | SLH-DSA-SHAKE-256s | 2.16.840.1.101.3.4.3.30 | ✔ | ✔ | | ✔ | ✔ | | +| `'x25519'` | X25519 | 1.3.101.110 | ✔ | ✔ | ✔ | ✔ | ✔ | | +| `'x448'` | X448 | 1.3.101.111 | ✔ | ✔ | ✔ | ✔ | ✔ | | + +### Key formats + +Asymmetric keys can be represented in several formats. **The recommended +approach is to import key material into a [`KeyObject`][] once and reuse it** +for all subsequent operations. A [`KeyObject`][] avoids repeated parsing +and delivers the best performance. + +When a [`KeyObject`][] is not practical - for example, when key material +arrives in a protocol message and is used only once - most cryptographic +functions also accept a PEM string or an object specifying the format +and key material directly. See [`crypto.createPublicKey()`][], +[`crypto.createPrivateKey()`][], and [`keyObject.export()`][] for the full +options accepted by each format. + +#### KeyObject + +A [`KeyObject`][] is the in-memory representation of a parsed key and is +**the preferred way to work with keys** in `node:crypto`. It is created by +[`crypto.createPublicKey()`][], [`crypto.createPrivateKey()`][], +[`crypto.createSecretKey()`][], or key generation functions such as +\[`crypto.generateKeyPair()`]\[]. + +Because the key material is parsed once at creation time, reusing a +[`KeyObject`][] across multiple operations avoids repeated parsing and +delivers the best performance - for example, each Ed25519 signing operation +with a reused [`KeyObject`][] is over 2× faster than passing a PEM string, +and the savings compound with every subsequent use of the same [`KeyObject`][]. +Always prefer creating a [`KeyObject`][] up front when the same key is used +more than once. The first cryptographic operation with a given +[`KeyObject`][] may be slower than subsequent ones because OpenSSL lazily +initializes internal caches on first use, but it will still generally be faster +than passing key material in any other format. + +#### PEM and DER + +PEM and DER are the traditional encoding formats for asymmetric keys based on +ASN.1 structures. For private keys, these structures typically carry both the +private and public key components, so no additional computation is needed +during import - however, the ASN.1 parsing itself is the main cost. + +* **PEM** is a text encoding that wraps Base64-encoded DER data between + header and footer lines (e.g. `-----BEGIN PUBLIC KEY-----`). PEM strings can + be passed directly to most cryptographic operations. Public keys typically use + the `'spki'` type and private keys typically use `'pkcs8'`. +* **DER** is the binary encoding of the same ASN.1 structures. When providing + DER input, the `type` (typically `'spki'` or `'pkcs8'`) must be specified + explicitly. DER avoids the Base64 decoding overhead of PEM, and the explicit + `type` lets the parser skip type detection, making DER slightly faster to + import than PEM. + +#### JSON Web Key (JWK) + +JSON Web Key (JWK) is a JSON-based key representation defined in +[RFC 7517][]. Instead of wrapping key material in ASN.1 structures, JWK +encodes each key component as an individual Base64url-encoded value inside a +JSON object. + +#### Raw key formats + +> Stability: 1.1 - Active development + +The `'raw-public'`, `'raw-private'`, and `'raw-seed'` key formats allow +importing and exporting raw key material without any encoding wrapper. +See [`keyObject.export()`][], [`crypto.createPublicKey()`][], and +[`crypto.createPrivateKey()`][] for usage details. + +The `'raw-public'` format is generally the fastest way to import a public key +because no ASN.1 or Base64 decoding is needed. The `'raw-private'` and +`'raw-seed'` formats, however, are not always faster than PEM, DER, or JWK +because those formats only contain the private scalar or seed - importing them +requires mathematically deriving the public key component for the purpose of +committing the public key to the cryptographic output (e.g. elliptic curve +point multiplication or seed expansion), which can be expensive depending on +the key type. Other formats like PEM, DER, or JWK include both private and +public components, avoiding that computation. + +### Choosing a key format + +**Always prefer a [`KeyObject`][]** - create one from whatever format you +have and reuse it. The guidance below applies only when choosing between +serialization formats, either for importing into a [`KeyObject`][] or for +passing key material inline when a [`KeyObject`][] is not practical. + +#### Importing keys + +When creating a [`KeyObject`][] for repeated use, the import cost is paid once, +so choosing a faster format reduces startup latency. + +The import cost breaks down into two parts: **parsing overhead** (decoding the +serialization wrapper) and **key computation** (any mathematical work needed to +reconstruct the full key, such as deriving a public key from a private scalar +or expanding a seed). Which part dominates depends on the key type. For +example: + +* Public keys - `'raw-public'` is the fastest serialized format because the + raw format skips all ASN.1 and Base64 decoding. For Ed25519 public key + import, `'raw-public'` can be over 2× faster than PEM. +* EC private keys - `'raw-private'` is faster than PEM or DER because it + avoids ASN.1 parsing. However, for larger curves (e.g. P-384, P-521) the + required derivation of the public point from the private scalar becomes + expensive, reducing the advantage. +* RSA keys - `'jwk'` is the fastest serialized format. JWK represents RSA + key components as individual Base64url-encoded integers, avoiding the + overhead of ASN.1 parsing entirely. + +#### Inline key material in operations + +When a [`KeyObject`][] cannot be reused (e.g. the key arrives as raw bytes in +a protocol message and is used only once), most cryptographic functions also +accept a PEM string or an object specifying the format and key +material directly. In this case the total cost is the sum of key import and +the cryptographic computation itself. + +For operations where the cryptographic computation dominates - such as +signing with RSA or ECDH key agreement with P-384 or P-521 - the +serialization format has negligible impact on overall throughput, so choose +whichever format is most convenient. For lightweight operations like Ed25519 +signing or verification, the import cost is a larger fraction of the total, +so a faster format like `'raw-public'` or `'raw-private'` can meaningfully +improve throughput. + +If the same key material is used only a few times, it is worth importing it +into a [`KeyObject`][] rather than passing the raw or PEM representation +repeatedly. + +### Examples + +Example: Reusing a [`KeyObject`][] across sign and verify operations: + +```mjs +const { generateKeyPair, sign, verify } = await import('node:crypto'); +const { promisify } = await import('node:util'); + +const { publicKey, privateKey } = await promisify(generateKeyPair)('ed25519'); + +// A KeyObject holds the parsed key in memory and can be reused +// across multiple operations without re-parsing. +const data = new TextEncoder().encode('message to sign'); +const signature = sign(null, data, privateKey); +verify(null, data, publicKey, signature); +``` + +Example: Importing a PEM-encoded key into a [`KeyObject`][]: + +```mjs +const { + createPrivateKey, createPublicKey, generateKeyPair, sign, verify, +} = await import('node:crypto'); +const { promisify } = await import('node:util'); + +// PEM-encoded keys, e.g. read from a file or environment variable. +const generated = await promisify(generateKeyPair)('ed25519'); +const privatePem = generated.privateKey.export({ format: 'pem', type: 'pkcs8' }); +const publicPem = generated.publicKey.export({ format: 'pem', type: 'spki' }); + +const privateKey = createPrivateKey(privatePem); +const publicKey = createPublicKey(publicPem); + +const data = new TextEncoder().encode('message to sign'); +const signature = sign(null, data, privateKey); +verify(null, data, publicKey, signature); +``` + +Example: Importing a JWK into a [`KeyObject`][]: + +```mjs +const { + createPrivateKey, createPublicKey, generateKeyPair, sign, verify, +} = await import('node:crypto'); +const { promisify } = await import('node:util'); + +// JWK objects, e.g. from a JSON configuration or API response. +const generated = await promisify(generateKeyPair)('ed25519'); +const privateJwk = generated.privateKey.export({ format: 'jwk' }); +const publicJwk = generated.publicKey.export({ format: 'jwk' }); + +const privateKey = createPrivateKey({ key: privateJwk, format: 'jwk' }); +const publicKey = createPublicKey({ key: publicJwk, format: 'jwk' }); + +const data = new TextEncoder().encode('message to sign'); +const signature = sign(null, data, privateKey); +verify(null, data, publicKey, signature); +``` + +Example: Importing a DER-encoded key into a [`KeyObject`][]: + +```mjs +const { + createPrivateKey, createPublicKey, generateKeyPair, sign, verify, +} = await import('node:crypto'); +const { promisify } = await import('node:util'); + +// DER-encoded keys, e.g. read from binary files or hex/base64url-decoded +// from environment variables. +const generated = await promisify(generateKeyPair)('ed25519'); +const privateDer = generated.privateKey.export({ format: 'der', type: 'pkcs8' }); +const publicDer = generated.publicKey.export({ format: 'der', type: 'spki' }); + +const privateKey = createPrivateKey({ + key: privateDer, + format: 'der', + type: 'pkcs8', +}); +const publicKey = createPublicKey({ + key: publicDer, + format: 'der', + type: 'spki', +}); + +const data = new TextEncoder().encode('message to sign'); +const signature = sign(null, data, privateKey); +verify(null, data, publicKey, signature); +``` + +Example: Passing PEM strings directly to [`crypto.sign()`][] and +[`crypto.verify()`][]: + +```mjs +const { generateKeyPair, sign, verify } = await import('node:crypto'); +const { promisify } = await import('node:util'); + +const generated = await promisify(generateKeyPair)('ed25519'); +const privatePem = generated.privateKey.export({ format: 'pem', type: 'pkcs8' }); +const publicPem = generated.publicKey.export({ format: 'pem', type: 'spki' }); + +// PEM strings can be passed directly without creating a KeyObject first. +const data = new TextEncoder().encode('message to sign'); +const signature = sign(null, data, privatePem); +verify(null, data, publicPem, signature); +``` + +Example: Passing JWK objects directly to [`crypto.sign()`][] and +[`crypto.verify()`][]: + +```mjs +const { generateKeyPair, sign, verify } = await import('node:crypto'); +const { promisify } = await import('node:util'); + +const generated = await promisify(generateKeyPair)('ed25519'); +const privateJwk = generated.privateKey.export({ format: 'jwk' }); +const publicJwk = generated.publicKey.export({ format: 'jwk' }); + +// JWK objects can be passed directly without creating a KeyObject first. +const data = new TextEncoder().encode('message to sign'); +const signature = sign(null, data, { key: privateJwk, format: 'jwk' }); +verify(null, data, { key: publicJwk, format: 'jwk' }, signature); +``` + +Example: Passing raw key bytes directly to [`crypto.sign()`][] and +[`crypto.verify()`][]: + +```mjs +const { generateKeyPair, sign, verify } = await import('node:crypto'); +const { promisify } = await import('node:util'); + +const generated = await promisify(generateKeyPair)('ed25519'); +const rawPrivateKey = generated.privateKey.export({ format: 'raw-private' }); +const rawPublicKey = generated.publicKey.export({ format: 'raw-public' }); + +// Raw key bytes can be passed directly without creating a KeyObject first. +const data = new TextEncoder().encode('message to sign'); +const signature = sign(null, data, { + key: rawPrivateKey, + format: 'raw-private', + asymmetricKeyType: 'ed25519', +}); +verify(null, data, { + key: rawPublicKey, + format: 'raw-public', + asymmetricKeyType: 'ed25519', +}, signature); +``` + +Example: Exporting raw keys and importing them: + +```mjs +const { + createPrivateKey, createPublicKey, generateKeyPair, sign, verify, +} = await import('node:crypto'); +const { promisify } = await import('node:util'); + +const generated = await promisify(generateKeyPair)('ed25519'); + +// Export the raw public key (32 bytes for Ed25519). +const rawPublicKey = generated.publicKey.export({ format: 'raw-public' }); + +// Export the raw private key (32 bytes for Ed25519). +const rawPrivateKey = generated.privateKey.export({ format: 'raw-private' }); + +// Import the raw public key. +const publicKey = createPublicKey({ + key: rawPublicKey, + format: 'raw-public', + asymmetricKeyType: 'ed25519', +}); + +// Import the raw private key. +const privateKey = createPrivateKey({ + key: rawPrivateKey, + format: 'raw-private', + asymmetricKeyType: 'ed25519', +}); + +const data = new TextEncoder().encode('message to sign'); +const signature = sign(null, data, privateKey); +verify(null, data, publicKey, signature); +``` + +Example: For EC keys, the `namedCurve` option is required when importing +`'raw-public'` keys: + +```mjs +const { + createPrivateKey, createPublicKey, generateKeyPair, sign, verify, +} = await import('node:crypto'); +const { promisify } = await import('node:util'); + +const generated = await promisify(generateKeyPair)('ec', { + namedCurve: 'P-256', +}); + +// Export the raw EC public key (compressed by default). +const rawPublicKey = generated.publicKey.export({ format: 'raw-public' }); + +// The following is equivalent. +const rawPublicKeyCompressed = generated.publicKey.export({ + format: 'raw-public', + type: 'compressed', +}); + +// Export uncompressed point format. +const rawPublicKeyUncompressed = generated.publicKey.export({ + format: 'raw-public', + type: 'uncompressed', +}); + +// Export the raw EC private key. +const rawPrivateKey = generated.privateKey.export({ format: 'raw-private' }); + +// Import the raw EC keys. +// Both compressed and uncompressed point formats are accepted. +const publicKey = createPublicKey({ + key: rawPublicKey, + format: 'raw-public', + asymmetricKeyType: 'ec', + namedCurve: 'P-256', +}); +const privateKey = createPrivateKey({ + key: rawPrivateKey, + format: 'raw-private', + asymmetricKeyType: 'ec', + namedCurve: 'P-256', +}); + +const data = new TextEncoder().encode('message to sign'); +const signature = sign('sha256', data, privateKey); +verify('sha256', data, publicKey, signature); +``` + +Example: Exporting raw seeds and importing them: + +```mjs +const { + createPrivateKey, decapsulate, encapsulate, generateKeyPair, +} = await import('node:crypto'); +const { promisify } = await import('node:util'); + +const generated = await promisify(generateKeyPair)('ml-kem-768'); + +// Export the raw seed (64 bytes for ML-KEM). +const seed = generated.privateKey.export({ format: 'raw-seed' }); + +// Import the raw seed. +const privateKey = createPrivateKey({ + key: seed, + format: 'raw-seed', + asymmetricKeyType: 'ml-kem-768', +}); + +const { ciphertext } = encapsulate(generated.publicKey); +decapsulate(privateKey, ciphertext); +``` ## Class: `Certificate` @@ -2121,6 +2505,10 @@ type, value, and parameters. This method is not @@ -3673,6 +4073,9 @@ of the passphrase is limited to 1024 bytes. @@ -6539,6 +6947,7 @@ See the [list of SSL OP Flags][] for details. [RFC 4122]: https://www.rfc-editor.org/rfc/rfc4122.txt [RFC 5208]: https://www.rfc-editor.org/rfc/rfc5208.txt [RFC 5280]: https://www.rfc-editor.org/rfc/rfc5280.txt +[RFC 7517]: https://www.rfc-editor.org/rfc/rfc7517.txt [Web Crypto API documentation]: webcrypto.md [`BN_is_prime_ex`]: https://www.openssl.org/docs/man1.1.1/man3/BN_is_prime_ex.html [`Buffer`]: buffer.md @@ -6572,6 +6981,8 @@ See the [list of SSL OP Flags][] for details. [`crypto.publicEncrypt()`]: #cryptopublicencryptkey-buffer [`crypto.randomBytes()`]: #cryptorandombytessize-callback [`crypto.randomFill()`]: #cryptorandomfillbuffer-offset-size-callback +[`crypto.sign()`]: #cryptosignalgorithm-data-key-callback +[`crypto.verify()`]: #cryptoverifyalgorithm-data-key-signature-callback [`crypto.webcrypto.getRandomValues()`]: webcrypto.md#cryptogetrandomvaluestypedarray [`crypto.webcrypto.subtle`]: webcrypto.md#class-subtlecrypto [`decipher.final()`]: #decipherfinaloutputencoding diff --git a/lib/internal/crypto/cfrg.js b/lib/internal/crypto/cfrg.js index fd3f168435ddcb..a3a29ce1dcc6cd 100644 --- a/lib/internal/crypto/cfrg.js +++ b/lib/internal/crypto/cfrg.js @@ -3,19 +3,23 @@ const { SafeSet, StringPrototypeToLowerCase, + TypedArrayPrototypeGetBuffer, } = primordials; const { Buffer } = require('buffer'); const { - ECKeyExportJob, KeyObjectHandle, SignJob, kCryptoJobAsync, + kKeyFormatDER, kKeyTypePrivate, kKeyTypePublic, kSignJobModeSign, kSignJobModeVerify, + kWebCryptoKeyFormatPKCS8, + kWebCryptoKeyFormatRaw, + kWebCryptoKeyFormatSPKI, } = internalBinding('crypto'); const { @@ -196,10 +200,30 @@ async function cfrgGenerateKey(algorithm, extractable, keyUsages) { } function cfrgExportKey(key, format) { - return jobPromise(() => new ECKeyExportJob( - kCryptoJobAsync, - format, - key[kKeyObject][kHandle])); + try { + switch (format) { + case kWebCryptoKeyFormatRaw: { + if (key[kKeyType] === 'private') { + return TypedArrayPrototypeGetBuffer(key[kKeyObject][kHandle].rawPrivateKey()); + } + return TypedArrayPrototypeGetBuffer(key[kKeyObject][kHandle].rawPublicKey()); + } + case kWebCryptoKeyFormatSPKI: { + return TypedArrayPrototypeGetBuffer( + key[kKeyObject][kHandle].export(kKeyFormatDER, kWebCryptoKeyFormatSPKI)); + } + case kWebCryptoKeyFormatPKCS8: { + return TypedArrayPrototypeGetBuffer( + key[kKeyObject][kHandle].export(kKeyFormatDER, kWebCryptoKeyFormatPKCS8, null, null)); + } + default: + return undefined; + } + } catch (err) { + throw lazyDOMException( + 'The operation failed for an operation-specific reason', + { name: 'OperationError', cause: err }); + } } function cfrgImportKey( diff --git a/lib/internal/crypto/ec.js b/lib/internal/crypto/ec.js index dd7997c82cbf91..29bd58db1cdf53 100644 --- a/lib/internal/crypto/ec.js +++ b/lib/internal/crypto/ec.js @@ -2,19 +2,30 @@ const { SafeSet, + TypedArrayPrototypeGetBuffer, + TypedArrayPrototypeGetByteLength, } = primordials; const { - ECKeyExportJob, KeyObjectHandle, SignJob, kCryptoJobAsync, + kKeyFormatDER, kKeyTypePrivate, kSignJobModeSign, kSignJobModeVerify, kSigEncP1363, + kWebCryptoKeyFormatPKCS8, + kWebCryptoKeyFormatRaw, + kWebCryptoKeyFormatSPKI, } = internalBinding('crypto'); +const { + crypto: { + POINT_CONVERSION_UNCOMPRESSED, + }, +} = internalBinding('constants'); + const { getUsagesUnion, hasAnyNotIn, @@ -41,6 +52,7 @@ const { PublicKeyObject, createPrivateKey, createPublicKey, + kAlgorithm, kKeyType, } = require('internal/crypto/keys'); @@ -139,10 +151,40 @@ async function ecGenerateKey(algorithm, extractable, keyUsages) { } function ecExportKey(key, format) { - return jobPromise(() => new ECKeyExportJob( - kCryptoJobAsync, - format, - key[kKeyObject][kHandle])); + try { + const handle = key[kKeyObject][kHandle]; + switch (format) { + case kWebCryptoKeyFormatRaw: { + return TypedArrayPrototypeGetBuffer( + handle.exportECPublicRaw(POINT_CONVERSION_UNCOMPRESSED)); + } + case kWebCryptoKeyFormatSPKI: { + let spki = handle.export(kKeyFormatDER, kWebCryptoKeyFormatSPKI); + // WebCrypto requires uncompressed point format for SPKI exports. + // This is a very rare edge case dependent on the imported key + // using compressed point format. + if (TypedArrayPrototypeGetByteLength(spki) !== { + '__proto__': null, 'P-256': 91, 'P-384': 120, 'P-521': 158, + }[key[kAlgorithm].namedCurve]) { + const raw = handle.exportECPublicRaw(POINT_CONVERSION_UNCOMPRESSED); + const tmp = new KeyObjectHandle(); + tmp.initECRaw(kNamedCurveAliases[key[kAlgorithm].namedCurve], raw); + spki = tmp.export(kKeyFormatDER, kWebCryptoKeyFormatSPKI); + } + return TypedArrayPrototypeGetBuffer(spki); + } + case kWebCryptoKeyFormatPKCS8: { + return TypedArrayPrototypeGetBuffer( + handle.export(kKeyFormatDER, kWebCryptoKeyFormatPKCS8, null, null)); + } + default: + return undefined; + } + } catch (err) { + throw lazyDOMException( + 'The operation failed for an operation-specific reason', + { name: 'OperationError', cause: err }); + } } function ecImportKey( diff --git a/lib/internal/crypto/keys.js b/lib/internal/crypto/keys.js index a3609690adeb36..8facbe46ebafd8 100644 --- a/lib/internal/crypto/keys.js +++ b/lib/internal/crypto/keys.js @@ -27,6 +27,13 @@ const { kKeyEncodingSEC1, } = internalBinding('crypto'); +const { + crypto: { + POINT_CONVERSION_COMPRESSED, + POINT_CONVERSION_UNCOMPRESSED, + }, +} = internalBinding('constants'); + const { validateObject, validateOneOf, @@ -82,6 +89,7 @@ const kKeyUsages = Symbol('kKeyUsages'); const kCachedAlgorithm = Symbol('kCachedAlgorithm'); const kCachedKeyUsages = Symbol('kCachedKeyUsages'); + // Key input contexts. const kConsumePublic = 0; const kConsumePrivate = 1; @@ -340,14 +348,27 @@ const { } export(options) { - if (options && options.format === 'jwk') { - return this[kHandle].exportJwk({}, false); + switch (options?.format) { + case 'jwk': + return this[kHandle].exportJwk({}, false); + case 'raw-public': { + if (this.asymmetricKeyType === 'ec') { + const { type = 'compressed' } = options; + validateOneOf(type, 'options.type', ['compressed', 'uncompressed']); + const form = type === 'compressed' ? + POINT_CONVERSION_COMPRESSED : POINT_CONVERSION_UNCOMPRESSED; + return this[kHandle].exportECPublicRaw(form); + } + return this[kHandle].rawPublicKey(); + } + default: { + const { + format, + type, + } = parsePublicKeyEncoding(options, this.asymmetricKeyType); + return this[kHandle].export(format, type); + } } - const { - format, - type, - } = parsePublicKeyEncoding(options, this.asymmetricKeyType); - return this[kHandle].export(format, type); } } @@ -357,20 +378,32 @@ const { } export(options) { - if (options && options.format === 'jwk') { - if (options.passphrase !== undefined) { - throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS( - 'jwk', 'does not support encryption'); + if (options?.passphrase !== undefined && + options.format !== 'pem' && options.format !== 'der') { + throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS( + options.format, 'does not support encryption'); + } + switch (options?.format) { + case 'jwk': + return this[kHandle].exportJwk({}, false); + case 'raw-private': { + if (this.asymmetricKeyType === 'ec') { + return this[kHandle].exportECPrivateRaw(); + } + return this[kHandle].rawPrivateKey(); + } + case 'raw-seed': + return this[kHandle].rawSeed(); + default: { + const { + format, + type, + cipher, + passphrase, + } = parsePrivateKeyEncoding(options, this.asymmetricKeyType); + return this[kHandle].export(format, type, cipher, passphrase); } - return this[kHandle].exportJwk({}, false); } - const { - format, - type, - cipher, - passphrase, - } = parsePrivateKeyEncoding(options, this.asymmetricKeyType); - return this[kHandle].export(format, type, cipher, passphrase); } } @@ -549,13 +582,8 @@ function mlDsaPubLen(alg) { function getKeyObjectHandleFromJwk(key, ctx) { validateObject(key, 'key'); - if (KeyObjectHandle.prototype.initPqcRaw) { - validateOneOf( - key.kty, 'key.kty', ['RSA', 'EC', 'OKP', 'AKP']); - } else { - validateOneOf( - key.kty, 'key.kty', ['RSA', 'EC', 'OKP']); - } + validateOneOf( + key.kty, 'key.kty', ['RSA', 'EC', 'OKP', 'AKP']); const isPublic = ctx === kConsumePublic || ctx === kCreatePublic; if (key.kty === 'AKP') { @@ -691,6 +719,79 @@ function getKeyObjectHandleFromJwk(key, ctx) { return handle; } + +function getKeyObjectHandleFromRaw(options, data, format) { + if (!isStringOrBuffer(data)) { + throw new ERR_INVALID_ARG_TYPE( + 'key.key', + ['ArrayBuffer', 'Buffer', 'TypedArray', 'DataView'], + data); + } + + const keyData = getArrayBufferOrView(data, 'key.key'); + + validateString(options.asymmetricKeyType, 'key.asymmetricKeyType'); + const asymmetricKeyType = options.asymmetricKeyType; + + const handle = new KeyObjectHandle(); + + switch (asymmetricKeyType) { + case 'ec': { + validateString(options.namedCurve, 'key.namedCurve'); + if (format === 'raw-public') { + if (!handle.initECRaw(options.namedCurve, keyData)) { + throw new ERR_INVALID_ARG_VALUE('key.key', keyData); + } + } else if (!handle.initECPrivateRaw(options.namedCurve, keyData)) { + throw new ERR_INVALID_ARG_VALUE('key.key', keyData); + } + return handle; + } + case 'ed25519': + case 'ed448': + case 'x25519': + case 'x448': { + const keyType = format === 'raw-public' ? kKeyTypePublic : kKeyTypePrivate; + if (!handle.initEDRaw(asymmetricKeyType, keyData, keyType)) { + throw new ERR_INVALID_ARG_VALUE('key.key', keyData); + } + return handle; + } + case 'rsa': + case 'rsa-pss': + case 'dsa': + case 'dh': + throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS( + format, `is not supported for ${asymmetricKeyType} keys`); + case 'ml-dsa-44': + case 'ml-dsa-65': + case 'ml-dsa-87': + case 'ml-kem-512': + case 'ml-kem-768': + case 'ml-kem-1024': + case 'slh-dsa-sha2-128f': + case 'slh-dsa-sha2-128s': + case 'slh-dsa-sha2-192f': + case 'slh-dsa-sha2-192s': + case 'slh-dsa-sha2-256f': + case 'slh-dsa-sha2-256s': + case 'slh-dsa-shake-128f': + case 'slh-dsa-shake-128s': + case 'slh-dsa-shake-192f': + case 'slh-dsa-shake-192s': + case 'slh-dsa-shake-256f': + case 'slh-dsa-shake-256s': { + const keyType = format === 'raw-public' ? kKeyTypePublic : kKeyTypePrivate; + if (!handle.initPqcRaw(asymmetricKeyType, keyData, keyType)) { + throw new ERR_INVALID_ARG_VALUE('key.key', keyData); + } + return handle; + } + default: + throw new ERR_INVALID_ARG_VALUE('asymmetricKeyType', asymmetricKeyType); + } +} + function prepareAsymmetricKey(key, ctx) { if (isKeyObject(key)) { // Best case: A key object, as simple as that. @@ -712,6 +813,12 @@ function prepareAsymmetricKey(key, ctx) { else if (format === 'jwk') { validateObject(data, 'key.key'); return { data: getKeyObjectHandleFromJwk(data, ctx), format: 'jwk' }; + } else if (format === 'raw-public' || format === 'raw-private' || + format === 'raw-seed') { + return { + data: getKeyObjectHandleFromRaw(key, data, format), + format, + }; } // Either PEM or DER using PKCS#1 or SPKI. @@ -777,7 +884,7 @@ function createPublicKey(key) { const { format, type, data, passphrase } = prepareAsymmetricKey(key, kCreatePublic); let handle; - if (format === 'jwk') { + if (format === 'jwk' || format === 'raw-public') { handle = data; } else { handle = new KeyObjectHandle(); @@ -790,7 +897,7 @@ function createPrivateKey(key) { const { format, type, data, passphrase } = prepareAsymmetricKey(key, kCreatePrivate); let handle; - if (format === 'jwk') { + if (format === 'jwk' || format === 'raw-private' || format === 'raw-seed') { handle = data; } else { handle = new KeyObjectHandle(); diff --git a/lib/internal/crypto/ml_dsa.js b/lib/internal/crypto/ml_dsa.js index 11a7e8d843a0a7..e6559d6166edc1 100644 --- a/lib/internal/crypto/ml_dsa.js +++ b/lib/internal/crypto/ml_dsa.js @@ -4,8 +4,7 @@ const { SafeSet, StringPrototypeToLowerCase, TypedArrayPrototypeGetBuffer, - TypedArrayPrototypeSet, - Uint8Array, + TypedArrayPrototypeGetByteLength, } = primordials; const { Buffer } = require('buffer'); @@ -54,7 +53,6 @@ const { PublicKeyObject, createPrivateKey, createPublicKey, - kAlgorithm, kKeyType, } = require('internal/crypto/keys'); @@ -134,21 +132,15 @@ function mlDsaExportKey(key, format) { return TypedArrayPrototypeGetBuffer(key[kKeyObject][kHandle].export(kKeyFormatDER, kWebCryptoKeyFormatSPKI)); } case kWebCryptoKeyFormatPKCS8: { - const seed = key[kKeyObject][kHandle].rawSeed(); - const buffer = new Uint8Array(54); - const orc = { - '__proto__': null, - 'ML-DSA-44': 0x11, - 'ML-DSA-65': 0x12, - 'ML-DSA-87': 0x13, - }[key[kAlgorithm].name]; - TypedArrayPrototypeSet(buffer, [ - 0x30, 0x34, 0x02, 0x01, 0x00, 0x30, 0x0b, 0x06, - 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, - 0x03, orc, 0x04, 0x22, 0x80, 0x20, - ], 0); - TypedArrayPrototypeSet(buffer, seed, 22); - return TypedArrayPrototypeGetBuffer(buffer); + const pkcs8 = key[kKeyObject][kHandle].export(kKeyFormatDER, kWebCryptoKeyFormatPKCS8, null, null); + // Edge case only possible when user creates a seedless KeyObject + // first and converts it with KeyObject.prototype.toCryptoKey. + if (TypedArrayPrototypeGetByteLength(pkcs8) !== 54) { + throw lazyDOMException( + 'The operation failed for an operation-specific reason', + { name: 'OperationError' }); + } + return TypedArrayPrototypeGetBuffer(pkcs8); } default: return undefined; diff --git a/lib/internal/crypto/ml_kem.js b/lib/internal/crypto/ml_kem.js index 9cf44efc37f20a..0ff7e1bd22b69a 100644 --- a/lib/internal/crypto/ml_kem.js +++ b/lib/internal/crypto/ml_kem.js @@ -5,8 +5,7 @@ const { SafeSet, StringPrototypeToLowerCase, TypedArrayPrototypeGetBuffer, - TypedArrayPrototypeSet, - Uint8Array, + TypedArrayPrototypeGetByteLength, } = primordials; const { @@ -44,7 +43,6 @@ const { PublicKeyObject, createPrivateKey, createPublicKey, - kAlgorithm, kKeyType, } = require('internal/crypto/keys'); @@ -105,21 +103,15 @@ function mlKemExportKey(key, format) { return TypedArrayPrototypeGetBuffer(key[kKeyObject][kHandle].export(kKeyFormatDER, kWebCryptoKeyFormatSPKI)); } case kWebCryptoKeyFormatPKCS8: { - const seed = key[kKeyObject][kHandle].rawSeed(); - const buffer = new Uint8Array(86); - const orc = { - '__proto__': null, - 'ML-KEM-512': 0x01, - 'ML-KEM-768': 0x02, - 'ML-KEM-1024': 0x03, - }[key[kAlgorithm].name]; - TypedArrayPrototypeSet(buffer, [ - 0x30, 0x54, 0x02, 0x01, 0x00, 0x30, 0x0b, 0x06, - 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, - 0x04, orc, 0x04, 0x42, 0x80, 0x40, - ], 0); - TypedArrayPrototypeSet(buffer, seed, 22); - return TypedArrayPrototypeGetBuffer(buffer); + const pkcs8 = key[kKeyObject][kHandle].export(kKeyFormatDER, kWebCryptoKeyFormatPKCS8, null, null); + // Edge case only possible when user creates a seedless KeyObject + // first and converts it with KeyObject.prototype.toCryptoKey. + if (TypedArrayPrototypeGetByteLength(pkcs8) !== 86) { + throw lazyDOMException( + 'The operation failed for an operation-specific reason', + { name: 'OperationError' }); + } + return TypedArrayPrototypeGetBuffer(pkcs8); } default: return undefined; diff --git a/lib/internal/crypto/rsa.js b/lib/internal/crypto/rsa.js index c6b3985dbaee66..52c1257f48b0c6 100644 --- a/lib/internal/crypto/rsa.js +++ b/lib/internal/crypto/rsa.js @@ -3,22 +3,23 @@ const { MathCeil, SafeSet, + TypedArrayPrototypeGetBuffer, Uint8Array, } = primordials; const { KeyObjectHandle, RSACipherJob, - RSAKeyExportJob, SignJob, kCryptoJobAsync, + kKeyFormatDER, kSignJobModeSign, kSignJobModeVerify, - kKeyVariantRSA_SSA_PKCS1_v1_5, - kKeyVariantRSA_PSS, kKeyVariantRSA_OAEP, kKeyTypePrivate, kWebCryptoCipherEncrypt, + kWebCryptoKeyFormatPKCS8, + kWebCryptoKeyFormatSPKI, RSA_PKCS1_PSS_PADDING, } = internalBinding('crypto'); @@ -58,11 +59,6 @@ const { generateKeyPair: _generateKeyPair, } = require('internal/crypto/keygen'); -const kRsaVariants = { - 'RSASSA-PKCS1-v1_5': kKeyVariantRSA_SSA_PKCS1_v1_5, - 'RSA-PSS': kKeyVariantRSA_PSS, - 'RSA-OAEP': kKeyVariantRSA_OAEP, -}; const generateKeyPair = promisify(_generateKeyPair); function verifyAcceptableRsaKeyUse(name, isPublic, usages) { @@ -202,11 +198,24 @@ async function rsaKeyGenerate( } function rsaExportKey(key, format) { - return jobPromise(() => new RSAKeyExportJob( - kCryptoJobAsync, - format, - key[kKeyObject][kHandle], - kRsaVariants[key[kAlgorithm].name])); + try { + switch (format) { + case kWebCryptoKeyFormatSPKI: { + return TypedArrayPrototypeGetBuffer( + key[kKeyObject][kHandle].export(kKeyFormatDER, kWebCryptoKeyFormatSPKI)); + } + case kWebCryptoKeyFormatPKCS8: { + return TypedArrayPrototypeGetBuffer( + key[kKeyObject][kHandle].export(kKeyFormatDER, kWebCryptoKeyFormatPKCS8, null, null)); + } + default: + return undefined; + } + } catch (err) { + throw lazyDOMException( + 'The operation failed for an operation-specific reason', + { name: 'OperationError', cause: err }); + } } function rsaImportKey( diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index 3ba8e9c63846d2..0d87c45b31db80 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -392,12 +392,12 @@ async function exportKeySpki(key) { case 'RSA-PSS': // Fall through case 'RSA-OAEP': - return await require('internal/crypto/rsa') + return require('internal/crypto/rsa') .rsaExportKey(key, kWebCryptoKeyFormatSPKI); case 'ECDSA': // Fall through case 'ECDH': - return await require('internal/crypto/ec') + return require('internal/crypto/ec') .ecExportKey(key, kWebCryptoKeyFormatSPKI); case 'Ed25519': // Fall through @@ -406,14 +406,13 @@ async function exportKeySpki(key) { case 'X25519': // Fall through case 'X448': - return await require('internal/crypto/cfrg') + return require('internal/crypto/cfrg') .cfrgExportKey(key, kWebCryptoKeyFormatSPKI); case 'ML-DSA-44': // Fall through case 'ML-DSA-65': // Fall through case 'ML-DSA-87': - // Note: mlDsaExportKey does not return a Promise. return require('internal/crypto/ml_dsa') .mlDsaExportKey(key, kWebCryptoKeyFormatSPKI); case 'ML-KEM-512': @@ -421,7 +420,6 @@ async function exportKeySpki(key) { case 'ML-KEM-768': // Fall through case 'ML-KEM-1024': - // Note: mlKemExportKey does not return a Promise. return require('internal/crypto/ml_kem') .mlKemExportKey(key, kWebCryptoKeyFormatSPKI); default: @@ -436,12 +434,12 @@ async function exportKeyPkcs8(key) { case 'RSA-PSS': // Fall through case 'RSA-OAEP': - return await require('internal/crypto/rsa') + return require('internal/crypto/rsa') .rsaExportKey(key, kWebCryptoKeyFormatPKCS8); case 'ECDSA': // Fall through case 'ECDH': - return await require('internal/crypto/ec') + return require('internal/crypto/ec') .ecExportKey(key, kWebCryptoKeyFormatPKCS8); case 'Ed25519': // Fall through @@ -450,14 +448,13 @@ async function exportKeyPkcs8(key) { case 'X25519': // Fall through case 'X448': - return await require('internal/crypto/cfrg') + return require('internal/crypto/cfrg') .cfrgExportKey(key, kWebCryptoKeyFormatPKCS8); case 'ML-DSA-44': // Fall through case 'ML-DSA-65': // Fall through case 'ML-DSA-87': - // Note: mlDsaExportKey does not return a Promise. return require('internal/crypto/ml_dsa') .mlDsaExportKey(key, kWebCryptoKeyFormatPKCS8); case 'ML-KEM-512': @@ -465,7 +462,6 @@ async function exportKeyPkcs8(key) { case 'ML-KEM-768': // Fall through case 'ML-KEM-1024': - // Note: mlKemExportKey does not return a Promise. return require('internal/crypto/ml_kem') .mlKemExportKey(key, kWebCryptoKeyFormatPKCS8); default: @@ -478,7 +474,7 @@ async function exportKeyRawPublic(key, format) { case 'ECDSA': // Fall through case 'ECDH': - return await require('internal/crypto/ec') + return require('internal/crypto/ec') .ecExportKey(key, kWebCryptoKeyFormatRaw); case 'Ed25519': // Fall through @@ -487,7 +483,7 @@ async function exportKeyRawPublic(key, format) { case 'X25519': // Fall through case 'X448': - return await require('internal/crypto/cfrg') + return require('internal/crypto/cfrg') .cfrgExportKey(key, kWebCryptoKeyFormatRaw); case 'ML-DSA-44': // Fall through @@ -498,7 +494,6 @@ async function exportKeyRawPublic(key, format) { if (format !== 'raw-public') { return undefined; } - // Note: mlDsaExportKey does not return a Promise. return require('internal/crypto/ml_dsa') .mlDsaExportKey(key, kWebCryptoKeyFormatRaw); } @@ -511,7 +506,6 @@ async function exportKeyRawPublic(key, format) { if (format !== 'raw-public') { return undefined; } - // Note: mlKemExportKey does not return a Promise. return require('internal/crypto/ml_kem') .mlKemExportKey(key, kWebCryptoKeyFormatRaw); } @@ -527,7 +521,6 @@ async function exportKeyRawSeed(key) { case 'ML-DSA-65': // Fall through case 'ML-DSA-87': - // Note: mlDsaExportKey does not return a Promise. return require('internal/crypto/ml_dsa') .mlDsaExportKey(key, kWebCryptoKeyFormatRaw); case 'ML-KEM-512': @@ -535,7 +528,6 @@ async function exportKeyRawSeed(key) { case 'ML-KEM-768': // Fall through case 'ML-KEM-1024': - // Note: mlKemExportKey does not return a Promise. return require('internal/crypto/ml_kem') .mlKemExportKey(key, kWebCryptoKeyFormatRaw); default: diff --git a/src/crypto/README.md b/src/crypto/README.md index 3f4400f595d356..0a2a5d6f285f60 100644 --- a/src/crypto/README.md +++ b/src/crypto/README.md @@ -180,14 +180,12 @@ The `CryptoJob` class itself is a C++ template that takes a single `CryptoJobTraits` struct as a parameter. The `CryptoJobTraits` provides the implementation detail of the job. -There are (currently) four basic `CryptoJob` specializations: +There are (currently) three basic `CryptoJob` specializations: * `CipherJob` (defined in `src/crypto_cipher.h`) -- Used for encrypt and decrypt operations. * `KeyGenJob` (defined in `src/crypto_keygen.h`) -- Used for secret and key pair generation operations. -* `KeyExportJob` (defined in `src/crypto_keys.h`) -- Used for - key export operations. * `DeriveBitsJob` (defined in `src/crypto_util.h`) -- Used for key and byte derivation operations. @@ -236,9 +234,6 @@ if successful. For `CipherJob` types, the output is always an `ArrayBuffer`. -For `KeyExportJob` types, the output is either an `ArrayBuffer` or -a JavaScript object (for JWK output format); - For `KeyGenJob` types, the output is either a single KeyObject, or an array containing a Public/Private key pair represented either as a `KeyObjectHandle` object or a `Buffer`. diff --git a/src/crypto/crypto_dh.cc b/src/crypto/crypto_dh.cc index e35fda9ad2e8c5..f2c43b796cf481 100644 --- a/src/crypto/crypto_dh.cc +++ b/src/crypto/crypto_dh.cc @@ -472,34 +472,6 @@ EVPKeyCtxPointer DhKeyGenTraits::Setup(DhKeyPairGenConfig* params) { return ctx; } -Maybe DHKeyExportTraits::AdditionalConfig( - const FunctionCallbackInfo& args, - unsigned int offset, - DHKeyExportConfig* params) { - return JustVoid(); -} - -WebCryptoKeyExportStatus DHKeyExportTraits::DoExport( - const KeyObjectData& key_data, - WebCryptoKeyFormat format, - const DHKeyExportConfig& params, - ByteSource* out) { - CHECK_NE(key_data.GetKeyType(), kKeyTypeSecret); - - switch (format) { - case kWebCryptoKeyFormatPKCS8: - if (key_data.GetKeyType() != kKeyTypePrivate) - return WebCryptoKeyExportStatus::INVALID_KEY_TYPE; - return PKEY_PKCS8_Export(key_data, out); - case kWebCryptoKeyFormatSPKI: - if (key_data.GetKeyType() != kKeyTypePublic) - return WebCryptoKeyExportStatus::INVALID_KEY_TYPE; - return PKEY_SPKI_Export(key_data, out); - default: - UNREACHABLE(); - } -} - Maybe DHBitsTraits::AdditionalConfig( CryptoJobMode mode, const FunctionCallbackInfo& args, @@ -600,7 +572,6 @@ void DiffieHellman::Initialize(Environment* env, Local target) { DiffieHellmanGroup); DHKeyPairGenJob::Initialize(env, target); - DHKeyExportJob::Initialize(env, target); DHBitsJob::Initialize(env, target); } @@ -621,7 +592,6 @@ void DiffieHellman::RegisterExternalReferences( registry->Register(Check); DHKeyPairGenJob::RegisterExternalReferences(registry); - DHKeyExportJob::RegisterExternalReferences(registry); DHBitsJob::RegisterExternalReferences(registry); } diff --git a/src/crypto/crypto_dh.h b/src/crypto/crypto_dh.h index 53e3a98c3918bd..72718ef757b16c 100644 --- a/src/crypto/crypto_dh.h +++ b/src/crypto/crypto_dh.h @@ -60,29 +60,6 @@ struct DhKeyGenTraits final { using DHKeyPairGenJob = KeyGenJob>; -struct DHKeyExportConfig final : public MemoryRetainer { - SET_NO_MEMORY_INFO() - SET_MEMORY_INFO_NAME(DHKeyExportConfig) - SET_SELF_SIZE(DHKeyExportConfig) -}; - -struct DHKeyExportTraits final { - static constexpr const char* JobName = "DHKeyExportJob"; - using AdditionalParameters = DHKeyExportConfig; - - static v8::Maybe AdditionalConfig( - const v8::FunctionCallbackInfo& args, - unsigned int offset, - DHKeyExportConfig* config); - - static WebCryptoKeyExportStatus DoExport(const KeyObjectData& key_data, - WebCryptoKeyFormat format, - const DHKeyExportConfig& params, - ByteSource* out); -}; - -using DHKeyExportJob = KeyExportJob; - struct DHBitsConfig final : public MemoryRetainer { KeyObjectData private_key; KeyObjectData public_key; diff --git a/src/crypto/crypto_ec.cc b/src/crypto/crypto_ec.cc index 95e1a68070ed67..dcf999fd3f3ca6 100644 --- a/src/crypto/crypto_ec.cc +++ b/src/crypto/crypto_ec.cc @@ -70,7 +70,6 @@ void ECDH::Initialize(Environment* env, Local target) { ECDHBitsJob::Initialize(env, target); ECKeyPairGenJob::Initialize(env, target); - ECKeyExportJob::Initialize(env, target); NODE_DEFINE_CONSTANT(target, OPENSSL_EC_NAMED_CURVE); NODE_DEFINE_CONSTANT(target, OPENSSL_EC_EXPLICIT_CURVE); @@ -89,7 +88,6 @@ void ECDH::RegisterExternalReferences(ExternalReferenceRegistry* registry) { ECDHBitsJob::RegisterExternalReferences(registry); ECKeyPairGenJob::RegisterExternalReferences(registry); - ECKeyExportJob::RegisterExternalReferences(registry); } void ECDH::GetCurves(const FunctionCallbackInfo& args) { @@ -559,137 +557,6 @@ Maybe EcKeyGenTraits::AdditionalConfig( return JustVoid(); } -namespace { -WebCryptoKeyExportStatus EC_Raw_Export(const KeyObjectData& key_data, - const ECKeyExportConfig& params, - ByteSource* out) { - const auto& m_pkey = key_data.GetAsymmetricKey(); - CHECK(m_pkey); - Mutex::ScopedLock lock(key_data.mutex()); - - const EC_KEY* ec_key = m_pkey; - - if (ec_key == nullptr) { - switch (key_data.GetKeyType()) { - case kKeyTypePrivate: { - auto data = m_pkey.rawPrivateKey(); - if (!data) return WebCryptoKeyExportStatus::INVALID_KEY_TYPE; - DCHECK(!data.isSecure()); - *out = ByteSource::Allocated(data.release()); - break; - } - case kKeyTypePublic: { - auto data = m_pkey.rawPublicKey(); - if (!data) return WebCryptoKeyExportStatus::INVALID_KEY_TYPE; - DCHECK(!data.isSecure()); - *out = ByteSource::Allocated(data.release()); - break; - } - case kKeyTypeSecret: - UNREACHABLE(); - } - } else { - if (key_data.GetKeyType() != kKeyTypePublic) - return WebCryptoKeyExportStatus::INVALID_KEY_TYPE; - const auto group = ECKeyPointer::GetGroup(ec_key); - const auto point = ECKeyPointer::GetPublicKey(ec_key); - point_conversion_form_t form = POINT_CONVERSION_UNCOMPRESSED; - - // Get the allocated data size... - size_t len = EC_POINT_point2oct(group, point, form, nullptr, 0, nullptr); - if (len == 0) - return WebCryptoKeyExportStatus::FAILED; - auto data = DataPointer::Alloc(len); - size_t check_len = - EC_POINT_point2oct(group, - point, - form, - static_cast(data.get()), - len, - nullptr); - if (check_len == 0) - return WebCryptoKeyExportStatus::FAILED; - - CHECK_EQ(len, check_len); - *out = ByteSource::Allocated(data.release()); - } - - return WebCryptoKeyExportStatus::OK; -} -} // namespace - -Maybe ECKeyExportTraits::AdditionalConfig( - const FunctionCallbackInfo& args, - unsigned int offset, - ECKeyExportConfig* params) { - return JustVoid(); -} - -WebCryptoKeyExportStatus ECKeyExportTraits::DoExport( - const KeyObjectData& key_data, - WebCryptoKeyFormat format, - const ECKeyExportConfig& params, - ByteSource* out) { - CHECK_NE(key_data.GetKeyType(), kKeyTypeSecret); - - switch (format) { - case kWebCryptoKeyFormatRaw: - return EC_Raw_Export(key_data, params, out); - case kWebCryptoKeyFormatPKCS8: - if (key_data.GetKeyType() != kKeyTypePrivate) - return WebCryptoKeyExportStatus::INVALID_KEY_TYPE; - return PKEY_PKCS8_Export(key_data, out); - case kWebCryptoKeyFormatSPKI: { - if (key_data.GetKeyType() != kKeyTypePublic) - return WebCryptoKeyExportStatus::INVALID_KEY_TYPE; - - const auto& m_pkey = key_data.GetAsymmetricKey(); - if (m_pkey.id() != EVP_PKEY_EC) { - return PKEY_SPKI_Export(key_data, out); - } else { - // Ensure exported key is in uncompressed point format. - // The temporary EC key is so we can have i2d_PUBKEY_bio() write out - // the header but it is a somewhat silly hoop to jump through because - // the header is for all practical purposes a static 26 byte sequence - // where only the second byte changes. - Mutex::ScopedLock lock(key_data.mutex()); - const auto group = ECKeyPointer::GetGroup(m_pkey); - const auto point = ECKeyPointer::GetPublicKey(m_pkey); - const point_conversion_form_t form = POINT_CONVERSION_UNCOMPRESSED; - const size_t need = - EC_POINT_point2oct(group, point, form, nullptr, 0, nullptr); - if (need == 0) return WebCryptoKeyExportStatus::FAILED; - auto data = DataPointer::Alloc(need); - const size_t have = - EC_POINT_point2oct(group, - point, - form, - static_cast(data.get()), - need, - nullptr); - if (have == 0) return WebCryptoKeyExportStatus::FAILED; - auto ec = ECKeyPointer::New(group); - CHECK(ec); - auto uncompressed = ECPointPointer::New(group); - ncrypto::Buffer buffer{ - .data = static_cast(data.get()), - .len = data.size(), - }; - CHECK(uncompressed.setFromBuffer(buffer, group)); - CHECK(ec.setPublicKey(uncompressed)); - auto pkey = EVPKeyPointer::New(); - CHECK(pkey.set(ec)); - auto bio = pkey.derPublicKey(); - if (!bio) return WebCryptoKeyExportStatus::FAILED; - *out = ByteSource::FromBIO(bio); - return WebCryptoKeyExportStatus::OK; - } - } - default: - UNREACHABLE(); - } -} - bool ExportJWKEcKey(Environment* env, const KeyObjectData& key, Local target) { diff --git a/src/crypto/crypto_ec.h b/src/crypto/crypto_ec.h index 101d07e54e8e57..f7aaf2d523493a 100644 --- a/src/crypto/crypto_ec.h +++ b/src/crypto/crypto_ec.h @@ -114,32 +114,6 @@ struct EcKeyGenTraits final { using ECKeyPairGenJob = KeyGenJob>; -// There is currently no additional information that the -// ECKeyExport needs to collect, but we need to provide -// the base struct anyway. -struct ECKeyExportConfig final : public MemoryRetainer { - SET_NO_MEMORY_INFO() - SET_MEMORY_INFO_NAME(ECKeyExportConfig) - SET_SELF_SIZE(ECKeyExportConfig) -}; - -struct ECKeyExportTraits final { - static constexpr const char* JobName = "ECKeyExportJob"; - using AdditionalParameters = ECKeyExportConfig; - - static v8::Maybe AdditionalConfig( - const v8::FunctionCallbackInfo& args, - unsigned int offset, - ECKeyExportConfig* config); - - static WebCryptoKeyExportStatus DoExport(const KeyObjectData& key_data, - WebCryptoKeyFormat format, - const ECKeyExportConfig& params, - ByteSource* out); -}; - -using ECKeyExportJob = KeyExportJob; - bool ExportJWKEcKey(Environment* env, const KeyObjectData& key, v8::Local target); diff --git a/src/crypto/crypto_keys.cc b/src/crypto/crypto_keys.cc index 93a741fb1758a4..2a42ee9cf1c57f 100644 --- a/src/crypto/crypto_keys.cc +++ b/src/crypto/crypto_keys.cc @@ -19,12 +19,13 @@ namespace node { +using ncrypto::BignumPointer; using ncrypto::BIOPointer; using ncrypto::ECKeyPointer; +using ncrypto::ECPointPointer; using ncrypto::EVPKeyCtxPointer; using ncrypto::EVPKeyPointer; using ncrypto::MarkPopErrorOnReturn; -using ncrypto::PKCS8Pointer; using v8::Array; using v8::Context; using v8::Function; @@ -278,33 +279,39 @@ bool ExportJWKInner(Environment* env, } int GetNidFromName(const char* name) { - int nid; - if (strcmp(name, "Ed25519") == 0) { - nid = EVP_PKEY_ED25519; - } else if (strcmp(name, "Ed448") == 0) { - nid = EVP_PKEY_ED448; - } else if (strcmp(name, "X25519") == 0) { - nid = EVP_PKEY_X25519; - } else if (strcmp(name, "X448") == 0) { - nid = EVP_PKEY_X448; + static constexpr struct { + const char* name; + int nid; + } kNameToNid[] = { + {"Ed25519", EVP_PKEY_ED25519}, + {"Ed448", EVP_PKEY_ED448}, + {"X25519", EVP_PKEY_X25519}, + {"X448", EVP_PKEY_X448}, #if OPENSSL_WITH_PQC - } else if (strcmp(name, "ML-DSA-44") == 0) { - nid = EVP_PKEY_ML_DSA_44; - } else if (strcmp(name, "ML-DSA-65") == 0) { - 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; + {"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}, + {"SLH-DSA-SHA2-128f", EVP_PKEY_SLH_DSA_SHA2_128F}, + {"SLH-DSA-SHA2-128s", EVP_PKEY_SLH_DSA_SHA2_128S}, + {"SLH-DSA-SHA2-192f", EVP_PKEY_SLH_DSA_SHA2_192F}, + {"SLH-DSA-SHA2-192s", EVP_PKEY_SLH_DSA_SHA2_192S}, + {"SLH-DSA-SHA2-256f", EVP_PKEY_SLH_DSA_SHA2_256F}, + {"SLH-DSA-SHA2-256s", EVP_PKEY_SLH_DSA_SHA2_256S}, + {"SLH-DSA-SHAKE-128f", EVP_PKEY_SLH_DSA_SHAKE_128F}, + {"SLH-DSA-SHAKE-128s", EVP_PKEY_SLH_DSA_SHAKE_128S}, + {"SLH-DSA-SHAKE-192f", EVP_PKEY_SLH_DSA_SHAKE_192F}, + {"SLH-DSA-SHAKE-192s", EVP_PKEY_SLH_DSA_SHAKE_192S}, + {"SLH-DSA-SHAKE-256f", EVP_PKEY_SLH_DSA_SHAKE_256F}, + {"SLH-DSA-SHAKE-256s", EVP_PKEY_SLH_DSA_SHAKE_256S}, #endif - } else { - nid = NID_undef; + }; + for (const auto& entry : kNameToNid) { + if (StringEqualNoCase(name, entry.name)) return entry.nid; } - return nid; + return NID_undef; } } // namespace @@ -633,11 +640,15 @@ Local KeyObjectHandle::Initialize(Environment* env) { SetProtoMethod(isolate, templ, "exportJwk", ExportJWK); SetProtoMethod(isolate, templ, "initECRaw", InitECRaw); SetProtoMethod(isolate, templ, "initEDRaw", InitEDRaw); -#if OPENSSL_WITH_PQC - SetProtoMethod(isolate, templ, "initPqcRaw", InitPqcRaw); SetProtoMethodNoSideEffect(isolate, templ, "rawPublicKey", RawPublicKey); + SetProtoMethodNoSideEffect(isolate, templ, "rawPrivateKey", RawPrivateKey); + SetProtoMethod(isolate, templ, "initPqcRaw", InitPqcRaw); SetProtoMethodNoSideEffect(isolate, templ, "rawSeed", RawSeed); -#endif + SetProtoMethod(isolate, templ, "initECPrivateRaw", InitECPrivateRaw); + SetProtoMethodNoSideEffect( + isolate, templ, "exportECPublicRaw", ExportECPublicRaw); + SetProtoMethodNoSideEffect( + isolate, templ, "exportECPrivateRaw", ExportECPrivateRaw); SetProtoMethod(isolate, templ, "initJwk", InitJWK); SetProtoMethod(isolate, templ, "keyDetail", GetKeyDetail); SetProtoMethod(isolate, templ, "equals", Equals); @@ -658,11 +669,13 @@ void KeyObjectHandle::RegisterExternalReferences( registry->Register(ExportJWK); registry->Register(InitECRaw); registry->Register(InitEDRaw); -#if OPENSSL_WITH_PQC - registry->Register(InitPqcRaw); registry->Register(RawPublicKey); + registry->Register(RawPrivateKey); + registry->Register(InitPqcRaw); registry->Register(RawSeed); -#endif + registry->Register(InitECPrivateRaw); + registry->Register(ExportECPublicRaw); + registry->Register(ExportECPrivateRaw); registry->Register(InitJWK); registry->Register(GetKeyDetail); registry->Register(Equals); @@ -787,7 +800,9 @@ void KeyObjectHandle::InitECRaw(const FunctionCallbackInfo& args) { MarkPopErrorOnReturn mark_pop_error_on_return; - int id = OBJ_txt2nid(*name); + int id = ncrypto::Ec::GetCurveIdFromName(*name); + if (id == NID_undef) return THROW_ERR_CRYPTO_INVALID_CURVE(env); + auto eckey = ECKeyPointer::NewByCurveName(id); if (!eckey) return args.GetReturnValue().Set(false); @@ -848,14 +863,14 @@ void KeyObjectHandle::InitEDRaw(const FunctionCallbackInfo& args) { break; } default: - UNREACHABLE(); + return args.GetReturnValue().Set(false); } args.GetReturnValue().Set(true); } -#if OPENSSL_WITH_PQC void KeyObjectHandle::InitPqcRaw(const FunctionCallbackInfo& args) { +#if OPENSSL_WITH_PQC KeyObjectHandle* key; ASSIGN_OR_RETURN_UNWRAP(&key, args.This()); @@ -867,12 +882,11 @@ void KeyObjectHandle::InitPqcRaw(const FunctionCallbackInfo& args) { MarkPopErrorOnReturn mark_pop_error_on_return; + int id = GetNidFromName(*name); + typedef EVPKeyPointer (*new_key_fn)( int, const ncrypto::Buffer&); - new_key_fn fn = type == kKeyTypePrivate ? EVPKeyPointer::NewRawSeed - : EVPKeyPointer::NewRawPublic; - - int id = GetNidFromName(*name); + new_key_fn fn; switch (id) { case EVP_PKEY_ML_DSA_44: @@ -880,26 +894,46 @@ void KeyObjectHandle::InitPqcRaw(const FunctionCallbackInfo& args) { 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{ - .data = key_data.data(), - .len = key_data.size(), - }); - if (!pkey) { - return args.GetReturnValue().Set(false); - } - key->data_ = KeyObjectData::CreateAsymmetric(type, std::move(pkey)); - CHECK(key->data_); + case EVP_PKEY_ML_KEM_1024: + fn = type == kKeyTypePrivate ? EVPKeyPointer::NewRawSeed + : EVPKeyPointer::NewRawPublic; + break; + case EVP_PKEY_SLH_DSA_SHA2_128F: + case EVP_PKEY_SLH_DSA_SHA2_128S: + case EVP_PKEY_SLH_DSA_SHA2_192F: + case EVP_PKEY_SLH_DSA_SHA2_192S: + case EVP_PKEY_SLH_DSA_SHA2_256F: + case EVP_PKEY_SLH_DSA_SHA2_256S: + case EVP_PKEY_SLH_DSA_SHAKE_128F: + case EVP_PKEY_SLH_DSA_SHAKE_128S: + case EVP_PKEY_SLH_DSA_SHAKE_192F: + case EVP_PKEY_SLH_DSA_SHAKE_192S: + case EVP_PKEY_SLH_DSA_SHAKE_256F: + case EVP_PKEY_SLH_DSA_SHAKE_256S: + fn = type == kKeyTypePrivate ? EVPKeyPointer::NewRawPrivate + : EVPKeyPointer::NewRawPublic; break; - } default: - UNREACHABLE(); + return args.GetReturnValue().Set(false); + } + + auto pkey = fn(id, + ncrypto::Buffer{ + .data = key_data.data(), + .len = key_data.size(), + }); + if (!pkey) { + return args.GetReturnValue().Set(false); } + key->data_ = KeyObjectData::CreateAsymmetric(type, std::move(pkey)); + CHECK(key->data_); args.GetReturnValue().Set(true); -} +#else + Environment* env = Environment::GetCurrent(args); + THROW_ERR_INVALID_ARG_VALUE(env, "Unsupported key type"); #endif +} void KeyObjectHandle::Equals(const FunctionCallbackInfo& args) { KeyObjectHandle* self_handle; @@ -1125,7 +1159,6 @@ MaybeLocal KeyObjectHandle::ExportPrivateKey( return WritePrivateKey(env(), data_.GetAsymmetricKey(), config); } -#if OPENSSL_WITH_PQC void KeyObjectHandle::RawPublicKey( const v8::FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); @@ -1136,7 +1169,39 @@ void KeyObjectHandle::RawPublicKey( CHECK_NE(data.GetKeyType(), kKeyTypeSecret); Mutex::ScopedLock lock(data.mutex()); - auto raw_data = data.GetAsymmetricKey().rawPublicKey(); + const auto& pkey = data.GetAsymmetricKey(); + + switch (pkey.id()) { + case EVP_PKEY_ED25519: + case EVP_PKEY_ED448: + case EVP_PKEY_X25519: + case EVP_PKEY_X448: +#if OPENSSL_WITH_PQC + case EVP_PKEY_ML_DSA_44: + case EVP_PKEY_ML_DSA_65: + case EVP_PKEY_ML_DSA_87: + case EVP_PKEY_ML_KEM_512: + case EVP_PKEY_ML_KEM_768: + case EVP_PKEY_ML_KEM_1024: + case EVP_PKEY_SLH_DSA_SHA2_128F: + case EVP_PKEY_SLH_DSA_SHA2_128S: + case EVP_PKEY_SLH_DSA_SHA2_192F: + case EVP_PKEY_SLH_DSA_SHA2_192S: + case EVP_PKEY_SLH_DSA_SHA2_256F: + case EVP_PKEY_SLH_DSA_SHA2_256S: + case EVP_PKEY_SLH_DSA_SHAKE_128F: + case EVP_PKEY_SLH_DSA_SHAKE_128S: + case EVP_PKEY_SLH_DSA_SHAKE_192F: + case EVP_PKEY_SLH_DSA_SHAKE_192S: + case EVP_PKEY_SLH_DSA_SHAKE_256F: + case EVP_PKEY_SLH_DSA_SHAKE_256S: +#endif + break; + default: + return THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); + } + + auto raw_data = pkey.rawPublicKey(); if (!raw_data) { return THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to get raw public key"); @@ -1148,6 +1213,175 @@ void KeyObjectHandle::RawPublicKey( .FromMaybe(Local())); } +void KeyObjectHandle::RawPrivateKey( + const v8::FunctionCallbackInfo& 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()); + const auto& pkey = data.GetAsymmetricKey(); + + switch (pkey.id()) { + case EVP_PKEY_ED25519: + case EVP_PKEY_ED448: + case EVP_PKEY_X25519: + case EVP_PKEY_X448: +#if OPENSSL_WITH_PQC + case EVP_PKEY_SLH_DSA_SHA2_128F: + case EVP_PKEY_SLH_DSA_SHA2_128S: + case EVP_PKEY_SLH_DSA_SHA2_192F: + case EVP_PKEY_SLH_DSA_SHA2_192S: + case EVP_PKEY_SLH_DSA_SHA2_256F: + case EVP_PKEY_SLH_DSA_SHA2_256S: + case EVP_PKEY_SLH_DSA_SHAKE_128F: + case EVP_PKEY_SLH_DSA_SHAKE_128S: + case EVP_PKEY_SLH_DSA_SHAKE_192F: + case EVP_PKEY_SLH_DSA_SHAKE_192S: + case EVP_PKEY_SLH_DSA_SHAKE_256F: + case EVP_PKEY_SLH_DSA_SHAKE_256S: +#endif + break; + default: + return THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); + } + + auto raw_data = pkey.rawPrivateKey(); + if (!raw_data) { + return THROW_ERR_CRYPTO_OPERATION_FAILED(env, + "Failed to get raw private key"); + } + + args.GetReturnValue().Set( + Buffer::Copy( + env, reinterpret_cast(raw_data.get()), raw_data.size()) + .FromMaybe(Local())); +} + +void KeyObjectHandle::ExportECPublicRaw( + const v8::FunctionCallbackInfo& 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()); + const auto& m_pkey = data.GetAsymmetricKey(); + if (m_pkey.id() != EVP_PKEY_EC) { + return THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); + } + + const EC_KEY* ec_key = m_pkey; + CHECK_NOT_NULL(ec_key); + + CHECK(args[0]->IsInt32()); + auto form = + static_cast(args[0].As()->Value()); + + const auto group = ECKeyPointer::GetGroup(ec_key); + const auto point = ECKeyPointer::GetPublicKey(ec_key); + + Local buf; + if (!ECPointToBuffer(env, group, point, form).ToLocal(&buf)) return; + + args.GetReturnValue().Set(buf); +} + +void KeyObjectHandle::ExportECPrivateRaw( + const v8::FunctionCallbackInfo& 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()); + const auto& m_pkey = data.GetAsymmetricKey(); + if (m_pkey.id() != EVP_PKEY_EC) { + return THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); + } + + const EC_KEY* ec_key = m_pkey; + CHECK_NOT_NULL(ec_key); + + const BIGNUM* private_key = ECKeyPointer::GetPrivateKey(ec_key); + CHECK_NOT_NULL(private_key); + + const auto group = ECKeyPointer::GetGroup(ec_key); + auto order = BignumPointer::New(); + CHECK(order); + CHECK(EC_GROUP_get_order(group, order.get(), nullptr)); + + auto buf = BignumPointer::EncodePadded(private_key, order.byteLength()); + if (!buf) { + return THROW_ERR_CRYPTO_OPERATION_FAILED(env, + "Failed to export EC private key"); + } + + args.GetReturnValue().Set( + Buffer::Copy(env, reinterpret_cast(buf.get()), buf.size()) + .FromMaybe(Local())); +} + +void KeyObjectHandle::InitECPrivateRaw( + const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + KeyObjectHandle* key; + ASSIGN_OR_RETURN_UNWRAP(&key, args.This()); + + CHECK(args[0]->IsString()); + Utf8Value name(env->isolate(), args[0]); + + ArrayBufferOrViewContents key_data(args[1]); + + MarkPopErrorOnReturn mark_pop_error_on_return; + + int nid = ncrypto::Ec::GetCurveIdFromName(*name); + if (nid == NID_undef) return THROW_ERR_CRYPTO_INVALID_CURVE(env); + + auto eckey = ECKeyPointer::NewByCurveName(nid); + if (!eckey) return args.GetReturnValue().Set(false); + + // Validate key data size matches the curve's expected private key length + const auto group = eckey.getGroup(); + auto order = BignumPointer::New(); + CHECK(order); + CHECK(EC_GROUP_get_order(group, order.get(), nullptr)); + if (key_data.size() != order.byteLength()) + return args.GetReturnValue().Set(false); + + BignumPointer priv_bn(key_data.data(), key_data.size()); + if (!priv_bn) return args.GetReturnValue().Set(false); + + if (!eckey.setPrivateKey(priv_bn)) return args.GetReturnValue().Set(false); + + // Compute public key from private key + auto pub_point = ECPointPointer::New(group); + if (!pub_point || !pub_point.mul(group, priv_bn.get())) { + return args.GetReturnValue().Set(false); + } + + if (!eckey.setPublicKey(pub_point)) return args.GetReturnValue().Set(false); + + auto pkey = EVPKeyPointer::New(); + if (!pkey.assign(eckey)) { + return args.GetReturnValue().Set(false); + } + + eckey.release(); + + key->data_ = + KeyObjectData::CreateAsymmetric(kKeyTypePrivate, std::move(pkey)); + + args.GetReturnValue().Set(true); +} + void KeyObjectHandle::RawSeed(const v8::FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); KeyObjectHandle* key; @@ -1157,7 +1391,24 @@ void KeyObjectHandle::RawSeed(const v8::FunctionCallbackInfo& args) { CHECK_EQ(data.GetKeyType(), kKeyTypePrivate); Mutex::ScopedLock lock(data.mutex()); - auto raw_data = data.GetAsymmetricKey().rawSeed(); + const auto& pkey = data.GetAsymmetricKey(); + + switch (pkey.id()) { +#if OPENSSL_WITH_PQC + case EVP_PKEY_ML_DSA_44: + case EVP_PKEY_ML_DSA_65: + case EVP_PKEY_ML_DSA_87: + case EVP_PKEY_ML_KEM_512: + case EVP_PKEY_ML_KEM_768: + case EVP_PKEY_ML_KEM_1024: + break; +#endif + default: + return THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); + } + +#if OPENSSL_WITH_PQC + auto raw_data = pkey.rawSeed(); if (!raw_data) { return THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to get raw seed"); } @@ -1166,8 +1417,8 @@ void KeyObjectHandle::RawSeed(const v8::FunctionCallbackInfo& args) { Buffer::Copy( env, reinterpret_cast(raw_data.get()), raw_data.size()) .FromMaybe(Local())); -} #endif +} void KeyObjectHandle::ExportJWK( const v8::FunctionCallbackInfo& args) { @@ -1290,32 +1541,6 @@ std::unique_ptr NativeKeyObject::CloneForMessaging() return std::make_unique(handle_data_); } -WebCryptoKeyExportStatus PKEY_SPKI_Export(const KeyObjectData& key_data, - ByteSource* out) { - CHECK_EQ(key_data.GetKeyType(), kKeyTypePublic); - Mutex::ScopedLock lock(key_data.mutex()); - auto bio = key_data.GetAsymmetricKey().derPublicKey(); - if (!bio) return WebCryptoKeyExportStatus::FAILED; - *out = ByteSource::FromBIO(bio); - return WebCryptoKeyExportStatus::OK; -} - -WebCryptoKeyExportStatus PKEY_PKCS8_Export(const KeyObjectData& key_data, - ByteSource* out) { - CHECK_EQ(key_data.GetKeyType(), kKeyTypePrivate); - Mutex::ScopedLock lock(key_data.mutex()); - const auto& m_pkey = key_data.GetAsymmetricKey(); - - auto bio = BIOPointer::NewMem(); - CHECK(bio); - PKCS8Pointer p8inf(EVP_PKEY2PKCS8(m_pkey.get())); - if (!i2d_PKCS8_PRIV_KEY_INFO_bio(bio.get(), p8inf.get())) - return WebCryptoKeyExportStatus::FAILED; - - *out = ByteSource::FromBIO(bio); - return WebCryptoKeyExportStatus::OK; -} - namespace Keys { void Initialize(Environment* env, Local target) { target->Set(env->context(), diff --git a/src/crypto/crypto_keys.h b/src/crypto/crypto_keys.h index 90c252eba28bea..4a8438b38c9f1e 100644 --- a/src/crypto/crypto_keys.h +++ b/src/crypto/crypto_keys.h @@ -170,11 +170,15 @@ class KeyObjectHandle : public BaseObject { static void Export(const v8::FunctionCallbackInfo& args); -#if OPENSSL_WITH_PQC - static void InitPqcRaw(const v8::FunctionCallbackInfo& args); static void RawPublicKey(const v8::FunctionCallbackInfo& args); + static void RawPrivateKey(const v8::FunctionCallbackInfo& args); + static void ExportECPublicRaw( + const v8::FunctionCallbackInfo& args); + static void ExportECPrivateRaw( + const v8::FunctionCallbackInfo& args); + static void InitECPrivateRaw(const v8::FunctionCallbackInfo& args); + static void InitPqcRaw(const v8::FunctionCallbackInfo& args); static void RawSeed(const v8::FunctionCallbackInfo& args); -#endif v8::MaybeLocal ExportSecretKey() const; v8::MaybeLocal ExportPublicKey( @@ -241,147 +245,6 @@ enum WebCryptoKeyFormat { kWebCryptoKeyFormatJWK }; -enum class WebCryptoKeyExportStatus { - OK, - INVALID_KEY_TYPE, - FAILED -}; - -template -class KeyExportJob final : public CryptoJob { - public: - using AdditionalParams = typename KeyExportTraits::AdditionalParameters; - - static void New(const v8::FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - CHECK(args.IsConstructCall()); - - CryptoJobMode mode = GetCryptoJobMode(args[0]); - - CHECK(args[1]->IsUint32()); // Export Type - CHECK(args[2]->IsObject()); // KeyObject - - WebCryptoKeyFormat format = - static_cast(args[1].As()->Value()); - - KeyObjectHandle* key; - ASSIGN_OR_RETURN_UNWRAP(&key, args[2]); - - CHECK_NOT_NULL(key); - - AdditionalParams params; - if (KeyExportTraits::AdditionalConfig(args, 3, ¶ms).IsNothing()) { - // The KeyExportTraits::AdditionalConfig is responsible for - // calling an appropriate THROW_CRYPTO_* variant reporting - // whatever error caused initialization to fail. - return; - } - - new KeyExportJob( - env, - args.This(), - mode, - key->Data(), - format, - std::move(params)); - } - - static void Initialize( - Environment* env, - v8::Local target) { - CryptoJob::Initialize(New, env, target); - } - - static void RegisterExternalReferences(ExternalReferenceRegistry* registry) { - CryptoJob::RegisterExternalReferences(New, registry); - } - - KeyExportJob(Environment* env, - v8::Local object, - CryptoJobMode mode, - const KeyObjectData& key, - WebCryptoKeyFormat format, - AdditionalParams&& params) - : CryptoJob(env, - object, - AsyncWrap::PROVIDER_KEYEXPORTREQUEST, - mode, - std::move(params)), - key_(key.addRef()), - format_(format) {} - - WebCryptoKeyFormat format() const { return format_; } - - void DoThreadPoolWork() override { - const WebCryptoKeyExportStatus status = - KeyExportTraits::DoExport( - key_, - format_, - *CryptoJob::params(), - &out_); - if (status == WebCryptoKeyExportStatus::OK) { - // Success! - return; - } - CryptoErrorStore* errors = CryptoJob::errors(); - errors->Capture(); - if (errors->Empty()) { - switch (status) { - case WebCryptoKeyExportStatus::OK: - UNREACHABLE(); - break; - case WebCryptoKeyExportStatus::INVALID_KEY_TYPE: - errors->Insert(NodeCryptoError::INVALID_KEY_TYPE); - break; - case WebCryptoKeyExportStatus::FAILED: - errors->Insert(NodeCryptoError::CIPHER_JOB_FAILED); - break; - } - } - } - - v8::Maybe ToResult(v8::Local* err, - v8::Local* result) override { - Environment* env = AsyncWrap::env(); - CryptoErrorStore* errors = CryptoJob::errors(); - if (out_.size() > 0) { - CHECK(errors->Empty()); - *err = v8::Undefined(env->isolate()); - *result = out_.ToArrayBuffer(env); - if (result->IsEmpty()) { - return v8::Nothing(); - } - } else { - if (errors->Empty()) errors->Capture(); - CHECK(!errors->Empty()); - *result = v8::Undefined(env->isolate()); - if (!errors->ToException(env).ToLocal(err)) { - return v8::Nothing(); - } - } - CHECK(!result->IsEmpty()); - CHECK(!err->IsEmpty()); - return v8::JustVoid(); - } - - SET_SELF_SIZE(KeyExportJob) - void MemoryInfo(MemoryTracker* tracker) const override { - tracker->TrackFieldWithSize("out", out_.size()); - CryptoJob::MemoryInfo(tracker); - } - - private: - KeyObjectData key_; - WebCryptoKeyFormat format_; - ByteSource out_; -}; - -WebCryptoKeyExportStatus PKEY_SPKI_Export(const KeyObjectData& key_data, - ByteSource* out); - -WebCryptoKeyExportStatus PKEY_PKCS8_Export(const KeyObjectData& key_data, - ByteSource* out); - namespace Keys { void Initialize(Environment* env, v8::Local target); void RegisterExternalReferences(ExternalReferenceRegistry* registry); diff --git a/src/crypto/crypto_rsa.cc b/src/crypto/crypto_rsa.cc index 62ee228945c45b..3619c1d21dd238 100644 --- a/src/crypto/crypto_rsa.cc +++ b/src/crypto/crypto_rsa.cc @@ -176,12 +176,6 @@ Maybe RsaKeyGenTraits::AdditionalConfig( } namespace { -WebCryptoKeyExportStatus RSA_JWK_Export(const KeyObjectData& key_data, - const RSAKeyExportConfig& params, - ByteSource* out) { - return WebCryptoKeyExportStatus::FAILED; -} - using Cipher_t = DataPointer(const EVPKeyPointer& key, const ncrypto::Rsa::CipherParams& params, const ncrypto::Buffer in); @@ -210,42 +204,6 @@ WebCryptoCipherStatus RSA_Cipher(Environment* env, } } // namespace -Maybe RSAKeyExportTraits::AdditionalConfig( - const FunctionCallbackInfo& args, - unsigned int offset, - RSAKeyExportConfig* params) { - CHECK(args[offset]->IsUint32()); // RSAKeyVariant - params->variant = - static_cast(args[offset].As()->Value()); - return JustVoid(); -} - -WebCryptoKeyExportStatus RSAKeyExportTraits::DoExport( - const KeyObjectData& key_data, - WebCryptoKeyFormat format, - const RSAKeyExportConfig& params, - ByteSource* out) { - CHECK_NE(key_data.GetKeyType(), kKeyTypeSecret); - - switch (format) { - case kWebCryptoKeyFormatRaw: - // Not supported for RSA keys of either type - return WebCryptoKeyExportStatus::FAILED; - case kWebCryptoKeyFormatJWK: - return RSA_JWK_Export(key_data, params, out); - case kWebCryptoKeyFormatPKCS8: - if (key_data.GetKeyType() != kKeyTypePrivate) - return WebCryptoKeyExportStatus::INVALID_KEY_TYPE; - return PKEY_PKCS8_Export(key_data, out); - case kWebCryptoKeyFormatSPKI: - if (key_data.GetKeyType() != kKeyTypePublic) - return WebCryptoKeyExportStatus::INVALID_KEY_TYPE; - return PKEY_SPKI_Export(key_data, out); - default: - UNREACHABLE(); - } -} - RSACipherConfig::RSACipherConfig(RSACipherConfig&& other) noexcept : mode(other.mode), label(std::move(other.label)), @@ -539,7 +497,6 @@ bool GetRsaKeyDetail(Environment* env, namespace RSAAlg { void Initialize(Environment* env, Local target) { RSAKeyPairGenJob::Initialize(env, target); - RSAKeyExportJob::Initialize(env, target); RSACipherJob::Initialize(env, target); NODE_DEFINE_CONSTANT(target, kKeyVariantRSA_SSA_PKCS1_v1_5); @@ -549,7 +506,6 @@ void Initialize(Environment* env, Local target) { void RegisterExternalReferences(ExternalReferenceRegistry* registry) { RSAKeyPairGenJob::RegisterExternalReferences(registry); - RSAKeyExportJob::RegisterExternalReferences(registry); RSACipherJob::RegisterExternalReferences(registry); } } // namespace RSAAlg diff --git a/src/crypto/crypto_rsa.h b/src/crypto/crypto_rsa.h index a9912d6f43674b..5b7f90f502df03 100644 --- a/src/crypto/crypto_rsa.h +++ b/src/crypto/crypto_rsa.h @@ -52,30 +52,6 @@ struct RsaKeyGenTraits final { using RSAKeyPairGenJob = KeyGenJob>; -struct RSAKeyExportConfig final : public MemoryRetainer { - RSAKeyVariant variant = kKeyVariantRSA_SSA_PKCS1_v1_5; - SET_NO_MEMORY_INFO() - SET_MEMORY_INFO_NAME(RSAKeyExportConfig) - SET_SELF_SIZE(RSAKeyExportConfig) -}; - -struct RSAKeyExportTraits final { - static constexpr const char* JobName = "RSAKeyExportJob"; - using AdditionalParameters = RSAKeyExportConfig; - - static v8::Maybe AdditionalConfig( - const v8::FunctionCallbackInfo& args, - unsigned int offset, - RSAKeyExportConfig* config); - - static WebCryptoKeyExportStatus DoExport(const KeyObjectData& key_data, - WebCryptoKeyFormat format, - const RSAKeyExportConfig& params, - ByteSource* out); -}; - -using RSAKeyExportJob = KeyExportJob; - struct RSACipherConfig final : public MemoryRetainer { CryptoJobMode mode = kCryptoJobAsync; ByteSource label; diff --git a/src/node_errors.h b/src/node_errors.h index 406ad251bcf4bb..8f14b75b10493c 100644 --- a/src/node_errors.h +++ b/src/node_errors.h @@ -51,6 +51,7 @@ void OOMErrorHandler(const char* location, const v8::OOMDetails& details); V(ERR_CPU_PROFILE_NOT_STARTED, Error) \ V(ERR_CPU_PROFILE_TOO_MANY, Error) \ V(ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED, Error) \ + V(ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS, Error) \ V(ERR_CRYPTO_INITIALIZATION_FAILED, Error) \ V(ERR_CRYPTO_INVALID_ARGON2_PARAMS, TypeError) \ V(ERR_CRYPTO_INVALID_AUTH_TAG, TypeError) \ @@ -191,6 +192,8 @@ ERRORS_WITH_CODE(V) V(ERR_CLOSED_MESSAGE_PORT, "Cannot send data on closed MessagePort") \ V(ERR_CONSTRUCT_CALL_INVALID, "Constructor cannot be called") \ V(ERR_CONSTRUCT_CALL_REQUIRED, "Cannot call constructor without `new`") \ + V(ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS, \ + "The selected key encoding is incompatible with the key type") \ V(ERR_CRYPTO_INITIALIZATION_FAILED, "Initialization failed") \ V(ERR_CRYPTO_INVALID_ARGON2_PARAMS, "Invalid Argon2 params") \ V(ERR_CRYPTO_INVALID_AUTH_TAG, "Invalid authentication tag") \ diff --git a/test/parallel/test-crypto-encap-decap.js b/test/parallel/test-crypto-encap-decap.js index 2c2ccd42ca2365..10dc451ef55d68 100644 --- a/test/parallel/test-crypto-encap-decap.js +++ b/test/parallel/test-crypto-encap-decap.js @@ -26,6 +26,7 @@ const keys = { privateKey: fixtures.readKey('rsa_private_2048.pem', 'ascii'), sharedSecretLength: 256, ciphertextLength: 256, + raw: false, }, 'rsa-pss': { supported: false, // Only raw RSA is supported @@ -38,6 +39,7 @@ const keys = { privateKey: fixtures.readKey('ec_p256_private.pem', 'ascii'), sharedSecretLength: 32, ciphertextLength: 65, + raw: true, }, 'p-384': { supported: hasOpenSSL(3, 2), // DHKEM was added in 3.2 @@ -45,6 +47,7 @@ const keys = { privateKey: fixtures.readKey('ec_p384_private.pem', 'ascii'), sharedSecretLength: 48, ciphertextLength: 97, + raw: true, }, 'p-521': { supported: hasOpenSSL(3, 2), // DHKEM was added in 3.2 @@ -52,6 +55,7 @@ const keys = { privateKey: fixtures.readKey('ec_p521_private.pem', 'ascii'), sharedSecretLength: 64, ciphertextLength: 133, + raw: true, }, 'secp256k1': { supported: false, // only P-256, P-384, and P-521 are supported @@ -64,6 +68,7 @@ const keys = { privateKey: fixtures.readKey('x25519_private.pem', 'ascii'), sharedSecretLength: 32, ciphertextLength: 32, + raw: true, }, 'x448': { supported: hasOpenSSL(3, 2), // DHKEM was added in 3.2 @@ -71,6 +76,7 @@ const keys = { privateKey: fixtures.readKey('x448_private.pem', 'ascii'), sharedSecretLength: 64, ciphertextLength: 56, + raw: true, }, 'ml-kem-512': { supported: hasOpenSSL(3, 5), @@ -78,6 +84,7 @@ const keys = { privateKey: fixtures.readKey('ml_kem_512_private.pem', 'ascii'), sharedSecretLength: 32, ciphertextLength: 768, + raw: true, }, 'ml-kem-768': { supported: hasOpenSSL(3, 5), @@ -85,6 +92,7 @@ const keys = { privateKey: fixtures.readKey('ml_kem_768_private.pem', 'ascii'), sharedSecretLength: 32, ciphertextLength: 1088, + raw: true, }, 'ml-kem-1024': { supported: hasOpenSSL(3, 5), @@ -92,10 +100,13 @@ const keys = { privateKey: fixtures.readKey('ml_kem_1024_private.pem', 'ascii'), sharedSecretLength: 32, ciphertextLength: 1568, + raw: true, }, }; -for (const [name, { supported, publicKey, privateKey, sharedSecretLength, ciphertextLength }] of Object.entries(keys)) { +for (const [name, { + supported, publicKey, privateKey, sharedSecretLength, ciphertextLength, raw, +}] of Object.entries(keys)) { if (!supported) { assert.throws(() => crypto.encapsulate(publicKey), { code: /ERR_OSSL_EVP_DECODE_ERROR|ERR_CRYPTO_OPERATION_FAILED/ }); @@ -136,6 +147,25 @@ for (const [name, { supported, publicKey, privateKey, sharedSecretLength, cipher }); } + if (raw) { + const { asymmetricKeyType } = keyObjects.privateKey; + const { namedCurve } = keyObjects.privateKey.asymmetricKeyDetails; + const privateFormat = asymmetricKeyType.startsWith('ml-') ? 'raw-seed' : 'raw-private'; + const rawPublic = { + key: keyObjects.publicKey.export({ format: 'raw-public' }), + format: 'raw-public', + asymmetricKeyType, + ...(namedCurve ? { namedCurve } : {}), + }; + const rawPrivate = { + key: keyObjects.privateKey.export({ format: privateFormat }), + format: privateFormat, + asymmetricKeyType, + ...(namedCurve ? { namedCurve } : {}), + }; + keyPairs.push({ publicKey: rawPublic, privateKey: rawPrivate }); + } + for (const kp of keyPairs) { // sync { diff --git a/test/parallel/test-crypto-key-objects-raw.js b/test/parallel/test-crypto-key-objects-raw.js new file mode 100644 index 00000000000000..ee9a09e24ce6c0 --- /dev/null +++ b/test/parallel/test-crypto-key-objects-raw.js @@ -0,0 +1,446 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); +const fixtures = require('../common/fixtures'); +const { hasOpenSSL } = require('../common/crypto'); + +// EC: NIST and OpenSSL curve names are both recognized for raw-public and raw-private +{ + const pubKeyObj = crypto.createPublicKey( + fixtures.readKey('ec_p256_public.pem', 'ascii')); + const privKeyObj = crypto.createPrivateKey( + fixtures.readKey('ec_p256_private.pem', 'ascii')); + + const rawPub = pubKeyObj.export({ format: 'raw-public' }); + const rawPriv = privKeyObj.export({ format: 'raw-private' }); + + for (const namedCurve of ['P-256', 'prime256v1']) { + const importedPub = crypto.createPublicKey({ + key: rawPub, format: 'raw-public', asymmetricKeyType: 'ec', namedCurve, + }); + assert.strictEqual(importedPub.equals(pubKeyObj), true); + + const importedPriv = crypto.createPrivateKey({ + key: rawPriv, format: 'raw-private', asymmetricKeyType: 'ec', namedCurve, + }); + assert.strictEqual(importedPriv.equals(privKeyObj), true); + } +} + +// Key types that don't support raw-* formats +{ + for (const [type, pub, priv] of [ + ['rsa', 'rsa_public_2048.pem', 'rsa_private_2048.pem'], + ['dsa', 'dsa_public.pem', 'dsa_private.pem'], + ]) { + const pubKeyObj = crypto.createPublicKey( + fixtures.readKey(pub, 'ascii')); + const privKeyObj = crypto.createPrivateKey( + fixtures.readKey(priv, 'ascii')); + + assert.throws(() => pubKeyObj.export({ format: 'raw-public' }), + { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); + assert.throws(() => privKeyObj.export({ format: 'raw-private' }), + { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); + assert.throws(() => privKeyObj.export({ format: 'raw-seed' }), + { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); + + for (const format of ['raw-public', 'raw-private', 'raw-seed']) { + assert.throws(() => crypto.createPublicKey({ + key: Buffer.alloc(32), format, asymmetricKeyType: type, + }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); + } + } + + // DH keys also don't support raw formats + { + const privKeyObj = crypto.createPrivateKey( + fixtures.readKey('dh_private.pem', 'ascii')); + assert.throws(() => privKeyObj.export({ format: 'raw-private' }), + { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); + + for (const format of ['raw-public', 'raw-private', 'raw-seed']) { + assert.throws(() => crypto.createPrivateKey({ + key: Buffer.alloc(32), format, asymmetricKeyType: 'dh', + }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); + } + } +} + +// PQC import throws when PQC is not supported +if (!hasOpenSSL(3, 5)) { + for (const asymmetricKeyType of [ + 'ml-dsa-44', 'ml-dsa-65', 'ml-dsa-87', + 'ml-kem-512', 'ml-kem-768', 'ml-kem-1024', + 'slh-dsa-sha2-128f', 'slh-dsa-shake-128f', + ]) { + for (const format of ['raw-public', 'raw-private', 'raw-seed']) { + assert.throws(() => crypto.createPublicKey({ + key: Buffer.alloc(32), format, asymmetricKeyType, + }), { code: 'ERR_INVALID_ARG_VALUE' }); + } + } +} + +// EC: P-256 and P-384 keys cannot be imported as private/public of the other type +{ + const p256Pub = crypto.createPublicKey( + fixtures.readKey('ec_p256_public.pem', 'ascii')); + const p384Pub = crypto.createPublicKey( + fixtures.readKey('ec_p384_public.pem', 'ascii')); + const p256Priv = crypto.createPrivateKey( + fixtures.readKey('ec_p256_private.pem', 'ascii')); + const p384Priv = crypto.createPrivateKey( + fixtures.readKey('ec_p384_private.pem', 'ascii')); + + const p256RawPub = p256Pub.export({ format: 'raw-public' }); + const p384RawPub = p384Pub.export({ format: 'raw-public' }); + const p256RawPriv = p256Priv.export({ format: 'raw-private' }); + const p384RawPriv = p384Priv.export({ format: 'raw-private' }); + + // P-256 public imported as P-384 + assert.throws(() => crypto.createPublicKey({ + key: p256RawPub, format: 'raw-public', + asymmetricKeyType: 'ec', namedCurve: 'P-384', + }), { code: 'ERR_INVALID_ARG_VALUE' }); + + // P-384 public imported as P-256 + assert.throws(() => crypto.createPublicKey({ + key: p384RawPub, format: 'raw-public', + asymmetricKeyType: 'ec', namedCurve: 'P-256', + }), { code: 'ERR_INVALID_ARG_VALUE' }); + + // P-256 private imported as P-384 + assert.throws(() => crypto.createPrivateKey({ + key: p256RawPriv, format: 'raw-private', + asymmetricKeyType: 'ec', namedCurve: 'P-384', + }), { code: 'ERR_INVALID_ARG_VALUE' }); + + // P-384 private imported as P-256 + assert.throws(() => crypto.createPrivateKey({ + key: p384RawPriv, format: 'raw-private', + asymmetricKeyType: 'ec', namedCurve: 'P-256', + }), { code: 'ERR_INVALID_ARG_VALUE' }); +} + +// ML-KEM: -768 and -512 public keys cannot be imported as the other type +if (hasOpenSSL(3, 5)) { + const mlKem512Pub = crypto.createPublicKey( + fixtures.readKey('ml_kem_512_public.pem', 'ascii')); + const mlKem768Pub = crypto.createPublicKey( + fixtures.readKey('ml_kem_768_public.pem', 'ascii')); + + const mlKem512RawPub = mlKem512Pub.export({ format: 'raw-public' }); + const mlKem768RawPub = mlKem768Pub.export({ format: 'raw-public' }); + + assert.throws(() => crypto.createPublicKey({ + key: mlKem512RawPub, format: 'raw-public', asymmetricKeyType: 'ml-kem-768', + }), { code: 'ERR_INVALID_ARG_VALUE' }); + + assert.throws(() => crypto.createPublicKey({ + key: mlKem768RawPub, format: 'raw-public', asymmetricKeyType: 'ml-kem-512', + }), { code: 'ERR_INVALID_ARG_VALUE' }); +} + +// ML-DSA: -44 and -65 public keys cannot be imported as the other type +if (hasOpenSSL(3, 5)) { + const mlDsa44Pub = crypto.createPublicKey( + fixtures.readKey('ml_dsa_44_public.pem', 'ascii')); + const mlDsa65Pub = crypto.createPublicKey( + fixtures.readKey('ml_dsa_65_public.pem', 'ascii')); + + const mlDsa44RawPub = mlDsa44Pub.export({ format: 'raw-public' }); + const mlDsa65RawPub = mlDsa65Pub.export({ format: 'raw-public' }); + + assert.throws(() => crypto.createPublicKey({ + key: mlDsa44RawPub, format: 'raw-public', asymmetricKeyType: 'ml-dsa-65', + }), { code: 'ERR_INVALID_ARG_VALUE' }); + + assert.throws(() => crypto.createPublicKey({ + key: mlDsa65RawPub, format: 'raw-public', asymmetricKeyType: 'ml-dsa-44', + }), { code: 'ERR_INVALID_ARG_VALUE' }); +} + +// SLH-DSA: mismatched key types with different sizes are rejected +if (hasOpenSSL(3, 5)) { + const slh128fPub = crypto.createPublicKey( + fixtures.readKey('slh_dsa_sha2_128f_public.pem', 'ascii')); + const slh192fPub = crypto.createPublicKey( + fixtures.readKey('slh_dsa_sha2_192f_public.pem', 'ascii')); + const slh128fPriv = crypto.createPrivateKey( + fixtures.readKey('slh_dsa_sha2_128f_private.pem', 'ascii')); + const slh192fPriv = crypto.createPrivateKey( + fixtures.readKey('slh_dsa_sha2_192f_private.pem', 'ascii')); + + const rawPub128f = slh128fPub.export({ format: 'raw-public' }); + const rawPub192f = slh192fPub.export({ format: 'raw-public' }); + const rawPriv128f = slh128fPriv.export({ format: 'raw-private' }); + const rawPriv192f = slh192fPriv.export({ format: 'raw-private' }); + + assert.throws(() => crypto.createPublicKey({ + key: rawPub128f, format: 'raw-public', + asymmetricKeyType: 'slh-dsa-sha2-192f', + }), { code: 'ERR_INVALID_ARG_VALUE' }); + + assert.throws(() => crypto.createPublicKey({ + key: rawPub192f, format: 'raw-public', + asymmetricKeyType: 'slh-dsa-sha2-128f', + }), { code: 'ERR_INVALID_ARG_VALUE' }); + + assert.throws(() => crypto.createPrivateKey({ + key: rawPriv128f, format: 'raw-private', + asymmetricKeyType: 'slh-dsa-sha2-192f', + }), { code: 'ERR_INVALID_ARG_VALUE' }); + + assert.throws(() => crypto.createPrivateKey({ + key: rawPriv192f, format: 'raw-private', + asymmetricKeyType: 'slh-dsa-sha2-128f', + }), { code: 'ERR_INVALID_ARG_VALUE' }); +} + +// Passphrase cannot be used with raw formats +{ + const privKeyObj = crypto.createPrivateKey( + fixtures.readKey('ed25519_private.pem', 'ascii')); + + assert.throws(() => privKeyObj.export({ + format: 'raw-private', passphrase: 'test', + }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); + + assert.throws(() => privKeyObj.export({ + format: 'raw-seed', passphrase: 'test', + }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); +} + +// raw-seed export is rejected for key types that do not support seeds +{ + const ecPriv = crypto.createPrivateKey( + fixtures.readKey('ec_p256_private.pem', 'ascii')); + assert.throws(() => ecPriv.export({ format: 'raw-seed' }), + { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); + + for (const type of ['ed25519', 'ed448', 'x25519', 'x448']) { + const priv = crypto.createPrivateKey( + fixtures.readKey(`${type}_private.pem`, 'ascii')); + assert.throws(() => priv.export({ format: 'raw-seed' }), + { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); + } + + if (hasOpenSSL(3, 5)) { + const slhPriv = crypto.createPrivateKey( + fixtures.readKey('slh_dsa_sha2_128f_private.pem', 'ascii')); + assert.throws(() => slhPriv.export({ format: 'raw-seed' }), + { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); + } +} + +// raw-private cannot be used for ml-kem and ml-dsa +if (hasOpenSSL(3, 5)) { + for (const type of ['ml-kem-512', 'ml-dsa-44']) { + const priv = crypto.createPrivateKey( + fixtures.readKey(`${type.replaceAll('-', '_')}_private.pem`, 'ascii')); + assert.throws(() => priv.export({ format: 'raw-private' }), + { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); + } +} + +// EC: defaults to compressed, can be switched to uncompressed, both can be imported +{ + const pubKeyObj = crypto.createPublicKey( + fixtures.readKey('ec_p256_public.pem', 'ascii')); + + const defaultExport = pubKeyObj.export({ format: 'raw-public' }); + const compressed = pubKeyObj.export({ format: 'raw-public', type: 'compressed' }); + const uncompressed = pubKeyObj.export({ format: 'raw-public', type: 'uncompressed' }); + + // Default is compressed + assert.deepStrictEqual(defaultExport, compressed); + + // Compressed starts with 0x02 or 0x03 and is 33 bytes for P-256 + assert.strictEqual(compressed.byteLength, 33); + assert(compressed[0] === 0x02 || compressed[0] === 0x03); + + // Uncompressed starts with 0x04 and is 65 bytes for P-256 + assert.strictEqual(uncompressed.byteLength, 65); + assert.strictEqual(uncompressed[0], 0x04); + + // Both can be imported + const fromCompressed = crypto.createPublicKey({ + key: compressed, format: 'raw-public', + asymmetricKeyType: 'ec', namedCurve: 'P-256', + }); + assert.strictEqual(fromCompressed.equals(pubKeyObj), true); + + const fromUncompressed = crypto.createPublicKey({ + key: uncompressed, format: 'raw-public', + asymmetricKeyType: 'ec', namedCurve: 'P-256', + }); + assert.strictEqual(fromUncompressed.equals(pubKeyObj), true); +} + +// Compressed and uncompressed are the only recognized options +{ + const pubKeyObj = crypto.createPublicKey( + fixtures.readKey('ec_p256_public.pem', 'ascii')); + + assert.throws(() => pubKeyObj.export({ format: 'raw-public', type: 'hybrid' }), + { code: 'ERR_INVALID_ARG_VALUE' }); + + assert.throws(() => pubKeyObj.export({ format: 'raw-public', type: 'invalid' }), + { code: 'ERR_INVALID_ARG_VALUE' }); +} + +// None of the raw types can be used with symmetric key objects +{ + const secretKey = crypto.createSecretKey(Buffer.alloc(32)); + + for (const format of ['raw-public', 'raw-private', 'raw-seed']) { + assert.throws(() => secretKey.export({ format }), + { code: 'ERR_INVALID_ARG_VALUE' }); + } +} + +// Private key objects created from raw-seed or raw-private can be passed to createPublicKey() +{ + // Ed25519 raw-private -> createPublicKey + const edPriv = crypto.createPrivateKey( + fixtures.readKey('ed25519_private.pem', 'ascii')); + const edPub = crypto.createPublicKey( + fixtures.readKey('ed25519_public.pem', 'ascii')); + const rawPriv = edPriv.export({ format: 'raw-private' }); + const importedPriv = crypto.createPrivateKey({ + key: rawPriv, format: 'raw-private', asymmetricKeyType: 'ed25519', + }); + const derivedPub = crypto.createPublicKey(importedPriv); + assert.strictEqual(derivedPub.equals(edPub), true); + // Private key must not be extractable from the derived public key. + assert.throws(() => derivedPub.export({ format: 'pem', type: 'pkcs8' }), + { code: 'ERR_INVALID_ARG_VALUE' }); + assert.throws(() => derivedPub.export({ format: 'der', type: 'pkcs8' }), + { code: 'ERR_INVALID_ARG_VALUE' }); + + // EC raw-private -> createPublicKey + const ecPriv = crypto.createPrivateKey( + fixtures.readKey('ec_p256_private.pem', 'ascii')); + const ecPub = crypto.createPublicKey( + fixtures.readKey('ec_p256_public.pem', 'ascii')); + const ecRawPriv = ecPriv.export({ format: 'raw-private' }); + const ecImportedPriv = crypto.createPrivateKey({ + key: ecRawPriv, format: 'raw-private', + asymmetricKeyType: 'ec', namedCurve: 'P-256', + }); + const ecDerivedPub = crypto.createPublicKey(ecImportedPriv); + assert.strictEqual(ecDerivedPub.equals(ecPub), true); + // Private key must not be extractable from the derived public key. + assert.throws(() => ecDerivedPub.export({ format: 'pem', type: 'pkcs8' }), + { code: 'ERR_INVALID_ARG_VALUE' }); + assert.throws(() => ecDerivedPub.export({ format: 'pem', type: 'sec1' }), + { code: 'ERR_INVALID_ARG_VALUE' }); + + // PQC raw-seed -> createPublicKey + if (hasOpenSSL(3, 5)) { + const mlDsaPriv = crypto.createPrivateKey( + fixtures.readKey('ml_dsa_44_private.pem', 'ascii')); + const mlDsaPub = crypto.createPublicKey( + fixtures.readKey('ml_dsa_44_public.pem', 'ascii')); + const mlDsaRawSeed = mlDsaPriv.export({ format: 'raw-seed' }); + const mlDsaImportedPriv = crypto.createPrivateKey({ + key: mlDsaRawSeed, format: 'raw-seed', asymmetricKeyType: 'ml-dsa-44', + }); + const mlDsaDerivedPub = crypto.createPublicKey(mlDsaImportedPriv); + assert.strictEqual(mlDsaDerivedPub.equals(mlDsaPub), true); + // Private key must not be extractable from the derived public key. + assert.throws(() => mlDsaDerivedPub.export({ format: 'pem', type: 'pkcs8' }), + { code: 'ERR_INVALID_ARG_VALUE' }); + } +} + +// raw-public EC keys that are garbage/not on curve are rejected +{ + const garbage = Buffer.alloc(33, 0xff); + garbage[0] = 0x02; // Valid compressed prefix but invalid point + + assert.throws(() => crypto.createPublicKey({ + key: garbage, format: 'raw-public', + asymmetricKeyType: 'ec', namedCurve: 'P-256', + }), { code: 'ERR_INVALID_ARG_VALUE' }); + + // Totally random garbage + assert.throws(() => crypto.createPublicKey({ + key: Buffer.alloc(10, 0xab), format: 'raw-public', + asymmetricKeyType: 'ec', namedCurve: 'P-256', + }), { code: 'ERR_INVALID_ARG_VALUE' }); +} + +// Unrecognized namedCurve values are rejected +{ + assert.throws(() => crypto.createPublicKey({ + key: Buffer.alloc(33), format: 'raw-public', + asymmetricKeyType: 'ec', namedCurve: 'not-a-curve', + }), { code: 'ERR_CRYPTO_INVALID_CURVE' }); + + assert.throws(() => crypto.createPrivateKey({ + key: Buffer.alloc(32), format: 'raw-private', + asymmetricKeyType: 'ec', namedCurve: 'not-a-curve', + }), { code: 'ERR_CRYPTO_INVALID_CURVE' }); +} + +// x25519, ed25519, x448, and ed448 cannot be used as 'ec' namedCurve values +{ + for (const type of ['ed25519', 'x25519', 'ed448', 'x448']) { + const priv = crypto.createPrivateKey( + fixtures.readKey(`${type}_private.pem`, 'ascii')); + const pub = crypto.createPublicKey( + fixtures.readKey(`${type}_public.pem`, 'ascii')); + + const rawPub = pub.export({ format: 'raw-public' }); + const rawPriv = priv.export({ format: 'raw-private' }); + + // Try to import as EC - must fail + assert.throws(() => crypto.createPublicKey({ + key: rawPub, format: 'raw-public', + asymmetricKeyType: 'ec', namedCurve: type, + }), { code: 'ERR_CRYPTO_INVALID_CURVE' }); + + assert.throws(() => crypto.createPrivateKey({ + key: rawPriv, format: 'raw-private', + asymmetricKeyType: 'ec', namedCurve: type, + }), { code: 'ERR_CRYPTO_INVALID_CURVE' }); + } +} + +// Missing asymmetricKeyType option +{ + assert.throws(() => crypto.createPublicKey({ + key: Buffer.alloc(32), format: 'raw-public', + }), { code: 'ERR_INVALID_ARG_TYPE' }); +} + +// Unknown asymmetricKeyType value +{ + assert.throws(() => crypto.createPublicKey({ + key: Buffer.alloc(32), format: 'raw-public', + asymmetricKeyType: 'unknown', + }), { code: 'ERR_INVALID_ARG_VALUE' }); +} + +// Non-buffer key data +{ + assert.throws(() => crypto.createPublicKey({ + key: 12345, format: 'raw-public', + asymmetricKeyType: 'ec', namedCurve: 'P-256', + }), { code: 'ERR_INVALID_ARG_TYPE' }); +} + +// Missing namedCurve for EC +{ + assert.throws(() => crypto.createPublicKey({ + key: Buffer.alloc(33), format: 'raw-public', + asymmetricKeyType: 'ec', + }), { code: 'ERR_INVALID_ARG_TYPE' }); +} diff --git a/test/parallel/test-crypto-key-objects.js b/test/parallel/test-crypto-key-objects.js index e8359ed6d0362c..6c1c3fd3afa448 100644 --- a/test/parallel/test-crypto-key-objects.js +++ b/test/parallel/test-crypto-key-objects.js @@ -170,6 +170,23 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', assert.strictEqual(derivedPublicKey.asymmetricKeyType, 'rsa'); assert.strictEqual(derivedPublicKey.symmetricKeySize, undefined); + // The private key should not be extractable from the derived public key. + assert.throws(() => derivedPublicKey.export({ format: 'pem', type: 'pkcs8' }), + { code: 'ERR_INVALID_ARG_VALUE' }); + assert.throws(() => derivedPublicKey.export({ format: 'der', type: 'pkcs8' }), + { code: 'ERR_INVALID_ARG_VALUE' }); + // JWK export should only contain public components, no 'd'. + { + const jwkExport = derivedPublicKey.export({ format: 'jwk' }); + assert.strictEqual(jwkExport.kty, 'RSA'); + assert.strictEqual(jwkExport.d, undefined); + assert.strictEqual(jwkExport.dp, undefined); + assert.strictEqual(jwkExport.dq, undefined); + assert.strictEqual(jwkExport.qi, undefined); + assert.strictEqual(jwkExport.p, undefined); + assert.strictEqual(jwkExport.q, undefined); + } + const publicKeyFromJwk = createPublicKey({ key: publicJwk, format: 'jwk' }); assert.strictEqual(publicKeyFromJwk.type, 'public'); assert.strictEqual(publicKeyFromJwk.toString(), '[object KeyObject]'); @@ -415,6 +432,33 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', key.export({ format: 'jwk' }), jwk); } } + + // Raw format round-trip + { + const privKey = createPrivateKey(info.private); + const pubKey = createPublicKey(info.public); + + const rawPriv = privKey.export({ format: 'raw-private' }); + const rawPub = pubKey.export({ format: 'raw-public' }); + assert(Buffer.isBuffer(rawPriv)); + assert(Buffer.isBuffer(rawPub)); + + const importedPriv = createPrivateKey({ + key: rawPriv, format: 'raw-private', asymmetricKeyType: keyType, + }); + assert.strictEqual(importedPriv.type, 'private'); + assert.strictEqual(importedPriv.asymmetricKeyType, keyType); + assert.deepStrictEqual( + importedPriv.export({ format: 'raw-private' }), rawPriv); + + const importedPub = createPublicKey({ + key: rawPub, format: 'raw-public', asymmetricKeyType: keyType, + }); + assert.strictEqual(importedPub.type, 'public'); + assert.strictEqual(importedPub.asymmetricKeyType, keyType); + assert.deepStrictEqual( + importedPub.export({ format: 'raw-public' }), rawPub); + } }); [ @@ -506,8 +550,47 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', delete jwk.d; assert.deepStrictEqual( key.export({ format: 'jwk' }), jwk); + + // Private key material must not be extractable from a derived public key. + assert.throws(() => key.export({ format: 'pem', type: 'pkcs8' }), + { code: 'ERR_INVALID_ARG_VALUE' }); + assert.throws(() => key.export({ format: 'pem', type: 'sec1' }), + { code: 'ERR_INVALID_ARG_VALUE' }); + assert.throws(() => key.export({ format: 'der', type: 'pkcs8' }), + { code: 'ERR_INVALID_ARG_VALUE' }); + assert.throws(() => key.export({ format: 'der', type: 'sec1' }), + { code: 'ERR_INVALID_ARG_VALUE' }); } } + + // Raw format round-trip + { + const privKey = createPrivateKey(info.private); + const pubKey = createPublicKey(info.public); + + const rawPriv = privKey.export({ format: 'raw-private' }); + const rawPub = pubKey.export({ format: 'raw-public' }); + assert(Buffer.isBuffer(rawPriv)); + assert(Buffer.isBuffer(rawPub)); + + const importedPriv = createPrivateKey({ + key: rawPriv, format: 'raw-private', + asymmetricKeyType: keyType, namedCurve, + }); + assert.strictEqual(importedPriv.type, 'private'); + assert.strictEqual(importedPriv.asymmetricKeyType, keyType); + assert.deepStrictEqual( + importedPriv.export({ format: 'raw-private' }), rawPriv); + + const importedPub = createPublicKey({ + key: rawPub, format: 'raw-public', + asymmetricKeyType: keyType, namedCurve, + }); + assert.strictEqual(importedPub.type, 'public'); + assert.strictEqual(importedPub.asymmetricKeyType, keyType); + assert.deepStrictEqual( + importedPub.export({ format: 'raw-public' }), rawPub); + } }); { diff --git a/test/parallel/test-crypto-pqc-key-objects-ml-dsa.js b/test/parallel/test-crypto-pqc-key-objects-ml-dsa.js index 8b9d0e197b9a55..1a832609d3f813 100644 --- a/test/parallel/test-crypto-pqc-key-objects-ml-dsa.js +++ b/test/parallel/test-crypto-pqc-key-objects-ml-dsa.js @@ -61,6 +61,15 @@ for (const [asymmetricKeyType, pubLen] of [ const jwk = key.export({ format: 'jwk' }); assertPublicJwk(jwk); assert.strictEqual(key.equals(createPublicKey({ format: 'jwk', key: jwk })), true); + + // Raw format round-trip + const rawPub = key.export({ format: 'raw-public' }); + assert(Buffer.isBuffer(rawPub)); + assert.strictEqual(rawPub.byteLength, pubLen); + const importedPub = createPublicKey({ + key: rawPub, format: 'raw-public', asymmetricKeyType, + }); + assert.strictEqual(importedPub.equals(key), true); } function assertPrivateKey(key, hasSeed) { @@ -78,6 +87,15 @@ for (const [asymmetricKeyType, pubLen] of [ assertPrivateJwk(jwk); assert.strictEqual(key.equals(createPrivateKey({ format: 'jwk', key: jwk })), true); assert.ok(createPublicKey({ format: 'jwk', key: jwk })); + + // Raw seed round-trip + const rawSeed = key.export({ format: 'raw-seed' }); + assert(Buffer.isBuffer(rawSeed)); + assert.strictEqual(rawSeed.byteLength, 32); + const importedPriv = createPrivateKey({ + key: rawSeed, format: 'raw-seed', asymmetricKeyType, + }); + assert.strictEqual(importedPriv.equals(key), true); } else { assert.throws(() => key.export({ format: 'jwk' }), { code: 'ERR_CRYPTO_OPERATION_FAILED', message: 'key does not have an available seed' }); @@ -172,8 +190,8 @@ for (const [asymmetricKeyType, pubLen] of [ } } else { assert.throws(() => createPrivateKey({ format, key: jwk }), - { code: 'ERR_INVALID_ARG_VALUE', message: /must be one of: 'RSA', 'EC', 'OKP'\. Received 'AKP'/ }); + { code: 'ERR_INVALID_ARG_VALUE', message: /Unsupported key type/ }); assert.throws(() => createPublicKey({ format, key: jwk }), - { code: 'ERR_INVALID_ARG_VALUE', message: /must be one of: 'RSA', 'EC', 'OKP'\. Received 'AKP'/ }); + { code: 'ERR_INVALID_ARG_VALUE', message: /Unsupported key type/ }); } } diff --git a/test/parallel/test-crypto-pqc-key-objects-ml-kem.js b/test/parallel/test-crypto-pqc-key-objects-ml-kem.js index d190a14dad87dd..0f4b226604c965 100644 --- a/test/parallel/test-crypto-pqc-key-objects-ml-kem.js +++ b/test/parallel/test-crypto-pqc-key-objects-ml-kem.js @@ -40,6 +40,14 @@ for (const asymmetricKeyType of ['ml-kem-512', 'ml-kem-768', 'ml-kem-1024']) { key.export({ format: 'der', type: 'spki' }); assert.throws(() => key.export({ format: 'jwk' }), { code: 'ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE', message: 'Unsupported JWK Key Type.' }); + + // Raw format round-trip + const rawPub = key.export({ format: 'raw-public' }); + assert(Buffer.isBuffer(rawPub)); + const importedPub = createPublicKey({ + key: rawPub, format: 'raw-public', asymmetricKeyType, + }); + assert.strictEqual(importedPub.equals(key), true); } function assertPrivateKey(key, hasSeed) { @@ -49,6 +57,14 @@ for (const asymmetricKeyType of ['ml-kem-512', 'ml-kem-768', 'ml-kem-1024']) { key.export({ format: 'der', type: 'pkcs8' }); if (hasSeed) { assert.strictEqual(key.export({ format: 'pem', type: 'pkcs8' }), keys.private_seed_only); + + // Raw seed round-trip + const rawSeed = key.export({ format: 'raw-seed' }); + assert(Buffer.isBuffer(rawSeed)); + const importedPriv = createPrivateKey({ + key: rawSeed, format: 'raw-seed', asymmetricKeyType, + }); + assert.strictEqual(importedPriv.equals(key), true); } else { assert.strictEqual(key.export({ format: 'pem', type: 'pkcs8' }), keys.private_priv_only); } diff --git a/test/parallel/test-crypto-pqc-key-objects-slh-dsa.js b/test/parallel/test-crypto-pqc-key-objects-slh-dsa.js index 1b612de8b2e582..fdae27f2da797f 100644 --- a/test/parallel/test-crypto-pqc-key-objects-slh-dsa.js +++ b/test/parallel/test-crypto-pqc-key-objects-slh-dsa.js @@ -42,6 +42,14 @@ for (const asymmetricKeyType of [ key.export({ format: 'der', type: 'spki' }); assert.throws(() => key.export({ format: 'jwk' }), { code: 'ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE', message: 'Unsupported JWK Key Type.' }); + + // Raw format round-trip + const rawPub = key.export({ format: 'raw-public' }); + assert(Buffer.isBuffer(rawPub)); + const importedPub = createPublicKey({ + key: rawPub, format: 'raw-public', asymmetricKeyType, + }); + assert.strictEqual(importedPub.equals(key), true); } function assertPrivateKey(key) { @@ -52,6 +60,14 @@ for (const asymmetricKeyType of [ assert.strictEqual(key.export({ format: 'pem', type: 'pkcs8' }), keys.private); assert.throws(() => key.export({ format: 'jwk' }), { code: 'ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE', message: 'Unsupported JWK Key Type.' }); + + // Raw format round-trip + const rawPriv = key.export({ format: 'raw-private' }); + assert(Buffer.isBuffer(rawPriv)); + const importedPriv = createPrivateKey({ + key: rawPriv, format: 'raw-private', asymmetricKeyType, + }); + assert.strictEqual(importedPriv.equals(key), true); } if (!hasOpenSSL(3, 5)) { diff --git a/test/parallel/test-crypto-pqc-sign-verify-ml-dsa.js b/test/parallel/test-crypto-pqc-sign-verify-ml-dsa.js index de3937dbc07486..57d6692ca79b55 100644 --- a/test/parallel/test-crypto-pqc-sign-verify-ml-dsa.js +++ b/test/parallel/test-crypto-pqc-sign-verify-ml-dsa.js @@ -67,6 +67,28 @@ for (const [asymmetricKeyType, sigLen] of [ } } } + + // Raw format sign/verify + { + const pubKeyObj = createPublicKey(keys.public); + const privKeyObj = createPrivateKey(keys.private_seed_only); + + const rawPublic = { + key: pubKeyObj.export({ format: 'raw-public' }), + format: 'raw-public', + asymmetricKeyType, + }; + const rawSeed = { + key: privKeyObj.export({ format: 'raw-seed' }), + format: 'raw-seed', + asymmetricKeyType, + }; + + const data = randomBytes(32); + const signature = sign(undefined, data, rawSeed); + assert.strictEqual(signature.byteLength, sigLen); + assert.strictEqual(verify(undefined, data, rawPublic, signature), true); + } } // Test vectors from ietf-cose-dilithium diff --git a/test/parallel/test-crypto-sign-verify.js b/test/parallel/test-crypto-sign-verify.js index a66f0a94efd7c9..1900f244b8491a 100644 --- a/test/parallel/test-crypto-sign-verify.js +++ b/test/parallel/test-crypto-sign-verify.js @@ -422,16 +422,19 @@ assert.throws( { private: fixtures.readKey('ed25519_private.pem', 'ascii'), public: fixtures.readKey('ed25519_public.pem', 'ascii'), algo: null, - sigLen: 64 }, + sigLen: 64, + raw: true }, { private: fixtures.readKey('ed448_private.pem', 'ascii'), public: fixtures.readKey('ed448_public.pem', 'ascii'), algo: null, supportsContext: true, - sigLen: 114 }, + sigLen: 114, + raw: true }, { private: fixtures.readKey('rsa_private_2048.pem', 'ascii'), public: fixtures.readKey('rsa_public_2048.pem', 'ascii'), algo: 'sha1', - sigLen: 256 }, + sigLen: 256, + raw: false }, ].forEach((pair) => { const algo = pair.algo; @@ -458,6 +461,29 @@ assert.throws( assert.strictEqual(crypto.verify(algo, data, pubKeyObj, sig), true); } + if (pair.raw) { + const data = Buffer.from('Hello world'); + const privKeyObj = crypto.createPrivateKey(pair.private); + const pubKeyObj = crypto.createPublicKey(pair.public); + const { asymmetricKeyType } = privKeyObj; + const rawPrivate = { + key: privKeyObj.export({ format: 'raw-private' }), + format: 'raw-private', + asymmetricKeyType, + }; + const rawPublic = { + key: pubKeyObj.export({ format: 'raw-public' }), + format: 'raw-public', + asymmetricKeyType, + }; + + const sig = crypto.sign(algo, data, rawPrivate); + assert.strictEqual(sig.length, pair.sigLen); + + assert.strictEqual(crypto.verify(algo, data, rawPrivate, sig), true); + assert.strictEqual(crypto.verify(algo, data, rawPublic, sig), true); + } + { const data = Buffer.from('Hello world'); const otherData = Buffer.from('Goodbye world'); diff --git a/test/parallel/test-webcrypto-export-import-cfrg.js b/test/parallel/test-webcrypto-export-import-cfrg.js index 9cfc6e9e4ecf5a..c6e3509a4362bc 100644 --- a/test/parallel/test-webcrypto-export-import-cfrg.js +++ b/test/parallel/test-webcrypto-export-import-cfrg.js @@ -389,9 +389,10 @@ async function testImportJwk({ name, publicUsages, privateUsages }, extractable) async function testImportRaw({ name, publicUsages }) { const jwk = keyData[name].jwk; + const rawKeyData = Buffer.from(jwk.x, 'base64url'); const publicKey = await subtle.importKey( 'raw', - Buffer.from(jwk.x, 'base64url'), + rawKeyData, { name }, true, publicUsages); @@ -400,6 +401,10 @@ async function testImportRaw({ name, publicUsages }) { assert.strictEqual(publicKey.algorithm.name, name); assert.strictEqual(publicKey.algorithm, publicKey.algorithm); assert.strictEqual(publicKey.usages, publicKey.usages); + + // Test raw export round-trip + const exported = await subtle.exportKey('raw', publicKey); + assert.deepStrictEqual(Buffer.from(exported), rawKeyData); } (async function() { diff --git a/test/parallel/test-webcrypto-export-import-ec.js b/test/parallel/test-webcrypto-export-import-ec.js index a2e9df73bc2926..98d0d7d3342ccb 100644 --- a/test/parallel/test-webcrypto-export-import-ec.js +++ b/test/parallel/test-webcrypto-export-import-ec.js @@ -356,14 +356,16 @@ async function testImportJwk( async function testImportRaw({ name, publicUsages }, namedCurve) { const jwk = keyData[namedCurve].jwk; + const uncompressedRaw = Buffer.concat([ + Buffer.alloc(1, 0x04), + Buffer.from(jwk.x, 'base64url'), + Buffer.from(jwk.y, 'base64url'), + ]); + const [publicKey] = await Promise.all([ subtle.importKey( 'raw', - Buffer.concat([ - Buffer.alloc(1, 0x04), - Buffer.from(jwk.x, 'base64url'), - Buffer.from(jwk.y, 'base64url'), - ]), + uncompressedRaw, { name, namedCurve }, true, publicUsages), subtle.importKey( @@ -382,6 +384,10 @@ async function testImportRaw({ name, publicUsages }, namedCurve) { assert.strictEqual(publicKey.algorithm.namedCurve, namedCurve); assert.strictEqual(publicKey.algorithm, publicKey.algorithm); assert.strictEqual(publicKey.usages, publicKey.usages); + + // Test raw export round-trip (always uncompressed) + const exported = await subtle.exportKey('raw', publicKey); + assert.deepStrictEqual(Buffer.from(exported), uncompressedRaw); } (async function() { diff --git a/test/parallel/test-webcrypto-export-import-ml-dsa.js b/test/parallel/test-webcrypto-export-import-ml-dsa.js index 8fd8abb6f44d76..ebd61c67f55d32 100644 --- a/test/parallel/test-webcrypto-export-import-ml-dsa.js +++ b/test/parallel/test-webcrypto-export-import-ml-dsa.js @@ -515,8 +515,6 @@ async function testImportRawSeed({ name, privateUsages }, extractable) { const key = keyObject.toCryptoKey({ name }, true, privateUsages); 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; }); } diff --git a/test/parallel/test-webcrypto-export-import-ml-kem.js b/test/parallel/test-webcrypto-export-import-ml-kem.js index 698de3cc1bcb97..50f3444ce87899 100644 --- a/test/parallel/test-webcrypto-export-import-ml-kem.js +++ b/test/parallel/test-webcrypto-export-import-ml-kem.js @@ -317,8 +317,6 @@ async function testImportRawSeed({ name, privateUsages }, extractable) { const key = keyObject.toCryptoKey({ name }, true, privateUsages); 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; }); }