UNPKG

@hellocoop/email-verification

Version:

Functions for generating and verifying JWT tokens used in the Email Verification Protocol

149 lines 7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.generatePresentationToken = generatePresentationToken; exports.verifyPresentationToken = verifyPresentationToken; const jose_1 = require("jose"); const dns_discovery_js_1 = require("../utils/dns-discovery.js"); const crypto_js_1 = require("../utils/crypto.js"); const time_js_1 = require("../utils/time.js"); const validation_js_1 = require("../utils/validation.js"); const issuance_token_js_1 = require("./issuance-token.js"); const errors_js_1 = require("../errors.js"); /** * Automatically resolves issuer's public key using DNS discovery * @param kid - Key identifier from JWT header * @param issuer - Issuer identifier from JWT payload * @returns Promise resolving to JWK for verification */ async function autoResolveKey(kid, issuer) { if (!issuer) { throw new errors_js_1.InvalidSignatureError('Issuer identifier is required for automatic key resolution'); } if (!kid) { throw new errors_js_1.InvalidSignatureError('Key identifier (kid) is required for automatic key resolution'); } try { // Fetch email-verification metadata from the issuer const metadata = await (0, dns_discovery_js_1.fetchEmailVerificationMetadata)(issuer); // Fetch JWKS from the metadata const jwks = await (0, dns_discovery_js_1.fetchJWKS)(metadata.jwks_uri); // Find the key with matching kid const key = jwks.keys.find((k) => k.kid === kid); if (!key) { throw new errors_js_1.InvalidSignatureError(`Key with kid '${kid}' not found in issuer's JWKS`); } return key; } catch (error) { if (error instanceof errors_js_1.InvalidSignatureError) { throw error; } throw new errors_js_1.InvalidSignatureError(`Automatic key resolution failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Generates a PresentationToken (SD-JWT+KB) for presenting verified email tokens to relying parties * Used by browsers in step 5.2 of the email-verification protocol * * @param sdJwt - SD-JWT string from issuer * @param audience - RP's origin * @param nonce - Nonce from original navigator.credentials.get() call * @param jwk - JWK containing browser's private key, alg, and kid * @param options - Optional token generation options * @returns Promise resolving to SD-JWT+KB string (SD-JWT~KB-JWT) */ async function generatePresentationToken(sdJwt, audience, nonce, jwk, options) { // Validate the JWK (0, crypto_js_1.validateJWK)(jwk); // Validate the SD-JWT format if (!sdJwt || typeof sdJwt !== 'string') { throw new errors_js_1.TokenFormatError('SD-JWT must be a non-empty string'); } // Calculate SHA-256 hash of the SD-JWT const sdHash = (0, crypto_js_1.calculateSHA256Hash)(sdJwt); // Create KB-JWT payload const kbJwtPayload = { aud: audience, nonce: nonce, sd_hash: sdHash, iat: undefined, }; // Ensure iat is set (current time if not provided) const kbJwtPayloadWithIat = (0, time_js_1.ensureIatClaim)(kbJwtPayload); // Extract algorithm from JWK const algorithm = options?.algorithm || jwk.alg; if (!algorithm) { throw new Error('Algorithm must be specified in JWK or options'); } // Import the private key const privateKey = await (0, jose_1.importJWK)(jwk, algorithm); // Create and sign the KB-JWT const kbJwt = await new jose_1.SignJWT(kbJwtPayloadWithIat) .setProtectedHeader({ alg: algorithm, typ: 'kb+jwt', }) .sign(privateKey); // Concatenate SD-JWT and KB-JWT with tilde separator return `${sdJwt}~${kbJwt}`; } /** * Verifies a PresentationToken (SD-JWT+KB) from browsers * Used by relying parties in steps 6.2-6.4 of the email-verification protocol * * @param token - SD-JWT+KB string to verify * @param expectedAudience - Expected audience (RP's origin) * @param expectedNonce - Expected nonce from RP's session * @param keyResolver - Optional callback to resolve issuer's public key for SD-JWT verification. If not provided, uses automatic DNS discovery * @returns Promise resolving to both SD-JWT and KB-JWT verified payloads */ async function verifyPresentationToken(token, expectedAudience, expectedNonce, keyResolver) { // Parse SD-JWT+KB by splitting on tilde separator const { sdJwt, kbJwt } = (0, validation_js_1.parsePresentationToken)(token); // First verify the SD-JWT using the existing verifyIssuanceToken function const resolverToUse = keyResolver || autoResolveKey; const sdJwtPayload = await (0, issuance_token_js_1.verifyIssuanceToken)(sdJwt, resolverToUse); // Parse the KB-JWT const { header: kbHeader, payload: kbPayload } = (0, validation_js_1.parseJWT)(kbJwt); // Validate KB-JWT type (0, validation_js_1.validateJWTType)(kbHeader, 'kb+jwt'); // Validate required KB-JWT header fields if (!kbHeader.alg) { throw new errors_js_1.InvalidSignatureError('KB-JWT header must contain algorithm (alg)'); } // Validate required KB-JWT payload claims (0, validation_js_1.validateRequiredClaims)(kbPayload, ['aud', 'nonce', 'iat', 'sd_hash']); // Validate KB-JWT claims if (kbPayload.aud !== expectedAudience) { throw new errors_js_1.InvalidSignatureError(`KB-JWT audience mismatch. Expected: ${expectedAudience}, Got: ${kbPayload.aud}`); } if (kbPayload.nonce !== expectedNonce) { throw new errors_js_1.InvalidSignatureError(`KB-JWT nonce mismatch. Expected: ${expectedNonce}, Got: ${kbPayload.nonce}`); } // Validate iat claim (0, time_js_1.validateIatForVerification)(kbPayload.iat); // Verify sd_hash matches SHA-256 hash of SD-JWT const expectedSdHash = (0, crypto_js_1.calculateSHA256Hash)(sdJwt); if (kbPayload.sd_hash !== expectedSdHash) { throw new errors_js_1.InvalidSignatureError(`KB-JWT sd_hash mismatch. Expected: ${expectedSdHash}, Got: ${kbPayload.sd_hash}`); } // Extract the public key from SD-JWT's cnf claim for KB-JWT verification if (!sdJwtPayload.cnf || !sdJwtPayload.cnf.jwk) { throw new errors_js_1.InvalidSignatureError('SD-JWT must contain cnf.jwk claim for KB-JWT verification'); } const browserPublicKey = await (0, jose_1.importJWK)(sdJwtPayload.cnf.jwk, kbHeader.alg); // Verify the KB-JWT signature using the browser's public key from SD-JWT try { const { payload: verifiedKbPayload } = await (0, jose_1.jwtVerify)(kbJwt, browserPublicKey, { algorithms: [kbHeader.alg], }); return { sdJwt: sdJwtPayload, kbJwt: verifiedKbPayload, }; } catch (error) { throw new errors_js_1.InvalidSignatureError(`KB-JWT signature verification failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } //# sourceMappingURL=presentation-token.js.map