denoland/deno · error · DOMException

Usages cannot be empty when importing a private key.

Error message

Usages cannot be empty when importing a private key.

What it means

Thrown by PrivateKeyObject.toCryptoKey() when an asymmetric private KeyObject is converted to a WebCrypto CryptoKey with an empty usages array. A private key always corresponds to at least one operation (sign, decrypt, deriveKey, deriveBits), so WebCrypto import rules reject empty usages with a SyntaxError DOMException. The check runs after the algorithm/curve validation, so it only fires once the algorithm name matches the key type.

Source

Thrown at ext/node/polyfills/internal/crypto/keys.ts:910

    };
  }

  toCryptoKey(
    algorithm: string | object,
    extractable: boolean,
    usages: string[],
  ): CryptoKey {
    const algName = typeof algorithm === "string"
      ? algorithm
      : (algorithm as { name: string }).name;

    _validateAsymmetricKeyAlgorithm(this, algName);
    if (typeof algorithm === "object") {
      _validateEcNamedCurve(this, algorithm);
    }

    if (usages.length === 0) {
      throw new DOMException(
        "Usages cannot be empty when importing a private key.",
        "SyntaxError",
      );
    }

    const pkcs8Data = Buffer.from(
      op_node_export_private_key_der(this[kHandle], "pkcs8", null, null),
    );
    return importCryptoKeySync(
      "pkcs8",
      pkcs8Data,
      algorithm,
      extractable,
      usages,
    );
  }

  export(options: any) {

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Pass the operations the key performs: ['sign'] for ECDSA/RSA-PSS, ['decrypt'] for RSA-OAEP, ['deriveKey','deriveBits'] for ECDH/X25519
  2. Keep one algorithm-to-default-usages mapping and use it everywhere
  3. Fail fast in your wrapper if usages is empty, with a message naming the algorithm

Example fix

// before
const ck = privateKey.toCryptoKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, []); // throws

// after
const ck = privateKey.toCryptoKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign']);
Defensive patterns

Strategy: validation

Validate before calling

const DEFAULT_USAGES: Record<string, string[]> = {
  ECDSA: ['sign'], 'RSA-PSS': ['sign'], 'RSA-OAEP': ['decrypt'],
  ECDH: ['deriveBits'], X25519: ['deriveBits'],
};
const usages = requested.length > 0 ? requested : DEFAULT_USAGES[algName];
if (!usages?.length) throw new Error(`no usages for ${algName}`);
const ck = privateKey.toCryptoKey(algorithm, extractable, usages);

Try / catch

try {
  ck = privateKey.toCryptoKey(algorithm, extractable, usages);
} catch (e) {
  if (e instanceof DOMException && e.name === 'SyntaxError' && e.message.includes('Usages cannot be empty')) {
    ck = privateKey.toCryptoKey(algorithm, extractable, ['sign']);
  } else throw e;
}

Prevention

When it happens

Trigger: generateKeyPairSync('ec', { namedCurve: 'prime256v1' }).privateKey.toCryptoKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, []).

Common situations: Wrappers with an optional usages parameter that defaults to empty; converting Node KeyObjects to CryptoKeys in a generic pipeline where usages were never threaded through; porting code that generated CryptoKeys directly and later switched to KeyObject-first APIs.

Related errors


AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20). Data as JSON: /api/errors/57a0ffc6ecd75e65. Report an issue: GitHub.