@sphereon/ssi-sdk.ebsi-support 0.26.1-unstable.101
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/LICENSE +201 -0
- package/README.md +13 -0
- package/dist/agent/EbsiSupport.d.ts +12 -0
- package/dist/agent/EbsiSupport.d.ts.map +1 -0
- package/dist/agent/EbsiSupport.js +202 -0
- package/dist/agent/EbsiSupport.js.map +1 -0
- package/dist/did/EbsiDidProvider.d.ts +47 -0
- package/dist/did/EbsiDidProvider.d.ts.map +1 -0
- package/dist/did/EbsiDidProvider.js +172 -0
- package/dist/did/EbsiDidProvider.js.map +1 -0
- package/dist/did/EbsiDidResolver.d.ts +5 -0
- package/dist/did/EbsiDidResolver.d.ts.map +1 -0
- package/dist/did/EbsiDidResolver.js +10 -0
- package/dist/did/EbsiDidResolver.js.map +1 -0
- package/dist/did/functions.d.ts +66 -0
- package/dist/did/functions.d.ts.map +1 -0
- package/dist/did/functions.js +416 -0
- package/dist/did/functions.js.map +1 -0
- package/dist/did/index.d.ts +6 -0
- package/dist/did/index.d.ts.map +1 -0
- package/dist/did/index.js +6 -0
- package/dist/did/index.js.map +1 -0
- package/dist/did/services/EbsiRPCService.d.ts +13 -0
- package/dist/did/services/EbsiRPCService.d.ts.map +1 -0
- package/dist/did/services/EbsiRPCService.js +64 -0
- package/dist/did/services/EbsiRPCService.js.map +1 -0
- package/dist/did/services/EbsiRestService.d.ts +37 -0
- package/dist/did/services/EbsiRestService.d.ts.map +1 -0
- package/dist/did/services/EbsiRestService.js +90 -0
- package/dist/did/services/EbsiRestService.js.map +1 -0
- package/dist/did/types.d.ts +386 -0
- package/dist/did/types.d.ts.map +1 -0
- package/dist/did/types.js +47 -0
- package/dist/did/types.js.map +1 -0
- package/dist/functions/Attestation.d.ts +32 -0
- package/dist/functions/Attestation.d.ts.map +1 -0
- package/dist/functions/Attestation.js +182 -0
- package/dist/functions/Attestation.js.map +1 -0
- package/dist/functions/AttestationHeadlessCallbacks.d.ts +17 -0
- package/dist/functions/AttestationHeadlessCallbacks.d.ts.map +1 -0
- package/dist/functions/AttestationHeadlessCallbacks.js +194 -0
- package/dist/functions/AttestationHeadlessCallbacks.js.map +1 -0
- package/dist/functions/index.d.ts +7 -0
- package/dist/functions/index.d.ts.map +1 -0
- package/dist/functions/index.js +8 -0
- package/dist/functions/index.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/types/IEbsiSupport.d.ts +211 -0
- package/dist/types/IEbsiSupport.d.ts.map +1 -0
- package/dist/types/IEbsiSupport.js +5 -0
- package/dist/types/IEbsiSupport.js.map +1 -0
- package/package.json +86 -0
- package/src/agent/EbsiSupport.ts +250 -0
- package/src/did/EbsiDidProvider.ts +269 -0
- package/src/did/EbsiDidResolver.ts +16 -0
- package/src/did/functions.ts +528 -0
- package/src/did/index.ts +5 -0
- package/src/did/services/EbsiRPCService.ts +68 -0
- package/src/did/services/EbsiRestService.ts +117 -0
- package/src/did/types.ts +449 -0
- package/src/functions/Attestation.ts +262 -0
- package/src/functions/AttestationHeadlessCallbacks.ts +242 -0
- package/src/functions/index.ts +15 -0
- package/src/index.ts +8 -0
- package/src/types/IEbsiSupport.ts +241 -0
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
import { randomBytes } from '@ethersproject/random'
|
|
2
|
+
import { CreateRequestObjectMode } from '@sphereon/oid4vci-common'
|
|
3
|
+
import { getControllerKey, getEthereumAddressFromKey, getKeys } from '@sphereon/ssi-sdk-ext.did-utils'
|
|
4
|
+
import { calculateJwkThumbprint, calculateJwkThumbprintForKey, JwkKeyUse, toJwk } from '@sphereon/ssi-sdk-ext.key-utils'
|
|
5
|
+
import { W3CVerifiableCredential } from '@sphereon/ssi-types'
|
|
6
|
+
import { IAgentContext, IIdentifier, IKey, IKeyManager, MinimalImportableKey, TKeyType } from '@veramo/core'
|
|
7
|
+
import { getBytes, SigningKey, Transaction } from 'ethers'
|
|
8
|
+
import { base58btc } from 'multiformats/bases/base58'
|
|
9
|
+
import * as u8a from 'uint8arrays'
|
|
10
|
+
import { getEbsiApiBaseUrl, wait } from '../functions'
|
|
11
|
+
import { logger } from '../index'
|
|
12
|
+
import { ApiOpts, EbsiApiVersion, EbsiEnvironment, IRequiredContext, WellknownOpts } from '../types/IEbsiSupport'
|
|
13
|
+
import { ebsiWaitTillDocumentAnchored } from './services/EbsiRestService'
|
|
14
|
+
import { callRpcMethod } from './services/EbsiRPCService'
|
|
15
|
+
import {
|
|
16
|
+
BASE_CONTEXT_DOC,
|
|
17
|
+
CreateEbsiDidParams,
|
|
18
|
+
EBSI_DID_SPEC_INFOS,
|
|
19
|
+
EbsiDidRegistryAPIEndpoints,
|
|
20
|
+
EbsiDidSpecInfo,
|
|
21
|
+
EbsiKeyType,
|
|
22
|
+
EbsiPublicKeyPurpose,
|
|
23
|
+
EbsiRpcMethod,
|
|
24
|
+
EbsiRPCResponse,
|
|
25
|
+
IContext,
|
|
26
|
+
IKeyOpts,
|
|
27
|
+
RpcMethodArgs,
|
|
28
|
+
RpcOkResponse,
|
|
29
|
+
} from './types'
|
|
30
|
+
|
|
31
|
+
export function generateEbsiMethodSpecificId(specInfo?: EbsiDidSpecInfo): string {
|
|
32
|
+
const spec = specInfo ?? EBSI_DID_SPEC_INFOS.V1
|
|
33
|
+
const length = spec.didLength ?? 16
|
|
34
|
+
|
|
35
|
+
const result = new Uint8Array(length + (spec.version ? 1 : 0))
|
|
36
|
+
if (spec.version) {
|
|
37
|
+
result.set([spec.version])
|
|
38
|
+
}
|
|
39
|
+
result.set(randomBytes(length), spec.version ? 1 : 0)
|
|
40
|
+
return base58btc.encode(result)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function generateOrUseProvidedEbsiPrivateKeyHex(specInfo?: EbsiDidSpecInfo, privateKeyBytes?: Uint8Array): string {
|
|
44
|
+
const spec = specInfo ?? EBSI_DID_SPEC_INFOS.V1
|
|
45
|
+
const length = spec.didLength ? 2 * spec.didLength : 32
|
|
46
|
+
|
|
47
|
+
if (privateKeyBytes) {
|
|
48
|
+
if (privateKeyBytes.length !== length) {
|
|
49
|
+
throw Error(`Invalid private key length supplied (${privateKeyBytes.length}. Expected ${length} for ${spec.type}`)
|
|
50
|
+
}
|
|
51
|
+
return u8a.toString(privateKeyBytes, 'base16')
|
|
52
|
+
}
|
|
53
|
+
return u8a.toString(randomBytes(length), 'base16')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Returns the public key in the correct format to be used with the did registry v5
|
|
58
|
+
* - in case of Secp256k1 - returns the uncompressed public key as hex string prefixed with 0x04
|
|
59
|
+
* - in case of Secp256r1 - returns the jwk public key as hex string
|
|
60
|
+
* @param {{ key: IKey, type: EbsiKeyType }} args
|
|
61
|
+
* - key is the cryptographic key containing the public key
|
|
62
|
+
* - type is the type of the key which can be Secp256k1 or Secp256r1
|
|
63
|
+
* @returns {string} The properly formatted public key
|
|
64
|
+
* @throws {Error} If the key type is invalid
|
|
65
|
+
*/
|
|
66
|
+
export const formatEbsiPublicKey = (args: { key: IKey; type: TKeyType }): string => {
|
|
67
|
+
const { key, type } = args
|
|
68
|
+
switch (type) {
|
|
69
|
+
case 'Secp256k1': {
|
|
70
|
+
const bytes = getBytes('0x' + key.publicKeyHex, 'key')
|
|
71
|
+
return SigningKey.computePublicKey(bytes, false)
|
|
72
|
+
}
|
|
73
|
+
case 'Secp256r1': {
|
|
74
|
+
/*
|
|
75
|
+
Public key as hex string. For an ES256K key, it must be in uncompressed format prefixed with "0x04".
|
|
76
|
+
For other algorithms, it must be the JWK transformed to string and then to hex format.
|
|
77
|
+
*/
|
|
78
|
+
const jwk: JsonWebKey = toJwk(key.publicKeyHex, type, { use: JwkKeyUse.Signature, key })
|
|
79
|
+
/*
|
|
80
|
+
Converting JWK to string and then hex is odd and may lead to errors. Implementing
|
|
81
|
+
it like that because it's how EBSI does it. However, it may be a point of pain
|
|
82
|
+
in the future.
|
|
83
|
+
*/
|
|
84
|
+
const jwkString = JSON.stringify(jwk, null, 2)
|
|
85
|
+
return `0x${u8a.toString(u8a.fromString(jwkString), 'base16')}`
|
|
86
|
+
}
|
|
87
|
+
default:
|
|
88
|
+
throw new Error(`Unsupported key type: ${type}`)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const ebsiGetIssuerMock = (args: { environment?: EbsiEnvironment; version?: EbsiApiVersion }): string => {
|
|
93
|
+
const { environment = 'conformance', version = 'v3' } = args
|
|
94
|
+
if (environment === 'pilot') {
|
|
95
|
+
throw Error(`EBSI Pilot network does not have a issuer mock server`)
|
|
96
|
+
}
|
|
97
|
+
return `${getEbsiApiBaseUrl({ environment, version, system: environment })}/issuer-mock`
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const ebsiGetAuthorisationServer = (args: { environment?: EbsiEnvironment; version?: EbsiApiVersion }): string => {
|
|
101
|
+
const { environment = 'pilot', version = 'v4' } = args
|
|
102
|
+
return `${getEbsiApiBaseUrl({ environment, version, system: 'authorisation' })}`
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const ebsiGetRegistryAPIUrls = (args: { environment?: EbsiEnvironment; version?: EbsiApiVersion }): EbsiDidRegistryAPIEndpoints => {
|
|
106
|
+
const { environment = 'pilot', version = 'v5' } = args
|
|
107
|
+
const baseUrl = `${getEbsiApiBaseUrl({ environment, version, system: 'did-registry' })}`
|
|
108
|
+
return {
|
|
109
|
+
mutate: `${baseUrl}/jsonrpc`,
|
|
110
|
+
query: `${baseUrl}/identifiers`,
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export const determineWellknownEndpoint = ({ environment, version, type, system = environment, mock }: WellknownOpts): string => {
|
|
115
|
+
const url = `${getEbsiApiBaseUrl({ environment, version, system })}${mock ? `/${mock}` : ''}/.well-known/${type}`
|
|
116
|
+
logger.debug(`wellknown url: ${url}`)
|
|
117
|
+
return url
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const ebsiSignAndSendTransaction = async (
|
|
121
|
+
args: {
|
|
122
|
+
rpcRequest: RpcMethodArgs
|
|
123
|
+
previousTxResponse?: EbsiRPCResponse
|
|
124
|
+
kid: string
|
|
125
|
+
accessToken: string
|
|
126
|
+
apiOpts?: ApiOpts
|
|
127
|
+
},
|
|
128
|
+
context: IContext,
|
|
129
|
+
): Promise<EbsiRPCResponse> => {
|
|
130
|
+
const { rpcRequest, accessToken, kid, apiOpts, previousTxResponse } = args
|
|
131
|
+
const unsignedTxResponse = await callRpcMethod(rpcRequest)
|
|
132
|
+
const nonce = 'result' in unsignedTxResponse ? unsignedTxResponse.result.nonce : undefined
|
|
133
|
+
// We should get a new nonce once the actual previous transaction has been anchored. Thus we retry if the nonce remains the same
|
|
134
|
+
if (
|
|
135
|
+
previousTxResponse &&
|
|
136
|
+
'result' in unsignedTxResponse &&
|
|
137
|
+
'nonce' in previousTxResponse &&
|
|
138
|
+
'nonce' in unsignedTxResponse.result &&
|
|
139
|
+
typeof unsignedTxResponse.result === 'object' &&
|
|
140
|
+
previousTxResponse.nonce === unsignedTxResponse.result.nonce
|
|
141
|
+
) {
|
|
142
|
+
await wait(1_000)
|
|
143
|
+
return await ebsiSignAndSendTransaction({ ...args, previousTxResponse }, context)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if ('error' in unsignedTxResponse && !!unsignedTxResponse.error) {
|
|
147
|
+
logger.error(JSON.stringify(unsignedTxResponse))
|
|
148
|
+
throw new Error(unsignedTxResponse.error.message ?? 'Unknown error occurred')
|
|
149
|
+
}
|
|
150
|
+
const unsignedTx = (unsignedTxResponse as RpcOkResponse).result
|
|
151
|
+
|
|
152
|
+
const agentUnsignedTx = JSON.parse(JSON.stringify(unsignedTx))
|
|
153
|
+
if (unsignedTx && 'chainId' in unsignedTx && typeof unsignedTx.chainId === 'string' && unsignedTx.chainId.toLowerCase().startsWith('0x')) {
|
|
154
|
+
// We expect the chain id to be a regular number and not a hex string
|
|
155
|
+
agentUnsignedTx.chainId = Number.parseInt(unsignedTx.chainId, 16)
|
|
156
|
+
}
|
|
157
|
+
const signedRawTx = await context.agent.keyManagerSignEthTX({
|
|
158
|
+
kid,
|
|
159
|
+
transaction: agentUnsignedTx,
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
const sig = Transaction.from(signedRawTx).signature!
|
|
163
|
+
const { r, s, v } = sig
|
|
164
|
+
|
|
165
|
+
const sTResponse = await callRpcMethod({
|
|
166
|
+
params: [
|
|
167
|
+
{
|
|
168
|
+
protocol: 'eth',
|
|
169
|
+
unsignedTransaction: unsignedTx,
|
|
170
|
+
r,
|
|
171
|
+
s,
|
|
172
|
+
v: `0x${v.toString(16)}`,
|
|
173
|
+
signedRawTransaction: signedRawTx,
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
rpcMethod: EbsiRpcMethod.SEND_SIGNED_TRANSACTION,
|
|
177
|
+
rpcId: unsignedTxResponse.id,
|
|
178
|
+
apiOpts,
|
|
179
|
+
accessToken: accessToken,
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
if ('status' in sTResponse) {
|
|
183
|
+
throw new Error(JSON.stringify(sTResponse, null, 2))
|
|
184
|
+
}
|
|
185
|
+
return { ...sTResponse, nonce }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export const ebsiGenerateOrUseKeyPair = async (
|
|
189
|
+
args: {
|
|
190
|
+
keyOpts?: IKeyOpts
|
|
191
|
+
keyType: EbsiKeyType
|
|
192
|
+
kms: string
|
|
193
|
+
controllerKey?: boolean
|
|
194
|
+
},
|
|
195
|
+
context: IAgentContext<IKeyManager>,
|
|
196
|
+
) => {
|
|
197
|
+
const { keyOpts, keyType, kms, controllerKey = false } = args
|
|
198
|
+
let privateKeyHex = generateOrUseProvidedEbsiPrivateKeyHex(
|
|
199
|
+
EBSI_DID_SPEC_INFOS.V1,
|
|
200
|
+
keyOpts?.privateKeyHex ? u8a.fromString(keyOpts.privateKeyHex, 'base16') : undefined,
|
|
201
|
+
)
|
|
202
|
+
if (privateKeyHex.startsWith('0x')) {
|
|
203
|
+
privateKeyHex = privateKeyHex.substring(2)
|
|
204
|
+
}
|
|
205
|
+
if (!privateKeyHex || privateKeyHex.length !== 64) {
|
|
206
|
+
throw new Error('Private key should be 32 bytes / 64 chars hex')
|
|
207
|
+
}
|
|
208
|
+
const importableKey = await toMinimalImportableKey({ key: { ...keyOpts, privateKeyHex }, type: keyType, kms })
|
|
209
|
+
|
|
210
|
+
if (keyType === 'Secp256k1') {
|
|
211
|
+
importableKey.meta = {
|
|
212
|
+
...importableKey.meta,
|
|
213
|
+
ebsi: {
|
|
214
|
+
anchored: false,
|
|
215
|
+
controllerKey,
|
|
216
|
+
},
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return importableKey
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export const toMinimalImportableKey = async (args: { key?: IKeyOpts; type: EbsiKeyType; kms: string }): Promise<MinimalImportableKey> => {
|
|
223
|
+
const { key, kms } = args
|
|
224
|
+
const minimalImportableKey: Partial<MinimalImportableKey> = { ...key }
|
|
225
|
+
const type = args.key?.type ?? args.type
|
|
226
|
+
minimalImportableKey.kms = kms
|
|
227
|
+
minimalImportableKey.type = type
|
|
228
|
+
if (!minimalImportableKey.privateKeyHex) {
|
|
229
|
+
throw Error(`Minimal importable key needs a private key`)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
minimalImportableKey.meta = {
|
|
233
|
+
purposes: assertedPurposes({ key }) ?? setDefaultPurposes({ key, type }),
|
|
234
|
+
jwkThumbprint: calculateJwkThumbprintForKey({
|
|
235
|
+
key: minimalImportableKey as MinimalImportableKey,
|
|
236
|
+
digestAlgorithm: 'sha256',
|
|
237
|
+
}),
|
|
238
|
+
}
|
|
239
|
+
return minimalImportableKey as MinimalImportableKey
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export const assertedPurposes = (args: { key?: IKeyOpts }): EbsiPublicKeyPurpose[] | undefined => {
|
|
243
|
+
const { key } = args
|
|
244
|
+
if (key?.purposes && key.purposes.length > 0) {
|
|
245
|
+
switch (key.type) {
|
|
246
|
+
case 'Secp256k1': {
|
|
247
|
+
if (key?.purposes && key.purposes.length > 0 && key.purposes?.includes(EbsiPublicKeyPurpose.CapabilityInvocation)) {
|
|
248
|
+
return key.purposes
|
|
249
|
+
}
|
|
250
|
+
throw new Error(`Secp256k1/ES256K key requires ${EbsiPublicKeyPurpose.CapabilityInvocation} purpose`)
|
|
251
|
+
}
|
|
252
|
+
case 'Secp256r1': {
|
|
253
|
+
if (
|
|
254
|
+
key?.purposes &&
|
|
255
|
+
key.purposes.length > 0 &&
|
|
256
|
+
key.purposes.every((purpose) => [EbsiPublicKeyPurpose.AssertionMethod, EbsiPublicKeyPurpose.Authentication].includes(purpose))
|
|
257
|
+
) {
|
|
258
|
+
return key.purposes
|
|
259
|
+
}
|
|
260
|
+
throw new Error(
|
|
261
|
+
`Secp256r1/ES256 key requires ${[EbsiPublicKeyPurpose.AssertionMethod, EbsiPublicKeyPurpose.Authentication].join(', ')} purposes`,
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
default:
|
|
265
|
+
throw new Error(`Unsupported key type: ${key.type}`)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return key?.purposes
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export const setDefaultPurposes = (args: { key?: IKeyOpts; type: EbsiKeyType }): EbsiPublicKeyPurpose[] => {
|
|
272
|
+
const { key, type } = args
|
|
273
|
+
if (!key?.purposes || key.purposes.length === 0) {
|
|
274
|
+
switch (type) {
|
|
275
|
+
case 'Secp256k1':
|
|
276
|
+
return [EbsiPublicKeyPurpose.CapabilityInvocation]
|
|
277
|
+
case 'Secp256r1':
|
|
278
|
+
return [EbsiPublicKeyPurpose.AssertionMethod, EbsiPublicKeyPurpose.Authentication]
|
|
279
|
+
default:
|
|
280
|
+
throw new Error(`Unsupported key type: ${key?.type}`)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return key.purposes
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export const randomRpcId = (): number => {
|
|
287
|
+
return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export const ebsiCreateDidOnLedger = async (
|
|
291
|
+
args: CreateEbsiDidParams,
|
|
292
|
+
context: IRequiredContext,
|
|
293
|
+
): Promise<{
|
|
294
|
+
identifier: IIdentifier
|
|
295
|
+
addVerificationMethod: EbsiRPCResponse
|
|
296
|
+
insertDidDoc: EbsiRPCResponse
|
|
297
|
+
addAssertionMethodRelationship: EbsiRPCResponse
|
|
298
|
+
addAuthenticationRelationship: EbsiRPCResponse
|
|
299
|
+
}> => {
|
|
300
|
+
const {
|
|
301
|
+
accessTokenOpts,
|
|
302
|
+
notBefore = Math.floor(Date.now() / 1000 - 60),
|
|
303
|
+
notAfter = Math.floor(Date.now() / 1000 + 10 * 365 * 24 * 60 * 60),
|
|
304
|
+
baseDocument,
|
|
305
|
+
identifier,
|
|
306
|
+
} = args
|
|
307
|
+
const { clientId, redirectUri, environment, credentialIssuer } = accessTokenOpts
|
|
308
|
+
const controllerKey = getControllerKey({ identifier })
|
|
309
|
+
const secp256r1 = getKeys({ identifier, keyType: 'Secp256r1' })?.[0]
|
|
310
|
+
let { attestationToOnboard } = accessTokenOpts
|
|
311
|
+
|
|
312
|
+
if (!controllerKey || !secp256r1) {
|
|
313
|
+
return Promise.reject(`No secp256k1 controller key and/or secp2561r key found for identifier ${identifier}`)
|
|
314
|
+
}
|
|
315
|
+
const from = getEthereumAddressFromKey({ key: controllerKey })
|
|
316
|
+
if (!from) {
|
|
317
|
+
return Promise.reject(Error(`EBSI 'from' address expected for key ${controllerKey.publicKeyHex}`))
|
|
318
|
+
}
|
|
319
|
+
const did = identifier.did
|
|
320
|
+
const kid = controllerKey.kid
|
|
321
|
+
const idOpts = { identifier, kid }
|
|
322
|
+
let rpcId = args.rpcId ?? randomRpcId()
|
|
323
|
+
const apiOpts = {
|
|
324
|
+
environment,
|
|
325
|
+
version: 'v5',
|
|
326
|
+
} satisfies ApiOpts
|
|
327
|
+
|
|
328
|
+
const jwksUri = args.accessTokenOpts.jwksUri ?? `${clientId}/.well-known/jwks/dids/${encodeURIComponent(identifier.did)}.json`
|
|
329
|
+
|
|
330
|
+
if (!attestationToOnboard) {
|
|
331
|
+
const authReqResult = await context.agent.ebsiCreateAttestationAuthRequestURL({
|
|
332
|
+
credentialIssuer,
|
|
333
|
+
idOpts,
|
|
334
|
+
formats: ['jwt_vc'],
|
|
335
|
+
clientId,
|
|
336
|
+
redirectUri,
|
|
337
|
+
credentialType: 'VerifiableAuthorisationToOnboard',
|
|
338
|
+
requestObjectOpts: { iss: clientId, requestObjectMode: CreateRequestObjectMode.REQUEST_OBJECT, jwksUri },
|
|
339
|
+
})
|
|
340
|
+
const attestationResult = await context.agent.ebsiGetAttestation({
|
|
341
|
+
clientId,
|
|
342
|
+
authReqResult,
|
|
343
|
+
opts: { timeout: 120_000 },
|
|
344
|
+
})
|
|
345
|
+
attestationToOnboard = attestationResult.credentials[0].rawVerifiableCredential as W3CVerifiableCredential
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const insertDidAccessTokenResponse = await context.agent.ebsiAccessTokenGet({
|
|
349
|
+
attestationCredential: attestationToOnboard,
|
|
350
|
+
jwksUri,
|
|
351
|
+
scope: 'didr_invite',
|
|
352
|
+
idOpts,
|
|
353
|
+
redirectUri,
|
|
354
|
+
credentialIssuer,
|
|
355
|
+
clientId,
|
|
356
|
+
environment,
|
|
357
|
+
skipDidResolution: true,
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
const insertDidDocRequest = {
|
|
361
|
+
params: [
|
|
362
|
+
{
|
|
363
|
+
from,
|
|
364
|
+
did,
|
|
365
|
+
baseDocument: baseDocument ?? BASE_CONTEXT_DOC,
|
|
366
|
+
vMethodId: calculateJwkThumbprint({ jwk: toJwk(controllerKey.publicKeyHex, 'Secp256k1') }),
|
|
367
|
+
isSecp256k1: true,
|
|
368
|
+
publicKey: formatEbsiPublicKey({ key: controllerKey, type: 'Secp256k1' }),
|
|
369
|
+
notBefore,
|
|
370
|
+
notAfter,
|
|
371
|
+
},
|
|
372
|
+
],
|
|
373
|
+
rpcMethod: EbsiRpcMethod.INSERT_DID_DOCUMENT,
|
|
374
|
+
rpcId,
|
|
375
|
+
apiOpts,
|
|
376
|
+
accessToken: insertDidAccessTokenResponse.accessTokenResponse.access_token,
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const insertDidDocResponse = await ebsiSignAndSendTransaction(
|
|
380
|
+
{
|
|
381
|
+
rpcRequest: insertDidDocRequest,
|
|
382
|
+
kid,
|
|
383
|
+
accessToken: insertDidAccessTokenResponse.accessTokenResponse.access_token,
|
|
384
|
+
apiOpts,
|
|
385
|
+
},
|
|
386
|
+
context,
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
let anchorTime = await ebsiWaitTillDocumentAnchored({
|
|
390
|
+
did,
|
|
391
|
+
...apiOpts,
|
|
392
|
+
maxWaitTime: 30_000,
|
|
393
|
+
startIntervalMS: 2000,
|
|
394
|
+
minIntervalMS: 500,
|
|
395
|
+
decreaseIntervalMSPerStep: 750,
|
|
396
|
+
})
|
|
397
|
+
if (!anchorTime.didDocument) {
|
|
398
|
+
throw Error(`did ${did} was not registered on EBSI network ${apiOpts.environment} in 45 seconds`)
|
|
399
|
+
}
|
|
400
|
+
logger.debug(`Anchoring did ${did} on network ${apiOpts.environment} took ${anchorTime.totalWaitTime / 1000} seconds in ${anchorTime.count} tries`)
|
|
401
|
+
|
|
402
|
+
// Update to the controller key for the remainder
|
|
403
|
+
idOpts.kid = calculateJwkThumbprintForKey({ key: controllerKey })
|
|
404
|
+
|
|
405
|
+
const addVMAccessTokenResponse = await context.agent.ebsiAccessTokenGet({
|
|
406
|
+
// attestationCredential: attestationToOnboard,
|
|
407
|
+
jwksUri,
|
|
408
|
+
scope: 'didr_write',
|
|
409
|
+
idOpts,
|
|
410
|
+
redirectUri,
|
|
411
|
+
credentialIssuer: undefined,
|
|
412
|
+
clientId,
|
|
413
|
+
environment,
|
|
414
|
+
skipDidResolution: true,
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
const vMethodId = calculateJwkThumbprint({ jwk: toJwk(secp256r1.publicKeyHex, 'Secp256r1') })
|
|
418
|
+
const publicKey = formatEbsiPublicKey({ key: secp256r1, type: 'Secp256r1' })
|
|
419
|
+
const addVerificationMethodRequest = {
|
|
420
|
+
params: [
|
|
421
|
+
{
|
|
422
|
+
from,
|
|
423
|
+
did,
|
|
424
|
+
isSecp256k1: false,
|
|
425
|
+
vMethodId,
|
|
426
|
+
publicKey,
|
|
427
|
+
},
|
|
428
|
+
],
|
|
429
|
+
rpcMethod: EbsiRpcMethod.ADD_VERIFICATION_METHOD,
|
|
430
|
+
rpcId,
|
|
431
|
+
apiOpts,
|
|
432
|
+
accessToken: addVMAccessTokenResponse.accessTokenResponse.access_token,
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const addVerificationMethodResponse = await ebsiSignAndSendTransaction(
|
|
436
|
+
{
|
|
437
|
+
rpcRequest: addVerificationMethodRequest,
|
|
438
|
+
previousTxResponse: insertDidDocResponse,
|
|
439
|
+
kid,
|
|
440
|
+
accessToken: addVMAccessTokenResponse.accessTokenResponse.access_token,
|
|
441
|
+
apiOpts,
|
|
442
|
+
},
|
|
443
|
+
context,
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
// We need to wait, even after the anchor. The methods below also retry in case the nonce does not get updated.
|
|
447
|
+
// But we simply know that at this point we need to introduce some delay
|
|
448
|
+
await wait(2_000)
|
|
449
|
+
|
|
450
|
+
const addAssertionMethodRelationshipRequest = {
|
|
451
|
+
params: [
|
|
452
|
+
{
|
|
453
|
+
from,
|
|
454
|
+
did,
|
|
455
|
+
vMethodId,
|
|
456
|
+
name: 'assertionMethod',
|
|
457
|
+
notAfter,
|
|
458
|
+
notBefore,
|
|
459
|
+
},
|
|
460
|
+
],
|
|
461
|
+
rpcMethod: EbsiRpcMethod.ADD_VERIFICATION_RELATIONSHIP,
|
|
462
|
+
rpcId,
|
|
463
|
+
apiOpts,
|
|
464
|
+
accessToken: addVMAccessTokenResponse.accessTokenResponse.access_token,
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const addAssertionMethodRelationshipResponse = await ebsiSignAndSendTransaction(
|
|
468
|
+
{
|
|
469
|
+
rpcRequest: addAssertionMethodRelationshipRequest,
|
|
470
|
+
previousTxResponse: addVerificationMethodResponse,
|
|
471
|
+
kid,
|
|
472
|
+
accessToken: addVMAccessTokenResponse.accessTokenResponse.access_token,
|
|
473
|
+
apiOpts,
|
|
474
|
+
},
|
|
475
|
+
context,
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
anchorTime = await ebsiWaitTillDocumentAnchored({
|
|
479
|
+
did,
|
|
480
|
+
...apiOpts,
|
|
481
|
+
maxWaitTime: 20_000,
|
|
482
|
+
minIntervalMS: 500,
|
|
483
|
+
decreaseIntervalMSPerStep: 500,
|
|
484
|
+
searchForObject: { assertionMethod: [`${did}#${vMethodId}`] },
|
|
485
|
+
})
|
|
486
|
+
if (!anchorTime.didDocument) {
|
|
487
|
+
throw Error(`did ${did} assertionMethod id ${vMethodId} was not registered on EBSI network ${apiOpts.environment} in 20 seconds`)
|
|
488
|
+
}
|
|
489
|
+
logger.debug(
|
|
490
|
+
`Anchoring assertionMethod ${vMethodId} for DID ${did} on network ${apiOpts.environment} took ${anchorTime.totalWaitTime / 1000} seconds in ${anchorTime.count} tries`,
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
const addAuthenticationRelationshipRequest = {
|
|
494
|
+
params: [
|
|
495
|
+
{
|
|
496
|
+
from,
|
|
497
|
+
did,
|
|
498
|
+
vMethodId,
|
|
499
|
+
name: 'authentication',
|
|
500
|
+
notAfter,
|
|
501
|
+
notBefore,
|
|
502
|
+
},
|
|
503
|
+
],
|
|
504
|
+
rpcMethod: EbsiRpcMethod.ADD_VERIFICATION_RELATIONSHIP,
|
|
505
|
+
rpcId,
|
|
506
|
+
apiOpts,
|
|
507
|
+
accessToken: addVMAccessTokenResponse.accessTokenResponse.access_token,
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const addAuthenticationRelationshipResponse = await ebsiSignAndSendTransaction(
|
|
511
|
+
{
|
|
512
|
+
rpcRequest: addAuthenticationRelationshipRequest,
|
|
513
|
+
previousTxResponse: addAssertionMethodRelationshipResponse,
|
|
514
|
+
kid,
|
|
515
|
+
accessToken: addVMAccessTokenResponse.accessTokenResponse.access_token,
|
|
516
|
+
apiOpts,
|
|
517
|
+
},
|
|
518
|
+
context,
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
return {
|
|
522
|
+
identifier,
|
|
523
|
+
insertDidDoc: insertDidDocResponse,
|
|
524
|
+
addVerificationMethod: addVerificationMethodResponse,
|
|
525
|
+
addAuthenticationRelationship: addAuthenticationRelationshipResponse,
|
|
526
|
+
addAssertionMethodRelationship: addAssertionMethodRelationshipResponse,
|
|
527
|
+
}
|
|
528
|
+
}
|
package/src/did/index.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { EbsiDidProvider } from './EbsiDidProvider'
|
|
2
|
+
export { getDidEbsiResolver } from './EbsiDidResolver'
|
|
3
|
+
export { ebsiCreateDidOnLedger, randomRpcId, ebsiSignAndSendTransaction, ebsiGetRegistryAPIUrls } from './functions'
|
|
4
|
+
export * from './services/EbsiRestService'
|
|
5
|
+
export * from './types'
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import fetch from 'cross-fetch'
|
|
2
|
+
import { wait } from '../../functions'
|
|
3
|
+
import { logger } from '../../index'
|
|
4
|
+
import { ebsiGetRegistryAPIUrls, randomRpcId } from '../functions'
|
|
5
|
+
import { EbsiRPCResponse, JSON_RPC_VERSION, RpcMethodArgs } from '../types'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Allows to call 5 api methods of the EBSI RPC api
|
|
9
|
+
* - insertDidDocument
|
|
10
|
+
* - updateBaseDocument
|
|
11
|
+
* - addVerificationMethod
|
|
12
|
+
* - addVerificationMethodRelationship
|
|
13
|
+
* - sendSignedTransaction
|
|
14
|
+
* @function callRpcMethod
|
|
15
|
+
* @param {{ params: RPCParams[]; id: number; token: string; method: EbsiRpcMethod; apiOpts? ApiOpts }} args
|
|
16
|
+
*/
|
|
17
|
+
export const callRpcMethod = async (args: RpcMethodArgs): Promise<EbsiRPCResponse> => {
|
|
18
|
+
return callRpcMethodImpl({ ...args, retries: 10 })
|
|
19
|
+
}
|
|
20
|
+
const callRpcMethodImpl = async (args: RpcMethodArgs & { retries: number }): Promise<EbsiRPCResponse> => {
|
|
21
|
+
const { params, rpcId, accessToken, rpcMethod, apiOpts, doNotThrowErrors = false, retries } = args
|
|
22
|
+
const options = buildFetchOptions({ accessToken: accessToken, params, rpcId, rpcMethod })
|
|
23
|
+
logger.debug(`RPC call:\r\n ${JSON.stringify(options, null, 2)}`)
|
|
24
|
+
const rpcResponse = await (await fetch(ebsiGetRegistryAPIUrls({ ...apiOpts }).mutate, options)).json()
|
|
25
|
+
|
|
26
|
+
let result = rpcResponse.result
|
|
27
|
+
logger.debug(`RPC RESPONSE:\r\n${JSON.stringify(result ?? rpcResponse.error)}`)
|
|
28
|
+
|
|
29
|
+
if (rpcResponse.error !== undefined && !doNotThrowErrors) {
|
|
30
|
+
logger.error(`RPC ERROR RESPONSE:`, rpcResponse)
|
|
31
|
+
if (rpcResponse.error.message.includes(`replacement fee too low`)) {
|
|
32
|
+
args.rpcId = randomRpcId()
|
|
33
|
+
if (retries <= 0) {
|
|
34
|
+
throw Error(rpcResponse.error.message)
|
|
35
|
+
}
|
|
36
|
+
logger.warning(`Replacement fee too low error. Waiting 1 sec. Retries: ${retries}`)
|
|
37
|
+
await wait(1000)
|
|
38
|
+
return callRpcMethodImpl({ ...args, retries: retries - 1 })
|
|
39
|
+
}
|
|
40
|
+
throw Error(rpcResponse.error.message)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return rpcResponse
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Builds the request body of the http request to EBSI RPC api
|
|
48
|
+
* @function buildFetchOptions
|
|
49
|
+
* @param {{ params: RPCParams[]; id: number; token: string; method: EbsiRpcMethod }} args
|
|
50
|
+
*/
|
|
51
|
+
const buildFetchOptions = (args: RpcMethodArgs): RequestInit => {
|
|
52
|
+
const { params, rpcId, accessToken, rpcMethod } = args
|
|
53
|
+
const fetchReq = {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: {
|
|
56
|
+
'Content-Type': 'application/json',
|
|
57
|
+
Authorization: `Bearer ${accessToken}`,
|
|
58
|
+
},
|
|
59
|
+
body: JSON.stringify({
|
|
60
|
+
jsonrpc: JSON_RPC_VERSION,
|
|
61
|
+
method: rpcMethod,
|
|
62
|
+
params: params,
|
|
63
|
+
id: rpcId,
|
|
64
|
+
}),
|
|
65
|
+
} satisfies RequestInit
|
|
66
|
+
logger.debug(fetchReq)
|
|
67
|
+
return fetchReq
|
|
68
|
+
}
|