generateJWK() / generateKey()

Make a fresh key for any JWS or JWE algorithm. generateJWK returns JSON ready to store; generateKey returns Web Crypto objects unless you ask for JSON.

import { generateJWK } from "unjwt/jwk";

const secret = await generateJWK("HS256");
// {
//   alg: "HS256",
//   ext: true,
//   k: "sQJeXE6RCGCL0PQ-oG9nK3HW…",
//   key_ops: ["sign", "verify"],
//   kty: "oct",
//   kid: "Z7Vbm5-ga-jt60sVMc9u6T5_V5uPrO_dUQNw-6uVeBM",
// }

const { privateKey, publicKey } = await generateJWK("ES256");
// privateKey: { crv: "P-256", d: "V5UCmgY_…", x: "mGQhkJr2…", y: "m-i6Bvx2…", kty: "EC",
//               key_ops: ["sign"], ext: true, alg: "ES256", kid: "vlj-lU5hMsOLA09r…" }
// publicKey:  the same without `d`, with key_ops: ["verify"] and the same kid

Reach for generateJWK by default. Use generateKey when you want a CryptoKey that never leaves the process (see Non-extractable keys).

#Signatures

function generateJWK<TAlg extends GenerateKeyAlgorithm, Kid extends string | false = string>(
  alg: TAlg,
  options?: {
    "kid"?: Kid;
    "keyUsage"?: KeyUsage[];
    "modulusLength"?: number;
    "publicExponent"?: Uint8Array;
    "namedCurve"?: "P-256" | "P-384" | "P-521" | "X25519" | "Ed25519";
    // JWK metadata merged onto the result
    "use"?: string;
    "x5c"?: string[];
    "x5t"?: string;
    "x5t#S256"?: string;
    "x5u"?: string;
  },
): Promise<GeneratedJWKMap<Kid>[TAlg]>;

function generateKey<TAlg extends GenerateKeyAlgorithm>(
  alg: TAlg,
  options?: {
    toJWK?: boolean;
    extractable?: boolean;
    kid?: string | false;
    keyUsage?: KeyUsage[];
    modulusLength?: number;
    publicExponent?: Uint8Array;
    namedCurve?: "P-256" | "P-384" | "P-521" | "X25519" | "Ed25519";
  },
): Promise<GeneratedKeyMap[TAlg] | GeneratedJWKMap[TAlg]>;

GenerateKeyAlgorithm is every JWS and JWE algorithm except "none", "dir" and the PBES2 family. A password-based key comes from deriveJWKFromPassword().

#Options

OptionTypeDefaultWhat it does
kidstring | falsethe RFC 7638 thumbprintA string is set verbatim. false sets no kid. Anything else is ERR_INVALID_OPTION. On generateKey it is read only with toJWK: true.
toJWKbooleanfalsegenerateKey only. Return JWKs instead of Web Crypto objects.
extractablebooleantruegenerateKey only. Ignored with toJWK, which must export the key.
keyUsageKeyUsage[]derived from algNarrows the usages. A usage the algorithm cannot serve, or a list that leaves one half of a pair with nothing, is ERR_INVALID_OPTION.
modulusLengthnumber2048RSA only. Below 2048 bits is ERR_INVALID_OPTION (RFC 7518 §3.3).
publicExponentUint8Array0x010001RSA only. Big-endian bytes.
namedCurvesee signature"P-256" for EC, "Ed25519" for OKPES256/ES384/ES512 fix their curve, so a different one is ERR_INVALID_OPTION. ECDH-ES* picks from P-256, P-384, P-521 and X25519. EdDSA accepts Ed25519 only.
use, x5c, x5t, x5t#S256, x5uJWK membersnonegenerateJWK only. Merged onto the key (both halves of a pair). alg, kty, key_ops, ext and key material are reserved and throw ERR_INVALID_OPTION.

#Return shape

The algorithm decides the shape. Every JWK carries alg and, unless kid: false, a kid.

