@sphereon/ssi-sdk-ext.jwt-service 0.24.1-next.93

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.
@@ -0,0 +1,47 @@
1
+ import { IAgentPlugin } from '@veramo/core'
2
+ import {
3
+ createJwsCompact,
4
+ CreateJwsCompactArgs,
5
+ CreateJwsFlattenedArgs,
6
+ CreateJwsJsonArgs,
7
+ createJwsJsonFlattened,
8
+ createJwsJsonGeneral,
9
+ IJwtService,
10
+ IRequiredContext,
11
+ JwsCompactResult,
12
+ JwsJsonFlattened,
13
+ JwsJsonGeneral,
14
+ PreparedJwsObject,
15
+ prepareJwsObject,
16
+ schema,
17
+ } from '..'
18
+
19
+ /**
20
+ * @public
21
+ */
22
+ export class JwtService implements IAgentPlugin {
23
+ readonly schema = schema.IMnemonicInfoGenerator
24
+ readonly methods: IJwtService = {
25
+ jwtPrepareJws: this.jwtPrepareJws.bind(this),
26
+ jwtCreateJwsJsonGeneralSignature: this.jwtCreateJwsJsonGeneralSignature.bind(this),
27
+ jwtCreateJwsJsonFlattenedSignature: this.jwtCreateJwsJsonFlattenedSignature.bind(this),
28
+ jwtCreateJwsCompactSignature: this.jwtCreateJwsCompactSignature.bind(this),
29
+ }
30
+
31
+ private async jwtPrepareJws(args: CreateJwsJsonArgs, context: IRequiredContext): Promise<PreparedJwsObject> {
32
+ return await prepareJwsObject(args, context)
33
+ }
34
+
35
+ private async jwtCreateJwsJsonGeneralSignature(args: CreateJwsJsonArgs, context: IRequiredContext): Promise<JwsJsonGeneral> {
36
+ return await createJwsJsonGeneral(args, context)
37
+ }
38
+
39
+ private async jwtCreateJwsJsonFlattenedSignature(args: CreateJwsFlattenedArgs, context: IRequiredContext): Promise<JwsJsonFlattened> {
40
+ return await createJwsJsonFlattened(args, context)
41
+ }
42
+
43
+ private async jwtCreateJwsCompactSignature(args: CreateJwsCompactArgs, context: IRequiredContext): Promise<JwsCompactResult> {
44
+ // We wrap it in a json object for remote REST calls
45
+ return { jwt: await createJwsCompact(args, context) }
46
+ }
47
+ }
@@ -0,0 +1,279 @@
1
+ import {
2
+ isManagedIdentifierDidResult,
3
+ isManagedIdentifierX5cResult,
4
+ ManagedIdentifierMethod,
5
+ ManagedIdentifierResult,
6
+ ensureManagedIdentifierResult,
7
+ } from '@sphereon/ssi-sdk-ext.identifier-resolution'
8
+ import { bytesToBase64url, encodeJoseBlob } from '@veramo/utils'
9
+ import * as u8a from 'uint8arrays'
10
+ import {
11
+ CreateJwsCompactArgs,
12
+ CreateJwsFlattenedArgs,
13
+ CreateJwsJsonArgs,
14
+ CreateJwsMode,
15
+ IRequiredContext,
16
+ JwsCompact,
17
+ JwsJsonFlattened,
18
+ JwsJsonGeneral,
19
+ JwsJsonSignature,
20
+ JwtHeader,
21
+ PreparedJwsObject,
22
+ } from '../types/IJwtService'
23
+
24
+ export const prepareJwsObject = async (args: CreateJwsJsonArgs, context: IRequiredContext): Promise<PreparedJwsObject> => {
25
+ const { existingSignatures, protectedHeader, unprotectedHeader, issuer, payload, mode = 'auto' } = args
26
+
27
+ 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
+ const identifier = await ensureManagedIdentifierResult(issuer, context)
33
+ await checkAndUpdateJwtHeader({ mode, identifier, noIdentifierInHeader, header: protectedHeader }, context)
34
+
35
+ const isBytes = payload instanceof Uint8Array
36
+ const isString = typeof payload === 'string'
37
+ if (!isBytes && !isString) {
38
+ if (issuer.noIssPayloadUpdate !== true && !payload.iss && identifier.issuer) {
39
+ payload.iss = identifier.issuer
40
+ }
41
+ }
42
+ const payloadBytes = isBytes ? payload : isString ? u8a.fromString(payload, 'base64url') : u8a.fromString(JSON.stringify(payload), 'utf-8')
43
+ const base64urlHeader = encodeJoseBlob(protectedHeader)
44
+ const base64urlPayload = bytesToBase64url(payloadBytes)
45
+
46
+ return {
47
+ jws: {
48
+ unprotectedHeader,
49
+ protectedHeader,
50
+ payload: payloadBytes,
51
+ existingSignatures,
52
+ },
53
+ b64: {
54
+ protectedHeader: base64urlHeader,
55
+ payload: base64urlPayload,
56
+ },
57
+ identifier,
58
+ }
59
+ }
60
+
61
+ export const createJwsCompact = async (args: CreateJwsCompactArgs, context: IRequiredContext): Promise<JwsCompact> => {
62
+ const { protected: protectedHeader, payload, signature } = await createJwsJsonFlattened(args, context)
63
+ return `${protectedHeader}.${payload}.${signature}`
64
+ }
65
+
66
+ export const createJwsJsonFlattened = async (args: CreateJwsFlattenedArgs, context: IRequiredContext): Promise<JwsJsonFlattened> => {
67
+ const jws = await createJwsJsonGeneral(args, context)
68
+ if (jws.signatures.length !== 1) {
69
+ return Promise.reject(Error(`JWS flattened signature can only contain 1 signature. Found ${jws.signatures.length}`))
70
+ }
71
+ return {
72
+ ...jws.signatures[0],
73
+ payload: jws.payload,
74
+ } satisfies JwsJsonFlattened
75
+ }
76
+
77
+ export const createJwsJsonGeneral = async (args: CreateJwsJsonArgs, context: IRequiredContext): Promise<JwsJsonGeneral> => {
78
+ const { payload, protectedHeader, unprotectedHeader, existingSignatures, mode, issuer } = args
79
+ const { b64, identifier } = await prepareJwsObject(
80
+ {
81
+ protectedHeader,
82
+ unprotectedHeader,
83
+ payload,
84
+ existingSignatures,
85
+ issuer,
86
+ mode,
87
+ },
88
+ context
89
+ )
90
+ // const algorithm = await signatureAlgorithmFromKey({ key: identifier.key })
91
+ const signature = await context.agent.keyManagerSign({
92
+ keyRef: identifier.kmsKeyRef,
93
+ data: `${b64.protectedHeader}.${b64.payload}`,
94
+ encoding: undefined,
95
+ })
96
+ const jsonSignature = {
97
+ protected: b64.protectedHeader,
98
+ header: unprotectedHeader,
99
+ signature,
100
+ } satisfies JwsJsonSignature
101
+ return {
102
+ payload: b64.payload,
103
+ signatures: [...(existingSignatures ?? []), jsonSignature],
104
+ } satisfies JwsJsonGeneral
105
+ }
106
+
107
+ /**
108
+ * Updates the JWT header to include x5c, kid, jwk objects using the supplied issuer identifier that will be used to sign. If not present will automatically make the header objects available
109
+ * @param mode The type of header to check or include
110
+ * @param identifier The identifier of the signer. This identifier will be used later to sign
111
+ * @param header The JWT header
112
+ * @param noIdentifierInHeader
113
+ * @param context
114
+ */
115
+
116
+ export const checkAndUpdateJwtHeader = async (
117
+ {
118
+ mode = 'auto',
119
+ identifier,
120
+ header,
121
+ noIdentifierInHeader = false,
122
+ }: {
123
+ mode?: CreateJwsMode
124
+ identifier: ManagedIdentifierResult
125
+ noIdentifierInHeader?: boolean
126
+ header: JwtHeader
127
+ },
128
+ context: IRequiredContext
129
+ ) => {
130
+ if (isMode(mode, identifier.method, 'did')) {
131
+ // kid is VM of the DID
132
+ // @see https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.4
133
+ await checkAndUpdateDidHeader({ header, identifier, noIdentifierInHeader }, context)
134
+ } else if (isMode(mode, identifier.method, 'x5c')) {
135
+ // Include the x5c in the header. No kid
136
+ // @see https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.6
137
+ await checkAndUpdateX5cHeader({ header, identifier, noIdentifierInHeader }, context)
138
+ } else if (isMode(mode, identifier.method, 'kid', false)) {
139
+ await checkAndUpdateKidHeader({ header, identifier, noIdentifierInHeader }, context)
140
+ } else if (isMode(mode, identifier.method, 'jwk', false)) {
141
+ // Include the JWK in the header as well as its kid if present
142
+ // @see https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.3
143
+ // @see https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.4
144
+ await checkAndUpdateJwkHeader({ header, identifier, noIdentifierInHeader }, context)
145
+ } else {
146
+ // Better safe than sorry. We could let it pass, but we want to force implementers to make a conscious choice
147
+ return Promise.reject(`Invalid combination of JWS creation mode ${mode} and identifier method ${identifier.method} chosen`)
148
+ }
149
+ }
150
+
151
+ const checkAndUpdateX5cHeader = async (
152
+ {
153
+ header,
154
+ identifier,
155
+ noIdentifierInHeader = false,
156
+ }: {
157
+ header: JwtHeader
158
+ identifier: ManagedIdentifierResult
159
+ noIdentifierInHeader?: boolean
160
+ },
161
+ context: IRequiredContext
162
+ ) => {
163
+ const { x5c } = header
164
+ if (x5c) {
165
+ // let's resolve the provided x5c to be sure
166
+ const x5cIdentifier = await context.agent.identifierManagedGetByX5c({ identifier: x5c })
167
+ if (x5cIdentifier.kmsKeyRef !== identifier.kmsKeyRef) {
168
+ return Promise.reject(Error(`An x5c header was present, but its issuer public key did not match the provided signing public key!`))
169
+ }
170
+ } else if (!noIdentifierInHeader) {
171
+ if (!isManagedIdentifierX5cResult(identifier)) {
172
+ return Promise.reject(Error('No x5c header in the JWT, but mode was x5c and also no x5x identifier was provided!'))
173
+ } else if (header.jwk || header.kid) {
174
+ return Promise.reject(Error('x5c mode was choosen, but jwk or kid headers were provided. These cannot be used together!'))
175
+ }
176
+ header.x5c = identifier.x5c
177
+ }
178
+ }
179
+
180
+ const checkAndUpdateDidHeader = async (
181
+ {
182
+ header,
183
+ identifier,
184
+ noIdentifierInHeader = false,
185
+ }: {
186
+ header: JwtHeader
187
+ identifier: ManagedIdentifierResult
188
+ noIdentifierInHeader?: boolean
189
+ },
190
+ context: IRequiredContext
191
+ ) => {
192
+ const { kid } = header
193
+ if (kid) {
194
+ // let's resolve the provided x5c to be sure
195
+ const vmIdentifier = await context.agent.identifierManagedGetByDid({ identifier: kid })
196
+ if (vmIdentifier.kmsKeyRef !== identifier.kmsKeyRef) {
197
+ return Promise.reject(Error(`A kid header was present, but its value did not match the provided signing kid!`))
198
+ }
199
+ } else if (!noIdentifierInHeader) {
200
+ if (!isManagedIdentifierDidResult(identifier)) {
201
+ return Promise.reject(Error('No kid header in the JWT, but mode was did and also no DID identifier was provided!'))
202
+ } else if (header.jwk || header.x5c) {
203
+ return Promise.reject(Error('did mode was chosen, but jwk or x5c headers were provided. These cannot be used together!'))
204
+ }
205
+ header.kid = identifier.kid
206
+ }
207
+ }
208
+
209
+ const checkAndUpdateJwkHeader = async (
210
+ {
211
+ header,
212
+ identifier,
213
+ noIdentifierInHeader = false,
214
+ }: {
215
+ header: JwtHeader
216
+ identifier: ManagedIdentifierResult
217
+ noIdentifierInHeader?: boolean
218
+ },
219
+ context: IRequiredContext
220
+ ) => {
221
+ const { jwk } = header
222
+ if (jwk) {
223
+ // let's resolve the provided x5c to be sure
224
+ const jwkIdentifier = await context.agent.identifierManagedGetByJwk({ identifier: jwk })
225
+ if (jwkIdentifier.kmsKeyRef !== identifier.kmsKeyRef) {
226
+ return Promise.reject(Error(`A jwk header was present, but its value did not match the provided signing jwk or kid!`))
227
+ }
228
+ } else if (!noIdentifierInHeader) {
229
+ // We basically accept everything for this mode, as we can always create JWKs from any key
230
+ if (header.x5c) {
231
+ return Promise.reject(Error('jwk mode was chosen, but x5c headers were provided. These cannot be used together!'))
232
+ }
233
+ header.jwk = identifier.jwk
234
+ }
235
+ }
236
+
237
+ const checkAndUpdateKidHeader = async (
238
+ {
239
+ header,
240
+ identifier,
241
+ noIdentifierInHeader = false,
242
+ }: {
243
+ header: JwtHeader
244
+ identifier: ManagedIdentifierResult
245
+ noIdentifierInHeader?: boolean
246
+ },
247
+ context: IRequiredContext
248
+ ) => {
249
+ const { kid } = header
250
+ if (kid) {
251
+ // let's resolve the provided x5c to be sure
252
+ const kidIdentifier = await context.agent.identifierManagedGetByKid({ identifier: kid })
253
+ if (kidIdentifier.kmsKeyRef !== identifier.kmsKeyRef) {
254
+ return Promise.reject(Error(`A kid header was present, but its value did not match the provided signing kid!`))
255
+ }
256
+ } else if (!noIdentifierInHeader) {
257
+ // We basically accept everything for this mode, as we can always create JWKs from any key
258
+ if (header.x5c) {
259
+ return Promise.reject(Error('kid mode was chosen, but x5c headers were provided. These cannot be used together!'))
260
+ }
261
+ header.kid = identifier.kid
262
+ }
263
+ }
264
+
265
+ const isMode = (mode: CreateJwsMode, identifierMethod: ManagedIdentifierMethod, checkMode: CreateJwsMode, loose = true) => {
266
+ if (loose && (checkMode === 'jwk' || checkMode === 'kid')) {
267
+ // we always have the kid and jwk at hand no matter the identifier method, so we are okay with that
268
+ // todo: check the impact on the above expressions, as this will now always return true for the both of them
269
+ return true
270
+ }
271
+ if (mode === checkMode) {
272
+ if (checkMode !== 'auto' && mode !== identifierMethod) {
273
+ throw Error(`Provided mode ${mode} conflicts with identifier method ${identifierMethod}`)
274
+ }
275
+ return true
276
+ }
277
+ // we always have the kid and jwk at hand no matter the identifier method, so we are okay with that
278
+ return mode === 'auto' && identifierMethod === checkMode
279
+ }
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @internal
3
+ */
4
+ const schema = require('../plugin.schema.json')
5
+ export { schema }
6
+ /**
7
+ * @public
8
+ */
9
+ export { JwtService } from './agent/JwtService'
10
+ export * from './functions'
11
+ export * from './types/IJwtService'
@@ -0,0 +1,113 @@
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?
7
+ export interface IJwtService extends IPluginMethodMap {
8
+ jwtPrepareJws(args: CreateJwsJsonArgs, context: IRequiredContext): Promise<PreparedJwsObject>
9
+
10
+ jwtCreateJwsJsonGeneralSignature(args: CreateJwsJsonArgs, context: IRequiredContext): Promise<JwsJsonGeneral>
11
+
12
+ jwtCreateJwsJsonFlattenedSignature(args: CreateJwsFlattenedArgs, context: IRequiredContext): Promise<JwsJsonFlattened>
13
+
14
+ jwtCreateJwsCompactSignature(args: CreateJwsCompactArgs, context: IRequiredContext): Promise<JwsCompactResult>
15
+
16
+ // jwtVerifyJwsCompactSignature(args: {jwt: string}): Promise<any>
17
+
18
+ // TODO: JWE/encryption
19
+ }
20
+
21
+ export interface PreparedJws {
22
+ protectedHeader: JwtHeader
23
+ payload: Uint8Array
24
+ unprotectedHeader?: JwtHeader // only for jws json and also then optional
25
+ existingSignatures?: Array<JwsJsonSignature> // only for jws json and also then optional
26
+ }
27
+
28
+ export interface JwsJsonSignature {
29
+ protected: string
30
+ header?: JwtHeader
31
+ signature: string
32
+ }
33
+
34
+ export type JwsCompact = string
35
+
36
+ export interface JwsJsonFlattened {
37
+ payload: string
38
+ protected: string
39
+ header?: JwtHeader
40
+ signature: string
41
+ }
42
+
43
+ export interface JwsJsonGeneral {
44
+ payload: string
45
+ signatures: Array<JwsJsonSignature>
46
+ }
47
+
48
+ export interface PreparedJwsObject {
49
+ jws: PreparedJws
50
+ b64: { payload: string; protectedHeader: string } // header is always json, as it can only be used in JwsJson
51
+ identifier: ManagedIdentifierResult
52
+ }
53
+
54
+ export interface BaseJwtHeader {
55
+ typ?: string
56
+ alg?: string
57
+ kid?: string
58
+ }
59
+ export interface BaseJwtPayload {
60
+ iss?: string
61
+ sub?: string
62
+ aud?: string[] | string
63
+ exp?: number
64
+ nbf?: number
65
+ iat?: number
66
+ jti?: string
67
+ }
68
+
69
+ export interface JwtHeader extends BaseJwtHeader {
70
+ kid?: string
71
+ jwk?: JWK
72
+ x5c?: string[]
73
+
74
+ [key: string]: unknown
75
+ }
76
+
77
+ export interface JwtPayload extends BaseJwtPayload {
78
+ [key: string]: unknown
79
+ }
80
+
81
+ export interface JwsHeaderOpts {
82
+ als: SignatureAlgorithmJwa
83
+ }
84
+
85
+ export type CreateJwsMode = 'x5c' | 'kid' | 'jwk' | 'did' | 'auto'
86
+
87
+ export type CreateJwsArgs = {
88
+ mode?: CreateJwsMode
89
+ issuer: ManagedIdentifierOptsOrResult & { noIssPayloadUpdate?: boolean; noIdentifierInHeader?: boolean }
90
+ protectedHeader: JwtHeader
91
+ payload: JwtPayload | Uint8Array | string
92
+ }
93
+
94
+ export type CreateJwsCompactArgs = CreateJwsArgs
95
+
96
+ export type CreateJwsFlattenedArgs = Exclude<CreateJwsJsonArgs, 'existingSignatures'>
97
+
98
+ /**
99
+ * @public
100
+ */
101
+ export type CreateJwsJsonArgs = CreateJwsArgs & {
102
+ unprotectedHeader?: JwtHeader // only for jws json
103
+ existingSignatures?: Array<JwsJsonSignature> // Only for jws json
104
+ }
105
+
106
+ /**
107
+ * @public
108
+ */
109
+ export interface JwsCompactResult {
110
+ jwt: JwsCompact
111
+ }
112
+
113
+ // export const COMPACT_JWS_REGEX = /^([a-zA-Z0-9_=-]+)\.([a-zA-Z0-9_=-]+)?\.([a-zA-Z0-9_=-]+)$/