Skip to content

Commit

Permalink
feat: Support for subordinate entities and authority hints (#2107)
Browse files Browse the repository at this point in the history
* feat: Support for subordinate entities and authority hints

Signed-off-by: Tom Lanser <tom@devv.nl>

* fix: Increased the openid fed version

Signed-off-by: Tom Lanser <tom@devv.nl>

* feat: tests for multiple layers

Signed-off-by: Tom Lanser <tom@devv.nl>

---------

Signed-off-by: Tom Lanser <tom@devv.nl>
  • Loading branch information
Tommylans authored Nov 25, 2024
1 parent 274b421 commit 8da4250
Show file tree
Hide file tree
Showing 7 changed files with 929 additions and 107 deletions.
2 changes: 1 addition & 1 deletion packages/openid4vc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +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.13",
"@openid-federation/core": "0.1.1-alpha.15",
"class-transformer": "^0.5.1",
"rxjs": "^7.8.0",
"zod": "^3.23.8",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export class OpenId4VcVerifierModule implements Module {

// 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)
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) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { OpenId4VcSiopAuthorizationEndpointConfig } from './router/authorizationEndpoint'
import type { OpenId4VcSiopAuthorizationRequestEndpointConfig } from './router/authorizationRequestEndpoint'
import type { Optional } from '@credo-ts/core'
import type { AgentContext, Optional } from '@credo-ts/core'
import type { Router } from 'express'

import { importExpress } from '../shared/router'
Expand All @@ -25,6 +25,30 @@ export interface OpenId4VcVerifierModuleConfigOptions {
authorization?: Optional<OpenId4VcSiopAuthorizationEndpointConfig, 'endpointPath'>
authorizationRequest?: Optional<OpenId4VcSiopAuthorizationRequestEndpointConfig, 'endpointPath'>
}

/**
* Configuration for the federation endpoint.
*/
federation?: {
// TODO: Make this functions also compatible with the issuer side
isSubordinateEntity?: (
agentContext: AgentContext,
options: {
verifierId: string

issuerEntityId: string
subjectEntityId: string
}
) => Promise<boolean>
getAuthorityHints?: (
agentContext: AgentContext,
options: {
verifierId: string

issuerEntityId: string
}
) => Promise<string[] | undefined>
}
}

export class OpenId4VcVerifierModuleConfig {
Expand Down Expand Up @@ -60,4 +84,8 @@ export class OpenId4VcVerifierModuleConfig {
endpointPath: userOptions?.endpointPath ?? '/authorize',
}
}

public get federation() {
return this.options.federation
}
}
113 changes: 110 additions & 3 deletions packages/openid4vc/src/openid4vc-verifier/router/federationEndpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import type { Key, Buffer } from '@credo-ts/core'
import type { RPRegistrationMetadataPayload } from '@sphereon/did-auth-siop'
import type { Router, Response } from 'express'

import { getJwkFromKey, KeyType } from '@credo-ts/core'
import { createEntityConfiguration } from '@openid-federation/core'
import { getJwkFromJson, getJwkFromKey, JwsService, KeyType } from '@credo-ts/core'
import { createEntityConfiguration, createEntityStatement, fetchEntityConfiguration } from '@openid-federation/core'
import { LanguageTagUtils, removeNullUndefined } from '@sphereon/did-auth-siop'

import { getRequestContext, sendErrorResponse } from '../../shared/router'
Expand Down Expand Up @@ -47,7 +47,10 @@ const createRPRegistrationMetadataPayload = (opts: any): RPRegistrationMetadataP
return removeNullUndefined(rpRegistrationMetadataPayload)
}

export function configureFederationEndpoint(router: Router) {
export function configureFederationEndpoint(
router: Router,
federationConfig: OpenId4VcVerifierModuleConfig['federation'] = {}
) {
// 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.
Expand Down Expand Up @@ -97,6 +100,11 @@ export function configureFederationEndpoint(router: Router) {
const alg = jwk.supportedSignatureAlgorithms[0]
const kid = federationKey.fingerprint

const authorityHints = await federationConfig.getAuthorityHints?.(agentContext, {
verifierId: verifier.verifierId,
issuerEntityId: verifierEntityId,
})

const entityConfiguration = await createEntityConfiguration({
header: {
kid,
Expand All @@ -111,10 +119,12 @@ export function configureFederationEndpoint(router: Router) {
jwks: {
keys: [{ kid, alg, ...jwk.toJson() }],
},
authority_hints: authorityHints,
metadata: {
federation_entity: {
organization_name: rpMetadata.client_name,
logo_uri: rpMetadata.logo_uri,
federation_fetch_endpoint: `${verifierEntityId}/openid-federation/fetch`,
},
openid_relying_party: {
...rpMetadata,
Expand Down Expand Up @@ -145,4 +155,101 @@ export function configureFederationEndpoint(router: Router) {
next()
}
)

// TODO: Currently it will fetch everything in realtime and creates a entity statement without even checking if it is allowed.
router.get('/openid-federation/fetch', async (request: OpenId4VcVerificationRequest, response: Response, next) => {
const { agentContext, verifier } = getRequestContext(request)

const { sub } = request.query
if (!sub || typeof sub !== 'string') {
sendErrorResponse(response, next, agentContext.config.logger, 400, 'invalid_request', 'sub is required')
return
}

const verifierConfig = agentContext.dependencyManager.resolve(OpenId4VcVerifierModuleConfig)

const entityId = `${verifierConfig.baseUrl}/${verifier.verifierId}`

const isSubordinateEntity = await federationConfig.isSubordinateEntity?.(agentContext, {
verifierId: verifier.verifierId,
issuerEntityId: entityId,
subjectEntityId: sub,
})
if (!isSubordinateEntity) {
if (!federationConfig.isSubordinateEntity) {
agentContext.config.logger.warn(
'isSubordinateEntity hook is not provided for the federation so we cannot check if this entity is a subordinate entity of the issuer',
{
verifierId: verifier.verifierId,
issuerEntityId: entityId,
subjectEntityId: sub,
}
)
}

sendErrorResponse(
response,
next,
agentContext.config.logger,
403,
'forbidden',
'This entity is not a subordinate entity of the issuer'
)
return
}

const jwsService = agentContext.dependencyManager.resolve(JwsService)

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

return res.isValid
},
})

let federationKey = federationKeyMapping.get(verifier.verifierId)
if (!federationKey) {
federationKey = await agentContext.wallet.createKey({
keyType: KeyType.Ed25519,
})
federationKeyMapping.set(verifier.verifierId, federationKey)
}

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

const entityStatement = await createEntityStatement({
header: {
kid,
alg,
typ: 'entity-statement+jwt',
},
jwk: {
...jwk.toJson(),
kid,
},
claims: {
sub: sub,
iss: entityId,
iat: new Date(),
exp: new Date(Date.now() + 1000 * 60 * 60 * 24), // 1 day TODO: Might needs to be a bit lower because a day is quite long for trust
jwks: {
keys: subjectEntityConfiguration.jwks.keys,
},
},
signJwtCallback: ({ toBeSigned }) =>
agentContext.wallet.sign({
data: toBeSigned as Buffer,
key: federationKey,
}),
})

response.writeHead(200, { 'Content-Type': 'application/entity-statement+jwt' }).end(entityStatement)
})
}
Loading

0 comments on commit 8da4250

Please sign in to comment.