Skip to content

feat: OpenIdFed for the verifier #2093

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 25 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b723485
feat: working version
Tommylans Nov 5, 2024
bcaed4d
feat: Littlebit of a cleanup for the verifier
Tommylans Nov 6, 2024
1743fb1
fix: typescript error
Tommylans Nov 7, 2024
dcd810d
feat: Processed feedback and used the right keys for the verifier
Tommylans Nov 18, 2024
cb6d70f
feat: Added more logging and added unhappy tests
Tommylans Nov 18, 2024
b06c546
chore: Made some things more logic
Tommylans Nov 18, 2024
2b8bde5
feat: Holder side api for getting more context information
Tommylans Nov 18, 2024
8bb4564
Merge branch 'main' into feature/openid-federation-verfier
Tommylans Nov 19, 2024
f6f766d
fix: Merge conflict and changes
Tommylans Nov 20, 2024
b2b3890
feat: Added fetchEntityConfiguration
Tommylans Nov 20, 2024
4515ad2
Merge branch 'main' into feature/openid-federation-verfier
TimoGlastra Nov 20, 2024
d5ea627
update lock
TimoGlastra Nov 20, 2024
11455b5
fix: OpenID Federation small fixes (#2099)
Tommylans Nov 21, 2024
367dfa2
chore: Update branch with main (#2106)
Tommylans Nov 24, 2024
ff73f53
Merge branch 'main' into feature/openid-federation-verfier
TimoGlastra Nov 24, 2024
274b421
update lockfile
TimoGlastra Nov 24, 2024
8da4250
feat: Support for subordinate entities and authority hints (#2107)
Tommylans Nov 25, 2024
ec09e8a
Merge remote-tracking branch 'upstream/main' into feature/openid-fede…
TimoGlastra Mar 24, 2025
40fd382
updates
TimoGlastra Mar 24, 2025
96dc3bb
cleanup and improvements
TimoGlastra Mar 24, 2025
41409d6
cleanup and improvements
TimoGlastra Mar 24, 2025
f0acafe
federation tests working
TimoGlastra Mar 24, 2025
b702af4
fix: set client metadata
TimoGlastra Mar 24, 2025
b46c116
Merge remote-tracking branch 'upstream/main' into feature/openid-fede…
TimoGlastra Apr 4, 2025
fd9c18e
fix: openid federation tests
TimoGlastra Apr 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions packages/openid4vc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@
"class-transformer": "^0.5.1",
"rxjs": "^7.8.0",
"zod": "^3.24.2",
"@openid4vc/openid4vci": "0.3.0-alpha-20250330133535",
"@openid4vc/oauth2": "0.3.0-alpha-20250330133535",
"@openid4vc/openid4vp": "0.3.0-alpha-20250330133535",
"@openid4vc/utils": "0.3.0-alpha-20250330133535"
"@openid-federation/core": "0.1.1-alpha.17",
"@openid4vc/openid4vci": "0.3.0-alpha-20250404080256",
"@openid4vc/oauth2": "0.3.0-alpha-20250404080256",
"@openid4vc/openid4vp": "0.3.0-alpha-20250404080256",
"@openid4vc/utils": "0.3.0-alpha-20250404080256"
},
"devDependencies": {
"@credo-ts/tenants": "workspace:*",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
} from './OpenId4VciHolderServiceOptions'
import type {
OpenId4VpAcceptAuthorizationRequestOptions,
OpenId4VpResolveTrustChainsOptions,
ResolveOpenId4VpAuthorizationRequestOptions,
} from './OpenId4vpHolderServiceOptions'

Expand Down Expand Up @@ -191,4 +192,8 @@ export class OpenId4VcHolderApi {
public async sendNotification(options: OpenId4VciSendNotificationOptions) {
return this.openId4VciHolderService.sendNotification(this.agentContext, options)
}

public async resolveOpenIdFederationChains(options: OpenId4VpResolveTrustChainsOptions) {
return this.openId4VpHolderService.resolveOpenIdFederationChains(this.agentContext, options)
}
}
111 changes: 109 additions & 2 deletions packages/openid4vc/src/openid4vc-holder/OpenId4vpHolderService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import type {
} from '@credo-ts/core'
import type {
OpenId4VpAcceptAuthorizationRequestOptions,
OpenId4VpFetchEntityConfigurationOptions,
OpenId4VpResolveTrustChainsOptions,
OpenId4VpResolvedAuthorizationRequest,
ParsedTransactionDataEntry,
ResolveOpenId4VpAuthorizationRequestOptions,
Expand All @@ -23,9 +25,15 @@ import {
DifPresentationExchangeService,
DifPresentationExchangeSubmissionLocation,
Hasher,
JwsService,
TypedArrayEncoder,
getJwkFromJson,
injectable,
} from '@credo-ts/core'
import {
fetchEntityConfiguration as federationFetchEntityConfiguration,
resolveTrustChains as federationResolveTrustChains,
} from '@openid-federation/core'
import {
Openid4vpAuthorizationResponse,
Openid4vpClient,
Expand All @@ -47,10 +55,15 @@ export class OpenId4VpHolderService {

private getOpenid4vpClient(
agentContext: AgentContext,
options?: { trustedCertificates?: EncodedX509Certificate[]; isVerifyOpenId4VpAuthorizationRequest?: boolean }
options?: {
trustedCertificates?: EncodedX509Certificate[]
trustedFederationEntityIds?: string[]
isVerifyOpenId4VpAuthorizationRequest?: boolean
}
) {
const callbacks = getOid4vcCallbacks(agentContext, {
trustedCertificates: options?.trustedCertificates,
trustedFederationEntityIds: options?.trustedFederationEntityIds,
isVerifyOpenId4VpAuthorizationRequest: options?.isVerifyOpenId4VpAuthorizationRequest,
})
return new Openid4vpClient({ callbacks })
Expand Down Expand Up @@ -119,6 +132,7 @@ export class OpenId4VpHolderService {
): Promise<OpenId4VpResolvedAuthorizationRequest> {
const openid4vpClient = this.getOpenid4vpClient(agentContext, {
trustedCertificates: options?.trustedCertificates,
trustedFederationEntityIds: options?.trustedFederationEntityIds,
isVerifyOpenId4VpAuthorizationRequest: true,
})
const { params } = openid4vpClient.parseOpenid4vpAuthorizationRequest({ authorizationRequest })
Expand All @@ -130,10 +144,50 @@ export class OpenId4VpHolderService {

const { client, pex, transactionData, dcql } = verifiedAuthorizationRequest

if (client.scheme !== 'x509_san_dns' && client.scheme !== 'did' && client.scheme !== 'web-origin') {
if (
client.scheme !== 'x509_san_dns' &&
client.scheme !== 'did' &&
client.scheme !== 'web-origin' &&
client.scheme !== 'https'
) {
throw new CredoError(`Client scheme '${client.scheme}' is not supported`)
}

if (client.scheme === 'https') {
const jwsService = agentContext.dependencyManager.resolve(JwsService)

const entityConfiguration = await federationFetchEntityConfiguration({
entityId: client.identifier,
verifyJwtCallback: async ({ jwt, jwk }) => {
const res = await jwsService.verifyJws(agentContext, {
jws: jwt,
jwsSigner: {
method: 'jwk',
jwk: getJwkFromJson(jwk),
},
})

return res.isValid
},
})
if (!entityConfiguration)
throw new CredoError(`Unable to fetch entity configuration for entityId '${client.identifier}'`)

const openidRelyingPartyMetadata = entityConfiguration.metadata?.openid_relying_party
if (!openidRelyingPartyMetadata) {
throw new CredoError(`Federation entity '${client.identifier}' does not have 'openid_relying_party' metadata.`)
}

// FIXME: we probably don't want to override this, but otherwise the accept logic doesn't have
// access to the correct metadata. Should we also pass client to accept?
// @ts-ignore
verifiedAuthorizationRequest.authorizationRequestPayload.client_metadata = openidRelyingPartyMetadata
// FIXME: we should not just override the metadata?
// When federation is used we need to use the federation metadata
// @ts-ignore
client.clientMetadata = openidRelyingPartyMetadata
}

const pexResult = pex?.presentation_definition
? await this.handlePresentationExchangeRequest(agentContext, pex.presentation_definition, transactionData)
: undefined
Expand All @@ -147,6 +201,10 @@ export class OpenId4VpHolderService {
authorizationRequestPayload: verifiedAuthorizationRequest.authorizationRequestPayload,
transactionData: pexResult?.matchedTransactionData ?? dcqlResult?.matchedTransactionData,
presentationExchange: pexResult?.pex,
verifier: {
clientIdScheme: client.scheme,
clientMetadata: client.clientMetadata,
},
dcql: dcqlResult?.dcql,
origin: options?.origin,
signedAuthorizationRequest: verifiedAuthorizationRequest.jar
Expand Down Expand Up @@ -406,6 +464,9 @@ export class OpenId4VpHolderService {

const response = await openid4vpClient.createOpenid4vpAuthorizationResponse({
authorizationRequestPayload,
// We overwrite the client metadata on the authorization request payload when using OpenID Federation
clientMetadata: authorizationRequestPayload.client_metadata,
origin: options.origin,
authorizationResponsePayload: {
vp_token: vpToken,
presentation_submission: presentationSubmission,
Expand Down Expand Up @@ -483,4 +544,50 @@ export class OpenId4VpHolderService {
presentationDuringIssuanceSession: responseJson?.presentation_during_issuance_session as string | undefined,
} as const
}

public async resolveOpenIdFederationChains(agentContext: AgentContext, options: OpenId4VpResolveTrustChainsOptions) {
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,
jwsSigner: {
method: 'jwk',
jwk: getJwkFromJson(jwk),
},
})

return res.isValid
},
})
}

public async fetchOpenIdFederationEntityConfiguration(
agentContext: AgentContext,
options: OpenId4VpFetchEntityConfigurationOptions
) {
const jwsService = agentContext.dependencyManager.resolve(JwsService)

const { entityId } = options

return federationFetchEntityConfiguration({
entityId,
verifyJwtCallback: async ({ jwt, jwk }) => {
const res = await jwsService.verifyJws(agentContext, {
jws: jwt,
jwsSigner: {
method: 'jwk',
jwk: getJwkFromJson(jwk),
},
})

return res.isValid
},
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ import type {
DifPresentationExchangeDefinition,
EncodedX509Certificate,
} from '@credo-ts/core'
import { ResolvedOpenid4vpAuthorizationRequest } from '@openid4vc/openid4vp'
import { ClientIdScheme, ClientMetadata, ResolvedOpenid4vpAuthorizationRequest } from '@openid4vc/openid4vp'
import type { OpenId4VpAuthorizationRequestPayload } from '../shared'

// TODO: export from oid4vp
export type ParsedTransactionDataEntry = NonNullable<ResolvedOpenid4vpAuthorizationRequest['transactionData']>[number]

export interface ResolveOpenId4VpAuthorizationRequestOptions {
trustedCertificates?: EncodedX509Certificate[]
trustedFederationEntityIds?: string[]
origin?: string
}

Expand All @@ -33,6 +34,20 @@ export interface OpenId4VpResolvedAuthorizationRequest {
queryResult: DcqlQueryResult
}

verifier: {
/**
* The client id scheme
*/
clientIdScheme: ClientIdScheme

/**
* The client id metadata.
*
* In case of 'https' (federation) client id scheme, this will be the metadata from the federation.
*/
clientMetadata?: ClientMetadata
}

/**
* The transaction data entries, with the matched credential ids.
* - For Presentation Exchange the id refers to the presentation exchange id
Expand Down Expand Up @@ -112,3 +127,12 @@ export interface OpenId4VpAcceptAuthorizationRequestOptions {
*/
origin?: string
}

export interface OpenId4VpResolveTrustChainsOptions {
entityId: string
trustAnchorEntityIds: [string, ...string[]]
}

export interface OpenId4VpFetchEntityConfigurationOptions {
entityId: string
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
configureNonceEndpoint,
configureOAuthAuthorizationServerMetadataEndpoint,
} from './router'
import { configureFederationEndpoint } from './router/federationEndpoint'

/**
* @public
Expand Down Expand Up @@ -134,6 +135,7 @@ export class OpenId4VcIssuerModule implements Module {
configureAccessTokenEndpoint(endpointRouter, this.config)
configureAuthorizationChallengeEndpoint(endpointRouter, this.config)
configureCredentialEndpoint(endpointRouter, this.config)
configureFederationEndpoint(endpointRouter)

// First one will be called for all requests (when next is called)
contextRouter.use(async (req: OpenId4VcIssuanceRequest, _res: unknown, next) => {
Expand Down
105 changes: 105 additions & 0 deletions packages/openid4vc/src/openid4vc-issuer/router/federationEndpoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import type { Buffer } from '@credo-ts/core'
import type { Response, Router } from 'express'
import type { OpenId4VcIssuanceRequest } from './requestContext'

import { Key, KeyType, getJwkFromKey } from '@credo-ts/core'
import { createEntityConfiguration } from '@openid-federation/core'

import { getRequestContext, sendErrorResponse } from '../../shared/router'

// 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) {
// 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)

try {
// TODO: Should be only created once per issuer and be used between instances
const federationKey = await agentContext.wallet.createKey({
keyType: KeyType.Ed25519,
})

const now = new Date()
const expires = new Date(now.getTime() + 1000 * 60 * 60 * 24) // 1 day from now

// TODO: We need to generate a key and always use that for the entity configuration

const jwk = getJwkFromKey(federationKey)

const kid = federationKey.fingerprint
const alg = jwk.supportedSignatureAlgorithms[0]

const issuerDisplay = issuer.display?.[0]

const accessTokenSigningKey = Key.fromFingerprint(issuer.accessTokenPublicKeyFingerprint)

const entityConfiguration = await createEntityConfiguration({
claims: {
sub: issuer.issuerId,
iss: issuer.issuerId,
iat: now,
exp: expires,
jwks: {
keys: [{ kid, alg, ...jwk.toJson() }],
},
metadata: {
federation_entity: issuerDisplay
? {
organization_name: issuerDisplay.name,
logo_uri: issuerDisplay.logo?.uri,
}
: undefined,
openid_provider: {
// 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: [
{
// 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: {
kid,
alg,
typ: 'entity-statement+jwt',
},
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, next, agentContext.config.logger, 500, 'invalid_request', error)
return
}

// NOTE: if we don't call next, the agentContext session handler will NOT be called
next()
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { OpenId4VcVerifierApi } from './OpenId4VcVerifierApi'
import { OpenId4VcVerifierModuleConfig } from './OpenId4VcVerifierModuleConfig'
import { OpenId4VpVerifierService } from './OpenId4VpVerifierService'
import { OpenId4VcVerifierRepository } from './repository'
import { configureAuthorizationEndpoint } from './router'
import { configureAuthorizationEndpoint, configureFederationEndpoint } from './router'
import { configureAuthorizationRequestEndpoint } from './router/authorizationRequestEndpoint'

/**
Expand Down Expand Up @@ -120,6 +120,10 @@ export class OpenId4VcVerifierModule implements Module {
configureAuthorizationEndpoint(endpointRouter, this.config)
configureAuthorizationRequestEndpoint(endpointRouter, this.config)

// 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, this.config.federation)

// First one will be called for all requests (when next is called)
contextRouter.use(async (req: OpenId4VcVerificationRequest, _res: unknown, next) => {
const { agentContext } = getRequestContext(req)
Expand Down
Loading
Loading