current commit
260929.23 · crypto: fix raw key export error for wrong key type
'use strict';

const common = require('../common');
const fixtures = require('../common/fixtures');

if (!common.hasCrypto)
  common.skip('missing crypto');

const assert = require('assert');
const crypto = require('crypto');
const { subtle } = globalThis.crypto;

const curves = ['P-256', 'P-384', 'P-521'];

const keyData = {
  'P-521': {
    jwsAlg: 'ES512',
    spki: Buffer.from(
      '30819b301006072a8648ce3d020106052b8104002303818600040156f479f8df' +
      '1e20a7ffc04ce420c3e154ae251996bee42f034b84d41b743f34e45f311b813a' +
      '9cdec8cda59bbbbd31d460b3292521e7c1b722e5667c03db2fae753f01501736' +
      'cfe247394320d8e4afc2fd39b5a9331061b81e2241282b9e17891822b5b79e05' +
      '2f4597b59643fd39379c51bd5125c4f48bc3f025ce3cd36953286ccb38fb',
      'hex'),
    pkcs8: Buffer.from(
      '3081ee020100301006072a8648ce3d020106052b810400230481d63081d3020' +
      '101044200f408758368ba930f30f76ae054fe5cd2ce7fda2c9f76a6d436cf75' +
      'd66c440bfe6331c7c172a12478193c8251487bc91263fa50217f85ff636f59c' +
      'd546e3ab483b4a1818903818600040156f479f8df1e20a7ffc04ce420c3e154' +
      'ae251996bee42f034b84d41b743f34e45f311b813a9cdec8cda59bbbbd31d46' +
      '0b3292521e7c1b722e5667c03db2fae753f01501736cfe247394320d8e4afc2' +
      'fd39b5a9331061b81e2241282b9e17891822b5b79e052f4597b59643fd39379' +
      'c51bd5125c4f48bc3f025ce3cd36953286ccb38fb', 'hex'),
    jwk: {
      kty: 'EC',
      crv: 'P-521',
      x: 'AVb0efjfHiCn_8BM5CDD4VSuJRmWvuQvA0uE1Bt0PzTkXzEbgTqc3sjN' +
          'pZu7vTHUYLMpJSHnwbci5WZ8A9svrnU_',
      y: 'AVAXNs_iRzlDINjkr8L9ObWpMxBhuB4iQSgrnheJGCK1t54FL0W' +
          'XtZZD_Tk3nFG9USXE9IvD8CXOPNNpUyhsyzj7',
      d: 'APQIdYNoupMPMPdq4FT-XNLOf9osn3am1DbPddZsRAv-YzHHw' +
          'XKhJHgZPIJRSHvJEmP6UCF_hf9jb1nNVG46tIO0'
    }
  },
  'P-384': {
    jwsAlg: 'ES384',
    spki: Buffer.from(
      '3076301006072a8648ce3d020106052b8104002203620004219c14d66617b36e' +
      'c6d8856b385b73a74d344fd8ae75ef046435dda54e3b44bd5fbdebd1d08dd69e' +
      '2d7dc1dc218cb435bd28138cc778337a842f6bd61b240e74249f24667c2a5810' +
      'a76bfc28e0335f88a6501dec01976da85afb00869cb6ace8', 'hex'),
    pkcs8: Buffer.from(
      '3081b6020100301006072a8648ce3d020106052b8104002204819e30819b0201' +
      '0104304537b5990784d3c2d22e96a8f92fa1aa492ee873e576a41582e144183c' +
      '9888d10e6b9eb4ced4b2cc4012e4ac5ea84073a16403620004219c14d66617b3' +
      '6ec6d8856b385b73a74d344fd8ae75ef046435dda54e3b44bd5fbdebd1d08dd6' +
      '9e2d7dc1dc218cb435bd28138cc778337a842f6bd61b240e74249f24667c2a58' +
      '10a76bfc28e0335f88a6501dec01976da85afb00869cb6ace8', 'hex'),
    jwk: {
      kty: 'EC',
      crv: 'P-384',
      x: 'IZwU1mYXs27G2IVrOFtzp000T9iude8EZDXdpU47RL1fvevR0I3Wni19wdwhjLQ1',
      y: 'vSgTjMd4M3qEL2vWGyQOdCSfJGZ8KlgQp2v8KOAzX4imUB3sAZdtqFr7AIactqzo',
      d: 'RTe1mQeE08LSLpao-S-hqkku6HPldqQVguFEGDyYiNEOa560ztSyzEAS5KxeqEBz'
    }
  },
  'P-256': {
    jwsAlg: 'ES256',
    spki: Buffer.from(
      '3059301306072a8648ce3d020106082a8648ce3d03010703420004d6e8328a95' +
      'fe29afcdc30977b9251efbb219022807f6b14bb34695b6b4bdb93ee6684548a4' +
      'ad13c49d00433c45315e8274f3540f58f5d79ef7a1b184f4c21d17', 'hex'),
    pkcs8: Buffer.from(
      '308187020100301306072a8648ce3d020106082a8648ce3d030107046d306b02' +
      '010104202bc2eda265e46866efa8f8f99da993175b6c85c246e15dceaed7e307' +
      '0f13fbf8a14403420004d6e8328a95fe29afcdc30977b9251efbb219022807f6' +
      'b14bb34695b6b4bdb93ee6684548a4ad13c49d00433c45315e8274f3540f58f5' +
      'd79ef7a1b184f4c21d17', 'hex'),
    jwk: {
      kty: 'EC',
      crv: 'P-256',
      x: '1ugyipX-Ka_Nwwl3uSUe-7IZAigH9rFLs0aVtrS9uT4',
      y: '5mhFSKStE8SdAEM8RTFegnTzVA9Y9dee96GxhPTCHRc',
      d: 'K8LtomXkaGbvqPj5namTF1tshcJG4V3OrtfjBw8T-_g'
    }
  },
};

