current commit
261025.12 · test,benchmark: drop OpenSSL 1.x-only coverage
// Flags: --expose-internals
'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 { types: { isCryptoKey } } = require('util');
const {
  createSecretKey,
  getFips,
  KeyObject,
} = require('crypto');
const { subtle } = globalThis.crypto;
const fips3 = hasFIPS(3);
const fips35 = hasFIPS(3, 5);
const rsaMinimumModulusLength = getFips() === 1 ? 2048 : 512;

const { bigIntArrayToUnsignedBigInt } = require('internal/crypto/util');

const allUsages = [
  'encrypt',
  'decrypt',
  'sign',
  'verify',
  'deriveBits',
  'deriveKey',
  'wrapKey',
  'unwrapKey',
];
const vectors = {
  'AES-CTR': {
    algorithm: { length: 256 },
    result: 'CryptoKey',
    usages: [
      'encrypt',
      'decrypt',
      'wrapKey',
      'unwrapKey',
    ],
  },
  'AES-CBC': {
    algorithm: { length: 256 },
    result: 'CryptoKey',
    usages: [
      'encrypt',
      'decrypt',
      'wrapKey',
      'unwrapKey',
    ],
  },
  'AES-GCM': {
    algorithm: { length: 256 },
    result: 'CryptoKey',
    usages: [
      'encrypt',
      'decrypt',
      'wrapKey',
      'unwrapKey',
    ],
  },
  'HMAC': {
    algorithm: { length: 256, hash: 'SHA-256' },
    result: 'CryptoKey',
    usages: [
      'sign',
      'verify',
    ],
  },
  'RSASSA-PKCS1-v1_5': {
    algorithm: {
      modulusLength: getFips() === 1 ? 2048 : 1024,
      publicExponent: new Uint8Array([1, 0, 1]),
      hash: 'SHA-256'
    },
    result: 'CryptoKeyPair',
    usages: [
      'sign',
      'verify',
    ],
  },
  'RSA-PSS': {
    algorithm: {
      modulusLength: getFips() === 1 ? 2048 : 1024,
      publicExponent: new Uint8Array([1, 0, 1]),
      hash: 'SHA-256'
    },
    result: 'CryptoKeyPair',
    usages: [
      'sign',
      'verify',
    ],
  },
  'RSA-OAEP': {
    algorithm: {
      modulusLength: getFips() === 1 ? 2048 : 1024,
      publicExponent: new Uint8Array([1, 0, 1]),
      hash: 'SHA-256'
    },
    result: 'CryptoKeyPair',
    usages: [
      'encrypt',
      'decrypt',
      'wrapKey',
      'unwrapKey',
    ],
  },
  'ECDSA': {
    algorithm: { namedCurve: 'P-521' },
    result: 'CryptoKeyPair',
    usages: [
      'sign',
      'verify',
    ],
  },
  'ECDH': {
    algorithm: { namedCurve: 'P-521' },
    result: 'CryptoKeyPair',
    usages: [
      'deriveKey',
      'deriveBits',
    ],
  },
  'Ed25519': {
    result: 'CryptoKeyPair',
    usages: [
      'sign',
      'verify',
    ],
  },
  'X25519': {
    result: 'CryptoKeyPair',
    usages: [
      'deriveKey',
      'deriveBits',
    ],
  },
  'AES-KW': {
    algorithm: { length: 256 },
    result: 'CryptoKey',
    usages: [
      'wrapKey',
      'unwrapKey',
    ],
  },
  'ChaCha20-Poly1305': {
    result: 'CryptoKey',
    usages: [
      'encrypt',
      'decrypt',
      'wrapKey',
      'unwrapKey',
    ],
  },
};

if (!isBoringSSL) {
  vectors.Ed448 = {
    result: 'CryptoKeyPair',
    usages: [
      'sign',
      'verify',
    ],
  };
  vectors.X448 = {
    result: 'CryptoKeyPair',
    usages: [
      'deriveKey',
      'deriveBits',
    ],
  };
} else {
  common.printSkipMessage('Skipping unsupported test cases');
}

