From b72348578b0461d4a5f3de5eab3e889857b117a1 Mon Sep 17 00:00:00 2001 From: Tom Lanser Date: Tue, 5 Nov 2024 11:34:39 +0100 Subject: [PATCH 01/14] feat: working version Signed-off-by: Tom Lanser --- packages/openid4vc/package.json | 1 + .../OpenId4VciHolderService.ts | 2 + .../openid4vc-issuer/OpenId4VcIssuerModule.ts | 6 + .../OpenId4VcIssuerModuleConfig.ts | 12 + .../router/federationEndpoint.ts | 102 ++++ .../OpenId4VcSiopVerifierService.ts | 13 +- .../OpenId4VcVerifierModule.ts | 5 +- .../OpenId4VcVerifierModuleConfig.ts | 13 + .../__tests__/openid4vc-verifier.test.ts | 39 ++ .../router/federationEndpoint.ts | 133 ++++++ .../src/openid4vc-verifier/router/index.ts | 1 + packages/openid4vc/src/shared/federation.ts | 9 + packages/openid4vc/src/shared/index.ts | 1 + .../src/shared/models/OpenId4VcJwtIssuer.ts | 11 +- packages/openid4vc/src/shared/utils.ts | 92 +++- .../tests/openid4vc-federation.e2e.test.ts | 445 ++++++++++++++++++ pnpm-lock.yaml | 30 +- 17 files changed, 903 insertions(+), 12 deletions(-) create mode 100644 packages/openid4vc/src/openid4vc-issuer/router/federationEndpoint.ts create mode 100644 packages/openid4vc/src/openid4vc-verifier/router/federationEndpoint.ts create mode 100644 packages/openid4vc/src/shared/federation.ts create mode 100644 packages/openid4vc/tests/openid4vc-federation.e2e.test.ts diff --git a/packages/openid4vc/package.json b/packages/openid4vc/package.json index d56d6b0bae..61defe7440 100644 --- a/packages/openid4vc/package.json +++ b/packages/openid4vc/package.json @@ -33,6 +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", "class-transformer": "^0.5.1", "rxjs": "^7.8.0" }, diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts index e46bed9a18..c111f30492 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts @@ -871,4 +871,6 @@ export class OpenId4VciHolderService { return jws } } + + // TODO: Add a function for resolving the entity statement. Which will be used in the holder to verify the entity statement and to show to the user } diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts index 44f4f6e84c..7d7e199a03 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts @@ -17,6 +17,7 @@ import { configureCredentialEndpoint, configureIssuerMetadataEndpoint, } from './router' +import { configureFederationEndpoint } from './router/federationEndpoint' /** * @public @@ -120,6 +121,11 @@ export class OpenId4VcIssuerModule implements Module { configureAccessTokenEndpoint(endpointRouter, this.config.accessTokenEndpoint) configureCredentialEndpoint(endpointRouter, this.config.credentialEndpoint) + // The federation endpoint is optional + if (this.config.federationEndpoint) { + configureFederationEndpoint(endpointRouter, this.config.federationEndpoint) + } + // First one will be called for all requests (when next is called) contextRouter.use(async (req: OpenId4VcIssuanceRequest, _res: unknown, next) => { const { agentContext } = getRequestContext(req) diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts index 71eaa43c9a..7697cc637c 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts @@ -3,6 +3,7 @@ import type { OpenId4VciCredentialEndpointConfig, OpenId4VciCredentialOfferEndpointConfig, } from './router' +import type { OpenId4VcSiopFederationEndpointConfig } from './router/federationEndpoint' import type { Optional } from '@credo-ts/core' import type { Router } from 'express' @@ -35,6 +36,7 @@ export interface OpenId4VcIssuerModuleConfigOptions { OpenId4VciAccessTokenEndpointConfig, 'cNonceExpiresInSeconds' | 'endpointPath' | 'preAuthorizedCodeExpirationInSeconds' | 'tokenExpiresInSeconds' > + federation?: Optional } } @@ -94,4 +96,14 @@ export class OpenId4VcIssuerModuleConfig { endpointPath: userOptions.endpointPath ?? '/offers', } } + + public get federationEndpoint(): OpenId4VcSiopFederationEndpointConfig | undefined { + const userOptions = this.options.endpoints.federation + if (!userOptions) return undefined + + return { + ...userOptions, + endpointPath: userOptions.endpointPath ?? '/.well-known/openid-federation', + } + } } diff --git a/packages/openid4vc/src/openid4vc-issuer/router/federationEndpoint.ts b/packages/openid4vc/src/openid4vc-issuer/router/federationEndpoint.ts new file mode 100644 index 0000000000..15f656a32c --- /dev/null +++ b/packages/openid4vc/src/openid4vc-issuer/router/federationEndpoint.ts @@ -0,0 +1,102 @@ +import type { OpenId4VcIssuanceRequest } from './requestContext' +import type { FederationKeyCallback } from '../../shared/federation' +import type { Buffer } from '@credo-ts/core' +import type { Router, Response } from 'express' + +import { getJwkFromKey } from '@credo-ts/core' +import { createEntityConfiguration } from '@openid-federation/core' + +import { getRequestContext, sendErrorResponse } from '../../shared/router' +import { OpenId4VcIssuerService } from '../OpenId4VcIssuerService' + +export interface OpenId4VcSiopFederationEndpointConfig { + /** + * The path at which the credential endpoint should be made available. Note that it will be + * hosted at a subpath to take into account multiple tenants and issuers. + * + * @default /.well-known/openid-federation + */ + endpointPath: string + + // TODO: Not sure about the property name yet. + //TODO: More information is needed than only the key also the client id etc + keyCallback: FederationKeyCallback<{ + issuerId: string + }> +} + +// TODO: It's also possible that the issuer and the verifier can have the same openid-federation endpoint. In that case we need to combine them. + +export function configureFederationEndpoint(router: Router, config: OpenId4VcSiopFederationEndpointConfig) { + router.get(config.endpointPath, async (request: OpenId4VcIssuanceRequest, response: Response, next) => { + const { agentContext, issuer } = getRequestContext(request) + const openId4VcIssuerService = agentContext.dependencyManager.resolve(OpenId4VcIssuerService) + + try { + const issuerMetadata = openId4VcIssuerService.getIssuerMetadata(agentContext, issuer) + // TODO: Use a type here from sphreon + const transformedMetadata = { + credential_issuer: issuerMetadata.issuerUrl, + token_endpoint: issuerMetadata.tokenEndpoint, + credential_endpoint: issuerMetadata.credentialEndpoint, + authorization_server: issuerMetadata.authorizationServer, + authorization_servers: issuerMetadata.authorizationServer ? [issuerMetadata.authorizationServer] : undefined, + credentials_supported: issuerMetadata.credentialsSupported, + credential_configurations_supported: issuerMetadata.credentialConfigurationsSupported, + display: issuerMetadata.issuerDisplay, + dpop_signing_alg_values_supported: issuerMetadata.dpopSigningAlgValuesSupported, + } as const + + const now = new Date() + const expires = new Date(now.getTime() + 1000 * 60 * 60 * 24) // 1 day from now + + const { key } = await config.keyCallback(agentContext, { + issuerId: issuer.issuerId, + }) + + const jwk = getJwkFromKey(key) + const kid = 'key-1' + const alg = jwk.supportedSignatureAlgorithms[0] + + const issuerDisplay = issuerMetadata.issuerDisplay?.[0] + + const entityConfiguration = await createEntityConfiguration({ + claims: { + sub: issuerMetadata.issuerUrl, + iss: issuerMetadata.issuerUrl, + iat: now, + exp: expires, + jwks: { + keys: [{ kid, alg, ...jwk.toJson() }], + }, + metadata: { + federation_entity: issuerDisplay + ? { + organization_name: issuerDisplay.organization_name, + logo_uri: issuerDisplay.logo_uri, + } + : undefined, + openid_credential_issuer: transformedMetadata, + }, + }, + header: { + kid, + alg, + typ: 'entity-statement+jwt', + }, + signJwtCallback: ({ toBeSigned }) => + agentContext.wallet.sign({ + data: toBeSigned as Buffer, + key, + }), + }) + + response.writeHead(200, { 'Content-Type': 'application/entity-statement+jwt' }).end(entityConfiguration) + } catch (error) { + sendErrorResponse(response, agentContext.config.logger, 500, 'invalid_request', error) + } + + // NOTE: if we don't call next, the agentContext session handler will NOT be called + next() + }) +} diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts index 7ff3e15a64..8bb73db0d5 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts @@ -142,6 +142,14 @@ export class OpenId4VcSiopVerifierService { } else if (jwtIssuer.method === 'did') { clientId = jwtIssuer.didUrl.split('#')[0] clientIdScheme = 'did' + } else if (jwtIssuer.method === 'custom') { + 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') + throw new CredoError(`Custom jwtIssuer's clientId must be a string.`) + + clientIdScheme = 'entity_id' + clientId = jwtIssuer.options.clientId } else { throw new CredoError( `Unsupported jwt issuer method '${options.requestSigner.method}'. Only 'did' and 'x5c' are supported.` @@ -231,6 +239,8 @@ export class OpenId4VcSiopVerifierService { ) const requestClientId = await authorizationRequest.getMergedProperty('client_id') + // TODO: Is this needed for the verification of the federation? + const requestClientIdScheme = await authorizationRequest.getMergedProperty('client_id_scheme') const requestNonce = await authorizationRequest.getMergedProperty('nonce') const requestState = await authorizationRequest.getMergedProperty('state') const responseUri = await authorizationRequest.getMergedProperty('response_uri') @@ -251,6 +261,7 @@ export class OpenId4VcSiopVerifierService { presentationDefinition: presentationDefinitionsWithLocation?.[0]?.definition, authorizationResponseUrl, clientId: requestClientId, + clientIdScheme: requestClientIdScheme, }) // This is very unfortunate, but storing state in sphereon's SiOP-OID4VP library @@ -463,7 +474,7 @@ export class OpenId4VcSiopVerifierService { return this.openId4VcVerificationSessionRepository.getById(agentContext, verificationSessionId) } - private async getRelyingParty( + public async getRelyingParty( agentContext: AgentContext, verifierId: string, { diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts index 4e44b2883e..9c81150c2e 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts @@ -12,7 +12,7 @@ import { OpenId4VcVerifierApi } from './OpenId4VcVerifierApi' import { OpenId4VcVerifierModuleConfig } from './OpenId4VcVerifierModuleConfig' import { OpenId4VcVerifierRepository } from './repository' import { OpenId4VcRelyingPartyEventHandler } from './repository/OpenId4VcRelyingPartyEventEmitter' -import { configureAuthorizationEndpoint } from './router' +import { configureAuthorizationEndpoint, configureFederationEndpoint } from './router' import { configureAuthorizationRequestEndpoint } from './router/authorizationRequestEndpoint' /** @@ -115,6 +115,9 @@ export class OpenId4VcVerifierModule implements Module { // Configure endpoints configureAuthorizationEndpoint(endpointRouter, this.config.authorizationEndpoint) configureAuthorizationRequestEndpoint(endpointRouter, this.config.authorizationRequestEndpoint) + if (this.config.federationEndpoint) { + configureFederationEndpoint(endpointRouter, this.config.federationEndpoint) + } // First one will be called for all requests (when next is called) contextRouter.use(async (req: OpenId4VcVerificationRequest, _res: unknown, next) => { diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModuleConfig.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModuleConfig.ts index b2ec763cbc..0360b3cf14 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModuleConfig.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModuleConfig.ts @@ -1,5 +1,6 @@ import type { OpenId4VcSiopAuthorizationEndpointConfig } from './router/authorizationEndpoint' import type { OpenId4VcSiopAuthorizationRequestEndpointConfig } from './router/authorizationRequestEndpoint' +import type { OpenId4VcSiopFederationEndpointConfig } from './router/federationEndpoint' import type { Optional } from '@credo-ts/core' import type { Router } from 'express' @@ -24,6 +25,7 @@ export interface OpenId4VcVerifierModuleConfigOptions { endpoints?: { authorization?: Optional authorizationRequest?: Optional + federation?: Optional } } @@ -60,4 +62,15 @@ export class OpenId4VcVerifierModuleConfig { endpointPath: userOptions?.endpointPath ?? '/authorize', } } + + public get federationEndpoint(): OpenId4VcSiopFederationEndpointConfig | undefined { + // Use user supplied options, or return defaults. + const userOptions = this.options.endpoints?.federation + if (!userOptions) return undefined + + return { + ...userOptions, + endpointPath: userOptions.endpointPath ?? '/.well-known/openid-federation', + } + } } diff --git a/packages/openid4vc/src/openid4vc-verifier/__tests__/openid4vc-verifier.test.ts b/packages/openid4vc/src/openid4vc-verifier/__tests__/openid4vc-verifier.test.ts index e40ef70579..883b5cb8a7 100644 --- a/packages/openid4vc/src/openid4vc-verifier/__tests__/openid4vc-verifier.test.ts +++ b/packages/openid4vc/src/openid4vc-verifier/__tests__/openid4vc-verifier.test.ts @@ -113,5 +113,44 @@ describe('OpenId4VcVerifier', () => { expect(jwt.payload.iss).toEqual(verifier.did) expect(jwt.payload.sub).toEqual(verifier.did) }) + + it('check openid proof request format (entity id)', async () => { + const openIdVerifier = await verifier.agent.modules.openId4VcVerifier.createVerifier() + const { authorizationRequest, verificationSession } = + await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest({ + requestSigner: { + method: 'openid-federation', + clientId: 'http://localhost:3001/verifier', + }, + verifierId: openIdVerifier.verifierId, + }) + + expect( + authorizationRequest.startsWith( + `openid://?client_id=${encodeURIComponent(verifier.did)}&request_uri=http%3A%2F%2Fredirect-uri%2F${ + openIdVerifier.verifierId + }%2Fauthorization-requests%2F` + ) + ).toBe(true) + + const jwt = Jwt.fromSerializedJwt(verificationSession.authorizationRequestJwt) + + expect(jwt.header.kid) + + expect(jwt.header.kid).toEqual(verifier.kid) + expect(jwt.header.alg).toEqual(SigningAlgo.EDDSA) + expect(jwt.header.typ).toEqual('JWT') + expect(jwt.payload.additionalClaims.scope).toEqual('openid') + expect(jwt.payload.additionalClaims.client_id).toEqual(verifier.did) + expect(jwt.payload.additionalClaims.response_uri).toEqual( + `http://redirect-uri/${openIdVerifier.verifierId}/authorize` + ) + expect(jwt.payload.additionalClaims.response_mode).toEqual('direct_post') + expect(jwt.payload.additionalClaims.nonce).toBeDefined() + expect(jwt.payload.additionalClaims.state).toBeDefined() + expect(jwt.payload.additionalClaims.response_type).toEqual('id_token') + expect(jwt.payload.iss).toEqual(verifier.did) + expect(jwt.payload.sub).toEqual(verifier.did) + }) }) }) diff --git a/packages/openid4vc/src/openid4vc-verifier/router/federationEndpoint.ts b/packages/openid4vc/src/openid4vc-verifier/router/federationEndpoint.ts new file mode 100644 index 0000000000..0e02d97271 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-verifier/router/federationEndpoint.ts @@ -0,0 +1,133 @@ +import type { OpenId4VcVerificationRequest } from './requestContext' +import type { FederationKeyCallback } from '../../shared/federation' +import type { RPRegistrationMetadataPayload } from '@sphereon/did-auth-siop' +import type { Router, Response } from 'express' + +import { getJwkFromKey, type Buffer } from '@credo-ts/core' +import { createEntityConfiguration } from '@openid-federation/core' +import { LanguageTagUtils, removeNullUndefined } from '@sphereon/did-auth-siop' + +import { getRequestContext, sendErrorResponse } from '../../shared/router' +import { OpenId4VcSiopVerifierService } from '../OpenId4VcSiopVerifierService' +import { OpenId4VcVerifierModuleConfig } from '../OpenId4VcVerifierModuleConfig' + +// TODO: Think about how we can have multiple issuers over the federation endpoint +export interface OpenId4VcSiopFederationEndpointConfig { + /** + * The path at which the authorization request should be made available. Note that it will be + * hosted at a subpath to take into account multiple tenants and verifiers. + * + * @default /.well-known/openid-federation + */ + endpointPath: string + + // TODO: Not sure about the property name yet. + keyCallback: FederationKeyCallback<{ + verifierId: string + }> +} + +// TODO: Add types but this function is originally from the @ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const createRPRegistrationMetadataPayload = (opts: any): RPRegistrationMetadataPayload => { + const rpRegistrationMetadataPayload: RPRegistrationMetadataPayload = { + id_token_signing_alg_values_supported: opts.idTokenSigningAlgValuesSupported, + request_object_signing_alg_values_supported: opts.requestObjectSigningAlgValuesSupported, + response_types_supported: opts.responseTypesSupported, + scopes_supported: opts.scopesSupported, + subject_types_supported: opts.subjectTypesSupported, + subject_syntax_types_supported: opts.subject_syntax_types_supported || ['did:web:', 'did:ion:'], + vp_formats: opts.vpFormatsSupported, + client_name: opts.clientName, + logo_uri: opts.logo_uri, + tos_uri: opts.tos_uri, + client_purpose: opts.clientPurpose, + client_id: opts.client_id, + } + + const languageTagEnabledFieldsNamesMapping = new Map() + languageTagEnabledFieldsNamesMapping.set('clientName', 'client_name') + languageTagEnabledFieldsNamesMapping.set('clientPurpose', 'client_purpose') + + // TODO: Do we need this? + const languageTaggedFields: Map = LanguageTagUtils.getLanguageTaggedPropertiesMapped( + opts, + languageTagEnabledFieldsNamesMapping + ) + + languageTaggedFields.forEach((value: string, key: string) => { + const _key = key as keyof typeof rpRegistrationMetadataPayload + rpRegistrationMetadataPayload[_key] = value + }) + + return removeNullUndefined(rpRegistrationMetadataPayload) +} + +export function configureFederationEndpoint(router: Router, config: OpenId4VcSiopFederationEndpointConfig) { + router.get(config.endpointPath, async (request: OpenId4VcVerificationRequest, response: Response, next) => { + const { agentContext, verifier } = getRequestContext(request) + const verifierService = agentContext.dependencyManager.resolve(OpenId4VcSiopVerifierService) + const verifierConfig = agentContext.dependencyManager.resolve(OpenId4VcVerifierModuleConfig) + + try { + const { key } = await config.keyCallback(agentContext, { + verifierId: verifier.verifierId, + }) + + const relyingParty = await verifierService.getRelyingParty(agentContext, verifier.verifierId, { + clientId: verifierConfig.baseUrl, + clientIdScheme: 'entity_id', + authorizationResponseUrl: `${verifierConfig.baseUrl}/siop/${verifier.verifierId}/authorize`, + }) + + const verifierEntityId = `${verifierConfig.baseUrl}/${verifier.verifierId}` + + const rpMetadata = createRPRegistrationMetadataPayload(relyingParty.createRequestOptions.clientMetadata) + + // TODO: We also need to cache the entity configuration until it expires + const now = new Date() + // TODO: We also need to check if the x509 certificate is still valid until this expires + const expires = new Date(now.getTime() + 1000 * 60 * 60 * 24) // 1 day + + const jwk = getJwkFromKey(key) + const alg = jwk.supportedSignatureAlgorithms[0] + const kid = 'key-1' + + const entityConfiguration = await createEntityConfiguration({ + header: { + kid, + alg, + typ: 'entity-statement+jwt', + }, + claims: { + sub: verifierEntityId, + iss: verifierEntityId, + iat: now, + exp: expires, + jwks: { + keys: [{ kid, alg, ...jwk.toJson() }], + }, + metadata: { + federation_entity: { + organization_name: rpMetadata.client_name, + logo_uri: rpMetadata.logo_uri, + }, + openid_credential_verifier: rpMetadata, + }, + }, + signJwtCallback: ({ toBeSigned }) => + agentContext.wallet.sign({ + data: toBeSigned as Buffer, + key, + }), + }) + + response.writeHead(200, { 'Content-Type': 'application/entity-statement+jwt' }).end(entityConfiguration) + } catch (error) { + sendErrorResponse(response, agentContext.config.logger, 500, 'invalid_request', error) + } + + // NOTE: if we don't call next, the agentContext session handler will NOT be called + next() + }) +} diff --git a/packages/openid4vc/src/openid4vc-verifier/router/index.ts b/packages/openid4vc/src/openid4vc-verifier/router/index.ts index 8242556be4..cfe20f0af2 100644 --- a/packages/openid4vc/src/openid4vc-verifier/router/index.ts +++ b/packages/openid4vc/src/openid4vc-verifier/router/index.ts @@ -1,2 +1,3 @@ export { configureAuthorizationEndpoint } from './authorizationEndpoint' export { OpenId4VcVerificationRequest } from './requestContext' +export { configureFederationEndpoint } from './federationEndpoint' diff --git a/packages/openid4vc/src/shared/federation.ts b/packages/openid4vc/src/shared/federation.ts new file mode 100644 index 0000000000..fefafdb426 --- /dev/null +++ b/packages/openid4vc/src/shared/federation.ts @@ -0,0 +1,9 @@ +import type { AgentContext, Key } from '@credo-ts/core' + +// TODO: Not really sure about this type yet but it's a start. +export type FederationKeyCallback = Record> = ( + agentContext: AgentContext, + context: TContext +) => Promise<{ + key: Key +}> diff --git a/packages/openid4vc/src/shared/index.ts b/packages/openid4vc/src/shared/index.ts index 8eacb927b2..3e3b26e4ce 100644 --- a/packages/openid4vc/src/shared/index.ts +++ b/packages/openid4vc/src/shared/index.ts @@ -1,2 +1,3 @@ export * from './models' export * from './issuerMetadataUtils' +export * from './federation' diff --git a/packages/openid4vc/src/shared/models/OpenId4VcJwtIssuer.ts b/packages/openid4vc/src/shared/models/OpenId4VcJwtIssuer.ts index edbd9574b3..c3fada42d4 100644 --- a/packages/openid4vc/src/shared/models/OpenId4VcJwtIssuer.ts +++ b/packages/openid4vc/src/shared/models/OpenId4VcJwtIssuer.ts @@ -30,4 +30,13 @@ interface OpenId4VcJwtIssuerJwk { jwk: Jwk } -export type OpenId4VcJwtIssuer = OpenId4VcJwtIssuerDid | OpenId4VcIssuerX5c | OpenId4VcJwtIssuerJwk +interface OpenId4VcJwtIssuerFederation { + method: 'openid-federation' + clientId: string +} + +export type OpenId4VcJwtIssuer = + | OpenId4VcJwtIssuerDid + | OpenId4VcIssuerX5c + | OpenId4VcJwtIssuerJwk + | OpenId4VcJwtIssuerFederation diff --git a/packages/openid4vc/src/shared/utils.ts b/packages/openid4vc/src/shared/utils.ts index f47fca1d1f..efdd3dfde3 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 } from '@sphereon/oid4vc-common' +import type { DPoPJwtIssuerWithContext, CreateJwtCallback, JwtIssuer, JwtIssuerBase } from '@sphereon/oid4vc-common' import type { CredentialOfferPayloadV1_0_11, CredentialOfferPayloadV1_0_13 } from '@sphereon/oid4vci-common' import { @@ -10,6 +10,7 @@ import { JwsService, JwtPayload, SignatureSuiteRegistry, + TypedArrayEncoder, X509Service, getDomainFromUrl, getJwkClassFromKeyType, @@ -17,6 +18,7 @@ import { getJwkFromKey, getKeyFromVerificationMethod, } from '@credo-ts/core' +import { fetchEntityConfiguration, fetchEntityConfigurationChains } from '@openid-federation/core' /** * Returns the JWA Signature Algorithms that are supported by the wallet. @@ -49,7 +51,14 @@ async function getKeyFromDid(agentContext: AgentContext, didUrl: string) { return getKeyFromVerificationMethod(verificationMethod) } -export function getVerifyJwtCallback(agentContext: AgentContext): VerifyJwtCallback { +type VerifyJwtCallbackOptions = { + trustedEntityIds?: string[] +} + +export function getVerifyJwtCallback( + agentContext: AgentContext, + options: VerifyJwtCallbackOptions = {} +): VerifyJwtCallback { return async (jwtVerifier, jwt) => { const jwsService = agentContext.dependencyManager.resolve(JwsService) if (jwtVerifier.method === 'did') { @@ -61,6 +70,30 @@ export function getVerifyJwtCallback(agentContext: AgentContext): VerifyJwtCallb } else 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') { + const { entityId } = jwtVerifier + const trustedEntityIds = options.trustedEntityIds ?? [entityId] // TODO: Just for testing + if (!trustedEntityIds) + throw new CredoError('No trusted entity ids provided but is required for the openid-federation method.') + + const entityConfigurationChains = await fetchEntityConfigurationChains({ + leafEntityId: entityId, + trustAnchorEntityIds: trustedEntityIds, + verifyJwtCallback: async ({ data, signature, jwk }) => { + const jws = `${TypedArrayEncoder.toUtf8String(data)}.${TypedArrayEncoder.toBase64URL(signature)}` + + const res = await jwsService.verifyJws(agentContext, { + jws, + jwkResolver: () => getJwkFromJson(jwk), + }) + return res.isValid + }, + }) + + // TODO: There is no check yet for the policies + + // TODO: I think this is correct but not sure? + return entityConfigurationChains.length > 0 } else { throw new Error(`Unsupported jwt verifier method: '${jwtVerifier.method}'`) } @@ -82,7 +115,9 @@ export function getCreateJwtCallback( }) return jws - } else if (jwtIssuer.method === 'jwk') { + } + + if (jwtIssuer.method === 'jwk') { if (!jwtIssuer.jwk.kty) { throw new CredoError('Missing required key type (kty) in the jwk.') } @@ -95,7 +130,9 @@ export function getCreateJwtCallback( }) return jws - } else if (jwtIssuer.method === 'x5c') { + } + + if (jwtIssuer.method === 'x5c') { const leafCertificate = X509Service.getLeafCertificate(agentContext, { certificateChain: jwtIssuer.x5c }) const jws = await jwsService.createJwsCompact(agentContext, { @@ -107,6 +144,35 @@ export function getCreateJwtCallback( return jws } + if (jwtIssuer.method === 'custom') { + const { options } = jwtIssuer + if (!options) throw new CredoError(`Custom jwtIssuer must have options defined.`) + 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 entityConfiguration = await fetchEntityConfiguration({ + entityId: clientId as string, + verifyJwtCallback: async ({ data, signature, jwk }) => { + const jws = `${TypedArrayEncoder.toUtf8String(data)}.${TypedArrayEncoder.toBase64URL(signature)}` + const res = await jwsService.verifyJws(agentContext, { jws, jwkResolver: () => getJwkFromJson(jwk) }) + return res.isValid + }, + }) + + // 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]) + + const jws = await jwsService.createJwsCompact(agentContext, { + protectedHeaderOptions: { ...jwt.header, jwk, alg: jwk.supportedSignatureAlgorithms[0] }, + payload: JwtPayload.fromJson(jwt.payload), + key: jwk.key, + }) + + return jws + } + throw new Error(`Unsupported jwt issuer method '${jwtIssuer.method}'`) } } @@ -125,7 +191,9 @@ export async function openIdTokenIssuerToJwtIssuer( didUrl: openId4VcTokenIssuer.didUrl, alg, } - } else if (openId4VcTokenIssuer.method === 'x5c') { + } + + if (openId4VcTokenIssuer.method === 'x5c') { const leafCertificate = X509Service.getLeafCertificate(agentContext, { certificateChain: openId4VcTokenIssuer.x5c, }) @@ -153,7 +221,9 @@ export async function openIdTokenIssuerToJwtIssuer( ...openId4VcTokenIssuer, alg, } - } else if (openId4VcTokenIssuer.method === 'jwk') { + } + + if (openId4VcTokenIssuer.method === 'jwk') { const alg = openId4VcTokenIssuer.jwk.supportedSignatureAlgorithms[0] if (!alg) { throw new CredoError(`No supported signature algorithms for key type: '${openId4VcTokenIssuer.jwk.keyType}'`) @@ -165,6 +235,16 @@ export async function openIdTokenIssuerToJwtIssuer( } } + if (openId4VcTokenIssuer.method === 'openid-federation') { + // TODO: Not sure what we want here if we need to add a new type to the sphereon library or that we can do it with the custom issuer + return { + method: 'custom', + options: { + clientId: openId4VcTokenIssuer.clientId, + }, + } + } + throw new CredoError(`Unsupported jwt issuer method '${(openId4VcTokenIssuer as OpenId4VcJwtIssuer).method}'`) } diff --git a/packages/openid4vc/tests/openid4vc-federation.e2e.test.ts b/packages/openid4vc/tests/openid4vc-federation.e2e.test.ts new file mode 100644 index 0000000000..a0d0c9ad2b --- /dev/null +++ b/packages/openid4vc/tests/openid4vc-federation.e2e.test.ts @@ -0,0 +1,445 @@ +import type { AgentType, TenantType } from './utils' +import type { OpenId4VciSignMdocCredential } from '../src' +import type { Server } from 'http' + +import { + ClaimFormat, + DidsApi, + DifPresentationExchangeService, + JwaSignatureAlgorithm, + KeyType, + W3cCredential, + W3cCredentialSubject, + w3cDate, + W3cIssuer, + WalletApi, + X509Module, + X509ModuleConfig, +} from '@credo-ts/core' +import express, { type Express } from 'express' + +import { AskarModule } from '../../askar/src' +import { askarModuleConfig } from '../../askar/tests/helpers' +import { TenantsModule } from '../../tenants/src' +import { + OpenId4VcHolderModule, + OpenId4VcIssuerModule, + OpenId4VcVerificationSessionState, + OpenId4VcVerifierModule, +} from '../src' + +import { waitForVerificationSessionRecordSubject, createAgentFromModules, createTenantForAgent } from './utils' +import { + universityDegreeCredentialConfigurationSupportedMdoc, + universityDegreeCredentialSdJwt, + universityDegreeCredentialSdJwt2, +} from './utilsVci' +import { openBadgePresentationDefinition, universityDegreePresentationDefinition } from './utilsVp' + +const serverPort = 1234 +const baseUrl = `http://localhost:${serverPort}` +const issuanceBaseUrl = `${baseUrl}/oid4vci` +const verificationBaseUrl = `${baseUrl}/oid4vp` + +describe('OpenId4Vc', () => { + let expressApp: Express + let expressServer: Server + + let issuer: AgentType<{ + openId4VcIssuer: OpenId4VcIssuerModule + tenants: TenantsModule<{ openId4VcIssuer: OpenId4VcIssuerModule }> + x509: X509Module + }> + let issuer1: TenantType + let issuer2: TenantType + + let holder: AgentType<{ + openId4VcHolder: OpenId4VcHolderModule + tenants: TenantsModule<{ openId4VcHolder: OpenId4VcHolderModule }> + }> + let holder1: TenantType + + let verifier: AgentType<{ + openId4VcVerifier: OpenId4VcVerifierModule + tenants: TenantsModule<{ openId4VcVerifier: OpenId4VcVerifierModule }> + }> + let verifier1: TenantType + let verifier2: TenantType + + beforeEach(async () => { + expressApp = express() + + issuer = (await createAgentFromModules( + 'issuer', + { + x509: new X509Module(), + openId4VcIssuer: new OpenId4VcIssuerModule({ + baseUrl: issuanceBaseUrl, + endpoints: { + credential: { + credentialRequestToCredentialMapper: async ({ agentContext, credentialRequest, holderBinding }) => { + // We sign the request with the first did:key did we have + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const [firstDidKeyDid] = await didsApi.getCreatedDids({ method: 'key' }) + const didDocument = await didsApi.resolveDidDocument(firstDidKeyDid.did) + const verificationMethod = didDocument.verificationMethod?.[0] + if (!verificationMethod) { + throw new Error('No verification method found') + } + + if (credentialRequest.format === 'vc+sd-jwt') { + return { + credentialSupportedId: + credentialRequest.vct === 'UniversityDegreeCredential' + ? universityDegreeCredentialSdJwt.id + : universityDegreeCredentialSdJwt2.id, + format: credentialRequest.format, + payload: { vct: credentialRequest.vct, university: 'innsbruck', degree: 'bachelor' }, + holder: holderBinding, + issuer: { + method: 'did', + didUrl: verificationMethod.id, + }, + disclosureFrame: { _sd: ['university', 'degree'] }, + } + } else if (credentialRequest.format === 'mso_mdoc') { + const trustedCertificates = + agentContext.dependencyManager.resolve(X509ModuleConfig).trustedCertificates + if (trustedCertificates?.length !== 1) { + throw new Error('Expected exactly one trusted certificate. Received 0.') + } + + return { + credentialSupportedId: '', + format: ClaimFormat.MsoMdoc, + docType: universityDegreeCredentialConfigurationSupportedMdoc.doctype, + issuerCertificate: trustedCertificates[0], + holderKey: holderBinding.key, + namespaces: { + 'Leopold-Franzens-University': { + degree: 'bachelor', + }, + }, + } satisfies OpenId4VciSignMdocCredential + } else { + throw new Error('Invalid request') + } + }, + }, + federation: { + keyCallback: async (agentContext) => { + const walletApi = agentContext.dependencyManager.resolve(WalletApi) + const key = await walletApi.createKey({ keyType: KeyType.Ed25519 }) + + return { + key, + } + }, + }, + }, + }), + askar: new AskarModule(askarModuleConfig), + tenants: new TenantsModule(), + }, + '96213c3d7fc8d4d6754c7a0fd969598g' + )) as unknown as typeof issuer + issuer1 = await createTenantForAgent(issuer.agent, 'iTenant1') + issuer2 = await createTenantForAgent(issuer.agent, 'iTenant2') + + holder = (await createAgentFromModules( + 'holder', + { + openId4VcHolder: new OpenId4VcHolderModule(), + askar: new AskarModule(askarModuleConfig), + tenants: new TenantsModule(), + x509: new X509Module(), + }, + '96213c3d7fc8d4d6754c7a0fd969598e' + )) as unknown as typeof holder + holder1 = await createTenantForAgent(holder.agent, 'hTenant1') + + verifier = (await createAgentFromModules( + 'verifier', + { + openId4VcVerifier: new OpenId4VcVerifierModule({ + baseUrl: verificationBaseUrl, + endpoints: { + federation: { + keyCallback: async (agentContext) => { + const walletApi = agentContext.dependencyManager.resolve(WalletApi) + const key = await walletApi.createKey({ keyType: KeyType.Ed25519 }) + + return { + key, + } + }, + }, + }, + }), + askar: new AskarModule(askarModuleConfig), + tenants: new TenantsModule(), + }, + '96213c3d7fc8d4d6754c7a0fd969598f' + )) as unknown as typeof verifier + verifier1 = await createTenantForAgent(verifier.agent, 'vTenant1') + verifier2 = await createTenantForAgent(verifier.agent, 'vTenant2') + + // We let AFJ create the router, so we have a fresh one each time + expressApp.use('/oid4vci', issuer.agent.modules.openId4VcIssuer.config.router) + expressApp.use('/oid4vp', verifier.agent.modules.openId4VcVerifier.config.router) + + expressServer = expressApp.listen(serverPort) + }) + + afterEach(async () => { + expressServer?.close() + + await issuer.agent.shutdown() + await issuer.agent.wallet.delete() + + await holder.agent.shutdown() + await holder.agent.wallet.delete() + }) + + it('e2e flow with tenants, verifier endpoints verifying a jwt-vc', async () => { + const holderTenant = await holder.agent.modules.tenants.getTenantAgent({ tenantId: holder1.tenantId }) + const verifierTenant1 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier1.tenantId }) + const verifierTenant2 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier2.tenantId }) + + const openIdVerifierTenant1 = await verifierTenant1.modules.openId4VcVerifier.createVerifier() + const openIdVerifierTenant2 = await verifierTenant2.modules.openId4VcVerifier.createVerifier() + + const signedCredential1 = await issuer.agent.w3cCredentials.signCredential({ + format: ClaimFormat.JwtVc, + credential: new W3cCredential({ + type: ['VerifiableCredential', 'OpenBadgeCredential'], + issuer: new W3cIssuer({ id: issuer.did }), + credentialSubject: new W3cCredentialSubject({ id: holder1.did }), + issuanceDate: w3cDate(Date.now()), + }), + alg: JwaSignatureAlgorithm.EdDSA, + verificationMethod: issuer.verificationMethod.id, + }) + + const signedCredential2 = await issuer.agent.w3cCredentials.signCredential({ + format: ClaimFormat.JwtVc, + credential: new W3cCredential({ + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + issuer: new W3cIssuer({ id: issuer.did }), + credentialSubject: new W3cCredentialSubject({ id: holder1.did }), + issuanceDate: w3cDate(Date.now()), + }), + alg: JwaSignatureAlgorithm.EdDSA, + verificationMethod: issuer.verificationMethod.id, + }) + + await holderTenant.w3cCredentials.storeCredential({ credential: signedCredential1 }) + await holderTenant.w3cCredentials.storeCredential({ credential: signedCredential2 }) + + const { authorizationRequest: authorizationRequestUri1, verificationSession: verificationSession1 } = + await verifierTenant1.modules.openId4VcVerifier.createAuthorizationRequest({ + verifierId: openIdVerifierTenant1.verifierId, + requestSigner: { + method: 'openid-federation', + clientId: `http://localhost:1234/oid4vp/${openIdVerifierTenant1.verifierId}`, + }, + presentationExchange: { + definition: openBadgePresentationDefinition, + }, + }) + + expect(authorizationRequestUri1).toEqual( + `openid4vp://?client_id=${encodeURIComponent( + `http://localhost:1234/oid4vp/${openIdVerifierTenant1.verifierId}` + )}&request_uri=${encodeURIComponent(verificationSession1.authorizationRequestUri)}` + ) + + const { authorizationRequest: authorizationRequestUri2, verificationSession: verificationSession2 } = + await verifierTenant2.modules.openId4VcVerifier.createAuthorizationRequest({ + requestSigner: { + method: 'openid-federation', + clientId: `http://localhost:1234/oid4vp/${openIdVerifierTenant2.verifierId}`, + }, + presentationExchange: { + definition: universityDegreePresentationDefinition, + }, + verifierId: openIdVerifierTenant2.verifierId, + }) + + expect(authorizationRequestUri2).toEqual( + `openid4vp://?client_id=${encodeURIComponent( + `http://localhost:1234/oid4vp/${openIdVerifierTenant2.verifierId}` + )}&request_uri=${encodeURIComponent(verificationSession2.authorizationRequestUri)}` + ) + + await verifierTenant1.endSession() + await verifierTenant2.endSession() + + const resolvedProofRequest1 = await holderTenant.modules.openId4VcHolder.resolveSiopAuthorizationRequest( + authorizationRequestUri1 + ) + + expect(resolvedProofRequest1.presentationExchange?.credentialsForRequest).toMatchObject({ + areRequirementsSatisfied: true, + requirements: [ + { + submissionEntry: [ + { + verifiableCredentials: [ + { + type: ClaimFormat.JwtVc, + credentialRecord: { + credential: { + type: ['VerifiableCredential', 'OpenBadgeCredential'], + }, + }, + }, + ], + }, + ], + }, + ], + }) + + const resolvedProofRequest2 = await holderTenant.modules.openId4VcHolder.resolveSiopAuthorizationRequest( + authorizationRequestUri2 + ) + + expect(resolvedProofRequest2.presentationExchange?.credentialsForRequest).toMatchObject({ + areRequirementsSatisfied: true, + requirements: [ + { + submissionEntry: [ + { + verifiableCredentials: [ + { + type: ClaimFormat.JwtVc, + credentialRecord: { + credential: { + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + }, + }, + }, + ], + }, + ], + }, + ], + }) + + if (!resolvedProofRequest1.presentationExchange || !resolvedProofRequest2.presentationExchange) { + throw new Error('Presentation exchange not defined') + } + + const presentationExchangeService = holderTenant.dependencyManager.resolve(DifPresentationExchangeService) + const selectedCredentials = presentationExchangeService.selectCredentialsForRequest( + resolvedProofRequest1.presentationExchange.credentialsForRequest + ) + + const { submittedResponse: submittedResponse1, serverResponse: serverResponse1 } = + await holderTenant.modules.openId4VcHolder.acceptSiopAuthorizationRequest({ + authorizationRequest: resolvedProofRequest1.authorizationRequest, + presentationExchange: { + credentials: selectedCredentials, + }, + }) + + expect(submittedResponse1).toEqual({ + presentation_submission: { + definition_id: 'OpenBadgeCredential', + descriptor_map: [ + { + format: 'jwt_vp', + id: 'OpenBadgeCredentialDescriptor', + path: '$', + path_nested: { + format: 'jwt_vc', + id: 'OpenBadgeCredentialDescriptor', + path: '$.vp.verifiableCredential[0]', + }, + }, + ], + id: expect.any(String), + }, + state: expect.any(String), + vp_token: expect.any(String), + }) + expect(serverResponse1).toMatchObject({ + status: 200, + }) + + // The RP MUST validate that the aud (audience) Claim contains the value of the client_id + // that the RP sent in the Authorization Request as an audience. + // When the request has been signed, the value might be an HTTPS URL, or a Decentralized Identifier. + const verifierTenant1_2 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier1.tenantId }) + await waitForVerificationSessionRecordSubject(verifier.replaySubject, { + contextCorrelationId: verifierTenant1_2.context.contextCorrelationId, + state: OpenId4VcVerificationSessionState.ResponseVerified, + verificationSessionId: verificationSession1.id, + }) + + const { idToken: idToken1, presentationExchange: presentationExchange1 } = + await verifierTenant1_2.modules.openId4VcVerifier.getVerifiedAuthorizationResponse(verificationSession1.id) + + expect(idToken1).toBeUndefined() + expect(presentationExchange1).toMatchObject({ + definition: openBadgePresentationDefinition, + submission: { + definition_id: 'OpenBadgeCredential', + }, + presentations: [ + { + verifiableCredential: [ + { + type: ['VerifiableCredential', 'OpenBadgeCredential'], + }, + ], + }, + ], + }) + + const selectedCredentials2 = presentationExchangeService.selectCredentialsForRequest( + resolvedProofRequest2.presentationExchange.credentialsForRequest + ) + + const { serverResponse: serverResponse2 } = + await holderTenant.modules.openId4VcHolder.acceptSiopAuthorizationRequest({ + authorizationRequest: resolvedProofRequest2.authorizationRequest, + presentationExchange: { + credentials: selectedCredentials2, + }, + }) + expect(serverResponse2).toMatchObject({ + status: 200, + }) + + // The RP MUST validate that the aud (audience) Claim contains the value of the client_id + // that the RP sent in the Authorization Request as an audience. + // When the request has been signed, the value might be an HTTPS URL, or a Decentralized Identifier. + const verifierTenant2_2 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier2.tenantId }) + await waitForVerificationSessionRecordSubject(verifier.replaySubject, { + contextCorrelationId: verifierTenant2_2.context.contextCorrelationId, + state: OpenId4VcVerificationSessionState.ResponseVerified, + verificationSessionId: verificationSession2.id, + }) + const { idToken: idToken2, presentationExchange: presentationExchange2 } = + await verifierTenant2_2.modules.openId4VcVerifier.getVerifiedAuthorizationResponse(verificationSession2.id) + expect(idToken2).toBeUndefined() + + expect(presentationExchange2).toMatchObject({ + definition: universityDegreePresentationDefinition, + submission: { + definition_id: 'UniversityDegreeCredential', + }, + presentations: [ + { + verifiableCredential: [ + { + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + }, + ], + }, + ], + }) + }) +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 13c3e021d5..c6f35137cb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -692,6 +692,9 @@ importers: '@credo-ts/core': specifier: workspace:* version: link:../core + '@openid-federation/core': + specifier: 0.1.1-alpha.5 + version: 0.1.1-alpha.5 '@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) @@ -2224,6 +2227,9 @@ 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==} + '@peculiar/asn1-cms@2.3.13': resolution: {integrity: sha512-joqu8A7KR2G85oLPq+vB+NFr2ro7Ls4ol13Zcse/giPSzUNN0n2k3v8kMpf6QdGUhI13e5SzQYN8AKP8sJ8v4w==} @@ -2524,6 +2530,9 @@ 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==} @@ -9802,6 +9811,11 @@ snapshots: dependencies: semver: 7.6.3 + '@openid-federation/core@0.1.1-alpha.5': + dependencies: + buffer: 6.0.3 + zod: 3.23.8 + '@peculiar/asn1-cms@2.3.13': dependencies: '@peculiar/asn1-schema': 2.3.13 @@ -10707,6 +10721,16 @@ 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 @@ -12542,7 +12566,7 @@ snapshots: debug: 4.3.6 enhanced-resolve: 5.17.1 eslint: 8.57.0 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.6 @@ -12554,7 +12578,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): + eslint-module-utils@2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): dependencies: debug: 3.2.7 optionalDependencies: @@ -12575,7 +12599,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) hasown: 2.0.2 is-core-module: 2.15.0 is-glob: 4.0.3 From bcaed4d3ef056ccfce10d6090621f6d04d406c26 Mon Sep 17 00:00:00 2001 From: Tom Lanser Date: Wed, 6 Nov 2024 20:49:29 +0100 Subject: [PATCH 02/14] 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 | 42 +++++++++++-------- 8 files changed, 103 insertions(+), 44 deletions(-) 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 From 1743fb1b3a9a63badcd234fa51fd1ddfaf61f4e7 Mon Sep 17 00:00:00 2001 From: Tom Lanser Date: Thu, 7 Nov 2024 11:42:02 +0100 Subject: [PATCH 03/14] fix: typescript error Signed-off-by: Tom Lanser --- packages/openid4vc/src/shared/utils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/openid4vc/src/shared/utils.ts b/packages/openid4vc/src/shared/utils.ts index 1ccc968a54..de0f72588b 100644 --- a/packages/openid4vc/src/shared/utils.ts +++ b/packages/openid4vc/src/shared/utils.ts @@ -184,6 +184,7 @@ export function getCreateJwtCallback( return jws } + // @ts-expect-error - All methods are supported currently so there is no unsupported method anymore throw new Error(`Unsupported jwt issuer method '${jwtIssuer.method}'`) } } From dcd810d22a398ac340755d8365302d69ca988eb7 Mon Sep 17 00:00:00 2001 From: Tom Lanser Date: Mon, 18 Nov 2024 12:37:18 +0100 Subject: [PATCH 04/14] feat: Processed feedback and used the right keys for the verifier Signed-off-by: Tom Lanser --- packages/openid4vc/package.json | 2 +- .../OpenId4vcSiopHolderService.ts | 34 +++- .../OpenId4vcSiopHolderServiceOptions.ts | 3 +- .../openid4vc-issuer/OpenId4VcIssuerModule.ts | 6 +- .../router/federationEndpoint.ts | 64 ++++--- .../OpenId4VcSiopVerifierService.ts | 15 +- .../OpenId4VcVerifierModule.ts | 7 +- .../OpenId4VcVerifierModuleConfig.ts | 13 -- .../router/federationEndpoint.ts | 176 ++++++++++-------- packages/openid4vc/src/shared/federation.ts | 9 - packages/openid4vc/src/shared/index.ts | 1 - .../src/shared/models/OpenId4VcJwtIssuer.ts | 3 +- packages/openid4vc/src/shared/utils.ts | 53 ++++-- .../tests/openid4vc-federation.e2e.test.ts | 28 +-- pnpm-lock.yaml | 28 +-- 15 files changed, 225 insertions(+), 217 deletions(-) delete mode 100644 packages/openid4vc/src/shared/federation.ts diff --git a/packages/openid4vc/package.json b/packages/openid4vc/package.json index 1e0e13d9b2..4bba471c05 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.6", + "@openid-federation/core": "0.1.1-alpha.12", "class-transformer": "^0.5.1", "rxjs": "^7.8.0" }, diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts index f84200ef87..49cf592f7c 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts @@ -4,7 +4,7 @@ import type { OpenId4VcSiopResolveAuthorizationRequestOptions, OpenId4VcSiopResolvedAuthorizationRequest, } from './OpenId4vcSiopHolderServiceOptions' -import type { OpenId4VcJwtIssuer } from '../shared' +import type { OpenId4VcJwtIssuer, OpenId4VcJwtIssuerFederation } from '../shared' import type { AgentContext, JwkJson, VerifiablePresentation } from '@credo-ts/core' import type { AuthorizationResponsePayload, @@ -28,7 +28,9 @@ import { injectable, parseDid, MdocDeviceResponse, + JwsService, } from '@credo-ts/core' +import { fetchEntityConfiguration } from '@openid-federation/core' import { OP, ResponseIss, ResponseMode, ResponseType, SupportedVersion, VPTokenLocation } from '@sphereon/did-auth-siop' import { getSphereonVerifiablePresentation } from '../shared/transform' @@ -64,6 +66,34 @@ export class OpenId4VcSiopHolderService { const presentationDefinition = verifiedAuthorizationRequest.presentationDefinitions?.[0]?.definition + if (verifiedAuthorizationRequest.clientIdScheme === 'entity_id') { + const clientId = verifiedAuthorizationRequest.authorizationRequestPayload.client_id + if (!clientId) { + throw new CredoError("Unable to extract 'client_id' from authorization request") + } + + const jwsService = agentContext.dependencyManager.resolve(JwsService) + + const entityConfiguration = await fetchEntityConfiguration({ + entityId: clientId, + verifyJwtCallback: async ({ jwt, jwk }) => { + const res = await jwsService.verifyJws(agentContext, { + jws: jwt, + jwkResolver: () => getJwkFromJson(jwk), + }) + + return res.isValid + }, + }) + if (!entityConfiguration) throw new CredoError(`Unable to fetch entity configuration for entityId '${clientId}'`) + + const openidRelyingPartyMetadata = entityConfiguration.metadata?.openid_relying_party + // When the metadata is present in the federation we want to use that instead of what is passed with the request + if (openidRelyingPartyMetadata) { + verifiedAuthorizationRequest.authorizationRequestPayload.client_metadata = openidRelyingPartyMetadata + } + } + return { authorizationRequest: verifiedAuthorizationRequest, @@ -261,7 +291,7 @@ export class OpenId4VcSiopHolderService { private getOpenIdTokenIssuerFromVerifiablePresentation( verifiablePresentation: VerifiablePresentation - ): OpenId4VcJwtIssuer { + ): Exclude { let openIdTokenIssuer: OpenId4VcJwtIssuer if (verifiablePresentation instanceof W3cJsonLdVerifiablePresentation) { diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderServiceOptions.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderServiceOptions.ts index 40a5d48d69..04aa764610 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderServiceOptions.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderServiceOptions.ts @@ -2,6 +2,7 @@ import type { OpenId4VcJwtIssuer, OpenId4VcSiopVerifiedAuthorizationRequest, OpenId4VcSiopAuthorizationResponsePayload, + OpenId4VcJwtIssuerFederation, } from '../shared' import type { DifPexCredentialsForRequest, @@ -42,7 +43,7 @@ export interface OpenId4VcSiopAcceptAuthorizationRequestOptions { * In case presentation exchange is used, and `openIdTokenIssuer` is not provided, the issuer of the ID Token * will be extracted from the signer of the first verifiable presentation. */ - openIdTokenIssuer?: OpenId4VcJwtIssuer + openIdTokenIssuer?: Exclude /** * The verified authorization request. diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts index 7d7e199a03..083297286b 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts @@ -120,11 +120,7 @@ export class OpenId4VcIssuerModule implements Module { configureCredentialOfferEndpoint(endpointRouter, this.config.credentialOfferEndpoint) configureAccessTokenEndpoint(endpointRouter, this.config.accessTokenEndpoint) configureCredentialEndpoint(endpointRouter, this.config.credentialEndpoint) - - // The federation endpoint is optional - if (this.config.federationEndpoint) { - configureFederationEndpoint(endpointRouter, this.config.federationEndpoint) - } + configureFederationEndpoint(endpointRouter) // First one will be called for all requests (when next is called) contextRouter.use(async (req: OpenId4VcIssuanceRequest, _res: unknown, next) => { diff --git a/packages/openid4vc/src/openid4vc-issuer/router/federationEndpoint.ts b/packages/openid4vc/src/openid4vc-issuer/router/federationEndpoint.ts index 15f656a32c..dacab4c2e9 100644 --- a/packages/openid4vc/src/openid4vc-issuer/router/federationEndpoint.ts +++ b/packages/openid4vc/src/openid4vc-issuer/router/federationEndpoint.ts @@ -1,38 +1,28 @@ import type { OpenId4VcIssuanceRequest } from './requestContext' -import type { FederationKeyCallback } from '../../shared/federation' import type { Buffer } from '@credo-ts/core' import type { Router, Response } from 'express' -import { getJwkFromKey } from '@credo-ts/core' +import { Key, getJwkFromKey, KeyType } from '@credo-ts/core' import { createEntityConfiguration } from '@openid-federation/core' import { getRequestContext, sendErrorResponse } from '../../shared/router' import { OpenId4VcIssuerService } from '../OpenId4VcIssuerService' -export interface OpenId4VcSiopFederationEndpointConfig { - /** - * The path at which the credential endpoint should be made available. Note that it will be - * hosted at a subpath to take into account multiple tenants and issuers. - * - * @default /.well-known/openid-federation - */ - endpointPath: string - - // TODO: Not sure about the property name yet. - //TODO: More information is needed than only the key also the client id etc - keyCallback: FederationKeyCallback<{ - issuerId: string - }> -} - // TODO: It's also possible that the issuer and the verifier can have the same openid-federation endpoint. In that case we need to combine them. -export function configureFederationEndpoint(router: Router, config: OpenId4VcSiopFederationEndpointConfig) { - router.get(config.endpointPath, async (request: OpenId4VcIssuanceRequest, response: Response, next) => { +export function configureFederationEndpoint(router: Router) { + // TODO: this whole result needs to be cached and the ttl should be the expires of this node + + router.get('/.well-known/openid-federation', async (request: OpenId4VcIssuanceRequest, response: Response, next) => { const { agentContext, issuer } = getRequestContext(request) const openId4VcIssuerService = agentContext.dependencyManager.resolve(OpenId4VcIssuerService) try { + // TODO: Should be only created once per issuer and be used between instances + const federationKey = await agentContext.wallet.createKey({ + keyType: KeyType.Ed25519, + }) + const issuerMetadata = openId4VcIssuerService.getIssuerMetadata(agentContext, issuer) // TODO: Use a type here from sphreon const transformedMetadata = { @@ -50,16 +40,17 @@ export function configureFederationEndpoint(router: Router, config: OpenId4VcSio const now = new Date() const expires = new Date(now.getTime() + 1000 * 60 * 60 * 24) // 1 day from now - const { key } = await config.keyCallback(agentContext, { - issuerId: issuer.issuerId, - }) + // TODO: We need to generate a key and always use that for the entity configuration + + const jwk = getJwkFromKey(federationKey) - const jwk = getJwkFromKey(key) - const kid = 'key-1' + const kid = federationKey.fingerprint const alg = jwk.supportedSignatureAlgorithms[0] const issuerDisplay = issuerMetadata.issuerDisplay?.[0] + const accessTokenSigningKey = Key.fromFingerprint(issuer.accessTokenPublicKeyFingerprint) + const entityConfiguration = await createEntityConfiguration({ claims: { sub: issuerMetadata.issuerUrl, @@ -72,11 +63,23 @@ export function configureFederationEndpoint(router: Router, config: OpenId4VcSio metadata: { federation_entity: issuerDisplay ? { - organization_name: issuerDisplay.organization_name, - logo_uri: issuerDisplay.logo_uri, + organization_name: issuerDisplay.name, + logo_uri: issuerDisplay.logo?.url, } : undefined, - openid_credential_issuer: transformedMetadata, + openid_provider: { + ...transformedMetadata, + client_registration_types_supported: ['automatic'], + jwks: { + keys: [ + { + // TODO: Not 100% sure if this is the right key that we want to expose here or a different one + kid: accessTokenSigningKey.fingerprint, + ...getJwkFromKey(accessTokenSigningKey).toJson(), + }, + ], + }, + }, }, }, header: { @@ -87,12 +90,15 @@ export function configureFederationEndpoint(router: Router, config: OpenId4VcSio signJwtCallback: ({ toBeSigned }) => agentContext.wallet.sign({ data: toBeSigned as Buffer, - key, + key: federationKey, }), }) response.writeHead(200, { 'Content-Type': 'application/entity-statement+jwt' }).end(entityConfiguration) } catch (error) { + agentContext.config.logger.error('Failed to create entity configuration', { + error, + }) sendErrorResponse(response, agentContext.config.logger, 500, 'invalid_request', error) } diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts index ef1b7319ea..2980c22b83 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts @@ -107,12 +107,19 @@ export class OpenId4VcSiopVerifierService { this.config.authorizationEndpoint.endpointPath, ]) + const federationClientId = joinUriParts(this.config.baseUrl, [options.verifier.verifierId]) + const jwtIssuer = options.requestSigner.method === 'x5c' ? await openIdTokenIssuerToJwtIssuer(agentContext, { ...options.requestSigner, issuer: authorizationResponseUrl, }) + : options.requestSigner.method === 'openid-federation' + ? await openIdTokenIssuerToJwtIssuer(agentContext, { + ...options.requestSigner, + clientId: federationClientId, + }) : await openIdTokenIssuerToJwtIssuer(agentContext, options.requestSigner) let clientIdScheme: ClientIdScheme @@ -144,16 +151,12 @@ export class OpenId4VcSiopVerifierService { 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') - throw new CredoError(`Custom jwtIssuer's clientId must be a string.`) clientIdScheme = 'entity_id' - clientId = jwtIssuer.options.clientId + clientId = federationClientId } else { throw new CredoError( - `Unsupported jwt issuer method '${options.requestSigner.method}'. Only 'did' and 'x5c' are supported.` + `Unsupported jwt issuer method '${options.requestSigner.method}'. Only 'did', 'x5c' and 'custom' are supported.` ) } diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts index 9c81150c2e..264709bfb1 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts @@ -115,9 +115,10 @@ export class OpenId4VcVerifierModule implements Module { // Configure endpoints configureAuthorizationEndpoint(endpointRouter, this.config.authorizationEndpoint) configureAuthorizationRequestEndpoint(endpointRouter, this.config.authorizationRequestEndpoint) - if (this.config.federationEndpoint) { - configureFederationEndpoint(endpointRouter, this.config.federationEndpoint) - } + + // TODO: The keys needs to be passed down to the federation endpoint to be used in the entity configuration for the openid relying party + // TODO: But the keys also needs to be available for the request signing. They also needs to get saved because it needs to survive a restart of the agent. + configureFederationEndpoint(endpointRouter) // First one will be called for all requests (when next is called) contextRouter.use(async (req: OpenId4VcVerificationRequest, _res: unknown, next) => { diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModuleConfig.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModuleConfig.ts index 0360b3cf14..b2ec763cbc 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModuleConfig.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModuleConfig.ts @@ -1,6 +1,5 @@ import type { OpenId4VcSiopAuthorizationEndpointConfig } from './router/authorizationEndpoint' import type { OpenId4VcSiopAuthorizationRequestEndpointConfig } from './router/authorizationRequestEndpoint' -import type { OpenId4VcSiopFederationEndpointConfig } from './router/federationEndpoint' import type { Optional } from '@credo-ts/core' import type { Router } from 'express' @@ -25,7 +24,6 @@ export interface OpenId4VcVerifierModuleConfigOptions { endpoints?: { authorization?: Optional authorizationRequest?: Optional - federation?: Optional } } @@ -62,15 +60,4 @@ export class OpenId4VcVerifierModuleConfig { endpointPath: userOptions?.endpointPath ?? '/authorize', } } - - public get federationEndpoint(): OpenId4VcSiopFederationEndpointConfig | undefined { - // Use user supplied options, or return defaults. - const userOptions = this.options.endpoints?.federation - if (!userOptions) return undefined - - return { - ...userOptions, - endpointPath: userOptions.endpointPath ?? '/.well-known/openid-federation', - } - } } diff --git a/packages/openid4vc/src/openid4vc-verifier/router/federationEndpoint.ts b/packages/openid4vc/src/openid4vc-verifier/router/federationEndpoint.ts index 0e02d97271..e3fa74fcea 100644 --- a/packages/openid4vc/src/openid4vc-verifier/router/federationEndpoint.ts +++ b/packages/openid4vc/src/openid4vc-verifier/router/federationEndpoint.ts @@ -1,9 +1,9 @@ import type { OpenId4VcVerificationRequest } from './requestContext' -import type { FederationKeyCallback } from '../../shared/federation' +import type { Key, Buffer } from '@credo-ts/core' import type { RPRegistrationMetadataPayload } from '@sphereon/did-auth-siop' import type { Router, Response } from 'express' -import { getJwkFromKey, type Buffer } from '@credo-ts/core' +import { getJwkFromKey, KeyType } from '@credo-ts/core' import { createEntityConfiguration } from '@openid-federation/core' import { LanguageTagUtils, removeNullUndefined } from '@sphereon/did-auth-siop' @@ -11,22 +11,6 @@ import { getRequestContext, sendErrorResponse } from '../../shared/router' import { OpenId4VcSiopVerifierService } from '../OpenId4VcSiopVerifierService' import { OpenId4VcVerifierModuleConfig } from '../OpenId4VcVerifierModuleConfig' -// TODO: Think about how we can have multiple issuers over the federation endpoint -export interface OpenId4VcSiopFederationEndpointConfig { - /** - * The path at which the authorization request should be made available. Note that it will be - * hosted at a subpath to take into account multiple tenants and verifiers. - * - * @default /.well-known/openid-federation - */ - endpointPath: string - - // TODO: Not sure about the property name yet. - keyCallback: FederationKeyCallback<{ - verifierId: string - }> -} - // TODO: Add types but this function is originally from the @ // eslint-disable-next-line @typescript-eslint/no-explicit-any const createRPRegistrationMetadataPayload = (opts: any): RPRegistrationMetadataPayload => { @@ -63,71 +47,101 @@ const createRPRegistrationMetadataPayload = (opts: any): RPRegistrationMetadataP return removeNullUndefined(rpRegistrationMetadataPayload) } -export function configureFederationEndpoint(router: Router, config: OpenId4VcSiopFederationEndpointConfig) { - router.get(config.endpointPath, async (request: OpenId4VcVerificationRequest, response: Response, next) => { - const { agentContext, verifier } = getRequestContext(request) - const verifierService = agentContext.dependencyManager.resolve(OpenId4VcSiopVerifierService) - const verifierConfig = agentContext.dependencyManager.resolve(OpenId4VcVerifierModuleConfig) - - try { - const { key } = await config.keyCallback(agentContext, { - verifierId: verifier.verifierId, - }) - - const relyingParty = await verifierService.getRelyingParty(agentContext, verifier.verifierId, { - clientId: verifierConfig.baseUrl, - clientIdScheme: 'entity_id', - authorizationResponseUrl: `${verifierConfig.baseUrl}/siop/${verifier.verifierId}/authorize`, - }) - - const verifierEntityId = `${verifierConfig.baseUrl}/${verifier.verifierId}` - - const rpMetadata = createRPRegistrationMetadataPayload(relyingParty.createRequestOptions.clientMetadata) - - // TODO: We also need to cache the entity configuration until it expires - const now = new Date() - // TODO: We also need to check if the x509 certificate is still valid until this expires - const expires = new Date(now.getTime() + 1000 * 60 * 60 * 24) // 1 day - - const jwk = getJwkFromKey(key) - const alg = jwk.supportedSignatureAlgorithms[0] - const kid = 'key-1' - - const entityConfiguration = await createEntityConfiguration({ - header: { - kid, - alg, - typ: 'entity-statement+jwt', - }, - claims: { - sub: verifierEntityId, - iss: verifierEntityId, - iat: now, - exp: expires, - jwks: { - keys: [{ kid, alg, ...jwk.toJson() }], +export function configureFederationEndpoint(router: Router) { + // TODO: this whole result needs to be cached and the ttl should be the expires of this node + + // TODO: This will not work for multiple instances so we have to save it in the database. + const federationKeyMapping = new Map() + const rpSigningKeyMapping = new Map() + + router.get( + '/.well-known/openid-federation', + async (request: OpenId4VcVerificationRequest, response: Response, next) => { + const { agentContext, verifier } = getRequestContext(request) + const verifierService = agentContext.dependencyManager.resolve(OpenId4VcSiopVerifierService) + const verifierConfig = agentContext.dependencyManager.resolve(OpenId4VcVerifierModuleConfig) + + try { + let federationKey = federationKeyMapping.get(verifier.verifierId) + if (!federationKey) { + federationKey = await agentContext.wallet.createKey({ + keyType: KeyType.Ed25519, + }) + federationKeyMapping.set(verifier.verifierId, federationKey) + } + + let rpSigningKey = rpSigningKeyMapping.get(verifier.verifierId) + if (!rpSigningKey) { + rpSigningKey = await agentContext.wallet.createKey({ + keyType: KeyType.Ed25519, + }) + rpSigningKeyMapping.set(verifier.verifierId, rpSigningKey) + } + + const relyingParty = await verifierService.getRelyingParty(agentContext, verifier.verifierId, { + clientId: verifierConfig.baseUrl, + clientIdScheme: 'entity_id', + authorizationResponseUrl: `${verifierConfig.baseUrl}/siop/${verifier.verifierId}/authorize`, + }) + + const verifierEntityId = `${verifierConfig.baseUrl}/${verifier.verifierId}` + + const rpMetadata = createRPRegistrationMetadataPayload(relyingParty.createRequestOptions.clientMetadata) + + // TODO: We also need to cache the entity configuration until it expires + const now = new Date() + // TODO: We also need to check if the x509 certificate is still valid until this expires + const expires = new Date(now.getTime() + 1000 * 60 * 60 * 24) // 1 day + + const jwk = getJwkFromKey(federationKey) + const alg = jwk.supportedSignatureAlgorithms[0] + const kid = federationKey.fingerprint + + const entityConfiguration = await createEntityConfiguration({ + header: { + kid, + alg, + typ: 'entity-statement+jwt', }, - metadata: { - federation_entity: { - organization_name: rpMetadata.client_name, - logo_uri: rpMetadata.logo_uri, + claims: { + sub: verifierEntityId, + iss: verifierEntityId, + iat: now, + exp: expires, + jwks: { + keys: [{ kid, alg, ...jwk.toJson() }], + }, + metadata: { + federation_entity: { + organization_name: rpMetadata.client_name, + logo_uri: rpMetadata.logo_uri, + }, + openid_relying_party: { + ...rpMetadata, + jwks: { + keys: [{ kid, alg, ...getJwkFromKey(rpSigningKey).toJson() }], + }, + client_registration_types: ['automatic'], // TODO: Not really sure why we need to provide this manually + }, }, - openid_credential_verifier: rpMetadata, }, - }, - signJwtCallback: ({ toBeSigned }) => - agentContext.wallet.sign({ - data: toBeSigned as Buffer, - key, - }), - }) - - response.writeHead(200, { 'Content-Type': 'application/entity-statement+jwt' }).end(entityConfiguration) - } catch (error) { - sendErrorResponse(response, agentContext.config.logger, 500, 'invalid_request', error) + signJwtCallback: ({ toBeSigned }) => + agentContext.wallet.sign({ + data: toBeSigned as Buffer, + key: federationKey, + }), + }) + + response.writeHead(200, { 'Content-Type': 'application/entity-statement+jwt' }).end(entityConfiguration) + } catch (error) { + agentContext.config.logger.error('Failed to create entity configuration', { + error, + }) + sendErrorResponse(response, agentContext.config.logger, 500, 'invalid_request', error) + } + + // NOTE: if we don't call next, the agentContext session handler will NOT be called + next() } - - // NOTE: if we don't call next, the agentContext session handler will NOT be called - next() - }) + ) } diff --git a/packages/openid4vc/src/shared/federation.ts b/packages/openid4vc/src/shared/federation.ts deleted file mode 100644 index fefafdb426..0000000000 --- a/packages/openid4vc/src/shared/federation.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { AgentContext, Key } from '@credo-ts/core' - -// TODO: Not really sure about this type yet but it's a start. -export type FederationKeyCallback = Record> = ( - agentContext: AgentContext, - context: TContext -) => Promise<{ - key: Key -}> diff --git a/packages/openid4vc/src/shared/index.ts b/packages/openid4vc/src/shared/index.ts index 3e3b26e4ce..8eacb927b2 100644 --- a/packages/openid4vc/src/shared/index.ts +++ b/packages/openid4vc/src/shared/index.ts @@ -1,3 +1,2 @@ export * from './models' export * from './issuerMetadataUtils' -export * from './federation' diff --git a/packages/openid4vc/src/shared/models/OpenId4VcJwtIssuer.ts b/packages/openid4vc/src/shared/models/OpenId4VcJwtIssuer.ts index c3fada42d4..49c296211b 100644 --- a/packages/openid4vc/src/shared/models/OpenId4VcJwtIssuer.ts +++ b/packages/openid4vc/src/shared/models/OpenId4VcJwtIssuer.ts @@ -30,9 +30,8 @@ interface OpenId4VcJwtIssuerJwk { jwk: Jwk } -interface OpenId4VcJwtIssuerFederation { +export interface OpenId4VcJwtIssuerFederation { method: 'openid-federation' - clientId: string } export type OpenId4VcJwtIssuer = diff --git a/packages/openid4vc/src/shared/utils.ts b/packages/openid4vc/src/shared/utils.ts index de0f72588b..d85f823060 100644 --- a/packages/openid4vc/src/shared/utils.ts +++ b/packages/openid4vc/src/shared/utils.ts @@ -1,4 +1,4 @@ -import type { OpenId4VcIssuerX5c, OpenId4VcJwtIssuer } from './models' +import type { OpenId4VcIssuerX5c, OpenId4VcJwtIssuer, OpenId4VcJwtIssuerFederation } 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 } from '@sphereon/oid4vc-common' @@ -10,7 +10,6 @@ import { JwsService, JwtPayload, SignatureSuiteRegistry, - TypedArrayEncoder, X509Service, getDomainFromUrl, getJwkClassFromKeyType, @@ -86,24 +85,35 @@ export function getVerifyJwtCallback( const validTrustChains = await resolveTrustChains({ entityId, trustAnchorEntityIds: trustedEntityIds, - verifyJwtCallback: async ({ data, signature, jwk }) => { - const jws = `${TypedArrayEncoder.toUtf8String(data)}.${TypedArrayEncoder.toBase64URL(signature)}` - + verifyJwtCallback: async ({ jwt, jwk }) => { const res = await jwsService.verifyJws(agentContext, { - jws, + jws: jwt, jwkResolver: () => getJwkFromJson(jwk), }) return res.isValid }, }) + // When the chain is already invalid we can return false immediately + if (validTrustChains.length === 0) return false + + // Pick the first valid trust chain for validation of the leaf entity jwks + const { entityConfiguration } = validTrustChains[0] + // TODO: No support yet for signed jwks and external jwks + const rpSigningKeys = entityConfiguration?.metadata?.openid_relying_party?.jwks?.keys + if (!rpSigningKeys || rpSigningKeys.length === 0) + throw new CredoError('No rp signing keys found in the entity configuration.') + + const res = await jwsService.verifyJws(agentContext, { + jws: jwt.raw, + jwkResolver: () => getJwkFromJson(rpSigningKeys[0]), + }) // 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 validTrustChains.length > 0 + return res.isValid } throw new Error(`Unsupported jwt verifier method: '${jwtVerifier.method}'`) @@ -155,6 +165,7 @@ export function getCreateJwtCallback( } if (jwtIssuer.method === 'custom') { + // TODO: This could be used as the issuer and verifier. Based on that we need to search for a jwk in the entity configuration const { options } = jwtIssuer if (!options) throw new CredoError(`Custom jwtIssuer must have options defined.`) if (!options.clientId) throw new CredoError(`Custom jwtIssuer must have clientId defined.`) @@ -163,18 +174,27 @@ export function getCreateJwtCallback( const { clientId } = options const entityConfiguration = await fetchEntityConfiguration({ - entityId: clientId as string, - verifyJwtCallback: async ({ data, signature, jwk }) => { - const jws = `${TypedArrayEncoder.toUtf8String(data)}.${TypedArrayEncoder.toBase64URL(signature)}` - const res = await jwsService.verifyJws(agentContext, { jws, jwkResolver: () => getJwkFromJson(jwk) }) + entityId: clientId, + verifyJwtCallback: async ({ jwt, jwk }) => { + const res = await jwsService.verifyJws(agentContext, { jws: jwt, jwkResolver: () => getJwkFromJson(jwk) }) return res.isValid }, }) + // TODO: Not really sure if this is also used for the issuer so if so we need to change this logic. But currently it's not possible to specify a issuer method with issuance so I think it's fine. + + // NOTE: Hardcoded part for the verifier + const openIdRelyingParty = entityConfiguration.metadata?.openid_relying_party + if (!openIdRelyingParty) throw new CredoError('No openid-relying-party found in the entity configuration.') + + // NOTE: No support for signed jwks and external jwks + const jwks = openIdRelyingParty.jwks + if (!jwks) throw new CredoError('No jwks found in the openid-relying-party.') + // 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]) + const jwk = getJwkFromJson(jwks.keys[0]) - // TODO: This gives a weird error when the private key is not available in the wallet + // TODO: This gives a weird error when the private key is not available in the wallet so we should handle that better const jws = await jwsService.createJwsCompact(agentContext, { protectedHeaderOptions: { ...jwt.header, jwk, alg: jwk.supportedSignatureAlgorithms[0] }, payload: JwtPayload.fromJson(jwt.payload), @@ -191,7 +211,10 @@ export function getCreateJwtCallback( export async function openIdTokenIssuerToJwtIssuer( agentContext: AgentContext, - openId4VcTokenIssuer: Exclude | (OpenId4VcIssuerX5c & { issuer: string }) + openId4VcTokenIssuer: + | Exclude + | (OpenId4VcIssuerX5c & { issuer: string }) + | (OpenId4VcJwtIssuerFederation & { clientId: string }) ): Promise { if (openId4VcTokenIssuer.method === 'did') { const key = await getKeyFromDid(agentContext, openId4VcTokenIssuer.didUrl) diff --git a/packages/openid4vc/tests/openid4vc-federation.e2e.test.ts b/packages/openid4vc/tests/openid4vc-federation.e2e.test.ts index 7eb667a143..3abd06dc9e 100644 --- a/packages/openid4vc/tests/openid4vc-federation.e2e.test.ts +++ b/packages/openid4vc/tests/openid4vc-federation.e2e.test.ts @@ -7,12 +7,10 @@ import { DidsApi, DifPresentationExchangeService, JwaSignatureAlgorithm, - KeyType, W3cCredential, W3cCredentialSubject, w3cDate, W3cIssuer, - WalletApi, X509Module, X509ModuleConfig, } from '@credo-ts/core' @@ -41,6 +39,8 @@ const baseUrl = `http://localhost:${serverPort}` const issuanceBaseUrl = `${baseUrl}/oid4vci` const verificationBaseUrl = `${baseUrl}/oid4vp` +// TODO: Add tests for invalid configurations so unhappy tests + describe('OpenId4Vc', () => { let expressApp: Express let expressServer: Server @@ -124,16 +124,6 @@ describe('OpenId4Vc', () => { } }, }, - federation: { - keyCallback: async (agentContext) => { - const walletApi = agentContext.dependencyManager.resolve(WalletApi) - const key = await walletApi.createKey({ keyType: KeyType.Ed25519 }) - - return { - key, - } - }, - }, }, }), askar: new AskarModule(askarModuleConfig), @@ -159,18 +149,6 @@ describe('OpenId4Vc', () => { { openId4VcVerifier: new OpenId4VcVerifierModule({ baseUrl: verificationBaseUrl, - endpoints: { - federation: { - keyCallback: async (agentContext) => { - const walletApi = agentContext.dependencyManager.resolve(WalletApi) - const key = await walletApi.createKey({ keyType: KeyType.Ed25519 }) - - return { - key, - } - }, - }, - }, }), askar: new AskarModule(askarModuleConfig), tenants: new TenantsModule(), @@ -237,7 +215,6 @@ describe('OpenId4Vc', () => { verifierId: openIdVerifierTenant1.verifierId, requestSigner: { method: 'openid-federation', - clientId: `http://localhost:1234/oid4vp/${openIdVerifierTenant1.verifierId}`, }, presentationExchange: { definition: openBadgePresentationDefinition, @@ -254,7 +231,6 @@ describe('OpenId4Vc', () => { await verifierTenant2.modules.openId4VcVerifier.createAuthorizationRequest({ requestSigner: { method: 'openid-federation', - clientId: `http://localhost:1234/oid4vp/${openIdVerifierTenant2.verifierId}`, }, presentationExchange: { definition: universityDegreePresentationDefinition, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1601d97c54..b1b08f440b 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.6 - version: 0.1.1-alpha.6 + specifier: 0.1.1-alpha.12 + version: 0.1.1-alpha.12 '@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.6': - resolution: {integrity: sha512-ipQtZYtFMUr2BvUmOxlQNVF7eILEq8isoO7rDYwIj4xafifdPAMxznzDxqlu3sHqbOO49PRDRjo9ESsHUfJLfg==} + '@openid-federation/core@0.1.1-alpha.12': + resolution: {integrity: sha512-pGEt0Zz0Y+l0mlayeT5oeHILd0XKmzfpgVJcKM/DgBYaMTa8MdEdVZj6GLpBIqZWHzxoJXM+DB6OeNi9EemAlQ==} '@peculiar/asn1-cms@2.3.13': resolution: {integrity: sha512-joqu8A7KR2G85oLPq+vB+NFr2ro7Ls4ol13Zcse/giPSzUNN0n2k3v8kMpf6QdGUhI13e5SzQYN8AKP8sJ8v4w==} @@ -9809,7 +9809,7 @@ snapshots: dependencies: semver: 7.6.3 - '@openid-federation/core@0.1.1-alpha.6': + '@openid-federation/core@0.1.1-alpha.12': dependencies: buffer: 6.0.3 zod: 3.23.8 @@ -10522,25 +10522,7 @@ 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: From cb6d70ffa2d466e2c283432bc6331adf8fe2a2a5 Mon Sep 17 00:00:00 2001 From: Tom Lanser Date: Mon, 18 Nov 2024 14:51:43 +0100 Subject: [PATCH 05/14] feat: Added more logging and added unhappy tests Signed-off-by: Tom Lanser --- .../OpenId4VcIssuerModuleConfig.ts | 10 -- .../router/federationEndpoint.ts | 26 +++--- .../__tests__/openid4vc-verifier.test.ts | 39 -------- packages/openid4vc/src/shared/utils.ts | 18 +++- .../tests/openid4vc-federation.e2e.test.ts | 92 +++++++++++++++++++ 5 files changed, 118 insertions(+), 67 deletions(-) diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts index 7697cc637c..ae8f7418f1 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts @@ -96,14 +96,4 @@ export class OpenId4VcIssuerModuleConfig { endpointPath: userOptions.endpointPath ?? '/offers', } } - - public get federationEndpoint(): OpenId4VcSiopFederationEndpointConfig | undefined { - const userOptions = this.options.endpoints.federation - if (!userOptions) return undefined - - return { - ...userOptions, - endpointPath: userOptions.endpointPath ?? '/.well-known/openid-federation', - } - } } diff --git a/packages/openid4vc/src/openid4vc-issuer/router/federationEndpoint.ts b/packages/openid4vc/src/openid4vc-issuer/router/federationEndpoint.ts index dacab4c2e9..788dea94f3 100644 --- a/packages/openid4vc/src/openid4vc-issuer/router/federationEndpoint.ts +++ b/packages/openid4vc/src/openid4vc-issuer/router/federationEndpoint.ts @@ -24,18 +24,6 @@ export function configureFederationEndpoint(router: Router) { }) const issuerMetadata = openId4VcIssuerService.getIssuerMetadata(agentContext, issuer) - // TODO: Use a type here from sphreon - const transformedMetadata = { - credential_issuer: issuerMetadata.issuerUrl, - token_endpoint: issuerMetadata.tokenEndpoint, - credential_endpoint: issuerMetadata.credentialEndpoint, - authorization_server: issuerMetadata.authorizationServer, - authorization_servers: issuerMetadata.authorizationServer ? [issuerMetadata.authorizationServer] : undefined, - credentials_supported: issuerMetadata.credentialsSupported, - credential_configurations_supported: issuerMetadata.credentialConfigurationsSupported, - display: issuerMetadata.issuerDisplay, - dpop_signing_alg_values_supported: issuerMetadata.dpopSigningAlgValuesSupported, - } as const const now = new Date() const expires = new Date(now.getTime() + 1000 * 60 * 60 * 24) // 1 day from now @@ -68,7 +56,19 @@ export function configureFederationEndpoint(router: Router) { } : undefined, openid_provider: { - ...transformedMetadata, + // TODO: The type isn't correct yet down the line so that needs to be updated before + // credential_issuer: issuerMetadata.issuerUrl, + // token_endpoint: issuerMetadata.tokenEndpoint, + // credential_endpoint: issuerMetadata.credentialEndpoint, + // authorization_server: issuerMetadata.authorizationServer, + // authorization_servers: issuerMetadata.authorizationServer + // ? [issuerMetadata.authorizationServer] + // : undefined, + // credentials_supported: issuerMetadata.credentialsSupported, + // credential_configurations_supported: issuerMetadata.credentialConfigurationsSupported, + // display: issuerMetadata.issuerDisplay, + // dpop_signing_alg_values_supported: issuerMetadata.dpopSigningAlgValuesSupported, + client_registration_types_supported: ['automatic'], jwks: { keys: [ diff --git a/packages/openid4vc/src/openid4vc-verifier/__tests__/openid4vc-verifier.test.ts b/packages/openid4vc/src/openid4vc-verifier/__tests__/openid4vc-verifier.test.ts index 883b5cb8a7..e40ef70579 100644 --- a/packages/openid4vc/src/openid4vc-verifier/__tests__/openid4vc-verifier.test.ts +++ b/packages/openid4vc/src/openid4vc-verifier/__tests__/openid4vc-verifier.test.ts @@ -113,44 +113,5 @@ describe('OpenId4VcVerifier', () => { expect(jwt.payload.iss).toEqual(verifier.did) expect(jwt.payload.sub).toEqual(verifier.did) }) - - it('check openid proof request format (entity id)', async () => { - const openIdVerifier = await verifier.agent.modules.openId4VcVerifier.createVerifier() - const { authorizationRequest, verificationSession } = - await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest({ - requestSigner: { - method: 'openid-federation', - clientId: 'http://localhost:3001/verifier', - }, - verifierId: openIdVerifier.verifierId, - }) - - expect( - authorizationRequest.startsWith( - `openid://?client_id=${encodeURIComponent(verifier.did)}&request_uri=http%3A%2F%2Fredirect-uri%2F${ - openIdVerifier.verifierId - }%2Fauthorization-requests%2F` - ) - ).toBe(true) - - const jwt = Jwt.fromSerializedJwt(verificationSession.authorizationRequestJwt) - - expect(jwt.header.kid) - - expect(jwt.header.kid).toEqual(verifier.kid) - expect(jwt.header.alg).toEqual(SigningAlgo.EDDSA) - expect(jwt.header.typ).toEqual('JWT') - expect(jwt.payload.additionalClaims.scope).toEqual('openid') - expect(jwt.payload.additionalClaims.client_id).toEqual(verifier.did) - expect(jwt.payload.additionalClaims.response_uri).toEqual( - `http://redirect-uri/${openIdVerifier.verifierId}/authorize` - ) - expect(jwt.payload.additionalClaims.response_mode).toEqual('direct_post') - expect(jwt.payload.additionalClaims.nonce).toBeDefined() - expect(jwt.payload.additionalClaims.state).toBeDefined() - expect(jwt.payload.additionalClaims.response_type).toEqual('id_token') - expect(jwt.payload.iss).toEqual(verifier.did) - expect(jwt.payload.sub).toEqual(verifier.did) - }) }) }) diff --git a/packages/openid4vc/src/shared/utils.ts b/packages/openid4vc/src/shared/utils.ts index d85f823060..fb6bbf3ebe 100644 --- a/packages/openid4vc/src/shared/utils.ts +++ b/packages/openid4vc/src/shared/utils.ts @@ -60,6 +60,8 @@ export function getVerifyJwtCallback( agentContext: AgentContext, options: VerifyJwtCallbackOptions = {} ): VerifyJwtCallback { + const logger = agentContext.config.logger + return async (jwtVerifier, jwt) => { const jwsService = agentContext.dependencyManager.resolve(JwsService) @@ -79,8 +81,10 @@ export function getVerifyJwtCallback( if (jwtVerifier.method === 'openid-federation') { const { entityId } = jwtVerifier const trustedEntityIds = options.federation?.trustedEntityIds - if (!trustedEntityIds) - throw new CredoError('No trusted entity ids provided but is required for the openid-federation method.') + if (!trustedEntityIds) { + logger.error('No trusted entity ids provided but is required for the "openid-federation" method.') + return false + } const validTrustChains = await resolveTrustChains({ entityId, @@ -95,7 +99,10 @@ export function getVerifyJwtCallback( }, }) // When the chain is already invalid we can return false immediately - if (validTrustChains.length === 0) return false + if (validTrustChains.length === 0) { + logger.error(`${entityId} is not part of a trusted federation.`) + return false + } // Pick the first valid trust chain for validation of the leaf entity jwks const { entityConfiguration } = validTrustChains[0] @@ -108,11 +115,12 @@ export function getVerifyJwtCallback( jws: jwt.raw, jwkResolver: () => getJwkFromJson(rpSigningKeys[0]), }) + if (!res.isValid) { + logger.error(`${entityId} does not match the expected signing key.`) + } // 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.' - return res.isValid } diff --git a/packages/openid4vc/tests/openid4vc-federation.e2e.test.ts b/packages/openid4vc/tests/openid4vc-federation.e2e.test.ts index 3abd06dc9e..f0f7f874b4 100644 --- a/packages/openid4vc/tests/openid4vc-federation.e2e.test.ts +++ b/packages/openid4vc/tests/openid4vc-federation.e2e.test.ts @@ -424,4 +424,96 @@ describe('OpenId4Vc', () => { ], }) }) + + it('e2e flow with tenants, unhappy flow', async () => { + const holderTenant = await holder.agent.modules.tenants.getTenantAgent({ tenantId: holder1.tenantId }) + const verifierTenant1 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier1.tenantId }) + const verifierTenant2 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier2.tenantId }) + + const openIdVerifierTenant1 = await verifierTenant1.modules.openId4VcVerifier.createVerifier() + const openIdVerifierTenant2 = await verifierTenant2.modules.openId4VcVerifier.createVerifier() + + const signedCredential1 = await issuer.agent.w3cCredentials.signCredential({ + format: ClaimFormat.JwtVc, + credential: new W3cCredential({ + type: ['VerifiableCredential', 'OpenBadgeCredential'], + issuer: new W3cIssuer({ id: issuer.did }), + credentialSubject: new W3cCredentialSubject({ id: holder1.did }), + issuanceDate: w3cDate(Date.now()), + }), + alg: JwaSignatureAlgorithm.EdDSA, + verificationMethod: issuer.verificationMethod.id, + }) + + const signedCredential2 = await issuer.agent.w3cCredentials.signCredential({ + format: ClaimFormat.JwtVc, + credential: new W3cCredential({ + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + issuer: new W3cIssuer({ id: issuer.did }), + credentialSubject: new W3cCredentialSubject({ id: holder1.did }), + issuanceDate: w3cDate(Date.now()), + }), + alg: JwaSignatureAlgorithm.EdDSA, + verificationMethod: issuer.verificationMethod.id, + }) + + await holderTenant.w3cCredentials.storeCredential({ credential: signedCredential1 }) + await holderTenant.w3cCredentials.storeCredential({ credential: signedCredential2 }) + + const { authorizationRequest: authorizationRequestUri1, verificationSession: verificationSession1 } = + await verifierTenant1.modules.openId4VcVerifier.createAuthorizationRequest({ + verifierId: openIdVerifierTenant1.verifierId, + requestSigner: { + method: 'openid-federation', + }, + presentationExchange: { + definition: openBadgePresentationDefinition, + }, + }) + + expect(authorizationRequestUri1).toEqual( + `openid4vp://?client_id=${encodeURIComponent( + `http://localhost:1234/oid4vp/${openIdVerifierTenant1.verifierId}` + )}&request_uri=${encodeURIComponent(verificationSession1.authorizationRequestUri)}` + ) + + const { authorizationRequest: authorizationRequestUri2, verificationSession: verificationSession2 } = + await verifierTenant2.modules.openId4VcVerifier.createAuthorizationRequest({ + requestSigner: { + method: 'openid-federation', + }, + presentationExchange: { + definition: universityDegreePresentationDefinition, + }, + verifierId: openIdVerifierTenant2.verifierId, + }) + + expect(authorizationRequestUri2).toEqual( + `openid4vp://?client_id=${encodeURIComponent( + `http://localhost:1234/oid4vp/${openIdVerifierTenant2.verifierId}` + )}&request_uri=${encodeURIComponent(verificationSession2.authorizationRequestUri)}` + ) + + await verifierTenant1.endSession() + await verifierTenant2.endSession() + + const resolvedProofRequestWithFederationPromise = + holderTenant.modules.openId4VcHolder.resolveSiopAuthorizationRequest(authorizationRequestUri1, { + federation: { + // This will look for a whole different trusted entity + trustedEntityIds: [`http://localhost:1234/oid4vp/${openIdVerifierTenant2.verifierId}`], + }, + }) + + // TODO: Look into this error see if we can make it more specific + await expect(resolvedProofRequestWithFederationPromise).rejects.toThrow( + `Error verifying the DID Auth Token signature.` + ) + + const resolvedProofRequestWithoutFederationPromise = + holderTenant.modules.openId4VcHolder.resolveSiopAuthorizationRequest(authorizationRequestUri2) + await expect(resolvedProofRequestWithoutFederationPromise).rejects.toThrow( + `Error verifying the DID Auth Token signature.` + ) + }) }) From b06c546e59e94180ebfd144ef4503bdfb67a4d37 Mon Sep 17 00:00:00 2001 From: Tom Lanser Date: Mon, 18 Nov 2024 15:29:32 +0100 Subject: [PATCH 06/14] chore: Made some things more logic Signed-off-by: Tom Lanser --- .../OpenId4VcSiopVerifierService.ts | 14 +++++++++----- packages/openid4vc/src/shared/utils.ts | 18 ++++++++++++------ 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts index 2980c22b83..50781786e7 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts @@ -118,7 +118,7 @@ export class OpenId4VcSiopVerifierService { : options.requestSigner.method === 'openid-federation' ? await openIdTokenIssuerToJwtIssuer(agentContext, { ...options.requestSigner, - clientId: federationClientId, + entityId: federationClientId, }) : await openIdTokenIssuerToJwtIssuer(agentContext, options.requestSigner) @@ -150,10 +150,14 @@ 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 - - clientIdScheme = 'entity_id' - clientId = federationClientId + if (jwtIssuer.options?.method === 'openid-federation') { + clientIdScheme = 'entity_id' + clientId = federationClientId + } else { + throw new CredoError( + `jwtIssuer 'method' 'custom' must have a 'method' property with value 'openid-federation' when using the 'custom' method.` + ) + } } else { throw new CredoError( `Unsupported jwt issuer method '${options.requestSigner.method}'. Only 'did', 'x5c' and 'custom' are supported.` diff --git a/packages/openid4vc/src/shared/utils.ts b/packages/openid4vc/src/shared/utils.ts index fb6bbf3ebe..5eea5dd8bc 100644 --- a/packages/openid4vc/src/shared/utils.ts +++ b/packages/openid4vc/src/shared/utils.ts @@ -176,13 +176,18 @@ export function getCreateJwtCallback( // TODO: This could be used as the issuer and verifier. Based on that we need to search for a jwk in the entity configuration const { options } = jwtIssuer if (!options) throw new CredoError(`Custom jwtIssuer must have options defined.`) - 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.`) + if (!options.method) throw new CredoError(`Custom jwtIssuer's options must have a 'method' property defined.`) + if (options.method !== 'openid-federation') + throw new CredoError( + `Custom jwtIssuer's options 'method' property must be 'openid-federation' when using the 'custom' method.` + ) + if (!options.entityId) throw new CredoError(`Custom jwtIssuer must have entityId defined.`) + if (typeof options.entityId !== 'string') throw new CredoError(`Custom jwtIssuer's entityId must be a string.`) - const { clientId } = options + const { entityId } = options const entityConfiguration = await fetchEntityConfiguration({ - entityId: clientId, + entityId, verifyJwtCallback: async ({ jwt, jwk }) => { const res = await jwsService.verifyJws(agentContext, { jws: jwt, jwkResolver: () => getJwkFromJson(jwk) }) return res.isValid @@ -222,7 +227,7 @@ export async function openIdTokenIssuerToJwtIssuer( openId4VcTokenIssuer: | Exclude | (OpenId4VcIssuerX5c & { issuer: string }) - | (OpenId4VcJwtIssuerFederation & { clientId: string }) + | (OpenId4VcJwtIssuerFederation & { entityId: string }) ): Promise { if (openId4VcTokenIssuer.method === 'did') { const key = await getKeyFromDid(agentContext, openId4VcTokenIssuer.didUrl) @@ -283,7 +288,8 @@ export async function openIdTokenIssuerToJwtIssuer( return { method: 'custom', options: { - clientId: openId4VcTokenIssuer.clientId, + method: 'openid-federation', + entityId: openId4VcTokenIssuer.entityId, }, } } From 2b8bde56cb3d44379fedcd72f973027a609b24ae Mon Sep 17 00:00:00 2001 From: Tom Lanser Date: Mon, 18 Nov 2024 16:30:07 +0100 Subject: [PATCH 07/14] feat: Holder side api for getting more context information Signed-off-by: Tom Lanser --- packages/openid4vc/package.json | 2 +- .../openid4vc-holder/OpenId4VcHolderApi.ts | 5 ++++ .../OpenId4VciHolderService.ts | 2 -- .../OpenId4vcSiopHolderService.ts | 30 +++++++++++++++++-- .../OpenId4vcSiopHolderServiceOptions.ts | 5 ++++ .../OpenId4VcIssuerModuleConfig.ts | 2 -- packages/openid4vc/src/shared/utils.ts | 4 +-- pnpm-lock.yaml | 10 +++---- 8 files changed, 46 insertions(+), 14 deletions(-) diff --git a/packages/openid4vc/package.json b/packages/openid4vc/package.json index 4bba471c05..09d3213fa9 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.12", + "@openid-federation/core": "0.1.1-alpha.13", "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 754f029238..4b4db015d2 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts @@ -11,6 +11,7 @@ import type { import type { OpenId4VcSiopAcceptAuthorizationRequestOptions, OpenId4VcSiopResolveAuthorizationRequestOptions, + OpenId4VcSiopResolveTrustChainsOptions, } from './OpenId4vcSiopHolderServiceOptions' import { injectable, AgentContext } from '@credo-ts/core' @@ -187,4 +188,8 @@ export class OpenId4VcHolderApi { public async sendNotification(options: OpenId4VciSendNotificationOptions) { return this.openId4VciHolderService.sendNotification(options) } + + public async resolveOpenIdFederationChains(options: OpenId4VcSiopResolveTrustChainsOptions) { + return this.openId4VcSiopHolderService.resolveOpenIdFederationChains(this.agentContext, options) + } } diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts index c111f30492..e46bed9a18 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts @@ -871,6 +871,4 @@ export class OpenId4VciHolderService { return jws } } - - // TODO: Add a function for resolving the entity statement. Which will be used in the holder to verify the entity statement and to show to the user } diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts index 49cf592f7c..413824a8a8 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts @@ -3,6 +3,7 @@ import type { OpenId4VcSiopGetOpenIdProviderOptions, OpenId4VcSiopResolveAuthorizationRequestOptions, OpenId4VcSiopResolvedAuthorizationRequest, + OpenId4VcSiopResolveTrustChainsOptions, } from './OpenId4vcSiopHolderServiceOptions' import type { OpenId4VcJwtIssuer, OpenId4VcJwtIssuerFederation } from '../shared' import type { AgentContext, JwkJson, VerifiablePresentation } from '@credo-ts/core' @@ -30,7 +31,10 @@ import { MdocDeviceResponse, JwsService, } from '@credo-ts/core' -import { fetchEntityConfiguration } from '@openid-federation/core' +import { + resolveTrustChains as federationResolveTrustChains, + fetchEntityConfiguration as federationFetchEntityConfiguration, +} from '@openid-federation/core' import { OP, ResponseIss, ResponseMode, ResponseType, SupportedVersion, VPTokenLocation } from '@sphereon/did-auth-siop' import { getSphereonVerifiablePresentation } from '../shared/transform' @@ -74,7 +78,7 @@ export class OpenId4VcSiopHolderService { const jwsService = agentContext.dependencyManager.resolve(JwsService) - const entityConfiguration = await fetchEntityConfiguration({ + const entityConfiguration = await federationFetchEntityConfiguration({ entityId: clientId, verifyJwtCallback: async ({ jwt, jwk }) => { const res = await jwsService.verifyJws(agentContext, { @@ -434,4 +438,26 @@ export class OpenId4VcSiopHolderService { return jwe } + + public async resolveOpenIdFederationChains( + agentContext: AgentContext, + options: OpenId4VcSiopResolveTrustChainsOptions + ) { + const jwsService = agentContext.dependencyManager.resolve(JwsService) + + const { entityId, trustAnchorEntityIds } = options + + return federationResolveTrustChains({ + entityId, + trustAnchorEntityIds, + verifyJwtCallback: async ({ jwt, jwk }) => { + const res = await jwsService.verifyJws(agentContext, { + jws: jwt, + jwkResolver: () => getJwkFromJson(jwk), + }) + + return res.isValid + }, + }) + } } diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderServiceOptions.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderServiceOptions.ts index 04aa764610..559e690210 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderServiceOptions.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderServiceOptions.ts @@ -77,3 +77,8 @@ export interface OpenId4VcSiopGetOpenIdProviderOptions { trustedEntityIds?: string[] } } + +export interface OpenId4VcSiopResolveTrustChainsOptions { + entityId: string + trustAnchorEntityIds: [string, ...string[]] +} diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts index ae8f7418f1..71eaa43c9a 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts @@ -3,7 +3,6 @@ import type { OpenId4VciCredentialEndpointConfig, OpenId4VciCredentialOfferEndpointConfig, } from './router' -import type { OpenId4VcSiopFederationEndpointConfig } from './router/federationEndpoint' import type { Optional } from '@credo-ts/core' import type { Router } from 'express' @@ -36,7 +35,6 @@ export interface OpenId4VcIssuerModuleConfigOptions { OpenId4VciAccessTokenEndpointConfig, 'cNonceExpiresInSeconds' | 'endpointPath' | 'preAuthorizedCodeExpirationInSeconds' | 'tokenExpiresInSeconds' > - federation?: Optional } } diff --git a/packages/openid4vc/src/shared/utils.ts b/packages/openid4vc/src/shared/utils.ts index 5eea5dd8bc..5c868f6732 100644 --- a/packages/openid4vc/src/shared/utils.ts +++ b/packages/openid4vc/src/shared/utils.ts @@ -105,9 +105,9 @@ export function getVerifyJwtCallback( } // Pick the first valid trust chain for validation of the leaf entity jwks - const { entityConfiguration } = validTrustChains[0] + const { leafEntityConfiguration } = validTrustChains[0] // TODO: No support yet for signed jwks and external jwks - const rpSigningKeys = entityConfiguration?.metadata?.openid_relying_party?.jwks?.keys + const rpSigningKeys = leafEntityConfiguration?.metadata?.openid_relying_party?.jwks?.keys if (!rpSigningKeys || rpSigningKeys.length === 0) throw new CredoError('No rp signing keys found in the entity configuration.') diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b1b08f440b..04f91bfb38 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.12 - version: 0.1.1-alpha.12 + specifier: 0.1.1-alpha.13 + version: 0.1.1-alpha.13 '@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.12': - resolution: {integrity: sha512-pGEt0Zz0Y+l0mlayeT5oeHILd0XKmzfpgVJcKM/DgBYaMTa8MdEdVZj6GLpBIqZWHzxoJXM+DB6OeNi9EemAlQ==} + '@openid-federation/core@0.1.1-alpha.13': + resolution: {integrity: sha512-QC4DSbiJ7eWstLs1O3XrX/yKFgaj+3ch8cA4N/02BywVNmkiYgW9qXhcvY50ULINuCeYdqIMIqCuHbaTa0A1hw==} '@peculiar/asn1-cms@2.3.13': resolution: {integrity: sha512-joqu8A7KR2G85oLPq+vB+NFr2ro7Ls4ol13Zcse/giPSzUNN0n2k3v8kMpf6QdGUhI13e5SzQYN8AKP8sJ8v4w==} @@ -9809,7 +9809,7 @@ snapshots: dependencies: semver: 7.6.3 - '@openid-federation/core@0.1.1-alpha.12': + '@openid-federation/core@0.1.1-alpha.13': dependencies: buffer: 6.0.3 zod: 3.23.8 From f6f766d6d86fb59e4558f2d250659e544359aac2 Mon Sep 17 00:00:00 2001 From: Tom Lanser Date: Wed, 20 Nov 2024 11:08:34 +0100 Subject: [PATCH 08/14] fix: Merge conflict and changes Signed-off-by: Tom Lanser --- .../router/federationEndpoint.ts | 15 +- .../router/federationEndpoint.ts | 5 +- .../tests/openid4vc-federation.e2e.test.ts | 519 ------------------ .../openid4vc/tests/openid4vc.e2e.test.ts | 342 ++++++++++++ 4 files changed, 351 insertions(+), 530 deletions(-) delete mode 100644 packages/openid4vc/tests/openid4vc-federation.e2e.test.ts diff --git a/packages/openid4vc/src/openid4vc-issuer/router/federationEndpoint.ts b/packages/openid4vc/src/openid4vc-issuer/router/federationEndpoint.ts index 788dea94f3..1a341aa01b 100644 --- a/packages/openid4vc/src/openid4vc-issuer/router/federationEndpoint.ts +++ b/packages/openid4vc/src/openid4vc-issuer/router/federationEndpoint.ts @@ -6,7 +6,6 @@ import { Key, getJwkFromKey, KeyType } from '@credo-ts/core' import { createEntityConfiguration } from '@openid-federation/core' import { getRequestContext, sendErrorResponse } from '../../shared/router' -import { OpenId4VcIssuerService } from '../OpenId4VcIssuerService' // TODO: It's also possible that the issuer and the verifier can have the same openid-federation endpoint. In that case we need to combine them. @@ -15,7 +14,6 @@ export function configureFederationEndpoint(router: Router) { router.get('/.well-known/openid-federation', async (request: OpenId4VcIssuanceRequest, response: Response, next) => { const { agentContext, issuer } = getRequestContext(request) - const openId4VcIssuerService = agentContext.dependencyManager.resolve(OpenId4VcIssuerService) try { // TODO: Should be only created once per issuer and be used between instances @@ -23,8 +21,6 @@ export function configureFederationEndpoint(router: Router) { keyType: KeyType.Ed25519, }) - const issuerMetadata = openId4VcIssuerService.getIssuerMetadata(agentContext, issuer) - const now = new Date() const expires = new Date(now.getTime() + 1000 * 60 * 60 * 24) // 1 day from now @@ -35,14 +31,14 @@ export function configureFederationEndpoint(router: Router) { const kid = federationKey.fingerprint const alg = jwk.supportedSignatureAlgorithms[0] - const issuerDisplay = issuerMetadata.issuerDisplay?.[0] + const issuerDisplay = issuer.display?.[0] const accessTokenSigningKey = Key.fromFingerprint(issuer.accessTokenPublicKeyFingerprint) const entityConfiguration = await createEntityConfiguration({ claims: { - sub: issuerMetadata.issuerUrl, - iss: issuerMetadata.issuerUrl, + sub: issuer.issuerId, + iss: issuer.issuerId, iat: now, exp: expires, jwks: { @@ -52,7 +48,7 @@ export function configureFederationEndpoint(router: Router) { federation_entity: issuerDisplay ? { organization_name: issuerDisplay.name, - logo_uri: issuerDisplay.logo?.url, + logo_uri: issuerDisplay.logo?.uri, } : undefined, openid_provider: { @@ -99,7 +95,8 @@ export function configureFederationEndpoint(router: Router) { agentContext.config.logger.error('Failed to create entity configuration', { error, }) - sendErrorResponse(response, agentContext.config.logger, 500, 'invalid_request', error) + sendErrorResponse(response, next, agentContext.config.logger, 500, 'invalid_request', error) + return } // NOTE: if we don't call next, the agentContext session handler will NOT be called diff --git a/packages/openid4vc/src/openid4vc-verifier/router/federationEndpoint.ts b/packages/openid4vc/src/openid4vc-verifier/router/federationEndpoint.ts index e3fa74fcea..b12f23f483 100644 --- a/packages/openid4vc/src/openid4vc-verifier/router/federationEndpoint.ts +++ b/packages/openid4vc/src/openid4vc-verifier/router/federationEndpoint.ts @@ -78,7 +78,7 @@ export function configureFederationEndpoint(router: Router) { rpSigningKeyMapping.set(verifier.verifierId, rpSigningKey) } - const relyingParty = await verifierService.getRelyingParty(agentContext, verifier.verifierId, { + const relyingParty = await verifierService.getRelyingParty(agentContext, verifier, { clientId: verifierConfig.baseUrl, clientIdScheme: 'entity_id', authorizationResponseUrl: `${verifierConfig.baseUrl}/siop/${verifier.verifierId}/authorize`, @@ -137,7 +137,8 @@ export function configureFederationEndpoint(router: Router) { agentContext.config.logger.error('Failed to create entity configuration', { error, }) - sendErrorResponse(response, agentContext.config.logger, 500, 'invalid_request', error) + sendErrorResponse(response, next, agentContext.config.logger, 500, 'invalid_request', error) + return } // NOTE: if we don't call next, the agentContext session handler will NOT be called diff --git a/packages/openid4vc/tests/openid4vc-federation.e2e.test.ts b/packages/openid4vc/tests/openid4vc-federation.e2e.test.ts deleted file mode 100644 index f0f7f874b4..0000000000 --- a/packages/openid4vc/tests/openid4vc-federation.e2e.test.ts +++ /dev/null @@ -1,519 +0,0 @@ -import type { AgentType, TenantType } from './utils' -import type { OpenId4VciSignMdocCredential } from '../src' -import type { Server } from 'http' - -import { - ClaimFormat, - DidsApi, - DifPresentationExchangeService, - JwaSignatureAlgorithm, - W3cCredential, - W3cCredentialSubject, - w3cDate, - W3cIssuer, - X509Module, - X509ModuleConfig, -} from '@credo-ts/core' -import express, { type Express } from 'express' - -import { AskarModule } from '../../askar/src' -import { askarModuleConfig } from '../../askar/tests/helpers' -import { TenantsModule } from '../../tenants/src' -import { - OpenId4VcHolderModule, - OpenId4VcIssuerModule, - OpenId4VcVerificationSessionState, - OpenId4VcVerifierModule, -} from '../src' - -import { waitForVerificationSessionRecordSubject, createAgentFromModules, createTenantForAgent } from './utils' -import { - universityDegreeCredentialConfigurationSupportedMdoc, - universityDegreeCredentialSdJwt, - universityDegreeCredentialSdJwt2, -} from './utilsVci' -import { openBadgePresentationDefinition, universityDegreePresentationDefinition } from './utilsVp' - -const serverPort = 1234 -const baseUrl = `http://localhost:${serverPort}` -const issuanceBaseUrl = `${baseUrl}/oid4vci` -const verificationBaseUrl = `${baseUrl}/oid4vp` - -// TODO: Add tests for invalid configurations so unhappy tests - -describe('OpenId4Vc', () => { - let expressApp: Express - let expressServer: Server - - let issuer: AgentType<{ - openId4VcIssuer: OpenId4VcIssuerModule - tenants: TenantsModule<{ openId4VcIssuer: OpenId4VcIssuerModule }> - x509: X509Module - }> - - let holder: AgentType<{ - openId4VcHolder: OpenId4VcHolderModule - tenants: TenantsModule<{ openId4VcHolder: OpenId4VcHolderModule }> - }> - let holder1: TenantType - - let verifier: AgentType<{ - openId4VcVerifier: OpenId4VcVerifierModule - tenants: TenantsModule<{ openId4VcVerifier: OpenId4VcVerifierModule }> - }> - let verifier1: TenantType - let verifier2: TenantType - - beforeEach(async () => { - expressApp = express() - - issuer = (await createAgentFromModules( - 'issuer', - { - x509: new X509Module(), - openId4VcIssuer: new OpenId4VcIssuerModule({ - baseUrl: issuanceBaseUrl, - endpoints: { - credential: { - credentialRequestToCredentialMapper: async ({ agentContext, credentialRequest, holderBinding }) => { - // We sign the request with the first did:key did we have - const didsApi = agentContext.dependencyManager.resolve(DidsApi) - const [firstDidKeyDid] = await didsApi.getCreatedDids({ method: 'key' }) - const didDocument = await didsApi.resolveDidDocument(firstDidKeyDid.did) - const verificationMethod = didDocument.verificationMethod?.[0] - if (!verificationMethod) { - throw new Error('No verification method found') - } - - if (credentialRequest.format === 'vc+sd-jwt') { - return { - credentialSupportedId: - credentialRequest.vct === 'UniversityDegreeCredential' - ? universityDegreeCredentialSdJwt.id - : universityDegreeCredentialSdJwt2.id, - format: credentialRequest.format, - payload: { vct: credentialRequest.vct, university: 'innsbruck', degree: 'bachelor' }, - holder: holderBinding, - issuer: { - method: 'did', - didUrl: verificationMethod.id, - }, - disclosureFrame: { _sd: ['university', 'degree'] }, - } - } else if (credentialRequest.format === 'mso_mdoc') { - const trustedCertificates = - agentContext.dependencyManager.resolve(X509ModuleConfig).trustedCertificates - if (trustedCertificates?.length !== 1) { - throw new Error('Expected exactly one trusted certificate. Received 0.') - } - - return { - credentialSupportedId: '', - format: ClaimFormat.MsoMdoc, - docType: universityDegreeCredentialConfigurationSupportedMdoc.doctype, - issuerCertificate: trustedCertificates[0], - holderKey: holderBinding.key, - namespaces: { - 'Leopold-Franzens-University': { - degree: 'bachelor', - }, - }, - } satisfies OpenId4VciSignMdocCredential - } else { - throw new Error('Invalid request') - } - }, - }, - }, - }), - askar: new AskarModule(askarModuleConfig), - tenants: new TenantsModule(), - }, - '96213c3d7fc8d4d6754c7a0fd969598g' - )) as unknown as typeof issuer - - holder = (await createAgentFromModules( - 'holder', - { - openId4VcHolder: new OpenId4VcHolderModule(), - askar: new AskarModule(askarModuleConfig), - tenants: new TenantsModule(), - x509: new X509Module(), - }, - '96213c3d7fc8d4d6754c7a0fd969598e' - )) as unknown as typeof holder - holder1 = await createTenantForAgent(holder.agent, 'hTenant1') - - verifier = (await createAgentFromModules( - 'verifier', - { - openId4VcVerifier: new OpenId4VcVerifierModule({ - baseUrl: verificationBaseUrl, - }), - askar: new AskarModule(askarModuleConfig), - tenants: new TenantsModule(), - }, - '96213c3d7fc8d4d6754c7a0fd969598f' - )) as unknown as typeof verifier - verifier1 = await createTenantForAgent(verifier.agent, 'vTenant1') - verifier2 = await createTenantForAgent(verifier.agent, 'vTenant2') - - // We let AFJ create the router, so we have a fresh one each time - expressApp.use('/oid4vci', issuer.agent.modules.openId4VcIssuer.config.router) - expressApp.use('/oid4vp', verifier.agent.modules.openId4VcVerifier.config.router) - - expressServer = expressApp.listen(serverPort) - }) - - afterEach(async () => { - expressServer?.close() - - await issuer.agent.shutdown() - await issuer.agent.wallet.delete() - - await holder.agent.shutdown() - await holder.agent.wallet.delete() - }) - - it('e2e flow with tenants, verifier endpoints verifying a jwt-vc', async () => { - const holderTenant = await holder.agent.modules.tenants.getTenantAgent({ tenantId: holder1.tenantId }) - const verifierTenant1 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier1.tenantId }) - const verifierTenant2 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier2.tenantId }) - - const openIdVerifierTenant1 = await verifierTenant1.modules.openId4VcVerifier.createVerifier() - const openIdVerifierTenant2 = await verifierTenant2.modules.openId4VcVerifier.createVerifier() - - const signedCredential1 = await issuer.agent.w3cCredentials.signCredential({ - format: ClaimFormat.JwtVc, - credential: new W3cCredential({ - type: ['VerifiableCredential', 'OpenBadgeCredential'], - issuer: new W3cIssuer({ id: issuer.did }), - credentialSubject: new W3cCredentialSubject({ id: holder1.did }), - issuanceDate: w3cDate(Date.now()), - }), - alg: JwaSignatureAlgorithm.EdDSA, - verificationMethod: issuer.verificationMethod.id, - }) - - const signedCredential2 = await issuer.agent.w3cCredentials.signCredential({ - format: ClaimFormat.JwtVc, - credential: new W3cCredential({ - type: ['VerifiableCredential', 'UniversityDegreeCredential'], - issuer: new W3cIssuer({ id: issuer.did }), - credentialSubject: new W3cCredentialSubject({ id: holder1.did }), - issuanceDate: w3cDate(Date.now()), - }), - alg: JwaSignatureAlgorithm.EdDSA, - verificationMethod: issuer.verificationMethod.id, - }) - - await holderTenant.w3cCredentials.storeCredential({ credential: signedCredential1 }) - await holderTenant.w3cCredentials.storeCredential({ credential: signedCredential2 }) - - const { authorizationRequest: authorizationRequestUri1, verificationSession: verificationSession1 } = - await verifierTenant1.modules.openId4VcVerifier.createAuthorizationRequest({ - verifierId: openIdVerifierTenant1.verifierId, - requestSigner: { - method: 'openid-federation', - }, - presentationExchange: { - definition: openBadgePresentationDefinition, - }, - }) - - expect(authorizationRequestUri1).toEqual( - `openid4vp://?client_id=${encodeURIComponent( - `http://localhost:1234/oid4vp/${openIdVerifierTenant1.verifierId}` - )}&request_uri=${encodeURIComponent(verificationSession1.authorizationRequestUri)}` - ) - - const { authorizationRequest: authorizationRequestUri2, verificationSession: verificationSession2 } = - await verifierTenant2.modules.openId4VcVerifier.createAuthorizationRequest({ - requestSigner: { - method: 'openid-federation', - }, - presentationExchange: { - definition: universityDegreePresentationDefinition, - }, - verifierId: openIdVerifierTenant2.verifierId, - }) - - expect(authorizationRequestUri2).toEqual( - `openid4vp://?client_id=${encodeURIComponent( - `http://localhost:1234/oid4vp/${openIdVerifierTenant2.verifierId}` - )}&request_uri=${encodeURIComponent(verificationSession2.authorizationRequestUri)}` - ) - - await verifierTenant1.endSession() - await verifierTenant2.endSession() - - const resolvedProofRequest1 = await holderTenant.modules.openId4VcHolder.resolveSiopAuthorizationRequest( - authorizationRequestUri1, - { - federation: { - trustedEntityIds: [`http://localhost:1234/oid4vp/${openIdVerifierTenant1.verifierId}`], - }, - } - ) - - expect(resolvedProofRequest1.presentationExchange?.credentialsForRequest).toMatchObject({ - areRequirementsSatisfied: true, - requirements: [ - { - submissionEntry: [ - { - verifiableCredentials: [ - { - type: ClaimFormat.JwtVc, - credentialRecord: { - credential: { - type: ['VerifiableCredential', 'OpenBadgeCredential'], - }, - }, - }, - ], - }, - ], - }, - ], - }) - - const resolvedProofRequest2 = await holderTenant.modules.openId4VcHolder.resolveSiopAuthorizationRequest( - authorizationRequestUri2, - { - federation: { - trustedEntityIds: [`http://localhost:1234/oid4vp/${openIdVerifierTenant2.verifierId}`], - }, - } - ) - - expect(resolvedProofRequest2.presentationExchange?.credentialsForRequest).toMatchObject({ - areRequirementsSatisfied: true, - requirements: [ - { - submissionEntry: [ - { - verifiableCredentials: [ - { - type: ClaimFormat.JwtVc, - credentialRecord: { - credential: { - type: ['VerifiableCredential', 'UniversityDegreeCredential'], - }, - }, - }, - ], - }, - ], - }, - ], - }) - - if (!resolvedProofRequest1.presentationExchange || !resolvedProofRequest2.presentationExchange) { - throw new Error('Presentation exchange not defined') - } - - const presentationExchangeService = holderTenant.dependencyManager.resolve(DifPresentationExchangeService) - const selectedCredentials = presentationExchangeService.selectCredentialsForRequest( - resolvedProofRequest1.presentationExchange.credentialsForRequest - ) - - const { submittedResponse: submittedResponse1, serverResponse: serverResponse1 } = - await holderTenant.modules.openId4VcHolder.acceptSiopAuthorizationRequest({ - authorizationRequest: resolvedProofRequest1.authorizationRequest, - presentationExchange: { - credentials: selectedCredentials, - }, - }) - - expect(submittedResponse1).toEqual({ - presentation_submission: { - definition_id: 'OpenBadgeCredential', - descriptor_map: [ - { - format: 'jwt_vp', - id: 'OpenBadgeCredentialDescriptor', - path: '$', - path_nested: { - format: 'jwt_vc', - id: 'OpenBadgeCredentialDescriptor', - path: '$.vp.verifiableCredential[0]', - }, - }, - ], - id: expect.any(String), - }, - state: expect.any(String), - vp_token: expect.any(String), - }) - expect(serverResponse1).toMatchObject({ - status: 200, - }) - - // The RP MUST validate that the aud (audience) Claim contains the value of the client_id - // that the RP sent in the Authorization Request as an audience. - // When the request has been signed, the value might be an HTTPS URL, or a Decentralized Identifier. - const verifierTenant1_2 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier1.tenantId }) - await waitForVerificationSessionRecordSubject(verifier.replaySubject, { - contextCorrelationId: verifierTenant1_2.context.contextCorrelationId, - state: OpenId4VcVerificationSessionState.ResponseVerified, - verificationSessionId: verificationSession1.id, - }) - - const { idToken: idToken1, presentationExchange: presentationExchange1 } = - await verifierTenant1_2.modules.openId4VcVerifier.getVerifiedAuthorizationResponse(verificationSession1.id) - - expect(idToken1).toBeUndefined() - expect(presentationExchange1).toMatchObject({ - definition: openBadgePresentationDefinition, - submission: { - definition_id: 'OpenBadgeCredential', - }, - presentations: [ - { - verifiableCredential: [ - { - type: ['VerifiableCredential', 'OpenBadgeCredential'], - }, - ], - }, - ], - }) - - const selectedCredentials2 = presentationExchangeService.selectCredentialsForRequest( - resolvedProofRequest2.presentationExchange.credentialsForRequest - ) - - const { serverResponse: serverResponse2 } = - await holderTenant.modules.openId4VcHolder.acceptSiopAuthorizationRequest({ - authorizationRequest: resolvedProofRequest2.authorizationRequest, - presentationExchange: { - credentials: selectedCredentials2, - }, - }) - expect(serverResponse2).toMatchObject({ - status: 200, - }) - - // The RP MUST validate that the aud (audience) Claim contains the value of the client_id - // that the RP sent in the Authorization Request as an audience. - // When the request has been signed, the value might be an HTTPS URL, or a Decentralized Identifier. - const verifierTenant2_2 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier2.tenantId }) - await waitForVerificationSessionRecordSubject(verifier.replaySubject, { - contextCorrelationId: verifierTenant2_2.context.contextCorrelationId, - state: OpenId4VcVerificationSessionState.ResponseVerified, - verificationSessionId: verificationSession2.id, - }) - const { idToken: idToken2, presentationExchange: presentationExchange2 } = - await verifierTenant2_2.modules.openId4VcVerifier.getVerifiedAuthorizationResponse(verificationSession2.id) - expect(idToken2).toBeUndefined() - - expect(presentationExchange2).toMatchObject({ - definition: universityDegreePresentationDefinition, - submission: { - definition_id: 'UniversityDegreeCredential', - }, - presentations: [ - { - verifiableCredential: [ - { - type: ['VerifiableCredential', 'UniversityDegreeCredential'], - }, - ], - }, - ], - }) - }) - - it('e2e flow with tenants, unhappy flow', async () => { - const holderTenant = await holder.agent.modules.tenants.getTenantAgent({ tenantId: holder1.tenantId }) - const verifierTenant1 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier1.tenantId }) - const verifierTenant2 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier2.tenantId }) - - const openIdVerifierTenant1 = await verifierTenant1.modules.openId4VcVerifier.createVerifier() - const openIdVerifierTenant2 = await verifierTenant2.modules.openId4VcVerifier.createVerifier() - - const signedCredential1 = await issuer.agent.w3cCredentials.signCredential({ - format: ClaimFormat.JwtVc, - credential: new W3cCredential({ - type: ['VerifiableCredential', 'OpenBadgeCredential'], - issuer: new W3cIssuer({ id: issuer.did }), - credentialSubject: new W3cCredentialSubject({ id: holder1.did }), - issuanceDate: w3cDate(Date.now()), - }), - alg: JwaSignatureAlgorithm.EdDSA, - verificationMethod: issuer.verificationMethod.id, - }) - - const signedCredential2 = await issuer.agent.w3cCredentials.signCredential({ - format: ClaimFormat.JwtVc, - credential: new W3cCredential({ - type: ['VerifiableCredential', 'UniversityDegreeCredential'], - issuer: new W3cIssuer({ id: issuer.did }), - credentialSubject: new W3cCredentialSubject({ id: holder1.did }), - issuanceDate: w3cDate(Date.now()), - }), - alg: JwaSignatureAlgorithm.EdDSA, - verificationMethod: issuer.verificationMethod.id, - }) - - await holderTenant.w3cCredentials.storeCredential({ credential: signedCredential1 }) - await holderTenant.w3cCredentials.storeCredential({ credential: signedCredential2 }) - - const { authorizationRequest: authorizationRequestUri1, verificationSession: verificationSession1 } = - await verifierTenant1.modules.openId4VcVerifier.createAuthorizationRequest({ - verifierId: openIdVerifierTenant1.verifierId, - requestSigner: { - method: 'openid-federation', - }, - presentationExchange: { - definition: openBadgePresentationDefinition, - }, - }) - - expect(authorizationRequestUri1).toEqual( - `openid4vp://?client_id=${encodeURIComponent( - `http://localhost:1234/oid4vp/${openIdVerifierTenant1.verifierId}` - )}&request_uri=${encodeURIComponent(verificationSession1.authorizationRequestUri)}` - ) - - const { authorizationRequest: authorizationRequestUri2, verificationSession: verificationSession2 } = - await verifierTenant2.modules.openId4VcVerifier.createAuthorizationRequest({ - requestSigner: { - method: 'openid-federation', - }, - presentationExchange: { - definition: universityDegreePresentationDefinition, - }, - verifierId: openIdVerifierTenant2.verifierId, - }) - - expect(authorizationRequestUri2).toEqual( - `openid4vp://?client_id=${encodeURIComponent( - `http://localhost:1234/oid4vp/${openIdVerifierTenant2.verifierId}` - )}&request_uri=${encodeURIComponent(verificationSession2.authorizationRequestUri)}` - ) - - await verifierTenant1.endSession() - await verifierTenant2.endSession() - - const resolvedProofRequestWithFederationPromise = - holderTenant.modules.openId4VcHolder.resolveSiopAuthorizationRequest(authorizationRequestUri1, { - federation: { - // This will look for a whole different trusted entity - trustedEntityIds: [`http://localhost:1234/oid4vp/${openIdVerifierTenant2.verifierId}`], - }, - }) - - // TODO: Look into this error see if we can make it more specific - await expect(resolvedProofRequestWithFederationPromise).rejects.toThrow( - `Error verifying the DID Auth Token signature.` - ) - - const resolvedProofRequestWithoutFederationPromise = - holderTenant.modules.openId4VcHolder.resolveSiopAuthorizationRequest(authorizationRequestUri2) - await expect(resolvedProofRequestWithoutFederationPromise).rejects.toThrow( - `Error verifying the DID Auth Token signature.` - ) - }) -}) diff --git a/packages/openid4vc/tests/openid4vc.e2e.test.ts b/packages/openid4vc/tests/openid4vc.e2e.test.ts index 12c7dff9fb..88d78f9f07 100644 --- a/packages/openid4vc/tests/openid4vc.e2e.test.ts +++ b/packages/openid4vc/tests/openid4vc.e2e.test.ts @@ -2538,4 +2538,346 @@ describe('OpenId4Vc', () => { descriptors: expect.any(Array), }) }) + + it('e2e flow with tenants and federation, verifier endpoints verifying a jwt-vc', async () => { + const holderTenant = await holder.agent.modules.tenants.getTenantAgent({ tenantId: holder1.tenantId }) + const verifierTenant1 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier1.tenantId }) + const verifierTenant2 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier2.tenantId }) + + const openIdVerifierTenant1 = await verifierTenant1.modules.openId4VcVerifier.createVerifier() + const openIdVerifierTenant2 = await verifierTenant2.modules.openId4VcVerifier.createVerifier() + + const signedCredential1 = await issuer.agent.w3cCredentials.signCredential({ + format: ClaimFormat.JwtVc, + credential: new W3cCredential({ + type: ['VerifiableCredential', 'OpenBadgeCredential'], + issuer: new W3cIssuer({ id: issuer.did }), + credentialSubject: new W3cCredentialSubject({ id: holder1.did }), + issuanceDate: w3cDate(Date.now()), + }), + alg: JwaSignatureAlgorithm.EdDSA, + verificationMethod: issuer.verificationMethod.id, + }) + + const signedCredential2 = await issuer.agent.w3cCredentials.signCredential({ + format: ClaimFormat.JwtVc, + credential: new W3cCredential({ + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + issuer: new W3cIssuer({ id: issuer.did }), + credentialSubject: new W3cCredentialSubject({ id: holder1.did }), + issuanceDate: w3cDate(Date.now()), + }), + alg: JwaSignatureAlgorithm.EdDSA, + verificationMethod: issuer.verificationMethod.id, + }) + + await holderTenant.w3cCredentials.storeCredential({ credential: signedCredential1 }) + await holderTenant.w3cCredentials.storeCredential({ credential: signedCredential2 }) + + const { authorizationRequest: authorizationRequestUri1, verificationSession: verificationSession1 } = + await verifierTenant1.modules.openId4VcVerifier.createAuthorizationRequest({ + verifierId: openIdVerifierTenant1.verifierId, + requestSigner: { + method: 'openid-federation', + }, + presentationExchange: { + definition: openBadgePresentationDefinition, + }, + }) + + expect(authorizationRequestUri1).toEqual( + `openid4vp://?client_id=${encodeURIComponent( + `http://localhost:1234/oid4vp/${openIdVerifierTenant1.verifierId}` + )}&request_uri=${encodeURIComponent(verificationSession1.authorizationRequestUri)}` + ) + + const { authorizationRequest: authorizationRequestUri2, verificationSession: verificationSession2 } = + await verifierTenant2.modules.openId4VcVerifier.createAuthorizationRequest({ + requestSigner: { + method: 'openid-federation', + }, + presentationExchange: { + definition: universityDegreePresentationDefinition, + }, + verifierId: openIdVerifierTenant2.verifierId, + }) + + expect(authorizationRequestUri2).toEqual( + `openid4vp://?client_id=${encodeURIComponent( + `http://localhost:1234/oid4vp/${openIdVerifierTenant2.verifierId}` + )}&request_uri=${encodeURIComponent(verificationSession2.authorizationRequestUri)}` + ) + + await verifierTenant1.endSession() + await verifierTenant2.endSession() + + const resolvedProofRequest1 = await holderTenant.modules.openId4VcHolder.resolveSiopAuthorizationRequest( + authorizationRequestUri1, + { + federation: { + trustedEntityIds: [`http://localhost:1234/oid4vp/${openIdVerifierTenant1.verifierId}`], + }, + } + ) + + expect(resolvedProofRequest1.presentationExchange?.credentialsForRequest).toMatchObject({ + areRequirementsSatisfied: true, + requirements: [ + { + submissionEntry: [ + { + verifiableCredentials: [ + { + type: ClaimFormat.JwtVc, + credentialRecord: { + credential: { + type: ['VerifiableCredential', 'OpenBadgeCredential'], + }, + }, + }, + ], + }, + ], + }, + ], + }) + + const resolvedProofRequest2 = await holderTenant.modules.openId4VcHolder.resolveSiopAuthorizationRequest( + authorizationRequestUri2, + { + federation: { + trustedEntityIds: [`http://localhost:1234/oid4vp/${openIdVerifierTenant2.verifierId}`], + }, + } + ) + + expect(resolvedProofRequest2.presentationExchange?.credentialsForRequest).toMatchObject({ + areRequirementsSatisfied: true, + requirements: [ + { + submissionEntry: [ + { + verifiableCredentials: [ + { + type: ClaimFormat.JwtVc, + credentialRecord: { + credential: { + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + }, + }, + }, + ], + }, + ], + }, + ], + }) + + if (!resolvedProofRequest1.presentationExchange || !resolvedProofRequest2.presentationExchange) { + throw new Error('Presentation exchange not defined') + } + + const presentationExchangeService = holderTenant.dependencyManager.resolve(DifPresentationExchangeService) + const selectedCredentials = presentationExchangeService.selectCredentialsForRequest( + resolvedProofRequest1.presentationExchange.credentialsForRequest + ) + + const { submittedResponse: submittedResponse1, serverResponse: serverResponse1 } = + await holderTenant.modules.openId4VcHolder.acceptSiopAuthorizationRequest({ + authorizationRequest: resolvedProofRequest1.authorizationRequest, + presentationExchange: { + credentials: selectedCredentials, + }, + }) + + expect(submittedResponse1).toEqual({ + presentation_submission: { + definition_id: 'OpenBadgeCredential', + descriptor_map: [ + { + format: 'jwt_vp', + id: 'OpenBadgeCredentialDescriptor', + path: '$', + path_nested: { + format: 'jwt_vc', + id: 'OpenBadgeCredentialDescriptor', + path: '$.vp.verifiableCredential[0]', + }, + }, + ], + id: expect.any(String), + }, + state: expect.any(String), + vp_token: expect.any(String), + }) + expect(serverResponse1).toMatchObject({ + status: 200, + }) + + // The RP MUST validate that the aud (audience) Claim contains the value of the client_id + // that the RP sent in the Authorization Request as an audience. + // When the request has been signed, the value might be an HTTPS URL, or a Decentralized Identifier. + const verifierTenant1_2 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier1.tenantId }) + await waitForVerificationSessionRecordSubject(verifier.replaySubject, { + contextCorrelationId: verifierTenant1_2.context.contextCorrelationId, + state: OpenId4VcVerificationSessionState.ResponseVerified, + verificationSessionId: verificationSession1.id, + }) + + const { idToken: idToken1, presentationExchange: presentationExchange1 } = + await verifierTenant1_2.modules.openId4VcVerifier.getVerifiedAuthorizationResponse(verificationSession1.id) + + expect(idToken1).toBeUndefined() + expect(presentationExchange1).toMatchObject({ + definition: openBadgePresentationDefinition, + submission: { + definition_id: 'OpenBadgeCredential', + }, + presentations: [ + { + verifiableCredential: [ + { + type: ['VerifiableCredential', 'OpenBadgeCredential'], + }, + ], + }, + ], + }) + + const selectedCredentials2 = presentationExchangeService.selectCredentialsForRequest( + resolvedProofRequest2.presentationExchange.credentialsForRequest + ) + + const { serverResponse: serverResponse2 } = + await holderTenant.modules.openId4VcHolder.acceptSiopAuthorizationRequest({ + authorizationRequest: resolvedProofRequest2.authorizationRequest, + presentationExchange: { + credentials: selectedCredentials2, + }, + }) + expect(serverResponse2).toMatchObject({ + status: 200, + }) + + // The RP MUST validate that the aud (audience) Claim contains the value of the client_id + // that the RP sent in the Authorization Request as an audience. + // When the request has been signed, the value might be an HTTPS URL, or a Decentralized Identifier. + const verifierTenant2_2 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier2.tenantId }) + await waitForVerificationSessionRecordSubject(verifier.replaySubject, { + contextCorrelationId: verifierTenant2_2.context.contextCorrelationId, + state: OpenId4VcVerificationSessionState.ResponseVerified, + verificationSessionId: verificationSession2.id, + }) + const { idToken: idToken2, presentationExchange: presentationExchange2 } = + await verifierTenant2_2.modules.openId4VcVerifier.getVerifiedAuthorizationResponse(verificationSession2.id) + expect(idToken2).toBeUndefined() + + expect(presentationExchange2).toMatchObject({ + definition: universityDegreePresentationDefinition, + submission: { + definition_id: 'UniversityDegreeCredential', + }, + presentations: [ + { + verifiableCredential: [ + { + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + }, + ], + }, + ], + }) + }) + + it('e2e flow with tenants and federation, unhappy flow', async () => { + const holderTenant = await holder.agent.modules.tenants.getTenantAgent({ tenantId: holder1.tenantId }) + const verifierTenant1 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier1.tenantId }) + const verifierTenant2 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier2.tenantId }) + + const openIdVerifierTenant1 = await verifierTenant1.modules.openId4VcVerifier.createVerifier() + const openIdVerifierTenant2 = await verifierTenant2.modules.openId4VcVerifier.createVerifier() + + const signedCredential1 = await issuer.agent.w3cCredentials.signCredential({ + format: ClaimFormat.JwtVc, + credential: new W3cCredential({ + type: ['VerifiableCredential', 'OpenBadgeCredential'], + issuer: new W3cIssuer({ id: issuer.did }), + credentialSubject: new W3cCredentialSubject({ id: holder1.did }), + issuanceDate: w3cDate(Date.now()), + }), + alg: JwaSignatureAlgorithm.EdDSA, + verificationMethod: issuer.verificationMethod.id, + }) + + const signedCredential2 = await issuer.agent.w3cCredentials.signCredential({ + format: ClaimFormat.JwtVc, + credential: new W3cCredential({ + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + issuer: new W3cIssuer({ id: issuer.did }), + credentialSubject: new W3cCredentialSubject({ id: holder1.did }), + issuanceDate: w3cDate(Date.now()), + }), + alg: JwaSignatureAlgorithm.EdDSA, + verificationMethod: issuer.verificationMethod.id, + }) + + await holderTenant.w3cCredentials.storeCredential({ credential: signedCredential1 }) + await holderTenant.w3cCredentials.storeCredential({ credential: signedCredential2 }) + + const { authorizationRequest: authorizationRequestUri1, verificationSession: verificationSession1 } = + await verifierTenant1.modules.openId4VcVerifier.createAuthorizationRequest({ + verifierId: openIdVerifierTenant1.verifierId, + requestSigner: { + method: 'openid-federation', + }, + presentationExchange: { + definition: openBadgePresentationDefinition, + }, + }) + + expect(authorizationRequestUri1).toEqual( + `openid4vp://?client_id=${encodeURIComponent( + `http://localhost:1234/oid4vp/${openIdVerifierTenant1.verifierId}` + )}&request_uri=${encodeURIComponent(verificationSession1.authorizationRequestUri)}` + ) + + const { authorizationRequest: authorizationRequestUri2, verificationSession: verificationSession2 } = + await verifierTenant2.modules.openId4VcVerifier.createAuthorizationRequest({ + requestSigner: { + method: 'openid-federation', + }, + presentationExchange: { + definition: universityDegreePresentationDefinition, + }, + verifierId: openIdVerifierTenant2.verifierId, + }) + + expect(authorizationRequestUri2).toEqual( + `openid4vp://?client_id=${encodeURIComponent( + `http://localhost:1234/oid4vp/${openIdVerifierTenant2.verifierId}` + )}&request_uri=${encodeURIComponent(verificationSession2.authorizationRequestUri)}` + ) + + await verifierTenant1.endSession() + await verifierTenant2.endSession() + + const resolvedProofRequestWithFederationPromise = + holderTenant.modules.openId4VcHolder.resolveSiopAuthorizationRequest(authorizationRequestUri1, { + federation: { + // This will look for a whole different trusted entity + trustedEntityIds: [`http://localhost:1234/oid4vp/${openIdVerifierTenant2.verifierId}`], + }, + }) + + // TODO: Look into this error see if we can make it more specific + await expect(resolvedProofRequestWithFederationPromise).rejects.toThrow( + `Error verifying the DID Auth Token signature.` + ) + + const resolvedProofRequestWithoutFederationPromise = + holderTenant.modules.openId4VcHolder.resolveSiopAuthorizationRequest(authorizationRequestUri2) + await expect(resolvedProofRequestWithoutFederationPromise).rejects.toThrow( + `Error verifying the DID Auth Token signature.` + ) + }) }) From b2b3890181716e787a39481052196f6ed3ff5607 Mon Sep 17 00:00:00 2001 From: Tom Lanser Date: Wed, 20 Nov 2024 12:01:34 +0100 Subject: [PATCH 09/14] feat: Added fetchEntityConfiguration Signed-off-by: Tom Lanser --- .../OpenId4vcSiopHolderService.ts | 22 +++++++++++++++++++ .../OpenId4vcSiopHolderServiceOptions.ts | 4 ++++ 2 files changed, 26 insertions(+) diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts index ca6bb2cb2a..d2a5f7da1e 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts @@ -1,5 +1,6 @@ import type { OpenId4VcSiopAcceptAuthorizationRequestOptions, + OpenId4VcSiopFetchEntityConfigurationOptions, OpenId4VcSiopGetOpenIdProviderOptions, OpenId4VcSiopResolveAuthorizationRequestOptions, OpenId4VcSiopResolvedAuthorizationRequest, @@ -474,4 +475,25 @@ export class OpenId4VcSiopHolderService { }, }) } + + public async fetchOpenIdFederationEntityConfiguration( + agentContext: AgentContext, + options: OpenId4VcSiopFetchEntityConfigurationOptions + ) { + const jwsService = agentContext.dependencyManager.resolve(JwsService) + + const { entityId } = options + + return federationFetchEntityConfiguration({ + entityId, + verifyJwtCallback: async ({ jwt, jwk }) => { + const res = await jwsService.verifyJws(agentContext, { + jws: jwt, + jwkResolver: () => getJwkFromJson(jwk), + }) + + return res.isValid + }, + }) + } } diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderServiceOptions.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderServiceOptions.ts index 5cc0d1295a..0b31cfc5c5 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderServiceOptions.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderServiceOptions.ts @@ -74,3 +74,7 @@ export interface OpenId4VcSiopResolveTrustChainsOptions { entityId: string trustAnchorEntityIds: [string, ...string[]] } + +export interface OpenId4VcSiopFetchEntityConfigurationOptions { + entityId: string +} From d5ea6279d373518498863b1ac105741a6a58864d Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Wed, 20 Nov 2024 14:00:04 +0100 Subject: [PATCH 10/14] update lock Signed-off-by: Timo Glastra --- pnpm-lock.yaml | 148 +++++++++++++++---------------------------------- 1 file changed, 46 insertions(+), 102 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9f18e0a64f..23852a41fb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -94,7 +94,7 @@ importers: specifier: ^29.7.0 version: 29.7.0(@types/node@18.18.8)(ts-node@10.9.2(@swc/core@1.7.40)(@types/node@18.18.8)(typescript@5.5.4)) nock: - specifier: ^14.0.0-beta.15 + specifier: ^14.0.0-beta.16 version: 14.0.0-beta.16 prettier: specifier: ^2.3.1 @@ -1563,12 +1563,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-export-namespace-from@7.24.7': - resolution: {integrity: sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-export-namespace-from@7.25.9': resolution: {integrity: sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==} engines: {node: '>=6.9.0'} @@ -1701,12 +1695,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-object-rest-spread@7.24.7': - resolution: {integrity: sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-object-rest-spread@7.25.9': resolution: {integrity: sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==} engines: {node: '>=6.9.0'} @@ -1749,24 +1737,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-private-methods@7.24.7': - resolution: {integrity: sha512-COTCOkG2hn4JKGEKBADkA8WNb35TGkkRbI5iT845dB+NyqgO8Hn+ajPbSnIQznneJTa3d30scb6iz/DhH8GsJQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-private-methods@7.25.9': resolution: {integrity: sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-private-property-in-object@7.24.7': - resolution: {integrity: sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-private-property-in-object@7.25.9': resolution: {integrity: sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==} engines: {node: '>=6.9.0'} @@ -8416,7 +8392,7 @@ snapshots: '@babel/generator@7.2.0': dependencies: - '@babel/types': 7.25.2 + '@babel/types': 7.26.0 jsesc: 2.5.2 lodash: 4.17.21 source-map: 0.5.7 @@ -8750,8 +8726,8 @@ snapshots: '@babel/plugin-proposal-decorators@7.24.7(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 - '@babel/helper-create-class-features-plugin': 7.25.0(@babel/core@7.26.0) - '@babel/helper-plugin-utils': 7.24.8 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 '@babel/plugin-syntax-decorators': 7.24.7(@babel/core@7.26.0) transitivePeerDependencies: - supports-color @@ -8771,7 +8747,7 @@ snapshots: '@babel/plugin-proposal-logical-assignment-operators@7.20.7(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.24.8 + '@babel/helper-plugin-utils': 7.25.9 '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.26.0) '@babel/plugin-proposal-nullish-coalescing-operator@7.18.6(@babel/core@7.26.0)': @@ -8783,7 +8759,7 @@ snapshots: '@babel/plugin-proposal-numeric-separator@7.18.6(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.24.8 + '@babel/helper-plugin-utils': 7.25.9 '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.26.0) '@babel/plugin-proposal-object-rest-spread@7.20.7(@babel/core@7.26.0)': @@ -8837,7 +8813,7 @@ snapshots: '@babel/plugin-syntax-decorators@7.24.7(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.24.8 + '@babel/helper-plugin-utils': 7.25.9 '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.26.0)': dependencies: @@ -9089,12 +9065,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-export-namespace-from@7.24.7(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.24.8 - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.26.0) - '@babel/plugin-transform-export-namespace-from@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -9241,14 +9211,6 @@ snapshots: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-object-rest-spread@7.24.7(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-compilation-targets': 7.25.2 - '@babel/helper-plugin-utils': 7.24.8 - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.26.0) - '@babel/plugin-transform-parameters': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-object-rest-spread@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -9295,14 +9257,6 @@ snapshots: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-private-methods@7.24.7(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-create-class-features-plugin': 7.25.0(@babel/core@7.26.0) - '@babel/helper-plugin-utils': 7.24.8 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-private-methods@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -9311,16 +9265,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-private-property-in-object@7.24.7(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-annotate-as-pure': 7.24.7 - '@babel/helper-create-class-features-plugin': 7.25.0(@babel/core@7.26.0) - '@babel/helper-plugin-utils': 7.24.8 - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.26.0) - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-private-property-in-object@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -9376,8 +9320,8 @@ snapshots: '@babel/plugin-transform-react-pure-annotations@7.24.7(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 - '@babel/helper-annotate-as-pure': 7.24.7 - '@babel/helper-plugin-utils': 7.24.8 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 '@babel/plugin-transform-regenerator@7.25.9(@babel/core@7.26.0)': dependencies: @@ -9591,8 +9535,8 @@ snapshots: '@babel/preset-react@7.24.7(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.24.8 - '@babel/helper-validator-option': 7.24.8 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-validator-option': 7.25.9 '@babel/plugin-transform-react-display-name': 7.24.7(@babel/core@7.26.0) '@babel/plugin-transform-react-jsx': 7.25.2(@babel/core@7.26.0) '@babel/plugin-transform-react-jsx-development': 7.24.7(@babel/core@7.26.0) @@ -10328,7 +10272,7 @@ snapshots: chalk: 4.1.2 ci-info: 3.9.0 connect: 3.7.0 - debug: 4.3.6 + debug: 4.3.7 env-editor: 0.4.2 fast-glob: 3.3.2 find-yarn-workspace-root: 2.0.0 @@ -10396,7 +10340,7 @@ snapshots: '@expo/plist': 0.1.3 '@expo/sdk-runtime-versions': 1.0.0 chalk: 4.1.2 - debug: 4.3.6 + debug: 4.3.7 find-up: 5.0.0 getenv: 1.0.0 glob: 7.1.6 @@ -10447,7 +10391,7 @@ snapshots: '@expo/env@0.3.0': dependencies: chalk: 4.1.2 - debug: 4.3.6 + debug: 4.3.7 dotenv: 16.4.5 dotenv-expand: 11.0.6 getenv: 1.0.0 @@ -10478,15 +10422,15 @@ snapshots: '@expo/metro-config@0.18.11': dependencies: '@babel/core': 7.26.0 - '@babel/generator': 7.25.0 - '@babel/parser': 7.25.3 - '@babel/types': 7.25.2 + '@babel/generator': 7.26.2 + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 '@expo/config': 9.0.3 '@expo/env': 0.3.0 '@expo/json-file': 8.3.3 '@expo/spawn-async': 1.7.2 chalk: 4.1.2 - debug: 4.3.6 + debug: 4.3.7 find-yarn-workspace-root: 2.0.0 fs-extra: 9.1.0 getenv: 1.0.0 @@ -10532,7 +10476,7 @@ snapshots: '@expo/image-utils': 0.5.1 '@expo/json-file': 8.3.3 '@react-native/normalize-colors': 0.74.85 - debug: 4.3.6 + debug: 4.3.7 expo-modules-autolinking: 1.11.2 fs-extra: 9.1.0 resolve-from: 5.0.0 @@ -11319,31 +11263,31 @@ snapshots: '@babel/plugin-syntax-flow': 7.24.7(@babel/core@7.26.0) '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.26.0) '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.26.0) - '@babel/plugin-transform-arrow-functions': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-async-to-generator': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-block-scoping': 7.25.0(@babel/core@7.26.0) - '@babel/plugin-transform-classes': 7.25.0(@babel/core@7.26.0) - '@babel/plugin-transform-computed-properties': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-destructuring': 7.24.8(@babel/core@7.26.0) + '@babel/plugin-transform-arrow-functions': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-async-to-generator': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-block-scoping': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-classes': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-computed-properties': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-destructuring': 7.25.9(@babel/core@7.26.0) '@babel/plugin-transform-flow-strip-types': 7.25.2(@babel/core@7.26.0) - '@babel/plugin-transform-function-name': 7.25.1(@babel/core@7.26.0) - '@babel/plugin-transform-literals': 7.25.2(@babel/core@7.26.0) - '@babel/plugin-transform-modules-commonjs': 7.24.8(@babel/core@7.26.0) - '@babel/plugin-transform-named-capturing-groups-regex': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-parameters': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-private-methods': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-private-property-in-object': 7.24.7(@babel/core@7.26.0) + '@babel/plugin-transform-function-name': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-literals': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-modules-commonjs': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-named-capturing-groups-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-private-methods': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-private-property-in-object': 7.25.9(@babel/core@7.26.0) '@babel/plugin-transform-react-display-name': 7.24.7(@babel/core@7.26.0) '@babel/plugin-transform-react-jsx': 7.25.2(@babel/core@7.26.0) '@babel/plugin-transform-react-jsx-self': 7.24.7(@babel/core@7.26.0) '@babel/plugin-transform-react-jsx-source': 7.24.7(@babel/core@7.26.0) '@babel/plugin-transform-runtime': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-shorthand-properties': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-spread': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-sticky-regex': 7.24.7(@babel/core@7.26.0) + '@babel/plugin-transform-shorthand-properties': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-spread': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-sticky-regex': 7.25.9(@babel/core@7.26.0) '@babel/plugin-transform-typescript': 7.25.2(@babel/core@7.26.0) - '@babel/plugin-transform-unicode-regex': 7.24.7(@babel/core@7.26.0) - '@babel/template': 7.25.0 + '@babel/plugin-transform-unicode-regex': 7.25.9(@babel/core@7.26.0) + '@babel/template': 7.25.9 '@react-native/babel-plugin-codegen': 0.74.87(@babel/preset-env@7.26.0(@babel/core@7.26.0)) babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.26.0) react-refresh: 0.14.2 @@ -11353,7 +11297,7 @@ snapshots: '@react-native/codegen@0.74.87(@babel/preset-env@7.26.0(@babel/core@7.26.0))': dependencies: - '@babel/parser': 7.25.3 + '@babel/parser': 7.26.2 '@babel/preset-env': 7.26.0(@babel/core@7.26.0) glob: 7.2.3 hermes-parser: 0.19.1 @@ -12727,7 +12671,7 @@ snapshots: babel-plugin-react-compiler@0.0.0-experimental-7d62301-20240819: dependencies: '@babel/generator': 7.2.0 - '@babel/types': 7.25.2 + '@babel/types': 7.26.0 chalk: 4.1.2 invariant: 2.2.4 pretty-format: 24.9.0 @@ -12766,9 +12710,9 @@ snapshots: babel-preset-expo@11.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)): dependencies: '@babel/plugin-proposal-decorators': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-export-namespace-from': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-object-rest-spread': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-parameters': 7.24.7(@babel/core@7.26.0) + '@babel/plugin-transform-export-namespace-from': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-object-rest-spread': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.26.0) '@babel/preset-react': 7.24.7(@babel/core@7.26.0) '@babel/preset-typescript': 7.24.7(@babel/core@7.26.0) '@react-native/babel-preset': 0.74.87(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)) @@ -13803,7 +13747,7 @@ snapshots: debug: 4.3.6 enhanced-resolve: 5.17.1 eslint: 8.57.0 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.6 @@ -13815,7 +13759,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): + eslint-module-utils@2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): dependencies: debug: 3.2.7 optionalDependencies: @@ -13836,7 +13780,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) hasown: 2.0.2 is-core-module: 2.15.0 is-glob: 4.0.3 @@ -16592,7 +16536,7 @@ snapshots: postcss@8.4.41: dependencies: nanoid: 3.3.7 - picocolors: 1.0.1 + picocolors: 1.1.1 source-map-js: 1.2.0 preferred-pm@3.1.4: From 11455b5f571a4c9da0f9a54affe6d7775d1b16d1 Mon Sep 17 00:00:00 2001 From: Tom Lanser Date: Thu, 21 Nov 2024 15:26:32 +0100 Subject: [PATCH 11/14] fix: OpenID Federation small fixes (#2099) * fix: Use the right fingerprint for the RP kid Signed-off-by: Tom Lanser * fix: Use mergedproperties Signed-off-by: Tom Lanser --------- Signed-off-by: Tom Lanser --- .../src/openid4vc-holder/OpenId4vcSiopHolderService.ts | 2 +- .../src/openid4vc-verifier/router/federationEndpoint.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts index d2a5f7da1e..710aab4935 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts @@ -72,7 +72,7 @@ export class OpenId4VcSiopHolderService { const presentationDefinition = verifiedAuthorizationRequest.presentationDefinitions?.[0]?.definition if (verifiedAuthorizationRequest.clientIdScheme === 'entity_id') { - const clientId = verifiedAuthorizationRequest.authorizationRequestPayload.client_id + const clientId = await verifiedAuthorizationRequest.authorizationRequest.getMergedProperty('client_id') if (!clientId) { throw new CredoError("Unable to extract 'client_id' from authorization request") } diff --git a/packages/openid4vc/src/openid4vc-verifier/router/federationEndpoint.ts b/packages/openid4vc/src/openid4vc-verifier/router/federationEndpoint.ts index b12f23f483..bcf9c92416 100644 --- a/packages/openid4vc/src/openid4vc-verifier/router/federationEndpoint.ts +++ b/packages/openid4vc/src/openid4vc-verifier/router/federationEndpoint.ts @@ -119,7 +119,7 @@ export function configureFederationEndpoint(router: Router) { openid_relying_party: { ...rpMetadata, jwks: { - keys: [{ kid, alg, ...getJwkFromKey(rpSigningKey).toJson() }], + keys: [{ kid: rpSigningKey.fingerprint, alg, ...getJwkFromKey(rpSigningKey).toJson() }], }, client_registration_types: ['automatic'], // TODO: Not really sure why we need to provide this manually }, From 367dfa229c68bbc8a0a854350feea35b3261bb11 Mon Sep 17 00:00:00 2001 From: Tom Lanser Date: Sun, 24 Nov 2024 15:59:27 +0100 Subject: [PATCH 12/14] chore: Update branch with main (#2106) * chore: return access token response (#2097) Signed-off-by: Timo Glastra * fix: exp encoding (#2101) Signed-off-by: Timo Glastra * fix: mdoc patches (#2102) Signed-off-by: Timo Glastra * fix: address pex fixes (#2104) Signed-off-by: Timo Glastra --------- Signed-off-by: Timo Glastra Co-authored-by: Timo Glastra --- packages/core/package.json | 7 +- .../DifPresentationExchangeService.ts | 6 +- .../DifPresentationExchangeService.test.ts | 374 ++++++++++++++++++ .../models/DifPexCredentialsForRequest.ts | 3 +- .../dif-presentation-exchange/models/index.ts | 2 +- .../utils/credentialSelection.ts | 269 +++++++------ .../utils/presentationSelection.ts | 6 +- .../utils/transform.ts | 6 +- packages/core/src/modules/mdoc/Mdoc.ts | 16 +- .../src/modules/mdoc/MdocDeviceResponse.ts | 4 +- packages/core/src/modules/mdoc/MdocOptions.ts | 6 +- ...entationExchangeProofFormatService.test.ts | 2 +- packages/openid4vc/package.json | 4 +- .../openid4vc-holder/OpenId4VcHolderApi.ts | 7 +- .../OpenId4VciHolderService.ts | 5 + .../OpenId4VciHolderServiceOptions.ts | 3 + packages/openid4vc/src/shared/models/index.ts | 2 + packages/openid4vc/src/shared/utils.ts | 2 +- .../openid4vc/tests/openid4vc.e2e.test.ts | 4 +- pnpm-lock.yaml | 129 +++--- 20 files changed, 610 insertions(+), 247 deletions(-) create mode 100644 packages/core/src/modules/dif-presentation-exchange/__tests__/DifPresentationExchangeService.test.ts diff --git a/packages/core/package.json b/packages/core/package.json index 8488342433..b4f8e333b3 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -35,14 +35,14 @@ "@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-x509": "^2.3.8", "@peculiar/x509": "^1.11.0", - "@animo-id/mdoc": "0.2.38", + "@animo-id/mdoc": "0.2.39", "@sd-jwt/core": "^0.7.0", "@sd-jwt/decode": "^0.7.0", "@sd-jwt/jwt-status-list": "^0.7.0", "@sd-jwt/sd-jwt-vc": "^0.7.0", "@sd-jwt/types": "^0.7.0", "@sd-jwt/utils": "^0.7.0", - "@sphereon/pex": "5.0.0-unstable.25", + "@animo-id/pex": "4.1.1-alpha.0", "@sphereon/pex-models": "^2.3.1", "@sphereon/ssi-types": "0.30.2-next.135", "@stablelib/ed25519": "^1.0.2", @@ -54,7 +54,7 @@ "class-transformer": "0.5.1", "class-validator": "0.14.1", "did-resolver": "^4.1.0", - "jsonpath": "^1.1.1", + "@astronautlabs/jsonpath": "^1.1.2", "lru_map": "^0.4.1", "luxon": "^3.5.0", "make-error": "^1.3.6", @@ -70,7 +70,6 @@ }, "devDependencies": { "@types/events": "^3.0.0", - "@types/jsonpath": "^0.2.4", "@types/luxon": "^3.2.0", "@types/object-inspect": "^1.8.0", "@types/uuid": "^9.0.1", diff --git a/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts index 69a6aade89..05be0137d0 100644 --- a/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts +++ b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts @@ -14,7 +14,7 @@ import type { VerificationMethod } from '../dids' import type { SdJwtVcRecord } from '../sd-jwt-vc' import type { W3cCredentialRecord } from '../vc' import type { IAnonCredsDataIntegrityService } from '../vc/data-integrity/models/IAnonCredsDataIntegrityService' -import type { PresentationSignCallBackParams, Validated, VerifiablePresentationResult } from '@sphereon/pex' +import type { PresentationSignCallBackParams, Validated, VerifiablePresentationResult } from '@animo-id/pex' import type { InputDescriptorV2 } from '@sphereon/pex-models' import type { SdJwtDecodedVerifiableCredential, @@ -22,8 +22,8 @@ import type { W3CVerifiablePresentation, } from '@sphereon/ssi-types' -import { PEVersion, PEX, PresentationSubmissionLocation, Status } from '@sphereon/pex' -import { PartialSdJwtDecodedVerifiableCredential } from '@sphereon/pex/dist/main/lib' +import { PEVersion, PEX, PresentationSubmissionLocation, Status } from '@animo-id/pex' +import { PartialSdJwtDecodedVerifiableCredential } from '@animo-id/pex/dist/main/lib' import { injectable } from 'tsyringe' import { Hasher, getJwkFromKey } from '../../crypto' diff --git a/packages/core/src/modules/dif-presentation-exchange/__tests__/DifPresentationExchangeService.test.ts b/packages/core/src/modules/dif-presentation-exchange/__tests__/DifPresentationExchangeService.test.ts new file mode 100644 index 0000000000..ad77f6af14 --- /dev/null +++ b/packages/core/src/modules/dif-presentation-exchange/__tests__/DifPresentationExchangeService.test.ts @@ -0,0 +1,374 @@ +import type { DifPresentationExchangeDefinitionV2 } from '../models' + +import { Subject } from 'rxjs' + +import { InMemoryStorageService } from '../../../../../../tests/InMemoryStorageService' +import { InMemoryWallet } from '../../../../../../tests/InMemoryWallet' +import { agentDependencies, getAgentContext } from '../../../../tests' +import { AgentContext } from '../../../agent' +import { InjectionSymbols } from '../../../constants' +import { Mdoc, MdocRecord, MdocRepository } from '../../mdoc' +import { sprindFunkeTestVectorBase64Url } from '../../mdoc/__tests__/mdoc.fixtures' +import { SdJwtVcRecord, SdJwtVcRepository } from '../../sd-jwt-vc' +import { SignatureSuiteToken, W3cCredentialService, W3cCredentialsModuleConfig } from '../../vc' +import { DifPresentationExchangeService } from '../DifPresentationExchangeService' + +const agentContext = getAgentContext({ + registerInstances: [ + [InjectionSymbols.StorageService, new InMemoryStorageService()], + [InjectionSymbols.AgentDependencies, agentDependencies], + [InjectionSymbols.Stop$, new Subject()], + [SignatureSuiteToken, 'default'], + [W3cCredentialsModuleConfig, new W3cCredentialsModuleConfig()], + ], + wallet: new InMemoryWallet(), +}) +agentContext.dependencyManager.registerInstance(AgentContext, agentContext) +const sdJwtVcRecord = new SdJwtVcRecord({ + compactSdJwtVc: + 'eyJ4NWMiOlsiTUlJQ2REQ0NBaHVnQXdJQkFnSUJBakFLQmdncWhrak9QUVFEQWpDQmlERUxNQWtHQTFVRUJoTUNSRVV4RHpBTkJnTlZCQWNNQmtKbGNteHBiakVkTUJzR0ExVUVDZ3dVUW5WdVpHVnpaSEoxWTJ0bGNtVnBJRWR0WWtneEVUQVBCZ05WQkFzTUNGUWdRMU1nU1VSRk1UWXdOQVlEVlFRRERDMVRVRkpKVGtRZ1JuVnVhMlVnUlZWRVNTQlhZV3hzWlhRZ1VISnZkRzkwZVhCbElFbHpjM1ZwYm1jZ1EwRXdIaGNOTWpRd05UTXhNRGd4TXpFM1doY05NalV3TnpBMU1EZ3hNekUzV2pCc01Rc3dDUVlEVlFRR0V3SkVSVEVkTUJzR0ExVUVDZ3dVUW5WdVpHVnpaSEoxWTJ0bGNtVnBJRWR0WWtneENqQUlCZ05WQkFzTUFVa3hNakF3QmdOVkJBTU1LVk5RVWtsT1JDQkdkVzVyWlNCRlZVUkpJRmRoYkd4bGRDQlFjbTkwYjNSNWNHVWdTWE56ZFdWeU1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRU9GQnE0WU1LZzR3NWZUaWZzeXR3QnVKZi83RTdWaFJQWGlObTUyUzNxMUVUSWdCZFh5REsza1Z4R3hnZUhQaXZMUDN1dU12UzZpREVjN3FNeG12ZHVLT0JrRENCalRBZEJnTlZIUTRFRmdRVWlQaENrTEVyRFhQTFcyL0owV1ZlZ2h5dyttSXdEQVlEVlIwVEFRSC9CQUl3QURBT0JnTlZIUThCQWY4RUJBTUNCNEF3TFFZRFZSMFJCQ1l3SklJaVpHVnRieTV3YVdRdGFYTnpkV1Z5TG1KMWJtUmxjMlJ5ZFdOclpYSmxhUzVrWlRBZkJnTlZIU01FR0RBV2dCVFVWaGpBaVRqb0RsaUVHTWwyWXIrcnU4V1F2akFLQmdncWhrak9QUVFEQWdOSEFEQkVBaUFiZjVUemtjUXpoZldvSW95aTFWTjdkOEk5QnNGS20xTVdsdVJwaDJieUdRSWdLWWtkck5mMnhYUGpWU2JqVy9VLzVTNXZBRUM1WHhjT2FudXNPQnJvQmJVPSIsIk1JSUNlVENDQWlDZ0F3SUJBZ0lVQjVFOVFWWnRtVVljRHRDaktCL0gzVlF2NzJnd0NnWUlLb1pJemowRUF3SXdnWWd4Q3pBSkJnTlZCQVlUQWtSRk1ROHdEUVlEVlFRSERBWkNaWEpzYVc0eEhUQWJCZ05WQkFvTUZFSjFibVJsYzJSeWRXTnJaWEpsYVNCSGJXSklNUkV3RHdZRFZRUUxEQWhVSUVOVElFbEVSVEUyTURRR0ExVUVBd3d0VTFCU1NVNUVJRVoxYm10bElFVlZSRWtnVjJGc2JHVjBJRkJ5YjNSdmRIbHdaU0JKYzNOMWFXNW5JRU5CTUI0WERUSTBNRFV6TVRBMk5EZ3dPVm9YRFRNME1EVXlPVEEyTkRnd09Wb3dnWWd4Q3pBSkJnTlZCQVlUQWtSRk1ROHdEUVlEVlFRSERBWkNaWEpzYVc0eEhUQWJCZ05WQkFvTUZFSjFibVJsYzJSeWRXTnJaWEpsYVNCSGJXSklNUkV3RHdZRFZRUUxEQWhVSUVOVElFbEVSVEUyTURRR0ExVUVBd3d0VTFCU1NVNUVJRVoxYm10bElFVlZSRWtnVjJGc2JHVjBJRkJ5YjNSdmRIbHdaU0JKYzNOMWFXNW5JRU5CTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFWUd6ZHdGRG5jNytLbjVpYkF2Q09NOGtlNzdWUXhxZk1jd1pMOElhSUErV0NST2NDZm1ZL2dpSDkycU1ydTVwL2t5T2l2RTBSQy9JYmRNT052RG9VeWFObU1HUXdIUVlEVlIwT0JCWUVGTlJXR01DSk9PZ09XSVFZeVhaaXY2dTd4WkMrTUI4R0ExVWRJd1FZTUJhQUZOUldHTUNKT09nT1dJUVl5WFppdjZ1N3haQytNQklHQTFVZEV3RUIvd1FJTUFZQkFmOENBUUF3RGdZRFZSMFBBUUgvQkFRREFnR0dNQW9HQ0NxR1NNNDlCQU1DQTBjQU1FUUNJR0VtN3drWktIdC9hdGI0TWRGblhXNnlybndNVVQydTEzNmdkdGwxMFk2aEFpQnVURnF2Vll0aDFyYnh6Q1AweFdaSG1RSzlrVnl4bjhHUGZYMjdFSXp6c3c9PSJdLCJraWQiOiJNSUdVTUlHT3BJR0xNSUdJTVFzd0NRWURWUVFHRXdKRVJURVBNQTBHQTFVRUJ3d0dRbVZ5YkdsdU1SMHdHd1lEVlFRS0RCUkNkVzVrWlhOa2NuVmphMlZ5WldrZ1IyMWlTREVSTUE4R0ExVUVDd3dJVkNCRFV5QkpSRVV4TmpBMEJnTlZCQU1NTFZOUVVrbE9SQ0JHZFc1clpTQkZWVVJKSUZkaGJHeGxkQ0JRY205MGIzUjVjR1VnU1hOemRXbHVaeUJEUVFJQkFnPT0iLCJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFUzI1NiJ9.eyJwbGFjZV9vZl9iaXJ0aCI6eyJfc2QiOlsiVS01ZlVXLU5EM1laajZTcUdyQXV4NXJWYWZOalhqZ2hvMmRUUmpQX3hOTSJdfSwiX3NkIjpbIjlFaUpQNEw2NDI0bEtTVGs5NHpIOWhaWVc5UjNuS1R3V0V5TVBJN2dvWHciLCJHVlhRWEtFMmpWR1d0VEF6T1d5ck85TTZySW1qYkZJWGFnRkMyWElMbGhJIiwiUUV2bHpNd0ozZS1tOEtpWEk5bGx2bnVQblh5UHRXN2VCSF9GcXFVTnk3WSIsImljWkpTRkFqLVg3T29Sam5vRFRReXFwU1dNQUVuaTcydWZDZmFFWC1uQkUiLCJsUHJqb3BqbEN5bFdHWVo0cmh4S1RUTUsxS3p1Sm5ISUtybzNwUUhlUXF3IiwicjJORHZtRFY3QmU3TlptVFR0VE9fekdZX3RTdWdYVXoxeDJBXzZuOFhvdyIsInJPbjFJUkpUQWtEV1pSTGc3MUYzaDVsbFpPc1ZPMl9aemlOUy1majNEUFUiXSwiYWRkcmVzcyI6eyJfc2QiOlsiQnI1aVZtZnZlaTloQ01mMktVOGRFVjFER2hrdUtsQ1pUeGFEQ0FMb3NJbyIsIkx6czJpR09SNHF0clhhYmdwMzFfcjFFUFNmazlaUDJQRElJUTRQaHlPT00iLCJadUV5cG41Y0s0WVpWdHdkeGFoWXJqMjZ1MFI2UmxpOVVJWlNjUGhoWTB3Iiwidi1rMzl2VGI5NFI5a25VWTZtbzlXUVdEQkNJS3lya0J4bExTQVl3T2MyNCJdfSwiaXNzdWluZ19jb3VudHJ5IjoiREUiLCJ2Y3QiOiJodHRwczovL2V4YW1wbGUuYm1pLmJ1bmQuZGUvY3JlZGVudGlhbC9waWQvMS4wIiwiaXNzdWluZ19hdXRob3JpdHkiOiJERSIsIl9zZF9hbGciOiJzaGEtMjU2IiwiaXNzIjoiaHR0cHM6Ly9kZW1vLnBpZC1pc3N1ZXIuYnVuZGVzZHJ1Y2tlcmVpLmRlL2MxIiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IkhzS194Tl95SVU4eWlqdW9BWlhsbndFRU00ZlhZenVNRmd5TTE5SmRYMUkiLCJ5IjoiQUR2NnplVDl3YmgxU0ZxMG14TkcxMUZueC05eFdSRFcwR18xN1dSRXpRSSJ9fSwiZXhwIjoxNzMzNTcxMzI3LCJpYXQiOjE3MzIzNjE3MjcsImFnZV9lcXVhbF9vcl9vdmVyIjp7Il9zZCI6WyJLRDF0U0hnYWotZi1qbkZURkRDMW1sZ0RwNzhMZE1KcHlqWnRRU0k4a1ZnIiwiTDRjTTMtZU1mRHg0Znc2UEw3OVRTVFBnM042VXdzOGNPc3JOYmNqaEEtYyIsImRYUFBQX2lmNFM3XzBzcXZXNTBwZEdlMWszbS1wMnM3M1JicDlncThGaDAiLCJtYnllcU05YUkzRkVvWmFoODA5eTN0dlRCV1NvZTBMSlRUYTlONGNjdmlZIiwicm1zd0dEZnhvS0ZFYlFsNzZ4S1ZVT0hrX0MyQlVpVnQ5RDlvMTFrMmZNSSIsInZsY2Y4WTNhQnNTeEZBeVZfYk9NTndvX3FTT1pHc3ViSVZiY0FVSWVBSGMiXX19.gruqjNOuJBgHXEnG9e60wOoqiyEaL1K9pdL215a0ffZCjtIZ_kICDrO5vBiTrEmvjjd6w_N_thEYLhzob77Epg~WyJWRXlWQWF0LXoyNU8tbkQ0MVBaOGdnIiwiZmFtaWx5X25hbWUiLCJNVVNURVJNQU5OIl0~WyJLcnRPei1lRk9hMU9JYmpmUHUxcHRBIiwiZ2l2ZW5fbmFtZSIsIkVSSUtBIl0~WyJQQUVjSHp0NWk5bFFzNUZlRmFGUS1RIiwiYmlydGhkYXRlIiwiMTk2NC0wOC0xMiJd~', +}) +const mdocRecord = new MdocRecord({ + mdoc: Mdoc.fromBase64Url(sprindFunkeTestVectorBase64Url), +}) +const sdJwtVcRepository = agentContext.dependencyManager.resolve(SdJwtVcRepository) +const mdocRepository = agentContext.dependencyManager.resolve(MdocRepository) +const pexService = new DifPresentationExchangeService(agentContext.dependencyManager.resolve(W3cCredentialService)) + +const presentationDefinition: DifPresentationExchangeDefinitionV2 = { + id: '1ad8ea6e-ec51-4e14-b316-dd76a6275480', + name: 'PID and MDL - Rent a Car (vc+sd-jwt)', + purpose: 'To secure your car reservations and finalize the transaction, we require the following attributes', + input_descriptors: [ + { + id: 'bf8669f4-0cf3-4d16-b72b-b47eb702a7cd', + format: { + 'vc+sd-jwt': { + 'sd-jwt_alg_values': ['ES256'], + 'kb-jwt_alg_values': ['ES256'], + }, + }, + group: ['A'], + constraints: { + limit_disclosure: 'required', + fields: [ + { path: ['$.document_number'] }, + { path: ['$.portrait'] }, + { path: ['$.issue_date'] }, + { path: ['$.expiry_date'] }, + { path: ['$.issuing_country'] }, + { path: ['$.issuing_authority'] }, + { path: ['$.driving_priviliges'] }, + { + path: ['$.vct'], + filter: { + type: 'string', + enum: ['https://example.eudi.ec.europa.eu/mdl/1'], + }, + }, + ], + }, + }, + { + id: '99fce09b-a0d3-415b-b8a7-3eab8829babc', + format: { + 'vc+sd-jwt': { + 'sd-jwt_alg_values': ['ES256'], + 'kb-jwt_alg_values': ['ES256'], + }, + }, + group: ['B'], + constraints: { + limit_disclosure: 'required', + fields: [ + { path: ['$.given_name'] }, + { path: ['$.family_name'] }, + { path: ['$.birthdate'] }, + { + path: ['$.vct'], + filter: { + type: 'string', + enum: ['https://example.bmi.bund.de/credential/pid/1.0', 'urn:eu.europa.ec.eudi:pid:1'], + }, + }, + { + path: ['$.iss'], + filter: { + type: 'string', + enum: [ + 'https://demo.pid-issuer.bundesdruckerei.de/c', + 'https://demo.pid-issuer.bundesdruckerei.de/c1', + 'https://demo.pid-issuer.bundesdruckerei.de/b1', + ], + }, + }, + ], + }, + }, + { + id: 'eu.europa.ec.eudi.pid.1', + format: { + mso_mdoc: { + alg: ['ES256'], + }, + }, + group: ['C'], + constraints: { + limit_disclosure: 'required', + fields: [ + { + intent_to_retain: false, + path: ["$['eu.europa.ec.eudi.pid.1']['birth_date']"], + }, + ], + }, + }, + { + id: 'org.iso.18013.5.1.mDL', + format: { + mso_mdoc: { + alg: ['ES256'], + }, + }, + group: ['D'], + constraints: { + limit_disclosure: 'required', + fields: [ + { + intent_to_retain: false, + path: ["$['org.iso.18013.5.1']['given_name']"], + }, + ], + }, + }, + ], +} + +describe('DifPresentationExchangeService', () => { + beforeAll(async () => { + await sdJwtVcRepository.save(agentContext, sdJwtVcRecord) + await mdocRepository.save(agentContext, mdocRecord) + }) + + test('handles request where two credentials are requested but only one available', async () => { + const credentialsForRequest = await pexService.getCredentialsForRequest(agentContext, presentationDefinition) + expect(credentialsForRequest).toEqual({ + requirements: [ + { + rule: 'pick', + needsCount: 1, + submissionEntry: [ + { + inputDescriptorId: 'bf8669f4-0cf3-4d16-b72b-b47eb702a7cd', + name: undefined, + purpose: undefined, + verifiableCredentials: [], + }, + ], + isRequirementSatisfied: false, + }, + { + rule: 'pick', + needsCount: 1, + submissionEntry: [ + { + inputDescriptorId: '99fce09b-a0d3-415b-b8a7-3eab8829babc', + name: undefined, + purpose: undefined, + verifiableCredentials: [ + { + credentialRecord: await sdJwtVcRepository.getById(agentContext, sdJwtVcRecord.id), + disclosedPayload: { + address: {}, + age_equal_or_over: {}, + birthdate: '1964-08-12', + cnf: { + jwk: { + crv: 'P-256', + kty: 'EC', + x: 'HsK_xN_yIU8yijuoAZXlnwEEM4fXYzuMFgyM19JdX1I', + y: 'ADv6zeT9wbh1SFq0mxNG11Fnx-9xWRDW0G_17WREzQI', + }, + }, + exp: 1733571327, + family_name: 'MUSTERMANN', + given_name: 'ERIKA', + iat: 1732361727, + iss: 'https://demo.pid-issuer.bundesdruckerei.de/c1', + issuing_authority: 'DE', + issuing_country: 'DE', + place_of_birth: {}, + vct: 'https://example.bmi.bund.de/credential/pid/1.0', + }, + type: 'vc+sd-jwt', + }, + ], + }, + ], + isRequirementSatisfied: true, + }, + { + isRequirementSatisfied: true, + needsCount: 1, + rule: 'pick', + submissionEntry: [ + { + inputDescriptorId: 'eu.europa.ec.eudi.pid.1', + name: undefined, + purpose: undefined, + verifiableCredentials: [ + { + credentialRecord: await mdocRepository.getById(agentContext, mdocRecord.id), + disclosedPayload: { + 'eu.europa.ec.eudi.pid.1': { + birth_date: '1984-01-26', + }, + }, + type: 'mso_mdoc', + }, + ], + }, + ], + }, + { + rule: 'pick', + needsCount: 1, + submissionEntry: [ + { + inputDescriptorId: 'org.iso.18013.5.1.mDL', + name: undefined, + purpose: undefined, + verifiableCredentials: [], + }, + ], + isRequirementSatisfied: false, + }, + ], + areRequirementsSatisfied: false, + name: 'PID and MDL - Rent a Car (vc+sd-jwt)', + purpose: 'To secure your car reservations and finalize the transaction, we require the following attributes', + }) + }) + + test('handles request with submission requirements where two credentials are requested but only one available', async () => { + const credentialsForRequest = await pexService.getCredentialsForRequest(agentContext, { + ...presentationDefinition, + submission_requirements: [ + { + rule: 'pick', + count: 1, + from: 'A', + }, + { + rule: 'all', + from: 'B', + }, + { + rule: 'pick', + count: 1, + from: 'C', + }, + { + rule: 'all', + from: 'D', + }, + ], + }) + expect(credentialsForRequest).toEqual({ + requirements: [ + { + rule: 'pick', + needsCount: 1, + submissionEntry: [ + { + inputDescriptorId: 'bf8669f4-0cf3-4d16-b72b-b47eb702a7cd', + name: undefined, + purpose: undefined, + verifiableCredentials: [], + }, + ], + isRequirementSatisfied: false, + }, + { + rule: 'all', + needsCount: 1, + submissionEntry: [ + { + inputDescriptorId: '99fce09b-a0d3-415b-b8a7-3eab8829babc', + name: undefined, + purpose: undefined, + verifiableCredentials: [ + { + credentialRecord: await sdJwtVcRepository.getById(agentContext, sdJwtVcRecord.id), + disclosedPayload: { + address: {}, + age_equal_or_over: {}, + birthdate: '1964-08-12', + cnf: { + jwk: { + crv: 'P-256', + kty: 'EC', + x: 'HsK_xN_yIU8yijuoAZXlnwEEM4fXYzuMFgyM19JdX1I', + y: 'ADv6zeT9wbh1SFq0mxNG11Fnx-9xWRDW0G_17WREzQI', + }, + }, + exp: 1733571327, + family_name: 'MUSTERMANN', + given_name: 'ERIKA', + iat: 1732361727, + iss: 'https://demo.pid-issuer.bundesdruckerei.de/c1', + issuing_authority: 'DE', + issuing_country: 'DE', + place_of_birth: {}, + vct: 'https://example.bmi.bund.de/credential/pid/1.0', + }, + type: 'vc+sd-jwt', + }, + ], + }, + ], + isRequirementSatisfied: true, + }, + { + isRequirementSatisfied: true, + needsCount: 1, + rule: 'pick', + submissionEntry: [ + { + inputDescriptorId: 'eu.europa.ec.eudi.pid.1', + name: undefined, + purpose: undefined, + verifiableCredentials: [ + { + credentialRecord: await mdocRepository.getById(agentContext, mdocRecord.id), + disclosedPayload: { + 'eu.europa.ec.eudi.pid.1': { + birth_date: '1984-01-26', + }, + }, + type: 'mso_mdoc', + }, + ], + }, + ], + }, + { + rule: 'all', + needsCount: 1, + submissionEntry: [ + { + inputDescriptorId: 'org.iso.18013.5.1.mDL', + name: undefined, + purpose: undefined, + verifiableCredentials: [], + }, + ], + isRequirementSatisfied: false, + }, + ], + areRequirementsSatisfied: false, + name: 'PID and MDL - Rent a Car (vc+sd-jwt)', + purpose: 'To secure your car reservations and finalize the transaction, we require the following attributes', + }) + }) +}) diff --git a/packages/core/src/modules/dif-presentation-exchange/models/DifPexCredentialsForRequest.ts b/packages/core/src/modules/dif-presentation-exchange/models/DifPexCredentialsForRequest.ts index 2d5e32c87d..9ac8e38daf 100644 --- a/packages/core/src/modules/dif-presentation-exchange/models/DifPexCredentialsForRequest.ts +++ b/packages/core/src/modules/dif-presentation-exchange/models/DifPexCredentialsForRequest.ts @@ -1,7 +1,6 @@ -import type { MdocRecord } from '../../mdoc' +import type { MdocNameSpaces, MdocRecord } from '../../mdoc' import type { SdJwtVcRecord } from '../../sd-jwt-vc' import type { ClaimFormat, W3cCredentialRecord } from '../../vc' -import type { MdocNameSpaces } from '@animo-id/mdoc' export interface DifPexCredentialsForRequest { /** diff --git a/packages/core/src/modules/dif-presentation-exchange/models/index.ts b/packages/core/src/modules/dif-presentation-exchange/models/index.ts index 1a86a09526..8447a360bb 100644 --- a/packages/core/src/modules/dif-presentation-exchange/models/index.ts +++ b/packages/core/src/modules/dif-presentation-exchange/models/index.ts @@ -4,7 +4,7 @@ import type { SdJwtVc } from '../../sd-jwt-vc' import type { W3cVerifiableCredential, W3cVerifiablePresentation } from '../../vc' import type { PresentationDefinitionV1, PresentationDefinitionV2, PresentationSubmission } from '@sphereon/pex-models' -import { PresentationSubmissionLocation } from '@sphereon/pex' +import { PresentationSubmissionLocation } from '@animo-id/pex' // Re-export some types from sphereon library, but under more explicit names export type DifPresentationExchangeDefinition = PresentationDefinitionV1 | PresentationDefinitionV2 diff --git a/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts b/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts index d63b795cc9..c6aef4c717 100644 --- a/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts +++ b/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts @@ -4,18 +4,21 @@ import type { DifPexCredentialsForRequestSubmissionEntry, SubmissionEntryCredential, } from '../models' -import type { IPresentationDefinition, SelectResults, SubmissionRequirementMatch, PEX } from '@sphereon/pex' +import type { IPresentationDefinition, SelectResults, SubmissionRequirementMatch, PEX } from '@animo-id/pex' +import type { + SubmissionRequirementMatchFrom, + SubmissionRequirementMatchInputDescriptor, +} from '@animo-id/pex/dist/main/lib/evaluation/core' import type { InputDescriptorV1, InputDescriptorV2, SubmissionRequirement } from '@sphereon/pex-models' +import { Status } from '@animo-id/pex' +import { SubmissionRequirementMatchType } from '@animo-id/pex/dist/main/lib/evaluation/core' +import { JSONPath } from '@astronautlabs/jsonpath' import { decodeSdJwtSync, getClaimsSync } from '@sd-jwt/decode' -import { Status } from '@sphereon/pex' -import { SubmissionRequirementMatchType } from '@sphereon/pex/dist/main/lib/evaluation/core' import { Rules } from '@sphereon/pex-models' -import { default as jp } from 'jsonpath' import { Hasher } from '../../../crypto' import { CredoError } from '../../../error' -import { deepEquality } from '../../../utils' import { MdocRecord } from '../../mdoc' import { Mdoc } from '../../mdoc/Mdoc' import { MdocDeviceResponse } from '../../mdoc/MdocDeviceResponse' @@ -31,106 +34,46 @@ export async function getCredentialsForRequest( presentationDefinition: IPresentationDefinition, credentialRecords: Array ): Promise { - const encodedCredentials = credentialRecords - .filter((c): c is Exclude => c instanceof MdocRecord === false) - .map((c) => getSphereonOriginalVerifiableCredential(c)) - - const { mdocPresentationDefinition, nonMdocPresentationDefinition } = - MdocDeviceResponse.partitionPresentationDefinition(presentationDefinition) - - const selectResultsRaw = pex.selectFrom(nonMdocPresentationDefinition, encodedCredentials) + const encodedCredentials = credentialRecords.map(getSphereonOriginalVerifiableCredential) + const selectResultsRaw = pex.selectFrom(presentationDefinition, encodedCredentials) const selectResults: CredentialRecordSelectResults = { ...selectResultsRaw, - areRequiredCredentialsPresent: - nonMdocPresentationDefinition.input_descriptors.length === 0 && - mdocPresentationDefinition.input_descriptors.length > 0 - ? Status.INFO - : selectResultsRaw.areRequiredCredentialsPresent, - // Map the encoded credential to their respective w3c credential record - verifiableCredential: selectResultsRaw.verifiableCredential?.map((selectedEncoded): SubmissionEntryCredential => { - const credentialRecordIndex = encodedCredentials.findIndex((encoded) => { - if ( - typeof selectedEncoded === 'string' && - selectedEncoded.includes('~') && - typeof encoded === 'string' && - encoded.includes('~') - ) { - // FIXME: pex applies SD-JWT, so we actually can't match the record anymore :( - // We take the first part of the sd-jwt, as that will never change, and should - // be unique on it's own - const [encodedJwt] = encoded.split('~') - const [selectedEncodedJwt] = selectedEncoded.split('~') - - return encodedJwt === selectedEncodedJwt - } else { - return deepEquality(selectedEncoded, encoded) - } - }) - - if (credentialRecordIndex === -1) { - throw new DifPresentationExchangeError('Unable to find credential in credential records.') - } - - const credentialRecord = credentialRecords[credentialRecordIndex] - if (credentialRecord instanceof SdJwtVcRecord) { - // selectedEncoded always string when SdJwtVcRecord - // Get the decoded payload from the the selected credential, this already has SD applied - const { jwt, disclosures } = decodeSdJwtSync(selectedEncoded as string, Hasher.hash) - const prettyClaims = getClaimsSync(jwt.payload, disclosures, Hasher.hash) - - return { - type: ClaimFormat.SdJwtVc, - credentialRecord, - disclosedPayload: prettyClaims as Record, + matches: selectResultsRaw.matches ?? [], + // Map the encoded credential to their respective credential record + verifiableCredential: + selectResultsRaw.verifiableCredential?.map((selectedEncoded, index): SubmissionEntryCredential => { + const credentialRecordIndex = selectResultsRaw.vcIndexes?.[index] + if (credentialRecordIndex === undefined || credentialRecordIndex === -1) { + throw new DifPresentationExchangeError('Unable to find credential in credential records.') } - } else if (credentialRecord instanceof W3cCredentialRecord) { - return { - type: credentialRecord.credential.claimFormat, - credentialRecord, + const credentialRecord = credentialRecords[credentialRecordIndex] + if (credentialRecord instanceof SdJwtVcRecord) { + // selectedEncoded always string when SdJwtVcRecord + // Get the decoded payload from the the selected credential, this already has SD applied + const { jwt, disclosures } = decodeSdJwtSync(selectedEncoded as string, Hasher.hash) + const prettyClaims = getClaimsSync(jwt.payload, disclosures, Hasher.hash) + + return { + type: ClaimFormat.SdJwtVc, + credentialRecord, + disclosedPayload: prettyClaims as Record, + } + } else if (credentialRecord instanceof MdocRecord) { + return { + type: ClaimFormat.MsoMdoc, + credentialRecord, + disclosedPayload: {}, + } + } else if (credentialRecord instanceof W3cCredentialRecord) { + return { + type: credentialRecord.credential.claimFormat, + credentialRecord, + } + } else { + throw new CredoError(`Unrecognized credential record type`) } - } else { - throw new CredoError(`Unrecognized credential record type`) - } - }), - } - - const mdocRecords = credentialRecords.filter((c) => c instanceof MdocRecord) - for (const mdocInputDescriptor of mdocPresentationDefinition.input_descriptors) { - if (!selectResults.verifiableCredential) selectResults.verifiableCredential = [] - if (!selectResults.matches) selectResults.matches = [] - - const mdocRecordsMatchingId = mdocRecords.filter( - (mdocRecord) => mdocRecord.getTags().docType === mdocInputDescriptor.id - ) - const submissionRequirementMatch: SubmissionRequirementMatch = { - id: mdocInputDescriptor.id, - type: SubmissionRequirementMatchType.InputDescriptor, - name: mdocInputDescriptor.id, - rule: Rules.Pick, - vc_path: [], - } - - for (const mdocRecordMatchingId of mdocRecordsMatchingId) { - selectResults.verifiableCredential.push({ - type: ClaimFormat.MsoMdoc, - credentialRecord: mdocRecordMatchingId, - disclosedPayload: MdocDeviceResponse.limitDisclosureToInputDescriptor({ - mdoc: Mdoc.fromBase64Url(mdocRecordMatchingId.base64Url), - inputDescriptor: mdocInputDescriptor as InputDescriptorV2, - }), - }) - - submissionRequirementMatch.vc_path.push( - `$.verifiableCredential[${selectResults.verifiableCredential.length - 1}]` - ) - } - - if (submissionRequirementMatch.vc_path.length >= 1) { - selectResults.matches.push(submissionRequirementMatch) - } else { - selectResultsRaw.areRequiredCredentialsPresent = 'error' - } + }) ?? [], } const presentationSubmission: DifPexCredentialsForRequest = { @@ -150,6 +93,47 @@ export async function getCredentialsForRequest( presentationSubmission.requirements = getSubmissionRequirements(presentationDefinition, selectResults) } + const allEntries = presentationSubmission.requirements.flatMap((requirement) => requirement.submissionEntry) + + const inputDescriptorsForMdocCredential = new Map>() + for (const entry of allEntries) + for (const verifiableCredential of entry.verifiableCredentials) { + if (verifiableCredential.type !== ClaimFormat.MsoMdoc) continue + + const set = inputDescriptorsForMdocCredential.get(verifiableCredential) ?? new Set() + set.add(entry.inputDescriptorId) + inputDescriptorsForMdocCredential.set(verifiableCredential, set) + } + + // NOTE: it might be better to apply disclosure per credential/match (as that's also how mdoc does this) + // however this doesn't work very well in wallets, as you usually won't show the same credential twice with + // different disclosed attributes + // Apply limit disclosure for all mdocs + for (const [verifiableCredential, inputDescriptorIds] of inputDescriptorsForMdocCredential.entries()) { + if (verifiableCredential.type !== ClaimFormat.MsoMdoc) continue + + const inputDescriptorsForCredential = presentationDefinition.input_descriptors.filter(({ id }) => + inputDescriptorIds.has(id) + ) + + const mdoc = Mdoc.fromBase64Url(verifiableCredential.credentialRecord.base64Url) + verifiableCredential.disclosedPayload = MdocDeviceResponse.limitDisclosureToInputDescriptor({ + inputDescriptor: { + id: mdoc.docType, + format: { + mso_mdoc: { + alg: [], + }, + }, + constraints: { + limit_disclosure: 'required', + fields: inputDescriptorsForCredential.flatMap((i) => i.constraints?.fields ?? []), + }, + }, + mdoc: Mdoc.fromBase64Url(verifiableCredential.credentialRecord.base64Url), + }) + } + // There may be no requirements if we filter out all optional ones. To not makes things too complicated, we see it as an error // for now if a request is made that has no required requirements (but only e.g. min: 0, which means we don't need to disclose anything) // I see this more as the fault of the presentation definition, as it should have at least some requirements. @@ -158,7 +142,8 @@ export async function getCredentialsForRequest( 'Presentation Definition does not require any credentials. Optional credentials are not included in the presentation submission.' ) } - if (selectResults.areRequiredCredentialsPresent === 'error') { + + if (selectResults.areRequiredCredentialsPresent === Status.ERROR) { return presentationSubmission } @@ -178,9 +163,16 @@ function getSubmissionRequirements( ): Array { const submissionRequirements: Array = [] + const matches = selectResults.matches as SubmissionRequirementMatchFrom[] + if (!matches.every((match) => match.type === SubmissionRequirementMatchType.SubmissionRequirement && match.from)) { + throw new DifPresentationExchangeError( + `Expected all matches to be of type '${SubmissionRequirementMatchType.SubmissionRequirement}' with 'from' key.` + ) + } + // There are submission requirements, so we need to select the input_descriptors // based on the submission requirements - for (const submissionRequirement of presentationDefinition.submission_requirements ?? []) { + presentationDefinition.submission_requirements?.forEach((submissionRequirement, submissionRequirementIndex) => { // Check: if the submissionRequirement uses `from_nested`, as we don't support this yet if (submissionRequirement.from_nested) { throw new DifPresentationExchangeError( @@ -193,23 +185,30 @@ function getSubmissionRequirements( throw new DifPresentationExchangeError("Missing 'from' in submission requirement match") } + const match = matches.find((match) => match.id === submissionRequirementIndex) + if (!match) { + throw new Error(`Unable to find a match for submission requirement with index '${submissionRequirementIndex}'`) + } + if (submissionRequirement.rule === Rules.All) { const selectedSubmission = getSubmissionRequirementRuleAll( submissionRequirement, presentationDefinition, - selectResults + selectResults.verifiableCredential, + match ) submissionRequirements.push(selectedSubmission) } else { const selectedSubmission = getSubmissionRequirementRulePick( submissionRequirement, presentationDefinition, - selectResults + selectResults.verifiableCredential, + match ) submissionRequirements.push(selectedSubmission) } - } + }) // Submission may have requirement that doesn't require a credential to be submitted (e.g. min: 0) // We use minimization strategy, and thus only disclose the minimum amount of information @@ -224,9 +223,15 @@ function getSubmissionRequirementsForAllInputDescriptors( ): Array { const submissionRequirements: Array = [] - for (const inputDescriptor of inputDescriptors) { - const submission = getSubmissionForInputDescriptor(inputDescriptor, selectResults) + const matches = selectResults.matches as SubmissionRequirementMatchInputDescriptor[] + if (!matches.every((match) => match.type === SubmissionRequirementMatchType.InputDescriptor)) { + throw new DifPresentationExchangeError( + `Expected all matches to be of type '${SubmissionRequirementMatchType.InputDescriptor}' when.` + ) + } + for (const inputDescriptor of inputDescriptors) { + const submission = getSubmissionForInputDescriptor(inputDescriptor, selectResults.verifiableCredential, matches) submissionRequirements.push({ rule: Rules.Pick, needsCount: 1, // Every input descriptor is a distinct requirement, so the count is always 1, @@ -241,7 +246,8 @@ function getSubmissionRequirementsForAllInputDescriptors( function getSubmissionRequirementRuleAll( submissionRequirement: SubmissionRequirement, presentationDefinition: IPresentationDefinition, - selectResults: CredentialRecordSelectResults + verifiableCredentials: SubmissionEntryCredential[], + match: SubmissionRequirementMatchFrom ) { // Check if there's a 'from'. If not the structure is not as we expect it if (!submissionRequirement.from) @@ -258,9 +264,9 @@ function getSubmissionRequirementRuleAll( for (const inputDescriptor of presentationDefinition.input_descriptors) { // We only want to get the submission if the input descriptor belongs to the group - if (!inputDescriptor.group?.includes(submissionRequirement.from)) continue + if (!inputDescriptor.group?.includes(match.from)) continue - const submission = getSubmissionForInputDescriptor(inputDescriptor, selectResults) + const submission = getSubmissionForInputDescriptor(inputDescriptor, verifiableCredentials, match.input_descriptors) // Rule ALL, so for every input descriptor that matches in this group, we need to add it selectedSubmission.needsCount += 1 @@ -280,7 +286,8 @@ function getSubmissionRequirementRuleAll( function getSubmissionRequirementRulePick( submissionRequirement: SubmissionRequirement, presentationDefinition: IPresentationDefinition, - selectResults: CredentialRecordSelectResults + verifiableCredentials: SubmissionEntryCredential[], + match: SubmissionRequirementMatchFrom ) { // Check if there's a 'from'. If not the structure is not as we expect it if (!submissionRequirement.from) { @@ -303,9 +310,9 @@ function getSubmissionRequirementRulePick( for (const inputDescriptor of presentationDefinition.input_descriptors) { // We only want to get the submission if the input descriptor belongs to the group - if (!inputDescriptor.group?.includes(submissionRequirement.from)) continue + if (!inputDescriptor.group?.includes(match.from)) continue - const submission = getSubmissionForInputDescriptor(inputDescriptor, selectResults) + const submission = getSubmissionForInputDescriptor(inputDescriptor, verifiableCredentials, match.input_descriptors) if (submission.verifiableCredentials.length >= 1) { satisfiedSubmissions.push(submission) @@ -336,47 +343,34 @@ function getSubmissionRequirementRulePick( function getSubmissionForInputDescriptor( inputDescriptor: InputDescriptorV1 | InputDescriptorV2, - selectResults: CredentialRecordSelectResults + verifiableCredentials: SubmissionEntryCredential[], + matches: SubmissionRequirementMatchInputDescriptor[] ): DifPexCredentialsForRequestSubmissionEntry { - // https://github.com/Sphereon-Opensource/PEX/issues/116 - // If the input descriptor doesn't contain a name, the name of the match will be the id of the input descriptor that satisfied it - const matchesForInputDescriptor = selectResults.matches?.filter( - (m) => - m.name === inputDescriptor.id || - // FIXME: this is not collision proof as the name doesn't have to be unique - m.name === inputDescriptor.name - ) + const matchesForInputDescriptor = matches.filter((m) => m.id === inputDescriptor.id) const submissionEntry: DifPexCredentialsForRequestSubmissionEntry = { inputDescriptorId: inputDescriptor.id, name: inputDescriptor.name, purpose: inputDescriptor.purpose, - verifiableCredentials: [], + verifiableCredentials: matchesForInputDescriptor.flatMap((matchForInputDescriptor) => + extractCredentialsFromInputDescriptorMatch(matchForInputDescriptor, verifiableCredentials) + ), } // return early if no matches. if (!matchesForInputDescriptor?.length) return submissionEntry - // FIXME: This can return multiple credentials for multiple input_descriptors, - // which I think is a bug in the PEX library - // Extract all credentials from the match - const verifiableCredentials = matchesForInputDescriptor.flatMap((matchForInputDescriptor) => - extractCredentialsFromMatch(matchForInputDescriptor, selectResults.verifiableCredential) - ) - - submissionEntry.verifiableCredentials = verifiableCredentials - return submissionEntry } -function extractCredentialsFromMatch( - match: SubmissionRequirementMatch, - availableCredentials?: SubmissionEntryCredential[] +function extractCredentialsFromInputDescriptorMatch( + match: SubmissionRequirementMatchInputDescriptor, + availableCredentials: SubmissionEntryCredential[] ) { const verifiableCredentials: SubmissionEntryCredential[] = [] for (const vcPath of match.vc_path) { - const [verifiableCredential] = jp.query( + const [verifiableCredential] = JSONPath.query( { verifiableCredential: availableCredentials }, vcPath ) as SubmissionEntryCredential[] @@ -390,5 +384,6 @@ function extractCredentialsFromMatch( * Custom SelectResults that includes the Credo records instead of the encoded verifiable credential */ type CredentialRecordSelectResults = Omit & { - verifiableCredential?: SubmissionEntryCredential[] + verifiableCredential: SubmissionEntryCredential[] + matches: SubmissionRequirementMatch[] } diff --git a/packages/core/src/modules/dif-presentation-exchange/utils/presentationSelection.ts b/packages/core/src/modules/dif-presentation-exchange/utils/presentationSelection.ts index b223fcd65b..79306b27fe 100644 --- a/packages/core/src/modules/dif-presentation-exchange/utils/presentationSelection.ts +++ b/packages/core/src/modules/dif-presentation-exchange/utils/presentationSelection.ts @@ -6,7 +6,7 @@ import type { VerifiablePresentation, } from '../models' -import { default as jp } from 'jsonpath' +import { JSONPath } from '@astronautlabs/jsonpath' import { CredoError } from '../../../error' import { MdocDeviceResponse } from '../../mdoc' @@ -21,7 +21,7 @@ export function extractPresentationsWithDescriptorsFromSubmission( definition: DifPresentationExchangeDefinition ) { return submission.descriptor_map.map((descriptor) => { - const [presentation] = jp.query(presentations, descriptor.path) as [VerifiablePresentation | undefined] + const [presentation] = JSONPath.query(presentations, descriptor.path) as [VerifiablePresentation | undefined] const inputDescriptor = definition.input_descriptors.find(({ id }) => id === descriptor.id) if (!presentation) { @@ -61,7 +61,7 @@ export function extractPresentationsWithDescriptorsFromSubmission( ) } - const [verifiableCredential] = jp.query( + const [verifiableCredential] = JSONPath.query( // Path is `$.vp.verifiableCredential[]` in case of jwt vp presentation.claimFormat === ClaimFormat.JwtVp ? { vp: presentation } : presentation, descriptor.path_nested.path diff --git a/packages/core/src/modules/dif-presentation-exchange/utils/transform.ts b/packages/core/src/modules/dif-presentation-exchange/utils/transform.ts index 87bcafdfbb..7fa2ba018e 100644 --- a/packages/core/src/modules/dif-presentation-exchange/utils/transform.ts +++ b/packages/core/src/modules/dif-presentation-exchange/utils/transform.ts @@ -10,15 +10,17 @@ import type { import { Jwt } from '../../../crypto' import { JsonTransformer } from '../../../utils' -import { MdocDeviceResponse } from '../../mdoc' +import { MdocDeviceResponse, MdocRecord } from '../../mdoc' import { SdJwtVcApi } from '../../sd-jwt-vc' import { W3cCredentialRecord, W3cJsonLdVerifiablePresentation, W3cJwtVerifiablePresentation } from '../../vc' export function getSphereonOriginalVerifiableCredential( - credentialRecord: W3cCredentialRecord | SdJwtVcRecord + credentialRecord: W3cCredentialRecord | SdJwtVcRecord | MdocRecord ): SphereonOriginalVerifiableCredential { if (credentialRecord instanceof W3cCredentialRecord) { return credentialRecord.credential.encoded as SphereonOriginalVerifiableCredential + } else if (credentialRecord instanceof MdocRecord) { + return credentialRecord.base64Url } else { return credentialRecord.compactSdJwtVc } diff --git a/packages/core/src/modules/mdoc/Mdoc.ts b/packages/core/src/modules/mdoc/Mdoc.ts index 85ee56e00c..1cf26c328d 100644 --- a/packages/core/src/modules/mdoc/Mdoc.ts +++ b/packages/core/src/modules/mdoc/Mdoc.ts @@ -80,18 +80,28 @@ export class Mdoc { throw new MdocError(`Cannot get 'device-namespaces from a IssuerSignedDocument. Must be a DeviceSignedDocument.`) } - return this.issuerSignedDocument.allDeviceSignedNamespaces + return Object.fromEntries( + Array.from(this.issuerSignedDocument.allDeviceSignedNamespaces.entries()).map(([namespace, value]) => [ + namespace, + Object.fromEntries(Array.from(value.entries())), + ]) + ) } public get issuerSignedNamespaces(): MdocNameSpaces { - return this.issuerSignedDocument.allIssuerSignedNamespaces + return Object.fromEntries( + Array.from(this.issuerSignedDocument.allIssuerSignedNamespaces.entries()).map(([namespace, value]) => [ + namespace, + Object.fromEntries(Array.from(value.entries())), + ]) + ) } public static async sign(agentContext: AgentContext, options: MdocSignOptions) { const { docType, validityInfo, namespaces, holderKey, issuerCertificate } = options const mdocContext = getMdocContext(agentContext) - const holderPublicJwk = await getJwkFromKey(holderKey) + const holderPublicJwk = getJwkFromKey(holderKey) const document = new Document(docType, mdocContext) .useDigestAlgorithm('SHA-256') .addValidityInfo(validityInfo) diff --git a/packages/core/src/modules/mdoc/MdocDeviceResponse.ts b/packages/core/src/modules/mdoc/MdocDeviceResponse.ts index a91931e095..dda7266c86 100644 --- a/packages/core/src/modules/mdoc/MdocDeviceResponse.ts +++ b/packages/core/src/modules/mdoc/MdocDeviceResponse.ts @@ -139,7 +139,7 @@ export class MdocDeviceResponse { const disclosure = mdocLimitDisclosureToInputDescriptor(_mdoc, inputDescriptor) const disclosedPayloadAsRecord = Object.fromEntries( - Object.entries(disclosure).map(([namespace, issuerSignedItem]) => { + Array.from(disclosure.entries()).map(([namespace, issuerSignedItem]) => { return [ namespace, Object.fromEntries(issuerSignedItem.map((item) => [item.elementIdentifier, item.elementValue])), @@ -174,7 +174,7 @@ export class MdocDeviceResponse { const publicDeviceJwk = COSEKey.import(deviceKeyInfo.deviceKey).toJWK() - const deviceResponseBuilder = await DeviceResponse.from(mdoc) + const deviceResponseBuilder = DeviceResponse.from(mdoc) .usingPresentationDefinition(presentationDefinition) .usingSessionTranscriptForOID4VP(sessionTranscriptOptions) .authenticateWithSignature(publicDeviceJwk, 'ES256') diff --git a/packages/core/src/modules/mdoc/MdocOptions.ts b/packages/core/src/modules/mdoc/MdocOptions.ts index db43969be1..843b923915 100644 --- a/packages/core/src/modules/mdoc/MdocOptions.ts +++ b/packages/core/src/modules/mdoc/MdocOptions.ts @@ -1,9 +1,9 @@ import type { Mdoc } from './Mdoc' import type { Key } from '../../crypto/Key' import type { DifPresentationExchangeDefinition } from '../dif-presentation-exchange' -import type { ValidityInfo, MdocNameSpaces } from '@animo-id/mdoc' +import type { ValidityInfo } from '@animo-id/mdoc' -export type { MdocNameSpaces } from '@animo-id/mdoc' +export type MdocNameSpaces = Record> export interface MdocVerificationContext { /** @@ -47,7 +47,7 @@ export type MdocSignOptions = { // eslint-disable-next-line @typescript-eslint/ban-types docType: 'org.iso.18013.5.1.mDL' | (string & {}) validityInfo?: Partial - namespaces: { [namespace: string]: Record } + namespaces: MdocNameSpaces /** * diff --git a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts index e02bc8b08e..e951708d34 100644 --- a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts +++ b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts @@ -2,7 +2,7 @@ import type { DifPresentationExchangeDefinitionV1 } from '../../../../dif-presen import type { ProofFormatService } from '../../ProofFormatService' import type { DifPresentationExchangeProofFormat } from '../DifPresentationExchangeProofFormat' -import { PresentationSubmissionLocation } from '@sphereon/pex' +import { PresentationSubmissionLocation } from '@animo-id/pex' import { getInMemoryAgentOptions } from '../../../../../../tests' import { Agent } from '../../../../../agent/Agent' diff --git a/packages/openid4vc/package.json b/packages/openid4vc/package.json index 850d87364c..1d85d1e105 100644 --- a/packages/openid4vc/package.json +++ b/packages/openid4vc/package.json @@ -34,8 +34,8 @@ "class-transformer": "^0.5.1", "rxjs": "^7.8.0", "zod": "^3.23.8", - "@animo-id/oid4vci": "0.1.3", - "@animo-id/oauth2": "0.1.3" + "@animo-id/oid4vci": "0.1.4", + "@animo-id/oauth2": "0.1.4" }, "devDependencies": { "@credo-ts/tenants": "workspace:*", diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts index bd85ceecf8..87d6af88d3 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts @@ -141,7 +141,12 @@ export class OpenId4VcHolderApi { options ) - return { accessToken: accessTokenResponse.access_token, cNonce: accessTokenResponse.c_nonce, dpop } + return { + accessToken: accessTokenResponse.access_token, + cNonce: accessTokenResponse.c_nonce, + dpop, + accessTokenResponse, + } } /** diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts index 1e342c8976..edf029283f 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts @@ -404,6 +404,11 @@ export class OpenId4VciHolderService { // Else: use 1 const batchSize = requestBatch === true ? metadata.credentialIssuer.batch_credential_issuance?.batch_size ?? 1 : requestBatch || 1 + if (typeof requestBatch === 'number' && requestBatch > 1 && !metadata.credentialIssuer.batch_credential_issuance) { + throw new CredoError( + `Credential issuer '${metadata.credentialIssuer.credential_issuer}' does not support batch credential issuance using the 'proofs' request property. Onlt 'proof' supported.` + ) + } for (const [offeredCredentialId, offeredCredentialConfiguration] of credentialConfigurationsToRequest) { // Get all options for the credential request (such as which kid to use, the signature algorithm, etc) diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderServiceOptions.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderServiceOptions.ts index 90678436e2..9379a79b8c 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderServiceOptions.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderServiceOptions.ts @@ -1,5 +1,6 @@ import type { OpenId4VcCredentialHolderBinding, + OpenId4VciAccessTokenResponse, OpenId4VciCredentialConfigurationsSupportedWithFormats, } from '../shared' import type { CredentialOfferObject, IssuerMetadataResult } from '@animo-id/oid4vci' @@ -43,6 +44,8 @@ export type OpenId4VciRequestTokenResponse = { accessToken: string cNonce?: string dpop?: OpenId4VciDpopRequestOptions + + accessTokenResponse: OpenId4VciAccessTokenResponse } // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/openid4vc/src/shared/models/index.ts b/packages/openid4vc/src/shared/models/index.ts index c99cbeab96..aa01206940 100644 --- a/packages/openid4vc/src/shared/models/index.ts +++ b/packages/openid4vc/src/shared/models/index.ts @@ -1,3 +1,4 @@ +import type { AccessTokenResponse } from '@animo-id/oauth2' import type { CredentialConfigurationSupported, CredentialConfigurationSupportedWithFormats, @@ -29,6 +30,7 @@ export type OpenId4VciCredentialConfigurationsSupportedWithFormats = Record< OpenId4VciCredentialConfigurationSupportedWithFormats > +export type OpenId4VciAccessTokenResponse = AccessTokenResponse export type OpenId4VciMetadata = IssuerMetadataResult export type OpenId4VciTxCode = CredentialOfferPreAuthorizedCodeGrantTxCode diff --git a/packages/openid4vc/src/shared/utils.ts b/packages/openid4vc/src/shared/utils.ts index 7ab19d555f..7b97e32c74 100644 --- a/packages/openid4vc/src/shared/utils.ts +++ b/packages/openid4vc/src/shared/utils.ts @@ -327,5 +327,5 @@ export function addSecondsToDate(date: Date, seconds: number) { } export function dateToSeconds(date: Date) { - return Math.floor(date.getTime() * 1000) + return Math.floor(date.getTime() / 1000) } diff --git a/packages/openid4vc/tests/openid4vc.e2e.test.ts b/packages/openid4vc/tests/openid4vc.e2e.test.ts index 88d78f9f07..d35092463b 100644 --- a/packages/openid4vc/tests/openid4vc.e2e.test.ts +++ b/packages/openid4vc/tests/openid4vc.e2e.test.ts @@ -2153,9 +2153,7 @@ describe('OpenId4Vc', () => { issuerSignedDocument: { docType: 'org.eu.university', issuerSigned: { - nameSpaces: { - 'eu.europa.ec.eudi.pid.1': [{}, {}], - }, + nameSpaces: new Map([['eu.europa.ec.eudi.pid.1', [{}, {}]]]), issuerAuth: expect.any(Object), }, deviceSigned: expect.any(Object), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 23852a41fb..e77d3407bb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -437,8 +437,14 @@ importers: packages/core: dependencies: '@animo-id/mdoc': - specifier: 0.2.38 - version: 0.2.38 + specifier: 0.2.39 + version: 0.2.39 + '@animo-id/pex': + specifier: 4.1.1-alpha.0 + version: 4.1.1-alpha.0 + '@astronautlabs/jsonpath': + specifier: ^1.1.2 + version: 1.1.2 '@digitalcredentials/jsonld': specifier: ^6.0.0 version: 6.0.0(expo@51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.71.19(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(react@18.3.1))(web-streams-polyfill@3.3.3) @@ -487,9 +493,6 @@ importers: '@sd-jwt/utils': specifier: ^0.7.0 version: 0.7.2 - '@sphereon/pex': - specifier: 5.0.0-unstable.25 - version: 5.0.0-unstable.25 '@sphereon/pex-models': specifier: ^2.3.1 version: 2.3.1 @@ -523,9 +526,6 @@ importers: did-resolver: specifier: ^4.1.0 version: 4.1.0 - jsonpath: - specifier: ^1.1.1 - version: 1.1.1 lru_map: specifier: ^0.4.1 version: 0.4.1 @@ -566,9 +566,6 @@ importers: '@types/events': specifier: ^3.0.0 version: 3.0.3 - '@types/jsonpath': - specifier: ^0.2.4 - version: 0.2.4 '@types/luxon': specifier: ^3.2.0 version: 3.4.2 @@ -724,11 +721,11 @@ importers: packages/openid4vc: dependencies: '@animo-id/oauth2': - specifier: 0.1.3 - version: 0.1.3(typescript@5.5.4) + specifier: 0.1.4 + version: 0.1.4(typescript@5.5.4) '@animo-id/oid4vci': - specifier: 0.1.3 - version: 0.1.3(typescript@5.5.4) + specifier: 0.1.4 + version: 0.1.4(typescript@5.5.4) '@credo-ts/core': specifier: workspace:* version: link:../core @@ -937,17 +934,21 @@ packages: react: '*' react-native: '*' - '@animo-id/mdoc@0.2.38': - resolution: {integrity: sha512-98KQ0jvwTYsFOffTGvvHXBDo23b5xmhYjPiMIX6e807I6iS4fZZ9ypfBySdA5IiGUvXELKqEv27AUaayQa/9bg==} + '@animo-id/mdoc@0.2.39': + resolution: {integrity: sha512-Fn+ltm086U8xentP4aJtGksfRFXa5YMg9/stAfDLgfpoue2gKaitwKUk7/shrl+bV1t1m5qdhKVcJ0uVMk/nLA==} - '@animo-id/oauth2-utils@0.1.3': - resolution: {integrity: sha512-PzAx57LbDmmhI1qnF6Y/soYHLyHXxheSzlle+8rHexZmnWHXwxJ5nyOn/EQhGOqk5UEXLHYsD+27oyrMH3iR4A==} + '@animo-id/oauth2-utils@0.1.4': + resolution: {integrity: sha512-JUIshkNCiX7myPwdiE+Da/g77OjsxZreezmfXWBSWOgCOZrnQX+t5vkhpe6Jc6DRh40Y8r9Fa1AesV0vtVpUMg==} - '@animo-id/oauth2@0.1.3': - resolution: {integrity: sha512-e4i+9nn3hyaxJ5LFTRb8Ri43VCAN5xpOvD0o2DcL6U90Y5ih3L+GVU6pzYylwk0iX/VD/HCMayDvF9qusDBh4w==} + '@animo-id/oauth2@0.1.4': + resolution: {integrity: sha512-7FgiSJpkuWrMmYfh39VHcqbuQIsY3Xm66IUQiGBgy4g4DvMnRZ2Orj4LNrEnlr8qUnk4GDJ56zW2Bp55T70gSQ==} - '@animo-id/oid4vci@0.1.3': - resolution: {integrity: sha512-01ka6sIQUVXNcrw6/fcWCbFpso60bb9Ejv4WUunA/7pGiowdzmmf4aaMRd2em/v5riRpo2tsIqv23SAbyTl41A==} + '@animo-id/oid4vci@0.1.4': + resolution: {integrity: sha512-0Tc8RFpY5dW/MOVA2Fz8f7giM/eIAk3G3OZUjy5POqg5Tkiv92CcaOxMk84JBNAMOks3LpKjf9PAiNh6pCDzeQ==} + + '@animo-id/pex@4.1.1-alpha.0': + resolution: {integrity: sha512-6ieHhH9UE9DLFOJegMCabG3qUFlQk4TLhBefxInpyjx2Ly6kuloVMScJYcnQTs/E6nuHGMd7ebUaKy4+0+ZbOA==} + engines: {node: '>=18'} '@animo-id/react-native-bbs-signatures@0.1.0': resolution: {integrity: sha512-7qvsiWhGfUev8ngE8YzF6ON9PtCID5LiYVYM4EC5eyj80gCdhx3R46CI7K1qbqIlGsoTYQ/Xx5Ubo5Ji9eaUEA==} @@ -2942,10 +2943,6 @@ packages: resolution: {integrity: sha512-CZc+kr8cJqPsFSpg4kHyamr5oB5xLVP2E5eJ0pbetOfOE2uSxqk0/A8zGazcPhU1zZILrO51hD4vW/hJRgtKJQ==} engines: {node: '>=18'} - '@sphereon/pex@5.0.0-unstable.25': - resolution: {integrity: sha512-EUWfGa6t20PPkYf+zbfWXhc1sSWiFNywbRah8R6grJPU738pfwWpZPunSEY3x0CoxAVaSVXn91wZ/sxmgPCFkA==} - engines: {node: '>=18'} - '@sphereon/ssi-sdk-ext.did-utils@0.24.1-unstable.130': resolution: {integrity: sha512-I+0VjitRjisABWm8RtTPQG57tFwfUS13Wud30OvBoADRxnaA0guUrkS82AYtV6YD0TBHdrd0D6a0RCJwK9SvDg==} @@ -3229,9 +3226,6 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/jsonpath@0.2.4': - resolution: {integrity: sha512-K3hxB8Blw0qgW6ExKgMbXQv2UPZBoE2GqLpVY+yr7nMD2Pq86lsuIzyAaiQ7eMqFL5B6di6pxSkogLJEyEHoGA==} - '@types/keygrip@1.0.6': resolution: {integrity: sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==} @@ -4669,11 +4663,6 @@ packages: resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - esprima@1.2.2: - resolution: {integrity: sha512-+JpPZam9w5DuJ3Q67SqsMGtiHKENSMRVoxvArfJZK01/BfLEObtZ6orJa/MtoGNR/rfMgp5837T41PAmTwAv/A==} - engines: {node: '>=0.4.0'} - hasBin: true - esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} @@ -5866,9 +5855,6 @@ packages: resolution: {integrity: sha512-MwBbq95szLwt8eVQ1Bcfwmgju/Y5P2GdtlHE2ncyfuYjIdEhluUVyj1eudacf1mOkWIoS9GpDBTECqhmq7EOaA==} engines: {node: '>=14'} - jsonpath@1.1.1: - resolution: {integrity: sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w==} - jsonpointer@5.0.1: resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} engines: {node: '>=0.10.0'} @@ -7893,9 +7879,6 @@ packages: unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} - underscore@1.12.1: - resolution: {integrity: sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==} - undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} @@ -8314,32 +8297,48 @@ snapshots: react: 18.3.1 react-native: 0.71.19(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(react@18.3.1) - '@animo-id/mdoc@0.2.38': + '@animo-id/mdoc@0.2.39': dependencies: compare-versions: 6.1.1 - '@animo-id/oauth2-utils@0.1.3(typescript@5.5.4)': + '@animo-id/oauth2-utils@0.1.4(typescript@5.5.4)': dependencies: buffer: 6.0.3 valibot: 0.42.1(typescript@5.5.4) transitivePeerDependencies: - typescript - '@animo-id/oauth2@0.1.3(typescript@5.5.4)': + '@animo-id/oauth2@0.1.4(typescript@5.5.4)': dependencies: - '@animo-id/oauth2-utils': 0.1.3(typescript@5.5.4) + '@animo-id/oauth2-utils': 0.1.4(typescript@5.5.4) valibot: 0.42.1(typescript@5.5.4) transitivePeerDependencies: - typescript - '@animo-id/oid4vci@0.1.3(typescript@5.5.4)': + '@animo-id/oid4vci@0.1.4(typescript@5.5.4)': dependencies: - '@animo-id/oauth2': 0.1.3(typescript@5.5.4) - '@animo-id/oauth2-utils': 0.1.3(typescript@5.5.4) + '@animo-id/oauth2': 0.1.4(typescript@5.5.4) + '@animo-id/oauth2-utils': 0.1.4(typescript@5.5.4) valibot: 0.42.1(typescript@5.5.4) transitivePeerDependencies: - typescript + '@animo-id/pex@4.1.1-alpha.0': + dependencies: + '@astronautlabs/jsonpath': 1.1.2 + '@sd-jwt/decode': 0.7.2 + '@sd-jwt/present': 0.7.2 + '@sd-jwt/types': 0.7.2 + '@sphereon/pex-models': 2.3.1 + '@sphereon/ssi-types': 0.30.2-next.135 + ajv: 8.17.1 + ajv-formats: 2.1.1(ajv@8.17.1) + jwt-decode: 3.1.2 + nanoid: 3.3.7 + uint8arrays: 3.1.1 + transitivePeerDependencies: + - supports-color + '@animo-id/react-native-bbs-signatures@0.1.0(react-native@0.71.19(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(react@18.3.1))(react@18.3.1)': dependencies: react: 18.3.1 @@ -11549,22 +11548,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@sphereon/pex@5.0.0-unstable.25': - dependencies: - '@astronautlabs/jsonpath': 1.1.2 - '@sd-jwt/decode': 0.7.2 - '@sd-jwt/present': 0.7.2 - '@sd-jwt/types': 0.7.2 - '@sphereon/pex-models': 2.3.1 - '@sphereon/ssi-types': 0.30.2-next.135 - ajv: 8.17.1 - ajv-formats: 2.1.1(ajv@8.17.1) - jwt-decode: 3.1.2 - nanoid: 3.3.7 - uint8arrays: 3.1.1 - transitivePeerDependencies: - - supports-color - '@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: '@ethersproject/networks': 5.7.1 @@ -12090,8 +12073,6 @@ snapshots: '@types/json5@0.0.29': {} - '@types/jsonpath@0.2.4': {} - '@types/keygrip@1.0.6': {} '@types/koa-compose@3.2.8': @@ -13747,7 +13728,7 @@ snapshots: debug: 4.3.6 enhanced-resolve: 5.17.1 eslint: 8.57.0 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.6 @@ -13759,7 +13740,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): + eslint-module-utils@2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): dependencies: debug: 3.2.7 optionalDependencies: @@ -13780,7 +13761,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) hasown: 2.0.2 is-core-module: 2.15.0 is-glob: 4.0.3 @@ -13872,8 +13853,6 @@ snapshots: acorn-jsx: 5.3.2(acorn@8.12.1) eslint-visitor-keys: 3.4.3 - esprima@1.2.2: {} - esprima@4.0.1: {} esquery@1.6.0: @@ -15466,12 +15445,6 @@ snapshots: transitivePeerDependencies: - web-streams-polyfill - jsonpath@1.1.1: - dependencies: - esprima: 1.2.2 - static-eval: 2.0.2 - underscore: 1.12.1 - jsonpointer@5.0.1: {} jwt-decode@3.1.2: {} @@ -17698,8 +17671,6 @@ snapshots: has-symbols: 1.0.3 which-boxed-primitive: 1.0.2 - underscore@1.12.1: {} - undici-types@5.26.5: {} undici@6.21.0: {} From 274b421b35218bd31804a7f63dd29596f75fc442 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Sun, 24 Nov 2024 16:52:08 +0100 Subject: [PATCH 13/14] update lockfile Signed-off-by: Timo Glastra --- pnpm-lock.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dfe6f70aaa..8d20f3209c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8339,7 +8339,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@animo-id/react-native-bbs-signatures@0.1.0(react-native@0.71.19(@babel/core@7.25.8)(@babel/preset-env@7.25.8(@babel/core@7.25.8))(react@18.3.1))(react@18.3.1)': + '@animo-id/react-native-bbs-signatures@0.1.0(react-native@0.71.19(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(react@18.3.1))(react@18.3.1)': dependencies: react: 18.3.1 react-native: 0.71.19(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(react@18.3.1) @@ -13728,7 +13728,7 @@ snapshots: debug: 4.3.6 enhanced-resolve: 5.17.1 eslint: 8.57.0 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.6 @@ -13740,7 +13740,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): + eslint-module-utils@2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): dependencies: debug: 3.2.7 optionalDependencies: @@ -13761,7 +13761,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) hasown: 2.0.2 is-core-module: 2.15.0 is-glob: 4.0.3 From 8da42505396d5ac67a366b7e248be9b86a37551f Mon Sep 17 00:00:00 2001 From: Tom Lanser Date: Mon, 25 Nov 2024 14:44:43 +0100 Subject: [PATCH 14/14] feat: Support for subordinate entities and authority hints (#2107) * feat: Support for subordinate entities and authority hints Signed-off-by: Tom Lanser * fix: Increased the openid fed version Signed-off-by: Tom Lanser * feat: tests for multiple layers Signed-off-by: Tom Lanser --------- Signed-off-by: Tom Lanser --- packages/openid4vc/package.json | 2 +- .../OpenId4VcVerifierModule.ts | 2 +- .../OpenId4VcVerifierModuleConfig.ts | 30 +- .../router/federationEndpoint.ts | 113 ++- .../tests/openid4vc-federation.e2e.test.ts | 778 ++++++++++++++++++ .../openid4vc/tests/openid4vc.e2e.test.ts | 95 +-- pnpm-lock.yaml | 16 +- 7 files changed, 929 insertions(+), 107 deletions(-) create mode 100644 packages/openid4vc/tests/openid4vc-federation.e2e.test.ts diff --git a/packages/openid4vc/package.json b/packages/openid4vc/package.json index 1d85d1e105..239b1b2076 100644 --- a/packages/openid4vc/package.json +++ b/packages/openid4vc/package.json @@ -30,7 +30,7 @@ "@sphereon/did-auth-siop": "0.16.1-fix.173", "@sphereon/oid4vc-common": "0.16.1-fix.173", "@sphereon/ssi-types": "0.30.2-next.135", - "@openid-federation/core": "0.1.1-alpha.13", + "@openid-federation/core": "0.1.1-alpha.15", "class-transformer": "^0.5.1", "rxjs": "^7.8.0", "zod": "^3.23.8", diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts index 0a0a0aaf77..07e7efd29e 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts @@ -126,7 +126,7 @@ export class OpenId4VcVerifierModule implements Module { // TODO: The keys needs to be passed down to the federation endpoint to be used in the entity configuration for the openid relying party // TODO: But the keys also needs to be available for the request signing. They also needs to get saved because it needs to survive a restart of the agent. - configureFederationEndpoint(endpointRouter) + configureFederationEndpoint(endpointRouter, this.config.federation) // First one will be called for all requests (when next is called) contextRouter.use(async (req: OpenId4VcVerificationRequest, _res: unknown, next) => { diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModuleConfig.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModuleConfig.ts index b2ec763cbc..ea5fb27f6f 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModuleConfig.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModuleConfig.ts @@ -1,6 +1,6 @@ import type { OpenId4VcSiopAuthorizationEndpointConfig } from './router/authorizationEndpoint' import type { OpenId4VcSiopAuthorizationRequestEndpointConfig } from './router/authorizationRequestEndpoint' -import type { Optional } from '@credo-ts/core' +import type { AgentContext, Optional } from '@credo-ts/core' import type { Router } from 'express' import { importExpress } from '../shared/router' @@ -25,6 +25,30 @@ export interface OpenId4VcVerifierModuleConfigOptions { authorization?: Optional authorizationRequest?: Optional } + + /** + * Configuration for the federation endpoint. + */ + federation?: { + // TODO: Make this functions also compatible with the issuer side + isSubordinateEntity?: ( + agentContext: AgentContext, + options: { + verifierId: string + + issuerEntityId: string + subjectEntityId: string + } + ) => Promise + getAuthorityHints?: ( + agentContext: AgentContext, + options: { + verifierId: string + + issuerEntityId: string + } + ) => Promise + } } export class OpenId4VcVerifierModuleConfig { @@ -60,4 +84,8 @@ export class OpenId4VcVerifierModuleConfig { endpointPath: userOptions?.endpointPath ?? '/authorize', } } + + public get federation() { + return this.options.federation + } } diff --git a/packages/openid4vc/src/openid4vc-verifier/router/federationEndpoint.ts b/packages/openid4vc/src/openid4vc-verifier/router/federationEndpoint.ts index bcf9c92416..77b7864f85 100644 --- a/packages/openid4vc/src/openid4vc-verifier/router/federationEndpoint.ts +++ b/packages/openid4vc/src/openid4vc-verifier/router/federationEndpoint.ts @@ -3,8 +3,8 @@ import type { Key, Buffer } from '@credo-ts/core' import type { RPRegistrationMetadataPayload } from '@sphereon/did-auth-siop' import type { Router, Response } from 'express' -import { getJwkFromKey, KeyType } from '@credo-ts/core' -import { createEntityConfiguration } from '@openid-federation/core' +import { getJwkFromJson, getJwkFromKey, JwsService, KeyType } from '@credo-ts/core' +import { createEntityConfiguration, createEntityStatement, fetchEntityConfiguration } from '@openid-federation/core' import { LanguageTagUtils, removeNullUndefined } from '@sphereon/did-auth-siop' import { getRequestContext, sendErrorResponse } from '../../shared/router' @@ -47,7 +47,10 @@ const createRPRegistrationMetadataPayload = (opts: any): RPRegistrationMetadataP return removeNullUndefined(rpRegistrationMetadataPayload) } -export function configureFederationEndpoint(router: Router) { +export function configureFederationEndpoint( + router: Router, + federationConfig: OpenId4VcVerifierModuleConfig['federation'] = {} +) { // TODO: this whole result needs to be cached and the ttl should be the expires of this node // TODO: This will not work for multiple instances so we have to save it in the database. @@ -97,6 +100,11 @@ export function configureFederationEndpoint(router: Router) { const alg = jwk.supportedSignatureAlgorithms[0] const kid = federationKey.fingerprint + const authorityHints = await federationConfig.getAuthorityHints?.(agentContext, { + verifierId: verifier.verifierId, + issuerEntityId: verifierEntityId, + }) + const entityConfiguration = await createEntityConfiguration({ header: { kid, @@ -111,10 +119,12 @@ export function configureFederationEndpoint(router: Router) { jwks: { keys: [{ kid, alg, ...jwk.toJson() }], }, + authority_hints: authorityHints, metadata: { federation_entity: { organization_name: rpMetadata.client_name, logo_uri: rpMetadata.logo_uri, + federation_fetch_endpoint: `${verifierEntityId}/openid-federation/fetch`, }, openid_relying_party: { ...rpMetadata, @@ -145,4 +155,101 @@ export function configureFederationEndpoint(router: Router) { next() } ) + + // TODO: Currently it will fetch everything in realtime and creates a entity statement without even checking if it is allowed. + router.get('/openid-federation/fetch', async (request: OpenId4VcVerificationRequest, response: Response, next) => { + const { agentContext, verifier } = getRequestContext(request) + + const { sub } = request.query + if (!sub || typeof sub !== 'string') { + sendErrorResponse(response, next, agentContext.config.logger, 400, 'invalid_request', 'sub is required') + return + } + + const verifierConfig = agentContext.dependencyManager.resolve(OpenId4VcVerifierModuleConfig) + + const entityId = `${verifierConfig.baseUrl}/${verifier.verifierId}` + + const isSubordinateEntity = await federationConfig.isSubordinateEntity?.(agentContext, { + verifierId: verifier.verifierId, + issuerEntityId: entityId, + subjectEntityId: sub, + }) + if (!isSubordinateEntity) { + if (!federationConfig.isSubordinateEntity) { + agentContext.config.logger.warn( + 'isSubordinateEntity hook is not provided for the federation so we cannot check if this entity is a subordinate entity of the issuer', + { + verifierId: verifier.verifierId, + issuerEntityId: entityId, + subjectEntityId: sub, + } + ) + } + + sendErrorResponse( + response, + next, + agentContext.config.logger, + 403, + 'forbidden', + 'This entity is not a subordinate entity of the issuer' + ) + return + } + + const jwsService = agentContext.dependencyManager.resolve(JwsService) + + const subjectEntityConfiguration = await fetchEntityConfiguration({ + entityId: sub, + verifyJwtCallback: async ({ jwt, jwk }) => { + const res = await jwsService.verifyJws(agentContext, { + jws: jwt, + jwkResolver: () => getJwkFromJson(jwk), + }) + + return res.isValid + }, + }) + + let federationKey = federationKeyMapping.get(verifier.verifierId) + if (!federationKey) { + federationKey = await agentContext.wallet.createKey({ + keyType: KeyType.Ed25519, + }) + federationKeyMapping.set(verifier.verifierId, federationKey) + } + + const jwk = getJwkFromKey(federationKey) + const alg = jwk.supportedSignatureAlgorithms[0] + const kid = federationKey.fingerprint + + const entityStatement = await createEntityStatement({ + header: { + kid, + alg, + typ: 'entity-statement+jwt', + }, + jwk: { + ...jwk.toJson(), + kid, + }, + claims: { + sub: sub, + iss: entityId, + iat: new Date(), + exp: new Date(Date.now() + 1000 * 60 * 60 * 24), // 1 day TODO: Might needs to be a bit lower because a day is quite long for trust + jwks: { + keys: subjectEntityConfiguration.jwks.keys, + }, + }, + signJwtCallback: ({ toBeSigned }) => + agentContext.wallet.sign({ + data: toBeSigned as Buffer, + key: federationKey, + }), + }) + + response.writeHead(200, { 'Content-Type': 'application/entity-statement+jwt' }).end(entityStatement) + }) } diff --git a/packages/openid4vc/tests/openid4vc-federation.e2e.test.ts b/packages/openid4vc/tests/openid4vc-federation.e2e.test.ts new file mode 100644 index 0000000000..6e4d65c07d --- /dev/null +++ b/packages/openid4vc/tests/openid4vc-federation.e2e.test.ts @@ -0,0 +1,778 @@ +import type { AgentType, TenantType } from './utils' +import type { OpenId4VcVerifierModuleConfig } from '../src' +import type { Server } from 'http' + +import { + ClaimFormat, + DidsApi, + JwaSignatureAlgorithm, + W3cCredential, + W3cCredentialSubject, + w3cDate, + W3cIssuer, + DifPresentationExchangeService, +} from '@credo-ts/core' +import express, { type Express } from 'express' + +import { AskarModule } from '../../askar/src' +import { askarModuleConfig } from '../../askar/tests/helpers' +import { TenantsModule } from '../../tenants/src' +import { + OpenId4VcHolderModule, + OpenId4VcIssuerModule, + OpenId4VcVerificationSessionState, + OpenId4VcVerifierModule, +} from '../src' + +import { createAgentFromModules, createTenantForAgent, waitForVerificationSessionRecordSubject } from './utils' +import { openBadgePresentationDefinition, universityDegreePresentationDefinition } from './utilsVp' + +const serverPort = 1234 +const baseUrl = `http://localhost:${serverPort}` +const issuanceBaseUrl = `${baseUrl}/oid4vci` +const verificationBaseUrl = `${baseUrl}/oid4vp` + +describe('OpenId4Vc-federation', () => { + let expressApp: Express + let expressServer: Server + + let issuer: AgentType<{ + openId4VcIssuer: OpenId4VcIssuerModule + tenants: TenantsModule<{ openId4VcIssuer: OpenId4VcIssuerModule }> + }> + // let issuer1: TenantType + // let issuer2: TenantType + + let holder: AgentType<{ + openId4VcHolder: OpenId4VcHolderModule + tenants: TenantsModule<{ openId4VcHolder: OpenId4VcHolderModule }> + }> + let holder1: TenantType + + let verifier: AgentType<{ + openId4VcVerifier: OpenId4VcVerifierModule + tenants: TenantsModule<{ openId4VcVerifier: OpenId4VcVerifierModule }> + }> + let verifier1: TenantType + let verifier2: TenantType + + let federationConfig: OpenId4VcVerifierModuleConfig['federation'] | undefined + + beforeEach(async () => { + expressApp = express() + + issuer = (await createAgentFromModules( + 'issuer', + { + openId4VcIssuer: new OpenId4VcIssuerModule({ + baseUrl: issuanceBaseUrl, + credentialRequestToCredentialMapper: async ({ + agentContext, + credentialRequest, + holderBindings, + credentialConfigurationIds, + }) => { + // We sign the request with the first did:key did we have + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const [firstDidKeyDid] = await didsApi.getCreatedDids({ method: 'key' }) + const didDocument = await didsApi.resolveDidDocument(firstDidKeyDid.did) + const verificationMethod = didDocument.verificationMethod?.[0] + if (!verificationMethod) { + throw new Error('No verification method found') + } + const credentialConfigurationId = credentialConfigurationIds[0] + + if (credentialRequest.format === 'vc+sd-jwt') { + return { + credentialConfigurationId, + format: credentialRequest.format, + credentials: holderBindings.map((holderBinding) => ({ + payload: { vct: credentialRequest.vct, university: 'innsbruck', degree: 'bachelor' }, + holder: holderBinding, + issuer: { + method: 'did', + didUrl: verificationMethod.id, + }, + disclosureFrame: { _sd: ['university', 'degree'] }, + })), + } + } else { + throw new Error('Invalid request') + } + }, + }), + askar: new AskarModule(askarModuleConfig), + tenants: new TenantsModule(), + }, + '96213c3d7fc8d4d6754c7a0fd969598g' + )) as unknown as typeof issuer + // issuer1 = await createTenantForAgent(issuer.agent, 'iTenant1') + // issuer2 = await createTenantForAgent(issuer.agent, 'iTenant2') + + holder = (await createAgentFromModules( + 'holder', + { + openId4VcHolder: new OpenId4VcHolderModule(), + askar: new AskarModule(askarModuleConfig), + tenants: new TenantsModule(), + }, + '96213c3d7fc8d4d6754c7a0fd969598e' + )) as unknown as typeof holder + holder1 = await createTenantForAgent(holder.agent, 'hTenant1') + + verifier = (await createAgentFromModules( + 'verifier', + { + openId4VcVerifier: new OpenId4VcVerifierModule({ + baseUrl: verificationBaseUrl, + federation: { + async isSubordinateEntity(agentContext, options) { + if (federationConfig && federationConfig.isSubordinateEntity) { + return federationConfig.isSubordinateEntity(agentContext, options) + } + return false + }, + async getAuthorityHints(agentContext, options) { + if (federationConfig && federationConfig.getAuthorityHints) { + return federationConfig.getAuthorityHints(agentContext, options) + } + return undefined + }, + }, + }), + askar: new AskarModule(askarModuleConfig), + tenants: new TenantsModule(), + }, + '96213c3d7fc8d4d6754c7a0fd969598f' + )) as unknown as typeof verifier + verifier1 = await createTenantForAgent(verifier.agent, 'vTenant1') + verifier2 = await createTenantForAgent(verifier.agent, 'vTenant2') + + // We let AFJ create the router, so we have a fresh one each time + expressApp.use('/oid4vci', issuer.agent.modules.openId4VcIssuer.config.router) + expressApp.use('/oid4vp', verifier.agent.modules.openId4VcVerifier.config.router) + + expressServer = expressApp.listen(serverPort) + }) + + afterEach(async () => { + expressServer?.close() + + await issuer.agent.shutdown() + await issuer.agent.wallet.delete() + + await holder.agent.shutdown() + await holder.agent.wallet.delete() + + await verifier.agent.shutdown() + await verifier.agent.wallet.delete() + + federationConfig = undefined + }) + + it('e2e flow with tenants and federation, verifier endpoints verifying a jwt-vc', async () => { + const holderTenant = await holder.agent.modules.tenants.getTenantAgent({ tenantId: holder1.tenantId }) + const verifierTenant1 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier1.tenantId }) + const verifierTenant2 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier2.tenantId }) + + const openIdVerifierTenant1 = await verifierTenant1.modules.openId4VcVerifier.createVerifier() + const openIdVerifierTenant2 = await verifierTenant2.modules.openId4VcVerifier.createVerifier() + + const signedCredential1 = await issuer.agent.w3cCredentials.signCredential({ + format: ClaimFormat.JwtVc, + credential: new W3cCredential({ + type: ['VerifiableCredential', 'OpenBadgeCredential'], + issuer: new W3cIssuer({ id: issuer.did }), + credentialSubject: new W3cCredentialSubject({ id: holder1.did }), + issuanceDate: w3cDate(Date.now()), + }), + alg: JwaSignatureAlgorithm.EdDSA, + verificationMethod: issuer.verificationMethod.id, + }) + + const signedCredential2 = await issuer.agent.w3cCredentials.signCredential({ + format: ClaimFormat.JwtVc, + credential: new W3cCredential({ + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + issuer: new W3cIssuer({ id: issuer.did }), + credentialSubject: new W3cCredentialSubject({ id: holder1.did }), + issuanceDate: w3cDate(Date.now()), + }), + alg: JwaSignatureAlgorithm.EdDSA, + verificationMethod: issuer.verificationMethod.id, + }) + + await holderTenant.w3cCredentials.storeCredential({ credential: signedCredential1 }) + await holderTenant.w3cCredentials.storeCredential({ credential: signedCredential2 }) + + const { authorizationRequest: authorizationRequestUri1, verificationSession: verificationSession1 } = + await verifierTenant1.modules.openId4VcVerifier.createAuthorizationRequest({ + verifierId: openIdVerifierTenant1.verifierId, + requestSigner: { + method: 'openid-federation', + }, + presentationExchange: { + definition: openBadgePresentationDefinition, + }, + }) + + expect(authorizationRequestUri1).toEqual( + `openid4vp://?client_id=${encodeURIComponent( + `http://localhost:1234/oid4vp/${openIdVerifierTenant1.verifierId}` + )}&request_uri=${encodeURIComponent(verificationSession1.authorizationRequestUri)}` + ) + + const { authorizationRequest: authorizationRequestUri2, verificationSession: verificationSession2 } = + await verifierTenant2.modules.openId4VcVerifier.createAuthorizationRequest({ + requestSigner: { + method: 'openid-federation', + }, + presentationExchange: { + definition: universityDegreePresentationDefinition, + }, + verifierId: openIdVerifierTenant2.verifierId, + }) + + expect(authorizationRequestUri2).toEqual( + `openid4vp://?client_id=${encodeURIComponent( + `http://localhost:1234/oid4vp/${openIdVerifierTenant2.verifierId}` + )}&request_uri=${encodeURIComponent(verificationSession2.authorizationRequestUri)}` + ) + + await verifierTenant1.endSession() + await verifierTenant2.endSession() + + const resolvedProofRequest1 = await holderTenant.modules.openId4VcHolder.resolveSiopAuthorizationRequest( + authorizationRequestUri1, + { + federation: { + trustedEntityIds: [`http://localhost:1234/oid4vp/${openIdVerifierTenant1.verifierId}`], + }, + } + ) + + expect(resolvedProofRequest1.presentationExchange?.credentialsForRequest).toMatchObject({ + areRequirementsSatisfied: true, + requirements: [ + { + submissionEntry: [ + { + verifiableCredentials: [ + { + type: ClaimFormat.JwtVc, + credentialRecord: { + credential: { + type: ['VerifiableCredential', 'OpenBadgeCredential'], + }, + }, + }, + ], + }, + ], + }, + ], + }) + + const resolvedProofRequest2 = await holderTenant.modules.openId4VcHolder.resolveSiopAuthorizationRequest( + authorizationRequestUri2, + { + federation: { + trustedEntityIds: [`http://localhost:1234/oid4vp/${openIdVerifierTenant2.verifierId}`], + }, + } + ) + + expect(resolvedProofRequest2.presentationExchange?.credentialsForRequest).toMatchObject({ + areRequirementsSatisfied: true, + requirements: [ + { + submissionEntry: [ + { + verifiableCredentials: [ + { + type: ClaimFormat.JwtVc, + credentialRecord: { + credential: { + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + }, + }, + }, + ], + }, + ], + }, + ], + }) + + if (!resolvedProofRequest1.presentationExchange || !resolvedProofRequest2.presentationExchange) { + throw new Error('Presentation exchange not defined') + } + + const presentationExchangeService = holderTenant.dependencyManager.resolve(DifPresentationExchangeService) + const selectedCredentials = presentationExchangeService.selectCredentialsForRequest( + resolvedProofRequest1.presentationExchange.credentialsForRequest + ) + + const { submittedResponse: submittedResponse1, serverResponse: serverResponse1 } = + await holderTenant.modules.openId4VcHolder.acceptSiopAuthorizationRequest({ + authorizationRequest: resolvedProofRequest1.authorizationRequest, + presentationExchange: { + credentials: selectedCredentials, + }, + }) + + expect(submittedResponse1).toEqual({ + presentation_submission: { + definition_id: 'OpenBadgeCredential', + descriptor_map: [ + { + format: 'jwt_vp', + id: 'OpenBadgeCredentialDescriptor', + path: '$', + path_nested: { + format: 'jwt_vc', + id: 'OpenBadgeCredentialDescriptor', + path: '$.vp.verifiableCredential[0]', + }, + }, + ], + id: expect.any(String), + }, + state: expect.any(String), + vp_token: expect.any(String), + }) + expect(serverResponse1).toMatchObject({ + status: 200, + }) + + // The RP MUST validate that the aud (audience) Claim contains the value of the client_id + // that the RP sent in the Authorization Request as an audience. + // When the request has been signed, the value might be an HTTPS URL, or a Decentralized Identifier. + const verifierTenant1_2 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier1.tenantId }) + await waitForVerificationSessionRecordSubject(verifier.replaySubject, { + contextCorrelationId: verifierTenant1_2.context.contextCorrelationId, + state: OpenId4VcVerificationSessionState.ResponseVerified, + verificationSessionId: verificationSession1.id, + }) + + const { idToken: idToken1, presentationExchange: presentationExchange1 } = + await verifierTenant1_2.modules.openId4VcVerifier.getVerifiedAuthorizationResponse(verificationSession1.id) + + expect(idToken1).toBeUndefined() + expect(presentationExchange1).toMatchObject({ + definition: openBadgePresentationDefinition, + submission: { + definition_id: 'OpenBadgeCredential', + }, + presentations: [ + { + verifiableCredential: [ + { + type: ['VerifiableCredential', 'OpenBadgeCredential'], + }, + ], + }, + ], + }) + + const selectedCredentials2 = presentationExchangeService.selectCredentialsForRequest( + resolvedProofRequest2.presentationExchange.credentialsForRequest + ) + + const { serverResponse: serverResponse2 } = + await holderTenant.modules.openId4VcHolder.acceptSiopAuthorizationRequest({ + authorizationRequest: resolvedProofRequest2.authorizationRequest, + presentationExchange: { + credentials: selectedCredentials2, + }, + }) + expect(serverResponse2).toMatchObject({ + status: 200, + }) + + // The RP MUST validate that the aud (audience) Claim contains the value of the client_id + // that the RP sent in the Authorization Request as an audience. + // When the request has been signed, the value might be an HTTPS URL, or a Decentralized Identifier. + const verifierTenant2_2 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier2.tenantId }) + await waitForVerificationSessionRecordSubject(verifier.replaySubject, { + contextCorrelationId: verifierTenant2_2.context.contextCorrelationId, + state: OpenId4VcVerificationSessionState.ResponseVerified, + verificationSessionId: verificationSession2.id, + }) + const { idToken: idToken2, presentationExchange: presentationExchange2 } = + await verifierTenant2_2.modules.openId4VcVerifier.getVerifiedAuthorizationResponse(verificationSession2.id) + expect(idToken2).toBeUndefined() + + expect(presentationExchange2).toMatchObject({ + definition: universityDegreePresentationDefinition, + submission: { + definition_id: 'UniversityDegreeCredential', + }, + presentations: [ + { + verifiableCredential: [ + { + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + }, + ], + }, + ], + }) + }) + + it('e2e flow with tenants and federation with multiple layers, verifier endpoints verifying a jwt-vc', async () => { + const holderTenant = await holder.agent.modules.tenants.getTenantAgent({ tenantId: holder1.tenantId }) + const verifierTenant1 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier1.tenantId }) + const verifierTenant2 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier2.tenantId }) + + const openIdVerifierTenant1 = await verifierTenant1.modules.openId4VcVerifier.createVerifier() + const openIdVerifierTenant2 = await verifierTenant2.modules.openId4VcVerifier.createVerifier() + + const signedCredential1 = await issuer.agent.w3cCredentials.signCredential({ + format: ClaimFormat.JwtVc, + credential: new W3cCredential({ + type: ['VerifiableCredential', 'OpenBadgeCredential'], + issuer: new W3cIssuer({ id: issuer.did }), + credentialSubject: new W3cCredentialSubject({ id: holder1.did }), + issuanceDate: w3cDate(Date.now()), + }), + alg: JwaSignatureAlgorithm.EdDSA, + verificationMethod: issuer.verificationMethod.id, + }) + + const signedCredential2 = await issuer.agent.w3cCredentials.signCredential({ + format: ClaimFormat.JwtVc, + credential: new W3cCredential({ + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + issuer: new W3cIssuer({ id: issuer.did }), + credentialSubject: new W3cCredentialSubject({ id: holder1.did }), + issuanceDate: w3cDate(Date.now()), + }), + alg: JwaSignatureAlgorithm.EdDSA, + verificationMethod: issuer.verificationMethod.id, + }) + + await holderTenant.w3cCredentials.storeCredential({ credential: signedCredential1 }) + await holderTenant.w3cCredentials.storeCredential({ credential: signedCredential2 }) + + const { authorizationRequest: authorizationRequestUri1, verificationSession: verificationSession1 } = + await verifierTenant1.modules.openId4VcVerifier.createAuthorizationRequest({ + verifierId: openIdVerifierTenant1.verifierId, + requestSigner: { + method: 'openid-federation', + }, + presentationExchange: { + definition: openBadgePresentationDefinition, + }, + }) + + expect(authorizationRequestUri1).toEqual( + `openid4vp://?client_id=${encodeURIComponent( + `http://localhost:1234/oid4vp/${openIdVerifierTenant1.verifierId}` + )}&request_uri=${encodeURIComponent(verificationSession1.authorizationRequestUri)}` + ) + + const { authorizationRequest: authorizationRequestUri2, verificationSession: verificationSession2 } = + await verifierTenant2.modules.openId4VcVerifier.createAuthorizationRequest({ + requestSigner: { + method: 'openid-federation', + }, + presentationExchange: { + definition: universityDegreePresentationDefinition, + }, + verifierId: openIdVerifierTenant2.verifierId, + }) + + expect(authorizationRequestUri2).toEqual( + `openid4vp://?client_id=${encodeURIComponent( + `http://localhost:1234/oid4vp/${openIdVerifierTenant2.verifierId}` + )}&request_uri=${encodeURIComponent(verificationSession2.authorizationRequestUri)}` + ) + + await verifierTenant1.endSession() + await verifierTenant2.endSession() + + federationConfig = { + isSubordinateEntity: async (agentContext, options) => { + // When the verifier 2 gets asked if verifier 1 is a subordinate entity, it should return true + return options.verifierId === openIdVerifierTenant2.verifierId + }, + getAuthorityHints: async (agentContext, options) => { + // The verifier 1 says that the verifier 2 is above him + return options.verifierId === openIdVerifierTenant1.verifierId + ? [`http://localhost:1234/oid4vp/${openIdVerifierTenant2.verifierId}`] + : undefined + }, + } + + // Gets a request from verifier 1 but we trust verifier 2 so if the verifier 1 is in the subordinate entity list of verifier 2 it should succeed + const resolvedProofRequest1 = await holderTenant.modules.openId4VcHolder.resolveSiopAuthorizationRequest( + authorizationRequestUri1, + { + federation: { + trustedEntityIds: [`http://localhost:1234/oid4vp/${openIdVerifierTenant2.verifierId}`], + }, + } + ) + + expect(resolvedProofRequest1.presentationExchange?.credentialsForRequest).toMatchObject({ + areRequirementsSatisfied: true, + requirements: [ + { + submissionEntry: [ + { + verifiableCredentials: [ + { + type: ClaimFormat.JwtVc, + credentialRecord: { + credential: { + type: ['VerifiableCredential', 'OpenBadgeCredential'], + }, + }, + }, + ], + }, + ], + }, + ], + }) + + const resolvedProofRequest2 = await holderTenant.modules.openId4VcHolder.resolveSiopAuthorizationRequest( + authorizationRequestUri2, + { + federation: { + trustedEntityIds: [`http://localhost:1234/oid4vp/${openIdVerifierTenant2.verifierId}`], + }, + } + ) + + expect(resolvedProofRequest2.presentationExchange?.credentialsForRequest).toMatchObject({ + areRequirementsSatisfied: true, + requirements: [ + { + submissionEntry: [ + { + verifiableCredentials: [ + { + type: ClaimFormat.JwtVc, + credentialRecord: { + credential: { + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + }, + }, + }, + ], + }, + ], + }, + ], + }) + + if (!resolvedProofRequest1.presentationExchange || !resolvedProofRequest2.presentationExchange) { + throw new Error('Presentation exchange not defined') + } + + const presentationExchangeService = holderTenant.dependencyManager.resolve(DifPresentationExchangeService) + const selectedCredentials = presentationExchangeService.selectCredentialsForRequest( + resolvedProofRequest1.presentationExchange.credentialsForRequest + ) + + const { submittedResponse: submittedResponse1, serverResponse: serverResponse1 } = + await holderTenant.modules.openId4VcHolder.acceptSiopAuthorizationRequest({ + authorizationRequest: resolvedProofRequest1.authorizationRequest, + presentationExchange: { + credentials: selectedCredentials, + }, + }) + + expect(submittedResponse1).toEqual({ + presentation_submission: { + definition_id: 'OpenBadgeCredential', + descriptor_map: [ + { + format: 'jwt_vp', + id: 'OpenBadgeCredentialDescriptor', + path: '$', + path_nested: { + format: 'jwt_vc', + id: 'OpenBadgeCredentialDescriptor', + path: '$.vp.verifiableCredential[0]', + }, + }, + ], + id: expect.any(String), + }, + state: expect.any(String), + vp_token: expect.any(String), + }) + expect(serverResponse1).toMatchObject({ + status: 200, + }) + + // The RP MUST validate that the aud (audience) Claim contains the value of the client_id + // that the RP sent in the Authorization Request as an audience. + // When the request has been signed, the value might be an HTTPS URL, or a Decentralized Identifier. + const verifierTenant1_2 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier1.tenantId }) + await waitForVerificationSessionRecordSubject(verifier.replaySubject, { + contextCorrelationId: verifierTenant1_2.context.contextCorrelationId, + state: OpenId4VcVerificationSessionState.ResponseVerified, + verificationSessionId: verificationSession1.id, + }) + + const { idToken: idToken1, presentationExchange: presentationExchange1 } = + await verifierTenant1_2.modules.openId4VcVerifier.getVerifiedAuthorizationResponse(verificationSession1.id) + + expect(idToken1).toBeUndefined() + expect(presentationExchange1).toMatchObject({ + definition: openBadgePresentationDefinition, + submission: { + definition_id: 'OpenBadgeCredential', + }, + presentations: [ + { + verifiableCredential: [ + { + type: ['VerifiableCredential', 'OpenBadgeCredential'], + }, + ], + }, + ], + }) + + const selectedCredentials2 = presentationExchangeService.selectCredentialsForRequest( + resolvedProofRequest2.presentationExchange.credentialsForRequest + ) + + const { serverResponse: serverResponse2 } = + await holderTenant.modules.openId4VcHolder.acceptSiopAuthorizationRequest({ + authorizationRequest: resolvedProofRequest2.authorizationRequest, + presentationExchange: { + credentials: selectedCredentials2, + }, + }) + expect(serverResponse2).toMatchObject({ + status: 200, + }) + + // The RP MUST validate that the aud (audience) Claim contains the value of the client_id + // that the RP sent in the Authorization Request as an audience. + // When the request has been signed, the value might be an HTTPS URL, or a Decentralized Identifier. + const verifierTenant2_2 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier2.tenantId }) + await waitForVerificationSessionRecordSubject(verifier.replaySubject, { + contextCorrelationId: verifierTenant2_2.context.contextCorrelationId, + state: OpenId4VcVerificationSessionState.ResponseVerified, + verificationSessionId: verificationSession2.id, + }) + const { idToken: idToken2, presentationExchange: presentationExchange2 } = + await verifierTenant2_2.modules.openId4VcVerifier.getVerifiedAuthorizationResponse(verificationSession2.id) + expect(idToken2).toBeUndefined() + + expect(presentationExchange2).toMatchObject({ + definition: universityDegreePresentationDefinition, + submission: { + definition_id: 'UniversityDegreeCredential', + }, + presentations: [ + { + verifiableCredential: [ + { + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + }, + ], + }, + ], + }) + }) + + it('e2e flow with tenants and federation, unhappy flow', async () => { + const holderTenant = await holder.agent.modules.tenants.getTenantAgent({ tenantId: holder1.tenantId }) + const verifierTenant1 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier1.tenantId }) + const verifierTenant2 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier2.tenantId }) + + const openIdVerifierTenant1 = await verifierTenant1.modules.openId4VcVerifier.createVerifier() + const openIdVerifierTenant2 = await verifierTenant2.modules.openId4VcVerifier.createVerifier() + + const signedCredential1 = await issuer.agent.w3cCredentials.signCredential({ + format: ClaimFormat.JwtVc, + credential: new W3cCredential({ + type: ['VerifiableCredential', 'OpenBadgeCredential'], + issuer: new W3cIssuer({ id: issuer.did }), + credentialSubject: new W3cCredentialSubject({ id: holder1.did }), + issuanceDate: w3cDate(Date.now()), + }), + alg: JwaSignatureAlgorithm.EdDSA, + verificationMethod: issuer.verificationMethod.id, + }) + + const signedCredential2 = await issuer.agent.w3cCredentials.signCredential({ + format: ClaimFormat.JwtVc, + credential: new W3cCredential({ + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + issuer: new W3cIssuer({ id: issuer.did }), + credentialSubject: new W3cCredentialSubject({ id: holder1.did }), + issuanceDate: w3cDate(Date.now()), + }), + alg: JwaSignatureAlgorithm.EdDSA, + verificationMethod: issuer.verificationMethod.id, + }) + + await holderTenant.w3cCredentials.storeCredential({ credential: signedCredential1 }) + await holderTenant.w3cCredentials.storeCredential({ credential: signedCredential2 }) + + const { authorizationRequest: authorizationRequestUri1, verificationSession: verificationSession1 } = + await verifierTenant1.modules.openId4VcVerifier.createAuthorizationRequest({ + verifierId: openIdVerifierTenant1.verifierId, + requestSigner: { + method: 'openid-federation', + }, + presentationExchange: { + definition: openBadgePresentationDefinition, + }, + }) + + expect(authorizationRequestUri1).toEqual( + `openid4vp://?client_id=${encodeURIComponent( + `http://localhost:1234/oid4vp/${openIdVerifierTenant1.verifierId}` + )}&request_uri=${encodeURIComponent(verificationSession1.authorizationRequestUri)}` + ) + + const { authorizationRequest: authorizationRequestUri2, verificationSession: verificationSession2 } = + await verifierTenant2.modules.openId4VcVerifier.createAuthorizationRequest({ + requestSigner: { + method: 'openid-federation', + }, + presentationExchange: { + definition: universityDegreePresentationDefinition, + }, + verifierId: openIdVerifierTenant2.verifierId, + }) + + expect(authorizationRequestUri2).toEqual( + `openid4vp://?client_id=${encodeURIComponent( + `http://localhost:1234/oid4vp/${openIdVerifierTenant2.verifierId}` + )}&request_uri=${encodeURIComponent(verificationSession2.authorizationRequestUri)}` + ) + + await verifierTenant1.endSession() + await verifierTenant2.endSession() + + const resolvedProofRequestWithFederationPromise = + holderTenant.modules.openId4VcHolder.resolveSiopAuthorizationRequest(authorizationRequestUri1, { + federation: { + // This will look for a whole different trusted entity + trustedEntityIds: [`http://localhost:1234/oid4vp/${openIdVerifierTenant2.verifierId}`], + }, + }) + + // TODO: Look into this error see if we can make it more specific + await expect(resolvedProofRequestWithFederationPromise).rejects.toThrow( + `Error verifying the DID Auth Token signature.` + ) + + const resolvedProofRequestWithoutFederationPromise = + holderTenant.modules.openId4VcHolder.resolveSiopAuthorizationRequest(authorizationRequestUri2) + await expect(resolvedProofRequestWithoutFederationPromise).rejects.toThrow( + `Error verifying the DID Auth Token signature.` + ) + }) +}) diff --git a/packages/openid4vc/tests/openid4vc.e2e.test.ts b/packages/openid4vc/tests/openid4vc.e2e.test.ts index d35092463b..c038286e47 100644 --- a/packages/openid4vc/tests/openid4vc.e2e.test.ts +++ b/packages/openid4vc/tests/openid4vc.e2e.test.ts @@ -1,5 +1,5 @@ import type { AgentType, TenantType } from './utils' -import type { OpenId4VciSignMdocCredentials } from '../src' +import type { OpenId4VciSignMdocCredentials, OpenId4VcVerifierModuleConfig } from '../src' import type { OpenId4VciCredentialBindingResolver } from '../src/openid4vc-holder' import type { AuthorizationServerMetadata } from '@animo-id/oauth2' import type { DifPresentationExchangeDefinitionV2, JwkJson, Mdoc, MdocDeviceResponse, SdJwtVc } from '@credo-ts/core' @@ -183,6 +183,7 @@ describe('OpenId4Vc', () => { { openId4VcVerifier: new OpenId4VcVerifierModule({ baseUrl: verificationBaseUrl, + federation: {}, }), askar: new AskarModule(askarModuleConfig), tenants: new TenantsModule(), @@ -2786,96 +2787,4 @@ describe('OpenId4Vc', () => { ], }) }) - - it('e2e flow with tenants and federation, unhappy flow', async () => { - const holderTenant = await holder.agent.modules.tenants.getTenantAgent({ tenantId: holder1.tenantId }) - const verifierTenant1 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier1.tenantId }) - const verifierTenant2 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier2.tenantId }) - - const openIdVerifierTenant1 = await verifierTenant1.modules.openId4VcVerifier.createVerifier() - const openIdVerifierTenant2 = await verifierTenant2.modules.openId4VcVerifier.createVerifier() - - const signedCredential1 = await issuer.agent.w3cCredentials.signCredential({ - format: ClaimFormat.JwtVc, - credential: new W3cCredential({ - type: ['VerifiableCredential', 'OpenBadgeCredential'], - issuer: new W3cIssuer({ id: issuer.did }), - credentialSubject: new W3cCredentialSubject({ id: holder1.did }), - issuanceDate: w3cDate(Date.now()), - }), - alg: JwaSignatureAlgorithm.EdDSA, - verificationMethod: issuer.verificationMethod.id, - }) - - const signedCredential2 = await issuer.agent.w3cCredentials.signCredential({ - format: ClaimFormat.JwtVc, - credential: new W3cCredential({ - type: ['VerifiableCredential', 'UniversityDegreeCredential'], - issuer: new W3cIssuer({ id: issuer.did }), - credentialSubject: new W3cCredentialSubject({ id: holder1.did }), - issuanceDate: w3cDate(Date.now()), - }), - alg: JwaSignatureAlgorithm.EdDSA, - verificationMethod: issuer.verificationMethod.id, - }) - - await holderTenant.w3cCredentials.storeCredential({ credential: signedCredential1 }) - await holderTenant.w3cCredentials.storeCredential({ credential: signedCredential2 }) - - const { authorizationRequest: authorizationRequestUri1, verificationSession: verificationSession1 } = - await verifierTenant1.modules.openId4VcVerifier.createAuthorizationRequest({ - verifierId: openIdVerifierTenant1.verifierId, - requestSigner: { - method: 'openid-federation', - }, - presentationExchange: { - definition: openBadgePresentationDefinition, - }, - }) - - expect(authorizationRequestUri1).toEqual( - `openid4vp://?client_id=${encodeURIComponent( - `http://localhost:1234/oid4vp/${openIdVerifierTenant1.verifierId}` - )}&request_uri=${encodeURIComponent(verificationSession1.authorizationRequestUri)}` - ) - - const { authorizationRequest: authorizationRequestUri2, verificationSession: verificationSession2 } = - await verifierTenant2.modules.openId4VcVerifier.createAuthorizationRequest({ - requestSigner: { - method: 'openid-federation', - }, - presentationExchange: { - definition: universityDegreePresentationDefinition, - }, - verifierId: openIdVerifierTenant2.verifierId, - }) - - expect(authorizationRequestUri2).toEqual( - `openid4vp://?client_id=${encodeURIComponent( - `http://localhost:1234/oid4vp/${openIdVerifierTenant2.verifierId}` - )}&request_uri=${encodeURIComponent(verificationSession2.authorizationRequestUri)}` - ) - - await verifierTenant1.endSession() - await verifierTenant2.endSession() - - const resolvedProofRequestWithFederationPromise = - holderTenant.modules.openId4VcHolder.resolveSiopAuthorizationRequest(authorizationRequestUri1, { - federation: { - // This will look for a whole different trusted entity - trustedEntityIds: [`http://localhost:1234/oid4vp/${openIdVerifierTenant2.verifierId}`], - }, - }) - - // TODO: Look into this error see if we can make it more specific - await expect(resolvedProofRequestWithFederationPromise).rejects.toThrow( - `Error verifying the DID Auth Token signature.` - ) - - const resolvedProofRequestWithoutFederationPromise = - holderTenant.modules.openId4VcHolder.resolveSiopAuthorizationRequest(authorizationRequestUri2) - await expect(resolvedProofRequestWithoutFederationPromise).rejects.toThrow( - `Error verifying the DID Auth Token signature.` - ) - }) }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8d20f3209c..f7739287e1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -730,8 +730,8 @@ importers: specifier: workspace:* version: link:../core '@openid-federation/core': - specifier: 0.1.1-alpha.13 - version: 0.1.1-alpha.13 + specifier: 0.1.1-alpha.15 + version: 0.1.1-alpha.15 '@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) @@ -2689,8 +2689,8 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} - '@openid-federation/core@0.1.1-alpha.13': - resolution: {integrity: sha512-QC4DSbiJ7eWstLs1O3XrX/yKFgaj+3ch8cA4N/02BywVNmkiYgW9qXhcvY50ULINuCeYdqIMIqCuHbaTa0A1hw==} + '@openid-federation/core@0.1.1-alpha.15': + resolution: {integrity: sha512-Q+GQfMpmP2zFtRdDFx6R9qBpyaWFlBVEZtbAEN9pA5H6AipuD90xZ25spAfBnQIsg3Q0QNhwjuI4kr/kbtJB4w==} '@peculiar/asn1-cms@2.3.13': resolution: {integrity: sha512-joqu8A7KR2G85oLPq+vB+NFr2ro7Ls4ol13Zcse/giPSzUNN0n2k3v8kMpf6QdGUhI13e5SzQYN8AKP8sJ8v4w==} @@ -10945,7 +10945,7 @@ snapshots: '@open-draft/until@2.1.0': {} - '@openid-federation/core@0.1.1-alpha.13': + '@openid-federation/core@0.1.1-alpha.15': dependencies: buffer: 6.0.3 zod: 3.23.8 @@ -13728,7 +13728,7 @@ snapshots: debug: 4.3.6 enhanced-resolve: 5.17.1 eslint: 8.57.0 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.6 @@ -13740,7 +13740,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): + eslint-module-utils@2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): dependencies: debug: 3.2.7 optionalDependencies: @@ -13761,7 +13761,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) hasown: 2.0.2 is-core-module: 2.15.0 is-glob: 4.0.3