@hellocoop/email-verification
Version:
Functions for generating and verifying JWT tokens used in the Email Verification Protocol
415 lines • 16.5 kB
JavaScript
;
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