const testVectors = [
  {
    name: 'ECDSA',
    privateUsages: ['sign'],
    publicUsages: ['verify']
  },
  {
    name: 'ECDH',
    privateUsages: ['deriveKey', 'deriveBits'],
    publicUsages: []
  },
];

async function testImportSpki({ name, publicUsages }, namedCurve, extractable) {
  const key = await subtle.importKey(
    'spki',
    keyData[namedCurve].spki,
    { name, namedCurve },
    extractable,
    publicUsages);
  assert.strictEqual(key.type, 'public');
  assert.strictEqual(key.extractable, extractable);
  assert.deepStrictEqual(key.usages, publicUsages);
  assert.deepStrictEqual(key.algorithm.name, name);
  assert.deepStrictEqual(key.algorithm.namedCurve, namedCurve);
  assert.strictEqual(key.algorithm, key.algorithm);
  assert.strictEqual(key.usages, key.usages);

  if (extractable) {
    // Test the roundtrip
    const spki = await subtle.exportKey('spki', key);
    assert.strictEqual(
      Buffer.from(spki).toString('hex'),
      keyData[namedCurve].spki.toString('hex'));

    await assert.rejects(
      subtle.exportKey('pkcs8', key), {
        message: 'Key must be a private key',
        name: 'InvalidAccessError',
      });
  } else {
    await assert.rejects(
      subtle.exportKey('spki', key), {
        message: /key is not extractable/,
        name: 'InvalidAccessError',
      });
  }

  // Bad usage
  await assert.rejects(
    subtle.importKey(
      'spki',
      keyData[namedCurve].spki,
      { name, namedCurve },
      extractable,
      ['wrapKey']),
    { message: /Unsupported key usage/ });
}

async function testImportPkcs8(
  { name, privateUsages },
  namedCurve,
  extractable) {
  const key = await subtle.importKey(
    'pkcs8',
    keyData[namedCurve].pkcs8,
    { name, namedCurve },
    extractable,
    privateUsages);
  assert.strictEqual(key.type, 'private');
  assert.strictEqual(key.extractable, extractable);
  assert.deepStrictEqual(key.usages, privateUsages);
  assert.deepStrictEqual(key.algorithm.name, name);
  assert.deepStrictEqual(key.algorithm.namedCurve, namedCurve);
  assert.strictEqual(key.algorithm, key.algorithm);
  assert.strictEqual(key.usages, key.usages);

  if (extractable) {
    // Test the roundtrip
    const pkcs8 = await subtle.exportKey('pkcs8', key);
    assert.strictEqual(
      Buffer.from(pkcs8).toString('hex'),
      keyData[namedCurve].pkcs8.toString('hex'));

    await assert.rejects(
      subtle.exportKey('spki', key), {
        message: 'Key must be a public key',
        name: 'InvalidAccessError',
      });

    for (const format of ['raw', 'raw-public']) {
      await assert.rejects(
        subtle.exportKey(format, key), {
          message: 'Key must be a public key',
          name: 'InvalidAccessError',
        });
    }
  } else {
    await assert.rejects(
      subtle.exportKey('pkcs8', key), {
        message: /key is not extractable/,
        name: 'InvalidAccessError',
      });
  }

  await assert.rejects(
    subtle.importKey(
      'pkcs8',
      keyData[namedCurve].pkcs8,
      { name, namedCurve },
      extractable,
      [/* empty usages */]),
    { name: 'SyntaxError', message: 'Usages cannot be empty when importing a private key.' });
}

