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: OpenID Federation for the verifier <-> holder #27

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -33,6 +33,7 @@
"@sphereon/oid4vci-common": "0.16.1-fix.173",
"@sphereon/oid4vci-issuer": "0.16.1-fix.173",
"@sphereon/ssi-types": "0.30.2-next.135",
"@openid-federation/core": "0.1.1-alpha.13",
"class-transformer": "^0.5.1",
"rxjs": "^7.8.0"
},
Expand Down
17 changes: 14 additions & 3 deletions packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import type {
OpenId4VciSendNotificationOptions,
OpenId4VciRequestTokenResponse,
} from './OpenId4VciHolderServiceOptions'
import type { OpenId4VcSiopAcceptAuthorizationRequestOptions } from './OpenId4vcSiopHolderServiceOptions'
import type {
OpenId4VcSiopAcceptAuthorizationRequestOptions,
OpenId4VcSiopResolveAuthorizationRequestOptions,
OpenId4VcSiopResolveTrustChainsOptions,
} from './OpenId4vcSiopHolderServiceOptions'

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

Expand Down Expand Up @@ -40,8 +44,11 @@ export class OpenId4VcHolderApi {
* @param requestJwtOrUri JWT or an SIOPv2 request URI
* @returns the resolved and verified authentication request.
*/
public async resolveSiopAuthorizationRequest(requestJwtOrUri: string) {
return this.openId4VcSiopHolderService.resolveAuthorizationRequest(this.agentContext, requestJwtOrUri)
public async resolveSiopAuthorizationRequest(
requestJwtOrUri: string,
options: OpenId4VcSiopResolveAuthorizationRequestOptions = {}
) {
return this.openId4VcSiopHolderService.resolveAuthorizationRequest(this.agentContext, requestJwtOrUri, options)
}

/**
Expand Down Expand Up @@ -181,4 +188,8 @@ export class OpenId4VcHolderApi {
public async sendNotification(options: OpenId4VciSendNotificationOptions) {
return this.openId4VciHolderService.sendNotification(options)
}

public async resolveOpenIdFederationChains(options: OpenId4VcSiopResolveTrustChainsOptions) {
return this.openId4VcSiopHolderService.resolveOpenIdFederationChains(this.agentContext, options)
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import type {
OpenId4VcSiopAcceptAuthorizationRequestOptions,
OpenId4VcSiopGetOpenIdProviderOptions,
OpenId4VcSiopResolveAuthorizationRequestOptions,
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 +29,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 All @@ -38,9 +46,12 @@ export class OpenId4VcSiopHolderService {

public async resolveAuthorizationRequest(
agentContext: AgentContext,
requestJwtOrUri: string
requestJwtOrUri: string,
options: OpenId4VcSiopResolveAuthorizationRequestOptions = {}
): Promise<OpenId4VcSiopResolvedAuthorizationRequest> {
const openidProvider = await this.getOpenIdProvider(agentContext)
const openidProvider = await this.getOpenIdProvider(agentContext, {
federation: options.federation,
})

Comment on lines +52 to 55
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is ok for now, but was also discussing with NF and i think we probably need to have some more control over 'trusted' entities in Credo.

So extend the x509 callbacks with global 'verificationContext` and you can either provide it to the call, or we use the global static config, or we use the dynamic callback. And you can either provide trusted federations, trusted x509s, or trusted dids / did methods.

(just rambling here 😄 )

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think we should think a bit more about this of what a good structure would be.

// parsing happens automatically in verifyAuthorizationRequest
const verifiedAuthorizationRequest = await openidProvider.verifyAuthorizationRequest(requestJwtOrUri)
Expand All @@ -59,6 +70,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 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
}
}

return {
authorizationRequest: verifiedAuthorizationRequest,

Expand Down Expand Up @@ -231,7 +270,7 @@ export class OpenId4VcSiopHolderService {
}
}

private async getOpenIdProvider(agentContext: AgentContext) {
private async getOpenIdProvider(agentContext: AgentContext, options: OpenId4VcSiopGetOpenIdProviderOptions = {}) {
const builder = OP.builder()
.withExpiresIn(6000)
.withIssuer(ResponseIss.SELF_ISSUED_V2)
Expand All @@ -242,7 +281,11 @@ export class OpenId4VcSiopHolderService {
SupportedVersion.SIOPv2_D12_OID4VP_D20,
])
.withCreateJwtCallback(getCreateJwtCallback(agentContext))
.withVerifyJwtCallback(getVerifyJwtCallback(agentContext))
.withVerifyJwtCallback(
getVerifyJwtCallback(agentContext, {
federation: options.federation,
})
)
.withHasher(Hasher.hash)

const openidProvider = builder.build()
Expand All @@ -252,7 +295,7 @@ export class OpenId4VcSiopHolderService {

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

if (verifiablePresentation instanceof W3cJsonLdVerifiablePresentation) {
Expand Down Expand Up @@ -395,4 +438,26 @@ export class OpenId4VcSiopHolderService {

return jwe
}

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

const { entityId, trustAnchorEntityIds } = options

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

return res.isValid
},
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {
OpenId4VcJwtIssuer,
OpenId4VcSiopVerifiedAuthorizationRequest,
OpenId4VcSiopAuthorizationResponsePayload,
OpenId4VcJwtIssuerFederation,
} from '../shared'
import type {
DifPexCredentialsForRequest,
Expand Down Expand Up @@ -42,12 +43,23 @@ 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 OpenId4VcSiopResolveAuthorizationRequestOptions {
federation?: {
/**
* The entity IDs of the trusted issuers.
*/
trustedEntityIds?: string[]
}
}

