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
JavaScript
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