current commit
260925.32 · crypto: include EC public keys in PKCS8 exports
'use strict';

const {
  TypedArrayPrototypeGetBuffer,
  TypedArrayPrototypeGetByteLength,
} = primordials;

const {
  EcKeyPairGenJob,
  KeyObjectHandle,
  SignJob,
  kCryptoJobWebCrypto,
  kKeyFormatDER,
  kKeyFormatRawPublic,
  kKeyTypePublic,
  kSignJobModeSign,
  kSignJobModeVerify,
  kSigEncP1363,
  kWebCryptoKeyFormatPKCS8,
  kWebCryptoKeyFormatRaw,
  kWebCryptoKeyFormatSPKI,
} = internalBinding('crypto');

const {
  crypto: {
    POINT_CONVERSION_UNCOMPRESSED,
  },
} = internalBinding('constants');

const {
  getUsagesMask,
  jobPromise,
  normalizeHashName,
  validateAlgorithm,
  kNamedCurveAliases,
  toUsagesSet,
} = require('internal/crypto/util');

const {
  lazyDOMException,
} = require('internal/util');

const {
  InternalCryptoKey,
  getCryptoKeyAlgorithm,
  getCryptoKeyHandle,
  getCryptoKeyType,
} = require('internal/crypto/keys');

const {
  createKeyUsages,
  getKeyPairUsages,
  importDerKey,
  importJwkKey,
  importRawKey,
  validateJwk,
  validateKeyUsages,
  validateUsagesNotEmpty,
  verifyAcceptableKeyUse,
} = require('internal/crypto/webcrypto_util');

const kDeriveUsages = createKeyUsages([], ['deriveKey', 'deriveBits']);

const kSignVerifyUsages = createKeyUsages(['verify'], ['sign']);

const kUsages = {
  '__proto__': null,
  'ECDH': kDeriveUsages,
  'ECDSA': kSignVerifyUsages,
};

function ecGenerateKey(algorithm, extractable, usages) {
  const { name, namedCurve } = algorithm;
  const allowedUsages = kUsages[name];
  const usagesSet = validateKeyUsages(usages, allowedUsages.keygen, name);
  validateAlgorithm(algorithm, 'generateKey');

  const keyAlgorithm = { name, namedCurve };
  const keyUsages = getKeyPairUsages(usagesSet, allowedUsages);
  validateUsagesNotEmpty(keyUsages.private);

  return jobPromise(() => new EcKeyPairGenJob(
    kCryptoJobWebCrypto,
    namedCurve,
    undefined,
    keyAlgorithm,
    getUsagesMask(keyUsages.public),
    getUsagesMask(keyUsages.private),
    extractable));
}

function ecExportKey(key, format) {
  try {
    const handle = getCryptoKeyHandle(key);
    switch (format) {
      case kWebCryptoKeyFormatRaw: {
        return TypedArrayPrototypeGetBuffer(
          handle.exportECPublicRaw(POINT_CONVERSION_UNCOMPRESSED));
      }
      case kWebCryptoKeyFormatSPKI: {
        let spki = handle.export(kKeyFormatDER, kWebCryptoKeyFormatSPKI);
        // WebCrypto requires uncompressed point format for SPKI exports.
        // This is a very rare edge case dependent on the imported key
        // using compressed point format.
        // Expected SPKI DER byte lengths with uncompressed points:
        // P-256: 91 = 26 bytes of SPKI ASN.1 + 65-byte uncompressed point.
        // P-384: 120 = 23 bytes of SPKI ASN.1 + 97-byte uncompressed point.
        // P-521: 158 = 25 bytes of SPKI ASN.1 + 133-byte uncompressed point.
        // Difference in initial SPKI ASN.1 is caused by OIDs and length encoding.
        const { namedCurve } = getCryptoKeyAlgorithm(key);
        if (TypedArrayPrototypeGetByteLength(spki) !== {
          '__proto__': null, 'P-256': 91, 'P-384': 120, 'P-521': 158,
        }[namedCurve]) {
          const raw = handle.exportECPublicRaw(POINT_CONVERSION_UNCOMPRESSED);
          const tmp = new KeyObjectHandle();
          tmp.init(kKeyTypePublic, raw, kKeyFormatRawPublic,
                   'ec', null, namedCurve);
          spki = tmp.export(kKeyFormatDER, kWebCryptoKeyFormatSPKI);
        }
        return TypedArrayPrototypeGetBuffer(spki);
      }
      case kWebCryptoKeyFormatPKCS8: {
        return TypedArrayPrototypeGetBuffer(
          handle.exportECPrivatePkcs8());
      }
      default:
        return undefined;
    }
  } catch (err) {
    throw lazyDOMException(
      'The operation failed for an operation-specific reason',
      { name: 'OperationError', cause: err });
  }
}