if (!isBoringSSL) {
  vectors['AES-OCB'] = {
    algorithm: { length: 256 },
    result: 'CryptoKey',
    usages: [
      'encrypt',
      'decrypt',
      'wrapKey',
      'unwrapKey',
    ],
  };

  for (const name of ['KMAC128', 'KMAC256']) {
    vectors[name] = {
      result: 'CryptoKey',
      usages: [
        'sign',
        'verify',
      ],
    };
  }
}

if (hasOpenSSL(3, 5) || isBoringSSL) {
  for (const name of ['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87']) {
    vectors[name] = {
      result: 'CryptoKeyPair',
      usages: [
        'sign',
        'verify',
      ],
    };
  }
}

// Test invalid algorithms
{
  async function test(algorithm) {
    return assert.rejects(
      // The extractable and usages values are invalid here also,
      // but the unrecognized algorithm name should be caught first.
      subtle.generateKey(algorithm, 7, []), { name: 'NotSupportedError' });
  }

  const tests = [
    'AES',
    { name: 'AES' },
    { name: 'AES-CMAC' },
    { name: 'AES-CFB' },
    { name: 'HMAC', hash: 'MD5' },
    {
      name: 'RSA',
      hash: 'SHA-256',
      modulusLength: 2048,
      publicExponent: new Uint8Array([1, 0, 1])
    },
    {
      name: 'RSA-PSS',
      hash: 'SHA',
      modulusLength: 2048,
      publicExponent: new Uint8Array([1, 0, 1])
    },
    {
      name: 'EC',
      namedCurve: 'P521'
    },
  ].map(async (algorithm) => test(algorithm));

  Promise.all(tests).then(common.mustCall());
}

// Test bad usages
{
  async function test(name) {
    if (fips3 && (name === 'ChaCha20-Poly1305' || name === 'AES-OCB')) {
      await assert.rejects(
        subtle.generateKey({ name }, true, []),
        { name: 'NotSupportedError', message: 'Unrecognized algorithm name' });
      return;
    }

    if (fips35 && (name === 'X25519' || name === 'X448')) {
      await assert.rejects(
        subtle.generateKey({ name }, true, ['deriveBits']),
        { name: 'NotSupportedError', message: 'Unrecognized algorithm name' });
      return;
    }

    await assert.rejects(
      subtle.generateKey(
        {
          name, ...vectors[name].algorithm
        },
        true,
        []),
      { message: /Usages cannot be empty/ });

    // For CryptoKeyPair results the private key
    // usages must not be empty.
    // - ECDH(-like) algorithm key pairs only have private key usages
    // - Signing algorithm key pairs may pass a non-empty array but
    //   with only a public key usage
    if (
      vectors[name].result === 'CryptoKeyPair' &&
      vectors[name].usages.includes('verify')
    ) {
      await assert.rejects(
        subtle.generateKey(
          {
            name, ...vectors[name].algorithm
          },
          true,
          ['verify']),
        { message: /Usages cannot be empty/ });
    }

    const invalidUsages = [];
    allUsages.forEach((usage) => {
      if (!vectors[name].usages.includes(usage))
        invalidUsages.push(usage);
    });
    for (const invalidUsage of invalidUsages) {
      await assert.rejects(
        subtle.generateKey(
          {
            name, ...vectors[name].algorithm
          },
          true,
          [...vectors[name].usages, invalidUsage]),
        { message: /Unsupported key usage/ });
    }
  }

  const tests = Object.keys(vectors).map(test);

  Promise.all(tests).then(common.mustCall());
}

// Test CryptoKeyPair prototype
{
  subtle.generateKey(
    { name: 'ECDSA', namedCurve: 'P-256' },
    true,
    ['sign', 'verify'])
    .then(common.mustCall((pair) => {
      assert.strictEqual(Object.getPrototypeOf(pair), Object.prototype);
    }));
}

