From 310e1a64c2af171caec8a7783cc558cb4ce51b8d Mon Sep 17 00:00:00 2001 From: Tom Lanser Date: Wed, 6 Nov 2024 20:49:29 +0100 Subject: [PATCH] feat: Littlebit of a cleanup for the verifier Signed-off-by: Tom Lanser --- packages/openid4vc/package.json | 2 +- .../openid4vc-holder/OpenId4VcHolderApi.ts | 12 +++- .../OpenId4vcSiopHolderService.ts | 17 ++++-- .../OpenId4vcSiopHolderServiceOptions.ts | 20 +++++++ .../OpenId4VcSiopVerifierService.ts | 1 + packages/openid4vc/src/shared/utils.ts | 35 +++++++---- .../tests/openid4vc-federation.e2e.test.ts | 18 ++++-- pnpm-lock.yaml | 58 +++++++++++-------- 8 files changed, 114 insertions(+), 49 deletions(-) diff --git a/packages/openid4vc/package.json b/packages/openid4vc/package.json index 0cb5858059..dd52bd98b7 100644 --- a/packages/openid4vc/package.json +++ b/packages/openid4vc/package.json @@ -33,7 +33,7 @@ "@sphereon/oid4vci-common": "0.16.1-next.168", "@sphereon/oid4vci-issuer": "0.16.1-next.168", "@sphereon/ssi-types": "0.29.1-unstable.121", - "@openid-federation/core": "0.1.1-alpha.5", + "@openid-federation/core": "0.1.1-alpha.6", "class-transformer": "^0.5.1", "rxjs": "^7.8.0" }, diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts index 1a9dd4ecd8..754f029238 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts @@ -8,7 +8,10 @@ import type { OpenId4VciSendNotificationOptions, OpenId4VciRequestTokenResponse, } from './OpenId4VciHolderServiceOptions' -import type { OpenId4VcSiopAcceptAuthorizationRequestOptions } from './OpenId4vcSiopHolderServiceOptions' +import type { + OpenId4VcSiopAcceptAuthorizationRequestOptions, + OpenId4VcSiopResolveAuthorizationRequestOptions, +} from './OpenId4vcSiopHolderServiceOptions' import { injectable, AgentContext } from '@credo-ts/core' @@ -40,8 +43,11 @@ export class OpenId4VcHolderApi { * @param requestJwtOrUri JWT or an SIOPv2 request URI * @returns the resolved and verified authentication request. */ - public async resolveSiopAuthorizationRequest(requestJwtOrUri: string) { - return this.openId4VcSiopHolderService.resolveAuthorizationRequest(this.agentContext, requestJwtOrUri) + public async resolveSiopAuthorizationRequest( + requestJwtOrUri: string, + options: OpenId4VcSiopResolveAuthorizationRequestOptions = {} + ) { + return this.openId4VcSiopHolderService.resolveAuthorizationRequest(this.agentContext, requestJwtOrUri, options) } /** diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts index ce98e14ddc..c772e7db1b 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts @@ -1,5 +1,7 @@ import type { OpenId4VcSiopAcceptAuthorizationRequestOptions, + OpenId4VcSiopGetOpenIdProviderOptions, + OpenId4VcSiopResolveAuthorizationRequestOptions, OpenId4VcSiopResolvedAuthorizationRequest, } from './OpenId4vcSiopHolderServiceOptions' import type { OpenId4VcJwtIssuer } from '../shared' @@ -38,9 +40,12 @@ export class OpenId4VcSiopHolderService { public async resolveAuthorizationRequest( agentContext: AgentContext, - requestJwtOrUri: string + requestJwtOrUri: string, + options: OpenId4VcSiopResolveAuthorizationRequestOptions = {} ): Promise { - const openidProvider = await this.getOpenIdProvider(agentContext) + const openidProvider = await this.getOpenIdProvider(agentContext, { + federation: options.federation, + }) // parsing happens automatically in verifyAuthorizationRequest const verifiedAuthorizationRequest = await openidProvider.verifyAuthorizationRequest(requestJwtOrUri) @@ -226,7 +231,7 @@ export class OpenId4VcSiopHolderService { } } - private async getOpenIdProvider(agentContext: AgentContext) { + private async getOpenIdProvider(agentContext: AgentContext, options: OpenId4VcSiopGetOpenIdProviderOptions = {}) { const builder = OP.builder() .withExpiresIn(6000) .withIssuer(ResponseIss.SELF_ISSUED_V2) @@ -237,7 +242,11 @@ export class OpenId4VcSiopHolderService { SupportedVersion.SIOPv2_D12_OID4VP_D20, ]) .withCreateJwtCallback(getCreateJwtCallback(agentContext)) - .withVerifyJwtCallback(getVerifyJwtCallback(agentContext)) + .withVerifyJwtCallback( + getVerifyJwtCallback(agentContext, { + federation: options.federation, + }) + ) .withHasher(Hasher.hash) const openidProvider = builder.build() diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderServiceOptions.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderServiceOptions.ts index c59a9dd53f..40a5d48d69 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderServiceOptions.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderServiceOptions.ts @@ -48,6 +48,17 @@ export interface OpenId4VcSiopAcceptAuthorizationRequestOptions { * The verified authorization request. */ authorizationRequest: OpenId4VcSiopVerifiedAuthorizationRequest + + // TODO: Not sure if this also needs the federation because the validation of the authorization is already done with the ResolveAuthorizationRequest +} + +export interface OpenId4VcSiopResolveAuthorizationRequestOptions { + federation?: { + /** + * The entity IDs of the trusted issuers. + */ + trustedEntityIds?: string[] + } } // FIXME: rethink properties @@ -56,3 +67,12 @@ export interface OpenId4VcSiopAuthorizationResponseSubmission { status: number submittedResponse: OpenId4VcSiopAuthorizationResponsePayload } + +export interface OpenId4VcSiopGetOpenIdProviderOptions { + federation?: { + /** + * The entity IDs of the trusted issuers. + */ + trustedEntityIds?: string[] + } +} diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts index 0350666e72..26c3a02724 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts @@ -140,6 +140,7 @@ export class OpenId4VcSiopVerifierService { clientId = jwtIssuer.didUrl.split('#')[0] clientIdScheme = 'did' } else if (jwtIssuer.method === 'custom') { + // TODO: Currently used as openid federation, but the jwtIssuer should also be openid-federation if (!jwtIssuer.options) throw new CredoError(`Custom jwtIssuer must have options defined.`) if (!jwtIssuer.options.clientId) throw new CredoError(`Custom jwtIssuer must have clientId defined.`) if (typeof jwtIssuer.options.clientId !== 'string') diff --git a/packages/openid4vc/src/shared/utils.ts b/packages/openid4vc/src/shared/utils.ts index efdd3dfde3..1ccc968a54 100644 --- a/packages/openid4vc/src/shared/utils.ts +++ b/packages/openid4vc/src/shared/utils.ts @@ -1,7 +1,7 @@ import type { OpenId4VcIssuerX5c, OpenId4VcJwtIssuer } from './models' import type { AgentContext, JwaSignatureAlgorithm, JwkJson, Key } from '@credo-ts/core' import type { JwtIssuerWithContext as VpJwtIssuerWithContext, VerifyJwtCallback } from '@sphereon/did-auth-siop' -import type { DPoPJwtIssuerWithContext, CreateJwtCallback, JwtIssuer, JwtIssuerBase } from '@sphereon/oid4vc-common' +import type { DPoPJwtIssuerWithContext, CreateJwtCallback, JwtIssuer } from '@sphereon/oid4vc-common' import type { CredentialOfferPayloadV1_0_11, CredentialOfferPayloadV1_0_13 } from '@sphereon/oid4vci-common' import { @@ -18,7 +18,7 @@ import { getJwkFromKey, getKeyFromVerificationMethod, } from '@credo-ts/core' -import { fetchEntityConfiguration, fetchEntityConfigurationChains } from '@openid-federation/core' +import { fetchEntityConfiguration, resolveTrustChains } from '@openid-federation/core' /** * Returns the JWA Signature Algorithms that are supported by the wallet. @@ -52,7 +52,9 @@ async function getKeyFromDid(agentContext: AgentContext, didUrl: string) { } type VerifyJwtCallbackOptions = { - trustedEntityIds?: string[] + federation?: { + trustedEntityIds?: string[] + } } export function getVerifyJwtCallback( @@ -61,23 +63,28 @@ export function getVerifyJwtCallback( ): VerifyJwtCallback { return async (jwtVerifier, jwt) => { const jwsService = agentContext.dependencyManager.resolve(JwsService) + if (jwtVerifier.method === 'did') { const key = await getKeyFromDid(agentContext, jwtVerifier.didUrl) const jwk = getJwkFromKey(key) const res = await jwsService.verifyJws(agentContext, { jws: jwt.raw, jwkResolver: () => jwk }) return res.isValid - } else if (jwtVerifier.method === 'x5c' || jwtVerifier.method === 'jwk') { + } + + if (jwtVerifier.method === 'x5c' || jwtVerifier.method === 'jwk') { const res = await jwsService.verifyJws(agentContext, { jws: jwt.raw }) return res.isValid - } else if (jwtVerifier.method === 'openid-federation') { + } + + if (jwtVerifier.method === 'openid-federation') { const { entityId } = jwtVerifier - const trustedEntityIds = options.trustedEntityIds ?? [entityId] // TODO: Just for testing + const trustedEntityIds = options.federation?.trustedEntityIds if (!trustedEntityIds) throw new CredoError('No trusted entity ids provided but is required for the openid-federation method.') - const entityConfigurationChains = await fetchEntityConfigurationChains({ - leafEntityId: entityId, + const validTrustChains = await resolveTrustChains({ + entityId, trustAnchorEntityIds: trustedEntityIds, verifyJwtCallback: async ({ data, signature, jwk }) => { const jws = `${TypedArrayEncoder.toUtf8String(data)}.${TypedArrayEncoder.toBase64URL(signature)}` @@ -86,17 +93,20 @@ export function getVerifyJwtCallback( jws, jwkResolver: () => getJwkFromJson(jwk), }) + return res.isValid }, }) // TODO: There is no check yet for the policies + // TODO: When this function results in a `false` it gives a really misleading error message: 'Error verifying the DID Auth Token signature.' + // TODO: I think this is correct but not sure? - return entityConfigurationChains.length > 0 - } else { - throw new Error(`Unsupported jwt verifier method: '${jwtVerifier.method}'`) + return validTrustChains.length > 0 } + + throw new Error(`Unsupported jwt verifier method: '${jwtVerifier.method}'`) } } @@ -150,7 +160,7 @@ export function getCreateJwtCallback( if (!options.clientId) throw new CredoError(`Custom jwtIssuer must have clientId defined.`) if (typeof options.clientId !== 'string') throw new CredoError(`Custom jwtIssuer's clientId must be a string.`) - const clientId = options.clientId + const { clientId } = options const entityConfiguration = await fetchEntityConfiguration({ entityId: clientId as string, @@ -164,6 +174,7 @@ export function getCreateJwtCallback( // TODO: Not 100% sure what key to pick here I think the one that matches the kid in the jwt header of the entity configuration or we should pass a alg and pick a jwk based on that? const jwk = getJwkFromJson(entityConfiguration.jwks.keys[0]) + // TODO: This gives a weird error when the private key is not available in the wallet const jws = await jwsService.createJwsCompact(agentContext, { protectedHeaderOptions: { ...jwt.header, jwk, alg: jwk.supportedSignatureAlgorithms[0] }, payload: JwtPayload.fromJson(jwt.payload), diff --git a/packages/openid4vc/tests/openid4vc-federation.e2e.test.ts b/packages/openid4vc/tests/openid4vc-federation.e2e.test.ts index a0d0c9ad2b..7eb667a143 100644 --- a/packages/openid4vc/tests/openid4vc-federation.e2e.test.ts +++ b/packages/openid4vc/tests/openid4vc-federation.e2e.test.ts @@ -50,8 +50,6 @@ describe('OpenId4Vc', () => { tenants: TenantsModule<{ openId4VcIssuer: OpenId4VcIssuerModule }> x509: X509Module }> - let issuer1: TenantType - let issuer2: TenantType let holder: AgentType<{ openId4VcHolder: OpenId4VcHolderModule @@ -143,8 +141,6 @@ describe('OpenId4Vc', () => { }, '96213c3d7fc8d4d6754c7a0fd969598g' )) as unknown as typeof issuer - issuer1 = await createTenantForAgent(issuer.agent, 'iTenant1') - issuer2 = await createTenantForAgent(issuer.agent, 'iTenant2') holder = (await createAgentFromModules( 'holder', @@ -276,7 +272,12 @@ describe('OpenId4Vc', () => { await verifierTenant2.endSession() const resolvedProofRequest1 = await holderTenant.modules.openId4VcHolder.resolveSiopAuthorizationRequest( - authorizationRequestUri1 + authorizationRequestUri1, + { + federation: { + trustedEntityIds: [`http://localhost:1234/oid4vp/${openIdVerifierTenant1.verifierId}`], + }, + } ) expect(resolvedProofRequest1.presentationExchange?.credentialsForRequest).toMatchObject({ @@ -302,7 +303,12 @@ describe('OpenId4Vc', () => { }) const resolvedProofRequest2 = await holderTenant.modules.openId4VcHolder.resolveSiopAuthorizationRequest( - authorizationRequestUri2 + authorizationRequestUri2, + { + federation: { + trustedEntityIds: [`http://localhost:1234/oid4vp/${openIdVerifierTenant2.verifierId}`], + }, + } ) expect(resolvedProofRequest2.presentationExchange?.credentialsForRequest).toMatchObject({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3f1873c8fd..6032fe7aa5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -455,7 +455,7 @@ importers: version: 0.7.2 '@sphereon/pex': specifier: 5.0.0-unstable.2 - version: 5.0.0-unstable.2 + version: 5.0.0-unstable.2(ts-node@10.9.2(@swc/core@1.7.40)(@types/node@18.18.8)(typescript@5.5.4)) '@sphereon/pex-models': specifier: ^2.3.1 version: 2.3.1 @@ -693,8 +693,8 @@ importers: specifier: workspace:* version: link:../core '@openid-federation/core': - specifier: 0.1.1-alpha.5 - version: 0.1.1-alpha.5 + specifier: 0.1.1-alpha.6 + version: 0.1.1-alpha.6 '@sphereon/did-auth-siop': specifier: 0.16.1-next.168 version: 0.16.1-next.168(ts-node@10.9.2(@swc/core@1.7.40)(@types/node@18.18.8)(typescript@5.5.4))(typescript@5.5.4) @@ -2260,8 +2260,8 @@ packages: resolution: {integrity: sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - '@openid-federation/core@0.1.1-alpha.5': - resolution: {integrity: sha512-DDrFtsrIpvw7JScIEm8KJU7H19lkgQ3AKweJld0Os3AQyOPlb0EvFaLdhS1EMmiWduGuxq3nHnWrWrod0MwyJA==} + '@openid-federation/core@0.1.1-alpha.6': + resolution: {integrity: sha512-ipQtZYtFMUr2BvUmOxlQNVF7eILEq8isoO7rDYwIj4xafifdPAMxznzDxqlu3sHqbOO49PRDRjo9ESsHUfJLfg==} '@peculiar/asn1-cms@2.3.13': resolution: {integrity: sha512-joqu8A7KR2G85oLPq+vB+NFr2ro7Ls4ol13Zcse/giPSzUNN0n2k3v8kMpf6QdGUhI13e5SzQYN8AKP8sJ8v4w==} @@ -2507,6 +2507,7 @@ packages: '@sphereon/kmp-mdl-mdoc@0.2.0-SNAPSHOT.22': resolution: {integrity: sha512-uAZZExVy+ug9JLircejWa5eLtAZ7bnBP6xb7DO2+86LRsHNLh2k2jMWJYxp+iWtGHTsh6RYsZl14ScQLvjiQ/A==} + bundledDependencies: [] '@sphereon/oid4vc-common@0.16.1-next.168': resolution: {integrity: sha512-QKLna4lY3V/0vLguaYa1Qc5L6AErpQ7aD+ITGW8hiVtqU9m/2Y/+wbQqwN8Nk0LnLWDDkBli+KGeZNDJO8g9sQ==} @@ -2588,12 +2589,12 @@ packages: '@sphereon/ssi-types@0.29.1-unstable.161': resolution: {integrity: sha512-ifMADjk6k0f97/isK/4Qw/PX6n4k+qS5k6mmmH47MTD3KMDddVghoXycsvNw7wObJdLUalHBX630ghr+u21oMg==} - '@sphereon/ssi-types@0.29.1-unstable.208': - resolution: {integrity: sha512-3YAFzy//BojsYN+RYoEjndWP3w5a8a3qRZi5dS0Gh6s4yMCiykqTJM1agJVeoaLce8JxFFaCWSpkzwbmJYGTaQ==} - '@sphereon/ssi-types@0.30.1': resolution: {integrity: sha512-vbYaxQXb71sOPwDj7TRDlUGfIHKVVs8PiHfImPBgSBshrD7VpEHOrB+EwwavMm5MAQvWK/yblGmzk7FHds7SHA==} + '@sphereon/ssi-types@0.9.0': + resolution: {integrity: sha512-umCr/syNcmvMMbQ+i/r/mwjI1Qw2aFPp9AwBTvTo1ailAVaaJjJGPkkVz1K9/2NZATNdDiQ3A8yGzdVJoKh9pA==} + '@sphereon/wellknown-dids-client@0.1.3': resolution: {integrity: sha512-TAT24L3RoXD8ocrkTcsz7HuJmgjNjdoV6IXP1p3DdaI/GqkynytXE3J1+F7vUFMRYwY5nW2RaXSgDQhrFJemaA==} @@ -9901,7 +9902,7 @@ snapshots: dependencies: semver: 7.6.3 - '@openid-federation/core@0.1.1-alpha.5': + '@openid-federation/core@0.1.1-alpha.6': dependencies: buffer: 6.0.3 zod: 3.23.8 @@ -10629,14 +10630,14 @@ snapshots: - ts-node - typeorm-aurora-data-api-driver - '@sphereon/pex@5.0.0-unstable.2': + '@sphereon/pex@5.0.0-unstable.2(ts-node@10.9.2(@swc/core@1.7.40)(@types/node@18.18.8)(typescript@5.5.4))': dependencies: '@astronautlabs/jsonpath': 1.1.2 '@sd-jwt/decode': 0.6.1 '@sd-jwt/present': 0.6.1 '@sd-jwt/types': 0.6.1 '@sphereon/pex-models': 2.3.1 - '@sphereon/ssi-types': 0.29.1-unstable.208 + '@sphereon/ssi-types': 0.29.1-unstable.121(ts-node@10.9.2(@swc/core@1.7.40)(@types/node@18.18.8)(typescript@5.5.4)) ajv: 8.17.1 ajv-formats: 2.1.1(ajv@8.17.1) jwt-decode: 3.1.2 @@ -10644,7 +10645,25 @@ snapshots: string.prototype.matchall: 4.0.11 uint8arrays: 3.1.1 transitivePeerDependencies: + - '@google-cloud/spanner' + - '@sap/hana-client' + - better-sqlite3 + - encoding + - hdb-pool + - ioredis + - mongodb + - mssql + - mysql2 + - oracledb + - pg + - pg-native + - pg-query-stream + - redis + - sql.js + - sqlite3 - supports-color + - ts-node + - typeorm-aurora-data-api-driver '@sphereon/ssi-sdk-ext.did-utils@0.24.1-unstable.112(ts-node@10.9.2(@swc/core@1.7.40)(@types/node@18.18.8)(typescript@5.5.4))': dependencies: @@ -11016,16 +11035,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@sphereon/ssi-types@0.29.1-unstable.208': - dependencies: - '@sd-jwt/decode': 0.6.1 - '@sphereon/kmp-mdl-mdoc': 0.2.0-SNAPSHOT.22 - debug: 4.3.6 - events: 3.3.0 - jwt-decode: 3.1.2 - transitivePeerDependencies: - - supports-color - '@sphereon/ssi-types@0.30.1(ts-node@10.9.2(@swc/core@1.7.40)(@types/node@18.18.8)(typescript@5.5.4))': dependencies: '@sd-jwt/decode': 0.7.2 @@ -11055,14 +11064,17 @@ snapshots: - ts-node - typeorm-aurora-data-api-driver + '@sphereon/ssi-types@0.9.0': + dependencies: + jwt-decode: 3.1.2 + '@sphereon/wellknown-dids-client@0.1.3': dependencies: - '@sphereon/ssi-types': 0.29.1-unstable.208 + '@sphereon/ssi-types': 0.9.0 cross-fetch: 3.1.8 jwt-decode: 3.1.2 transitivePeerDependencies: - encoding - - supports-color '@sqltools/formatter@1.2.5': {}