From 18bc6dbe1f3bc103b0b45d5ceb4851a5f965d8d6 Mon Sep 17 00:00:00 2001 From: Orie Steele Date: Sat, 10 Aug 2024 17:02:56 -0500 Subject: [PATCH] flatten verifier --- src/index.ts | 3 +- src/lib/Verifier.ts | 119 -------------------- src/lib/_verify.ts | 102 +++++++++++++++++ src/v2/verifier.ts | 10 +- test/headers.test.ts | 9 +- test/vc-jose-cose-test/vc-jose-cose.test.ts | 28 ++--- 6 files changed, 126 insertions(+), 145 deletions(-) delete mode 100644 src/lib/Verifier.ts create mode 100644 src/lib/_verify.ts diff --git a/src/index.ts b/src/index.ts index ee9278b..12df900 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,6 @@ -import Verifier from './lib/Verifier' import JWK from './lib/JWK' import JWS from './lib/JWS' import Parse from './lib/Parse' @@ -12,6 +11,6 @@ import v2 from './v2' export * from './types' -const sd = { ...v2, YAML, JWK, JWS, Verifier, Parse } +const sd = { ...v2, YAML, JWK, JWS, Parse } export default sd \ No newline at end of file diff --git a/src/lib/Verifier.ts b/src/lib/Verifier.ts deleted file mode 100644 index 15cf896..0000000 --- a/src/lib/Verifier.ts +++ /dev/null @@ -1,119 +0,0 @@ - -import { DIGEST_ALG_KEY } from "./constants"; - -import { VerifierCtx, RequestPresentationVerify, VerifiedSdJwt } from '../types' - -import JWS from './JWS'; -import Parse from './Parse'; - - -import _unpack_disclosed_claims from './_unpack_disclosed_claims' -import { decodeJwt, decodeProtectedHeader } from "jose"; - -import { validate_public_claims } from './validate_public_claims' -import { validate_sd_hash } from './validate_sd_hash' - -export default class Verifier { - - public debug: boolean; - public alg: string; - public digester; - public verifier; - public resolver; - - constructor(ctx: VerifierCtx) { - this.alg = ctx.alg; - this.digester = ctx.digester; - this.verifier = ctx.verifier; - this.resolver = ctx.resolver; - this.debug = ctx.debug || false; - } - - verify = async ({ presentation, aud, nonce }: RequestPresentationVerify) => { - const { debug, verifier, resolver, digester } = this; - const { jwt, kbt } = Parse.compact(presentation) - const decodedHeader = decodeProtectedHeader(jwt) - let verifiedIssuanceToken - if (verifier) { - verifiedIssuanceToken = await verifier.verify(presentation) - } else if (resolver) { - if (!decodedHeader.kid) { - throw new Error('kid is required when resolver is used to obtain public keys') - } - const issuerPublicKey = await resolver.resolve(decodedHeader.kid) - const compactJwsVerifier = await JWS.verifier(issuerPublicKey) - verifiedIssuanceToken = await compactJwsVerifier.verify(jwt) - } else { - throw new Error('a verifier or resolver is required, but not present.') - } - if (verifiedIssuanceToken.claimset[DIGEST_ALG_KEY] !== digester.name) { - throw new Error('Invalid hash algorithm') - } - // here we are verifying the "Issuer Signed JWT" - // aud and nonce, are expected to be checked in the KBT, not the "Issuer Signed JWT". - // See: https://github.com/oauth-wg/oauth-selective-disclosure-jwt/issues/395 - validate_public_claims('Issuer-signed JWT', verifiedIssuanceToken.claimset, { - debug, - reference_audience: verifiedIssuanceToken.claimset.aud, - reference_nonce: verifiedIssuanceToken.claimset.nonce - }) - if (debug) { - console.info('Verified Issuer-signed JWT: ', JSON.stringify(verifiedIssuanceToken, null, 2)) - } - const { cnf } = verifiedIssuanceToken.claimset - if (cnf) { - if (!kbt) { - throw new Error('Verification of this credential requires proof of posession from the holder. Key binding token is expected based on claims, but was not found.') - } - try { - let kid; - let jwk; - let confirmationPublicKey - let verified; - const { cnf } = verifiedIssuanceToken.claimset - if (cnf.jwk) { - ({ cnf: { jwk } } = verifiedIssuanceToken.claimset) - confirmationPublicKey = jwk - if (debug) { - console.info('Issued JWT has JWK confirmation method.') - } - } - if ((verifiedIssuanceToken.claimset.cnf).kid) { - ({ cnf: { kid } } = verifiedIssuanceToken.claimset) - if (debug) { - console.info('Issued JWT has kid confirmation method.') - } - if (!resolver) { - throw new Error('Resolver is required for kid confirmation method') - } - confirmationPublicKey = await resolver?.resolve(kid) - } - const compactJwsVerifier = await JWS.verifier(confirmationPublicKey) - verified = await compactJwsVerifier.verify(kbt) - if (!verified) { - throw new Error('Failed to verify key binding token') - } - await validate_sd_hash(presentation, verified.claimset.sd_hash, debug) - validate_public_claims('Key Binding Token', verified.claimset, { - debug, - reference_audience: aud, - reference_nonce: nonce - }) - if (debug) { - console.info('Verified Key Binding Token: ', JSON.stringify(verified, null, 2)) - } - } catch (e) { - console.error(e) - throw new Error('Failed to validate key binding token.') - } - } else { - if (debug) { - console.info('Issued JWT has no confirmation method.') - } - } - const { disclosureMap, hashToEncodedDisclosureMap } = await Parse.expload(presentation, { digester: digester }) - const state = { _hash_to_disclosure: hashToEncodedDisclosureMap, _hash_to_decoded_disclosure: disclosureMap } - const output = _unpack_disclosed_claims(verifiedIssuanceToken.claimset, state) - return JSON.parse(JSON.stringify({ protectedHeader: verifiedIssuanceToken.protectedHeader, claimset: output })) as VerifiedSdJwt - } -} \ No newline at end of file diff --git a/src/lib/_verify.ts b/src/lib/_verify.ts new file mode 100644 index 0000000..2b2972a --- /dev/null +++ b/src/lib/_verify.ts @@ -0,0 +1,102 @@ + +import { DIGEST_ALG_KEY } from "./constants"; + +import { VerifiedSdJwt } from '../types' + +import JWS from './JWS'; +import Parse from './Parse'; + + +import _unpack_disclosed_claims from './_unpack_disclosed_claims' +import { decodeProtectedHeader } from "jose"; + +import { validate_public_claims } from './validate_public_claims' +import { validate_sd_hash } from './validate_sd_hash' + + +export const _verify = async ({ presentation, aud, nonce, debug, verifier, resolver, digester }: any) => { + const { jwt, kbt } = Parse.compact(presentation) + const decodedHeader = decodeProtectedHeader(jwt) + let verifiedIssuanceToken + if (verifier) { + verifiedIssuanceToken = await verifier.verify(presentation) + } else if (resolver) { + if (!decodedHeader.kid) { + throw new Error('kid is required when resolver is used to obtain public keys') + } + const issuerPublicKey = await resolver.resolve(decodedHeader.kid) + const compactJwsVerifier = await JWS.verifier(issuerPublicKey) + verifiedIssuanceToken = await compactJwsVerifier.verify(jwt) + } else { + throw new Error('a verifier or resolver is required, but not present.') + } + if (verifiedIssuanceToken.claimset[DIGEST_ALG_KEY] !== digester.name) { + throw new Error('Invalid hash algorithm') + } + // here we are verifying the "Issuer Signed JWT" + // aud and nonce, are expected to be checked in the KBT, not the "Issuer Signed JWT". + // See: https://github.com/oauth-wg/oauth-selective-disclosure-jwt/issues/395 + validate_public_claims('Issuer-signed JWT', verifiedIssuanceToken.claimset, { + debug, + reference_audience: verifiedIssuanceToken.claimset.aud, + reference_nonce: verifiedIssuanceToken.claimset.nonce + }) + if (debug) { + console.info('Verified Issuer-signed JWT: ', JSON.stringify(verifiedIssuanceToken, null, 2)) + } + const { cnf } = verifiedIssuanceToken.claimset + if (cnf) { + if (!kbt) { + throw new Error('Verification of this credential requires proof of posession from the holder. Key binding token is expected based on claims, but was not found.') + } + try { + let kid; + let jwk; + let confirmationPublicKey + let verified; + const { cnf } = verifiedIssuanceToken.claimset + if (cnf.jwk) { + ({ cnf: { jwk } } = verifiedIssuanceToken.claimset) + confirmationPublicKey = jwk + if (debug) { + console.info('Issued JWT has JWK confirmation method.') + } + } + if ((verifiedIssuanceToken.claimset.cnf).kid) { + ({ cnf: { kid } } = verifiedIssuanceToken.claimset) + if (debug) { + console.info('Issued JWT has kid confirmation method.') + } + if (!resolver) { + throw new Error('Resolver is required for kid confirmation method') + } + confirmationPublicKey = await resolver?.resolve(kid) + } + const compactJwsVerifier = await JWS.verifier(confirmationPublicKey) + verified = await compactJwsVerifier.verify(kbt) + if (!verified) { + throw new Error('Failed to verify key binding token') + } + await validate_sd_hash(presentation, verified.claimset.sd_hash, debug) + validate_public_claims('Key Binding Token', verified.claimset, { + debug, + reference_audience: aud, + reference_nonce: nonce + }) + if (debug) { + console.info('Verified Key Binding Token: ', JSON.stringify(verified, null, 2)) + } + } catch (e) { + console.error(e) + throw new Error('Failed to validate key binding token.') + } + } else { + if (debug) { + console.info('Issued JWT has no confirmation method.') + } + } + const { disclosureMap, hashToEncodedDisclosureMap } = await Parse.expload(presentation, { digester: digester }) + const state = { _hash_to_disclosure: hashToEncodedDisclosureMap, _hash_to_decoded_disclosure: disclosureMap } + const output = _unpack_disclosed_claims(verifiedIssuanceToken.claimset, state) + return JSON.parse(JSON.stringify({ protectedHeader: verifiedIssuanceToken.protectedHeader, claimset: output })) as VerifiedSdJwt +} \ No newline at end of file diff --git a/src/v2/verifier.ts b/src/v2/verifier.ts index a518b70..c74eba9 100644 --- a/src/v2/verifier.ts +++ b/src/v2/verifier.ts @@ -1,12 +1,12 @@ -import Verifier from "../lib/Verifier" - import digester from "./digester" import JWS from "../lib/JWS" import Parse from "../lib/Parse" -import { PublicKeyJwk, RequestVerifier, VerifierCtx, VerifiedSdJwt } from '../types' +import { PublicKeyJwk, RequestVerifier, VerifiedSdJwt } from '../types' + +import { _verify } from "../lib/_verify" export default function verifier(options: RequestVerifier){ if (!options.digester){ @@ -28,8 +28,8 @@ export default function verifier(options: RequestVerifier){ } return { verify: async ({ token, audience, nonce }: { token: string, audience ?: string, nonce?: string }): Promise => { - const role = new Verifier(options as VerifierCtx) - const verified = await role.verify({ + const verified = await _verify({ + ...options, presentation: token, aud: audience, nonce: nonce diff --git a/test/headers.test.ts b/test/headers.test.ts index 6202ae7..576be0f 100644 --- a/test/headers.test.ts +++ b/test/headers.test.ts @@ -74,7 +74,7 @@ credentialSubject: entryNumber: True `, }) - const verifier = new SD.Verifier({ + const verified = await SD.verifier({ alg, digester, verifier: { @@ -84,11 +84,10 @@ credentialSubject: return verifier.verify(parsed.jwt) } } - }) - const verified = await verifier.verify({ - presentation: vp, + }).verify({ + token: vp, nonce, - aud: audience + audience: audience }) expect(verified.claimset.issuer.location).toBeUndefined() expect(verified.claimset.credentialSubject.entryNumber).toBe('12345123456') diff --git a/test/vc-jose-cose-test/vc-jose-cose.test.ts b/test/vc-jose-cose-test/vc-jose-cose.test.ts index 0a689e7..0fe53d3 100644 --- a/test/vc-jose-cose-test/vc-jose-cose.test.ts +++ b/test/vc-jose-cose-test/vc-jose-cose.test.ts @@ -20,17 +20,7 @@ it('W3C VC JOSE COSE Test', async () => { salter }) - const verifier = new SD.Verifier({ - alg, - digester, - verifier: { - verify: async (token: string) => { - const parsed = SD.Parse.compact(token) - const verifier = await SD.JWS.verifier(issuerKeyPair.publicKeyJwk) - return verifier.verify(parsed.jwt) - } - } - }) + const claimsYaml = fs.readFileSync(`test/vc-jose-cose-test/payload.yaml`).toString() const vc = await issuer.issue({ jwk: holderKeyPair.publicKeyJwk, @@ -50,10 +40,20 @@ it('W3C VC JOSE COSE Test', async () => { disclosure: claimsDisclosureYaml, }) - const verified = await verifier.verify({ - presentation: vp, + const verified = await SD.verifier({ + alg, + digester, + verifier: { + verify: async (token: string) => { + const parsed = SD.Parse.compact(token) + const verifier = await SD.JWS.verifier(issuerKeyPair.publicKeyJwk) + return verifier.verify(parsed.jwt) + } + } + }).verify({ + token: vp, nonce, - aud: audience + audience: audience }) expect(verified.claimset.proof.created).toBe('2023-06-18T21:19:10Z') }); \ No newline at end of file