@sphereon/ssi-sdk-ext.jwt-service 0.24.1-unstable.93 → 0.25.1-feature.OIDF.69.39

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,45 +1,70 @@
1
+ import { jwkTtoPublicKeyHex } from '@sphereon/ssi-sdk-ext.did-utils'
1
2
  import {
3
+ ensureManagedIdentifierResult,
4
+ ExternalIdentifierDidOpts,
5
+ ExternalIdentifierX5cOpts,
6
+ IIdentifierResolution,
2
7
  isManagedIdentifierDidResult,
3
8
  isManagedIdentifierX5cResult,
4
9
  ManagedIdentifierMethod,
5
10
  ManagedIdentifierResult,
6
- ensureManagedIdentifierResult,
11
+ resolveExternalJwkIdentifier,
7
12
  } from '@sphereon/ssi-sdk-ext.identifier-resolution'
8
- import { bytesToBase64url, encodeJoseBlob } from '@veramo/utils'
13
+ import { keyTypeFromCryptographicSuite, verifySignatureWithSubtle } from '@sphereon/ssi-sdk-ext.key-utils'
14
+ import { contextHasPlugin } from '@sphereon/ssi-sdk.agent-config'
15
+ import { JWK } from '@sphereon/ssi-types'
16
+ import { IAgentContext } from '@veramo/core'
17
+ import { bytesToBase64url, decodeJoseBlob, encodeJoseBlob } from '@veramo/utils'
18
+ import { base64ToBytes } from '@veramo/utils'
9
19
  import * as u8a from 'uint8arrays'
10
20
  import {
11
21
  CreateJwsCompactArgs,
12
22
  CreateJwsFlattenedArgs,
13
23
  CreateJwsJsonArgs,
14
- CreateJwsMode,
24
+ IJwsValidationResult,
15
25
  IRequiredContext,
26
+ isJwsCompact,
27
+ isJwsJsonFlattened,
28
+ isJwsJsonGeneral,
29
+ Jws,
16
30
  JwsCompact,
31
+ JwsIdentifierMode,
17
32
  JwsJsonFlattened,
18
33
  JwsJsonGeneral,
19
- JwsJsonSignature,
20
- JwtHeader,
34
+ JwsJsonGeneralWithIdentifiers,
35
+ JwsJsonSignature, JwsJsonSignatureWithIdentifier,
36
+ JwsHeader,
37
+ JwsPayload,
21
38
  PreparedJwsObject,
39
+ VerifyJwsArgs, JweHeader,
22
40
  } from '../types/IJwtService'
23
41
 
