Skip to content

Commit 8da4250

Browse files
authored
feat: Support for subordinate entities and authority hints (#2107)
* 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>
1 parent 274b421 commit 8da4250

File tree

7 files changed

+929
-107
lines changed

7 files changed

+929
-107
lines changed

packages/openid4vc/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"@sphereon/did-auth-siop": "0.16.1-fix.173",
3131
"@sphereon/oid4vc-common": "0.16.1-fix.173",
3232
"@sphereon/ssi-types": "0.30.2-next.135",
33-
"@openid-federation/core": "0.1.1-alpha.13",
33+
"@openid-federation/core": "0.1.1-alpha.15",
3434
"class-transformer": "^0.5.1",
3535
"rxjs": "^7.8.0",
3636
"zod": "^3.23.8",

packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ export class OpenId4VcVerifierModule implements Module {
126126

127127
// TODO: The keys needs to be passed down to the federation endpoint to be used in the entity configuration for the openid relying party
128128
// 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.
129-
configureFederationEndpoint(endpointRouter)
129+
configureFederationEndpoint(endpointRouter, this.config.federation)
130130

131131
// First one will be called for all requests (when next is called)
132132
contextRouter.use(async (req: OpenId4VcVerificationRequest, _res: unknown, next) => {

packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModuleConfig.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { OpenId4VcSiopAuthorizationEndpointConfig } from './router/authorizationEndpoint'
22
import type { OpenId4VcSiopAuthorizationRequestEndpointConfig } from './router/authorizationRequestEndpoint'
3-
import type { Optional } from '@credo-ts/core'
3+
import type { AgentContext, Optional } from '@credo-ts/core'
44
import type { Router } from 'express'
55

66
import { importExpress } from '../shared/router'
@@ -25,6 +25,30 @@ export interface OpenId4VcVerifierModuleConfigOptions {
2525
authorization?: Optional<OpenId4VcSiopAuthorizationEndpointConfig, 'endpointPath'>
2626
authorizationRequest?: Optional<OpenId4VcSiopAuthorizationRequestEndpointConfig, 'endpointPath'>
2727
}
28+
29+
/**
30+
* Configuration for the federation endpoint.
31+
*/
32+
federation?: {
33+
// TODO: Make this functions also compatible with the issuer side
34+
isSubordinateEntity?: (
35+
agentContext: AgentContext,
36+
options: {
37+
verifierId: string
38+
39+
issuerEntityId: string
40+
subjectEntityId: string
41+
}
42+
) => Promise<boolean>
43+
getAuthorityHints?: (
44+
agentContext: AgentContext,
45+
options: {
46+
verifierId: string
47+
48+
issuerEntityId: string
49+
}
50+
) => Promise<string[] | undefined>
51+
}
2852
}
2953

3054
export class OpenId4VcVerifierModuleConfig {
@@ -60,4 +84,8 @@ export class OpenId4VcVerifierModuleConfig {
6084
endpointPath: userOptions?.endpointPath ?? '/authorize',
6185
}
6286
}
87+
88+
public get federation() {
89+
return this.options.federation
90+
}
6391
}

packages/openid4vc/src/openid4vc-verifier/router/federationEndpoint.ts

Lines changed: 110 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import type { Key, Buffer } from '@credo-ts/core'
33
import type { RPRegistrationMetadataPayload } from '@sphereon/did-auth-siop'
44
import type { Router, Response } from 'express'
55

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

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

50-
export function configureFederationEndpoint(router: Router) {
50+
export function configureFederationEndpoint(
51+
router: Router,
52+
federationConfig: OpenId4VcVerifierModuleConfig['federation'] = {}
53+
) {
5154
// TODO: this whole result needs to be cached and the ttl should be the expires of this node
5255

5356
// TODO: This will not work for multiple instances so we have to save it in the database.
@@ -97,6 +100,11 @@ export function configureFederationEndpoint(router: Router) {
97100
const alg = jwk.supportedSignatureAlgorithms[0]
98101
const kid = federationKey.fingerprint
99102

103+
const authorityHints = await federationConfig.getAuthorityHints?.(agentContext, {
104+
verifierId: verifier.verifierId,
105+
issuerEntityId: verifierEntityId,
106+
})
107+
100108
const entityConfiguration = await createEntityConfiguration({
101109
header: {
102110
kid,
@@ -111,10 +119,12 @@ export function configureFederationEndpoint(router: Router) {
111119
jwks: {
112120
keys: [{ kid, alg, ...jwk.toJson() }],
113121
},
122+
authority_hints: authorityHints,
114123
metadata: {
115124
federation_entity: {
116125
organization_name: rpMetadata.client_name,
117126
logo_uri: rpMetadata.logo_uri,
127+
federation_fetch_endpoint: `${verifierEntityId}/openid-federation/fetch`,
118128
},
119129
openid_relying_party: {
120130
...rpMetadata,
@@ -145,4 +155,101 @@ export function configureFederationEndpoint(router: Router) {
145155
next()
146156
}
147157
)
158+
159+
// TODO: Currently it will fetch everything in realtime and creates a entity statement without even checking if it is allowed.
160+
router.get('/openid-federation/fetch', async (request: OpenId4VcVerificationRequest, response: Response, next) => {
161+
const { agentContext, verifier } = getRequestContext(request)
162+
163+
const { sub } = request.query
164+
if (!sub || typeof sub !== 'string') {
165+
sendErrorResponse(response, next, agentContext.config.logger, 400, 'invalid_request', 'sub is required')
166+
return
167+
}
168+
169+
const verifierConfig = agentContext.dependencyManager.resolve(OpenId4VcVerifierModuleConfig)
170+
171+
const entityId = `${verifierConfig.baseUrl}/${verifier.verifierId}`
172+
173+
const isSubordinateEntity = await federationConfig.isSubordinateEntity?.(agentContext, {
174+
verifierId: verifier.verifierId,
175+
issuerEntityId: entityId,
176+
subjectEntityId: sub,
177+
})
178+
if (!isSubordinateEntity) {
179+
if (!federationConfig.isSubordinateEntity) {
180+
agentContext.config.logger.warn(
181+
'isSubordinateEntity hook is not provided for the federation so we cannot check if this entity is a subordinate entity of the issuer',
182+
{
183+
verifierId: verifier.verifierId,
184+
issuerEntityId: entityId,
185+
subjectEntityId: sub,
186+
}
187+
)
188+
}
189+
190+
sendErrorResponse(
191+
response,
192+
next,
193+
agentContext.config.logger,
194+
403,
195+
'forbidden',
196+
'This entity is not a subordinate entity of the issuer'
197+
)
198+
return
199+
}
200+
201+
const jwsService = agentContext.dependencyManager.resolve(JwsService)
202+
203+
const subjectEntityConfiguration = await fetchEntityConfiguration({
204+
entityId: sub,
205+
verifyJwtCallback: async ({ jwt, jwk }) => {
206+
const res = await jwsService.verifyJws(agentContext, {
207+
jws: jwt,
208+
jwkResolver: () => getJwkFromJson(jwk),
209+
})
210+
211+
return res.isValid
212+
},
213+
})
214+
215+
let federationKey = federationKeyMapping.get(verifier.verifierId)
216+
if (!federationKey) {
217+
federationKey = await agentContext.wallet.createKey({
218+
keyType: KeyType.Ed25519,
219+
})
220+
federationKeyMapping.set(verifier.verifierId, federationKey)
221+
}
222+
223+
const jwk = getJwkFromKey(federationKey)
224+
const alg = jwk.supportedSignatureAlgorithms[0]
225+
const kid = federationKey.fingerprint
226+
227+
const entityStatement = await createEntityStatement({
228+
header: {
229+
kid,
230+
alg,
231+
typ: 'entity-statement+jwt',
232+
},
233+
jwk: {
234+
...jwk.toJson(),
235+
kid,
236+
},
237+
claims: {
238+
sub: sub,
239+
iss: entityId,
240+
iat: new Date(),
241+
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
242+
jwks: {
243+
keys: subjectEntityConfiguration.jwks.keys,
244+
},
245+
},
246+
signJwtCallback: ({ toBeSigned }) =>
247+
agentContext.wallet.sign({
248+
data: toBeSigned as Buffer,
249+
key: federationKey,
250+
}),
251+
})
252+
253+
response.writeHead(200, { 'Content-Type': 'application/entity-statement+jwt' }).end(entityStatement)
254+
})
148255
}

0 commit comments

Comments
 (0)