@sphereon/ssi-sdk-ext.jwt-service 0.24.1-next.103 → 0.24.1-next.104

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.
@@ -6,6 +6,7 @@ import {
6
6
  CreateJwsJsonArgs,
7
7
  createJwsJsonFlattened,
8
8
  createJwsJsonGeneral,
9
+ IJwsValidationResult,
9
10
  IJwtService,
10
11
  IRequiredContext,
11
12
  JwsCompactResult,
@@ -14,18 +15,21 @@ import {
14
15
  PreparedJwsObject,
15
16
  prepareJwsObject,
16
17
  schema,
18
+ verifyJws,
19
+ VerifyJwsArgs,
17
20
  } from '..'
18
21
 
19
22
  /**
20
23
  * @public
21
24
  */
22
25
  export class JwtService implements IAgentPlugin {
23
- readonly schema = schema.IMnemonicInfoGenerator
26
+ readonly schema = schema.IJwtService
24
27
  readonly methods: IJwtService = {
25
28
  jwtPrepareJws: this.jwtPrepareJws.bind(this),
26
29
  jwtCreateJwsJsonGeneralSignature: this.jwtCreateJwsJsonGeneralSignature.bind(this),
27
30
  jwtCreateJwsJsonFlattenedSignature: this.jwtCreateJwsJsonFlattenedSignature.bind(this),
28
31
  jwtCreateJwsCompactSignature: this.jwtCreateJwsCompactSignature.bind(this),
32
+ jwtVerifyJwsSignature: this.jwtVerifyJwsSignature.bind(this),
29
33
  }
30
34
 
31
35
  private async jwtPrepareJws(args: CreateJwsJsonArgs, context: IRequiredContext): Promise<PreparedJwsObject> {
@@ -44,4 +48,8 @@ export class JwtService implements IAgentPlugin {
44
48
  // We wrap it in a json object for remote REST calls
45
49
  return { jwt: await createJwsCompact(args, context) }
46
50
  }
51
+
52
+ private async jwtVerifyJwsSignature(args: VerifyJwsArgs, context: IRequiredContext): Promise<IJwsValidationResult> {
53
+ return await verifyJws(args, context)
54
+ }
47
55
  }
@@ -1,26 +1,50 @@
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,
34
+ JwsJsonGeneralWithIdentifiers,
19
35
  JwsJsonSignature,
20
36
  JwtHeader,
37
+ JwtPayload,
21
38
  PreparedJwsObject,
39
+ VerifyJwsArgs,
22
40
  } from '../types/IJwtService'
23
41
 
42
+ const payloadToBytes = (payload: string | JwtPayload | 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
49
  const { existingSignatures, protectedHeader, unprotectedHeader, issuer, payload, mode = 'auto' } = args
26
50
 
@@ -31,7 +55,6 @@ export const prepareJwsObject = async (args: CreateJwsJsonArgs, context: IRequir
31
55
  }
32
56
  const identifier = await ensureManagedIdentifierResult(issuer, context)
33
57
  await checkAndUpdateJwtHeader({ mode, identifier, noIdentifierInHeader, header: protectedHeader }, context)
34
-
35
58
  const isBytes = payload instanceof Uint8Array
36
59
  const isString = typeof payload === 'string'
37
60
  if (!isBytes && !isString) {
@@ -39,7 +62,7 @@ export const prepareJwsObject = async (args: CreateJwsJsonArgs, context: IRequir
39
62
  payload.iss = identifier.issuer
40
63
  }
41
64
  }
42
- const payloadBytes = isBytes ? payload : isString ? u8a.fromString(payload, 'base64url') : u8a.fromString(JSON.stringify(payload), 'utf-8')
65
+ const payloadBytes = payloadToBytes(payload)
43
66
  const base64urlHeader = encodeJoseBlob(protectedHeader)
44
67
  const base64urlPayload = bytesToBase64url(payloadBytes)
45
68
 
@@ -120,24 +143,24 @@ export const checkAndUpdateJwtHeader = async (
120
143
  header,
121
144
  noIdentifierInHeader = false,
122
145
  }: {
123
- mode?: CreateJwsMode
146
+ mode?: JwsIdentifierMode
124
147
  identifier: ManagedIdentifierResult
125
148
  noIdentifierInHeader?: boolean
126
149
  header: JwtHeader
127
150
  },
128
151
  context: IRequiredContext
129
152
  ) => {
130
- if (isMode(mode, identifier.method, 'did')) {
153
+ if (isIdentifierMode(mode, identifier.method, 'did')) {
131
154
  // kid is VM of the DID
132
155
  // @see https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.4
133
156
  await checkAndUpdateDidHeader({ header, identifier, noIdentifierInHeader }, context)
134
- } else if (isMode(mode, identifier.method, 'x5c')) {
157
+ } else if (isIdentifierMode(mode, identifier.method, 'x5c')) {
135
158
  // Include the x5c in the header. No kid
136
159
  // @see https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.6
137
160
  await checkAndUpdateX5cHeader({ header, identifier, noIdentifierInHeader }, context)
138
- } else if (isMode(mode, identifier.method, 'kid', false)) {
161
+ } else if (isIdentifierMode(mode, identifier.method, 'kid', false)) {
139
162
  await checkAndUpdateKidHeader({ header, identifier, noIdentifierInHeader }, context)
140
- } else if (isMode(mode, identifier.method, 'jwk', false)) {
163
+ } else if (isIdentifierMode(mode, identifier.method, 'jwk', false)) {
141
164
  // Include the JWK in the header as well as its kid if present
142
165
  // @see https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.3
143
166
  // @see https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.4
@@ -262,7 +285,7 @@ const checkAndUpdateKidHeader = async (
262
285
  }
263
286
  }
264
287
 
265
- const isMode = (mode: CreateJwsMode, identifierMethod: ManagedIdentifierMethod, checkMode: CreateJwsMode, loose = true) => {
288
+ const isIdentifierMode = (mode: JwsIdentifierMode, identifierMethod: ManagedIdentifierMethod, checkMode: JwsIdentifierMode, loose = true) => {
266
289
  if (loose && (checkMode === 'jwk' || checkMode === 'kid')) {
267
290
  // we always have the kid and jwk at hand no matter the identifier method, so we are okay with that
268
291
  // todo: check the impact on the above expressions, as this will now always return true for the both of them
@@ -277,3 +300,130 @@ const isMode = (mode: CreateJwsMode, identifierMethod: ManagedIdentifierMethod,
277
300
  // we always have the kid and jwk at hand no matter the identifier method, so we are okay with that
278
301
  return mode === 'auto' && identifierMethod === checkMode
279
302
  }
303
+
304
+ export const verifyJws = async (args: VerifyJwsArgs, context: IAgentContext<IIdentifierResolution>): Promise<IJwsValidationResult> => {
305
+ const jws = await toJwsJsonGeneralWithIdentifiers(args, context)
306
+
307
+ let errorMessages: string[] = []
308
+ let index = 0
309
+ await Promise.all(
310
+ jws.signatures.map(async (sigWithId) => {
311
+ // If we have a specific KMS agent plugin that can do the verification prefer that over the generic verification
312
+ index++
313
+ let valid: boolean
314
+ const data = u8a.fromString(`${sigWithId.protected}.${jws.payload}`, 'utf-8')
315
+ const jwkInfo = sigWithId.identifier.jwks[0]
316
+ if (sigWithId.header?.alg === 'RSA' && contextHasPlugin(context, 'keyManagerVerify')) {
317
+ const publicKeyHex = jwkTtoPublicKeyHex(jwkInfo.jwk)
318
+ valid = await context.agent.keyManagerVerify({
319
+ signature: sigWithId.signature,
320
+ data,
321
+ publicKeyHex,
322
+ type: keyTypeFromCryptographicSuite({ suite: jwkInfo.jwk.crv ?? 'ES256' }),
323
+ // no kms arg, as the current key manager needs a bit more work
324
+ })
325
+ } else {
326
+ const signature = base64ToBytes(sigWithId.signature)
327
+ valid = await verifySignatureWithSubtle({ data, signature, key: jwkInfo.jwk })
328
+ }
329
+ if (!valid) {
330
+ errorMessages.push(`Signature ${index} was not valid`)
331
+ }
332
+
333
+ return {
334
+ sigWithId,
335
+ valid,
336
+ }
337
+ })
338
+ )
339
+ const error = errorMessages.length !== 0
340
+ return {
341
+ name: 'jws',
342
+ jws,
343
+ error,
344
+ critical: error,
345
+ message: error ? errorMessages.join(', ') : 'Signature validated',
346
+ verificationTime: new Date(),
347
+ } satisfies IJwsValidationResult
348
+ }
349
+ export const toJwsJsonGeneral = async ({ jws }: { jws: Jws }, context: IAgentContext<any>): Promise<JwsJsonGeneral> => {
350
+ let payload: string
351
+ let signatures: JwsJsonSignature[] = []
352
+
353
+ if (isJwsCompact(jws)) {
354
+ const split = jws.split('.')
355
+ payload = split[1]
356
+ signatures[0] = {
357
+ protected: split[0],
358
+ signature: split[2],
359
+ } satisfies JwsJsonSignature
360
+ } else if (isJwsJsonGeneral(jws)) {
361
+ payload = jws.payload
362
+ signatures = jws.signatures
363
+ } else if (isJwsJsonFlattened(jws)) {
364
+ const { payload: _payload, ...signature } = jws
365
+ payload = _payload
366
+ signatures = [signature]
367
+ } else {
368
+ return Promise.reject(Error(`Invalid JWS supplied`))
369
+ }
370
+ return {
371
+ payload,
372
+ signatures,
373
+ }
374
+ }
375
+
376
+ async function resolveExternalIdentifierFromJwsHeader(
377
+ protectedHeader: JwtHeader,
378
+ context: IAgentContext<IIdentifierResolution>,
379
+ args: {
380
+ jws: Jws
381
+ opts?: { x5c?: Omit<ExternalIdentifierX5cOpts, 'identifier'>; did?: Omit<ExternalIdentifierDidOpts, 'identifier'> }
382
+ }
383
+ ) {
384
+ if (protectedHeader.x5c) {
385
+ const x5c = protectedHeader.x5c
386
+ return await context.agent.identifierExternalResolveByX5c({
387
+ ...args.opts?.x5c,
388
+ identifier: x5c,
389
+ verify: true,
390
+ })
391
+ } else if (protectedHeader.jwk) {
392
+ const jwk = protectedHeader.jwk
393
+ const x5c = jwk.x5c // todo resolve x5u
394
+ return await context.agent.identifierExternalResolveByJwk({
395
+ identifier: protectedHeader.jwk,
396
+ ...(x5c && {
397
+ x5c: {
398
+ ...args?.opts?.x5c,
399
+ identifier: x5c,
400
+ },
401
+ }),
402
+ })
403
+ } else if (protectedHeader.kid && protectedHeader.kid.startsWith('did:')) {
404
+ return await context.agent.identifierExternalResolveByDid({ ...args?.opts?.did, identifier: protectedHeader.kid })
405
+ } else {
406
+ return Promise.reject(Error(`We can only process DIDs, X.509 certificate chains and JWKs for signature validation at present`))
407
+ }
408
+ }
409
+
410
+ export const toJwsJsonGeneralWithIdentifiers = async (
411
+ args: {
412
+ jws: Jws
413
+ jwk?: JWK
414
+ opts?: { x5c?: Omit<ExternalIdentifierX5cOpts, 'identifier'>; did?: Omit<ExternalIdentifierDidOpts, 'identifier'> }
415
+ },
416
+ context: IAgentContext<IIdentifierResolution>
417
+ ): Promise<JwsJsonGeneralWithIdentifiers> => {
418
+ const jws = await toJwsJsonGeneral(args, context)
419
+ const signatures = await Promise.all(
420
+ jws.signatures.map(async (signature) => {
421
+ const protectedHeader: JwtHeader = decodeJoseBlob(signature.protected)
422
+ const identifier = args.jwk
423
+ ? await resolveExternalJwkIdentifier({ identifier: args.jwk }, context)
424
+ : await resolveExternalIdentifierFromJwsHeader(protectedHeader, context, args)
425
+ return { ...signature, identifier }
426
+ })
427
+ )
428
+ return { payload: jws.payload, signatures }
429
+ }
@@ -1,17 +1,24 @@
1
- import { IIdentifierResolution, ManagedIdentifierOptsOrResult, ManagedIdentifierResult } from '@sphereon/ssi-sdk-ext.identifier-resolution'
2
- import { ISphereonKeyManager } from '@sphereon/ssi-sdk-ext.key-manager'
3
- import { JWK, SignatureAlgorithmJwa } from '@sphereon/ssi-sdk-ext.key-utils'
4
- import { IAgentContext, IPluginMethodMap } from '@veramo/core'
5
-
6
- export type IRequiredContext = IAgentContext<IIdentifierResolution & ISphereonKeyManager> // could we still interop with Veramo?
1
+ import {
2
+ ExternalIdentifierDidOpts,
3
+ ExternalIdentifierResult,
4
+ ExternalIdentifierX5cOpts,
5
+ IIdentifierResolution,
6
+ ManagedIdentifierOptsOrResult,
7
+ ManagedIdentifierResult,
8
+ } from '@sphereon/ssi-sdk-ext.identifier-resolution'
9
+ import { IValidationResult, JoseSignatureAlgorithm, JoseSignatureAlgorithmString, JWK } from '@sphereon/ssi-types'
10
+ import { IAgentContext, IKeyManager, IPluginMethodMap } from '@veramo/core'
11
+
12
+ export type IRequiredContext = IAgentContext<IIdentifierResolution & IKeyManager> // could we still interop with Veramo?
7
13
 
8
14
  export const jwtServiceContextMethods: Array<string> = [
9
15
  'jwtPrepareJws',
10
16
  'jwtCreateJwsJsonGeneralSignature',
11
17
  'jwtCreateJwsJsonFlattenedSignature',
12
18
  'jwtCreateJwsCompactSignature',
13
- 'jwtVerifyJwsCompactSignature',
19
+ 'jwtVerifyJwsSignature',
14
20
  ]
21
+
15
22
  export interface IJwtService extends IPluginMethodMap {
16
23
  jwtPrepareJws(args: CreateJwsJsonArgs, context: IRequiredContext): Promise<PreparedJwsObject>
17
24
 
@@ -21,11 +28,15 @@ export interface IJwtService extends IPluginMethodMap {
21
28
 
22
29
  jwtCreateJwsCompactSignature(args: CreateJwsCompactArgs, context: IRequiredContext): Promise<JwsCompactResult>
23
30
 
24
- // jwtVerifyJwsCompactSignature(args: {jwt: string}): Promise<any>
31
+ jwtVerifyJwsSignature(args: VerifyJwsArgs, context: IRequiredContext): Promise<IJwsValidationResult>
25
32
 
26
33
  // TODO: JWE/encryption
27
34
  }
28
35
 
36
+ export type IJwsValidationResult = IValidationResult & {
37
+ jws: JwsJsonGeneralWithIdentifiers // We always translate to general as that is the most flexible format allowing multiple sigs
38
+ }
39
+
29
40
  export interface PreparedJws {
30
41
  protectedHeader: JwtHeader
31
42
  payload: Uint8Array
@@ -39,6 +50,8 @@ export interface JwsJsonSignature {
39
50
  signature: string
40
51
  }
41
52
 
53
+ export type Jws = JwsCompact | JwsJsonFlattened | JwsJsonGeneral
54
+
42
55
  export type JwsCompact = string
43
56
 
44
57
  export interface JwsJsonFlattened {
@@ -53,6 +66,14 @@ export interface JwsJsonGeneral {
53
66
  signatures: Array<JwsJsonSignature>
54
67
  }
55
68
 
69
+ export interface JwsJsonGeneralWithIdentifiers extends JwsJsonGeneral {
70
+ signatures: Array<JwsJsonSignatureWithIdentifier>
71
+ }
72
+
73
+ export interface JwsJsonSignatureWithIdentifier extends JwsJsonSignature {
74
+ identifier: ExternalIdentifierResult
75
+ }
76
+
56
77
  export interface PreparedJwsObject {
57
78
  jws: PreparedJws
58
79
  b64: { payload: string; protectedHeader: string } // header is always json, as it can only be used in JwsJson
@@ -64,6 +85,7 @@ export interface BaseJwtHeader {
64
85
  alg?: string
65
86
  kid?: string
66
87
  }
88
+
67
89
  export interface BaseJwtPayload {
68
90
  iss?: string
69
91
  sub?: string
@@ -87,14 +109,17 @@ export interface JwtPayload extends BaseJwtPayload {
87
109
  }
88
110
 
89
111
  export interface JwsHeaderOpts {
90
- als: SignatureAlgorithmJwa
112
+ alg: JoseSignatureAlgorithm | JoseSignatureAlgorithmString
91
113
  }
92
114
 
93
- export type CreateJwsMode = 'x5c' | 'kid' | 'jwk' | 'did' | 'auto'
115
+ export type JwsIdentifierMode = 'x5c' | 'kid' | 'jwk' | 'did' | 'auto'
94
116
 
95
117
  export type CreateJwsArgs = {
96
- mode?: CreateJwsMode
97
- issuer: ManagedIdentifierOptsOrResult & { noIssPayloadUpdate?: boolean; noIdentifierInHeader?: boolean }
118
+ mode?: JwsIdentifierMode
119
+ issuer: ManagedIdentifierOptsOrResult & {
120
+ noIssPayloadUpdate?: boolean
121
+ noIdentifierInHeader?: boolean
122
+ }
98
123
  protectedHeader: JwtHeader
99
124
  payload: JwtPayload | Uint8Array | string
100
125
  }
@@ -103,6 +128,12 @@ export type CreateJwsCompactArgs = CreateJwsArgs
103
128
 
104
129
  export type CreateJwsFlattenedArgs = Exclude<CreateJwsJsonArgs, 'existingSignatures'>
105
130
 
131
+ export type VerifyJwsArgs = {
132
+ jws: Jws
133
+ jwk?: JWK // Jwk will be resolved from jws, but you can also provide one
134
+ opts?: { x5c?: Omit<ExternalIdentifierX5cOpts, 'identifier'>; did?: Omit<ExternalIdentifierDidOpts, 'identifier'> }
135
+ }
136
+
106
137
  /**
107
138
  * @public
108
139
  */
@@ -118,4 +149,16 @@ export interface JwsCompactResult {
118
149
  jwt: JwsCompact
119
150
  }
120
151
 
121
- // export const COMPACT_JWS_REGEX = /^([a-zA-Z0-9_=-]+)\.([a-zA-Z0-9_=-]+)?\.([a-zA-Z0-9_=-]+)$/
152
+ export function isJwsCompact(jws: Jws): jws is JwsCompact {
153
+ return typeof jws === 'string' && jws.split('~')[0].match(COMPACT_JWS_REGEX) !== null
154
+ }
155
+
156
+ export function isJwsJsonFlattened(jws: Jws): jws is JwsJsonFlattened {
157
+ return typeof jws === 'object' && 'signature' in jws && 'protected' in jws
158
+ }
159
+
160
+ export function isJwsJsonGeneral(jws: Jws): jws is JwsJsonGeneral {
161
+ return typeof jws === 'object' && 'signatures' in jws
162
+ }
163
+
164
+ export const COMPACT_JWS_REGEX = /^([a-zA-Z0-9_=-]+)\.([a-zA-Z0-9_=-]+)?\.([a-zA-Z0-9_=-]+)$/