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

[RELEASE FOR TESTING]: feat: OpenIdFed for the verifier #2094

Closed
wants to merge 17 commits into from
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 @@ -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.15",
"class-transformer": "^0.5.1",
"rxjs": "^7.8.0",
"zod": "^3.23.8",
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 @@ -7,7 +7,11 @@ import type {
OpenId4VciRequestTokenResponse,
OpenId4VciRetrieveAuthorizationCodeUsingPresentationOptions,
} from './OpenId4VciHolderServiceOptions'
import type { OpenId4VcSiopAcceptAuthorizationRequestOptions } from './OpenId4vcSiopHolderServiceOptions'
import type {
OpenId4VcSiopAcceptAuthorizationRequestOptions,
OpenId4VcSiopResolveAuthorizationRequestOptions,
OpenId4VcSiopResolveTrustChainsOptions,
} from './OpenId4vcSiopHolderServiceOptions'

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

Expand Down Expand Up @@ -42,8 +46,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 @@ -165,4 +172,8 @@ 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)
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import type {
OpenId4VcSiopAcceptAuthorizationRequestOptions,
OpenId4VcSiopFetchEntityConfigurationOptions,
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 +30,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 +47,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,
})

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

return {
authorizationRequest: verifiedAuthorizationRequest,

Expand Down Expand Up @@ -244,7 +284,7 @@ export class OpenId4VcSiopHolderService {
} as const
}

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 @@ -255,7 +295,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 @@ -265,7 +309,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 +453,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,39 @@ 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[]
}
}

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

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()
})
}
Loading
Loading