/** * DPoP proof generation for e2e tests */ import { base64UrlEncode, computeJwkThumbprint } from '../../src/pds.js'; /** * Generate an ES256 key pair for DPoP * @returns {Promise<{privateKey: CryptoKey, publicKey: CryptoKey, jwk: object}>} */ export async function generateKeyPair() { const keyPair = await crypto.subtle.generateKey( { name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify'], ); const jwk = await crypto.subtle.exportKey('jwk', keyPair.publicKey); const publicJwk = { kty: jwk.kty, crv: jwk.crv, x: jwk.x, y: jwk.y }; return { privateKey: keyPair.privateKey, publicKey: keyPair.publicKey, jwk: publicJwk, }; } /** * Create a DPoP proof JWT * @param {object} params * @param {CryptoKey} params.privateKey * @param {object} params.jwk * @param {string} params.method * @param {string} params.url * @param {string} [params.accessToken] * @returns {Promise} */ export async function createDpopProof({ privateKey, jwk, method, url, accessToken, }) { const header = { typ: 'dpop+jwt', alg: 'ES256', jwk }; const payload = { jti: base64UrlEncode(crypto.getRandomValues(new Uint8Array(16))), htm: method, htu: url, iat: Math.floor(Date.now() / 1000), }; if (accessToken) { const hash = await crypto.subtle.digest( 'SHA-256', new TextEncoder().encode(accessToken), ); payload.ath = base64UrlEncode(new Uint8Array(hash)); } const headerB64 = base64UrlEncode( new TextEncoder().encode(JSON.stringify(header)), ); const payloadB64 = base64UrlEncode( new TextEncoder().encode(JSON.stringify(payload)), ); const signingInput = `${headerB64}.${payloadB64}`; const signature = await crypto.subtle.sign( { name: 'ECDSA', hash: 'SHA-256' }, privateKey, new TextEncoder().encode(signingInput), ); return `${signingInput}.${base64UrlEncode(new Uint8Array(signature))}`; } /** * DPoP client helper */ export class DpopClient { #privateKey; #jwk; #jkt = null; constructor(privateKey, jwk) { this.#privateKey = privateKey; this.#jwk = jwk; } static async create() { const { privateKey, jwk } = await generateKeyPair(); return new DpopClient(privateKey, jwk); } async getJkt() { if (!this.#jkt) this.#jkt = await computeJwkThumbprint(this.#jwk); return this.#jkt; } getJwk() { return this.#jwk; } async createProof(method, url, accessToken) { return createDpopProof({ privateKey: this.#privateKey, jwk: this.#jwk, method, url, accessToken, }); } }