async function testImportJwk(
  { name, publicUsages, privateUsages },
  namedCurve,
  extractable) {

  const jwk = keyData[namedCurve].jwk;

  const [
    publicKey,
    privateKey,
  ] = await Promise.all([
    subtle.importKey(
      'jwk',
      {
        kty: jwk.kty,
        crv: jwk.crv,
        x: jwk.x,
        y: jwk.y,
      },
      { name, namedCurve },
      extractable, publicUsages),
    subtle.importKey(
      'jwk',
      jwk,
      { name, namedCurve },
      extractable,
      privateUsages),
    subtle.importKey(
      'jwk',
      {
        alg: name === 'ECDSA' ? keyData[namedCurve].jwsAlg : 'ECDH-ES',
        kty: jwk.kty,
        crv: jwk.crv,
        x: jwk.x,
        y: jwk.y,
      },
      { name, namedCurve },
      extractable, publicUsages),
    subtle.importKey(
      'jwk',
      {
        ...jwk,
        alg: name === 'ECDSA' ? keyData[namedCurve].jwsAlg : 'ECDH-ES',
      },
      { name, namedCurve },
      extractable,
      privateUsages),
  ]);

  assert.strictEqual(publicKey.type, 'public');
  assert.strictEqual(privateKey.type, 'private');
  assert.strictEqual(publicKey.extractable, extractable);
  assert.strictEqual(privateKey.extractable, extractable);
  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);

  if (extractable) {
    // Test the round trip
    const [
      pubJwk,
      pvtJwk,
    ] = await Promise.all([
      subtle.exportKey('jwk', publicKey),
      subtle.exportKey('jwk', privateKey),
    ]);

    assert.deepStrictEqual(pubJwk.key_ops, publicUsages);
    assert.strictEqual(pubJwk.ext, true);
    assert.strictEqual(pubJwk.kty, 'EC');
    assert.strictEqual(pubJwk.x, jwk.x);
    assert.strictEqual(pubJwk.y, jwk.y);
    assert.strictEqual(pubJwk.crv, jwk.crv);

    assert.deepStrictEqual(pvtJwk.key_ops, privateUsages);
    assert.strictEqual(pvtJwk.ext, true);
    assert.strictEqual(pvtJwk.kty, 'EC');
    assert.strictEqual(pvtJwk.x, jwk.x);
    assert.strictEqual(pvtJwk.y, jwk.y);
    assert.strictEqual(pvtJwk.crv, jwk.crv);
    assert.strictEqual(pvtJwk.d, jwk.d);
  } else {
    await assert.rejects(
      subtle.exportKey('jwk', publicKey), {
        message: /key is not extractable/,
        name: 'InvalidAccessError',
      });
    await assert.rejects(
      subtle.exportKey('jwk', privateKey), {
        message: /key is not extractable/,
        name: 'InvalidAccessError',
      });
  }

  {
    const invalidUse = name === 'ECDH' ? 'sig' : 'enc';
    await assert.rejects(
      subtle.importKey(
        'jwk',
        { ...jwk, use: invalidUse },
        { name, namedCurve },
        extractable,
        privateUsages),
      { message: 'Invalid JWK "use" Parameter' });
  }

  if (name === 'ECDSA') {
    await assert.rejects(
      subtle.importKey(
        'jwk',
        { kty: jwk.kty, x: jwk.x, y: jwk.y, crv: jwk.crv, alg: jwk.crv === 'P-256' ? 'ES384' : 'ES256' },
        { name, namedCurve },
        extractable,
        publicUsages),
      { message: 'JWK "alg" does not match the requested algorithm' });

    await assert.rejects(
      subtle.importKey(
        'jwk',
        { ...jwk, alg: jwk.crv === 'P-256' ? 'ES384' : 'ES256' },
        { name, namedCurve },
        extractable,
        privateUsages),
      { message: 'JWK "alg" does not match the requested algorithm' });
  }

  for (const crv of [undefined, namedCurve === 'P-256' ? 'P-384' : 'P-256']) {
    await assert.rejects(
      subtle.importKey(
        'jwk',
        { kty: jwk.kty, x: jwk.x, y: jwk.y, crv },
        { name, namedCurve },
        extractable,
        publicUsages),
      { message: crv ? 'JWK "crv" does not match the requested algorithm' : 'Invalid keyData' });

    await assert.rejects(
      subtle.importKey(
        'jwk',
        { ...jwk, crv },
        { name, namedCurve },
        extractable,
        privateUsages),
      { message: crv ? 'JWK "crv" does not match the requested algorithm' : 'Invalid keyData' });
  }

  await assert.rejects(
    subtle.importKey(
      'jwk',
      { ...jwk },
      { name, namedCurve },
      extractable,
      [/* empty usages */]),
    { name: 'SyntaxError', message: 'Usages cannot be empty when importing a private key.' });

  await assert.rejects(
    subtle.importKey(
      'jwk',
      { kty: jwk.kty, /* missing x */ y: jwk.y, crv: jwk.crv },
      { name, namedCurve },
      extractable,
      publicUsages),
    { name: 'DataError', message: 'Invalid keyData' });
}

