@sphereon/ssi-sdk.vc-status-list 0.32.1-next.54 → 0.33.1-feature.vcdm2.4
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.
- package/dist/functions.d.ts +13 -13
- package/dist/functions.d.ts.map +1 -1
- package/dist/functions.js +141 -225
- package/dist/functions.js.map +1 -1
- package/dist/impl/IStatusList.d.ts +26 -0
- package/dist/impl/IStatusList.d.ts.map +1 -0
- package/dist/impl/IStatusList.js +2 -0
- package/dist/impl/IStatusList.js.map +1 -0
- package/dist/impl/OAuthStatusList.d.ts +21 -0
- package/dist/impl/OAuthStatusList.d.ts.map +1 -0
- package/dist/impl/OAuthStatusList.js +144 -0
- package/dist/impl/OAuthStatusList.js.map +1 -0
- package/dist/impl/StatusList2021.d.ts +16 -0
- package/dist/impl/StatusList2021.d.ts.map +1 -0
- package/dist/impl/StatusList2021.js +179 -0
- package/dist/impl/StatusList2021.js.map +1 -0
- package/dist/impl/StatusListFactory.d.ts +11 -0
- package/dist/impl/StatusListFactory.d.ts.map +1 -0
- package/dist/impl/StatusListFactory.js +29 -0
- package/dist/impl/StatusListFactory.js.map +1 -0
- package/dist/impl/encoding/cbor.d.ts +6 -0
- package/dist/impl/encoding/cbor.d.ts.map +1 -0
- package/dist/impl/encoding/cbor.js +123 -0
- package/dist/impl/encoding/cbor.js.map +1 -0
- package/dist/impl/encoding/common.d.ts +12 -0
- package/dist/impl/encoding/common.d.ts.map +1 -0
- package/dist/impl/encoding/common.js +9 -0
- package/dist/impl/encoding/common.js.map +1 -0
- package/dist/impl/encoding/jwt.d.ts +9 -0
- package/dist/impl/encoding/jwt.d.ts.map +1 -0
- package/dist/impl/encoding/jwt.js +61 -0
- package/dist/impl/encoding/jwt.js.map +1 -0
- package/dist/index.js +2 -18
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +131 -33
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +11 -2
- package/dist/types/index.js.map +1 -1
- package/dist/utils.d.ts +17 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +79 -0
- package/dist/utils.js.map +1 -0
- package/package.json +17 -6
- package/src/functions.ts +73 -159
- package/src/impl/IStatusList.ts +42 -0
- package/src/impl/OAuthStatusList.ts +206 -0
- package/src/impl/StatusList2021.ts +241 -0
- package/src/impl/StatusListFactory.ts +34 -0
- package/src/impl/encoding/cbor.ts +171 -0
- package/src/impl/encoding/common.ts +20 -0
- package/src/impl/encoding/jwt.ts +80 -0
- package/src/types/index.ts +151 -37
- package/src/utils.ts +95 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { IAgentContext, ICredentialPlugin, ProofFormat as VeramoProofFormat } from '@veramo/core'
|
|
2
|
+
import { IIdentifierResolution } from '@sphereon/ssi-sdk-ext.identifier-resolution'
|
|
3
|
+
import { CredentialMapper, DocumentFormat, IIssuer, ProofFormat, StatusListCredential, StatusListType } from '@sphereon/ssi-types'
|
|
4
|
+
|
|
5
|
+
import { StatusList } from '@sphereon/vc-status-list'
|
|
6
|
+
import { IStatusList } from './IStatusList'
|
|
7
|
+
import {
|
|
8
|
+
CheckStatusIndexArgs,
|
|
9
|
+
CreateStatusListArgs,
|
|
10
|
+
Status2021,
|
|
11
|
+
StatusListResult,
|
|
12
|
+
ToStatusListDetailsArgs,
|
|
13
|
+
UpdateStatusListFromEncodedListArgs,
|
|
14
|
+
UpdateStatusListIndexArgs,
|
|
15
|
+
} from '../types'
|
|
16
|
+
import { assertValidProofType, getAssertedProperty, getAssertedValue, getAssertedValues } from '../utils'
|
|
17
|
+
|
|
18
|
+
export const DEFAULT_LIST_LENGTH = 250000
|
|
19
|
+
export const DEFAULT_PROOF_FORMAT = 'lds' as VeramoProofFormat
|
|
20
|
+
|
|
21
|
+
export class StatusList2021Implementation implements IStatusList {
|
|
22
|
+
async createNewStatusList(
|
|
23
|
+
args: CreateStatusListArgs,
|
|
24
|
+
context: IAgentContext<ICredentialPlugin & IIdentifierResolution>,
|
|
25
|
+
): Promise<StatusListResult> {
|
|
26
|
+
const length = args?.length ?? DEFAULT_LIST_LENGTH
|
|
27
|
+
const proofFormat: ProofFormat = args?.proofFormat ?? DEFAULT_PROOF_FORMAT
|
|
28
|
+
assertValidProofType(StatusListType.StatusList2021, proofFormat)
|
|
29
|
+
const veramoProofFormat: VeramoProofFormat = proofFormat as VeramoProofFormat
|
|
30
|
+
|
|
31
|
+
const { issuer, id } = args
|
|
32
|
+
const correlationId = getAssertedValue('correlationId', args.correlationId)
|
|
33
|
+
|
|
34
|
+
const list = new StatusList({ length })
|
|
35
|
+
const encodedList = await list.encode()
|
|
36
|
+
const statusPurpose = 'revocation'
|
|
37
|
+
|
|
38
|
+
const statusListCredential = await this.createVerifiableCredential(
|
|
39
|
+
{
|
|
40
|
+
...args,
|
|
41
|
+
encodedList,
|
|
42
|
+
proofFormat: veramoProofFormat,
|
|
43
|
+
},
|
|
44
|
+
context,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
encodedList,
|
|
49
|
+
statusListCredential: statusListCredential,
|
|
50
|
+
statusList2021: {
|
|
51
|
+
statusPurpose,
|
|
52
|
+
indexingDirection: 'rightToLeft',
|
|
53
|
+
},
|
|
54
|
+
length,
|
|
55
|
+
type: StatusListType.StatusList2021,
|
|
56
|
+
proofFormat,
|
|
57
|
+
id,
|
|
58
|
+
correlationId,
|
|
59
|
+
issuer,
|
|
60
|
+
statuslistContentType: this.buildContentType(proofFormat),
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async updateStatusListIndex(
|
|
65
|
+
args: UpdateStatusListIndexArgs,
|
|
66
|
+
context: IAgentContext<ICredentialPlugin & IIdentifierResolution>,
|
|
67
|
+
): Promise<StatusListResult> {
|
|
68
|
+
const credential = args.statusListCredential
|
|
69
|
+
const uniform = CredentialMapper.toUniformCredential(credential)
|
|
70
|
+
const { issuer, credentialSubject } = uniform
|
|
71
|
+
const id = getAssertedValue('id', uniform.id)
|
|
72
|
+
const origEncodedList = getAssertedProperty('encodedList', credentialSubject)
|
|
73
|
+
|
|
74
|
+
const index = typeof args.statusListIndex === 'number' ? args.statusListIndex : parseInt(args.statusListIndex)
|
|
75
|
+
const statusList = await StatusList.decode({ encodedList: origEncodedList })
|
|
76
|
+
statusList.setStatus(index, args.value != 0)
|
|
77
|
+
const encodedList = await statusList.encode()
|
|
78
|
+
|
|
79
|
+
const proofFormat = CredentialMapper.detectDocumentType(credential) === DocumentFormat.JWT ? 'jwt' : 'lds'
|
|
80
|
+
const updatedCredential = await this.createVerifiableCredential(
|
|
81
|
+
{
|
|
82
|
+
...args,
|
|
83
|
+
id,
|
|
84
|
+
issuer,
|
|
85
|
+
encodedList,
|
|
86
|
+
proofFormat: proofFormat,
|
|
87
|
+
},
|
|
88
|
+
context,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
statusListCredential: updatedCredential,
|
|
93
|
+
encodedList,
|
|
94
|
+
statusList2021: {
|
|
95
|
+
...('statusPurpose' in credentialSubject ? { statusPurpose: credentialSubject.statusPurpose } : {}),
|
|
96
|
+
indexingDirection: 'rightToLeft',
|
|
97
|
+
},
|
|
98
|
+
length: statusList.length - 1,
|
|
99
|
+
type: StatusListType.StatusList2021,
|
|
100
|
+
proofFormat: proofFormat,
|
|
101
|
+
id,
|
|
102
|
+
issuer,
|
|
103
|
+
statuslistContentType: this.buildContentType(proofFormat),
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async updateStatusListFromEncodedList(
|
|
108
|
+
args: UpdateStatusListFromEncodedListArgs,
|
|
109
|
+
context: IAgentContext<ICredentialPlugin & IIdentifierResolution>,
|
|
110
|
+
): Promise<StatusListResult> {
|
|
111
|
+
if (!args.statusList2021) {
|
|
112
|
+
throw new Error('statusList2021 options required for type StatusList2021')
|
|
113
|
+
}
|
|
114
|
+
const proofFormat: ProofFormat = args?.proofFormat ?? DEFAULT_PROOF_FORMAT
|
|
115
|
+
assertValidProofType(StatusListType.StatusList2021, proofFormat)
|
|
116
|
+
const veramoProofFormat: VeramoProofFormat = proofFormat as VeramoProofFormat
|
|
117
|
+
|
|
118
|
+
const { issuer, id } = getAssertedValues(args)
|
|
119
|
+
const statusList = await StatusList.decode({ encodedList: args.encodedList })
|
|
120
|
+
const index = typeof args.statusListIndex === 'number' ? args.statusListIndex : parseInt(args.statusListIndex)
|
|
121
|
+
statusList.setStatus(index, args.value)
|
|
122
|
+
|
|
123
|
+
const newEncodedList = await statusList.encode()
|
|
124
|
+
const credential = await this.createVerifiableCredential(
|
|
125
|
+
{
|
|
126
|
+
id,
|
|
127
|
+
issuer,
|
|
128
|
+
encodedList: newEncodedList,
|
|
129
|
+
proofFormat: veramoProofFormat,
|
|
130
|
+
keyRef: args.keyRef,
|
|
131
|
+
},
|
|
132
|
+
context,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
type: StatusListType.StatusList2021,
|
|
137
|
+
statusListCredential: credential,
|
|
138
|
+
encodedList: newEncodedList,
|
|
139
|
+
statusList2021: {
|
|
140
|
+
statusPurpose: args.statusList2021.statusPurpose,
|
|
141
|
+
indexingDirection: 'rightToLeft',
|
|
142
|
+
},
|
|
143
|
+
length: statusList.length,
|
|
144
|
+
proofFormat: args.proofFormat ?? 'lds',
|
|
145
|
+
id: id,
|
|
146
|
+
issuer: issuer,
|
|
147
|
+
statuslistContentType: this.buildContentType(proofFormat),
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async checkStatusIndex(args: CheckStatusIndexArgs): Promise<number | Status2021> {
|
|
152
|
+
const uniform = CredentialMapper.toUniformCredential(args.statusListCredential)
|
|
153
|
+
const { credentialSubject } = uniform
|
|
154
|
+
const encodedList = getAssertedProperty('encodedList', credentialSubject)
|
|
155
|
+
|
|
156
|
+
const statusList = await StatusList.decode({ encodedList })
|
|
157
|
+
const status = statusList.getStatus(typeof args.statusListIndex === 'number' ? args.statusListIndex : parseInt(args.statusListIndex))
|
|
158
|
+
return status ? Status2021.Invalid : Status2021.Valid
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async toStatusListDetails(args: ToStatusListDetailsArgs): Promise<StatusListResult> {
|
|
162
|
+
const { statusListPayload } = args
|
|
163
|
+
const uniform = CredentialMapper.toUniformCredential(statusListPayload)
|
|
164
|
+
const { issuer, credentialSubject } = uniform
|
|
165
|
+
const id = getAssertedValue('id', uniform.id)
|
|
166
|
+
const encodedList = getAssertedProperty('encodedList', credentialSubject)
|
|
167
|
+
const proofFormat: ProofFormat = CredentialMapper.detectDocumentType(statusListPayload) === DocumentFormat.JWT ? 'jwt' : 'lds'
|
|
168
|
+
|
|
169
|
+
const statusPurpose = getAssertedProperty('statusPurpose', credentialSubject)
|
|
170
|
+
const list = await StatusList.decode({ encodedList })
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
id,
|
|
174
|
+
encodedList,
|
|
175
|
+
issuer,
|
|
176
|
+
type: StatusListType.StatusList2021,
|
|
177
|
+
proofFormat,
|
|
178
|
+
length: list.length,
|
|
179
|
+
statusListCredential: statusListPayload,
|
|
180
|
+
statuslistContentType: this.buildContentType(proofFormat),
|
|
181
|
+
statusList2021: {
|
|
182
|
+
indexingDirection: 'rightToLeft',
|
|
183
|
+
statusPurpose,
|
|
184
|
+
},
|
|
185
|
+
...(args.correlationId && { correlationId: args.correlationId }),
|
|
186
|
+
...(args.driverType && { driverType: args.driverType }),
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private async createVerifiableCredential(
|
|
191
|
+
args: {
|
|
192
|
+
id: string
|
|
193
|
+
issuer: string | IIssuer
|
|
194
|
+
encodedList: string
|
|
195
|
+
proofFormat: VeramoProofFormat
|
|
196
|
+
keyRef?: string
|
|
197
|
+
},
|
|
198
|
+
context: IAgentContext<ICredentialPlugin & IIdentifierResolution>,
|
|
199
|
+
): Promise<StatusListCredential> {
|
|
200
|
+
const identifier = await context.agent.identifierManagedGet({
|
|
201
|
+
identifier: typeof args.issuer === 'string' ? args.issuer : args.issuer.id,
|
|
202
|
+
vmRelationship: 'assertionMethod',
|
|
203
|
+
offlineWhenNoDIDRegistered: true,
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
const credential = {
|
|
207
|
+
'@context': ['https://www.w3.org/2018/credentials/v1', 'https://w3id.org/vc/status-list/2021/v1'],
|
|
208
|
+
id: args.id,
|
|
209
|
+
issuer: args.issuer,
|
|
210
|
+
type: ['VerifiableCredential', 'StatusList2021Credential'],
|
|
211
|
+
credentialSubject: {
|
|
212
|
+
id: args.id,
|
|
213
|
+
type: 'StatusList2021',
|
|
214
|
+
statusPurpose: 'revocation',
|
|
215
|
+
encodedList: args.encodedList,
|
|
216
|
+
},
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const verifiableCredential = await context.agent.createVerifiableCredential({
|
|
220
|
+
credential,
|
|
221
|
+
keyRef: args.keyRef ?? identifier.kmsKeyRef,
|
|
222
|
+
proofFormat: args.proofFormat,
|
|
223
|
+
fetchRemoteContexts: true,
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
return CredentialMapper.toWrappedVerifiableCredential(verifiableCredential as StatusListCredential).original as StatusListCredential
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private buildContentType(proofFormat: 'jwt' | 'lds' | 'EthereumEip712Signature2021' | 'cbor' | undefined) {
|
|
230
|
+
switch (proofFormat) {
|
|
231
|
+
case 'jwt':
|
|
232
|
+
return `application/statuslist+jwt`
|
|
233
|
+
case 'cbor':
|
|
234
|
+
return `application/statuslist+cwt`
|
|
235
|
+
case 'lds':
|
|
236
|
+
return 'application/statuslist+ld+json'
|
|
237
|
+
default:
|
|
238
|
+
throw Error(`Unsupported content type '${proofFormat}' for status lists`)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { IStatusList } from './IStatusList'
|
|
2
|
+
import { StatusList2021Implementation } from './StatusList2021'
|
|
3
|
+
import { OAuthStatusListImplementation } from './OAuthStatusList'
|
|
4
|
+
import { StatusListType } from '@sphereon/ssi-types'
|
|
5
|
+
|
|
6
|
+
export class StatusListFactory {
|
|
7
|
+
private static instance: StatusListFactory
|
|
8
|
+
private implementations: Map<StatusListType, IStatusList>
|
|
9
|
+
|
|
10
|
+
private constructor() {
|
|
11
|
+
this.implementations = new Map()
|
|
12
|
+
this.implementations.set(StatusListType.StatusList2021, new StatusList2021Implementation())
|
|
13
|
+
this.implementations.set(StatusListType.OAuthStatusList, new OAuthStatusListImplementation())
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public static getInstance(): StatusListFactory {
|
|
17
|
+
if (!StatusListFactory.instance) {
|
|
18
|
+
StatusListFactory.instance = new StatusListFactory()
|
|
19
|
+
}
|
|
20
|
+
return StatusListFactory.instance
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public getByType(type: StatusListType): IStatusList {
|
|
24
|
+
const statusList = this.implementations.get(type)
|
|
25
|
+
if (!statusList) {
|
|
26
|
+
throw new Error(`No implementation found for status list type: ${type}`)
|
|
27
|
+
}
|
|
28
|
+
return statusList
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getStatusListImplementation(type: StatusListType): IStatusList {
|
|
33
|
+
return StatusListFactory.getInstance().getByType(type)
|
|
34
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { StatusList } from '@sd-jwt/jwt-status-list'
|
|
2
|
+
import { deflate, inflate } from 'pako'
|
|
3
|
+
import { com, kotlin } from '@sphereon/kmp-cbor'
|
|
4
|
+
import base64url from 'base64url'
|
|
5
|
+
import { IRequiredContext, SignedStatusListData } from '../../types'
|
|
6
|
+
import { DecodedStatusListPayload, resolveIdentifier } from './common'
|
|
7
|
+
import { BitsPerStatus } from '@sd-jwt/jwt-status-list/dist'
|
|
8
|
+
|
|
9
|
+
const cbor = com.sphereon.cbor
|
|
10
|
+
const kmp = com.sphereon.kmp
|
|
11
|
+
const decompressRawStatusList = (StatusList as any).decodeStatusList.bind(StatusList)
|
|
12
|
+
|
|
13
|
+
const CWT_CLAIMS = {
|
|
14
|
+
SUBJECT: 2,
|
|
15
|
+
ISSUER: 1,
|
|
16
|
+
ISSUED_AT: 6,
|
|
17
|
+
EXPIRATION: 4,
|
|
18
|
+
TIME_TO_LIVE: 65534,
|
|
19
|
+
STATUS_LIST: 65533,
|
|
20
|
+
} as const
|
|
21
|
+
|
|
22
|
+
export const createSignedCbor = async (
|
|
23
|
+
context: IRequiredContext,
|
|
24
|
+
statusList: StatusList,
|
|
25
|
+
issuerString: string,
|
|
26
|
+
id: string,
|
|
27
|
+
expiresAt?: Date,
|
|
28
|
+
keyRef?: string,
|
|
29
|
+
): Promise<SignedStatusListData> => {
|
|
30
|
+
const identifier = await resolveIdentifier(context, issuerString, keyRef)
|
|
31
|
+
|
|
32
|
+
const encodeStatusList = statusList.encodeStatusList()
|
|
33
|
+
const compressedList = deflate(encodeStatusList, { level: 9 })
|
|
34
|
+
const compressedBytes = new Int8Array(compressedList)
|
|
35
|
+
|
|
36
|
+
const statusListMap = new cbor.CborMap(
|
|
37
|
+
kotlin.collections.KtMutableMap.fromJsMap(
|
|
38
|
+
new Map<com.sphereon.cbor.CborString, com.sphereon.cbor.CborItem<any>>([
|
|
39
|
+
[new cbor.CborString('bits'), new cbor.CborUInt(kmp.LongKMP.fromNumber(statusList.getBitsPerStatus()))],
|
|
40
|
+
[new cbor.CborString('lst'), new cbor.CborByteString(compressedBytes)],
|
|
41
|
+
]),
|
|
42
|
+
),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
const protectedHeader = new cbor.CborMap(
|
|
46
|
+
kotlin.collections.KtMutableMap.fromJsMap(
|
|
47
|
+
new Map([[new cbor.CborUInt(kmp.LongKMP.fromNumber(16)), new cbor.CborString('statuslist+cwt')]]), // "type"
|
|
48
|
+
),
|
|
49
|
+
)
|
|
50
|
+
const protectedHeaderEncoded = cbor.Cbor.encode(protectedHeader)
|
|
51
|
+
const claimsMap = buildClaimsMap(id, issuerString, statusListMap, expiresAt)
|
|
52
|
+
const claimsEncoded: Int8Array = cbor.Cbor.encode(claimsMap)
|
|
53
|
+
|
|
54
|
+
const signedCWT: string = await context.agent.keyManagerSign({
|
|
55
|
+
keyRef: identifier.kmsKeyRef,
|
|
56
|
+
data: base64url.encode(Buffer.from(claimsEncoded)), // TODO test on RN
|
|
57
|
+
encoding: undefined,
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const protectedHeaderEncodedInt8 = new Int8Array(protectedHeaderEncoded)
|
|
61
|
+
const claimsEncodedInt8 = new Int8Array(claimsEncoded)
|
|
62
|
+
const signatureBytes = base64url.decode(signedCWT)
|
|
63
|
+
const signatureInt8 = new Int8Array(Buffer.from(signatureBytes))
|
|
64
|
+
|
|
65
|
+
const cwtArrayElements: Array<com.sphereon.cbor.CborItem<any>> = [
|
|
66
|
+
new cbor.CborByteString(protectedHeaderEncodedInt8),
|
|
67
|
+
new cbor.CborByteString(claimsEncodedInt8),
|
|
68
|
+
new cbor.CborByteString(signatureInt8),
|
|
69
|
+
]
|
|
70
|
+
const cwtArray = new cbor.CborArray(kotlin.collections.KtMutableList.fromJsArray(cwtArrayElements))
|
|
71
|
+
const cwtEncoded = cbor.Cbor.encode(cwtArray)
|
|
72
|
+
const cwtBuffer = Buffer.from(cwtEncoded)
|
|
73
|
+
return {
|
|
74
|
+
statusListCredential: base64url.encode(cwtBuffer),
|
|
75
|
+
encodedList: base64url.encode(compressedList as Buffer), // JS in @sd-jwt/jwt-status-list drops it in like this, so keep the same method
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function buildClaimsMap(
|
|
80
|
+
id: string,
|
|
81
|
+
issuerString: string,
|
|
82
|
+
statusListMap: com.sphereon.cbor.CborMap<com.sphereon.cbor.CborString, com.sphereon.cbor.CborItem<any>>,
|
|
83
|
+
expiresAt?: Date,
|
|
84
|
+
) {
|
|
85
|
+
const ttl = 65535 // FIXME figure out what value should be / come from and what the difference is with exp
|
|
86
|
+
const claimsEntries: Array<[com.sphereon.cbor.CborUInt, com.sphereon.cbor.CborItem<any>]> = [
|
|
87
|
+
[new cbor.CborUInt(kmp.LongKMP.fromNumber(CWT_CLAIMS.SUBJECT)), new cbor.CborString(id)], // "sub"
|
|
88
|
+
[new cbor.CborUInt(kmp.LongKMP.fromNumber(CWT_CLAIMS.ISSUER)), new cbor.CborString(issuerString)], // "iss"
|
|
89
|
+
[
|
|
90
|
+
new cbor.CborUInt(kmp.LongKMP.fromNumber(CWT_CLAIMS.ISSUED_AT)),
|
|
91
|
+
new cbor.CborUInt(kmp.LongKMP.fromNumber(Math.floor(Date.now() / 1000))), // "iat"
|
|
92
|
+
],
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
if (expiresAt) {
|
|
96
|
+
claimsEntries.push([
|
|
97
|
+
new cbor.CborUInt(kmp.LongKMP.fromNumber(CWT_CLAIMS.EXPIRATION)),
|
|
98
|
+
new cbor.CborUInt(kmp.LongKMP.fromNumber(Math.floor(expiresAt.getTime() / 1000))), // "exp"
|
|
99
|
+
])
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (ttl) {
|
|
103
|
+
claimsEntries.push([
|
|
104
|
+
new cbor.CborUInt(kmp.LongKMP.fromNumber(CWT_CLAIMS.TIME_TO_LIVE)),
|
|
105
|
+
new cbor.CborUInt(kmp.LongKMP.fromNumber(ttl)), // "time to live"
|
|
106
|
+
])
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
claimsEntries.push([new cbor.CborUInt(kmp.LongKMP.fromNumber(CWT_CLAIMS.STATUS_LIST)), statusListMap])
|
|
110
|
+
|
|
111
|
+
const claimsMap = new cbor.CborMap(kotlin.collections.KtMutableMap.fromJsMap(new Map(claimsEntries)))
|
|
112
|
+
return claimsMap
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const getCborValueFromMap = <T>(map: Map<com.sphereon.cbor.CborItem<any>, com.sphereon.cbor.CborItem<any>>, key: number): T => {
|
|
116
|
+
const value = getCborOptionalValueFromMap<T>(map, key)
|
|
117
|
+
if (value === undefined) {
|
|
118
|
+
throw new Error(`Required claim ${key} not found`)
|
|
119
|
+
}
|
|
120
|
+
return value
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const getCborOptionalValueFromMap = <T>(
|
|
124
|
+
map: Map<com.sphereon.cbor.CborItem<any>, com.sphereon.cbor.CborItem<any>>,
|
|
125
|
+
key: number,
|
|
126
|
+
): T | undefined | never => {
|
|
127
|
+
const value = map.get(new com.sphereon.cbor.CborUInt(kmp.LongKMP.fromNumber(key)))
|
|
128
|
+
if (!value) {
|
|
129
|
+
return undefined
|
|
130
|
+
}
|
|
131
|
+
return value.value as T
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export const decodeStatusListCWT = (cwt: string): DecodedStatusListPayload => {
|
|
135
|
+
const encodedCbor = base64url.toBuffer(cwt)
|
|
136
|
+
const encodedCborArray = new Int8Array(encodedCbor)
|
|
137
|
+
const decodedCbor = com.sphereon.cbor.Cbor.decode(encodedCborArray)
|
|
138
|
+
|
|
139
|
+
if (!(decodedCbor instanceof com.sphereon.cbor.CborArray)) {
|
|
140
|
+
throw new Error('Invalid CWT format: Expected a CBOR array')
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const [, payload] = decodedCbor.value.asJsArrayView()
|
|
144
|
+
if (!(payload instanceof com.sphereon.cbor.CborByteString)) {
|
|
145
|
+
throw new Error('Invalid payload format: Expected a CBOR ByteString')
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const claims = com.sphereon.cbor.Cbor.decode(payload.value)
|
|
149
|
+
if (!(claims instanceof com.sphereon.cbor.CborMap)) {
|
|
150
|
+
throw new Error('Invalid claims format: Expected a CBOR map')
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const claimsMap = claims.value.asJsMapView()
|
|
154
|
+
|
|
155
|
+
const statusListMap = claimsMap.get(new com.sphereon.cbor.CborUInt(kmp.LongKMP.fromNumber(65533))).value.asJsMapView()
|
|
156
|
+
|
|
157
|
+
const bits = Number(statusListMap.get(new com.sphereon.cbor.CborString('bits')).value) as BitsPerStatus
|
|
158
|
+
const decoded = new Uint8Array(statusListMap.get(new com.sphereon.cbor.CborString('lst')).value)
|
|
159
|
+
const uint8Array = inflate(decoded)
|
|
160
|
+
const rawStatusList = decompressRawStatusList(uint8Array, bits)
|
|
161
|
+
const statusList = new StatusList(rawStatusList, bits)
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
issuer: getCborValueFromMap<string>(claimsMap, CWT_CLAIMS.ISSUER),
|
|
165
|
+
id: getCborValueFromMap<string>(claimsMap, CWT_CLAIMS.SUBJECT),
|
|
166
|
+
statusList,
|
|
167
|
+
iat: Number(getCborValueFromMap<number>(claimsMap, CWT_CLAIMS.ISSUED_AT)),
|
|
168
|
+
exp: getCborOptionalValueFromMap<number>(claimsMap, CWT_CLAIMS.EXPIRATION),
|
|
169
|
+
ttl: getCborOptionalValueFromMap<number>(claimsMap, CWT_CLAIMS.TIME_TO_LIVE),
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { IRequiredContext } from '../../types'
|
|
2
|
+
import { StatusList } from '@sd-jwt/jwt-status-list'
|
|
3
|
+
|
|
4
|
+
export interface DecodedStatusListPayload {
|
|
5
|
+
issuer: string
|
|
6
|
+
id: string
|
|
7
|
+
statusList: StatusList
|
|
8
|
+
exp?: number
|
|
9
|
+
ttl?: number
|
|
10
|
+
iat: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const resolveIdentifier = async (context: IRequiredContext, issuer: string, keyRef?: string) => {
|
|
14
|
+
return await context.agent.identifierManagedGet({
|
|
15
|
+
identifier: issuer,
|
|
16
|
+
vmRelationship: 'assertionMethod',
|
|
17
|
+
offlineWhenNoDIDRegistered: true,
|
|
18
|
+
...(keyRef && { kmsKeyRef: keyRef }), // TODO the getDid resolver should look at this ASAP
|
|
19
|
+
})
|
|
20
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { CompactJWT, JoseSignatureAlgorithm } from '@sphereon/ssi-types'
|
|
2
|
+
import { createHeaderAndPayload, StatusList, StatusListJWTHeaderParameters, StatusListJWTPayload } from '@sd-jwt/jwt-status-list'
|
|
3
|
+
import base64url from 'base64url'
|
|
4
|
+
import { JWTPayload } from 'did-jwt'
|
|
5
|
+
import { IRequiredContext, SignedStatusListData } from '../../types'
|
|
6
|
+
import { DecodedStatusListPayload, resolveIdentifier } from './common'
|
|
7
|
+
import { TKeyType } from '@veramo/core'
|
|
8
|
+
import { ensureManagedIdentifierResult } from '@sphereon/ssi-sdk-ext.identifier-resolution'
|
|
9
|
+
|
|
10
|
+
const STATUS_LIST_JWT_TYP = 'statuslist+jwt'
|
|
11
|
+
|
|
12
|
+
export const createSignedJwt = async (
|
|
13
|
+
context: IRequiredContext,
|
|
14
|
+
statusList: StatusList,
|
|
15
|
+
issuerString: string,
|
|
16
|
+
id: string,
|
|
17
|
+
expiresAt?: Date,
|
|
18
|
+
keyRef?: string,
|
|
19
|
+
): Promise<SignedStatusListData> => {
|
|
20
|
+
const identifier = await resolveIdentifier(context, issuerString, keyRef)
|
|
21
|
+
const resolution = await ensureManagedIdentifierResult(identifier, context)
|
|
22
|
+
|
|
23
|
+
const payload: JWTPayload = {
|
|
24
|
+
iss: issuerString,
|
|
25
|
+
sub: id,
|
|
26
|
+
iat: Math.floor(Date.now() / 1000),
|
|
27
|
+
...(expiresAt && { exp: Math.floor(expiresAt.getTime() / 1000) }),
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const header: StatusListJWTHeaderParameters = {
|
|
31
|
+
alg: getSigningAlgo(resolution.key.type),
|
|
32
|
+
typ: STATUS_LIST_JWT_TYP,
|
|
33
|
+
}
|
|
34
|
+
const values = createHeaderAndPayload(statusList, payload, header)
|
|
35
|
+
const signedJwt = await context.agent.jwtCreateJwsCompactSignature({
|
|
36
|
+
issuer: { ...identifier, noIssPayloadUpdate: false },
|
|
37
|
+
protectedHeader: values.header,
|
|
38
|
+
payload: values.payload,
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
statusListCredential: signedJwt.jwt,
|
|
43
|
+
encodedList: (values.payload as StatusListJWTPayload).status_list.lst,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const decodeStatusListJWT = (jwt: CompactJWT): DecodedStatusListPayload => {
|
|
48
|
+
const [, payloadBase64] = jwt.split('.')
|
|
49
|
+
const payload = JSON.parse(base64url.decode(payloadBase64))
|
|
50
|
+
|
|
51
|
+
if (!payload.iss || !payload.sub || !payload.status_list) {
|
|
52
|
+
throw new Error('Missing required fields in JWT payload')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const statusList = StatusList.decompressStatusList(payload.status_list.lst, payload.status_list.bits)
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
issuer: payload.iss,
|
|
59
|
+
id: payload.sub,
|
|
60
|
+
statusList,
|
|
61
|
+
exp: payload.exp,
|
|
62
|
+
ttl: payload.ttl,
|
|
63
|
+
iat: payload.iat,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const getSigningAlgo = (type: TKeyType): JoseSignatureAlgorithm => {
|
|
68
|
+
switch (type) {
|
|
69
|
+
case 'Ed25519':
|
|
70
|
+
return JoseSignatureAlgorithm.EdDSA
|
|
71
|
+
case 'Secp256k1':
|
|
72
|
+
return JoseSignatureAlgorithm.ES256K
|
|
73
|
+
case 'Secp256r1':
|
|
74
|
+
return JoseSignatureAlgorithm.ES256
|
|
75
|
+
case 'RSA':
|
|
76
|
+
return JoseSignatureAlgorithm.RS256
|
|
77
|
+
default:
|
|
78
|
+
throw Error('Key type not yet supported')
|
|
79
|
+
}
|
|
80
|
+
}
|