diff --git a/src/JWE.ts b/src/JWE.ts index ba32418b..25cb149b 100644 --- a/src/JWE.ts +++ b/src/JWE.ts @@ -48,7 +48,7 @@ export interface Decrypter { iv: Uint8Array, aad?: Uint8Array, recipient?: Record - ) => Promise + ) => Promise } function validateJWE(jwe: JWE) { @@ -66,7 +66,7 @@ function validateJWE(jwe: JWE) { function encodeJWE({ ciphertext, tag, iv, protectedHeader, recipient }: EncryptionResult, aad?: Uint8Array): JWE { const jwe: JWE = { - protected: protectedHeader, + protected: protectedHeader, iv: bytesToBase64url(iv), ciphertext: bytesToBase64url(ciphertext), tag: bytesToBase64url(tag) @@ -99,10 +99,13 @@ export async function createJWE( cek = encryptionResult.cek jwe = encodeJWE(encryptionResult, aad) } else { - jwe.recipients.push(await encrypter.encryptCek(cek)) + const recipient = await encrypter.encryptCek?.(cek) + if (recipient) { + jwe?.recipients?.push(recipient) + } } } - return jwe + return jwe } } diff --git a/src/JWT.ts b/src/JWT.ts index 3133262e..e8ce84e5 100644 --- a/src/JWT.ts +++ b/src/JWT.ts @@ -7,6 +7,13 @@ import VerifierAlgorithm from './VerifierAlgorithm' export type Signer = (data: string | Uint8Array) => Promise export type SignerAlgorithm = (payload: string, signer: Signer) => Promise +export type ProofPurposeTypes = + | 'assertionMethod' + | 'authentication' + | 'keyAgreement' + | 'capabilityDelegation' + | 'capabilityInvocation' + export interface JWTOptions { issuer: string signer: Signer @@ -26,7 +33,7 @@ export interface JWTVerifyOptions { resolver?: Resolvable skewTime?: number /** See https://www.w3.org/TR/did-spec-registries/#verification-relationships */ - proofPurpose?: 'authentication' | 'assertionMethod' | 'capabilityDelegation' | 'capabilityInvocation' | string + proofPurpose?: ProofPurposeTypes } export interface JWSCreationOptions { @@ -139,7 +146,7 @@ function encodeSection(data: any, shouldCanonicalize: boolean = false): string { export const NBF_SKEW: number = 300 function decodeJWS(jws: string): JWSDecoded { - const parts: RegExpMatchArray = jws.match(/^([a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)$/) + const parts = jws.match(/^([a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)$/) if (parts) { return { header: JSON.parse(decodeBase64url(parts[1])), @@ -292,17 +299,17 @@ export function verifyJWS(jws: string, pubkeys: VerificationMethod | Verificatio export async function verifyJWT( jwt: string, options: JWTVerifyOptions = { - resolver: null, - auth: null, - audience: null, - callbackUrl: null, - skewTime: null, - proofPurpose: null + resolver: undefined, + auth: undefined, + audience: undefined, + callbackUrl: undefined, + skewTime: undefined, + proofPurpose: undefined } ): Promise { if (!options.resolver) throw new Error('No DID resolver has been configured') const { payload, header, signature, data }: JWTDecoded = decodeJWT(jwt) - const proofPurpose: string | undefined = options.hasOwnProperty('auth') + const proofPurpose: ProofPurposeTypes | undefined = options.hasOwnProperty('auth') ? options.auth ? 'authentication' : undefined @@ -310,12 +317,12 @@ export async function verifyJWT( const { didResolutionResult, authenticators, issuer }: DIDAuthenticator = await resolveAuthenticator( options.resolver, header.alg, - payload.iss, + payload.iss || '', proofPurpose ) const signer: VerificationMethod = await verifyJWSDecoded({ header, data, signature } as JWSDecoded, authenticators) const now: number = Math.floor(Date.now() / 1000) - const skewTime = options.skewTime >= 0 ? options.skewTime : NBF_SKEW + const skewTime = typeof options.skewTime !== 'undefined' && options.skewTime >= 0 ? options.skewTime : NBF_SKEW if (signer) { const nowSkewed = now + skewTime if (payload.nbf) { @@ -341,6 +348,9 @@ export async function verifyJWT( } return { payload, didResolutionResult, issuer, signer, jwt } } + throw new Error( + `JWT not valid. issuer DID document does not contain a verificationMethod that matches the signature.` + ) } /** @@ -363,7 +373,7 @@ export async function resolveAuthenticator( resolver: Resolvable, alg: string, issuer: string, - proofPurpose?: string + proofPurpose?: ProofPurposeTypes ): Promise { const types: string[] = SUPPORTED_PUBLIC_KEY_TYPES[alg] if (!types || types.length === 0) { @@ -383,7 +393,7 @@ export async function resolveAuthenticator( didResult = result as DIDResolutionResult } - if (didResult.didResolutionMetadata?.error) { + if (didResult.didResolutionMetadata?.error || didResult.didDocument == null) { const { error, message } = didResult.didResolutionMetadata throw new Error(`Unable to resolve DID document for ${issuer}: ${error}, ${message || ''}`) } @@ -399,7 +409,8 @@ export async function resolveAuthenticator( ] if (typeof proofPurpose === 'string') { // support legacy DID Documents that do not list assertionMethod - if (proofPurpose.startsWith('assertion') && !didResult.didDocument.hasOwnProperty('assertionMethod')) { + if (proofPurpose.startsWith('assertion') && !didResult?.didDocument?.hasOwnProperty('assertionMethod')) { + didResult.didDocument = { ...(didResult.didDocument) } didResult.didDocument.assertionMethod = [...publicKeysToCheck.map((pk) => pk.id)] } @@ -414,7 +425,7 @@ export async function resolveAuthenticator( return verificationMethod } }) - .filter((key) => key != null) + .filter((key) => key != null) as VerificationMethod[] } const authenticators: VerificationMethod[] = publicKeysToCheck.filter(({ type }) => diff --git a/src/VerifierAlgorithm.ts b/src/VerifierAlgorithm.ts index fb7c203d..59136f26 100644 --- a/src/VerifierAlgorithm.ts +++ b/src/VerifierAlgorithm.ts @@ -1,4 +1,4 @@ -import { ec as EC } from 'elliptic' +import { ec as EC, SignatureInput } from 'elliptic' import { sha256, toEthereumAddress } from './Digest' import { verify } from '@stablelib/ed25519' import type { VerificationMethod } from 'did-resolver' @@ -8,15 +8,15 @@ const secp256k1 = new EC('secp256k1') // converts a JOSE signature to it's components export function toSignatureObject(signature: string, recoverable = false): EcdsaSignature { - const rawsig: Uint8Array = base64ToBytes(signature) - if (rawsig.length !== (recoverable ? 65 : 64)) { + const rawSig: Uint8Array = base64ToBytes(signature) + if (rawSig.length !== (recoverable ? 65 : 64)) { throw new Error('wrong signature length') } - const r: string = bytesToHex(rawsig.slice(0, 32)) - const s: string = bytesToHex(rawsig.slice(32, 64)) + const r: string = bytesToHex(rawSig.slice(0, 32)) + const s: string = bytesToHex(rawSig.slice(32, 64)) const sigObj: EcdsaSignature = { r, s } if (recoverable) { - sigObj.recoveryParam = rawsig[64] + sigObj.recoveryParam = rawSig[64] } return sigObj } @@ -32,7 +32,7 @@ function extractPublicKeyBytes(pk: VerificationMethod): Uint8Array { return base64ToBytes((pk).publicKeyBase64) } else if (pk.publicKeyHex) { return hexToBytes(pk.publicKeyHex) - } else if (pk.publicKeyJwk && pk.publicKeyJwk.crv === 'secp256k1') { + } else if (pk.publicKeyJwk && pk.publicKeyJwk.crv === 'secp256k1' && pk.publicKeyJwk.x && pk.publicKeyJwk.y) { return hexToBytes( secp256k1 .keyFromPublic({ @@ -59,10 +59,10 @@ export function verifyES256K( return typeof ethereumAddress !== 'undefined' || typeof blockchainAccountId !== undefined }) - let signer: VerificationMethod = fullPublicKeys.find((pk: VerificationMethod) => { + let signer: VerificationMethod | undefined = fullPublicKeys.find((pk: VerificationMethod) => { try { const pubBytes = extractPublicKeyBytes(pk) - return secp256k1.keyFromPublic(pubBytes).verify(hash, sigObj) + return secp256k1.keyFromPublic(pubBytes).verify(hash, sigObj) } catch (err) { return false } @@ -92,14 +92,14 @@ export function verifyRecoverableES256K( ] } - const checkSignatureAgainstSigner = (sigObj: EcdsaSignature): VerificationMethod => { + const checkSignatureAgainstSigner = (sigObj: EcdsaSignature): VerificationMethod | undefined => { const hash: Uint8Array = sha256(data) - const recoveredKey: any = secp256k1.recoverPubKey(hash, sigObj, sigObj.recoveryParam) + const recoveredKey: any = secp256k1.recoverPubKey(hash, sigObj, sigObj.recoveryParam) const recoveredPublicKeyHex: string = recoveredKey.encode('hex') const recoveredCompressedPublicKeyHex: string = recoveredKey.encode('hex', true) const recoveredAddress: string = toEthereumAddress(recoveredPublicKeyHex) - const signer: VerificationMethod = authenticators.find((pk: VerificationMethod) => { + const signer: VerificationMethod | undefined = authenticators.find((pk: VerificationMethod) => { const keyHex = bytesToHex(extractPublicKeyBytes(pk)) return ( keyHex === recoveredPublicKeyHex || @@ -112,7 +112,9 @@ export function verifyRecoverableES256K( return signer } - const signer: VerificationMethod[] = signatures.map(checkSignatureAgainstSigner).filter((key) => key != null) + const signer: VerificationMethod[] = signatures + .map(checkSignatureAgainstSigner) + .filter((key) => typeof key !== 'undefined') as VerificationMethod[] if (signer.length === 0) throw new Error('Signature invalid for JWT') return signer[0] @@ -125,7 +127,7 @@ export function verifyEd25519( ): VerificationMethod { const clear: Uint8Array = stringToBytes(data) const sig: Uint8Array = base64ToBytes(signature) - const signer: VerificationMethod = authenticators.find((pk: VerificationMethod) => { + const signer = authenticators.find((pk: VerificationMethod) => { return verify(extractPublicKeyBytes(pk), clear, sig) }) if (!signer) throw new Error('Signature invalid for JWT') diff --git a/src/util.ts b/src/util.ts index 6a3c278b..691a37a5 100644 --- a/src/util.ts +++ b/src/util.ts @@ -6,7 +6,7 @@ import * as u8a from 'uint8arrays' export interface EcdsaSignature { r: string s: string - recoveryParam?: number + recoveryParam?: number | null } export function bytesToBase64url(b: Uint8Array): string { @@ -56,15 +56,15 @@ export function toJose({ r, s, recoveryParam }: EcdsaSignature, recoverable?: bo jose.set(u8a.fromString(r, 'base16'), 0) jose.set(u8a.fromString(s, 'base16'), 32) if (recoverable) { - if (recoveryParam === undefined) { + if (typeof recoveryParam === 'undefined') { throw new Error('Signer did not return a recoveryParam') } - jose[64] = recoveryParam + jose[64] = recoveryParam } return bytesToBase64url(jose) } -export function fromJose(signature: string): { r: string; s: string; recoveryParam: number } { +export function fromJose(signature: string): { r: string; s: string; recoveryParam?: number } { const signatureBytes: Uint8Array = 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}`)