async function testImportRaw({ name, publicUsages }, namedCurve) {
  const jwk = keyData[namedCurve].jwk;

  const uncompressedRaw = Buffer.concat([
    Buffer.alloc(1, 0x04),
    Buffer.from(jwk.x, 'base64url'),
    Buffer.from(jwk.y, 'base64url'),
  ]);

  const [publicKey] = await Promise.all([
    subtle.importKey(
      'raw',
      uncompressedRaw,
      { name, namedCurve },
      true, publicUsages),
    subtle.importKey(
      'raw',
      Buffer.concat([
        Buffer.alloc(1, 0x03),
        Buffer.from(jwk.x, 'base64url'),
      ]),
      { name, namedCurve },
      true, publicUsages),
  ]);

  assert.strictEqual(publicKey.type, 'public');
  assert.deepStrictEqual(publicKey.usages, publicUsages);
  assert.strictEqual(publicKey.algorithm.name, name);
  assert.strictEqual(publicKey.algorithm.namedCurve, namedCurve);
  assert.strictEqual(publicKey.algorithm, publicKey.algorithm);
  assert.strictEqual(publicKey.usages, publicKey.usages);

  // Test raw export round-trip (always uncompressed)
  const exported = await subtle.exportKey('raw', publicKey);
  assert.deepStrictEqual(Buffer.from(exported), uncompressedRaw);
}

