diff --git a/packages/openid4vc/package.json b/packages/openid4vc/package.json index 1e0e13d9b..4bba471c0 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 f84200ef8..49cf592f7 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 40a5d48d6..04aa76461 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 7d7e199a0..083297286 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 15f656a32..dacab4c2e 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 ef1b7319e..2980c22b8 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 9c81150c2..264709bfb 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 0360b3cf1..b2ec763cb 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 0e02d9727..e3fa74fce 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 fefafdb42..000000000 --- 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 3e3b26e4c..8eacb927b 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 c3fada42d..49c296211 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 de0f72588..d85f82306 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 7eb667a14..3abd06dc9 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 1601d97c5..b1b08f440 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: