UNPKG

vouchsafe

Version:

Self-verifying identity and offline trust verification for JWTs, including attestations, vouches, revocations, and multi-hop trust chains.

1,487 lines (1,403 loc) 108 kB
'use strict'; const VOUCHSAFE_SPEC_VERSION = "1.3.0"; // crypto/index.mjs let cryptoImpl = null; async function loadImpl() { if (cryptoImpl) return cryptoImpl; const isNode = typeof process !== 'undefined' && process.versions?.node && typeof window === 'undefined'; cryptoImpl = isNode ? await Promise.resolve().then(function () { return require('./node-XM4sulif.js'); }) : await Promise.resolve().then(function () { return require('./browser-z_Qw4Knz.js'); }); return cryptoImpl; } const generateKeyPair = async function(...args) { const { generateKeyPair } = await loadImpl(); return generateKeyPair(...args); }; const sha256 = async function(...args) { const { sha256 } = await loadImpl(); return sha256(...args); }; const sha512 = async function(...args) { const { sha512 } = await loadImpl(); return sha512(...args); }; const getKeyBytes = async function(...args) { const { getKeyBytes } = await loadImpl(); return getKeyBytes(...args); }; const ALPHABET = 'abcdefghijklmnopqrstuvwxyz234567'; /** * Encode a byte array into base32 (RFC 4648, lowercase, no padding) * @param {Uint8Array} bytes * @returns {string} */ function base32Encode(bytes) { let bits = 0; let value = 0; let output = ''; for (let i = 0; i < bytes.length; i++) { value = (value << 8) | bytes[i]; bits += 8; while (bits >= 5) { output += ALPHABET[(value >>> (bits - 5)) & 31]; bits -= 5; } } if (bits > 0) { output += ALPHABET[(value << (5 - bits)) & 31]; } return output; } /** * Decode a base32 string (RFC 4648, lowercase, no padding) into a byte array * @param {string} str * @returns {Uint8Array} */ function base32Decode(str) { const clean = str.toLowerCase().replace(/=+$/, ''); let bits = 0; let value = 0; const output = []; for (let i = 0; i < clean.length; i++) { const idx = ALPHABET.indexOf(clean[i]); if (idx === -1) throw new Error(`Invalid base32 character: ${clean[i]}`); value = (value << 5) | idx; bits += 5; if (bits >= 8) { output.push((value >>> (bits - 8)) & 0xff); bits -= 8; } } return new Uint8Array(output); } function toBase64(input) { if (typeof input === 'string') { // Assume it's already base64 (classic or url-safe) — return as-is return input; } if (!(input instanceof Uint8Array)) { throw new Error('Expected Uint8Array or base64 string'); } if (typeof window === 'undefined') { // Node.js return Buffer.from(input).toString('base64'); } else { // Browser return btoa(String.fromCharCode(...input)); } } function isValidUUID(input) { return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(input); } const SUPPORTED_HASHES = { sha256, sha512, }; /** * Create a new Vouchsafe identity * @param {string} label - Required, lowercase a-z0-9 and hyphen * @param {string} hashAlg - Optional: 'sha256' (default) or 'sha512' * @returns {Promise<{ urn, publicKey, privateKey, publicKeyHash }>} */ async function createVouchsafeIdentity(label, hashAlg = 'sha256') { if (!label || typeof label !== 'string' || !/^[a-zA-Z0-9\-_%\+]{1,32}$/.test(label)) { throw new Error("Invalid label. Must be lowercase, 1–32 chars, letters/numbers/hyphens only."); } const hashFn = SUPPORTED_HASHES[hashAlg]; if (!hashFn) throw new Error(`Unsupported hash algorithm: ${hashAlg}`); const { publicKey, privateKey } = await generateKeyPair(); const pemPubBytes = new Uint8Array(publicKey); const rawPubKey = await getKeyBytes('public', publicKey); const pubBytes = new Uint8Array(rawPubKey); const hash = new Uint8Array(await hashFn(pubBytes)); const hashB32 = base32Encode(hash).toLowerCase(); const urn = `urn:vouchsafe:${label}.${hashB32}` + (hashAlg !== 'sha256' ? `.${hashAlg}` : ''); return { urn, keypair: { publicKey: toBase64(pemPubBytes), privateKey: toBase64(new Uint8Array(privateKey)), }, publicKeyHash: hashB32, version: VOUCHSAFE_SPEC_VERSION }; } /** * Verify a Vouchsafe URN matches a given public key * @param {string} urn * @param {string} publicKeyBase64 * @returns {Promise<boolean>} */ async function verifyUrnMatchesKey(urn, publicKeyBase64) { const match = urn.match(/^urn:vouchsafe:([a-zA-Z0-9\-_%\+]+)\.([a-z2-7]{52})(?:\.(sha256|sha512))?$/); if (!match) return false; const [, , expectedHash, hashAlg = 'sha256'] = match; const hashFn = SUPPORTED_HASHES[hashAlg]; if (!hashFn) return false; const rawPubKey = await getKeyBytes('public', publicKeyBase64); const pubBytes = new Uint8Array(rawPubKey); const hash = new Uint8Array(await hashFn(pubBytes)); const actualB32 = base32Encode(hash).toLowerCase(); return actualB32 === expectedHash; } /** * Create a Vouchsafe identity from an existing DER-based Ed25519 keypair * @param {string} label - human-readable label (3–32 chars, a-zA-Z0-9-_+%) * @param {{ publicKey: string, privateKey: string }} keypair - base64-encoded DER keys * @param {string} hashAlg - 'sha256' (default) or 'sha512' * @returns {Promise<{ urn, keypair, publicKeyHash, version }>} */ async function createVouchsafeIdentityFromKeypair(label, keypair, hashAlg = 'sha256') { if (!label || typeof label !== 'string' || !/^[a-zA-Z0-9\-_%\+]{3,32}$/.test(label)) { throw new Error("Invalid label. Must be 1–32 characters (a–z, 0–9, -, _, %, +)."); } const hashFn = SUPPORTED_HASHES[hashAlg]; if (!hashFn) throw new Error(`Unsupported hash algorithm: ${hashAlg}`); if (!keypair || typeof keypair !== 'object' || !keypair.publicKey || !keypair.privateKey) { throw new Error("Keypair must include base64-encoded publicKey and privateKey."); } // Verify public key and extract raw bytes (throws if invalid) const rawPubKey = await getKeyBytes('public', keypair.publicKey); // Optionally verify private key format and Ed25519 algorithm await getKeyBytes('private', keypair.privateKey); // throws if invalid // Hash the raw public key for URN const pubBytes = new Uint8Array(rawPubKey); const hash = new Uint8Array(await hashFn(pubBytes)); const hashB32 = base32Encode(hash).toLowerCase(); /* const hash = new Uint8Array(await hashFn(rawPubKey)); const hashB32 = base32Encode(hash).toLowerCase(); */ const urn = `urn:vouchsafe:${label}.${hashB32}` + (hashAlg !== 'sha256' ? `.${hashAlg}` : ''); return { urn, keypair: { publicKey: keypair.publicKey, // original DER b64 privateKey: keypair.privateKey }, publicKeyHash: hashB32, version: VOUCHSAFE_SPEC_VERSION }; } function validateIssuerString(iss) { if (typeof iss !== "string") return false; const prefix = "urn:vouchsafe:"; if (!iss.startsWith(prefix)) return false; const rest = iss.slice(prefix.length); // split on first '.' const dot = rest.indexOf("."); if (dot === -1) return false; const label = rest.slice(0, dot); const afterLabel = rest.slice(dot + 1); // --- Optional .sha256 suffix --- let hashPart = afterLabel; let suffix = null; const secondDot = afterLabel.indexOf("."); if (secondDot !== -1) { hashPart = afterLabel.slice(0, secondDot); suffix = afterLabel.slice(secondDot + 1); if (suffix !== "sha256") return false; } // --- Label validation --- if (label.length < 3 || label.length > 32) return false; if (!/^[A-Za-z0-9_\-%+]+$/.test(label)) return false; // --- Hash validation --- if (!/^[a-z2-7]+$/.test(hashPart)) return false; // Base32 decode must succeed let decoded; try { decoded = base32Decode(hashPart); } catch { return false; } if (!decoded || decoded.length === 0) return false; return true; } const encoder = new TextEncoder(); const decoder = new TextDecoder(); function concat(...buffers) { const size = buffers.reduce((acc, { length }) => acc + length, 0); const buf = new Uint8Array(size); let i = 0; for (const buffer of buffers) { buf.set(buffer, i); i += buffer.length; } return buf; } function encodeBase64(input) { if (Uint8Array.prototype.toBase64) { return input.toBase64(); } const CHUNK_SIZE = 0x8000; const arr = []; for (let i = 0; i < input.length; i += CHUNK_SIZE) { arr.push(String.fromCharCode.apply(null, input.subarray(i, i + CHUNK_SIZE))); } return btoa(arr.join('')); } function decodeBase64(encoded) { if (Uint8Array.fromBase64) { return Uint8Array.fromBase64(encoded); } const binary = atob(encoded); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i); } return bytes; } function decode(input) { if (Uint8Array.fromBase64) { return Uint8Array.fromBase64(typeof input === 'string' ? input : decoder.decode(input), { alphabet: 'base64url', }); } let encoded = input; if (encoded instanceof Uint8Array) { encoded = decoder.decode(encoded); } encoded = encoded.replace(/-/g, '+').replace(/_/g, '/').replace(/\s/g, ''); try { return decodeBase64(encoded); } catch { throw new TypeError('The input to be decoded is not correctly encoded.'); } } function encode(input) { let unencoded = input; if (typeof unencoded === 'string') { unencoded = encoder.encode(unencoded); } if (Uint8Array.prototype.toBase64) { return unencoded.toBase64({ alphabet: 'base64url', omitPadding: true }); } return encodeBase64(unencoded).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); } class JOSEError extends Error { static code = 'ERR_JOSE_GENERIC'; code = 'ERR_JOSE_GENERIC'; constructor(message, options) { super(message, options); this.name = this.constructor.name; Error.captureStackTrace?.(this, this.constructor); } } class JWTClaimValidationFailed extends JOSEError { static code = 'ERR_JWT_CLAIM_VALIDATION_FAILED'; code = 'ERR_JWT_CLAIM_VALIDATION_FAILED'; claim; reason; payload; constructor(message, payload, claim = 'unspecified', reason = 'unspecified') { super(message, { cause: { claim, reason, payload } }); this.claim = claim; this.reason = reason; this.payload = payload; } } class JWTExpired extends JOSEError { static code = 'ERR_JWT_EXPIRED'; code = 'ERR_JWT_EXPIRED'; claim; reason; payload; constructor(message, payload, claim = 'unspecified', reason = 'unspecified') { super(message, { cause: { claim, reason, payload } }); this.claim = claim; this.reason = reason; this.payload = payload; } } class JOSEAlgNotAllowed extends JOSEError { static code = 'ERR_JOSE_ALG_NOT_ALLOWED'; code = 'ERR_JOSE_ALG_NOT_ALLOWED'; } class JOSENotSupported extends JOSEError { static code = 'ERR_JOSE_NOT_SUPPORTED'; code = 'ERR_JOSE_NOT_SUPPORTED'; } class JWSInvalid extends JOSEError { static code = 'ERR_JWS_INVALID'; code = 'ERR_JWS_INVALID'; } class JWTInvalid extends JOSEError { static code = 'ERR_JWT_INVALID'; code = 'ERR_JWT_INVALID'; } class JWSSignatureVerificationFailed extends JOSEError { static code = 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED'; code = 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED'; constructor(message = 'signature verification failed', options) { super(message, options); } } function unusable(name, prop = 'algorithm.name') { return new TypeError(`CryptoKey does not support this operation, its ${prop} must be ${name}`); } function isAlgorithm(algorithm, name) { return algorithm.name === name; } function getHashLength(hash) { return parseInt(hash.name.slice(4), 10); } function getNamedCurve$1(alg) { switch (alg) { case 'ES256': return 'P-256'; case 'ES384': return 'P-384'; case 'ES512': return 'P-521'; default: throw new Error('unreachable'); } } function checkUsage(key, usage) { if (usage && !key.usages.includes(usage)) { throw new TypeError(`CryptoKey does not support this operation, its usages must include ${usage}.`); } } function checkSigCryptoKey(key, alg, usage) { switch (alg) { case 'HS256': case 'HS384': case 'HS512': { if (!isAlgorithm(key.algorithm, 'HMAC')) throw unusable('HMAC'); const expected = parseInt(alg.slice(2), 10); const actual = getHashLength(key.algorithm.hash); if (actual !== expected) throw unusable(`SHA-${expected}`, 'algorithm.hash'); break; } case 'RS256': case 'RS384': case 'RS512': { if (!isAlgorithm(key.algorithm, 'RSASSA-PKCS1-v1_5')) throw unusable('RSASSA-PKCS1-v1_5'); const expected = parseInt(alg.slice(2), 10); const actual = getHashLength(key.algorithm.hash); if (actual !== expected) throw unusable(`SHA-${expected}`, 'algorithm.hash'); break; } case 'PS256': case 'PS384': case 'PS512': { if (!isAlgorithm(key.algorithm, 'RSA-PSS')) throw unusable('RSA-PSS'); const expected = parseInt(alg.slice(2), 10); const actual = getHashLength(key.algorithm.hash); if (actual !== expected) throw unusable(`SHA-${expected}`, 'algorithm.hash'); break; } case 'Ed25519': case 'EdDSA': { if (!isAlgorithm(key.algorithm, 'Ed25519')) throw unusable('Ed25519'); break; } case 'ES256': case 'ES384': case 'ES512': { if (!isAlgorithm(key.algorithm, 'ECDSA')) throw unusable('ECDSA'); const expected = getNamedCurve$1(alg); const actual = key.algorithm.namedCurve; if (actual !== expected) throw unusable(expected, 'algorithm.namedCurve'); break; } default: throw new TypeError('CryptoKey does not support this operation'); } checkUsage(key, usage); } function message(msg, actual, ...types) { types = types.filter(Boolean); if (types.length > 2) { const last = types.pop(); msg += `one of type ${types.join(', ')}, or ${last}.`; } else if (types.length === 2) { msg += `one of type ${types[0]} or ${types[1]}.`; } else { msg += `of type ${types[0]}.`; } if (actual == null) { msg += ` Received ${actual}`; } else if (typeof actual === 'function' && actual.name) { msg += ` Received function ${actual.name}`; } else if (typeof actual === 'object' && actual != null) { if (actual.constructor?.name) { msg += ` Received an instance of ${actual.constructor.name}`; } } return msg; } var invalidKeyInput = (actual, ...types) => { return message('Key must be ', actual, ...types); }; function withAlg(alg, actual, ...types) { return message(`Key for the ${alg} algorithm must be `, actual, ...types); } function isCryptoKey(key) { return key?.[Symbol.toStringTag] === 'CryptoKey'; } function isKeyObject(key) { return key?.[Symbol.toStringTag] === 'KeyObject'; } var isKeyLike = (key) => { return isCryptoKey(key) || isKeyObject(key); }; var isDisjoint = (...headers) => { const sources = headers.filter(Boolean); if (sources.length === 0 || sources.length === 1) { return true; } let acc; for (const header of sources) { const parameters = Object.keys(header); if (!acc || acc.size === 0) { acc = new Set(parameters); continue; } for (const parameter of parameters) { if (acc.has(parameter)) { return false; } acc.add(parameter); } } return true; }; function isObjectLike(value) { return typeof value === 'object' && value !== null; } var isObject = (input) => { if (!isObjectLike(input) || Object.prototype.toString.call(input) !== '[object Object]') { return false; } if (Object.getPrototypeOf(input) === null) { return true; } let proto = input; while (Object.getPrototypeOf(proto) !== null) { proto = Object.getPrototypeOf(proto); } return Object.getPrototypeOf(input) === proto; }; var checkKeyLength = (alg, key) => { if (alg.startsWith('RS') || alg.startsWith('PS')) { const { modulusLength } = key.algorithm; if (typeof modulusLength !== 'number' || modulusLength < 2048) { throw new TypeError(`${alg} requires key modulusLength to be 2048 bits or larger`); } } }; const getNamedCurve = (keyData) => { const patterns = Object.entries({ 'P-256': [0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07], 'P-384': [0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22], 'P-521': [0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x23], }); const maxPatternLen = Math.max(...patterns.map(([, bytes]) => bytes.length)); for (let i = 0; i <= keyData.byteLength - maxPatternLen; i++) { for (const [curve, bytes] of patterns) { if (i <= keyData.byteLength - bytes.length) { if (keyData.subarray(i, i + bytes.length).every((byte, idx) => byte === bytes[idx])) { return curve; } } } } return undefined; }; const genericImport = async (keyFormat, keyData, alg, options) => { let algorithm; let keyUsages; const isPublic = keyFormat === 'spki'; const getSignatureUsages = () => (isPublic ? ['verify'] : ['sign']); const getEncryptionUsages = () => isPublic ? ['encrypt', 'wrapKey'] : ['decrypt', 'unwrapKey']; switch (alg) { case 'PS256': case 'PS384': case 'PS512': algorithm = { name: 'RSA-PSS', hash: `SHA-${alg.slice(-3)}` }; keyUsages = getSignatureUsages(); break; case 'RS256': case 'RS384': case 'RS512': algorithm = { name: 'RSASSA-PKCS1-v1_5', hash: `SHA-${alg.slice(-3)}` }; keyUsages = getSignatureUsages(); break; case 'RSA-OAEP': case 'RSA-OAEP-256': case 'RSA-OAEP-384': case 'RSA-OAEP-512': algorithm = { name: 'RSA-OAEP', hash: `SHA-${parseInt(alg.slice(-3), 10) || 1}`, }; keyUsages = getEncryptionUsages(); break; case 'ES256': case 'ES384': case 'ES512': { const curveMap = { ES256: 'P-256', ES384: 'P-384', ES512: 'P-521' }; algorithm = { name: 'ECDSA', namedCurve: curveMap[alg] }; keyUsages = getSignatureUsages(); break; } case 'ECDH-ES': case 'ECDH-ES+A128KW': case 'ECDH-ES+A192KW': case 'ECDH-ES+A256KW': { const namedCurve = getNamedCurve(keyData); algorithm = namedCurve ? { name: 'ECDH', namedCurve } : { name: 'X25519' }; keyUsages = isPublic ? [] : ['deriveBits']; break; } case 'Ed25519': case 'EdDSA': algorithm = { name: 'Ed25519' }; keyUsages = getSignatureUsages(); break; default: throw new JOSENotSupported('Invalid or unsupported "alg" (Algorithm) value'); } return crypto.subtle.importKey(keyFormat, keyData, algorithm, (isPublic ? true : false), keyUsages); }; const fromPKCS8 = (pem, alg, options) => { const keyData = decodeBase64(pem.replace(/(?:-----(?:BEGIN|END) PRIVATE KEY-----|\s)/g, '')); return genericImport('pkcs8', keyData, alg); }; const fromSPKI = (pem, alg, options) => { const keyData = decodeBase64(pem.replace(/(?:-----(?:BEGIN|END) PUBLIC KEY-----|\s)/g, '')); return genericImport('spki', keyData, alg); }; function subtleMapping(jwk) { let algorithm; let keyUsages; switch (jwk.kty) { case 'RSA': { switch (jwk.alg) { case 'PS256': case 'PS384': case 'PS512': algorithm = { name: 'RSA-PSS', hash: `SHA-${jwk.alg.slice(-3)}` }; keyUsages = jwk.d ? ['sign'] : ['verify']; break; case 'RS256': case 'RS384': case 'RS512': algorithm = { name: 'RSASSA-PKCS1-v1_5', hash: `SHA-${jwk.alg.slice(-3)}` }; keyUsages = jwk.d ? ['sign'] : ['verify']; break; case 'RSA-OAEP': case 'RSA-OAEP-256': case 'RSA-OAEP-384': case 'RSA-OAEP-512': algorithm = { name: 'RSA-OAEP', hash: `SHA-${parseInt(jwk.alg.slice(-3), 10) || 1}`, }; keyUsages = jwk.d ? ['decrypt', 'unwrapKey'] : ['encrypt', 'wrapKey']; break; default: throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value'); } break; } case 'EC': { switch (jwk.alg) { case 'ES256': algorithm = { name: 'ECDSA', namedCurve: 'P-256' }; keyUsages = jwk.d ? ['sign'] : ['verify']; break; case 'ES384': algorithm = { name: 'ECDSA', namedCurve: 'P-384' }; keyUsages = jwk.d ? ['sign'] : ['verify']; break; case 'ES512': algorithm = { name: 'ECDSA', namedCurve: 'P-521' }; keyUsages = jwk.d ? ['sign'] : ['verify']; break; case 'ECDH-ES': case 'ECDH-ES+A128KW': case 'ECDH-ES+A192KW': case 'ECDH-ES+A256KW': algorithm = { name: 'ECDH', namedCurve: jwk.crv }; keyUsages = jwk.d ? ['deriveBits'] : []; break; default: throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value'); } break; } case 'OKP': { switch (jwk.alg) { case 'Ed25519': case 'EdDSA': algorithm = { name: 'Ed25519' }; keyUsages = jwk.d ? ['sign'] : ['verify']; break; case 'ECDH-ES': case 'ECDH-ES+A128KW': case 'ECDH-ES+A192KW': case 'ECDH-ES+A256KW': algorithm = { name: jwk.crv }; keyUsages = jwk.d ? ['deriveBits'] : []; break; default: throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value'); } break; } default: throw new JOSENotSupported('Invalid or unsupported JWK "kty" (Key Type) Parameter value'); } return { algorithm, keyUsages }; } var importJWK = async (jwk) => { if (!jwk.alg) { throw new TypeError('"alg" argument is required when "jwk.alg" is not present'); } const { algorithm, keyUsages } = subtleMapping(jwk); const keyData = { ...jwk }; delete keyData.alg; delete keyData.use; return crypto.subtle.importKey('jwk', keyData, algorithm, jwk.ext ?? (jwk.d ? false : true), jwk.key_ops ?? keyUsages); }; async function importSPKI(spki, alg, options) { if (typeof spki !== 'string' || spki.indexOf('-----BEGIN PUBLIC KEY-----') !== 0) { throw new TypeError('"spki" must be SPKI formatted string'); } return fromSPKI(spki, alg); } async function importPKCS8(pkcs8, alg, options) { if (typeof pkcs8 !== 'string' || pkcs8.indexOf('-----BEGIN PRIVATE KEY-----') !== 0) { throw new TypeError('"pkcs8" must be PKCS#8 formatted string'); } return fromPKCS8(pkcs8, alg); } var validateCrit = (Err, recognizedDefault, recognizedOption, protectedHeader, joseHeader) => { if (joseHeader.crit !== undefined && protectedHeader?.crit === undefined) { throw new Err('"crit" (Critical) Header Parameter MUST be integrity protected'); } if (!protectedHeader || protectedHeader.crit === undefined) { return new Set(); } if (!Array.isArray(protectedHeader.crit) || protectedHeader.crit.length === 0 || protectedHeader.crit.some((input) => typeof input !== 'string' || input.length === 0)) { throw new Err('"crit" (Critical) Header Parameter MUST be an array of non-empty strings when present'); } let recognized; if (recognizedOption !== undefined) { recognized = new Map([...Object.entries(recognizedOption), ...recognizedDefault.entries()]); } else { recognized = recognizedDefault; } for (const parameter of protectedHeader.crit) { if (!recognized.has(parameter)) { throw new JOSENotSupported(`Extension Header Parameter "${parameter}" is not recognized`); } if (joseHeader[parameter] === undefined) { throw new Err(`Extension Header Parameter "${parameter}" is missing`); } if (recognized.get(parameter) && protectedHeader[parameter] === undefined) { throw new Err(`Extension Header Parameter "${parameter}" MUST be integrity protected`); } } return new Set(protectedHeader.crit); }; var validateAlgorithms = (option, algorithms) => { if (algorithms !== undefined && (!Array.isArray(algorithms) || algorithms.some((s) => typeof s !== 'string'))) { throw new TypeError(`"${option}" option must be an array of strings`); } if (!algorithms) { return undefined; } return new Set(algorithms); }; function isJWK(key) { return isObject(key) && typeof key.kty === 'string'; } function isPrivateJWK(key) { return key.kty !== 'oct' && typeof key.d === 'string'; } function isPublicJWK(key) { return key.kty !== 'oct' && typeof key.d === 'undefined'; } function isSecretJWK(key) { return key.kty === 'oct' && typeof key.k === 'string'; } let cache; const handleJWK = async (key, jwk, alg, freeze = false) => { cache ||= new WeakMap(); let cached = cache.get(key); if (cached?.[alg]) { return cached[alg]; } const cryptoKey = await importJWK({ ...jwk, alg }); if (freeze) Object.freeze(key); if (!cached) { cache.set(key, { [alg]: cryptoKey }); } else { cached[alg] = cryptoKey; } return cryptoKey; }; const handleKeyObject = (keyObject, alg) => { cache ||= new WeakMap(); let cached = cache.get(keyObject); if (cached?.[alg]) { return cached[alg]; } const isPublic = keyObject.type === 'public'; const extractable = isPublic ? true : false; let cryptoKey; if (keyObject.asymmetricKeyType === 'x25519') { switch (alg) { case 'ECDH-ES': case 'ECDH-ES+A128KW': case 'ECDH-ES+A192KW': case 'ECDH-ES+A256KW': break; default: throw new TypeError('given KeyObject instance cannot be used for this algorithm'); } cryptoKey = keyObject.toCryptoKey(keyObject.asymmetricKeyType, extractable, isPublic ? [] : ['deriveBits']); } if (keyObject.asymmetricKeyType === 'ed25519') { if (alg !== 'EdDSA' && alg !== 'Ed25519') { throw new TypeError('given KeyObject instance cannot be used for this algorithm'); } cryptoKey = keyObject.toCryptoKey(keyObject.asymmetricKeyType, extractable, [ isPublic ? 'verify' : 'sign', ]); } if (keyObject.asymmetricKeyType === 'rsa') { let hash; switch (alg) { case 'RSA-OAEP': hash = 'SHA-1'; break; case 'RS256': case 'PS256': case 'RSA-OAEP-256': hash = 'SHA-256'; break; case 'RS384': case 'PS384': case 'RSA-OAEP-384': hash = 'SHA-384'; break; case 'RS512': case 'PS512': case 'RSA-OAEP-512': hash = 'SHA-512'; break; default: throw new TypeError('given KeyObject instance cannot be used for this algorithm'); } if (alg.startsWith('RSA-OAEP')) { return keyObject.toCryptoKey({ name: 'RSA-OAEP', hash, }, extractable, isPublic ? ['encrypt'] : ['decrypt']); } cryptoKey = keyObject.toCryptoKey({ name: alg.startsWith('PS') ? 'RSA-PSS' : 'RSASSA-PKCS1-v1_5', hash, }, extractable, [isPublic ? 'verify' : 'sign']); } if (keyObject.asymmetricKeyType === 'ec') { const nist = new Map([ ['prime256v1', 'P-256'], ['secp384r1', 'P-384'], ['secp521r1', 'P-521'], ]); const namedCurve = nist.get(keyObject.asymmetricKeyDetails?.namedCurve); if (!namedCurve) { throw new TypeError('given KeyObject instance cannot be used for this algorithm'); } if (alg === 'ES256' && namedCurve === 'P-256') { cryptoKey = keyObject.toCryptoKey({ name: 'ECDSA', namedCurve, }, extractable, [isPublic ? 'verify' : 'sign']); } if (alg === 'ES384' && namedCurve === 'P-384') { cryptoKey = keyObject.toCryptoKey({ name: 'ECDSA', namedCurve, }, extractable, [isPublic ? 'verify' : 'sign']); } if (alg === 'ES512' && namedCurve === 'P-521') { cryptoKey = keyObject.toCryptoKey({ name: 'ECDSA', namedCurve, }, extractable, [isPublic ? 'verify' : 'sign']); } if (alg.startsWith('ECDH-ES')) { cryptoKey = keyObject.toCryptoKey({ name: 'ECDH', namedCurve, }, extractable, isPublic ? [] : ['deriveBits']); } } if (!cryptoKey) { throw new TypeError('given KeyObject instance cannot be used for this algorithm'); } if (!cached) { cache.set(keyObject, { [alg]: cryptoKey }); } else { cached[alg] = cryptoKey; } return cryptoKey; }; var normalizeKey = async (key, alg) => { if (key instanceof Uint8Array) { return key; } if (isCryptoKey(key)) { return key; } if (isKeyObject(key)) { if (key.type === 'secret') { return key.export(); } if ('toCryptoKey' in key && typeof key.toCryptoKey === 'function') { try { return handleKeyObject(key, alg); } catch (err) { if (err instanceof TypeError) { throw err; } } } let jwk = key.export({ format: 'jwk' }); return handleJWK(key, jwk, alg); } if (isJWK(key)) { if (key.k) { return decode(key.k); } return handleJWK(key, key, alg, true); } throw new Error('unreachable'); }; const tag = (key) => key?.[Symbol.toStringTag]; const jwkMatchesOp = (alg, key, usage) => { if (key.use !== undefined) { let expected; switch (usage) { case 'sign': case 'verify': expected = 'sig'; break; case 'encrypt': case 'decrypt': expected = 'enc'; break; } if (key.use !== expected) { throw new TypeError(`Invalid key for this operation, its "use" must be "${expected}" when present`); } } if (key.alg !== undefined && key.alg !== alg) { throw new TypeError(`Invalid key for this operation, its "alg" must be "${alg}" when present`); } if (Array.isArray(key.key_ops)) { let expectedKeyOp; switch (true) { case usage === 'sign' || usage === 'verify': case alg === 'dir': case alg.includes('CBC-HS'): expectedKeyOp = usage; break; case alg.startsWith('PBES2'): expectedKeyOp = 'deriveBits'; break; case /^A\d{3}(?:GCM)?(?:KW)?$/.test(alg): if (!alg.includes('GCM') && alg.endsWith('KW')) { expectedKeyOp = usage === 'encrypt' ? 'wrapKey' : 'unwrapKey'; } else { expectedKeyOp = usage; } break; case usage === 'encrypt' && alg.startsWith('RSA'): expectedKeyOp = 'wrapKey'; break; case usage === 'decrypt': expectedKeyOp = alg.startsWith('RSA') ? 'unwrapKey' : 'deriveBits'; break; } if (expectedKeyOp && key.key_ops?.includes?.(expectedKeyOp) === false) { throw new TypeError(`Invalid key for this operation, its "key_ops" must include "${expectedKeyOp}" when present`); } } return true; }; const symmetricTypeCheck = (alg, key, usage) => { if (key instanceof Uint8Array) return; if (isJWK(key)) { if (isSecretJWK(key) && jwkMatchesOp(alg, key, usage)) return; throw new TypeError(`JSON Web Key for symmetric algorithms must have JWK "kty" (Key Type) equal to "oct" and the JWK "k" (Key Value) present`); } if (!isKeyLike(key)) { throw new TypeError(withAlg(alg, key, 'CryptoKey', 'KeyObject', 'JSON Web Key', 'Uint8Array')); } if (key.type !== 'secret') { throw new TypeError(`${tag(key)} instances for symmetric algorithms must be of type "secret"`); } }; const asymmetricTypeCheck = (alg, key, usage) => { if (isJWK(key)) { switch (usage) { case 'decrypt': case 'sign': if (isPrivateJWK(key) && jwkMatchesOp(alg, key, usage)) return; throw new TypeError(`JSON Web Key for this operation be a private JWK`); case 'encrypt': case 'verify': if (isPublicJWK(key) && jwkMatchesOp(alg, key, usage)) return; throw new TypeError(`JSON Web Key for this operation be a public JWK`); } } if (!isKeyLike(key)) { throw new TypeError(withAlg(alg, key, 'CryptoKey', 'KeyObject', 'JSON Web Key')); } if (key.type === 'secret') { throw new TypeError(`${tag(key)} instances for asymmetric algorithms must not be of type "secret"`); } if (key.type === 'public') { switch (usage) { case 'sign': throw new TypeError(`${tag(key)} instances for asymmetric algorithm signing must be of type "private"`); case 'decrypt': throw new TypeError(`${tag(key)} instances for asymmetric algorithm decryption must be of type "private"`); } } if (key.type === 'private') { switch (usage) { case 'verify': throw new TypeError(`${tag(key)} instances for asymmetric algorithm verifying must be of type "public"`); case 'encrypt': throw new TypeError(`${tag(key)} instances for asymmetric algorithm encryption must be of type "public"`); } } }; var checkKeyType = (alg, key, usage) => { const symmetric = alg.startsWith('HS') || alg === 'dir' || alg.startsWith('PBES2') || /^A(?:128|192|256)(?:GCM)?(?:KW)?$/.test(alg) || /^A(?:128|192|256)CBC-HS(?:256|384|512)$/.test(alg); if (symmetric) { symmetricTypeCheck(alg, key, usage); } else { asymmetricTypeCheck(alg, key, usage); } }; var subtleAlgorithm = (alg, algorithm) => { const hash = `SHA-${alg.slice(-3)}`; switch (alg) { case 'HS256': case 'HS384': case 'HS512': return { hash, name: 'HMAC' }; case 'PS256': case 'PS384': case 'PS512': return { hash, name: 'RSA-PSS', saltLength: parseInt(alg.slice(-3), 10) >> 3 }; case 'RS256': case 'RS384': case 'RS512': return { hash, name: 'RSASSA-PKCS1-v1_5' }; case 'ES256': case 'ES384': case 'ES512': return { hash, name: 'ECDSA', namedCurve: algorithm.namedCurve }; case 'Ed25519': case 'EdDSA': return { name: 'Ed25519' }; default: throw new JOSENotSupported(`alg ${alg} is not supported either by JOSE or your javascript runtime`); } }; var getSignKey = async (alg, key, usage) => { if (key instanceof Uint8Array) { if (!alg.startsWith('HS')) { throw new TypeError(invalidKeyInput(key, 'CryptoKey', 'KeyObject', 'JSON Web Key')); } return crypto.subtle.importKey('raw', key, { hash: `SHA-${alg.slice(-3)}`, name: 'HMAC' }, false, [usage]); } checkSigCryptoKey(key, alg, usage); return key; }; var verify = async (alg, key, signature, data) => { const cryptoKey = await getSignKey(alg, key, 'verify'); checkKeyLength(alg, cryptoKey); const algorithm = subtleAlgorithm(alg, cryptoKey.algorithm); try { return await crypto.subtle.verify(algorithm, cryptoKey, signature, data); } catch { return false; } }; async function flattenedVerify(jws, key, options) { if (!isObject(jws)) { throw new JWSInvalid('Flattened JWS must be an object'); } if (jws.protected === undefined && jws.header === undefined) { throw new JWSInvalid('Flattened JWS must have either of the "protected" or "header" members'); } if (jws.protected !== undefined && typeof jws.protected !== 'string') { throw new JWSInvalid('JWS Protected Header incorrect type'); } if (jws.payload === undefined) { throw new JWSInvalid('JWS Payload missing'); } if (typeof jws.signature !== 'string') { throw new JWSInvalid('JWS Signature missing or incorrect type'); } if (jws.header !== undefined && !isObject(jws.header)) { throw new JWSInvalid('JWS Unprotected Header incorrect type'); } let parsedProt = {}; if (jws.protected) { try { const protectedHeader = decode(jws.protected); parsedProt = JSON.parse(decoder.decode(protectedHeader)); } catch { throw new JWSInvalid('JWS Protected Header is invalid'); } } if (!isDisjoint(parsedProt, jws.header)) { throw new JWSInvalid('JWS Protected and JWS Unprotected Header Parameter names must be disjoint'); } const joseHeader = { ...parsedProt, ...jws.header, }; const extensions = validateCrit(JWSInvalid, new Map([['b64', true]]), options?.crit, parsedProt, joseHeader); let b64 = true; if (extensions.has('b64')) { b64 = parsedProt.b64; if (typeof b64 !== 'boolean') { throw new JWSInvalid('The "b64" (base64url-encode payload) Header Parameter must be a boolean'); } } const { alg } = joseHeader; if (typeof alg !== 'string' || !alg) { throw new JWSInvalid('JWS "alg" (Algorithm) Header Parameter missing or invalid'); } const algorithms = options && validateAlgorithms('algorithms', options.algorithms); if (algorithms && !algorithms.has(alg)) { throw new JOSEAlgNotAllowed('"alg" (Algorithm) Header Parameter value not allowed'); } if (b64) { if (typeof jws.payload !== 'string') { throw new JWSInvalid('JWS Payload must be a string'); } } else if (typeof jws.payload !== 'string' && !(jws.payload instanceof Uint8Array)) { throw new JWSInvalid('JWS Payload must be a string or an Uint8Array instance'); } let resolvedKey = false; if (typeof key === 'function') { key = await key(parsedProt, jws); resolvedKey = true; } checkKeyType(alg, key, 'verify'); const data = concat(encoder.encode(jws.protected ?? ''), encoder.encode('.'), typeof jws.payload === 'string' ? encoder.encode(jws.payload) : jws.payload); let signature; try { signature = decode(jws.signature); } catch { throw new JWSInvalid('Failed to base64url decode the signature'); } const k = await normalizeKey(key, alg); const verified = await verify(alg, k, signature, data); if (!verified) { throw new JWSSignatureVerificationFailed(); } let payload; if (b64) { try { payload = decode(jws.payload); } catch { throw new JWSInvalid('Failed to base64url decode the payload'); } } else if (typeof jws.payload === 'string') { payload = encoder.encode(jws.payload); } else { payload = jws.payload; } const result = { payload }; if (jws.protected !== undefined) { result.protectedHeader = parsedProt; } if (jws.header !== undefined) { result.unprotectedHeader = jws.header; } if (resolvedKey) { return { ...result, key: k }; } return result; } async function compactVerify(jws, key, options) { if (jws instanceof Uint8Array) { jws = decoder.decode(jws); } if (typeof jws !== 'string') { throw new JWSInvalid('Compact JWS must be a string or Uint8Array'); } const { 0: protectedHeader, 1: payload, 2: signature, length } = jws.split('.'); if (length !== 3) { throw new JWSInvalid('Invalid Compact JWS'); } const verified = await flattenedVerify({ payload, protected: protectedHeader, signature }, key, options); const result = { payload: verified.payload, protectedHeader: verified.protectedHeader }; if (typeof key === 'function') { return { ...result, key: verified.key }; } return result; } var epoch = (date) => Math.floor(date.getTime() / 1000); const minute = 60; const hour = minute * 60; const day = hour * 24; const week = day * 7; const year = day * 365.25; const REGEX = /^(\+|\-)? ?(\d+|\d+\.\d+) ?(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)(?: (ago|from now))?$/i; var secs = (str) => { const matched = REGEX.exec(str); if (!matched || (matched[4] && matched[1])) { throw new TypeError('Invalid time period format'); } const value = parseFloat(matched[2]); const unit = matched[3].toLowerCase(); let numericDate; switch (unit) { case 'sec': case 'secs': case 'second': case 'seconds': case 's': numericDate = Math.round(value); break; case 'minute': case 'minutes': case 'min': case 'mins': case 'm': numericDate = Math.round(value * minute); break; case 'hour': case 'hours': case 'hr': case 'hrs': case 'h': numericDate = Math.round(value * hour); break; case 'day': case 'days': case 'd': numericDate = Math.round(value * day); break; case 'week': case 'weeks': case 'w': numericDate = Math.round(value * week); break; default: numericDate = Math.round(value * year); break; } if (matched[1] === '-' || matched[4] === 'ago') { return -numericDate; } return numericDate; }; function validateInput(label, input) { if (!Number.isFinite(input)) { throw new TypeError(`Invalid ${label} input`); } return input; } const normalizeTyp = (value) => { if (value.includes('/')) { return value.toLowerCase(); } return `application/${value.toLowerCase()}`; }; const checkAudiencePresence = (audPayload, audOption) => { if (typeof audPayload === 'string') { return audOption.includes(audPayload); } if (Array.isArray(audPayload)) { return audOption.some(Set.prototype.has.bind(new Set(audPayload))); } return false; }; function validateClaimsSet(protectedHeader, encodedPayload, options = {}) { let payload; try { payload = JSON.parse(decoder.decode(encodedPayload)); } catch { } if (!isObject(payload)) { throw new JWTInvalid('JWT Claims Set must be a top-level JSON object'); } const { typ } = options; if (typ && (typeof protectedHeader.typ !== 'string' || normalizeTyp(protectedHeader.typ) !== normalizeTyp(typ))) { throw new JWTClaimValidationFailed('unexpected "typ" JWT header value', payload, 'typ', 'check_failed'); } const { requiredClaims = [], issuer, subject, audience, maxTokenAge } = options; const presenceCheck = [...requiredClaims]; if (maxTokenAge !== undefined) presenceCheck.push('iat'); if (audience !== undefined) presenceCheck.push('aud'); if (subject !== undefined) presenceCheck.push('sub'); if (issuer !== undefined) presenceCheck.push('iss'); for (const claim of new Set(presenceCheck.reverse())) { if (!(claim in payload)) { throw new JWTClaimValidationFailed(`missing required "${claim}" claim`, payload, claim, 'missing'); } } if (issuer && !(Array.isArray(issuer) ? issuer : [issuer]).includes(payload.iss)) { throw new JWTClaimValidationFailed('unexpected "iss" claim value', payload, 'iss', 'check_failed'); } if (subject && payload.sub !== subject) { throw new JWTClaimValidationFailed('unexpected "sub" claim value', payload, 'sub', 'check_failed'); } if (audience && !checkAudiencePresence(payload.aud, typeof audience === 'string' ? [audience] : audience)) { throw new JWTClaimValidationFailed('unexpected "aud" claim value', payload, 'aud', 'check_failed'); } let tolerance; switch (typeof options.clockTolerance) { case 'string': tolerance = secs(options.clockTolerance); break; case 'number': tolerance = options.clockTolerance; break; case 'undefined': tolerance = 0; break; default: throw new TypeError('Invalid clockTolerance option type'); } const { currentDate } = options; const now = epoch(currentDate || new Date()); if ((payload.iat !== undefined || maxTokenAge) && typeof payload.iat !== 'number') { throw new JWTClaimValidationFailed('"iat" claim must be a number', payload, 'iat', 'invalid'); } if (payload.nbf !== undefined) { if (typeof payload.nbf !== 'number') { throw new JWTClaimValidationFailed('"nbf" claim must be a number', payload, 'nbf', 'invalid'); } if (payload.nbf > now + tolerance) { throw new JWTClaimValidationFailed('"nbf" claim timestamp check failed', payload, 'nbf', 'check_failed'); } } if (payload.exp !== undefined) { if (typeof payload.exp !== 'number') { throw new JWTClaimValidationFailed('"exp" claim must be a number', payload, 'exp', 'invalid'); } if (payload.exp <= now - tolerance) { throw new JWTExpired('"exp" claim timestamp check failed', payload, 'exp', 'check_failed'); } } if (maxTokenAge) { const age = now - payload.iat; const max = typeof maxTokenAge === 'number' ? maxTokenAge : secs(maxTokenAge); if (age - tolerance > max) { throw new JWTExpired('"iat" claim timestamp check failed (too far in the past)', payload, 'iat', 'check_failed'); } if (age < 0 - tolerance) { throw new JWTClaimValidationFailed('"iat" claim timestamp check failed (it should be in the past)', payload, 'iat', 'check_failed'); } } return payload; } class JWTClaimsBuilder { #payload; constructor(payload) { if (!isObject(payload)) { throw new TypeError('JWT Claims Set MUST be an object'); } this.#payload = structuredClone(payload); } data() { return encoder.encode(JSON.stringify(this.#payload)); } get iss() { return this.#payload.iss; } set iss(value) { this.#payload.iss = value; } get sub() { return this.#payload.sub; } set sub(value) { this.#payload.sub = value; } get aud() { return this.#payload.aud; } set aud(value) { this.#payload.aud = value; } set jti(value) { this.#payload.jti = value; } set nbf(value) { if (typeof value === 'number') { this.#payload.nbf = validateInput('setNotBefore', value); } else if