Skip to content

Commit

Permalink
feat: Siopv2Holder module implementing xstate Siopv2Machine
Browse files Browse the repository at this point in the history
  • Loading branch information
sanderPostma committed Jun 17, 2024
1 parent 2a300f4 commit 7dd0651
Show file tree
Hide file tree
Showing 13 changed files with 603 additions and 414 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { TAgent } from '@veramo/core'
import { ISIOPV2Holder } from '../../src'
import { ISiopv2Holder } from '../../src'

type ConfiguredAgent = TAgent<ISIOPV2Holder>
type ConfiguredAgent = TAgent<ISiopv2Holder>

export default (testContext: { getAgent: () => ConfiguredAgent; setup: () => Promise<boolean>; tearDown: () => Promise<boolean> }): void => {
describe.skip('Event Logger Agent Plugin', (): void => {
Expand Down
7 changes: 3 additions & 4 deletions packages/siopv2-holder/package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"name": "@sphereon/ssi-sdk.oid4vci-holder",
"name": "@sphereon/ssi-sdk.siopv2-holder",
"version": "0.25.0",
"source": "src/index.ts",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"veramo": {
"pluginInterfaces": {
"IOID4VCIHolder": "./src/types/IOID4VCIHolder.ts"
"IOID4VCIHolder": "./src/types/ISiopv2Holder.ts"
}
},
"scripts": {
Expand All @@ -24,7 +24,6 @@
"@sphereon/ssi-types": "workspace:*",
"@sphereon/did-auth-siop": "^0.6.4",
"@veramo/core": "4.2.0",
"@veramo/data-store": "4.2.0",
"@veramo/utils": "4.2.0",
"@types/uuid": "^9.0.6",
"i18n-js": "^3.8.0",
Expand Down Expand Up @@ -57,7 +56,7 @@
"license": "Apache-2.0",
"keywords": [
"Sphereon",
"OID4VCI",
"SIOPv2",
"State Machine"
]
}
77 changes: 77 additions & 0 deletions packages/siopv2-holder/src/agent/IdentifierService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { KeyUse } from '@sphereon/ssi-sdk-ext.did-resolver-jwk'
import { _ExtendedIKey } from '@veramo/utils'
import {
CreateIdentifierArgs,
DID_PREFIX,
GetAuthenticationKeyArgs,
GetIdentifierArgs,
GetOrCreatePrimaryIdentifierArgs,
IdentifierAliasEnum,
IdentifierOpts,
KeyManagementSystemEnum,
SupportedDidMethodEnum,
} from '../types/identifier'
import { IDIDManager, IIdentifier, IKey, IResolver, TAgent } from '@veramo/core'
import { getFirstKeyWithRelation } from '@sphereon/ssi-sdk-ext.did-utils'
import { Siopv2HolderEvent } from '../types/ISiopv2Holder'

export const getIdentifier = async (args: GetIdentifierArgs): Promise<IdentifierOpts> => {
const { keyOpts, context } = args

const identifier =
keyOpts.identifier ??
(await getOrCreatePrimaryIdentifier({
context,
opts: {
method: keyOpts.didMethod,
createOpts: { options: { type: keyOpts.keyType, use: KeyUse.Signature, codecName: keyOpts.codecName } },
},
}))
const key: _ExtendedIKey = await getAuthenticationKey({ identifier, context })
const kid: string = key.meta.verificationMethod.id

return { identifier, key, kid }
}

export const getAuthenticationKey = async (args: GetAuthenticationKeyArgs): Promise<_ExtendedIKey> => {
const { identifier, context } = args
const agentContext = { ...context, agent: context.agent as TAgent<IResolver & IDIDManager> }

return (
(await getFirstKeyWithRelation(identifier, agentContext, 'authentication', false)) ||
((await getFirstKeyWithRelation(identifier, agentContext, 'verificationMethod', true)) as _ExtendedIKey)
)
}

export const getOrCreatePrimaryIdentifier = async (args: GetOrCreatePrimaryIdentifierArgs): Promise<IIdentifier> => {
const { context, opts } = args

const identifiers = (await context.agent.didManagerFind(opts?.method ? { provider: `${DID_PREFIX}:${opts?.method}` } : {})).filter(
(identifier: IIdentifier) =>
opts?.createOpts?.options?.type === undefined || identifier.keys.some((key: IKey) => key.type === opts?.createOpts?.options?.type),
)

if (opts?.method === SupportedDidMethodEnum.DID_KEY) {
const createOpts = opts?.createOpts ?? {}
createOpts.options = { codecName: 'EBSI', type: 'Secp256r1', ...createOpts }
opts.createOpts = createOpts
}
const identifier: IIdentifier = !identifiers || identifiers.length == 0 ? await createIdentifier({ context, opts }) : identifiers[0]

return await context.agent.didManagerGet({ did: identifier.did })
}

export const createIdentifier = async (args: CreateIdentifierArgs): Promise<IIdentifier> => {
const { context, opts } = args

const identifier = await context.agent.didManagerCreate({
kms: opts?.createOpts?.kms ?? KeyManagementSystemEnum.LOCAL,
...(opts?.method && { provider: `${DID_PREFIX}:${opts?.method}` }),
alias: opts?.createOpts?.alias ?? `${IdentifierAliasEnum.PRIMARY}-${opts?.method}-${opts?.createOpts?.options?.type}-${new Date().toUTCString()}`,
options: opts?.createOpts?.options,
})

await context.agent.emit(Siopv2HolderEvent.IDENTIFIER_CREATED, { identifier })

return identifier
}
235 changes: 209 additions & 26 deletions packages/siopv2-holder/src/agent/Siopv2Holder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,70 +3,253 @@
*/
import { IAgentPlugin } from '@veramo/core'
import {
AddIdentityArgs,
CreateConfigArgs,
CreateConfigResult,
GetSiopRequestArgs,
ISiopv2Holder,
Siopv2Machine as Siopv2MachineId,
OnContactIdentityCreatedArgs,
OnCredentialStoredArgs,
OnIdentifierCreatedArgs,
RequiredContext,
Siopv2HolderEvent,
Siopv2HolderOptions,
Siopv2MachineContext,
Siopv2MachineInstanceOpts,
GetSiopRequestArgs,
Siopv2AuthorizationRequestData,
RetrieveContactArgs,
AddIdentityArgs,
SendResponseArgs,
Siopv2AuthorizationRequestData,
Siopv2HolderEvent,
Siopv2HolderOptions,
} from '../types/ISiopv2Holder'
import { Siopv2Machine } from '../machine/Siopv2Machine'
import { Siopv2Machine as Siopv2MachineId } from '../types/machine'

import { Loggers, LogMethod } from '@sphereon/ssi-types'
import { VerifiedAuthorizationRequest } from '@sphereon/did-auth-siop'
import { DidAuthConfig } from '@sphereon/ssi-sdk.data-store'
import { addContactIdentity, createConfig, getSiopRequest, retrieveContact, sendResponse } from './Siopv2HolderService'
import { Loggers, LogMethod, W3CVerifiableCredential } from '@sphereon/ssi-types'
import { v4 as uuidv4 } from 'uuid'
import { OpSession } from '@sphereon/ssi-sdk.siopv2-oid4vp-op-auth'
import { SupportedVersion } from '@sphereon/did-auth-siop'
import { ConnectionType, CorrelationIdentifierType, CredentialRole, IdentityOrigin, NonPersistedIdentity, Party } from '@sphereon/ssi-sdk.data-store'
import { siopSendAuthorizationResponse, translateCorrelationIdToName } from './Siopv2HolderService'
import { Siopv2MachineInstanceOpts } from '../types/machine'

// Exposing the methods here for any REST implementation
export const Siopv2HolderContextMethods: Array<string> = []

const logger = Loggers.DEFAULT.options('sphereon:Siopv2:holder', { methods: [LogMethod.CONSOLE, LogMethod.DEBUG_PKG] }).get('sphereon:Siopv2:holder')

export class Siopv2Holder implements IAgentPlugin {
readonly eventTypes: Array<Siopv2HolderEvent> = []
readonly eventTypes: Array<Siopv2HolderEvent> = [
Siopv2HolderEvent.IDENTIFIER_CREATED,
Siopv2HolderEvent.CONTACT_IDENTITY_CREATED,
Siopv2HolderEvent.CREDENTIAL_STORED,
]

readonly methods: ISiopv2Holder = {
Siopv2HolderGetMachineInterpreter: this.Siopv2HolderGetMachineInterpreter.bind(this),
siopv2HolderGetMachineInterpreter: this.siopv2HolderGetMachineInterpreter.bind(this),
siopv2HolderCreateConfig: this.siopv2HolderCreateConfig.bind(this).bind(this),
siopv2HolderGetSiopRequest: this.siopv2HolderGetSiopRequest.bind(this).bind(this),
siopv2HolderRetrieveContact: this.siopv2HolderRetrieveContact.bind(this).bind(this),
siopv2HolderAddIdentity: this.siopv2HolderAddContactIdentity.bind(this).bind(this),
siopv2HolderSendResponse: this.siopv2HolderSendResponse.bind(this).bind(this),
}

private readonly onContactIdentityCreated?: (args: OnContactIdentityCreatedArgs) => Promise<void>
private readonly onCredentialStored?: (args: OnCredentialStoredArgs) => Promise<void>
private readonly onIdentifierCreated?: (args: OnIdentifierCreatedArgs) => Promise<void>

constructor(options?: Siopv2HolderOptions) {
const {} = options ?? {}
const { onContactIdentityCreated, onCredentialStored, onIdentifierCreated } = options ?? {}

this.onContactIdentityCreated = onContactIdentityCreated
this.onCredentialStored = onCredentialStored
this.onIdentifierCreated = onIdentifierCreated
}

public async onEvent(event: any, context: RequiredContext): Promise<void> {
switch (event.type) {
case Siopv2HolderEvent.CONTACT_IDENTITY_CREATED:
this.onContactIdentityCreated?.(event.data)
break
case Siopv2HolderEvent.CREDENTIAL_STORED:
this.onCredentialStored?.(event.data)
break
case Siopv2HolderEvent.IDENTIFIER_CREATED:
this.onIdentifierCreated?.(event.data)
break
default:
return Promise.reject(Error(`Event type ${event.type} not supported`))
}
}

private async Siopv2HolderGetMachineInterpreter(opts: Siopv2MachineInstanceOpts, context: RequiredContext): Promise<Siopv2MachineId> {
// const { stateNavigationListener, url } = args
private async siopv2HolderGetMachineInterpreter(opts: Siopv2MachineInstanceOpts, context: RequiredContext): Promise<Siopv2MachineId> {
const { stateNavigationListener, url } = opts
const services = {
createConfig: (args: CreateConfigArgs) => createConfig(args),
getSiopRequest: (args: GetSiopRequestArgs) => getSiopRequest(args, context),
retrieveContact: (args: RetrieveContactArgs) => retrieveContact(args, context),
addContactIdentity: (args: AddIdentityArgs) => addContactIdentity(args),
sendResponse: (args: SendResponseArgs) => sendResponse(args, context),
...opts?.servces,
createConfig: (args: CreateConfigArgs) => this.siopv2HolderCreateConfig(args),
getSiopRequest: (args: GetSiopRequestArgs) => this.siopv2HolderGetSiopRequest(args, context),
retrieveContact: (args: RetrieveContactArgs) => this.siopv2HolderRetrieveContact(args, context),
addContactIdentity: (args: AddIdentityArgs) => this.siopv2HolderAddContactIdentity(args, context),
sendResponse: (args: SendResponseArgs) => this.siopv2HolderSendResponse(args, context),
...opts?.services,
}

const Siopv2MachineOpts: Siopv2MachineInstanceOpts = {
const siopv2MachineOpts: Siopv2MachineInstanceOpts = {
url,
stateNavigationListener,
services: {
...services,
...args.services,
...opts.services,
},
}

return Siopv2Machine.newInstance(Siopv2MachineOpts)
return Siopv2Machine.newInstance(siopv2MachineOpts)
}

private async siopv2HolderCreateConfig(args: CreateConfigArgs): Promise<CreateConfigResult> {
const { url } = args

if (!url) {
return Promise.reject(Error('Missing request uri in context'))
}

return {
id: uuidv4(),
// FIXME: Update these values in SSI-SDK. Only the URI (not a redirectURI) would be available at this point
sessionId: uuidv4(),
redirectUrl: url,
}
}

private async siopv2HolderGetSiopRequest(args: GetSiopRequestArgs, context: RequiredContext): Promise<Siopv2AuthorizationRequestData> {
const { agent } = context
const { didAuthConfig } = args

if (args.url === undefined) {
return Promise.reject(Error('Missing request uri in context'))
}

if (didAuthConfig === undefined) {
return Promise.reject(Error('Missing config in context'))
}
const { sessionId, redirectUrl } = didAuthConfig

const session: OpSession = await agent
.siopGetOPSession({ sessionId })
.catch(async () => await agent.siopRegisterSession({ requestJwtOrUri: redirectUrl, sessionId }))

logger.debug(`session: ${JSON.stringify(session.id, null, 2)}`)
const verifiedAuthorizationRequest = await session.getAuthorizationRequest()
logger.debug('Request: ' + JSON.stringify(verifiedAuthorizationRequest, null, 2))
const name = verifiedAuthorizationRequest.registrationMetadataPayload?.client_name
const url =
verifiedAuthorizationRequest.responseURI ??
(args.url.includes('request_uri')
? decodeURIComponent(args.url.split('?request_uri=')[1].trim())
: verifiedAuthorizationRequest.issuer ?? verifiedAuthorizationRequest.registrationMetadataPayload?.client_id)
const uri: URL | undefined = url.includes('://') ? new URL(url) : undefined
const correlationIdName = uri
? translateCorrelationIdToName(uri.hostname)
: verifiedAuthorizationRequest.issuer
? translateCorrelationIdToName(verifiedAuthorizationRequest.issuer.split('://')[1])
: name
const correlationId: string = uri?.hostname ?? correlationIdName
const clientId: string | undefined = await verifiedAuthorizationRequest.authorizationRequest.getMergedProperty<string>('client_id')

return {
issuer: verifiedAuthorizationRequest.issuer,
correlationId,
registrationMetadataPayload: verifiedAuthorizationRequest.registrationMetadataPayload,
uri,
name,
clientId,
presentationDefinitions:
(await verifiedAuthorizationRequest.authorizationRequest.containsResponseType('vp_token')) ||
(verifiedAuthorizationRequest.versions.every((version) => version <= SupportedVersion.JWT_VC_PRESENTATION_PROFILE_v1) &&
verifiedAuthorizationRequest.presentationDefinitions &&
verifiedAuthorizationRequest.presentationDefinitions.length > 0)
? verifiedAuthorizationRequest.presentationDefinitions
: undefined,
}
}

private async siopv2HolderRetrieveContact(args: RetrieveContactArgs, context: RequiredContext): Promise<Party | undefined> {
const { authorizationRequestData } = args
const { agent } = context

if (authorizationRequestData === undefined) {
return Promise.reject(Error('Missing authorization request data in context'))
}

return agent
.getContacts({
filter: [
{
identities: {
identifier: {
correlationId: authorizationRequestData.correlationId,
},
},
},
],
})
.then((contacts: Array<Party>): Party | undefined => (contacts.length === 1 ? contacts[0] : undefined))
}

private async siopv2HolderAddContactIdentity(args: AddIdentityArgs, context: RequiredContext): Promise<void> {
const { agent } = context
const { contact, authorizationRequestData } = args

if (contact === undefined) {
return Promise.reject(Error('Missing contact in context'))
}

if (authorizationRequestData === undefined) {
return Promise.reject(Error('Missing authorization request data in context'))
}

// TODO: Makes sense to move these types of common queries/retrievals to the SIOP auth request object
const clientId: string | undefined = authorizationRequestData.clientId ?? authorizationRequestData.issuer
const correlationId: string | undefined = clientId
? clientId.startsWith('did:')
? clientId
: `${new URL(clientId).protocol}//${new URL(clientId).hostname}`
: undefined

if (correlationId) {
const identity: NonPersistedIdentity = {
alias: correlationId,
origin: IdentityOrigin.EXTERNAL,
roles: [CredentialRole.ISSUER],
identifier: {
type: CorrelationIdentifierType.DID,
correlationId,
},
}
return agent.cmAddIdentity({ contactId: contact.id, identity })
}
}

private async siopv2HolderSendResponse(args: SendResponseArgs, context: RequiredContext): Promise<Response> {
const { didAuthConfig, authorizationRequestData, selectedCredentials } = args

if (didAuthConfig === undefined) {
return Promise.reject(Error('Missing config in context'))
}

if (authorizationRequestData === undefined) {
return Promise.reject(Error('Missing authorization request data in context'))
}

return await siopSendAuthorizationResponse(
ConnectionType.SIOPv2_OpenID4VP,
{
sessionId: didAuthConfig.sessionId,
...(authorizationRequestData.presentationDefinitions !== undefined && {
verifiableCredentialsWithDefinition: [
{
definition: authorizationRequestData.presentationDefinitions[0], // TODO BEFORE PR 0 check, check siop only
credentials: selectedCredentials as Array<W3CVerifiableCredential>,
},
],
}),
},
context,
)
}
}
Loading

0 comments on commit 7dd0651

Please sign in to comment.