Skip to content

Commit

Permalink
feat: Get the authorization URL from a TI using a cloud/service walle…
Browse files Browse the repository at this point in the history
…t when requesting a particular attestation credential
  • Loading branch information
nklomp committed Jun 20, 2024
1 parent 1162eba commit 222c4d4
Show file tree
Hide file tree
Showing 27 changed files with 239 additions and 212 deletions.
4 changes: 2 additions & 2 deletions packages/contact-manager-rest-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
},
"dependencies": {
"@sphereon/ssi-express-support": "workspace:*",
"@sphereon/ssi-sdk-ext.key-manager": "0.21.0",
"@sphereon/ssi-sdk-ext.key-utils": "0.21.0",
"@sphereon/ssi-sdk-ext.key-manager": "0.21.1-next.4",
"@sphereon/ssi-sdk-ext.key-utils": "0.21.1-next.4",
"@sphereon/ssi-sdk.contact-manager": "workspace:*",
"@sphereon/ssi-sdk.core": "workspace:*",
"@sphereon/ssi-sdk.data-store": "workspace:*",
Expand Down
11 changes: 7 additions & 4 deletions packages/ebsi-authorization-client/__tests__/attestation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { createObjects, getConfig } from '@sphereon/ssi-sdk.agent-config'
import { IOID4VCIHolder } from '@sphereon/ssi-sdk.oid4vci-holder'
import { IPresentationExchange } from '@sphereon/ssi-sdk.presentation-exchange'
import { IDidAuthSiopOpAuthenticator } from '@sphereon/ssi-sdk.siopv2-oid4vp-op-auth'
import { IDIDManager, IIdentifier, IKeyManager, MinimalImportableKey, TAgent } from '@veramo/core'
import {IDIDManager, IIdentifier, IKeyManager, IResolver, MinimalImportableKey, TAgent} from '@veramo/core'
// @ts-ignore
import cors from 'cors'

Expand All @@ -16,7 +16,7 @@ import { DataSource } from 'typeorm'
import { ebsiCreateAttestationRequestAuthURL } from '../src/functions'

let dbConnection: Promise<DataSource>
let agent: TAgent<IKeyManager & IDIDManager & IDidAuthSiopOpAuthenticator & IPresentationExchange & IOID4VCIHolder>
let agent: TAgent<IKeyManager & IDIDManager & IDidAuthSiopOpAuthenticator & IPresentationExchange & IOID4VCIHolder & IResolver>
let app: Express | undefined
let server: Server<any, any> | undefined
const port = 3333
Expand Down Expand Up @@ -47,7 +47,7 @@ jest.setTimeout(600000)
const setup = async (): Promise<boolean> => {
const config = await getConfig('packages/ebsi-authorization-client/agent.yml')
const { localAgent, db } = await createObjects(config, { localAgent: '/agent', db: '/dbConnection' })
agent = localAgent as TAgent<IKeyManager & IDIDManager & IDidAuthSiopOpAuthenticator & IPresentationExchange & IOID4VCIHolder>
agent = localAgent as TAgent<IKeyManager & IDIDManager & IDidAuthSiopOpAuthenticator & IPresentationExchange & IOID4VCIHolder & IResolver>
dbConnection = db

app = express()
Expand Down Expand Up @@ -88,7 +88,10 @@ describe('attestation client should', () => {
let identifier: IIdentifier
beforeAll(async (): Promise<void> => {
await setup()
identifier = await agent.didManagerCreate({ provider: 'did:ebsi', options: { secp256k1Key: secp256k1PrivateKey, secp256r1Key: secp256r1PrivateKey } })
identifier = await agent.didManagerCreate({
provider: 'did:ebsi',
options: { secp256k1Key: secp256k1PrivateKey, secp256r1Key: secp256r1PrivateKey },
})
console.log(identifier.did)
})

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { OpenID4VCIClient } from '@sphereon/oid4vci-client'
import { Alg, AuthorizationDetails, CredentialResponse, Jwt } from '@sphereon/oid4vci-common'
import { toJwk } from '@sphereon/ssi-sdk-ext.key-utils'
import { toJwk, JWK as SphereonJWK } from '@sphereon/ssi-sdk-ext.key-utils'
import { IDidAuthSiopOpAuthenticator } from '@sphereon/ssi-sdk.siopv2-oid4vp-op-auth'
import { IDIDManager, IIdentifier, IKeyManager, MinimalImportableKey, TAgent } from '@veramo/core'
import { fetch } from 'cross-fetch'
//@ts-ignore
import express, { Application, NextFunction, Request, Response } from 'express'
import { importJWK, JWK, SignJWT } from 'jose'
import { importJWK, SignJWT, JWK } from 'jose'
import * as http from 'node:http'
import { EbsiEnvironment, IEBSIAuthorizationClient, ScopeByDefinition } from '../../src'

Expand Down Expand Up @@ -60,11 +60,11 @@ export default (testContext: { getAgent: () => ConfiguredAgent; setup: () => Pro
provider: 'did:ebsi',
}

const jwk: JWK = toJwk(secp256r1.privateKeyHex, 'Secp256r1', { isPrivateKey: true })
const jwk: SphereonJWK = toJwk(secp256r1.privateKeyHex, 'Secp256r1', { isPrivateKey: true })
const kid = `${identifier.did}#keys-1`

async function proofOfPossessionCallbackFunction(args: Jwt, kid?: string): Promise<string> {
const importedJwk = await importJWK(jwk)
const importedJwk = await importJWK(jwk as JWK)
return await new SignJWT({ ...args.payload })
.setProtectedHeader({ ...args.header, kid: kid! })
.setIssuer(identifier.did)
Expand Down
4 changes: 2 additions & 2 deletions packages/ebsi-authorization-client/agent.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,15 @@ server:
messagingServiceEndpoint: /messaging

keyManager:
$require: '@veramo/key-manager#KeyManager'
$require: '@sphereon/ssi-sdk-ext.key-manager#SphereonKeyManager'
$args:
- store:
$require: '@veramo/data-store#KeyStore'
$args:
- $ref: /dbConnection
kms:
local:
$require: '@sphereon/ssi-sdk-ext.kms-local#KeyManagementSystem'
$require: '@sphereon/ssi-sdk-ext.kms-local#SphereonKeyManagementSystem'
$args:
- $require: '@veramo/data-store#PrivateKeyStore'
$args:
Expand Down
10 changes: 5 additions & 5 deletions packages/ebsi-authorization-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@
"@sphereon/ssi-sdk.presentation-exchange": "workspace:*",
"@sphereon/ssi-sdk.oid4vci-holder": "workspace:*",
"@sphereon/ssi-sdk.siopv2-oid4vp-op-auth": "workspace:*",
"@sphereon/ssi-sdk-ext.did-utils": "0.21.0",
"@sphereon/ssi-sdk-ext.did-utils": "0.21.1-next.4",
"@sphereon/ssi-sdk.core": "workspace:*",
"@sphereon/ssi-types": "workspace:*",
"@sphereon/ssi-sdk-ext.did-provider-ebsi": "0.21.0",
"@sphereon/ssi-sdk-ext.key-utils": "0.21.0",
"@sphereon/ssi-sdk-ext.did-provider-ebsi": "0.21.1-next.4",
"@sphereon/ssi-sdk-ext.key-utils": "0.21.1-next.4",
"@veramo/core": "4.2.0",
"@veramo/utils": "4.2.0",
"cross-fetch": "^3.1.8",
Expand All @@ -38,8 +38,8 @@
"@sphereon/oid4vci-common": "0.12.0",
"tunnelmole": "^2.2.14",
"@sphereon/ssi-sdk.agent-config": "workspace:*",
"@sphereon/ssi-sdk-ext.key-manager": "0.21.0",
"@sphereon/ssi-sdk-ext.kms-local": "0.21.0",
"@sphereon/ssi-sdk-ext.key-manager": "0.21.1-next.4",
"@sphereon/ssi-sdk-ext.kms-local": "0.21.1-next.4",
"@transmute/json-web-signature": "0.7.0-unstable.81",
"@types/express": "^4.17.21",
"@types/express-serve-static-core": "^4.19.1",
Expand Down
11 changes: 8 additions & 3 deletions packages/ebsi-authorization-client/src/functions/Attestation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,13 @@ export const ebsiCreateAttestationRequestAuthURL = async (
)
}
// This only works of the DID is actually registered, otherwise use our internal KMS; that is why the offline argument is passed in when type is Verifiable Auth to Onboard, as no DID is present at that point yet
const authKey = await getAuthenticationKey({ identifier, offlineWhenNoDIDRegistered: credentialType === 'VerifiableAuthorisationToOnboard', context })
const kid = authKey.meta.jwkThumbprint ?? calculateJwkThumbprintForKey({ key: authKey})
const authKey = await getAuthenticationKey({
identifier,
offlineWhenNoDIDRegistered: credentialType === 'VerifiableAuthorisationToOnboard',
noVerificationMethodFallback: true,
context,
})
const kid = authKey.meta.jwkThumbprint ?? calculateJwkThumbprintForKey({ key: authKey })
const clientId = opts.clientId ?? identifier.did

const vciClient = await OpenID4VCIClient.fromCredentialIssuer({
Expand Down Expand Up @@ -82,7 +87,7 @@ export const ebsiCreateAttestationRequestAuthURL = async (
})

const signCallbacks: ProofOfPossessionCallbacks<never> = requestObjectOpts.signCallbacks ?? {
signCallback: signCallback(vciClient, identifier, context),
signCallback: signCallback(vciClient, idOpts, context),
}
const authUrl = await vciClient.createAuthorizationRequestUrl({
authorizationRequest: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { IOID4VCIHolder } from '@sphereon/ssi-sdk.oid4vci-holder'
import { IPresentationExchange } from '@sphereon/ssi-sdk.presentation-exchange'
import { IAgentContext, IDIDManager, IKeyManager, IPluginMethodMap } from '@veramo/core'
import { IAgentContext, IDIDManager, IKeyManager, IPluginMethodMap, IResolver } from '@veramo/core'
import { Format, PresentationDefinitionV2 } from '@sphereon/pex-models'
import { DiscoveryMetadataPayload, JWK } from '@sphereon/did-auth-siop'
import { IDidAuthSiopOpAuthenticator } from '@sphereon/ssi-sdk.siopv2-oid4vp-op-auth'
Expand Down Expand Up @@ -202,4 +202,6 @@ export type GetOIDProviderMetadataResponse = EBSIOIDMetadata
export type GetOIDProviderJwksResponse = GetOIDProviderJwksSuccessResponse | ExceptionResponse
export type GetPresentationDefinitionResponse = GetPresentationDefinitionSuccessResponse
export type GetAccessTokenResponse = GetAccessTokenSuccessResponse | ExceptionResponse
export type IRequiredContext = IAgentContext<IKeyManager & IDIDManager & IDidAuthSiopOpAuthenticator & IPresentationExchange & IOID4VCIHolder>
export type IRequiredContext = IAgentContext<
IKeyManager & IDIDManager & IResolver & IDidAuthSiopOpAuthenticator & IPresentationExchange & IOID4VCIHolder
>
4 changes: 2 additions & 2 deletions packages/oid4vci-holder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
"dependencies": {
"@sphereon/oid4vci-client": "0.12.0",
"@sphereon/oid4vci-common": "0.12.0",
"@sphereon/ssi-sdk-ext.did-resolver-jwk": "0.21.0",
"@sphereon/ssi-sdk-ext.did-utils": "0.21.0",
"@sphereon/ssi-sdk-ext.did-resolver-jwk": "0.21.1-next.4",
"@sphereon/ssi-sdk-ext.did-utils": "0.21.1-next.4",
"@sphereon/ssi-sdk.contact-manager": "workspace:*",
"@sphereon/ssi-sdk.core": "workspace:*",
"@sphereon/ssi-sdk.data-store": "workspace:*",
Expand Down
28 changes: 21 additions & 7 deletions packages/oid4vci-holder/src/agent/OID4VCIHolder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
NotificationRequest,
ProofOfPossessionCallbacks,
} from '@sphereon/oid4vci-common'
import { IIdentifierOpts } from '@sphereon/ssi-sdk-ext.did-utils'
import {
CorrelationIdentifierType,
CredentialRole,
Expand All @@ -28,7 +29,7 @@ import {
parseDid,
SdJwtDecodedVerifiableCredentialPayload,
} from '@sphereon/ssi-types'
import { CredentialPayload, DIDDocument, IAgentPlugin, IIdentifier, ProofFormat, VerifiableCredential, W3CVerifiableCredential } from '@veramo/core'
import { CredentialPayload, DIDDocument, IAgentPlugin, ProofFormat, VerifiableCredential, W3CVerifiableCredential } from '@veramo/core'
import { computeEntryHash } from '@veramo/utils'
import { decodeJWT, JWTHeader } from 'did-jwt'
import { v4 as uuidv4 } from 'uuid'
Expand All @@ -45,6 +46,7 @@ import {
InitiateOID4VCIArgs,
InitiationData,
IOID4VCIHolder,
IRequiredSignAgentContext,
IssuanceOpts,
MappedCredentialToAccept,
OID4VCIHolderEvent,
Expand Down Expand Up @@ -97,10 +99,21 @@ const logger = Loggers.DEFAULT.options('sphereon:oid4vci:holder', { methods: [Lo
'sphereon:oid4vci:holder',
)

export function signCallback(client: OpenID4VCIClient, identifier: IIdentifier, context: RequiredContext) {
export function signCallback(client: OpenID4VCIClient, idOpts: IIdentifierOpts, context: IRequiredSignAgentContext) {
return (jwt: Jwt, kid?: string) => {
let iss = jwt.payload.iss

if (!kid) {
kid = jwt.header.kid
}
if (!kid) {
kid = idOpts.kid
}
if (kid) {
// sync back to id opts
idOpts.kid = kid
}

if (!jwt.payload.client_id?.startsWith('http') && client.isEBSI()) {
iss = jwt.header.kid?.split('#')[0]
} else if (!iss) {
Expand All @@ -109,10 +122,10 @@ export function signCallback(client: OpenID4VCIClient, identifier: IIdentifier,
if (!iss) {
return Promise.reject(Error(`No issuer could be determined from the JWT ${JSON.stringify(jwt)}`))
}
const header = { ...jwt.header, kid } as Partial<JWTHeader>
const header = { ...jwt.header, ...(kid && { kid }) } as Partial<JWTHeader>
const payload = { ...jwt.payload, ...(iss && { iss }) }
return signJWT({
identifier,
idOpts,
header,
payload,
options: { issuer: iss, expiresIn: jwt.payload.exp, canonicalize: false },
Expand Down Expand Up @@ -436,11 +449,12 @@ export class OID4VCIHolder implements IAgentPlugin {
if (!issuanceOpt) {
return Promise.reject(Error(`Cannot get credential issuance options`))
}
const { identifier, key, kid } = await getIdentifier({ issuanceOpt, context })
const idOpts = await getIdentifier({ issuanceOpt, context })
const { key, kid } = idOpts
const alg: SignatureAlgorithmEnum = await signatureAlgorithmFromKey({ key })

const callbacks: ProofOfPossessionCallbacks<DIDDocument> = {
signCallback: signCallback(client, identifier, context),
signCallback: signCallback(client, idOpts, context),
}

try {
Expand Down Expand Up @@ -655,7 +669,7 @@ export class OID4VCIHolder implements IAgentPlugin {
throw Error(`Could not issue holder credential from the wallet`)
}
logger.log(`Holder ${issuedVC.issuer} issued new credential with id ${issuedVC.id}`, issuedVC)
holderCredential = CredentialMapper.storedCredentialToOriginalFormat(issuedVC)
holderCredential = CredentialMapper.storedCredentialToOriginalFormat(issuedVC as IVerifiableCredential)
persist = event === 'credential_accepted_holder_signed'
}

Expand Down
26 changes: 13 additions & 13 deletions packages/oid4vci-holder/src/agent/OID4VCIHolderService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
OpenId4VCIVersion,
} from '@sphereon/oid4vci-common'
import { KeyUse } from '@sphereon/ssi-sdk-ext.did-resolver-jwk'
import {getFirstKeyWithRelation, toDidDocument} from '@sphereon/ssi-sdk-ext.did-utils'
import { getFirstKeyWithRelation, getKey, getIdentifier as getIdentifierFromOpts, toDidDocument } from '@sphereon/ssi-sdk-ext.did-utils'
import { IBasicCredentialLocaleBranding, IBasicIssuerLocaleBranding } from '@sphereon/ssi-sdk.data-store'
import {
CredentialMapper,
Expand Down Expand Up @@ -263,14 +263,14 @@ export const getIdentifier = async (args: GetIdentifierArgs): Promise<Identifier
}

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

let key: _ExtendedIKey | undefined = undefined
try {
key =
(await getFirstKeyWithRelation(identifier, agentContext, 'authentication', false)) ??
(await getFirstKeyWithRelation(identifier, agentContext, 'verificationMethod', false))
(noVerificationMethodFallback ? undefined : await getFirstKeyWithRelation(identifier, agentContext, 'verificationMethod', false))
} catch (e) {
if (!e.message.includes('404') || !offlineWhenNoDIDRegistered) {
throw e
Expand All @@ -279,12 +279,12 @@ export const getAuthenticationKey = async (args: GetAuthenticationKeyArgs): Prom
if (!key && offlineWhenNoDIDRegistered) {
const offlineDID = toDidDocument(identifier)
key =
(await getFirstKeyWithRelation(identifier, agentContext, 'authentication', false, offlineDID)) ??
(await getFirstKeyWithRelation(identifier, agentContext, 'verificationMethod', false, offlineDID))
(await getFirstKeyWithRelation(identifier, agentContext, 'authentication', false, offlineDID)) ??
(noVerificationMethodFallback ? undefined : await getFirstKeyWithRelation(identifier, agentContext, 'verificationMethod', false, offlineDID))
if (!key) {
key = identifier.keys
.map((key) => key as _ExtendedIKey)
.find((key) => key.meta.verificationMethod?.type.includes('authentication') || key.meta.purposes?.includes('authentication'))
.map((key) => key as _ExtendedIKey)
.find((key) => key.meta.verificationMethod?.type.includes('authentication') || key.meta.purposes?.includes('authentication'))
}
}
if (!key) {
Expand Down Expand Up @@ -550,20 +550,20 @@ export const signatureAlgorithmFromKey = async (args: SignatureAlgorithmFromKeyA
}

export const signJWT = async (args: SignJwtArgs): Promise<string> => {
const { identifier, header, payload, context, options } = args
const { idOpts, header, payload, context, options } = args
const jwtOptions = {
...options,
signer: await getSigner({ identifier, context }),
signer: await getSigner({ idOpts, context }),
}

return createJWT(payload, jwtOptions, header)
}

export const getSigner = async (args: GetSignerArgs): Promise<Signer> => {
const { identifier, context } = args
// TODO currently we assume an identifier only has one key
const key = identifier.keys[0]
// TODO See if this is mandatory for a correct JWT
const { idOpts, context } = args

const identifier = await getIdentifierFromOpts(idOpts, context)
const key = await getKey(identifier, idOpts.verificationMethodSection, context, idOpts.kid)
const algorithm = await signatureAlgorithmFromKey({ key })

return async (data: string | Uint8Array): Promise<string> => {
Expand Down
15 changes: 9 additions & 6 deletions packages/oid4vci-holder/src/types/IOID4VCIHolder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
ExperimentalSubjectIssuance,
NotificationRequest,
} from '@sphereon/oid4vci-common'
import { IIdentifierOpts } from '@sphereon/ssi-sdk-ext.did-utils'
import { IContactManager } from '@sphereon/ssi-sdk.contact-manager'
import { IBasicCredentialLocaleBranding, IBasicIssuerLocaleBranding, Identity, Party } from '@sphereon/ssi-sdk.data-store'
import { IIssuanceBranding } from '@sphereon/ssi-sdk.issuance-branding'
Expand Down Expand Up @@ -420,7 +421,8 @@ export type GetIdentifierArgs = {
export type GetAuthenticationKeyArgs = {
identifier: IIdentifier
offlineWhenNoDIDRegistered?: boolean
context: RequiredContext
noVerificationMethodFallback?: boolean
context: IAgentContext<IResolver & IDIDManager & IKeyManager>
}

export type GetOrCreatePrimaryIdentifierArgs = {
Expand Down Expand Up @@ -479,17 +481,18 @@ export type KeyTypeFromCryptographicSuiteArgs = {
suite: string
}

export type IRequiredSignAgentContext = IAgentContext<IKeyManager & IDIDManager & IResolver>
export type SignJwtArgs = {
identifier: IIdentifier
idOpts: IIdentifierOpts
header: Partial<JWTHeader>
payload: Partial<JWTPayload>
options: { issuer: string; expiresIn?: number; canonicalize?: boolean }
context: RequiredContext
context: IRequiredSignAgentContext
}

export type GetSignerArgs = {
identifier: IIdentifier
context: RequiredContext
idOpts: IIdentifierOpts
context: IRequiredSignAgentContext
}

export type GetCredentialArgs = {
Expand Down Expand Up @@ -530,5 +533,5 @@ export type IdentifierOpts = {
}

export type RequiredContext = IAgentContext<
IIssuanceBranding | IContactManager | ICredentialVerifier | ICredentialIssuer | IDataStore | IDataStoreORM | IDIDManager | IResolver | IKeyManager
IIssuanceBranding & IContactManager & ICredentialVerifier & ICredentialIssuer & IDataStore & IDataStoreORM & IDIDManager & IResolver & IKeyManager
>
8 changes: 4 additions & 4 deletions packages/oid4vci-issuer-rest-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@
"@sphereon/did-uni-client": "^0.6.3",
"@sphereon/pex": "3.3.3",
"@sphereon/pex-models": "^2.2.4",
"@sphereon/ssi-sdk-ext.did-provider-jwk": "0.21.0",
"@sphereon/ssi-sdk-ext.key-manager": "0.21.0",
"@sphereon/ssi-sdk-ext.key-utils": "0.21.0",
"@sphereon/ssi-sdk-ext.kms-local": "0.21.0",
"@sphereon/ssi-sdk-ext.did-provider-jwk": "0.21.1-next.4",
"@sphereon/ssi-sdk-ext.key-manager": "0.21.1-next.4",
"@sphereon/ssi-sdk-ext.key-utils": "0.21.1-next.4",
"@sphereon/ssi-sdk-ext.kms-local": "0.21.1-next.4",
"@sphereon/ssi-sdk.data-store": "workspace:*",
"@sphereon/ssi-sdk.vc-handler-ld-local": "workspace:*",
"@types/body-parser": "^1.19.2",
Expand Down
Loading

0 comments on commit 222c4d4

Please sign in to comment.