crypto: use backend cSHAKE and KMAC

Require cSHAKE and KMAC output lengths and KMAC key lengths to be
multiples of 8 bits. KMAC keys must be at least 32 bits. Share these
restrictions between operations and supports.

Use OpenSSL's KMAC provider for all supported inputs and its cSHAKE
implementation for non-empty function names or customization strings.
Keep using SHAKE when both cSHAKE parameters are empty. Remove the
custom Keccak framing, partial-bit handling, and short-key fallback.

Document the OpenSSL 4.0 requirement for non-empty cSHAKE parameters and
reject customization strings containing null bytes. Keep the documented
512-byte customization limit and let backend failures reach callers.

Signed-off-by: Filip Skokan <panva.ip@gmail.com>
Assisted-by: Codex
PR-URL: https://github.com/nodejs/node/pull/66237
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Aviv Keller <me@aviv.sh>
This commit is contained in:
Filip Skokan authored and Node.js GitHub Bot committed 2026-09-25 13:23:26 +00:00
1 parent e0748dfd9d
commit 38187f81eb
25 files changed
+368 -956

No files matched your search

+17 -10
View File
@@ -1842,6 +1842,9 @@ changes:
description: Renamed `cShakeParams.length` to `cShakeParams.outputLength`.
-->
When both `functionName` and `customization` are empty or `undefined`, cSHAKE is
equivalent to plain SHAKE.
#### `cShakeParams.name`
<!-- YAML
@@ -1858,7 +1861,8 @@ added:
- v24.15.0
-->
* Type: {number} represents the requested output length in bits.
* Type: {number} represents the requested output length in bits. Must be a
multiple of 8.
#### `cShakeParams.functionName`
@@ -1875,9 +1879,10 @@ changes:
* Type: {ArrayBuffer|TypedArray|DataView|Buffer|undefined}
The `functionName` member represents the NIST function-name byte string used to
domain-separate functions built on top of cSHAKE. Accepted values are:
domain-separate functions built on top of cSHAKE. Non-empty values require
OpenSSL 4.0 or later. Accepted values are:
* empty or `undefined`, in which case cSHAKE is equivalent to plain SHAKE
* empty or `undefined`
* the ASCII byte sequence `'KMAC'`
* the ASCII byte sequence `'TupleHash'`
* the ASCII byte sequence `'ParallelHash'`
@@ -1896,11 +1901,11 @@ changes:
* Type: {ArrayBuffer|TypedArray|DataView|Buffer|undefined}
The `customization` member represents the customization data. Accepted
values are:
The `customization` member represents the customization data. Non-empty values
require OpenSSL 4.0 or later. Accepted values are:
* empty or `undefined`, in which case cSHAKE is equivalent to plain SHAKE
* up to 512 bytes of arbitrary data
* empty or `undefined`
* up to 512 bytes of data without null bytes
### Class: `EcdhKeyDeriveParams`
@@ -2360,7 +2365,7 @@ added: v24.8.0
* Type: {number}
The optional number of bits in the KMAC key. This is optional and should
be omitted for most cases.
be omitted for most cases. The key length must be at least 32 and a multiple of 8.
#### `kmacImportParams.name`
@@ -2410,7 +2415,8 @@ added: v24.8.0
The number of bits to generate for the KMAC key. If omitted,
the length will be determined by the KMAC algorithm used.
This is optional and should be omitted for most cases.
This is optional and should be omitted for most cases. Must be at least 32 and a
multiple of 8.
#### `kmacKeyGenParams.name`
@@ -2448,7 +2454,8 @@ added:
- v24.15.0
-->
* Type: {number} represents the requested output length in bits.
* Type: {number} represents the requested output length in bits. Must be a
multiple of 8.
#### `kmacParams.customization`
+6 -22
View File
@@ -5,12 +5,10 @@ const {
ObjectSetPrototypeOf,
StringPrototypeToLowerCase,
Symbol,
TypedArrayPrototypeGetBuffer,
TypedArrayPrototypeIncludes,
} = primordials;
const {
CShakeJob,
Hash: _Hash,
HashJob,
Hmac: _Hmac,
@@ -23,10 +21,7 @@ const {
const {
getStringOption,
jobPromise,
jobPromiseThen,
normalizeHashName,
numBitsToBytes,
truncateToBitLength,
validateAlgorithm,
validateMaxBufferLength,
kHandle,
@@ -42,7 +37,6 @@ const {
} = require('internal/crypto/keys');
const {
lazyDOMException,
normalizeEncoding,
encodingsMap,
getDeprecationWarningEmitter,
@@ -267,30 +261,20 @@ function asyncDigest(algorithm, data) {
const outputLength = algorithm.outputLength;
if (getOptionalByteLength(algorithm.functionName) ||
getOptionalByteLength(algorithm.customization)) {
if (CShakeJob === undefined) {
throw lazyDOMException(
'Non-empty CShakeParams functionName or customization is not supported',
'NotSupportedError');
}
return jobPromise(() => new CShakeJob(
return jobPromise(() => new HashJob(
kCryptoJobWebCrypto,
algorithm.name,
StringPrototypeToLowerCase(algorithm.name),
data,
outputLength,
algorithm.functionName,
algorithm.customization,
outputLength));
algorithm.customization));
}
const bits = jobPromise(() => new HashJob(
return jobPromise(() => new HashJob(
kCryptoJobWebCrypto,
normalizeHashName(algorithm.name),
data,
numBitsToBytes(outputLength) * 8));
if (outputLength % 8 === 0)
return bits;
return jobPromiseThen(bits, (bits) =>
TypedArrayPrototypeGetBuffer(truncateToBitLength(outputLength, bits)));
outputLength));
}
case 'TurboSHAKE128':
// Fall through
-1
View File
@@ -193,7 +193,6 @@ function kmacSignVerify(key, data, algorithm, signature) {
getCryptoKeyHandle(key),
algorithm.name,
algorithm.customization,
getCryptoKeyAlgorithm(key).length,
algorithm.outputLength,
data,
signature));
+1 -1
View File
@@ -729,7 +729,7 @@ function validateMaxBufferLength(data, name, max = kMaxBufferLength) {
}
function validateKmacKeyLength(length) {
if ((length < 32 || length % 8) && isFips())
if (length < 32 || length % 8 !== 0)
throw lazyDOMException('Invalid key length', 'NotSupportedError');
}
+1 -11
View File
@@ -23,7 +23,6 @@ const {
} = primordials;
const {
CShakeJob,
kWebCryptoKeyFormatRaw,
kWebCryptoKeyFormatPKCS8,
kWebCryptoKeyFormatSPKI,
@@ -72,7 +71,6 @@ const {
prepareWebCryptoResult,
validateAlgorithm,
validateMaxBufferLength,
getOptionalByteLength,
} = require('internal/crypto/util');
const {
@@ -1923,15 +1921,7 @@ function check(op, alg, length) {
}
switch (op) {
case 'digest': {
if ((normalizedAlgorithm.name === 'cSHAKE128' ||
normalizedAlgorithm.name === 'cSHAKE256') &&
(getOptionalByteLength(normalizedAlgorithm.functionName) ||
getOptionalByteLength(normalizedAlgorithm.customization))) {
return CShakeJob !== undefined;
}
return true;
}
case 'digest':
case 'decapsulate':
case 'decrypt':
case 'encapsulate':
+14 -11
View File
@@ -11,14 +11,12 @@ const {
StringPrototypeCharCodeAt,
StringPrototypeSplit,
StringPrototypeToLowerCase,
TypedArrayPrototypeIncludes,
} = primordials;
const {
lazyDOMException,
} = require('internal/util');
const {
isUint32,
} = require('internal/validators');
const {
getCryptoKeyAlgorithm,
getCryptoKeyType,
@@ -29,9 +27,9 @@ const {
validateMaxBufferLength,
getBufferSourceByteLength,
getBufferSourceBytes,
getHashes,
isFips,
kNamedCurveAliases,
numBitsToBytes,
validateKmacKeyLength,
} = require('internal/crypto/util');
const {
@@ -293,20 +291,20 @@ function validateZeroLength(parameterName) {
}
function validateCShakeOutputLength(V) {
if (!isUint32(numBitsToBytes(V) * 8)) {
if (V % 8 !== 0) {
throw lazyDOMException(
'Invalid CShakeParams outputLength',
'OperationError');
'NotSupportedError');
}
}
const kCShakeFunctionNames = ['KMAC', 'TupleHash', 'ParallelHash'];
function validateCShakeFunctionName(V) {
function validateCShakeFunctionName(V, dict) {
const length = getBufferSourceByteLength(V);
if (length === 0) return;
if (!isFips()) {
if (ArrayPrototypeIncludes(getHashes(), StringPrototypeToLowerCase(dict.name))) {
const bytes = getBufferSourceBytes(V);
for (let i = 0; i < kCShakeFunctionNames.length; i++) {
const functionName = kCShakeFunctionNames[i];
@@ -325,12 +323,17 @@ function validateCShakeFunctionName(V) {
'NotSupportedError');
}
function validateCShakeCustomization(V) {
if (isFips() && getBufferSourceByteLength(V) !== 0)
function validateCShakeCustomization(V, dict) {
if (getBufferSourceByteLength(V) === 0) return;
if (!ArrayPrototypeIncludes(getHashes(), StringPrototypeToLowerCase(dict.name)))
throw lazyDOMException(
'Unsupported CShakeParams customization',
'NotSupportedError');
validateMaxBufferLength(V, 'CShakeParams.customization', 512);
if (TypedArrayPrototypeIncludes(getBufferSourceBytes(V), 0))
throw lazyDOMException(
'Unsupported CShakeParams customization',
'NotSupportedError');
}
converters.RsaPssParams = createAlgorithmDictionaryConverter(
@@ -790,7 +793,7 @@ converters.KmacParams = createAlgorithmDictionaryConverter(
converter: (V, opts) =>
converters['unsigned long'](V, enforceRangeOptions(opts)),
validator: (V) => {
if ((V === 0 || V % 8) && isFips())
if (V % 8 !== 0 || (V === 0 && isFips()))
throw lazyDOMException(
'Invalid KmacParams outputLength',
'NotSupportedError');
-353
View File
@@ -7,21 +7,15 @@
#include "threadpoolwork-inl.h"
#include "v8.h"
#if OPENSSL_WITH_EVP_MAC
#include <openssl/core_names.h>
#include <openssl/evp.h>
#endif
#if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK
#include <openssl/digest.h>
#endif
#include <algorithm>
#include <array>
#include <climits>
#include <cstdio>
#include <limits>
#include <memory>
#include <string_view>
#include <utility>
@@ -588,9 +582,6 @@ void Hash::Initialize(Environment* env, Local<Object> 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) {
@@ -602,9 +593,6 @@ void Hash::RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(OneShotDigest);
HashJob::RegisterExternalReferences(registry);
#if OPENSSL_WITH_EVP_MAC
CShakeJob::RegisterExternalReferences(registry);
#endif
}
// new Hash(algorithm, xofLen, algorithmId, algorithmCache[, functionName,
@@ -895,346 +883,5 @@ bool HashTraits::DeriveBits(Environment* env,
return true;
}
#if OPENSSL_WITH_EVP_MAC
namespace {
static constexpr std::array<unsigned char, 1> kEmptyString = {};
static constexpr size_t kKeccakKmac128Rate = 168;
static constexpr size_t kKeccakKmac256Rate = 136;
static constexpr size_t kMaxCShakeCustomizationSize = 512;
struct EncodedLength {
std::array<unsigned char, sizeof(size_t) + 1> 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<size_t>::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<unsigned char>(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<unsigned char>(value >> shift);
}
if (!left) encoded.data[value_size] = static_cast<unsigned char>(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<const void>{
.data = data,
.len = size,
});
}
bool DigestUpdateZeros(ncrypto::EVPMDCtxPointer* ctx, size_t size) {
static constexpr std::array<unsigned char, 168> 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<size_t>::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, decltype(&EVP_MD_free)>{
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
: 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 {
tracker->TraitTrackInline(in, "in");
tracker->TraitTrackInline(function_name, "function_name");
tracker->TraitTrackInline(customization, "customization");
}
MaybeLocal<Value> CShakeTraits::EncodeOutput(Environment* env,
const CShakeConfig& params,
ByteSource* out) {
return out->ToArrayBuffer(env);
}
Maybe<void> CShakeTraits::AdditionalConfig(
CryptoJobMode mode,
const FunctionCallbackInfo<Value>& args,
unsigned int offset,
CShakeConfig* params) {
Environment* env = Environment::GetCurrent(args);
if (IsFipsEnabled()) {
THROW_ERR_CRYPTO_UNSUPPORTED_OPERATION(env);
return Nothing<void>();
}
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<char> data(args[offset + 1]);
if (!data.CheckSizeInt32()) [[unlikely]] {
THROW_ERR_OUT_OF_RANGE(env, "data is too big");
return Nothing<void>();
}
params->in = IsCryptoJobAsync(mode) ? data.ToCopy() : data.ToByteSource();
if (!args[offset + 2]->IsUndefined()) {
ArrayBufferOrViewContents<char> function_name(args[offset + 2]);
if (!function_name.CheckSizeInt32()) [[unlikely]] {
THROW_ERR_OUT_OF_RANGE(env, "functionName is too big");
return Nothing<void>();
}
params->function_name = IsCryptoJobAsync(mode)
? function_name.ToCopy()
: function_name.ToByteSource();
}
if (!args[offset + 3]->IsUndefined()) {
ArrayBufferOrViewContents<char> customization(args[offset + 3]);
if (!customization.CheckSizeInt32()) [[unlikely]] {
THROW_ERR_OUT_OF_RANGE(env, "customization is too big");
return Nothing<void>();
}
params->customization = IsCryptoJobAsync(mode)
? customization.ToCopy()
: customization.ToByteSource();
}
CHECK(args[offset + 4]->IsUint32()); // Length
params->length = args[offset + 4].As<Uint32>()->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<size_t>(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
-68
View File
@@ -86,74 +86,6 @@ struct HashTraits final {
using HashJob = DeriveBitsJob<HashTraits>;
#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 {
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<void> AdditionalConfig(
CryptoJobMode mode,
const v8::FunctionCallbackInfo<v8::Value>& args,
unsigned int offset,
CShakeConfig* params);
static bool DeriveBits(Environment* env,
const CShakeConfig& params,
ByteSource* out,
CryptoJobMode mode,
CryptoErrorStore* errors);
static v8::MaybeLocal<v8::Value> EncodeOutput(Environment* env,
const CShakeConfig& params,
ByteSource* out);
};
using CShakeJob = DeriveBitsJob<CShakeTraits>;
#endif // OPENSSL_WITH_EVP_MAC
} // namespace crypto
} // namespace node
+7 -90
View File
@@ -1,14 +1,11 @@
#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 <openssl/core_names.h>
#include <openssl/params.h>
#include <array>
#include <limits>
#include <utility>
#include "crypto/crypto_keys.h"
#include "crypto/crypto_sig.h"
@@ -26,7 +23,6 @@ using v8::Local;
using v8::Maybe;
using v8::MaybeLocal;
using v8::Nothing;
using v8::Number;
using v8::Object;
using v8::Uint32;
using v8::Value;
@@ -38,7 +34,6 @@ 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,27 +91,18 @@ Maybe<void> KmacTraits::AdditionalConfig(
}
// If undefined, params->customization remains uninitialized (size 0).
CHECK(args[offset + 4]->IsNumber()); // Key length
double key_length = args[offset + 4].As<Number>()->Value();
if (!(key_length >= 0) ||
key_length > static_cast<double>(std::numeric_limits<size_t>::max())) {
THROW_ERR_OUT_OF_RANGE(env, "key length is too big");
return Nothing<void>();
}
params->key_length = static_cast<size_t>(key_length);
CHECK(args[offset + 4]->IsUint32()); // Length
params->length = args[offset + 4].As<Uint32>()->Value();
CHECK(args[offset + 5]->IsUint32()); // Length
params->length = args[offset + 5].As<Uint32>()->Value();
ArrayBufferOrViewContents<char> data(args[offset + 6]);
ArrayBufferOrViewContents<char> data(args[offset + 5]);
if (!data.CheckSizeInt32()) [[unlikely]] {
THROW_ERR_OUT_OF_RANGE(env, "data is too big");
return Nothing<void>();
}
params->data = IsCryptoJobAsync(mode) ? data.ToCopy() : data.ToByteSource();
if (!args[offset + 7]->IsUndefined()) {
ArrayBufferOrViewContents<char> signature(args[offset + 7]);
if (!args[offset + 6]->IsUndefined()) {
ArrayBufferOrViewContents<char> signature(args[offset + 6]);
if (!signature.CheckSizeInt32()) [[unlikely]] {
THROW_ERR_OUT_OF_RANGE(env, "signature is too big");
return Nothing<void>();
@@ -128,86 +114,18 @@ Maybe<void> KmacTraits::AdditionalConfig(
return JustVoid();
}
namespace {
static constexpr std::array<unsigned char, 4> 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) {
if (IsFipsEnabled()) return false;
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*) {
const bool truncate_to_bit_length = params.length % CHAR_BIT != 0;
const size_t length_bytes =
NumBitsToBytes(static_cast<size_t>(params.length));
if (params.length % CHAR_BIT != 0) return false;
const size_t length_bytes = params.length / CHAR_BIT;
// Get the key data.
const void* key_data = params.key.GetSymmetricKey();
size_t key_size = params.key.GetSymmetricKeySize();
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
@@ -261,7 +179,6 @@ bool KmacTraits::DeriveBits(Environment* env,
auto buffer = result.release();
*out = ByteSource::Allocated(buffer.data, buffer.len);
if (truncate_to_bit_length) TruncateToBitLength(params.length, out);
return true;
}
+1 -2
View File
@@ -21,8 +21,7 @@ struct KmacConfig final : public MemoryRetainer {
ByteSource signature;
ByteSource customization;
KmacVariant variant;
size_t key_length; // Key length in bits
uint32_t length; // Output length in bits
uint32_t length; // Output length in bits
KmacConfig() = default;
-55
View File
@@ -114,61 +114,6 @@ 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;
+30 -18
View File
@@ -11,6 +11,8 @@ const pqc = hasOpenSSL(3, 5) || boringSSL;
const argon2 = hasOpenSSL(3, 2) && !fips;
const shake128 = crypto.getHashes().includes('shake128');
const shake256 = crypto.getHashes().includes('shake256');
const cshake128 = crypto.getHashes().includes('cshake128');
const cshake256 = crypto.getHashes().includes('cshake256');
const sha3 = crypto.getHashes().includes('sha3-256');
const ocb = hasOpenSSL(3) && crypto.getCiphers().includes('aes-128-ocb');
const kmac = hasOpenSSL(3) && crypto.getMacs().includes('kmac128');
@@ -22,19 +24,21 @@ export const vectors = {
[false, 'cSHAKE128'],
[shake128, { name: 'cSHAKE128', outputLength: 128 }],
[shake128, { name: 'cSHAKE128', outputLength: 128, functionName: Buffer.alloc(0), customization: Buffer.alloc(0) }],
[shake128 && kmac && !fips, { name: 'cSHAKE128', outputLength: 128, functionName: Buffer.from('KMAC') }],
[cshake128, { name: 'cSHAKE128', outputLength: 128, functionName: Buffer.from('KMAC') }],
[false, { name: 'cSHAKE128', outputLength: 128, functionName: Buffer.from('SHAKE') }],
[shake128 && kmac && !fips, { name: 'cSHAKE128', outputLength: 128, customization: Buffer.alloc(1) }],
[cshake128, { name: 'cSHAKE128', outputLength: 128, customization: Buffer.alloc(1, 1) }],
[false, { name: 'cSHAKE128', outputLength: 128, customization: Buffer.alloc(1) }],
[false, { name: 'cSHAKE128', outputLength: 128, customization: Buffer.alloc(513) }],
[shake128, { name: 'cSHAKE128', outputLength: 127 }],
[false, { name: 'cSHAKE128', outputLength: 127 }],
[false, 'cSHAKE256'],
[shake256, { name: 'cSHAKE256', outputLength: 256 }],
[shake256, { name: 'cSHAKE256', outputLength: 256, functionName: Buffer.alloc(0), customization: Buffer.alloc(0) }],
[shake256 && kmac && !fips, { name: 'cSHAKE256', outputLength: 256, functionName: Buffer.from('KMAC') }],
[cshake256, { name: 'cSHAKE256', outputLength: 256, functionName: Buffer.from('KMAC') }],
[false, { name: 'cSHAKE256', outputLength: 256, functionName: Buffer.from('SHAKE') }],
[shake256 && kmac && !fips, { name: 'cSHAKE256', outputLength: 256, customization: Buffer.alloc(1) }],
[cshake256, { name: 'cSHAKE256', outputLength: 256, customization: Buffer.alloc(1, 1) }],
[false, { name: 'cSHAKE256', outputLength: 256, customization: Buffer.alloc(1) }],
[false, { name: 'cSHAKE256', outputLength: 256, customization: Buffer.alloc(513) }],
[shake256, { name: 'cSHAKE256', outputLength: 255 }],
[false, { name: 'cSHAKE256', outputLength: 255 }],
[false, 'TurboSHAKE128'],
[!fips, { name: 'TurboSHAKE128', outputLength: 128 }],
[!fips, { name: 'TurboSHAKE128', outputLength: 128, domainSeparation: 0x07 }],
@@ -86,9 +90,9 @@ export const vectors = {
[false, 'KMAC128'],
[false, 'KMAC256'],
[kmac, { name: 'KMAC128', outputLength: 256 }],
[kmac && !fips, { name: 'KMAC128', outputLength: 255 }],
[false, { name: 'KMAC128', outputLength: 255 }],
[kmac, { name: 'KMAC256', outputLength: 256 }],
[kmac && !fips, { name: 'KMAC256', outputLength: 255 }],
[false, { name: 'KMAC256', outputLength: 255 }],
],
'generateKey': [
[pqc, 'ML-DSA-44'],
@@ -109,10 +113,14 @@ export const vectors = {
[kmac, 'KMAC256'],
[kmac, { name: 'KMAC128', length: 256 }],
[kmac, { name: 'KMAC256', length: 128 }],
[kmac && !fips, { name: 'KMAC128', length: 0 }],
[kmac && !fips, { name: 'KMAC256', length: 0 }],
[kmac && !fips, { name: 'KMAC128', length: 1 }],
[kmac && !fips, { name: 'KMAC256', length: 1 }],
[false, { name: 'KMAC128', length: 0 }],
[false, { name: 'KMAC256', length: 0 }],
[false, { name: 'KMAC128', length: 1 }],
[false, { name: 'KMAC128', length: 24 }],
[kmac, { name: 'KMAC128', length: 32 }],
[false, { name: 'KMAC256', length: 1 }],
[false, { name: 'KMAC256', length: 24 }],
[kmac, { name: 'KMAC256', length: 32 }],
],
'importKey': [
[pqc, 'ML-DSA-44'],
@@ -133,10 +141,14 @@ export const vectors = {
[kmac, 'KMAC256'],
[kmac, { name: 'KMAC128', length: 256 }],
[kmac, { name: 'KMAC256', length: 128 }],
[kmac && !fips, { name: 'KMAC128', length: 0 }],
[kmac && !fips, { name: 'KMAC256', length: 0 }],
[kmac && !fips, { name: 'KMAC128', length: 1 }],
[kmac && !fips, { name: 'KMAC256', length: 1 }],
[false, { name: 'KMAC128', length: 0 }],
[false, { name: 'KMAC256', length: 0 }],
[false, { name: 'KMAC128', length: 1 }],
[false, { name: 'KMAC128', length: 24 }],
[kmac, { name: 'KMAC128', length: 32 }],
[false, { name: 'KMAC256', length: 1 }],
[false, { name: 'KMAC256', length: 24 }],
[kmac, { name: 'KMAC256', length: 32 }],
],
'exportKey': [
[pqc, 'ML-DSA-44'],
@@ -251,7 +263,7 @@ export const vectors = {
[pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 256 }],
[pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 255 }],
[pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 249 }],
[pqc && kmac && !fips, 'ML-KEM-768', { name: 'KMAC128', length: 255 }],
[false, 'ML-KEM-768', { name: 'KMAC128', length: 255 }],
[hybridKems, 'MLKEM768-P256', 'HKDF'],
[hybridKems, 'MLKEM768-X25519', 'HKDF'],
[hybridKems, 'MLKEM1024-P384', 'HKDF'],
@@ -283,7 +295,7 @@ export const vectors = {
[pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 256 }],
[pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 255 }],
[pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 249 }],
[pqc && kmac && !fips, 'ML-KEM-768', { name: 'KMAC128', length: 255 }],
[false, 'ML-KEM-768', { name: 'KMAC128', length: 255 }],
[hybridKems, 'MLKEM768-P256', 'HKDF'],
[hybridKems, 'MLKEM768-X25519', 'HKDF'],
[hybridKems, 'MLKEM1024-P384', 'HKDF'],
@@ -14,7 +14,6 @@ const {
} = require('crypto');
const { hasFIPS } = require('../common/crypto');
const { kSupportedAlgorithms } = require('internal/crypto/util');
const fips = hasFIPS();
const rejectsXCurves = hasFIPS(3, 5);
const hashes = Object.keys(kSupportedAlgorithms.digest).filter((name) => {
@@ -139,19 +138,11 @@ function genericSecretVectors(name) {
];
}
function macInvalid(algorithm, invalidLengthMessage, allowZeroKey = false) {
function macInvalid(algorithm, invalidLengthMessage, isKmac = false) {
const key = createSecretKey(randomBytes(32));
const usages = ['sign', 'verify'];
if (allowZeroKey && !fips) {
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 if (allowZeroKey) {
if (isKmac) {
for (const zeroAlgorithm of [algorithm, { ...algorithm, length: 0 }]) {
assert.throws(() => {
createSecretKey(Buffer.alloc(0))
@@ -177,7 +168,7 @@ function macInvalid(algorithm, invalidLengthMessage, allowZeroKey = false) {
assert.throws(
() => key.toCryptoKey({ ...algorithm, length: 0 }, true, usages),
allowZeroKey && fips ? {
isKmac ? {
name: 'NotSupportedError',
message: 'Invalid key length',
} : {
@@ -6,18 +6,13 @@ if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const { getFips } = require('crypto');
const { hasOpenSSL } = require('../common/crypto');
const { subtle } = globalThis.crypto;
(async () => {
const keyToWrap = await subtle.importKey(
'raw', new Uint8Array(16), 'AES-GCM', true, ['encrypt']);
let emptyKey;
if (hasOpenSSL(3) && getFips() !== 1) {
emptyKey = await subtle.importKey(
'raw-secret', new Uint8Array(0), 'KMAC128', true, ['sign']);
}
const shortKey = await subtle.importKey(
'raw', new Uint8Array(8), { name: 'HMAC', hash: 'SHA-256' }, true, ['sign']);
for (const length of [128, 192, 256]) {
const wrappingKey = await subtle.generateKey(
@@ -31,11 +26,8 @@ const { subtle } = globalThis.crypto;
'HKDF', false, ['deriveBits']), { name: 'OperationError' });
}
if (emptyKey !== undefined) {
await assert.rejects(subtle.wrapKey(
'raw-secret', emptyKey, wrappingKey, 'AES-KW'),
{ name: 'OperationError' });
}
await assert.rejects(subtle.wrapKey(
'raw', shortKey, wrappingKey, 'AES-KW'), { name: 'OperationError' });
const wrapped = await subtle.wrapKey(
'raw', keyToWrap, wrappingKey, 'AES-KW');
+4 -14
View File
@@ -284,7 +284,7 @@ const fips4 = hasFIPS(4);
})().then(common.mustCall());
}
if (hasOpenSSL(3) && !hasFIPS()) {
if (hasOpenSSL(3)) {
(async () => {
const derivedKeyAlgorithm = { name: 'KMAC128', length: 0 };
const usages = ['sign'];
@@ -314,19 +314,9 @@ if (hasOpenSSL(3) && !hasFIPS()) {
baseKeyAlgorithm,
false,
['deriveKey']);
const derived = await subtle.deriveKey(
algorithm,
baseKey,
derivedKeyAlgorithm,
false,
usages);
assert.strictEqual(derived.algorithm.length, 0);
const signature = subtle.sign({
name: 'KMAC128',
outputLength: 256,
}, derived, new Uint8Array());
assert.strictEqual((await signature).byteLength, 32);
await assert.rejects(
subtle.deriveKey(algorithm, baseKey, derivedKeyAlgorithm, false, usages),
{ name: 'NotSupportedError', message: 'Invalid key length' });
}
})().then(common.mustCall());
}
+66 -32
View File
@@ -9,8 +9,7 @@ const assert = require('assert');
const { Buffer } = require('buffer');
const { subtle } = globalThis.crypto;
const { createHash, getHashes } = require('crypto');
const { hasOpenSSL, hasFIPS, isBoringSSL } = require('../common/crypto');
const fips = hasFIPS();
const { isBoringSSL } = require('../common/crypto');
const kTests = [
['SHA-1', ['sha1'], 160],
@@ -265,16 +264,16 @@ if (getHashes().includes('shake128')) {
new Uint8Array(0),
);
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: 7 }, Buffer.alloc(1)),
{ name: 'NotSupportedError', message: 'Invalid CShakeParams outputLength' });
await assert.rejects(
subtle.digest(
{ name: 'cSHAKE128', outputLength: 0xffffffff },
Buffer.alloc(1)),
{
name: 'OperationError',
name: 'NotSupportedError',
message: 'Invalid CShakeParams outputLength',
});
@@ -291,22 +290,53 @@ if (getHashes().includes('shake128')) {
message: 'Unsupported CShakeParams functionName',
});
if (fips) return;
await assert.rejects(
subtle.digest(
{
name: 'cSHAKE128',
for (const name of ['cSHAKE128', 'cSHAKE256']) {
const supported = getHashes().includes(name.toLowerCase());
assert.deepStrictEqual(
await subtle.digest({
name,
outputLength: 256,
customization: Buffer.alloc(513),
},
Buffer.alloc(1)),
{
name: 'OperationError',
message: 'CShakeParams.customization must be at most 512 bytes',
});
functionName: Buffer.alloc(0),
customization: Buffer.alloc(0),
}, Buffer.alloc(1)),
await subtle.digest({ name, outputLength: 256 }, Buffer.alloc(1)));
if (!hasOpenSSL(3)) return;
await assert.rejects(
subtle.digest({
name,
outputLength: 256,
customization: Buffer.alloc(513, 1),
}, Buffer.alloc(1)),
supported ? {
name: 'OperationError',
message: 'CShakeParams.customization must be at most 512 bytes',
} : {
name: 'NotSupportedError',
message: 'Unsupported CShakeParams customization',
});
await assert.rejects(
subtle.digest({
name,
outputLength: 256,
customization: Buffer.from([0x61, 0x00, 0x62]),
}, Buffer.alloc(1)),
{ name: 'NotSupportedError', message: 'Unsupported CShakeParams customization' });
for (const params of [
{ functionName: Buffer.from('KMAC') },
{ customization: Buffer.from('Node.js') },
]) {
const algorithm = { name, outputLength: 256, ...params };
if (supported) {
assert.strictEqual((await subtle.digest(algorithm, Buffer.alloc(1))).byteLength, 32);
} else {
await assert.rejects(subtle.digest(algorithm, Buffer.alloc(1)), {
name: 'NotSupportedError',
});
}
}
}
const nistCShakeShortInput = Buffer.from('00010203', 'hex');
const nistCShakeLongInput =
@@ -400,19 +430,23 @@ if (getHashes().includes('shake128')) {
'ca6f88db415829',
},
]) {
assert.strictEqual(
Buffer.from(await subtle.digest(algorithm, data)).toString('hex'),
expected);
if (getHashes().includes(algorithm.name.toLowerCase())) {
assert.strictEqual(
Buffer.from(await subtle.digest(algorithm, data)).toString('hex'),
expected);
} else {
await assert.rejects(subtle.digest(algorithm, data), {
name: 'NotSupportedError',
});
}
}
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]);
if (getHashes().includes('cshake128')) {
await assert.rejects(
subtle.digest(
{ ...nistCShakeSample1.algorithm, outputLength: 255 },
nistCShakeSample1.data),
{ name: 'NotSupportedError', message: 'Invalid CShakeParams outputLength' });
}
})().then(common.mustCall());
}
+26 -60
View File
@@ -285,69 +285,35 @@ if (hasOpenSSL(3)) {
[/* empty usages */]),
{ name: 'SyntaxError', message: 'Usages cannot be empty when importing a secret key.' });
{
if (getFips() !== 1) {
const importedZeroImplicit = await subtle.importKey(
for (const algorithm of [name, { name, length: 0 }]) {
await assert.rejects(
subtle.importKey(
'raw-secret', new Uint8Array(), algorithm, true, ['sign', 'verify']),
{ name: 'NotSupportedError', message: 'Invalid key length' });
}
await assert.rejects(
subtle.importKey(
'raw-secret',
new Uint8Array([0xff]),
{ name, length: 0 },
true,
['sign', 'verify']),
{ name: 'NotSupportedError', message: 'Invalid key length' });
await assert.rejects(
subtle.generateKey({ name, length: 9 }, true, ['sign', 'verify']),
{ name: 'NotSupportedError', message: 'Invalid key length' });
for (const byteLength of [1, 2]) {
await assert.rejects(
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(
new Uint8Array(byteLength).fill(0xff),
{ 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' });
}
['sign', 'verify']),
{ name: 'NotSupportedError', message: 'Invalid key length' });
}
}
@@ -12,10 +12,10 @@ if (!hasFIPS(3))
common.skip('requires OpenSSL >= 3 in FIPS mode');
const require = createRequire(import.meta.url);
const { getHashes } = require('node:crypto');
const { internalBinding } = require('internal/test/binding');
const { getCryptoKeyHandle } = require('internal/crypto/keys');
const {
CShakeJob,
KangarooTwelveJob,
KmacJob,
TurboShakeJob,
@@ -52,13 +52,6 @@ for (const createJob of [
kCryptoJobWebCrypto, 'TurboSHAKE128', 0x1f, 16, data),
() => new KangarooTwelveJob(
kCryptoJobWebCrypto, 'KT128', undefined, 16, data),
() => new CShakeJob(
kCryptoJobWebCrypto,
'cSHAKE128',
data,
Buffer.from('KMAC'),
undefined,
128),
]) {
assert.throws(createJob, {
code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION',
@@ -66,48 +59,34 @@ for (const createJob of [
});
}
const emptyCShake = {
name: 'cSHAKE128',
outputLength: 256,
customization: data,
functionName: data,
};
assert.strictEqual(SubtleCrypto.supports('digest', emptyCShake), true);
for (const length of [1, 513]) {
const algorithm = {
name: 'cSHAKE128',
for (const name of ['cSHAKE128', 'cSHAKE256']) {
const emptyCShake = {
name,
outputLength: 256,
customization: new Uint8Array(length),
customization: data,
functionName: data,
};
await assertFipsException(
'digest',
algorithm,
() => subtle.digest(algorithm, data),
'Unsupported CShakeParams customization');
assert.strictEqual(SubtleCrypto.supports('digest', emptyCShake), true);
assert.strictEqual((await subtle.digest(emptyCShake, data)).byteLength, 32);
for (const params of [
{ customization: Buffer.from('Node.js') },
{ functionName: Buffer.from('KMAC') },
{ functionName: Buffer.from('KMAC'), customization: Buffer.from('Node.js') },
]) {
const algorithm = { name, outputLength: 256, ...params };
const supported = getHashes().includes(name.toLowerCase());
assert.strictEqual(SubtleCrypto.supports('digest', algorithm), supported);
if (supported) {
assert.strictEqual((await subtle.digest(algorithm, data)).byteLength, 32);
} else {
await assert.rejects(subtle.digest(algorithm, data), {
name: 'NotSupportedError',
});
}
}
}
const functionName = {
name: 'cSHAKE256',
outputLength: 256,
functionName: Buffer.from('KMAC'),
};
await assertFipsException(
'digest',
functionName,
() => subtle.digest(functionName, data),
'Unsupported CShakeParams functionName');
const bothCShakeParams = {
...functionName,
customization: new Uint8Array(1),
};
await assertFipsException(
'digest',
bothCShakeParams,
() => subtle.digest(bothCShakeParams, data),
'Unsupported CShakeParams customization');
for (const length of [0, 24, 33]) {
const algorithm = { name: 'KMAC128', length };
await assertFipsException(
@@ -170,7 +149,6 @@ await assert.rejects(
getCryptoKeyHandle(key),
'KMAC128',
undefined,
32,
9,
data,
undefined).run(),
+3 -3
View File
@@ -6,7 +6,7 @@ if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const { getFips, setFips } = require('crypto');
const { getFips, getHashes, setFips } = require('crypto');
const { internalBinding } = require('internal/test/binding');
const { getOptionValue } = require('internal/options');
if (!internalBinding('crypto').testFipsCrypto())
@@ -25,8 +25,8 @@ try {
name: 'KT128', outputLength: 128,
}), !fips);
assert.strictEqual(SubtleCrypto.supports('digest', {
name: 'cSHAKE128', outputLength: 128, customization: new Uint8Array(1),
}), !fips);
name: 'cSHAKE128', outputLength: 128, customization: new Uint8Array([1]),
}), getHashes().includes('cshake128'));
assert.strictEqual(SubtleCrypto.supports('generateKey', {
name: 'RSA-PSS', hash: 'SHA-256', modulusLength: 1024,
publicExponent: new Uint8Array([1, 0, 1]),
@@ -0,0 +1,90 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const { createSecretKey, subtle } = require('crypto');
const { SubtleCrypto } = globalThis;
const unsupported = { name: 'NotSupportedError' };
(async () => {
const data = new Uint8Array([1, 2, 3]);
for (const name of ['cSHAKE128', 'cSHAKE256']) {
if (!SubtleCrypto.supports('digest', { name, outputLength: 128 }))
continue;
for (const customization of [undefined, new Uint8Array([1])]) {
if (!SubtleCrypto.supports('digest', { name, outputLength: 128, customization }))
continue;
for (let remainder = 1; remainder < 8; remainder++) {
const algorithm = { name, outputLength: 128 + remainder, customization };
assert.strictEqual(SubtleCrypto.supports('digest', algorithm), false);
await assert.rejects(subtle.digest(algorithm, data), unsupported);
}
const digest = await subtle.digest({ name, outputLength: 128, customization }, data);
assert.strictEqual(digest.byteLength, 16);
}
// Check the uint32 boundary without allocating a large digest.
assert.strictEqual(SubtleCrypto.supports('digest', { name, outputLength: 0xfffffff8 }), true);
for (const outputLength of [0xfffffff9, 0xffffffff]) {
assert.strictEqual(SubtleCrypto.supports('digest', { name, outputLength }), false);
await assert.rejects(subtle.digest({ name, outputLength }, data), unsupported);
}
}
const baseKey = await subtle.importKey('raw-secret', data, 'HKDF', false, ['deriveKey']);
const derivation = { name: 'HKDF', hash: 'SHA-256', salt: data, info: data };
const raw = Buffer.alloc(32, 1);
const secretKey = createSecretKey(raw);
for (const name of ['KMAC128', 'KMAC256']) {
if (!SubtleCrypto.supports('importKey', name))
continue;
const key = await subtle.importKey('raw-secret', raw, name, false, ['sign', 'verify']);
for (let remainder = 1; remainder < 8; remainder++) {
const algorithm = { name, length: 248 + remainder };
assert.strictEqual(SubtleCrypto.supports('generateKey', algorithm), false);
assert.strictEqual(SubtleCrypto.supports('importKey', algorithm), false);
assert.strictEqual(SubtleCrypto.supports('deriveKey', derivation, algorithm), false);
await assert.rejects(subtle.generateKey(algorithm, false, ['sign']), unsupported);
await assert.rejects(subtle.importKey('raw-secret', raw, algorithm, false, ['sign']), unsupported);
await assert.rejects(subtle.importKey(
'jwk', { kty: 'oct', k: raw.toString('base64url') }, algorithm, false, ['sign']), unsupported);
await assert.rejects(subtle.deriveKey(derivation, baseKey, algorithm, false, ['sign']), unsupported);
assert.throws(() => secretKey.toCryptoKey(algorithm, false, ['sign']), unsupported);
const params = { name, outputLength: 248 + remainder };
assert.strictEqual(SubtleCrypto.supports('sign', params), false);
assert.strictEqual(SubtleCrypto.supports('verify', params), false);
await assert.rejects(subtle.sign(params, key, data), unsupported);
await assert.rejects(subtle.verify(params, key, raw, data), unsupported);
}
}
if (SubtleCrypto.supports('generateKey', 'ML-KEM-768')) {
const { publicKey, privateKey } = await subtle.generateKey(
'ML-KEM-768', false, ['encapsulateBits', 'encapsulateKey', 'decapsulateKey']);
const { ciphertext } = await subtle.encapsulateBits('ML-KEM-768', publicKey);
for (const name of ['KMAC128', 'KMAC256']) {
if (!SubtleCrypto.supports('importKey', name))
continue;
const algorithm = { name, length: 255 };
assert.strictEqual(SubtleCrypto.supports('encapsulateKey', 'ML-KEM-768', algorithm), false);
assert.strictEqual(SubtleCrypto.supports('decapsulateKey', 'ML-KEM-768', algorithm), false);
await assert.rejects(subtle.encapsulateKey(
'ML-KEM-768', publicKey, algorithm, false, ['sign']), unsupported);
await assert.rejects(subtle.decapsulateKey(
'ML-KEM-768', privateKey, ciphertext, algorithm, false, ['sign']), unsupported);
assert.strictEqual(SubtleCrypto.supports('encapsulateKey', 'ML-KEM-768', { name, length: 256 }), true);
assert.strictEqual(SubtleCrypto.supports('decapsulateKey', 'ML-KEM-768', { name, length: 256 }), true);
}
}
})().then(common.mustCall());
+11 -7
View File
@@ -5,7 +5,7 @@ const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const { hasFIPS, hasOpenSSL } = require('../common/crypto');
const { hasOpenSSL } = require('../common/crypto');
if (!hasOpenSSL(3))
common.skip('requires OpenSSL >= 3');
@@ -13,7 +13,6 @@ if (!hasOpenSSL(3))
const assert = require('assert');
const { types: { isCryptoKey } } = require('util');
const { subtle } = globalThis.crypto;
const fips = hasFIPS();
const usages = ['sign', 'verify'];
@@ -23,9 +22,6 @@ async function test(name, length) {
if (length !== undefined)
algorithm.length = length;
if (fips && length !== undefined &&
(length < 32 || length % 8 !== 0)) return;
const generatedKey = await subtle.generateKey(algorithm, true, usages);
assert(generatedKey);
@@ -45,12 +41,10 @@ async function test(name, length) {
}
const kTests = [
['KMAC128', 0],
['KMAC128', 32],
['KMAC128', 128],
['KMAC128', 256],
['KMAC128'],
['KMAC256', 0],
['KMAC256', 32],
['KMAC256', 128],
['KMAC256', 256],
@@ -60,3 +54,13 @@ const kTests = [
const tests = Promise.all(kTests.map((args) => test(...args)));
tests.then(common.mustCall());
(async () => {
for (const name of ['KMAC128', 'KMAC256']) {
for (const length of [0, 8, 16, 24]) {
await assert.rejects(
subtle.generateKey({ name, length }, true, usages),
{ name: 'NotSupportedError', message: 'Invalid key length' });
}
}
})().then(common.mustCall());
@@ -13,8 +13,7 @@ if (!common.hasCrypto) common.skip('missing crypto');
const require = createRequire(import.meta.url);
const { kSupportedAlgorithms } = require('internal/crypto/util');
const { getFips } = require('node:crypto');
const { hasOpenSSL } = require('../common/crypto');
const { getFips, getHashes } = require('node:crypto');
const { subtle } = globalThis.crypto;
const TypedArrayPrototype = Object.getPrototypeOf(Uint8Array.prototype);
@@ -134,15 +133,15 @@ if (supports('digest', 'cSHAKE128')) {
message: /Unsupported CShakeParams functionName/,
})));
// asyncDigest() picks the cSHAKE job over plain SHAKE on a non-empty
// asyncDigest() picks cSHAKE over plain SHAKE on a non-empty
// customization.
if (hasOpenSSL(3)) {
{
const algorithm = {
name: 'cSHAKE128',
outputLength: 256,
customization: new Uint8Array([1, 2, 3]),
};
if (getFips() === 1) {
if (!getHashes().includes('cshake128')) {
await withPoisoned(poisonTypedArrayByteLength(0), common.mustCall(() =>
assert.rejects(subtle.digest(algorithm, data), {
name: 'NotSupportedError',
@@ -5,38 +5,16 @@ const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const { hasFIPS, hasOpenSSL } = require('../common/crypto');
const { hasOpenSSL } = require('../common/crypto');
if (!hasOpenSSL(3))
common.skip('requires OpenSSL >= 3');
const assert = require('assert');
const { subtle } = globalThis.crypto;
const fips = hasFIPS();
const fips4 = hasFIPS(4);
const vectors = require('../fixtures/crypto/kmac')();
function isFipsProviderUnsupported(err) {
return err.name === 'OperationError' &&
err.cause?.code === 'ERR_OSSL_EVP_UNSUPPORTED';
}
function usesNonFipsImplementation({ key, keyLength, outputLength }) {
const keyLengthInBits = keyLength ?? key.byteLength * 8;
return outputLength === 0 ||
outputLength % 8 !== 0 ||
keyLengthInBits < 32 ||
keyLengthInBits % 8 !== 0;
}
function isFips4Incompatible({ key, keyLength, outputLength }) {
const keyLengthInBits = keyLength ?? key.byteLength * 8;
return keyLengthInBits < 128 ||
keyLengthInBits % 8 !== 0 ||
outputLength % 8 !== 0;
}
async function testVerify({ algorithm,
key,
keyLength,
@@ -215,17 +193,8 @@ async function testSign({ algorithm,
const variations = [];
for (const vector of vectors) {
if (fips && usesNonFipsImplementation(vector)) continue;
if (fips4 && isFips4Incompatible(vector)) {
variations.push(assert.rejects(
testVerify(vector), isFipsProviderUnsupported));
variations.push(assert.rejects(
testSign(vector), isFipsProviderUnsupported));
} else {
variations.push(testVerify(vector));
variations.push(testSign(vector));
}
variations.push(testVerify(vector));
variations.push(testSign(vector));
}
await Promise.all(variations);
@@ -240,87 +209,54 @@ async function testSign({ algorithm,
['sign', 'verify']);
const algorithm = {
name: 'KMAC128',
outputLength: fips ? 16 : 9,
outputLength: 16,
customization: new Uint8Array(),
};
const data = new Uint8Array([1, 2, 3]);
const signature = await subtle.sign(algorithm, key, data);
assert.strictEqual(signature.byteLength, 2);
if (!fips)
assert.strictEqual(new Uint8Array(signature)[1] & 0b01111111, 0);
assert(await subtle.verify(algorithm, key, signature, data));
if (fips) {
const signature128 = await subtle.sign({
...algorithm,
outputLength: 128,
}, key, data);
assert.strictEqual(signature128.byteLength, 16);
assert(await subtle.verify({
...algorithm,
outputLength: 128,
}, key, signature128, data));
} else {
const signature16 = new Uint8Array(await subtle.sign({
...algorithm,
outputLength: 16,
}, key, data));
signature16[1] &= 0b10000000;
assert.notDeepStrictEqual(new Uint8Array(signature), signature16);
}
const signature128 = await subtle.sign({
...algorithm,
outputLength: 128,
}, key, data);
assert.strictEqual(signature128.byteLength, 16);
assert(await subtle.verify({
...algorithm,
outputLength: 128,
}, key, signature128, data));
const invalidSignature = new Uint8Array(signature);
if (fips)
invalidSignature[0] ^= 0b00000001;
else
invalidSignature[1] |= 0b00000001;
invalidSignature[0] ^= 0b00000001;
assert(!(await subtle.verify(algorithm, key, invalidSignature, data)));
if (!fips) {
const nonByteKey = await subtle.importKey(
const nonByteOutput = { ...algorithm, outputLength: 9 };
await assert.rejects(
subtle.sign(nonByteOutput, key, data),
{ name: 'NotSupportedError', message: 'Invalid KmacParams outputLength' });
await assert.rejects(
subtle.verify(nonByteOutput, key, signature, data),
{ name: 'NotSupportedError', message: 'Invalid KmacParams outputLength' });
await assert.rejects(
subtle.importKey(
'raw-secret',
new Uint8Array([0xff, 0xff, 0xff, 0xff]),
{ name: 'KMAC128', length: 25 },
false,
['sign', 'verify']);
const nonByteKeySignature = subtle.sign({
...algorithm,
outputLength: 16,
}, nonByteKey, data);
const result = await nonByteKeySignature;
assert.strictEqual(result.byteLength, 2);
assert(await subtle.verify({
...algorithm,
outputLength: 16,
}, nonByteKey, result, data));
}
['sign', 'verify']),
{ name: 'NotSupportedError', message: 'Invalid key length' });
})().then(common.mustCall());
(async function() {
if (fips) return;
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 = subtle.sign(algorithm, key, data);
const result = await signature;
assert.strictEqual(result.byteLength, 32);
assert(await subtle.verify(algorithm, key, result, data));
for (const byteLength of [0, 1, 2, 3]) {
await assert.rejects(
subtle.importKey(
'raw-secret', new Uint8Array(byteLength), name, true, ['sign', 'verify']),
{ name: 'NotSupportedError', message: 'Invalid key length' });
}
}
})().then(common.mustCall());
+24 -14
View File
@@ -485,20 +485,30 @@ async function testNonByteLengthWrapUnwrap({
});
if (hasOpenSSL(3) && getFips() !== 1) {
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,
});
for (const name of ['KMAC128', 'KMAC256']) {
const keyData = new Uint8Array(32).fill(0xff);
const kmacKey = await subtle.importKey(
'raw-secret', keyData, name, true, ['sign', 'verify']);
const wrappingKey = await subtle.generateKey(
{ name: 'AES-GCM', length: 128 }, true, ['wrapKey', 'unwrapKey']);
for (const [i, format] of ['raw-secret', 'jwk'].entries()) {
const wrapAlgorithm = { name: 'AES-GCM', iv: new Uint8Array(12).fill(i) };
const wrapped = await subtle.wrapKey(format, kmacKey, wrappingKey, wrapAlgorithm);
await assert.rejects(
subtle.unwrapKey(
format, wrapped, wrappingKey, wrapAlgorithm,
{ name, length: 255 }, true, ['sign', 'verify']),
{ name: 'NotSupportedError', message: 'Invalid key length' });
const unwrapped = await subtle.unwrapKey(
format, wrapped, wrappingKey, wrapAlgorithm,
{ name, length: 256 }, true, ['sign', 'verify']);
assert.strictEqual(unwrapped.algorithm.length, 256);
assert.deepStrictEqual(
new Uint8Array(await subtle.exportKey('raw-secret', unwrapped)), keyData);
}
}
}
})().then(common.mustCall());
-13
View File
@@ -182,17 +182,6 @@ declare namespace InternalCryptoBinding {
): CryptoJobWebCrypto<ArrayBuffer>;
}
interface CShakeJobConstructor {
new(
mode: CryptoJobWebCryptoMode,
algorithm: string,
data: ByteSource,
functionName: OptionalByteSource,
customization: OptionalByteSource,
outputLength: number,
): CryptoJobWebCrypto<ArrayBuffer>;
}
interface ChaCha20Poly1305CipherJobConstructor {
new(
mode: CryptoJobWebCryptoMode,
@@ -375,7 +364,6 @@ declare namespace InternalCryptoBinding {
key: KeyObjectHandle,
algorithm: string,
customization: OptionalByteSource,
keyLength: number,
outputLength: number,
data: ByteSource,
...signature: MacJobSignatureArgs<S>
@@ -818,7 +806,6 @@ declare namespace InternalCryptoBinding {
export interface CryptoBinding {
AESCipherJob: InternalCryptoBinding.AESCipherJobConstructor;
Argon2Job: InternalCryptoBinding.Argon2JobConstructor;
CShakeJob?: InternalCryptoBinding.CShakeJobConstructor;
ChaCha20Poly1305CipherJob: InternalCryptoBinding.ChaCha20Poly1305CipherJobConstructor;
CheckPrimeJob: InternalCryptoBinding.CheckPrimeJobConstructor;
DHBitsJob: InternalCryptoBinding.DHBitsJobConstructor;