(async function() {
  const tests = [];
  for (const vector of testVectors) {
    for (const namedCurve of curves) {
      for (const extractable of [true, false]) {
        tests.push(testImportSpki(vector, namedCurve, extractable));
        tests.push(testImportPkcs8(vector, namedCurve, extractable));
        tests.push(testImportJwk(vector, namedCurve, extractable));
      }
      tests.push(testImportRaw(vector, namedCurve));
    }
  }

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

// JWK key usage validation precedes `key_ops` validation.
(async function() {
  const jwk = keyData['P-256'].jwk;
  const publicJwk = {
    kty: jwk.kty,
    crv: jwk.crv,
    x: jwk.x,
    y: jwk.y,
  };

  for (const { name, publicUsages, privateUsages } of testVectors) {
    const invalidUsage = name === 'ECDH' ?
      privateUsages[0] : publicUsages[0];
    const invalidJwk = name === 'ECDH' ? publicJwk : jwk;

    await assert.rejects(
      subtle.importKey(
        'jwk',
        { ...invalidJwk, key_ops: [invalidUsage, invalidUsage] },
        { name, namedCurve: 'P-256' },
        true,
        [invalidUsage]),
      { name: 'SyntaxError', message: /Unsupported key usage/ });

    const validUsage = privateUsages[0];
    await assert.rejects(
      subtle.importKey(
        'jwk',
        { ...jwk, key_ops: [validUsage, validUsage] },
        { name, namedCurve: 'P-256' },
        true,
        [validUsage]),
      { name: 'DataError', message: 'Duplicate key operation' });
  }
})().then(common.mustCall());


// https://github.com/nodejs/node/issues/45859
(async function() {
  const compressed = Buffer.from([48, 57, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, 134, 72, 206, 61, 3, 1, 7, 3, 34, 0, 2, 210, 16, 176, 166, 249, 217, 240, 18, 134, 128, 88, 180, 63, 164, 244, 113, 1, 133, 67, 187, 160, 12, 146, 80, 223, 146, 87, 194, 172, 174, 93, 209]);  // eslint-disable-line @stylistic/js/max-len
  const uncompressed = Buffer.from([48, 89, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, 134, 72, 206, 61, 3, 1, 7, 3, 66, 0, 4, 210, 16, 176, 166, 249, 217, 240, 18, 134, 128, 88, 180, 63, 164, 244, 113, 1, 133, 67, 187, 160, 12, 146, 80, 223, 146, 87, 194, 172, 174, 93, 209, 206, 3, 117, 82, 212, 129, 69, 12, 227, 155, 77, 16, 149, 112, 27, 23, 91, 250, 179, 75, 142, 108, 9, 158, 24, 241, 193, 152, 53, 131, 97, 232]);  // eslint-disable-line @stylistic/js/max-len
  for (const name of ['ECDH', 'ECDSA']) {
    const options = { name, namedCurve: 'P-256' };
    const key = await subtle.importKey('spki', compressed, options, true, []);
    const spki = await subtle.exportKey('spki', key);
    assert.deepStrictEqual(uncompressed, Buffer.from(spki));
  }
})().then(common.mustCall());

{
  const rsaPublic = crypto.createPublicKey(
    fixtures.readKey('rsa_public_2048.pem'));
  const rsaPrivate = crypto.createPrivateKey(
    fixtures.readKey('rsa_private_2048.pem'));

  for (const [name, publicUsages, privateUsages] of [
    ['ECDSA', ['verify'], ['sign']],
    ['ECDH', [], ['deriveBits', 'deriveBits']],
  ]) {
    assert.rejects(
      subtle.importKey(
        'spki',
        rsaPublic.export({ format: 'der', type: 'spki' }),
        { name, namedCurve: 'P-256' },
        true, publicUsages), { message: /Invalid key type/ },
    ).then(common.mustCall());
    assert.rejects(
      subtle.importKey(
        'pkcs8',
        rsaPrivate.export({ format: 'der', type: 'pkcs8' }),
        { name, namedCurve: 'P-256' },
        true, privateUsages), { message: /Invalid key type/ },
    ).then(common.mustCall());
  }
}

// Bad private keys
{
  for (const { namedCurve, key: pkcs8 } of [
    // The private key is exactly equal to the order, and the public key is
    // private key * order.
    {
      namedCurve: 'P-256',
      key: Buffer.from(
        '3066020100301306072a8648ce3d020106082a8648ce3d030107044c304a0201' +
        '010420ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc' +
        '632551a12303210000ffffff00000000ffffffffffffffffbce6faada7179e84' +
        'f3b9cac2fc632551', 'hex'),
    },
    // The private key is exactly equal to the order, and the public key is
    // omitted.
    {
      namedCurve: 'P-256',
      key: Buffer.from(
        '3041020100301306072a8648ce3d020106082a8648ce3d030107042730250201' +
        '010420ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc' +
        '632551', 'hex'),
    },
    // The private key is exactly equal to the order + 11, and the public key is
    // private key * order.
    {
      namedCurve: 'P-521',
      key: Buffer.from(
        '3081ee020100301006072a8648ce3d020106052b810400230481d63081d30201' +
        '01044201ffffffffffffffffffffffffffffffffffffffffffffffffffffffff' +
        'fffffffffa51868783bf2f966b7fcc0148f709a5d03bb5c9b8899c47aebb6fb7' +
        '1e91386414a181890381860004008a75841259fdedff546f1a39573b4315cfed' +
        '5dc7ed7c17849543ef2c54f2991652f3dbc5332663da1bd19b1aebe319108501' +
        '5c024fa4c9a902ecc0e02dda0cdb9a0096fb303fcbba2129849d0ca877054fb2' +
        '293add566210bd0493ed2e95d4e0b9b82b1bc8a90e8b42a4ab3892331914a953' +
        '36dcac80e3f4819b5d58874f92ce48c808', 'hex'),
    },
    // The private key is exactly equal to the order + 11, and the public key is
    // omitted.
    {
      namedCurve: 'P-521',
      key: Buffer.from(
        '3060020100301006072a8648ce3d020106052b81040023044930470201010442' +
        '01ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' +
        'fffa51868783bf2f966b7fcc0148f709a5d03bb5c9b8899c47aebb6fb71e9138' +
        '6414', 'hex'),
    },
  ]) {
    for (const [name, privateUsages] of [
      ['ECDSA', ['sign']],
      ['ECDH', ['deriveBits', 'deriveBits']],
    ]) {
      assert.rejects(
        subtle.importKey(
          'pkcs8',
          pkcs8,
          { name, namedCurve },
          true, privateUsages), { name: 'DataError', message: /Invalid keyData/ },
      ).then(common.mustCall());
    }
  }
}