mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 10:15:07 +08:00
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:
1 parent
e0748dfd9d
commit
38187f81eb
25 files changed
+368
-956
No files matched your search
+17
-10
@@ -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`
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -193,7 +193,6 @@ function kmacSignVerify(key, data, algorithm, signature) {
|
||||
getCryptoKeyHandle(key),
|
||||
algorithm.name,
|
||||
algorithm.customization,
|
||||
getCryptoKeyAlgorithm(key).length,
|
||||
algorithm.outputLength,
|
||||
data,
|
||||
signature));
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Vendored
-55
@@ -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
@@ -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');
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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());
|
||||
@@ -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());
|
||||
@@ -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());
|
||||
|
||||
|
||||
Vendored
-13
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user