AlgorithmgenerateKey(alg)generateJWK(alg) or generateKey(alg, { toJWK: true })
HS*, A*KW, A*GCM, A*GCMKWCryptoKeyone oct JWK
A128CBC-HS256, A192CBC-HS384, A256CBC-HS512Uint8Array (Web Crypto has no such key type)one oct JWK
RS*, PS*, RSA-OAEP*CryptoKeyPair{ privateKey, publicKey }, kty: "RSA"
ES*CryptoKeyPair{ privateKey, publicKey }, kty: "EC"
Ed25519, EdDSACryptoKeyPair{ privateKey, publicKey }, kty: "OKP"
ECDH-ES*CryptoKeyPair{ privateKey, publicKey }, kty: "EC", or "OKP" for X25519

The table is two exported types, GeneratedKeyMap and GeneratedJWKMap, keyed by algorithm. A literal alg narrows the result, so no cast is needed:

import { generateJWK, generateKey } from "unjwt/jwk";

const a = await generateJWK("HS256");
// typeof a: JWK_oct<"HS256"> & { alg: "HS256"; kid: string }

const b = await generateJWK("HS256", { kid: false });
// typeof b: JWK_oct<"HS256"> & { alg: "HS256" }

const c = await generateJWK("ES256");
// typeof c: {
//   privateKey: JWK_EC_Private<"ES256"> & { alg: "ES256"; kid: string };
//   publicKey: JWK_EC_Public<"ES256"> & { alg: "ES256"; kid: string };
// }

const d = await generateKey("HS256"); // typeof d: CryptoKey
const e = await generateKey("ES256"); // typeof e: CryptoKeyPair
const f = await generateKey("A128CBC-HS256"); // typeof f: Uint8Array<ArrayBuffer>
const g = await generateKey("ES256", { toJWK: true }); // typeof g: GeneratedJWKMap["ES256"]

A non-literal algorithm (a variable typed GenerateKeyAlgorithm) returns the union of its entries.

#The default kid is a thumbprint

A kid is the name a token carries in its header so a verifier can find the right key. When you do not pass one, the key is named by its RFC 7638 thumbprint: a SHA-256 digest of the key's required public members. Two consequences follow. Both halves of a pair get the same kid, because the private members are not part of the digest. And two different keys never share a kid by accident.

import { generateJWK, thumbprint } from "unjwt/jwk";

const key = await generateJWK("HS256");
key.kid === (await thumbprint(key)); // true

await generateJWK("HS256", { kid: "hmac-2026-10" }); // kid: "hmac-2026-10"
await generateJWK("HS256", { kid: false }); // no kid member at all

thumbprint(jwk, { hash }) is exported too, with hash one of "SHA-256" (default), "SHA-384" or "SHA-512". Passing your own kid, or false, skips the digest.

#Wrapping keys get the alg you asked for

generateJWK("A256GCMKW") returns a key with alg: "A256GCMKW". Web Crypto itself only knows AES-GCM, so exporting the same key through exportKey() labels it A256GCM, the content encryption role. Mint key-wrapping keys with generateJWK so the label matches the job.

#Non-extractable keys

generateKey defaults to extractable: true so keys can be exported. For a long-lived private key that should never leave the process, ask for a non-extractable pair:

import { generateKey } from "unjwt/jwk";

const pair = await generateKey("ES256", { extractable: false });
pair.privateKey.extractable; // false
pair.publicKey.extractable; // true, Web Crypto always keeps public keys exportable

The private CryptoKey still signs, but its material cannot be read back out.

#Errors

Causecode
An algorithm with no generated key (PBES2-*, dir, none, anything unknown)ERR_JWK_UNSUPPORTED
EdDSA with a curve other than Ed25519, or ECDH-ES* with an unknown curveERR_JWK_UNSUPPORTED
modulusLength below 2048, a malformed publicExponentERR_INVALID_OPTION
A namedCurve that contradicts ES* or Ed25519ERR_INVALID_OPTION
A keyUsage the algorithm cannot serve, or one that empties a pair halfERR_INVALID_OPTION
A kid that is neither a string nor false, or a reserved JWK member in the metadataERR_INVALID_OPTION
await generateJWK("ES256", { keyUsage: ["sign"] });
// JWTError ERR_INVALID_OPTION: "keyUsage" leaves the public half of a ES256 key pair with no usage; include one of verify.

#See also