diff --git a/packages/openid4vc/package.json b/packages/openid4vc/package.json index 61defe7440..1e0e13d9b2 100644 --- a/packages/openid4vc/package.json +++ b/packages/openid4vc/package.json @@ -33,7 +33,7 @@ "@sphereon/oid4vci-common": "0.16.1-fix.173", "@sphereon/oid4vci-issuer": "0.16.1-fix.173", "@sphereon/ssi-types": "0.30.2-next.135", - "@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 09f1629fe0..f84200ef87 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) @@ -231,7 +236,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) @@ -242,7 +247,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 8bb73db0d5..ef1b7319ea 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts @@ -143,6 +143,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 c6f35137cb..1601d97c54 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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-fix.173 version: 0.16.1-fix.173(ts-node@10.9.2(@swc/core@1.7.40)(@types/node@18.18.8)(typescript@5.5.4))(typescript@5.5.4) @@ -2227,8 +2227,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==} @@ -2470,6 +2470,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-fix.173': resolution: {integrity: sha512-+AAUvEEFs0vzz1mrgjSgvDkcBtr18d2XEVgJex7QlAqxCKVGfjzZlqL2Q2vOLKYVaXsazhD5LnYiY6B5WMTC3Q==} @@ -2530,9 +2531,6 @@ 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==} @@ -9811,7 +9809,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 @@ -10524,7 +10522,25 @@ snapshots: nanoid: 3.3.7 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.130(ts-node@10.9.2(@swc/core@1.7.40)(@types/node@18.18.8)(typescript@5.5.4))': dependencies: @@ -10721,16 +10737,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