42
+ const payloadToBytes = (payload: string | JwsPayload | Uint8Array): Uint8Array => {
43
+ const isBytes = payload instanceof Uint8Array
44
+ const isString = typeof payload === 'string'
45
+ return isBytes ? payload : isString ? u8a.fromString(payload, 'base64url') : u8a.fromString(JSON.stringify(payload), 'utf-8')
46
+ }
47
+
24
48
  export const prepareJwsObject = async (args: CreateJwsJsonArgs, context: IRequiredContext): Promise<PreparedJwsObject> => {
25
- const { existingSignatures, protectedHeader, unprotectedHeader, issuer, payload, mode = 'auto' } = args
49
+ const { existingSignatures, protectedHeader, unprotectedHeader, issuer, payload, mode = 'auto', clientId, clientIdScheme } = args
26
50
 
27
51
  const { noIdentifierInHeader = false } = issuer
28
- const combinedHeader = { ...unprotectedHeader, ...protectedHeader }
29
- if (!combinedHeader.alg) {
30
- return Promise.reject(`No 'alg' key present in the JWS header`)
31
- }
32
52
  const identifier = await ensureManagedIdentifierResult(issuer, context)
33
- await checkAndUpdateJwtHeader({ mode, identifier, noIdentifierInHeader, header: protectedHeader }, context)
34
-
53
+ await checkAndUpdateJwsHeader({ mode, identifier, noIdentifierInHeader, header: protectedHeader }, context)
35
54
  const isBytes = payload instanceof Uint8Array
36
55
  const isString = typeof payload === 'string'
37
56
  if (!isBytes && !isString) {
38
57
  if (issuer.noIssPayloadUpdate !== true && !payload.iss && identifier.issuer) {
39
58
  payload.iss = identifier.issuer
40
59
  }
60
+ if (clientIdScheme && !payload.client_id_scheme) {
61
+ payload.client_id_scheme = clientIdScheme
62
+ }
63
+ if (clientId && !payload.client_id) {
64
+ payload.client_id = clientId
65
+ }
41
66
  }
42
- const payloadBytes = isBytes ? payload : isString ? u8a.fromString(payload, 'base64url') : u8a.fromString(JSON.stringify(payload), 'utf-8')
67
+ const payloadBytes = payloadToBytes(payload)
43
68
  const base64urlHeader = encodeJoseBlob(protectedHeader)
44
69
  const base64urlPayload = bytesToBase64url(payloadBytes)
45
70
 
@@ -113,31 +138,31 @@ export const createJwsJsonGeneral = async (args: CreateJwsJsonArgs, context: IRe
113
138
  * @param context
114
139
  */
115
140
 
116
- export const checkAndUpdateJwtHeader = async (
141
+ export const checkAndUpdateJwsHeader = async (
117
142
  {
118
143
  mode = 'auto',
119
144
  identifier,
120
145
  header,
121
146
  noIdentifierInHeader = false,
122
147
  }: {
123
- mode?: CreateJwsMode
148
+ mode?: JwsIdentifierMode
124
149
  identifier: ManagedIdentifierResult
125
150
  noIdentifierInHeader?: boolean
126
- header: JwtHeader
151
+ header: JwsHeader
127
152
  },
128
153
  context: IRequiredContext
129
154
  ) => {
130
- if (isMode(mode, identifier.method, 'did')) {
155
+ if (isIdentifierMode(mode, identifier.method, 'did')) {
131
156
  // kid is VM of the DID
132
157
  // @see https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.4
133
158
  await checkAndUpdateDidHeader({ header, identifier, noIdentifierInHeader }, context)
134
- } else if (isMode(mode, identifier.method, 'x5c')) {
159
+ } else if (isIdentifierMode(mode, identifier.method, 'x5c')) {
135
160
  // Include the x5c in the header. No kid
136
161
  // @see https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.6
137
162
  await checkAndUpdateX5cHeader({ header, identifier, noIdentifierInHeader }, context)
138
- } else if (isMode(mode, identifier.method, 'kid', false)) {
163
+ } else if (isIdentifierMode(mode, identifier.method, 'kid', false)) {
139
164
  await checkAndUpdateKidHeader({ header, identifier, noIdentifierInHeader }, context)
140
- } else if (isMode(mode, identifier.method, 'jwk', false)) {
165
+ } else if (isIdentifierMode(mode, identifier.method, 'jwk', false)) {
141
166
  // Include the JWK in the header as well as its kid if present
142
167
  // @see https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.3
143
168
  // @see https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.4
@@ -154,7 +179,7 @@ const checkAndUpdateX5cHeader = async (
154
179
  identifier,
155
180
  noIdentifierInHeader = false,
156
181
  }: {
157
- header: JwtHeader
182
+ header: JwsHeader | JweHeader
158
183
  identifier: ManagedIdentifierResult
159
184
  noIdentifierInHeader?: boolean
160
185
  },
@@ -183,7 +208,7 @@ const checkAndUpdateDidHeader = async (
183
208
  identifier,
184
209
  noIdentifierInHeader = false,
185
210
  }: {
186
- header: JwtHeader
211
+ header: JwsHeader | JweHeader
187
212
  identifier: ManagedIdentifierResult
188
213
  noIdentifierInHeader?: boolean
189
214
  },
@@ -212,7 +237,7 @@ const checkAndUpdateJwkHeader = async (
212
237
  identifier,
213
238
  noIdentifierInHeader = false,
214
239
  }: {
215
- header: JwtHeader
240
+ header: JwsHeader | JweHeader
216
241
  identifier: ManagedIdentifierResult
217
242
  noIdentifierInHeader?: boolean
218
243
  },
@@ -221,7 +246,7 @@ const checkAndUpdateJwkHeader = async (
221
246
  const { jwk } = header
222
247
  if (jwk) {
223
248
  // let's resolve the provided x5c to be sure
224
- const jwkIdentifier = await context.agent.identifierManagedGetByJwk({ identifier: jwk })
249
+ const jwkIdentifier = await context.agent.identifierManagedGetByJwk({ identifier: jwk as JWK })
225
250
  if (jwkIdentifier.kmsKeyRef !== identifier.kmsKeyRef) {
226
251
  return Promise.reject(Error(`A jwk header was present, but its value did not match the provided signing jwk or kid!`))
227
252
  }
@@ -240,7 +265,7 @@ const checkAndUpdateKidHeader = async (
240
265
  identifier,
241
266
  noIdentifierInHeader = false,
242
267
  }: {
243
- header: JwtHeader
268
+ header: JwsHeader | JweHeader
244
269
  identifier: ManagedIdentifierResult
245
270
  noIdentifierInHeader?: boolean
246
271
  },
@@ -262,7 +287,7 @@ const checkAndUpdateKidHeader = async (
262
287
  }
263
288
  }
264
289
 
265
- const isMode = (mode: CreateJwsMode, identifierMethod: ManagedIdentifierMethod, checkMode: CreateJwsMode, loose = true) => {
290
+ const isIdentifierMode = (mode: JwsIdentifierMode, identifierMethod: ManagedIdentifierMethod, checkMode: JwsIdentifierMode, loose = true) => {
266
291
  if (loose && (checkMode === 'jwk' || checkMode === 'kid')) {
267
292
  // we always have the kid and jwk at hand no matter the identifier method, so we are okay with that
268
293
  // todo: check the impact on the above expressions, as this will now always return true for the both of them
@@ -277,3 +302,158 @@ const isMode = (mode: CreateJwsMode, identifierMethod: ManagedIdentifierMethod,
277
302
  // we always have the kid and jwk at hand no matter the identifier method, so we are okay with that
278
303
  return mode === 'auto' && identifierMethod === checkMode
279
304
  }
305
+
306
+ export const verifyJws = async (args: VerifyJwsArgs, context: IAgentContext<IIdentifierResolution>): Promise<IJwsValidationResult> => {
307
+ const jws = await toJwsJsonGeneralWithIdentifiers(args, context)
308
+
309
+ let errorMessages: string[] = []
310
+ let index = 0
311
+ await Promise.all(
312
+ jws.signatures.map(async (sigWithId) => {
313
+ // If we have a specific KMS agent plugin that can do the verification prefer that over the generic verification
314
+ index++
315
+ let valid: boolean
316
+ const data = u8a.fromString(`${sigWithId.protected}.${jws.payload}`, 'utf-8')
317
+ const jwkInfo = sigWithId.identifier.jwks[0]
318
+ if (sigWithId.header?.alg === 'RSA' && contextHasPlugin(context, 'keyManagerVerify')) {
319
+ const publicKeyHex = jwkTtoPublicKeyHex(jwkInfo.jwk)
320
+ valid = await context.agent.keyManagerVerify({
321
+ signature: sigWithId.signature,
322
+ data,
323
+ publicKeyHex,
324
+ type: keyTypeFromCryptographicSuite({ suite: jwkInfo.jwk.crv ?? 'ES256' }),
325
+ // no kms arg, as the current key manager needs a bit more work
326
+ })
327
+ } else {
328
+ const signature = base64ToBytes(sigWithId.signature)
329
+ valid = await verifySignatureWithSubtle({ data, signature, key: jwkInfo.jwk })
330
+ }
331
+ if (!valid) {
332
+ errorMessages.push(`Signature ${index} was not valid`)
333
+ }
334
+
335
+ return {
336
+ sigWithId,
337
+ valid,
338
+ }
339
+ })
340
+ )
341
+ const error = errorMessages.length !== 0
342
+ return {
343
+ name: 'jws',
344
+ jws,
345
+ error,
346
+ critical: error,
347
+ message: error ? errorMessages.join(', ') : 'Signature validated',
348
+ verificationTime: new Date(),
349
+ } satisfies IJwsValidationResult
350
+ }
351
+
352
+ export const toJwsJsonGeneral = async ({ jws }: { jws: Jws }, context: IAgentContext<any>): Promise<JwsJsonGeneral> => {
353
+ let payload: string
354
+ let signatures: JwsJsonSignature[] = []
355
+
356
+ if (isJwsCompact(jws)) {
357
+ const split = jws.split('.')
358
+ payload = split[1]
359
+ signatures[0] = {
360
+ protected: split[0],
361
+ signature: split[2],
362
+ } satisfies JwsJsonSignature
363
+ } else if (isJwsJsonGeneral(jws)) {
364
+ payload = jws.payload
365
+ signatures = jws.signatures
366
+ } else if (isJwsJsonFlattened(jws)) {
367
+ const { payload: _payload, ...signature } = jws
368
+ payload = _payload
369
+ signatures = [signature]
370
+ } else {
371
+ return Promise.reject(Error(`Invalid JWS supplied`))
372
+ }
373
+ return {
374
+ payload,
375
+ signatures,
376
+ }
377
+ }
378
+
379
+ async function resolveExternalIdentifierFromJwsHeader(
380
+ protectedHeader: JwsHeader,
381
+ context: IAgentContext<IIdentifierResolution>,
382
+ args: {
383
+ jws: Jws
384
+ opts?: { x5c?: Omit<ExternalIdentifierX5cOpts, 'identifier'>; did?: Omit<ExternalIdentifierDidOpts, 'identifier'> }
385
+ }
386
+ ) {
387
+ if (protectedHeader.x5c) {
388
+ const x5c = protectedHeader.x5c
389
+ return await context.agent.identifierExternalResolveByX5c({
390
+ ...args.opts?.x5c,
391
+ identifier: x5c,
392
+ verify: true,
393
+ })
394
+ } else if (protectedHeader.jwk) {
395
+ const jwk = protectedHeader.jwk
396
+ const x5c = jwk.x5c // todo resolve x5u
397
+ return await context.agent.identifierExternalResolveByJwk({
398
+ identifier: protectedHeader.jwk,
399
+ ...(x5c && {
400
+ x5c: {
401
+ ...args?.opts?.x5c,
402
+ identifier: x5c,
403
+ },
404
+ }),
405
+ })
406
+ } else if (protectedHeader.kid && protectedHeader.kid.startsWith('did:')) {
407
+ return await context.agent.identifierExternalResolveByDid({ ...args?.opts?.did, identifier: protectedHeader.kid })
408
+ } else if (protectedHeader.alg === 'none') {
409
+ return undefined
410
+ } else {
411
+ return Promise.reject(Error(`We can only process DIDs, X.509 certificate chains and JWKs for signature validation at present`))
412
+ }
413
+ }
414
+
415
+ function loadJWK(
416
+ providedJwk: JWK | undefined,
417
+ protectedHeader: JwsHeader,
418
+ jws: JwsJsonGeneral,
419
+ ): JWK | undefined {
420
+ if (providedJwk) {
421
+ return providedJwk
422
+ }
423
+ // TODO SDK-47 the identityResolver could handle this as well, but it's a really tiny function
424
+ if (protectedHeader?.typ === 'entity-statement+jwt') {
425
+ const payload = decodeJoseBlob(jws.payload)
426
+ if (!payload?.jwks?.keys?.[0]) {
427
+ throw new Error('Missing or invalid JWK in payload')
428
+ }
429
+ return payload.jwks.keys[0]
430
+ }
431
+
432
+ return undefined
433
+ }
434
+
435
+ export const toJwsJsonGeneralWithIdentifiers = async (
436
+ args: {
437
+ jws: Jws
438
+ jwk?: JWK
439
+ opts?: { x5c?: Omit<ExternalIdentifierX5cOpts, 'identifier'>; did?: Omit<ExternalIdentifierDidOpts, 'identifier'> }
440
+ },
441
+ context: IAgentContext<IIdentifierResolution>
442
+ ): Promise<JwsJsonGeneralWithIdentifiers> => {
443
+ const jws = await toJwsJsonGeneral(args, context)
444
+ const signatures = (await Promise.all(
445
+ jws.signatures.map(async (signature) => {
446
+ const protectedHeader: JwsHeader = decodeJoseBlob(signature.protected)
447
+ const jwk = loadJWK(args.jwk, protectedHeader, jws)
448
+ const identifier = jwk
449
+ ? await resolveExternalJwkIdentifier({ identifier: jwk, method: 'jwk' }, context)
450
+ : await resolveExternalIdentifierFromJwsHeader(protectedHeader, context, args)
451
+ if (identifier !== undefined) {
452
+ return { ...signature, identifier }
453
+ }
454
+ return undefined
455
+ })
456
+ )) as Array<JwsJsonSignatureWithIdentifier>
457
+
458
+ return { payload: jws.payload, signatures }
459
+ }
package/src/index.ts CHANGED
@@ -1,8 +1,12 @@
1
+ import {Loggers} from "@sphereon/ssi-types";
2
+
1
3
  /**
2
4
  * @internal
3
5
  */
4
6
  const schema = require('../plugin.schema.json')
5
7
  export { schema }
8
+
9
+ export const JwtLogger = Loggers.DEFAULT.get('sphereon:sdk:jwt')
6
10
  /**
7
11
  * @public
8
12
  */