crypto: support Ed448 and ML-DSA context parameter in Web Cryptography

PR-URL: https://github.com/nodejs/node/pull/59570
Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
Filip Skokan 2025-08-11 10:12:44 +02:00 committed by Node.js GitHub Bot
parent 3837993b65
commit 0124e0e0d7
21 changed files with 362 additions and 206 deletions

View File

@ -4288,6 +4288,54 @@ std::optional<EVP_PKEY_CTX*> EVPMDCtxPointer::verifyInit(
return ctx;
}
std::optional<EVP_PKEY_CTX*> EVPMDCtxPointer::signInitWithContext(
const EVPKeyPointer& key,
const Digest& digest,
const Buffer<const unsigned char>& context_string) {
#ifdef OSSL_SIGNATURE_PARAM_CONTEXT_STRING
EVP_PKEY_CTX* ctx = nullptr;
const OSSL_PARAM params[] = {
OSSL_PARAM_construct_octet_string(
OSSL_SIGNATURE_PARAM_CONTEXT_STRING,
const_cast<unsigned char*>(context_string.data),
context_string.len),
OSSL_PARAM_END};
if (!EVP_DigestSignInit_ex(
ctx_.get(), &ctx, nullptr, nullptr, nullptr, key.get(), params)) {
return std::nullopt;
}
return ctx;
#else
return std::nullopt;
#endif
}
std::optional<EVP_PKEY_CTX*> EVPMDCtxPointer::verifyInitWithContext(
const EVPKeyPointer& key,
const Digest& digest,
const Buffer<const unsigned char>& context_string) {
#ifdef OSSL_SIGNATURE_PARAM_CONTEXT_STRING
EVP_PKEY_CTX* ctx = nullptr;
const OSSL_PARAM params[] = {
OSSL_PARAM_construct_octet_string(
OSSL_SIGNATURE_PARAM_CONTEXT_STRING,
const_cast<unsigned char*>(context_string.data),
context_string.len),
OSSL_PARAM_END};
if (!EVP_DigestVerifyInit_ex(
ctx_.get(), &ctx, nullptr, nullptr, nullptr, key.get(), params)) {
return std::nullopt;
}
return ctx;
#else
return std::nullopt;
#endif
}
DataPointer EVPMDCtxPointer::signOneShot(
const Buffer<const unsigned char>& buf) const {
if (!ctx_) return {};

View File

@ -1409,6 +1409,15 @@ class EVPMDCtxPointer final {
std::optional<EVP_PKEY_CTX*> verifyInit(const EVPKeyPointer& key,
const Digest& digest);
std::optional<EVP_PKEY_CTX*> signInitWithContext(
const EVPKeyPointer& key,
const Digest& digest,
const Buffer<const unsigned char>& context_string);
std::optional<EVP_PKEY_CTX*> verifyInitWithContext(
const EVPKeyPointer& key,
const Digest& digest,
const Buffer<const unsigned char>& context_string);
DataPointer signOneShot(const Buffer<const unsigned char>& buf) const;
DataPointer sign(const Buffer<const unsigned char>& buf) const;
bool verify(const Buffer<const unsigned char>& buf,

View File

@ -1342,7 +1342,7 @@ changes:
<!--lint disable maximum-line-length remark-lint-->
* `algorithm` {string|Algorithm|RsaPssParams|EcdsaParams|Ed448Params|ContextParams|KmacParams}
* `algorithm` {string|Algorithm|RsaPssParams|EcdsaParams|ContextParams|KmacParams}
* `key` {CryptoKey}
* `data` {ArrayBuffer|TypedArray|DataView|Buffer}
* Returns: {Promise} Fulfills with an {ArrayBuffer} upon success.
@ -1463,7 +1463,7 @@ changes:
<!--lint disable maximum-line-length remark-lint-->
* `algorithm` {string|Algorithm|RsaPssParams|EcdsaParams|Ed448Params|ContextParams}
* `algorithm` {string|Algorithm|RsaPssParams|EcdsaParams|ContextParams|KmacParams}
* `key` {CryptoKey}
* `signature` {ArrayBuffer|TypedArray|DataView|Buffer}
* `data` {ArrayBuffer|TypedArray|DataView|Buffer}
@ -1830,20 +1830,23 @@ added: v24.7.0
added: v24.7.0
-->
* Type: {string} Must be `'ML-DSA-44'`[^modern-algos], `'ML-DSA-65'`[^modern-algos], or `'ML-DSA-87'`[^modern-algos].
* Type: {string} Must be `Ed448`[^secure-curves], `'ML-DSA-44'`[^modern-algos],
`'ML-DSA-65'`[^modern-algos], or `'ML-DSA-87'`[^modern-algos].
#### `contextParams.context`
<!-- YAML
added: v24.7.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/59570
description: Non-empty context is now supported.
-->
* Type: {ArrayBuffer|TypedArray|DataView|Buffer|undefined}
The `context` member represents the optional context data to associate with
the message.
The Node.js Web Crypto API implementation only supports zero-length context
which is equivalent to not providing context at all.
### Class: `CShakeParams`
@ -2024,37 +2027,6 @@ added: v15.0.0
* Type: {string} Must be one of `'P-256'`, `'P-384'`, `'P-521'`.
### Class: `Ed448Params`
<!-- YAML
added: v15.0.0
-->
#### `ed448Params.name`
<!-- YAML
added:
- v18.4.0
- v16.17.0
-->
* Type: {string} Must be `'Ed448'`[^secure-curves].
#### `ed448Params.context`
<!-- YAML
added:
- v18.4.0
- v16.17.0
-->
* Type: {ArrayBuffer|TypedArray|DataView|Buffer|undefined}
The `context` member represents the optional context data to associate with
the message.
The Node.js Web Crypto API implementation only supports zero-length context
which is equivalent to not providing context at all.
### Class: `EncapsulatedBits`
<!-- YAML

View File

@ -359,6 +359,7 @@ function eddsaSignVerify(key, data, algorithm, signature) {
undefined,
undefined,
undefined,
algorithm.name === 'Ed448' ? algorithm.context : undefined,
signature));
}

View File

@ -302,6 +302,7 @@ function ecdsaSignVerify(key, data, { name, hash }, signature) {
undefined, // Salt length, not used with ECDSA
undefined, // PSS Padding, not used with ECDSA
kSigEncP1363,
undefined,
signature));
}

