UNPKG

@hellocoop/email-verification

Version:

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

415 lines 16.5 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.parseJWTIndependent = parseJWTIndependent; exports.verifyJWTSignatureIndependent = verifyJWTSignatureIndependent; exports.calculateSHA256Independent = calculateSHA256Independent; exports.verifyPresentationTokenIndependent = verifyPresentationTokenIndependent; exports.verifyRequestTokenIndependent = verifyRequestTokenIndependent; exports.verifyIssuanceTokenIndependent = verifyIssuanceTokenIndependent; const crypto_1 = require("crypto"); const buffer_1 = require("buffer"); /** * Independent JWT verification using Node.js crypto module * This provides verification separate from the jose library to validate our token generation */ /** * Base64url decode a string */ function base64urlDecode(str) { // Add padding if needed const padded = str + '='.repeat((4 - (str.length % 4)) % 4); // Replace URL-safe characters const base64 = padded.replace(/-/g, '+').replace(/_/g, '/'); return buffer_1.Buffer.from(base64, 'base64'); } /** * Base64url encode a buffer */ function base64urlEncode(buffer) { return buffer .toString('base64') .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, ''); } /** * Parse JWT into components */ function parseJWTIndependent(token) { const parts = token.split('.'); if (parts.length !== 3) { throw new Error('Invalid JWT format'); } const [headerPart, payloadPart, signaturePart] = parts; const header = JSON.parse(base64urlDecode(headerPart).toString('utf8')); const payload = JSON.parse(base64urlDecode(payloadPart).toString('utf8')); return { header, payload, signature: signaturePart }; } /** * Convert JWK to KeyObject using Node.js crypto */ function jwkToKeyObject(jwk) { return (0, crypto_1.createPublicKey)({ key: jwk, format: 'jwk' }); } /** * Verify JWT signature independently using Node.js crypto */ async function verifyJWTSignatureIndependent(token) { const { header, payload, signature } = parseJWTIndependent(token); // Get the signing input (header.payload) const parts = token.split('.'); const signingInput = `${parts[0]}.${parts[1]}`; let keyObject; let algorithm; // Convert JWK to KeyObject based on key type try { keyObject = jwkToKeyObject(header.jwk); if (header.jwk.kty === 'RSA') { algorithm = 'RSA-SHA256'; } else if (header.jwk.kty === 'OKP' && header.jwk.crv === 'Ed25519') { algorithm = 'ed25519'; } else { throw new Error(`Unsupported key type: ${header.jwk.kty}`); } } catch (error) { throw new Error(`Failed to create key object: ${error instanceof Error ? error.message : 'Unknown error'}`); } // Verify signature let valid; const signatureBuffer = base64urlDecode(signature); if (algorithm === 'ed25519') { // For EdDSA, use verify method directly without createVerify const { verify } = await Promise.resolve().then(() => __importStar(require('crypto'))); valid = verify(null, buffer_1.Buffer.from(signingInput), keyObject, signatureBuffer); } else { // For RSA and other algorithms, use createVerify const verifier = (0, crypto_1.createVerify)(algorithm); verifier.update(signingInput); valid = verifier.verify(keyObject, signatureBuffer); } return { valid, header, payload }; } /** * Calculate SHA-256 hash independently */ function calculateSHA256Independent(input) { const hash = (0, crypto_1.createHash)('sha256'); hash.update(input); return base64urlEncode(hash.digest()); } /** * Verify PresentationToken (SD-JWT+KB) independently */ async function verifyPresentationTokenIndependent(token, issuerPublicKey, expectedAudience, expectedNonce) { const errors = []; try { // Parse SD-JWT+KB by splitting on tilde const parts = token.split('~'); if (parts.length !== 2) { errors.push('PresentationToken must contain exactly one tilde separator'); return { valid: false, sdJwtPayload: null, kbJwtPayload: null, errors, }; } const [sdJwt, kbJwt] = parts; if (!sdJwt || !kbJwt) { errors.push('PresentationToken parts cannot be empty'); return { valid: false, sdJwtPayload: null, kbJwtPayload: null, errors, }; } // Verify SD-JWT independently const sdJwtVerification = await verifyIssuanceTokenIndependent(sdJwt, issuerPublicKey); if (!sdJwtVerification.valid) { errors.push(...sdJwtVerification.errors.map((e) => `SD-JWT: ${e}`)); } // Parse KB-JWT const { header: kbHeader, payload: kbPayload } = parseJWTIndependent(kbJwt); // Validate KB-JWT header if (kbHeader.typ !== 'kb+jwt') { errors.push(`Expected KB-JWT typ 'kb+jwt', got '${kbHeader.typ}'`); } if (!kbHeader.alg) { errors.push('Missing alg in KB-JWT header'); } // Validate KB-JWT payload const requiredKbClaims = ['aud', 'nonce', 'iat', 'sd_hash']; for (const claim of requiredKbClaims) { if (kbPayload[claim] === undefined) { errors.push(`Missing required KB-JWT claim: ${claim}`); } } // Validate KB-JWT claims if (kbPayload.aud !== expectedAudience) { errors.push(`KB-JWT audience mismatch. Expected: ${expectedAudience}, Got: ${kbPayload.aud}`); } if (kbPayload.nonce !== expectedNonce) { errors.push(`KB-JWT nonce mismatch. Expected: ${expectedNonce}, Got: ${kbPayload.nonce}`); } // Validate iat (within 60 seconds) if (kbPayload.iat) { const currentTime = Math.floor(Date.now() / 1000); const timeDiff = Math.abs(currentTime - kbPayload.iat); if (timeDiff > 60) { errors.push(`KB-JWT iat claim outside acceptable window: ${timeDiff}s`); } } // Verify sd_hash matches SHA-256 hash of SD-JWT const expectedSdHash = calculateSHA256Independent(sdJwt); if (kbPayload.sd_hash !== expectedSdHash) { errors.push(`KB-JWT sd_hash mismatch. Expected: ${expectedSdHash}, Got: ${kbPayload.sd_hash}`); } // Verify KB-JWT signature using browser's public key from SD-JWT cnf claim let kbSignatureValid = false; if (sdJwtVerification.valid && sdJwtVerification.payload && sdJwtVerification.payload.cnf && sdJwtVerification.payload.cnf.jwk) { try { const browserPublicKey = sdJwtVerification.payload.cnf.jwk; const kbSigningInput = kbJwt.split('.').slice(0, 2).join('.'); const kbSignature = kbJwt.split('.')[2]; const keyObject = jwkToKeyObject(browserPublicKey); let algorithm; if (browserPublicKey.kty === 'RSA') { algorithm = 'RSA-SHA256'; } else if (browserPublicKey.kty === 'OKP' && browserPublicKey.crv === 'Ed25519') { algorithm = 'ed25519'; } else { throw new Error(`Unsupported browser key type: ${browserPublicKey.kty}`); } const kbSignatureBuffer = base64urlDecode(kbSignature); if (algorithm === 'ed25519') { const { verify } = await Promise.resolve().then(() => __importStar(require('crypto'))); kbSignatureValid = verify(null, buffer_1.Buffer.from(kbSigningInput), keyObject, kbSignatureBuffer); } else { const verifier = (0, crypto_1.createVerify)(algorithm); verifier.update(kbSigningInput); kbSignatureValid = verifier.verify(keyObject, kbSignatureBuffer); } if (!kbSignatureValid) { errors.push('Invalid KB-JWT signature'); } } catch (error) { errors.push(`KB-JWT signature verification error: ${error instanceof Error ? error.message : 'Unknown error'}`); } } else { errors.push('Cannot verify KB-JWT signature: SD-JWT verification failed or missing cnf.jwk'); } const overallValid = sdJwtVerification.valid && kbSignatureValid && errors.length === 0; return { valid: overallValid, sdJwtPayload: sdJwtVerification.payload, kbJwtPayload: kbPayload, errors, }; } catch (error) { errors.push(`Verification error: ${error instanceof Error ? error.message : 'Unknown error'}`); return { valid: false, sdJwtPayload: null, kbJwtPayload: null, errors }; } } /** * Verify RequestToken independently */ async function verifyRequestTokenIndependent(token) { const errors = []; try { const { valid, header, payload } = await verifyJWTSignatureIndependent(token); if (!valid) { errors.push('Invalid signature'); } // Validate header if (header.typ !== 'JWT') { errors.push(`Expected typ 'JWT', got '${header.typ}'`); } if (!header.jwk) { errors.push('Missing jwk in header'); } if (!header.alg) { errors.push('Missing alg in header'); } // Validate payload const requiredClaims = ['aud', 'iat', 'nonce', 'email']; for (const claim of requiredClaims) { if (payload[claim] === undefined) { errors.push(`Missing required claim: ${claim}`); } } // Validate email format if (payload.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(payload.email)) { errors.push('Invalid email format'); } // Validate iat (within 60 seconds) if (payload.iat) { const currentTime = Math.floor(Date.now() / 1000); const timeDiff = Math.abs(currentTime - payload.iat); if (timeDiff > 60) { errors.push(`iat claim outside acceptable window: ${timeDiff}s`); } } return { valid: valid && errors.length === 0, payload, errors }; } catch (error) { errors.push(`Verification error: ${error instanceof Error ? error.message : 'Unknown error'}`); return { valid: false, payload: null, errors }; } } /** * Verify IssuanceToken (SD-JWT) independently using external key */ async function verifyIssuanceTokenIndependent(token, issuerPublicKey) { const errors = []; try { const { header, payload } = parseJWTIndependent(token); // Validate header if (header.typ !== 'evp+sd-jwt') { errors.push(`Expected typ 'evp+sd-jwt', got '${header.typ}'`); } if (!header.kid) { errors.push('Missing kid in header'); } if (!header.alg) { errors.push('Missing alg in header'); } // Validate payload const requiredClaims = ['iss', 'iat', 'cnf', 'email', 'email_verified']; for (const claim of requiredClaims) { if (payload[claim] === undefined) { errors.push(`Missing required claim: ${claim}`); } } // Validate email format if (payload.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(payload.email)) { errors.push('Invalid email format'); } // Validate email_verified if (payload.email_verified !== true) { errors.push('email_verified must be true'); } // Validate cnf claim structure if (!payload.cnf || !payload.cnf.jwk) { errors.push('Missing cnf.jwk claim'); } // Validate cnf.jwk contains only public key parameters if (payload.cnf && payload.cnf.jwk) { const jwk = payload.cnf.jwk; if (jwk.d) { errors.push('cnf.jwk should not contain private key material (d parameter)'); } // Check required public key parameters based on key type if (jwk.kty === 'RSA') { if (!jwk.n || !jwk.e) { errors.push('RSA public key missing required parameters (n, e)'); } } else if (jwk.kty === 'OKP') { if (!jwk.x || !jwk.crv) { errors.push('OKP public key missing required parameters (x, crv)'); } } } // Validate iat (within 60 seconds) if (payload.iat) { const currentTime = Math.floor(Date.now() / 1000); const timeDiff = Math.abs(currentTime - payload.iat); if (timeDiff > 60) { errors.push(`iat claim outside acceptable window: ${timeDiff}s`); } } // Verify signature using provided issuer public key const signingInput = token.split('.').slice(0, 2).join('.'); const signature = token.split('.')[2]; let keyObject; let algorithm; try { keyObject = jwkToKeyObject(issuerPublicKey); if (issuerPublicKey.kty === 'RSA') { algorithm = 'RSA-SHA256'; } else if (issuerPublicKey.kty === 'OKP' && issuerPublicKey.crv === 'Ed25519') { algorithm = 'ed25519'; } else { throw new Error(`Unsupported key type: ${issuerPublicKey.kty}`); } } catch (error) { errors.push(`Failed to create key object: ${error instanceof Error ? error.message : 'Unknown error'}`); return { valid: false, payload, errors }; } // Verify signature let signatureValid = false; const signatureBuffer = base64urlDecode(signature); if (algorithm === 'ed25519') { const { verify } = await Promise.resolve().then(() => __importStar(require('crypto'))); signatureValid = verify(null, buffer_1.Buffer.from(signingInput), keyObject, signatureBuffer); } else { const verifier = (0, crypto_1.createVerify)(algorithm); verifier.update(signingInput); signatureValid = verifier.verify(keyObject, signatureBuffer); } if (!signatureValid) { errors.push('Invalid signature'); } return { valid: signatureValid && errors.length === 0, payload, errors }; } catch (error) { errors.push(`Verification error: ${error instanceof Error ? error.message : 'Unknown error'}`); return { valid: false, payload: null, errors }; } } //# sourceMappingURL=independent-verify.js.map