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