@sphereon/ssi-sdk.siopv2-oid4vp-op-auth 0.34.1-feat.SSISDK.35.64 → 0.34.1-feature.DIIPv4.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,38 +1,31 @@
1
- import {
2
- AuthorizationRequest,
3
- Json,
4
- SupportedVersion
5
- } from '@sphereon/did-auth-siop'
1
+ import { AuthorizationRequest, SupportedVersion } from '@sphereon/did-auth-siop'
2
+ import { IPresentationDefinition, PEX } from '@sphereon/pex'
3
+ import { InputDescriptorV1, InputDescriptorV2, PresentationDefinitionV1, PresentationDefinitionV2 } from '@sphereon/pex-models'
6
4
  import { isOID4VCIssuerIdentifier, ManagedIdentifierOptsOrResult } from '@sphereon/ssi-sdk-ext.identifier-resolution'
7
5
  import { UniqueDigitalCredential, verifiableCredentialForRoleFilter } from '@sphereon/ssi-sdk.credential-store'
8
6
  import { ConnectionType, CredentialRole } from '@sphereon/ssi-sdk.data-store'
7
+ import { CredentialMapper, HasherSync, Loggers, OriginalVerifiableCredential, PresentationSubmission } from '@sphereon/ssi-types'
8
+ import { OID4VP, OpSession } from '../session'
9
9
  import {
10
- CredentialMapper,
11
- HasherSync,
12
- Loggers,
13
- OriginalVerifiableCredential,
14
- SdJwtDecodedVerifiableCredential
15
- } from '@sphereon/ssi-types'
16
- import { OpSession } from '../session'
17
- import {
10
+ DidAgents,
18
11
  LOGGER_NAMESPACE,
19
12
  RequiredContext,
20
13
  SelectableCredential,
21
14
  SelectableCredentialsMap,
22
- Siopv2HolderEvent
15
+ Siopv2HolderEvent,
16
+ SuitableCredentialAgents,
17
+ VerifiableCredentialsWithDefinition,
18
+ VerifiablePresentationWithDefinition,
23
19
  } from '../types'
24
- import { encodeJoseBlob } from '@sphereon/ssi-sdk.core'
25
- import { DcqlPresentation, DcqlQuery } from 'dcql'
26
- import { convertToDcqlCredentials } from '../utils/dcql'
27
20
  import { IAgentContext, IDIDManager } from '@veramo/core'
28
- import {
29
- getOrCreatePrimaryIdentifier,
30
- SupportedDidMethodEnum
31
- } from '@sphereon/ssi-sdk-ext.did-utils'
21
+ import { getOrCreatePrimaryIdentifier, SupportedDidMethodEnum } from '@sphereon/ssi-sdk-ext.did-utils'
22
+ import { defaultHasher, encodeJoseBlob } from '@sphereon/ssi-sdk.core'
23
+ import { DcqlCredential, DcqlCredentialPresentation, DcqlPresentation, DcqlQuery } from 'dcql'
24
+ import { convertToDcqlCredentials } from '../utils/dcql'
25
+ import { getOriginalVerifiableCredential } from '../utils/CredentialUtils'
32
26
 
33
27
  export const logger = Loggers.DEFAULT.get(LOGGER_NAMESPACE)
34
28
 