// Test RSA key generation
{
  async function test(
    name,
    modulusLength,
    publicExponent,
    hash,
    privateUsages,
    publicUsages = privateUsages) {
    let usages = privateUsages;
    if (publicUsages !== privateUsages)
      usages = usages.concat(publicUsages);
    const { publicKey, privateKey } = await subtle.generateKey({
      name,
      modulusLength,
      publicExponent,
      hash
    }, true, usages);

    assert(publicKey);
    assert(privateKey);
    assert(isCryptoKey(publicKey));
    assert(isCryptoKey(privateKey));

    assert(publicKey instanceof CryptoKey);
    assert(privateKey instanceof CryptoKey);

    assert.strictEqual(publicKey.type, 'public');
    assert.strictEqual(privateKey.type, 'private');
    assert.strictEqual(publicKey.toString(), '[object CryptoKey]');
    assert.strictEqual(privateKey.toString(), '[object CryptoKey]');
    assert.strictEqual(publicKey.extractable, true);
    assert.strictEqual(privateKey.extractable, true);
    assert.deepStrictEqual(publicKey.usages, publicUsages);
    assert.deepStrictEqual(privateKey.usages, privateUsages);
    assert.strictEqual(publicKey.algorithm.name, name);
    assert.strictEqual(publicKey.algorithm.modulusLength, modulusLength);
    assert(publicKey.algorithm.publicExponent instanceof Uint8Array);
    assert.notStrictEqual(publicKey.algorithm.publicExponent, publicExponent);
    assert(!Buffer.isBuffer(publicKey.algorithm.publicExponent));
    assert.deepStrictEqual(publicKey.algorithm.publicExponent, new Uint8Array(publicExponent));
    assert.strictEqual(
      KeyObject.from(publicKey).asymmetricKeyDetails.publicExponent,
      bigIntArrayToUnsignedBigInt(publicExponent));
    assert.strictEqual(publicKey.algorithm.hash.name, hash);
    assert.strictEqual(privateKey.algorithm.name, name);
    assert.strictEqual(privateKey.algorithm.modulusLength, modulusLength);
    assert(privateKey.algorithm.publicExponent instanceof Uint8Array);
    assert.notStrictEqual(privateKey.algorithm.publicExponent, publicExponent);
    assert(!Buffer.isBuffer(privateKey.algorithm.publicExponent));
    assert.deepStrictEqual(privateKey.algorithm.publicExponent, new Uint8Array(publicExponent));
    assert.strictEqual(
      KeyObject.from(privateKey).asymmetricKeyDetails.publicExponent,
      bigIntArrayToUnsignedBigInt(publicExponent));
    assert.strictEqual(privateKey.algorithm.hash.name, hash);
    assert.strictEqual(privateKey.algorithm, privateKey.algorithm);
    assert.strictEqual(privateKey.usages, privateKey.usages);
    assert.strictEqual(publicKey.algorithm, publicKey.algorithm);
    assert.strictEqual(publicKey.usages, publicKey.usages);

    // Missing parameters
    await assert.rejects(
      subtle.generateKey({ name, publicExponent, hash }, true, usages), {
        code: 'ERR_MISSING_OPTION'
      });

    await assert.rejects(
      subtle.generateKey({ name, modulusLength, hash }, true, usages), {
        code: 'ERR_MISSING_OPTION'
      });

    await assert.rejects(
      subtle.generateKey({ name, modulusLength }, true, usages), {
        code: 'ERR_MISSING_OPTION'
      });

    await Promise.all([{}].map((modulusLength) => {
      return assert.rejects(subtle.generateKey({
        name,
        modulusLength,
        publicExponent,
        hash
      }, true, usages), {
        code: 'ERR_INVALID_ARG_TYPE'
      });
    }));

    await Promise.all(
      [
        '',
        true,
        {},
        1,
        [],
        new Uint32Array(2),
      ].map((publicExponent) => {
        return assert.rejects(
          subtle.generateKey(
            { name, modulusLength, publicExponent, hash }, true, usages),
          { code: 'ERR_INVALID_ARG_TYPE' });
      }));

    await assert.rejects(
      subtle.generateKey(
        { name, modulusLength, publicExponent: new Uint8Array([1, 1, 1, 1, 1]), hash }, true, usages),
      {
        message: 'algorithm.publicExponent must fit in an unsigned 32-bit integer',
        name: 'OperationError',
      });

    await Promise.all([true, 1].map((hash) => {
      return assert.rejects(subtle.generateKey({
        name,
        modulusLength,
        publicExponent,
        hash
      }, true, usages), { name: 'NotSupportedError' });
    }));

    await Promise.all(['', {}, 1, false].map((usages) => {
      return assert.rejects(subtle.generateKey({
        name,
        modulusLength,
        publicExponent,
        hash
      }, true, usages), {
        code: 'ERR_INVALID_ARG_TYPE'
      });
    }));

    await Promise.all([
      [[1], 'algorithm.publicExponent must be at least 3'],
      [[1, 0, 0], 'algorithm.publicExponent must be odd'],
    ].map(({ 0: publicExponent, 1: message }) => {
      return assert.rejects(subtle.generateKey({
        name,
        modulusLength,
        publicExponent: new Uint8Array(publicExponent),
        hash
      }, true, usages), {
        message,
        name: 'OperationError',
      });
    }));

    await assert.rejects(subtle.generateKey({
      name,
      modulusLength: rsaMinimumModulusLength - 1,
      publicExponent: new Uint8Array([3]),
      hash,
    }, true, usages), {
      message: `algorithm.modulusLength must be at least ${rsaMinimumModulusLength}`,
      name: 'OperationError',
    });
  }

  const kTests = [
    [
      'RSASSA-PKCS1-v1_5',
      getFips() === 1 ? 2048 : 1024,
      Buffer.from([1, 0, 1]),
      'SHA-1',
      ['sign'],
      ['verify'],
    ],
    [
      'RSA-PSS',
      getFips() === 1 ? 2048 : 1024,
      Buffer.from([1, 0, 1]),
      'SHA-256',
      ['sign'],
      ['verify'],
    ],
  ];


  let fipsExponentTest;
  if (!isBoringSSL) {
    if (fips3) {
      fipsExponentTest = assert.rejects(
        subtle.generateKey({
          name: 'RSA-OAEP',
          modulusLength: 2048,
          publicExponent: Buffer.from([3]),
          hash: 'SHA3-256',
        }, true, ['decrypt', 'unwrapKey', 'encrypt', 'wrapKey']),
        (err) => err.name === 'OperationError' &&
                 err.cause?.code === 'ERR_OSSL_RSA_PUB_EXPONENT_OUT_OF_RANGE');
    } else {
      kTests.push(
        [
          'RSA-OAEP',
          1024,
          Buffer.from([3]),
          'SHA3-256',
          ['decrypt', 'unwrapKey'],
          ['encrypt', 'wrapKey'],
        ],
      );
    }
  } else {
    common.printSkipMessage('SHA-3 is only supported for digest operations');
  }

  const tests = kTests.map((args) => test(...args));
  if (fipsExponentTest !== undefined)
    tests.push(fipsExponentTest);

  Promise.all(tests).then(common.mustCall());
}

