@sphereon/ssi-sdk-ext.jwt-service 0.24.1-unstable.85

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