-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathindex.ts
614 lines (564 loc) · 19 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
// @ts-nocheck
import { Buffer } from "buffer"
// End of compatibility with browsers.
import sha3 from "js-sha3"
import hrtime from "browser-process-hrtime"
// utilities for verifying signatures
import * as ethers from "ethers"
import * as cES from "./checkEtherScan.js"
import * as formatter from "./formatter.js"
// Currently supported API version.
const apiVersion = "0.3.0"
let VERBOSE = undefined
// Verification status
const INVALID_VERIFICATION_STATUS = "INVALID"
const VERIFIED_VERIFICATION_STATUS = "VERIFIED"
const ERROR_VERIFICATION_STATUS = "ERROR"
function getElapsedTime(start) {
const precision = 2 // 2 decimal places
const elapsed = hrtime(start)
// elapsed[1] is in nanosecond, so we divide by a billion to get nanosecond
// to second.
return (elapsed[0] + elapsed[1] / 1e9).toFixed(precision)
}
function getHashSum(content: string) {
return content === "" ? "" : sha3.sha3_512(content)
}
function calculateMetadataHash(
domainId: string,
timestamp: string,
previousVerificationHash: string = "",
mergeHash: string = ""
) {
return getHashSum(domainId + timestamp + previousVerificationHash + mergeHash)
}
function calculateSignatureHash(signature: string, publicKey: string) {
return getHashSum(signature + publicKey)
}
function calculateWitnessHash(
domain_snapshot_genesis_hash: string,
merkle_root: string,
witness_network: string,
witness_tx_hash: string,
) {
return getHashSum(
domain_snapshot_genesis_hash +
merkle_root +
witness_network +
witness_tx_hash
)
}
function calculateVerificationHash(
contentHash: string,
metadataHash: string,
signature_hash: string,
witness_hash: string,
) {
return getHashSum(contentHash + metadataHash + signature_hash + witness_hash)
}
/**
* Verifies the integrity of the merkle branch.
* Steps:
* - Traverses the nodes in the passed merkle branch.
* - Returns false if the verification hash is not found in the first leaves pair.
* - Returns false if the merkle branch hashes are inconsistent.
* @param {array} merkleBranch Array of merkle nodes.
* @param {string} verificationHash
* @returns {boolean} Whether the merkle integrity is OK.
*/
function verifyMerkleIntegrity(merkleBranch, verificationHash: string) {
if (merkleBranch.length === 0) {
return false
}
let prevSuccessor = null
for (const idx in merkleBranch) {
const node = merkleBranch[idx]
const leaves = [node.left_leaf, node.right_leaf]
if (prevSuccessor) {
if (!leaves.includes(prevSuccessor)) {
return false
}
} else {
// This means we are at the beginning of the loop.
if (!leaves.includes(verificationHash)) {
// In the beginning, either the left or right leaf must match the
// verification hash.
return false
}
}
let calculatedSuccessor: string
if (!node.left_leaf) {
calculatedSuccessor = node.right_leaf
} else if (!node.right_leaf) {
calculatedSuccessor = node.left_leaf
} else {
calculatedSuccessor = getHashSum(node.left_leaf + node.right_leaf)
}
if (calculatedSuccessor !== node.successor) {
//console.log("Expected successor", calculatedSuccessor)
//console.log("Actual successor", node.successor)
return false
}
prevSuccessor = node.successor
}
return true
}
/**
* TODO THIS DOCSTRING IS OUTDATED!
* Analyses the witnessing steps for a revision of a page and builds a
* verification log.
* Steps:
* - Calls get_witness_data API passing witness event ID.
* - Calls function getHashSum passing domain_snapshot_genesis_hash and
* merkle_root from the get_witness_data API call.
* - Writes witness event ID and transaction hash to the log.
* - Calls function checkEtherScan (see the file checkEtherScan.js) passing
* witness network, witness event transaction hash and the actual witness
* event verification hash.
* - If checkEtherScan returns true, writes to the log that witness is
* verified.
* - Else logs error from the checkEtherScan call.
* - If doVerifyMerkleProof is set, calls function verifyMerkleIntegrity.
* - Writes the teturned boolean value from verifyMerkleIntegrity to the
* log.
* - Returns the structured data summary of the witness verification.
* @param {int} witness_event_id
* @param {string} verificationHash
* @param {boolean} doVerifyMerkleProof Flag for do Verify Merkle Proof.
* @returns {Promise<string>} The verification log.
*/
async function verifyWitness(
witnessData,
verification_hash: string,
doVerifyMerkleProof: boolean,
) {
const actual_witness_event_verification_hash = getHashSum(
witnessData.domain_snapshot_genesis_hash + witnessData.merkle_root
)
const result = {
witness_hash: witnessData.witness_hash,
tx_hash: witnessData.witness_event_transaction_hash,
witness_network: witnessData.witness_network,
etherscan_result: "",
etherscan_error_message: "",
actual_witness_event_verification_hash:
actual_witness_event_verification_hash,
witness_event_vh_matches: true,
// `extra` is populated with useful info when the witness event verification
// doesn't match.
extra: null,
doVerifyMerkleProof: doVerifyMerkleProof,
merkle_proof_status: "",
}
// Do online lookup of transaction hash
const etherScanResult = await cES.checkEtherScan(
witnessData.witness_network,
witnessData.witness_event_transaction_hash,
actual_witness_event_verification_hash
)
result.etherscan_result = etherScanResult
if (etherScanResult !== "true" && etherScanResult !== "false") {
let errMsg
if (etherScanResult === "Transaction hash not found") {
errMsg = "Transaction hash not found"
} else if (etherScanResult.includes("ENETUNREACH")) {
errMsg = "Server is unreachable"
} else {
errMsg = "Online lookup failed"
}
result.etherscan_error_message = errMsg
}
if (
actual_witness_event_verification_hash !=
witnessData.witness_event_verification_hash
) {
result.witness_event_vh_matches = false
result.extra = {
domain_snapshot_genesis_hash: witnessData.domain_snapshot_genesis_hash,
merkle_root: witnessData.merkle_root,
witness_event_verification_hash:
witnessData.witness_event_verification_hash,
}
return ["INVALID", result]
}
// At this point, we know that the witness matches.
if (doVerifyMerkleProof) {
// Only verify the witness merkle proof when verifyWitness is successful,
// because this step is expensive.
if (verification_hash === witnessData.domain_snapshot_genesis_hash) {
// Corner case when the page is a Domain Snapshot.
result.merkle_proof_status = "DOMAIN_SNAPSHOT"
} else {
const merkleProofIsOK = verifyMerkleIntegrity(
witnessData.structured_merkle_proof,
verification_hash
)
result.merkle_proof_status = merkleProofIsOK ? "VALID" : "INVALID"
if (!merkleProofIsOK) {
return ["INVALID", result]
}
}
}
if (etherScanResult !== "true") {
return ["INVALID", result]
}
return ["VALID", result]
}
function verifyFile(data) {
const fileContentHash = data.content.content.file_hash || null
if (fileContentHash === null) {
return [
false,
{ error_message: "Revision contains a file, but no file content hash" },
]
}
const rawFileContent = Buffer.from(data.content.file.data || "", "base64")
if (fileContentHash !== getHashSum(rawFileContent)) {
return [false, { error_message: "File content hash does not match" }]
}
return [true, { file_hash: fileContentHash }]
}
function verifySignature(data: object, verificationHash: string) {
// TODO enforce that the verificationHash is a correct SHA3 sum string
// Specify signature correctness
let signatureOk = false
if (verificationHash === "") {
// The verificationHash MUST NOT be empty. This also implies that a genesis revision cannot
// contain a signature.
return [signatureOk, "INVALID"]
}
// Signature verification
// The padded message is required
const paddedMessage =
`I sign the following page verification_hash: [0x${verificationHash}]`
try {
const recoveredAddress = ethers.recoverAddress(
ethers.hashMessage(paddedMessage),
data.signature.signature
)
signatureOk = recoveredAddress.toLowerCase() === data.signature.wallet_address.toLowerCase()
} catch (e) {
// continue regardless of error
}
const status = signatureOk ? "VALID" : "INVALID"
return [signatureOk, status]
}
function verifyContent(data) {
let content = ""
for (const slotContent of Object.values(data.content.content)) {
content += slotContent
}
const contentHash = getHashSum(content)
return [contentHash === data.content.content_hash, contentHash]
}
function verifyMetadata(data) {
const metadataHash = calculateMetadataHash(
data.metadata.domain_id,
data.metadata.time_stamp,
data.metadata.previous_verification_hash ?? "",
data.metadata.merge_hash ?? ""
)
return [metadataHash === data.metadata.metadata_hash, metadataHash]
}
/**
* TODO THIS DOCSTRING IS OUTDATED!
* Verifies a revision from a page.
* Steps:
* - Calls verify_page API passing revision id.
* - Calculates metadata hash using previous verification hash.
* - Calls function verifyWitness using data from the verify_page API call.
* - Calculates the verification hash using content hash, metadata hash,
* signature hash and witness hash.
* - If the calculated verification hash is different from the verification
* hash returned from the first verify_page API calls then logs a hash
* mismatch error, else sets verification status to VERIFIED.
* - Does lookup on the Ethereum blockchain to find the witness_verification hash for digital timestamping
* stored in a smart contract to verify.
* - If the recovered Address equals the current wallet address, sets valid
* signature to true.
* - If witness status is inconsistent, sets witnessOk flag to false.
* @param {string} apiURL The URL for the API call.
* @param {Object} token The OAuth2 token required to make the API call or PKC must allow any request (LocalSettings.php).
* @param {string} revid The page revision id.
* @param {string} prevRevId The previous page revision id.
* @param {string} previousVerificationHash The previous verification hash string.
* @param {string} contentHash The page content hash string.
* @param {boolean} doVerifyMerkleProof Flag for do Verify Merkle Proof.
* @returns {Promise<Array>} An array containing verification data,
* verification-is-correct flag, and an array of page revision
* details.
*/
async function verifyRevision(
verificationHash: string,
input,
doVerifyMerkleProof: boolean
) {
let result = {
verification_hash: verificationHash,
status: {
content: false,
metadata: false,
signature: "MISSING",
witness: "MISSING",
verification: INVALID_VERIFICATION_STATUS,
file: "MISSING",
},
witness_result: {},
file_hash: "",
data: input.offline_data
}
const data = result.data
// File
if ("file" in data.content) {
// This is a file
const [fileIsCorrect, fileOut] = verifyFile(data)
if (!fileIsCorrect) {
return [fileIsCorrect, fileOut]
}
result.status.file = "VERIFIED"
result.file_hash = fileOut.file_hash
}
// Content
let [ok, contentHash] = verifyContent(data)
if (!ok) {
return [false, { error_message: "Content hash doesn't match" }]
}
// Mark content as correct
result.status.content = true
// To save storage for the cacher, e.g the Chrome extension.
delete result.data.content.content
delete result.data.content.file
// Metadata
let metadataHash
[ok, metadataHash] = verifyMetadata(data)
if (!ok) {
return [false, { error_message: "Metadata hash doesn't match" }]
}
// Mark metadata as correct
result.status.metadata = true
// TODO comparison with null is probably not needed. Needs testing.
const hasSignature = !(
!("signature" in data) ||
data.signature === null ||
data.signature.signature === "" ||
data.signature.signature === null
)
const hasWitness = !(data.witness === null || data.witness === undefined)
if (hasSignature && hasWitness) {
return [false, { error_message: "Signature and witness must not both be present"}]
}
let signatureHash = ""
if (hasSignature) {
let sigStatus
[ok, sigStatus] = verifySignature(
data,
data.metadata.previous_verification_hash
)
result.status.signature = sigStatus
signatureHash = data.signature.signature_hash
} else if (hasWitness) {
// Witness
const [witnessStatus, witnessResult] = await verifyWitness(
data.witness,
//as of version v1.2 Aqua protocol it takes always the previous verification hash
//as a witness and a signature MUST create a new revision of the Aqua-Chain
data.metadata.previous_verification_hash,
doVerifyMerkleProof
)
result.witness_result = witnessResult
result.status.witness = witnessStatus
// Specify witness correctness
ok = result.status.witness !== "INVALID"
}
const calculatedVerificationHash = calculateVerificationHash(
contentHash,
metadataHash,
signatureHash,
data.witness ? data.witness.witness_hash : ""
)
if (calculatedVerificationHash !== verificationHash) {
result.status.verification = INVALID_VERIFICATION_STATUS
return [false, result]
} else {
result.status.verification = VERIFIED_VERIFICATION_STATUS
}
return [ok, result]
}
function calculateStatus(count: number, totalLength: number) {
if (count == totalLength) {
if (count === 0) {
return "NORECORD"
} else {
return VERIFIED_VERIFICATION_STATUS
}
} else {
return INVALID_VERIFICATION_STATUS
}
}
/**
* TODO THIS DOCSTRING IS OUTDATED!
* Verifies all of the verified revisions of a page.
* Steps:
* - Loops through the revision IDs for the page.
* Calls function verifyRevision, if isCorrect flag is returned as true,
* yield true and the revision detail.
* @param {Array} verifiedRevIds Array of revision ids which have verification detail.
* @param {string} server The server URL for the API call.
* @param {boolean} verbose
* @param {boolean} doVerifyMerkleProof The flag for whether to do rigorous
* verification of the merkle proof. TODO clarify this.
* @param {Object} token (Optional) The OAuth2 token required to make the API call.
* @returns {Generator} Generator for isCorrect boolean and detail object of
* each revisions.
*/
async function* generateVerifyPage(
verificationHashes,
input,
verbose: boolean | undefined,
doVerifyMerkleProof: boolean
) {
let revisionInput
VERBOSE = verbose
let elapsed
let totalElapsed = 0.0
for (const vh of verificationHashes) {
const elapsedStart = hrtime()
// For offline verification, we simply pass in the data.
if ("offline_data" in input) {
revisionInput = {
offline_data: input.offline_data.revisions[vh],
}
}
const [isCorrect, detail] = await verifyRevision(
vh,
revisionInput,
doVerifyMerkleProof
)
elapsed = getElapsedTime(elapsedStart)
detail.elapsed = elapsed
totalElapsed += elapsed
if (!isCorrect) {
yield [false, detail]
return
}
yield [true, detail]
}
}
async function verifyPage(input, verbose, doVerifyMerkleProof) {
let verificationHashes
verificationHashes = Object.keys(input.offline_data.revisions)
console.log("Page Verification Hashes: ", verificationHashes)
let verificationStatus
// Secure feature to detect detached chain, missing genesis revision
const firstRevision = input.offline_data.revisions[verificationHashes[verificationHashes.length - 1]]
if (!firstRevision.metadata.previous_verification_hash === '') {
verificationStatus = INVALID_VERIFICATION_STATUS
console.log(`Status: ${verificationStatus}`)
return [verificationStatus, null]
}
let count = 0
if (verificationHashes.length > 0) {
// Print out the verification hash of the first one.
console.log(`${count + 1}. Verification of ${verificationHashes[0]}.`)
}
const details = {
verification_hashes: verificationHashes,
revision_details: [],
}
for await (const value of generateVerifyPage(
verificationHashes,
input,
verbose,
doVerifyMerkleProof
)) {
const [isCorrect, detail] = value
formatter.printRevisionInfo(detail, verbose)
details.revision_details.unshift(detail)
if (!isCorrect) {
verificationStatus = INVALID_VERIFICATION_STATUS
break
}
count += 1
console.log(
` Progress: ${count} / ${verificationHashes.length} (${(
(100 * count) /
verificationHashes.length
).toFixed(1)}%)`
)
if (count < verificationHashes.length) {
console.log(
`${count + 1}. Verification of Revision ${verificationHashes[count]}.`
)
}
}
verificationStatus = calculateStatus(count, verificationHashes.length)
console.log(`Status: ${verificationStatus}`)
return [verificationStatus, details]
}
async function readFromMediaWikiAPI(server, title) {
let response, data
response = await fetch(
`${server}/rest.php/data_accounting/get_page_last_rev?page_title=${title}`,
)
data = await response.json()
if (!response.ok) {
formatter.log_red(`Error: get_page_last_rev: ${data.message}`)
}
const verificationHash = data.verification_hash
response = await fetch(
`${server}/rest.php/data_accounting/get_branch/${verificationHash}`
)
data = await response.json()
const hashes = data.hashes
const revisions = {}
for (const vh of hashes) {
response = await fetch(
`${server}/rest.php/data_accounting/get_revision/${vh}`
)
revisions[vh] = await response.json()
}
return { revisions }
}
async function getServerInfo(server) {
const url = `${server}/rest.php/data_accounting/get_server_info`
return fetch(url)
}
async function checkAPIVersionCompatibility(server) {
const response = await getServerInfo(server)
if (!response.ok) {
return [formatHTTPError(response), false, ""]
}
const data = await response.json()
if (data && data.api_version) {
return ["FOUND", data.api_version === apiVersion, data.api_version]
}
return ["API endpoint found, but API version can't be retrieved", false, ""]
}
async function verifyPageFromMwAPI(server, title, verbose, ignoreMerkleProof) {
let verifiedContent
try {
verifiedContent = await readFromMediaWikiAPI(server, title)
} catch (e) {
// TODO: be more specific than just returning empty revisions
// NORECORD
verifiedContent = { revisions: {} }
}
const input = { offline_data: verifiedContent}
return await verifyPage(input, verbose, !ignoreMerkleProof)
}
export {
generateVerifyPage,
verifyPage,
apiVersion,
// For verified_import.js
ERROR_VERIFICATION_STATUS,
// For notarize.js
getHashSum,
calculateMetadataHash,
calculateVerificationHash,
calculateSignatureHash,
// For the VerifyPage Chrome extension and CLI
verifyPageFromMwAPI,
formatter,
checkAPIVersionCompatibility,
}