35
- // @ts-ignore
36
29
  const createEbsiIdentifier = async (agentContext: IAgentContext<IDIDManager>): Promise<ManagedIdentifierOptsOrResult> => {
37
30
  logger.log(`No EBSI key present yet. Creating a new one...`)
38
31
  const { result: newIdentifier, created } = await getOrCreatePrimaryIdentifier(agentContext, {
@@ -46,10 +39,9 @@ const createEbsiIdentifier = async (agentContext: IAgentContext<IDIDManager>): P
46
39
  return await agentContext.agent.identifierManagedGetByDid({ identifier: newIdentifier.did })
47
40
  }
48
41
 
49
- // @ts-ignore
50
42
  const hasEbsiClient = async (authorizationRequest: AuthorizationRequest) => {
51
- const clientId = authorizationRequest.getMergedProperty<string>('client_id')
52
- const redirectUri = authorizationRequest.getMergedProperty<string>('redirect_uri')
43
+ const clientId = await authorizationRequest.getMergedProperty<string>('client_id')
44
+ const redirectUri = await authorizationRequest.getMergedProperty<string>('redirect_uri')
53
45
  return clientId?.toLowerCase().includes('.ebsi.eu') || redirectUri?.toLowerCase().includes('.ebsi.eu')
54
46
  }
55
47
 
@@ -57,25 +49,137 @@ export const siopSendAuthorizationResponse = async (
57
49
  connectionType: ConnectionType,
58
50
  args: {
59
51
  sessionId: string
60
- credentials: Array<UniqueDigitalCredential | OriginalVerifiableCredential>
52
+ verifiableCredentialsWithDefinition?: VerifiableCredentialsWithDefinition[]
61
53
  idOpts?: ManagedIdentifierOptsOrResult
62
54
  isFirstParty?: boolean
63
55
  hasher?: HasherSync
56
+ dcqlQuery?: DcqlQuery
64
57
  },
65
58
  context: RequiredContext,
66
59
  ) => {
67
60
  const { agent } = context
68
- const { credentials } = args
61
+ const agentContext = { ...context, agent: context.agent as DidAgents }
62
+ let { idOpts, isFirstParty, hasher = defaultHasher } = args
63
+
69
64
  if (connectionType !== ConnectionType.SIOPv2_OpenID4VP) {
70
65
  return Promise.reject(Error(`No supported authentication provider for type: ${connectionType}`))
71
66
  }
72
-
73
67
  const session: OpSession = await agent.siopGetOPSession({ sessionId: args.sessionId })
74
68
  const request = await session.getAuthorizationRequest()
75
- const aud = request.authorizationRequest.getMergedProperty<string>('aud')
69
+ const aud = await request.authorizationRequest.getMergedProperty<string>('aud')
76
70
  logger.debug(`AUD: ${aud}`)
77
71
  logger.debug(JSON.stringify(request.authorizationRequest))
78
72
 
73
+ let presentationsAndDefs: VerifiablePresentationWithDefinition[] | undefined
74
+ let presentationSubmission: PresentationSubmission | undefined
75
+ if (await session.hasPresentationDefinitions()) {
76
+ const oid4vp: OID4VP = await session.getOID4VP({ hasher })
77
+
78
+ const credentialsAndDefinitions = args.verifiableCredentialsWithDefinition
79
+ ? args.verifiableCredentialsWithDefinition
80
+ : await oid4vp.filterCredentialsAgainstAllDefinitions(CredentialRole.HOLDER)
81
+ const domain =
82
+ ((await request.authorizationRequest.getMergedProperty('client_id')) as string) ??
83
+ request.issuer ??
84
+ (request.versions.includes(SupportedVersion.JWT_VC_PRESENTATION_PROFILE_v1)
85
+ ? 'https://self-issued.me/v2/openid-vc'
86
+ : 'https://self-issued.me/v2')
87
+ logger.log(`NONCE: ${session.nonce}, domain: ${domain}`)
88
+
89
+ const firstUniqueDC = credentialsAndDefinitions[0].credentials[0]
90
+ if (typeof firstUniqueDC !== 'object' || !('digitalCredential' in firstUniqueDC)) {
91
+ return Promise.reject(Error('SiopMachine only supports UniqueDigitalCredentials for now'))
92
+ }
93
+
94
+ let identifier: ManagedIdentifierOptsOrResult
95
+ const digitalCredential = firstUniqueDC.digitalCredential
96
+ const firstVC = firstUniqueDC.uniformVerifiableCredential
97
+ const holder = CredentialMapper.isSdJwtDecodedCredential(firstVC)
98
+ ? firstVC.decodedPayload.cnf?.jwk
99
+ ? //TODO SDK-19: convert the JWK to hex and search for the appropriate key and associated DID
100
+ //doesn't apply to did:jwk only, as you can represent any DID key as a JWK. So whenever you encounter a JWK it doesn't mean it had to come from a did:jwk in the system. It just can always be represented as a did:jwk
101
+ `did:jwk:${encodeJoseBlob(firstVC.decodedPayload.cnf?.jwk)}#0`
102
+ : firstVC.decodedPayload.sub
103
+ : Array.isArray(firstVC.credentialSubject)
104
+ ? firstVC.credentialSubject[0].id
105
+ : firstVC.credentialSubject.id
106
+ if (!digitalCredential.kmsKeyRef) {
107
+ // In case the store does not have the kmsKeyRef lets search for the holder
108
+
109
+ if (!holder) {
110
+ return Promise.reject(`No holder found and no kmsKeyRef in DB. Cannot determine identifier to use`)
111
+ }
112
+ try {
113
+ identifier = await session.context.agent.identifierManagedGet({ identifier: holder })
114
+ } catch (e) {
115
+ logger.debug(`Holder DID not found: ${holder}`)
116
+ throw e
117
+ }
118
+ } else if (isOID4VCIssuerIdentifier(digitalCredential.kmsKeyRef)) {
119
+ identifier = await session.context.agent.identifierManagedGetByOID4VCIssuer({
120
+ identifier: firstUniqueDC.digitalCredential.kmsKeyRef,
121
+ })
122
+ } else {
123
+ switch (digitalCredential.subjectCorrelationType) {
124
+ case 'DID':
125
+ identifier = await session.context.agent.identifierManagedGetByDid({
126
+ identifier: digitalCredential.subjectCorrelationId ?? holder,
127
+ kmsKeyRef: digitalCredential.kmsKeyRef,
128
+ })
129
+ break
130
+ // TODO other implementations?
131
+ default:
132
+ if (digitalCredential.subjectCorrelationId?.startsWith('did:') || holder?.startsWith('did:')) {
133
+ identifier = await session.context.agent.identifierManagedGetByDid({
134
+ identifier: digitalCredential.subjectCorrelationId ?? holder,
135
+ kmsKeyRef: digitalCredential.kmsKeyRef,
136
+ })
137
+ } else {
138
+ // Since we are using the kmsKeyRef we will find the KID regardless of the identifier. We set it for later access though
139
+ identifier = await session.context.agent.identifierManagedGetByKid({
140
+ identifier: digitalCredential.subjectCorrelationId ?? holder ?? digitalCredential.kmsKeyRef,
141
+ kmsKeyRef: digitalCredential.kmsKeyRef,
142
+ })
143
+ }
144
+ }
145
+ }
146
+
147
+ if (identifier === undefined && idOpts !== undefined && (await hasEbsiClient(request.authorizationRequest))) {
148
+ identifier = await createEbsiIdentifier(agentContext)
149
+ }
150
+ logger.debug(`Identifier`, identifier)
151
+
152
+ // TODO Add mdoc support
153
+
154
+ presentationsAndDefs = await oid4vp.createVerifiablePresentations(CredentialRole.HOLDER, credentialsAndDefinitions, {
155
+ idOpts: identifier,
156
+ proofOpts: {
157
+ nonce: session.nonce,
158
+ domain,
159
+ },
160
+ })
161
+ if (!presentationsAndDefs || presentationsAndDefs.length === 0) {
162
+ throw Error('No verifiable presentations could be created')
163
+ } else if (presentationsAndDefs.length > 1) {
164
+ throw Error(`Only one verifiable presentation supported for now. Got ${presentationsAndDefs.length}`)
165
+ }
166
+
167
+ idOpts = presentationsAndDefs[0].idOpts
168
+ presentationSubmission = presentationsAndDefs[0].presentationSubmission
169
+
170
+ logger.log(`Definitions and locations:`, JSON.stringify(presentationsAndDefs?.[0]?.verifiablePresentations, null, 2))
171
+ logger.log(`Presentation Submission:`, JSON.stringify(presentationSubmission, null, 2))
172
+ const mergedVerifiablePresentations = presentationsAndDefs?.flatMap((pd) => pd.verifiablePresentations) || []
173
+ return await session.sendAuthorizationResponse({
174
+ ...(presentationsAndDefs && { verifiablePresentations: mergedVerifiablePresentations }),
175
+ ...(presentationSubmission && { presentationSubmission }),
176
+ // todo: Change issuer value in case we do not use identifier. Use key.meta.jwkThumbprint then
177
+ responseSignerOpts: idOpts!,
178
+ isFirstParty,
179
+ })
180
+ } else if (request.dcqlQuery) {
181
+ if (args.verifiableCredentialsWithDefinition !== undefined && args.verifiableCredentialsWithDefinition !== null) {
182
+ const vcs = args.verifiableCredentialsWithDefinition.flatMap((vcd) => vcd.credentials)
79
183
  const domain =
80
184
  ((await request.authorizationRequest.getMergedProperty('client_id')) as string) ??
81
185
  request.issuer ??
@@ -84,7 +188,7 @@ export const siopSendAuthorizationResponse = async (
84
188
  : 'https://self-issued.me/v2')
85
189
  logger.debug(`NONCE: ${session.nonce}, domain: ${domain}`)
86
190
 
87
- const firstUniqueDC = credentials[0]
191
+ const firstUniqueDC = vcs[0]
88
192
  if (typeof firstUniqueDC !== 'object' || !('digitalCredential' in firstUniqueDC)) {
89
193
  return Promise.reject(Error('SiopMachine only supports UniqueDigitalCredentials for now'))
90
194
  }
@@ -134,102 +238,104 @@ export const siopSendAuthorizationResponse = async (
134
238
  })
135
239
  }
136
240
  }
241
+ console.log(`Identifier`, identifier)
137
242
 
138
- const dcqlCredentialsWithCredentials = new Map(
139
- credentials.map((vc) => [convertToDcqlCredentials(vc), vc])
140
- )
141
-
142
- const queryResult = DcqlQuery.query(request.dcqlQuery, Array.from(dcqlCredentialsWithCredentials.keys()))
243
+ const dcqlRepresentations: DcqlCredential[] = []
244
+ vcs.forEach((vc: UniqueDigitalCredential | OriginalVerifiableCredential) => {
245
+ const rep = convertToDcqlCredentials(vc, args.hasher)
246
+ if (rep) {
247
+ dcqlRepresentations.push(rep)
248
+ }
249
+ })
143
250
 
144
- if (!queryResult.can_be_satisfied) {
145
- return Promise.reject(Error('Credentials do not match required query request'))
146
- }
251
+ const queryResult = DcqlQuery.query(request.dcqlQuery, dcqlRepresentations)
252
+ const presentation: Record<string, DcqlCredentialPresentation> = {}
147
253
 
148
- const presentation: DcqlPresentation.Output = {}
149
- const uniqueCredentials = Array.from(dcqlCredentialsWithCredentials.values())
150
- for (const [key, value] of Object.entries(queryResult.credential_matches)) {
151
- if (value.success) {
152
- const matchedCredentials = value.valid_credentials.map(cred => uniqueCredentials[cred.input_credential_index])
153
- const vc = matchedCredentials[0] // taking the first match for now //uniqueCredentials[value.input_credential_index]
154
- if (!vc) {
155
- continue
156
- }
157
- const originalVc = retrieveEncodedCredential(vc as UniqueDigitalCredential) // TODO this is not nice // also always a UniqueDigitalCredential
158
- if (!originalVc) {
159
- continue
160
- }
161
- if (originalVc) {
162
- presentation[key] = originalVc as | string | { [x: string]: Json }
254
+ for (const [key, value] of Object.entries(queryResult.credential_matches)) {
255
+ const allMatches = Array.isArray(value) ? value : [value]
256
+ allMatches.forEach((match) => {
257
+ if (match.success) {
258
+ const originalCredential = getOriginalVerifiableCredential(vcs[match.input_credential_index])
259
+ if (!originalCredential) {
260
+ throw new Error(`Index ${match.input_credential_index} out of range in credentials array`)
261
+ }
262
+ presentation[key] =
263
+ (originalCredential as any)['compactSdJwtVc'] !== undefined ? (originalCredential as any).compactSdJwtVc : originalCredential
264
+ }
265
+ })
163
266
  }
164
- }
165
- }
166
-
167
- const dcqlPresentation = DcqlPresentation.parse(presentation)
168
267
 
169
268
  const response = session.sendAuthorizationResponse({
170
269
  responseSignerOpts: identifier,
171
- dcqlResponse: {
172
- dcqlPresentation
173
- }
270
+ ...{ dcqlQuery: { dcqlPresentation: DcqlPresentation.parse(presentation) } },
174
271
  })
175
272
 
176
273
  logger.debug(`Response: `, response)
274
+
177
275
  return response
276
+ }
277
+ }
278
+ throw Error('Presentation Definition or DCQL is required')
178
279
  }
