lib: improve Web Cryptography key validation ordering

Signed-off-by: Filip Skokan <panva.ip@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/62749
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
This commit is contained in:
Filip Skokan authored and GitHub committed 2026-04-17 07:47:49 +00:00
1 parent b178842482
commit 4cb1f28413
12 files changed
+201 -57

No files matched your search

+36 -17
View File
@@ -919,6 +919,14 @@ async function wrapKey(format, key, wrappingKey, algorithm) {
} catch {
algorithm = normalizeAlgorithm(algorithm, 'encrypt');
}
if (algorithm.name !== wrappingKey[kAlgorithm].name)
throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError');
if (!ArrayPrototypeIncludes(wrappingKey[kKeyUsages], 'wrapKey'))
throw lazyDOMException(
'Unable to use this key to wrapKey', 'InvalidAccessError');
let keyData = await FunctionPrototypeCall(exportKey, this, format, key);
if (format === 'jwk') {
@@ -997,6 +1005,13 @@ async function unwrapKey(
unwrappedKeyAlgo = normalizeAlgorithm(unwrappedKeyAlgo, 'importKey');
if (unwrapAlgo.name !== unwrappingKey[kAlgorithm].name)
throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError');
if (!ArrayPrototypeIncludes(unwrappingKey[kKeyUsages], 'unwrapKey'))
throw lazyDOMException(
'Unable to use this key to unwrapKey', 'InvalidAccessError');
let keyData = await cipherOrWrap(
kWebCryptoCipherDecrypt,
unwrapAlgo,
@@ -1030,12 +1045,12 @@ async function signVerify(algorithm, key, data, signature) {
}
algorithm = normalizeAlgorithm(algorithm, usage);
if (!ArrayPrototypeIncludes(key[kKeyUsages], usage) ||
algorithm.name !== key[kAlgorithm].name) {
if (algorithm.name !== key[kAlgorithm].name)
throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError');
if (!ArrayPrototypeIncludes(key[kKeyUsages], usage))
throw lazyDOMException(
`Unable to use this key to ${usage}`,
'InvalidAccessError');
}
`Unable to use this key to ${usage}`, 'InvalidAccessError');
switch (algorithm.name) {
case 'RSA-PSS':
@@ -1120,18 +1135,6 @@ async function verify(algorithm, key, signature, data) {
}
async function cipherOrWrap(mode, algorithm, key, data, op) {
// We use a Node.js style error here instead of a DOMException because
// the WebCrypto spec is not specific what kind of error is to be thrown
// in this case. Both Firefox and Chrome throw simple TypeErrors here.
// The key algorithm and cipher algorithm must match, and the
// key must have the proper usage.
if (key[kAlgorithm].name !== algorithm.name ||
!ArrayPrototypeIncludes(key[kKeyUsages], op)) {
throw lazyDOMException(
'The requested operation is not valid for the provided key',
'InvalidAccessError');
}
// While WebCrypto allows for larger input buffer sizes, we limit
// those to sizes that can fit within uint32_t because of limitations
// in the OpenSSL API.
@@ -1182,6 +1185,14 @@ async function encrypt(algorithm, key, data) {
});
algorithm = normalizeAlgorithm(algorithm, 'encrypt');
if (algorithm.name !== key[kAlgorithm].name)
throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError');
if (!ArrayPrototypeIncludes(key[kKeyUsages], 'encrypt'))
throw lazyDOMException(
'Unable to use this key to encrypt', 'InvalidAccessError');
return await cipherOrWrap(
kWebCryptoCipherEncrypt,
algorithm,
@@ -1211,6 +1222,14 @@ async function decrypt(algorithm, key, data) {
});
algorithm = normalizeAlgorithm(algorithm, 'decrypt');
if (algorithm.name !== key[kAlgorithm].name)
throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError');
if (!ArrayPrototypeIncludes(key[kKeyUsages], 'decrypt'))
throw lazyDOMException(
'Unable to use this key to decrypt', 'InvalidAccessError');
return await cipherOrWrap(
kWebCryptoCipherDecrypt,
algorithm,
@@ -49,7 +49,7 @@ async function testEncryptNoEncrypt({ keyBuffer, algorithm, plaintext }) {
['decrypt']);
return assert.rejects(subtle.encrypt(algorithm, key, plaintext), {
message: /The requested operation is not valid for the provided key/
message: /Unable to use this key to encrypt/
});
}
@@ -65,7 +65,7 @@ async function testEncryptNoDecrypt({ keyBuffer, algorithm, plaintext }) {
const output = await subtle.encrypt(algorithm, key, plaintext);
return assert.rejects(subtle.decrypt(algorithm, key, output), {
message: /The requested operation is not valid for the provided key/
message: /Unable to use this key to decrypt/
});
}
@@ -80,7 +80,23 @@ async function testEncryptWrongAlg({ keyBuffer, algorithm, plaintext }, alg) {
['encrypt']);
return assert.rejects(subtle.encrypt(algorithm, key, plaintext), {
message: /The requested operation is not valid for the provided key/
message: /Key algorithm mismatch/
});
}
async function testDecryptWrongAlg({ keyBuffer, algorithm, result }, alg) {
if (result === undefined) return;
assert.notStrictEqual(algorithm.name, alg);
const keyFormat = alg === 'AES-OCB' ? 'raw-secret' : 'raw';
const key = await subtle.importKey(
keyFormat,
keyBuffer,
{ name: alg },
false,
['decrypt']);
return assert.rejects(subtle.decrypt(algorithm, key, result), {
message: /Key algorithm mismatch/
});
}
@@ -112,6 +128,7 @@ async function testDecrypt({ keyBuffer, algorithm, result }) {
variations.push(testEncryptNoEncrypt(vector));
variations.push(testEncryptNoDecrypt(vector));
variations.push(testEncryptWrongAlg(vector, 'AES-CTR'));
variations.push(testDecryptWrongAlg(vector, 'AES-CTR'));
});
failing.forEach((vector) => {
@@ -149,6 +166,7 @@ async function testDecrypt({ keyBuffer, algorithm, result }) {
variations.push(testEncryptNoEncrypt(vector));
variations.push(testEncryptNoDecrypt(vector));
variations.push(testEncryptWrongAlg(vector, 'AES-CBC'));
variations.push(testDecryptWrongAlg(vector, 'AES-CBC'));
});
// TODO(@jasnell): These fail for different reasons. Need to
@@ -188,6 +206,7 @@ async function testDecrypt({ keyBuffer, algorithm, result }) {
variations.push(testEncryptNoEncrypt(vector));
variations.push(testEncryptNoDecrypt(vector));
variations.push(testEncryptWrongAlg(vector, 'AES-CBC'));
variations.push(testDecryptWrongAlg(vector, 'AES-CBC'));
});
failing.forEach((vector) => {
@@ -225,6 +244,7 @@ if (hasOpenSSL(3)) {
variations.push(testEncryptNoEncrypt(vector));
variations.push(testEncryptNoDecrypt(vector));
variations.push(testEncryptWrongAlg(vector, 'AES-GCM'));
variations.push(testDecryptWrongAlg(vector, 'AES-GCM'));
});
failing.forEach((vector) => {
@@ -48,7 +48,7 @@ async function testEncryptNoEncrypt({ keyBuffer, algorithm, plaintext }) {
['decrypt']);
return assert.rejects(subtle.encrypt(algorithm, key, plaintext), {
message: /The requested operation is not valid for the provided key/
message: /Unable to use this key to encrypt/
});
}
@@ -63,7 +63,7 @@ async function testEncryptNoDecrypt({ keyBuffer, algorithm, plaintext }) {
const output = await subtle.encrypt(algorithm, key, plaintext);
return assert.rejects(subtle.decrypt(algorithm, key, output), {
message: /The requested operation is not valid for the provided key/
message: /Unable to use this key to decrypt/
});
}
@@ -77,7 +77,22 @@ async function testEncryptWrongAlg({ keyBuffer, algorithm, plaintext }, alg) {
['encrypt']);
return assert.rejects(subtle.encrypt(algorithm, key, plaintext), {
message: /The requested operation is not valid for the provided key/
message: /Key algorithm mismatch/
});
}
async function testDecryptWrongAlg({ keyBuffer, algorithm, result }, alg) {
if (result === undefined) return;
assert.notStrictEqual(algorithm.name, alg);
const key = await subtle.importKey(
'raw-secret',
keyBuffer,
{ name: alg },
false,
['decrypt']);
return assert.rejects(subtle.decrypt(algorithm, key, result), {
message: /Key algorithm mismatch/
});
}
@@ -107,6 +122,7 @@ async function testDecrypt({ keyBuffer, algorithm, result }) {
variations.push(testEncryptNoEncrypt(vector));
variations.push(testEncryptNoDecrypt(vector));
variations.push(testEncryptWrongAlg(vector, 'AES-GCM'));
variations.push(testDecryptWrongAlg(vector, 'AES-GCM'));
});
failing.forEach((vector) => {
@@ -147,7 +147,7 @@ async function testEncryptionWrongKey({ algorithm,
['decrypt']);
return assert.rejects(
subtle.encrypt(algorithm, privateKey, plaintext), {
message: /The requested operation is not valid/
message: /Unable to use this key to encrypt/
});
}
@@ -167,7 +167,7 @@ async function testEncryptionBadUsage({ algorithm,
['decrypt']);
return assert.rejects(
subtle.encrypt(algorithm, publicKey, plaintext), {
message: /The requested operation is not valid/
message: /Unable to use this key to encrypt/
});
}
@@ -191,7 +191,7 @@ async function testDecryptionWrongKey({ ciphertext,
return assert.rejects(
subtle.decrypt(algorithm, publicKey, ciphertext), {
message: /The requested operation is not valid/
message: /Unable to use this key to decrypt/
});
}
@@ -215,7 +215,7 @@ async function testDecryptionBadUsage({ ciphertext,
return assert.rejects(
subtle.decrypt(algorithm, publicKey, ciphertext), {
message: /The requested operation is not valid/
message: /Unable to use this key to decrypt/
});
}
@@ -43,14 +43,14 @@ const { subtle } = globalThis.crypto;
name: 'RSA-OAEP',
}, privateKey, buf), {
name: 'InvalidAccessError',
message: 'The requested operation is not valid for the provided key'
message: 'Unable to use this key to encrypt'
});
await assert.rejects(() => subtle.decrypt({
name: 'RSA-OAEP',
}, publicKey, ciphertext), {
name: 'InvalidAccessError',
message: 'The requested operation is not valid for the provided key'
message: 'Unable to use this key to decrypt'
});
}
@@ -88,14 +88,14 @@ if (!process.features.openssl_is_boringssl) {
name: 'RSA-OAEP',
}, privateKey, buf), {
name: 'InvalidAccessError',
message: 'The requested operation is not valid for the provided key'
message: 'Unable to use this key to encrypt'
});
await assert.rejects(() => subtle.decrypt({
name: 'RSA-OAEP',
}, publicKey, ciphertext), {
name: 'InvalidAccessError',
message: 'The requested operation is not valid for the provided key'
message: 'Unable to use this key to decrypt'
});
}
@@ -88,17 +88,17 @@ async function testVerify({ name,
// Test failure when using the wrong algorithms
await assert.rejects(
subtle.verify({ name, hash }, hmacKey, signature, plaintext), {
message: /Unable to use this key to verify/
message: /Key algorithm mismatch/
});
await assert.rejects(
subtle.verify({ name, hash }, rsaKeys.publicKey, signature, plaintext), {
message: /Unable to use this key to verify/
message: /Key algorithm mismatch/
});
await assert.rejects(
subtle.verify({ name, hash }, okpKeys.publicKey, signature, plaintext), {
message: /Unable to use this key to verify/
message: /Key algorithm mismatch/
});
// Test failure when signature is altered
@@ -210,17 +210,17 @@ async function testSign({ name,
// Test failure when using the wrong algorithms
await assert.rejects(
subtle.sign({ name, hash }, hmacKey, plaintext), {
message: /Unable to use this key to sign/
message: /Key algorithm mismatch/
});
await assert.rejects(
subtle.sign({ name, hash }, rsaKeys.privateKey, plaintext), {
message: /Unable to use this key to sign/
message: /Key algorithm mismatch/
});
await assert.rejects(
subtle.sign({ name, hash }, okpKeys.privateKey, plaintext), {
message: /Unable to use this key to sign/
message: /Key algorithm mismatch/
});
}
@@ -101,17 +101,17 @@ async function testVerify({ name,
// Test failure when using the wrong algorithms
await assert.rejects(
subtle.verify({ name, context }, hmacKey, signature, data), {
message: /Unable to use this key to verify/
message: /Key algorithm mismatch/
});
await assert.rejects(
subtle.verify({ name, context }, rsaKeys.publicKey, signature, data), {
message: /Unable to use this key to verify/
message: /Key algorithm mismatch/
});
await assert.rejects(
subtle.verify({ name, context }, ecKeys.publicKey, signature, data), {
message: /Unable to use this key to verify/
message: /Key algorithm mismatch/
});
if (name === 'Ed448' && supportsContext) {
@@ -227,17 +227,17 @@ async function testSign({ name,
// Test failure when using the wrong algorithms
await assert.rejects(
subtle.sign({ name, context }, hmacKey, data), {
message: /Unable to use this key to sign/
message: /Key algorithm mismatch/
});
await assert.rejects(
subtle.sign({ name, context }, rsaKeys.privateKey, data), {
message: /Unable to use this key to sign/
message: /Key algorithm mismatch/
});
await assert.rejects(
subtle.sign({ name, context }, ecKeys.privateKey, data), {
message: /Unable to use this key to sign/
message: /Key algorithm mismatch/
});
if (name === 'Ed448' && supportsContext) {
@@ -62,7 +62,7 @@ async function testVerify({ hash,
// Test failure when using the wrong algorithms
await assert.rejects(
subtle.verify({ name, hash }, rsaKeys.publicKey, signature, plaintext), {
message: /Unable to use this key to verify/
message: /Key algorithm mismatch/
});
// Test failure when signature is altered
@@ -165,7 +165,7 @@ async function testSign({ hash,
// Test failure when using the wrong algorithms
await assert.rejects(
subtle.sign({ name, hash }, rsaKeys.privateKey, plaintext), {
message: /Unable to use this key to sign/
message: /Key algorithm mismatch/
});
}
@@ -69,7 +69,7 @@ async function testVerify({ algorithm,
// Test failure when using the wrong algorithms
await assert.rejects(
subtle.verify(signParams, keyPair.publicKey, expected, data), {
message: /Unable to use this key to verify/
message: /Key algorithm mismatch/
});
// Test failure when signature is altered
@@ -177,7 +177,7 @@ async function testSign({ algorithm,
// Test failure when using the wrong algorithms
await assert.rejects(
subtle.sign(signParams, keyPair.privateKey, data), {
message: /Unable to use this key to sign/
message: /Key algorithm mismatch/
});
}
@@ -86,17 +86,17 @@ async function testVerify({ name,
// Test failure when using the wrong algorithms
await assert.rejects(
subtle.verify({ name, context }, hmacKey, signature, data), {
message: /Unable to use this key to verify/
message: /Key algorithm mismatch/
});
await assert.rejects(
subtle.verify({ name, context }, rsaKeys.publicKey, signature, data), {
message: /Unable to use this key to verify/
message: /Key algorithm mismatch/
});
await assert.rejects(
subtle.verify({ name, context }, ecKeys.publicKey, signature, data), {
message: /Unable to use this key to verify/
message: /Key algorithm mismatch/
});
// Test failure when too long context
@@ -194,17 +194,17 @@ async function testSign({ name,
// Test failure when using the wrong algorithms
await assert.rejects(
subtle.sign({ name, context }, hmacKey, data), {
message: /Unable to use this key to sign/
message: /Key algorithm mismatch/
});
await assert.rejects(
subtle.sign({ name, context }, rsaKeys.privateKey, data), {
message: /Unable to use this key to sign/
message: /Key algorithm mismatch/
});
await assert.rejects(
subtle.sign({ name, context }, ecKeys.privateKey, data), {
message: /Unable to use this key to sign/
message: /Key algorithm mismatch/
});
// Test failure when too long context
@@ -82,12 +82,12 @@ async function testVerify({
// Test failure when using the wrong algorithms
await assert.rejects(
subtle.verify(algorithm, hmacKey, signature, plaintext), {
message: /Unable to use this key to verify/
message: /Key algorithm mismatch/
});
await assert.rejects(
subtle.verify(algorithm, ecdsaKeys.publicKey, signature, plaintext), {
message: /Unable to use this key to verify/
message: /Key algorithm mismatch/
});
// Test failure when signature is altered
@@ -185,12 +185,12 @@ async function testSign({
// Test failure when using the wrong algorithms
await assert.rejects(
subtle.sign(algorithm, hmacKey, plaintext), {
message: /Unable to use this key to sign/
message: /Key algorithm mismatch/
});
await assert.rejects(
subtle.sign(algorithm, ecdsaKeys.privateKey, plaintext), {
message: /Unable to use this key to sign/
message: /Key algorithm mismatch/
});
}
@@ -378,3 +378,92 @@ function testWrapping(name, keys) {
});
await Promise.all(variations);
})().then(common.mustCall());
// Test that wrapKey/unwrapKey validate the wrapping/unwrapping key's
// algorithm and usage before proceeding.
// Spec: https://w3c.github.io/webcrypto/#SubtleCrypto-method-wrapKey
// Steps 9-10 (wrapping key checks) must precede step 12 (exportKey).
(async function() {
const hmacKey = await subtle.generateKey(
{ name: 'HMAC', hash: 'SHA-256' },
true,
['sign', 'verify'],
);
const ecKey = await subtle.generateKey(
{ name: 'ECDSA', namedCurve: 'P-256' },
true,
['sign', 'verify'],
);
// Wrong algorithm: wrapping key is HMAC but algorithm says AES-GCM.
// Even though exporting ecKey.privateKey as 'spki' would also fail
// (wrong key type for spki), the wrapping key check must come first.
await assert.rejects(
subtle.wrapKey('spki', ecKey.privateKey, hmacKey, {
name: 'AES-GCM',
iv: new Uint8Array(12),
}), {
name: 'InvalidAccessError',
message: 'Key algorithm mismatch',
});
// Missing wrapKey usage: aesKey only has encrypt/decrypt, not wrapKey.
// Even though exporting ecKey.privateKey as 'spki' would also fail,
// the usage check must come first.
const aesKey = await subtle.generateKey(
{ name: 'AES-GCM', length: 128 },
true,
['encrypt', 'decrypt'],
);
await assert.rejects(
subtle.wrapKey('spki', ecKey.privateKey, aesKey, {
name: 'AES-GCM',
iv: new Uint8Array(12),
}), {
name: 'InvalidAccessError',
message: 'Unable to use this key to wrapKey',
});
// Correct wrapping key algorithm and usage results in the expected
// exportKey error (not the wrapping key validation error).
const wrapKey = await subtle.generateKey(
{ name: 'AES-GCM', length: 128 },
true,
['wrapKey'],
);
await assert.rejects(
subtle.wrapKey('spki', ecKey.privateKey, wrapKey, {
name: 'AES-GCM',
iv: new Uint8Array(12),
}), {
// exportKey('spki', privateKey) throws NotSupportedError
name: 'NotSupportedError',
});
// --- unwrapKey validation tests ---
const ciphertext = new Uint8Array(32); // Dummy ciphertext
// Wrong algorithm: unwrapping key is HMAC but algorithm says AES-GCM.
await assert.rejects(
subtle.unwrapKey('raw', ciphertext, hmacKey, {
name: 'AES-GCM',
iv: new Uint8Array(12),
}, { name: 'AES-GCM', length: 128 }, true, ['encrypt']), {
name: 'InvalidAccessError',
message: 'Key algorithm mismatch',
});
// Missing unwrapKey usage: aesKey only has encrypt/decrypt, not unwrapKey.
await assert.rejects(
subtle.unwrapKey('raw', ciphertext, aesKey, {
name: 'AES-GCM',
iv: new Uint8Array(12),
}, { name: 'AES-GCM', length: 128 }, true, ['encrypt']), {
name: 'InvalidAccessError',
message: 'Unable to use this key to unwrapKey',
});
})().then(common.mustCall());