View File

@ -303,6 +303,7 @@ function mlDsaSignVerify(key, data, algorithm, signature) {
undefined,
undefined,
undefined,
algorithm.context,
signature));
}

View File

@ -355,6 +355,7 @@ function rsaSignVerify(key, data, { saltLength }, signature) {
saltLength,
key[kAlgorithm].name === 'RSA-PSS' ? RSA_PKCS1_PSS_PADDING : undefined,
undefined,
undefined,
signature);
});
}

View File

@ -171,7 +171,9 @@ function signOneShot(algorithm, data, key, callback) {
algorithm,
pssSaltLength,
rsaPadding,
dsaSigEnc);
dsaSigEnc,
undefined,
undefined);
if (!callback) {
const { 0: err, 1: signature } = job.run();
@ -276,6 +278,7 @@ function verifyOneShot(algorithm, data, key, signature, callback) {
pssSaltLength,
rsaPadding,
dsaSigEnc,
undefined,
signature);
if (!callback) {

View File

@ -268,8 +268,8 @@ const kAlgorithmDefinitions = {
'generateKey': null,
'exportKey': null,
'importKey': null,
'sign': 'Ed448Params',
'verify': 'Ed448Params',
'sign': 'ContextParams',
'verify': 'ContextParams',
},
'HKDF': {
'importKey': null,
@ -494,7 +494,6 @@ const simpleAlgorithmDictionaries = {
salt: 'BufferSource',
info: 'BufferSource',
},
Ed448Params: { context: 'BufferSource' },
ContextParams: { context: 'BufferSource' },
Pbkdf2Params: { hash: 'HashAlgorithmIdentifier', salt: 'BufferSource' },
RsaOaepParams: { label: 'BufferSource' },

View File

@ -19,6 +19,7 @@ const {
MathTrunc,
Number,
NumberIsFinite,
NumberParseInt,
ObjectPrototypeHasOwnProperty,
ObjectPrototypeIsPrototypeOf,
SafeArrayIterator,
@ -304,13 +305,12 @@ function createDictionaryConverter(name, dictionaries) {
const context = `'${key}' of '${name}'${
opts.context ? ` (${opts.context})` : ''
}`;
const { converter, validator } = member;
const idlMemberValue = converter(esMemberValue, {
const idlMemberValue = member.converter(esMemberValue, {
__proto__: null,
...opts,
context,
});
validator?.(idlMemberValue, esDict);
member.validator?.(idlMemberValue, esDict);
setOwnProperty(idlDict, key, idlMemberValue);
} else if (member.required) {
throw makeException(
@ -769,17 +769,25 @@ converters.EcdhKeyDeriveParams = createDictionaryConverter(
},
]);
for (const name of ['Ed448Params', 'ContextParams']) {
converters[name] = createDictionaryConverter(
name, [
...new SafeArrayIterator(dictAlgorithm),
{
key: 'context',
converter: converters.BufferSource,
validator: validateZeroLength(`${name}.context`),
converters.ContextParams = createDictionaryConverter(
'ContextParams', [
...new SafeArrayIterator(dictAlgorithm),
{
key: 'context',
converter: converters.BufferSource,
validator(V, dict) {
let { 0: major, 1: minor } = process.versions.openssl.split('.');
major = NumberParseInt(major, 10);
minor = NumberParseInt(minor, 10);
if (major > 3 || (major === 3 && minor >= 2)) {
this.validator = undefined;
} else {
this.validator = validateZeroLength('ContextParams.context');
this.validator(V, dict);
}
},
]);
}
},
]);
converters.Argon2Params = createDictionaryConverter(
'Argon2Params', [

View File

@ -199,6 +199,10 @@ void CheckThrow(Environment* env, SignBase::Error error) {
case SignBase::Error::MalformedSignature:
return THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Malformed signature");
case SignBase::Error::ContextUnsupported:
return THROW_ERR_CRYPTO_OPERATION_FAILED(
env, "Context parameter is unsupported");
case SignBase::Error::Init:
case SignBase::Error::Update:
case SignBase::Error::PrivateKey:
@ -231,6 +235,24 @@ void CheckThrow(Environment* env, SignBase::Error error) {
bool UseP1363Encoding(const EVPKeyPointer& key, const DSASigEnc dsa_encoding) {
return key.isSigVariant() && dsa_encoding == DSASigEnc::P1363;
}
bool SupportsContextString(const EVPKeyPointer& key) {
#if OPENSSL_VERSION_NUMBER < 0x3020000fL
return false;
#else
switch (key.id()) {
case EVP_PKEY_ED448:
#if OPENSSL_WITH_PQC
case EVP_PKEY_ML_DSA_44:
case EVP_PKEY_ML_DSA_65:
case EVP_PKEY_ML_DSA_87:
#endif
return true;
default:
return false;
}
#endif
}
} // namespace
SignBase::Error SignBase::Init(const char* digest) {
@ -534,7 +556,8 @@ SignConfiguration::SignConfiguration(SignConfiguration&& other) noexcept
flags(other.flags),
padding(other.padding),
salt_length(other.salt_length),
dsa_encoding(other.dsa_encoding) {}
dsa_encoding(other.dsa_encoding),
context_string(std::move(other.context_string)) {}
SignConfiguration& SignConfiguration::operator=(
SignConfiguration&& other) noexcept {
@ -548,6 +571,7 @@ void SignConfiguration::MemoryInfo(MemoryTracker* tracker) const {
if (job_mode == kCryptoJobAsync) {
tracker->TrackFieldWithSize("data", data.size());
tracker->TrackFieldWithSize("signature", signature.size());
tracker->TrackFieldWithSize("context_string", context_string.size());
}
}
@ -615,8 +639,20 @@ Maybe<void> SignTraits::AdditionalConfig(
}
}
if (!args[offset + 10]->IsUndefined()) { // Context string
ArrayBufferOrViewContents<char> context_string(args[offset + 10]);
if (context_string.size() > 255) [[unlikely]] {
THROW_ERR_OUT_OF_RANGE(env, "context string must be at most 255 bytes");
return Nothing<void>();
}
params->flags |= SignConfiguration::kHasContextString;
params->context_string = mode == kCryptoJobAsync
? context_string.ToCopy()
: context_string.ToByteSource();
}
if (params->mode == SignConfiguration::Mode::Verify) {
ArrayBufferOrViewContents<char> signature(args[offset + 10]);
ArrayBufferOrViewContents<char> signature(args[offset + 11]);
if (!signature.CheckSizeInt32()) [[unlikely]] {
THROW_ERR_OUT_OF_RANGE(env, "signature is too big");
return Nothing<void>();
@ -647,12 +683,34 @@ bool SignTraits::DeriveBits(Environment* env,
return false;
const auto& key = params.key.GetAsymmetricKey();
bool has_context = (params.flags & SignConfiguration::kHasContextString &&
params.context_string.size() > 0);
if (has_context && !SupportsContextString(key)) {
if (can_throw) crypto::CheckThrow(env, SignBase::Error::ContextUnsupported);
return false;
}
auto ctx = ([&] {
switch (params.mode) {
case SignConfiguration::Mode::Sign:
return context.signInit(key, params.digest);
case SignConfiguration::Mode::Verify:
return context.verifyInit(key, params.digest);
if (has_context) {
ncrypto::Buffer<const unsigned char> context_buf{
.data = params.context_string.data<unsigned char>(),
.len = params.context_string.size(),
};
switch (params.mode) {
case SignConfiguration::Mode::Sign:
return context.signInitWithContext(key, params.digest, context_buf);
case SignConfiguration::Mode::Verify:
return context.verifyInitWithContext(key, params.digest, context_buf);
}
} else {
switch (params.mode) {
case SignConfiguration::Mode::Sign:
return context.signInit(key, params.digest);
case SignConfiguration::Mode::Verify:
return context.verifyInit(key, params.digest);
}
}
UNREACHABLE();
})();

View File

@ -25,7 +25,8 @@ class SignBase : public BaseObject {
Update,
PrivateKey,
PublicKey,
MalformedSignature
MalformedSignature,
ContextUnsupported,
};
SignBase(Environment* env, v8::Local<v8::Object> wrap);
@ -99,7 +100,8 @@ struct SignConfiguration final : public MemoryRetainer {
enum Flags {
kHasNone = 0,
kHasSaltLength = 1,
kHasPadding = 2
kHasPadding = 2,
kHasContextString = 4
};
CryptoJobMode job_mode;
@ -112,6 +114,7 @@ struct SignConfiguration final : public MemoryRetainer {
int padding = 0;
int salt_length = 0;
DSASigEnc dsa_encoding = DSASigEnc::DER;
ByteSource context_string;
SignConfiguration() = default;

View File

@ -28,18 +28,23 @@ module.exports = function() {
'b219e30a1beea8fe869082d99fc8282f9050d024e59eaf0730ba9db70a', 'hex');
// For verification tests.
// eslint-disable @stylistic/js/max-len
const signatures = {
'Ed25519': Buffer.from(
'3d90de5e5743dfc28225bfadb341b116cbf8a3f1ceedbf4adc350ef5d3471843a418' +
'614dcb6e614862614cf7af1496f9340b3c844ea4dceab1d3d155eb7ecc00', 'hex'),
'Ed448': Buffer.from(
'76897e8c50ac6b1132735c09c55f506c0149d2677c75664f8bc10b826fbd9df0a03c' +
'd986bce8339e64c7d1720ea9361784dc73837765ac2980c0dac0814a8bc187d1c9c9' +
'07c5dcc07956f85b70930fe42de764177217cb2d52bab7c1debe0ca89ccecbcd63f7' +
'025a2a5a572b9d23b0642f00', 'hex')
'Ed25519': {
// Ed25519 does not support context
'0': Buffer.from('3d90de5e5743dfc28225bfadb341b116cbf8a3f1ceedbf4adc350ef5d3471843a418614dcb6e614862614cf7af1496f9340b3c844ea4dceab1d3d155eb7ecc00', 'hex'),
},
'Ed448': {
'0': Buffer.from('76897e8c50ac6b1132735c09c55f506c0149d2677c75664f8bc10b826fbd9df0a03cd986bce8339e64c7d1720ea9361784dc73837765ac2980c0dac0814a8bc187d1c9c907c5dcc07956f85b70930fe42de764177217cb2d52bab7c1debe0ca89ccecbcd63f7025a2a5a572b9d23b0642f00', 'hex'),
'32': Buffer.from('0294186f0305dd3a2d5ac86eeb7e73c05d419e84152c2341ae24e55c3889e878f4acb537f3651a50b0b1c26739721b168499337537c92727003480be61fc23f519ed772ebf2977f6bda5259235ded904959227beaf0adfbd28288358854e9abe089dc8075998993b86280b0bd89bdacc3c00', 'hex'),
'255': Buffer.from('6dfef748ab53ca856b3ffd84c62ae167c2737dfe4eae89c6c1edc0adc685b73f8170eacd723ec76fb31318ebe47c908722000129b2e9806e8040a4d4d90ac1d1b539199e33553300dcdf4989e4b77c835b53f4ee0d114845ad97047ad0d112e05304b38f5836bbe024a6f700a368d9910100', 'hex'),
}
}
// eslint-disable @stylistic/js/max-len
const algorithms = ['Ed25519'];
const contexts = [new Uint8Array(0), new Uint8Array(32), new Uint8Array(255)];
if (!process.features.openssl_is_boringssl) {
algorithms.push('Ed448')
@ -47,13 +52,23 @@ module.exports = function() {
common.printSkipMessage(`Skipping unsupported Ed448 test cases`);
}
const vectors = algorithms.map((algorithm) => ({
publicKeyBuffer: spki[algorithm],
privateKeyBuffer: pkcs8[algorithm],
name: algorithm,
data,
signature: signatures[algorithm],
}));
const vectors = [];
for (const algorithm of algorithms) {
for (const context of contexts) {
if (algorithm === 'Ed25519' && context.byteLength !== 0) {
continue;
}
vectors.push({
publicKeyBuffer: spki[algorithm],
privateKeyBuffer: pkcs8[algorithm],
name: algorithm,
context: algorithm === 'Ed25519' ? undefined : context,
data,
signature: signatures[algorithm][context.byteLength],
})
}
}
return vectors;
}

File diff suppressed because one or more lines are too long

View File

@ -5,9 +5,8 @@ const RSA_KEY_GEN = {
publicExponent: new Uint8Array([1, 0, 1])
};
const [ECDH, X448, X25519] = await Promise.all([
const [ECDH, X25519] = await Promise.all([
subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, false, ['deriveBits', 'deriveKey']),
subtle.generateKey('X448', false, ['deriveBits', 'deriveKey']),
subtle.generateKey('X25519', false, ['deriveBits', 'deriveKey']),
]);
@ -34,10 +33,6 @@ export const vectors = {
[true, 'Ed25519'],
[true, 'Ed448'],
[true, { name: 'Ed448', context: Buffer.alloc(0) }],
[false, { name: 'Ed448', context: Buffer.alloc(1) }],
[true, 'RSASSA-PKCS1-v1_5'],
[true, { name: 'RSA-PSS', saltLength: 32 }],
@ -64,9 +59,7 @@ export const vectors = {
[false, 'HKDF'],
[false, 'PBKDF2'],
[true, 'X25519'],
[true, 'X448'],
[true, 'Ed25519'],
[true, 'Ed448'],
[true, { name: 'HMAC', hash: 'SHA-256' }],
[true, { name: 'HMAC', hash: 'SHA-256', length: 256 }],
[false, { name: 'HMAC', hash: 'SHA-256', length: 25 }],
@ -116,15 +109,6 @@ export const vectors = {
[true,
{ name: 'X25519', public: X25519.publicKey },
'HKDF'],
[true,
{ name: 'X448', public: X448.publicKey },
{ name: 'AES-CBC', length: 128 }],
[true,
{ name: 'X448', public: X448.publicKey },
{ name: 'HMAC', hash: 'SHA-256' }],
[true,
{ name: 'X448', public: X448.publicKey },
'HKDF'],
[true,
{ name: 'ECDH', public: ECDH.publicKey },
{ name: 'AES-CBC', length: 128 }],
@ -159,27 +143,18 @@ export const vectors = {
[true,
{ name: 'ECDH', public: ECDH.publicKey }],
[false, { name: 'ECDH', public: X448.publicKey }],
[false, { name: 'ECDH', public: ECDH.privateKey }],
[false, 'ECDH'],
[true, { name: 'X25519', public: X25519.publicKey }],
[false, { name: 'X25519', public: X448.publicKey }],
[false, { name: 'X25519', public: X25519.privateKey }],
[false, 'X25519'],
[true, { name: 'X448', public: X448.publicKey }],
[false, { name: 'X448', public: X25519.publicKey }],
[false, { name: 'X448', public: X448.privateKey }],
[false, 'X448'],
],
'importKey': [
[false, 'SHA-1'],
[false, 'Invalid'],
[true, 'X25519'],
[true, 'X448'],
[true, 'Ed25519'],
[true, 'Ed448'],
[true, { name: 'HMAC', hash: 'SHA-256' }],
[true, { name: 'HMAC', hash: 'SHA-256', length: 256 }],
[false, { name: 'HMAC', hash: 'SHA-256', length: 25 }],

View File

@ -32,6 +32,12 @@ export const vectors = {
[pqc, 'ML-DSA-44'],
[pqc, 'ML-DSA-65'],
[pqc, 'ML-DSA-87'],
[pqc, { name: 'ML-DSA-44', context: Buffer.alloc(0) }],
[pqc, { name: 'ML-DSA-65', context: Buffer.alloc(0) }],
[pqc, { name: 'ML-DSA-87', context: Buffer.alloc(0) }],
[pqc, { name: 'ML-DSA-44', context: Buffer.alloc(32) }],
[pqc, { name: 'ML-DSA-65', context: Buffer.alloc(32) }],
[pqc, { name: 'ML-DSA-87', context: Buffer.alloc(32) }],
[false, 'Argon2d'],
[false, 'Argon2i'],
[false, 'Argon2id'],

View File

@ -1,14 +1,25 @@
import { hasOpenSSL } from '../../common/crypto.js'
const supportsContext = hasOpenSSL(3, 2);
const { subtle } = globalThis.crypto;
const boringSSL = process.features.openssl_is_boringssl;
const X25519 = await subtle.generateKey('X25519', false, ['deriveBits', 'deriveKey']);
let X448;
let Ed448;
if (!boringSSL) {
X448 = await subtle.generateKey('X448', false, ['deriveBits', 'deriveKey'])
Ed448 = await subtle.generateKey('Ed448', false, ['sign', 'verify'])
}
export const vectors = {
'sign': [
[!boringSSL, 'Ed448'],
[!boringSSL, { name: 'Ed448', context: Buffer.alloc(0) }],
[!boringSSL && supportsContext, { name: 'Ed448', context: Buffer.alloc(32) }],
],
'generateKey': [
[!boringSSL, 'X448'],
[!boringSSL, 'Ed448'],

View File

@ -5,12 +5,18 @@ const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const { hasOpenSSL } = require('../common/crypto');
const assert = require('assert');
const crypto = require('crypto');
const { subtle } = globalThis.crypto;
const vectors = require('../fixtures/crypto/eddsa')();
const supportsContext = hasOpenSSL(3, 2);
async function testVerify({ name,
context,
publicKeyBuffer,
privateKeyBuffer,
signature,
@ -63,43 +69,62 @@ async function testVerify({ name,
['sign']),
]);
assert(await subtle.verify({ name }, publicKey, signature, data));
assert(await subtle.verify({ name, context }, publicKey, signature, data));
if (context?.byteLength !== undefined) {
if (supportsContext) {
assert(!(await subtle.verify({ name, context: crypto.randomBytes(30) }, publicKey, signature, data)));
}
if (context.byteLength === 0) {
assert(await subtle.verify({ name }, publicKey, signature, data));
}
}
// Test verification with altered buffers
const copy = Buffer.from(data);
const sigcopy = Buffer.from(signature);
const p = subtle.verify({ name }, publicKey, sigcopy, copy);
const p = subtle.verify({ name, context }, publicKey, sigcopy, copy);
copy[0] = 255 - copy[0];
sigcopy[0] = 255 - sigcopy[0];
assert(await p);
// Test failure when using wrong key
await assert.rejects(
subtle.verify({ name }, privateKey, signature, data), {
subtle.verify({ name, context }, privateKey, signature, data), {
message: /Unable to use this key to verify/
});
await assert.rejects(
subtle.verify({ name }, noVerifyPublicKey, signature, data), {
subtle.verify({ name, context }, noVerifyPublicKey, signature, data), {
message: /Unable to use this key to verify/
});
// Test failure when using the wrong algorithms
await assert.rejects(
subtle.verify({ name }, hmacKey, signature, data), {
subtle.verify({ name, context }, hmacKey, signature, data), {
message: /Unable to use this key to verify/
});
await assert.rejects(
subtle.verify({ name }, rsaKeys.publicKey, signature, data), {
subtle.verify({ name, context }, rsaKeys.publicKey, signature, data), {
message: /Unable to use this key to verify/
});
await assert.rejects(
subtle.verify({ name }, ecKeys.publicKey, signature, data), {
subtle.verify({ name, context }, ecKeys.publicKey, signature, data), {
message: /Unable to use this key to verify/
});
if (name === 'Ed448' && supportsContext) {
// Test failure when too long context
await assert.rejects(
subtle.verify({ name, context: new Uint8Array(256) }, publicKey, signature, data), (err) => {
assert.strictEqual(err.name, 'OperationError');
assert.strictEqual(err.cause.code, 'ERR_OUT_OF_RANGE');
assert.strictEqual(err.cause.message, 'context string must be at most 255 bytes');
return true;
});
}
// Test failure when signature is altered
{
const copy = Buffer.from(signature);
@ -120,11 +145,12 @@ async function testVerify({ name,
{
const copy = Buffer.from(data);
copy[0] = 255 - copy[0];
assert(!(await subtle.verify({ name }, publicKey, signature, copy)));
assert(!(await subtle.verify({ name, context }, publicKey, signature, copy)));
}
}
async function testSign({ name,
context,
publicKeyBuffer,
privateKeyBuffer,
signature,
@ -171,46 +197,66 @@ async function testSign({ name,
]);
{
const sig = await subtle.sign({ name }, privateKey, data);
const sig = await subtle.sign({ name, context }, privateKey, data);
assert.strictEqual(sig.byteLength, signature.byteLength);
assert(await subtle.verify({ name }, publicKey, sig, data));
assert(await subtle.verify({ name, context }, publicKey, sig, data));
if (context?.byteLength !== undefined) {
if (supportsContext) {
assert(!(await subtle.verify({ name, context: crypto.randomBytes(30) }, publicKey, signature, data)));
}
if (context.byteLength === 0) {
assert(await subtle.verify({ name }, publicKey, signature, data));
}
}
}
{
const copy = Buffer.from(data);
const p = subtle.sign({ name }, privateKey, copy);
const p = subtle.sign({ name, context }, privateKey, copy);
copy[0] = 255 - copy[0];
const sig = await p;
assert(await subtle.verify({ name }, publicKey, sig, data));
assert(await subtle.verify({ name, context }, publicKey, sig, data));
}
// Test failure when using wrong key
await assert.rejects(
subtle.sign({ name }, publicKey, data), {
subtle.sign({ name, context }, publicKey, data), {
message: /Unable to use this key to sign/
});
// Test failure when using the wrong algorithms
await assert.rejects(
subtle.sign({ name }, hmacKey, data), {
subtle.sign({ name, context }, hmacKey, data), {
message: /Unable to use this key to sign/
});
await assert.rejects(
subtle.sign({ name }, rsaKeys.privateKey, data), {
subtle.sign({ name, context }, rsaKeys.privateKey, data), {
message: /Unable to use this key to sign/
});
await assert.rejects(
subtle.sign({ name }, ecKeys.privateKey, data), {
subtle.sign({ name, context }, ecKeys.privateKey, data), {
message: /Unable to use this key to sign/
});
if (name === 'Ed448' && supportsContext) {
// Test failure when too long context
await assert.rejects(
subtle.sign({ name, context: new Uint8Array(256) }, privateKey, data), (err) => {
assert.strictEqual(err.name, 'OperationError');
assert.strictEqual(err.cause.code, 'ERR_OUT_OF_RANGE');
assert.strictEqual(err.cause.message, 'context string must be at most 255 bytes');
return true;
});
}
}
(async function() {
const variations = [];
vectors.forEach((vector) => {
if (!supportsContext && vector.context?.byteLength) return;
variations.push(testVerify(vector));
variations.push(testSign(vector));
});
@ -218,35 +264,16 @@ async function testSign({ name,
await Promise.all(variations);
})().then(common.mustCall());
// Ed448 context
if (!process.features.openssl_is_boringssl) {
const vector = vectors.find(({ name }) => name === 'Ed448');
Promise.all([
subtle.importKey(
'pkcs8',
vector.privateKeyBuffer,
{ name: 'Ed448' },
false,
['sign']),
subtle.importKey(
'spki',
vector.publicKeyBuffer,
{ name: 'Ed448' },
false,
['verify']),
]).then(async ([privateKey, publicKey]) => {
const sig = await subtle.sign({ name: 'Ed448', context: Buffer.alloc(0) }, privateKey, vector.data);
assert.deepStrictEqual(Buffer.from(sig), vector.signature);
assert.strictEqual(
await subtle.verify({ name: 'Ed448', context: Buffer.alloc(0) }, publicKey, sig, vector.data), true);
if (!supportsContext) {
assert.rejects(async () => {
const kp = await subtle.generateKey('Ed448', false, ['sign', 'verify']);
const data = crypto.randomBytes(32);
await subtle.sign({ name: 'Ed448', context: new Uint8Array(32) }, kp.privateKey, data);
}, { name: /OperationError|NotSupportedError/ }).then(common.mustCall());
await assert.rejects(subtle.sign({ name: 'Ed448', context: Buffer.alloc(1) }, privateKey, vector.data), {
message: /Non zero-length Ed448Params\.context is not supported/
});
await assert.rejects(subtle.verify({ name: 'Ed448', context: Buffer.alloc(1) }, publicKey, sig, vector.data), {
message: /Non zero-length Ed448Params\.context is not supported/
});
}).then(common.mustCall());
} else {
common.printSkipMessage('Skipping unsupported Ed448 test case');
assert.rejects(async () => {
const kp = await subtle.generateKey('Ed448', false, ['sign', 'verify']);
const data = crypto.randomBytes(32);
await subtle.verify({ name: 'Ed448', context: new Uint8Array(32) }, kp.publicKey, data, data);
}, { name: /OperationError|NotSupportedError/ }).then(common.mustCall());
}

View File

@ -17,6 +17,7 @@ const { subtle } = globalThis.crypto;
const vectors = require('../fixtures/crypto/ml-dsa')();
async function testVerify({ name,
context,
publicKeyPem,
privateKeyPem,
signature,
@ -57,54 +58,67 @@ async function testVerify({ name,
['sign']),
]);
assert(await subtle.verify({ name }, publicKey, signature, data));
assert(await subtle.verify({ name, context }, publicKey, signature, data));
assert(!(await subtle.verify({ name, context: crypto.randomBytes(30) }, publicKey, signature, data)));
if (context.byteLength === 0) {
assert(await subtle.verify({ name }, publicKey, signature, data));
}
// Test verification with altered buffers
const copy = Buffer.from(data);
const sigcopy = Buffer.from(signature);
const p = subtle.verify({ name }, publicKey, sigcopy, copy);
const p = subtle.verify({ name, context }, publicKey, sigcopy, copy);
copy[0] = 255 - copy[0];
sigcopy[0] = 255 - sigcopy[0];
assert(await p);
// Test failure when using wrong key
await assert.rejects(
subtle.verify({ name }, privateKey, signature, data), {
subtle.verify({ name, context }, privateKey, signature, data), {
message: /Unable to use this key to verify/
});
await assert.rejects(
subtle.verify({ name }, noVerifyPublicKey, signature, data), {
subtle.verify({ name, context }, noVerifyPublicKey, signature, data), {
message: /Unable to use this key to verify/
});
// Test failure when using the wrong algorithms
await assert.rejects(
subtle.verify({ name }, hmacKey, signature, data), {
subtle.verify({ name, context }, hmacKey, signature, data), {
message: /Unable to use this key to verify/
});
await assert.rejects(
subtle.verify({ name }, rsaKeys.publicKey, signature, data), {
subtle.verify({ name, context }, rsaKeys.publicKey, signature, data), {
message: /Unable to use this key to verify/
});
await assert.rejects(
subtle.verify({ name }, ecKeys.publicKey, signature, data), {
subtle.verify({ name, context }, ecKeys.publicKey, signature, data), {
message: /Unable to use this key to verify/
});
// Test failure when too long context
await assert.rejects(
subtle.verify({ name, context: new Uint8Array(256) }, publicKey, signature, data), (err) => {
assert.strictEqual(err.name, 'OperationError');
assert.strictEqual(err.cause.code, 'ERR_OUT_OF_RANGE');
assert.strictEqual(err.cause.message, 'context string must be at most 255 bytes');
return true;
});
// Test failure when signature is altered
{
const copy = Buffer.from(signature);
copy[0] = 255 - copy[0];
assert(!(await subtle.verify(
{ name },
{ name, context },
publicKey,
copy,
data)));
assert(!(await subtle.verify(
{ name },
{ name, context },
publicKey,
copy.slice(1),
data)));
@ -114,11 +128,12 @@ async function testVerify({ name,
{
const copy = Buffer.from(data);
copy[0] = 255 - copy[0];
assert(!(await subtle.verify({ name }, publicKey, signature, copy)));
assert(!(await subtle.verify({ name, context }, publicKey, signature, copy)));
}
}
async function testSign({ name,
context,
publicKeyPem,
privateKeyPem,
signature,
@ -157,40 +172,49 @@ async function testSign({ name,
]);
{
const sig = await subtle.sign({ name }, privateKey, data);
const sig = await subtle.sign({ name, context }, privateKey, data);
assert.strictEqual(sig.byteLength, signature.byteLength);
assert(await subtle.verify({ name }, publicKey, sig, data));
assert(await subtle.verify({ name, context }, publicKey, sig, data));
}
{
const copy = Buffer.from(data);
const p = subtle.sign({ name }, privateKey, copy);
const p = subtle.sign({ name, context }, privateKey, copy);
copy[0] = 255 - copy[0];
const sig = await p;
assert(await subtle.verify({ name }, publicKey, sig, data));
assert(await subtle.verify({ name, context }, publicKey, sig, data));
}
// Test failure when using wrong key
await assert.rejects(
subtle.sign({ name }, publicKey, data), {
subtle.sign({ name, context }, publicKey, data), {
message: /Unable to use this key to sign/
});
// Test failure when using the wrong algorithms
await assert.rejects(
subtle.sign({ name }, hmacKey, data), {
subtle.sign({ name, context }, hmacKey, data), {
message: /Unable to use this key to sign/
});
await assert.rejects(
subtle.sign({ name }, rsaKeys.privateKey, data), {
subtle.sign({ name, context }, rsaKeys.privateKey, data), {
message: /Unable to use this key to sign/
});
await assert.rejects(
subtle.sign({ name }, ecKeys.privateKey, data), {
subtle.sign({ name, context }, ecKeys.privateKey, data), {
message: /Unable to use this key to sign/
});
// Test failure when too long context
await assert.rejects(
subtle.sign({ name, context: new Uint8Array(256) }, privateKey, data), (err) => {
assert.strictEqual(err.name, 'OperationError');
assert.strictEqual(err.cause.code, 'ERR_OUT_OF_RANGE');
assert.strictEqual(err.cause.message, 'context string must be at most 255 bytes');
return true;
});
}
(async function() {
@ -203,26 +227,3 @@ async function testSign({ name,
await Promise.all(variations);
})().then(common.mustCall());
// ContextParams context not supported
{
const vector = vectors[0];
const name = vector.name;
const publicKey = crypto.createPublicKey(vector.publicKeyPem)
.toCryptoKey(vector.name, false, ['verify']);
const privateKey = crypto.createPrivateKey(vector.privateKeyPem)
.toCryptoKey(vector.name, false, ['sign']);
(async () => {
const sig = await subtle.sign({ name, context: Buffer.alloc(0) }, privateKey, vector.data);
assert.strictEqual(
await subtle.verify({ name, context: Buffer.alloc(0) }, publicKey, sig, vector.data), true);
await assert.rejects(subtle.sign({ name, context: Buffer.alloc(1) }, privateKey, vector.data), {
message: /Non zero-length ContextParams\.context is not supported/
});
await assert.rejects(subtle.verify({ name, context: Buffer.alloc(1) }, publicKey, sig, vector.data), {
message: /Non zero-length ContextParams\.context is not supported/
});
})().then(common.mustCall());
}

View File

@ -507,12 +507,12 @@ const opts = { prefix, context };
}).then(common.mustCall());
}
// Ed448Params
// ContextParams
{
for (const good of [
{ name: 'Ed448', context: new Uint8Array() },
{ name: 'Ed448' },
]) {
assert.deepStrictEqual(converters.Ed448Params({ ...good, filtered: 'out' }, opts), good);
assert.deepStrictEqual(converters.ContextParams({ ...good, filtered: 'out' }, opts), good);
}
}

View File

@ -122,7 +122,6 @@ const customTypesMap = {
'HmacImportParams': 'webcrypto.html#class-hmacimportparams',
'EcdsaParams': 'webcrypto.html#class-ecdsaparams',
'RsaPssParams': 'webcrypto.html#class-rsapssparams',
'Ed448Params': 'webcrypto.html#class-ed448params',
'ContextParams': 'webcrypto.html#class-contextparams',
'CShakeParams': 'webcrypto.html#class-cshakeparams',
'KmacImportParams': 'webcrypto.html#class-kmacimportparams',