diff --git a/doc/api/webcrypto.md b/doc/api/webcrypto.md index fd4b5609a42f2f..898d7779d783d2 100644 --- a/doc/api/webcrypto.md +++ b/doc/api/webcrypto.md @@ -1924,26 +1924,39 @@ added: v24.15.0 * Type: {ArrayBuffer|TypedArray|DataView|Buffer|undefined} -The `functionName` member represents the function name, used by NIST to define -functions based on cSHAKE. -The Node.js Web Crypto API implementation only supports zero-length functionName -which is equivalent to not providing functionName at all. +The `functionName` member represents the NIST function-name byte string used to +domain-separate functions built on top of cSHAKE. Accepted values are: + +* empty or `undefined`, in which case cSHAKE is equivalent to plain SHAKE +* the ASCII byte sequence `'KMAC'` +* the ASCII byte sequence `'TupleHash'` +* the ASCII byte sequence `'ParallelHash'` #### `cShakeParams.customization` * Type: {ArrayBuffer|TypedArray|DataView|Buffer|undefined} -The `customization` member represents the customization string. -The Node.js Web Crypto API implementation only supports zero-length customization -which is equivalent to not providing customization at all. +The `customization` member represents the customization data. Accepted +values are: + +* empty or `undefined`, in which case cSHAKE is equivalent to plain SHAKE +* up to 512 bytes of arbitrary data ### Class: `EcdhKeyDeriveParams` @@ -2472,9 +2485,7 @@ added: v24.8.0 added: v24.15.0 --> -* Type: {number} - -The length of the output in bytes. This must be a positive integer. +* Type: {number} represents the requested output length in bits. #### `kmacParams.customization` diff --git a/lib/internal/crypto/aes.js b/lib/internal/crypto/aes.js index 981502c51700be..7ce1dabbf7c5b6 100644 --- a/lib/internal/crypto/aes.js +++ b/lib/internal/crypto/aes.js @@ -1,10 +1,5 @@ 'use strict'; -const { - ArrayPrototypePush, - SafeSet, -} = primordials; - const { AESCipherJob, kCryptoJobWebCrypto, @@ -28,7 +23,6 @@ const { const { getUsagesMask, - hasAnyNotIn, jobPromise, } = require('internal/crypto/util'); @@ -40,16 +34,28 @@ const { InternalCryptoKey, getCryptoKeyAlgorithm, getCryptoKeyHandle, - getKeyObjectHandle, - getKeyObjectSymmetricKeySize, } = require('internal/crypto/keys'); const { importJwkSecretKey, importSecretKey, validateJwk, + validateKeyUsages, + validateUsagesNotEmpty, } = require('internal/crypto/webcrypto_util'); +const kCipherUsages = ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey']; +const kWrapUsages = ['wrapKey', 'unwrapKey']; + +const kUsages = { + '__proto__': null, + 'AES-CBC': kCipherUsages, + 'AES-CTR': kCipherUsages, + 'AES-GCM': kCipherUsages, + 'AES-KW': kWrapUsages, + 'AES-OCB': kCipherUsages, +}; + function getAlgorithmName(name, length) { switch (name) { case 'AES-CBC': return `A${length}CBC`; @@ -178,21 +184,8 @@ function aesCipher(mode, key, data, algorithm) { function aesGenerateKey(algorithm, extractable, usages) { const { name, length } = algorithm; - const checkUsages = ['wrapKey', 'unwrapKey']; - if (name !== 'AES-KW') - ArrayPrototypePush(checkUsages, 'encrypt', 'decrypt'); - - const usagesSet = new SafeSet(usages); - if (hasAnyNotIn(usagesSet, checkUsages)) { - throw lazyDOMException( - 'Unsupported key usage for an AES key', - 'SyntaxError'); - } - if (usagesSet.size === 0) { - throw lazyDOMException( - 'Usages cannot be empty when creating a key.', - 'SyntaxError'); - } + const usagesSet = validateUsagesNotEmpty( + validateKeyUsages(usages, kUsages[name], name)); return jobPromise(() => new SecretKeyGenJob( kCryptoJobWebCrypto, @@ -209,24 +202,15 @@ function aesImportKey( extractable, usages) { const { name } = algorithm; - const checkUsages = ['wrapKey', 'unwrapKey']; - if (name !== 'AES-KW') - ArrayPrototypePush(checkUsages, 'encrypt', 'decrypt'); - - const usagesSet = new SafeSet(usages); - if (hasAnyNotIn(usagesSet, checkUsages)) { - throw lazyDOMException( - 'Unsupported key usage for an AES key', - 'SyntaxError'); - } + const usagesSet = validateKeyUsages(usages, kUsages[name], name); let handle; let length; switch (format) { - case 'KeyObject': { - length = getKeyObjectSymmetricKeySize(keyData) * 8; + case 'KeyObjectHandle': { + length = keyData.getSymmetricKeySize() * 8; validateKeyLength(length); - handle = getKeyObjectHandle(keyData); + handle = keyData; break; } case 'raw-secret': diff --git a/lib/internal/crypto/cfrg.js b/lib/internal/crypto/cfrg.js index 97660f87a271b0..b561ae479abffd 100644 --- a/lib/internal/crypto/cfrg.js +++ b/lib/internal/crypto/cfrg.js @@ -13,6 +13,7 @@ const { kCryptoJobWebCrypto, kKeyFormatDER, kKeyFormatRawPublic, + kKeyTypePublic, kSignJobModeSign, kSignJobModeVerify, kWebCryptoKeyFormatPKCS8, @@ -27,8 +28,6 @@ const { const { getUsagesMask, - getUsagesUnion, - hasAnyNotIn, jobPromise, } = require('internal/crypto/util'); @@ -39,66 +38,37 @@ const { const { getCryptoKeyHandle, getCryptoKeyType, - getKeyObjectHandle, - getKeyObjectType, InternalCryptoKey, } = require('internal/crypto/keys'); const { + createKeyUsages, + getKeyPairUsages, importDerKey, importJwkKey, importRawKey, validateJwk, + validateKeyUsages, + validateUsagesNotEmpty, + verifyAcceptableKeyUse, } = require('internal/crypto/webcrypto_util'); -function verifyAcceptableCfrgKeyUse(name, isPublic, usages) { - let checkSet; - switch (name) { - case 'X25519': - // Fall through - case 'X448': - checkSet = isPublic ? [] : ['deriveKey', 'deriveBits']; - break; - case 'Ed25519': - // Fall through - case 'Ed448': - checkSet = isPublic ? ['verify'] : ['sign']; - break; - default: - throw lazyDOMException( - 'The algorithm is not supported', 'NotSupportedError'); - } - if (hasAnyNotIn(usages, checkSet)) { - throw lazyDOMException( - `Unsupported key usage for a ${name} key`, - 'SyntaxError'); - } -} +const kDeriveUsages = createKeyUsages([], ['deriveKey', 'deriveBits']); + +const kSignVerifyUsages = createKeyUsages(['verify'], ['sign']); + +const kUsages = { + '__proto__': null, + 'X25519': kDeriveUsages, + 'X448': kDeriveUsages, + 'Ed25519': kSignVerifyUsages, + 'Ed448': kSignVerifyUsages, +}; function cfrgGenerateKey(algorithm, extractable, usages) { const { name } = algorithm; - - const usageSet = new SafeSet(usages); - switch (name) { - case 'Ed25519': - // Fall through - case 'Ed448': - if (hasAnyNotIn(usageSet, ['sign', 'verify'])) { - throw lazyDOMException( - `Unsupported key usage for an ${name} key`, - 'SyntaxError'); - } - break; - case 'X25519': - // Fall through - case 'X448': - if (hasAnyNotIn(usageSet, ['deriveKey', 'deriveBits'])) { - throw lazyDOMException( - `Unsupported key usage for an ${name} key`, - 'SyntaxError'); - } - break; - } + const allowedUsages = kUsages[name]; + const usagesSet = validateKeyUsages(usages, allowedUsages.keygen, name); const nid = { '__proto__': null, 'Ed25519': EVP_PKEY_ED25519, @@ -107,37 +77,16 @@ function cfrgGenerateKey(algorithm, extractable, usages) { 'X448': EVP_PKEY_X448, }[name]; - let publicUsages; - let privateUsages; - switch (name) { - case 'Ed25519': - // Fall through - case 'Ed448': - publicUsages = getUsagesUnion(usageSet, 'verify'); - privateUsages = getUsagesUnion(usageSet, 'sign'); - break; - case 'X25519': - // Fall through - case 'X448': - publicUsages = new SafeSet(); - privateUsages = getUsagesUnion(usageSet, 'deriveKey', 'deriveBits'); - break; - } - const keyAlgorithm = { name }; - - if (privateUsages.size === 0) { - throw lazyDOMException( - 'Usages cannot be empty when creating a key.', - 'SyntaxError'); - } + const keyUsages = getKeyPairUsages(usagesSet, allowedUsages); + validateUsagesNotEmpty(keyUsages.private); return jobPromise(() => new NidKeyPairGenJob( kCryptoJobWebCrypto, nid, keyAlgorithm, - getUsagesMask(publicUsages), - getUsagesMask(privateUsages), + getUsagesMask(keyUsages.public), + getUsagesMask(keyUsages.private), extractable)); } @@ -176,21 +125,25 @@ function cfrgImportKey( const { name } = algorithm; let handle; + const allowedUsages = kUsages[name]; const usagesSet = new SafeSet(usages); switch (format) { - case 'KeyObject': { - verifyAcceptableCfrgKeyUse( - name, getKeyObjectType(keyData) === 'public', usagesSet); - handle = getKeyObjectHandle(keyData); + case 'KeyObjectHandle': + verifyAcceptableKeyUse( + name, + usagesSet, + keyData.getKeyType() === kKeyTypePublic ? + allowedUsages.public : + allowedUsages.private); + handle = keyData; break; - } case 'spki': { - verifyAcceptableCfrgKeyUse(name, true, usagesSet); + verifyAcceptableKeyUse(name, usagesSet, allowedUsages.public); handle = importDerKey(keyData, true); break; } case 'pkcs8': { - verifyAcceptableCfrgKeyUse(name, false, usagesSet); + verifyAcceptableKeyUse(name, usagesSet, allowedUsages.private); handle = importDerKey(keyData, false); break; } @@ -209,7 +162,10 @@ function cfrgImportKey( } const isPublic = keyData.d === undefined; - verifyAcceptableCfrgKeyUse(name, isPublic, usagesSet); + verifyAcceptableKeyUse( + name, + usagesSet, + isPublic ? allowedUsages.public : allowedUsages.private); handle = importJwkKey(isPublic, keyData); if (!isPublic) { @@ -220,7 +176,7 @@ function cfrgImportKey( break; } case 'raw': { - verifyAcceptableCfrgKeyUse(name, true, usagesSet); + verifyAcceptableKeyUse(name, usagesSet, allowedUsages.public); handle = importRawKey(true, keyData, kKeyFormatRawPublic, name); break; } diff --git a/lib/internal/crypto/chacha20_poly1305.js b/lib/internal/crypto/chacha20_poly1305.js index 689cab59f3fbf2..45071cded217ee 100644 --- a/lib/internal/crypto/chacha20_poly1305.js +++ b/lib/internal/crypto/chacha20_poly1305.js @@ -1,9 +1,5 @@ 'use strict'; -const { - SafeSet, -} = primordials; - const { ChaCha20Poly1305CipherJob, SecretKeyGenJob, @@ -12,7 +8,6 @@ const { const { getUsagesMask, - hasAnyNotIn, jobPromise, } = require('internal/crypto/util'); @@ -23,15 +18,18 @@ const { const { InternalCryptoKey, getCryptoKeyHandle, - getKeyObjectHandle, } = require('internal/crypto/keys'); const { importJwkSecretKey, importSecretKey, validateJwk, + validateKeyUsages, + validateUsagesNotEmpty, } = require('internal/crypto/webcrypto_util'); +const kUsages = ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey']; + function validateKeyLength(length) { if (length !== 256) throw lazyDOMException('Invalid key length', 'DataError'); @@ -50,19 +48,8 @@ function c20pCipher(mode, key, data, algorithm) { function c20pGenerateKey(algorithm, extractable, usages) { const { name } = algorithm; - const checkUsages = ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey']; - - const usagesSet = new SafeSet(usages); - if (hasAnyNotIn(usagesSet, checkUsages)) { - throw lazyDOMException( - `Unsupported key usage for a ${algorithm.name} key`, - 'SyntaxError'); - } - if (usagesSet.size === 0) { - throw lazyDOMException( - 'Usages cannot be empty when creating a key.', - 'SyntaxError'); - } + const usagesSet = validateUsagesNotEmpty( + validateKeyUsages(usages, kUsages, name)); return jobPromise(() => new SecretKeyGenJob( kCryptoJobWebCrypto, @@ -79,19 +66,14 @@ function c20pImportKey( extractable, usages) { const { name } = algorithm; - const checkUsages = ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey']; - const usagesSet = new SafeSet(usages); - if (hasAnyNotIn(usagesSet, checkUsages)) { - throw lazyDOMException( - `Unsupported key usage for a ${algorithm.name} key`, - 'SyntaxError'); - } + const usagesSet = validateKeyUsages( + usages, kUsages, name); let handle; switch (format) { - case 'KeyObject': { - handle = getKeyObjectHandle(keyData); + case 'KeyObjectHandle': { + handle = keyData; break; } case 'raw-secret': { diff --git a/lib/internal/crypto/cipher.js b/lib/internal/crypto/cipher.js index 1aeec8275d7b2d..d9eef29c5f71dc 100644 --- a/lib/internal/crypto/cipher.js +++ b/lib/internal/crypto/cipher.js @@ -108,7 +108,8 @@ function getUIntOption(options, key) { if (options && (value = options[key]) != null) { if (value >>> 0 !== value) throw new ERR_INVALID_ARG_VALUE(`options.${key}`, value); - return value; + // Coerce -0 to +0. + return value + 0; } return -1; } @@ -265,10 +266,16 @@ function getCipherInfo(nameOrNid, options) { if (options !== undefined) { validateObject(options, 'options'); ({ keyLength, ivLength } = options); - if (keyLength !== undefined) + if (keyLength !== undefined) { validateInt32(keyLength, 'options.keyLength'); - if (ivLength !== undefined) + // Coerce -0 to +0. + keyLength += 0; + } + if (ivLength !== undefined) { validateInt32(ivLength, 'options.ivLength'); + // Coerce -0 to +0. + ivLength += 0; + } } const ret = _getCipherInfo({}, nameOrNid, keyLength, ivLength); diff --git a/lib/internal/crypto/diffiehellman.js b/lib/internal/crypto/diffiehellman.js index 876b64077a2aae..8eae1f6890313e 100644 --- a/lib/internal/crypto/diffiehellman.js +++ b/lib/internal/crypto/diffiehellman.js @@ -3,11 +3,9 @@ const { ArrayBufferPrototypeSlice, FunctionPrototypeCall, - MathCeil, ObjectDefineProperty, SafeSet, TypedArrayPrototypeGetBuffer, - Uint8Array, } = primordials; const { Buffer } = require('buffer'); @@ -65,7 +63,9 @@ const { getArrayBufferOrView, jobPromise, jobPromiseThen, + numBitsToBytes, toBuf, + truncateToBitLength, kHandle, } = require('internal/crypto/util'); @@ -98,8 +98,11 @@ function DiffieHellman(sizeOrKey, keyEncoding, generator, genEncoding) { // rejected with ERR_OSSL_BN_BITS_TOO_SMALL) by OpenSSL. The glue code // in node_crypto.cc accepts values that are IsInt32() for that reason // and that's why we do that here too. - if (typeof sizeOrKey === 'number') + if (typeof sizeOrKey === 'number') { validateInt32(sizeOrKey, 'sizeOrKey'); + // Coerce -0 to +0. + sizeOrKey += 0; + } if (keyEncoding && !Buffer.isEncoding(keyEncoding) && keyEncoding !== 'buffer') { @@ -362,7 +365,6 @@ function diffieHellman(options, callback) { job.run(); } -let masks; // The ecdhDeriveBits function is part of the Web Crypto API and serves both // deriveKeys and deriveBits functions. function ecdhDeriveBits(algorithm, baseKey, length) { @@ -406,27 +408,20 @@ function ecdhDeriveBits(algorithm, baseKey, length) { return bits; return jobPromiseThen(bits, (bits) => { - // If the length is not a multiple of 8 the nearest ceiled - // multiple of 8 is sliced. - const sliceLength = MathCeil(length / 8); + const sliceLength = numBitsToBytes(length); const { byteLength } = bits; // If the length is larger than the derived secret, throw. if (byteLength < sliceLength) throw lazyDOMException('derived bit length is too small', 'OperationError'); - const slice = ArrayBufferPrototypeSlice(bits, 0, sliceLength); - - const mod = length % 8; - if (mod === 0) - return slice; - - // eslint-disable-next-line no-sparse-arrays - masks ||= [, 0b10000000, 0b11000000, 0b11100000, 0b11110000, 0b11111000, 0b11111100, 0b11111110]; + if (length % 8 === 0) { + if (byteLength === sliceLength) + return bits; + return ArrayBufferPrototypeSlice(bits, 0, sliceLength); + } - const masked = new Uint8Array(slice); - masked[sliceLength - 1] = masked[sliceLength - 1] & masks[mod]; - return TypedArrayPrototypeGetBuffer(masked); + return TypedArrayPrototypeGetBuffer(truncateToBitLength(length, bits)); }); } diff --git a/lib/internal/crypto/ec.js b/lib/internal/crypto/ec.js index d102b3fe05a29c..fc19e7cce6a5e7 100644 --- a/lib/internal/crypto/ec.js +++ b/lib/internal/crypto/ec.js @@ -30,8 +30,6 @@ const { const { getUsagesMask, - getUsagesUnion, - hasAnyNotIn, jobPromise, normalizeHashName, kNamedCurveAliases, @@ -46,86 +44,46 @@ const { getCryptoKeyAlgorithm, getCryptoKeyHandle, getCryptoKeyType, - getKeyObjectHandle, - getKeyObjectType, } = require('internal/crypto/keys'); const { + createKeyUsages, + getKeyPairUsages, importDerKey, importJwkKey, importRawKey, validateJwk, + validateKeyUsages, + validateUsagesNotEmpty, + verifyAcceptableKeyUse, } = require('internal/crypto/webcrypto_util'); -function verifyAcceptableEcKeyUse(name, isPublic, usages) { - let checkSet; - switch (name) { - case 'ECDH': - checkSet = isPublic ? [] : ['deriveKey', 'deriveBits']; - break; - case 'ECDSA': - checkSet = isPublic ? ['verify'] : ['sign']; - break; - default: - throw lazyDOMException( - 'The algorithm is not supported', 'NotSupportedError'); - } - if (hasAnyNotIn(usages, checkSet)) { - throw lazyDOMException( - `Unsupported key usage for a ${name} key`, - 'SyntaxError'); - } -} +const kDeriveUsages = createKeyUsages([], ['deriveKey', 'deriveBits']); -function ecGenerateKey(algorithm, extractable, usages) { - const { name, namedCurve } = algorithm; +const kSignVerifyUsages = createKeyUsages(['verify'], ['sign']); - const usageSet = new SafeSet(usages); - switch (name) { - case 'ECDSA': - if (hasAnyNotIn(usageSet, ['sign', 'verify'])) { - throw lazyDOMException( - 'Unsupported key usage for an ECDSA key', - 'SyntaxError'); - } - break; - case 'ECDH': - if (hasAnyNotIn(usageSet, ['deriveKey', 'deriveBits'])) { - throw lazyDOMException( - 'Unsupported key usage for an ECDH key', - 'SyntaxError'); - } - // Fall through - } +const kUsages = { + '__proto__': null, + 'ECDH': kDeriveUsages, + 'ECDSA': kSignVerifyUsages, +}; - let publicUsages; - let privateUsages; - switch (name) { - case 'ECDSA': - publicUsages = getUsagesUnion(usageSet, 'verify'); - privateUsages = getUsagesUnion(usageSet, 'sign'); - break; - case 'ECDH': - publicUsages = new SafeSet(); - privateUsages = getUsagesUnion(usageSet, 'deriveKey', 'deriveBits'); - break; - } +function ecGenerateKey(algorithm, extractable, usages) { + const { name, namedCurve } = algorithm; + const allowedUsages = kUsages[name]; + const usagesSet = validateKeyUsages(usages, allowedUsages.keygen, name); const keyAlgorithm = { name, namedCurve }; - - if (privateUsages.size === 0) { - throw lazyDOMException( - 'Usages cannot be empty when creating a key.', - 'SyntaxError'); - } + const keyUsages = getKeyPairUsages(usagesSet, allowedUsages); + validateUsagesNotEmpty(keyUsages.private); return jobPromise(() => new EcKeyPairGenJob( kCryptoJobWebCrypto, namedCurve, undefined, keyAlgorithm, - getUsagesMask(publicUsages), - getUsagesMask(privateUsages), + getUsagesMask(keyUsages.public), + getUsagesMask(keyUsages.private), extractable)); } @@ -183,21 +141,25 @@ function ecImportKey( const { name, namedCurve } = algorithm; let handle; + const allowedUsages = kUsages[name]; const usagesSet = new SafeSet(usages); switch (format) { - case 'KeyObject': { - verifyAcceptableEcKeyUse( - name, getKeyObjectType(keyData) === 'public', usagesSet); - handle = getKeyObjectHandle(keyData); + case 'KeyObjectHandle': + verifyAcceptableKeyUse( + name, + usagesSet, + keyData.getKeyType() === kKeyTypePublic ? + allowedUsages.public : + allowedUsages.private); + handle = keyData; break; - } case 'spki': { - verifyAcceptableEcKeyUse(name, true, usagesSet); + verifyAcceptableKeyUse(name, usagesSet, allowedUsages.public); handle = importDerKey(keyData, true); break; } case 'pkcs8': { - verifyAcceptableEcKeyUse(name, false, usagesSet); + verifyAcceptableKeyUse(name, usagesSet, allowedUsages.private); handle = importDerKey(keyData, false); break; } @@ -224,12 +186,15 @@ function ecImportKey( } const isPublic = keyData.d === undefined; - verifyAcceptableEcKeyUse(name, isPublic, usagesSet); + verifyAcceptableKeyUse( + name, + usagesSet, + isPublic ? allowedUsages.public : allowedUsages.private); handle = importJwkKey(isPublic, keyData); break; } case 'raw': { - verifyAcceptableEcKeyUse(name, true, usagesSet); + verifyAcceptableKeyUse(name, usagesSet, allowedUsages.public); handle = importRawKey(true, keyData, kKeyFormatRawPublic, 'ec', namedCurve); break; } diff --git a/lib/internal/crypto/hash.js b/lib/internal/crypto/hash.js index 7e8561f4b344c8..b51388418d2ee1 100644 --- a/lib/internal/crypto/hash.js +++ b/lib/internal/crypto/hash.js @@ -6,9 +6,11 @@ const { StringPrototypeReplace, StringPrototypeToLowerCase, Symbol, + TypedArrayPrototypeGetBuffer, } = primordials; const { + CShakeJob, Hash: _Hash, HashJob, Hmac: _Hmac, @@ -21,7 +23,10 @@ const { const { getStringOption, jobPromise, + jobPromiseThen, normalizeHashName, + numBitsToBytes, + truncateToBitLength, validateMaxBufferLength, kHandle, getCachedHashId, @@ -95,10 +100,13 @@ function Hash(algorithm, options) { const isCopy = algorithm instanceof _Hash; if (!isCopy) validateString(algorithm, 'algorithm'); - const xofLen = typeof options === 'object' && options !== null ? + let xofLen = typeof options === 'object' && options !== null ? options.outputLength : undefined; - if (xofLen !== undefined) + if (xofLen !== undefined) { validateUint32(xofLen, 'options.outputLength'); + // Coerce -0 to +0. + xofLen += 0; + } // Lookup the cached ID from JS land because it's faster than decoding // the string in C++ land. const algorithmId = isCopy ? -1 : getCachedHashId(algorithm); @@ -220,15 +228,41 @@ function asyncDigest(algorithm, data) { case 'SHA3-384': // Fall through case 'SHA3-512': - // Fall through + return jobPromise(() => new HashJob( + kCryptoJobWebCrypto, + normalizeHashName(algorithm.name), + data)); case 'cSHAKE128': // Fall through - case 'cSHAKE256': - return jobPromise(() => new HashJob( + case 'cSHAKE256': { + const outputLength = algorithm.outputLength; + if (algorithm.functionName?.byteLength || + algorithm.customization?.byteLength) { + if (CShakeJob === undefined) { + throw lazyDOMException( + 'Non-empty CShakeParams functionName or customization is not supported', + 'NotSupportedError'); + } + + return jobPromise(() => new CShakeJob( + kCryptoJobWebCrypto, + algorithm.name, + data, + algorithm.functionName, + algorithm.customization, + outputLength)); + } + + const bits = jobPromise(() => new HashJob( kCryptoJobWebCrypto, normalizeHashName(algorithm.name), data, - algorithm.outputLength)); + numBitsToBytes(outputLength) * 8)); + if (outputLength % 8 === 0) + return bits; + return jobPromiseThen(bits, (bits) => + TypedArrayPrototypeGetBuffer(truncateToBitLength(outputLength, bits))); + } case 'TurboSHAKE128': // Fall through case 'TurboSHAKE256': @@ -247,9 +281,13 @@ function asyncDigest(algorithm, data) { algorithm.customization, algorithm.outputLength / 8, data)); + /* c8 ignore start */ + default: { + const assert = require('internal/assert'); + assert.fail('Unreachable code'); + } + /* c8 ignore stop */ } - - throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } function hash(algorithm, input, options) { @@ -288,6 +326,8 @@ function hash(algorithm, input, options) { if (outputLength !== undefined) { validateUint32(outputLength, 'outputLength'); + // Coerce -0 to +0. + outputLength += 0; } if (outputLength === undefined) { diff --git a/lib/internal/crypto/hkdf.js b/lib/internal/crypto/hkdf.js index 73b16da6923024..c9b868e23af4e2 100644 --- a/lib/internal/crypto/hkdf.js +++ b/lib/internal/crypto/hkdf.js @@ -58,6 +58,8 @@ const validateParameters = hideStackFrames((hash, key, salt, info, length) => { info = validateByteSource.withoutStackTrace(info, 'info'); validateInteger.withoutStackTrace(length, 'length', 0, kMaxLength); + // Coerce -0 to +0. + length += 0; if (info.byteLength > 1024) { throw new ERR_OUT_OF_RANGE.HideStackFramesError( diff --git a/lib/internal/crypto/keygen.js b/lib/internal/crypto/keygen.js index 515a31141913df..553aca2daa4877 100644 --- a/lib/internal/crypto/keygen.js +++ b/lib/internal/crypto/keygen.js @@ -219,14 +219,18 @@ function createJob(mode, type, options) { case 'rsa-pss': { validateObject(options, 'options'); - const { modulusLength } = options; + let { modulusLength } = options; validateUint32(modulusLength, 'options.modulusLength'); + // Coerce -0 to +0. + modulusLength += 0; let { publicExponent } = options; if (publicExponent == null) { publicExponent = 0x10001; } else { validateUint32(publicExponent, 'options.publicExponent'); + // Coerce -0 to +0. + publicExponent += 0; } if (type === 'rsa') { @@ -239,11 +243,15 @@ function createJob(mode, type, options) { } const { - hash, mgf1Hash, hashAlgorithm, mgf1HashAlgorithm, saltLength, + hash, mgf1Hash, hashAlgorithm, mgf1HashAlgorithm, } = options; + let { saltLength } = options; - if (saltLength !== undefined) + if (saltLength !== undefined) { validateInt32(saltLength, 'options.saltLength', 0); + // Coerce -0 to +0. + saltLength += 0; + } if (hashAlgorithm !== undefined) validateString(hashAlgorithm, 'options.hashAlgorithm'); if (mgf1HashAlgorithm !== undefined) @@ -284,14 +292,19 @@ function createJob(mode, type, options) { case 'dsa': { validateObject(options, 'options'); - const { modulusLength } = options; + let { modulusLength } = options; validateUint32(modulusLength, 'options.modulusLength'); + // Coerce -0 to +0. + modulusLength += 0; let { divisorLength } = options; if (divisorLength == null) { divisorLength = -1; - } else + } else { validateInt32(divisorLength, 'options.divisorLength', 0); + // Coerce -0 to +0. + divisorLength += 0; + } return new DsaKeyPairGenJob( mode, @@ -321,7 +334,8 @@ function createJob(mode, type, options) { case 'dh': { validateObject(options, 'options'); - const { group, primeLength, prime, generator } = options; + const { group, prime } = options; + let { primeLength, generator } = options; if (group != null) { if (prime != null) throw new ERR_INCOMPATIBLE_OPTION_PAIR('group', 'prime'); @@ -342,6 +356,8 @@ function createJob(mode, type, options) { validateBuffer(prime, 'options.prime'); } else if (primeLength != null) { validateInt32(primeLength, 'options.primeLength', 0); + // Coerce -0 to +0. + primeLength += 0; } else { throw new ERR_MISSING_OPTION( 'At least one of the group, prime, or primeLength options'); @@ -349,6 +365,8 @@ function createJob(mode, type, options) { if (generator != null) { validateInt32(generator, 'options.generator', 0); + // Coerce -0 to +0. + generator += 0; } return new DhKeyPairGenJob( mode, diff --git a/lib/internal/crypto/keys.js b/lib/internal/crypto/keys.js index 2722ecc0520e2c..9c6c238c00e193 100644 --- a/lib/internal/crypto/keys.js +++ b/lib/internal/crypto/keys.js @@ -143,6 +143,8 @@ const { 2: PublicKeyObject, 3: PrivateKeyObject, } = createNativeKeyObjectClass((NativeKeyObject) => { + let webidl; + // Publicly visible KeyObject class. class KeyObject extends NativeKeyObject { #slots; @@ -160,6 +162,32 @@ const { return getKeyObjectType(this); } + toCryptoKey(algorithm, extractable, keyUsages) { + webidl ??= require('internal/crypto/webidl'); + algorithm = normalizeAlgorithm(webidl.converters.AlgorithmIdentifier(algorithm), 'importKey'); + extractable = webidl.converters.boolean(extractable); + keyUsages = webidl.converters['sequence'](keyUsages); + + const type = getKeyObjectType(this); + const handle = getKeyObjectHandle(this); + switch (type) { + case 'secret': + return toCryptoKeySecret( + handle, + algorithm, + extractable, + keyUsages); + case 'public': + // Fall through + case 'private': + return toCryptoKey( + handle, + algorithm, + extractable, + keyUsages); + } + } + static from(key) { if (!isCryptoKey(key)) throw new ERR_INVALID_ARG_TYPE('key', 'CryptoKey', key); @@ -210,8 +238,6 @@ const { }, }); - let webidl; - class SecretKeyObject extends KeyObject { constructor(handle) { super('secret', handle); @@ -233,67 +259,6 @@ const { } return handle.export(); } - - toCryptoKey(algorithm, extractable, keyUsages) { - webidl ??= require('internal/crypto/webidl'); - algorithm = normalizeAlgorithm(webidl.converters.AlgorithmIdentifier(algorithm), 'importKey'); - extractable = webidl.converters.boolean(extractable); - keyUsages = webidl.converters['sequence'](keyUsages); - - let result; - switch (algorithm.name) { - case 'HMAC': - // Fall through - case 'KMAC128': - // Fall through - case 'KMAC256': - result = require('internal/crypto/mac') - .macImportKey('KeyObject', this, algorithm, extractable, keyUsages); - break; - case 'AES-CTR': - // Fall through - case 'AES-CBC': - // Fall through - case 'AES-GCM': - // Fall through - case 'AES-KW': - // Fall through - case 'AES-OCB': - result = require('internal/crypto/aes') - .aesImportKey(algorithm, 'KeyObject', this, extractable, keyUsages); - break; - case 'ChaCha20-Poly1305': - result = require('internal/crypto/chacha20_poly1305') - .c20pImportKey(algorithm, 'KeyObject', this, extractable, keyUsages); - break; - case 'HKDF': - // Fall through - case 'PBKDF2': - // Fall through - case 'Argon2d': - // Fall through - case 'Argon2i': - // Fall through - case 'Argon2id': - result = importGenericSecretKey( - algorithm, - 'KeyObject', - this, - extractable, - keyUsages); - break; - default: - throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); - } - - if (getCryptoKeyUsagesMask(result) === 0) { - throw lazyDOMException( - `Usages cannot be empty when importing a ${getCryptoKeyType(result)} key.`, - 'SyntaxError'); - } - - return result; - } } class AsymmetricKeyObject extends KeyObject { @@ -304,68 +269,6 @@ const { get asymmetricKeyDetails() { return { ...getKeyObjectAsymmetricKeyDetails(this) }; } - - toCryptoKey(algorithm, extractable, keyUsages) { - webidl ??= require('internal/crypto/webidl'); - algorithm = normalizeAlgorithm(webidl.converters.AlgorithmIdentifier(algorithm), 'importKey'); - extractable = webidl.converters.boolean(extractable); - keyUsages = webidl.converters['sequence'](keyUsages); - - let result; - switch (algorithm.name) { - case 'RSASSA-PKCS1-v1_5': - // Fall through - case 'RSA-PSS': - // Fall through - case 'RSA-OAEP': - result = require('internal/crypto/rsa') - .rsaImportKey('KeyObject', this, algorithm, extractable, keyUsages); - break; - case 'ECDSA': - // Fall through - case 'ECDH': - result = require('internal/crypto/ec') - .ecImportKey('KeyObject', this, algorithm, extractable, keyUsages); - break; - case 'Ed25519': - // Fall through - case 'Ed448': - // Fall through - case 'X25519': - // Fall through - case 'X448': - result = require('internal/crypto/cfrg') - .cfrgImportKey('KeyObject', this, algorithm, extractable, keyUsages); - break; - case 'ML-DSA-44': - // Fall through - case 'ML-DSA-65': - // Fall through - case 'ML-DSA-87': - result = require('internal/crypto/ml_dsa') - .mlDsaImportKey('KeyObject', this, algorithm, extractable, keyUsages); - break; - case 'ML-KEM-512': - // Fall through - case 'ML-KEM-768': - // Fall through - case 'ML-KEM-1024': - result = require('internal/crypto/ml_kem') - .mlKemImportKey('KeyObject', this, algorithm, extractable, keyUsages); - break; - default: - throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); - } - - const resultType = getCryptoKeyType(result); - if (resultType === 'private' && getCryptoKeyUsagesMask(result) === 0) { - throw lazyDOMException( - `Usages cannot be empty when importing a ${resultType} key.`, - 'SyntaxError'); - } - - return result; - } } class PublicKeyObject extends AsymmetricKeyObject { @@ -778,6 +681,167 @@ function createPublicKey(key) { return new PublicKeyObject(handle); } +/** + * Converts a secret KeyObjectHandle to a CryptoKey by dispatching to the + * algorithm-specific Web Crypto import path. + * @param {KeyObjectHandle} keyData + * @param {object} algorithm + * @param {boolean} extractable + * @param {string[]} keyUsages + * @returns {CryptoKey} + */ +function toCryptoKeySecret( + keyData, + algorithm, + extractable, + keyUsages, +) { + let result; + switch (algorithm.name) { + case 'HMAC': + // Fall through + case 'KMAC128': + // Fall through + case 'KMAC256': + result = require('internal/crypto/mac') + .macImportKey('KeyObjectHandle', keyData, algorithm, extractable, keyUsages); + break; + case 'AES-CTR': + // Fall through + case 'AES-CBC': + // Fall through + case 'AES-GCM': + // Fall through + case 'AES-KW': + // Fall through + case 'AES-OCB': + result = require('internal/crypto/aes') + .aesImportKey(algorithm, 'KeyObjectHandle', keyData, extractable, keyUsages); + break; + case 'ChaCha20-Poly1305': + result = require('internal/crypto/chacha20_poly1305') + .c20pImportKey(algorithm, 'KeyObjectHandle', keyData, extractable, keyUsages); + break; + case 'HKDF': + // Fall through + case 'PBKDF2': + // Fall through + case 'Argon2d': + // Fall through + case 'Argon2i': + // Fall through + case 'Argon2id': + result = importGenericSecretKey( + algorithm, + 'KeyObjectHandle', + keyData, + extractable, + keyUsages); + break; + default: + throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); + } + + if (getCryptoKeyUsagesMask(result) === 0) { + throw lazyDOMException( + `Usages cannot be empty when importing a ${getCryptoKeyType(result)} key.`, + 'SyntaxError'); + } + + return result; +} + +/** + * Converts an asymmetric KeyObjectHandle to a CryptoKey by dispatching to + * the algorithm-specific Web Crypto import path. This preserves the same + * algorithm and usage validation used by SubtleCrypto.importKey(). + * @param {KeyObjectHandle} keyData + * @param {object} algorithm + * @param {boolean} extractable + * @param {string[]} keyUsages + * @returns {CryptoKey} + */ +function toCryptoKey( + keyData, + algorithm, + extractable, + keyUsages, +) { + let result; + switch (algorithm.name) { + case 'RSASSA-PKCS1-v1_5': + // Fall through + case 'RSA-PSS': + // Fall through + case 'RSA-OAEP': + result = require('internal/crypto/rsa') + .rsaImportKey('KeyObjectHandle', keyData, algorithm, extractable, keyUsages); + break; + case 'ECDSA': + // Fall through + case 'ECDH': + result = require('internal/crypto/ec') + .ecImportKey('KeyObjectHandle', keyData, algorithm, extractable, keyUsages); + break; + case 'Ed25519': + // Fall through + case 'Ed448': + // Fall through + case 'X25519': + // Fall through + case 'X448': + result = require('internal/crypto/cfrg') + .cfrgImportKey('KeyObjectHandle', keyData, algorithm, extractable, keyUsages); + break; + case 'ML-DSA-44': + // Fall through + case 'ML-DSA-65': + // Fall through + case 'ML-DSA-87': + result = require('internal/crypto/ml_dsa') + .mlDsaImportKey('KeyObjectHandle', keyData, algorithm, extractable, keyUsages); + break; + case 'ML-KEM-512': + // Fall through + case 'ML-KEM-768': + // Fall through + case 'ML-KEM-1024': + result = require('internal/crypto/ml_kem') + .mlKemImportKey('KeyObjectHandle', keyData, algorithm, extractable, keyUsages); + break; + default: + throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); + } + + const resultType = getCryptoKeyType(result); + if (resultType === 'private' && getCryptoKeyUsagesMask(result) === 0) { + throw lazyDOMException( + `Usages cannot be empty when importing a ${resultType} key.`, + 'SyntaxError'); + } + + return result; +} + +/** + * Derives a public CryptoKey from a private CryptoKey. The resulting key uses + * the private key's algorithm, is extractable, and validates the requested + * public usages through the shared asymmetric import path. + * @param {CryptoKey} key + * @param {string[]} keyUsages + * @returns {CryptoKey} + */ +function toPublicCryptoKey(key, keyUsages) { + const handle = new KeyObjectHandle(); + handle.init(kKeyTypePublic, getCryptoKeyHandle(key), + null, null, null, null); + return toCryptoKey( + handle, + getCryptoKeyAlgorithm(key), + true, + keyUsages); +} + function createPrivateKey(key) { const { format, type, data, passphrase, namedCurve } = prepareAsymmetricKey(key, kCreatePrivate); @@ -1204,8 +1268,8 @@ function importGenericSecretKey( let handle; switch (format) { - case 'KeyObject': { - handle = getKeyObjectHandle(keyData); + case 'KeyObjectHandle': { + handle = keyData; break; } case 'raw-secret': @@ -1238,6 +1302,7 @@ module.exports = { parsePublicKeyEncoding, parsePrivateKeyEncoding, parseKeyEncoding, + toPublicCryptoKey, preparePrivateKey, preparePublicOrPrivateKey, prepareSecretKey, diff --git a/lib/internal/crypto/mac.js b/lib/internal/crypto/mac.js index 724b2104d4b8c8..f6f82238b54986 100644 --- a/lib/internal/crypto/mac.js +++ b/lib/internal/crypto/mac.js @@ -1,7 +1,6 @@ 'use strict'; const { - SafeSet, StringPrototypeSubstring, } = primordials; @@ -17,9 +16,10 @@ const { const { getBlockSize, getUsagesMask, - hasAnyNotIn, jobPromise, normalizeHashName, + numBitsToBytes, + truncateToBitLength, } = require('internal/crypto/util'); const { @@ -30,16 +30,39 @@ const { InternalCryptoKey, getCryptoKeyAlgorithm, getCryptoKeyHandle, - getKeyObjectHandle, - getKeyObjectSymmetricKeySize, } = require('internal/crypto/keys'); const { importJwkSecretKey, importSecretKey, validateJwk, + validateKeyUsages, + validateUsagesNotEmpty, } = require('internal/crypto/webcrypto_util'); +const kUsages = ['sign', 'verify']; + +function normalizeKeyLength(handle, algorithm) { + let length = handle.getSymmetricKeySize() * 8; + if (length === 0 && algorithm.name === 'HMAC') + throw lazyDOMException('Zero-length key is not supported', 'DataError'); + + if (algorithm.length !== undefined) { + const byteLength = numBitsToBytes(algorithm.length); + if (byteLength !== handle.getSymmetricKeySize()) + throw lazyDOMException('Invalid key length', 'DataError'); + + if (algorithm.length % 8 !== 0) { + handle = importSecretKey( + truncateToBitLength(algorithm.length, handle.export())); + } + + length = algorithm.length; + } + + return { handle, length }; +} + function hmacGenerateKey(algorithm, extractable, usages) { const { hash, @@ -47,17 +70,8 @@ function hmacGenerateKey(algorithm, extractable, usages) { length = getBlockSize(hash.name), } = algorithm; - const usageSet = new SafeSet(usages); - if (hasAnyNotIn(usageSet, ['sign', 'verify'])) { - throw lazyDOMException( - 'Unsupported key usage for an HMAC key', - 'SyntaxError'); - } - if (usageSet.size === 0) { - throw lazyDOMException( - 'Usages cannot be empty when creating a key.', - 'SyntaxError'); - } + const usageSet = validateUsagesNotEmpty( + validateKeyUsages(usages, kUsages, name)); return jobPromise(() => new SecretKeyGenJob( kCryptoJobWebCrypto, @@ -77,17 +91,8 @@ function kmacGenerateKey(algorithm, extractable, usages) { }[name], } = algorithm; - const usageSet = new SafeSet(usages); - if (hasAnyNotIn(usageSet, ['sign', 'verify'])) { - throw lazyDOMException( - `Unsupported key usage for ${name} key`, - 'SyntaxError'); - } - if (usageSet.size === 0) { - throw lazyDOMException( - 'Usages cannot be empty when creating a key.', - 'SyntaxError'); - } + const usageSet = validateUsagesNotEmpty( + validateKeyUsages(usages, kUsages, name)); return jobPromise(() => new SecretKeyGenJob( kCryptoJobWebCrypto, @@ -105,18 +110,13 @@ function macImportKey( usages, ) { const isHmac = algorithm.name === 'HMAC'; - const usagesSet = new SafeSet(usages); - if (hasAnyNotIn(usagesSet, ['sign', 'verify'])) { - throw lazyDOMException( - `Unsupported key usage for ${algorithm.name} key`, - 'SyntaxError'); - } + const usagesSet = validateKeyUsages( + usages, kUsages, algorithm.name); let handle; let length; switch (format) { - case 'KeyObject': { - length = getKeyObjectSymmetricKeySize(keyData) * 8; - handle = getKeyObjectHandle(keyData); + case 'KeyObjectHandle': { + handle = keyData; break; } case 'raw-secret': @@ -124,7 +124,6 @@ function macImportKey( if (format === 'raw' && !isHmac) { return undefined; } - length = keyData.byteLength * 8; handle = importSecretKey(keyData); break; } @@ -142,20 +141,13 @@ function macImportKey( } handle = importJwkSecretKey(keyData); - length = handle.getSymmetricKeySize() * 8; break; } default: return undefined; } - if (length === 0) - throw lazyDOMException('Zero-length key is not supported', 'DataError'); - - if (algorithm.length !== undefined && - algorithm.length !== length) { - throw lazyDOMException('Invalid key length', 'DataError'); - } + ({ handle, length } = normalizeKeyLength(handle, algorithm)); // eslint-disable-line prefer-const const algorithmObject = { name: algorithm.name, @@ -192,7 +184,8 @@ function kmacSignVerify(key, data, algorithm, signature) { getCryptoKeyHandle(key), algorithm.name, algorithm.customization, - algorithm.outputLength / 8, + getCryptoKeyAlgorithm(key).length, + algorithm.outputLength, data, signature)); } diff --git a/lib/internal/crypto/ml_dsa.js b/lib/internal/crypto/ml_dsa.js index 26eb2e51e9bb6f..bfaa57d92c46d1 100644 --- a/lib/internal/crypto/ml_dsa.js +++ b/lib/internal/crypto/ml_dsa.js @@ -16,6 +16,7 @@ const { kKeyFormatDER, kKeyFormatRawPublic, kKeyFormatRawSeed, + kKeyTypePublic, kSignJobModeSign, kSignJobModeVerify, kWebCryptoKeyFormatRaw, @@ -29,8 +30,6 @@ const { const { getUsagesMask, - getUsagesUnion, - hasAnyNotIn, jobPromise, } = require('internal/crypto/util'); @@ -42,36 +41,26 @@ const { getCryptoKeyAlgorithm, getCryptoKeyHandle, getCryptoKeyType, - getKeyObjectHandle, - getKeyObjectType, InternalCryptoKey, } = require('internal/crypto/keys'); const { + createKeyUsages, + getKeyPairUsages, importDerKey, importJwkKey, importRawKey, validateJwk, + validateKeyUsages, + validateUsagesNotEmpty, + verifyAcceptableKeyUse, } = require('internal/crypto/webcrypto_util'); -function verifyAcceptableMlDsaKeyUse(name, isPublic, usages) { - const checkSet = isPublic ? ['verify'] : ['sign']; - if (hasAnyNotIn(usages, checkSet)) { - throw lazyDOMException( - `Unsupported key usage for a ${name} key`, - 'SyntaxError'); - } -} +const kUsages = createKeyUsages(['verify'], ['sign']); function mlDsaGenerateKey(algorithm, extractable, usages) { const { name } = algorithm; - - const usageSet = new SafeSet(usages); - if (hasAnyNotIn(usageSet, ['sign', 'verify'])) { - throw lazyDOMException( - `Unsupported key usage for an ${name} key`, - 'SyntaxError'); - } + const usagesSet = validateKeyUsages(usages, kUsages.keygen, name); const nid = { '__proto__': null, @@ -80,23 +69,16 @@ function mlDsaGenerateKey(algorithm, extractable, usages) { 'ML-DSA-87': EVP_PKEY_ML_DSA_87, }[name]; - const publicUsages = getUsagesUnion(usageSet, 'verify'); - const privateUsages = getUsagesUnion(usageSet, 'sign'); - const keyAlgorithm = { name }; - - if (privateUsages.size === 0) { - throw lazyDOMException( - 'Usages cannot be empty when creating a key.', - 'SyntaxError'); - } + const keyUsages = getKeyPairUsages(usagesSet, kUsages); + validateUsagesNotEmpty(keyUsages.private); return jobPromise(() => new NidKeyPairGenJob( kCryptoJobWebCrypto, nid, keyAlgorithm, - getUsagesMask(publicUsages), - getUsagesMask(privateUsages), + getUsagesMask(keyUsages.public), + getUsagesMask(keyUsages.private), extractable)); } @@ -150,19 +132,22 @@ function mlDsaImportKey( let handle; const usagesSet = new SafeSet(usages); switch (format) { - case 'KeyObject': { - verifyAcceptableMlDsaKeyUse( - name, getKeyObjectType(keyData) === 'public', usagesSet); - handle = getKeyObjectHandle(keyData); + case 'KeyObjectHandle': + verifyAcceptableKeyUse( + name, + usagesSet, + keyData.getKeyType() === kKeyTypePublic ? + kUsages.public : + kUsages.private); + handle = keyData; break; - } case 'spki': { - verifyAcceptableMlDsaKeyUse(name, true, usagesSet); + verifyAcceptableKeyUse(name, usagesSet, kUsages.public); handle = importDerKey(keyData, true); break; } case 'pkcs8': { - verifyAcceptableMlDsaKeyUse(name, false, usagesSet); + verifyAcceptableKeyUse(name, usagesSet, kUsages.private); const privOnlyLengths = { '__proto__': null, @@ -187,7 +172,10 @@ function mlDsaImportKey( 'JWK "alg" Parameter and algorithm name mismatch', 'DataError'); const isPublic = keyData.priv === undefined; - verifyAcceptableMlDsaKeyUse(name, isPublic, usagesSet); + verifyAcceptableKeyUse( + name, + usagesSet, + isPublic ? kUsages.public : kUsages.private); handle = importJwkKey(isPublic, keyData); if (!isPublic) { @@ -200,7 +188,10 @@ function mlDsaImportKey( case 'raw-public': case 'raw-seed': { const isPublic = format === 'raw-public'; - verifyAcceptableMlDsaKeyUse(name, isPublic, usagesSet); + verifyAcceptableKeyUse( + name, + usagesSet, + isPublic ? kUsages.public : kUsages.private); handle = importRawKey(isPublic, keyData, isPublic ? kKeyFormatRawPublic : kKeyFormatRawSeed, name); break; } diff --git a/lib/internal/crypto/ml_kem.js b/lib/internal/crypto/ml_kem.js index cc09c24048d60b..bc3caf917c78ac 100644 --- a/lib/internal/crypto/ml_kem.js +++ b/lib/internal/crypto/ml_kem.js @@ -13,8 +13,9 @@ const { KEMDecapsulateJob, KEMEncapsulateJob, kKeyFormatDER, - kKeyFormatRawPrivate, kKeyFormatRawPublic, + kKeyFormatRawSeed, + kKeyTypePublic, kWebCryptoKeyFormatPKCS8, kWebCryptoKeyFormatRaw, kWebCryptoKeyFormatSPKI, @@ -26,8 +27,6 @@ const { const { getUsagesMask, - getUsagesUnion, - hasAnyNotIn, jobPromise, } = require('internal/crypto/util'); @@ -39,27 +38,28 @@ const { getCryptoKeyAlgorithm, getCryptoKeyHandle, getCryptoKeyType, - getKeyObjectHandle, - getKeyObjectType, InternalCryptoKey, } = require('internal/crypto/keys'); const { + createKeyUsages, + getKeyPairUsages, importDerKey, importJwkKey, importRawKey, validateJwk, + validateKeyUsages, + validateUsagesNotEmpty, + verifyAcceptableKeyUse, } = require('internal/crypto/webcrypto_util'); +const kUsages = createKeyUsages( + ['encapsulateKey', 'encapsulateBits'], + ['decapsulateKey', 'decapsulateBits']); + function mlKemGenerateKey(algorithm, extractable, usages) { const { name } = algorithm; - - const usageSet = new SafeSet(usages); - if (hasAnyNotIn(usageSet, ['encapsulateKey', 'encapsulateBits', 'decapsulateKey', 'decapsulateBits'])) { - throw lazyDOMException( - `Unsupported key usage for an ${name} key`, - 'SyntaxError'); - } + const usagesSet = validateKeyUsages(usages, kUsages.keygen, name); const nid = { '__proto__': null, @@ -68,25 +68,16 @@ function mlKemGenerateKey(algorithm, extractable, usages) { 'ML-KEM-1024': EVP_PKEY_ML_KEM_1024, }[name]; - const publicUsages = - getUsagesUnion(usageSet, 'encapsulateKey', 'encapsulateBits'); - const privateUsages = - getUsagesUnion(usageSet, 'decapsulateKey', 'decapsulateBits'); - const keyAlgorithm = { name }; - - if (privateUsages.size === 0) { - throw lazyDOMException( - 'Usages cannot be empty when creating a key.', - 'SyntaxError'); - } + const keyUsages = getKeyPairUsages(usagesSet, kUsages); + validateUsagesNotEmpty(keyUsages.private); return jobPromise(() => new NidKeyPairGenJob( kCryptoJobWebCrypto, nid, keyAlgorithm, - getUsagesMask(publicUsages), - getUsagesMask(privateUsages), + getUsagesMask(keyUsages.public), + getUsagesMask(keyUsages.private), extractable)); } @@ -129,15 +120,6 @@ function mlKemExportKey(key, format) { } } -function verifyAcceptableMlKemKeyUse(name, isPublic, usages) { - const checkSet = isPublic ? ['encapsulateKey', 'encapsulateBits'] : ['decapsulateKey', 'decapsulateBits']; - if (hasAnyNotIn(usages, checkSet)) { - throw lazyDOMException( - `Unsupported key usage for a ${name} key`, - 'SyntaxError'); - } -} - function mlKemImportKey( format, keyData, @@ -149,19 +131,22 @@ function mlKemImportKey( let handle; const usagesSet = new SafeSet(usages); switch (format) { - case 'KeyObject': { - verifyAcceptableMlKemKeyUse( - name, getKeyObjectType(keyData) === 'public', usagesSet); - handle = getKeyObjectHandle(keyData); + case 'KeyObjectHandle': + verifyAcceptableKeyUse( + name, + usagesSet, + keyData.getKeyType() === kKeyTypePublic ? + kUsages.public : + kUsages.private); + handle = keyData; break; - } case 'spki': { - verifyAcceptableMlKemKeyUse(name, true, usagesSet); + verifyAcceptableKeyUse(name, usagesSet, kUsages.public); handle = importDerKey(keyData, true); break; } case 'pkcs8': { - verifyAcceptableMlKemKeyUse(name, false, usagesSet); + verifyAcceptableKeyUse(name, usagesSet, kUsages.private); const privOnlyLengths = { '__proto__': null, @@ -181,8 +166,11 @@ function mlKemImportKey( case 'raw-public': case 'raw-seed': { const isPublic = format === 'raw-public'; - verifyAcceptableMlKemKeyUse(name, isPublic, usagesSet); - handle = importRawKey(isPublic, keyData, isPublic ? kKeyFormatRawPublic : kKeyFormatRawPrivate, name); + verifyAcceptableKeyUse( + name, + usagesSet, + isPublic ? kUsages.public : kUsages.private); + handle = importRawKey(isPublic, keyData, isPublic ? kKeyFormatRawPublic : kKeyFormatRawSeed, name); break; } case 'jwk': { @@ -193,7 +181,10 @@ function mlKemImportKey( 'JWK "alg" Parameter and algorithm name mismatch', 'DataError'); const isPublic = keyData.priv === undefined; - verifyAcceptableMlKemKeyUse(name, isPublic, usagesSet); + verifyAcceptableKeyUse( + name, + usagesSet, + isPublic ? kUsages.public : kUsages.private); handle = importJwkKey(isPublic, keyData); break; } diff --git a/lib/internal/crypto/random.js b/lib/internal/crypto/random.js index b4bfcb6c0a05de..339fbb7066528c 100644 --- a/lib/internal/crypto/random.js +++ b/lib/internal/crypto/random.js @@ -603,12 +603,14 @@ function checkPrime(candidate, options = kEmptyObject, callback) { } validateFunction(callback, 'callback'); validateObject(options, 'options'); - const { + let { checks = 0, } = options; // The checks option is unsigned but must fit into a signed C int for OpenSSL. validateInt32(checks, 'options.checks', 0); + // Coerce -0 to +0. + checks += 0; const job = new CheckPrimeJob(kCryptoJobAsync, candidate, checks); job.ondone = callback; @@ -632,12 +634,14 @@ function checkPrimeSync(candidate, options = kEmptyObject) { ); } validateObject(options, 'options'); - const { + let { checks = 0, } = options; // The checks option is unsigned but must fit into a signed C int for OpenSSL. validateInt32(checks, 'options.checks', 0); + // Coerce -0 to +0. + checks += 0; const job = new CheckPrimeJob(kCryptoJobSync, candidate, checks); const { 0: err, 1: result } = job.run(); diff --git a/lib/internal/crypto/rsa.js b/lib/internal/crypto/rsa.js index a09dd7b9f0fda9..a2757384f4f490 100644 --- a/lib/internal/crypto/rsa.js +++ b/lib/internal/crypto/rsa.js @@ -12,6 +12,7 @@ const { SignJob, kCryptoJobWebCrypto, kKeyFormatDER, + kKeyTypePublic, kSignJobModeSign, kSignJobModeVerify, kKeyVariantRSA_OAEP, @@ -31,8 +32,6 @@ const { bigIntArrayToUnsignedInt, getDigestSizeInBytes, getUsagesMask, - getUsagesUnion, - hasAnyNotIn, jobPromise, normalizeHashName, validateMaxBufferLength, @@ -47,37 +46,31 @@ const { getCryptoKeyAlgorithm, getCryptoKeyHandle, getCryptoKeyType, - getKeyObjectHandle, - getKeyObjectType, } = require('internal/crypto/keys'); const { + createKeyUsages, + getKeyPairUsages, importDerKey, importJwkKey, validateJwk, + validateKeyUsages, + validateUsagesNotEmpty, + verifyAcceptableKeyUse, } = require('internal/crypto/webcrypto_util'); -function verifyAcceptableRsaKeyUse(name, isPublic, usages) { - let checkSet; - switch (name) { - case 'RSA-OAEP': - checkSet = isPublic ? ['encrypt', 'wrapKey'] : ['decrypt', 'unwrapKey']; - break; - case 'RSA-PSS': - // Fall through - case 'RSASSA-PKCS1-v1_5': - checkSet = isPublic ? ['verify'] : ['sign']; - break; - default: - throw lazyDOMException( - 'The algorithm is not supported', 'NotSupportedError'); - } - if (hasAnyNotIn(usages, checkSet)) { - throw lazyDOMException( - `Unsupported key usage for an ${name} key`, - 'SyntaxError'); - } -} +const kOaepUsages = createKeyUsages( + ['encrypt', 'wrapKey'], + ['decrypt', 'unwrapKey']); + +const kSignVerifyUsages = createKeyUsages(['verify'], ['sign']); + +const kUsages = { + '__proto__': null, + 'RSA-OAEP': kOaepUsages, + 'RSA-PSS': kSignVerifyUsages, + 'RSASSA-PKCS1-v1_5': kSignVerifyUsages, +}; function validateRsaOaepAlgorithm(algorithm) { if (algorithm.label !== undefined) { @@ -123,24 +116,8 @@ function rsaKeyGenerate( hash, } = algorithm; - const usageSet = new SafeSet(usages); - - switch (name) { - case 'RSA-OAEP': - if (hasAnyNotIn(usageSet, - ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'])) { - throw lazyDOMException( - 'Unsupported key usage for a RSA key', - 'SyntaxError'); - } - break; - default: - if (hasAnyNotIn(usageSet, ['sign', 'verify'])) { - throw lazyDOMException( - 'Unsupported key usage for a RSA key', - 'SyntaxError'); - } - } + const allowedUsages = kUsages[name]; + const usagesSet = validateKeyUsages(usages, allowedUsages.keygen, name); const keyAlgorithm = { name, @@ -155,26 +132,8 @@ function rsaKeyGenerate( 'OperationError'); } - let publicUsages; - let privateUsages; - switch (name) { - case 'RSA-OAEP': { - publicUsages = getUsagesUnion(usageSet, 'encrypt', 'wrapKey'); - privateUsages = getUsagesUnion(usageSet, 'decrypt', 'unwrapKey'); - break; - } - default: { - publicUsages = getUsagesUnion(usageSet, 'verify'); - privateUsages = getUsagesUnion(usageSet, 'sign'); - break; - } - } - - if (privateUsages.size === 0) { - throw lazyDOMException( - 'Usages cannot be empty when creating a key.', - 'SyntaxError'); - } + const keyUsages = getKeyPairUsages(usagesSet, allowedUsages); + validateUsagesNotEmpty(keyUsages.private); return jobPromise(() => new RsaKeyPairGenJob( kCryptoJobWebCrypto, @@ -182,8 +141,8 @@ function rsaKeyGenerate( modulusLength, publicExponentConverted, keyAlgorithm, - getUsagesMask(publicUsages), - getUsagesMask(privateUsages), + getUsagesMask(keyUsages.public), + getUsagesMask(keyUsages.private), extractable)); } @@ -214,22 +173,28 @@ function rsaImportKey( algorithm, extractable, usages) { + const allowedUsages = kUsages[algorithm.name]; const usagesSet = new SafeSet(usages); let handle; switch (format) { - case 'KeyObject': { - verifyAcceptableRsaKeyUse( - algorithm.name, getKeyObjectType(keyData) === 'public', usagesSet); - handle = getKeyObjectHandle(keyData); + case 'KeyObjectHandle': + verifyAcceptableKeyUse( + algorithm.name, + usagesSet, + keyData.getKeyType() === kKeyTypePublic ? + allowedUsages.public : + allowedUsages.private); + handle = keyData; break; - } case 'spki': { - verifyAcceptableRsaKeyUse(algorithm.name, true, usagesSet); + verifyAcceptableKeyUse( + algorithm.name, usagesSet, allowedUsages.public); handle = importDerKey(keyData, true); break; } case 'pkcs8': { - verifyAcceptableRsaKeyUse(algorithm.name, false, usagesSet); + verifyAcceptableKeyUse( + algorithm.name, usagesSet, allowedUsages.private); handle = importDerKey(keyData, false); break; } @@ -251,7 +216,10 @@ function rsaImportKey( } const isPublic = keyData.d === undefined; - verifyAcceptableRsaKeyUse(algorithm.name, isPublic, usagesSet); + verifyAcceptableKeyUse( + algorithm.name, + usagesSet, + isPublic ? allowedUsages.public : allowedUsages.private); handle = importJwkKey(isPublic, keyData); break; } diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index ddb053967389ed..ca02c6907dad73 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -9,6 +9,7 @@ const { DataViewPrototypeGetBuffer, DataViewPrototypeGetByteLength, DataViewPrototypeGetByteOffset, + MathFloor, Number, ObjectDefineProperty, ObjectEntries, @@ -18,7 +19,6 @@ const { PromiseReject, PromiseWithResolvers, SafeMap, - SafeSet, StringPrototypeToUpperCase, Symbol, TypedArrayPrototypeGetBuffer, @@ -550,6 +550,46 @@ function validateMaxBufferLength(data, name, max = kMaxBufferLength) { } } +/** + * Converts a bit length to the number of bytes needed to contain it. + * Non-byte lengths are rounded up to the next byte. + * @param {number} length + * @returns {number} + */ +function numBitsToBytes(length) { + return MathFloor(length / 8) + MathFloor((7 + (length % 8)) / 8); +} + +/** + * Copies `bytes` up to the byte length needed for `length` bits, then clears + * unused least-significant bits in the final byte. + * @param {number} length + * @param {ArrayBuffer|ArrayBufferView} bytes + * @returns {Uint8Array} + */ +function truncateToBitLength(length, bytes) { + const lengthBytes = numBitsToBytes(length); + const isView = ArrayBufferIsView(bytes); + const byteView = isView ? + new Uint8Array( + getDataViewOrTypedArrayBuffer(bytes), + getDataViewOrTypedArrayByteOffset(bytes), + getDataViewOrTypedArrayByteLength(bytes), + ) : + new Uint8Array(bytes, 0, ArrayBufferPrototypeGetByteLength(bytes)); + const result = TypedArrayPrototypeSlice( + byteView, + 0, + lengthBytes, + ); + + const remainder = length % 8; + if (remainder !== 0) + result[lengthBytes - 1] &= (0xff << (8 - remainder)) & 0xff; + + return result; +} + let webidl; // Keep this as a regular object. The WebIDL converters read and spread these @@ -609,12 +649,19 @@ function normalizeAlgorithm(algorithm, op) { // 3. if (idlType === 'BufferSource' && idlValue) { const isView = ArrayBufferIsView(idlValue); - normalizedAlgorithm[member] = TypedArrayPrototypeSlice( + const idlValueBytes = isView ? new Uint8Array( - isView ? getDataViewOrTypedArrayBuffer(idlValue) : idlValue, - isView ? getDataViewOrTypedArrayByteOffset(idlValue) : 0, - isView ? getDataViewOrTypedArrayByteLength(idlValue) : ArrayBufferPrototypeGetByteLength(idlValue), - ), + getDataViewOrTypedArrayBuffer(idlValue), + getDataViewOrTypedArrayByteOffset(idlValue), + getDataViewOrTypedArrayByteLength(idlValue), + ) : + new Uint8Array( + idlValue, + 0, + ArrayBufferPrototypeGetByteLength(idlValue), + ); + normalizedAlgorithm[member] = TypedArrayPrototypeSlice( + idlValueBytes, ); } else if (idlType === 'HashAlgorithmIdentifier') { normalizedAlgorithm[member] = normalizeAlgorithm(idlValue, 'digest'); @@ -830,21 +877,6 @@ function getStringOption(options, key) { return value; } -/** - * Returns the requested usages that are present in `usageSet`. - * @param {SafeSet} usageSet - * @param {...string} usages - * @returns {SafeSet} - */ -function getUsagesUnion(usageSet, ...usages) { - const newset = new SafeSet(); - for (let n = 0; n < usages.length; n++) { - if (usageSet.has(usages[n])) - newset.add(usages[n]); - } - return newset; -} - // Must be at most 31 entries. const kCanonicalUsageOrder = [ 'encrypt', 'decrypt', @@ -1019,12 +1051,13 @@ module.exports = { cleanupWebCryptoResult, prepareWebCryptoResult, validateMaxBufferLength, + numBitsToBytes, + truncateToBitLength, bigIntArrayToUnsignedBigInt, bigIntArrayToUnsignedInt, getBlockSize, getDigestSizeInBytes, getStringOption, - getUsagesUnion, getUsagesMask, getUsagesFromMask, hasUsage, diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index 663ca798248ec3..3c1dd9e27d78a8 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -24,6 +24,7 @@ const { } = primordials; const { + CShakeJob, kWebCryptoKeyFormatRaw, kWebCryptoKeyFormatPKCS8, kWebCryptoKeyFormatSPKI, @@ -44,7 +45,6 @@ const { } = require('internal/errors'); const { - createPublicKey, CryptoKey, getCryptoKeyAlgorithm, getCryptoKeyExtractable, @@ -54,7 +54,7 @@ const { getCryptoKeyUsagesMask, hasCryptoKeyUsage, importGenericSecretKey, - PrivateKeyObject, + toPublicCryptoKey, } = require('internal/crypto/keys'); const { @@ -67,6 +67,7 @@ const { jobPromiseThen, normalizeAlgorithm, normalizeHashName, + numBitsToBytes, prepareWebCryptoResult, validateMaxBufferLength, } = require('internal/crypto/util'); @@ -110,24 +111,42 @@ function callSubtleCryptoMethod(fn, receiver, args) { } } -function digest(algorithm, data) { - return callSubtleCryptoMethod(digestImpl, this, arguments); -} +const kArgumentContexts = [ + '1st argument', + '2nd argument', + '3rd argument', + '4th argument', + '5th argument', + '6th argument', + '7th argument', +]; -function digestImpl(algorithm, data) { - if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); +function prepareSubtleMethod(receiver, method, argLength, required) { + if (receiver !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); webidl ??= require('internal/crypto/webidl'); - const prefix = "Failed to execute 'digest' on 'SubtleCrypto'"; - webidl.requiredArguments(arguments.length, 2, { prefix }); - algorithm = webidl.converters.AlgorithmIdentifier(algorithm, { - prefix, - context: '1st argument', - }); - data = webidl.converters.BufferSource(data, { + const prefix = `Failed to execute '${method}' on 'SubtleCrypto'`; + webidl.requiredArguments(argLength, required, { prefix }); + return prefix; +} + +function convertSubtleArgument(prefix, converter, value, index) { + return webidl.converters[converter](value, { prefix, - context: '2nd argument', + context: kArgumentContexts[index], }); +} + +function digest(algorithm, data) { + return callSubtleCryptoMethod(digestImpl, this, arguments); +} + +function digestImpl(algorithm, data) { + const prefix = prepareSubtleMethod(this, 'digest', arguments.length, 2); + let i = 0; + algorithm = convertSubtleArgument( + prefix, 'AlgorithmIdentifier', algorithm, i++); + data = convertSubtleArgument(prefix, 'BufferSource', data, i++); const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'digest'); @@ -150,23 +169,13 @@ function generateKeyImpl( algorithm, extractable, keyUsages) { - if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); - - webidl ??= require('internal/crypto/webidl'); - const prefix = "Failed to execute 'generateKey' on 'SubtleCrypto'"; - webidl.requiredArguments(arguments.length, 3, { prefix }); - algorithm = webidl.converters.AlgorithmIdentifier(algorithm, { - prefix, - context: '1st argument', - }); - extractable = webidl.converters.boolean(extractable, { - prefix, - context: '2nd argument', - }); - const usages = webidl.converters['sequence'](keyUsages, { - prefix, - context: '3rd argument', - }); + const prefix = prepareSubtleMethod(this, 'generateKey', arguments.length, 3); + let i = 0; + algorithm = convertSubtleArgument( + prefix, 'AlgorithmIdentifier', algorithm, i++); + extractable = convertSubtleArgument(prefix, 'boolean', extractable, i++); + const usages = convertSubtleArgument( + prefix, 'sequence', keyUsages, i++); const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'generateKey'); switch (normalizedAlgorithm.name) { @@ -227,8 +236,12 @@ function generateKeyImpl( case 'KMAC256': return require('internal/crypto/mac') .kmacGenerateKey(normalizedAlgorithm, extractable, usages); - default: - throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); + /* c8 ignore start */ + default: { + const assert = require('internal/assert'); + assert.fail('Unreachable code'); + } + /* c8 ignore stop */ } } @@ -237,24 +250,13 @@ function deriveBits(algorithm, baseKey, length = null) { } function deriveBitsImpl(algorithm, baseKey, length = null) { - if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); - - webidl ??= require('internal/crypto/webidl'); - const prefix = "Failed to execute 'deriveBits' on 'SubtleCrypto'"; - webidl.requiredArguments(arguments.length, 2, { prefix }); - algorithm = webidl.converters.AlgorithmIdentifier(algorithm, { - prefix, - context: '1st argument', - }); - baseKey = webidl.converters.CryptoKey(baseKey, { - prefix, - context: '2nd argument', - }); + const prefix = prepareSubtleMethod(this, 'deriveBits', arguments.length, 2); + let i = 0; + algorithm = convertSubtleArgument( + prefix, 'AlgorithmIdentifier', algorithm, i++); + baseKey = convertSubtleArgument(prefix, 'CryptoKey', baseKey, i++); if (length !== null) { - length = webidl.converters['unsigned long'](length, { - prefix, - context: '3rd argument', - }); + length = convertSubtleArgument(prefix, 'unsigned long', length, i++); } const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'deriveBits'); @@ -286,8 +288,13 @@ function deriveBitsImpl(algorithm, baseKey, length = null) { case 'Argon2id': return require('internal/crypto/argon2') .argon2DeriveBits(normalizedAlgorithm, baseKey, length); + /* c8 ignore start */ + default: { + const assert = require('internal/assert'); + assert.fail('Unreachable code'); + } + /* c8 ignore stop */ } - throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } function getKeyLength({ name, length, hash }) { @@ -344,31 +351,16 @@ function deriveKeyImpl( derivedKeyType, extractable, keyUsages) { - if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); - - webidl ??= require('internal/crypto/webidl'); - const prefix = "Failed to execute 'deriveKey' on 'SubtleCrypto'"; - webidl.requiredArguments(arguments.length, 5, { prefix }); - algorithm = webidl.converters.AlgorithmIdentifier(algorithm, { - prefix, - context: '1st argument', - }); - baseKey = webidl.converters.CryptoKey(baseKey, { - prefix, - context: '2nd argument', - }); - derivedKeyType = webidl.converters.AlgorithmIdentifier(derivedKeyType, { - prefix, - context: '3rd argument', - }); - extractable = webidl.converters.boolean(extractable, { - prefix, - context: '4th argument', - }); - const usages = webidl.converters['sequence'](keyUsages, { - prefix, - context: '5th argument', - }); + const prefix = prepareSubtleMethod(this, 'deriveKey', arguments.length, 5); + let i = 0; + algorithm = convertSubtleArgument( + prefix, 'AlgorithmIdentifier', algorithm, i++); + baseKey = convertSubtleArgument(prefix, 'CryptoKey', baseKey, i++); + derivedKeyType = convertSubtleArgument( + prefix, 'AlgorithmIdentifier', derivedKeyType, i++); + extractable = convertSubtleArgument(prefix, 'boolean', extractable, i++); + const usages = convertSubtleArgument( + prefix, 'sequence', keyUsages, i++); const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'deriveBits'); const normalizedDerivedKeyAlgorithmImport = @@ -410,8 +402,12 @@ function deriveKeyImpl( secret = require('internal/crypto/argon2') .argon2DeriveBits(normalizedAlgorithm, baseKey, length); break; - default: - throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); + /* c8 ignore start */ + default: { + const assert = require('internal/assert'); + assert.fail('Unreachable code'); + } + /* c8 ignore stop */ } return jobPromiseThen(secret, (secret) => FunctionPrototypeCall( @@ -768,19 +764,10 @@ function exportKey(format, key) { } function exportKeyImpl(format, key) { - if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); - - webidl ??= require('internal/crypto/webidl'); - const prefix = "Failed to execute 'exportKey' on 'SubtleCrypto'"; - webidl.requiredArguments(arguments.length, 2, { prefix }); - format = webidl.converters.KeyFormat(format, { - prefix, - context: '1st argument', - }); - key = webidl.converters.CryptoKey(key, { - prefix, - context: '2nd argument', - }); + const prefix = prepareSubtleMethod(this, 'exportKey', arguments.length, 2); + let i = 0; + format = convertSubtleArgument(prefix, 'KeyFormat', format, i++); + key = convertSubtleArgument(prefix, 'CryptoKey', key, i++); return exportKeySync(format, key); } @@ -1010,32 +997,16 @@ function importKeyImpl( algorithm, extractable, keyUsages) { - if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); - - webidl ??= require('internal/crypto/webidl'); - const prefix = "Failed to execute 'importKey' on 'SubtleCrypto'"; - webidl.requiredArguments(arguments.length, 5, { prefix }); - format = webidl.converters.KeyFormat(format, { - prefix, - context: '1st argument', - }); + const prefix = prepareSubtleMethod(this, 'importKey', arguments.length, 5); + let i = 0; + format = convertSubtleArgument(prefix, 'KeyFormat', format, i++); const type = format === 'jwk' ? 'JsonWebKey' : 'BufferSource'; - keyData = webidl.converters[type](keyData, { - prefix, - context: '2nd argument', - }); - algorithm = webidl.converters.AlgorithmIdentifier(algorithm, { - prefix, - context: '3rd argument', - }); - extractable = webidl.converters.boolean(extractable, { - prefix, - context: '4th argument', - }); - const usages = webidl.converters['sequence'](keyUsages, { - prefix, - context: '5th argument', - }); + keyData = convertSubtleArgument(prefix, type, keyData, i++); + algorithm = convertSubtleArgument( + prefix, 'AlgorithmIdentifier', algorithm, i++); + extractable = convertSubtleArgument(prefix, 'boolean', extractable, i++); + const usages = convertSubtleArgument( + prefix, 'sequence', keyUsages, i++); const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'importKey'); @@ -1057,29 +1028,14 @@ function wrapKey(format, key, wrappingKey, wrapAlgorithm) { } function wrapKeyImpl(format, key, wrappingKey, wrapAlgorithm) { - if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); - - webidl ??= require('internal/crypto/webidl'); - const prefix = "Failed to execute 'wrapKey' on 'SubtleCrypto'"; - webidl.requiredArguments(arguments.length, 4, { prefix }); - format = webidl.converters.KeyFormat(format, { - prefix, - context: '1st argument', - }); - key = webidl.converters.CryptoKey(key, { - prefix, - context: '2nd argument', - }); - wrappingKey = webidl.converters.CryptoKey(wrappingKey, { - prefix, - context: '3rd argument', - }); - const algorithm = webidl.converters.AlgorithmIdentifier(wrapAlgorithm, { - prefix, - context: '4th argument', - }); + const prefix = prepareSubtleMethod(this, 'wrapKey', arguments.length, 4); + let i = 0; + format = convertSubtleArgument(prefix, 'KeyFormat', format, i++); + key = convertSubtleArgument(prefix, 'CryptoKey', key, i++); + wrappingKey = convertSubtleArgument(prefix, 'CryptoKey', wrappingKey, i++); + const algorithm = convertSubtleArgument( + prefix, 'AlgorithmIdentifier', wrapAlgorithm, i++); let normalizedAlgorithm; - try { normalizedAlgorithm = normalizeAlgorithm(algorithm, 'wrapKey'); } catch { @@ -1119,8 +1075,7 @@ function wrapKeyImpl(format, key, wrappingKey, wrapAlgorithm) { kWebCryptoCipherEncrypt, normalizedAlgorithm, wrappingKey, - bytes, - 'wrapKey'); + bytes); } // subtle.unwrapKey() is essentially a subtle.decrypt() followed @@ -1144,42 +1099,19 @@ function unwrapKeyImpl( unwrappedKeyAlgorithm, extractable, keyUsages) { - if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); - - webidl ??= require('internal/crypto/webidl'); - const prefix = "Failed to execute 'unwrapKey' on 'SubtleCrypto'"; - webidl.requiredArguments(arguments.length, 7, { prefix }); - format = webidl.converters.KeyFormat(format, { - prefix, - context: '1st argument', - }); - wrappedKey = webidl.converters.BufferSource(wrappedKey, { - prefix, - context: '2nd argument', - }); - unwrappingKey = webidl.converters.CryptoKey(unwrappingKey, { - prefix, - context: '3rd argument', - }); - const algorithm = webidl.converters.AlgorithmIdentifier(unwrapAlgorithm, { - prefix, - context: '4th argument', - }); - unwrappedKeyAlgorithm = webidl.converters.AlgorithmIdentifier( - unwrappedKeyAlgorithm, - { - prefix, - context: '5th argument', - }, - ); - extractable = webidl.converters.boolean(extractable, { - prefix, - context: '6th argument', - }); - const usages = webidl.converters['sequence'](keyUsages, { - prefix, - context: '7th argument', - }); + const prefix = prepareSubtleMethod(this, 'unwrapKey', arguments.length, 7); + let i = 0; + format = convertSubtleArgument(prefix, 'KeyFormat', format, i++); + wrappedKey = convertSubtleArgument(prefix, 'BufferSource', wrappedKey, i++); + unwrappingKey = convertSubtleArgument( + prefix, 'CryptoKey', unwrappingKey, i++); + const algorithm = convertSubtleArgument( + prefix, 'AlgorithmIdentifier', unwrapAlgorithm, i++); + unwrappedKeyAlgorithm = convertSubtleArgument( + prefix, 'AlgorithmIdentifier', unwrappedKeyAlgorithm, i++); + extractable = convertSubtleArgument(prefix, 'boolean', extractable, i++); + const usages = convertSubtleArgument( + prefix, 'sequence', keyUsages, i++); let normalizedAlgorithm; try { @@ -1202,8 +1134,7 @@ function unwrapKeyImpl( kWebCryptoCipherDecrypt, normalizedAlgorithm, unwrappingKey, - wrappedKey, - 'unwrapKey'); + wrappedKey); return jobPromiseThen(bytes, (bytes) => { let keyData = bytes; @@ -1264,8 +1195,13 @@ function signVerify(algorithm, key, data, signature) { case 'KMAC256': return require('internal/crypto/mac') .kmacSignVerify(key, data, normalizedAlgorithm, signature); + /* c8 ignore start */ + default: { + const assert = require('internal/assert'); + assert.fail('Unreachable code'); + } + /* c8 ignore stop */ } - throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } function sign(algorithm, key, data) { @@ -1273,23 +1209,12 @@ function sign(algorithm, key, data) { } function signImpl(algorithm, key, data) { - if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); - - webidl ??= require('internal/crypto/webidl'); - const prefix = "Failed to execute 'sign' on 'SubtleCrypto'"; - webidl.requiredArguments(arguments.length, 3, { prefix }); - algorithm = webidl.converters.AlgorithmIdentifier(algorithm, { - prefix, - context: '1st argument', - }); - key = webidl.converters.CryptoKey(key, { - prefix, - context: '2nd argument', - }); - data = webidl.converters.BufferSource(data, { - prefix, - context: '3rd argument', - }); + const prefix = prepareSubtleMethod(this, 'sign', arguments.length, 3); + let i = 0; + algorithm = convertSubtleArgument( + prefix, 'AlgorithmIdentifier', algorithm, i++); + key = convertSubtleArgument(prefix, 'CryptoKey', key, i++); + data = convertSubtleArgument(prefix, 'BufferSource', data, i++); return signVerify(algorithm, key, data); } @@ -1299,32 +1224,18 @@ function verify(algorithm, key, signature, data) { } function verifyImpl(algorithm, key, signature, data) { - if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); - - webidl ??= require('internal/crypto/webidl'); - const prefix = "Failed to execute 'verify' on 'SubtleCrypto'"; - webidl.requiredArguments(arguments.length, 4, { prefix }); - algorithm = webidl.converters.AlgorithmIdentifier(algorithm, { - prefix, - context: '1st argument', - }); - key = webidl.converters.CryptoKey(key, { - prefix, - context: '2nd argument', - }); - signature = webidl.converters.BufferSource(signature, { - prefix, - context: '3rd argument', - }); - data = webidl.converters.BufferSource(data, { - prefix, - context: '4th argument', - }); + const prefix = prepareSubtleMethod(this, 'verify', arguments.length, 4); + let i = 0; + algorithm = convertSubtleArgument( + prefix, 'AlgorithmIdentifier', algorithm, i++); + key = convertSubtleArgument(prefix, 'CryptoKey', key, i++); + signature = convertSubtleArgument(prefix, 'BufferSource', signature, i++); + data = convertSubtleArgument(prefix, 'BufferSource', data, i++); return signVerify(algorithm, key, data, signature); } -function cipherOrWrap(mode, normalizedAlgorithm, key, data, operation) { +function cipherOrWrap(mode, normalizedAlgorithm, key, data) { // While WebCrypto allows for larger input buffer sizes, we limit // those to sizes that can fit within uint32_t because of limitations // in the OpenSSL API. @@ -1341,18 +1252,20 @@ function cipherOrWrap(mode, normalizedAlgorithm, key, data, operation) { case 'AES-GCM': // Fall through case 'AES-OCB': + // Fall through + case 'AES-KW': return require('internal/crypto/aes') .aesCipher(mode, key, data, normalizedAlgorithm); case 'ChaCha20-Poly1305': return require('internal/crypto/chacha20_poly1305') .c20pCipher(mode, key, data, normalizedAlgorithm); - case 'AES-KW': - if (operation === 'wrapKey' || operation === 'unwrapKey') { - return require('internal/crypto/aes') - .aesCipher(mode, key, data, normalizedAlgorithm); - } + /* c8 ignore start */ + default: { + const assert = require('internal/assert'); + assert.fail('Unreachable code'); + } + /* c8 ignore stop */ } - throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } function encrypt(algorithm, key, data) { @@ -1360,23 +1273,12 @@ function encrypt(algorithm, key, data) { } function encryptImpl(algorithm, key, data) { - if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); - - webidl ??= require('internal/crypto/webidl'); - const prefix = "Failed to execute 'encrypt' on 'SubtleCrypto'"; - webidl.requiredArguments(arguments.length, 3, { prefix }); - algorithm = webidl.converters.AlgorithmIdentifier(algorithm, { - prefix, - context: '1st argument', - }); - key = webidl.converters.CryptoKey(key, { - prefix, - context: '2nd argument', - }); - data = webidl.converters.BufferSource(data, { - prefix, - context: '3rd argument', - }); + const prefix = prepareSubtleMethod(this, 'encrypt', arguments.length, 3); + let i = 0; + algorithm = convertSubtleArgument( + prefix, 'AlgorithmIdentifier', algorithm, i++); + key = convertSubtleArgument(prefix, 'CryptoKey', key, i++); + data = convertSubtleArgument(prefix, 'BufferSource', data, i++); const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'encrypt'); @@ -1392,7 +1294,6 @@ function encryptImpl(algorithm, key, data) { normalizedAlgorithm, key, data, - 'encrypt', ); } @@ -1401,23 +1302,12 @@ function decrypt(algorithm, key, data) { } function decryptImpl(algorithm, key, data) { - if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); - - webidl ??= require('internal/crypto/webidl'); - const prefix = "Failed to execute 'decrypt' on 'SubtleCrypto'"; - webidl.requiredArguments(arguments.length, 3, { prefix }); - algorithm = webidl.converters.AlgorithmIdentifier(algorithm, { - prefix, - context: '1st argument', - }); - key = webidl.converters.CryptoKey(key, { - prefix, - context: '2nd argument', - }); - data = webidl.converters.BufferSource(data, { - prefix, - context: '3rd argument', - }); + const prefix = prepareSubtleMethod(this, 'decrypt', arguments.length, 3); + let i = 0; + algorithm = convertSubtleArgument( + prefix, 'AlgorithmIdentifier', algorithm, i++); + key = convertSubtleArgument(prefix, 'CryptoKey', key, i++); + data = convertSubtleArgument(prefix, 'BufferSource', data, i++); const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'decrypt'); @@ -1433,7 +1323,6 @@ function decryptImpl(algorithm, key, data) { normalizedAlgorithm, key, data, - 'decrypt', ); } @@ -1444,29 +1333,18 @@ function getPublicKey(key, keyUsages) { function getPublicKeyImpl(key, keyUsages) { emitExperimentalWarning('The getPublicKey Web Crypto API method'); - if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); - - webidl ??= require('internal/crypto/webidl'); - const prefix = "Failed to execute 'getPublicKey' on 'SubtleCrypto'"; - webidl.requiredArguments(arguments.length, 2, { prefix }); - key = webidl.converters.CryptoKey(key, { - prefix, - context: '1st argument', - }); - const usages = webidl.converters['sequence'](keyUsages, { - prefix, - context: '2nd argument', - }); + const prefix = prepareSubtleMethod(this, 'getPublicKey', arguments.length, 2); + let i = 0; + key = convertSubtleArgument(prefix, 'CryptoKey', key, i++); + const usages = convertSubtleArgument( + prefix, 'sequence', keyUsages, i++); const type = getCryptoKeyType(key); if (type !== 'private') throw lazyDOMException('key must be a private key', type === 'secret' ? 'NotSupportedError' : 'InvalidAccessError'); - // TODO(panva): this is by no means a hot path, but let's still follow up to get - // rid of this awkwardness - const keyObject = createPublicKey(new PrivateKeyObject(getCryptoKeyHandle(key))); - return keyObject.toCryptoKey(getCryptoKeyAlgorithm(key), true, usages); + return toPublicCryptoKey(key, usages); } function encapsulateBits(encapsulationAlgorithm, encapsulationKey) { @@ -1475,22 +1353,13 @@ function encapsulateBits(encapsulationAlgorithm, encapsulationKey) { function encapsulateBitsImpl(encapsulationAlgorithm, encapsulationKey) { emitExperimentalWarning('The encapsulateBits Web Crypto API method'); - if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); - - webidl ??= require('internal/crypto/webidl'); - const prefix = "Failed to execute 'encapsulateBits' on 'SubtleCrypto'"; - webidl.requiredArguments(arguments.length, 2, { prefix }); - encapsulationAlgorithm = webidl.converters.AlgorithmIdentifier( - encapsulationAlgorithm, - { - prefix, - context: '1st argument', - }, - ); - encapsulationKey = webidl.converters.CryptoKey(encapsulationKey, { - prefix, - context: '2nd argument', - }); + const prefix = prepareSubtleMethod( + this, 'encapsulateBits', arguments.length, 2); + let i = 0; + encapsulationAlgorithm = convertSubtleArgument( + prefix, 'AlgorithmIdentifier', encapsulationAlgorithm, i++); + encapsulationKey = convertSubtleArgument( + prefix, 'CryptoKey', encapsulationKey, i++); const normalizedEncapsulationAlgorithm = normalizeAlgorithm(encapsulationAlgorithm, 'encapsulate'); @@ -1514,9 +1383,13 @@ function encapsulateBitsImpl(encapsulationAlgorithm, encapsulationKey) { case 'ML-KEM-1024': return require('internal/crypto/ml_kem') .mlKemEncapsulate(encapsulationKey); + /* c8 ignore start */ + default: { + const assert = require('internal/assert'); + assert.fail('Unreachable code'); + } + /* c8 ignore stop */ } - - throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } function encapsulateKey( @@ -1535,37 +1408,18 @@ function encapsulateKeyImpl( extractable, keyUsages) { emitExperimentalWarning('The encapsulateKey Web Crypto API method'); - if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); - - webidl ??= require('internal/crypto/webidl'); - const prefix = "Failed to execute 'encapsulateKey' on 'SubtleCrypto'"; - webidl.requiredArguments(arguments.length, 5, { prefix }); - encapsulationAlgorithm = webidl.converters.AlgorithmIdentifier( - encapsulationAlgorithm, - { - prefix, - context: '1st argument', - }, - ); - encapsulationKey = webidl.converters.CryptoKey(encapsulationKey, { - prefix, - context: '2nd argument', - }); - sharedKeyAlgorithm = webidl.converters.AlgorithmIdentifier( - sharedKeyAlgorithm, - { - prefix, - context: '3rd argument', - }, - ); - extractable = webidl.converters.boolean(extractable, { - prefix, - context: '4th argument', - }); - const usages = webidl.converters['sequence'](keyUsages, { - prefix, - context: '5th argument', - }); + const prefix = prepareSubtleMethod( + this, 'encapsulateKey', arguments.length, 5); + let i = 0; + encapsulationAlgorithm = convertSubtleArgument( + prefix, 'AlgorithmIdentifier', encapsulationAlgorithm, i++); + encapsulationKey = convertSubtleArgument( + prefix, 'CryptoKey', encapsulationKey, i++); + sharedKeyAlgorithm = convertSubtleArgument( + prefix, 'AlgorithmIdentifier', sharedKeyAlgorithm, i++); + extractable = convertSubtleArgument(prefix, 'boolean', extractable, i++); + const usages = convertSubtleArgument( + prefix, 'sequence', keyUsages, i++); const normalizedEncapsulationAlgorithm = normalizeAlgorithm(encapsulationAlgorithm, 'encapsulate'); @@ -1593,8 +1447,12 @@ function encapsulateKeyImpl( encapsulatedBits = require('internal/crypto/ml_kem') .mlKemEncapsulate(encapsulationKey); break; - default: - throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); + /* c8 ignore start */ + default: { + const assert = require('internal/assert'); + assert.fail('Unreachable code'); + } + /* c8 ignore stop */ } return jobPromiseThen(encapsulatedBits, (encapsulatedBits) => { @@ -1621,26 +1479,14 @@ function decapsulateBits(decapsulationAlgorithm, decapsulationKey, ciphertext) { function decapsulateBitsImpl(decapsulationAlgorithm, decapsulationKey, ciphertext) { emitExperimentalWarning('The decapsulateBits Web Crypto API method'); - if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); - - webidl ??= require('internal/crypto/webidl'); - const prefix = "Failed to execute 'decapsulateBits' on 'SubtleCrypto'"; - webidl.requiredArguments(arguments.length, 3, { prefix }); - decapsulationAlgorithm = webidl.converters.AlgorithmIdentifier( - decapsulationAlgorithm, - { - prefix, - context: '1st argument', - }, - ); - decapsulationKey = webidl.converters.CryptoKey(decapsulationKey, { - prefix, - context: '2nd argument', - }); - ciphertext = webidl.converters.BufferSource(ciphertext, { - prefix, - context: '3rd argument', - }); + const prefix = prepareSubtleMethod( + this, 'decapsulateBits', arguments.length, 3); + let i = 0; + decapsulationAlgorithm = convertSubtleArgument( + prefix, 'AlgorithmIdentifier', decapsulationAlgorithm, i++); + decapsulationKey = convertSubtleArgument( + prefix, 'CryptoKey', decapsulationKey, i++); + ciphertext = convertSubtleArgument(prefix, 'BufferSource', ciphertext, i++); const normalizedDecapsulationAlgorithm = normalizeAlgorithm(decapsulationAlgorithm, 'decapsulate'); @@ -1664,9 +1510,13 @@ function decapsulateBitsImpl(decapsulationAlgorithm, decapsulationKey, ciphertex case 'ML-KEM-1024': return require('internal/crypto/ml_kem') .mlKemDecapsulate(decapsulationKey, ciphertext); + /* c8 ignore start */ + default: { + const assert = require('internal/assert'); + assert.fail('Unreachable code'); + } + /* c8 ignore stop */ } - - throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } function decapsulateKey( @@ -1687,41 +1537,19 @@ function decapsulateKeyImpl( extractable, keyUsages) { emitExperimentalWarning('The decapsulateKey Web Crypto API method'); - if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); - - webidl ??= require('internal/crypto/webidl'); - const prefix = "Failed to execute 'decapsulateKey' on 'SubtleCrypto'"; - webidl.requiredArguments(arguments.length, 6, { prefix }); - decapsulationAlgorithm = webidl.converters.AlgorithmIdentifier( - decapsulationAlgorithm, - { - prefix, - context: '1st argument', - }, - ); - decapsulationKey = webidl.converters.CryptoKey(decapsulationKey, { - prefix, - context: '2nd argument', - }); - ciphertext = webidl.converters.BufferSource(ciphertext, { - prefix, - context: '3rd argument', - }); - sharedKeyAlgorithm = webidl.converters.AlgorithmIdentifier( - sharedKeyAlgorithm, - { - prefix, - context: '4th argument', - }, - ); - extractable = webidl.converters.boolean(extractable, { - prefix, - context: '5th argument', - }); - const usages = webidl.converters['sequence'](keyUsages, { - prefix, - context: '6th argument', - }); + const prefix = prepareSubtleMethod( + this, 'decapsulateKey', arguments.length, 6); + let i = 0; + decapsulationAlgorithm = convertSubtleArgument( + prefix, 'AlgorithmIdentifier', decapsulationAlgorithm, i++); + decapsulationKey = convertSubtleArgument( + prefix, 'CryptoKey', decapsulationKey, i++); + ciphertext = convertSubtleArgument(prefix, 'BufferSource', ciphertext, i++); + sharedKeyAlgorithm = convertSubtleArgument( + prefix, 'AlgorithmIdentifier', sharedKeyAlgorithm, i++); + extractable = convertSubtleArgument(prefix, 'boolean', extractable, i++); + const usages = convertSubtleArgument( + prefix, 'sequence', keyUsages, i++); const normalizedDecapsulationAlgorithm = normalizeAlgorithm(decapsulationAlgorithm, 'decapsulate'); @@ -1749,8 +1577,12 @@ function decapsulateKeyImpl( decapsulatedBits = require('internal/crypto/ml_kem') .mlKemDecapsulate(decapsulationKey, ciphertext); break; - default: - throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); + /* c8 ignore start */ + default: { + const assert = require('internal/assert'); + assert.fail('Unreachable code'); + } + /* c8 ignore stop */ } return jobPromiseThen(decapsulatedBits, (decapsulatedBits) => FunctionPrototypeCall( @@ -1919,7 +1751,7 @@ class SubtleCrypto { case 'KMAC128': case 'KMAC256': if (normalizedAdditionalAlgorithm.length === undefined || - normalizedAdditionalAlgorithm.length === 256) { + numBitsToBytes(normalizedAdditionalAlgorithm.length) === 32) { break; } return false; @@ -1928,7 +1760,11 @@ class SubtleCrypto { } } - return check(operation, algorithm, length); + try { + return check(operation, algorithm, length); + } catch { + return false; + } } } @@ -1959,7 +1795,15 @@ function check(op, alg, length) { switch (op) { case 'decapsulate': case 'decrypt': - case 'digest': + case 'digest': { + if ((normalizedAlgorithm.name === 'cSHAKE128' || + normalizedAlgorithm.name === 'cSHAKE256') && + (normalizedAlgorithm.functionName?.byteLength || + normalizedAlgorithm.customization?.byteLength)) { + return CShakeJob !== undefined; + } + return true; + } case 'encapsulate': case 'encrypt': case 'exportKey': @@ -1971,25 +1815,35 @@ function check(op, alg, length) { return true; case 'deriveBits': { if (normalizedAlgorithm.name === 'HKDF') { - try { - require('internal/crypto/hkdf').validateHkdfDeriveBitsLength(length); - } catch { - return false; - } + require('internal/crypto/hkdf').validateHkdfDeriveBitsLength(length); } if (normalizedAlgorithm.name === 'PBKDF2') { - try { - require('internal/crypto/pbkdf2').validatePbkdf2DeriveBitsLength(length); - } catch { - return false; - } + require('internal/crypto/pbkdf2').validatePbkdf2DeriveBitsLength(length); } if (StringPrototypeStartsWith(normalizedAlgorithm.name, 'Argon2')) { - try { - require('internal/crypto/argon2').validateArgon2DeriveBitsLength(length); - } catch { + require('internal/crypto/argon2').validateArgon2DeriveBitsLength(length); + } + + if (normalizedAlgorithm.name === 'X25519' && length > 256) { + return false; + } + + if (normalizedAlgorithm.name === 'X448' && length > 448) { + return false; + } + + if (normalizedAlgorithm.name === 'ECDH') { + const namedCurve = getCryptoKeyAlgorithm(normalizedAlgorithm.public).namedCurve; + const maxLength = { + '__proto__': null, + 'P-256': 256, + 'P-384': 384, + 'P-521': 528, + }[namedCurve]; + + if (length > maxLength) { return false; } } @@ -2007,10 +1861,12 @@ function check(op, alg, length) { return true; } + /* c8 ignore start */ default: { const assert = require('internal/assert'); assert.fail('Unreachable code'); } + /* c8 ignore stop */ } } diff --git a/lib/internal/crypto/webcrypto_util.js b/lib/internal/crypto/webcrypto_util.js index 320bf0436de553..1b802a6dc46624 100644 --- a/lib/internal/crypto/webcrypto_util.js +++ b/lib/internal/crypto/webcrypto_util.js @@ -1,5 +1,10 @@ 'use strict'; +const { + ArrayPrototypePush, + SafeSet, +} = primordials; + const { KeyObjectHandle, kKeyFormatDER, @@ -12,6 +17,7 @@ const { } = internalBinding('crypto'); const { + hasAnyNotIn, validateKeyOps, } = require('internal/crypto/util'); @@ -19,6 +25,110 @@ const { lazyDOMException, } = require('internal/util'); +/** + * @typedef {object} KeyUsageLists + * @property {string[]} public Usages accepted for public keys. + * @property {string[]} private Usages accepted for private keys. + * @property {string[]} keygen Usages accepted during key generation. + */ + +/** + * @typedef {object} KeyUsageSets + * @property {SafeSet} public Requested public key usages. + * @property {SafeSet} private Requested private key usages. + */ + +/** + * Validates that every requested key usage is allowed for `subject`. + * @param {string} subject + * @param {SafeSet} usagesSet + * @param {string[]} allowed + */ +function verifyAcceptableKeyUse(subject, usagesSet, allowed) { + if (hasAnyNotIn(usagesSet, allowed)) { + throw lazyDOMException( + `Unsupported key usage for ${subject} key`, + 'SyntaxError'); + } +} + +/** + * Converts a usage list to a set and validates it against `allowed`. + * @param {string[]} usages + * @param {string[]} allowed + * @param {string} subject + * @returns {SafeSet} + */ +function validateKeyUsages(usages, allowed, subject) { + const usagesSet = new SafeSet(usages); + verifyAcceptableKeyUse(subject, usagesSet, allowed); + return usagesSet; +} + +/** + * Validates that a usage set is not empty. + * @param {SafeSet} usagesSet + * @returns {SafeSet} + */ +function validateUsagesNotEmpty(usagesSet) { + if (usagesSet.size === 0) { + throw lazyDOMException( + 'Usages cannot be empty when creating a key.', + 'SyntaxError'); + } + return usagesSet; +} + +/** + * Returns the requested usages that are present in `usagesSet`. + * @param {SafeSet} usagesSet + * @param {string[]} usages + * @returns {SafeSet} + */ +function getUsagesUnion(usagesSet, usages) { + const newset = new SafeSet(); + for (let n = 0; n < usages.length; n++) { + if (usagesSet.has(usages[n])) + newset.add(usages[n]); + } + return newset; +} + +/** + * Splits requested usages into public and private key usage sets. + * @param {SafeSet} usagesSet + * @param {KeyUsageLists} allowed + * @returns {KeyUsageSets} + */ +function getKeyPairUsages(usagesSet, allowed) { + return { + public: getUsagesUnion(usagesSet, allowed.public), + private: getUsagesUnion(usagesSet, allowed.private), + }; +} + +/** + * Creates the accepted public, private, and key generation usage lists. + * @param {string[]} publicUsages + * @param {string[]} privateUsages + * @returns {KeyUsageLists} + */ +function createKeyUsages(publicUsages, privateUsages) { + const keygen = []; + for (let n = 0; n < publicUsages.length; n++) { + ArrayPrototypePush(keygen, publicUsages[n]); + } + for (let n = 0; n < privateUsages.length; n++) { + ArrayPrototypePush(keygen, privateUsages[n]); + } + return { + __proto__: null, + public: publicUsages, + private: privateUsages, + keygen, + }; +} + function importDerKey(keyData, isPublic) { const handle = new KeyObjectHandle(); const keyType = isPublic ? kKeyTypePublic : kKeyTypePrivate; @@ -74,11 +184,12 @@ function validateJwk(keyData, kty, extractable, usagesSet, expectedUse) { (keyData.priv !== undefined && typeof keyData.priv !== 'string')) throw lazyDOMException('Invalid keyData', 'DataError'); break; + /* c8 ignore start */ default: { - // It is not possible to get here because all possible cases are handled above. const assert = require('internal/assert'); assert.fail('Unreachable code'); } + /* c8 ignore stop */ } if (usagesSet.size > 0 && keyData.use !== undefined) { if (keyData.use !== expectedUse) @@ -136,10 +247,15 @@ function importJwkSecretKey(keyData) { } module.exports = { + createKeyUsages, + getKeyPairUsages, importDerKey, importJwkKey, importJwkSecretKey, importRawKey, importSecretKey, validateJwk, + validateKeyUsages, + validateUsagesNotEmpty, + verifyAcceptableKeyUse, }; diff --git a/lib/internal/crypto/webidl.js b/lib/internal/crypto/webidl.js index cca3a5fb044d92..dbdb7a5ea47cb6 100644 --- a/lib/internal/crypto/webidl.js +++ b/lib/internal/crypto/webidl.js @@ -1,18 +1,24 @@ 'use strict'; const { + ArrayBufferIsView, ArrayPrototypeIncludes, MathPow, NumberParseInt, ObjectPrototypeHasOwnProperty, + StringPrototypeCharCodeAt, StringPrototypeStartsWith, StringPrototypeToLowerCase, + Uint8Array, } = primordials; const { lazyDOMException, kEmptyObject, } = require('internal/util'); +const { + isUint32, +} = require('internal/validators'); const { CryptoKey } = require('internal/crypto/webcrypto'); const { getCryptoKeyAlgorithm, @@ -21,6 +27,7 @@ const { const { validateMaxBufferLength, kNamedCurveAliases, + numBitsToBytes, } = require('internal/crypto/util'); const { converters: webidl, @@ -256,6 +263,43 @@ function validateZeroLength(parameterName) { }; } +function validateCShakeOutputLength(V) { + if (!isUint32(numBitsToBytes(V) * 8)) { + throw lazyDOMException( + 'Invalid CShakeParams outputLength', + 'OperationError'); + } +} + +function bufferSourceEqualsAscii(V, string) { + if (V.byteLength !== string.length) return false; + + const bytes = ArrayBufferIsView(V) ? + new Uint8Array(V.buffer, V.byteOffset, V.byteLength) : + new Uint8Array(V); + for (let i = 0; i < bytes.length; i++) { + if (bytes[i] !== StringPrototypeCharCodeAt(string, i)) return false; + } + return true; +} + +function validateCShakeFunctionName(V) { + if (V.byteLength === 0 || + bufferSourceEqualsAscii(V, 'KMAC') || + bufferSourceEqualsAscii(V, 'TupleHash') || + bufferSourceEqualsAscii(V, 'ParallelHash')) { + return; + } + + throw lazyDOMException( + 'Unsupported CShakeParams functionName', + 'NotSupportedError'); +} + +function validateCShakeCustomization(V) { + validateMaxBufferLength(V, 'CShakeParams.customization', 512); +} + converters.RsaPssParams = createDictionaryConverter( 'RsaPssParams', [ dictAlgorithm, @@ -293,6 +337,13 @@ converters.EcdsaParams = createDictionaryConverter( ], ]); +function validateHmacKeyLength(parameterName, zeroError) { + return (V) => { + if (V === 0) + throw lazyDOMException(`${parameterName} cannot be 0`, zeroError); + }; +} + for (const { 0: name, 1: zeroError } of [['HmacKeyGenParams', 'OperationError'], ['HmacImportParams', 'DataError']]) { converters[name] = createDictionaryConverter( name, [ @@ -308,7 +359,7 @@ for (const { 0: name, 1: zeroError } of [['HmacKeyGenParams', 'OperationError'], key: 'length', converter: (V, opts) => converters['unsigned long'](V, enforceRangeOptions(opts)), - validator: validateMacKeyLength(`${name}.length`, zeroError), + validator: validateHmacKeyLength(`${name}.length`, zeroError), }, ], ]); @@ -390,23 +441,18 @@ converters.CShakeParams = createDictionaryConverter( key: 'outputLength', converter: (V, opts) => converters['unsigned long'](V, enforceRangeOptions(opts)), - validator: (V, opts) => { - // The Web Crypto spec allows for SHAKE output length that are not multiples of - // 8. We don't. - if (V % 8) - throw lazyDOMException('Unsupported CShakeParams outputLength', 'NotSupportedError'); - }, + validator: validateCShakeOutputLength, required: true, }, { key: 'functionName', converter: converters.BufferSource, - validator: validateZeroLength('CShakeParams.functionName'), + validator: validateCShakeFunctionName, }, { key: 'customization', converter: converters.BufferSource, - validator: validateZeroLength('CShakeParams.customization'), + validator: validateCShakeCustomization, }, ], ]); @@ -680,18 +726,7 @@ converters.Argon2Params = createDictionaryConverter( ], ]); -function validateMacKeyLength(parameterName, zeroError) { - return (V) => { - if (V === 0) - throw lazyDOMException(`${parameterName} cannot be 0`, zeroError); - - // The Web Crypto spec allows for key lengths that are not multiples of 8. We don't. - if (V % 8) - throw lazyDOMException(`Unsupported ${parameterName}`, 'NotSupportedError'); - }; -} - -for (const { 0: name, 1: zeroError } of [['KmacKeyGenParams', 'OperationError'], ['KmacImportParams', 'DataError']]) { +for (const name of ['KmacKeyGenParams', 'KmacImportParams']) { converters[name] = createDictionaryConverter( name, [ dictAlgorithm, @@ -700,7 +735,6 @@ for (const { 0: name, 1: zeroError } of [['KmacKeyGenParams', 'OperationError'], key: 'length', converter: (V, opts) => converters['unsigned long'](V, enforceRangeOptions(opts)), - validator: validateMacKeyLength(`${name}.length`, zeroError), }, ], ]); @@ -714,11 +748,6 @@ converters.KmacParams = createDictionaryConverter( key: 'outputLength', converter: (V, opts) => converters['unsigned long'](V, enforceRangeOptions(opts)), - validator: (V, opts) => { - // The Web Crypto spec allows for KMAC output length that are not multiples of 8. We don't. - if (V % 8) - throw lazyDOMException('Unsupported KmacParams outputLength', 'NotSupportedError'); - }, required: true, }, { diff --git a/src/crypto/crypto_hash.cc b/src/crypto/crypto_hash.cc index 58c4a45986bf0f..946f6850d5e68f 100644 --- a/src/crypto/crypto_hash.cc +++ b/src/crypto/crypto_hash.cc @@ -7,11 +7,23 @@ #include "threadpoolwork-inl.h" #include "v8.h" +#if OPENSSL_WITH_EVP_MAC +#include +#include +#endif + #if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK #include #endif +#include +#include +#include #include +#include +#include +#include +#include namespace node { @@ -351,6 +363,9 @@ void Hash::Initialize(Environment* env, Local target) { SetMethodNoSideEffect(context, target, "oneShotDigest", OneShotDigest); HashJob::Initialize(env, target); +#if OPENSSL_WITH_EVP_MAC + CShakeJob::Initialize(env, target); +#endif } void Hash::RegisterExternalReferences(ExternalReferenceRegistry* registry) { @@ -362,6 +377,9 @@ void Hash::RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(OneShotDigest); HashJob::RegisterExternalReferences(registry); +#if OPENSSL_WITH_EVP_MAC + CShakeJob::RegisterExternalReferences(registry); +#endif } // new Hash(algorithm, algorithmId, xofLen, algorithmCache) @@ -544,8 +562,8 @@ Maybe HashTraits::AdditionalConfig( if (args[offset + 2]->IsUint32()) [[unlikely]] { // length is expressed in terms of bits params->length = - static_cast(args[offset + 2] - .As()->Value()) / CHAR_BIT; + static_cast(args[offset + 2].As()->Value()) / + CHAR_BIT; if (params->length != expected) { if ((EVP_MD_flags(params->digest) & EVP_MD_FLAG_XOF) == 0) [[unlikely]] { THROW_ERR_CRYPTO_INVALID_DIGEST(env, "Digest method not supported"); @@ -580,5 +598,346 @@ bool HashTraits::DeriveBits(Environment* env, return true; } +#if OPENSSL_WITH_EVP_MAC +namespace { + +static constexpr std::array kEmptyString = {}; +static constexpr size_t kKeccakKmac128Rate = 168; +static constexpr size_t kKeccakKmac256Rate = 136; +static constexpr size_t kMaxCShakeCustomizationSize = 512; + +struct EncodedLength { + std::array data; + size_t size; +}; + +struct EncodedStringInput { + const void* data; + size_t byte_length; + size_t bit_length; +}; + +struct KeccakKmacXof { + ncrypto::EVPMDCtxPointer ctx; + size_t rate; +}; + +size_t EncodedLengthSize(size_t value) { + size_t size = 1; + size_t remaining = value; + while (remaining >>= CHAR_BIT) size++; + return size + 1; +} + +bool AddSize(size_t a, size_t b, size_t* out) { + if (a > std::numeric_limits::max() - b) return false; + *out = a + b; + return true; +} + +EncodedLength EncodeLength(size_t value, bool left) { + const size_t value_size = EncodedLengthSize(value) - 1; + EncodedLength encoded = {{}, value_size + 1}; + + if (left) encoded.data[0] = static_cast(value_size); + for (size_t n = 0; n < value_size; n++) { + const size_t shift = CHAR_BIT * (value_size - n - 1); + encoded.data[(left ? 1 : 0) + n] = + static_cast(value >> shift); + } + if (!left) encoded.data[value_size] = static_cast(value_size); + + return encoded; +} + +bool DigestUpdate(ncrypto::EVPMDCtxPointer* ctx, + const void* data, + size_t size) { + if (size == 0) return true; + return ctx->digestUpdate(ncrypto::Buffer{ + .data = data, + .len = size, + }); +} + +bool DigestUpdateZeros(ncrypto::EVPMDCtxPointer* ctx, size_t size) { + static constexpr std::array zeros = {}; + while (size > 0) { + const size_t chunk = std::min(size, zeros.size()); + if (!DigestUpdate(ctx, zeros.data(), chunk)) return false; + size -= chunk; + } + return true; +} + +bool EncodedStringSize(size_t byte_length, size_t bit_length, size_t* size) { + return AddSize(EncodedLengthSize(bit_length), byte_length, size); +} + +bool ByteLengthToBitLength(size_t byte_length, size_t* bit_length) { + if (byte_length > std::numeric_limits::max() / CHAR_BIT) { + return false; + } + *bit_length = byte_length * CHAR_BIT; + return true; +} + +KeccakKmacXof NewKeccakKmacXof(bool use_128_bits) { + // OpenSSL 3.x exposes the cSHAKE/KMAC suffix primitive as KECCAK-KMAC-*. + const char* digest_name = use_128_bits ? OSSL_DIGEST_NAME_KECCAK_KMAC128 + : OSSL_DIGEST_NAME_KECCAK_KMAC256; + auto digest = std::unique_ptr{ + EVP_MD_fetch(nullptr, digest_name, nullptr), EVP_MD_free}; + if (!digest) return {}; + + auto ctx = ncrypto::EVPMDCtxPointer::New(); + if (!ctx.digestInit(digest.get())) return {}; + + return { + .ctx = std::move(ctx), + .rate = use_128_bits ? kKeccakKmac128Rate : kKeccakKmac256Rate, + }; +} + +bool ToEncodedStringInput(const void* data, + size_t byte_length, + EncodedStringInput* input) { + if (byte_length > 0 && data == nullptr) return false; + + size_t bit_length; + if (!ByteLengthToBitLength(byte_length, &bit_length)) return false; + + *input = { + .data = byte_length == 0 ? kEmptyString.data() : data, + .byte_length = byte_length, + .bit_length = bit_length, + }; + return true; +} + +bool DigestUpdateEncodedLength(ncrypto::EVPMDCtxPointer* ctx, + size_t value, + bool left) { + const EncodedLength encoded = EncodeLength(value, left); + return DigestUpdate(ctx, encoded.data.data(), encoded.size); +} + +bool DigestUpdateEncodedString(ncrypto::EVPMDCtxPointer* ctx, + const void* data, + size_t byte_length, + size_t bit_length) { + return DigestUpdateEncodedLength(ctx, bit_length, true) && + DigestUpdate(ctx, data, byte_length); +} + +bool DigestUpdateBytepad(ncrypto::EVPMDCtxPointer* ctx, + size_t width, + const void* data, + size_t byte_length, + size_t bit_length, + const void* data2 = nullptr, + size_t byte_length2 = 0, + size_t bit_length2 = 0) { + if (width == 0) return false; + + size_t encoded_size; + size_t written = EncodedLengthSize(width); + if (!EncodedStringSize(byte_length, bit_length, &encoded_size) || + !AddSize(written, encoded_size, &written)) { + return false; + } + if (data2 != nullptr) { + if (!EncodedStringSize(byte_length2, bit_length2, &encoded_size) || + !AddSize(written, encoded_size, &written)) { + return false; + } + } + + size_t padded_size; + if (!AddSize(written, width - 1, &padded_size)) return false; + padded_size = padded_size / width * width; + DCHECK_GE(padded_size, written); + const size_t padding = padded_size - written; + + return DigestUpdateEncodedLength(ctx, width, true) && + DigestUpdateEncodedString(ctx, data, byte_length, bit_length) && + (data2 == nullptr || + DigestUpdateEncodedString(ctx, data2, byte_length2, bit_length2)) && + DigestUpdateZeros(ctx, padding); +} + +} // namespace + +CShakeConfig::CShakeConfig(CShakeConfig&& other) noexcept + : mode(other.mode), + in(std::move(other.in)), + function_name(std::move(other.function_name)), + customization(std::move(other.customization)), + variant(other.variant), + length(other.length) {} + +CShakeConfig& CShakeConfig::operator=(CShakeConfig&& other) noexcept { + if (&other == this) return *this; + this->~CShakeConfig(); + return *new (this) CShakeConfig(std::move(other)); +} + +void CShakeConfig::MemoryInfo(MemoryTracker* tracker) const { + // If the Job is sync, then the CShakeConfig does not own the data. + if (IsCryptoJobAsync(mode)) { + tracker->TrackFieldWithSize("in", in.size()); + tracker->TrackFieldWithSize("function_name", function_name.size()); + tracker->TrackFieldWithSize("customization", customization.size()); + } +} + +MaybeLocal CShakeTraits::EncodeOutput(Environment* env, + const CShakeConfig& params, + ByteSource* out) { + return out->ToArrayBuffer(env); +} + +Maybe CShakeTraits::AdditionalConfig( + CryptoJobMode mode, + const FunctionCallbackInfo& args, + unsigned int offset, + CShakeConfig* params) { + Environment* env = Environment::GetCurrent(args); + + params->mode = mode; + + CHECK(args[offset]->IsString()); // Algorithm name + Utf8Value algorithm_name(env->isolate(), args[offset]); + std::string_view algorithm_str = algorithm_name.ToStringView(); + + if (algorithm_str == "cSHAKE128") { + params->variant = CShakeVariant::CSHAKE128; + } else if (algorithm_str == "cSHAKE256") { + params->variant = CShakeVariant::CSHAKE256; + } else { + UNREACHABLE(); + } + + ArrayBufferOrViewContents data(args[offset + 1]); + if (!data.CheckSizeInt32()) [[unlikely]] { + THROW_ERR_OUT_OF_RANGE(env, "data is too big"); + return Nothing(); + } + params->in = IsCryptoJobAsync(mode) ? data.ToCopy() : data.ToByteSource(); + + if (!args[offset + 2]->IsUndefined()) { + ArrayBufferOrViewContents function_name(args[offset + 2]); + if (!function_name.CheckSizeInt32()) [[unlikely]] { + THROW_ERR_OUT_OF_RANGE(env, "functionName is too big"); + return Nothing(); + } + params->function_name = IsCryptoJobAsync(mode) + ? function_name.ToCopy() + : function_name.ToByteSource(); + } + + if (!args[offset + 3]->IsUndefined()) { + ArrayBufferOrViewContents customization(args[offset + 3]); + if (!customization.CheckSizeInt32()) [[unlikely]] { + THROW_ERR_OUT_OF_RANGE(env, "customization is too big"); + return Nothing(); + } + params->customization = IsCryptoJobAsync(mode) + ? customization.ToCopy() + : customization.ToByteSource(); + } + + CHECK(args[offset + 4]->IsUint32()); // Length + params->length = args[offset + 4].As()->Value(); + + return JustVoid(); +} + +bool CShakeTraits::DeriveBits(Environment* env, + const CShakeConfig& params, + ByteSource* out, + CryptoJobMode mode) { + CShakeParams cshake_params = { + .variant = params.variant, + .function_name_data = params.function_name.data(), + .function_name_size = params.function_name.size(), + .customization_data = params.customization.data(), + .customization_size = params.customization.size(), + .bytepad_input = nullptr, + .input_data = params.in.data(), + .input_size = params.in.size(), + .append_output_length = false, + .length = params.length, + }; + return DeriveCShakeBits(cshake_params, out); +} + +bool DeriveCShakeBits(const CShakeParams& params, ByteSource* out) { + if (params.customization_size > kMaxCShakeCustomizationSize) { + return false; + } + + if (params.length == 0) { + *out = ByteSource(); + return true; + } + + auto xof = NewKeccakKmacXof(params.variant == CShakeVariant::CSHAKE128); + if (!xof.ctx) return false; + auto ctx = std::move(xof.ctx); + + EncodedStringInput function_name; + EncodedStringInput customization; + if (!ToEncodedStringInput(params.function_name_data, + params.function_name_size, + &function_name) || + !ToEncodedStringInput(params.customization_data, + params.customization_size, + &customization)) { + return false; + } + + if (!DigestUpdateBytepad(&ctx, + xof.rate, + function_name.data, + function_name.byte_length, + function_name.bit_length, + customization.data, + customization.byte_length, + customization.bit_length)) { + return false; + } + + if (params.bytepad_input != nullptr && + !DigestUpdateBytepad(&ctx, + xof.rate, + params.bytepad_input->data, + params.bytepad_input->byte_length, + params.bytepad_input->bit_length)) { + return false; + } + + if (!DigestUpdate(&ctx, params.input_data, params.input_size)) { + return false; + } + + if (params.append_output_length && + !DigestUpdateEncodedLength(&ctx, params.length, false)) { + return false; + } + + const size_t length_bytes = + NumBitsToBytes(static_cast(params.length)); + auto data = ctx.digestFinal(length_bytes); + if (!data) [[unlikely]] + return false; + + DCHECK(!data.isSecure()); + *out = ByteSource::Allocated(data.release()); + if (params.length % CHAR_BIT != 0) TruncateToBitLength(params.length, out); + return true; +} +#endif // OPENSSL_WITH_EVP_MAC + } // namespace crypto } // namespace node diff --git a/src/crypto/crypto_hash.h b/src/crypto/crypto_hash.h index 0a839733cc156e..d9fe46f37b3190 100644 --- a/src/crypto/crypto_hash.h +++ b/src/crypto/crypto_hash.h @@ -82,6 +82,74 @@ struct HashTraits final { using HashJob = DeriveBitsJob; +#if OPENSSL_WITH_EVP_MAC +enum class CShakeVariant { CSHAKE128, CSHAKE256 }; + +struct CShakeBytepadInput final { + const void* data; + size_t byte_length; + size_t bit_length; +}; + +struct CShakeParams final { + CShakeVariant variant; + const void* function_name_data; + size_t function_name_size; + const void* customization_data; + size_t customization_size; + const CShakeBytepadInput* bytepad_input; + const void* input_data; + size_t input_size; + bool append_output_length; + uint32_t length; // Output length in bits +}; + +bool DeriveCShakeBits(const CShakeParams& params, ByteSource* out); + +struct CShakeConfig final : public MemoryRetainer { + CryptoJobMode mode; + ByteSource in; + ByteSource function_name; + ByteSource customization; + CShakeVariant variant; + uint32_t length; // Output length in bits + + CShakeConfig() = default; + + explicit CShakeConfig(CShakeConfig&& other) noexcept; + + CShakeConfig& operator=(CShakeConfig&& other) noexcept; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(CShakeConfig) + SET_SELF_SIZE(CShakeConfig) +}; + +struct CShakeTraits final { + using AdditionalParameters = CShakeConfig; + static constexpr const char* JobName = "CShakeJob"; + static constexpr AsyncWrap::ProviderType Provider = + AsyncWrap::PROVIDER_HASHREQUEST; + + static v8::Maybe AdditionalConfig( + CryptoJobMode mode, + const v8::FunctionCallbackInfo& args, + unsigned int offset, + CShakeConfig* params); + + static bool DeriveBits(Environment* env, + const CShakeConfig& params, + ByteSource* out, + CryptoJobMode mode); + + static v8::MaybeLocal EncodeOutput(Environment* env, + const CShakeConfig& params, + ByteSource* out); +}; + +using CShakeJob = DeriveBitsJob; +#endif // OPENSSL_WITH_EVP_MAC + } // namespace crypto } // namespace node diff --git a/src/crypto/crypto_keygen.cc b/src/crypto/crypto_keygen.cc index 597c8bab781fc0..24b0f5aca2b4b3 100644 --- a/src/crypto/crypto_keygen.cc +++ b/src/crypto/crypto_keygen.cc @@ -64,7 +64,13 @@ Maybe SecretKeyGenTraits::AdditionalConfig( SecretKeyGenConfig* params) { CHECK(args[*offset]->IsUint32()); uint32_t bits = args[*offset].As()->Value(); - params->length = bits / CHAR_BIT; + params->length_bits = bits; + if (mode == kCryptoJobWebCrypto) { + params->length = NumBitsToBytes(static_cast(bits)); + params->truncate_to_bit_length = bits % CHAR_BIT != 0; + } else { + params->length = bits / CHAR_BIT; + } *offset += 1; return JustVoid(); } @@ -77,6 +83,8 @@ KeyGenJobStatus SecretKeyGenTraits::DoKeyGen(Environment* env, return KeyGenJobStatus::FAILED; } params->out = ByteSource::Allocated(bytes.release()); + if (params->truncate_to_bit_length) + TruncateToBitLength(params->length_bits, ¶ms->out); return KeyGenJobStatus::OK; } diff --git a/src/crypto/crypto_keygen.h b/src/crypto/crypto_keygen.h index 1702dfabb4af2a..61421bfe5c11d7 100644 --- a/src/crypto/crypto_keygen.h +++ b/src/crypto/crypto_keygen.h @@ -284,8 +284,10 @@ struct KeyPairGenTraits final { }; struct SecretKeyGenConfig final : public MemoryRetainer { - size_t length; // In bytes. - ByteSource out; // Placeholder for the generated key bytes. + size_t length = 0; // In bytes. + size_t length_bits = 0; + bool truncate_to_bit_length = false; + ByteSource out; // Placeholder for the generated key bytes. void MemoryInfo(MemoryTracker* tracker) const override; SET_MEMORY_INFO_NAME(SecretKeyGenConfig) diff --git a/src/crypto/crypto_keys.cc b/src/crypto/crypto_keys.cc index 81451d88f7e4b7..bdc9aad1cad4e1 100644 --- a/src/crypto/crypto_keys.cc +++ b/src/crypto/crypto_keys.cc @@ -287,6 +287,85 @@ int GetNidFromName(const char* name) { return NID_undef; #endif } + +bool IsUnavailablePqcKeyType(Environment* env, Local key_type) { + return key_type->StringEquals(env->crypto_ml_dsa_44_string()) || + key_type->StringEquals(env->crypto_ml_dsa_65_string()) || + key_type->StringEquals(env->crypto_ml_dsa_87_string()) || + key_type->StringEquals(env->crypto_ml_kem_512_string()) || + key_type->StringEquals(env->crypto_ml_kem_768_string()) || + key_type->StringEquals(env->crypto_ml_kem_1024_string()) || + key_type->StringEquals(env->crypto_slh_dsa_sha2_128f_string()) || + key_type->StringEquals(env->crypto_slh_dsa_sha2_128s_string()) || + key_type->StringEquals(env->crypto_slh_dsa_sha2_192f_string()) || + key_type->StringEquals(env->crypto_slh_dsa_sha2_192s_string()) || + key_type->StringEquals(env->crypto_slh_dsa_sha2_256f_string()) || + key_type->StringEquals(env->crypto_slh_dsa_sha2_256s_string()) || + key_type->StringEquals(env->crypto_slh_dsa_shake_128f_string()) || + key_type->StringEquals(env->crypto_slh_dsa_shake_128s_string()) || + key_type->StringEquals(env->crypto_slh_dsa_shake_192f_string()) || + key_type->StringEquals(env->crypto_slh_dsa_shake_192s_string()) || + key_type->StringEquals(env->crypto_slh_dsa_shake_256f_string()) || + key_type->StringEquals(env->crypto_slh_dsa_shake_256s_string()); +} + +bool IsUnsupportedRawKeyType(Environment* env, Local key_type) { + return key_type->StringEquals(env->crypto_rsa_string()) || + key_type->StringEquals(env->crypto_rsa_pss_string()) || + key_type->StringEquals(env->crypto_dsa_string()) || + key_type->StringEquals(env->crypto_dh_string()); +} + +void ValidateRawKeyImportFormat(Environment* env, + Local key_type, + const char* key_type_name, + int id, + EVPKeyPointer::PKFormatType format) { + auto validate_raw_format = + [&](EVPKeyPointer::PKFormatType expected_private_format) { + if (format == EVPKeyPointer::PKFormatType::RAW_PUBLIC || + format == expected_private_format) { + return; + } + THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); + }; + + if (key_type->StringEquals(env->crypto_ec_string())) { + return validate_raw_format(EVPKeyPointer::PKFormatType::RAW_PRIVATE); + } + + switch (id) { + case EVP_PKEY_X25519: + case EVP_PKEY_X448: + case EVP_PKEY_ED25519: + case EVP_PKEY_ED448: + return validate_raw_format(EVPKeyPointer::PKFormatType::RAW_PRIVATE); + default: + break; + } + +#if OPENSSL_WITH_PQC + if (IsPqcSeedKeyId(id)) { + return validate_raw_format(EVPKeyPointer::PKFormatType::RAW_SEED); + } + if (IsPqcRawPrivateKeyId(id)) { + return validate_raw_format(EVPKeyPointer::PKFormatType::RAW_PRIVATE); + } +#endif + + if (IsUnavailablePqcKeyType(env, key_type)) { + THROW_ERR_INVALID_ARG_VALUE(env, "Unsupported key type"); + return; + } + + if (IsUnsupportedRawKeyType(env, key_type)) { + THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); + return; + } + + THROW_ERR_INVALID_ARG_VALUE( + env, "Invalid asymmetricKeyType: %s", key_type_name); +} } // namespace bool KeyObjectData::ToEncodedPublicKey( @@ -492,6 +571,12 @@ static KeyObjectData ImportRawKey(Environment* env, } }; + const int id = GetNidFromName(key_type_name); + ValidateRawKeyImportFormat(env, key_type, key_type_name, id, format); + if (env->isolate()->HasPendingException()) { + return {}; + } + // EC keys if (key_type->StringEquals(env->crypto_ec_string())) { int curve_nid = ncrypto::Ec::GetCurveIdFromName(named_curve); @@ -549,8 +634,6 @@ static KeyObjectData ImportRawKey(Environment* env, return KeyObjectData::CreateAsymmetric(target_type, std::move(pkey)); } - int id = GetNidFromName(key_type_name); - typedef EVPKeyPointer (*new_key_fn)( int, const ncrypto::Buffer&); new_key_fn fn = nullptr; @@ -589,40 +672,6 @@ static KeyObjectData ImportRawKey(Environment* env, return KeyObjectData::CreateAsymmetric(target_type, std::move(pkey)); } - if (key_type->StringEquals(env->crypto_rsa_string()) || - key_type->StringEquals(env->crypto_rsa_pss_string()) || - key_type->StringEquals(env->crypto_dsa_string()) || - key_type->StringEquals(env->crypto_dh_string())) { - THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); - return {}; - } - -#if !OPENSSL_WITH_PQC - if (key_type->StringEquals(env->crypto_ml_dsa_44_string()) || - key_type->StringEquals(env->crypto_ml_dsa_65_string()) || - key_type->StringEquals(env->crypto_ml_dsa_87_string()) || - key_type->StringEquals(env->crypto_ml_kem_512_string()) || - key_type->StringEquals(env->crypto_ml_kem_768_string()) || - key_type->StringEquals(env->crypto_ml_kem_1024_string()) || - key_type->StringEquals(env->crypto_slh_dsa_sha2_128f_string()) || - key_type->StringEquals(env->crypto_slh_dsa_sha2_128s_string()) || - key_type->StringEquals(env->crypto_slh_dsa_sha2_192f_string()) || - key_type->StringEquals(env->crypto_slh_dsa_sha2_192s_string()) || - key_type->StringEquals(env->crypto_slh_dsa_sha2_256f_string()) || - key_type->StringEquals(env->crypto_slh_dsa_sha2_256s_string()) || - key_type->StringEquals(env->crypto_slh_dsa_shake_128f_string()) || - key_type->StringEquals(env->crypto_slh_dsa_shake_128s_string()) || - key_type->StringEquals(env->crypto_slh_dsa_shake_192f_string()) || - key_type->StringEquals(env->crypto_slh_dsa_shake_192s_string()) || - key_type->StringEquals(env->crypto_slh_dsa_shake_256f_string()) || - key_type->StringEquals(env->crypto_slh_dsa_shake_256s_string())) { - THROW_ERR_INVALID_ARG_VALUE(env, "Unsupported key type"); - return {}; - } -#endif - - THROW_ERR_INVALID_ARG_VALUE( - env, "Invalid asymmetricKeyType: %s", key_type_name); return {}; } @@ -973,6 +1022,7 @@ Local KeyObjectHandle::Initialize(Environment* env) { KeyObjectHandle::kInternalFieldCount); SetProtoMethod(isolate, templ, "init", Init); + SetProtoMethodNoSideEffect(isolate, templ, "getKeyType", GetKeyType); SetProtoMethodNoSideEffect( isolate, templ, "getSymmetricKeySize", GetSymmetricKeySize); SetProtoMethodNoSideEffect( @@ -1000,6 +1050,7 @@ void KeyObjectHandle::RegisterExternalReferences( ExternalReferenceRegistry* registry) { registry->Register(New); registry->Register(Init); + registry->Register(GetKeyType); registry->Register(GetSymmetricKeySize); registry->Register(GetAsymmetricKeyType); registry->Register(CheckEcKeyData); @@ -1130,6 +1181,14 @@ void KeyObjectHandle::Init(const FunctionCallbackInfo& args) { } } +void KeyObjectHandle::GetKeyType(const FunctionCallbackInfo& args) { + KeyObjectHandle* key; + ASSIGN_OR_RETURN_UNWRAP(&key, args.This()); + + args.GetReturnValue().Set( + Uint32::NewFromUnsigned(args.GetIsolate(), key->Data().GetKeyType())); +} + void KeyObjectHandle::Equals(const FunctionCallbackInfo& args) { KeyObjectHandle* self_handle; KeyObjectHandle* arg_handle; diff --git a/src/crypto/crypto_keys.h b/src/crypto/crypto_keys.h index b91a69bb048783..49714bdb75e434 100644 --- a/src/crypto/crypto_keys.h +++ b/src/crypto/crypto_keys.h @@ -151,6 +151,7 @@ class KeyObjectHandle : public BaseObject { static void New(const v8::FunctionCallbackInfo& args); static void Init(const v8::FunctionCallbackInfo& args); + static void GetKeyType(const v8::FunctionCallbackInfo& args); static void GetKeyDetail(const v8::FunctionCallbackInfo& args); static void Equals(const v8::FunctionCallbackInfo& args); diff --git a/src/crypto/crypto_kmac.cc b/src/crypto/crypto_kmac.cc index e041ecc5d74b53..5f151c9a62a66e 100644 --- a/src/crypto/crypto_kmac.cc +++ b/src/crypto/crypto_kmac.cc @@ -1,11 +1,15 @@ #include "crypto/crypto_kmac.h" #include "async_wrap-inl.h" +#include "crypto/crypto_hash.h" #include "node_internals.h" #include "threadpoolwork-inl.h" #if OPENSSL_WITH_EVP_MAC #include #include +#include +#include +#include #include "crypto/crypto_keys.h" #include "crypto/crypto_sig.h" #include "ncrypto.h" @@ -22,6 +26,7 @@ using v8::Local; using v8::Maybe; using v8::MaybeLocal; using v8::Nothing; +using v8::Number; using v8::Object; using v8::Uint32; using v8::Value; @@ -34,6 +39,7 @@ KmacConfig::KmacConfig(KmacConfig&& other) noexcept signature(std::move(other.signature)), customization(std::move(other.customization)), variant(other.variant), + key_length(other.key_length), length(other.length) {} KmacConfig& KmacConfig::operator=(KmacConfig&& other) noexcept { @@ -96,18 +102,27 @@ Maybe KmacTraits::AdditionalConfig( } // If undefined, params->customization remains uninitialized (size 0). - CHECK(args[offset + 4]->IsUint32()); // Length - params->length = args[offset + 4].As()->Value(); + CHECK(args[offset + 4]->IsNumber()); // Key length + double key_length = args[offset + 4].As()->Value(); + if (!(key_length >= 0) || + key_length > static_cast(std::numeric_limits::max())) { + THROW_ERR_OUT_OF_RANGE(env, "key length is too big"); + return Nothing(); + } + params->key_length = static_cast(key_length); + + CHECK(args[offset + 5]->IsUint32()); // Length + params->length = args[offset + 5].As()->Value(); - ArrayBufferOrViewContents data(args[offset + 5]); + ArrayBufferOrViewContents data(args[offset + 6]); if (!data.CheckSizeInt32()) [[unlikely]] { THROW_ERR_OUT_OF_RANGE(env, "data is too big"); return Nothing(); } params->data = IsCryptoJobAsync(mode) ? data.ToCopy() : data.ToByteSource(); - if (!args[offset + 6]->IsUndefined()) { - ArrayBufferOrViewContents signature(args[offset + 6]); + if (!args[offset + 7]->IsUndefined()) { + ArrayBufferOrViewContents signature(args[offset + 7]); if (!signature.CheckSizeInt32()) [[unlikely]] { THROW_ERR_OUT_OF_RANGE(env, "signature is too big"); return Nothing(); @@ -119,23 +134,83 @@ Maybe KmacTraits::AdditionalConfig( return JustVoid(); } +namespace { + +static constexpr std::array kKmacFunctionName = { + 'K', 'M', 'A', 'C'}; +static constexpr size_t kKmacMinOpenSSLKeySize = 4; +// Keep the bit-aware path within OpenSSL's KMAC provider limits. +static constexpr size_t kKmacMaxOpenSSLKeySize = 512; +static constexpr size_t kKmacMaxOpenSSLCustomizationSize = 512; +static constexpr size_t kKmacMaxOpenSSLOutputSize = 0xffffff / CHAR_BIT; + +bool KmacParamsWithinOpenSSLLimits(const KmacConfig& params, + size_t key_size, + size_t length_bytes) { + return key_size <= kKmacMaxOpenSSLKeySize && + NumBitsToBytes(params.key_length) <= kKmacMaxOpenSSLKeySize && + params.customization.size() <= kKmacMaxOpenSSLCustomizationSize && + length_bytes <= kKmacMaxOpenSSLOutputSize; +} + +bool DeriveBitsWithCShake(const KmacConfig& params, + const void* key_data, + size_t key_size, + ByteSource* out) { + const size_t key_length_bytes = NumBitsToBytes(params.key_length); + if (key_size < key_length_bytes) return false; + + CShakeBytepadInput key_input = { + .data = key_data, + .byte_length = key_length_bytes, + .bit_length = params.key_length, + }; + CShakeParams cshake_params = { + .variant = params.variant == KmacVariant::KMAC128 + ? CShakeVariant::CSHAKE128 + : CShakeVariant::CSHAKE256, + .function_name_data = kKmacFunctionName.data(), + .function_name_size = kKmacFunctionName.size(), + .customization_data = params.customization.data(), + .customization_size = params.customization.size(), + .bytepad_input = &key_input, + .input_data = params.data.data(), + .input_size = params.data.size(), + .append_output_length = true, + .length = params.length, + }; + return DeriveCShakeBits(cshake_params, out); +} + +} // namespace + bool KmacTraits::DeriveBits(Environment* env, const KmacConfig& params, ByteSource* out, CryptoJobMode mode) { - if (params.length == 0) { - *out = ByteSource(); - return true; - } + const bool truncate_to_bit_length = params.length % CHAR_BIT != 0; + const size_t length_bytes = + NumBitsToBytes(static_cast(params.length)); // Get the key data. const void* key_data = params.key.GetSymmetricKey(); size_t key_size = params.key.GetSymmetricKeySize(); - if (key_size == 0) { + if (!KmacParamsWithinOpenSSLLimits(params, key_size, length_bytes)) { return false; } + if (params.length == 0) { + *out = ByteSource(); + return true; + } + + // OpenSSL's EVP_MAC provider rejects KMAC keys shorter than 4 bytes. + if (params.length % CHAR_BIT != 0 || params.key_length % CHAR_BIT != 0 || + key_size < kKmacMinOpenSSLKeySize) { + return DeriveBitsWithCShake(params, key_data, key_size, out); + } + // Fetch the KMAC algorithm auto mac = EVPMacPointer::Fetch((params.variant == KmacVariant::KMAC128) ? OSSL_MAC_NAME_KMAC128 @@ -155,7 +230,7 @@ bool KmacTraits::DeriveBits(Environment* env, size_t params_count = 0; // Set output length (always required for KMAC). - size_t outlen = params.length; + size_t outlen = length_bytes; params_array[params_count++] = OSSL_PARAM_construct_size_t(OSSL_MAC_PARAM_SIZE, &outlen); @@ -182,13 +257,14 @@ bool KmacTraits::DeriveBits(Environment* env, } // Finalize and get the result. - auto result = mac_ctx.final(params.length); + auto result = mac_ctx.final(length_bytes); if (!result) { return false; } auto buffer = result.release(); *out = ByteSource::Allocated(buffer.data, buffer.len); + if (truncate_to_bit_length) TruncateToBitLength(params.length, out); return true; } diff --git a/src/crypto/crypto_kmac.h b/src/crypto/crypto_kmac.h index 05b9077fbf35e0..40aa83580d0f2d 100644 --- a/src/crypto/crypto_kmac.h +++ b/src/crypto/crypto_kmac.h @@ -22,7 +22,8 @@ struct KmacConfig final : public MemoryRetainer { ByteSource signature; ByteSource customization; KmacVariant variant; - uint32_t length; // Output length in bytes + size_t key_length; // Key length in bits + uint32_t length; // Output length in bits KmacConfig() = default; diff --git a/src/crypto/crypto_sig.cc b/src/crypto/crypto_sig.cc index 851b715c67b495..eab8a30b4f15ef 100644 --- a/src/crypto/crypto_sig.cc +++ b/src/crypto/crypto_sig.cc @@ -78,6 +78,147 @@ bool ApplyRSAOptions(const EVPKeyPointer& pkey, return true; } +constexpr size_t kEd25519PointSize = 32; +constexpr size_t kEd448PointSize = 57; + +// Ed25519 has cofactor 8, so the first eight entries are the full +// canonical small-order subgroup: identity, one point of order 2, +// two points of order 4, and four points of order 8. +constexpr unsigned char kEd25519SmallOrderPoints[][kEd25519PointSize] = { + // Identity. + {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + // Order 2. + {0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f}, + // Order 4. + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80}, + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + // Order 8. + {0xc7, 0x17, 0x6a, 0x70, 0x3d, 0x4d, 0xd8, 0x4f, 0xba, 0x3c, 0x0b, + 0x76, 0x0d, 0x10, 0x67, 0x0f, 0x2a, 0x20, 0x53, 0xfa, 0x2c, 0x39, + 0xcc, 0xc6, 0x4e, 0xc7, 0xfd, 0x77, 0x92, 0xac, 0x03, 0x7a}, + {0xc7, 0x17, 0x6a, 0x70, 0x3d, 0x4d, 0xd8, 0x4f, 0xba, 0x3c, 0x0b, + 0x76, 0x0d, 0x10, 0x67, 0x0f, 0x2a, 0x20, 0x53, 0xfa, 0x2c, 0x39, + 0xcc, 0xc6, 0x4e, 0xc7, 0xfd, 0x77, 0x92, 0xac, 0x03, 0xfa}, + {0x26, 0xe8, 0x95, 0x8f, 0xc2, 0xb2, 0x27, 0xb0, 0x45, 0xc3, 0xf4, + 0x89, 0xf2, 0xef, 0x98, 0xf0, 0xd5, 0xdf, 0xac, 0x05, 0xd3, 0xc6, + 0x33, 0x39, 0xb1, 0x38, 0x02, 0x88, 0x6d, 0x53, 0xfc, 0x05}, + {0x26, 0xe8, 0x95, 0x8f, 0xc2, 0xb2, 0x27, 0xb0, 0x45, 0xc3, 0xf4, + 0x89, 0xf2, 0xef, 0x98, 0xf0, 0xd5, 0xdf, 0xac, 0x05, 0xd3, 0xc6, + 0x33, 0x39, 0xb1, 0x38, 0x02, 0x88, 0x6d, 0x53, 0xfc, 0x85}, + // Non-canonical encodings of the same small-order points. + {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80}, + {0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, + {0xee, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f}, + {0xee, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, + {0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, + {0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f}, +}; + +// Ed448 has cofactor 4, so these four entries are the full canonical +// small-order subgroup: identity, one point of order 2, and two points +// of order 4. +constexpr unsigned char kEd448SmallOrderPoints[][kEd448PointSize] = { + // Identity. + {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + // Order 2. + {0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00}, + // Order 4. + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80}, +}; + +template +bool ContainsPoint(const unsigned char* candidate, + const unsigned char (&points)[Count][PointSize]) { + for (const auto& point : points) { + if (memcmp(candidate, point, PointSize) == 0) return true; + } + return false; +} + +bool IsSmallOrderEdDsaPoint(int id, + const unsigned char* candidate, + size_t size) { + switch (id) { + case EVP_PKEY_ED25519: + return size == kEd25519PointSize && + ContainsPoint(candidate, kEd25519SmallOrderPoints); + case EVP_PKEY_ED448: + return size == kEd448PointSize && + ContainsPoint(candidate, kEd448SmallOrderPoints); + default: + return false; + } +} + +bool HasSmallOrderEdDsaPoint(const EVPKeyPointer& key, + const ByteSource& signature) { + const int id = key.id(); + size_t point_size; + + switch (id) { + case EVP_PKEY_ED25519: + point_size = kEd25519PointSize; + break; + case EVP_PKEY_ED448: + point_size = kEd448PointSize; + break; + default: + return false; + } + + if (signature.size() != point_size * 2) return false; + + if (IsSmallOrderEdDsaPoint(id, signature.data(), point_size)) { + return true; + } + + unsigned char raw_public_key[kEd448PointSize]; + size_t raw_public_key_size = point_size; + if (EVP_PKEY_get_raw_public_key( + key.get(), raw_public_key, &raw_public_key_size) != 1) { + return false; + } + + return IsSmallOrderEdDsaPoint(id, raw_public_key, raw_public_key_size); +} + std::unique_ptr Node_SignFinal(Environment* env, EVPMDCtxPointer&& mdctx, const EVPKeyPointer& pkey, @@ -757,7 +898,8 @@ bool SignTraits::DeriveBits(Environment* env, case SignConfiguration::Mode::Verify: { auto buf = DataPointer::Alloc(1); static_cast(buf.get())[0] = 0; - if (context.verify(params.data, params.signature)) { + if (context.verify(params.data, params.signature) && + !HasSmallOrderEdDsaPoint(key, params.signature)) { static_cast(buf.get())[0] = 1; } *out = ByteSource::Allocated(buf.release()); diff --git a/src/crypto/crypto_util.cc b/src/crypto/crypto_util.cc index e5c1c254179033..06c4aa87464de6 100644 --- a/src/crypto/crypto_util.cc +++ b/src/crypto/crypto_util.cc @@ -344,6 +344,30 @@ ByteSource& ByteSource::operator=(ByteSource&& other) noexcept { return *this; } +void TruncateToBitLength(size_t length_bits, ByteSource* bytes) { + CHECK_NOT_NULL(bytes); + const size_t length_bytes = NumBitsToBytes(length_bits); + CHECK_LE(length_bytes, bytes->size()); + + if (bytes->allocated_data_ == nullptr || bytes->size() != length_bytes) { + auto data = DataPointer::Alloc(length_bytes); + if (length_bytes > 0) { + CHECK_NOT_NULL(data.get()); + memcpy(data.get(), bytes->data(), length_bytes); + } + *bytes = ByteSource::Allocated(data.release()); + } + + const size_t remainder_bits = length_bits % CHAR_BIT; + if (remainder_bits != 0) { + auto* data = static_cast(bytes->allocated_data_); + CHECK_NOT_NULL(data); + const unsigned char mask = + static_cast(0xff << (CHAR_BIT - remainder_bits)); + data[length_bytes - 1] &= mask; + } +} + std::unique_ptr ByteSource::ReleaseToBackingStore( Environment* env) { // It's ok for allocated_data_ to be nullptr but diff --git a/src/crypto/crypto_util.h b/src/crypto/crypto_util.h index 16d39a72a59b88..a99ab94fff5d6b 100644 --- a/src/crypto/crypto_util.h +++ b/src/crypto/crypto_util.h @@ -43,6 +43,11 @@ constexpr size_t kSizeOf_HMAC_CTX = 32; constexpr size_t kSizeOf_SSL_CTX = 240; constexpr size_t kSizeOf_X509 = 128; +template +constexpr T NumBitsToBytes(T bits) { + return (bits / CHAR_BIT) + ((CHAR_BIT - 1 + (bits % CHAR_BIT)) / CHAR_BIT); +} + bool ProcessFipsOptions(); bool InitCryptoOnce(v8::Isolate* isolate); @@ -231,6 +236,8 @@ class ByteSource final { Environment* env, v8::Local value); private: + friend void TruncateToBitLength(size_t length_bits, ByteSource* bytes); + const void* data_ = nullptr; void* allocated_data_ = nullptr; size_t size_ = 0; @@ -239,6 +246,8 @@ class ByteSource final { : data_(data), allocated_data_(allocated_data), size_(size) {} }; +void TruncateToBitLength(size_t length_bits, ByteSource* bytes); + enum CryptoJobMode { kCryptoJobAsync, kCryptoJobSync, kCryptoJobWebCrypto }; CryptoJobMode GetCryptoJobMode(v8::Local args); diff --git a/test/fixtures/crypto/kmac.js b/test/fixtures/crypto/kmac.js index cc1870af2bb04f..ed265bb9c9f3e5 100644 --- a/test/fixtures/crypto/kmac.js +++ b/test/fixtures/crypto/kmac.js @@ -114,6 +114,61 @@ module.exports = function() { 0x76, 0xfc, 0x89, 0x65, ]), }, + { + // KMAC128 with a short key, generated with OpenSSL's KECCAK-KMAC128 + // digest over independently encoded NIST SP 800-185 framing. + algorithm: 'KMAC128', + key: Buffer.from([0x00, 0x01, 0x02]), + data: Buffer.from([0x01, 0x02, 0x03]), + customization: Buffer.from('Node.js'), + outputLength: 256, + expected: Buffer.from([ + 0xfb, 0x7c, 0xbb, 0xa2, 0xa1, 0x0d, 0x1a, 0x87, 0x9a, 0x9f, 0x96, 0x8c, + 0x58, 0x9d, 0x2a, 0xfe, 0x4a, 0x9b, 0xbf, 0x03, 0x7e, 0x85, 0x2c, 0xac, + 0x05, 0xdd, 0x78, 0x5b, 0x78, 0xd6, 0x57, 0x1c, + ]), + }, + { + // KMAC256 with a short key, generated with OpenSSL's KECCAK-KMAC256 + // digest over independently encoded NIST SP 800-185 framing. + algorithm: 'KMAC256', + key: Buffer.from([0x00, 0x01, 0x02]), + data: Buffer.from([0x01, 0x02, 0x03]), + customization: Buffer.from('Node.js'), + outputLength: 512, + expected: Buffer.from([ + 0x2c, 0xcf, 0x20, 0xde, 0xd8, 0xc9, 0x6d, 0xb0, 0x5f, 0x15, 0xe0, 0xb3, + 0xce, 0x5d, 0xf0, 0x45, 0xc7, 0xd7, 0x4e, 0xfe, 0x18, 0xee, 0x36, 0xa8, + 0xe4, 0x9a, 0x37, 0xfb, 0xc2, 0xb1, 0xbb, 0xfd, 0xad, 0xf5, 0xb7, 0x89, + 0xd3, 0xa4, 0xbc, 0xb3, 0xa8, 0x28, 0x8e, 0x9f, 0x25, 0xe6, 0x8d, 0x5b, + 0x4a, 0x01, 0x0b, 0x90, 0xae, 0x6d, 0x2b, 0xfc, 0xf1, 0xb6, 0xbb, 0x82, + 0x34, 0x8b, 0x51, 0xd9, + ]), + }, + { + // KMAC128 with a non-byte-aligned output length. The second byte has its + // unused low bits cleared after squeezing a 9-bit result. + algorithm: 'KMAC128', + key: Buffer.from([0x00, 0x01, 0x02, 0x03]), + data: Buffer.from([0x01, 0x02, 0x03]), + customization: undefined, + outputLength: 9, + expected: Buffer.from([0x63, 0x80]), + }, + { + // KMAC128 with a non-byte-aligned key length. The raw key is already + // truncated to 25 bits, matching WebCrypto import semantics. + algorithm: 'KMAC128', + key: Buffer.from([0xff, 0xff, 0xff, 0x80]), + keyLength: 25, + data: Buffer.from([0x01, 0x02, 0x03]), + customization: undefined, + outputLength: 128, + expected: Buffer.from([ + 0x25, 0xea, 0xc7, 0x06, 0x82, 0x47, 0x7e, 0x3c, 0x9b, 0xf0, 0xf1, 0x51, + 0x87, 0x46, 0x40, 0x0c, + ]), + }, ]; return vectors; diff --git a/test/fixtures/webcrypto/supports-level-2.mjs b/test/fixtures/webcrypto/supports-level-2.mjs index 51a3ff10ac2188..674e4cf8c6de1a 100644 --- a/test/fixtures/webcrypto/supports-level-2.mjs +++ b/test/fixtures/webcrypto/supports-level-2.mjs @@ -62,7 +62,7 @@ export const vectors = { [true, 'Ed25519'], [true, { name: 'HMAC', hash: 'SHA-256' }], [true, { name: 'HMAC', hash: 'SHA-256', length: 256 }], - [false, { name: 'HMAC', hash: 'SHA-256', length: 25 }], + [true, { name: 'HMAC', hash: 'SHA-256', length: 25 }], [true, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256', ...RSA_KEY_GEN }], [true, { name: 'RSA-PSS', hash: 'SHA-256', ...RSA_KEY_GEN }], [true, { name: 'RSA-OAEP', hash: 'SHA-256', ...RSA_KEY_GEN }], @@ -78,7 +78,7 @@ export const vectors = { [false, { name: 'AES-KW', length: 25 }], [true, { name: 'HMAC', hash: 'SHA-256' }], [true, { name: 'HMAC', hash: 'SHA-256', length: 256 }], - [false, { name: 'HMAC', hash: 'SHA-256', length: 25 }], + [true, { name: 'HMAC', hash: 'SHA-256', length: 25 }], [false, { name: 'HMAC', hash: 'SHA-256', length: 0 }], ], 'deriveKey': [ @@ -103,18 +103,36 @@ export const vectors = { [true, { name: 'X25519', public: X25519.publicKey }, { name: 'AES-CBC', length: 128 }], - [true, + [false, { name: 'X25519', public: X25519.publicKey }, { name: 'HMAC', hash: 'SHA-256' }], + [true, + { name: 'X25519', public: X25519.publicKey }, + { name: 'HMAC', hash: 'SHA-256', length: 256 }], + [false, + { name: 'X25519', public: X25519.publicKey }, + { name: 'HMAC', hash: 'SHA-256', length: 257 }], [true, { name: 'X25519', public: X25519.publicKey }, 'HKDF'], [true, { name: 'ECDH', public: ECDH.publicKey }, { name: 'AES-CBC', length: 128 }], - [true, + [false, { name: 'ECDH', public: ECDH.publicKey }, { name: 'HMAC', hash: 'SHA-256' }], + [true, + { name: 'ECDH', public: ECDH.publicKey }, + { name: 'HMAC', hash: 'SHA-256', length: 255 }], + [true, + { name: 'ECDH', public: ECDH.publicKey }, + { name: 'HMAC', hash: 'SHA-256', length: 256 }], + [false, + { name: 'ECDH', public: ECDH.publicKey }, + { name: 'HMAC', hash: 'SHA-256', length: 257 }], + [false, + { name: 'ECDH', public: ECDH.publicKey }, + { name: 'HMAC', hash: 'SHA-256', length: 264 }], [true, { name: 'ECDH', public: ECDH.publicKey }, 'HKDF'], @@ -143,10 +161,18 @@ export const vectors = { [true, { name: 'ECDH', public: ECDH.publicKey }], + [true, + { name: 'ECDH', public: ECDH.publicKey }, 256], + [false, + { name: 'ECDH', public: ECDH.publicKey }, 257], + [false, + { name: 'ECDH', public: ECDH.publicKey }, 264], [false, { name: 'ECDH', public: ECDH.privateKey }], [false, 'ECDH'], [true, { name: 'X25519', public: X25519.publicKey }], + [true, { name: 'X25519', public: X25519.publicKey }, 256], + [false, { name: 'X25519', public: X25519.publicKey }, 257], [false, { name: 'X25519', public: X25519.privateKey }], [false, 'X25519'], ], @@ -157,7 +183,7 @@ export const vectors = { [true, 'Ed25519'], [true, { name: 'HMAC', hash: 'SHA-256' }], [true, { name: 'HMAC', hash: 'SHA-256', length: 256 }], - [false, { name: 'HMAC', hash: 'SHA-256', length: 25 }], + [true, { name: 'HMAC', hash: 'SHA-256', length: 25 }], [true, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256', ...RSA_KEY_GEN }], [true, { name: 'RSA-PSS', hash: 'SHA-256', ...RSA_KEY_GEN }], [true, { name: 'RSA-OAEP', hash: 'SHA-256', ...RSA_KEY_GEN }], @@ -169,7 +195,7 @@ export const vectors = { [true, 'AES-KW'], [true, { name: 'HMAC', hash: 'SHA-256' }], [true, { name: 'HMAC', hash: 'SHA-256', length: 256 }], - [false, { name: 'HMAC', hash: 'SHA-256', length: 25 }], + [true, { name: 'HMAC', hash: 'SHA-256', length: 25 }], [false, { name: 'HMAC', hash: 'SHA-256', length: 0 }], [true, 'HKDF'], [true, 'PBKDF2'], diff --git a/test/fixtures/webcrypto/supports-modern-algorithms.mjs b/test/fixtures/webcrypto/supports-modern-algorithms.mjs index 2d370b8e21d3d5..a3e5fc54976483 100644 --- a/test/fixtures/webcrypto/supports-modern-algorithms.mjs +++ b/test/fixtures/webcrypto/supports-modern-algorithms.mjs @@ -18,15 +18,19 @@ export const vectors = { [false, 'cSHAKE128'], [shake128, { name: 'cSHAKE128', outputLength: 128 }], [shake128, { name: 'cSHAKE128', outputLength: 128, functionName: Buffer.alloc(0), customization: Buffer.alloc(0) }], - [false, { name: 'cSHAKE128', outputLength: 128, functionName: Buffer.alloc(1) }], - [false, { name: 'cSHAKE128', outputLength: 128, customization: Buffer.alloc(1) }], - [false, { name: 'cSHAKE128', outputLength: 127 }], + [shake128 && kmac, { name: 'cSHAKE128', outputLength: 128, functionName: Buffer.from('KMAC') }], + [false, { name: 'cSHAKE128', outputLength: 128, functionName: Buffer.from('SHAKE') }], + [shake128 && kmac, { name: 'cSHAKE128', outputLength: 128, customization: Buffer.alloc(1) }], + [false, { name: 'cSHAKE128', outputLength: 128, customization: Buffer.alloc(513) }], + [shake128, { name: 'cSHAKE128', outputLength: 127 }], [false, 'cSHAKE256'], [shake256, { name: 'cSHAKE256', outputLength: 256 }], [shake256, { name: 'cSHAKE256', outputLength: 256, functionName: Buffer.alloc(0), customization: Buffer.alloc(0) }], - [false, { name: 'cSHAKE256', outputLength: 256, functionName: Buffer.alloc(1) }], - [false, { name: 'cSHAKE256', outputLength: 256, customization: Buffer.alloc(1) }], - [false, { name: 'cSHAKE256', outputLength: 255 }], + [shake256 && kmac, { name: 'cSHAKE256', outputLength: 256, functionName: Buffer.from('KMAC') }], + [false, { name: 'cSHAKE256', outputLength: 256, functionName: Buffer.from('SHAKE') }], + [shake256 && kmac, { name: 'cSHAKE256', outputLength: 256, customization: Buffer.alloc(1) }], + [false, { name: 'cSHAKE256', outputLength: 256, customization: Buffer.alloc(513) }], + [shake256, { name: 'cSHAKE256', outputLength: 255 }], [false, 'TurboSHAKE128'], [true, { name: 'TurboSHAKE128', outputLength: 128 }], [true, { name: 'TurboSHAKE128', outputLength: 128, domainSeparation: 0x07 }], @@ -68,7 +72,9 @@ export const vectors = { [false, 'KMAC128'], [false, 'KMAC256'], [kmac, { name: 'KMAC128', outputLength: 256 }], + [kmac, { name: 'KMAC128', outputLength: 255 }], [kmac, { name: 'KMAC256', outputLength: 256 }], + [kmac, { name: 'KMAC256', outputLength: 255 }], ], 'generateKey': [ [pqc, 'ML-DSA-44'], @@ -86,10 +92,10 @@ export const vectors = { [kmac, 'KMAC256'], [kmac, { name: 'KMAC128', length: 256 }], [kmac, { name: 'KMAC256', length: 128 }], - [false, { name: 'KMAC128', length: 0 }], - [false, { name: 'KMAC256', length: 0 }], - [false, { name: 'KMAC128', length: 1 }], - [false, { name: 'KMAC256', length: 1 }], + [kmac, { name: 'KMAC128', length: 0 }], + [kmac, { name: 'KMAC256', length: 0 }], + [kmac, { name: 'KMAC128', length: 1 }], + [kmac, { name: 'KMAC256', length: 1 }], ], 'importKey': [ [pqc, 'ML-DSA-44'], @@ -107,10 +113,10 @@ export const vectors = { [kmac, 'KMAC256'], [kmac, { name: 'KMAC128', length: 256 }], [kmac, { name: 'KMAC256', length: 128 }], - [false, { name: 'KMAC128', length: 0 }], - [false, { name: 'KMAC256', length: 0 }], - [false, { name: 'KMAC128', length: 1 }], - [false, { name: 'KMAC256', length: 1 }], + [kmac, { name: 'KMAC128', length: 0 }], + [kmac, { name: 'KMAC256', length: 0 }], + [kmac, { name: 'KMAC128', length: 1 }], + [kmac, { name: 'KMAC256', length: 1 }], ], 'exportKey': [ [pqc, 'ML-DSA-44'], @@ -214,7 +220,10 @@ export const vectors = { [pqc, 'ML-KEM-768', 'PBKDF2'], [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256' }], [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 256 }], + [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 255 }], + [pqc && kmac, 'ML-KEM-768', { name: 'KMAC128', length: 255 }], [false, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 128 }], + [false, 'ML-KEM-768', { name: 'KMAC128', length: 248 }], ], 'decapsulateBits': [ [pqc && !boringSSL, 'ML-KEM-512'], @@ -232,6 +241,9 @@ export const vectors = { [pqc, 'ML-KEM-768', 'PBKDF2'], [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256' }], [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 256 }], + [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 255 }], + [pqc && kmac, 'ML-KEM-768', { name: 'KMAC128', length: 255 }], [false, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 128 }], + [false, 'ML-KEM-768', { name: 'KMAC128', length: 248 }], ], }; diff --git a/test/fixtures/webcrypto/supports-secure-curves.mjs b/test/fixtures/webcrypto/supports-secure-curves.mjs index e2799b5baf40fc..56bb89c28afaae 100644 --- a/test/fixtures/webcrypto/supports-secure-curves.mjs +++ b/test/fixtures/webcrypto/supports-secure-curves.mjs @@ -28,15 +28,23 @@ export const vectors = { [!boringSSL, { name: 'X448', public: X448?.publicKey }, { name: 'AES-CBC', length: 128 }], - [!boringSSL, + [false, { name: 'X448', public: X448?.publicKey }, { name: 'HMAC', hash: 'SHA-256' }], + [!boringSSL, + { name: 'X448', public: X448?.publicKey }, + { name: 'HMAC', hash: 'SHA-256', length: 448 }], + [false, + { name: 'X448', public: X448?.publicKey }, + { name: 'HMAC', hash: 'SHA-256', length: 449 }], [!boringSSL, { name: 'X448', public: X448?.publicKey }, 'HKDF'], ], 'deriveBits': [ [!boringSSL, { name: 'X448', public: X448?.publicKey }], + [!boringSSL, { name: 'X448', public: X448?.publicKey }, 448], + [false, { name: 'X448', public: X448?.publicKey }, 449], [false, { name: 'X448', public: X25519.publicKey }], [false, { name: 'X448', public: X448?.privateKey }], [false, 'X448'], diff --git a/test/fixtures/webcrypto/supports-sha3.mjs b/test/fixtures/webcrypto/supports-sha3.mjs index db1aa0e2ac4c0e..73f5b778eeb97f 100644 --- a/test/fixtures/webcrypto/supports-sha3.mjs +++ b/test/fixtures/webcrypto/supports-sha3.mjs @@ -20,12 +20,12 @@ export const vectors = { ], 'generateKey': [ [!boringSSL, { name: 'HMAC', hash: 'SHA3-256', length: 256 }], - [false, { name: 'HMAC', hash: 'SHA3-256', length: 25 }], + [!boringSSL, { name: 'HMAC', hash: 'SHA3-256', length: 25 }], [!boringSSL, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA3-256', ...RSA_KEY_GEN }], [!boringSSL, { name: 'RSA-PSS', hash: 'SHA3-256', ...RSA_KEY_GEN }], [!boringSSL, { name: 'RSA-OAEP', hash: 'SHA3-256', ...RSA_KEY_GEN }], [!boringSSL, { name: 'HMAC', hash: 'SHA3-256', length: 256 }], - [false, { name: 'HMAC', hash: 'SHA3-256', length: 25 }], + [!boringSSL, { name: 'HMAC', hash: 'SHA3-256', length: 25 }], [false, { name: 'HMAC', hash: 'SHA3-256', length: 0 }], // This interaction is not defined for now. @@ -88,13 +88,13 @@ export const vectors = { 'importKey': [ [!boringSSL, { name: 'HMAC', hash: 'SHA3-256' }], [!boringSSL, { name: 'HMAC', hash: 'SHA3-256', length: 256 }], - [false, { name: 'HMAC', hash: 'SHA3-256', length: 25 }], + [!boringSSL, { name: 'HMAC', hash: 'SHA3-256', length: 25 }], [!boringSSL, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA3-256', ...RSA_KEY_GEN }], [!boringSSL, { name: 'RSA-PSS', hash: 'SHA3-256', ...RSA_KEY_GEN }], [!boringSSL, { name: 'RSA-OAEP', hash: 'SHA3-256', ...RSA_KEY_GEN }], [!boringSSL, { name: 'HMAC', hash: 'SHA3-256' }], [!boringSSL, { name: 'HMAC', hash: 'SHA3-256', length: 256 }], - [false, { name: 'HMAC', hash: 'SHA3-256', length: 25 }], + [!boringSSL, { name: 'HMAC', hash: 'SHA3-256', length: 25 }], [false, { name: 'HMAC', hash: 'SHA3-256', length: 0 }], ], 'get key length': [ diff --git a/test/fixtures/wpt/README.md b/test/fixtures/wpt/README.md index 67be1d3780932c..1d4324a93b6872 100644 --- a/test/fixtures/wpt/README.md +++ b/test/fixtures/wpt/README.md @@ -29,12 +29,12 @@ Last update: - resources: https://github.com/web-platform-tests/wpt/tree/6a2f322376/resources - streams: https://github.com/web-platform-tests/wpt/tree/f8f26a372f/streams - url: https://github.com/web-platform-tests/wpt/tree/d4598eba09/url -- urlpattern: https://github.com/web-platform-tests/wpt/tree/23aac92784/urlpattern +- urlpattern: https://github.com/web-platform-tests/wpt/tree/11a459a2b1/urlpattern - user-timing: https://github.com/web-platform-tests/wpt/tree/5ae85bf826/user-timing - wasm/jsapi: https://github.com/web-platform-tests/wpt/tree/cde25e7e3c/wasm/jsapi - wasm/webapi: https://github.com/web-platform-tests/wpt/tree/fd1b23eeaa/wasm/webapi - web-locks: https://github.com/web-platform-tests/wpt/tree/10a122a6bc/web-locks -- WebCryptoAPI: https://github.com/web-platform-tests/wpt/tree/97bbc7247a/WebCryptoAPI +- WebCryptoAPI: https://github.com/web-platform-tests/wpt/tree/03a1476844/WebCryptoAPI - webidl: https://github.com/web-platform-tests/wpt/tree/63ca529a02/webidl - webidl/ecmascript-binding/es-exceptions: https://github.com/web-platform-tests/wpt/tree/2f96fa1996/webidl/ecmascript-binding/es-exceptions - webmessaging/broadcastchannel: https://github.com/web-platform-tests/wpt/tree/6495c91853/webmessaging/broadcastchannel diff --git a/test/fixtures/wpt/WebCryptoAPI/digest/cshake.tentative.https.any.js b/test/fixtures/wpt/WebCryptoAPI/digest/cshake.tentative.https.any.js index 71c717b62550bc..81793666294c23 100644 --- a/test/fixtures/wpt/WebCryptoAPI/digest/cshake.tentative.https.any.js +++ b/test/fixtures/wpt/WebCryptoAPI/digest/cshake.tentative.https.any.js @@ -163,7 +163,7 @@ Object.keys(digestedData).forEach(function (alg) { Object.keys(sourceData).forEach(function (size) { promise_test(function (test) { return crypto.subtle - .digest({ name: alg, length: length }, sourceData[size]) + .digest({ name: alg, outputLength: length }, sourceData[size]) .then(function (result) { assert_true( equalBuffers(result, digestedData[alg][length][size]), @@ -184,7 +184,7 @@ Object.keys(digestedData).forEach(function (alg) { buffer[0] = sourceData[size][0]; return alg; }, - length + outputLength: length }, buffer) .then(function (result) { assert_true( @@ -197,7 +197,7 @@ Object.keys(digestedData).forEach(function (alg) { promise_test(function (test) { var buffer = new Uint8Array(sourceData[size]); var promise = crypto.subtle - .digest({ name: alg, length: length }, buffer) + .digest({ name: alg, outputLength: length }, buffer) .then(function (result) { assert_true( equalBuffers(result, digestedData[alg][length][size]), @@ -218,7 +218,7 @@ Object.keys(digestedData).forEach(function (alg) { buffer.buffer.transfer(); return alg; }, - length + outputLength: length }, buffer) .then(function (result) { assert_true( @@ -231,7 +231,7 @@ Object.keys(digestedData).forEach(function (alg) { promise_test(function (test) { var buffer = new Uint8Array(sourceData[size]); var promise = crypto.subtle - .digest({ name: alg, length: length }, buffer) + .digest({ name: alg, outputLength: length }, buffer) .then(function (result) { assert_true( equalBuffers(result, digestedData[alg][length][size]), diff --git a/test/fixtures/wpt/WebCryptoAPI/getPublicKey.tentative.https.any.js b/test/fixtures/wpt/WebCryptoAPI/getPublicKey.tentative.https.any.js index 1b151345c3416d..73e4ad8231b04c 100644 --- a/test/fixtures/wpt/WebCryptoAPI/getPublicKey.tentative.https.any.js +++ b/test/fixtures/wpt/WebCryptoAPI/getPublicKey.tentative.https.any.js @@ -61,6 +61,42 @@ const algorithms = [ generateKeyParams: { name: "X25519" }, usages: ["deriveKey", "deriveBits"], publicKeyUsages: [] + }, + { + name: "ML-DSA-44", + generateKeyParams: { name: "ML-DSA-44" }, + usages: ["sign", "verify"], + publicKeyUsages: ["verify"] + }, + { + name: "ML-DSA-65", + generateKeyParams: { name: "ML-DSA-65" }, + usages: ["sign", "verify"], + publicKeyUsages: ["verify"] + }, + { + name: "ML-DSA-87", + generateKeyParams: { name: "ML-DSA-87" }, + usages: ["sign", "verify"], + publicKeyUsages: ["verify"] + }, + { + name: "ML-KEM-512", + generateKeyParams: { name: "ML-KEM-512" }, + usages: ["encapsulateBits", "encapsulateKey", "decapsulateBits", "decapsulateKey"], + publicKeyUsages: ["encapsulateBits", "encapsulateKey"] + }, + { + name: "ML-KEM-768", + generateKeyParams: { name: "ML-KEM-768" }, + usages: ["encapsulateBits", "encapsulateKey", "decapsulateBits", "decapsulateKey"], + publicKeyUsages: ["encapsulateBits", "encapsulateKey"] + }, + { + name: "ML-KEM-1024", + generateKeyParams: { name: "ML-KEM-1024" }, + usages: ["encapsulateBits", "encapsulateKey", "decapsulateBits", "decapsulateKey"], + publicKeyUsages: ["encapsulateBits", "encapsulateKey"] } ]; diff --git a/test/fixtures/wpt/WebCryptoAPI/import_export/symmetric_importKey.js b/test/fixtures/wpt/WebCryptoAPI/import_export/symmetric_importKey.js index 8633af5279caa1..795d90ffbff4fd 100644 --- a/test/fixtures/wpt/WebCryptoAPI/import_export/symmetric_importKey.js +++ b/test/fixtures/wpt/WebCryptoAPI/import_export/symmetric_importKey.js @@ -59,6 +59,20 @@ function runTests(algorithmName) { testFormat(format, algorithm, data, keyData.length * 8, usages, extractable); }); testEmptyUsages(format, algorithm, data, keyData.length * 8, extractable); + + // Verify the key data is copied only after the algorithm is + // normalized (per spec, "get a copy of the bytes held by the + // keyData parameter" follows "normalize an algorithm"). A + // getter on the algorithm name mutates the key data during + // normalization; the imported key must reflect the restored + // bytes. Checked for BufferSource formats (jwk key data is + // not a BufferSource) when the key is extractable, so the + // imported bytes can be read back and compared. + if (extractable && format !== "jwk") { + allValidUsages(vector.legalUsages).forEach(function(usages) { + testKeyDataAlteredDuringCall(format, algorithm, keyData, keyData.length * 8, usages); + }); + } }); }); @@ -111,6 +125,35 @@ function runTests(algorithmName) { }, "Empty Usages: " + keySize.toString() + " bits " + parameterString(format, keyData, algorithm, extractable, usages)); } + // Test that importKey copies the key data only after the algorithm has been + // normalized. The spec normalizes the algorithm (which can run author + // getters on the algorithm dictionary) before getting a copy of the bytes + // held by the keyData parameter. Here a getter on the algorithm's name + // mutates the key data during normalization and then restores it, so a + // spec-compliant implementation imports the restored (original) bytes. + function testKeyDataAlteredDuringCall(format, algorithm, keyData, keySize, usages) { + promise_test(function(test) { + var alteredKeyData = copyBuffer(keyData); + alteredKeyData[0] = 255 - alteredKeyData[0]; + var algorithmWithGetter = { + ...algorithm, + get name() { + alteredKeyData[0] = keyData[0]; + return algorithm.name; + } + }; + return subtle.importKey(format, alteredKeyData, algorithmWithGetter, true, usages). + then(function(key) { + return subtle.exportKey(format, key); + }). + then(function(result) { + assert_true(equalBuffers(keyData, result), "Imported key reflects the key data as it was after normalization"); + }, function(err) { + assert_unreached("Threw an unexpected error: " + err.toString()); + }); + }, "Key data altered during call: " + keySize.toString() + " bits " + parameterString(format, keyData, algorithm, true, usages)); + } + // Helper methods follow: diff --git a/test/fixtures/wpt/WebCryptoAPI/sign_verify/kmac.js b/test/fixtures/wpt/WebCryptoAPI/sign_verify/kmac.js index c696fa46c064a4..d9d82d150b3933 100644 --- a/test/fixtures/wpt/WebCryptoAPI/sign_verify/kmac.js +++ b/test/fixtures/wpt/WebCryptoAPI/sign_verify/kmac.js @@ -15,7 +15,7 @@ function run_test() { var promise = importVectorKeys(vector, ["verify", "sign"]) .then(function(vector) { promise_test(function(test) { - var algorithmParams = {name: vector.algorithm, length: vector.length}; + var algorithmParams = {name: vector.algorithm, outputLength: vector.outputLength}; if (vector.customization !== undefined) { algorithmParams.customization = vector.customization; } @@ -48,7 +48,7 @@ function run_test() { var signature = copyBuffer(vector.signature); signature[0] = 255 - signature[0]; var algorithmParams = { - length: vector.length, + outputLength: vector.outputLength, get name() { signature[0] = vector.signature[0]; return vector.algorithm; @@ -81,7 +81,7 @@ function run_test() { .then(function(vector) { promise_test(function(test) { var signature = copyBuffer(vector.signature); - var algorithmParams = {name: vector.algorithm, length: vector.length}; + var algorithmParams = {name: vector.algorithm, outputLength: vector.outputLength}; if (vector.customization !== undefined) { algorithmParams.customization = vector.customization; } @@ -115,7 +115,7 @@ function run_test() { signature.buffer.transfer(); return vector.algorithm; }, - length: vector.length + outputLength: vector.outputLength }; if (vector.customization !== undefined) { algorithmParams.customization = vector.customization; @@ -144,7 +144,7 @@ function run_test() { .then(function(vector) { promise_test(function(test) { var signature = copyBuffer(vector.signature); - var algorithmParams = {name: vector.algorithm, length: vector.length}; + var algorithmParams = {name: vector.algorithm, outputLength: vector.outputLength}; if (vector.customization !== undefined) { algorithmParams.customization = vector.customization; } @@ -175,7 +175,7 @@ function run_test() { var plaintext = copyBuffer(vector.plaintext); plaintext[0] = 255 - plaintext[0]; var algorithmParams = { - length: vector.length, + outputLength: vector.outputLength, get name() { plaintext[0] = vector.plaintext[0]; return vector.algorithm; @@ -208,7 +208,7 @@ function run_test() { .then(function(vector) { promise_test(function(test) { var plaintext = copyBuffer(vector.plaintext); - var algorithmParams = {name: vector.algorithm, length: vector.length}; + var algorithmParams = {name: vector.algorithm, outputLength: vector.outputLength}; if (vector.customization !== undefined) { algorithmParams.customization = vector.customization; } @@ -242,7 +242,7 @@ function run_test() { plaintext.buffer.transfer(); return vector.algorithm; }, - length: vector.length + outputLength: vector.outputLength }; if (vector.customization !== undefined) { algorithmParams.customization = vector.customization; @@ -271,7 +271,7 @@ function run_test() { .then(function(vector) { promise_test(function(test) { var plaintext = copyBuffer(vector.plaintext); - var algorithmParams = {name: vector.algorithm, length: vector.length}; + var algorithmParams = {name: vector.algorithm, outputLength: vector.outputLength}; if (vector.customization !== undefined) { algorithmParams.customization = vector.customization; } @@ -301,7 +301,7 @@ function run_test() { var promise = importVectorKeys(vector, ["sign"]) .then(function(vector) { promise_test(function(test) { - var algorithmParams = {name: vector.algorithm, length: vector.length}; + var algorithmParams = {name: vector.algorithm, outputLength: vector.outputLength}; if (vector.customization !== undefined) { algorithmParams.customization = vector.customization; } @@ -326,7 +326,7 @@ function run_test() { var promise = importVectorKeys(vector, ["verify", "sign"]) .then(function(vectors) { promise_test(function(test) { - var algorithmParams = {name: vector.algorithm, length: vector.length}; + var algorithmParams = {name: vector.algorithm, outputLength: vector.outputLength}; if (vector.customization !== undefined) { algorithmParams.customization = vector.customization; } @@ -363,7 +363,7 @@ function run_test() { return importVectorKeys(vector, ["verify", "sign"]) .then(function(vectors) { promise_test(function(test) { - var algorithmParams = {name: vector.algorithm, length: vector.length}; + var algorithmParams = {name: vector.algorithm, outputLength: vector.outputLength}; if (vector.customization !== undefined) { algorithmParams.customization = vector.customization; } @@ -401,7 +401,7 @@ function run_test() { return importVectorKeys(vector, ["verify", "sign"]) .then(function(vector) { promise_test(function(test) { - var algorithmParams = {name: vector.algorithm, length: vector.length}; + var algorithmParams = {name: vector.algorithm, outputLength: vector.outputLength}; if (vector.customization !== undefined) { algorithmParams.customization = vector.customization; } @@ -438,7 +438,7 @@ function run_test() { var plaintext = copyBuffer(vector.plaintext); plaintext[0] = 255 - plaintext[0]; promise_test(function(test) { - var algorithmParams = {name: vector.algorithm, length: vector.length}; + var algorithmParams = {name: vector.algorithm, outputLength: vector.outputLength}; if (vector.customization !== undefined) { algorithmParams.customization = vector.customization; } @@ -470,7 +470,7 @@ function run_test() { var signature = copyBuffer(vector.signature); signature[0] = 255 - signature[0]; promise_test(function(test) { - var algorithmParams = {name: vector.algorithm, length: vector.length}; + var algorithmParams = {name: vector.algorithm, outputLength: vector.outputLength}; if (vector.customization !== undefined) { algorithmParams.customization = vector.customization; } @@ -501,7 +501,7 @@ function run_test() { .then(function(vector) { var signature = vector.signature.slice(1); // Drop first byte promise_test(function(test) { - var algorithmParams = {name: vector.algorithm, length: vector.length}; + var algorithmParams = {name: vector.algorithm, outputLength: vector.outputLength}; if (vector.customization !== undefined) { algorithmParams.customization = vector.customization; } @@ -531,8 +531,8 @@ function run_test() { var promise = importVectorKeys(vector, ["verify", "sign"]) .then(function(vector) { promise_test(function(test) { - var differentLength = vector.length === 256 ? 512 : 256; - var algorithmParams = {name: vector.algorithm, length: differentLength}; + var differentLength = vector.outputLength === 256 ? 512 : 256; + var algorithmParams = {name: vector.algorithm, outputLength: differentLength}; if (vector.customization !== undefined) { algorithmParams.customization = vector.customization; } diff --git a/test/fixtures/wpt/WebCryptoAPI/sign_verify/kmac_vectors.js b/test/fixtures/wpt/WebCryptoAPI/sign_verify/kmac_vectors.js index 39d0efe71ba3e2..6b35cab168c9c6 100644 --- a/test/fixtures/wpt/WebCryptoAPI/sign_verify/kmac_vectors.js +++ b/test/fixtures/wpt/WebCryptoAPI/sign_verify/kmac_vectors.js @@ -6,7 +6,7 @@ function getTestVectors() { // Sample #1 - KMAC128, no customization name: "KMAC128 with no customization", algorithm: "KMAC128", - length: 256, + outputLength: 256, keyBuffer: new Uint8Array([ 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, @@ -25,7 +25,7 @@ function getTestVectors() { // Sample #2 - KMAC128, with customization name: "KMAC128 with customization", algorithm: "KMAC128", - length: 256, + outputLength: 256, keyBuffer: new Uint8Array([ 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, @@ -47,7 +47,7 @@ function getTestVectors() { // Sample #3 - KMAC128, large data, with customization name: "KMAC128 with large data and customization", algorithm: "KMAC128", - length: 256, + outputLength: 256, keyBuffer: new Uint8Array([ 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, @@ -69,7 +69,7 @@ function getTestVectors() { // Sample #4 - KMAC256, with customization, 512-bit output name: "KMAC256 with customization and 512-bit output", algorithm: "KMAC256", - length: 512, + outputLength: 512, keyBuffer: new Uint8Array([ 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, @@ -94,7 +94,7 @@ function getTestVectors() { // Sample #5 - KMAC256, large data, no customization, 512-bit output name: "KMAC256 with large data and no customization", algorithm: "KMAC256", - length: 512, + outputLength: 512, keyBuffer: new Uint8Array([ 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, @@ -116,7 +116,7 @@ function getTestVectors() { // Sample #6 - KMAC256, large data, with customization, 512-bit output name: "KMAC256 with large data and customization", algorithm: "KMAC256", - length: 512, + outputLength: 512, keyBuffer: new Uint8Array([ 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, diff --git a/test/fixtures/wpt/WebCryptoAPI/supports-modern.tentative.https.any.js b/test/fixtures/wpt/WebCryptoAPI/supports-modern.tentative.https.any.js new file mode 100644 index 00000000000000..ca23b782317762 --- /dev/null +++ b/test/fixtures/wpt/WebCryptoAPI/supports-modern.tentative.https.any.js @@ -0,0 +1,160 @@ +// META: title=WebCrypto API: supports method tests for algorithms in https://wicg.github.io/webcrypto-modern-algos/ +// META: script=util/helpers.js + +'use strict'; + +const modernAlgorithms = { + // Asymmetric algorithms + 'ML-DSA-44': { + operations: ['generateKey', 'importKey', 'sign', 'verify', 'getPublicKey'], + }, + 'ML-DSA-65': { + operations: ['generateKey', 'importKey', 'sign', 'verify', 'getPublicKey'], + }, + 'ML-DSA-87': { + operations: ['generateKey', 'importKey', 'sign', 'verify', 'getPublicKey'], + }, + 'ML-KEM-512': { + operations: [ + 'generateKey', 'importKey', 'encapsulateKey', 'encapsulateBits', + 'decapsulateKey', 'decapsulateBits', 'getPublicKey' + ], + }, + 'ML-KEM-768': { + operations: [ + 'generateKey', 'importKey', 'encapsulateKey', 'encapsulateBits', + 'decapsulateKey', 'decapsulateBits', 'getPublicKey' + ], + }, + 'ML-KEM-1024': { + operations: [ + 'generateKey', 'importKey', 'encapsulateKey', 'encapsulateBits', + 'decapsulateKey', 'decapsulateBits', 'getPublicKey' + ], + }, + + // Symmetric algorithms + 'ChaCha20-Poly1305': { + operations: ['generateKey', 'importKey', 'encrypt', 'decrypt'], + encryptParams: {name: 'ChaCha20-Poly1305', iv: new Uint8Array(12)}, + }, + +}; + +const operations = [ + 'generateKey', + 'importKey', + 'sign', + 'verify', + 'encrypt', + 'decrypt', + 'deriveBits', + 'digest', + 'encapsulateKey', + 'encapsulateBits', + 'decapsulateKey', + 'decapsulateBits', + 'getPublicKey', +]; + +// Test that supports method exists and is a static method +test(() => { + assert_true( + typeof SubtleCrypto.supports === 'function', + 'SubtleCrypto.supports should be a function'); +}, 'SubtleCrypto.supports method exists'); + + +// Test standard WebCrypto algorithms for requested operations +for (const [algorithmName, algorithmInfo] of Object.entries(modernAlgorithms)) { + for (const operation of operations) { + promise_test(async (t) => { + const isSupported = algorithmInfo.operations.includes(operation); + + // Use appropriate algorithm parameters for each operation + let algorithm; + let lengthOrAdditionalAlgorithm; + switch (operation) { + case 'generateKey': + algorithm = algorithmInfo.keyGenParams || algorithmName; + break; + case 'importKey': + algorithm = algorithmInfo.importParams || algorithmName; + break; + case 'sign': + case 'verify': + algorithm = algorithmInfo.signParams || algorithmName; + break; + case 'encrypt': + case 'decrypt': + algorithm = algorithmInfo.encryptParams || algorithmName; + break; + case 'deriveBits': + algorithm = algorithmInfo.deriveBitsParams || algorithmName; + if (algorithm?.public instanceof Promise) { + algorithm.public = (await algorithm.public).publicKey; + } + if (algorithmName === 'PBKDF2' || algorithmName === 'HKDF') { + lengthOrAdditionalAlgorithm = 256; + } + break; + case 'digest': + algorithm = algorithmName; + break; + case 'encapsulateKey': + case 'encapsulateBits': + case 'decapsulateKey': + case 'decapsulateBits': + algorithm = algorithmName; + if (operation === 'encapsulateKey' || operation === 'decapsulateKey') { + lengthOrAdditionalAlgorithm = { name: 'AES-GCM', length: 256 }; + } + break; + default: + algorithm = algorithmName; + } + + const result = SubtleCrypto.supports(operation, algorithm, lengthOrAdditionalAlgorithm); + + if (isSupported) { + assert_true(result, `${algorithmName} should support ${operation}`); + } else { + assert_false( + result, `${algorithmName} should not support ${operation}`); + } + }, `supports(${operation}, ${algorithmName})`); + } +} + +// Test some algorithm objects with valid parameters +test(() => { + assert_true( + SubtleCrypto.supports('encrypt', { + name: 'ChaCha20-Poly1305', + iv: new Uint8Array(12), + tagLength: 128, + }), + 'ChaCha20-Poly1305 encrypt with valid tagLength'); +}, 'supports returns true for algorithm objects with valid parameters'); + +// Test some algorithm objects with invalid parameters +test(() => { + assert_false( + SubtleCrypto.supports('encrypt', { + name: 'ChaCha20-Poly1305', + iv: new Uint8Array(12), + tagLength: 100, + }), + 'ChaCha20-Poly1305 encrypt with invalid tagLength'); + + assert_false( + SubtleCrypto.supports('encrypt', { + name: 'ChaCha20-Poly1305', + iv: new Uint8Array(10), + tagLength: 128, + }), + 'ChaCha20-Poly1305 encrypt with invalid iv'); +}, 'supports returns false for algorithm objects with invalid parameters'); + + +done(); diff --git a/test/fixtures/wpt/WebCryptoAPI/supports.tentative.https.any.js b/test/fixtures/wpt/WebCryptoAPI/supports.tentative.https.any.js index dd39273e4b918b..ac3a32c741000c 100644 --- a/test/fixtures/wpt/WebCryptoAPI/supports.tentative.https.any.js +++ b/test/fixtures/wpt/WebCryptoAPI/supports.tentative.https.any.js @@ -6,7 +6,7 @@ const standardAlgorithms = { // Asymmetric algorithms 'RSASSA-PKCS1-v1_5': { - operations: ['generateKey', 'importKey', 'sign', 'verify'], + operations: ['generateKey', 'importKey', 'sign', 'verify', 'getPublicKey'], keyGenParams: { name: 'RSASSA-PKCS1-v1_5', modulusLength: 2048, @@ -17,7 +17,7 @@ const standardAlgorithms = { signParams: { name: 'RSASSA-PKCS1-v1_5' }, }, 'RSA-PSS': { - operations: ['generateKey', 'importKey', 'sign', 'verify'], + operations: ['generateKey', 'importKey', 'sign', 'verify', 'getPublicKey'], keyGenParams: { name: 'RSA-PSS', modulusLength: 2048, @@ -28,7 +28,7 @@ const standardAlgorithms = { signParams: { name: 'RSA-PSS', saltLength: 32 }, }, 'RSA-OAEP': { - operations: ['generateKey', 'importKey', 'encrypt', 'decrypt'], + operations: ['generateKey', 'importKey', 'encrypt', 'decrypt', 'getPublicKey'], keyGenParams: { name: 'RSA-OAEP', modulusLength: 2048, @@ -39,13 +39,13 @@ const standardAlgorithms = { encryptParams: { name: 'RSA-OAEP' }, }, ECDSA: { - operations: ['generateKey', 'importKey', 'sign', 'verify'], + operations: ['generateKey', 'importKey', 'sign', 'verify', 'getPublicKey'], keyGenParams: { name: 'ECDSA', namedCurve: 'P-256' }, importParams: { name: 'ECDSA', namedCurve: 'P-256' }, signParams: { name: 'ECDSA', hash: 'SHA-256' }, }, ECDH: { - operations: ['generateKey', 'importKey', 'deriveBits'], + operations: ['generateKey', 'importKey', 'deriveBits', 'getPublicKey'], keyGenParams: { name: 'ECDH', namedCurve: 'P-256' }, importParams: { name: 'ECDH', namedCurve: 'P-256' }, deriveBitsParams: { @@ -58,12 +58,12 @@ const standardAlgorithms = { }, }, Ed25519: { - operations: ['generateKey', 'importKey', 'sign', 'verify'], + operations: ['generateKey', 'importKey', 'sign', 'verify', 'getPublicKey'], keyGenParams: null, signParams: { name: 'Ed25519' }, }, X25519: { - operations: ['generateKey', 'importKey', 'deriveBits'], + operations: ['generateKey', 'importKey', 'deriveBits', 'getPublicKey'], keyGenParams: null, deriveBitsParams: { name: 'X25519', @@ -152,6 +152,7 @@ const operations = [ 'decrypt', 'deriveBits', 'digest', + 'getPublicKey', ]; // Test that supports method exists and is a static method @@ -268,6 +269,137 @@ test(() => { }), 'Invalid hash parameter should return false' ); + assert_false( + SubtleCrypto.supports( + 'encrypt', {name: 'AES-CBC', iv: new Uint8Array(10)}), + 'Invalid IV for AES-CBC should return false'); + assert_false( + SubtleCrypto.supports('encrypt', { + name: 'AES-CTR', + counter: new Uint8Array(10), + length: 128, + }), + 'Invalid IV for AES-CTR should return false'); + assert_false( + SubtleCrypto.supports('encrypt', { + name: 'AES-CTR', + counter: new Uint8Array(16), + length: 0, + }), + 'Invalid length=0 for AES-CTR should return false'); + assert_false( + SubtleCrypto.supports('encrypt', { + name: 'AES-CTR', + counter: new Uint8Array(16), + length: 129, + }), + 'Invalid length=129 for AES-CTR should return false'); + assert_false( + SubtleCrypto.supports('encrypt', { + name: 'AES-GCM', + iv: new Uint8Array(16), + tagLength: 100, + }), + 'Invalid tag length for AES-GCM should return false'); + assert_false( + SubtleCrypto.supports('generateKey', {name: 'ECDH', namedCurve: 'P-51'}), + 'Invalid curve for ECDH should return false'); + assert_false( + SubtleCrypto.supports( + 'deriveBits', { + name: 'HKDF', + hash: 'SHA-25', + salt: new Uint8Array(16), + info: new Uint8Array(0), + }, + 8), + 'Invalid hash for HKDF should return false'); + assert_false( + SubtleCrypto.supports( + 'deriveBits', { + name: 'HKDF', + hash: 'SHA-256', + salt: new Uint8Array(16), + info: new Uint8Array(0), + }, + 11), + 'Invalid length for HKDF should return false'); + assert_false( + SubtleCrypto.supports( + 'deriveBits', { + name: 'HKDF', + hash: 'SHA-256', + salt: new Uint8Array(16), + info: new Uint8Array(0), + }), + 'null length for HKDF should return false'); + assert_false( + SubtleCrypto.supports('generateKey', { + name: 'HMAC', + hash: 'SHA-25', + }), + 'Invalid hash for HMAC should return false'); + assert_false( + SubtleCrypto.supports('generateKey', { + name: 'HMAC', + hash: 'SHA-256', + length: 0, + }), + 'Invalid length for HMAC should return false'); + assert_false( + SubtleCrypto.supports( + 'deriveBits', { + name: 'PBKDF2', + hash: 'SHA-25', + salt: new Uint8Array(16), + iterations: 100000, + }, + 8), + 'Invalid hash for PBKDF2 should return false'); + assert_false( + SubtleCrypto.supports( + 'deriveBits', { + name: 'PBKDF2', + hash: 'SHA-256', + salt: new Uint8Array(16), + iterations: 100000, + }, + 11), + 'Invalid length for PBKDF2 should return false'); + assert_false( + SubtleCrypto.supports( + 'deriveBits', { + name: 'PBKDF2', + hash: 'SHA-256', + salt: new Uint8Array(16), + iterations: 100000, + }), + 'null length for PBKDF2 should return false'); + assert_false( + SubtleCrypto.supports('generateKey', { + name: 'RSASSA-PKCS1-v1_5', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-56', + }), + 'Invalid hash for RSA PKCS1 should return false'); + assert_false( + SubtleCrypto.supports('generateKey', { + name: 'RSA-PSS', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-56', + }), + 'Invalid hash for RSA PSS should return false'); + assert_false( + SubtleCrypto.supports('generateKey', { + name: 'RSA-OAEP', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-26', + }), + 'Invalid hash for RSA OAEP should return false'); + }, 'supports returns false for algorithm objects with invalid parameters'); // Test some specific combinations that should work @@ -394,4 +526,49 @@ test(() => { ); }, 'Invalid algorithm and operation combinations fail'); +// Test supports for deriveKey op +test(() => { + assert_true( + SubtleCrypto.supports( + 'deriveKey', { + name: 'HKDF', + hash: 'SHA-256', + salt: new Uint8Array(16), + info: new Uint8Array(0), + }, + {name: 'HMAC', hash: 'SHA-256'}), + + 'deriveKey HKDF-HMAC should pass'); +}, 'deriveKey tests'); + +promise_test(async (t) => { + let keypair = await crypto.subtle.generateKey( + { + name: 'X25519', + }, + false, ['deriveKey', 'deriveBits']); + + assert_true( + SubtleCrypto.supports( + 'deriveKey', { + name: 'X25519', + public: keypair.publicKey, + }, + {name: 'AES-GCM', length: 256}), + + 'deriveKey X25519-AES-GCM-256 should pass'); + + assert_false( + SubtleCrypto.supports( + 'deriveKey', { + name: 'X25519', + public: keypair.publicKey, + }, + {name: 'HMAC', hash: 'SHA-256'}), + + 'deriveKey X25519-HMAC-SHA-256 should fail'); +}, 'deriveKey promise tests'); + + + done(); diff --git a/test/fixtures/wpt/versions.json b/test/fixtures/wpt/versions.json index 440400cfe03767..bb3bb4d731d98d 100644 --- a/test/fixtures/wpt/versions.json +++ b/test/fixtures/wpt/versions.json @@ -96,7 +96,7 @@ "path": "web-locks" }, "WebCryptoAPI": { - "commit": "97bbc7247a16231f4744a47a1d9b3d29633d5292", + "commit": "03a14768441c1ce2a31678d0dc080e81dc449d0a", "path": "WebCryptoAPI" }, "webidl": { diff --git a/test/parallel/test-crypto-key-objects-raw.js b/test/parallel/test-crypto-key-objects-raw.js index e0871c2f0cd3fd..e8d5aced4e22cb 100644 --- a/test/parallel/test-crypto-key-objects-raw.js +++ b/test/parallel/test-crypto-key-objects-raw.js @@ -154,7 +154,11 @@ if (hasOpenSSL(3, 5)) { 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: 'raw-public', asymmetricKeyType: 'dh', + }), { code: 'ERR_INVALID_ARG_VALUE' }); + + for (const format of ['raw-private', 'raw-seed']) { assert.throws(() => crypto.createPrivateKey({ key: Buffer.alloc(32), format, asymmetricKeyType: 'dh', }), { @@ -326,6 +330,12 @@ if (hasOpenSSL(3, 5)) { fixtures.readKey('ec_p256_private.pem', 'ascii')); assert.throws(() => ecPriv.export({ format: 'raw-seed' }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); + assert.throws(() => crypto.createPrivateKey({ + key: ecPriv.export({ format: 'raw-private' }), + format: 'raw-seed', + asymmetricKeyType: 'ec', + namedCurve: 'P-256', + }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); if (process.features.openssl_is_boringssl) { common.printSkipMessage('Skipping unsupported ed448/x448 test cases'); @@ -337,6 +347,11 @@ if (hasOpenSSL(3, 5)) { fixtures.readKey(`${type}_private.pem`, 'ascii')); assert.throws(() => priv.export({ format: 'raw-seed' }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); + assert.throws(() => crypto.createPrivateKey({ + key: priv.export({ format: 'raw-private' }), + format: 'raw-seed', + asymmetricKeyType: type, + }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); } if (hasOpenSSL(3, 5)) { @@ -344,6 +359,11 @@ if (hasOpenSSL(3, 5)) { fixtures.readKey('slh_dsa_sha2_128f_private.pem', 'ascii')); assert.throws(() => slhPriv.export({ format: 'raw-seed' }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); + assert.throws(() => crypto.createPrivateKey({ + key: slhPriv.export({ format: 'raw-private' }), + format: 'raw-seed', + asymmetricKeyType: 'slh-dsa-sha2-128f', + }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); } } @@ -354,6 +374,11 @@ if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { fixtures.readKey(`${type.replaceAll('-', '_')}_private_seed_only.pem`, 'ascii')); assert.throws(() => priv.export({ format: 'raw-private' }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); + assert.throws(() => crypto.createPrivateKey({ + key: priv.export({ format: 'raw-seed' }), + format: 'raw-private', + asymmetricKeyType: type, + }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); } } diff --git a/test/parallel/test-crypto-key-objects-to-crypto-key.js b/test/parallel/test-crypto-key-objects-to-crypto-key.js index 5c3148647324b0..a498df144f3350 100644 --- a/test/parallel/test-crypto-key-objects-to-crypto-key.js +++ b/test/parallel/test-crypto-key-objects-to-crypto-key.js @@ -1,11 +1,10 @@ 'use strict'; +// Flags: --expose-internals const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL } = require('../common/crypto'); - const assert = require('assert'); const { createSecretKey, @@ -13,6 +12,29 @@ const { randomBytes, generateKeyPairSync, } = require('crypto'); +const { kSupportedAlgorithms } = require('internal/crypto/util'); + +const hashes = Object.keys(kSupportedAlgorithms.digest).filter((name) => { + return name.startsWith('SHA-') || name.startsWith('SHA3-'); +}); +const namedCurves = ['P-256', 'P-384', 'P-521']; + +const signingUsages = { + public: ['verify'], + private: ['sign'], +}; +const derivationUsages = { + public: [], + private: ['deriveBits'], +}; +const rsaOaepUsages = { + public: ['encrypt', 'wrapKey'], + private: ['decrypt', 'unwrapKey'], +}; +const mlKemUsages = { + public: ['encapsulateBits'], + private: ['decapsulateBits'], +}; function assertCryptoKey(cryptoKey, keyObject, algorithm, extractable, usages) { assert.strictEqual(cryptoKey instanceof CryptoKey, true); @@ -23,229 +45,346 @@ function assertCryptoKey(cryptoKey, keyObject, algorithm, extractable, usages) { assert.strictEqual(keyObject.equals(KeyObject.from(cryptoKey)), true); } -{ +function algorithmName(algorithm) { + return typeof algorithm === 'string' ? algorithm : algorithm.name; +} + +function getUsages(keyObject, usages) { + return keyObject.type === 'public' ? usages.public : usages.private; +} + +function secretKey(length) { + return createSecretKey(randomBytes(length >> 3)); +} + +function vector( + keyObject, + algorithm, + usages, + expected, + extractable, +) { + return { + keyObject, + algorithm, + usages, + expected, + extractable, + }; +} + +function assertVector(vector, extractable) { + const { keyObject, algorithm, usages, expected } = vector; + const name = algorithmName(algorithm); + + const cryptoKey = keyObject.toCryptoKey(algorithm, extractable, usages); + assertCryptoKey(cryptoKey, keyObject, name, extractable, usages); + if (expected !== undefined && 'length' in expected) + assert.strictEqual(cryptoKey.algorithm.length, expected.length); + if (expected !== undefined && 'hash' in expected) + assert.strictEqual(cryptoKey.algorithm.hash.name, expected.hash); + if (expected !== undefined && 'namedCurve' in expected) + assert.strictEqual(cryptoKey.algorithm.namedCurve, expected.namedCurve); +} + +function runVector(vector) { + if (vector.extractable !== undefined) { + assertVector(vector, vector.extractable); + return; + } + assertVector(vector, true); + assertVector(vector, false); +} + +function symmetricVectors(name, usages) { + const vectors = []; for (const length of [128, 192, 256]) { - const key = createSecretKey(randomBytes(length >> 3)); - const algorithms = ['AES-CTR', 'AES-CBC', 'AES-GCM', 'AES-KW']; - if (length === 256) - algorithms.push('ChaCha20-Poly1305'); - - for (const algorithm of algorithms) { - const usages = algorithm === 'AES-KW' ? ['wrapKey', 'unwrapKey'] : ['encrypt', 'decrypt']; - for (const extractable of [true, false]) { - const cryptoKey = key.toCryptoKey(algorithm, extractable, usages); - assertCryptoKey(cryptoKey, key, algorithm, extractable, usages); - assert.strictEqual(cryptoKey.algorithm.length, algorithm !== 'ChaCha20-Poly1305' ? length : undefined); - } - } + vectors.push(vector( + secretKey(length), + name, + usages, + { length })); } + return vectors; } -{ - const pbkdf2 = createSecretKey(randomBytes(16)); - const algorithm = 'PBKDF2'; - const usages = ['deriveBits']; - assert.throws(() => pbkdf2.toCryptoKey(algorithm, true, usages), { - name: 'SyntaxError', - message: 'PBKDF2 keys are not extractable' - }); - assert.throws(() => pbkdf2.toCryptoKey(algorithm, false, ['wrapKey']), { - name: 'SyntaxError', - message: 'Unsupported key usage for a PBKDF2 key' - }); - const cryptoKey = pbkdf2.toCryptoKey(algorithm, false, usages); - assertCryptoKey(cryptoKey, pbkdf2, algorithm, false, usages); - assert.strictEqual(cryptoKey.algorithm.length, undefined); +function chacha20Poly1305Vectors() { + return [ + vector( + secretKey(256), + 'ChaCha20-Poly1305', + ['encrypt', 'decrypt'], + { length: undefined }), + ]; } -{ - for (const length of [128, 192, 256]) { - const hmac = createSecretKey(randomBytes(length >> 3)); - const algorithm = 'HMAC'; - const usages = ['sign', 'verify']; +function genericSecretVectors(name) { + return [ + vector( + createSecretKey(randomBytes(16)), + name, + ['deriveBits'], + undefined, + false), + ]; +} + +function macInvalid(algorithm, invalidLengthMessage, allowZeroKey = false) { + const key = createSecretKey(randomBytes(32)); + const usages = ['sign', 'verify']; + + if (allowZeroKey) { + const zeroKey = createSecretKey(Buffer.alloc(0)) + .toCryptoKey(algorithm, true, usages); + assert.strictEqual(zeroKey.algorithm.length, 0); + const explicitZeroKey = createSecretKey(Buffer.alloc(0)) + .toCryptoKey({ ...algorithm, length: 0 }, true, usages); + assert.strictEqual(explicitZeroKey.algorithm.length, 0); + } else { assert.throws(() => { - createSecretKey(Buffer.alloc(0)).toCryptoKey({ name: algorithm, hash: 'SHA-256' }, true, usages); + createSecretKey(Buffer.alloc(0)).toCryptoKey(algorithm, true, usages); }, { name: 'DataError', message: 'Zero-length key is not supported', }); + } - assert.throws(() => { - hmac.toCryptoKey({ - name: algorithm, - hash: 'SHA-256', - }, true, []); - }, { - name: 'SyntaxError', - message: 'Usages cannot be empty when importing a secret key.' - }); + assert.throws(() => key.toCryptoKey(algorithm, true, []), { + name: 'SyntaxError', + message: 'Usages cannot be empty when importing a secret key.' + }); - for (const hash of ['SHA-1', 'SHA-256', 'SHA-384', 'SHA-512']) { - for (const extractable of [true, false]) { - assert.throws(() => { - hmac.toCryptoKey({ name: algorithm, hash: 'SHA-256', length: 0 }, true, usages); - }, { - name: 'DataError', - message: 'HmacImportParams.length cannot be 0', - }); - const cryptoKey = hmac.toCryptoKey({ name: algorithm, hash }, extractable, usages); - assertCryptoKey(cryptoKey, hmac, algorithm, extractable, usages); - assert.strictEqual(cryptoKey.algorithm.length, length); - } + assert.throws(() => { + key.toCryptoKey({ ...algorithm, length: 0 }, true, usages); + }, { + name: 'DataError', + message: invalidLengthMessage, + }); +} + +function hmacVectors() { + const vectors = []; + for (const length of [128, 192, 256]) { + const key = secretKey(length); + for (const hash of hashes) { + vectors.push(vector( + key, + { name: 'HMAC', hash }, + ['sign', 'verify'], + { length })); } } + return vectors; } -{ - const algorithms = ['Ed25519', 'X25519']; +function kmacVectors(name) { + return [ + vector( + secretKey(256), + { name }, + ['sign', 'verify'], + { length: 256 }), + ]; +} - if (!process.features.openssl_is_boringssl) { - algorithms.push('X448', 'Ed448'); - } else { - common.printSkipMessage('Skipping unsupported Ed448/X448 test cases'); +function asymmetricVectors(keyPair, algorithm, usagesByType, expected) { + const { publicKey, privateKey } = keyPair; + const vectors = []; + for (const keyObject of [publicKey, privateKey]) { + vectors.push(vector( + keyObject, + algorithm, + getUsages(keyObject, usagesByType), + expected)); } + return vectors; +} - for (const algorithm of algorithms) { - const { publicKey, privateKey } = generateKeyPairSync(algorithm.toLowerCase()); - assert.throws(() => { - publicKey.toCryptoKey(algorithm === 'Ed25519' ? 'X25519' : 'Ed25519', true, []); - }, { - name: 'DataError', - message: 'Invalid key type' - }); - for (const key of [publicKey, privateKey]) { - let usages; - if (algorithm.startsWith('E')) { - usages = key.type === 'public' ? ['verify'] : ['sign']; - } else { - usages = key.type === 'public' ? [] : ['deriveBits']; - } - for (const extractable of [true, false]) { - const cryptoKey = key.toCryptoKey(algorithm, extractable, usages); - assertCryptoKey(cryptoKey, key, algorithm, extractable, usages); - } +function rsaVectors(name, usagesByType) { + const keyPair = generateKeyPairSync('rsa', { modulusLength: 2048 }); + const vectors = []; + for (const keyObject of [keyPair.publicKey, keyPair.privateKey]) { + for (const hash of hashes) { + vectors.push(vector( + keyObject, + { name, hash }, + getUsages(keyObject, usagesByType), + { hash })); } } + return vectors; } -{ - const { publicKey, privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 }); - for (const key of [publicKey, privateKey]) { - for (const algorithm of ['RSASSA-PKCS1-v1_5', 'RSA-PSS', 'RSA-OAEP']) { - let usages; - if (algorithm === 'RSA-OAEP') { - usages = key.type === 'public' ? ['encrypt', 'wrapKey'] : ['decrypt', 'unwrapKey']; - } else { - usages = key.type === 'public' ? ['verify'] : ['sign']; - } - for (const extractable of [true, false]) { - for (const hash of ['SHA-1', 'SHA-256', 'SHA-384', 'SHA-512']) { - const cryptoKey = key.toCryptoKey({ - name: algorithm, - hash - }, extractable, usages); - assertCryptoKey(cryptoKey, key, algorithm, extractable, usages); - assert.strictEqual(cryptoKey.algorithm.hash.name, hash); - } - } +function ecVectors(name, usagesByType) { + const vectors = []; + for (const namedCurve of namedCurves) { + const keyPair = generateKeyPairSync('ec', { namedCurve }); + for (const keyObject of [keyPair.publicKey, keyPair.privateKey]) { + vectors.push(vector( + keyObject, + { name, namedCurve }, + getUsages(keyObject, usagesByType), + { namedCurve })); } } + return vectors; } -{ - for (const namedCurve of ['P-256', 'P-384', 'P-521']) { - const { publicKey, privateKey } = generateKeyPairSync('ec', { namedCurve }); - assert.throws(() => { - privateKey.toCryptoKey({ - name: 'ECDH', - namedCurve, - }, true, []); - }, { - name: 'SyntaxError', - message: 'Usages cannot be empty when importing a private key.' - }); - assert.throws(() => { - publicKey.toCryptoKey({ - name: 'ECDH', - namedCurve: namedCurve === 'P-256' ? 'P-384' : 'P-256' - }, true, []); - }, { - name: 'DataError', - message: 'Named curve mismatch' - }); - for (const key of [publicKey, privateKey]) { - for (const algorithm of ['ECDH', 'ECDSA']) { - let usages; - if (algorithm === 'ECDH') { - usages = key.type === 'public' ? [] : ['deriveBits']; - } else { - usages = key.type === 'public' ? ['verify'] : ['sign']; - } - for (const extractable of [true, false]) { - const cryptoKey = key.toCryptoKey({ - name: algorithm, - namedCurve - }, extractable, usages); - assertCryptoKey(cryptoKey, key, algorithm, extractable, usages); - assert.strictEqual(cryptoKey.algorithm.namedCurve, namedCurve); - } - } - } +function cfrgVectors(name, usagesByType) { + const keyPair = generateKeyPairSync(name.toLowerCase()); + return asymmetricVectors(keyPair, name, usagesByType); +} + +function simpleAsymmetricVectors(name, usagesByType) { + const keyPair = generateKeyPairSync(name.toLowerCase()); + return asymmetricVectors(keyPair, { name }, usagesByType); +} + +function genericSecretInvalid(name) { + const key = createSecretKey(randomBytes(16)); + assert.throws(() => key.toCryptoKey(name, true, ['deriveBits']), { + name: 'SyntaxError', + message: `${name} keys are not extractable` + }); + assert.throws(() => key.toCryptoKey(name, false, ['wrapKey']), { + name: 'SyntaxError', + message: `Unsupported key usage for a ${name} key` + }); +} + +function ecdhInvalid() { + const { publicKey, privateKey } = generateKeyPairSync('ec', { + namedCurve: 'P-256', + }); + + assert.throws(() => { + privateKey.toCryptoKey({ + name: 'ECDH', + namedCurve: 'P-256', + }, true, []); + }, { + name: 'SyntaxError', + message: 'Usages cannot be empty when importing a private key.' + }); + + assert.throws(() => { + publicKey.toCryptoKey({ + name: 'ECDH', + namedCurve: 'P-384', + }, true, []); + }, { + name: 'DataError', + message: 'Named curve mismatch' + }); +} + +function invalidAsymmetricKeyType(name, invalidAlgorithm) { + const { publicKey } = generateKeyPairSync(name.toLowerCase()); + assert.throws(() => { + publicKey.toCryptoKey(invalidAlgorithm, true, []); + }, { + name: 'DataError', + message: 'Invalid key type' + }); +} + +function emptyPrivateUsagesInvalid(name) { + const { privateKey } = generateKeyPairSync(name.toLowerCase()); + assert.throws(() => { + privateKey.toCryptoKey(name, true, []); + }, { + name: 'SyntaxError', + message: 'Usages cannot be empty when importing a private key.' + }); +} + +const tests = { + 'AES-KW': symmetricVectors('AES-KW', ['wrapKey', 'unwrapKey']), + 'ECDH': ecVectors('ECDH', derivationUsages), + 'ECDSA': ecVectors('ECDSA', signingUsages), + 'Ed25519': cfrgVectors('Ed25519', signingUsages), + 'HMAC': hmacVectors(), + 'RSA-OAEP': rsaVectors('RSA-OAEP', rsaOaepUsages), + 'RSA-PSS': rsaVectors('RSA-PSS', signingUsages), + 'RSASSA-PKCS1-v1_5': rsaVectors('RSASSA-PKCS1-v1_5', signingUsages), + 'X25519': cfrgVectors('X25519', derivationUsages), +}; + +const invalid = { + 'ECDH': ecdhInvalid, + 'Ed25519': () => invalidAsymmetricKeyType('Ed25519', 'X25519'), + 'HMAC': () => macInvalid( + { name: 'HMAC', hash: 'SHA-256' }, + 'HmacImportParams.length cannot be 0'), + 'X25519': () => invalidAsymmetricKeyType('X25519', 'Ed25519'), +}; + +for (const name of ['AES-CBC', 'AES-CTR', 'AES-GCM', 'AES-OCB']) { + if (name in kSupportedAlgorithms.importKey) + tests[name] = symmetricVectors(name, ['encrypt', 'decrypt']); +} + +if ('ChaCha20-Poly1305' in kSupportedAlgorithms.importKey) + tests['ChaCha20-Poly1305'] = chacha20Poly1305Vectors(); + +for (const name of ['HKDF', 'PBKDF2', 'Argon2d', 'Argon2i', 'Argon2id']) { + if (name in kSupportedAlgorithms.importKey) { + tests[name] = genericSecretVectors(name); + invalid[name] = () => genericSecretInvalid(name); } } -if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { - for (const name of ['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87']) { - const { publicKey, privateKey } = generateKeyPairSync(name.toLowerCase()); - assert.throws(() => { - privateKey.toCryptoKey(name, true, []); - }, { - name: 'SyntaxError', - message: 'Usages cannot be empty when importing a private key.' - }); - for (const key of [publicKey, privateKey]) { - const usages = key.type === 'public' ? ['verify'] : ['sign']; - for (const extractable of [true, false]) { - const cryptoKey = key.toCryptoKey({ name }, extractable, usages); - assertCryptoKey(cryptoKey, key, name, extractable, usages); - assert.strictEqual(cryptoKey.algorithm.name, name); - } - } +for (const name of ['KMAC128', 'KMAC256']) { + if (name in kSupportedAlgorithms.importKey) { + tests[name] = kmacVectors(name); + invalid[name] = () => { + macInvalid({ name }, 'Invalid key length', true); + }; } } -if (hasOpenSSL(3)) { - for (const algorithm of ['KMAC128', 'KMAC256']) { - const hmac = createSecretKey(randomBytes(32)); - const usages = ['sign', 'verify']; +for (const [name, usages, invalidAlgorithm] of [ + ['Ed448', signingUsages, 'X25519'], + ['X448', derivationUsages, 'Ed25519'], +]) { + if (name in kSupportedAlgorithms.importKey) { + tests[name] = cfrgVectors(name, usages); + invalid[name] = () => { + invalidAsymmetricKeyType(name, invalidAlgorithm); + }; + } +} - assert.throws(() => { - createSecretKey(Buffer.alloc(0)).toCryptoKey({ name: algorithm }, true, usages); - }, { - name: 'DataError', - message: 'Zero-length key is not supported', - }); +for (const name of ['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87']) { + if (name in kSupportedAlgorithms.importKey) { + tests[name] = simpleAsymmetricVectors(name, signingUsages); + invalid[name] = () => emptyPrivateUsagesInvalid(name); + } +} - assert.throws(() => { - hmac.toCryptoKey({ - name: algorithm, - }, true, []); - }, { - name: 'SyntaxError', - message: 'Usages cannot be empty when importing a secret key.' - }); +for (const name of ['ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024']) { + if (name in kSupportedAlgorithms.importKey) { + tests[name] = simpleAsymmetricVectors(name, mlKemUsages); + invalid[name] = () => emptyPrivateUsagesInvalid(name); + } +} - for (const extractable of [true, false]) { - assert.throws(() => { - hmac.toCryptoKey({ name: algorithm, length: 0 }, true, usages); - }, { - name: 'DataError', - message: 'KmacImportParams.length cannot be 0', - }); - const cryptoKey = hmac.toCryptoKey({ name: algorithm }, extractable, usages); - assertCryptoKey(cryptoKey, hmac, algorithm, extractable, usages); - assert.strictEqual(cryptoKey.algorithm.length, 256); +const unsupportedToCryptoKeyAlgorithms = new Set(); + +for (const name of Object.keys(kSupportedAlgorithms.importKey)) { + const vectors = tests[name]; + if (vectors === undefined) { + if (!unsupportedToCryptoKeyAlgorithms.has(name)) { + assert.fail( + `${name} needs a tests entry or must be listed in ` + + 'unsupportedToCryptoKeyAlgorithms'); } + continue; } + + for (const vector of vectors) + runVector(vector); + + invalid[name]?.(); } diff --git a/test/parallel/test-crypto-negative-zero.js b/test/parallel/test-crypto-negative-zero.js new file mode 100644 index 00000000000000..0af9220569c374 --- /dev/null +++ b/test/parallel/test-crypto-negative-zero.js @@ -0,0 +1,160 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); +const { hasOpenSSL } = require('../common/crypto'); + +function getOutcome(fn) { + try { + return { result: fn() }; + } catch (err) { + return { err }; + } +} + +function assertSameOutcome(actual, expected) { + if (expected.err !== undefined) { + assert(actual.err instanceof Error); + assert.strictEqual(actual.err.name, expected.err.name); + assert.strictEqual(actual.err.code, expected.err.code); + assert.strictEqual(actual.err.message, expected.err.message); + } else { + assert.deepStrictEqual(actual.result, expected.result); + } +} + +function assertSameErrorOrSuccess(actual, expected) { + if (expected.err !== undefined) { + assert(actual.err instanceof Error); + assert.strictEqual(actual.err.name, expected.err.name); + assert.strictEqual(actual.err.code, expected.err.code); + assert.strictEqual(actual.err.message, expected.err.message); + } else { + assert.strictEqual(actual.err, undefined); + } +} + +{ + const expected = getOutcome(() => + crypto.hkdfSync('sha256', 'key', 'salt', 'info', 0), + ); + assertSameOutcome( + getOutcome(() => crypto.hkdfSync('sha256', 'key', 'salt', 'info', -0)), + expected, + ); + crypto.hkdf('sha256', 'key', 'salt', 'info', -0, + common.mustCall((err, result) => { + assertSameOutcome({ err, result }, expected); + })); +} + +{ + assert.strictEqual( + crypto.checkPrimeSync(Buffer.from([3]), { checks: -0 }), + true, + ); + crypto.checkPrime(Buffer.from([3]), { checks: -0 }, + common.mustSucceed((result) => { + assert.strictEqual(result, true); + })); +} + +{ + assert.throws(() => crypto.createDiffieHellman(-0, 2), { + name: 'Error', + }); +} + +{ + for (const [type, getOptions] of [ + ['rsa', (zero) => ({ modulusLength: zero })], + ['rsa', (zero) => ({ modulusLength: 512, publicExponent: zero })], + ['rsa-pss', (zero) => ({ + modulusLength: 512, + publicExponent: 65537, + saltLength: zero, + })], + ['dsa', (zero) => ({ modulusLength: zero })], + ['dh', (zero) => ({ primeLength: zero })], + ['dh', (zero) => ({ primeLength: 2, generator: zero })], + ]) { + assertSameErrorOrSuccess( + getOutcome(() => crypto.generateKeyPairSync(type, getOptions(-0))), + getOutcome(() => crypto.generateKeyPairSync(type, getOptions(0))), + ); + } + + if (!hasOpenSSL(3)) { + common.printSkipMessage( + 'Skipping DSA divisorLength 0 key generation on OpenSSL 1.1.1'); + } else { + assertSameErrorOrSuccess( + getOutcome(() => crypto.generateKeyPairSync('dsa', { + modulusLength: 512, + divisorLength: -0, + })), + getOutcome(() => crypto.generateKeyPairSync('dsa', { + modulusLength: 512, + divisorLength: 0, + })), + ); + } + + crypto.generateKeyPair('rsa', { modulusLength: -0 }, + common.mustCall((err) => { + assert(err instanceof Error); + })); +} + +if (!process.features.openssl_is_boringssl) { + assert.strictEqual( + crypto.createHash('shake128', { outputLength: -0 }).digest('hex'), + '', + ); + assert.strictEqual( + crypto.createHash('shake128', { outputLength: 5 }) + .copy({ outputLength: -0 }) + .digest('hex'), + '', + ); + assert.strictEqual( + crypto.hash('shake128', 'data', { outputLength: -0 }), + '', + ); +} + +{ + const key = Buffer.alloc(16); + const iv = Buffer.alloc(12); + + assertSameErrorOrSuccess( + getOutcome(() => crypto.createCipheriv( + 'aes-128-gcm', key, iv, { authTagLength: -0 })), + getOutcome(() => crypto.createCipheriv( + 'aes-128-gcm', key, iv, { authTagLength: 0 })), + ); + assertSameErrorOrSuccess( + getOutcome(() => crypto.createCipheriv( + 'aes-128-gcm', key, iv).setAAD( + Buffer.alloc(0), + { plaintextLength: -0 }, + )), + getOutcome(() => crypto.createCipheriv( + 'aes-128-gcm', key, iv).setAAD( + Buffer.alloc(0), + { plaintextLength: 0 }, + )), + ); + assert.strictEqual( + crypto.getCipherInfo('aes-128-cbc', { keyLength: -0 }), + undefined, + ); + assert.strictEqual( + crypto.getCipherInfo('aes-128-cbc', { ivLength: -0 }), + undefined, + ); +} 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 4cc8646cd8b90e..4a143a77d6a3c1 100644 --- a/test/parallel/test-crypto-pqc-key-objects-slh-dsa.js +++ b/test/parallel/test-crypto-pqc-key-objects-slh-dsa.js @@ -91,6 +91,12 @@ for (const asymmetricKeyType of [ key: rawPriv, format: 'raw-private', asymmetricKeyType, }); assert.strictEqual(importedPriv.equals(key), true); + assert.throws(() => createPrivateKey({ + key: rawPriv, format: 'raw-seed', asymmetricKeyType, + }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); + assert.throws(() => createPublicKey({ + key: rawPriv, format: 'raw-seed', asymmetricKeyType, + }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); } if (!hasOpenSSL(3, 5)) { diff --git a/test/parallel/test-webcrypto-derivekey-ecdh.js b/test/parallel/test-webcrypto-derivekey-ecdh.js index 61a284cb8f8dea..1c825b40db7e40 100644 --- a/test/parallel/test-webcrypto-derivekey-ecdh.js +++ b/test/parallel/test-webcrypto-derivekey-ecdh.js @@ -102,6 +102,25 @@ async function prepareKeys() { assert.strictEqual(Buffer.from(raw).toString('hex'), result); } + { + // Non-multiple of 8 derived HMAC key length + const key = await subtle.deriveKey({ + name: 'ECDH', + public: publicKey + }, privateKey, { + name: 'HMAC', + hash: 'SHA-256', + length: 9 + }, true, ['sign', 'verify']); + + const raw = await subtle.exportKey('raw', key); + const expected = Buffer.from(result.slice(0, 4), 'hex'); + expected[1] &= 0b10000000; + + assert.strictEqual(key.algorithm.length, 9); + assert.deepStrictEqual(Buffer.from(raw), expected); + } + { // Case insensitivity const key = await subtle.deriveKey({ diff --git a/test/parallel/test-webcrypto-derivekey.js b/test/parallel/test-webcrypto-derivekey.js index 32c6a93efbbc4e..f9323bca2caf57 100644 --- a/test/parallel/test-webcrypto-derivekey.js +++ b/test/parallel/test-webcrypto-derivekey.js @@ -285,12 +285,19 @@ if (hasOpenSSL(3)) { baseKeyAlgorithm, false, ['deriveKey']); - await assert.rejects( - subtle.deriveKey(algorithm, baseKey, derivedKeyAlgorithm, false, usages), - { - name: 'DataError', - message: /KmacImportParams\.length cannot be 0/, - }); + const derived = await subtle.deriveKey( + algorithm, + baseKey, + derivedKeyAlgorithm, + false, + usages); + assert.strictEqual(derived.algorithm.length, 0); + + const signature = await subtle.sign({ + name: 'KMAC128', + outputLength: 256, + }, derived, new Uint8Array()); + assert.strictEqual(signature.byteLength, 32); } })().then(common.mustCall()); } diff --git a/test/parallel/test-webcrypto-digest.js b/test/parallel/test-webcrypto-digest.js index 4a28d88dcd72c3..8e1b6797ee865b 100644 --- a/test/parallel/test-webcrypto-digest.js +++ b/test/parallel/test-webcrypto-digest.js @@ -9,6 +9,7 @@ const assert = require('assert'); const { Buffer } = require('buffer'); const { subtle } = globalThis.crypto; const { createHash, getHashes } = require('crypto'); +const { hasOpenSSL } = require('../common/crypto'); const kTests = [ ['SHA-1', ['sha1'], 160], @@ -263,9 +264,151 @@ if (getHashes().includes('shake128')) { new Uint8Array(0), ); - await assert.rejects(subtle.digest({ name: 'cSHAKE128', outputLength: 7 }, Buffer.alloc(1)), { - name: 'NotSupportedError', - message: 'Unsupported CShakeParams outputLength', - }); + const digest = await subtle.digest({ name: 'cSHAKE128', outputLength: 7 }, Buffer.alloc(1)); + assert.strictEqual(digest.byteLength, 1); + assert.strictEqual(new Uint8Array(digest)[0] & 0b00000001, 0); + + await assert.rejects( + subtle.digest( + { name: 'cSHAKE128', outputLength: 0xffffffff }, + Buffer.alloc(1)), + { + name: 'OperationError', + message: 'Invalid CShakeParams outputLength', + }); + + await assert.rejects( + subtle.digest( + { + name: 'cSHAKE128', + outputLength: 256, + functionName: Buffer.from('SHAKE'), + }, + Buffer.alloc(1)), + { + name: 'NotSupportedError', + message: 'Unsupported CShakeParams functionName', + }); + + await assert.rejects( + subtle.digest( + { + name: 'cSHAKE128', + outputLength: 256, + customization: Buffer.alloc(513), + }, + Buffer.alloc(1)), + { + name: 'OperationError', + message: 'CShakeParams.customization must be at most 512 bytes', + }); + + if (!hasOpenSSL(3)) return; + + const nistCShakeShortInput = Buffer.from('00010203', 'hex'); + const nistCShakeLongInput = + Buffer.from(Array.from({ length: 200 }, (_, i) => i)); + const nistCShakeSample1 = { + algorithm: { + name: 'cSHAKE128', + outputLength: 256, + customization: Buffer.from('Email Signature'), + }, + data: nistCShakeShortInput, + expected: 'c1c36925b6409a04f1b504fcbca9d82b' + + '4017277cb5ed2b2065fc1d3814d5aaf5', + }; + + for (const { algorithm, data, expected } of [ + nistCShakeSample1, + { + algorithm: { + name: 'cSHAKE128', + outputLength: 256, + customization: Buffer.from('Email Signature'), + }, + data: nistCShakeLongInput, + expected: 'c5221d50e4f822d96a2e8881a961420f' + + '294b7b24fe3d2094baed2c6524cc166b', + }, + { + algorithm: { + name: 'cSHAKE256', + outputLength: 512, + customization: Buffer.from('Email Signature'), + }, + data: nistCShakeShortInput, + expected: 'd008828e2b80ac9d2218ffee1d070c48' + + 'b8e4c87bff32c9699d5b6896eee0edd1' + + '64020e2be0560858d9c00c037e34a96' + + '937c561a74c412bb4c746469527281c8c', + }, + { + algorithm: { + name: 'cSHAKE256', + outputLength: 512, + customization: Buffer.from('Email Signature'), + }, + data: nistCShakeLongInput, + expected: '07dc27b11e51fbac75bc7b3c1d983e8b' + + '4b85fb1defaf218912ac864302730917' + + '27f42b17ed1df63e8ec118f04b23633c' + + '1dfb1574c8fb55cb45da8e25afb092bb', + }, + { + algorithm: { + name: 'cSHAKE128', + outputLength: 312, + functionName: Buffer.from('KMAC'), + customization: Buffer.from( + '`kiEF`&I))7]yq0?*sKa q)[jP`4R=)lV_9tyvT$kAbH$)1}p].' + + 'bbeomb.'), + }, + data: Buffer.from('ca88f708fa', 'hex'), + expected: 'bebb534ccfccd300f731d2911fb4351d' + + '5fcc95ac2509e9abae8f9dc51106e28d' + + '7f25ae11738334', + }, + { + algorithm: { + name: 'cSHAKE256', + outputLength: 264, + functionName: Buffer.from('TupleHash'), + customization: Buffer.from( + 'q8gN}O&V*VDU4Y.^5J13tG2,1^Lw~C2rw $AB3.SX)=@z'), + }, + data: Buffer.from('13d101da', 'hex'), + expected: '43163a57fc1ee8f1c501a2add927698c' + + 'a5a4b52c0d3ef3fd6d91d8d2386765e0ae', + }, + { + algorithm: { + name: 'cSHAKE256', + outputLength: 312, + functionName: Buffer.from('ParallelHash'), + customization: Buffer.from( + 'vD-1>T,f.R*V%ZA[ OyJ'), + }, + data: Buffer.from('d5d7e7517f', 'hex'), + expected: '442be69b2afd7c8282839920a8446aaf' + + '16a5049d3d018eac87e04cf9225870ef' + + 'ca6f88db415829', + }, + ]) { + assert.strictEqual( + Buffer.from(await subtle.digest(algorithm, data)).toString('hex'), + expected); + } + + const truncated = Buffer.from(await subtle.digest( + { ...nistCShakeSample1.algorithm, outputLength: 255 }, + nistCShakeSample1.data)); + const expected = Buffer.from(nistCShakeSample1.expected, 'hex'); + assert.strictEqual(truncated.byteLength, expected.byteLength); + assert.deepStrictEqual(truncated.subarray(0, 31), expected.subarray(0, 31)); + assert.strictEqual(truncated[31] & 0b00000001, 0); + assert.strictEqual(truncated[31] | 0b00000001, expected[31]); })().then(common.mustCall()); } diff --git a/test/parallel/test-webcrypto-encap-decap-ml-kem.js b/test/parallel/test-webcrypto-encap-decap-ml-kem.js index f3850dcdf02546..e31be79e6bfe4b 100644 --- a/test/parallel/test-webcrypto-encap-decap-ml-kem.js +++ b/test/parallel/test-webcrypto-encap-decap-ml-kem.js @@ -1,3 +1,4 @@ +// Flags: --expose-internals 'use strict'; const common = require('../common'); @@ -12,11 +13,16 @@ if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) const assert = require('assert'); const crypto = require('crypto'); +const { getCryptoKeyHandle } = require('internal/crypto/keys'); const { KeyObject } = crypto; const { subtle } = globalThis.crypto; const vectors = require('../fixtures/crypto/ml-kem')(); +function getCryptoKeyData(key) { + return getCryptoKeyHandle(key).export(); +} + async function testEncapsulateKey({ name, publicKeyPem, privateKeyPem, results }) { const [ publicKey, @@ -65,6 +71,19 @@ async function testEncapsulateKey({ name, publicKeyPem, privateKeyPem, results } assert.strictEqual(encapsulated2.sharedKey.algorithm.name, 'HMAC'); assert.strictEqual(encapsulated2.sharedKey.extractable, false); + const encapsulated3 = await subtle.encapsulateKey( + { name }, + publicKey, + { name: 'HMAC', hash: 'SHA-256', length: 255 }, + false, + ['sign', 'verify'] + ); + + assert.strictEqual(encapsulated3.sharedKey.algorithm.name, 'HMAC'); + assert.strictEqual(encapsulated3.sharedKey.algorithm.length, 255); + assert.strictEqual(getCryptoKeyData(encapsulated3.sharedKey).length, 32); + assert.strictEqual(getCryptoKeyData(encapsulated3.sharedKey)[31] & 0b00000001, 0); + // Test failure when using wrong key type await assert.rejects( subtle.encapsulateKey({ name }, privateKey, 'HKDF', false, ['deriveBits']), { @@ -159,6 +178,19 @@ async function testDecapsulateKey({ name, publicKeyPem, privateKeyPem, results } const decapsulatedKeyData = KeyObject.from(decapsulatedKey).export(); assert(originalKeyData.equals(decapsulatedKeyData)); + const decapsulatedHmac = await subtle.decapsulateKey( + { name }, + privateKey, + encapsulated.ciphertext, + { name: 'HMAC', hash: 'SHA-256', length: 255 }, + false, + ['sign', 'verify'] + ); + assert.strictEqual(decapsulatedHmac.algorithm.name, 'HMAC'); + assert.strictEqual(decapsulatedHmac.algorithm.length, 255); + assert.strictEqual(getCryptoKeyData(decapsulatedHmac).length, 32); + assert.strictEqual(getCryptoKeyData(decapsulatedHmac)[31] & 0b00000001, 0); + // Test with test vector ciphertext and expected shared key const vectorDecapsulatedKey = await subtle.decapsulateKey( { name }, diff --git a/test/parallel/test-webcrypto-export-import.js b/test/parallel/test-webcrypto-export-import.js index 6c8f36a62eccb3..c7399c69d9c93f 100644 --- a/test/parallel/test-webcrypto-export-import.js +++ b/test/parallel/test-webcrypto-export-import.js @@ -72,8 +72,8 @@ const { createPrivateKey, createPublicKey, createSecretKey } = require('crypto') hash: 'SHA-256', length: 1 }, false, ['sign', 'verify']), { - name: 'NotSupportedError', - message: 'Unsupported HmacImportParams.length' + name: 'DataError', + message: 'Invalid key length' }); await assert.rejects( subtle.importKey('jwk', null, { @@ -88,6 +88,55 @@ const { createPrivateKey, createPublicKey, createSecretKey } = require('crypto') test().then(common.mustCall()); } +// HMAC non-byte key lengths +{ + async function test() { + const generated = await subtle.generateKey( + { name: 'HMAC', hash: 'SHA-256', length: 9 }, + true, + ['sign', 'verify']); + const generatedRaw = await subtle.exportKey('raw', generated); + assert.strictEqual(generated.algorithm.length, 9); + assert.strictEqual(generatedRaw.byteLength, 2); + assert.strictEqual(new Uint8Array(generatedRaw)[1] & 0b01111111, 0); + + const importedExplicit = await subtle.importKey( + 'raw', + new Uint8Array([0xff, 0xff]), + { name: 'HMAC', hash: 'SHA-256', length: 9 }, + true, + ['sign', 'verify']); + const importedExplicitRaw = await subtle.exportKey('raw', importedExplicit); + assert.strictEqual(importedExplicit.algorithm.length, 9); + assert.deepStrictEqual( + new Uint8Array(importedExplicitRaw), + new Uint8Array([0xff, 0x80])); + + const importedImplicit = await subtle.importKey( + 'raw', + new Uint8Array([0xff, 0xff]), + { name: 'HMAC', hash: 'SHA-256' }, + true, + ['sign', 'verify']); + const importedImplicitRaw = await subtle.exportKey('raw', importedImplicit); + assert.strictEqual(importedImplicit.algorithm.length, 16); + assert.deepStrictEqual( + new Uint8Array(importedImplicitRaw), + new Uint8Array([0xff, 0xff])); + + await assert.rejects( + subtle.importKey( + 'raw', + new Uint8Array([0xff]), + { name: 'HMAC', hash: 'SHA-256', length: 9 }, + true, + ['sign', 'verify']), + { name: 'DataError', message: 'Invalid key length' }); + } + + test().then(common.mustCall()); +} + // Import/Export HMAC Secret Key { async function test() { @@ -230,6 +279,69 @@ if (hasOpenSSL(3)) { true, [/* empty usages */]), { name: 'SyntaxError', message: 'Usages cannot be empty when importing a secret key.' }); + + { + const importedZeroImplicit = await subtle.importKey( + 'raw-secret', + new Uint8Array(), + name, + true, + ['sign', 'verify']); + const importedZeroImplicitRaw = + await subtle.exportKey('raw-secret', importedZeroImplicit); + assert.strictEqual(importedZeroImplicit.algorithm.length, 0); + assert.strictEqual(importedZeroImplicitRaw.byteLength, 0); + + const importedZeroExplicit = await subtle.importKey( + 'raw-secret', + new Uint8Array(), + { name, length: 0 }, + true, + ['sign', 'verify']); + const importedZeroExplicitRaw = + await subtle.exportKey('raw-secret', importedZeroExplicit); + assert.strictEqual(importedZeroExplicit.algorithm.length, 0); + assert.strictEqual(importedZeroExplicitRaw.byteLength, 0); + + await assert.rejects( + subtle.importKey( + 'raw-secret', + new Uint8Array([0xff]), + { name, length: 0 }, + true, + ['sign', 'verify']), + { name: 'DataError', message: 'Invalid key length' }); + + const generated = await subtle.generateKey( + { name, length: 9 }, + true, + ['sign', 'verify']); + const generatedRaw = await subtle.exportKey('raw-secret', generated); + assert.strictEqual(generated.algorithm.length, 9); + assert.strictEqual(generatedRaw.byteLength, 2); + assert.strictEqual(new Uint8Array(generatedRaw)[1] & 0b01111111, 0); + + const importedExplicit = await subtle.importKey( + 'raw-secret', + new Uint8Array([0xff, 0xff]), + { name, length: 9 }, + true, + ['sign', 'verify']); + const importedExplicitRaw = await subtle.exportKey('raw-secret', importedExplicit); + assert.strictEqual(importedExplicit.algorithm.length, 9); + assert.deepStrictEqual( + new Uint8Array(importedExplicitRaw), + new Uint8Array([0xff, 0x80])); + + await assert.rejects( + subtle.importKey( + 'raw-secret', + new Uint8Array([0xff]), + { name, length: 9 }, + true, + ['sign', 'verify']), + { name: 'DataError', message: 'Invalid key length' }); + } } test('KMAC128').then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-get-public-key.mjs b/test/parallel/test-webcrypto-get-public-key.mjs index 9764aabd0a887e..622ec4adca6891 100644 --- a/test/parallel/test-webcrypto-get-public-key.mjs +++ b/test/parallel/test-webcrypto-get-public-key.mjs @@ -1,8 +1,15 @@ +// Flags: --expose-internals + import * as common from '../common/index.mjs'; if (!common.hasCrypto) common.skip('missing crypto'); import * as assert from 'node:assert'; +import { createRequire } from 'node:module'; + +const require = createRequire(import.meta.url); +const { kSupportedAlgorithms } = require('internal/crypto/util'); +const { SubtleCrypto } = globalThis; const { subtle } = globalThis.crypto; const RSA_KEY_GEN = { @@ -11,27 +18,71 @@ const RSA_KEY_GEN = { hash: 'SHA-256', }; -const publicUsages = { - 'ECDH': [], - 'ECDSA': ['verify'], - 'Ed25519': ['verify'], - 'RSA-OAEP': ['encrypt', 'wrapKey'], - 'RSA-PSS': ['verify'], - 'RSASSA-PKCS1-v1_5': ['verify'], - 'X25519': [], +function vector(algorithm, privateUsages, publicUsages) { + return { algorithm, privateUsages, publicUsages }; +} + +const keyGeneration = { + 'ECDH': vector({ name: 'ECDH', namedCurve: 'P-256' }, ['deriveBits'], []), + 'ECDSA': vector({ name: 'ECDSA', namedCurve: 'P-256' }, ['sign'], ['verify']), + 'RSA-OAEP': vector( + { name: 'RSA-OAEP', ...RSA_KEY_GEN }, + ['decrypt', 'unwrapKey'], + ['encrypt', 'wrapKey']), + 'RSA-PSS': vector({ name: 'RSA-PSS', ...RSA_KEY_GEN }, ['sign'], ['verify']), + 'RSASSA-PKCS1-v1_5': vector( + { name: 'RSASSA-PKCS1-v1_5', ...RSA_KEY_GEN }, + ['sign'], + ['verify']), }; -for await (const { privateKey } of [ - subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, false, ['deriveBits']), - subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, false, ['sign']), - subtle.generateKey('Ed25519', false, ['sign']), - subtle.generateKey({ name: 'RSA-OAEP', ...RSA_KEY_GEN }, false, ['decrypt', 'unwrapKey']), - subtle.generateKey({ name: 'RSA-PSS', ...RSA_KEY_GEN }, false, ['sign']), - subtle.generateKey({ name: 'RSASSA-PKCS1-v1_5', ...RSA_KEY_GEN }, false, ['sign']), - subtle.generateKey('X25519', false, ['deriveBits']), +for (const name of [ + 'Ed25519', + 'Ed448', + 'ML-DSA-44', + 'ML-DSA-65', + 'ML-DSA-87', ]) { - const { name } = privateKey.algorithm; - const usages = publicUsages[name]; + keyGeneration[name] = vector(name, ['sign'], ['verify']); +} + +for (const name of ['X25519', 'X448']) { + keyGeneration[name] = vector(name, ['deriveBits'], []); +} + +for (const name of ['ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024']) { + keyGeneration[name] = vector(name, ['decapsulateBits'], ['encapsulateBits']); +} + +const unsupportedGetPublicKeyAlgorithms = new Set([ + 'AES-CBC', + 'AES-CTR', + 'AES-GCM', + 'AES-KW', + 'AES-OCB', + 'ChaCha20-Poly1305', + 'HMAC', + 'KMAC128', + 'KMAC256', +]); + +for (const name of Object.keys(kSupportedAlgorithms.exportKey)) { + const test = keyGeneration[name]; + if (test === undefined) { + if (!unsupportedGetPublicKeyAlgorithms.has(name)) { + assert.fail( + `${name} needs a keyGeneration entry or must be listed in ` + + 'unsupportedGetPublicKeyAlgorithms'); + } + assert.strictEqual(SubtleCrypto.supports('getPublicKey', name), false); + continue; + } + + assert.strictEqual(SubtleCrypto.supports('getPublicKey', name), true); + + const { privateKey } = await subtle.generateKey( + test.algorithm, false, test.privateUsages); + const usages = test.publicUsages; const publicKey = await subtle.getPublicKey(privateKey, usages); assert.deepStrictEqual(publicKey.algorithm, privateKey.algorithm); assert.strictEqual(publicKey.type, 'public'); @@ -50,7 +101,9 @@ for await (const { privateKey } of [ const secretKey = await subtle.generateKey( { name: 'AES-CBC', length: 128 }, true, ['encrypt', 'decrypt']); -await assert.rejects(() => subtle.getPublicKey(secretKey, ['encrypt', 'decrypt']), { - name: 'NotSupportedError', - message: 'key must be a private key' -}); +await assert.rejects( + () => subtle.getPublicKey(secretKey, ['encrypt', 'decrypt']), + { + name: 'NotSupportedError', + message: 'key must be a private key' + }); diff --git a/test/parallel/test-webcrypto-keygen-kmac.js b/test/parallel/test-webcrypto-keygen-kmac.js index 3061f19c826ff5..c1125412892e16 100644 --- a/test/parallel/test-webcrypto-keygen-kmac.js +++ b/test/parallel/test-webcrypto-keygen-kmac.js @@ -17,7 +17,7 @@ const { subtle } = globalThis.crypto; const usages = ['sign', 'verify']; async function test(name, length) { - length = length ?? name === 'KMAC128' ? 128 : 256; + length ??= name === 'KMAC128' ? 128 : 256; const key = await subtle.generateKey({ name, length, @@ -34,12 +34,17 @@ async function test(name, length) { assert.strictEqual(key.algorithm.length, length); assert.strictEqual(key.algorithm, key.algorithm); assert.strictEqual(key.usages, key.usages); + + const raw = await subtle.exportKey('raw-secret', key); + assert.strictEqual(raw.byteLength, Math.ceil(length / 8)); } const kTests = [ + ['KMAC128', 0], ['KMAC128', 128], ['KMAC128', 256], ['KMAC128'], + ['KMAC256', 0], ['KMAC256', 128], ['KMAC256', 256], ['KMAC256'], diff --git a/test/parallel/test-webcrypto-sign-verify-eddsa.js b/test/parallel/test-webcrypto-sign-verify-eddsa.js index 1dfd3ddd76a6a0..3c40139754bea9 100644 --- a/test/parallel/test-webcrypto-sign-verify-eddsa.js +++ b/test/parallel/test-webcrypto-sign-verify-eddsa.js @@ -15,6 +15,44 @@ const vectors = require('../fixtures/crypto/eddsa')(); const supportsContext = hasOpenSSL(3, 2); +const smallOrderVerifyVectors = [ + { + name: 'Ed25519', + publicKey: Buffer.from( + 'c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa', + 'hex'), + signature: Buffer.from( + 'c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a' + + '0000000000000000000000000000000000000000000000000000000000000000', + 'hex'), + data: Buffer.from( + '8c93255d71dcab10e8f379c26200f3c7bd5f09d9bc3068d3ef4edeb4853022b6', + 'hex'), + }, +]; + +if (!process.features.openssl_is_boringssl) { + smallOrderVerifyVectors.push({ + name: 'Ed448', + publicKey: Buffer.concat([Buffer.from([1]), Buffer.alloc(56)]), + signature: Buffer.concat([Buffer.from([1]), Buffer.alloc(113)]), + data: Buffer.from([1, 2, 3]), + }); +} + +async function testSmallOrderVerify({ name, publicKey, signature, data }) { + const key = await subtle.importKey( + 'raw', + publicKey, + { name }, + false, + ['verify']); + + assert.strictEqual( + await subtle.verify({ name }, key, signature, data), + false); +} + async function testVerify({ name, context, publicKeyBuffer, @@ -260,6 +298,9 @@ async function testSign({ name, variations.push(testVerify(vector)); variations.push(testSign(vector)); }); + smallOrderVerifyVectors.forEach((vector) => { + variations.push(testSmallOrderVerify(vector)); + }); await Promise.all(variations); })().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-sign-verify-kmac.js b/test/parallel/test-webcrypto-sign-verify-kmac.js index 008047630753b2..f93fc293b2a4f7 100644 --- a/test/parallel/test-webcrypto-sign-verify-kmac.js +++ b/test/parallel/test-webcrypto-sign-verify-kmac.js @@ -17,10 +17,14 @@ const vectors = require('../fixtures/crypto/kmac')(); async function testVerify({ algorithm, key, + keyLength, data, customization, outputLength, expected }) { + const importAlgorithm = keyLength === undefined ? + { name: algorithm } : + { name: algorithm, length: keyLength }; const [ verifyKey, noVerifyKey, @@ -29,13 +33,13 @@ async function testVerify({ algorithm, subtle.importKey( 'raw-secret', key, - { name: algorithm }, + importAlgorithm, false, ['verify']), subtle.importKey( 'raw-secret', key, - { name: algorithm }, + importAlgorithm, false, ['sign']), subtle.generateKey( @@ -119,10 +123,14 @@ async function testVerify({ algorithm, async function testSign({ algorithm, key, + keyLength, data, customization, outputLength, expected }) { + const importAlgorithm = keyLength === undefined ? + { name: algorithm } : + { name: algorithm, length: keyLength }; const [ signKey, noSignKey, @@ -131,13 +139,13 @@ async function testSign({ algorithm, subtle.importKey( 'raw-secret', key, - { name: algorithm }, + importAlgorithm, false, ['verify', 'sign']), subtle.importKey( 'raw-secret', key, - { name: algorithm }, + importAlgorithm, false, ['verify']), subtle.generateKey( @@ -191,3 +199,75 @@ async function testSign({ algorithm, await Promise.all(variations); })().then(common.mustCall()); + +(async function() { + const key = await subtle.importKey( + 'raw-secret', + new Uint8Array(16), + { name: 'KMAC128' }, + false, + ['sign', 'verify']); + const algorithm = { + name: 'KMAC128', + outputLength: 9, + customization: new Uint8Array(), + }; + const data = new Uint8Array([1, 2, 3]); + + const signature = await subtle.sign(algorithm, key, data); + assert.strictEqual(signature.byteLength, 2); + assert.strictEqual(new Uint8Array(signature)[1] & 0b01111111, 0); + assert(await subtle.verify(algorithm, key, signature, data)); + + const signature16 = new Uint8Array(await subtle.sign({ + ...algorithm, + outputLength: 16, + }, key, data)); + signature16[1] &= 0b10000000; + assert.notDeepStrictEqual(new Uint8Array(signature), signature16); + + const invalidSignature = new Uint8Array(signature); + invalidSignature[1] |= 0b00000001; + assert(!(await subtle.verify(algorithm, key, invalidSignature, data))); + + const nonByteKey = await subtle.importKey( + 'raw-secret', + new Uint8Array([0xff, 0xff, 0xff, 0xff]), + { name: 'KMAC128', length: 25 }, + false, + ['sign', 'verify']); + const nonByteKeySignature = await subtle.sign({ + ...algorithm, + outputLength: 16, + }, nonByteKey, data); + assert.strictEqual(nonByteKeySignature.byteLength, 2); + assert(await subtle.verify({ + ...algorithm, + outputLength: 16, + }, nonByteKey, nonByteKeySignature, data)); +})().then(common.mustCall()); + +(async function() { + const data = new Uint8Array([1, 2, 3]); + + for (const name of ['KMAC128', 'KMAC256']) { + for (const keyData of [ + new Uint8Array(), + new Uint8Array([1]), + new Uint8Array([1, 2, 3]), + ]) { + const key = await subtle.importKey( + 'raw-secret', + keyData, + { name }, + true, + ['sign', 'verify']); + assert.strictEqual(key.algorithm.length, keyData.byteLength * 8); + + const algorithm = { name, outputLength: 256 }; + const signature = await subtle.sign(algorithm, key, data); + assert.strictEqual(signature.byteLength, 32); + assert(await subtle.verify(algorithm, key, signature, data)); + } + } +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-supports.mjs b/test/parallel/test-webcrypto-supports.mjs index c3c976f730e065..43d3ee03c8bc29 100644 --- a/test/parallel/test-webcrypto-supports.mjs +++ b/test/parallel/test-webcrypto-supports.mjs @@ -61,13 +61,13 @@ function supportsRawSecret(alg) { return false; } -function supports256RawSecret(alg) { +function supportsEncapsulatedRawSecret(alg) { if (!supportsRawSecret(alg)) return false; switch (alg?.name?.toLowerCase?.()) { case 'hmac': case 'kmac128': case 'kmac256': - return typeof alg.length !== 'number' || alg.length === 256; + return typeof alg.length !== 'number' || Math.ceil(alg.length / 8) === 32; default: return true; } @@ -75,7 +75,7 @@ function supports256RawSecret(alg) { for (const encap of vectors.encapsulateBits) { for (const imp of vectors.importKey) { - if (supports256RawSecret(imp[1])) { + if (supportsEncapsulatedRawSecret(imp[1])) { vectors.encapsulateKey.push([encap[0] && imp[0], encap[1], imp[1]]); } else { vectors.encapsulateKey.push([false, encap[1], imp[1]]); @@ -85,7 +85,7 @@ for (const encap of vectors.encapsulateBits) { for (const decap of vectors.decapsulateBits) { for (const imp of vectors.importKey) { - if (supports256RawSecret(imp[1])) { + if (supportsEncapsulatedRawSecret(imp[1])) { vectors.decapsulateKey.push([decap[0] && imp[0], decap[1], imp[1]]); } else { vectors.decapsulateKey.push([false, decap[1], imp[1]]); diff --git a/test/parallel/test-webcrypto-wrap-unwrap.js b/test/parallel/test-webcrypto-wrap-unwrap.js index 49f63e215fadfc..18ba6dc3796378 100644 --- a/test/parallel/test-webcrypto-wrap-unwrap.js +++ b/test/parallel/test-webcrypto-wrap-unwrap.js @@ -369,6 +369,89 @@ function testWrapping(name, keys) { await Promise.all(variations); })().then(common.mustCall()); +async function testNonByteLengthWrapUnwrap({ + key, + formats, + rawFormat, + explicitAlgorithm, + implicitAlgorithm, +}) { + const wrappingKey = await subtle.generateKey( + { name: 'AES-GCM', length: 128 }, + true, + ['wrapKey', 'unwrapKey']); + const expectedRaw = new Uint8Array(await subtle.exportKey(rawFormat, key)); + + for (const [i, format] of formats.entries()) { + const wrapAlgorithm = { + name: 'AES-GCM', + iv: new Uint8Array(12).fill(i), + }; + const wrapped = await subtle.wrapKey(format, key, wrappingKey, wrapAlgorithm); + + // The serialized key material carries bytes, not the requested bit length. + const explicit = await subtle.unwrapKey( + format, + wrapped, + wrappingKey, + wrapAlgorithm, + explicitAlgorithm, + true, + ['sign', 'verify']); + assert.strictEqual(explicit.algorithm.length, 9); + assert.deepStrictEqual( + new Uint8Array(await subtle.exportKey(rawFormat, explicit)), + expectedRaw); + + const implicit = await subtle.unwrapKey( + format, + wrapped, + wrappingKey, + wrapAlgorithm, + implicitAlgorithm, + true, + ['sign', 'verify']); + assert.strictEqual(implicit.algorithm.length, expectedRaw.byteLength * 8); + assert.deepStrictEqual( + new Uint8Array(await subtle.exportKey(rawFormat, implicit)), + expectedRaw); + } +} + +(async function() { + const hmacAlgorithm = { name: 'HMAC', hash: 'SHA-256' }; + const hmacKey = await subtle.importKey( + 'raw', + new Uint8Array([0xff, 0xff]), + { ...hmacAlgorithm, length: 9 }, + true, + ['sign', 'verify']); + await testNonByteLengthWrapUnwrap({ + key: hmacKey, + formats: ['raw', 'jwk'], + rawFormat: 'raw', + explicitAlgorithm: { ...hmacAlgorithm, length: 9 }, + implicitAlgorithm: hmacAlgorithm, + }); + + if (hasOpenSSL(3)) { + const kmacAlgorithm = { name: 'KMAC128' }; + const kmacKey = await subtle.importKey( + 'raw-secret', + new Uint8Array([0xff, 0xff]), + { ...kmacAlgorithm, length: 9 }, + true, + ['sign', 'verify']); + await testNonByteLengthWrapUnwrap({ + key: kmacKey, + formats: ['raw-secret', 'jwk'], + rawFormat: 'raw-secret', + explicitAlgorithm: { ...kmacAlgorithm, length: 9 }, + implicitAlgorithm: kmacAlgorithm, + }); + } +})().then(common.mustCall()); + // Test that wrapKey/unwrapKey validate the wrapping/unwrapping key's // algorithm and usage before proceeding. // Spec: https://w3c.github.io/webcrypto/#SubtleCrypto-method-wrapKey diff --git a/test/wpt/status/WebCryptoAPI.cjs b/test/wpt/status/WebCryptoAPI.cjs index 5d1d630c436311..8fdbb458fac7e3 100644 --- a/test/wpt/status/WebCryptoAPI.cjs +++ b/test/wpt/status/WebCryptoAPI.cjs @@ -1,11 +1,7 @@ 'use strict'; -const os = require('node:os'); - const { hasOpenSSL } = require('../../common/crypto.js'); -const s390x = os.arch() === 's390x'; - const conditionalFileSkips = {}; const conditionalSubtestSkips = {}; @@ -62,6 +58,7 @@ if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) { 'sign_verify/mldsa.tentative.https.any.js'); skipSubtests( + ['getPublicKey.tentative.https.any.js', /ml-(?:kem|dsa)/i], ['supports-modern.tentative.https.any.js', /ml-(?:kem|dsa)/i]); } @@ -88,6 +85,7 @@ if (process.features.openssl_is_boringssl) { ['encap_decap/encap_decap_keys.tentative.https.any.js', /ml-kem-512/i], ['generateKey/failures_ML-KEM.tentative.https.any.js', /ml-kem-512/i], ['generateKey/successes_ML-KEM.tentative.https.any.js', /ml-kem-512/i], + ['getPublicKey.tentative.https.any.js', /(?:ed448|x448|ml-kem-512)/i], ['import_export/ML-KEM_importKey.tentative.https.any.js', /ml-kem-512/i], ['serialization/mlkem.tentative.https.any.js', /ml-kem-512/i], ['supports-modern.tentative.https.any.js', /ml-kem-512/i]); @@ -104,53 +102,6 @@ function assertNoOverlap(fileSkips, subtestSkips) { assertNoOverlap(conditionalFileSkips, conditionalSubtestSkips); -const cshakeExpectedFailures = ['cSHAKE128', 'cSHAKE256'].flatMap((algorithm) => { - return [0, 256, 384, 512].flatMap((length) => { - return ['empty', 'short', 'medium'].flatMap((size) => { - const base = `${algorithm} with ${length} bit output and ${size} source data`; - return [ - base, - ].concat(size !== 'empty' ? [ - `${base} and altered buffer after call`, - `${base} and altered buffer during call`, - `${base} and transferred buffer after call`, - `${base} and transferred buffer during call`, - ] : []); - }); - }); -}); - -const kmacVectorNames = [ - 'KMAC128 with no customization', - 'KMAC128 with customization', - 'KMAC128 with large data and customization', - 'KMAC256 with customization and 512-bit output', - 'KMAC256 with large data and no customization', - 'KMAC256 with large data and customization', -]; - -const kmacExpectedFailures = kmacVectorNames.flatMap((name) => { - return [ - `${name} verification`, - `${name} verification with transferred signature during call`, - `${name} verification with transferred signature after call`, - `${name} verification with altered signature during call`, - `${name} verification with altered signature after call`, - `${name} with altered plaintext during call`, - `${name} with altered plaintext after call`, - `${name} with transferred plaintext during call`, - `${name} with transferred plaintext after call`, - `${name} no verify usage`, - `${name} round trip`, - `${name} verification failure due to wrong plaintext`, - `${name} verification failure due to wrong signature`, - `${name} verification failure due to short signature`, - `${name} verification failure due to wrong length parameter`, - `${name} signing with wrong algorithm name`, - `${name} verifying with wrong algorithm name`, - ]; -}); - module.exports = { ...conditionalFileSkips, ...conditionalSubtestSkips, @@ -160,21 +111,6 @@ module.exports = { 'historical.any.js': { 'skip': 'Not relevant in Node.js context', }, - 'sign_verify/eddsa_small_order_points.https.any.js': { - 'fail': { - 'note': 'see https://github.com/nodejs/node/issues/54572', - 'expected': [ - 'Ed25519 Verification checks with small-order key of order - Test 1', - 'Ed25519 Verification checks with small-order key of order - Test 2', - 'Ed25519 Verification checks with small-order key of order - Test 12', - 'Ed25519 Verification checks with small-order key of order - Test 13', - ...(s390x ? [] : [ - 'Ed25519 Verification checks with small-order key of order - Test 0', - 'Ed25519 Verification checks with small-order key of order - Test 11', - ]), - ], - }, - }, 'getRandomValues.any.js': { 'fail': { 'note': 'https://github.com/nodejs/node/issues/58987', @@ -191,16 +127,4 @@ module.exports = { ], }, }, - 'digest/cshake.tentative.https.any.js': conditionalFileSkips['digest/cshake.tentative.https.any.js'] ?? { - 'fail': { - 'note': 'WPT still uses CShakeParams.length; implementation moved to CShakeParams.outputLength', - 'expected': cshakeExpectedFailures, - }, - }, - 'sign_verify/kmac.tentative.https.any.js': conditionalFileSkips['sign_verify/kmac.tentative.https.any.js'] ?? { - 'fail': { - 'note': 'WPT still uses KmacParams.length; implementation moved to KmacParams.outputLength', - 'expected': kmacExpectedFailures, - }, - }, };