/**
* JWS / JWK Cryptographic Utilities
* ES256 key generation, payload signing, and signature verification using jose.
*/
import { randomUUID } from 'node:crypto';
import { generateKeyPair, exportJWK, importJWK, CompactSign, compactVerify } from 'jose';
import type { JWK } from 'jose';
/**
* Generate a new ES256 (P-256) key pair in JWK format.
*/
export async function generateES256KeyPair(): Promise<{
publicKey: JWK;
privateKey: JWK;
}> {
const { publicKey, privateKey } = await generateKeyPair('ES256', { extractable: true });
const publicJWK = await exportJWK(publicKey);
const privateJWK = await exportJWK(privateKey);
return {
publicKey: publicJWK,
privateKey: privateJWK,
};
}
/**
* Sign a JSON payload with an ES256 private key.
* Returns a JWS compact serialization string.
*/
export async function signPayload(
payload: Record<string, unknown>,
privateKey: JWK,
): Promise<string> {
const key = await importJWK(privateKey, 'ES256');
const encoder = new TextEncoder();
const data = encoder.encode(JSON.stringify(payload));
const jws = await new CompactSign(data)
.setProtectedHeader({ alg: 'ES256', typ: 'mandate+jwt' })
.sign(key);
return jws;
}
/**
* Verify a JWS compact serialization token against an ES256 public key.
* Returns the decoded payload if valid, null otherwise.
*/
export async function verifySignature(
token: string,
publicKey: JWK,
): Promise<{ valid: boolean; payload: Record<string, unknown> | null }> {
try {
const key = await importJWK(publicKey, 'ES256');
const { payload: rawPayload } = await compactVerify(token, key);
const decoder = new TextDecoder();
const parsed = JSON.parse(decoder.decode(rawPayload)) as Record<string, unknown>;
return { valid: true, payload: parsed };
} catch {
return { valid: false, payload: null };
}
}
/**
* Generate a random Key ID (kid) for JWK identification.
*/
export function generateKid(): string {
return randomUUID();
}