UNPKG

did-jwt

Version:

Library for Signing and Verifying JWTs that use DIDs as issuers and JWEs that use DIDs as recipients

1,709 lines (1,492 loc) 98.8 kB
import { toString, fromString, concat } from 'uint8arrays'; import { x25519, ed25519 } from '@noble/curves/ed25519'; import { varint } from 'multiformats'; import { encode, decode } from 'multibase'; import { secp256k1 } from '@noble/curves/secp256k1'; import { p256 } from '@noble/curves/p256'; import { sha256 as sha256$1 } from '@noble/hashes/sha256'; import { ripemd160 } from '@noble/hashes/ripemd160'; import { keccak_256 } from '@noble/hashes/sha3'; import canonicalizeData from 'canonicalize'; import { parse } from 'did-resolver'; import { bech32 } from '@scure/base'; import { xchacha20poly1305 } from '@noble/ciphers/chacha'; import { randomBytes } from '@noble/hashes/utils'; const u8a = { toString, fromString, concat }; function bytesToBase64url(b) { return u8a.toString(b, 'base64url'); } function base64ToBytes(s) { const inputBase64Url = s.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); return u8a.fromString(inputBase64Url, 'base64url'); } function base58ToBytes(s) { return u8a.fromString(s, 'base58btc'); } function bytesToBase58(b) { return u8a.toString(b, 'base58btc'); } const SUPPORTED_PUBLIC_KEY_TYPES = { ES256: ['JsonWebKey2020', 'Multikey', 'EcdsaSecp256r1VerificationKey2019'], ES256K: ['EcdsaSecp256k1VerificationKey2019', /** * Equivalent to EcdsaSecp256k1VerificationKey2019 when key is an ethereumAddress */ 'EcdsaSecp256k1RecoveryMethod2020', /** * @deprecated, supported for backward compatibility. Equivalent to EcdsaSecp256k1VerificationKey2019 when key is * not an ethereumAddress */ 'Secp256k1VerificationKey2018', /** * @deprecated, supported for backward compatibility. Equivalent to EcdsaSecp256k1VerificationKey2019 when key is * not an ethereumAddress */ 'Secp256k1SignatureVerificationKey2018', /** * @deprecated, supported for backward compatibility. Equivalent to EcdsaSecp256k1VerificationKey2019 when key is * not an ethereumAddress */ 'EcdsaPublicKeySecp256k1', /** * TODO - support R1 key as well * 'ConditionalProof2022', */ 'JsonWebKey2020', 'Multikey'], 'ES256K-R': ['EcdsaSecp256k1VerificationKey2019', /** * Equivalent to EcdsaSecp256k1VerificationKey2019 when key is an ethereumAddress */ 'EcdsaSecp256k1RecoveryMethod2020', /** * @deprecated, supported for backward compatibility. Equivalent to EcdsaSecp256k1VerificationKey2019 when key is * not an ethereumAddress */ 'Secp256k1VerificationKey2018', /** * @deprecated, supported for backward compatibility. Equivalent to EcdsaSecp256k1VerificationKey2019 when key is * not an ethereumAddress */ 'Secp256k1SignatureVerificationKey2018', /** * @deprecated, supported for backward compatibility. Equivalent to EcdsaSecp256k1VerificationKey2019 when key is * not an ethereumAddress */ 'EcdsaPublicKeySecp256k1', 'ConditionalProof2022', 'JsonWebKey2020', 'Multikey'], Ed25519: ['ED25519SignatureVerification', 'Ed25519VerificationKey2018', 'Ed25519VerificationKey2020', 'JsonWebKey2020', 'Multikey'], EdDSA: ['ED25519SignatureVerification', 'Ed25519VerificationKey2018', 'Ed25519VerificationKey2020', 'JsonWebKey2020', 'Multikey'] }; const VM_TO_KEY_TYPE = { Secp256k1SignatureVerificationKey2018: 'Secp256k1', Secp256k1VerificationKey2018: 'Secp256k1', EcdsaSecp256k1VerificationKey2019: 'Secp256k1', EcdsaPublicKeySecp256k1: 'Secp256k1', EcdsaSecp256k1RecoveryMethod2020: 'Secp256k1', EcdsaSecp256r1VerificationKey2019: 'P-256', Ed25519VerificationKey2018: 'Ed25519', Ed25519VerificationKey2020: 'Ed25519', ED25519SignatureVerification: 'Ed25519', X25519KeyAgreementKey2019: 'X25519', X25519KeyAgreementKey2020: 'X25519', ConditionalProof2022: undefined, JsonWebKey2020: undefined, // key type must be specified in the JWK Multikey: undefined // key type must be extracted from the multicodec }; // this is from the multicodec table https://github.com/multiformats/multicodec/blob/master/table.csv const supportedCodecs = { 'ed25519-pub': 0xed, 'x25519-pub': 0xec, 'secp256k1-pub': 0xe7, 'bls12_381-g1-pub': 0xea, 'bls12_381-g2-pub': 0xeb, 'p256-pub': 0x1200 }; const CODEC_TO_KEY_TYPE = { 'bls12_381-g1-pub': 'Bls12381G1', 'bls12_381-g2-pub': 'Bls12381G2', 'ed25519-pub': 'Ed25519', 'p256-pub': 'P-256', 'secp256k1-pub': 'Secp256k1', 'x25519-pub': 'X25519' }; /** * Extracts the raw byte representation of a public key from a VerificationMethod along with an inferred key type * @param pk a VerificationMethod entry from a DIDDocument * @return an object containing the `keyBytes` of the public key and an inferred `keyType` */ function extractPublicKeyBytes(pk) { if (pk.publicKeyBase58) { return { keyBytes: base58ToBytes(pk.publicKeyBase58), keyType: VM_TO_KEY_TYPE[pk.type] }; } else if (pk.publicKeyBase64) { return { keyBytes: base64ToBytes(pk.publicKeyBase64), keyType: VM_TO_KEY_TYPE[pk.type] }; } else if (pk.publicKeyHex) { return { keyBytes: hexToBytes(pk.publicKeyHex), keyType: VM_TO_KEY_TYPE[pk.type] }; } else if (pk.publicKeyJwk && pk.publicKeyJwk.crv === 'secp256k1' && pk.publicKeyJwk.x && pk.publicKeyJwk.y) { return { keyBytes: secp256k1.ProjectivePoint.fromAffine({ x: bytesToBigInt(base64ToBytes(pk.publicKeyJwk.x)), y: bytesToBigInt(base64ToBytes(pk.publicKeyJwk.y)) }).toRawBytes(false), keyType: 'Secp256k1' }; } else if (pk.publicKeyJwk && pk.publicKeyJwk.crv === 'P-256' && pk.publicKeyJwk.x && pk.publicKeyJwk.y) { return { keyBytes: p256.ProjectivePoint.fromAffine({ x: bytesToBigInt(base64ToBytes(pk.publicKeyJwk.x)), y: bytesToBigInt(base64ToBytes(pk.publicKeyJwk.y)) }).toRawBytes(false), keyType: 'P-256' }; } else if (pk.publicKeyJwk && pk.publicKeyJwk.kty === 'OKP' && ['Ed25519', 'X25519'].includes(pk.publicKeyJwk.crv ?? '') && pk.publicKeyJwk.x) { return { keyBytes: base64ToBytes(pk.publicKeyJwk.x), keyType: pk.publicKeyJwk.crv }; } else if (pk.publicKeyMultibase) { const { keyBytes, keyType } = multibaseToBytes(pk.publicKeyMultibase); return { keyBytes, keyType: keyType ?? VM_TO_KEY_TYPE[pk.type] }; } return { keyBytes: new Uint8Array() }; } /** * Encodes the given byte array to a multibase string (defaulting to base58btc). * If a codec is provided, the corresponding multicodec prefix will be added. * * @param b - the Uint8Array to be encoded * @param base - the base to use for encoding (defaults to base58btc) * @param codec - the codec to use for encoding (defaults to no codec) * * @returns the multibase encoded string * * @public */ function bytesToMultibase(b, base = 'base58btc', codec) { if (!codec) { return u8a.toString(encode(base, b), 'utf-8'); } else { const codecCode = typeof codec === 'string' ? supportedCodecs[codec] : codec; const prefixLength = varint.encodingLength(codecCode); const multicodecEncoding = new Uint8Array(prefixLength + b.length); varint.encodeTo(codecCode, multicodecEncoding); // set prefix multicodecEncoding.set(b, prefixLength); // add the original bytes return u8a.toString(encode(base, multicodecEncoding), 'utf-8'); } } /** * Converts a multibase string to the Uint8Array it represents. * This method will assume the byte array that is multibase encoded is a multicodec and will attempt to decode it. * * @param s - the string to be converted * * @throws if the string is not formatted correctly. * * @public */ function multibaseToBytes(s) { const bytes = decode(s); // look for known key lengths first // Ed25519/X25519, secp256k1/P256 compressed or not, BLS12-381 G1/G2 compressed if ([32, 33, 48, 64, 65, 96].includes(bytes.length)) { return { keyBytes: bytes }; } // then assume multicodec, otherwise return the bytes try { // eslint-disable-next-line @typescript-eslint/no-unused-vars const [codec, length] = varint.decode(bytes); const possibleCodec = Object.entries(supportedCodecs).filter(([, code]) => code === codec)?.[0][0] ?? ''; return { keyBytes: bytes.slice(length), keyType: CODEC_TO_KEY_TYPE[possibleCodec] }; } catch (e) { // not a multicodec, return the bytes return { keyBytes: bytes }; } } function hexToBytes(s, minLength) { let input = s.startsWith('0x') ? s.substring(2) : s; if (input.length % 2 !== 0) { input = `0${input}`; } if (minLength) { const paddedLength = Math.max(input.length, minLength * 2); input = input.padStart(paddedLength, '00'); } return u8a.fromString(input.toLowerCase(), 'base16'); } function encodeBase64url(s) { return bytesToBase64url(u8a.fromString(s)); } function decodeBase64url(s) { return u8a.toString(base64ToBytes(s)); } function bytesToHex(b) { return u8a.toString(b, 'base16'); } function bytesToBigInt(b) { return BigInt(`0x` + u8a.toString(b, 'base16')); } function stringToBytes(s) { return u8a.fromString(s, 'utf-8'); } function toJose({ r, s, recoveryParam }, recoverable) { const jose = new Uint8Array(recoverable ? 65 : 64); jose.set(u8a.fromString(r, 'base16'), 0); jose.set(u8a.fromString(s, 'base16'), 32); if (recoverable) { if (typeof recoveryParam === 'undefined') { throw new Error('Signer did not return a recoveryParam'); } jose[64] = recoveryParam; } return bytesToBase64url(jose); } function fromJose(signature) { const signatureBytes = base64ToBytes(signature); if (signatureBytes.length < 64 || signatureBytes.length > 65) { throw new TypeError(`Wrong size for signature. Expected 64 or 65 bytes, but got ${signatureBytes.length}`); } const r = bytesToHex(signatureBytes.slice(0, 32)); const s = bytesToHex(signatureBytes.slice(32, 64)); const recoveryParam = signatureBytes.length === 65 ? signatureBytes[64] : undefined; return { r, s, recoveryParam }; } function toSealed(ciphertext, tag) { return u8a.concat([base64ToBytes(ciphertext), tag ? base64ToBytes(tag) : new Uint8Array(0)]); } function leftpad(data, size = 64) { if (data.length === size) return data; return '0'.repeat(size - data.length) + data; } /** * Generate random x25519 key pair. */ function generateKeyPair() { const secretKey = x25519.utils.randomPrivateKey(); const publicKey = x25519.getPublicKey(secretKey); return { secretKey: secretKey, publicKey: publicKey }; } /** * Generate private-public x25519 key pair from `seed`. */ function generateKeyPairFromSeed(seed) { if (seed.length !== 32) { throw new Error(`x25519: seed must be ${32} bytes`); } return { publicKey: x25519.getPublicKey(seed), secretKey: seed }; } function genX25519EphemeralKeyPair() { const epk = generateKeyPair(); return { publicKeyJWK: { kty: 'OKP', crv: 'X25519', x: bytesToBase64url(epk.publicKey) }, secretKey: epk.secretKey }; } /** * Checks if a variable is defined and not null. * After this check, typescript sees the variable as defined. * * @param arg - The input to be verified * * @returns true if the input variable is defined. */ function isDefined(arg) { return arg !== null && typeof arg !== 'undefined'; } function sha256(payload) { const data = typeof payload === 'string' ? fromString(payload) : payload; return sha256$1(data); } const keccak = keccak_256; function toEthereumAddress(hexPublicKey) { const hashInput = fromString(hexPublicKey.slice(2), 'base16'); return `0x${toString(keccak(hashInput).slice(-20), 'base16')}`; } function writeUint32BE(value, array = new Uint8Array(4)) { const encoded = fromString(value.toString(), 'base10'); array.set(encoded, 4 - encoded.length); return array; } const lengthAndInput = input => concat([writeUint32BE(input.length), input]); // This implementation of concatKDF was inspired by these two implementations: // https://github.com/digitalbazaar/minimal-cipher/blob/master/algorithms/ecdhkdf.js // https://github.com/panva/jose/blob/master/lib/jwa/ecdh/derive.js function concatKDF(secret, keyLen, alg, producerInfo, consumerInfo) { if (keyLen !== 256) throw new Error(`Unsupported key length: ${keyLen}`); const value = concat([lengthAndInput(fromString(alg)), lengthAndInput(typeof producerInfo === 'undefined' ? new Uint8Array(0) : producerInfo), // apu lengthAndInput(typeof consumerInfo === 'undefined' ? new Uint8Array(0) : consumerInfo), // apv writeUint32BE(keyLen)]); // since our key lenght is 256 we only have to do one round const roundNumber = 1; return sha256(concat([writeUint32BE(roundNumber), secret, value])); } /** * Creates a configured signer function for signing data using the ES256K (secp256k1 + sha256) algorithm. * * The signing function itself takes the data as a `Uint8Array` or `string` and returns a `base64Url`-encoded signature * * @example * ```typescript * const sign: Signer = ES256KSigner(process.env.PRIVATE_KEY) * const signature: string = await sign(data) * ``` * * @param {String} privateKey a private key as `Uint8Array` * @param {Boolean} recoverable an optional flag to add the recovery param to the generated signatures * @return {Function} a configured signer function `(data: string | Uint8Array): Promise<string>` */ function ES256KSigner(privateKey, recoverable = false) { const privateKeyBytes = privateKey; if (privateKeyBytes.length !== 32) { throw new Error(`bad_key: Invalid private key format. Expecting 32 bytes, but got ${privateKeyBytes.length}`); } return function (data) { try { const signature = secp256k1.sign(sha256(data), privateKeyBytes); return Promise.resolve(toJose({ r: leftpad(signature.r.toString(16)), s: leftpad(signature.s.toString(16)), recoveryParam: signature.recovery }, recoverable)); } catch (e) { return Promise.reject(e); } }; } /** * @deprecated Please use ES256KSigner * The SimpleSigner returns a configured function for signing data. * * @example * const signer = SimpleSigner(process.env.PRIVATE_KEY) * signer(data, (err, signature) => { * ... * }) * * @param {String} hexPrivateKey a hex encoded private key * @return {Function} a configured signer function */ function SimpleSigner(hexPrivateKey) { const signer = ES256KSigner(hexToBytes(hexPrivateKey), true); return function (data) { try { return Promise.resolve(signer(data)).then(fromJose); } catch (e) { return Promise.reject(e); } }; } /** * @deprecated Please use ES256KSigner * The EllipticSigner returns a configured function for signing data. * * @example * ```typescript * const signer = EllipticSigner(process.env.PRIVATE_KEY) * signer(data).then( (signature: string) => { * ... * }) * ``` * * @param {String} hexPrivateKey a hex encoded private key * @return {Function} a configured signer function */ function EllipticSigner(hexPrivateKey) { return ES256KSigner(hexToBytes(hexPrivateKey)); } /** * Creates a configured signer function for signing data using the EdDSA (Ed25519) algorithm. * * The private key is expected to be a `Uint8Array` of 32 bytes, but for compatibility 64 bytes are also acceptable. * Users of `@stablelib/ed25519` or `tweetnacl` will be able to use the 64 byte secret keys that library generates. * These libraries precompute the public key and append it as the last 32 bytes of the secretKey, to speed up later * signing operations. * * The signing function itself takes the data as a `Uint8Array` or utf8 `string` and returns a `base64Url`-encoded * signature * * @example * ```typescript * const sign: Signer = EdDSASigner(process.env.PRIVATE_KEY) * const signature: string = await sign(data) * ``` * * @param {String} secretKey a 32 or 64 byte secret key as `Uint8Array` * @return {Function} a configured signer function `(data: string | Uint8Array): Promise<string>` */ function EdDSASigner(secretKey) { const privateKeyBytes = secretKey; if (![32, 64].includes(privateKeyBytes.length)) { throw new Error(`bad_key: Invalid private key format. Expecting 32 or 64 bytes, but got ${privateKeyBytes.length}`); } return function (data) { try { const dataBytes = typeof data === 'string' ? stringToBytes(data) : data; const signature = ed25519.sign(dataBytes, privateKeyBytes.slice(0, 32)); return Promise.resolve(bytesToBase64url(signature)); } catch (e) { return Promise.reject(e); } }; } /** * @deprecated Please use EdDSASigner * * The NaclSigner returns a configured function for signing data using the Ed25519 algorithm. * * The signing function itself takes the data as a `string` or `Uint8Array` parameter and returns a * `base64Url`-encoded signature. * * @example * const signer = NaclSigner(process.env.PRIVATE_KEY) * const data: string = '...' * signer(data).then( (signature: string) => { * ... * }) * * @param {String} base64PrivateKey a 64 byte base64 encoded private key * @return {Function} a configured signer function */ function NaclSigner(base64PrivateKey) { return EdDSASigner(base64ToBytes(base64PrivateKey)); } /** * Creates a configured signer function for signing data using the ES256 (secp256r1 + sha256) algorithm. * * The signing function itself takes the data as a `Uint8Array` or `string` and returns a `base64Url`-encoded signature * * @example * ```typescript * const sign: Signer = ES256Signer(process.env.PRIVATE_KEY) * const signature: string = await sign(data) * ``` * * @param {String} privateKey a private key as `Uint8Array` * @return {Function} a configured signer function `(data: string | Uint8Array): Promise<string>` */ function ES256Signer(privateKey) { if (privateKey.length !== 32) { throw new Error(`bad_key: Invalid private key format. Expecting 32 bytes, but got ${privateKey.length}`); } return function (data) { try { const signature = p256.sign(sha256(data), privateKey); return Promise.resolve(toJose({ r: leftpad(signature.r.toString(16)), s: leftpad(signature.s.toString(16)) })); } catch (e) { return Promise.reject(e); } }; } function instanceOfEcdsaSignature(object) { return typeof object === 'object' && 'r' in object && 's' in object; } function ES256SignerAlg() { return function sign(payload, signer) { try { return Promise.resolve(signer(payload)).then(function (signature) { if (instanceOfEcdsaSignature(signature)) { return toJose(signature); } else { return signature; } }); } catch (e) { return Promise.reject(e); } }; } function ES256KSignerAlg(recoverable) { return function sign(payload, signer) { try { return Promise.resolve(signer(payload)).then(function (signature) { if (instanceOfEcdsaSignature(signature)) { return toJose(signature, recoverable); } else { if (recoverable && typeof fromJose(signature).recoveryParam === 'undefined') { throw new Error(`not_supported: ES256K-R not supported when signer doesn't provide a recovery param`); } return signature; } }); } catch (e) { return Promise.reject(e); } }; } function Ed25519SignerAlg() { return function sign(payload, signer) { try { return Promise.resolve(signer(payload)).then(function (signature) { if (!instanceOfEcdsaSignature(signature)) { return signature; } else { throw new Error('invalid_config: expected a signer function that returns a string instead of signature object'); } }); } catch (e) { return Promise.reject(e); } }; } const algorithms$1 = { ES256: ES256SignerAlg(), ES256K: ES256KSignerAlg(), // This is a non-standard algorithm but retained for backwards compatibility // see https://github.com/decentralized-identity/did-jwt/issues/146 'ES256K-R': ES256KSignerAlg(true), // This is actually incorrect but retained for backwards compatibility // see https://github.com/decentralized-identity/did-jwt/issues/130 Ed25519: Ed25519SignerAlg(), EdDSA: Ed25519SignerAlg() }; function SignerAlg(alg) { const impl = algorithms$1[alg]; if (!impl) throw new Error(`not_supported: Unsupported algorithm ${alg}`); return impl; } function publicKeyToAddress$1(publicKey, otherAddress) { // Use the same version/prefix byte as the given address. const version = bytesToHex(base58ToBytes(otherAddress).slice(0, 1)); const publicKeyBuffer = hexToBytes(publicKey); const publicKeyHash = ripemd160(sha256(publicKeyBuffer)); const step1 = version + bytesToHex(publicKeyHash); const step2 = sha256(hexToBytes(step1)); const step3 = sha256(step2); const checksum = bytesToHex(step3).substring(0, 8); const step4 = step1 + checksum; return bytesToBase58(hexToBytes(step4)); } function publicKeyToAddress(publicKey, prefix) { const publicKeyBuffer = secp256k1.ProjectivePoint.fromHex(publicKey).toRawBytes(); const hash = ripemd160(sha256(publicKeyBuffer)); const words = bech32.toWords(hash); return bech32.encode(prefix, words).replace(prefix, ''); } function verifyBlockchainAccountId(publicKey, blockchainAccountId) { if (blockchainAccountId) { const chain = blockchainAccountId.split(':'); switch (chain[0]) { case 'bip122': chain[chain.length - 1] = publicKeyToAddress$1(publicKey, chain[chain.length - 1]); break; case 'cosmos': chain[chain.length - 1] = publicKeyToAddress(publicKey, chain[1]); break; case 'eip155': chain[chain.length - 1] = toEthereumAddress(publicKey); break; default: return false; } return chain.join(':').toLowerCase() === blockchainAccountId.toLowerCase(); } return false; } function toSignatureObject(signature, recoverable = false) { const rawSig = base64ToBytes(signature); if (rawSig.length !== (recoverable ? 65 : 64)) { throw new Error('wrong signature length'); } const r = bytesToHex(rawSig.slice(0, 32)); const s = bytesToHex(rawSig.slice(32, 64)); const sigObj = { r, s }; if (recoverable) { sigObj.recoveryParam = rawSig[64]; } return sigObj; } function toSignatureObject2(signature, recoverable = false) { const bytes = base64ToBytes(signature); if (bytes.length !== (recoverable ? 65 : 64)) { throw new Error('wrong signature length'); } return { compact: bytes.slice(0, 64), recovery: bytes[64] }; } function verifyES256(data, signature, authenticators) { const hash = sha256(data); const sig = p256.Signature.fromCompact(toSignatureObject2(signature).compact); const fullPublicKeys = authenticators.filter(a => !a.ethereumAddress && !a.blockchainAccountId); const signer = fullPublicKeys.find(pk => { try { const { keyBytes } = extractPublicKeyBytes(pk); return p256.verify(sig, hash, keyBytes); } catch (err) { return false; } }); if (!signer) throw new Error('invalid_signature: Signature invalid for JWT'); return signer; } function verifyES256K(data, signature, authenticators) { const hash = sha256(data); const signatureNormalized = secp256k1.Signature.fromCompact(base64ToBytes(signature)).normalizeS(); const fullPublicKeys = authenticators.filter(a => { return !a.ethereumAddress && !a.blockchainAccountId; }); const blockchainAddressKeys = authenticators.filter(a => { return a.ethereumAddress || a.blockchainAccountId; }); let signer = fullPublicKeys.find(pk => { try { const { keyBytes } = extractPublicKeyBytes(pk); return secp256k1.verify(signatureNormalized, hash, keyBytes); } catch (err) { return false; } }); if (!signer && blockchainAddressKeys.length > 0) { signer = verifyRecoverableES256K(data, signature, blockchainAddressKeys); } if (!signer) throw new Error('invalid_signature: Signature invalid for JWT'); return signer; } function verifyRecoverableES256K(data, signature, authenticators) { const signatures = []; if (signature.length > 86) { signatures.push(toSignatureObject2(signature, true)); } else { const so = toSignatureObject2(signature, false); signatures.push({ ...so, recovery: 0 }); signatures.push({ ...so, recovery: 1 }); } const hash = sha256(data); const checkSignatureAgainstSigner = sigObj => { const signature = secp256k1.Signature.fromCompact(sigObj.compact).addRecoveryBit(sigObj.recovery || 0); const recoveredPublicKey = signature.recoverPublicKey(hash); const recoveredAddress = toEthereumAddress(recoveredPublicKey.toHex(false)).toLowerCase(); const recoveredPublicKeyHex = recoveredPublicKey.toHex(false); const recoveredCompressedPublicKeyHex = recoveredPublicKey.toHex(true); return authenticators.find(a => { const { keyBytes } = extractPublicKeyBytes(a); const keyHex = bytesToHex(keyBytes); return keyHex === recoveredPublicKeyHex || keyHex === recoveredCompressedPublicKeyHex || a.ethereumAddress?.toLowerCase() === recoveredAddress || a.blockchainAccountId?.split('@eip155')?.[0].toLowerCase() === recoveredAddress || // CAIP-2 verifyBlockchainAccountId(recoveredPublicKeyHex, a.blockchainAccountId) // CAIP-10 ; }); }; // Find first verification method for (const signature of signatures) { const verificationMethod = checkSignatureAgainstSigner(signature); if (verificationMethod) return verificationMethod; } // If no one found matching throw new Error('invalid_signature: Signature invalid for JWT'); } function verifyEd25519(data, signature, authenticators) { const clear = stringToBytes(data); const signatureBytes = base64ToBytes(signature); const signer = authenticators.find(a => { const { keyBytes, keyType } = extractPublicKeyBytes(a); if (keyType === 'Ed25519') { return ed25519.verify(signatureBytes, clear, keyBytes); } else { return false; } }); if (!signer) throw new Error('invalid_signature: Signature invalid for JWT'); return signer; } const algorithms = { ES256: verifyES256, ES256K: verifyES256K, // This is a non-standard algorithm but retained for backwards compatibility // see https://github.com/decentralized-identity/did-jwt/issues/146 'ES256K-R': verifyRecoverableES256K, // This is actually incorrect but retained for backwards compatibility // see https://github.com/decentralized-identity/did-jwt/issues/130 Ed25519: verifyEd25519, EdDSA: verifyEd25519 }; function VerifierAlgorithm(alg) { const impl = algorithms[alg]; if (!impl) throw new Error(`not_supported: Unsupported algorithm ${alg}`); return impl; } VerifierAlgorithm.toSignatureObject = toSignatureObject; /** * Error prefixes used for known verification failure cases. * * For compatibility, these error prefixes match the existing error messages, but will be adjusted in a future major * version update to match the scenarios better. * * @beta */ const JWT_ERROR = { /** * Thrown when a JWT payload schema is unexpected or when validity period does not match */ INVALID_JWT: 'invalid_jwt', /** * Thrown when the verifier audience does not match the one set in the JWT payload */ INVALID_AUDIENCE: 'invalid_config', /** * Thrown when none of the public keys of the issuer match the signature of the JWT. * * This is equivalent to `NO_SUITABLE_KEYS` when the `proofPurpose` is NOT specified. */ INVALID_SIGNATURE: 'invalid_signature', /** * Thrown when the DID document of the issuer does not have any keys that match the signature for the given * `proofPurpose`. * * This is equivalent to `invalid_signature`, when a `proofPurpose` is specified. */ NO_SUITABLE_KEYS: 'no_suitable_keys', /** * Thrown when the `alg` of the JWT or the encoding of the key is not supported */ NOT_SUPPORTED: 'not_supported', /** * Thrown when the DID resolver is unable to resolve the issuer DID. */ RESOLVER_ERROR: 'resolver_error' }; function _catch$1(body, recover) { try { var result = body(); } catch (e) { return recover(e); } if (result && result.then) { return result.then(void 0, recover); } return result; } const verifyConditionDelegated = function (jwt, { header, payload, data, signature }, authenticator, options) { try { if (!authenticator.conditionDelegated) { throw new Error('Expected conditionDelegated'); } if (!options.resolver) { throw new Error('Expected resolver'); } let foundSigner; const issuer = authenticator.conditionDelegated; return Promise.resolve(resolveAuthenticator(options.resolver, header.alg, issuer, options.proofPurpose)).then(function (didAuthenticator) { let _exit2; function _temp6(_result4) { if (_exit2) ; if (foundSigner) { return authenticator; } throw new Error(`${JWT_ERROR.INVALID_SIGNATURE}: condition for authenticator ${authenticator.id} is not met.`); } const didResolutionResult = didAuthenticator.didResolutionResult; if (!didResolutionResult?.didDocument) { throw new Error(`${JWT_ERROR.RESOLVER_ERROR}: Could not resolve delegated DID ${issuer}.`); } const delegatedAuthenticator = didAuthenticator.authenticators.find(authenticator => authenticator.id === issuer); if (!delegatedAuthenticator) { throw new Error(`${JWT_ERROR.NO_SUITABLE_KEYS}: Could not find delegated authenticator ${issuer} in it's DID Document`); } const _temp5 = function () { if (delegatedAuthenticator.type === CONDITIONAL_PROOF_2022) { return Promise.resolve(verifyJWT(jwt, { ...options, ...{ didAuthenticator: { didResolutionResult, authenticators: [delegatedAuthenticator], issuer: delegatedAuthenticator.id } } })).then(function ({ verified }) { if (verified) { foundSigner = delegatedAuthenticator; } }); } else { try { foundSigner = verifyJWTDecoded({ header, payload, data, signature }, delegatedAuthenticator); } catch (e) { if (!e.message.startsWith('invalid_signature:')) throw e; } } }(); return _temp5 && _temp5.then ? _temp5.then(_temp6) : _temp6(_temp5); }); } catch (e) { return Promise.reject(e); } }; const _iteratorSymbol$1 = typeof Symbol !== "undefined" ? Symbol.iterator || (Symbol.iterator = Symbol("Symbol.iterator")) : "@@iterator"; function _settle$2(pact, state, value) { if (!pact.s) { if (value instanceof _Pact$2) { if (value.s) { if (state & 1) { state = value.s; } value = value.v; } else { value.o = _settle$2.bind(null, pact, state); return; } } if (value && value.then) { value.then(_settle$2.bind(null, pact, state), _settle$2.bind(null, pact, 2)); return; } pact.s = state; pact.v = value; const observer = pact.o; if (observer) { observer(pact); } } } const _Pact$2 = /*#__PURE__*/function () { function _Pact() {} _Pact.prototype.then = function (onFulfilled, onRejected) { const result = new _Pact(); const state = this.s; if (state) { const callback = state & 1 ? onFulfilled : onRejected; if (callback) { try { _settle$2(result, 1, callback(this.v)); } catch (e) { _settle$2(result, 2, e); } return result; } else { return this; } } this.o = function (_this) { try { const value = _this.v; if (_this.s & 1) { _settle$2(result, 1, onFulfilled ? onFulfilled(value) : value); } else if (onRejected) { _settle$2(result, 1, onRejected(value)); } else { _settle$2(result, 2, value); } } catch (e) { _settle$2(result, 2, e); } }; return result; }; return _Pact; }(); function _isSettledPact$2(thenable) { return thenable instanceof _Pact$2 && thenable.s & 1; } function _forTo$2(array, body, check) { var i = -1, pact, reject; function _cycle(result) { try { while (++i < array.length && (!check || !check())) { result = body(i); if (result && result.then) { if (_isSettledPact$2(result)) { result = result.v; } else { result.then(_cycle, reject || (reject = _settle$2.bind(null, pact = new _Pact$2(), 2))); return; } } } if (pact) { _settle$2(pact, 1, result); } else { pact = result; } } catch (e) { _settle$2(pact || (pact = new _Pact$2()), 2, e); } } _cycle(); return pact; } const verifyConditionWeightedThreshold = function (jwt, { header, payload, data, signature }, authenticator, options) { try { let _exit; function _temp4(_result3) { if (_exit) return _result3; throw new Error(`${JWT_ERROR.INVALID_SIGNATURE}: condition for authenticator ${authenticator.id} is not met.`); } if (!authenticator.conditionWeightedThreshold || !authenticator.threshold) { throw new Error('Expected conditionWeightedThreshold and threshold'); } const issuers = []; const threshold = authenticator.threshold; let weightCount = 0; const _temp3 = _forOf$1(authenticator.conditionWeightedThreshold, function (weightedCondition) { function _temp2(_result2) { if (_exit) return _result2; if (foundSigner && !issuers.includes(foundSigner.id)) { issuers.push(foundSigner.id); weightCount += weightedCondition.weight; if (weightCount >= threshold) { _exit = 1; return authenticator; } } } const currentCondition = weightedCondition.condition; let foundSigner; const _temp = _catch$1(function () { if (currentCondition.type === CONDITIONAL_PROOF_2022) { if (!options.didAuthenticator) { throw new Error('Expected didAuthenticator'); } const newOptions = { ...options, didAuthenticator: { didResolutionResult: options.didAuthenticator?.didResolutionResult, authenticators: [currentCondition], issuer: currentCondition.id } }; return Promise.resolve(verifyJWT(jwt, newOptions)).then(function ({ verified }) { if (verified) { foundSigner = currentCondition; } }); } else { return Promise.resolve(verifyJWTDecoded({ header, payload, data, signature }, currentCondition)).then(function (_verifyJWTDecoded) { foundSigner = _verifyJWTDecoded; }); } }, function (e) { if (!e.message.startsWith(JWT_ERROR.INVALID_SIGNATURE)) throw e; }); return _temp && _temp.then ? _temp.then(_temp2) : _temp2(_temp); }, function () { return _exit; }); return Promise.resolve(_temp3 && _temp3.then ? _temp3.then(_temp4) : _temp4(_temp3)); } catch (e) { return Promise.reject(e); } }; function _forOf$1(target, body, check) { if (typeof target[_iteratorSymbol$1] === "function") { var iterator = target[_iteratorSymbol$1](), step, pact, reject; function _cycle(result) { try { while (!(step = iterator.next()).done && (!check || !check())) { result = body(step.value); if (result && result.then) { if (_isSettledPact$2(result)) { result = result.v; } else { result.then(_cycle, reject || (reject = _settle$2.bind(null, pact = new _Pact$2(), 2))); return; } } } if (pact) { _settle$2(pact, 1, result); } else { pact = result; } } catch (e) { _settle$2(pact || (pact = new _Pact$2()), 2, e); } } _cycle(); if (iterator.return) { var _fixup = function (value) { try { if (!step.done) { iterator.return(); } } catch (e) {} return value; }; if (pact && pact.then) { return pact.then(_fixup, function (e) { throw _fixup(e); }); } _fixup(); } return pact; } // No support for Symbol.iterator if (!("length" in target)) { throw new TypeError("Object is not iterable"); } // Handle live collections properly var values = []; for (var i = 0; i < target.length; i++) { values.push(target[i]); } return _forTo$2(values, function (i) { return body(values[i]); }, check); } const verifyConditionalProof = function (jwt, { header, payload, signature, data }, authenticator, options) { try { // Validate the condition according to its condition property if (authenticator.conditionWeightedThreshold) { return verifyConditionWeightedThreshold(jwt, { header, payload, data, signature }, authenticator, options); } else if (authenticator.conditionDelegated) { return verifyConditionDelegated(jwt, { header, payload, data, signature }, authenticator, options); } // TODO other conditions throw new Error(`${JWT_ERROR.INVALID_JWT}: conditional proof type did not find condition for authenticator ${authenticator.id}.`); } catch (e) { return Promise.reject(e); } }; const verifyProof = function (jwt, { header, payload, signature, data }, authenticator, options) { try { if (authenticator.type === CONDITIONAL_PROOF_2022) { return verifyConditionalProof(jwt, { payload, header, signature, data }, authenticator, options); } else { return Promise.resolve(verifyJWTDecoded({ header, payload, data, signature }, [authenticator])); } } catch (e) { return Promise.reject(e); } }; const CONDITIONAL_PROOF_2022 = 'ConditionalProof2022'; /** * Resolves relevant public keys or other authenticating material used to verify signature from the DID document of * provided DID * * @example * ```ts * resolveAuthenticator(resolver, 'ES256K', 'did:uport:2nQtiQG6Cgm1GYTBaaKAgr76uY7iSexUkqX').then(obj => { * const payload = obj.payload * const profile = obj.profile * const jwt = obj.jwt * // ... * }) * ``` * * @param resolver - {Resolvable} a DID resolver function that can obtain the `DIDDocument` for the `issuer` * @param alg - {String} a JWT algorithm * @param issuer - {String} a Decentralized Identifier (DID) to lookup * @param proofPurpose - {ProofPurposeTypes} *Optional* Use the verificationMethod linked in that section of the * issuer DID document * @return {Promise<DIDAuthenticator>} a promise which resolves with an object containing an array of authenticators * or rejects with an error if none exist */ function _settle$1(pact, state, value) { if (!pact.s) { if (value instanceof _Pact$1) { if (value.s) { if (state & 1) { state = value.s; } value = value.v; } else { value.o = _settle$1.bind(null, pact, state); return; } } if (value && value.then) { value.then(_settle$1.bind(null, pact, state), _settle$1.bind(null, pact, 2)); return; } pact.s = state; pact.v = value; const observer = pact.o; if (observer) { observer(pact); } } } const _Pact$1 = /*#__PURE__*/function () { function _Pact() {} _Pact.prototype.then = function (onFulfilled, onRejected) { const result = new _Pact(); const state = this.s; if (state) { const callback = state & 1 ? onFulfilled : onRejected; if (callback) { try { _settle$1(result, 1, callback(this.v)); } catch (e) { _settle$1(result, 2, e); } return result; } else { return this; } } this.o = function (_this) { try { const value = _this.v; if (_this.s & 1) { _settle$1(result, 1, onFulfilled ? onFulfilled(value) : value); } else if (onRejected) { _settle$1(result, 1, onRejected(value)); } else { _settle$1(result, 2, value); } } catch (e) { _settle$1(result, 2, e); } }; return result; }; return _Pact; }(); function _isSettledPact$1(thenable) { return thenable instanceof _Pact$1 && thenable.s & 1; } function _forTo$1(array, body, check) { var i = -1, pact, reject; function _cycle(result) { try { while (++i < array.length && (!check || !check())) { result = body(i); if (result && result.then) { if (_isSettledPact$1(result)) { result = result.v; } else { result.then(_cycle, reject || (reject = _settle$1.bind(null, pact = new _Pact$1(), 2))); return; } } } if (pact) { _settle$1(pact, 1, result); } else { pact = result; } } catch (e) { _settle$1(pact || (pact = new _Pact$1()), 2, e); } } _cycle(); return pact; } function _catch(body, recover) { try { var result = body(); } catch (e) { return recover(e); } if (result && result.then) { return result.then(void 0, recover); } return result; } function _for$1(test, update, body) { var stage; for (;;) { var shouldContinue = test(); if (_isSettledPact$1(shouldContinue)) { shouldContinue = shouldContinue.v; } if (!shouldContinue) { return result; } if (shouldContinue.then) { stage = 0; break; } var result = body(); if (result && result.then) { if (_isSettledPact$1(result)) { result = result.s; } else { stage = 1; break; } } if (update) { var updateValue = update(); if (updateValue && updateValue.then && !_isSettledPact$1(updateValue)) { stage = 2; break; } } } var pact = new _Pact$1(); var reject = _settle$1.bind(null, pact, 2); (stage === 0 ? shouldContinue.then(_resumeAfterTest) : stage === 1 ? result.then(_resumeAfterBody) : updateValue.then(_resumeAfterUpdate)).then(void 0, reject); return pact; function _resumeAfterBody(value) { result = value; do { if (update) { updateValue = update(); if (updateValue && updateValue.then && !_isSettledPact$1(updateValue)) { updateValue.then(_resumeAfterUpdate).then(void 0, reject); return; } } shouldContinue = test(); if (!shouldContinue || _isSettledPact$1(shouldContinue) && !shouldContinue.v) { _settle$1(pact, 1, result); return; } if (shouldContinue.then) { shouldContinue.then(_resumeAfterTest).then(void 0, reject); return; } result = body(); if (_isSettledPact$1(result)) { result = result.v; } } while (!result || !result.then); result.then(_resumeAfterBody).then(void 0, reject); } function _resumeAfterTest(shouldContinue) { if (shouldContinue) { result = body(); if (result && result.then) { result.then(_resumeAfterBody).then(void 0, reject); } else { _resumeAfterBody(result); } } else { _settle$1(pact, 1, result); } } function _resumeAfterUpdate() { if (shouldContinue = test()) { if (shouldContinue.then) { shouldContinue.then(_resumeAfterTest).then(void 0, reject); } else { _resumeAfterTest(shouldContinue); } } else { _settle$1(pact, 1, result); } } } const resolveAuthenticator = function (resolver, alg, issuer, proofPurpose) { try { const types = SUPPORTED_PUBLIC_KEY_TYPES[alg]; if (!types || types.length === 0) { throw new Error(`${JWT_ERROR.NOT_SUPPORTED}: No supported signature types for algorithm ${alg}`); } let didResult; return Promise.resolve(resolver.resolve(issuer, { accept: DID_JSON })).then(function (result) { // support legacy resolvers that do not produce DIDResolutionResult if (Object.getOwnPropertyNames(result).indexOf('didDocument') === -1) { didResult = { didDocument: result, didDocumentMetadata: {}, didResolutionMetadata: { contentType: DID_JSON } }; } else { didResult = result; } if (didResult.didResolutionMetadata?.error || didResult.didDocument == null) { const { error, message } = didResult.didResolutionMetadata; throw new Error(`${JWT_ERROR.RESOLVER_ERROR}: Unable to resolve DID document for ${issuer}: ${error}, ${message || ''}`); } const getPublicKeyById = (verificationMethods, pubid) => { const filtered = verificationMethods.filter(({ id }) => pubid === id); return filtered.length > 0 ? filtered[0] : null; }; let publicKeysToCheck = [...(didResult?.didDocument?.verificationMethod || []), ...(didResult?.didDocument?.publicKey || [])]; if (typeof proofPurpose === 'string') { // support legacy DID Documents that do not list assertionMethod if (proofPurpose.startsWith('assertion') && !Object.getOwnPropertyNames(didResult?.didDocument).includes('assertionMethod')) { didResult.didDocument = { ...didResult.didDocument }; didResult.didDocument.assertionMethod = [...publicKeysToCheck.map(pk => pk.id)]; } publicKeysToCheck = (didResult.didDocument[proofPurpose] || []).map(verificationMethod => { if (typeof verificationMethod === 'string') { return getPublicKeyById(publicKeysToCheck, verificationMethod); } else if (typeof verificationMethod.publicKey === 'string') { // this is a legacy format return getPublicKeyById(publicKeysToCheck, verificationMethod.publicKey); } else { return verificationMethod; } }).filter(key => key != null); } const authenticators = publicKeysToCheck.filter(({ type }) => types.find(supported => supported === type)); if (typeof proofPurpose === 'string' && (!authenticators || authenticators.length === 0)) { throw new Error(`${JWT_ERROR.NO_SUITABLE_KEYS}: DID document for ${issuer} does not have public keys suitable for ${alg} with ${proofPurpose} purpose`); } if (!authenticators || authenticators.length === 0) { throw new Error(`${JWT_ERROR.NO_SUITABLE_KEYS}: DID document for ${issuer} does not have public keys for ${alg}`); } return { authenticators, issuer, didResolutionResult: didResult }; }); } catch (e) { return Promise.reject(e); } }; /** * Verifies given JWT. If the JWT is valid, the promise returns an object including the JWT, the payload of the JWT, * and the DID document of the issuer of the JWT. * * @example * ```ts * verifyJWT( * 'did:uport:eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJyZXF1Z....', * {audience: '5A8bRWU3F7j3REx3vkJ...', callbackUrl: 'https://...'} * ).then(obj => { * const did = obj.did // DID of signer * const payload = obj.payload * const doc = obj.didResolutionResult.didDocument // DID Document of issuer * const jwt = obj.jwt * const signerKeyId = obj.signer.id // ID of key in DID document that signed JWT * ... * }) * ``` * * @param {String} jwt a JSON Web Token to verify * @param {Object} [options] an unsigned credential object * @param {Boolean} options.auth Require signer to be listed in the authentication section of the * DID document (for Authentication purposes) * @param {String} options.audience DID of the recipient of the JWT * @param {String} options.callbackUrl callback url in JWT * @return {Promise<Object, Error>} a promise which resolves with a response object or rejects with an * error */ const verifyJWT = function (jwt, options = { resolver: undefined, auth: undefined, audience: undefined, callbackUrl: undefined, skewTime: undefined, proofPurpose: undefined, policies: {}, didAuthenticator: undefined }) { try { function _temp7() { let _exit; function _temp5(_result) { if (_exit) ; if (signer) { const now = typeof options.policies?.now === 'number' ? options.policies.now : Math.floor(Date.now() / 1000); const skewTime = typeof options.skewTime !== 'undefined' && options.sk