// Test EC Key Generation
{
  async function test(
    name,
    namedCurve,
    privateUsages,
    publicUsages = privateUsages) {

    let usages = privateUsages;
    if (publicUsages !== privateUsages)
      usages = usages.concat(publicUsages);

    const { publicKey, privateKey } = await subtle.generateKey({
      name,
      namedCurve
    }, true, usages);

    assert(publicKey);
    assert(privateKey);
    assert(isCryptoKey(publicKey));
    assert(isCryptoKey(privateKey));

    assert.strictEqual(publicKey.type, 'public');
    assert.strictEqual(privateKey.type, 'private');
    assert.strictEqual(publicKey.toString(), '[object CryptoKey]');
    assert.strictEqual(privateKey.toString(), '[object CryptoKey]');
    assert.strictEqual(publicKey.extractable, true);
    assert.strictEqual(privateKey.extractable, true);
    assert.deepStrictEqual(publicKey.usages, publicUsages);
    assert.deepStrictEqual(privateKey.usages, privateUsages);
    assert.strictEqual(publicKey.algorithm.name, name);
    assert.strictEqual(privateKey.algorithm.name, name);
    assert.strictEqual(publicKey.algorithm.namedCurve, namedCurve);
    assert.strictEqual(privateKey.algorithm.namedCurve, namedCurve);
    assert.strictEqual(privateKey.algorithm, privateKey.algorithm);
    assert.strictEqual(privateKey.usages, privateKey.usages);
    assert.strictEqual(publicKey.algorithm, publicKey.algorithm);
    assert.strictEqual(publicKey.usages, publicKey.usages);

    // Invalid parameters
    [1, true, {}, [], null].forEach(async (namedCurve) => {
      await assert.rejects(
        subtle.generateKey({ name, namedCurve }, true, privateUsages), {
          name: 'NotSupportedError'
        });
    });
    await assert.rejects(
      subtle.generateKey({ name, namedCurve: undefined }, true, privateUsages), {
        name: 'TypeError',
        code: 'ERR_MISSING_OPTION'
      });
  }

  const kTests = [
    [
      'ECDSA',
      'P-384',
      ['sign'],
      ['verify'],
    ],
    [
      'ECDSA',
      'P-521',
      ['sign'],
      ['verify'],
    ],
    [
      'ECDH',
      'P-384',
      ['deriveKey', 'deriveBits'],
      [],
    ],
    [
      'ECDH',
      'P-521',
      ['deriveKey', 'deriveBits'],
      [],
    ],
  ];

  const tests = kTests.map((args) => test(...args));

  Promise.all(tests).then(common.mustCall());
}

