'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const { hasOpenSSL, hasFIPS, isBoringSSL } = require('../common/crypto');
const assert = require('assert');
const { getFips } = require('crypto');
const { subtle } = globalThis.crypto;
const fips3 = hasFIPS(3);
const fips35 = hasFIPS(3, 5);
const kWrappingData = {
'RSA-OAEP': {
generate: {
modulusLength: 4096,
publicExponent: new Uint8Array([1, 0, 1]),
hash: 'SHA-256',
},
wrap: { label: new Uint8Array(8) },
pair: true
},
'AES-CTR': {
generate: { length: 128 },
wrap: { counter: new Uint8Array(16), length: 64 },
pair: false
},
'AES-CBC': {
generate: { length: 128 },
wrap: { iv: new Uint8Array(16) },
pair: false
},
'AES-GCM': {
generate: { length: 128 },
wrap: {
iv: new Uint8Array(16),
additionalData: new Uint8Array(16),
tagLength: fips3 ? 128 : 64
},
pair: false
},
'AES-KW': {
generate: { length: 128 },
wrap: { },
pair: false
},
'ChaCha20-Poly1305': {
wrap: {
iv: new Uint8Array(12),
additionalData: new Uint8Array(16),
tagLength: 128
},
pair: false
}
};
if (fips3)
delete kWrappingData['ChaCha20-Poly1305'];
if (!isBoringSSL && !fips3) {
kWrappingData['AES-OCB'] = {
generate: { length: 128 },
wrap: {
iv: new Uint8Array(15),
additionalData: new Uint8Array(16),
tagLength: 128
},
pair: false
};
}
function generateWrappingKeys() {
return Promise.all(Object.keys(kWrappingData).map(async (name) => {
const keys = await subtle.generateKey(
{ name, ...kWrappingData[name].generate },
true,
['wrapKey', 'unwrapKey']);
if (kWrappingData[name].pair) {
kWrappingData[name].wrappingKey = keys.publicKey;
kWrappingData[name].unwrappingKey = keys.privateKey;
} else {
kWrappingData[name].wrappingKey = keys;
kWrappingData[name].unwrappingKey = keys;
}
}));
}
async function generateKeysToWrap() {
const parameters = [
{
algorithm: {
name: 'RSASSA-PKCS1-v1_5',
modulusLength: getFips() === 1 ? 2048 : 1024,
publicExponent: new Uint8Array([1, 0, 1]),
hash: 'SHA-256'
},
privateUsages: ['sign'],
publicUsages: ['verify'],
pair: true,
},
{
algorithm: {
name: 'RSA-PSS',
modulusLength: getFips() === 1 ? 2048 : 1024,
publicExponent: new Uint8Array([1, 0, 1]),
hash: 'SHA-256'
},
privateUsages: ['sign'],
publicUsages: ['verify'],
pair: true,
},
{
algorithm: {
name: 'RSA-OAEP',
modulusLength: getFips() === 1 ? 2048 : 1024,
publicExponent: new Uint8Array([1, 0, 1]),
hash: 'SHA-256'
},
privateUsages: ['decrypt'],
publicUsages: ['encrypt'],
pair: true,
},
{
algorithm: {
name: 'ECDSA',
namedCurve: 'P-384'
},
privateUsages: ['sign'],
publicUsages: ['verify'],
pair: true,
},
{
algorithm: {
name: 'ECDH',
namedCurve: 'P-384'
},
privateUsages: ['deriveBits'],
publicUsages: [],
pair: true,
},
{
algorithm: {
name: 'Ed25519',
},
privateUsages: ['sign'],
publicUsages: ['verify'],
pair: true,
},
{
algorithm: {
name: 'X25519',
},
privateUsages: ['deriveBits'],
publicUsages: [],
pair: true,
},
{
algorithm: {
name: 'AES-CTR',
length: 128
},
usages: ['encrypt', 'decrypt'],
pair: false,
},
{
algorithm: {
name: 'AES-CBC',
length: 128
},
usages: ['encrypt', 'decrypt'],
pair: false,
},
{
algorithm: {
name: 'AES-GCM', length: 128
},
usages: ['encrypt', 'decrypt'],
pair: false,
},
{
algorithm: {
name: 'HMAC',
length: 128,
hash: 'SHA-256'
},
usages: ['sign', 'verify'],
pair: false,
},
{
algorithm: {
name: 'AES-KW',
length: 128
},
usages: ['wrapKey', 'unwrapKey'],
pair: false,
},
{
algorithm: {
name: 'ChaCha20-Poly1305'
},
usages: ['encrypt', 'decrypt'],
pair: false,
},
];
if (hasOpenSSL(3, 5) || isBoringSSL) {
for (const name of ['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87']) {
parameters.push({
algorithm: { name },
privateUsages: ['sign'],
publicUsages: ['verify'],
pair: true,
});
}
}
if (!isBoringSSL) {
parameters.push(
{
algorithm: {
name: 'Ed448',
},
privateUsages: ['sign'],
publicUsages: ['verify'],
pair: true,
},
{
algorithm: {
name: 'X448',
},
privateUsages: ['deriveBits'],
publicUsages: [],
pair: true,
},
);
} else {
common.printSkipMessage('Skipping unsupported Curve test cases');
}
if (fips3) {
const unsupported = new Set([
'ChaCha20-Poly1305',
]);
if (fips35) {
unsupported.add('X25519');
unsupported.add('X448');
}
for (let i = parameters.length - 1; i >= 0; --i) {
if (unsupported.has(parameters[i].algorithm.name))
parameters.splice(i, 1);
}
}
const allkeys = await Promise.all(parameters.map(async (params) => {
const usages = 'usages' in params ?
params.usages :
params.publicUsages.concat(params.privateUsages);
const keys = await subtle.generateKey(params.algorithm, true, usages);
if (params.pair) {
return [
{
algorithm: params.algorithm,
usages: params.publicUsages,
key: keys.publicKey,
},
{
algorithm: params.algorithm,
usages: params.privateUsages,
key: keys.privateKey,
},
];
}
return [{
algorithm: params.algorithm,
usages: params.usages,
key: keys,
}];
}));
return allkeys.flat();
}
function getFormats(key) {
switch (key.type) {
case 'secret': {
if (key.algorithm.name === 'ChaCha20-Poly1305') return ['raw-secret', 'jwk'];
return ['raw-secret', 'raw', 'jwk'];
};
case 'public': {
switch (key.algorithm.name.slice(0, 2)) {
case 'EC': // ECDSA, ECDH
return ['spki', 'jwk', 'raw', 'raw-public'];
case 'ML': // ML-DSA
return ['jwk', 'raw-public'];
default:
return ['spki', 'jwk'];
}
}
case 'private': {
switch (key.algorithm.name.slice(0, 2)) {
case 'ML': // ML-DSA
return ['jwk', 'raw-seed'];
default:
return ['pkcs8', 'jwk'];
}
}
}
}
// If the wrapping algorithm is AES-KW, the exported key
// material length must be a multiple of 8.
// If the wrapping algorithm is RSA-OAEP, the exported key
// material maximum length is a factor of the modulusLength
//
// As per the NOTE in step 13 https://w3c.github.io/webcrypto/#SubtleCrypto-method-wrapKey
// we're padding AES-KW wrapped JWK to make sure it is always a multiple of 8 bytes
// in length
async function wrappingIsPossible(name, exported) {
if ('byteLength' in exported) {
switch (name) {
case 'AES-KW':
return exported.byteLength % 8 === 0;
case 'RSA-OAEP':
return exported.byteLength <= 446;
}
} else if ('kty' in exported && name === 'RSA-OAEP') {
return JSON.stringify(exported).length <= 478;
}
return true;
}
async function testWrap(wrappingKey, unwrappingKey, key, wrap, format) {
const exported = await subtle.exportKey(format, key.key);
if (!(await wrappingIsPossible(wrappingKey.algorithm.name, exported)))
return;
const wrapped =
await subtle.wrapKey(
format,
key.key,
wrappingKey,
{ name: wrappingKey.algorithm.name, ...wrap });
const unwrapped =
await subtle.unwrapKey(
format,
wrapped,
unwrappingKey,
{ name: wrappingKey.algorithm.name, ...wrap },
key.algorithm,
true,
key.usages);
assert(unwrapped.extractable);
const exportedAgain = await subtle.exportKey(format, unwrapped);
assert.deepStrictEqual(exported, exportedAgain);
}
function testWrapping(name, keys) {
const variations = [];
const {
wrappingKey,
unwrappingKey,
wrap
} = kWrappingData[name];
keys.forEach((key) => {
getFormats(key.key).forEach((format) => {
variations.push(testWrap(wrappingKey, unwrappingKey, key, wrap, format));
});
});
return variations;
}
(async function() {
if (fips3) {
await assert.rejects(
subtle.generateKey(
{ name: 'ChaCha20-Poly1305' }, true, ['wrapKey']),
{ name: 'NotSupportedError' });
if (fips35) {
for (const name of ['X25519', 'X448']) {
await assert.rejects(
subtle.generateKey({ name }, true, ['deriveBits']),
{ name: 'NotSupportedError', message: 'Unrecognized algorithm name' });
}
}
await assert.rejects(
subtle.generateKey({ name: 'AES-OCB', length: 128 }, true, ['wrapKey']),
{ name: 'NotSupportedError', message: 'Unrecognized algorithm name' });
const key = await subtle.generateKey(
{ name: 'HMAC', hash: 'SHA-256', length: 256 },
true,
['sign']);
await assert.rejects(
subtle.wrapKey(
'raw',
key,
key,
{ name: 'AES-OCB', iv: new Uint8Array(15), tagLength: 128 }),
{ name: 'NotSupportedError', message: 'Unrecognized algorithm name' });
}
await generateWrappingKeys();
const keys = await generateKeysToWrap();
const variations = [];
Object.keys(kWrappingData).forEach((name) => {
variations.push(...testWrapping(name, keys));
});
await Promise.all(variations);
})().then(common.mustCall());
async function testNonByteLengthWrapUnwrap({
key,
formats,
rawFormat,
explicitAlgorithm,
implicitAlgorithm,
}) {
const wrappingKey = await subtle.generateKey(
{ name: 'AES-GCM', length: 128 },
true,
['wrapKey', 'unwrapKey']);
const expectedRaw = new Uint8Array(await subtle.exportKey(rawFormat, key));
for (const [i, format] of formats.entries()) {
const wrapAlgorithm = {
name: 'AES-GCM',
iv: new Uint8Array(12).fill(i),
};
const wrapped = await subtle.wrapKey(format, key, wrappingKey, wrapAlgorithm);
// The serialized key material carries bytes, not the requested bit length.
const explicit = await subtle.unwrapKey(
format,
wrapped,
wrappingKey,
wrapAlgorithm,
explicitAlgorithm,
true,
['sign', 'verify']);
assert.strictEqual(explicit.algorithm.length, 9);
assert.deepStrictEqual(
new Uint8Array(await subtle.exportKey(rawFormat, explicit)),
expectedRaw);
const implicit = await subtle.unwrapKey(
format,
wrapped,
wrappingKey,
wrapAlgorithm,
implicitAlgorithm,
true,
['sign', 'verify']);
assert.strictEqual(implicit.algorithm.length, expectedRaw.byteLength * 8);
assert.deepStrictEqual(
new Uint8Array(await subtle.exportKey(rawFormat, implicit)),
expectedRaw);
}
}
(async function() {
const hmacAlgorithm = { name: 'HMAC', hash: 'SHA-256' };
const hmacKey = await subtle.importKey(
'raw',
new Uint8Array([0xff, 0xff]),
{ ...hmacAlgorithm, length: 9 },
true,
['sign', 'verify']);
await testNonByteLengthWrapUnwrap({
key: hmacKey,
formats: ['raw', 'jwk'],
rawFormat: 'raw',
explicitAlgorithm: { ...hmacAlgorithm, length: 9 },
implicitAlgorithm: hmacAlgorithm,
});
if (!isBoringSSL && getFips() !== 1) {
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());
// 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 13 (export operation).
(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
// export operation 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),
}), {
message: 'Key must be a public key',
name: 'InvalidAccessError',
});
// Symmetric case: exporting a public key as 'pkcs8' must also fail with
// InvalidAccessError, not the generic NotSupportedError.
await assert.rejects(
subtle.wrapKey('pkcs8', ecKey.publicKey, wrapKey, {
name: 'AES-GCM',
iv: new Uint8Array(12),
}), {
message: 'Key must be a private key',
name: 'InvalidAccessError',
});
// Exporting a private key as 'raw' must also fail with InvalidAccessError.
await assert.rejects(
subtle.wrapKey('raw', ecKey.privateKey, wrapKey, {
name: 'AES-GCM',
iv: new Uint8Array(12),
}), {
message: 'Key must be a public key',
name: 'InvalidAccessError',
});
// --- 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());