function ecImportKey(
  format,
  keyData,
  algorithm,
  extractable,
  usages,
) {
  const { name, namedCurve } = algorithm;
  validateAlgorithm(algorithm, 'importKey');

  let handle;
  const allowedUsages = kUsages[name];
  const usagesSet = toUsagesSet(usages);
  switch (format) {
    case 'KeyObjectHandle':
      verifyAcceptableKeyUse(
        name,
        usagesSet,
        keyData.getKeyType() === kKeyTypePublic ?
          allowedUsages.public :
          allowedUsages.private);
      handle = keyData;
      break;
    case 'spki': {
      verifyAcceptableKeyUse(name, usagesSet, allowedUsages.public);
      handle = importDerKey(keyData, true);
      break;
    }
    case 'pkcs8': {
      verifyAcceptableKeyUse(name, usagesSet, allowedUsages.private);
      handle = importDerKey(keyData, false);
      break;
    }
    case 'jwk': {
      const isPublic = keyData.d === undefined;
      verifyAcceptableKeyUse(
        name,
        usagesSet,
        isPublic ? allowedUsages.public : allowedUsages.private);

      const expectedUse = name === 'ECDH' ? 'enc' : 'sig';
      validateJwk(keyData, 'EC', extractable, usagesSet, expectedUse);

      if (keyData.crv !== namedCurve)
        throw lazyDOMException(
          'JWK "crv" does not match the requested algorithm',
          'DataError');

      if (algorithm.name === 'ECDSA' && keyData.alg !== undefined) {
        let algNamedCurve;
        switch (keyData.alg) {
          case 'ES256': algNamedCurve = 'P-256'; break;
          case 'ES384': algNamedCurve = 'P-384'; break;
          case 'ES512': algNamedCurve = 'P-521'; break;
        }
        if (algNamedCurve !== namedCurve)
          throw lazyDOMException(
            'JWK "alg" does not match the requested algorithm',
            'DataError');
      }

      handle = importJwkKey(isPublic, keyData);
      break;
    }
    case 'raw': {
      verifyAcceptableKeyUse(name, usagesSet, allowedUsages.public);
      handle = importRawKey(true, keyData, kKeyFormatRawPublic, 'ec', namedCurve);
      break;
    }
    default:
      return undefined;
  }

  switch (algorithm.name) {
    case 'ECDSA':
      // Fall through
    case 'ECDH':
      if (handle.getAsymmetricKeyType() !== 'ec')
        throw lazyDOMException('Invalid key type', 'DataError');
      break;
  }

  if (!handle.checkEcKeyData()) {
    throw lazyDOMException('Invalid keyData', 'DataError');
  }

  if (kNamedCurveAliases[namedCurve] !==
      handle.keyDetail({ __proto__: null }).namedCurve)
    throw lazyDOMException('Named curve mismatch', 'DataError');

  return new InternalCryptoKey(
    handle,
    { name, namedCurve },
    getUsagesMask(usagesSet),
    extractable);
}

function ecdsaSignVerify(key, data, { name, hash }, signature) {
  const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify;
  const type = mode === kSignJobModeSign ? 'private' : 'public';

  if (getCryptoKeyType(key) !== type)
    throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError');

  return jobPromise(() => new SignJob(
    kCryptoJobWebCrypto,
    mode,
    getCryptoKeyHandle(key),
    undefined,
    undefined,
    undefined,
    undefined,
    data,
    normalizeHashName(hash.name),
    undefined,  // Salt length, not used with ECDSA
    undefined,  // PSS Padding, not used with ECDSA
    kSigEncP1363,
    undefined,
    signature));
}

module.exports = {
  ecExportKey,
  ecImportKey,
  ecGenerateKey,
  ecdsaSignVerify,
};