diff options
-rw-r--r-- | doc/api/webcrypto.md | 101 | ||||
-rw-r--r-- | lib/internal/crypto/diffiehellman.js | 3 | ||||
-rw-r--r-- | lib/internal/crypto/ec.js | 233 | ||||
-rw-r--r-- | lib/internal/crypto/util.js | 8 | ||||
-rw-r--r-- | lib/internal/crypto/webcrypto.js | 31 | ||||
-rw-r--r-- | src/crypto/crypto_ecdh.cc | 308 | ||||
-rw-r--r-- | src/crypto/crypto_ecdh.h | 15 | ||||
-rw-r--r-- | src/crypto/crypto_keygen.h | 3 | ||||
-rw-r--r-- | src/crypto/crypto_keys.cc | 50 | ||||
-rw-r--r-- | src/crypto/crypto_keys.h | 1 | ||||
-rw-r--r-- | src/crypto/crypto_sig.cc | 82 | ||||
-rw-r--r-- | src/env.h | 2 | ||||
-rw-r--r-- | test/parallel/test-webcrypto-ed25519-ed448.js | 367 | ||||
-rw-r--r-- | test/parallel/test-webcrypto-x25519-x448.js | 246 | ||||
-rw-r--r-- | tools/doc/type-parser.js | 4 |
15 files changed, 1287 insertions, 167 deletions
diff --git a/doc/api/webcrypto.md b/doc/api/webcrypto.md index ce9999a744f..c238d32fd71 100644 --- a/doc/api/webcrypto.md +++ b/doc/api/webcrypto.md @@ -65,6 +65,26 @@ async function generateEcKey(namedCurve = 'P-521') { } ``` +#### ED25519/ED448/X25519/X448 Elliptic curve key pairs + +```js +const { subtle } = require('crypto').webcrypto; + +async function generateEd25519Key() { + return subtle.generateKey({ + name: 'NODE-ED25519', + namedCurve: 'NODE-ED25519', + }, true, ['sign', 'verify']); +} + +async function generateX25519Key() { + return subtle.generateKey({ + name: 'ECDH', + namedCurve: 'NODE-X25519', + }, true, ['deriveKey']); +} +``` + #### HMAC keys ```js @@ -305,6 +325,8 @@ implementation and the APIs supported for each: | `'SHA-512'` | | | | | | | | | | | | ✔ | | `'NODE-DSA'`<sup>1</sup> | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | | `'NODE-DH'`<sup>1</sup> | ✔ | ✔ | ✔ | | | | | ✔ | ✔ | | | | +| `'NODE-ED25519'`<sup>1</sup> | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | +| `'NODE-ED448'`<sup>1</sup> | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | <sup>1</sup> Node.js-specific extension @@ -420,6 +442,8 @@ Valid key usages depend on the key algorithm (identified by | `'NODE-DSA'` <sup>1</sup> | | | ✔ | ✔ | | | | | | `'NODE-DH'` <sup>1</sup> | | | | | ✔ | ✔ | | | | `'NODE-SCRYPT'` <sup>1</sup> | | | | | ✔ | ✔ | | | +| `'NODE-ED25519'` <sup>1</sup> | | | ✔ | ✔ | | | | | +| `'NODE-ED448'` <sup>1</sup> | | | ✔ | ✔ | | | | | <sup>1</sup> Node.js-specific extension. @@ -620,6 +644,8 @@ extension that allows converting a {CryptoKey} into a Node.js {KeyObject}. | `'NODE-DSA'` <sup>1</sup> | ✔ | ✔ | ✔ | | | `'NODE-DH'` <sup>1</sup> | ✔ | ✔ | | | | `'NODE-SCRYPT'` <sup>1</sup> | | | | | +| `'NODE-ED25519'` <sup>1</sup> | ✔ | ✔ | ✔ | ✔ | +| `'NODE-ED448'` <sup>1</sup> | ✔ | ✔ | ✔ | ✔ | <sup>1</sup> Node.js-specific extension @@ -629,7 +655,7 @@ added: v15.0.0 --> <!--lint disable maximum-line-length remark-lint--> -* `algorithm`: {RsaHashedKeyGenParams|EcKeyGenParams|HmacKeyGenParams|AesKeyGenParams|NodeDsaKeyGenParams|NodeDhKeyGenParams} +* `algorithm`: {RsaHashedKeyGenParams|EcKeyGenParams|HmacKeyGenParams|AesKeyGenParams|NodeDsaKeyGenParams|NodeDhKeyGenParams|NodeEdKeyGenParams} <!--lint enable maximum-line-length remark-lint--> * `extractable`: {boolean} * `keyUsages`: {string[]} See [Key usages][]. @@ -649,6 +675,8 @@ include: * `'ECDH'` * `'NODE-DSA'` <sup>1</sup> * `'NODE-DH'` <sup>1</sup> +* `'NODE-ED25519'` <sup>1</sup> +* `'NODE-ED448'` <sup>1</sup> The {CryptoKey} (secret key) generating algorithms supported include: @@ -669,7 +697,7 @@ added: v15.0.0 `node.keyObject`. * `keyData`: {ArrayBuffer|TypedArray|DataView|Buffer|KeyObject} <!--lint disable maximum-line-length remark-lint--> -* `algorithm`: {RsaHashedImportParams|EcKeyImportParams|HmacImportParams|AesImportParams|Pbkdf2ImportParams|NodeDsaImportParams|NodeDhImportParams|NodeScryptImportParams} +* `algorithm`: {RsaHashedImportParams|EcKeyImportParams|HmacImportParams|AesImportParams|Pbkdf2ImportParams|NodeDsaImportParams|NodeDhImportParams|NodeScryptImportParams|NodeEdKeyImportParams} <!--lint enable maximum-line-length remark-lint--> * `extractable`: {boolean} * `keyUsages`: {string[]} See [Key usages][]. @@ -704,6 +732,8 @@ The algorithms currently supported include: | `'NODE-DSA'` <sup>1</sup> | ✔ | ✔ | ✔ | | | `'NODE-DH'` <sup>1</sup> | ✔ | ✔ | | | | `'NODE-SCRYPT'` <sup>1</sup> | | | | ✔ | +| `'NODE-ED25519'` <sup>1</sup> | ✔ | ✔ | ✔ | ✔ | +| `'NODE-ED448'` <sup>1</sup> | ✔ | ✔ | ✔ | ✔ | <sup>1</sup> Node.js-specific extension @@ -731,6 +761,8 @@ The algorithms currently supported include: * `'ECDSA'` * `'HMAC'` * `'NODE-DSA'`<sup>1</sup> +* `'NODE-ED25519'`<sup>1</sup> +* `'NODE-ED448'`<sup>1</sup> <sup>1</sup> Non-standadrd Node.js extension @@ -809,6 +841,8 @@ The algorithms currently supported include: * `'ECDSA'` * `'HMAC'` * `'NODE-DSA'`<sup>1</sup> +* `'NODE-ED25519'`<sup>1</sup> +* `'NODE-ED448'`<sup>1</sup> <sup>1</sup> Non-standard Node.js extension @@ -1062,7 +1096,8 @@ added: v15.0.0 added: v15.0.0 --> -* Type: {string} Must be one of `'P-256'`, `'P-384'` or `'P-521'`. +* Type: {string} Must be one of `'P-256'`, `'P-384'`, `'P-521'`, + `'NODE-ED25519'`, `'NODE-ED448'`, `'NODE-X25519'`, or `'NODE-X448'`. ### Class: `EcKeyImportParams` <!-- YAML @@ -1081,7 +1116,8 @@ added: v15.0.0 added: v15.0.0 --> -* Type: {string} Must be one of `'P-256'`, `'P-384'` or `'P-521'`. +* Type: {string} Must be one of `'P-256'`, `'P-384'`, `'P-521'`, + `'NODE-ED25519'`, `'NODE-ED448'`, `'NODE-X25519'`, or `'NODE-X448'`. ### Class: `HkdfParams` <!-- YAML @@ -1598,6 +1634,63 @@ added: v15.0.0 * Type: {string} Must be `'NODE-DSA'` +### `NODE-ED25519` and `NODE-ED448` Algorithms +<!-- YAML +added: REPLACEME +--> + +#### Class: `NodeEdKeyGenParams` +<!-- YAML +added: REPLACEME +--> + +##### `nodeEdKeyGenParams.name` +<!-- YAML +added: REPLACEME +--> + +* Type: {string} Must be one of `'NODE-ED25519'`, `'NODE-ED448'` or `'ECDH'`. + +##### `nodeEdKeyGenParams.namedCurve` +<!-- YAML +added: REPLACEME +--> + +* Type: {string} Must be one of `'NODE-ED25519'`, `'NODE-ED448'`, + `'NODE-X25519'`, or `'NODE-X448'`. + +#### Class: `NodeEdKeyImportParams` +<!-- YAML +added: REPLACEME +--> + +##### `nodeEdKeyImportParams.name` +<!-- YAML +added: REPLACEME +--> + +* Type: {string} Must be one of `'NODE-ED25519'` or `'NODE-ED448'` + if importing an `Ed25519` or `Ed448` key, or `'ECDH'` if importing + an `X25519` or `X448` key. + +##### `nodeEdKeyImportParams.namedCurve` +<!-- YAML +added: REPLACEME +--> + +* Type: {string} Must be one of `'NODE-ED25519'`, `'NODE-ED448'`, + `'NODE-X25519'`, or `'NODE-X448'`. + +##### `nodeEdKeyImportParams.public` +<!-- YAML +added: REPLACEME +--> + +* Type: {boolean} + +The `public` parameter is used to specify that the key is to be interpreted +as a public key. + ### `NODE-SCRYPT` Algorithm <!-- YAML added: v15.0.0 diff --git a/lib/internal/crypto/diffiehellman.js b/lib/internal/crypto/diffiehellman.js index a28d61369b1..a82c03eb76d 100644 --- a/lib/internal/crypto/diffiehellman.js +++ b/lib/internal/crypto/diffiehellman.js @@ -70,7 +70,6 @@ const { toBuf, kHandle, kKeyObject, - kNamedCurveAliases, } = require('internal/crypto/util'); const { @@ -451,7 +450,7 @@ async function asyncDeriveBitsECDH(algorithm, baseKey, length) { const bits = await new Promise((resolve, reject) => { deriveBitsECDH( - kNamedCurveAliases[baseKey.algorithm.namedCurve], + baseKey.algorithm.namedCurve, key[kKeyObject][kHandle], baseKey[kKeyObject][kHandle], (err, bits) => { if (err) return reject(err); diff --git a/lib/internal/crypto/ec.js b/lib/internal/crypto/ec.js index b14bddd4911..248bba57ad2 100644 --- a/lib/internal/crypto/ec.js +++ b/lib/internal/crypto/ec.js @@ -6,17 +6,21 @@ const { SafeSet, } = primordials; +const { Buffer } = require('buffer'); + const { ECKeyExportJob, KeyObjectHandle, SignJob, kCryptoJobAsync, kKeyTypePrivate, + kKeyTypePublic, kSignJobModeSign, kSignJobModeVerify, } = internalBinding('crypto'); const { + validateBoolean, validateOneOf, validateString, } = require('internal/validators'); @@ -60,6 +64,10 @@ function verifyAcceptableEcKeyUse(name, type, usages) { case 'ECDH': checkSet = ['deriveKey', 'deriveBits']; break; + case 'NODE-ED25519': + // Fall through + case 'NODE-ED448': + // Fall through case 'ECDSA': switch (type) { case 'private': @@ -84,6 +92,41 @@ function createECPublicKeyRaw(namedCurve, keyData) { return new PublicKeyObject(handle); } +function createECRawKey(namedCurve, keyData, isPublic) { + const handle = new KeyObjectHandle(); + keyData = getArrayBufferOrView(keyData, 'keyData'); + + switch (namedCurve) { + case 'NODE-ED25519': + case 'NODE-X25519': + if (keyData.byteLength !== 32) { + throw lazyDOMException( + `${namedCurve} raw keys must be exactly 32-bytes`); + } + break; + case 'NODE-ED448': + if (keyData.byteLength !== 57) { + throw lazyDOMException( + `${namedCurve} raw keys must be exactly 57-bytes`); + } + break; + case 'NODE-X448': + if (keyData.byteLength !== 56) { + throw lazyDOMException( + `${namedCurve} raw keys must be exactly 56-bytes`); + } + break; + } + + if (isPublic) { + handle.initEDRaw(namedCurve, keyData, kKeyTypePublic); + return new PublicKeyObject(handle); + } + + handle.initEDRaw(namedCurve, keyData, kKeyTypePrivate); + return new PrivateKeyObject(handle); +} + async function ecGenerateKey(algorithm, extractable, keyUsages) { const { name, namedCurve } = algorithm; validateString(namedCurve, 'algorithm.namedCurve'); @@ -95,6 +138,16 @@ async function ecGenerateKey(algorithm, extractable, keyUsages) { const usageSet = new SafeSet(keyUsages); switch (name) { case 'ECDSA': + if (namedCurve === 'NODE-ED25519' || + namedCurve === 'NODE-ED448' || + namedCurve === 'NODE-X25519' || + namedCurve === 'NODE-X448') { + throw lazyDOMException('Unsupported named curves for ECDSA'); + } + // Fall through + case 'NODE-ED25519': + // Fall through + case 'NODE-ED448': if (hasAnyNotIn(usageSet, 'sign', 'verify')) { throw lazyDOMException( 'Unsupported key usage for an ECDSA key', @@ -107,6 +160,10 @@ async function ecGenerateKey(algorithm, extractable, keyUsages) { 'Unsupported key usage for an ECDH key', 'SyntaxError'); } + if (namedCurve === 'NODE-ED25519' || namedCurve === 'NODE-ED448') { + throw lazyDOMException('Unsupported named curves for ECDH'); + } + // Fall through } return new Promise((resolve, reject) => { generateKeyPair('ec', { namedCurve }, (err, pubKey, privKey) => { @@ -121,6 +178,10 @@ async function ecGenerateKey(algorithm, extractable, keyUsages) { let publicUsages; let privateUsages; switch (name) { + case 'NODE-ED25519': + // Fall through + case 'NODE-ED448': + // Fall through case 'ECDSA': publicUsages = getUsagesUnion(usageSet, 'verify'); privateUsages = getUsagesUnion(usageSet, 'sign'); @@ -170,9 +231,13 @@ async function ecImportKey( namedCurve, 'algorithm.namedCurve', ObjectKeys(kNamedCurveAliases)); - + // Only used for NODE-EDnnnn key variants to distinguish between + // importing a raw public key or raw private key. + if (algorithm.public !== undefined) + validateBoolean(algorithm.public, 'algorithm.public'); let keyObject; const usagesSet = new SafeSet(keyUsages); + let checkNamedCurve = true; switch (format) { case 'node.keyObject': { if (!isKeyObject(keyData)) @@ -205,68 +270,111 @@ async function ecImportKey( let curve; if (keyData == null || typeof keyData !== 'object') throw lazyDOMException('Invalid JWK keyData', 'DataError'); - - if (keyData.kty !== 'EC') - throw lazyDOMException('Invalid key type', 'DataError'); - - if (keyData.d !== undefined) { - verifyAcceptableEcKeyUse(name, 'private', usagesSet); - } else { - verifyAcceptableEcKeyUse(name, 'public', usagesSet); - } - - if (usagesSet.size > 0 && keyData.use !== undefined) { - if (algorithm.name === 'ECDSA' && keyData.use !== 'sig') - throw lazyDOMException('Invalid use type', 'DataError'); - if (algorithm.name === 'ECDH' && keyData.use !== 'enc') - throw lazyDOMException('Invalid use type', 'DataError'); - } - - validateKeyOps(keyData.key_ops, usagesSet); - - if (keyData.ext !== undefined && - keyData.ext === false && - extractable === true) { - throw lazyDOMException('JWK is not extractable', 'DataError'); - } - - if (algorithm.name === 'ECDSA' && keyData.alg !== undefined) { - if (typeof keyData.alg !== 'string') - throw lazyDOMException('Invalid alg', 'DataError'); - switch (keyData.alg) { - case 'ES256': curve = 'P-256'; break; - case 'ES384': curve = 'P-384'; break; - case 'ES512': curve = 'P-521'; break; + switch (keyData.kty) { + case 'OKP': { + checkNamedCurve = false; + const isPublic = keyData.d === undefined; + const type = + namedCurve === 'NODE-X25519' || 'NODE-X448' ? 'ECDH' : 'ECDSA'; + verifyAcceptableEcKeyUse( + type, + isPublic ? 'public' : 'private', + usagesSet); + keyObject = createECRawKey( + namedCurve, + Buffer.from( + isPublic ? keyData.k : keyData.d, + 'base64'), + isPublic); + break; + } + default: { + if (keyData.kty !== 'EC') + throw lazyDOMException('Invalid key type', 'DataError'); + + if (keyData.d !== undefined) { + verifyAcceptableEcKeyUse(name, 'private', usagesSet); + } else { + verifyAcceptableEcKeyUse(name, 'public', usagesSet); + } + + if (usagesSet.size > 0 && keyData.use !== undefined) { + if (algorithm.name === 'ECDSA' && keyData.use !== 'sig') + throw lazyDOMException('Invalid use type', 'DataError'); + if (algorithm.name === 'ECDH' && keyData.use !== 'enc') + throw lazyDOMException('Invalid use type', 'DataError'); + } + + validateKeyOps(keyData.key_ops, usagesSet); + + if (keyData.ext !== undefined && + keyData.ext === false && + extractable === true) { + throw lazyDOMException('JWK is not extractable', 'DataError'); + } + + if (algorithm.name === 'ECDSA' && keyData.alg !== undefined) { + if (typeof keyData.alg !== 'string') + throw lazyDOMException('Invalid alg', 'DataError'); + switch (keyData.alg) { + case 'ES256': curve = 'P-256'; break; + case 'ES384': curve = 'P-384'; break; + case 'ES512': curve = 'P-521'; break; + } + if (curve !== namedCurve) + throw lazyDOMException('Named curve mismatch', 'DataError'); + } + + const handle = new KeyObjectHandle(); + const type = handle.initJwk(keyData, namedCurve); + if (type === undefined) + throw lazyDOMException('Invalid JWK keyData', 'DataError'); + keyObject = type === kKeyTypePrivate ? + new PrivateKeyObject(handle) : + new PublicKeyObject(handle); } - if (curve !== namedCurve) - throw lazyDOMException('Named curve mismatch', 'DataError'); } - - const handle = new KeyObjectHandle(); - const type = handle.initJwk(keyData, namedCurve); - if (type === undefined) - throw lazyDOMException('Invalid JWK keyData', 'DataError'); - - keyObject = type === kKeyTypePrivate ? - new PrivateKeyObject(handle) : - new PublicKeyObject(handle); - break; } case 'raw': { - verifyAcceptableEcKeyUse(name, 'public', usagesSet); - keyObject = createECPublicKeyRaw(namedCurve, keyData); + switch (namedCurve) { + case 'NODE-X25519': + // Fall through + case 'NODE-X448': + checkNamedCurve = false; + verifyAcceptableEcKeyUse( + 'ECDH', + algorithm.public === true ? 'public' : 'private', + usagesSet); + keyObject = createECRawKey(namedCurve, keyData, algorithm.public); + break; + case 'NODE-ED25519': + // Fall through + case 'NODE-ED448': + checkNamedCurve = false; + verifyAcceptableEcKeyUse( + 'ECDSA', + algorithm.public === true ? 'public' : 'private', + usagesSet); + keyObject = createECRawKey(namedCurve, keyData, algorithm.public); + break; + default: + verifyAcceptableEcKeyUse(name, 'public', usagesSet); + keyObject = createECPublicKeyRaw(namedCurve, keyData); + } if (keyObject === undefined) throw lazyDOMException('Unable to import EC key', 'OperationError'); break; } } - const { - namedCurve: checkNamedCurve - } = keyObject[kHandle].keyDetail({}); - if (kNamedCurveAliases[namedCurve] !== checkNamedCurve) - throw lazyDOMException('Named curve mismatch', 'DataError'); + if (checkNamedCurve) { + const { + namedCurve: checkNamedCurve + } = keyObject[kHandle].keyDetail({}); + if (kNamedCurveAliases[namedCurve] !== checkNamedCurve) + throw lazyDOMException('Named curve mismatch', 'DataError'); + } return new InternalCryptoKey( keyObject, @@ -275,22 +383,33 @@ async function ecImportKey( extractable); } -function ecdsaSignVerify(key, data, { hash }, signature) { - if (hash === undefined) - throw new ERR_MISSING_OPTION('algorithm.hash'); - +function ecdsaSignVerify(key, data, { name, hash }, signature) { const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify; const type = mode === kSignJobModeSign ? 'private' : 'public'; if (key.type !== type) throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError'); + let hashname; + switch (name) { + case 'NODE-ED25519': + // Fall through + case 'NODE-ED448': + if (hash !== undefined) + throw new lazyDOMException(`Hash is not permitted for ${name}`); + break; + default: + if (hash === undefined) + throw new ERR_MISSING_OPTION('algorithm.hash'); + hashname = normalizeHashName(hash.name); + } + return jobPromise(new SignJob( kCryptoJobAsync, mode, key[kKeyObject][kHandle], data, - normalizeHashName(hash.name), + hashname, undefined, // Salt length, not used with ECDSA undefined, // PSS Padding, not used with ECDSA signature)); diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index 9d2fee2b2dd..d08249559a8 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -148,6 +148,10 @@ const kNamedCurveAliases = { 'P-256': 'prime256v1', 'P-384': 'secp384r1', 'P-521': 'secp521r1', + 'NODE-ED25519': 'ed25519', + 'NODE-ED448': 'ed448', + 'NODE-X25519': 'x25519', + 'NODE-X448': 'x448', }; const kAesKeyLengths = [128, 192, 256]; @@ -175,7 +179,9 @@ const kAlgorithms = [ // should be prefixed with 'node-' 'node-dsa', 'node-dh', - 'node-scrypt' + 'node-scrypt', + 'node-ed25519', + 'node-ed448', ]; // These are the only export and import formats we currently diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index 7bd487972eb..2e14fcc90c3 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -78,6 +78,10 @@ async function generateKey( case 'RSA-OAEP': return lazyRequire('internal/crypto/rsa') .rsaKeyGenerate(algorithm, extractable, keyUsages); + case 'NODE-ED25519': + // Fall through + case 'NODE-ED448': + // Fall through case 'ECDSA': // Fall through case 'ECDH': @@ -203,6 +207,10 @@ async function exportKeySpki(key) { .rsaExportKey(key, kWebCryptoKeyFormatSPKI); } break; + case 'NODE-ED25519': + // Fall through + case 'NODE-ED448': + // Fall through case 'ECDSA': // Fall through case 'ECDH': @@ -242,6 +250,10 @@ async function exportKeyPkcs8(key) { .rsaExportKey(key, kWebCryptoKeyFormatPKCS8); } break; + case 'NODE-ED25519': + // Fall through + case 'NODE-ED448': + // Fall through case 'ECDSA': // Fall through case 'ECDH': @@ -271,6 +283,11 @@ async function exportKeyPkcs8(key) { async function exportKeyRaw(key) { switch (key.algorithm.name) { + case 'NODE-ED25519': + // Fall through + case 'NODE-ED448': + return lazyRequire('internal/crypto/ec') + .ecExportKey(key, kWebCryptoKeyFormatRaw); case 'ECDSA': // Fall through case 'ECDH': @@ -320,7 +337,7 @@ async function exportKeyJWK(key) { case 'ECDSA': // Fall through case 'ECDH': - jwk.crv = key.algorithm.namedCurve; + jwk.crv ||= key.algorithm.namedCurve; return jwk; case 'AES-CTR': // Fall through @@ -342,6 +359,10 @@ async function exportKeyJWK(key) { key.algorithm.hash.name, normalizeHashName.kContextJwkDsa); return jwk; + case 'NODE-ED25519': + // Fall through + case 'NODE-ED448': + return jwk; default: // Fall through } @@ -445,6 +466,10 @@ async function importKey( case 'RSA-OAEP': return lazyRequire('internal/crypto/rsa') .rsaImportKey(format, keyData, algorithm, extractable, keyUsages); + case 'NODE-ED25519': + // Fall through + case 'NODE-ED448': + // Fall through case 'ECDSA': // Fall through case 'ECDH': @@ -564,6 +589,10 @@ function signVerify(algorithm, key, data, signature) { case 'RSASSA-PKCS1-V1_5': return lazyRequire('internal/crypto/rsa') .rsaSignVerify(key, data, algorithm, signature); + case 'NODE-ED25519': + // Fall through + case 'NODE-ED448': + // Fall through case 'ECDSA': return lazyRequire('internal/crypto/ec') .ecdsaSignVerify(key, data, algorithm, signature); diff --git a/src/crypto/crypto_ecdh.cc b/src/crypto/crypto_ecdh.cc index efeb08b908e..6d3ee26fc70 100644 --- a/src/crypto/crypto_ecdh.cc +++ b/src/crypto/crypto_ecdh.cc @@ -7,6 +7,7 @@ #include "env-inl.h" #include "memory_tracker-inl.h" #include "node_buffer.h" +#include "string_bytes.h" #include "threadpoolwork-inl.h" #include "v8.h" @@ -30,14 +31,25 @@ using v8::Uint32; using v8::Value; namespace crypto { -namespace { + int GetCurveFromName(const char* name) { int nid = EC_curve_nist2nid(name); if (nid == NID_undef) nid = OBJ_sn2nid(name); + // If there is still no match, check manually for known curves + if (nid == NID_undef) { + if (strcmp(name, "NODE-ED25519") == 0) { + nid = EVP_PKEY_ED25519; + } else if (strcmp(name, "NODE-ED448") == 0) { + nid = EVP_PKEY_ED448; + } else if (strcmp(name, "NODE-X25519") == 0) { + nid = EVP_PKEY_X25519; + } else if (strcmp(name, "NODE-X448") == 0) { + nid = EVP_PKEY_X448; + } + } return nid; } -} // namespace void ECDH::Initialize(Environment* env, Local<Object> target) { Local<FunctionTemplate> t = env->NewFunctionTemplate(New); @@ -392,6 +404,11 @@ void ECDH::ConvertKey(const FunctionCallbackInfo<Value>& args) { args.GetReturnValue().Set(buf); } +void ECDHBitsConfig::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("public", public_); + tracker->TrackField("private", private_); +} + Maybe<bool> ECDHBitsTraits::EncodeOutput( Environment* env, const ECDHBitsConfig& params, @@ -416,6 +433,7 @@ Maybe<bool> ECDHBitsTraits::AdditionalConfig( KeyObjectHandle* public_key; Utf8Value name(env->isolate(), args[offset]); + ASSIGN_OR_RETURN_UNWRAP(&public_key, args[offset + 1], Nothing<bool>()); ASSIGN_OR_RETURN_UNWRAP(&private_key, args[offset + 2], Nothing<bool>()); @@ -425,23 +443,9 @@ Maybe<bool> ECDHBitsTraits::AdditionalConfig( return Nothing<bool>(); } - params->private_key = ECKeyPointer( - EC_KEY_dup( - EVP_PKEY_get1_EC_KEY(private_key->Data()->GetAsymmetricKey().get()))); - if (!params->private_key) { - THROW_ERR_CRYPTO_INVALID_KEYTYPE(env); - return Nothing<bool>(); - } - - params->public_key = ECKeyPointer( - EC_KEY_dup( - EVP_PKEY_get1_EC_KEY(public_key->Data()->GetAsymmetricKey().get()))); - if (!params->public_key) { - THROW_ERR_CRYPTO_INVALID_KEYTYPE(env); - return Nothing<bool>(); - } - - params->group = EC_KEY_get0_group(params->private_key.get()); + params->id_ = GetCurveFromName(*name); + params->private_ = private_key->Data(); + params->public_ = public_key->Data(); return Just(true); } @@ -450,44 +454,102 @@ bool ECDHBitsTraits::DeriveBits( Environment* env, const ECDHBitsConfig& params, ByteSource* out) { - if (params.group == nullptr) - return false; - CHECK_EQ(EC_KEY_check_key(params.private_key.get()), 1); - CHECK_EQ(EC_KEY_check_key(params.public_key.get()), 1); - const EC_POINT* pub = EC_KEY_get0_public_key(params.public_key.get()); - int field_size = EC_GROUP_get_degree(params.group); - size_t len = (field_size + 7) / 8; - char* data = MallocOpenSSL<char>(len); - ByteSource buf = ByteSource::Allocated(data, len); - if (ECDH_compute_key( - data, - len, - pub, - params.private_key.get(), - nullptr) <= 0) { - return false; + + char* data = nullptr; + size_t len = 0; + + switch (params.id_) { + case EVP_PKEY_X25519: + // Fall through + case EVP_PKEY_X448: { + EVPKeyCtxPointer ctx( + EVP_PKEY_CTX_new( + params.private_->GetAsymmetricKey().get(), + nullptr)); + if (EVP_PKEY_derive_init(ctx.get()) <= 0 || + EVP_PKEY_derive_set_peer( + ctx.get(), + params.public_->GetAsymmetricKey().get()) <= 0 || + EVP_PKEY_derive(ctx.get(), nullptr, &len) <= 0) { + return false; + } + + data = MallocOpenSSL<char>(len); + + if (EVP_PKEY_derive( + ctx.get(), + reinterpret_cast<unsigned char*>(data), + &len) <= 0) { + return false; + } + + break; + } + default: { + const EC_KEY* private_key = + EVP_PKEY_get0_EC_KEY(params.private_->GetAsymmetricKey().get()); + const EC_KEY* public_key = + EVP_PKEY_get0_EC_KEY(params.public_->GetAsymmetricKey().get()); + + const EC_GROUP* group = EC_KEY_get0_group(private_key); + if (group == nullptr) + return false; + + CHECK_EQ(EC_KEY_check_key(private_key), 1); + CHECK_EQ(EC_KEY_check_key(public_key), 1); + const EC_POINT* pub = EC_KEY_get0_public_key(public_key); + int field_size = EC_GROUP_get_degree(group); + len = (field_size + 7) / 8; + data = MallocOpenSSL<char>(len); + CHECK_NOT_NULL(data); + CHECK_NOT_NULL(pub); + CHECK_NOT_NULL(private_key); + if (ECDH_compute_key( + data, + len, + pub, + private_key, + nullptr) <= 0) { + return false; + } + } } + ByteSource buf = ByteSource::Allocated(data, len); *out = std::move(buf); return true; } EVPKeyCtxPointer EcKeyGenTraits::Setup(EcKeyPairGenConfig* params) { - EVPKeyCtxPointer param_ctx(EVP_PKEY_CTX_new_id(EVP_PKEY_EC, nullptr)); - EVP_PKEY* raw_params = nullptr; - if (!param_ctx || - EVP_PKEY_paramgen_init(param_ctx.get()) <= 0 || - EVP_PKEY_CTX_set_ec_paramgen_curve_nid( - param_ctx.get(), params->params.curve_nid) <= 0 || - EVP_PKEY_CTX_set_ec_param_enc( - param_ctx.get(), params->params.param_encoding) <= 0 || - EVP_PKEY_paramgen(param_ctx.get(), &raw_params) <= 0) { - return EVPKeyCtxPointer(); - } - EVPKeyPointer key_params(raw_params); - EVPKeyCtxPointer key_ctx(EVP_PKEY_CTX_new(key_params.get(), nullptr)); - - if (!key_ctx || EVP_PKEY_keygen_init(key_ctx.get()) <= 0) - return EVPKeyCtxPointer(); + EVPKeyCtxPointer key_ctx; + switch (params->params.curve_nid) { + case EVP_PKEY_ED25519: + // Fall through + case EVP_PKEY_ED448: + // Fall through + case EVP_PKEY_X25519: + // Fall through + case EVP_PKEY_X448: + key_ctx.reset(EVP_PKEY_CTX_new_id(params->params.curve_nid, nullptr)); + break; + default: { + EVPKeyCtxPointer param_ctx(EVP_PKEY_CTX_new_id(EVP_PKEY_EC, nullptr)); + EVP_PKEY* raw_params = nullptr; + if (!param_ctx || + EVP_PKEY_paramgen_init(param_ctx.get()) <= 0 || + EVP_PKEY_CTX_set_ec_paramgen_curve_nid( + param_ctx.get(), params->params.curve_nid) <= 0 || + EVP_PKEY_CTX_set_ec_param_enc( + param_ctx.get(), params->params.param_encoding) <= 0 || + EVP_PKEY_paramgen(param_ctx.get(), &raw_params) <= 0) { + return EVPKeyCtxPointer(); + } + EVPKeyPointer key_params(raw_params); + key_ctx.reset(EVP_PKEY_CTX_new(key_params.get(), nullptr)); + } + } + + if (key_ctx && EVP_PKEY_keygen_init(key_ctx.get()) <= 0) + key_ctx.reset(); return key_ctx; } @@ -538,23 +600,49 @@ WebCryptoKeyExportStatus EC_Raw_Export( CHECK(key_data->GetAsymmetricKey()); EC_KEY* ec_key = EVP_PKEY_get0_EC_KEY(key_data->GetAsymmetricKey().get()); - CHECK_NOT_NULL(ec_key); - const EC_GROUP* group = EC_KEY_get0_group(ec_key); - const EC_POINT* point = EC_KEY_get0_public_key(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; - - unsigned char* data = MallocOpenSSL<unsigned char>(len); - size_t check_len = EC_POINT_point2oct(group, point, form, data, len, nullptr); - if (check_len == 0) - return WebCryptoKeyExportStatus::FAILED; - - CHECK_EQ(len, check_len); + unsigned char* data; + size_t len = 0; + + if (ec_key == nullptr) { + typedef int (*export_fn)(const EVP_PKEY*, unsigned char*, size_t* len); + export_fn fn = nullptr; + switch (key_data->GetKeyType()) { + case kKeyTypePrivate: + fn = EVP_PKEY_get_raw_private_key; + break; + case kKeyTypePublic: + fn = EVP_PKEY_get_raw_public_key; + break; + case kKeyTypeSecret: + UNREACHABLE(); + } + CHECK_NOT_NULL(fn); + // Get the size of the raw key data + if (fn(key_data->GetAsymmetricKey().get(), nullptr, &len) == 0) + return WebCryptoKeyExportStatus::INVALID_KEY_TYPE; + data = MallocOpenSSL<unsigned char>(len); + if (fn(key_data->GetAsymmetricKey().get(), data, &len) == 0) + return WebCryptoKeyExportStatus::INVALID_KEY_TYPE; + } else { + if (key_data->GetKeyType() != kKeyTypePublic) + return WebCryptoKeyExportStatus::INVALID_KEY_TYPE; + const EC_GROUP* group = EC_KEY_get0_group(ec_key); + const EC_POINT* point = EC_KEY_get0_public_key(ec_key); + point_conversion_form_t form = POINT_CONVERSION_UNCOMPRESSED; + + // Get the allocated data size... + len = EC_POINT_point2oct(group, point, form, nullptr, 0, nullptr); + if (len == 0) + return WebCryptoKeyExportStatus::FAILED; + data = MallocOpenSSL<unsigned char>(len); + size_t check_len = + EC_POINT_point2oct(group, point, form, data, len, nullptr); + if (check_len == 0) + return WebCryptoKeyExportStatus::FAILED; + + CHECK_EQ(len, check_len); + } *out = ByteSource::Allocated(reinterpret_cast<char*>(data), len); @@ -578,8 +666,6 @@ WebCryptoKeyExportStatus ECKeyExportTraits::DoExport( switch (format) { case kWebCryptoKeyFormatRaw: - if (key_data->GetKeyType() != kKeyTypePublic) - return WebCryptoKeyExportStatus::INVALID_KEY_TYPE; return EC_Raw_Export(key_data.get(), params, out); case kWebCryptoKeyFormatPKCS8: if (key_data->GetKeyType() != kKeyTypePrivate) @@ -651,6 +737,90 @@ Maybe<bool> ExportJWKEcKey( return Just(true); } +Maybe<bool> ExportJWKEdKey( + Environment* env, + std::shared_ptr<KeyObjectData> key, + Local<Object> target) { + ManagedEVPPKey pkey = key->GetAsymmetricKey(); + + const char* curve = nullptr; + switch (EVP_PKEY_id(pkey.get())) { + case EVP_PKEY_ED25519: + curve = "Ed25519"; + break; + case EVP_PKEY_ED448: + curve = "Ed448"; + break; + case EVP_PKEY_X25519: + curve = "X25519"; + break; + case EVP_PKEY_X448: + curve = "X448"; + break; + default: + UNREACHABLE(); + } + if (target->Set( + env->context(), + env->jwk_crv_string(), + OneByteString(env->isolate(), curve)).IsNothing()) { + return Nothing<bool>(); + } + + size_t len = 0; + Local<Value> encoded; + Local<Value> error; + + if (!EVP_PKEY_get_raw_public_key(pkey.get(), nullptr, &len)) + return Nothing<bool>(); + + unsigned char* data = MallocOpenSSL<unsigned char>(len); + ByteSource out = ByteSource::Allocated(reinterpret_cast<char*>(data), len); + + if (key->GetKeyType() == kKeyTypePrivate) { + if (!EVP_PKEY_get_raw_private_key(pkey.get(), data, &len) || + !StringBytes::Encode( + env->isolate(), + reinterpret_cast<const char*>(data), + len, + BASE64URL, + &error).ToLocal(&encoded) || + !target->Set( + env->context(), + env->jwk_d_string(), + encoded).IsJust()) { + if (!error.IsEmpty()) + env->isolate()->ThrowException(error); + return Nothing<bool>(); + } + } + + if (!EVP_PKEY_get_raw_public_key(pkey.get(), data, &len) || + !StringBytes::Encode( + env->isolate(), + reinterpret_cast<const char*>(data), + len, + BASE64URL, + &error).ToLocal(&encoded) || + !target->Set( + env->context(), + env->jwk_x_string(), + encoded).IsJust()) { + if (!error.IsEmpty()) + env->isolate()->ThrowException(error); + return Nothing<bool>(); + } + + if (target->Set( + env->context(), + env->jwk_kty_string(), + env->jwk_okp_string()).IsNothing()) { + return Nothing<bool>(); + } + + return Just(true); +} + std::shared_ptr<KeyObjectData> ImportJWKEcKey( Environment* env, Local<Object> jwk, diff --git a/src/crypto/crypto_ecdh.h b/src/crypto/crypto_ecdh.h index c83818d16be..b2ae9e5b2be 100644 --- a/src/crypto/crypto_ecdh.h +++ b/src/crypto/crypto_ecdh.h @@ -16,6 +16,8 @@ namespace node { namespace crypto { +int GetCurveFromName(const char* name); + class ECDH final : public BaseObject { public: ~ECDH() override; @@ -52,11 +54,11 @@ class ECDH final : public BaseObject { }; struct ECDHBitsConfig final : public MemoryRetainer { - ECKeyPointer private_key; - ECKeyPointer public_key; - const EC_GROUP* group = nullptr; + int id_; + std::shared_ptr<KeyObjectData> private_; + std::shared_ptr<KeyObjectData> public_; - SET_NO_MEMORY_INFO(); + void MemoryInfo(MemoryTracker* tracker) const override; SET_MEMORY_INFO_NAME(ECDHBitsConfig); SET_SELF_SIZE(ECDHBitsConfig); }; @@ -144,6 +146,11 @@ v8::Maybe<bool> ExportJWKEcKey( std::shared_ptr<KeyObjectData> key, v8::Local<v8::Object> target); +v8::Maybe<bool> ExportJWKEdKey( + Environment* env, + std::shared_ptr<KeyObjectData> key, + v8::Local<v8::Object> target); + std::shared_ptr<KeyObjectData> ImportJWKEcKey( Environment* env, v8::Local<v8::Object> jwk, diff --git a/src/crypto/crypto_keygen.h b/src/crypto/crypto_keygen.h index c4197c6eaed..58e3b8211d6 100644 --- a/src/crypto/crypto_keygen.h +++ b/src/crypto/crypto_keygen.h @@ -161,7 +161,8 @@ struct KeyPairGenTraits final { Environment* env, AdditionalParameters* params) { EVPKeyCtxPointer ctx = KeyPairAlgorithmTraits::Setup(params); - if (!ctx || EVP_PKEY_keygen_init(ctx.get()) <= 0) + + if (!ctx) return KeyGenJobStatus::FAILED; // Generate the key diff --git a/src/crypto/crypto_keys.cc b/src/crypto/crypto_keys.cc index f80a39ce5de..4457141f461 100644 --- a/src/crypto/crypto_keys.cc +++ b/src/crypto/crypto_keys.cc @@ -492,6 +492,13 @@ Maybe<bool> ExportJWKAsymmetricKey( case EVP_PKEY_RSA_PSS: return ExportJWKRsaKey(env, key, target); case EVP_PKEY_DSA: return ExportJWKDsaKey(env, key, target); case EVP_PKEY_EC: return ExportJWKEcKey(env, key, target); + case EVP_PKEY_ED25519: + // Fall through + case EVP_PKEY_ED448: + // Fall through + case EVP_PKEY_X25519: + // Fall through + case EVP_PKEY_X448: return ExportJWKEdKey(env, key, target); } THROW_ERR_CRYPTO_INVALID_KEYTYPE(env); return Just(false); @@ -873,6 +880,7 @@ v8::Local<v8::Function> KeyObjectHandle::Initialize(Environment* env) { env->SetProtoMethod(t, "export", Export); env->SetProtoMethod(t, "exportJwk", ExportJWK); env->SetProtoMethod(t, "initECRaw", InitECRaw); + env->SetProtoMethod(t, "initEDRaw", InitEDRaw); env->SetProtoMethod(t, "initJwk", InitJWK); env->SetProtoMethod(t, "keyDetail", GetKeyDetail); @@ -1032,6 +1040,48 @@ void KeyObjectHandle::InitECRaw(const FunctionCallbackInfo<Value>& args) { args.GetReturnValue().Set(true); } +void KeyObjectHandle::InitEDRaw(const FunctionCallbackInfo<Value>& args) { + Environment* env = Environment::GetCurrent(args); + KeyObjectHandle* key; + ASSIGN_OR_RETURN_UNWRAP(&key, args.Holder()); + + CHECK(args[0]->IsString()); + Utf8Value name(env->isolate(), args[0]); + + ArrayBufferOrViewContents<unsigned char> key_data(args[1]); + KeyType type = static_cast<KeyType>(args[2].As<Int32>()->Value()); + + MarkPopErrorOnReturn mark_pop_error_on_return; + + typedef EVP_PKEY* (*new_key_fn)(int, ENGINE*, const unsigned char*, size_t); + new_key_fn fn = type == kKeyTypePrivate + ? EVP_PKEY_new_raw_private_key + : EVP_PKEY_new_raw_public_key; + + int id = GetCurveFromName(*name); + + switch (id) { + case EVP_PKEY_X25519: + case EVP_PKEY_X448: + case EVP_PKEY_ED25519: + case EVP_PKEY_ED448: { + EVPKeyPointer pkey(fn(id, nullptr, key_data.data(), key_data.size())); + if (!pkey) + return args.GetReturnValue().Set(false); + key->data_ = + KeyObjectData::CreateAsymmetric( + type, + ManagedEVPPKey(std::move(pkey))); + CHECK(key->data_); + break; + } + default: + UNREACHABLE(); + } + + args.GetReturnValue().Set(true); +} + void KeyObjectHandle::GetKeyDetail(const FunctionCallbackInfo<Value>& args) { Environment* env = Environment::GetCurrent(args); KeyObjectHandle* key; diff --git a/src/crypto/crypto_keys.h b/src/crypto/crypto_keys.h index 8938a203eb1..64bf45a1dba 100644 --- a/src/crypto/crypto_keys.h +++ b/src/crypto/crypto_keys.h @@ -185,6 +185,7 @@ class KeyObjectHandle : public BaseObject { static void Init(const v8::FunctionCallbackInfo<v8::Value>& args); static void InitECRaw(const v8::FunctionCallbackInfo<v8::Value>& args); + static void InitEDRaw(const v8::FunctionCallbackInfo<v8::Value>& args); static void InitJWK(const v8::FunctionCallbackInfo<v8::Value>& args); static void GetKeyDetail(const v8::FunctionCallbackInfo<v8::Value>& args); diff --git a/src/crypto/crypto_sig.cc b/src/crypto/crypto_sig.cc index a5a95878a9e..455d1bfdd6c 100644 --- a/src/crypto/crypto_sig.cc +++ b/src/crypto/crypto_sig.cc @@ -223,6 +223,16 @@ void CheckThrow(Environment* env, SignBase::Error error) { return; } } + +bool IsOneShot(const ManagedEVPPKey& key) { + switch (EVP_PKEY_id(key.get())) { + case EVP_PKEY_ED25519: + case EVP_PKEY_ED448: + return true; + default: + return false; + } +} } // namespace SignBase::Error SignBase::Init(const char* sign_type) { @@ -807,26 +817,47 @@ bool SignTraits::DeriveBits( switch (params.mode) { case SignConfiguration::kSign: { size_t len; - if (!EVP_DigestSignUpdate( - context.get(), - params.data.data<unsigned char>(), - params.data.size()) || - !EVP_DigestSignFinal(context.get(), nullptr, &len)) { - return false; - } - char* data = MallocOpenSSL<char>(len); - ByteSource buf = ByteSource::Allocated(data, len); - unsigned char* ptr = reinterpret_cast<unsigned char*>(data); - if (!EVP_DigestSignFinal(context.get(), ptr, &len)) - return false; - - // If this is an EC key (assuming ECDSA) we have to - // convert the signature in to the proper format. - if (EVP_PKEY_id(params.key->GetAsymmetricKey().get()) == EVP_PKEY_EC) { - *out = ConvertToWebCryptoSignature(params.key->GetAsymmetricKey(), buf); - } else { - buf.Resize(len); + unsigned char* data = nullptr; + if (IsOneShot(params.key->GetAsymmetricKey())) { + EVP_DigestSign( + context.get(), + nullptr, + &len, + params.data.data<unsigned char>(), + params.data.size()); + data = MallocOpenSSL<unsigned char>(len); + EVP_DigestSign( + context.get(), + data, + &len, + params.data.data<unsigned char>(), + params.data.size()); + ByteSource buf = + ByteSource::Allocated(reinterpret_cast<char*>(data), len); *out = std::move(buf); + } else { + if (!EVP_DigestSignUpdate( + context.get(), + params.data.data<unsigned char>(), + params.data.size()) || + !EVP_DigestSignFinal(context.get(), nullptr, &len)) { + return false; + } + data = MallocOpenSSL<unsigned char>(len); + ByteSource buf = + ByteSource::Allocated(reinterpret_cast<char*>(data), len); + if (!EVP_DigestSignFinal(context.get(), data, &len)) + return false; + + // If this is an EC key (assuming ECDSA) we have to + // convert the signature in to the proper format. + if (EVP_PKEY_id(params.key->GetAsymmetricKey().get()) == EVP_PKEY_EC) { + *out = ConvertToWebCryptoSignature( + params.key->GetAsymmetricKey(), buf); + } else { + buf.Resize(len); + *out = std::move(buf); + } } break; } @@ -834,17 +865,12 @@ bool SignTraits::DeriveBits( char* data = MallocOpenSSL<char>(1); data[0] = 0; *out = ByteSource::Allocated(data, 1); - if (!EVP_DigestVerifyUpdate( - context.get(), - params.data.data<unsigned char>(), - params.data.size())) { - return false; - } - - if (EVP_DigestVerifyFinal( + if (EVP_DigestVerify( context.get(), params.signature.data<unsigned char>(), - params.signature.size()) == 1) { + params.signature.size(), + params.data.data<unsigned char>(), + params.data.size()) == 1) { data[0] = 1; } } diff --git a/src/env.h b/src/env.h index 6be3cdb22fc..1268cb071ee 100644 --- a/src/env.h +++ b/src/env.h @@ -280,6 +280,7 @@ constexpr size_t kFsStatsBufferLength = V(isclosing_string, "isClosing") \ V(issuer_string, "issuer") \ V(issuercert_string, "issuerCertificate") \ + V(jwk_crv_string, "crv") \ V(jwk_d_string, "d") \ V(jwk_dp_string, "dp") \ V(jwk_dq_string, "dq") \ @@ -294,6 +295,7 @@ constexpr size_t kFsStatsBufferLength = V(jwk_kty_string, "kty") \ V(jwk_n_string, "n") \ V(jwk_oct_string, "oct") \ + V(jwk_okp_string, "OKP") \ V(jwk_rsa_string, "RSA") \ V(jwk_x_string, "x") \ V(jwk_y_string, "y") \ diff --git a/test/parallel/test-webcrypto-ed25519-ed448.js b/test/parallel/test-webcrypto-ed25519-ed448.js new file mode 100644 index 00000000000..b06de24c78f --- /dev/null +++ b/test/parallel/test-webcrypto-ed25519-ed448.js @@ -0,0 +1,367 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = require('crypto').webcrypto; + +async function generateKey(namedCurve) { + return subtle.generateKey( + { + name: namedCurve, + namedCurve + }, + true, + ['sign', 'verify']); +} + +async function test1(namedCurve) { + const { + publicKey, + privateKey, + } = await generateKey(namedCurve); + + const data = Buffer.from('hello world'); + + assert(publicKey); + assert(privateKey); + + const sig = await subtle.sign( + { name: namedCurve }, + privateKey, + data + ); + + assert(sig); + + assert(await subtle.verify( + { name: namedCurve }, + publicKey, + sig, + data + )); +} + +Promise.all([ + test1('NODE-ED25519'), + test1('NODE-ED448') +]).then(common.mustCall()); + +assert.rejects( + subtle.importKey( + 'raw', + Buffer.alloc(10), + { + name: 'NODE-ED25519', + namedCurve: 'NODE-ED25519' + }, + false, + ['sign']), + { + message: /NODE-ED25519 raw keys must be exactly 32-bytes/ + }); + +assert.rejects( + subtle.importKey( + 'raw', + Buffer.alloc(10), + { + name: 'NODE-ED448', + namedCurve: 'NODE-ED448' + }, + false, + ['sign']), + { + message: /NODE-ED448 raw keys must be exactly 57-bytes/ + }); + +const testVectors = { + 'NODE-ED25519': [ + { + privateKey: + Buffer.from( + '9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60', + 'hex'), + publicKey: + Buffer.from( + 'd75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a', + 'hex'), + message: Buffer.alloc(0), + sig: + Buffer.from( + 'e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e06522490155' + + '5fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b', + 'hex'), + crv: 'Ed25519', + }, + { + privateKey: + Buffer.from( + '4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb', + 'hex'), + publicKey: + Buffer.from( + '3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c', + 'hex'), + message: Buffer.from('72', 'hex'), + sig: + Buffer.from( + '92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da' + + '085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00', + 'hex'), + crv: 'Ed25519', + }, + { + privateKey: + Buffer.from( + 'c5aa8df43f9f837bedb7442f31dcb7b166d38535076f094b85ce3a2e0b4458f7', + 'hex'), + publicKey: + Buffer.from( + 'fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025', + 'hex'), + message: Buffer.from('af82', 'hex'), + sig: + Buffer.from( + '6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac' + + '18ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a', + 'hex'), + crv: 'Ed25519', + }, + { + privateKey: + Buffer.from( + 'f5e5767cf153319517630f226876b86c8160cc583bc013744c6bf255f5cc0ee5', + 'hex'), + publicKey: + Buffer.from( + '278117fc144c72340f67d0f2316e8386ceffbf2b2428c9c51fef7c597f1d426e', + 'hex'), + message: Buffer.from( + '08b8b2b733424243760fe426a4b54908632110a66c2f6591eabd3345e3e4eb98' + + 'fa6e264bf09efe12ee50f8f54e9f77b1e355f6c50544e23fb1433ddf73be84d8' + + '79de7c0046dc4996d9e773f4bc9efe5738829adb26c81b37c93a1b270b20329d' + + '658675fc6ea534e0810a4432826bf58c941efb65d57a338bbd2e26640f89ffbc' + + '1a858efcb8550ee3a5e1998bd177e93a7363c344fe6b199ee5d02e82d522c4fe' + + 'ba15452f80288a821a579116ec6dad2b3b310da903401aa62100ab5d1a36553e' + + '06203b33890cc9b832f79ef80560ccb9a39ce767967ed628c6ad573cb116dbef' + + 'efd75499da96bd68a8a97b928a8bbc103b6621fcde2beca1231d206be6cd9ec7' + + 'aff6f6c94fcd7204ed3455c68c83f4a41da4af2b74ef5c53f1d8ac70bdcb7ed1' + + '85ce81bd84359d44254d95629e9855a94a7c1958d1f8ada5d0532ed8a5aa3fb2' + + 'd17ba70eb6248e594e1a2297acbbb39d502f1a8c6eb6f1ce22b3de1a1f40cc24' + + '554119a831a9aad6079cad88425de6bde1a9187ebb6092cf67bf2b13fd65f270' + + '88d78b7e883c8759d2c4f5c65adb7553878ad575f9fad878e80a0c9ba63bcbcc' + + '2732e69485bbc9c90bfbd62481d9089beccf80cfe2df16a2cf65bd92dd597b07' + + '07e0917af48bbb75fed413d238f5555a7a569d80c3414a8d0859dc65a46128ba' + + 'b27af87a71314f318c782b23ebfe808b82b0ce26401d2e22f04d83d1255dc51a' + + 'ddd3b75a2b1ae0784504df543af8969be3ea7082ff7fc9888c144da2af58429e' + + 'c96031dbcad3dad9af0dcbaaaf268cb8fcffead94f3c7ca495e056a9b47acdb7' + + '51fb73e666c6c655ade8297297d07ad1ba5e43f1bca32301651339e22904cc8c' + + '42f58c30c04aafdb038dda0847dd988dcda6f3bfd15c4b4c4525004aa06eeff8' + + 'ca61783aacec57fb3d1f92b0fe2fd1a85f6724517b65e614ad6808d6f6ee34df' + + 'f7310fdc82aebfd904b01e1dc54b2927094b2db68d6f903b68401adebf5a7e08' + + 'd78ff4ef5d63653a65040cf9bfd4aca7984a74d37145986780fc0b16ac451649' + + 'de6188a7dbdf191f64b5fc5e2ab47b57f7f7276cd419c17a3ca8e1b939ae49e4' + + '88acba6b965610b5480109c8b17b80e1b7b750dfc7598d5d5011fd2dcc5600a3' + + '2ef5b52a1ecc820e308aa342721aac0943bf6686b64b2579376504ccc493d97e' + + '6aed3fb0f9cd71a43dd497f01f17c0e2cb3797aa2a2f256656168e6c496afc5f' + + 'b93246f6b1116398a346f1a641f3b041e989f7914f90cc2c7fff357876e506b5' + + '0d334ba77c225bc307ba537152f3f1610e4eafe595f6d9d90d11faa933a15ef1' + + '369546868a7f3a45a96768d40fd9d03412c091c6315cf4fde7cb68606937380d' + + 'b2eaaa707b4c4185c32eddcdd306705e4dc1ffc872eeee475a64dfac86aba41c' + + '0618983f8741c5ef68d3a101e8a3b8cac60c905c15fc910840b94c00a0b9d0', + 'hex'), + sig: Buffer.from( + '0aab4c900501b3e24d7cdf4663326a3a87df5e4843b2cbdb67cbf6e460fec350' + + 'aa5371b1508f9f4528ecea23c436d94b5e8fcd4f681e30a6ac00a9704a188a03', + 'hex'), + crv: 'Ed25519', + } + ], + 'NODE-ED448': [ + { + privateKey: + Buffer.from( + '6c82a562cb808d10d632be89c8513ebf6c929f34ddfa8c9f63c9960ef6e348a3' + + '528c8a3fcc2f044e39a3fc5b94492f8f032e7549a20098f95b', 'hex'), + publicKey: + Buffer.from( + '5fd7449b59b461fd2ce787ec616ad46a1da1342485a70e1f8a0ea75d80e96778' + + 'edf124769b46c7061bd6783df1e50f6cd1fa1abeafe8256180', 'hex'), + message: Buffer.alloc(0), + sig: + Buffer.from( + '533a37f6bbe457251f023c0d88f976ae2dfb504a843e34d2074fd823d41a591f' + + '2b233f034f628281f2fd7a22ddd47d7828c59bd0a21bfd3980ff0d2028d4b18a' + + '9df63e006c5d1c2d345b925d8dc00b4104852db99ac5c7cdda8530a113a0f4db' + + 'b61149f05a7363268c71d95808ff2e652600', 'hex'), + crv: 'Ed448', + }, + { + privateKey: + Buffer.from( + 'c4eab05d357007c632f3dbb48489924d552b08fe0c353a0d4a1f00acda2c463a' + + 'fbea67c5e8d2877c5e3bc397a659949ef8021e954e0a12274e', 'hex'), + publicKey: + Buffer.from( + '43ba28f430cdff456ae531545f7ecd0ac834a55d9358c0372bfa0c6c6798c086' + + '6aea01eb00742802b8438ea4cb82169c235160627b4c3a9480', 'hex'), + message: Buffer.from('03', 'hex'), + sig: + Buffer.from( + '26b8f91727bd62897af15e41eb43c377efb9c610d48f2335cb0bd0087810f435' + + '2541b143c4b981b7e18f62de8ccdf633fc1bf037ab7cd779805e0dbcc0aae1cb' + + 'cee1afb2e027df36bc04dcecbf154336c19f0af7e0a6472905e799f1953d2a0f' + + 'f3348ab21aa4adafd1d234441cf807c03a00', 'hex'), + crv: 'Ed448', + }, + { + privateKey: + Buffer.from( + 'cd23d24f714274e744343237b93290f511f6425f98e64459ff203e8985083ffd' + + 'f60500553abc0e05cd02184bdb89c4ccd67e187951267eb328', 'hex'), + publicKey: + Buffer.from( + 'dcea9e78f35a1bf3499a831b10b86c90aac01cd84b67a0109b55a36e9328b1e3' + + '65fce161d71ce7131a543ea4cb5f7e9f1d8b00696447001400', 'hex'), + message: Buffer.from('0c3e544074ec63b0265e0c', 'hex'), + sig: + Buffer.from( + '1f0a8888ce25e8d458a21130879b840a9089d999aaba039eaf3e3afa090a09d3' + + '89dba82c4ff2ae8ac5cdfb7c55e94d5d961a29fe0109941e00b8dbdeea6d3b05' + + '1068df7254c0cdc129cbe62db2dc957dbb47b51fd3f213fb8698f064774250a5' + + '028961c9bf8ffd973fe5d5c206492b140e00', 'hex'), + crv: 'Ed448', + } + ] +}; + +async function test2(namedCurve) { + const vectors = testVectors[namedCurve]; + await Promise.all(vectors.map(async (vector) => { + const [ + privateKey, + publicKey, + ] = await Promise.all([ + subtle.importKey( + 'raw', + vector.privateKey, + { + name: namedCurve, + namedCurve + }, + true, ['sign']), + subtle.importKey( + 'raw', + vector.publicKey, + { + name: namedCurve, + namedCurve, + public: true + }, + true, ['verify']) + ]); + + const [ + rawKey1, + rawKey2 + ] = await Promise.all([ + subtle.exportKey('raw', privateKey), + subtle.exportKey('raw', publicKey) + ]); + assert.deepStrictEqual(Buffer.from(rawKey1), vector.privateKey); + assert.deepStrictEqual(Buffer.from(rawKey2), vector.publicKey); + + const sig = await subtle.sign( + { name: namedCurve }, + privateKey, + vector.message + ); + + assert(sig); + + assert(await subtle.verify( + { name: namedCurve }, + publicKey, + vector.sig, + vector.message + )); + + const [ + publicKeyJwk, + privateKeyJwk + ] = await Promise.all([ + subtle.exportKey('jwk', publicKey), + subtle.exportKey('jwk', privateKey) + ]); + assert.strictEqual(publicKeyJwk.kty, 'OKP'); + assert.strictEqual(privateKeyJwk.kty, 'OKP'); + assert.strictEqual(publicKeyJwk.crv, vector.crv); + assert.strictEqual(privateKeyJwk.crv, vector.crv); + assert.deepStrictEqual( + Buffer.from(publicKeyJwk.x, 'base64'), + vector.publicKey); + assert.deepStrictEqual( + Buffer.from(privateKeyJwk.x, 'base64'), + vector.publicKey); + assert.deepStrictEqual( + Buffer.from(privateKeyJwk.d, 'base64'), + vector.privateKey); + })); +} + +Promise.all([ + test2('NODE-ED25519'), + test2('NODE-ED448') +]).then(common.mustCall()); + +assert.rejects( + subtle.generateKey( + { + name: 'ECDSA', + namedCurve: 'NODE-X25519' + }, + true, + ['sign', 'verify']), + { + message: /Unsupported named curves for ECDSA/ + }); + +assert.rejects( + subtle.generateKey( + { + name: 'ECDSA', + namedCurve: 'NODE-X448' + }, + true, + ['sign', 'verify']), + { + message: /Unsupported named curves for ECDSA/ + }); + +assert.rejects( + subtle.generateKey( + { + name: 'ECDSA', + namedCurve: 'NODE-ED25519' + }, + true, + ['sign', 'verify']), + { + message: /Unsupported named curves for ECDSA/ + }); + +assert.rejects( + subtle.generateKey( + { + name: 'ECDSA', + namedCurve: 'NODE-ED448' + }, + true, + ['sign', 'verify']), + { + message: /Unsupported named curves for ECDSA/ + }); diff --git a/test/parallel/test-webcrypto-x25519-x448.js b/test/parallel/test-webcrypto-x25519-x448.js new file mode 100644 index 00000000000..94794840630 --- /dev/null +++ b/test/parallel/test-webcrypto-x25519-x448.js @@ -0,0 +1,246 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = require('crypto').webcrypto; + +// X25519 and X448 are ECDH named curves that should work +// with the existing ECDH mechanisms with no additional +// changes. + +async function generateKeys(namedCurve, ...usages) { + return subtle.generateKey( + { + name: 'ECDH', + namedCurve + }, + true, + usages); +} + +async function deriveKey(publicKey, privateKey, length = 256) { + return subtle.deriveKey( + { + name: 'ECDH', + public: publicKey, + }, + privateKey, + { + name: 'HMAC', + length, + hash: 'SHA-512', + }, + true, + ['sign', 'verify'] + ); +} + +async function exportKey(secretKey) { + return subtle.exportKey('raw', secretKey); +} + +async function importKey(namedCurve, keyData, isPublic = false) { + return subtle.importKey( + 'raw', + keyData, + { name: 'ECDH', namedCurve, public: isPublic }, + true, + ['deriveKey'] + ); +} + +assert.rejects(importKey('NODE-X25519', Buffer.alloc(10), true), { + message: /NODE-X25519 raw keys must be exactly 32-bytes/ +}); +assert.rejects(importKey('NODE-X448', Buffer.alloc(10), true), { + message: /NODE-X448 raw keys must be exactly 56-bytes/ +}); + +async function test1(namedCurve) { + const { + publicKey: publicKey1, + privateKey: privateKey1, + } = await generateKeys(namedCurve, 'deriveKey', 'deriveBits'); + + const { + publicKey: publicKey2, + privateKey: privateKey2, + } = await generateKeys(namedCurve, 'deriveKey', 'deriveBits'); + + assert(publicKey1); + assert(privateKey1); + assert(publicKey2); + assert(privateKey2); + + assert.strictEqual(publicKey1.algorithm.namedCurve, namedCurve); + assert.strictEqual(privateKey1.algorithm.namedCurve, namedCurve); + assert.strictEqual(publicKey2.algorithm.namedCurve, namedCurve); + assert.strictEqual(privateKey2.algorithm.namedCurve, namedCurve); + + const [key1, key2] = await Promise.all([ + deriveKey(publicKey1, privateKey2), + deriveKey(publicKey2, privateKey1), + ]); + + assert(key1); + assert(key2); + + const [secret1, secret2] = await Promise.all([ + exportKey(key1), + exportKey(key2), + ]); + + assert.deepStrictEqual(secret1, secret2); +} + +Promise.all([ + test1('NODE-X25519'), + test1('NODE-X448'), +]).then(common.mustCall()); + +const testVectors = { + 'NODE-X25519': { + alice: { + privateKey: + Buffer.from( + '77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a', + 'hex'), + publicKey: + Buffer.from( + '8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a', + 'hex'), + }, + bob: { + privateKey: + Buffer.from( + '5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb', + 'hex'), + publicKey: + Buffer.from( + 'de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f', + 'hex'), + }, + sharedSecret: + Buffer.from( + '4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742', + 'hex'), + }, + 'NODE-X448': { + alice: { + privateKey: + Buffer.from( + '9a8f4925d1519f5775cf46b04b5800d4ee9ee8bae8bc5565d498c28d' + + 'd9c9baf574a9419744897391006382a6f127ab1d9ac2d8c0a598726b', + 'hex'), + publicKey: + Buffer.from( + '9b08f7cc31b7e3e67d22d5aea121074a273bd2b83de09c63faa73d2c' + + '22c5d9bbc836647241d953d40c5b12da88120d53177f80e532c41fa0', + 'hex'), + }, + bob: { + privateKey: + Buffer.from( + '1c306a7ac2a0e2e0990b294470cba339e6453772b075811d8fad0d1d' + + '6927c120bb5ee8972b0d3e21374c9c921b09d1b0366f10b65173992d', + 'hex'), + publicKey: + Buffer.from( + '3eb7a829b0cd20f5bcfc0b599b6feccf6da4627107bdb0d4f345b430' + + '27d8b972fc3e34fb4232a13ca706dcb57aec3dae07bdc1c67bf33609', + 'hex'), + }, + sharedSecret: + Buffer.from( + '07fff4181ac6cc95ec1c16a94a0f74d12da232ce40a77552281d282b' + + 'b60c0b56fd2464c335543936521c24403085d59a449a5037514a879d', + 'hex'), + }, +}; + +async function test2(namedCurve, length) { + const [ + publicKey1, + publicKey2, + privateKey1, + privateKey2, + ] = await Promise.all([ + importKey(namedCurve, testVectors[namedCurve].alice.publicKey, true), + importKey(namedCurve, testVectors[namedCurve].bob.publicKey, true), + importKey(namedCurve, testVectors[namedCurve].alice.privateKey), + importKey(namedCurve, testVectors[namedCurve].bob.privateKey), + ]); + + const [key1, key2] = await Promise.all([ + deriveKey(publicKey1, privateKey2, length), + deriveKey(publicKey2, privateKey1, length), + ]); + + assert(key1); + assert(key2); + + const [secret1, secret2] = await Promise.all([ + exportKey(key1), + exportKey(key2), + ]); + + assert.deepStrictEqual(secret1, secret2); + + assert.deepStrictEqual( + Buffer.from(secret1), + testVectors[namedCurve].sharedSecret); + + const [ + publicKeyJwk, + privateKeyJwk + ] = await Promise.all([ + subtle.exportKey('jwk', publicKey1), + subtle.exportKey('jwk', privateKey1) + ]); + assert.strictEqual(publicKeyJwk.kty, 'OKP'); + assert.strictEqual(privateKeyJwk.kty, 'OKP'); + assert.strictEqual(publicKeyJwk.crv, namedCurve.slice(5)); + assert.strictEqual(privateKeyJwk.crv, namedCurve.slice(5)); + assert.deepStrictEqual( + Buffer.from(publicKeyJwk.x, 'base64'), + testVectors[namedCurve].alice.publicKey); + assert.deepStrictEqual( + Buffer.from(privateKeyJwk.x, 'base64'), + testVectors[namedCurve].alice.publicKey); + assert.deepStrictEqual( + Buffer.from(privateKeyJwk.d, 'base64'), + testVectors[namedCurve].alice.privateKey); +} + +Promise.all([ + test2('NODE-X25519', 256), + test2('NODE-X448', 448), +]).then(common.mustCall()); + +assert.rejects( + subtle.generateKey( + { + name: 'ECDH', + namedCurve: 'NODE-ED25519' + }, + true, + ['deriveBits']), + { + message: /Unsupported named curves for ECDH/ + }); + +assert.rejects( + subtle.generateKey( + { + name: 'ECDH', + namedCurve: 'NODE-ED448' + }, + true, + ['deriveBits']), + { + message: /Unsupported named curves for ECDH/ + }); diff --git a/tools/doc/type-parser.js b/tools/doc/type-parser.js index 5a91b1556c5..41e6086bf00 100644 --- a/tools/doc/type-parser.js +++ b/tools/doc/type-parser.js @@ -114,6 +114,10 @@ const customTypesMap = { 'NodeScryptImportParams': 'webcrypto.html#webcrypto_class_nodescryptimportparams', 'NodeScryptParams': 'webcrypto.html#webcrypto_class_nodescryptparams', + 'NodeEdKeyImportParams': + 'webcrypto.html#webcrypto_class_nodeedkeyimportparams', + 'NodeEdKeyGenParams': + 'webcrypto.html#webcrypto_class_nodeedkeygenparams', 'dgram.Socket': 'dgram.html#dgram_class_dgram_socket', |