Skip to content

Commit

Permalink
Merge pull request #29 from animo/feature/openid-federation
Browse files Browse the repository at this point in the history
feat: OpenId Federation with multiple layers
  • Loading branch information
Tommylans authored Nov 26, 2024
2 parents 3b9f02c + c8cd156 commit 78d684d
Show file tree
Hide file tree
Showing 15 changed files with 191 additions and 57 deletions.
2 changes: 1 addition & 1 deletion agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"scripts": {
"build": "tsc -p tsconfig.json",
"start": "node dist/server.js",
"dev": "tsx watch -r dotenv/config src/server.ts dotenv_config_path=.env.development"
"dev": "tsx watch -r dotenv/config src/server.ts dotenv_config_path=.env.development dotenv_config_path=.env"
},
"pnpm": {
"overrides": {
Expand Down
12 changes: 12 additions & 0 deletions agent/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { ariesAskar } from '@hyperledger/aries-askar-nodejs'
import { Router } from 'express'
import { AGENT_HOST, AGENT_WALLET_KEY } from './constants'
import { credentialRequestToCredentialMapper, getVerificationSessionForIssuanceSession } from './issuer'
import { verifierTrustChains } from './verifiers'
import { getAuthorityHints, isSubordinateTo } from './verifiers/trustChains'

process.on('unhandledRejection', (reason) => {
console.error('Unhandled rejection', reason)
Expand Down Expand Up @@ -44,6 +46,16 @@ export const agent = new Agent({
openId4VcVerifier: new OpenId4VcVerifierModule({
baseUrl: joinUriParts(AGENT_HOST, ['siop']),
router: openId4VpRouter,
federation: {
async getAuthorityHints(agentContext, { verifierId }) {
return getAuthorityHints(verifierTrustChains, verifierId).map((verifierId) =>
joinUriParts(AGENT_HOST, ['siop', verifierId])
)
},
async isSubordinateEntity(agentContext, options) {
return isSubordinateTo(verifierTrustChains, options.verifierId, options.subjectEntityId).length > 0
},
},
}),
x509: new X509Module({
trustedCertificates: [x509PidIssuerCertificate, x509PidIssuerRootCertificate],
Expand Down
52 changes: 42 additions & 10 deletions agent/src/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,18 +214,45 @@ apiRouter.get('/verifier', async (_, response: Response) => {
})
})

apiRouter.post('/trust-chains', async (request: Request, response: Response) => {
const parseResult = await z
.object({
entityId: z.string(),
trustAnchorEntityIds: z.array(z.string()),
})
.safeParseAsync(request.body)

if (!parseResult.success) {
return response.status(400).json({
error: parseResult.error.message,
details: parseResult.error.issues,
})
}

const { entityId, trustAnchorEntityIds } = parseResult.data

const chains = await agent.modules.openId4VcHolder.resolveOpenIdFederationChains({
entityId,
trustAnchorEntityIds: trustAnchorEntityIds as [string, ...string[]],
})

return response.json(chains)
})

const zCreatePresentationRequestBody = z.object({
requestSignerType: z.enum(['x5c', 'openid-federation']),
presentationDefinitionId: z.string(),
requestScheme: z.string(),
responseMode: z.enum(['direct_post.jwt', 'direct_post']),
})

apiRouter.post('/requests/create', async (request: Request, response: Response) => {
const createPresentationRequestBody = zCreatePresentationRequestBody.parse(request.body)
const { requestSignerType, presentationDefinitionId, requestScheme, responseMode } =
await zCreatePresentationRequestBody.parseAsync(request.body)

const x509Certificate = getX509Certificate()

const definitionId = createPresentationRequestBody.presentationDefinitionId
const definitionId = presentationDefinitionId
const definition = allDefinitions.find((d) => d.id === definitionId)
if (!definition) {
return response.status(404).json({
Expand All @@ -248,12 +275,17 @@ apiRouter.post('/requests/create', async (request: Request, response: Response)
const { authorizationRequest, verificationSession } =
await agent.modules.openId4VcVerifier.createAuthorizationRequest({
verifierId: verifier.verifierId,
requestSigner: {
method: 'x5c',
x5c: [x509Certificate],
// FIXME: remove issuer param from credo as we can infer it from the url
issuer: `${AGENT_HOST}/siop/${verifier.verifierId}/authorize`,
},
requestSigner:
requestSignerType === 'x5c'
? {
method: 'x5c',
x5c: [x509Certificate],
// FIXME: remove issuer param from credo as we can infer it from the url
issuer: `${AGENT_HOST}/siop/${verifier.verifierId}/authorize`,
}
: {
method: 'openid-federation',
},
presentationExchange:
'input_descriptors' in definition
? {
Expand All @@ -266,7 +298,7 @@ apiRouter.post('/requests/create', async (request: Request, response: Response)
query: definition,
}
: undefined,
responseMode: createPresentationRequestBody.responseMode,
responseMode,
})

console.log(authorizationRequest)
Expand All @@ -276,7 +308,7 @@ apiRouter.post('/requests/create', async (request: Request, response: Response)
const presentationDefinition = authorizationRequestJwt.payload.additionalClaims.presentation_definition

return response.json({
authorizationRequestUri: authorizationRequest.replace('openid4vp://', createPresentationRequestBody.requestScheme),
authorizationRequestUri: authorizationRequest.replace('openid4vp://', requestScheme),
verificationSessionId: verificationSession.id,
responseStatus: verificationSession.state,
dcqlQuery: dcqlQuery ? JSON.parse(dcqlQuery as string) : undefined,
Expand Down
2 changes: 1 addition & 1 deletion agent/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ async function run() {
app.use(express.urlencoded())

// Hack for making images available
if (AGENT_HOST.includes('ngrok') || AGENT_HOST.includes('localhost')) {
if (AGENT_HOST.includes('ngrok') || AGENT_HOST.includes('.ts.net') || AGENT_HOST.includes('localhost')) {
console.log(path.join(__dirname, '../../app/public/assets'))
app.use('/assets', express.static(path.join(__dirname, '../../app/public/assets')))
}
Expand Down
22 changes: 20 additions & 2 deletions agent/src/verifiers/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,30 @@
import type { PlaygroundVerifierOptions } from '../verifier'
import { animoVerifier } from './animo'
import { sixtVerifier } from './sixt'
import { kvkVerifier } from './kvk'
import { turboKeysVerifier } from './sixt'
import type { TrustChain } from './trustChains'
import { trustPilotVerifier } from './trustPilot'

export const verifiers = [animoVerifier, sixtVerifier]
export const verifiers = [animoVerifier, turboKeysVerifier, kvkVerifier, trustPilotVerifier]
export const allDefinitions = verifiers.flatMap(
(
v
): Array<
PlaygroundVerifierOptions['presentationRequests'][number] | PlaygroundVerifierOptions['dcqlRequests'][number]
> => [...v.presentationRequests, ...v.dcqlRequests]
)

export const verifierTrustChains = [
{
leaf: turboKeysVerifier.verifierId,
trustAnchor: kvkVerifier.verifierId,
},
{
leaf: turboKeysVerifier.verifierId,
trustAnchor: trustPilotVerifier.verifierId,
},
{
leaf: trustPilotVerifier.verifierId,
trustAnchor: kvkVerifier.verifierId,
},
] as const satisfies Array<TrustChain>
12 changes: 12 additions & 0 deletions agent/src/verifiers/kvk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { AGENT_HOST } from '../constants'
import type { PlaygroundVerifierOptions } from '../verifier'

export const kvkVerifier = {
verifierId: '0193687b-0c27-7b82-a686-ff857dc6bbb3',
clientMetadata: {
logo_uri: `${AGENT_HOST}/assets/verifiers/kvk/verifier.png`,
client_name: 'KVK',
},
presentationRequests: [],
dcqlRequests: [],
} as const satisfies PlaygroundVerifierOptions
6 changes: 3 additions & 3 deletions agent/src/verifiers/sixt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ import {
sdJwtInputDescriptor,
} from './util'

export const sixtVerifier = {
export const turboKeysVerifier = {
verifierId: 'c01ea0f3-34df-41d5-89d1-50ef3d181855',
clientMetadata: {
logo_uri: `${AGENT_HOST}/assets/verifiers/sixt/verifier.png`,
client_name: 'Sixt - Rent a Car',
logo_uri: `${AGENT_HOST}/assets/verifiers/turbokeys/verifier.png`,
client_name: 'TurboKeys - Rent a Car',
},
presentationRequests: [
{
Expand Down
46 changes: 46 additions & 0 deletions agent/src/verifiers/trustChains.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { animoVerifier } from './animo'
import { turboKeysVerifier } from './sixt'

export type EntityId = string

export type TrustChain = {
leaf: EntityId
intermediates?: Array<EntityId>
trustAnchor: EntityId
}

export const flattenTrustChains = (trustChain: TrustChain) => {
return [trustChain.leaf, ...(trustChain.intermediates ?? []), trustChain.trustAnchor]
}

export const isSubordinateTo = (trustChains: Array<TrustChain>, issuer: EntityId, subject: EntityId) => {
// We only want to check one index so if the subject is directly under the issuer
// return chain.indexOf(issuer) + 1 === chain.indexOf(subject)

const subjectVerifierId = subject.split('/').pop()
if (!subjectVerifierId) {
throw new Error('Subject verifier id not found')
}

return trustChains
.map(flattenTrustChains)
.filter((chain) => chain.includes(issuer) && chain.includes(subjectVerifierId))
.flatMap((chain) => {
const indexIssuer = chain.indexOf(issuer)
const indexSubject = chain.indexOf(subjectVerifierId)

// TODO: Not sure if this is correct
return indexIssuer === indexSubject - 1
})
}

export const getAuthorityHints = (trustChains: Array<TrustChain>, entityId: EntityId) => {
return trustChains
.map(flattenTrustChains)
.filter((chain) => chain.includes(entityId))
.flatMap((chain) => {
const index = chain.indexOf(entityId)

return chain[index + 1] ?? []
})
}
12 changes: 12 additions & 0 deletions agent/src/verifiers/trustPilot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { AGENT_HOST } from '../constants'
import type { PlaygroundVerifierOptions } from '../verifier'

export const trustPilotVerifier = {
verifierId: '0193687f-20d8-720a-9139-ed939ba510fa',
clientMetadata: {
logo_uri: `${AGENT_HOST}/assets/verifiers/trustpilot/verifier.webp`,
client_name: 'TrustPilot',
},
presentationRequests: [],
dcqlRequests: [],
} as const satisfies PlaygroundVerifierOptions
50 changes: 34 additions & 16 deletions app/components/VerifyBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Link from 'next/link'
import { type FormEvent, useEffect, useState } from 'react'
import QRCode from 'react-qr-code'
import { CollapsibleSection } from './CollapsibleSection'
import type { CreateRequestOptions, CreateRequestResponse } from './VerifyTab'
import { X509Certificates } from './X509Certificates'
import { HighLight } from './highLight'
import { Alert, AlertDescription, AlertTitle } from './ui/alert'
Expand All @@ -21,22 +22,11 @@ type ResponseStatus = 'RequestCreated' | 'RequestUriRetrieved' | 'ResponseVerifi

type VerifyBlockProps = {
flowName: string
createRequest: ({
presentationDefinitionId,
requestScheme,
responseMode,
}: {
presentationDefinitionId: string
requestScheme: string
responseMode: ResponseMode
}) => Promise<{
verificationSessionId: string
authorizationRequestUri: string
authorizationRequest: Record<string, unknown>
responseStatus: ResponseStatus
}>
createRequest: (options: CreateRequestOptions) => Promise<CreateRequestResponse>
}

type RequestSignerType = CreateRequestOptions['requestSignerType']

export const VerifyBlock: React.FC<VerifyBlockProps> = ({ createRequest, flowName }) => {
const [authorizationRequestUri, setAuthorizationRequestUri] = useState<string>()
const [verificationSessionId, setVerificationSessionId] = useState<string>()
Expand Down Expand Up @@ -69,7 +59,7 @@ export const VerifyBlock: React.FC<VerifyBlockProps> = ({ createRequest, flowNam
const isSuccess = requestStatus?.responseStatus === 'ResponseVerified'
const [presentationDefinitionId, setPresentationDefinitionId] = useState<string>()
const [requestScheme, setRequestScheme] = useState<string>('openid4vp://')

const [requestSignerType, setRequestSignerType] = useState<RequestSignerType>('x5c')
useEffect(() => {
getVerifier().then(setVerifier)
}, [])
Expand Down Expand Up @@ -97,7 +87,12 @@ export const VerifyBlock: React.FC<VerifyBlockProps> = ({ createRequest, flowNam
if (!id) {
throw new Error('No definition')
}
const request = await createRequest({ presentationDefinitionId: id, requestScheme, responseMode })
const request = await createRequest({
presentationDefinitionId: id,
requestScheme,
responseMode,
requestSignerType,
})
setRequestStatus(request)
setVerificationSessionId(request.verificationSessionId)
setAuthorizationRequestUri(request.authorizationRequestUri)
Expand Down Expand Up @@ -142,6 +137,29 @@ export const VerifyBlock: React.FC<VerifyBlockProps> = ({ createRequest, flowNam
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="request-signer-type">Request Signer Type</Label>
<Select
name="request-signer-type"
required
value={requestSignerType}
onValueChange={(value) => setRequestSignerType(value as RequestSignerType)}
>
<SelectTrigger className="w-1/2">
<SelectValue placeholder="Select a request signer type" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem key="x5c" value="x5c">
<pre>x509 Certificate</pre>
</SelectItem>
<SelectItem key="openid-federation" value="openid-federation">
<pre>OpenID Federation</pre>
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="request-scheme">Scheme</Label>
<Input
Expand Down
19 changes: 5 additions & 14 deletions app/components/VerifyTab.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,9 @@
import { createRequest } from '../lib/api'
import { type ResponseMode, VerifyBlock } from './VerifyBlock'
import { VerifyBlock } from './VerifyBlock'

export function VerifyTab() {
const createRequestForVerification = async (options: {
presentationDefinitionId: string
requestScheme: string
responseMode: ResponseMode
}) => {
return await createRequest({
requestScheme: options.requestScheme,
presentationDefinitionId: options.presentationDefinitionId,
responseMode: options.responseMode,
})
}
export type CreateRequestOptions = Parameters<typeof createRequest>[0]
export type CreateRequestResponse = Awaited<ReturnType<typeof createRequest>>

return <VerifyBlock flowName="Verify" createRequest={createRequestForVerification} />
export function VerifyTab() {
return <VerifyBlock flowName="Verify" createRequest={createRequest} />
}
13 changes: 3 additions & 10 deletions app/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,8 @@ export async function receiveOffer(offerUri: string) {
return response.json()
}

export async function createRequest({
presentationDefinitionId,
requestScheme,
responseMode,
}: {
export async function createRequest(data: {
requestSignerType: 'x5c' | 'openid-federation'
presentationDefinitionId: string
requestScheme: string
responseMode: 'direct_post' | 'direct_post.jwt'
Expand All @@ -101,11 +98,7 @@ export async function createRequest({
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
presentationDefinitionId,
requestScheme,
responseMode,
}),
body: JSON.stringify(data),
})

if (!response.ok) {
Expand Down
Binary file added app/public/assets/verifiers/kvk/verifier.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 78d684d

Please sign in to comment.