179
280
 
180
- const retrieveEncodedCredential = (credential: UniqueDigitalCredential): OriginalVerifiableCredential | undefined => {
181
- return credential.originalVerifiableCredential !== undefined &&
182
- credential.originalVerifiableCredential !== null &&
183
- (credential?.originalVerifiableCredential as SdJwtDecodedVerifiableCredential)?.compactSdJwtVc !== undefined &&
184
- (credential?.originalVerifiableCredential as SdJwtDecodedVerifiableCredential)?.compactSdJwtVc !== null
185
- ? (credential.originalVerifiableCredential as SdJwtDecodedVerifiableCredential).compactSdJwtVc
186
- : credential.originalVerifiableCredential
281
+ function buildPartialPD(
282
+ inputDescriptor: InputDescriptorV1 | InputDescriptorV2,
283
+ presentationDefinition: PresentationDefinitionV1 | PresentationDefinitionV2,
284
+ ): IPresentationDefinition {
285
+ return {
286
+ ...presentationDefinition,
287
+ input_descriptors: [inputDescriptor],
288
+ } as IPresentationDefinition
187
289
  }
188
290
 
189
291
  export const getSelectableCredentials = async (
190
- dcqlQuery: DcqlQuery,
292
+ presentationDefinition: IPresentationDefinition,
191
293
  context: RequiredContext,
192
294
  ): Promise<SelectableCredentialsMap> => {
193
- const agentContext = { ...context, agent: context.agent }
295
+ const agentContext = { ...context, agent: context.agent as SuitableCredentialAgents }
194
296
  const { agent } = agentContext
297
+ const pex = new PEX()
298
+
195
299
  const uniqueVerifiableCredentials = await agent.crsGetUniqueCredentials({
196
300
  filter: verifiableCredentialForRoleFilter(CredentialRole.HOLDER),
197
301
  })
198
- const branding = await agent.ibGetCredentialBranding()
199
- const dcqlCredentialsWithCredentials = new Map(
200
- uniqueVerifiableCredentials.map((vc) => [convertToDcqlCredentials(vc), vc])
201
- )
202
- const queryResult = DcqlQuery.query(dcqlQuery, Array.from(dcqlCredentialsWithCredentials.keys()))
203
- const uniqueCredentials = Array.from(dcqlCredentialsWithCredentials.values())
302
+ const credentialBranding = await agent.ibGetCredentialBranding()
303
+
204
304
  const selectableCredentialsMap: SelectableCredentialsMap = new Map()
205
305
 
206
- for (const [key, value] of Object.entries(queryResult.credential_matches)) {
207
- if (!value.valid_credentials) {
208
- continue
209
- }
306
+ for (const inputDescriptor of presentationDefinition.input_descriptors) {
307
+ const partialPD = buildPartialPD(inputDescriptor, presentationDefinition)
308
+ const originalCredentials = uniqueVerifiableCredentials.map((uniqueVC) => {
309
+ return CredentialMapper.storedCredentialToOriginalFormat(uniqueVC.originalVerifiableCredential!) // ( ! is valid for verifiableCredentialForRoleFilter )
310
+ })
311
+ const selectionResults = pex.selectFrom(partialPD, originalCredentials)
210
312
 
211
- const mapSelectableCredentialPromises = value.valid_credentials.map(async cred => {
212
- const matchedCredential = uniqueCredentials[cred.input_credential_index]
213
- const credentialBranding = branding.filter((cb) => cb.vcHash === matchedCredential.hash)
214
- const issuerPartyIdentity = await agent.cmGetContacts({
215
- filter: [{ identities: { identifier: { correlationId: matchedCredential.uniformVerifiableCredential!.issuerDid } } }],
216
- })
217
- const subjectPartyIdentity = await agent.cmGetContacts({
218
- filter: [{ identities: { identifier: { correlationId: matchedCredential.uniformVerifiableCredential!.subjectDid } } }],
313
+ const selectableCredentials: Array<SelectableCredential> = []
314
+ for (const selectedCredential of selectionResults.verifiableCredential || []) {
315
+ const filteredUniqueVC = uniqueVerifiableCredentials.find((uniqueVC) => {
316
+ const proof = uniqueVC.uniformVerifiableCredential!.proof
317
+ return Array.isArray(proof) ? proof.some((proofItem) => proofItem.jwt === selectedCredential) : proof.jwt === selectedCredential
219
318
  })
220
319
 
221
- return {
222
- credential: matchedCredential,
223
- credentialBranding: credentialBranding[0]?.localeBranding,
224
- issuerParty: issuerPartyIdentity?.[0],
225
- subjectParty: subjectPartyIdentity?.[0],
226
- }
227
- })
320
+ if (filteredUniqueVC) {
321
+ const filteredCredentialBrandings = credentialBranding.filter((cb) => cb.vcHash === filteredUniqueVC.hash)
322
+ const issuerPartyIdentity = await agent.cmGetContacts({
323
+ filter: [{ identities: { identifier: { correlationId: filteredUniqueVC.uniformVerifiableCredential!.issuerDid } } }],
324
+ })
325
+ const subjectPartyIdentity = await agent.cmGetContacts({
326
+ filter: [{ identities: { identifier: { correlationId: filteredUniqueVC.uniformVerifiableCredential!.subjectDid } } }],
327
+ })
228
328
 
229
- const selectableCredentials: Array<SelectableCredential> = await Promise.all(mapSelectableCredentialPromises)
230
- selectableCredentialsMap.set(key, selectableCredentials)
329
+ selectableCredentials.push({
330
+ credential: filteredUniqueVC,
331
+ credentialBranding: filteredCredentialBrandings[0]?.localeBranding,
332
+ issuerParty: issuerPartyIdentity?.[0],
333
+ subjectParty: subjectPartyIdentity?.[0],
334
+ })
335
+ }
336
+ }
337
+ selectableCredentialsMap.set(inputDescriptor.id, selectableCredentials)
231
338
  }
232
-
233
339
  return selectableCredentialsMap
234
340
  }
235
341