diff --git a/doc/api/webcrypto.md b/doc/api/webcrypto.md index d7551e4e3dd5cc..8bcb0b53c066b0 100644 --- a/doc/api/webcrypto.md +++ b/doc/api/webcrypto.md @@ -1933,26 +1933,39 @@ added: * 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` @@ -2489,9 +2502,7 @@ 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/diffiehellman.js b/lib/internal/crypto/diffiehellman.js index 3bd4bd33156c94..edf429fc02810f 100644 --- a/lib/internal/crypto/diffiehellman.js +++ b/lib/internal/crypto/diffiehellman.js @@ -3,10 +3,8 @@ const { ArrayBufferPrototypeSlice, FunctionPrototypeCall, - MathCeil, ObjectDefineProperty, TypedArrayPrototypeGetBuffer, - Uint8Array, } = primordials; const { Buffer } = require('buffer'); @@ -59,7 +57,9 @@ const { getArrayBufferOrView, jobPromise, jobPromiseThen, + numBitsToBytes, toBuf, + truncateToBitLength, kHandle, } = require('internal/crypto/util'); @@ -328,7 +328,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) { @@ -372,27 +371,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); + if (length % 8 === 0) { + if (byteLength === sliceLength) + return bits; + return 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]; - - 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/hash.js b/lib/internal/crypto/hash.js index 86052cbc6bd440..b27295f9c0b3b8 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, @@ -227,15 +232,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': diff --git a/lib/internal/crypto/mac.js b/lib/internal/crypto/mac.js index 5c6955d22b1904..ec394954242b24 100644 --- a/lib/internal/crypto/mac.js +++ b/lib/internal/crypto/mac.js @@ -20,6 +20,8 @@ const { hasAnyNotIn, jobPromise, normalizeHashName, + numBitsToBytes, + truncateToBitLength, } = require('internal/crypto/util'); const { @@ -38,6 +40,27 @@ const { validateJwk, } = require('internal/crypto/webcrypto_util'); +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, @@ -113,7 +136,6 @@ function macImportKey( let length; switch (format) { case 'KeyObjectHandle': { - length = keyData.getSymmetricKeySize() * 8; handle = keyData; break; } @@ -122,7 +144,6 @@ function macImportKey( if (format === 'raw' && !isHmac) { return undefined; } - length = keyData.byteLength * 8; handle = importSecretKey(keyData); break; } @@ -140,20 +161,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, @@ -190,7 +204,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/util.js b/lib/internal/crypto/util.js index 74d86de3f1b9e1..aac190d292e999 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, @@ -550,6 +551,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 +650,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'); @@ -971,6 +1019,8 @@ module.exports = { cleanupWebCryptoResult, prepareWebCryptoResult, validateMaxBufferLength, + numBitsToBytes, + truncateToBitLength, bigIntArrayToUnsignedBigInt, bigIntArrayToUnsignedInt, getBlockSize, diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index f86ded93312859..9f85dc0aa56a1c 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -23,6 +23,7 @@ const { } = primordials; const { + CShakeJob, kWebCryptoKeyFormatRaw, kWebCryptoKeyFormatPKCS8, kWebCryptoKeyFormatSPKI, @@ -65,6 +66,7 @@ const { jobPromiseThen, normalizeAlgorithm, normalizeHashName, + numBitsToBytes, prepareWebCryptoResult, validateMaxBufferLength, } = require('internal/crypto/util'); @@ -1914,7 +1916,7 @@ class SubtleCrypto { case 'KMAC128': case 'KMAC256': if (normalizedAdditionalAlgorithm.length === undefined || - normalizedAdditionalAlgorithm.length === 256) { + numBitsToBytes(normalizedAdditionalAlgorithm.length) === 32) { break; } return false; @@ -1958,7 +1960,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': diff --git a/lib/internal/crypto/webidl.js b/lib/internal/crypto/webidl.js index 44a62eb67fd857..c886a4a2280ad2 100644 --- a/lib/internal/crypto/webidl.js +++ b/lib/internal/crypto/webidl.js @@ -1,17 +1,23 @@ 'use strict'; const { + ArrayBufferIsView, ArrayPrototypeIncludes, MathPow, NumberParseInt, ObjectPrototypeHasOwnProperty, + StringPrototypeCharCodeAt, StringPrototypeStartsWith, StringPrototypeToLowerCase, + Uint8Array, } = primordials; const { lazyDOMException, } = require('internal/util'); +const { + isUint32, +} = require('internal/validators'); const { CryptoKey } = require('internal/crypto/webcrypto'); const { getCryptoKeyAlgorithm, @@ -20,6 +26,7 @@ const { const { validateMaxBufferLength, kNamedCurveAliases, + numBitsToBytes, } = require('internal/crypto/util'); const { converters: webidl, @@ -232,6 +239,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, @@ -269,6 +313,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, [ @@ -284,7 +335,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), }, ], ]); @@ -366,23 +417,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, }, ], ]); @@ -656,18 +702,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, @@ -676,7 +711,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), }, ], ]); @@ -690,11 +724,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 44181cd045b429..20cadc610bace1 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 { @@ -352,6 +364,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) { @@ -363,6 +378,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) @@ -548,8 +566,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"); @@ -585,5 +603,347 @@ 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, + CryptoErrorStore*) { + 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 9991985d58477f..533f43c391574b 100644 --- a/src/crypto/crypto_hash.h +++ b/src/crypto/crypto_hash.h @@ -83,6 +83,75 @@ 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, + CryptoErrorStore* errors); + + 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_kmac.cc b/src/crypto/crypto_kmac.cc index 01e9e85092aedc..c77c21d0a1aeeb 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,25 +134,84 @@ 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, - CryptoErrorStore* errors) { - if (params.length == 0) { - *out = ByteSource(); - return true; - } + CryptoErrorStore*) { + 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) { - errors->Insert(NodeCryptoError::KMAC_FAILED); + 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 @@ -157,7 +231,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); @@ -184,13 +258,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 fa44cc8d80cde6..f6300fb5478571 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_util.cc b/src/crypto/crypto_util.cc index 42b248d84b43e5..8d22350adffaac 100644 --- a/src/crypto/crypto_util.cc +++ b/src/crypto/crypto_util.cc @@ -383,6 +383,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 742f23b0f5e789..a9ac62625ccf0d 100644 --- a/src/crypto/crypto_util.h +++ b/src/crypto/crypto_util.h @@ -39,6 +39,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); @@ -241,6 +246,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; @@ -249,6 +256,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 1850acacdc22e9..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': [ @@ -121,7 +121,7 @@ export const vectors = { [false, { name: 'ECDH', public: ECDH.publicKey }, { name: 'HMAC', hash: 'SHA-256' }], - [false, + [true, { name: 'ECDH', public: ECDH.publicKey }, { name: 'HMAC', hash: 'SHA-256', length: 255 }], [true, @@ -183,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 }], @@ -195,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-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/parallel/test-crypto-key-objects-to-crypto-key.js b/test/parallel/test-crypto-key-objects-to-crypto-key.js index bc89093bde51b0..46029f5a436261 100644 --- a/test/parallel/test-crypto-key-objects-to-crypto-key.js +++ b/test/parallel/test-crypto-key-objects-to-crypto-key.js @@ -136,16 +136,26 @@ function genericSecretVectors(name) { ]; } -function macInvalid(algorithm, invalidLengthMessage) { +function macInvalid(algorithm, invalidLengthMessage, allowZeroKey = false) { const key = createSecretKey(randomBytes(32)); const usages = ['sign', 'verify']; - assert.throws(() => { - createSecretKey(Buffer.alloc(0)).toCryptoKey(algorithm, true, usages); - }, { - name: 'DataError', - message: 'Zero-length key is not supported', - }); + 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(algorithm, true, usages); + }, { + name: 'DataError', + message: 'Zero-length key is not supported', + }); + } assert.throws(() => key.toCryptoKey(algorithm, true, []), { name: 'SyntaxError', @@ -336,7 +346,7 @@ for (const name of ['KMAC128', 'KMAC256']) { if (name in kSupportedAlgorithms.importKey) { tests[name] = kmacVectors(name); invalid[name] = () => { - macInvalid({ name }, 'KmacImportParams.length cannot be 0'); + macInvalid({ name }, 'Invalid key length', true); }; } } 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 5f8cd7dee43b87..3ac17b2ff01caa 100644 --- a/test/parallel/test-webcrypto-derivekey.js +++ b/test/parallel/test-webcrypto-derivekey.js @@ -286,12 +286,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 d23a05a02d5d6f..dbb0a7d3fa8b13 100644 --- a/test/parallel/test-webcrypto-encap-decap-ml-kem.js +++ b/test/parallel/test-webcrypto-encap-decap-ml-kem.js @@ -70,6 +70,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']), { @@ -164,6 +177,19 @@ async function testDecapsulateKey({ name, publicKeyPem, privateKeyPem, results } const decapsulatedKeyData = getCryptoKeyData(decapsulatedKey); 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-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-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