From 592ab26071d404d34ecf986162f857e258a1abdc Mon Sep 17 00:00:00 2001 From: Tom Lanser Date: Tue, 5 Nov 2024 11:34:39 +0100 Subject: [PATCH] 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 | 64 +-- 17 files changed, 908 insertions(+), 41 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 4b76a0272e..0cb5858059 100644 --- a/packages/openid4vc/package.json +++ b/packages/openid4vc/package.json @@ -33,6 +33,7 @@ "@sphereon/oid4vci-common": "0.16.1-next.168", "@sphereon/oid4vci-issuer": "0.16.1-next.168", "@sphereon/ssi-types": "0.29.1-unstable.121", + "@openid-federation/core": "0.1.1-alpha.5", "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 6617204056..80b5771c1d 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 555ad5606c..0350666e72 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts @@ -139,6 +139,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.` @@ -227,6 +235,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 presentationDefinitionsWithLocation = await authorizationRequest.getPresentationDefinitions() @@ -246,6 +256,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 @@ -452,7 +463,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 8e25089ba7..3f1873c8fd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -455,7 +455,7 @@ importers: version: 0.7.2 '@sphereon/pex': specifier: 5.0.0-unstable.2 - version: 5.0.0-unstable.2(ts-node@10.9.2(@swc/core@1.7.40)(@types/node@18.18.8)(typescript@5.5.4)) + version: 5.0.0-unstable.2 '@sphereon/pex-models': specifier: ^2.3.1 version: 2.3.1 @@ -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-next.168 version: 0.16.1-next.168(ts-node@10.9.2(@swc/core@1.7.40)(@types/node@18.18.8)(typescript@5.5.4))(typescript@5.5.4) @@ -2257,6 +2260,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==} @@ -2582,12 +2588,12 @@ packages: '@sphereon/ssi-types@0.29.1-unstable.161': resolution: {integrity: sha512-ifMADjk6k0f97/isK/4Qw/PX6n4k+qS5k6mmmH47MTD3KMDddVghoXycsvNw7wObJdLUalHBX630ghr+u21oMg==} + '@sphereon/ssi-types@0.29.1-unstable.208': + resolution: {integrity: sha512-3YAFzy//BojsYN+RYoEjndWP3w5a8a3qRZi5dS0Gh6s4yMCiykqTJM1agJVeoaLce8JxFFaCWSpkzwbmJYGTaQ==} + '@sphereon/ssi-types@0.30.1': resolution: {integrity: sha512-vbYaxQXb71sOPwDj7TRDlUGfIHKVVs8PiHfImPBgSBshrD7VpEHOrB+EwwavMm5MAQvWK/yblGmzk7FHds7SHA==} - '@sphereon/ssi-types@0.9.0': - resolution: {integrity: sha512-umCr/syNcmvMMbQ+i/r/mwjI1Qw2aFPp9AwBTvTo1ailAVaaJjJGPkkVz1K9/2NZATNdDiQ3A8yGzdVJoKh9pA==} - '@sphereon/wellknown-dids-client@0.1.3': resolution: {integrity: sha512-TAT24L3RoXD8ocrkTcsz7HuJmgjNjdoV6IXP1p3DdaI/GqkynytXE3J1+F7vUFMRYwY5nW2RaXSgDQhrFJemaA==} @@ -9895,6 +9901,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 @@ -10618,14 +10629,14 @@ snapshots: - ts-node - typeorm-aurora-data-api-driver - '@sphereon/pex@5.0.0-unstable.2(ts-node@10.9.2(@swc/core@1.7.40)(@types/node@18.18.8)(typescript@5.5.4))': + '@sphereon/pex@5.0.0-unstable.2': dependencies: '@astronautlabs/jsonpath': 1.1.2 '@sd-jwt/decode': 0.6.1 '@sd-jwt/present': 0.6.1 '@sd-jwt/types': 0.6.1 '@sphereon/pex-models': 2.3.1 - '@sphereon/ssi-types': 0.29.1-unstable.121(ts-node@10.9.2(@swc/core@1.7.40)(@types/node@18.18.8)(typescript@5.5.4)) + '@sphereon/ssi-types': 0.29.1-unstable.208 ajv: 8.17.1 ajv-formats: 2.1.1(ajv@8.17.1) jwt-decode: 3.1.2 @@ -10633,25 +10644,7 @@ snapshots: string.prototype.matchall: 4.0.11 uint8arrays: 3.1.1 transitivePeerDependencies: - - '@google-cloud/spanner' - - '@sap/hana-client' - - better-sqlite3 - - encoding - - hdb-pool - - ioredis - - mongodb - - mssql - - mysql2 - - oracledb - - pg - - pg-native - - pg-query-stream - - redis - - sql.js - - sqlite3 - supports-color - - ts-node - - typeorm-aurora-data-api-driver '@sphereon/ssi-sdk-ext.did-utils@0.24.1-unstable.112(ts-node@10.9.2(@swc/core@1.7.40)(@types/node@18.18.8)(typescript@5.5.4))': dependencies: @@ -11023,6 +11016,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 @@ -11052,17 +11055,14 @@ snapshots: - ts-node - typeorm-aurora-data-api-driver - '@sphereon/ssi-types@0.9.0': - dependencies: - jwt-decode: 3.1.2 - '@sphereon/wellknown-dids-client@0.1.3': dependencies: - '@sphereon/ssi-types': 0.9.0 + '@sphereon/ssi-types': 0.29.1-unstable.208 cross-fetch: 3.1.8 jwt-decode: 3.1.2 transitivePeerDependencies: - encoding + - supports-color '@sqltools/formatter@1.2.5': {} @@ -12866,7 +12866,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 @@ -12878,7 +12878,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: @@ -12899,7 +12899,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