@zkpassport/sdk 0.1.2 → 0.2.0
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/README.md +80 -25
- package/dist/cjs/encryption.js +69 -0
- package/dist/cjs/index.d.ts +191 -0
- package/dist/cjs/index.js +857 -0
- package/dist/{json-rpc.d.ts → cjs/json-rpc.d.ts} +1 -1
- package/dist/cjs/json-rpc.js +48 -0
- package/dist/{logger.js → cjs/logger.js} +5 -38
- package/dist/cjs/mobile.js +131 -0
- package/dist/{websocket.js → cjs/websocket.js} +2 -2
- package/dist/esm/encryption.d.ts +7 -0
- package/dist/esm/encryption.js +40 -0
- package/dist/esm/index.d.ts +191 -0
- package/dist/esm/index.js +846 -0
- package/dist/esm/json-rpc.d.ts +6 -0
- package/dist/esm/json-rpc.js +41 -0
- package/dist/esm/logger.d.ts +7 -0
- package/dist/esm/logger.js +37 -0
- package/dist/esm/mobile.d.ts +39 -0
- package/dist/esm/mobile.js +126 -0
- package/dist/esm/websocket.d.ts +2 -0
- package/dist/esm/websocket.js +15 -0
- package/package.json +15 -7
- package/src/index.ts +974 -63
- package/src/json-rpc.ts +6 -2
- package/src/mobile.ts +14 -5
- package/tsconfig.json +14 -9
- package/dist/constants/index.d.ts +0 -13
- package/dist/constants/index.js +0 -52
- package/dist/encryption.js +0 -126
- package/dist/index.d.ts +0 -79
- package/dist/index.js +0 -384
- package/dist/json-rpc.js +0 -105
- package/dist/mobile.js +0 -253
- package/dist/types/countries.d.ts +0 -1
- package/dist/types/countries.js +0 -2
- package/dist/types/credentials.d.ts +0 -17
- package/dist/types/credentials.js +0 -2
- package/dist/types/index.d.ts +0 -4
- package/dist/types/index.js +0 -2
- package/dist/types/json-rpc.d.ts +0 -12
- package/dist/types/json-rpc.js +0 -2
- package/dist/types/query-result.d.ts +0 -46
- package/dist/types/query-result.js +0 -2
- package/src/circuits/proof_age.json +0 -1
- package/src/constants/index.ts +0 -54
- package/src/types/countries.ts +0 -278
- package/src/types/credentials.ts +0 -40
- package/src/types/index.ts +0 -13
- package/src/types/json-rpc.ts +0 -13
- package/src/types/query-result.ts +0 -49
- /package/dist/{encryption.d.ts → cjs/encryption.d.ts} +0 -0
- /package/dist/{logger.d.ts → cjs/logger.d.ts} +0 -0
- /package/dist/{mobile.d.ts → cjs/mobile.d.ts} +0 -0
- /package/dist/{websocket.d.ts → cjs/websocket.d.ts} +0 -0
package/src/index.ts
CHANGED
|
@@ -1,21 +1,44 @@
|
|
|
1
1
|
import { randomBytes } from 'crypto'
|
|
2
2
|
import { Alpha3Code, getAlpha3Code, registerLocale } from 'i18n-iso-countries'
|
|
3
3
|
import {
|
|
4
|
-
DisclosableIDCredential,
|
|
5
|
-
IDCredential,
|
|
6
|
-
IDCredentialConfig,
|
|
7
|
-
IDCredentialValue,
|
|
8
|
-
NumericalIDCredential,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
4
|
+
type DisclosableIDCredential,
|
|
5
|
+
type IDCredential,
|
|
6
|
+
type IDCredentialConfig,
|
|
7
|
+
type IDCredentialValue,
|
|
8
|
+
type NumericalIDCredential,
|
|
9
|
+
type ProofResult,
|
|
10
|
+
type QueryResult,
|
|
11
|
+
type CountryName,
|
|
12
|
+
type JsonRpcRequest,
|
|
13
|
+
getProofData,
|
|
14
|
+
getCommitmentFromDSCProof,
|
|
15
|
+
getCommitmentInFromIDDataProof,
|
|
16
|
+
getCommitmentOutFromIDDataProof,
|
|
17
|
+
getNullifierFromDisclosureProof,
|
|
18
|
+
getCommitmentInFromIntegrityProof,
|
|
19
|
+
getCommitmentOutFromIntegrityProof,
|
|
20
|
+
getCommitmentInFromDisclosureProof,
|
|
21
|
+
getMerkleRootFromDSCProof,
|
|
22
|
+
getCurrentDateFromIntegrityProof,
|
|
23
|
+
getMaxAgeFromProof,
|
|
24
|
+
getMinAgeFromProof,
|
|
25
|
+
getCurrentDateFromAgeProof,
|
|
26
|
+
getMinDateFromProof,
|
|
27
|
+
getMaxDateFromProof,
|
|
28
|
+
getCountryListFromExclusionProof,
|
|
29
|
+
getCountryListFromInclusionProof,
|
|
30
|
+
DisclosedData,
|
|
31
|
+
formatName,
|
|
32
|
+
getHostedPackagedCircuitByName,
|
|
33
|
+
} from '@zkpassport/utils'
|
|
13
34
|
import { bytesToHex } from '@noble/ciphers/utils'
|
|
14
35
|
import { getWebSocketClient, WebSocketClient } from './websocket'
|
|
15
36
|
import { createEncryptedJsonRpcRequest } from './json-rpc'
|
|
16
37
|
import { decrypt, generateECDHKeyPair, getSharedSecret } from './encryption'
|
|
17
|
-
import { JsonRpcRequest } from './types/json-rpc'
|
|
18
38
|
import logger from './logger'
|
|
39
|
+
import { ungzip } from 'node-gzip'
|
|
40
|
+
//import initNoirC from '@noir-lang/noirc_abi'
|
|
41
|
+
//import initACVM from '@noir-lang/acvm_js'
|
|
19
42
|
|
|
20
43
|
registerLocale(require('i18n-iso-countries/langs/en.json'))
|
|
21
44
|
|
|
@@ -64,24 +87,178 @@ function generalCompare(
|
|
|
64
87
|
}
|
|
65
88
|
}
|
|
66
89
|
|
|
67
|
-
export * from '
|
|
68
|
-
export
|
|
90
|
+
export type * from '@zkpassport/utils'
|
|
91
|
+
export {
|
|
92
|
+
SANCTIONED_COUNTRIES,
|
|
93
|
+
EU_COUNTRIES,
|
|
94
|
+
EEA_COUNTRIES,
|
|
95
|
+
SCHENGEN_COUNTRIES,
|
|
96
|
+
ASEAN_COUNTRIES,
|
|
97
|
+
MERCOSUR_COUNTRIES,
|
|
98
|
+
} from '@zkpassport/utils'
|
|
69
99
|
|
|
70
|
-
export
|
|
100
|
+
export type QueryBuilderResult = {
|
|
101
|
+
/**
|
|
102
|
+
* The URL of the request.
|
|
103
|
+
*
|
|
104
|
+
* You can either encode the URL in a QR code or let the user click the link
|
|
105
|
+
* to this URL on your website if they're visiting your website on their phone.
|
|
106
|
+
*/
|
|
107
|
+
url: string
|
|
108
|
+
/**
|
|
109
|
+
* The id of the request.
|
|
110
|
+
*/
|
|
111
|
+
requestId: string
|
|
112
|
+
/**
|
|
113
|
+
* Called when the user has scanned the QR code or clicked the link to the request.
|
|
114
|
+
*
|
|
115
|
+
* This means the user is currently viewing the request popup with your website information
|
|
116
|
+
* and the information requested from them.
|
|
117
|
+
*/
|
|
118
|
+
onRequestReceived: (callback: () => void) => void
|
|
119
|
+
/**
|
|
120
|
+
* Called when the user has accepted the request and
|
|
121
|
+
* started to generate the proof on their phone.
|
|
122
|
+
*/
|
|
123
|
+
onGeneratingProof: (callback: () => void) => void
|
|
124
|
+
/**
|
|
125
|
+
* Called when the SDK successfully connects to the bridge with the mobile app.
|
|
126
|
+
*/
|
|
127
|
+
onBridgeConnect: (callback: () => void) => void
|
|
128
|
+
/**
|
|
129
|
+
* Called when the user has generated a proof.
|
|
130
|
+
*
|
|
131
|
+
* There is a minimum of 4 proofs, but there can be more depending
|
|
132
|
+
* on the type of information requested from the user.
|
|
133
|
+
*/
|
|
134
|
+
onProofGenerated: (callback: (proof: ProofResult) => void) => void
|
|
135
|
+
/**
|
|
136
|
+
* Called when the user has sent the query result.
|
|
137
|
+
*
|
|
138
|
+
* The response contains the unique identifier associated to the user,
|
|
139
|
+
* your domain name and chosen scope, along with the query result and whether
|
|
140
|
+
* the proofs were successfully verified.
|
|
141
|
+
*/
|
|
142
|
+
onResult: (
|
|
143
|
+
callback: (response: {
|
|
144
|
+
uniqueIdentifier: string | undefined
|
|
145
|
+
verified: boolean
|
|
146
|
+
result: QueryResult
|
|
147
|
+
}) => void,
|
|
148
|
+
) => void
|
|
149
|
+
/**
|
|
150
|
+
* Called when the user has rejected the request.
|
|
151
|
+
*/
|
|
152
|
+
onReject: (callback: () => void) => void
|
|
153
|
+
/**
|
|
154
|
+
* Called when an error occurs, such as one of the requirements not being met
|
|
155
|
+
* or a proof failing to be generated.
|
|
156
|
+
*/
|
|
157
|
+
onError: (callback: (error: string) => void) => void
|
|
158
|
+
/**
|
|
159
|
+
* @returns true if the bridge with the mobile app is connected
|
|
160
|
+
*/
|
|
161
|
+
isBridgeConnected: () => boolean
|
|
162
|
+
/**
|
|
163
|
+
* Get if the user has scanned the QR code or the link to this request
|
|
164
|
+
* @returns true if the request has been received by the user on their phone
|
|
165
|
+
*/
|
|
166
|
+
requestReceived: () => boolean
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export type QueryBuilder = {
|
|
170
|
+
/**
|
|
171
|
+
* Requires this attribute to be equal to the provided value.
|
|
172
|
+
* @param key The attribute to compare.
|
|
173
|
+
* @param value The value of the attribute you require.
|
|
174
|
+
*/
|
|
175
|
+
eq: <T extends IDCredential>(key: T, value: IDCredentialValue<T>) => QueryBuilder
|
|
176
|
+
/**
|
|
177
|
+
* Requires this attribute to be greater than or equal to the provided value.
|
|
178
|
+
* @param key The attribute to compare.
|
|
179
|
+
* @param value The value of the attribute you require.
|
|
180
|
+
*/
|
|
181
|
+
gte: <T extends NumericalIDCredential>(key: T, value: IDCredentialValue<T>) => QueryBuilder
|
|
182
|
+
/**
|
|
183
|
+
* Requires this attribute to be less than or equal to the provided value.
|
|
184
|
+
* @param key The attribute to compare.
|
|
185
|
+
* @param value The value of the attribute you require.
|
|
186
|
+
*/
|
|
187
|
+
lte: <T extends 'birthdate' | 'expiry_date'>(key: T, value: IDCredentialValue<T>) => QueryBuilder
|
|
188
|
+
/**
|
|
189
|
+
* Requires this attribute to be less than the provided value.
|
|
190
|
+
* @param key The attribute to compare.
|
|
191
|
+
* @param value The value of the attribute you require.
|
|
192
|
+
*/
|
|
193
|
+
lt: <T extends 'age'>(key: T, value: IDCredentialValue<T>) => QueryBuilder
|
|
194
|
+
/**
|
|
195
|
+
* Requires this attribute to be included in the provided range.
|
|
196
|
+
* @param key The attribute to compare.
|
|
197
|
+
* @param start The start of the range.
|
|
198
|
+
* @param end The end of the range.
|
|
199
|
+
*/
|
|
200
|
+
range: <T extends NumericalIDCredential>(
|
|
201
|
+
key: T,
|
|
202
|
+
start: IDCredentialValue<T>,
|
|
203
|
+
end: IDCredentialValue<T>,
|
|
204
|
+
) => QueryBuilder
|
|
205
|
+
/**
|
|
206
|
+
* Requires this attribute to be included in the provided list.
|
|
207
|
+
* @param key The attribute to compare.
|
|
208
|
+
* @param value The list of values to check inclusion against.
|
|
209
|
+
*/
|
|
210
|
+
in: <T extends 'nationality'>(key: T, value: IDCredentialValue<T>[]) => QueryBuilder
|
|
211
|
+
/**
|
|
212
|
+
* Requires this attribute to be excluded from the provided list.
|
|
213
|
+
* @param key The attribute to compare.
|
|
214
|
+
* @param value The list of values to check exclusion against.
|
|
215
|
+
*/
|
|
216
|
+
out: <T extends 'nationality'>(key: T, value: IDCredentialValue<T>[]) => QueryBuilder
|
|
217
|
+
/**
|
|
218
|
+
* Requires this attribute to be disclosed.
|
|
219
|
+
* @param key The attribute to disclose.
|
|
220
|
+
*/
|
|
221
|
+
disclose: (key: DisclosableIDCredential) => QueryBuilder
|
|
222
|
+
/**
|
|
223
|
+
* Builds the request.
|
|
224
|
+
*
|
|
225
|
+
* This will return the URL of the request, which you can either encode in a QR code
|
|
226
|
+
* or provide as a link to the user if they're visiting your website on their phone.
|
|
227
|
+
* It also returns all the callbacks you can use to handle the user's response.
|
|
228
|
+
*/
|
|
229
|
+
done: () => QueryBuilderResult
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export class ZKPassport {
|
|
71
233
|
private domain: string
|
|
72
234
|
private topicToConfig: Record<string, Record<string, IDCredentialConfig>> = {}
|
|
73
235
|
private topicToKeyPair: Record<string, { privateKey: Uint8Array; publicKey: Uint8Array }> = {}
|
|
74
236
|
private topicToWebSocketClient: Record<string, WebSocketClient> = {}
|
|
75
237
|
private topicToSharedSecret: Record<string, Uint8Array> = {}
|
|
76
|
-
private
|
|
238
|
+
private topicToRequestReceived: Record<string, boolean> = {}
|
|
239
|
+
private topicToService: Record<
|
|
240
|
+
string,
|
|
241
|
+
{ name: string; logo: string; purpose: string; scope?: string }
|
|
242
|
+
> = {}
|
|
243
|
+
private topicToProofs: Record<string, Array<ProofResult>> = {}
|
|
77
244
|
|
|
78
|
-
private
|
|
245
|
+
private onRequestReceivedCallbacks: Record<string, Array<() => void>> = {}
|
|
79
246
|
private onGeneratingProofCallbacks: Record<string, Array<(topic: string) => void>> = {}
|
|
80
247
|
private onBridgeConnectCallbacks: Record<string, Array<() => void>> = {}
|
|
81
|
-
private onProofGeneratedCallbacks: Record<string, Array<(
|
|
248
|
+
private onProofGeneratedCallbacks: Record<string, Array<(proof: ProofResult) => void>> = {}
|
|
249
|
+
private onResultCallbacks: Record<
|
|
250
|
+
string,
|
|
251
|
+
Array<
|
|
252
|
+
(response: {
|
|
253
|
+
uniqueIdentifier: string | undefined
|
|
254
|
+
verified: boolean
|
|
255
|
+
result: QueryResult
|
|
256
|
+
}) => void
|
|
257
|
+
>
|
|
258
|
+
> = {}
|
|
82
259
|
private onRejectCallbacks: Record<string, Array<() => void>> = {}
|
|
83
260
|
private onErrorCallbacks: Record<string, Array<(topic: string) => void>> = {}
|
|
84
|
-
private
|
|
261
|
+
//private wasmVerifierInit: boolean = false
|
|
85
262
|
|
|
86
263
|
constructor(_domain?: string) {
|
|
87
264
|
if (!_domain && typeof window === 'undefined') {
|
|
@@ -90,12 +267,23 @@ export class ZkPassport {
|
|
|
90
267
|
this.domain = _domain || window.location.hostname
|
|
91
268
|
}
|
|
92
269
|
|
|
270
|
+
/*private async initWasmVerifier() {
|
|
271
|
+
const acvm = await import('@noir-lang/acvm_js/web/acvm_js_bg.wasm')
|
|
272
|
+
const noirc = await import('@noir-lang/noirc_abi/web/noirc_abi_wasm_bg.wasm')
|
|
273
|
+
await Promise.all([initACVM(acvm), initNoirC(noirc)])
|
|
274
|
+
this.wasmVerifierInit = true
|
|
275
|
+
}*/
|
|
276
|
+
|
|
93
277
|
/**
|
|
94
278
|
* @notice Handle an encrypted message.
|
|
95
279
|
* @param request The request.
|
|
96
280
|
* @param outerRequest The outer request.
|
|
97
281
|
*/
|
|
98
|
-
private async handleEncryptedMessage(
|
|
282
|
+
private async handleEncryptedMessage(
|
|
283
|
+
topic: string,
|
|
284
|
+
request: JsonRpcRequest,
|
|
285
|
+
outerRequest: JsonRpcRequest,
|
|
286
|
+
) {
|
|
99
287
|
logger.debug('Received encrypted message:', request)
|
|
100
288
|
if (request.method === 'accept') {
|
|
101
289
|
logger.debug(`User accepted the request and is generating a proof`)
|
|
@@ -103,15 +291,50 @@ export class ZkPassport {
|
|
|
103
291
|
} else if (request.method === 'reject') {
|
|
104
292
|
logger.debug(`User rejected the request`)
|
|
105
293
|
await Promise.all(this.onRejectCallbacks[topic].map((callback) => callback()))
|
|
106
|
-
} else if (request.method === '
|
|
294
|
+
} else if (request.method === 'proof') {
|
|
107
295
|
logger.debug(`User generated proof`)
|
|
108
|
-
|
|
296
|
+
// Uncompress the proof and convert it to a hex string
|
|
297
|
+
const bytesProof = Buffer.from(request.params.proof, 'base64')
|
|
298
|
+
const uncompressedProof = await ungzip(bytesProof)
|
|
299
|
+
// The gzip lib in the app compress the proof as ASCII
|
|
300
|
+
// and since the app passes the proof as a hex string, we can
|
|
301
|
+
// just decode the bytes as hex characters using the TextDecoder
|
|
302
|
+
const hexProof = new TextDecoder().decode(uncompressedProof)
|
|
303
|
+
const processedProof: ProofResult = {
|
|
304
|
+
proof: hexProof,
|
|
305
|
+
vkeyHash: request.params.vkeyHash,
|
|
306
|
+
name: request.params.name,
|
|
307
|
+
version: request.params.version,
|
|
308
|
+
}
|
|
309
|
+
this.topicToProofs[topic].push(processedProof)
|
|
310
|
+
await Promise.all(
|
|
311
|
+
this.onProofGeneratedCallbacks[topic].map((callback) => callback(processedProof)),
|
|
312
|
+
)
|
|
313
|
+
} else if (request.method === 'done') {
|
|
314
|
+
logger.debug(`User sent the query result`)
|
|
315
|
+
// Verify the proofs and extract the unique identifier (aka nullifier) and the verification result
|
|
316
|
+
const { uniqueIdentifier, verified } = await this.verify(
|
|
317
|
+
topic,
|
|
318
|
+
this.topicToProofs[topic],
|
|
319
|
+
request.params,
|
|
320
|
+
)
|
|
321
|
+
await Promise.all(
|
|
322
|
+
this.onResultCallbacks[topic].map((callback) =>
|
|
323
|
+
callback({
|
|
324
|
+
uniqueIdentifier,
|
|
325
|
+
verified,
|
|
326
|
+
result: request.params,
|
|
327
|
+
}),
|
|
328
|
+
),
|
|
329
|
+
)
|
|
109
330
|
} else if (request.method === 'error') {
|
|
110
|
-
await Promise.all(
|
|
331
|
+
await Promise.all(
|
|
332
|
+
this.onErrorCallbacks[topic].map((callback) => callback(request.params.error)),
|
|
333
|
+
)
|
|
111
334
|
}
|
|
112
335
|
}
|
|
113
336
|
|
|
114
|
-
private getZkPassportRequest(topic: string) {
|
|
337
|
+
private getZkPassportRequest(topic: string): QueryBuilder {
|
|
115
338
|
return {
|
|
116
339
|
eq: <T extends IDCredential>(key: T, value: IDCredentialValue<T>) => {
|
|
117
340
|
if (key === 'issuing_country' || key === 'nationality') {
|
|
@@ -124,33 +347,33 @@ export class ZkPassport {
|
|
|
124
347
|
numericalCompare('gte', key, value, topic, this.topicToConfig)
|
|
125
348
|
return this.getZkPassportRequest(topic)
|
|
126
349
|
},
|
|
127
|
-
gt: <T extends NumericalIDCredential>(key: T, value: IDCredentialValue<T>) => {
|
|
350
|
+
/*gt: <T extends NumericalIDCredential>(key: T, value: IDCredentialValue<T>) => {
|
|
128
351
|
numericalCompare('gt', key, value, topic, this.topicToConfig)
|
|
129
352
|
return this.getZkPassportRequest(topic)
|
|
130
|
-
}
|
|
131
|
-
lte: <T extends
|
|
353
|
+
},*/
|
|
354
|
+
lte: <T extends 'birthdate' | 'expiry_date'>(key: T, value: IDCredentialValue<T>) => {
|
|
132
355
|
numericalCompare('lte', key, value, topic, this.topicToConfig)
|
|
133
356
|
return this.getZkPassportRequest(topic)
|
|
134
357
|
},
|
|
135
|
-
lt: <T extends
|
|
358
|
+
lt: <T extends 'age'>(key: T, value: IDCredentialValue<T>) => {
|
|
136
359
|
numericalCompare('lt', key, value, topic, this.topicToConfig)
|
|
137
360
|
return this.getZkPassportRequest(topic)
|
|
138
361
|
},
|
|
139
|
-
range: <T extends NumericalIDCredential>(
|
|
362
|
+
range: <T extends NumericalIDCredential>(
|
|
363
|
+
key: T,
|
|
364
|
+
start: IDCredentialValue<T>,
|
|
365
|
+
end: IDCredentialValue<T>,
|
|
366
|
+
) => {
|
|
140
367
|
rangeCompare(key, [start, end], topic, this.topicToConfig)
|
|
141
368
|
return this.getZkPassportRequest(topic)
|
|
142
369
|
},
|
|
143
|
-
in: <T extends
|
|
144
|
-
|
|
145
|
-
value = value.map((v) => normalizeCountry(v as CountryName)) as IDCredentialValue<T>[]
|
|
146
|
-
}
|
|
370
|
+
in: <T extends 'nationality'>(key: T, value: IDCredentialValue<T>[]) => {
|
|
371
|
+
value = value.map((v) => normalizeCountry(v as CountryName)) as IDCredentialValue<T>[]
|
|
147
372
|
generalCompare('in', key, value, topic, this.topicToConfig)
|
|
148
373
|
return this.getZkPassportRequest(topic)
|
|
149
374
|
},
|
|
150
|
-
out: <T extends
|
|
151
|
-
|
|
152
|
-
value = value.map((v) => normalizeCountry(v as CountryName)) as IDCredentialValue<T>[]
|
|
153
|
-
}
|
|
375
|
+
out: <T extends 'nationality'>(key: T, value: IDCredentialValue<T>[]) => {
|
|
376
|
+
value = value.map((v) => normalizeCountry(v as CountryName)) as IDCredentialValue<T>[]
|
|
154
377
|
generalCompare('out', key, value, topic, this.topicToConfig)
|
|
155
378
|
return this.getZkPassportRequest(topic)
|
|
156
379
|
},
|
|
@@ -165,21 +388,36 @@ export class ZkPassport {
|
|
|
165
388
|
return this.getZkPassportRequest(topic)
|
|
166
389
|
},*/
|
|
167
390
|
done: () => {
|
|
168
|
-
const base64Config = Buffer.from(JSON.stringify(this.topicToConfig[topic])).toString(
|
|
169
|
-
|
|
391
|
+
const base64Config = Buffer.from(JSON.stringify(this.topicToConfig[topic])).toString(
|
|
392
|
+
'base64',
|
|
393
|
+
)
|
|
394
|
+
const base64Service = Buffer.from(JSON.stringify(this.topicToService[topic])).toString(
|
|
395
|
+
'base64',
|
|
396
|
+
)
|
|
170
397
|
const pubkey = bytesToHex(this.topicToKeyPair[topic].publicKey)
|
|
171
398
|
return {
|
|
172
399
|
url: `https://zkpassport.id/r?d=${this.domain}&t=${topic}&c=${base64Config}&s=${base64Service}&p=${pubkey}`,
|
|
173
400
|
requestId: topic,
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
401
|
+
onRequestReceived: (callback: () => void) =>
|
|
402
|
+
this.onRequestReceivedCallbacks[topic].push(callback),
|
|
403
|
+
onGeneratingProof: (callback: () => void) =>
|
|
404
|
+
this.onGeneratingProofCallbacks[topic].push(callback),
|
|
405
|
+
onBridgeConnect: (callback: () => void) =>
|
|
406
|
+
this.onBridgeConnectCallbacks[topic].push(callback),
|
|
407
|
+
onProofGenerated: (callback: (proof: ProofResult) => void) =>
|
|
178
408
|
this.onProofGeneratedCallbacks[topic].push(callback),
|
|
409
|
+
onResult: (
|
|
410
|
+
callback: (response: {
|
|
411
|
+
uniqueIdentifier: string | undefined
|
|
412
|
+
verified: boolean
|
|
413
|
+
result: QueryResult
|
|
414
|
+
}) => void,
|
|
415
|
+
) => this.onResultCallbacks[topic].push(callback),
|
|
179
416
|
onReject: (callback: () => void) => this.onRejectCallbacks[topic].push(callback),
|
|
180
|
-
onError: (callback: (error: string) => void) =>
|
|
417
|
+
onError: (callback: (error: string) => void) =>
|
|
418
|
+
this.onErrorCallbacks[topic].push(callback),
|
|
181
419
|
isBridgeConnected: () => this.topicToWebSocketClient[topic].readyState === WebSocket.OPEN,
|
|
182
|
-
|
|
420
|
+
requestReceived: () => this.topicToRequestReceived[topic] === true,
|
|
183
421
|
}
|
|
184
422
|
},
|
|
185
423
|
}
|
|
@@ -193,12 +431,14 @@ export class ZkPassport {
|
|
|
193
431
|
name,
|
|
194
432
|
logo,
|
|
195
433
|
purpose,
|
|
434
|
+
scope,
|
|
196
435
|
topicOverride,
|
|
197
436
|
keyPairOverride,
|
|
198
437
|
}: {
|
|
199
438
|
name: string
|
|
200
439
|
logo: string
|
|
201
440
|
purpose: string
|
|
441
|
+
scope?: string
|
|
202
442
|
topicOverride?: string
|
|
203
443
|
keyPairOverride?: { privateKey: Uint8Array; publicKey: Uint8Array }
|
|
204
444
|
}) {
|
|
@@ -211,12 +451,14 @@ export class ZkPassport {
|
|
|
211
451
|
}
|
|
212
452
|
|
|
213
453
|
this.topicToConfig[topic] = {}
|
|
214
|
-
this.topicToService[topic] = { name, logo, purpose }
|
|
454
|
+
this.topicToService[topic] = { name, logo, purpose, scope }
|
|
455
|
+
this.topicToProofs[topic] = []
|
|
215
456
|
|
|
216
|
-
this.
|
|
457
|
+
this.onRequestReceivedCallbacks[topic] = []
|
|
217
458
|
this.onGeneratingProofCallbacks[topic] = []
|
|
218
459
|
this.onBridgeConnectCallbacks[topic] = []
|
|
219
460
|
this.onProofGeneratedCallbacks[topic] = []
|
|
461
|
+
this.onResultCallbacks[topic] = []
|
|
220
462
|
this.onRejectCallbacks[topic] = []
|
|
221
463
|
this.onErrorCallbacks[topic] = []
|
|
222
464
|
|
|
@@ -234,9 +476,15 @@ export class ZkPassport {
|
|
|
234
476
|
if (data.method === 'handshake') {
|
|
235
477
|
logger.debug('[frontend] Received handshake:', event.data)
|
|
236
478
|
|
|
237
|
-
this.
|
|
238
|
-
this.topicToSharedSecret[topic] = await getSharedSecret(
|
|
239
|
-
|
|
479
|
+
this.topicToRequestReceived[topic] = true
|
|
480
|
+
this.topicToSharedSecret[topic] = await getSharedSecret(
|
|
481
|
+
bytesToHex(keyPair.privateKey),
|
|
482
|
+
data.params.pubkey,
|
|
483
|
+
)
|
|
484
|
+
logger.debug(
|
|
485
|
+
'[frontend] Shared secret:',
|
|
486
|
+
Buffer.from(this.topicToSharedSecret[topic]).toString('hex'),
|
|
487
|
+
)
|
|
240
488
|
|
|
241
489
|
const encryptedMessage = await createEncryptedJsonRpcRequest(
|
|
242
490
|
'hello',
|
|
@@ -247,7 +495,7 @@ export class ZkPassport {
|
|
|
247
495
|
logger.debug('[frontend] Sending encrypted message:', encryptedMessage)
|
|
248
496
|
wsClient.send(JSON.stringify(encryptedMessage))
|
|
249
497
|
|
|
250
|
-
await Promise.all(this.
|
|
498
|
+
await Promise.all(this.onRequestReceivedCallbacks[topic].map((callback) => callback()))
|
|
251
499
|
return
|
|
252
500
|
}
|
|
253
501
|
|
|
@@ -279,20 +527,678 @@ export class ZkPassport {
|
|
|
279
527
|
return this.getZkPassportRequest(topic)
|
|
280
528
|
}
|
|
281
529
|
|
|
530
|
+
private async checkPublicInputs(proofs: Array<ProofResult>, queryResult: QueryResult) {
|
|
531
|
+
let commitmentIn: bigint | undefined
|
|
532
|
+
let commitmentOut: bigint | undefined
|
|
533
|
+
let isCorrect = true
|
|
534
|
+
let uniqueIdentifier: string | undefined
|
|
535
|
+
const expectedMerkleRoot = BigInt(
|
|
536
|
+
'21301853597069384763054217328384418971999152625381818922211526730996340553696',
|
|
537
|
+
)
|
|
538
|
+
const defaultDateValue = new Date(1111, 10, 11)
|
|
539
|
+
const currentTime = new Date()
|
|
540
|
+
const today = new Date(
|
|
541
|
+
currentTime.getFullYear(),
|
|
542
|
+
currentTime.getMonth(),
|
|
543
|
+
currentTime.getDate(),
|
|
544
|
+
0,
|
|
545
|
+
0,
|
|
546
|
+
0,
|
|
547
|
+
0,
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
// Since the order is important for the commitments, we need to sort the proofs
|
|
551
|
+
// by their expected order: root signature check -> ID signature check -> integrity check -> disclosure
|
|
552
|
+
const sortedProofs = proofs.sort((a, b) => {
|
|
553
|
+
const proofOrder = [
|
|
554
|
+
'sig_check_dsc',
|
|
555
|
+
'sig_check_id_data',
|
|
556
|
+
'data_check_integrity',
|
|
557
|
+
'disclose_bytes',
|
|
558
|
+
'compare_age',
|
|
559
|
+
'compare_birthdate',
|
|
560
|
+
'compare_expiry',
|
|
561
|
+
'exclusion_check_country',
|
|
562
|
+
'inclusion_check_country',
|
|
563
|
+
]
|
|
564
|
+
const getIndex = (proof: ProofResult) => {
|
|
565
|
+
const name = proof.name || ''
|
|
566
|
+
return proofOrder.findIndex((p) => name.startsWith(p))
|
|
567
|
+
}
|
|
568
|
+
return getIndex(a) - getIndex(b)
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
for (const proof of sortedProofs!) {
|
|
572
|
+
const proofData = getProofData(proof.proof as string, true)
|
|
573
|
+
if (proof.name?.startsWith('sig_check_dsc')) {
|
|
574
|
+
commitmentOut = getCommitmentFromDSCProof(proofData)
|
|
575
|
+
const merkleRoot = getMerkleRootFromDSCProof(proofData)
|
|
576
|
+
if (merkleRoot !== expectedMerkleRoot) {
|
|
577
|
+
console.warn('The ID was signed by an unrecognized root certificate')
|
|
578
|
+
isCorrect = false
|
|
579
|
+
break
|
|
580
|
+
}
|
|
581
|
+
} else if (proof.name?.startsWith('sig_check_id_data')) {
|
|
582
|
+
commitmentIn = getCommitmentInFromIDDataProof(proofData)
|
|
583
|
+
if (commitmentIn !== commitmentOut) {
|
|
584
|
+
console.warn(
|
|
585
|
+
'Failed to check the link between the certificate signature and ID signature',
|
|
586
|
+
)
|
|
587
|
+
isCorrect = false
|
|
588
|
+
break
|
|
589
|
+
}
|
|
590
|
+
commitmentOut = getCommitmentOutFromIDDataProof(proofData)
|
|
591
|
+
} else if (proof.name?.startsWith('data_check_integrity')) {
|
|
592
|
+
commitmentIn = getCommitmentInFromIntegrityProof(proofData)
|
|
593
|
+
if (commitmentIn !== commitmentOut) {
|
|
594
|
+
console.warn('Failed to check the link between the ID signature and the data signed')
|
|
595
|
+
isCorrect = false
|
|
596
|
+
break
|
|
597
|
+
}
|
|
598
|
+
commitmentOut = getCommitmentOutFromIntegrityProof(proofData)
|
|
599
|
+
const currentDate = getCurrentDateFromIntegrityProof(proofData)
|
|
600
|
+
// The date should be today or yesterday
|
|
601
|
+
// (if the proof request was requested just before midnight and is finalized after)
|
|
602
|
+
if (
|
|
603
|
+
currentDate.getTime() !== today.getTime() &&
|
|
604
|
+
currentDate.getTime() !== today.getTime() - 86400000
|
|
605
|
+
) {
|
|
606
|
+
console.warn('Current date used to check the validity of the ID is too old')
|
|
607
|
+
isCorrect = false
|
|
608
|
+
break
|
|
609
|
+
}
|
|
610
|
+
} else if (proof.name === 'disclose_bytes') {
|
|
611
|
+
commitmentIn = getCommitmentInFromDisclosureProof(proofData)
|
|
612
|
+
if (commitmentIn !== commitmentOut) {
|
|
613
|
+
console.warn(
|
|
614
|
+
'Failed to check the link between the validity of the ID and the data to disclose',
|
|
615
|
+
)
|
|
616
|
+
isCorrect = false
|
|
617
|
+
break
|
|
618
|
+
}
|
|
619
|
+
// We can't be certain that the disclosed data is for a passport or an ID card
|
|
620
|
+
// so we need to check both (unless the document type is revealed)
|
|
621
|
+
const disclosedDataPassport = DisclosedData.fromBytesProof(proofData, 'passport')
|
|
622
|
+
const disclosedDataIDCard = DisclosedData.fromBytesProof(proofData, 'id_card')
|
|
623
|
+
if (queryResult.document_type) {
|
|
624
|
+
// Document type is always at the same index in the disclosed data
|
|
625
|
+
if (
|
|
626
|
+
queryResult.document_type.eq &&
|
|
627
|
+
queryResult.document_type.eq.result &&
|
|
628
|
+
queryResult.document_type.eq.expected !== disclosedDataPassport.documentType
|
|
629
|
+
) {
|
|
630
|
+
console.warn('Document type does not match the expected document type')
|
|
631
|
+
isCorrect = false
|
|
632
|
+
break
|
|
633
|
+
}
|
|
634
|
+
if (queryResult.document_type.disclose?.result !== disclosedDataIDCard.documentType) {
|
|
635
|
+
console.warn('Document type does not match the disclosed document type in query result')
|
|
636
|
+
isCorrect = false
|
|
637
|
+
break
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
if (queryResult.birthdate) {
|
|
641
|
+
const birthdatePassport = disclosedDataPassport.dateOfBirth
|
|
642
|
+
const birthdateIDCard = disclosedDataIDCard.dateOfBirth
|
|
643
|
+
if (
|
|
644
|
+
queryResult.birthdate.eq &&
|
|
645
|
+
queryResult.birthdate.eq.result &&
|
|
646
|
+
queryResult.birthdate.eq.expected.getTime() !== birthdatePassport.getTime() &&
|
|
647
|
+
queryResult.birthdate.eq.expected.getTime() !== birthdateIDCard.getTime()
|
|
648
|
+
) {
|
|
649
|
+
console.warn('Birthdate does not match the expected birthdate')
|
|
650
|
+
isCorrect = false
|
|
651
|
+
break
|
|
652
|
+
}
|
|
653
|
+
if (
|
|
654
|
+
queryResult.birthdate.disclose &&
|
|
655
|
+
queryResult.birthdate.disclose.result.getTime() !== birthdatePassport.getTime() &&
|
|
656
|
+
queryResult.birthdate.disclose.result.getTime() !== birthdateIDCard.getTime()
|
|
657
|
+
) {
|
|
658
|
+
console.warn('Birthdate does not match the disclosed birthdate in query result')
|
|
659
|
+
isCorrect = false
|
|
660
|
+
break
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
if (queryResult.expiry_date) {
|
|
664
|
+
const expiryDatePassport = disclosedDataPassport.dateOfExpiry
|
|
665
|
+
const expiryDateIDCard = disclosedDataIDCard.dateOfExpiry
|
|
666
|
+
if (
|
|
667
|
+
queryResult.expiry_date.eq &&
|
|
668
|
+
queryResult.expiry_date.eq.result &&
|
|
669
|
+
queryResult.expiry_date.eq.expected.getTime() !== expiryDatePassport.getTime() &&
|
|
670
|
+
queryResult.expiry_date.eq.expected.getTime() !== expiryDateIDCard.getTime()
|
|
671
|
+
) {
|
|
672
|
+
console.warn('Expiry date does not match the expected expiry date')
|
|
673
|
+
isCorrect = false
|
|
674
|
+
break
|
|
675
|
+
}
|
|
676
|
+
if (
|
|
677
|
+
queryResult.expiry_date.disclose &&
|
|
678
|
+
queryResult.expiry_date.disclose.result.getTime() !== expiryDatePassport.getTime() &&
|
|
679
|
+
queryResult.expiry_date.disclose.result.getTime() !== expiryDateIDCard.getTime()
|
|
680
|
+
) {
|
|
681
|
+
console.warn('Expiry date does not match the disclosed expiry date in query result')
|
|
682
|
+
isCorrect = false
|
|
683
|
+
break
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
if (queryResult.nationality) {
|
|
687
|
+
const nationalityPassport = disclosedDataPassport.nationality
|
|
688
|
+
const nationalityIDCard = disclosedDataIDCard.nationality
|
|
689
|
+
if (
|
|
690
|
+
queryResult.nationality.eq &&
|
|
691
|
+
queryResult.nationality.eq.result &&
|
|
692
|
+
queryResult.nationality.eq.expected !== nationalityPassport &&
|
|
693
|
+
queryResult.nationality.eq.expected !== nationalityIDCard
|
|
694
|
+
) {
|
|
695
|
+
console.warn('Nationality does not match the expected nationality')
|
|
696
|
+
isCorrect = false
|
|
697
|
+
break
|
|
698
|
+
}
|
|
699
|
+
if (
|
|
700
|
+
queryResult.nationality.disclose &&
|
|
701
|
+
queryResult.nationality.disclose.result !== nationalityPassport &&
|
|
702
|
+
queryResult.nationality.disclose.result !== nationalityIDCard
|
|
703
|
+
) {
|
|
704
|
+
console.warn('Nationality does not match the disclosed nationality in query result')
|
|
705
|
+
isCorrect = false
|
|
706
|
+
break
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
if (queryResult.document_number) {
|
|
710
|
+
const documentNumberPassport = disclosedDataPassport.documentNumber
|
|
711
|
+
const documentNumberIDCard = disclosedDataIDCard.documentNumber
|
|
712
|
+
if (
|
|
713
|
+
queryResult.document_number.eq &&
|
|
714
|
+
queryResult.document_number.eq.result &&
|
|
715
|
+
queryResult.document_number.eq.expected !== documentNumberPassport &&
|
|
716
|
+
queryResult.document_number.eq.expected !== documentNumberIDCard
|
|
717
|
+
) {
|
|
718
|
+
console.warn('Document number does not match the expected document number')
|
|
719
|
+
isCorrect = false
|
|
720
|
+
break
|
|
721
|
+
}
|
|
722
|
+
if (
|
|
723
|
+
queryResult.document_number.disclose &&
|
|
724
|
+
queryResult.document_number.disclose.result !== documentNumberPassport &&
|
|
725
|
+
queryResult.document_number.disclose.result !== documentNumberIDCard
|
|
726
|
+
) {
|
|
727
|
+
console.warn(
|
|
728
|
+
'Document number does not match the disclosed document number in query result',
|
|
729
|
+
)
|
|
730
|
+
isCorrect = false
|
|
731
|
+
break
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
if (queryResult.gender) {
|
|
735
|
+
const genderPassport = disclosedDataPassport.gender
|
|
736
|
+
const genderIDCard = disclosedDataIDCard.gender
|
|
737
|
+
if (
|
|
738
|
+
queryResult.gender.eq &&
|
|
739
|
+
queryResult.gender.eq.result &&
|
|
740
|
+
queryResult.gender.eq.expected !== genderPassport &&
|
|
741
|
+
queryResult.gender.eq.expected !== genderIDCard
|
|
742
|
+
) {
|
|
743
|
+
console.warn('Gender does not match the expected gender')
|
|
744
|
+
isCorrect = false
|
|
745
|
+
break
|
|
746
|
+
}
|
|
747
|
+
if (
|
|
748
|
+
queryResult.gender.disclose &&
|
|
749
|
+
queryResult.gender.disclose.result !== genderPassport &&
|
|
750
|
+
queryResult.gender.disclose.result !== genderIDCard
|
|
751
|
+
) {
|
|
752
|
+
console.warn('Gender does not match the disclosed gender in query result')
|
|
753
|
+
isCorrect = false
|
|
754
|
+
break
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
if (queryResult.issuing_country) {
|
|
758
|
+
const issuingCountryPassport = disclosedDataPassport.issuingCountry
|
|
759
|
+
const issuingCountryIDCard = disclosedDataIDCard.issuingCountry
|
|
760
|
+
if (
|
|
761
|
+
queryResult.issuing_country.eq &&
|
|
762
|
+
queryResult.issuing_country.eq.result &&
|
|
763
|
+
queryResult.issuing_country.eq.expected !== issuingCountryPassport &&
|
|
764
|
+
queryResult.issuing_country.eq.expected !== issuingCountryIDCard
|
|
765
|
+
) {
|
|
766
|
+
console.warn('Issuing country does not match the expected issuing country')
|
|
767
|
+
isCorrect = false
|
|
768
|
+
break
|
|
769
|
+
}
|
|
770
|
+
if (
|
|
771
|
+
queryResult.issuing_country.disclose &&
|
|
772
|
+
queryResult.issuing_country.disclose.result !== issuingCountryPassport &&
|
|
773
|
+
queryResult.issuing_country.disclose.result !== issuingCountryIDCard
|
|
774
|
+
) {
|
|
775
|
+
console.warn(
|
|
776
|
+
'Issuing country does not match the disclosed issuing country in query result',
|
|
777
|
+
)
|
|
778
|
+
isCorrect = false
|
|
779
|
+
break
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
if (queryResult.fullname) {
|
|
783
|
+
const fullnamePassport = disclosedDataPassport.name
|
|
784
|
+
const fullnameIDCard = disclosedDataIDCard.name
|
|
785
|
+
if (
|
|
786
|
+
queryResult.fullname.eq &&
|
|
787
|
+
queryResult.fullname.eq.result &&
|
|
788
|
+
formatName(queryResult.fullname.eq.expected).toLowerCase() !==
|
|
789
|
+
fullnamePassport.toLowerCase() &&
|
|
790
|
+
formatName(queryResult.fullname.eq.expected).toLowerCase() !==
|
|
791
|
+
fullnameIDCard.toLowerCase()
|
|
792
|
+
) {
|
|
793
|
+
console.warn('Fullname does not match the expected fullname')
|
|
794
|
+
isCorrect = false
|
|
795
|
+
break
|
|
796
|
+
}
|
|
797
|
+
if (
|
|
798
|
+
queryResult.fullname.disclose &&
|
|
799
|
+
formatName(queryResult.fullname.disclose.result).toLowerCase() !==
|
|
800
|
+
fullnamePassport.toLowerCase() &&
|
|
801
|
+
formatName(queryResult.fullname.disclose.result).toLowerCase() !==
|
|
802
|
+
fullnameIDCard.toLowerCase()
|
|
803
|
+
) {
|
|
804
|
+
console.warn('Fullname does not match the disclosed fullname in query result')
|
|
805
|
+
isCorrect = false
|
|
806
|
+
break
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
if (queryResult.firstname) {
|
|
810
|
+
// If fullname was not revealed, then the name could be either the first name or last name
|
|
811
|
+
const firstnamePassport =
|
|
812
|
+
disclosedDataPassport.firstName && disclosedDataPassport.firstName.length > 0
|
|
813
|
+
? disclosedDataPassport.firstName
|
|
814
|
+
: disclosedDataPassport.name
|
|
815
|
+
const firstnameIDCard =
|
|
816
|
+
disclosedDataIDCard.firstName && disclosedDataIDCard.firstName.length > 0
|
|
817
|
+
? disclosedDataIDCard.firstName
|
|
818
|
+
: disclosedDataIDCard.name
|
|
819
|
+
if (
|
|
820
|
+
queryResult.firstname.eq &&
|
|
821
|
+
queryResult.firstname.eq.result &&
|
|
822
|
+
formatName(queryResult.firstname.eq.expected).toLowerCase() !==
|
|
823
|
+
firstnamePassport.toLowerCase() &&
|
|
824
|
+
formatName(queryResult.firstname.eq.expected).toLowerCase() !==
|
|
825
|
+
firstnameIDCard.toLowerCase()
|
|
826
|
+
) {
|
|
827
|
+
console.warn('Firstname does not match the expected firstname')
|
|
828
|
+
isCorrect = false
|
|
829
|
+
break
|
|
830
|
+
}
|
|
831
|
+
if (
|
|
832
|
+
queryResult.firstname.disclose &&
|
|
833
|
+
formatName(queryResult.firstname.disclose.result).toLowerCase() !==
|
|
834
|
+
firstnamePassport.toLowerCase() &&
|
|
835
|
+
formatName(queryResult.firstname.disclose.result).toLowerCase() !==
|
|
836
|
+
firstnameIDCard.toLowerCase()
|
|
837
|
+
) {
|
|
838
|
+
console.warn('Firstname does not match the disclosed firstname in query result')
|
|
839
|
+
isCorrect = false
|
|
840
|
+
break
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
if (queryResult.lastname) {
|
|
844
|
+
// If fullname was not revealed, then the name could be either the first name or last name
|
|
845
|
+
const lastnamePassport =
|
|
846
|
+
disclosedDataPassport.lastName && disclosedDataPassport.lastName.length > 0
|
|
847
|
+
? disclosedDataPassport.lastName
|
|
848
|
+
: disclosedDataPassport.name
|
|
849
|
+
const lastnameIDCard =
|
|
850
|
+
disclosedDataIDCard.lastName && disclosedDataIDCard.lastName.length > 0
|
|
851
|
+
? disclosedDataIDCard.lastName
|
|
852
|
+
: disclosedDataIDCard.name
|
|
853
|
+
if (
|
|
854
|
+
queryResult.lastname.eq &&
|
|
855
|
+
queryResult.lastname.eq.result &&
|
|
856
|
+
formatName(queryResult.lastname.eq.expected).toLowerCase() !==
|
|
857
|
+
lastnamePassport.toLowerCase() &&
|
|
858
|
+
formatName(queryResult.lastname.eq.expected).toLowerCase() !==
|
|
859
|
+
lastnameIDCard.toLowerCase()
|
|
860
|
+
) {
|
|
861
|
+
console.warn('Lastname does not match the expected lastname')
|
|
862
|
+
isCorrect = false
|
|
863
|
+
break
|
|
864
|
+
}
|
|
865
|
+
if (
|
|
866
|
+
queryResult.lastname.disclose &&
|
|
867
|
+
formatName(queryResult.lastname.disclose.result).toLowerCase() !==
|
|
868
|
+
lastnamePassport.toLowerCase() &&
|
|
869
|
+
formatName(queryResult.lastname.disclose.result).toLowerCase() !==
|
|
870
|
+
lastnameIDCard.toLowerCase()
|
|
871
|
+
) {
|
|
872
|
+
console.warn('Lastname does not match the disclosed lastname in query result')
|
|
873
|
+
isCorrect = false
|
|
874
|
+
break
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
uniqueIdentifier = getNullifierFromDisclosureProof(proofData).toString(10)
|
|
878
|
+
} else if (proof.name === 'compare_age') {
|
|
879
|
+
commitmentIn = getCommitmentInFromDisclosureProof(proofData)
|
|
880
|
+
if (commitmentIn !== commitmentOut) {
|
|
881
|
+
console.warn(
|
|
882
|
+
'Failed to check the link between the validity of the ID and the age derived from it',
|
|
883
|
+
)
|
|
884
|
+
isCorrect = false
|
|
885
|
+
break
|
|
886
|
+
}
|
|
887
|
+
const minAge = getMinAgeFromProof(proofData)
|
|
888
|
+
const maxAge = getMaxAgeFromProof(proofData)
|
|
889
|
+
if (queryResult.age) {
|
|
890
|
+
if (
|
|
891
|
+
queryResult.age.gte &&
|
|
892
|
+
queryResult.age.gte.result &&
|
|
893
|
+
minAge < (queryResult.age.gte.expected as number)
|
|
894
|
+
) {
|
|
895
|
+
console.warn('Age is not greater than or equal to the expected age')
|
|
896
|
+
isCorrect = false
|
|
897
|
+
break
|
|
898
|
+
}
|
|
899
|
+
if (
|
|
900
|
+
queryResult.age.lt &&
|
|
901
|
+
queryResult.age.lt.result &&
|
|
902
|
+
maxAge >= (queryResult.age.lt.expected as number)
|
|
903
|
+
) {
|
|
904
|
+
console.warn('Age is not less than the expected age')
|
|
905
|
+
isCorrect = false
|
|
906
|
+
break
|
|
907
|
+
}
|
|
908
|
+
if (queryResult.age.range) {
|
|
909
|
+
if (
|
|
910
|
+
queryResult.age.range.result &&
|
|
911
|
+
(minAge < (queryResult.age.range.expected[0] as number) ||
|
|
912
|
+
maxAge >= (queryResult.age.range.expected[1] as number))
|
|
913
|
+
) {
|
|
914
|
+
console.warn('Age is not in the expected range')
|
|
915
|
+
isCorrect = false
|
|
916
|
+
break
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
if (!queryResult.age.lt && !queryResult.age.range && maxAge != 0) {
|
|
920
|
+
console.warn('Maximum age should be equal to 0')
|
|
921
|
+
isCorrect = false
|
|
922
|
+
break
|
|
923
|
+
}
|
|
924
|
+
if (!queryResult.age.gte && !queryResult.age.range && minAge != 0) {
|
|
925
|
+
console.warn('Minimum age should be equal to 0')
|
|
926
|
+
isCorrect = false
|
|
927
|
+
break
|
|
928
|
+
}
|
|
929
|
+
if (
|
|
930
|
+
queryResult.age.disclose &&
|
|
931
|
+
(queryResult.age.disclose.result !== minAge ||
|
|
932
|
+
queryResult.age.disclose.result !== maxAge)
|
|
933
|
+
) {
|
|
934
|
+
console.warn('Age does not match the disclosed age in query result')
|
|
935
|
+
isCorrect = false
|
|
936
|
+
break
|
|
937
|
+
}
|
|
938
|
+
} else {
|
|
939
|
+
console.warn('Age is not set in the query result')
|
|
940
|
+
isCorrect = false
|
|
941
|
+
break
|
|
942
|
+
}
|
|
943
|
+
const currentDate = getCurrentDateFromAgeProof(proofData)
|
|
944
|
+
if (
|
|
945
|
+
currentDate.getTime() !== today.getTime() &&
|
|
946
|
+
currentDate.getTime() !== today.getTime() - 86400000
|
|
947
|
+
) {
|
|
948
|
+
console.warn('Current date in the proof is too old')
|
|
949
|
+
isCorrect = false
|
|
950
|
+
break
|
|
951
|
+
}
|
|
952
|
+
uniqueIdentifier = getCommitmentInFromDisclosureProof(proofData).toString(10)
|
|
953
|
+
} else if (proof.name === 'compare_birthdate') {
|
|
954
|
+
commitmentIn = getCommitmentInFromDisclosureProof(proofData)
|
|
955
|
+
if (commitmentIn !== commitmentOut) {
|
|
956
|
+
console.warn(
|
|
957
|
+
'Failed to check the link between the validity of the ID and the birthdate derived from it',
|
|
958
|
+
)
|
|
959
|
+
isCorrect = false
|
|
960
|
+
break
|
|
961
|
+
}
|
|
962
|
+
const minDate = getMinDateFromProof(proofData)
|
|
963
|
+
const maxDate = getMaxDateFromProof(proofData)
|
|
964
|
+
if (queryResult.birthdate) {
|
|
965
|
+
if (
|
|
966
|
+
queryResult.birthdate.gte &&
|
|
967
|
+
queryResult.birthdate.gte.result &&
|
|
968
|
+
minDate < queryResult.birthdate.gte.expected
|
|
969
|
+
) {
|
|
970
|
+
console.warn('Birthdate is not greater than or equal to the expected birthdate')
|
|
971
|
+
isCorrect = false
|
|
972
|
+
break
|
|
973
|
+
}
|
|
974
|
+
if (
|
|
975
|
+
queryResult.birthdate.lte &&
|
|
976
|
+
queryResult.birthdate.lte.result &&
|
|
977
|
+
maxDate > queryResult.birthdate.lte.expected
|
|
978
|
+
) {
|
|
979
|
+
console.warn('Birthdate is not less than the expected birthdate')
|
|
980
|
+
isCorrect = false
|
|
981
|
+
break
|
|
982
|
+
}
|
|
983
|
+
if (queryResult.birthdate.range) {
|
|
984
|
+
if (
|
|
985
|
+
queryResult.birthdate.range.result &&
|
|
986
|
+
(minDate < queryResult.birthdate.range.expected[0] ||
|
|
987
|
+
maxDate > queryResult.birthdate.range.expected[1])
|
|
988
|
+
) {
|
|
989
|
+
console.warn('Birthdate is not in the expected range')
|
|
990
|
+
isCorrect = false
|
|
991
|
+
break
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
if (
|
|
995
|
+
!queryResult.birthdate.lte &&
|
|
996
|
+
!queryResult.birthdate.range &&
|
|
997
|
+
maxDate.getTime() != defaultDateValue.getTime()
|
|
998
|
+
) {
|
|
999
|
+
console.warn('Maximum birthdate should be equal to default date value')
|
|
1000
|
+
isCorrect = false
|
|
1001
|
+
break
|
|
1002
|
+
}
|
|
1003
|
+
if (
|
|
1004
|
+
!queryResult.birthdate.gte &&
|
|
1005
|
+
!queryResult.birthdate.range &&
|
|
1006
|
+
minDate.getTime() != defaultDateValue.getTime()
|
|
1007
|
+
) {
|
|
1008
|
+
console.warn('Minimum birthdate should be equal to default date value')
|
|
1009
|
+
isCorrect = false
|
|
1010
|
+
break
|
|
1011
|
+
}
|
|
1012
|
+
} else {
|
|
1013
|
+
console.warn('Birthdate is not set in the query result')
|
|
1014
|
+
isCorrect = false
|
|
1015
|
+
break
|
|
1016
|
+
}
|
|
1017
|
+
uniqueIdentifier = getCommitmentInFromDisclosureProof(proofData).toString(10)
|
|
1018
|
+
} else if (proof.name === 'compare_expiry') {
|
|
1019
|
+
commitmentIn = getCommitmentInFromDisclosureProof(proofData)
|
|
1020
|
+
if (commitmentIn !== commitmentOut) {
|
|
1021
|
+
console.warn(
|
|
1022
|
+
'Failed to check the link between the validity of the ID and its expiry date',
|
|
1023
|
+
)
|
|
1024
|
+
isCorrect = false
|
|
1025
|
+
break
|
|
1026
|
+
}
|
|
1027
|
+
const minDate = getMinDateFromProof(proofData)
|
|
1028
|
+
const maxDate = getMaxDateFromProof(proofData)
|
|
1029
|
+
if (queryResult.expiry_date) {
|
|
1030
|
+
if (
|
|
1031
|
+
queryResult.expiry_date.gte &&
|
|
1032
|
+
queryResult.expiry_date.gte.result &&
|
|
1033
|
+
minDate < queryResult.expiry_date.gte.expected
|
|
1034
|
+
) {
|
|
1035
|
+
console.warn('Expiry date is not greater than or equal to the expected expiry date')
|
|
1036
|
+
isCorrect = false
|
|
1037
|
+
break
|
|
1038
|
+
}
|
|
1039
|
+
if (
|
|
1040
|
+
queryResult.expiry_date.lte &&
|
|
1041
|
+
queryResult.expiry_date.lte.result &&
|
|
1042
|
+
maxDate > queryResult.expiry_date.lte.expected
|
|
1043
|
+
) {
|
|
1044
|
+
console.warn('Expiry date is not less than the expected expiry date')
|
|
1045
|
+
isCorrect = false
|
|
1046
|
+
break
|
|
1047
|
+
}
|
|
1048
|
+
if (queryResult.expiry_date.range) {
|
|
1049
|
+
if (
|
|
1050
|
+
queryResult.expiry_date.range.result &&
|
|
1051
|
+
(minDate < queryResult.expiry_date.range.expected[0] ||
|
|
1052
|
+
maxDate > queryResult.expiry_date.range.expected[1])
|
|
1053
|
+
) {
|
|
1054
|
+
console.warn('Expiry date is not in the expected range')
|
|
1055
|
+
isCorrect = false
|
|
1056
|
+
break
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
if (
|
|
1060
|
+
!queryResult.expiry_date.lte &&
|
|
1061
|
+
!queryResult.expiry_date.range &&
|
|
1062
|
+
maxDate.getTime() != defaultDateValue.getTime()
|
|
1063
|
+
) {
|
|
1064
|
+
console.warn('Maximum expiry date should be equal to default date value')
|
|
1065
|
+
isCorrect = false
|
|
1066
|
+
break
|
|
1067
|
+
}
|
|
1068
|
+
if (
|
|
1069
|
+
!queryResult.expiry_date.gte &&
|
|
1070
|
+
!queryResult.expiry_date.range &&
|
|
1071
|
+
minDate.getTime() != defaultDateValue.getTime()
|
|
1072
|
+
) {
|
|
1073
|
+
console.warn('Minimum expiry date should be equal to default date value')
|
|
1074
|
+
isCorrect = false
|
|
1075
|
+
break
|
|
1076
|
+
}
|
|
1077
|
+
} else {
|
|
1078
|
+
console.warn('Expiry date is not set in the query result')
|
|
1079
|
+
isCorrect = false
|
|
1080
|
+
break
|
|
1081
|
+
}
|
|
1082
|
+
uniqueIdentifier = getNullifierFromDisclosureProof(proofData).toString(10)
|
|
1083
|
+
} else if (proof.name === 'exclusion_check_country') {
|
|
1084
|
+
commitmentIn = getCommitmentInFromDisclosureProof(proofData)
|
|
1085
|
+
if (commitmentIn !== commitmentOut) {
|
|
1086
|
+
console.warn(
|
|
1087
|
+
'Failed to check the link between the validity of the ID and the country exclusion check',
|
|
1088
|
+
)
|
|
1089
|
+
isCorrect = false
|
|
1090
|
+
break
|
|
1091
|
+
}
|
|
1092
|
+
const countryList = getCountryListFromExclusionProof(proofData)
|
|
1093
|
+
if (
|
|
1094
|
+
queryResult.nationality &&
|
|
1095
|
+
queryResult.nationality.out &&
|
|
1096
|
+
queryResult.nationality.out.result
|
|
1097
|
+
) {
|
|
1098
|
+
if (
|
|
1099
|
+
!queryResult.nationality.out.expected?.every((country) => countryList.includes(country))
|
|
1100
|
+
) {
|
|
1101
|
+
console.warn('Country exclusion list does not match the one from the query results')
|
|
1102
|
+
isCorrect = false
|
|
1103
|
+
break
|
|
1104
|
+
}
|
|
1105
|
+
} else if (!queryResult.nationality || !queryResult.nationality.out) {
|
|
1106
|
+
console.warn('Nationality exclusion is not set in the query result')
|
|
1107
|
+
isCorrect = false
|
|
1108
|
+
break
|
|
1109
|
+
}
|
|
1110
|
+
uniqueIdentifier = getNullifierFromDisclosureProof(proofData).toString(10)
|
|
1111
|
+
} else if (proof.name === 'inclusion_check_country') {
|
|
1112
|
+
commitmentIn = getCommitmentInFromDisclosureProof(proofData)
|
|
1113
|
+
if (commitmentIn !== commitmentOut) {
|
|
1114
|
+
console.warn(
|
|
1115
|
+
'Failed to check the link between the validity of the ID and the country inclusion check',
|
|
1116
|
+
)
|
|
1117
|
+
isCorrect = false
|
|
1118
|
+
break
|
|
1119
|
+
}
|
|
1120
|
+
const countryList = getCountryListFromInclusionProof(proofData)
|
|
1121
|
+
if (
|
|
1122
|
+
queryResult.nationality &&
|
|
1123
|
+
queryResult.nationality.in &&
|
|
1124
|
+
queryResult.nationality.in.result
|
|
1125
|
+
) {
|
|
1126
|
+
if (
|
|
1127
|
+
!queryResult.nationality.in.expected?.every((country) => countryList.includes(country))
|
|
1128
|
+
) {
|
|
1129
|
+
console.warn('Country inclusion list does not match the one from the query results')
|
|
1130
|
+
isCorrect = false
|
|
1131
|
+
break
|
|
1132
|
+
}
|
|
1133
|
+
} else if (!queryResult.nationality || !queryResult.nationality.in) {
|
|
1134
|
+
console.warn('Nationality inclusion is not set in the query result')
|
|
1135
|
+
isCorrect = false
|
|
1136
|
+
break
|
|
1137
|
+
}
|
|
1138
|
+
uniqueIdentifier = getNullifierFromDisclosureProof(proofData).toString(10)
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
return { isCorrect, uniqueIdentifier }
|
|
1142
|
+
}
|
|
1143
|
+
|
|
282
1144
|
/**
|
|
283
|
-
* @notice
|
|
284
|
-
* @param
|
|
285
|
-
* @
|
|
1145
|
+
* @notice Verify the proofs received from the mobile app.
|
|
1146
|
+
* @param requestId The request ID.
|
|
1147
|
+
* @param proofs The proofs to verify.
|
|
1148
|
+
* @param queryResult The query result to verify against
|
|
1149
|
+
* @returns An object containing the unique identifier associated to the user
|
|
1150
|
+
* and a boolean indicating whether the proofs were successfully verified.
|
|
286
1151
|
*/
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
1152
|
+
public async verify(
|
|
1153
|
+
requestId: string,
|
|
1154
|
+
proofs?: Array<ProofResult>,
|
|
1155
|
+
queryResult?: QueryResult,
|
|
1156
|
+
): Promise<{ uniqueIdentifier: string | undefined; verified: boolean }> {
|
|
1157
|
+
let proofsToVerify = proofs
|
|
1158
|
+
if (!proofs) {
|
|
1159
|
+
proofsToVerify = this.topicToProofs[requestId]
|
|
1160
|
+
if (!proofsToVerify || proofsToVerify.length === 0) {
|
|
1161
|
+
throw new Error('No proofs to verify')
|
|
1162
|
+
}
|
|
293
1163
|
}
|
|
294
|
-
|
|
295
|
-
|
|
1164
|
+
const { BarretenbergVerifier } = await import('@aztec/bb.js')
|
|
1165
|
+
const verifier = new BarretenbergVerifier()
|
|
1166
|
+
/*if (!this.wasmVerifierInit) {
|
|
1167
|
+
await this.initWasmVerifier()
|
|
1168
|
+
}*/
|
|
1169
|
+
let verified = true
|
|
1170
|
+
let uniqueIdentifier: string | undefined
|
|
1171
|
+
if (queryResult) {
|
|
1172
|
+
const { isCorrect, uniqueIdentifier: uniqueIdentifierFromPublicInputs } =
|
|
1173
|
+
await this.checkPublicInputs(proofsToVerify!, queryResult!)
|
|
1174
|
+
uniqueIdentifier = uniqueIdentifierFromPublicInputs
|
|
1175
|
+
verified = isCorrect
|
|
1176
|
+
}
|
|
1177
|
+
// Only proceed with the proof verification if the public inputs are correct
|
|
1178
|
+
if (verified) {
|
|
1179
|
+
for (const proof of proofsToVerify!) {
|
|
1180
|
+
const proofData = getProofData(proof.proof as string, true)
|
|
1181
|
+
const hostedPackagedCircuit = await getHostedPackagedCircuitByName(
|
|
1182
|
+
proof.version as any,
|
|
1183
|
+
proof.name!,
|
|
1184
|
+
)
|
|
1185
|
+
const vkeyBytes = Buffer.from(hostedPackagedCircuit.vkey, 'base64')
|
|
1186
|
+
try {
|
|
1187
|
+
verified = await verifier.verifyUltraHonkProof(proofData, new Uint8Array(vkeyBytes))
|
|
1188
|
+
} catch (e) {
|
|
1189
|
+
console.warn('Error verifying proof', e)
|
|
1190
|
+
verified = false
|
|
1191
|
+
}
|
|
1192
|
+
if (!verified) {
|
|
1193
|
+
// Break the loop if the proof is not valid
|
|
1194
|
+
// and don't bother checking the other proofs
|
|
1195
|
+
break
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
this.topicToProofs[requestId] = []
|
|
1200
|
+
return { uniqueIdentifier, verified }
|
|
1201
|
+
}
|
|
296
1202
|
|
|
297
1203
|
/**
|
|
298
1204
|
* @notice Returns the URL of the request.
|
|
@@ -301,8 +1207,12 @@ export class ZkPassport {
|
|
|
301
1207
|
*/
|
|
302
1208
|
public getUrl(requestId: string) {
|
|
303
1209
|
const pubkey = bytesToHex(this.topicToKeyPair[requestId].publicKey)
|
|
304
|
-
const base64Config = Buffer.from(JSON.stringify(this.topicToConfig[requestId])).toString(
|
|
305
|
-
|
|
1210
|
+
const base64Config = Buffer.from(JSON.stringify(this.topicToConfig[requestId])).toString(
|
|
1211
|
+
'base64',
|
|
1212
|
+
)
|
|
1213
|
+
const base64Service = Buffer.from(JSON.stringify(this.topicToService[requestId])).toString(
|
|
1214
|
+
'base64',
|
|
1215
|
+
)
|
|
306
1216
|
return `https://zkpassport.id/r?d=${this.domain}&t=${requestId}&c=${base64Config}&s=${base64Service}&p=${pubkey}`
|
|
307
1217
|
}
|
|
308
1218
|
|
|
@@ -316,7 +1226,8 @@ export class ZkPassport {
|
|
|
316
1226
|
delete this.topicToKeyPair[requestId]
|
|
317
1227
|
delete this.topicToConfig[requestId]
|
|
318
1228
|
delete this.topicToSharedSecret[requestId]
|
|
319
|
-
this.
|
|
1229
|
+
delete this.topicToProofs[requestId]
|
|
1230
|
+
this.onRequestReceivedCallbacks[requestId] = []
|
|
320
1231
|
this.onGeneratingProofCallbacks[requestId] = []
|
|
321
1232
|
this.onBridgeConnectCallbacks[requestId] = []
|
|
322
1233
|
this.onProofGeneratedCallbacks[requestId] = []
|