// Test AES Key Generation
{
  async function test(name, length, usages) {
    const key = await subtle.generateKey({
      name,
      length
    }, true, usages);

    assert(key);
    assert(isCryptoKey(key));

    assert.strictEqual(key.type, 'secret');
    assert.strictEqual(key.toString(), '[object CryptoKey]');
    assert.strictEqual(key.extractable, true);
    assert.deepStrictEqual(key.usages, usages);
    assert.strictEqual(key.algorithm.name, name);
    assert.strictEqual(key.algorithm.length, length);
    assert.strictEqual(key.algorithm, key.algorithm);
    assert.strictEqual(key.usages, key.usages);

    // Invalid parameters
    [1, 100, 257, '', false, null].forEach(async (length) => {
      await assert.rejects(
        subtle.generateKey({ name, length }, true, usages), {
          name: 'OperationError'
        });
    });

    await assert.rejects(
      subtle.generateKey({ name, length: undefined }, true, usages), {
        name: 'TypeError',
        code: 'ERR_MISSING_OPTION'
      });
  }

  const kTests = [
    [ 'AES-CTR', 128, ['encrypt', 'decrypt', 'wrapKey']],
    [ 'AES-CTR', 256, ['encrypt', 'decrypt', 'unwrapKey']],
    [ 'AES-CBC', 128, ['encrypt', 'decrypt']],
    [ 'AES-CBC', 256, ['encrypt', 'decrypt']],
    [ 'AES-GCM', 128, ['encrypt', 'decrypt']],
    [ 'AES-GCM', 256, ['encrypt', 'decrypt']],
    [ 'AES-KW', 128, ['wrapKey', 'unwrapKey']],
    [ 'AES-KW', 256, ['wrapKey', 'unwrapKey']],
  ];

  const tests = Promise.all(kTests.map((args) => test(...args)));

  tests.then(common.mustCall());
}

