@sphereon/ssi-sdk-ext.jwt-service 0.24.1-next.98 → 0.24.1-unstable.112

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,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,
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
- 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
53
  await checkAndUpdateJwtHeader({ mode, identifier, noIdentifierInHeader, header: protectedHeader }, context)
34
-
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
 
@@ -120,24 +145,24 @@ export const checkAndUpdateJwtHeader = async (
120
145
  header,
121
146
  noIdentifierInHeader = false,
122
147
  }: {
123
- mode?: CreateJwsMode
148
+ mode?: JwsIdentifierMode
124
149
  identifier: ManagedIdentifierResult
125
150
  noIdentifierInHeader?: boolean
126
151
  header: JwtHeader
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
@@ -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,130 @@ 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
+ export const toJwsJsonGeneral = async ({ jws }: { jws: Jws }, context: IAgentContext<any>): Promise<JwsJsonGeneral> => {
352
+ let payload: string
353
+ let signatures: JwsJsonSignature[] = []
354
+
355
+ if (isJwsCompact(jws)) {
356
+ const split = jws.split('.')
357
+ payload = split[1]
358
+ signatures[0] = {
359
+ protected: split[0],
360
+ signature: split[2],
361
+ } satisfies JwsJsonSignature
362
+ } else if (isJwsJsonGeneral(jws)) {
363
+ payload = jws.payload
364
+ signatures = jws.signatures
365
+ } else if (isJwsJsonFlattened(jws)) {
366
+ const { payload: _payload, ...signature } = jws
367
+ payload = _payload
368
+ signatures = [signature]
369
+ } else {
370
+ return Promise.reject(Error(`Invalid JWS supplied`))
371
+ }
372
+ return {
373
+ payload,
374
+ signatures,
375
+ }
376
+ }
377
+
378
+ async function resolveExternalIdentifierFromJwsHeader(
379
+ protectedHeader: JwtHeader,
380
+ context: IAgentContext<IIdentifierResolution>,
381
+ args: {
382
+ jws: Jws
383
+ opts?: { x5c?: Omit<ExternalIdentifierX5cOpts, 'identifier'>; did?: Omit<ExternalIdentifierDidOpts, 'identifier'> }
384
+ }
385
+ ) {
386
+ if (protectedHeader.x5c) {
387
+ const x5c = protectedHeader.x5c
388
+ return await context.agent.identifierExternalResolveByX5c({
389
+ ...args.opts?.x5c,
390
+ identifier: x5c,
391
+ verify: true,
392
+ })
393
+ } else if (protectedHeader.jwk) {
394
+ const jwk = protectedHeader.jwk
395
+ const x5c = jwk.x5c // todo resolve x5u
396
+ return await context.agent.identifierExternalResolveByJwk({
397
+ identifier: protectedHeader.jwk,
398
+ ...(x5c && {
399
+ x5c: {
400
+ ...args?.opts?.x5c,
401
+ identifier: x5c,
402
+ },
403
+ }),
404
+ })
405
+ } else if (protectedHeader.kid && protectedHeader.kid.startsWith('did:')) {
406
+ return await context.agent.identifierExternalResolveByDid({ ...args?.opts?.did, identifier: protectedHeader.kid })
407
+ } else {
408
+ return Promise.reject(Error(`We can only process DIDs, X.509 certificate chains and JWKs for signature validation at present`))
409
+ }
410
+ }
411
+
412
+ export const toJwsJsonGeneralWithIdentifiers = async (
413
+ args: {
414
+ jws: Jws
415
+ jwk?: JWK
416
+ opts?: { x5c?: Omit<ExternalIdentifierX5cOpts, 'identifier'>; did?: Omit<ExternalIdentifierDidOpts, 'identifier'> }
417
+ },
418
+ context: IAgentContext<IIdentifierResolution>
419
+ ): Promise<JwsJsonGeneralWithIdentifiers> => {
420
+ const jws = await toJwsJsonGeneral(args, context)
421
+ const signatures = await Promise.all(
422
+ jws.signatures.map(async (signature) => {
423
+ const protectedHeader: JwtHeader = decodeJoseBlob(signature.protected)
424
+ const identifier = args.jwk
425
+ ? await resolveExternalJwkIdentifier({ identifier: args.jwk }, context)
426
+ : await resolveExternalIdentifierFromJwsHeader(protectedHeader, context, args)
427
+ return { ...signature, identifier }
428
+ })
429
+ )
430
+ return { payload: jws.payload, signatures }
431
+ }
@@ -1,17 +1,25 @@
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 { ClientIdScheme } from '@sphereon/ssi-sdk-ext.x509-utils'
10
+ import { IValidationResult, JoseSignatureAlgorithm, JoseSignatureAlgorithmString, JWK } from '@sphereon/ssi-types'
11
+ import { IAgentContext, IKeyManager, IPluginMethodMap } from '@veramo/core'
12
+
13
+ export type IRequiredContext = IAgentContext<IIdentifierResolution & IKeyManager> // could we still interop with Veramo?
7
14
 
8
15
  export const jwtServiceContextMethods: Array<string> = [
9
16
  'jwtPrepareJws',
10
17
  'jwtCreateJwsJsonGeneralSignature',
11
18
  'jwtCreateJwsJsonFlattenedSignature',
12
19
  'jwtCreateJwsCompactSignature',
13
- 'jwtVerifyJwsCompactSignature',
20
+ 'jwtVerifyJwsSignature',
14
21
  ]
22
+
15
23
  export interface IJwtService extends IPluginMethodMap {
16
24
  jwtPrepareJws(args: CreateJwsJsonArgs, context: IRequiredContext): Promise<PreparedJwsObject>
17
25
 
@@ -21,11 +29,15 @@ export interface IJwtService extends IPluginMethodMap {
21
29
 
22
30
  jwtCreateJwsCompactSignature(args: CreateJwsCompactArgs, context: IRequiredContext): Promise<JwsCompactResult>
23
31
 
24
- // jwtVerifyJwsCompactSignature(args: {jwt: string}): Promise<any>
32
+ jwtVerifyJwsSignature(args: VerifyJwsArgs, context: IRequiredContext): Promise<IJwsValidationResult>
25
33
 
26
34
  // TODO: JWE/encryption
27
35
  }
28
36
 
37
+ export type IJwsValidationResult = IValidationResult & {
38
+ jws: JwsJsonGeneralWithIdentifiers // We always translate to general as that is the most flexible format allowing multiple sigs
39
+ }
40
+
29
41
  export interface PreparedJws {
30
42
  protectedHeader: JwtHeader
31
43
  payload: Uint8Array
@@ -39,6 +51,8 @@ export interface JwsJsonSignature {
39
51
  signature: string
40
52
  }
41
53
 
54
+ export type Jws = JwsCompact | JwsJsonFlattened | JwsJsonGeneral
55
+
42
56
  export type JwsCompact = string
43
57
 
44
58
  export interface JwsJsonFlattened {
@@ -53,6 +67,14 @@ export interface JwsJsonGeneral {
53
67
  signatures: Array<JwsJsonSignature>
54
68
  }
55
69
 
70
+ export interface JwsJsonGeneralWithIdentifiers extends JwsJsonGeneral {
71
+ signatures: Array<JwsJsonSignatureWithIdentifier>
72
+ }
73
+
74
+ export interface JwsJsonSignatureWithIdentifier extends JwsJsonSignature {
75
+ identifier: ExternalIdentifierResult
76
+ }
77
+
56
78
  export interface PreparedJwsObject {
57
79
  jws: PreparedJws
58
80
  b64: { payload: string; protectedHeader: string } // header is always json, as it can only be used in JwsJson
@@ -64,6 +86,7 @@ export interface BaseJwtHeader {
64
86
  alg?: string
65
87
  kid?: string
66
88
  }
89
+
67
90
  export interface BaseJwtPayload {
68
91
  iss?: string
69
92
  sub?: string
@@ -87,14 +110,19 @@ export interface JwtPayload extends BaseJwtPayload {
87
110
  }
88
111
 
89
112
  export interface JwsHeaderOpts {
90
- als: SignatureAlgorithmJwa
113
+ alg: JoseSignatureAlgorithm | JoseSignatureAlgorithmString
91
114
  }
92
115
 
93
- export type CreateJwsMode = 'x5c' | 'kid' | 'jwk' | 'did' | 'auto'
116
+ export type JwsIdentifierMode = 'x5c' | 'kid' | 'jwk' | 'did' | 'auto'
94
117
 
95
118
  export type CreateJwsArgs = {
96
- mode?: CreateJwsMode
97
- issuer: ManagedIdentifierOptsOrResult & { noIssPayloadUpdate?: boolean; noIdentifierInHeader?: boolean }
119
+ mode?: JwsIdentifierMode
120
+ issuer: ManagedIdentifierOptsOrResult & {
121
+ noIssPayloadUpdate?: boolean
122
+ noIdentifierInHeader?: boolean
123
+ }
124
+ clientId?: string
125
+ clientIdScheme?: ClientIdScheme | 'did' | string
98
126
  protectedHeader: JwtHeader
99
127
  payload: JwtPayload | Uint8Array | string
100
128
  }
@@ -103,6 +131,12 @@ export type CreateJwsCompactArgs = CreateJwsArgs
103
131
 
104
132
  export type CreateJwsFlattenedArgs = Exclude<CreateJwsJsonArgs, 'existingSignatures'>
105
133
 
134
+ export type VerifyJwsArgs = {
135
+ jws: Jws
136
+ jwk?: JWK // Jwk will be resolved from jws, but you can also provide one
137
+ opts?: { x5c?: Omit<ExternalIdentifierX5cOpts, 'identifier'>; did?: Omit<ExternalIdentifierDidOpts, 'identifier'> }
138
+ }
139
+
106
140
  /**
107
141
  * @public
108
142
  */
@@ -118,4 +152,16 @@ export interface JwsCompactResult {
118
152
  jwt: JwsCompact
119
153
  }
120
154
 
121
- // export const COMPACT_JWS_REGEX = /^([a-zA-Z0-9_=-]+)\.([a-zA-Z0-9_=-]+)?\.([a-zA-Z0-9_=-]+)$/
155
+ export function isJwsCompact(jws: Jws): jws is JwsCompact {
156
+ return typeof jws === 'string' && jws.split('~')[0].match(COMPACT_JWS_REGEX) !== null
157
+ }
158
+
159
+ export function isJwsJsonFlattened(jws: Jws): jws is JwsJsonFlattened {
160
+ return typeof jws === 'object' && 'signature' in jws && 'protected' in jws
161
+ }
162
+
163
+ export function isJwsJsonGeneral(jws: Jws): jws is JwsJsonGeneral {
164
+ return typeof jws === 'object' && 'signatures' in jws
165
+ }
166
+
167
+ export const COMPACT_JWS_REGEX = /^([a-zA-Z0-9_=-]+)\.([a-zA-Z0-9_=-]+)?\.([a-zA-Z0-9_=-]+)$/