From 85c0155205500933a6d94fbc515c28afe9af9a56 Mon Sep 17 00:00:00 2001 From: Berend Sliedrecht Date: Fri, 13 Jun 2025 10:25:51 +0200 Subject: [PATCH 1/4] feat: Relying Party Authorization Signed-off-by: Berend Sliedrecht --- src/authorizeAllowlist.ts | 4 + src/authorizeAttributeBasedAccessControl.ts | 7 + src/authorizeRootOfTrust.ts | 4 + src/isDcqlQueryEqualOrSubset.ts | 24 ++- src/verifyAuthorizationAttestation.ts | 73 ++++++++ src/verifyOpenid4VpAuthorizationRequest.ts | 191 +++----------------- src/verifyRegistrationCertificate.ts | 184 +++++++++++++++++++ 7 files changed, 311 insertions(+), 176 deletions(-) create mode 100644 src/authorizeAllowlist.ts create mode 100644 src/authorizeAttributeBasedAccessControl.ts create mode 100644 src/authorizeRootOfTrust.ts create mode 100644 src/verifyAuthorizationAttestation.ts create mode 100644 src/verifyRegistrationCertificate.ts diff --git a/src/authorizeAllowlist.ts b/src/authorizeAllowlist.ts new file mode 100644 index 0000000..bb7bffe --- /dev/null +++ b/src/authorizeAllowlist.ts @@ -0,0 +1,4 @@ +import type { X509Certificate } from '@credo-ts/core' + +export const authorizeAllowList = (allowList: Array, relyingPartyAccessCertificate: X509Certificate) => + allowList.includes(relyingPartyAccessCertificate.subject) diff --git a/src/authorizeAttributeBasedAccessControl.ts b/src/authorizeAttributeBasedAccessControl.ts new file mode 100644 index 0000000..b085afe --- /dev/null +++ b/src/authorizeAttributeBasedAccessControl.ts @@ -0,0 +1,7 @@ +import type { DcqlQuery } from '@credo-ts/core' +import { isDcqlQueryEqualOrSubset } from './isDcqlQueryEqualOrSubset' + +export const authorizeAttributeBasedAccessControl = ( + requestQuery: DcqlQuery, + attributeBasedAccessControlQuery: DcqlQuery +) => isDcqlQueryEqualOrSubset(requestQuery, attributeBasedAccessControlQuery) diff --git a/src/authorizeRootOfTrust.ts b/src/authorizeRootOfTrust.ts new file mode 100644 index 0000000..c62e831 --- /dev/null +++ b/src/authorizeRootOfTrust.ts @@ -0,0 +1,4 @@ +import type { X509Certificate } from '@credo-ts/core' + +export const authorizeRootOfTrust = (rootOfTrustDistinguishedName: string, rootCertificates: Array) => + rootCertificates.some((rc) => rc.subject === rootOfTrustDistinguishedName) diff --git a/src/isDcqlQueryEqualOrSubset.ts b/src/isDcqlQueryEqualOrSubset.ts index 5604ed6..8aa13b6 100644 --- a/src/isDcqlQueryEqualOrSubset.ts +++ b/src/isDcqlQueryEqualOrSubset.ts @@ -1,23 +1,29 @@ import { type DcqlQuery, equalsIgnoreOrder, equalsWithOrder } from '@credo-ts/core' -export function isDcqlQueryEqualOrSubset(arq: DcqlQuery, rcq: DcqlQuery): boolean { - if (rcq.credential_sets) { +/** + * + * Check whether query `lhs` is equal or a subset of `rhs` + * + */ +export function isDcqlQueryEqualOrSubset(lhs: DcqlQuery, rhs: DcqlQuery): boolean { + // `credential_sets` are currently not supported + if (rhs.credential_sets) { return false } - if (rcq.credentials.some((c) => c.id)) { + if (rhs.credentials.some((c) => c.id)) { return false } // only sd-jwt and mdoc are supported - if (arq.credentials.some((c) => c.format !== 'mso_mdoc' && c.format !== 'vc+sd-jwt' && c.format !== 'dc+sd-jwt')) { + if (lhs.credentials.some((c) => c.format !== 'mso_mdoc' && c.format !== 'vc+sd-jwt' && c.format !== 'dc+sd-jwt')) { return false } - credentialQueryLoop: for (const credentialQuery of arq.credentials) { - const matchingRcqCredentialQueriesBasedOnFormat = rcq.credentials.filter((c) => c.format === credentialQuery.format) + credentialQueryLoop: for (const credentialQuery of lhs.credentials) { + const matchingRhsCredentialQueriesBasedOnFormat = rhs.credentials.filter((c) => c.format === credentialQuery.format) - if (matchingRcqCredentialQueriesBasedOnFormat.length === 0) return false + if (matchingRhsCredentialQueriesBasedOnFormat.length === 0) return false switch (credentialQuery.format) { case 'mso_mdoc': { @@ -25,7 +31,7 @@ export function isDcqlQueryEqualOrSubset(arq: DcqlQuery, rcq: DcqlQuery): boolea if (!doctypeValue) return false if (typeof credentialQuery.meta?.doctype_value !== 'string') return false - const foundMatchingRequests = matchingRcqCredentialQueriesBasedOnFormat.filter( + const foundMatchingRequests = matchingRhsCredentialQueriesBasedOnFormat.filter( (c): c is typeof c & { format: 'mso_mdoc' } => !!(c.format === 'mso_mdoc' && c.meta && c.meta.doctype_value === doctypeValue) ) @@ -68,7 +74,7 @@ export function isDcqlQueryEqualOrSubset(arq: DcqlQuery, rcq: DcqlQuery): boolea if (!vctValues) return false if (credentialQuery.meta?.vct_values?.length === 0) return false - const foundMatchingRequests = matchingRcqCredentialQueriesBasedOnFormat.filter( + const foundMatchingRequests = matchingRhsCredentialQueriesBasedOnFormat.filter( (c): c is typeof c & ({ format: 'dc+sd-jwt' } | { format: 'vc+sd-jwt' }) => !!( (c.format === 'dc+sd-jwt' || c.format === 'vc+sd-jwt') && diff --git a/src/verifyAuthorizationAttestation.ts b/src/verifyAuthorizationAttestation.ts new file mode 100644 index 0000000..7c8c0bf --- /dev/null +++ b/src/verifyAuthorizationAttestation.ts @@ -0,0 +1,73 @@ +import { type AgentContext, JwsService, Jwt, X509Certificate } from '@credo-ts/core' +import type { OpenId4VpResolvedAuthorizationRequest } from '@credo-ts/openid4vc' +import { z } from 'zod' +import { isRegistrationCertificate } from './verifyRegistrationCertificate' + +const authorizationAttestationHeaderSchema = z.object({}) + +const authorizationAttestationPayloadSchema = z.object({}) + +/** + * + * According to: https://bmi.usercontent.opencode.de/eudi-wallet/eidas-2.0-architekturkonzept/flows/OID4VC-with-WRP-attestations/#verifier-attestations + * + * "Each credential, that is not a Registration Certificate, is treated as an Authorization Attestation. For possible formats, see the examples section like for SD-JWT-VC." + * + */ +export const isAuthorizationAttestation = (agentContext: AgentContext, jwt: string) => { + return !isRegistrationCertificate(agentContext, jwt) +} + +export type VerifyAuthorizationAttestationOptions = { + authorizationAttestation: string + resolvedAuthorizationRequest: OpenId4VpResolvedAuthorizationRequest + trustedCertificates?: Array + allowUntrustedSigned?: boolean +} + +export const verifyAuthorizationAttestation = async ( + agentContext: AgentContext, + { + authorizationAttestation, + resolvedAuthorizationRequest: { signedAuthorizationRequest }, + allowUntrustedSigned, + trustedCertificates, + }: VerifyAuthorizationAttestationOptions +) => { + const jwsService = agentContext.dependencyManager.resolve(JwsService) + + let isValidButUntrusted = false + let isValidAndTrusted = false + + const jwt = Jwt.fromSerializedJwt(authorizationAttestation) + + try { + const { isValid } = await jwsService.verifyJws(agentContext, { + jws: authorizationAttestation, + trustedCertificates, + }) + isValidAndTrusted = isValid + } catch { + if (allowUntrustedSigned) { + const { isValid } = await jwsService.verifyJws(agentContext, { + jws: authorizationAttestation, + trustedCertificates: jwt.header.x5c ?? [], + }) + isValidButUntrusted = isValid + } + } + + if (!signedAuthorizationRequest) { + throw new Error('Authorization request must be signed for the authorization attestation') + } + + if (signedAuthorizationRequest.signer.method !== 'x5c') { + throw new Error( + 'x5c is only supported for key derivation on the authorization request containing a authorization attestation' + ) + } + + const accessCertificate = X509Certificate.fromEncodedCertificate(signedAuthorizationRequest.signer.x5c[0]) + + // TODO: zod validate header + payload +} diff --git a/src/verifyOpenid4VpAuthorizationRequest.ts b/src/verifyOpenid4VpAuthorizationRequest.ts index 4a8a5d8..5bf314e 100644 --- a/src/verifyOpenid4VpAuthorizationRequest.ts +++ b/src/verifyOpenid4VpAuthorizationRequest.ts @@ -1,7 +1,7 @@ -import { type AgentContext, type DcqlQuery, JwsService, Jwt, X509Certificate } from '@credo-ts/core' +import type { AgentContext } from '@credo-ts/core' import type { OpenId4VpResolvedAuthorizationRequest } from '@credo-ts/openid4vc' -import z from 'zod' -import { isDcqlQueryEqualOrSubset } from './isDcqlQueryEqualOrSubset' +import { isAuthorizationAttestation, verifyAuthorizationAttestation } from './verifyAuthorizationAttestation' +import { isRegistrationCertificate, verifyIfRegistrationCertificate } from './verifyRegistrationCertificate' export type VerifyAuthorizationRequestOptions = { resolvedAuthorizationRequest: OpenId4VpResolvedAuthorizationRequest @@ -11,174 +11,31 @@ export type VerifyAuthorizationRequestOptions = { export const verifyOpenid4VpAuthorizationRequest = async ( agentContext: AgentContext, - { - resolvedAuthorizationRequest: { authorizationRequestPayload, signedAuthorizationRequest, dcql }, - trustedCertificates, - allowUntrustedSigned, - }: VerifyAuthorizationRequestOptions + { resolvedAuthorizationRequest, trustedCertificates, allowUntrustedSigned }: VerifyAuthorizationRequestOptions ) => { - const results = [] - if (!authorizationRequestPayload.verifier_attestations) return - for (const va of authorizationRequestPayload.verifier_attestations) { - // Here we verify it as a registration certificate according to - // https://bmi.usercontent.opencode.de/eudi-wallet/eidas-2.0-architekturkonzept/flows/Wallet-Relying-Party-Authentication/#registration-certificate - if (va.format === 'jwt') { - if (typeof va.data !== 'string') { - throw new Error('Only inline JWTs are supported') - } - - const jwsService = agentContext.dependencyManager.resolve(JwsService) - - let isValidButUntrusted = false - let isValidAndTrusted = false - - const jwt = Jwt.fromSerializedJwt(va.data) - - try { - const { isValid } = await jwsService.verifyJws(agentContext, { - jws: va.data, - trustedCertificates, - }) - isValidAndTrusted = isValid - } catch { - if (allowUntrustedSigned) { - const { isValid } = await jwsService.verifyJws(agentContext, { - jws: va.data, - trustedCertificates: jwt.header.x5c ?? [], - }) - isValidButUntrusted = isValid - } - } - - if (jwt.header.typ !== 'rc-rp+jwt') { - throw new Error(`only 'rc-rp+jwt' is supported as header typ. Request included: ${jwt.header.typ}`) - } - - if (!signedAuthorizationRequest) { - throw new Error('Request must be signed for the registration certificate') - } - - if (signedAuthorizationRequest.signer.method !== 'x5c') { - throw new Error('x5c is only supported for registration certificate') - } - - const registrationCertificateHeaderSchema = z - .object({ - typ: z.literal('rc-rp+jwt'), - alg: z.string(), - // sprin-d did not define this - x5u: z.string().url().optional(), - // sprin-d did not define this - 'x5t#s256': z.string().optional(), - }) - .passthrough() - - // TODO: does not support intermediaries - const registrationCertificatePayloadSchema = z - .object({ - credentials: z.array( - z.object({ - format: z.string(), - multiple: z.boolean().default(false), - meta: z - .object({ - vct_values: z.array(z.string()).optional(), - doctype_value: z.string().optional(), - }) - .optional(), - trusted_authorities: z - .array(z.object({ type: z.string(), values: z.array(z.string()) })) - .nonempty() - .optional(), - require_cryptographic_holder_binding: z.boolean().default(true), - claims: z - .array( - z.object({ - id: z.string().optional(), - path: z.array(z.string()).nonempty().nonempty(), - values: z.array(z.number().or(z.boolean())).optional(), - }) - ) - .nonempty() - .optional(), - claim_sets: z.array(z.array(z.string())).nonempty().optional(), - }) - ), - contact: z.object({ - website: z.string().url(), - 'e-mail': z.string().email(), - phone: z.string(), - }), - sub: z.string(), - // Should be service - services: z.array(z.object({ lang: z.string(), name: z.string() })), - public_body: z.boolean().default(false), - entitlements: z.array(z.any()), - provided_attestations: z - .array( - z.object({ - format: z.string(), - meta: z.any(), - }) - ) - .optional(), - privacy_policy: z.string().url(), - iat: z.number().optional(), - exp: z.number().optional(), - purpose: z - .array( - z.object({ - locale: z.string().optional(), - lang: z.string().optional(), - name: z.string(), - }) - ) - .optional(), - status: z.any(), - }) - .passthrough() - - registrationCertificateHeaderSchema.parse(jwt.header) - const parsedPayload = registrationCertificatePayloadSchema.parse(jwt.payload.toJson()) - - const [rpCertEncoded] = signedAuthorizationRequest.signer.x5c - const rpCert = X509Certificate.fromEncodedCertificate(rpCertEncoded) - - if (rpCert.subject !== parsedPayload.sub) { - throw new Error( - `Subject in the certificate of the auth request: '${rpCert.subject}' is not equal to the subject of the registration certificate: '${parsedPayload.sub}'` - ) - } - - if (parsedPayload.iat && new Date().getTime() / 1000 <= parsedPayload.iat) { - throw new Error('Issued at timestamp of the registration certificate is in the future') - } - - // TODO: check the status of the registration certificate - - if (!dcql) { - throw new Error('DCQL must be used when working registration certificates') - } - - if ( - authorizationRequestPayload.presentation_definition || - authorizationRequestPayload.presentation_definition_uri - ) { - throw new Error('Presentation Exchange is not supported for the registration certificate') - } + let registrationCertificate: Awaited> | undefined + if (!resolvedAuthorizationRequest.authorizationRequestPayload.verifier_attestations) return + for (const va of resolvedAuthorizationRequest.authorizationRequestPayload.verifier_attestations) { + if (va.format !== 'jwt') { + throw new Error(`only format of 'jwt' is supported`) + } - const isValidDcqlQuery = isDcqlQueryEqualOrSubset(dcql.queryResult, parsedPayload as unknown as DcqlQuery) + if (typeof va.data !== 'string') { + throw new Error('Only inline JWTs are supported') + } - if (!isValidDcqlQuery) { - throw new Error( - 'DCQL query in the authorization request is not equal or a valid subset of the DCQl query provided in the registration certificate' - ) - } + if (isRegistrationCertificate(agentContext, va.data)) { + registrationCertificate = await verifyIfRegistrationCertificate(agentContext, { + registrationCertificate: va.data, + resolvedAuthorizationRequest, + allowUntrustedSigned, + trustedCertificates, + }) + } - results.push({ isValidButUntrusted, isValidAndTrusted, x509RegistrationCertificate: rpCert }) - } else { - throw new Error(`only format of 'jwt' is supported`) + if (isAuthorizationAttestation(agentContext, va.data)) { + await verifyAuthorizationAttestation(agentContext, { authorizationAttestation: va.data }) } } - return results + return registrationCertificate } diff --git a/src/verifyRegistrationCertificate.ts b/src/verifyRegistrationCertificate.ts new file mode 100644 index 0000000..9136270 --- /dev/null +++ b/src/verifyRegistrationCertificate.ts @@ -0,0 +1,184 @@ +import { type AgentContext, type DcqlQuery, JwsService, Jwt, X509Certificate } from '@credo-ts/core' +import type { OpenId4VpResolvedAuthorizationRequest } from '@credo-ts/openid4vc' +import { z } from 'zod' +import { isDcqlQueryEqualOrSubset } from './isDcqlQueryEqualOrSubset' + +type VerifyRegistrationCertificateOptions = { + registrationCertificate: string + resolvedAuthorizationRequest: OpenId4VpResolvedAuthorizationRequest + trustedCertificates?: Array + allowUntrustedSigned?: boolean +} + +const registrationCertificateHeaderSchema = z + .object({ + typ: z.literal('rc-rp+jwt'), + alg: z.string(), + // sprin-d did not define this + x5u: z.string().url().optional(), + // sprin-d did not define this + 'x5t#s256': z.string().optional(), + }) + .passthrough() + +// TODO: does not support intermediaries +const registrationCertificatePayloadSchema = z + .object({ + credentials: z.array( + z.object({ + format: z.string(), + multiple: z.boolean().default(false), + meta: z + .object({ + vct_values: z.array(z.string()).optional(), + doctype_value: z.string().optional(), + }) + .optional(), + trusted_authorities: z + .array(z.object({ type: z.string(), values: z.array(z.string()) })) + .nonempty() + .optional(), + require_cryptographic_holder_binding: z.boolean().default(true), + claims: z + .array( + z.object({ + id: z.string().optional(), + path: z.array(z.string()).nonempty().nonempty(), + values: z.array(z.number().or(z.boolean())).optional(), + }) + ) + .nonempty() + .optional(), + claim_sets: z.array(z.array(z.string())).nonempty().optional(), + }) + ), + contact: z.object({ + website: z.string().url(), + 'e-mail': z.string().email(), + phone: z.string(), + }), + sub: z.string(), + // Should be service + services: z.array(z.object({ lang: z.string(), name: z.string() })), + public_body: z.boolean().default(false), + entitlements: z.array(z.any()), + provided_attestations: z + .array( + z.object({ + format: z.string(), + meta: z.any(), + }) + ) + .optional(), + privacy_policy: z.string().url(), + iat: z.number().optional(), + exp: z.number().optional(), + purpose: z + .array( + z.object({ + locale: z.string().optional(), + lang: z.string().optional(), + name: z.string(), + }) + ) + .optional(), + status: z.any(), + }) + .passthrough() + +export const isRegistrationCertificate = (agentContext: AgentContext, jwt: string) => { + try { + const { header } = Jwt.fromSerializedJwt(jwt) + + if (header.typ !== 'rc-rp+jwt') { + return true + } + return false + } catch { + return false + } +} + +/** + * + * + * If it has a header of `rc-rp+jwt` it is validated as a registration certificate according to: + * https://bmi.usercontent.opencode.de/eudi-wallet/eidas-2.0-architekturkonzept/flows/Wallet-Relying-Party-Authentication/#registration-certificate + * + */ +export const verifyIfRegistrationCertificate = async ( + agentContext: AgentContext, + { + registrationCertificate, + trustedCertificates, + allowUntrustedSigned, + resolvedAuthorizationRequest: { signedAuthorizationRequest, authorizationRequestPayload, dcql }, + }: VerifyRegistrationCertificateOptions +) => { + const jwsService = agentContext.dependencyManager.resolve(JwsService) + + let isValidButUntrusted = false + let isValidAndTrusted = false + + const jwt = Jwt.fromSerializedJwt(registrationCertificate) + + try { + const { isValid } = await jwsService.verifyJws(agentContext, { + jws: registrationCertificate, + trustedCertificates, + }) + isValidAndTrusted = isValid + } catch { + if (allowUntrustedSigned) { + const { isValid } = await jwsService.verifyJws(agentContext, { + jws: registrationCertificate, + trustedCertificates: jwt.header.x5c ?? [], + }) + isValidButUntrusted = isValid + } + } + + if (!signedAuthorizationRequest) { + throw new Error('Request must be signed for the registration certificate') + } + + if (signedAuthorizationRequest.signer.method !== 'x5c') { + throw new Error('x5c is only supported for registration certificate') + } + + registrationCertificateHeaderSchema.parse(jwt.header) + const parsedPayload = registrationCertificatePayloadSchema.parse(jwt.payload.toJson()) + + const [rpCertEncoded] = signedAuthorizationRequest.signer.x5c + const rpCert = X509Certificate.fromEncodedCertificate(rpCertEncoded) + + if (rpCert.subject !== parsedPayload.sub) { + throw new Error( + `Subject in the certificate of the auth request: '${rpCert.subject}' is not equal to the subject of the registration certificate: '${parsedPayload.sub}'` + ) + } + + if (parsedPayload.iat && new Date().getTime() / 1000 <= parsedPayload.iat) { + throw new Error('Issued at timestamp of the registration certificate is in the future') + } + + // TODO: check the status of the registration certificate + + if (!dcql) { + throw new Error('DCQL must be used when working registration certificates') + } + + if (authorizationRequestPayload.presentation_definition || authorizationRequestPayload.presentation_definition_uri) { + throw new Error('Presentation Exchange is not supported for the registration certificate') + } + + const isValidDcqlQuery = isDcqlQueryEqualOrSubset(dcql.queryResult, parsedPayload as unknown as DcqlQuery) + + if (!isValidDcqlQuery) { + throw new Error( + 'DCQL query in the authorization request is not equal or a valid subset of the DCQl query provided in the registration certificate' + ) + } + + return { isValidButUntrusted, isValidAndTrusted, x509RegistrationCertificate: rpCert } +} From ff507354dc9c23a2511c435af8ad82f66d252d9b Mon Sep 17 00:00:00 2001 From: Berend Sliedrecht Date: Mon, 16 Jun 2025 13:33:34 +0200 Subject: [PATCH 2/4] feat: authz disclosure policy verification Signed-off-by: Berend Sliedrecht --- package.json | 1 + pnpm-lock.yaml | 4 + src/authorizeAllowlist.ts | 4 - src/authorizeAttributeBasedAccessControl.ts | 7 - src/authorizeRootOfTrust.ts | 4 - .../validateAllowlistPolicy.ts | 10 ++ ...lidateAttributeBasedAccessControlPolicy.ts | 36 +++++ .../validateRootOfTrustPolicy.ts | 11 ++ .../verifyAuthorizationAttestation.ts | 125 ++++++++++++++++++ ...izationForOpenid4VpAuthorizationRequest.ts | 101 ++++++++++++++ src/index.ts | 6 +- .../isDcqlQueryEqualOrSubset.ts | 0 .../verifyRegistrationCertificate.ts | 4 +- ...ificateInOpenid4VpAuthorizationRequest.ts} | 23 ++-- src/verifyAuthorizationAttestation.ts | 73 ---------- 15 files changed, 306 insertions(+), 103 deletions(-) delete mode 100644 src/authorizeAllowlist.ts delete mode 100644 src/authorizeAttributeBasedAccessControl.ts delete mode 100644 src/authorizeRootOfTrust.ts create mode 100644 src/disclosure-policy/validateAllowlistPolicy.ts create mode 100644 src/disclosure-policy/validateAttributeBasedAccessControlPolicy.ts create mode 100644 src/disclosure-policy/validateRootOfTrustPolicy.ts create mode 100644 src/disclosure-policy/verifyAuthorizationAttestation.ts create mode 100644 src/disclosure-policy/verifyAuthorizationForOpenid4VpAuthorizationRequest.ts rename src/{ => registration-certificate}/isDcqlQueryEqualOrSubset.ts (100%) rename src/{ => registration-certificate}/verifyRegistrationCertificate.ts (98%) rename src/{verifyOpenid4VpAuthorizationRequest.ts => registration-certificate/verifyRegistrationCertificateInOpenid4VpAuthorizationRequest.ts} (66%) delete mode 100644 src/verifyAuthorizationAttestation.ts diff --git a/package.json b/package.json index bea8a5a..b5f555d 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "typescript": "~5.8.3" }, "dependencies": { + "dcql": "^0.2.22", "zod": "^3.25.42" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ace96ab..8c35f3c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + dcql: + specifier: ^0.2.22 + version: 0.2.22(typescript@5.8.3) zod: specifier: ^3.25.42 version: 3.25.42 @@ -1389,6 +1392,7 @@ packages: '@sphereon/kmp-mdoc-core@0.2.0-SNAPSHOT.26': resolution: {integrity: sha512-QXJ6R8ENiZV2rPMbn06cw5JKwqUYN1kzVRbYfONqE1PEXx1noQ4md7uxr2zSczi0ubKkNcbyYDNtIMTZIhGzmQ==} + bundledDependencies: [] '@sphereon/pex-models@2.3.2': resolution: {integrity: sha512-foFxfLkRwcn/MOp/eht46Q7wsvpQGlO7aowowIIb5Tz9u97kYZ2kz6K2h2ODxWuv5CRA7Q0MY8XUBGE2lfOhOQ==} diff --git a/src/authorizeAllowlist.ts b/src/authorizeAllowlist.ts deleted file mode 100644 index bb7bffe..0000000 --- a/src/authorizeAllowlist.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type { X509Certificate } from '@credo-ts/core' - -export const authorizeAllowList = (allowList: Array, relyingPartyAccessCertificate: X509Certificate) => - allowList.includes(relyingPartyAccessCertificate.subject) diff --git a/src/authorizeAttributeBasedAccessControl.ts b/src/authorizeAttributeBasedAccessControl.ts deleted file mode 100644 index b085afe..0000000 --- a/src/authorizeAttributeBasedAccessControl.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { DcqlQuery } from '@credo-ts/core' -import { isDcqlQueryEqualOrSubset } from './isDcqlQueryEqualOrSubset' - -export const authorizeAttributeBasedAccessControl = ( - requestQuery: DcqlQuery, - attributeBasedAccessControlQuery: DcqlQuery -) => isDcqlQueryEqualOrSubset(requestQuery, attributeBasedAccessControlQuery) diff --git a/src/authorizeRootOfTrust.ts b/src/authorizeRootOfTrust.ts deleted file mode 100644 index c62e831..0000000 --- a/src/authorizeRootOfTrust.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type { X509Certificate } from '@credo-ts/core' - -export const authorizeRootOfTrust = (rootOfTrustDistinguishedName: string, rootCertificates: Array) => - rootCertificates.some((rc) => rc.subject === rootOfTrustDistinguishedName) diff --git a/src/disclosure-policy/validateAllowlistPolicy.ts b/src/disclosure-policy/validateAllowlistPolicy.ts new file mode 100644 index 0000000..57a0c7c --- /dev/null +++ b/src/disclosure-policy/validateAllowlistPolicy.ts @@ -0,0 +1,10 @@ +import type { X509Certificate } from '@credo-ts/core' + +export type AllowListPolicy = { + allowlist: Array +} + +export const validateAllowListPolicy = ( + allowListPolicy: AllowListPolicy, + relyingPartyAccessCertificate: X509Certificate +) => allowListPolicy.allowlist.includes(relyingPartyAccessCertificate.subject) diff --git a/src/disclosure-policy/validateAttributeBasedAccessControlPolicy.ts b/src/disclosure-policy/validateAttributeBasedAccessControlPolicy.ts new file mode 100644 index 0000000..69b74b5 --- /dev/null +++ b/src/disclosure-policy/validateAttributeBasedAccessControlPolicy.ts @@ -0,0 +1,36 @@ +import type { AgentContext, DcqlQuery } from '@credo-ts/core' +import { SdJwtVcService } from '@credo-ts/core' +import { type DcqlCredential, type DcqlQuery as Query, runDcqlQuery } from 'dcql' + +export type AttributeBasedAccessControlPolicy = { + attribute_based_access_control: DcqlQuery +} + +export const validateAttributeBasedAccessControlPolicy = ( + agentContext: AgentContext, + attributeBasedAccessControlPolicy: AttributeBasedAccessControlPolicy, + authorizationAttestations: Array +) => { + const credentials: DcqlCredential[] = [] + const sdJwtService = agentContext.dependencyManager.resolve(SdJwtVcService) + for (const authorizationAttestation of authorizationAttestations) { + const sdJwtCredential = sdJwtService.fromCompact(authorizationAttestation) + credentials.push({ + credential_format: 'dc+sd-jwt', + vct: sdJwtCredential.prettyClaims.vct, + claims: sdJwtCredential.prettyClaims, + }) + credentials.push({ + credential_format: 'vc+sd-jwt', + vct: sdJwtCredential.prettyClaims.vct, + claims: sdJwtCredential.prettyClaims, + }) + } + + const result = runDcqlQuery(attributeBasedAccessControlPolicy.attribute_based_access_control as Query.Output, { + credentials, + presentation: false, + }) + + return result.canBeSatisfied +} diff --git a/src/disclosure-policy/validateRootOfTrustPolicy.ts b/src/disclosure-policy/validateRootOfTrustPolicy.ts new file mode 100644 index 0000000..c2f481d --- /dev/null +++ b/src/disclosure-policy/validateRootOfTrustPolicy.ts @@ -0,0 +1,11 @@ +import type { X509Certificate } from '@credo-ts/core' + +// TODO: it is unclear from the spec what is inluded in this type +export type RootOfTrustPolicy = { + rootOfTrust: string +} + +// TODO: I am not sure how to validate this, based on the description +// +// rootOfTrust: the certificate of a RP must be derived from a list of specific root certificates. In the Implementing Acts it's called Specific root of trust. The value has to be the distinguished name of the root certificate. +export const validateRootOfTrustPolicy = (rootOfTrustPolicy: RootOfTrustPolicy) => true diff --git a/src/disclosure-policy/verifyAuthorizationAttestation.ts b/src/disclosure-policy/verifyAuthorizationAttestation.ts new file mode 100644 index 0000000..f8d779a --- /dev/null +++ b/src/disclosure-policy/verifyAuthorizationAttestation.ts @@ -0,0 +1,125 @@ +import { type AgentContext, JwsService, Jwt, SdJwtVcService, X509Certificate } from '@credo-ts/core' +import type { OpenId4VpResolvedAuthorizationRequest } from '@credo-ts/openid4vc' +import { z } from 'zod' +import { isRegistrationCertificate } from '../registration-certificate/verifyRegistrationCertificate' +import { getAuthorizationDisclosurePolicy } from './getAuthorizationDisclosurePolicy' + +// TODO: support multiple authorization attestation formats. +// Currently, only sd-jwt is defined +const authorizationAttestationHeaderSchema = z.object({ + alg: z.string(), + typ: z.literal('dc+sd-jwt'), + x5c: z.array(z.string()), +}) + +// TODO: +// - Should we support more hashing confirmation claim values? +const authorizationAttestationPayloadSchema = z.object({ + iss: z.string(), + sub: z.string(), + status: z.object({ + status_list: z.object({ + idx: z.number(), + uri: z.string().url(), + }), + }), + iat: z.number(), + exp: z.number().optional(), + cnf: z + .object({ + 'x5t#S256': z.string(), + }) + .optional(), +}) + +/** + * + * According to: https://bmi.usercontent.opencode.de/eudi-wallet/eidas-2.0-architekturkonzept/flows/OID4VC-with-WRP-attestations/#verifier-attestations + * + * "Each credential, that is not a Registration Certificate, is treated as an Authorization Attestation. For possible formats, see the examples section like for SD-JWT-VC." + * + */ +export const isAuthorizationAttestation = (format: string, jwt: string) => { + if (format !== 'eudi_registration_certifiate') return false + + return !isRegistrationCertificate(format, jwt) +} + +export type VerifyAuthorizationAttestationOptions = { + authorizationAttestation: string + resolvedAuthorizationRequest: OpenId4VpResolvedAuthorizationRequest + trustedCertificates?: Array + allowUntrustedSigned?: boolean +} + +export const verifyAuthorizationAttestation = async ( + agentContext: AgentContext, + { + authorizationAttestation, + resolvedAuthorizationRequest: { signedAuthorizationRequest }, + allowUntrustedSigned, + trustedCertificates, + }: VerifyAuthorizationAttestationOptions +) => { + const jwsService = agentContext.dependencyManager.resolve(JwsService) + const sdJwtService = agentContext.dependencyManager.resolve(SdJwtVcService) + + let isValidButUntrusted = false + let isValidAndTrusted = false + + const jwt = Jwt.fromSerializedJwt(authorizationAttestation) + + try { + const { isValid } = await jwsService.verifyJws(agentContext, { + jws: authorizationAttestation, + trustedCertificates, + }) + isValidAndTrusted = isValid + } catch { + if (allowUntrustedSigned) { + const { isValid } = await jwsService.verifyJws(agentContext, { + jws: authorizationAttestation, + trustedCertificates: jwt.header.x5c ?? [], + }) + isValidButUntrusted = isValid + } + } + + if (!signedAuthorizationRequest) { + throw new Error('Authorization request must be signed for the authorization attestation') + } + + if (signedAuthorizationRequest.signer.method !== 'x5c') { + throw new Error( + 'x5c is only supported for key derivation on the authorization request containing a authorization attestation' + ) + } + const accessCertificate = X509Certificate.fromEncodedCertificate(signedAuthorizationRequest.signer.x5c[0]) + + authorizationAttestationHeaderSchema.parse(jwt.header) + const payload = authorizationAttestationPayloadSchema.parse(jwt.payload) + + // Validate the identifier of the issuer. + // It should be the same that issues the Authorization Certificate as that issues this attestation + // TODO: what is the Authorization Certificate? Is it the signer of the auth request? + + // TODO: confirm that the `signingCertificate.subject` is the DN of the RP + if (payload.sub !== accessCertificate.subject) { + throw new Error('The Subject of the Authorization Attestation should equal the distinguished of the Relying Party') + } + + const { isValid, verification } = await sdJwtService.verify(agentContext, { + compactSdJwtVc: authorizationAttestation, + }) + + if (!isValid) { + throw new Error( + `Could not verify the sd-jwt authorization attestation. Verifications that failed at '${Object.values( + verification + ) + .filter(([, value]) => !value) + .map(([key]) => key) + .join(', ')}'` + ) + } +} diff --git a/src/disclosure-policy/verifyAuthorizationForOpenid4VpAuthorizationRequest.ts b/src/disclosure-policy/verifyAuthorizationForOpenid4VpAuthorizationRequest.ts new file mode 100644 index 0000000..7cc2f8d --- /dev/null +++ b/src/disclosure-policy/verifyAuthorizationForOpenid4VpAuthorizationRequest.ts @@ -0,0 +1,101 @@ +import { type AgentContext, type DcqlCredentialsForRequest, X509Certificate } from '@credo-ts/core' +import type { OpenId4VpAuthorizationRequestPayload, OpenId4VpResolvedAuthorizationRequest } from '@credo-ts/openid4vc' +import { type AllowListPolicy, validateAllowListPolicy } from './validateAllowlistPolicy' +import { + type AttributeBasedAccessControlPolicy, + validateAttributeBasedAccessControlPolicy, +} from './validateAttributeBasedAccessControlPolicy' +import { type RootOfTrustPolicy, validateRootOfTrustPolicy } from './validateRootOfTrustPolicy' +import { isAuthorizationAttestation } from './verifyAuthorizationAttestation' + +export type AuthzPolicy = AllowListPolicy | RootOfTrustPolicy | AttributeBasedAccessControlPolicy + +type VerifyAuthorizationForOpenid4VpAuthorizationRequestOptions = { + resolvedAuthorizationRequest: OpenId4VpResolvedAuthorizationRequest + matchedCredentials: DcqlCredentialsForRequest + trustedRootCertificates?: Array +} + +/** + * + * Checks all the matched credentials for additional disclosure policies as set by the issuer + * + * Make sure the disclosure policies are set manually on the metadata of the record, under the `__disclosurePolicy` key + * + */ +export const verifyAuthorizationForOpenid4VpAuthorizationRequest = async ( + agentContext: AgentContext, + options: VerifyAuthorizationForOpenid4VpAuthorizationRequestOptions +) => { + for (const [credentialId, matchedCredential] of Object.entries(options.matchedCredentials)) { + const disclosurePolicy = matchedCredential.credentialRecord.metadata.get('__disclosurePolicy') + + if (!disclosurePolicy) { + continue + } + + if ('allowList' in disclosurePolicy) { + if (!options.resolvedAuthorizationRequest.signedAuthorizationRequest) { + throw new Error( + `Allow list was used and authorization request must be signed. Discovered in credential id: ${credentialId}` + ) + } + + if (options.resolvedAuthorizationRequest.signedAuthorizationRequest.signer.method !== 'x5c') { + throw new Error( + `Allow list was used and authorization request must be signed with x5c to determine the access certificate. Discovered in credential id: ${credentialId}` + ) + } + + const relyingPartyAccessCertificate = X509Certificate.fromEncodedCertificate( + options.resolvedAuthorizationRequest.signedAuthorizationRequest.signer.x5c[0] + ) + if (!validateAllowListPolicy(disclosurePolicy as AllowListPolicy, relyingPartyAccessCertificate)) { + throw new Error( + `Allow list policy was used, but the relying party access certificate was not allowed to make the request. Discovered in credential id: ${credentialId}` + ) + } + continue + } + + if ('rootOfTrust' in disclosurePolicy) { + if (!options.trustedRootCertificates) { + throw new Error( + `rootOfTrust disclosure policy found, but no trustedRootCertificates were provided. Discovered in credential id: ${credentialId}` + ) + } + if (!validateRootOfTrustPolicy(disclosurePolicy as RootOfTrustPolicy)) { + throw new Error( + `allow list policy was used, but the rp access certificate was not allowed to make the request. Discovered in credential id: ${credentialId}` + ) + } + continue + } + + if ('attribute_based_access_control' in disclosurePolicy) { + const authorizationAttestations = getAuthorizationAttestations( + options.resolvedAuthorizationRequest.authorizationRequestPayload + ) + + if ( + !validateAttributeBasedAccessControlPolicy( + agentContext, + disclosurePolicy as AttributeBasedAccessControlPolicy, + authorizationAttestations ?? [] + ) + ) { + throw new Error( + `Attribute based access control disclosure policy was found, but it did not match the dcql query in the authorization request. Discovered for credentialId: ${credentialId}` + ) + } + continue + } + throw new Error(`Found an unsupported disclosure policy: ${disclosurePolicy}`) + } +} + +// TODO: credentialId in the verifier_attestation is ignored +const getAuthorizationAttestations = (request: OpenId4VpAuthorizationRequestPayload): Array => + request.verifier_attestations + ?.filter((va) => typeof va.data === 'string' && isAuthorizationAttestation(va.format, va.data)) + .map((va) => va.data as string) ?? [] diff --git a/src/index.ts b/src/index.ts index 263a48b..828ed0e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,5 @@ -export { verifyOpenid4VpAuthorizationRequest } from './verifyOpenid4VpAuthorizationRequest' +export { verifyRegistrationCertificateInOpenid4VpAuthorizationRequest } from './registration-certificate/verifyRegistrationCertificateInOpenid4VpAuthorizationRequest' +export { + verifyAuthorizationForOpenid4VpAuthorizationRequest, + AuthzPolicy, +} from './disclosure-policy/verifyAuthorizationForOpenid4VpAuthorizationRequest' diff --git a/src/isDcqlQueryEqualOrSubset.ts b/src/registration-certificate/isDcqlQueryEqualOrSubset.ts similarity index 100% rename from src/isDcqlQueryEqualOrSubset.ts rename to src/registration-certificate/isDcqlQueryEqualOrSubset.ts diff --git a/src/verifyRegistrationCertificate.ts b/src/registration-certificate/verifyRegistrationCertificate.ts similarity index 98% rename from src/verifyRegistrationCertificate.ts rename to src/registration-certificate/verifyRegistrationCertificate.ts index 9136270..aa741de 100644 --- a/src/verifyRegistrationCertificate.ts +++ b/src/registration-certificate/verifyRegistrationCertificate.ts @@ -86,7 +86,9 @@ const registrationCertificatePayloadSchema = z }) .passthrough() -export const isRegistrationCertificate = (agentContext: AgentContext, jwt: string) => { +export const isRegistrationCertificate = (format: string, jwt: string) => { + if (format !== 'jwt') return false + try { const { header } = Jwt.fromSerializedJwt(jwt) diff --git a/src/verifyOpenid4VpAuthorizationRequest.ts b/src/registration-certificate/verifyRegistrationCertificateInOpenid4VpAuthorizationRequest.ts similarity index 66% rename from src/verifyOpenid4VpAuthorizationRequest.ts rename to src/registration-certificate/verifyRegistrationCertificateInOpenid4VpAuthorizationRequest.ts index 5bf314e..0ef4bb2 100644 --- a/src/verifyOpenid4VpAuthorizationRequest.ts +++ b/src/registration-certificate/verifyRegistrationCertificateInOpenid4VpAuthorizationRequest.ts @@ -1,30 +1,31 @@ import type { AgentContext } from '@credo-ts/core' import type { OpenId4VpResolvedAuthorizationRequest } from '@credo-ts/openid4vc' -import { isAuthorizationAttestation, verifyAuthorizationAttestation } from './verifyAuthorizationAttestation' import { isRegistrationCertificate, verifyIfRegistrationCertificate } from './verifyRegistrationCertificate' -export type VerifyAuthorizationRequestOptions = { +type VerifyAuthorizationRequestOptions = { resolvedAuthorizationRequest: OpenId4VpResolvedAuthorizationRequest trustedCertificates?: Array allowUntrustedSigned?: boolean } -export const verifyOpenid4VpAuthorizationRequest = async ( +/** + * + * Verify the Registration certificate if it is included in the authorization request + * If it is not included, `undefined` will be returned and the caller should handle accordingly + * + */ +export const verifyRegistrationCertificateInOpenid4VpAuthorizationRequest = async ( agentContext: AgentContext, { resolvedAuthorizationRequest, trustedCertificates, allowUntrustedSigned }: VerifyAuthorizationRequestOptions ) => { let registrationCertificate: Awaited> | undefined if (!resolvedAuthorizationRequest.authorizationRequestPayload.verifier_attestations) return for (const va of resolvedAuthorizationRequest.authorizationRequestPayload.verifier_attestations) { - if (va.format !== 'jwt') { - throw new Error(`only format of 'jwt' is supported`) - } - if (typeof va.data !== 'string') { - throw new Error('Only inline JWTs are supported') + throw new Error('Only inline data supported') } - if (isRegistrationCertificate(agentContext, va.data)) { + if (isRegistrationCertificate(va.format, va.data)) { registrationCertificate = await verifyIfRegistrationCertificate(agentContext, { registrationCertificate: va.data, resolvedAuthorizationRequest, @@ -32,10 +33,6 @@ export const verifyOpenid4VpAuthorizationRequest = async ( trustedCertificates, }) } - - if (isAuthorizationAttestation(agentContext, va.data)) { - await verifyAuthorizationAttestation(agentContext, { authorizationAttestation: va.data }) - } } return registrationCertificate } diff --git a/src/verifyAuthorizationAttestation.ts b/src/verifyAuthorizationAttestation.ts deleted file mode 100644 index 7c8c0bf..0000000 --- a/src/verifyAuthorizationAttestation.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { type AgentContext, JwsService, Jwt, X509Certificate } from '@credo-ts/core' -import type { OpenId4VpResolvedAuthorizationRequest } from '@credo-ts/openid4vc' -import { z } from 'zod' -import { isRegistrationCertificate } from './verifyRegistrationCertificate' - -const authorizationAttestationHeaderSchema = z.object({}) - -const authorizationAttestationPayloadSchema = z.object({}) - -/** - * - * According to: https://bmi.usercontent.opencode.de/eudi-wallet/eidas-2.0-architekturkonzept/flows/OID4VC-with-WRP-attestations/#verifier-attestations - * - * "Each credential, that is not a Registration Certificate, is treated as an Authorization Attestation. For possible formats, see the examples section like for SD-JWT-VC." - * - */ -export const isAuthorizationAttestation = (agentContext: AgentContext, jwt: string) => { - return !isRegistrationCertificate(agentContext, jwt) -} - -export type VerifyAuthorizationAttestationOptions = { - authorizationAttestation: string - resolvedAuthorizationRequest: OpenId4VpResolvedAuthorizationRequest - trustedCertificates?: Array - allowUntrustedSigned?: boolean -} - -export const verifyAuthorizationAttestation = async ( - agentContext: AgentContext, - { - authorizationAttestation, - resolvedAuthorizationRequest: { signedAuthorizationRequest }, - allowUntrustedSigned, - trustedCertificates, - }: VerifyAuthorizationAttestationOptions -) => { - const jwsService = agentContext.dependencyManager.resolve(JwsService) - - let isValidButUntrusted = false - let isValidAndTrusted = false - - const jwt = Jwt.fromSerializedJwt(authorizationAttestation) - - try { - const { isValid } = await jwsService.verifyJws(agentContext, { - jws: authorizationAttestation, - trustedCertificates, - }) - isValidAndTrusted = isValid - } catch { - if (allowUntrustedSigned) { - const { isValid } = await jwsService.verifyJws(agentContext, { - jws: authorizationAttestation, - trustedCertificates: jwt.header.x5c ?? [], - }) - isValidButUntrusted = isValid - } - } - - if (!signedAuthorizationRequest) { - throw new Error('Authorization request must be signed for the authorization attestation') - } - - if (signedAuthorizationRequest.signer.method !== 'x5c') { - throw new Error( - 'x5c is only supported for key derivation on the authorization request containing a authorization attestation' - ) - } - - const accessCertificate = X509Certificate.fromEncodedCertificate(signedAuthorizationRequest.signer.x5c[0]) - - // TODO: zod validate header + payload -} From 193202a46212fe3f9ab603b491c55ab3e40d3521 Mon Sep 17 00:00:00 2001 From: Berend Sliedrecht Date: Mon, 16 Jun 2025 16:27:24 +0200 Subject: [PATCH 3/4] chore: resolved feedback Signed-off-by: Berend Sliedrecht --- ...lidateAttributeBasedAccessControlPolicy.ts | 69 +++++++---- .../verifyAuthorizationAttestation.ts | 34 +----- ...izationForOpenid4VpAuthorizationRequest.ts | 101 ---------------- ...oliciesForOpenid4VpAuthorizationRequest.ts | 111 ++++++++++++++++++ src/index.ts | 7 +- 5 files changed, 163 insertions(+), 159 deletions(-) delete mode 100644 src/disclosure-policy/verifyAuthorizationForOpenid4VpAuthorizationRequest.ts create mode 100644 src/disclosure-policy/verifyDisclosurePoliciesForOpenid4VpAuthorizationRequest.ts diff --git a/src/disclosure-policy/validateAttributeBasedAccessControlPolicy.ts b/src/disclosure-policy/validateAttributeBasedAccessControlPolicy.ts index 69b74b5..f5e056d 100644 --- a/src/disclosure-policy/validateAttributeBasedAccessControlPolicy.ts +++ b/src/disclosure-policy/validateAttributeBasedAccessControlPolicy.ts @@ -1,36 +1,55 @@ -import type { AgentContext, DcqlQuery } from '@credo-ts/core' +import type { AgentContext, DcqlQuery, X509Certificate } from '@credo-ts/core' import { SdJwtVcService } from '@credo-ts/core' -import { type DcqlCredential, type DcqlQuery as Query, runDcqlQuery } from 'dcql' +import { type DcqlCredential, DcqlQuery as Query } from 'dcql' +import { verifyAuthorizationAttestation } from './verifyAuthorizationAttestation' export type AttributeBasedAccessControlPolicy = { attribute_based_access_control: DcqlQuery } -export const validateAttributeBasedAccessControlPolicy = ( - agentContext: AgentContext, - attributeBasedAccessControlPolicy: AttributeBasedAccessControlPolicy, +type ValidateAttributeBasedAccessControlPolicyOptions = { + attributeBasedAccessControlPolicy: AttributeBasedAccessControlPolicy authorizationAttestations: Array + accessCertificate: X509Certificate + trustedCertificates?: Array + allowUntrustedSigned?: boolean +} + +export const validateAttributeBasedAccessControlPolicy = async ( + agentContext: AgentContext, + { + accessCertificate, + attributeBasedAccessControlPolicy, + trustedCertificates, + allowUntrustedSigned, + authorizationAttestations, + }: ValidateAttributeBasedAccessControlPolicyOptions ) => { - const credentials: DcqlCredential[] = [] - const sdJwtService = agentContext.dependencyManager.resolve(SdJwtVcService) - for (const authorizationAttestation of authorizationAttestations) { - const sdJwtCredential = sdJwtService.fromCompact(authorizationAttestation) - credentials.push({ - credential_format: 'dc+sd-jwt', - vct: sdJwtCredential.prettyClaims.vct, - claims: sdJwtCredential.prettyClaims, - }) - credentials.push({ - credential_format: 'vc+sd-jwt', - vct: sdJwtCredential.prettyClaims.vct, - claims: sdJwtCredential.prettyClaims, - }) - } + try { + const credentials: DcqlCredential[] = [] + const sdJwtService = agentContext.dependencyManager.resolve(SdJwtVcService) + for (const authorizationAttestation of authorizationAttestations) { + await verifyAuthorizationAttestation(agentContext, { + authorizationAttestation, + accessCertificate, + allowUntrustedSigned, + trustedCertificates, + }) + const sdJwtCredential = sdJwtService.fromCompact(authorizationAttestation) + credentials.push({ + credential_format: 'dc+sd-jwt', + vct: sdJwtCredential.prettyClaims.vct, + claims: sdJwtCredential.prettyClaims, + }) + } - const result = runDcqlQuery(attributeBasedAccessControlPolicy.attribute_based_access_control as Query.Output, { - credentials, - presentation: false, - }) + const queryResult = Query.query( + Query.parse(attributeBasedAccessControlPolicy.attribute_based_access_control), + credentials + ) - return result.canBeSatisfied + return queryResult.canBeSatisfied + } catch { + return false + } } diff --git a/src/disclosure-policy/verifyAuthorizationAttestation.ts b/src/disclosure-policy/verifyAuthorizationAttestation.ts index f8d779a..34d02d2 100644 --- a/src/disclosure-policy/verifyAuthorizationAttestation.ts +++ b/src/disclosure-policy/verifyAuthorizationAttestation.ts @@ -1,8 +1,6 @@ -import { type AgentContext, JwsService, Jwt, SdJwtVcService, X509Certificate } from '@credo-ts/core' -import type { OpenId4VpResolvedAuthorizationRequest } from '@credo-ts/openid4vc' +import { type AgentContext, JwsService, Jwt, SdJwtVcService, type X509Certificate } from '@credo-ts/core' import { z } from 'zod' import { isRegistrationCertificate } from '../registration-certificate/verifyRegistrationCertificate' -import { getAuthorizationDisclosurePolicy } from './getAuthorizationDisclosurePolicy' // TODO: support multiple authorization attestation formats. // Currently, only sd-jwt is defined @@ -47,7 +45,7 @@ export const isAuthorizationAttestation = (format: string, jwt: string) => { export type VerifyAuthorizationAttestationOptions = { authorizationAttestation: string - resolvedAuthorizationRequest: OpenId4VpResolvedAuthorizationRequest + accessCertificate: X509Certificate trustedCertificates?: Array allowUntrustedSigned?: boolean } @@ -56,7 +54,7 @@ export const verifyAuthorizationAttestation = async ( agentContext: AgentContext, { authorizationAttestation, - resolvedAuthorizationRequest: { signedAuthorizationRequest }, + accessCertificate, allowUntrustedSigned, trustedCertificates, }: VerifyAuthorizationAttestationOptions @@ -85,17 +83,6 @@ export const verifyAuthorizationAttestation = async ( } } - if (!signedAuthorizationRequest) { - throw new Error('Authorization request must be signed for the authorization attestation') - } - - if (signedAuthorizationRequest.signer.method !== 'x5c') { - throw new Error( - 'x5c is only supported for key derivation on the authorization request containing a authorization attestation' - ) - } - const accessCertificate = X509Certificate.fromEncodedCertificate(signedAuthorizationRequest.signer.x5c[0]) - authorizationAttestationHeaderSchema.parse(jwt.header) const payload = authorizationAttestationPayloadSchema.parse(jwt.payload) @@ -105,21 +92,12 @@ export const verifyAuthorizationAttestation = async ( // TODO: confirm that the `signingCertificate.subject` is the DN of the RP if (payload.sub !== accessCertificate.subject) { - throw new Error('The Subject of the Authorization Attestation should equal the distinguished of the Relying Party') + return false } - const { isValid, verification } = await sdJwtService.verify(agentContext, { + const { isValid } = await sdJwtService.verify(agentContext, { compactSdJwtVc: authorizationAttestation, }) - if (!isValid) { - throw new Error( - `Could not verify the sd-jwt authorization attestation. Verifications that failed at '${Object.values( - verification - ) - .filter(([, value]) => !value) - .map(([key]) => key) - .join(', ')}'` - ) - } + return isValid } diff --git a/src/disclosure-policy/verifyAuthorizationForOpenid4VpAuthorizationRequest.ts b/src/disclosure-policy/verifyAuthorizationForOpenid4VpAuthorizationRequest.ts deleted file mode 100644 index 7cc2f8d..0000000 --- a/src/disclosure-policy/verifyAuthorizationForOpenid4VpAuthorizationRequest.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { type AgentContext, type DcqlCredentialsForRequest, X509Certificate } from '@credo-ts/core' -import type { OpenId4VpAuthorizationRequestPayload, OpenId4VpResolvedAuthorizationRequest } from '@credo-ts/openid4vc' -import { type AllowListPolicy, validateAllowListPolicy } from './validateAllowlistPolicy' -import { - type AttributeBasedAccessControlPolicy, - validateAttributeBasedAccessControlPolicy, -} from './validateAttributeBasedAccessControlPolicy' -import { type RootOfTrustPolicy, validateRootOfTrustPolicy } from './validateRootOfTrustPolicy' -import { isAuthorizationAttestation } from './verifyAuthorizationAttestation' - -export type AuthzPolicy = AllowListPolicy | RootOfTrustPolicy | AttributeBasedAccessControlPolicy - -type VerifyAuthorizationForOpenid4VpAuthorizationRequestOptions = { - resolvedAuthorizationRequest: OpenId4VpResolvedAuthorizationRequest - matchedCredentials: DcqlCredentialsForRequest - trustedRootCertificates?: Array -} - -/** - * - * Checks all the matched credentials for additional disclosure policies as set by the issuer - * - * Make sure the disclosure policies are set manually on the metadata of the record, under the `__disclosurePolicy` key - * - */ -export const verifyAuthorizationForOpenid4VpAuthorizationRequest = async ( - agentContext: AgentContext, - options: VerifyAuthorizationForOpenid4VpAuthorizationRequestOptions -) => { - for (const [credentialId, matchedCredential] of Object.entries(options.matchedCredentials)) { - const disclosurePolicy = matchedCredential.credentialRecord.metadata.get('__disclosurePolicy') - - if (!disclosurePolicy) { - continue - } - - if ('allowList' in disclosurePolicy) { - if (!options.resolvedAuthorizationRequest.signedAuthorizationRequest) { - throw new Error( - `Allow list was used and authorization request must be signed. Discovered in credential id: ${credentialId}` - ) - } - - if (options.resolvedAuthorizationRequest.signedAuthorizationRequest.signer.method !== 'x5c') { - throw new Error( - `Allow list was used and authorization request must be signed with x5c to determine the access certificate. Discovered in credential id: ${credentialId}` - ) - } - - const relyingPartyAccessCertificate = X509Certificate.fromEncodedCertificate( - options.resolvedAuthorizationRequest.signedAuthorizationRequest.signer.x5c[0] - ) - if (!validateAllowListPolicy(disclosurePolicy as AllowListPolicy, relyingPartyAccessCertificate)) { - throw new Error( - `Allow list policy was used, but the relying party access certificate was not allowed to make the request. Discovered in credential id: ${credentialId}` - ) - } - continue - } - - if ('rootOfTrust' in disclosurePolicy) { - if (!options.trustedRootCertificates) { - throw new Error( - `rootOfTrust disclosure policy found, but no trustedRootCertificates were provided. Discovered in credential id: ${credentialId}` - ) - } - if (!validateRootOfTrustPolicy(disclosurePolicy as RootOfTrustPolicy)) { - throw new Error( - `allow list policy was used, but the rp access certificate was not allowed to make the request. Discovered in credential id: ${credentialId}` - ) - } - continue - } - - if ('attribute_based_access_control' in disclosurePolicy) { - const authorizationAttestations = getAuthorizationAttestations( - options.resolvedAuthorizationRequest.authorizationRequestPayload - ) - - if ( - !validateAttributeBasedAccessControlPolicy( - agentContext, - disclosurePolicy as AttributeBasedAccessControlPolicy, - authorizationAttestations ?? [] - ) - ) { - throw new Error( - `Attribute based access control disclosure policy was found, but it did not match the dcql query in the authorization request. Discovered for credentialId: ${credentialId}` - ) - } - continue - } - throw new Error(`Found an unsupported disclosure policy: ${disclosurePolicy}`) - } -} - -// TODO: credentialId in the verifier_attestation is ignored -const getAuthorizationAttestations = (request: OpenId4VpAuthorizationRequestPayload): Array => - request.verifier_attestations - ?.filter((va) => typeof va.data === 'string' && isAuthorizationAttestation(va.format, va.data)) - .map((va) => va.data as string) ?? [] diff --git a/src/disclosure-policy/verifyDisclosurePoliciesForOpenid4VpAuthorizationRequest.ts b/src/disclosure-policy/verifyDisclosurePoliciesForOpenid4VpAuthorizationRequest.ts new file mode 100644 index 0000000..64f1c91 --- /dev/null +++ b/src/disclosure-policy/verifyDisclosurePoliciesForOpenid4VpAuthorizationRequest.ts @@ -0,0 +1,111 @@ +import { type AgentContext, type DcqlCredentialsForRequest, X509Certificate } from '@credo-ts/core' +import type { OpenId4VpAuthorizationRequestPayload, OpenId4VpResolvedAuthorizationRequest } from '@credo-ts/openid4vc' +import { type AllowListPolicy, validateAllowListPolicy } from './validateAllowlistPolicy' +import { + type AttributeBasedAccessControlPolicy, + validateAttributeBasedAccessControlPolicy, +} from './validateAttributeBasedAccessControlPolicy' +import { type RootOfTrustPolicy, validateRootOfTrustPolicy } from './validateRootOfTrustPolicy' +import { isAuthorizationAttestation } from './verifyAuthorizationAttestation' + +export type VerifyDisclosurePoliciesForOpenId4VpAuthorizationRequestReturnContext = { + isValid: boolean + isSignedWithX509: boolean + disclosurePolicies: { + [credentialId: string]: { + isAllowListPolicyValid?: boolean + isRootOfTrustPolicyValid?: boolean + isAttributeBasedAccessControlValid?: boolean + } + } +} + +export type DisclosurePolicy = AllowListPolicy | RootOfTrustPolicy | AttributeBasedAccessControlPolicy + +export type VerifyDisclosurePoliciesForOpenId4VpAuthorizationRequestOptions = { + resolvedAuthorizationRequest: OpenId4VpResolvedAuthorizationRequest + matchedCredentials: DcqlCredentialsForRequest + trustedCertificates?: Array + allowUntrustedSigned?: boolean +} + +/** + * + * Checks all the matched credentials for additional disclosure policies as set by the issuer + * + * Make sure the disclosure policies are set manually on the metadata of the record, under the `eudi::disclosurePolicy` key + * + */ +export const verifyAuthorizationForOpenid4VpAuthorizationRequest = async ( + agentContext: AgentContext, + { + matchedCredentials, + resolvedAuthorizationRequest, + trustedCertificates, + allowUntrustedSigned, + }: VerifyDisclosurePoliciesForOpenId4VpAuthorizationRequestOptions +): Promise => { + const err: VerifyDisclosurePoliciesForOpenId4VpAuthorizationRequestReturnContext = { + isValid: true, + isSignedWithX509: resolvedAuthorizationRequest.signedAuthorizationRequest?.signer.method === 'x5c', + disclosurePolicies: {}, + } + + if (resolvedAuthorizationRequest.signedAuthorizationRequest?.signer.method !== 'x5c') { + return { isValid: false, isSignedWithX509: false, disclosurePolicies: {} } + } + + const relyingPartyAccessCertificate = X509Certificate.fromEncodedCertificate( + resolvedAuthorizationRequest.signedAuthorizationRequest.signer.x5c[0] + ) + + for (const [credentialId, matchedCredential] of Object.entries(matchedCredentials)) { + const disclosurePolicy = matchedCredential.credentialRecord.metadata.get('eudi::disclosurePolicy') + + if (!disclosurePolicy) { + continue + } + + err.disclosurePolicies[credentialId] = { + isAllowListPolicyValid: + 'allowList' in disclosurePolicy + ? validateAllowListPolicy(disclosurePolicy as AllowListPolicy, relyingPartyAccessCertificate) + : undefined, + isRootOfTrustPolicyValid: + 'rootOfTrust' in disclosurePolicy + ? validateRootOfTrustPolicy(disclosurePolicy as RootOfTrustPolicy) + : undefined, + isAttributeBasedAccessControlValid: + 'attribute_based_access_control' in disclosurePolicy + ? await validateAttributeBasedAccessControlPolicy(agentContext, { + attributeBasedAccessControlPolicy: disclosurePolicy as AttributeBasedAccessControlPolicy, + accessCertificate: relyingPartyAccessCertificate, + trustedCertificates, + allowUntrustedSigned, + authorizationAttestations: getAuthorizationAttestations( + resolvedAuthorizationRequest.authorizationRequestPayload + ), + }) + : undefined, + } + } + + // If there is any error at all, set `isValid` to false + err.isValid = Object.values(err) + .map((value) => { + if (typeof value === 'boolean') return value + if (typeof value === 'object') + return Object.values(value).some( + (v) => v.isAllowListPolicyValid || v.isRootOfTrustPolicyValid || v.isAttributeBasedAccessControlValid + ) + }) + .every((value) => value !== false) + + return err +} + +// TODO: credentialId in the verifier_attestation is ignored +const getAuthorizationAttestations = (request: OpenId4VpAuthorizationRequestPayload): Array => + request.verifier_attestations + ?.filter((va) => typeof va.data === 'string' && isAuthorizationAttestation(va.format, va.data)) + .map((va) => va.data as string) ?? [] diff --git a/src/index.ts b/src/index.ts index 828ed0e..b4a3ae0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,2 @@ -export { verifyRegistrationCertificateInOpenid4VpAuthorizationRequest } from './registration-certificate/verifyRegistrationCertificateInOpenid4VpAuthorizationRequest' -export { - verifyAuthorizationForOpenid4VpAuthorizationRequest, - AuthzPolicy, -} from './disclosure-policy/verifyAuthorizationForOpenid4VpAuthorizationRequest' +export * from './registration-certificate/verifyRegistrationCertificateInOpenid4VpAuthorizationRequest' +export * from './disclosure-policy/verifyDisclosurePoliciesForOpenid4VpAuthorizationRequest' From faf6ee14e1704155a3733b5308e54ce2f0fe60e8 Mon Sep 17 00:00:00 2001 From: Berend Sliedrecht Date: Tue, 17 Jun 2025 14:56:23 +0200 Subject: [PATCH 4/4] feat: add error context to registration certificate validation Signed-off-by: Berend Sliedrecht --- .../verifyRegistrationCertificate.ts | 111 +++++++++++------- ...tificateInOpenid4VpAuthorizationRequest.ts | 8 +- ...erifyOpenid4VpAuthorizationRequest.test.ts | 29 +++-- 3 files changed, 84 insertions(+), 64 deletions(-) diff --git a/src/registration-certificate/verifyRegistrationCertificate.ts b/src/registration-certificate/verifyRegistrationCertificate.ts index aa741de..138fbd7 100644 --- a/src/registration-certificate/verifyRegistrationCertificate.ts +++ b/src/registration-certificate/verifyRegistrationCertificate.ts @@ -1,4 +1,4 @@ -import { type AgentContext, type DcqlQuery, JwsService, Jwt, X509Certificate } from '@credo-ts/core' +import { type AgentContext, CredoError, type DcqlQuery, JwsService, Jwt, X509Certificate } from '@credo-ts/core' import type { OpenId4VpResolvedAuthorizationRequest } from '@credo-ts/openid4vc' import { z } from 'zod' import { isDcqlQueryEqualOrSubset } from './isDcqlQueryEqualOrSubset' @@ -90,17 +90,25 @@ export const isRegistrationCertificate = (format: string, jwt: string) => { if (format !== 'jwt') return false try { - const { header } = Jwt.fromSerializedJwt(jwt) - - if (header.typ !== 'rc-rp+jwt') { - return true - } - return false + const { + header: { typ }, + } = Jwt.fromSerializedJwt(jwt) + return typ === 'rc-rp+jwt' } catch { return false } } +export type VerifyIfRegistrationCertificateReturnContext = { + isValid: boolean + isSignedWithX509?: boolean + isAccessCertificateSubjectEqualToRegistrationCertificate?: boolean + isTimestampValid?: boolean + isJwsValid?: boolean + isRegistrationCertificateQueryEqualOrSubsetOfAuthorizationRequestQuery?: boolean + isDcqlUsed?: boolean +} + /** * * @@ -116,36 +124,59 @@ export const verifyIfRegistrationCertificate = async ( allowUntrustedSigned, resolvedAuthorizationRequest: { signedAuthorizationRequest, authorizationRequestPayload, dcql }, }: VerifyRegistrationCertificateOptions -) => { - const jwsService = agentContext.dependencyManager.resolve(JwsService) +): Promise => { + if ((!trustedCertificates || trustedCertificates.length === 0) && !allowUntrustedSigned) { + throw new Error('Either provide trusted certificates, or allow for untrusted signers') + } - let isValidButUntrusted = false - let isValidAndTrusted = false + const returnContext: VerifyIfRegistrationCertificateReturnContext = { + isValid: true, + } + + if ( + !dcql || + authorizationRequestPayload.presentation_definition || + authorizationRequestPayload.presentation_definition_uri + ) { + return { + isValid: false, + isDcqlUsed: false, + } + } + + if (signedAuthorizationRequest?.signer.method !== 'x5c') { + return { + isValid: false, + isSignedWithX509: false, + } + } + + const jwsService = agentContext.dependencyManager.resolve(JwsService) const jwt = Jwt.fromSerializedJwt(registrationCertificate) - try { + const verifySignature = async (certs: Array) => { const { isValid } = await jwsService.verifyJws(agentContext, { jws: registrationCertificate, - trustedCertificates, + trustedCertificates: certs, }) - isValidAndTrusted = isValid - } catch { - if (allowUntrustedSigned) { - const { isValid } = await jwsService.verifyJws(agentContext, { - jws: registrationCertificate, - trustedCertificates: jwt.header.x5c ?? [], - }) - isValidButUntrusted = isValid - } + return isValid } - if (!signedAuthorizationRequest) { - throw new Error('Request must be signed for the registration certificate') - } - - if (signedAuthorizationRequest.signer.method !== 'x5c') { - throw new Error('x5c is only supported for registration certificate') + try { + let isValid = true + if (allowUntrustedSigned) { + isValid = await verifySignature([...(trustedCertificates ?? []), ...(jwt.header.x5c ?? [])]) + } else { + isValid = await verifySignature(trustedCertificates ?? []) + } + if (!isValid) { + returnContext.isValid = false + returnContext.isJwsValid = false + } + } catch { + returnContext.isValid = false + returnContext.isJwsValid = false } registrationCertificateHeaderSchema.parse(jwt.header) @@ -155,32 +186,22 @@ export const verifyIfRegistrationCertificate = async ( const rpCert = X509Certificate.fromEncodedCertificate(rpCertEncoded) if (rpCert.subject !== parsedPayload.sub) { - throw new Error( - `Subject in the certificate of the auth request: '${rpCert.subject}' is not equal to the subject of the registration certificate: '${parsedPayload.sub}'` - ) + returnContext.isAccessCertificateSubjectEqualToRegistrationCertificate = false + returnContext.isValid = false } if (parsedPayload.iat && new Date().getTime() / 1000 <= parsedPayload.iat) { - throw new Error('Issued at timestamp of the registration certificate is in the future') + returnContext.isTimestampValid = false + returnContext.isValid = false } // TODO: check the status of the registration certificate - if (!dcql) { - throw new Error('DCQL must be used when working registration certificates') - } - - if (authorizationRequestPayload.presentation_definition || authorizationRequestPayload.presentation_definition_uri) { - throw new Error('Presentation Exchange is not supported for the registration certificate') - } - const isValidDcqlQuery = isDcqlQueryEqualOrSubset(dcql.queryResult, parsedPayload as unknown as DcqlQuery) - if (!isValidDcqlQuery) { - throw new Error( - 'DCQL query in the authorization request is not equal or a valid subset of the DCQl query provided in the registration certificate' - ) + returnContext.isValid = false + returnContext.isRegistrationCertificateQueryEqualOrSubsetOfAuthorizationRequestQuery = false } - return { isValidButUntrusted, isValidAndTrusted, x509RegistrationCertificate: rpCert } + return returnContext } diff --git a/src/registration-certificate/verifyRegistrationCertificateInOpenid4VpAuthorizationRequest.ts b/src/registration-certificate/verifyRegistrationCertificateInOpenid4VpAuthorizationRequest.ts index 0ef4bb2..2f404f3 100644 --- a/src/registration-certificate/verifyRegistrationCertificateInOpenid4VpAuthorizationRequest.ts +++ b/src/registration-certificate/verifyRegistrationCertificateInOpenid4VpAuthorizationRequest.ts @@ -18,15 +18,15 @@ export const verifyRegistrationCertificateInOpenid4VpAuthorizationRequest = asyn agentContext: AgentContext, { resolvedAuthorizationRequest, trustedCertificates, allowUntrustedSigned }: VerifyAuthorizationRequestOptions ) => { - let registrationCertificate: Awaited> | undefined + let registrationCertificateResult: Awaited> | undefined if (!resolvedAuthorizationRequest.authorizationRequestPayload.verifier_attestations) return for (const va of resolvedAuthorizationRequest.authorizationRequestPayload.verifier_attestations) { if (typeof va.data !== 'string') { - throw new Error('Only inline data supported') + throw new Error('Authorization Attestations of string are currently only supported') } if (isRegistrationCertificate(va.format, va.data)) { - registrationCertificate = await verifyIfRegistrationCertificate(agentContext, { + registrationCertificateResult = await verifyIfRegistrationCertificate(agentContext, { registrationCertificate: va.data, resolvedAuthorizationRequest, allowUntrustedSigned, @@ -34,5 +34,5 @@ export const verifyRegistrationCertificateInOpenid4VpAuthorizationRequest = asyn }) } } - return registrationCertificate + return registrationCertificateResult } diff --git a/tests/verifyOpenid4VpAuthorizationRequest.test.ts b/tests/verifyOpenid4VpAuthorizationRequest.test.ts index 843837e..c720a69 100644 --- a/tests/verifyOpenid4VpAuthorizationRequest.test.ts +++ b/tests/verifyOpenid4VpAuthorizationRequest.test.ts @@ -1,11 +1,11 @@ -import { doesNotReject, equal, ok, rejects } from 'node:assert' +import { equal, ok, rejects, strictEqual } from 'node:assert' import { after, before, beforeEach, suite, test } from 'node:test' import { AskarModule } from '@credo-ts/askar' import { Agent } from '@credo-ts/core' import { agentDependencies } from '@credo-ts/node' import { OpenId4VcHolderModule } from '@credo-ts/openid4vc' import { askar } from '@openwallet-foundation/askar-nodejs' -import { verifyOpenid4VpAuthorizationRequest } from '../src' +import { verifyRegistrationCertificateInOpenid4VpAuthorizationRequest } from '../src' const trustedCertificates = [ `-----BEGIN CERTIFICATE----- @@ -56,13 +56,12 @@ suite('verify openid4vp authorization request', () => { trustedCertificates, }) - const result = await verifyOpenid4VpAuthorizationRequest(agent.context, { + const result = await verifyRegistrationCertificateInOpenid4VpAuthorizationRequest(agent.context, { resolvedAuthorizationRequest: request, trustedCertificates, }) - equal(result?.[0].isValidAndTrusted, true) - equal(result?.[0].isValidButUntrusted, false) + equal(result?.isValid, true) }) test('Successfully verify: draft-24, valid request, dcql, allow all certificates', async () => { @@ -73,13 +72,12 @@ suite('verify openid4vp authorization request', () => { trustedCertificates, }) - const result = await verifyOpenid4VpAuthorizationRequest(agent.context, { + const result = await verifyRegistrationCertificateInOpenid4VpAuthorizationRequest(agent.context, { resolvedAuthorizationRequest: request, allowUntrustedSigned: true, }) - equal(result?.[0].isValidAndTrusted, false) - equal(result?.[0].isValidButUntrusted, true) + equal(result?.isValid, true) }) test('Fail verify: draft-24, valid request, pex', async () => { @@ -91,7 +89,7 @@ suite('verify openid4vp authorization request', () => { }) await rejects( - verifyOpenid4VpAuthorizationRequest(agent.context, { + verifyRegistrationCertificateInOpenid4VpAuthorizationRequest(agent.context, { resolvedAuthorizationRequest: request, trustedCertificates, }) @@ -106,12 +104,13 @@ suite('verify openid4vp authorization request', () => { trustedCertificates, }) - await rejects( - verifyOpenid4VpAuthorizationRequest(agent.context, { - resolvedAuthorizationRequest: request, - trustedCertificates, - }) - ) + const result = await verifyRegistrationCertificateInOpenid4VpAuthorizationRequest(agent.context, { + resolvedAuthorizationRequest: request, + trustedCertificates, + }) + + strictEqual(result?.isValid, false) + equal(result?.isRegistrationCertificateQueryEqualOrSubsetOfAuthorizationRequestQuery, false) }) }) })