// Test HMAC Key Generation
{
  async function test(length, hash, usages) {
    const key = await subtle.generateKey({
      name: 'HMAC',
      length,
      hash
    }, true, usages);

    if (length === undefined) {
      switch (hash) {
        case 'SHA-1': length = 512; break;
        case 'SHA-256': length = 512; break;
        case 'SHA-384': length = 1024; break;
        case 'SHA-512': length = 1024; break;
      }
    }

    assert(key);
    assert(isCryptoKey(key));

    assert.strictEqual(key.type, 'secret');
    assert.strictEqual(key.toString(), '[object CryptoKey]');
    assert.strictEqual(key.extractable, true);
    assert.deepStrictEqual(key.usages, usages);
    assert.strictEqual(key.algorithm.name, 'HMAC');
    assert.strictEqual(key.algorithm.length, length);
    assert.strictEqual(key.algorithm.hash.name, hash);
    assert.strictEqual(key.algorithm, key.algorithm);
    assert.strictEqual(key.usages, key.usages);

    [1, false, null].forEach(async (hash) => {
      await assert.rejects(
        subtle.generateKey({ name: 'HMAC', length, hash }, true, usages), {
          name: 'NotSupportedError',
        });
    });
  }

  const kTests = [
    [undefined, 'SHA-1', ['sign', 'verify']],
    [undefined, 'SHA-256', ['sign', 'verify']],
    [undefined, 'SHA-384', ['sign', 'verify']],
    [undefined, 'SHA-512', ['sign', 'verify']],
    [128, 'SHA-256', ['sign', 'verify']],
    [1024, 'SHA-512', ['sign', 'verify']],
  ];

  if (!isBoringSSL) {
    kTests.push(
      [256, 'SHA3-256', ['sign', 'verify']],
      [384, 'SHA3-384', ['sign', 'verify']],
      [512, 'SHA3-512', ['sign', 'verify']],
      // This interaction is not defined for now.
      // https://github.com/WICG/webcrypto-modern-algos/issues/23
      // [undefined, 'SHA3-256', ['sign', 'verify']],
      // [undefined, 'SHA3-384', ['sign', 'verify']],
      // [undefined, 'SHA3-512', ['sign', 'verify']],
    );
  } else {
    common.printSkipMessage('SHA-3 is only supported for digest operations');
  }

  const tests = Promise.all(kTests.map((args) => test(...args)));

  tests.then(common.mustCall());
}

// End user code cannot create CryptoKey directly
assert.throws(() => new CryptoKey(), { code: 'ERR_ILLEGAL_CONSTRUCTOR' });

{
  const buffer = Buffer.from('Hello World');
  const keyObject = createSecretKey(buffer);
  assert(!isCryptoKey(buffer));
  assert(!isCryptoKey(keyObject));
}

// Test OKP Key Generation
{
  async function testFipsUnsupported(name) {
    await assert.rejects(
      subtle.generateKey({ name }, true, ['deriveKey', 'deriveBits']),
      { name: 'NotSupportedError', message: 'Unrecognized algorithm name' });
  }

  async function test(
    name,
    privateUsages,
    publicUsages = privateUsages) {

    let usages = privateUsages;
    if (publicUsages !== privateUsages)
      usages = usages.concat(publicUsages);

    const { publicKey, privateKey } = await subtle.generateKey({
      name,
    }, true, usages);

    assert(publicKey);
    assert(privateKey);
    assert(isCryptoKey(publicKey));
    assert(isCryptoKey(privateKey));

    assert.strictEqual(publicKey.type, 'public');
    assert.strictEqual(privateKey.type, 'private');
    assert.strictEqual(publicKey.toString(), '[object CryptoKey]');
    assert.strictEqual(privateKey.toString(), '[object CryptoKey]');
    assert.strictEqual(publicKey.extractable, true);
    assert.strictEqual(privateKey.extractable, true);
    assert.deepStrictEqual(publicKey.usages, publicUsages);
    assert.deepStrictEqual(privateKey.usages, privateUsages);
    assert.strictEqual(publicKey.algorithm.name, name);
    assert.strictEqual(privateKey.algorithm.name, name);
    assert.strictEqual(privateKey.algorithm, privateKey.algorithm);
    assert.strictEqual(privateKey.usages, privateKey.usages);
    assert.strictEqual(publicKey.algorithm, publicKey.algorithm);
    assert.strictEqual(publicKey.usages, publicKey.usages);
  }

  const kTests = [
    [
      'Ed25519',
      ['sign'],
      ['verify'],
    ],
    [
      'X25519',
      ['deriveKey', 'deriveBits'],
      [],
    ],
  ];

  if (!isBoringSSL) {
    kTests.push(
      [
        'Ed448',
        ['sign'],
        ['verify'],
      ],
      [
        'X448',
        ['deriveKey', 'deriveBits'],
        [],
      ],
    );
  } else {
    common.printSkipMessage('Skipping unsupported Curve448 test cases');
  }

  const tests = kTests.map((args) => {
    const [name] = args;
    if (fips35 && (name === 'X25519' || name === 'X448'))
      return testFipsUnsupported(name);
    return test(...args);
  });

  Promise.all(tests).then(common.mustCall());
}