// FIXME: rethink properties
Expand All @@ -56,3 +68,17 @@ export interface OpenId4VcSiopAuthorizationResponseSubmission {
status: number
submittedResponse: OpenId4VcSiopAuthorizationResponsePayload
}

export interface OpenId4VcSiopGetOpenIdProviderOptions {
federation?: {
/**
* The entity IDs of the trusted issuers.
*/
trustedEntityIds?: string[]
}
}

export interface OpenId4VcSiopResolveTrustChainsOptions {
entityId: string
trustAnchorEntityIds: [string, ...string[]]
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
configureCredentialEndpoint,
configureIssuerMetadataEndpoint,
} from './router'
import { configureFederationEndpoint } from './router/federationEndpoint'

/**
* @public
Expand Down Expand Up @@ -119,6 +120,7 @@ export class OpenId4VcIssuerModule implements Module {
configureCredentialOfferEndpoint(endpointRouter, this.config.credentialOfferEndpoint)
configureAccessTokenEndpoint(endpointRouter, this.config.accessTokenEndpoint)
configureCredentialEndpoint(endpointRouter, this.config.credentialEndpoint)
configureFederationEndpoint(endpointRouter)

// First one will be called for all requests (when next is called)
contextRouter.use(async (req: OpenId4VcIssuanceRequest, _res: unknown, next) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -35,6 +36,7 @@ export interface OpenId4VcIssuerModuleConfigOptions {
OpenId4VciAccessTokenEndpointConfig,
'cNonceExpiresInSeconds' | 'endpointPath' | 'preAuthorizedCodeExpirationInSeconds' | 'tokenExpiresInSeconds'
>
federation?: Optional<OpenId4VcSiopFederationEndpointConfig, 'endpointPath'>
}
}

Expand Down
108 changes: 108 additions & 0 deletions packages/openid4vc/src/openid4vc-issuer/router/federationEndpoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
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'
import { OpenId4VcIssuerService } from '../OpenId4VcIssuerService'

// TODO: It's also possible that the issuer and the verifier can have the same openid-federation endpoint. In that case we need to combine them.

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)

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 = issuerMetadata.issuerDisplay?.[0]

const accessTokenSigningKey = Key.fromFingerprint(issuer.accessTokenPublicKeyFingerprint)

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.name,
logo_uri: issuerDisplay.logo?.url,
}
: 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, agentContext.config.logger, 500, 'invalid_request', error)
}

// NOTE: if we don't call next, the agentContext session handler will NOT be called
next()
})
}
Loading
Loading