Skip to content
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

feat: OpenIdFed for the verifier #2093

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 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
94d22bd
fix: Use the right fingerprint for the RP kid
Tommylans Nov 21, 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
262ee63
Merge branch 'feature/openid-federation-verfier--openwallet' into fea…
Tommylans Nov 25, 2024
623c3b7
fix: Apply withEntityId patch for the normal version
Tommylans Nov 26, 2024
c3939b2
chore: Bumped the federation package
Tommylans Nov 26, 2024
8628b5e
fix: changed clientName into client_name
Tommylans Nov 26, 2024
5f0b11a
fix: Search the jwk based on the kid
Tommylans Dec 5, 2024
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
1 change: 1 addition & 0 deletions packages/openid4vc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"@sphereon/did-auth-siop": "0.16.1-fix.173",
"@sphereon/oid4vc-common": "0.16.1-fix.173",
"@sphereon/ssi-types": "0.30.2-next.135",
"@openid-federation/core": "0.1.1-alpha.17",
"class-transformer": "^0.5.1",
"rxjs": "^7.8.0",
"zod": "^3.23.8",
Expand Down
14 changes: 13 additions & 1 deletion packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import type {
OpenId4VciRequestTokenResponse,
OpenId4VciRetrieveAuthorizationCodeUsingPresentationOptions,
} from './OpenId4VciHolderServiceOptions'
import type { OpenId4VcSiopAcceptAuthorizationRequestOptions } from './OpenId4vcSiopHolderServiceOptions'
import type {
OpenId4VcSiopAcceptAuthorizationRequestOptions,
OpenId4VcSiopResolveTrustChainsOptions,
OpenId4VcSiopFetchEntityConfigurationOptions,
} from './OpenId4vcSiopHolderServiceOptions'

import { injectable, AgentContext, DifPresentationExchangeService, DifPexCredentialsForRequest } from '@credo-ts/core'

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

public async resolveOpenIdFederationChains(options: OpenId4VcSiopResolveTrustChainsOptions) {
return this.openId4VcSiopHolderService.resolveOpenIdFederationChains(this.agentContext, options)
}