// Test ML-DSA Key Generation
if (hasOpenSSL(3, 5) || isBoringSSL) {
  async function test(
    name,
    privateUsages,
    publicUsages = privateUsages) {

    let usages = privateUsages;
    if (publicUsages !== privateUsages)
      usages = usages.concat(publicUsages);

    const { publicKey, privateKey } = await subtle.generateKey({
      name,
    }, true, usages);

    assert(publicKey);
    assert(privateKey);
    assert(isCryptoKey(publicKey));
    assert(isCryptoKey(privateKey));

    assert.strictEqual(publicKey.type, 'public');
    assert.strictEqual(privateKey.type, 'private');
    assert.strictEqual(publicKey.toString(), '[object CryptoKey]');
    assert.strictEqual(privateKey.toString(), '[object CryptoKey]');
    assert.strictEqual(publicKey.extractable, true);
    assert.strictEqual(privateKey.extractable, true);
    assert.deepStrictEqual(publicKey.usages, publicUsages);
    assert.deepStrictEqual(privateKey.usages, privateUsages);
    assert.strictEqual(publicKey.algorithm.name, name);
    assert.strictEqual(privateKey.algorithm.name, name);
    assert.strictEqual(privateKey.algorithm, privateKey.algorithm);
    assert.strictEqual(privateKey.usages, privateKey.usages);
    assert.strictEqual(publicKey.algorithm, publicKey.algorithm);
    assert.strictEqual(publicKey.usages, publicKey.usages);
  }

  const kTests = ['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87'];

  const tests = kTests.map((name) => test(name, ['sign'], ['verify']));

  Promise.all(tests).then(common.mustCall());
}

// Test ML-KEM Key Generation
if (hasOpenSSL(3, 5) || isBoringSSL) {
  async function test(
    name,
    privateUsages,
    publicUsages = privateUsages) {

    let usages = privateUsages;
    if (publicUsages !== privateUsages)
      usages = usages.concat(publicUsages);

    const { publicKey, privateKey } = await subtle.generateKey({
      name,
    }, true, usages);

    assert(publicKey);
    assert(privateKey);
    assert(isCryptoKey(publicKey));
    assert(isCryptoKey(privateKey));

    assert.strictEqual(publicKey.type, 'public');
    assert.strictEqual(privateKey.type, 'private');
    assert.strictEqual(publicKey.toString(), '[object CryptoKey]');
    assert.strictEqual(privateKey.toString(), '[object CryptoKey]');
    assert.strictEqual(publicKey.extractable, true);
    assert.strictEqual(privateKey.extractable, true);
    assert.deepStrictEqual(publicKey.usages, publicUsages);
    assert.deepStrictEqual(privateKey.usages, privateUsages);
    assert.strictEqual(publicKey.algorithm.name, name);
    assert.strictEqual(privateKey.algorithm.name, name);
    assert.strictEqual(privateKey.algorithm, privateKey.algorithm);
    assert.strictEqual(privateKey.usages, privateKey.usages);
    assert.strictEqual(publicKey.algorithm, publicKey.algorithm);
    assert.strictEqual(publicKey.usages, publicKey.usages);
  }

  const kTests = ['ML-KEM-768', 'ML-KEM-1024'];

  if (!isBoringSSL) {
    kTests.unshift('ML-KEM-512');
  } else {
    common.printSkipMessage('Skipping unsupported ML-KEM-512 test');
  }

  const tests = kTests.map((name) => test(name,
                                          ['decapsulateKey', 'decapsulateBits'],
                                          ['encapsulateKey', 'encapsulateBits']));

  Promise.all(tests).then(common.mustCall());
}