public async fetchOpenIdFederationEntityConfiguration(options: OpenId4VcSiopFetchEntityConfigurationOptions) {
return this.openId4VcSiopHolderService.fetchOpenIdFederationEntityConfiguration(this.agentContext, options)
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type {
OpenId4VcSiopAcceptAuthorizationRequestOptions,
OpenId4VcSiopFetchEntityConfigurationOptions,
OpenId4VcSiopResolvedAuthorizationRequest,
OpenId4VcSiopResolveTrustChainsOptions,
} 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,
Expand All @@ -26,7 +28,12 @@ import {
injectable,
parseDid,
MdocDeviceResponse,
JwsService,
} from '@credo-ts/core'
import {
resolveTrustChains as federationResolveTrustChains,
fetchEntityConfiguration as federationFetchEntityConfiguration,
} from '@openid-federation/core'
import { OP, ResponseIss, ResponseMode, ResponseType, SupportedVersion, VPTokenLocation } from '@sphereon/did-auth-siop'

import { getSphereonVerifiablePresentation } from '../shared/transform'
Expand Down Expand Up @@ -59,6 +66,38 @@ export class OpenId4VcSiopHolderService {

const presentationDefinition = verifiedAuthorizationRequest.presentationDefinitions?.[0]?.definition

if (verifiedAuthorizationRequest.clientIdScheme === 'entity_id') {
const clientId = await verifiedAuthorizationRequest.authorizationRequest.getMergedProperty<string>('client_id')
if (!clientId) {
throw new CredoError("Unable to extract 'client_id' from authorization request")
}

const jwsService = agentContext.dependencyManager.resolve(JwsService)

const entityConfiguration = await federationFetchEntityConfiguration({
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
verifiedAuthorizationRequest.authorizationRequest.payload.client_metadata = openidRelyingPartyMetadata
}

// TODO: Do we want to do something with the real chain of do we want to give the user the possibility to do that somewhere else with the risk of being forgotten or that it doesn't have enough information at that place?
}

return {
authorizationRequest: verifiedAuthorizationRequest,

Expand Down Expand Up @@ -265,7 +304,7 @@ export class OpenId4VcSiopHolderService {

private getOpenIdTokenIssuerFromVerifiablePresentation(
verifiablePresentation: VerifiablePresentation
): OpenId4VcJwtIssuer {
): Exclude<OpenId4VcJwtIssuer, OpenId4VcJwtIssuerFederation> {
let openIdTokenIssuer: OpenId4VcJwtIssuer

if (verifiablePresentation instanceof W3cJsonLdVerifiablePresentation) {
Expand Down Expand Up @@ -409,4 +448,47 @@ export class OpenId4VcSiopHolderService {

return jwe
}

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

const { entityId, trustAnchorEntityIds } = options

return federationResolveTrustChains({
entityId,
trustAnchorEntityIds,
verifyJwtCallback: async ({ jwt, jwk }) => {
const res = await jwsService.verifyJws(agentContext, {
jws: jwt,
jwkResolver: () => getJwkFromJson(jwk),
})

return res.isValid
},
})
}

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

const { entityId } = options

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

return res.isValid
},
})
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import type { OpenId4VcJwtIssuer, OpenId4VcSiopVerifiedAuthorizationRequest } from '../shared'
import type {
OpenId4VcJwtIssuer,
OpenId4VcSiopVerifiedAuthorizationRequest,
OpenId4VcJwtIssuerFederation,
} from '../shared'
import type {
DifPexCredentialsForRequest,
DifPexInputDescriptorToCredentials,
Expand Down Expand Up @@ -38,10 +42,21 @@ 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<OpenId4VcJwtIssuer, OpenId4VcJwtIssuerFederation>

/**
* The verified authorization request.
*/
authorizationRequest: OpenId4VcSiopVerifiedAuthorizationRequest

// TODO: Not sure if this also needs the federation because the validation of the authorization is already done with the ResolveAuthorizationRequest
}

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

export interface OpenId4VcSiopFetchEntityConfigurationOptions {
entityId: string
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
configureNonceEndpoint,
configureAuthorizationChallengeEndpoint,
} 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 { OpenId4VcIssuanceRequest } from './requestContext'
import type { Buffer } from '@credo-ts/core'
import type { Router, Response } from 'express'

import { Key, getJwkFromKey, KeyType } 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 @@ -108,12 +108,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,
entityId: federationClientId,
})
: await openIdTokenIssuerToJwtIssuer(agentContext, options.requestSigner)

let clientIdScheme: ClientIdScheme
Expand Down Expand Up @@ -143,9 +150,18 @@ export class OpenId4VcSiopVerifierService {
} else if (jwtIssuer.method === 'did') {
clientId = jwtIssuer.didUrl.split('#')[0]
clientIdScheme = 'did'
} else if (jwtIssuer.method === 'custom') {
if (jwtIssuer.options?.method === 'openid-federation') {
clientIdScheme = 'entity_id'
clientId = federationClientId
} else {
throw new CredoError(
`jwtIssuer 'method' 'custom' must have a 'method' property with value 'openid-federation' when using the 'custom' method.`
)
}
} else {
throw new CredoError(
`Unsupported jwt issuer method '${options.requestSigner.method}'. Only 'did' and 'x5c' are supported.`
`Unsupported jwt issuer method '${options.requestSigner.method}'. Only 'did', 'x5c' and 'custom' are supported.`
)
}

Expand Down Expand Up @@ -232,6 +248,8 @@ export class OpenId4VcSiopVerifierService {

const verifier = await this.getVerifierByVerifierId(agentContext, options.verificationSession.verifierId)
const requestClientId = await authorizationRequest.getMergedProperty<string>('client_id')
// TODO: Is this needed for the verification of the federation?
const requestClientIdScheme = await authorizationRequest.getMergedProperty<ClientIdScheme>('client_id_scheme')
const requestNonce = await authorizationRequest.getMergedProperty<string>('nonce')
const requestState = await authorizationRequest.getMergedProperty<string>('state')
const responseUri = await authorizationRequest.getMergedProperty<string>('response_uri')
Expand All @@ -252,6 +270,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
Expand Down Expand Up @@ -469,7 +488,7 @@ export class OpenId4VcSiopVerifierService {
return this.openId4VcVerificationSessionRepository.getById(agentContext, verificationSessionId)
}

private async getRelyingParty(
public async getRelyingParty(
agentContext: AgentContext,
verifier: OpenId4VcVerifierRecord,
{
Expand Down Expand Up @@ -607,6 +626,11 @@ export class OpenId4VcSiopVerifierService {

if (clientIdScheme) {
builder.withClientIdScheme(clientIdScheme)

if (clientIdScheme === 'entity_id') {
// @ts-expect-error - This is something in the new OIDVC package but is't used yet in this credo version
builder.withEntityId(clientId)
}
}

if (presentationDefinition) {
Expand Down
Loading
Loading