burnledger 0.9.0 → 0.10.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 +9 -4
- package/dist/cjs/audit-pack.d.ts +201 -0
- package/dist/cjs/audit-pack.d.ts.map +1 -0
- package/dist/cjs/audit-pack.js +867 -0
- package/dist/cjs/audit-pack.js.map +1 -0
- package/dist/cjs/client.d.ts +41 -1
- package/dist/cjs/client.d.ts.map +1 -1
- package/dist/cjs/client.js +107 -59
- package/dist/cjs/client.js.map +1 -1
- package/dist/cjs/enclave-registration.d.ts +14 -2
- package/dist/cjs/enclave-registration.d.ts.map +1 -1
- package/dist/cjs/enclave-registration.js +14 -2
- package/dist/cjs/enclave-registration.js.map +1 -1
- package/dist/cjs/index.d.ts +46 -2
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +114 -2
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/models.d.ts +28 -2
- package/dist/cjs/models.d.ts.map +1 -1
- package/dist/cjs/models.js +19 -1
- package/dist/cjs/models.js.map +1 -1
- package/dist/cjs/node-runtime.d.ts +40 -0
- package/dist/cjs/node-runtime.d.ts.map +1 -0
- package/dist/cjs/node-runtime.js +42 -0
- package/dist/cjs/node-runtime.js.map +1 -0
- package/dist/cjs/run-record.d.ts +130 -0
- package/dist/cjs/run-record.d.ts.map +1 -0
- package/dist/cjs/run-record.js +272 -0
- package/dist/cjs/run-record.js.map +1 -0
- package/dist/cjs/verify.d.ts +58 -0
- package/dist/cjs/verify.d.ts.map +1 -1
- package/dist/cjs/verify.js +277 -38
- package/dist/cjs/verify.js.map +1 -1
- package/dist/cjs/webhooks.d.ts +9 -2
- package/dist/cjs/webhooks.d.ts.map +1 -1
- package/dist/cjs/webhooks.js +29 -11
- package/dist/cjs/webhooks.js.map +1 -1
- package/dist/esm/audit-pack.d.ts +201 -0
- package/dist/esm/audit-pack.d.ts.map +1 -0
- package/dist/esm/audit-pack.js +858 -0
- package/dist/esm/audit-pack.js.map +1 -0
- package/dist/esm/cli.d.ts +52 -0
- package/dist/esm/cli.d.ts.map +1 -1
- package/dist/esm/cli.js +283 -11
- package/dist/esm/cli.js.map +1 -1
- package/dist/esm/client.d.ts +41 -1
- package/dist/esm/client.d.ts.map +1 -1
- package/dist/esm/client.js +108 -27
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/enclave-registration.d.ts +14 -2
- package/dist/esm/enclave-registration.d.ts.map +1 -1
- package/dist/esm/enclave-registration.js +14 -2
- package/dist/esm/enclave-registration.js.map +1 -1
- package/dist/esm/index.d.ts +46 -2
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +64 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/models.d.ts +28 -2
- package/dist/esm/models.d.ts.map +1 -1
- package/dist/esm/models.js +18 -1
- package/dist/esm/models.js.map +1 -1
- package/dist/esm/node-runtime.d.ts +40 -0
- package/dist/esm/node-runtime.d.ts.map +1 -0
- package/dist/esm/node-runtime.js +38 -0
- package/dist/esm/node-runtime.js.map +1 -0
- package/dist/esm/run-record.d.ts +130 -0
- package/dist/esm/run-record.d.ts.map +1 -0
- package/dist/esm/run-record.js +262 -0
- package/dist/esm/run-record.js.map +1 -0
- package/dist/esm/verify.d.ts +58 -0
- package/dist/esm/verify.d.ts.map +1 -1
- package/dist/esm/verify.js +270 -41
- package/dist/esm/verify.js.map +1 -1
- package/dist/esm/webhooks.d.ts +9 -2
- package/dist/esm/webhooks.d.ts.map +1 -1
- package/dist/esm/webhooks.js +29 -11
- package/dist/esm/webhooks.js.map +1 -1
- package/package.json +1 -1
- package/src/audit-pack.ts +1067 -0
- package/src/cli.ts +289 -10
- package/src/client.ts +132 -27
- package/src/enclave-registration.ts +14 -2
- package/src/index.ts +130 -1
- package/src/models.ts +47 -3
- package/src/node-runtime.ts +57 -0
- package/src/run-record.ts +371 -0
- package/src/verify.ts +301 -41
- package/src/webhooks.ts +28 -11
package/dist/cjs/verify.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
20
|
exports.VALID_REVOCATION_UNKNOWN = exports.PRE_PROTOCOL_12_IMAGES = exports.VALID_KEY_WINDOW_UNKNOWN = exports.VALID_KEY_COMPROMISED_LATER = exports.KEY_COMPROMISED = exports.KEY_OUTSIDE_VALIDITY = exports.KNOWN_FORMAT_VERSIONS = void 0;
|
|
21
|
+
exports.signatureCoversRunId = signatureCoversRunId;
|
|
21
22
|
exports.hexToBytes = hexToBytes;
|
|
22
23
|
exports.bytesToHex = bytesToHex;
|
|
23
24
|
exports.publicKeyFromHex = publicKeyFromHex;
|
|
@@ -28,10 +29,19 @@ exports.issuingImage = issuingImage;
|
|
|
28
29
|
exports.registrationQualifier = registrationQualifier;
|
|
29
30
|
exports.verifyCertificate = verifyCertificate;
|
|
30
31
|
exports.verifyTransparency = verifyTransparency;
|
|
32
|
+
exports.verifyTransparencyHead = verifyTransparencyHead;
|
|
33
|
+
exports.verifyTransparencyInclusion = verifyTransparencyInclusion;
|
|
34
|
+
exports.transparencyVerdictName = transparencyVerdictName;
|
|
31
35
|
exports.issuanceBytes = issuanceBytes;
|
|
32
36
|
exports.canonicalJson = canonicalJson;
|
|
37
|
+
exports.parseUuid = parseUuid;
|
|
38
|
+
exports.formatUuid = formatUuid;
|
|
39
|
+
exports.decodeFixed = decodeFixed;
|
|
33
40
|
exports.formatTimestamp = formatTimestamp;
|
|
41
|
+
exports.buildLogLeafPayload = buildLogLeafPayload;
|
|
34
42
|
exports.buildTreeHeadPayload = buildTreeHeadPayload;
|
|
43
|
+
exports.hashLeaf = hashLeaf;
|
|
44
|
+
exports.merkleRoot = merkleRoot;
|
|
35
45
|
exports.verifyConsistency = verifyConsistency;
|
|
36
46
|
exports.buildCertificateStatusPayload = buildCertificateStatusPayload;
|
|
37
47
|
exports.buildKeyListPayload = buildKeyListPayload;
|
|
@@ -98,6 +108,17 @@ const FORMAT_VERSION_V8 = "8.0";
|
|
|
98
108
|
const PAYLOAD_TYPE_ATTESTATION_V9 = "burnledger.attestation.v9";
|
|
99
109
|
const PAYLOAD_TYPE_VERIFICATION_RECORD_V9 = "burnledger.verification_record.v9";
|
|
100
110
|
const FORMAT_VERSION_V9 = "9.0";
|
|
111
|
+
// v10 signs one top-level field, run_id: the run-level record (ADR-031) the
|
|
112
|
+
// record was issued under, when it was issued under one. The attestation bytes
|
|
113
|
+
// are v9's; its tag still moves, for the reason every version's does.
|
|
114
|
+
//
|
|
115
|
+
// READ-ONLY IN THIS BUILD, mirroring core, for the runbook-invariant-5 reason
|
|
116
|
+
// spelled out for v8 above. Until issuance moves, a run binds its records by
|
|
117
|
+
// the run record's records_root alone (run-record.ts), which is the binding
|
|
118
|
+
// the verifier checks in every case.
|
|
119
|
+
const PAYLOAD_TYPE_ATTESTATION_V10 = "burnledger.attestation.v10";
|
|
120
|
+
const PAYLOAD_TYPE_VERIFICATION_RECORD_V10 = "burnledger.verification_record.v10";
|
|
121
|
+
const FORMAT_VERSION_V10 = "10.0";
|
|
101
122
|
const FORMAT_VERSION_V3 = "3.0";
|
|
102
123
|
/**
|
|
103
124
|
* The only signature scheme this SDK can check. A v7 record names its own
|
|
@@ -123,6 +144,7 @@ exports.KNOWN_FORMAT_VERSIONS = Object.freeze([
|
|
|
123
144
|
FORMAT_VERSION_V7,
|
|
124
145
|
FORMAT_VERSION_V8,
|
|
125
146
|
FORMAT_VERSION_V9,
|
|
147
|
+
FORMAT_VERSION_V10,
|
|
126
148
|
]);
|
|
127
149
|
/**
|
|
128
150
|
* Returns the version, or throws if this SDK cannot read it.
|
|
@@ -190,6 +212,19 @@ function signatureCoversRecoverableState(version) {
|
|
|
190
212
|
function signatureCoversAuthorization(version) {
|
|
191
213
|
return formatAtLeast(version, FORMAT_VERSION_V9);
|
|
192
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Does this format sign the top-level run_id (ADR-031)?
|
|
217
|
+
*
|
|
218
|
+
* v10 onward, and optional even there: a record issued outside a run has none.
|
|
219
|
+
* A record OLDER than v10 that carries one is refused outright rather than
|
|
220
|
+
* signed without it — the field would ride in the JSON beside a signature that
|
|
221
|
+
* says nothing about it. Exported for the audit pack verifier, which asks the
|
|
222
|
+
* same question of a record that carries none: below v10 that says nothing
|
|
223
|
+
* about its run, from v10 it says the record was issued under no run.
|
|
224
|
+
*/
|
|
225
|
+
function signatureCoversRunId(version) {
|
|
226
|
+
return formatAtLeast(version, FORMAT_VERSION_V10);
|
|
227
|
+
}
|
|
193
228
|
/** The two values a v9 record's per-system authorization may take. */
|
|
194
229
|
const AUTHORIZATION_CERTIFIED = "certified";
|
|
195
230
|
const AUTHORIZATION_NONE = "none";
|
|
@@ -373,6 +408,18 @@ function evaluateKey(pki, anchor) {
|
|
|
373
408
|
if (pki.keyStatus === "compromised") {
|
|
374
409
|
if (anchor === null || pki.compromisedFrom === undefined)
|
|
375
410
|
return exports.KEY_COMPROMISED;
|
|
411
|
+
// The validity window is tested before the compromise anchor (R03-1). A
|
|
412
|
+
// signature made outside the stated interval was never authorized, whatever
|
|
413
|
+
// the key's later disposition, so an anchor outside the window is
|
|
414
|
+
// KEY_OUTSIDE_VALIDITY even when it predates compromisedFrom. Skipping the
|
|
415
|
+
// window handed the soft, usable VALID_KEY_COMPROMISED_LATER to a signature
|
|
416
|
+
// the window alone already refuses.
|
|
417
|
+
if (pki.notBefore !== undefined && anchor < parseRfc3339(pki.notBefore)) {
|
|
418
|
+
return exports.KEY_OUTSIDE_VALIDITY;
|
|
419
|
+
}
|
|
420
|
+
if (pki.notAfter !== undefined && anchor >= parseRfc3339(pki.notAfter)) {
|
|
421
|
+
return exports.KEY_OUTSIDE_VALIDITY;
|
|
422
|
+
}
|
|
376
423
|
return anchor < parseRfc3339(pki.compromisedFrom)
|
|
377
424
|
? exports.VALID_KEY_COMPROMISED_LATER
|
|
378
425
|
: exports.KEY_COMPROMISED;
|
|
@@ -420,42 +467,44 @@ function certificateAnchor(certificate) {
|
|
|
420
467
|
}
|
|
421
468
|
}
|
|
422
469
|
/**
|
|
423
|
-
* The anchor, but only once
|
|
424
|
-
*
|
|
470
|
+
* The anchor, but only once this record has been PROVEN to be in the tree head
|
|
471
|
+
* carrying it — its inclusion proof verified against that head, not merely the
|
|
472
|
+
* head's own signature checked.
|
|
425
473
|
*
|
|
426
|
-
* The bypass this closes: for a key declared compromised,
|
|
427
|
-
* KEY_COMPROMISED with no anchor and
|
|
428
|
-
* anchor predates the compromise — and the
|
|
429
|
-
* passes through as the RESULT of
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
474
|
+
* The bypass this closes (R12-1/R14-1): for a key declared compromised,
|
|
475
|
+
* evaluateKey answers KEY_COMPROMISED with no anchor and
|
|
476
|
+
* VALID_KEY_COMPROMISED_LATER when the anchor predates the compromise — and the
|
|
477
|
+
* second is a verdict requireUsableKey passes through as the RESULT of
|
|
478
|
+
* verifyCertificate. A signed tree head is a public artifact: any genuine older
|
|
479
|
+
* head the issuer ever published can be stapled onto a record it never
|
|
480
|
+
* contained, and its signature still verifies. Checking only the head signature
|
|
481
|
+
* let a holder of a certificate signed after the compromise attach a
|
|
482
|
+
* pre-compromise head and have the forgery reported as verified-with-a-caveat.
|
|
483
|
+
* The head's timestamp is an honest anchor for THIS record only once THIS record
|
|
484
|
+
* is shown to be committed to under it.
|
|
433
485
|
*
|
|
434
|
-
*
|
|
435
|
-
*
|
|
486
|
+
* checkCertificateInclusion is exactly that proof; it deliberately does NOT test
|
|
487
|
+
* the key's usability at head time — that is the verdict this anchor exists to
|
|
488
|
+
* compute, so gating the anchor on it would drop the anchor for an out-of-window
|
|
489
|
+
* key and soften KEY_OUTSIDE_VALIDITY to VALID_KEY_WINDOW_UNKNOWN. Anything short
|
|
490
|
+
* of a held inclusion proof yields no anchor, the same conservative answer as a
|
|
491
|
+
* certificate with no transparency block at all.
|
|
436
492
|
*/
|
|
437
493
|
async function verifiedCertificateAnchor(crypto, certificate, pki) {
|
|
438
494
|
const anchor = certificateAnchor(certificate);
|
|
439
495
|
if (anchor === null)
|
|
440
496
|
return null;
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
//
|
|
444
|
-
// here gave a defect in this library — or a crypto provider that threw — the
|
|
445
|
-
// same quiet answer as a malformed document.
|
|
446
|
-
let headPayload;
|
|
447
|
-
let headSig;
|
|
497
|
+
// Only a refusal of the proof means "no anchor". A defect in this library, or
|
|
498
|
+
// a crypto provider that threw, must not get the same quiet answer as a
|
|
499
|
+
// document whose inclusion proof does not verify.
|
|
448
500
|
try {
|
|
449
|
-
|
|
450
|
-
headSig = decodeFixed(sth.signature, "signed_tree_head.signature", 64);
|
|
501
|
+
await checkCertificateInclusion(crypto, certificate, pki);
|
|
451
502
|
}
|
|
452
503
|
catch (e) {
|
|
453
504
|
if (e instanceof errors_js_1.VerificationError)
|
|
454
505
|
return null;
|
|
455
506
|
throw e;
|
|
456
507
|
}
|
|
457
|
-
if (!(await crypto.ed25519Verify(pki.keyBytes, headPayload, headSig)))
|
|
458
|
-
return null;
|
|
459
508
|
return anchor;
|
|
460
509
|
}
|
|
461
510
|
/** Throws for a key that cannot be used at all; returns the verdict otherwise.
|
|
@@ -739,14 +788,37 @@ async function verifyTransparency(crypto, certificate, publicKeys) {
|
|
|
739
788
|
return "NOT_AVAILABLE";
|
|
740
789
|
}
|
|
741
790
|
const transparency = objectAt(certificate, "transparency", "transparency");
|
|
742
|
-
|
|
791
|
+
// Read before the key lookup, as it always was: a proof with no head is
|
|
792
|
+
// reported as that, not as a key the reader does not hold.
|
|
793
|
+
objectAt(transparency, "signed_tree_head", "transparency.signed_tree_head");
|
|
743
794
|
const issuer = objectAt(certificate, "issuer", "issuer");
|
|
744
795
|
const keyId = stringAt(issuer, "key_id", "issuer.key_id");
|
|
745
796
|
const pki = publicKeys.get(keyId);
|
|
746
797
|
if (pki === undefined)
|
|
747
798
|
throw new errors_js_1.VerificationError(`unknown issuer key: ${keyId}`);
|
|
748
|
-
|
|
749
|
-
|
|
799
|
+
await verifyTransparencyHead(crypto, transparency, pki, keyId);
|
|
800
|
+
// The leaf is NOT the certificate. It is the canonical log_leaf.v3 payload
|
|
801
|
+
// over entry_type, certificate_id, certificate_hash and appended_at, where
|
|
802
|
+
// certificate_hash is SHA-256 of the issuance-time certificate JSON
|
|
803
|
+
// (ADR-016 §4). v2 hashed the certificate directly, so an issuance and a
|
|
804
|
+
// revocation of the same certificate produced identical leaves and the tree
|
|
805
|
+
// committed to neither the entry type nor when it happened.
|
|
806
|
+
const issuanceData = issuanceBytes(certificate);
|
|
807
|
+
const leafPayload = buildLogLeafPayload(transparency.entry_type, stringAt(certificate, "certificate_id", "certificate_id"), await crypto.sha256(issuanceData), transparency.appended_at);
|
|
808
|
+
await verifyTransparencyInclusion(crypto, transparency, await hashLeaf(crypto, leafPayload));
|
|
809
|
+
return "INCLUDED";
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Verify the head signature and the Merkle inclusion of a certificate's proof —
|
|
813
|
+
* everything about whether this record is committed to under its head, but NOT
|
|
814
|
+
* whether the key was usable when it signed. That last question is the caller's;
|
|
815
|
+
* certificateAnchor needs membership, not authority, and folding authority in
|
|
816
|
+
* here made the anchor for an out-of-window key collapse to "no anchor" and the
|
|
817
|
+
* verdict soften. Throws VerificationError on any inclusion failure.
|
|
818
|
+
*/
|
|
819
|
+
async function checkCertificateInclusion(crypto, certificate, pki) {
|
|
820
|
+
const transparency = objectAt(certificate, "transparency", "transparency");
|
|
821
|
+
const sth = objectAt(transparency, "signed_tree_head", "transparency.signed_tree_head");
|
|
750
822
|
// 1. Tree head signature
|
|
751
823
|
const headPayload = buildTreeHeadPayload(sth);
|
|
752
824
|
const headSig = decodeFixed(sth.signature, "signed_tree_head.signature", 64);
|
|
@@ -797,7 +869,91 @@ async function verifyTransparency(crypto, certificate, publicKeys) {
|
|
|
797
869
|
if (!(await verifyInclusion(crypto, leaf, index, treeSize, proofHashes, root))) {
|
|
798
870
|
throw new errors_js_1.VerificationError("merkle inclusion proof is invalid");
|
|
799
871
|
}
|
|
800
|
-
|
|
872
|
+
}
|
|
873
|
+
/** The first half of a transparency check, shared with the run record's
|
|
874
|
+
* (run-record.ts): the key's authority at the head's own timestamp, then the
|
|
875
|
+
* head's signature. The tree head carries its own timestamp, so this path
|
|
876
|
+
* always has an anchor; a timestamp that does not read leaves none, which is
|
|
877
|
+
* the same conservative answer as no proof at all. Throws the VerificationError
|
|
878
|
+
* the caller reports. Exported as an internal seam, not from index.ts. */
|
|
879
|
+
async function verifyTransparencyHead(crypto, transparency, pki, keyId) {
|
|
880
|
+
const sth = objectAt(transparency, "signed_tree_head", "transparency.signed_tree_head");
|
|
881
|
+
let anchor = null;
|
|
882
|
+
if (typeof sth.timestamp === "string") {
|
|
883
|
+
try {
|
|
884
|
+
anchor = parseRfc3339(sth.timestamp);
|
|
885
|
+
}
|
|
886
|
+
catch {
|
|
887
|
+
anchor = null;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
requireUsableKey(pki, anchor, keyId);
|
|
891
|
+
const headPayload = buildTreeHeadPayload(sth);
|
|
892
|
+
const headSig = decodeFixed(sth.signature, "signed_tree_head.signature", 64);
|
|
893
|
+
if (!(await crypto.ed25519Verify(pki.keyBytes, headPayload, headSig))) {
|
|
894
|
+
throw new errors_js_1.VerificationError("tree head signature is invalid");
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
/** The second half: an already-hashed leaf against the signed head, after the
|
|
898
|
+
* unsigned duplicates beside the proof are held to the signed copies. Throws
|
|
899
|
+
* the VerificationError the caller reports. Exported as an internal seam. */
|
|
900
|
+
async function verifyTransparencyInclusion(crypto, transparency, leaf) {
|
|
901
|
+
const sth = objectAt(transparency, "signed_tree_head", "transparency.signed_tree_head");
|
|
902
|
+
const proofHashes = arrayAt(transparency, "inclusion_proof", "transparency.inclusion_proof").map((h, i) => decodeFixed(h, `transparency.inclusion_proof[${i}]`, 32));
|
|
903
|
+
const root = decodeFixed(sth.root_hash, "signed_tree_head.root_hash", 32);
|
|
904
|
+
// `?? 0` is not a convenience: encoding/json leaves 0 in Go's uint64 fields
|
|
905
|
+
// for an explicit null and for an absent key rather than failing, so refusing
|
|
906
|
+
// either would reject documents the reference accepts — the same class of
|
|
907
|
+
// divergence as #452. An offline verifier is only useful while it agrees.
|
|
908
|
+
const index = requireUint(transparency.entry_index ?? 0, "entry_index");
|
|
909
|
+
const treeSize = requireUint(sth.tree_size ?? 0, "signed_tree_head.tree_size");
|
|
910
|
+
// transparency.tree_size duplicates the signed one but carries no signature:
|
|
911
|
+
// BuildTreeHeadPayload covers only the copy inside signed_tree_head. Go used
|
|
912
|
+
// to verify against the unsigned copy while this SDK used the signed one, so
|
|
913
|
+
// the same document got two verdicts (#456). Both now require the two to agree
|
|
914
|
+
// and then verify against the signed copy — the server always writes them
|
|
915
|
+
// equal, so this rejects only edited documents.
|
|
916
|
+
const unsignedSize = requireUint(transparency.tree_size ?? 0, "transparency.tree_size");
|
|
917
|
+
if (unsignedSize !== treeSize) {
|
|
918
|
+
throw new errors_js_1.VerificationError(`transparency.tree_size (${unsignedSize}) does not match the signed tree head ` +
|
|
919
|
+
`(${treeSize}); it is not covered by any signature`);
|
|
920
|
+
}
|
|
921
|
+
// Same rule for the log id (Q31). transparency.log_id sits beside log_url and
|
|
922
|
+
// is the field a reader looks at to answer "which log is this?", and nothing
|
|
923
|
+
// signs it — so a holder could relabel a genuine proof as belonging to a
|
|
924
|
+
// different log while every signature still checked out. A head predating log
|
|
925
|
+
// ids has neither side set and passes.
|
|
926
|
+
const signedLogId = sth.log_id ?? "";
|
|
927
|
+
const unsignedLogId = transparency.log_id ?? "";
|
|
928
|
+
if (unsignedLogId !== signedLogId) {
|
|
929
|
+
throw new errors_js_1.VerificationError(`transparency.log_id (${JSON.stringify(unsignedLogId)}) does not match the signed ` +
|
|
930
|
+
`tree head (${JSON.stringify(signedLogId)}); it is not covered by any signature`);
|
|
931
|
+
}
|
|
932
|
+
if (!(await verifyInclusion(crypto, leaf, index, treeSize, proofHashes, root))) {
|
|
933
|
+
throw new errors_js_1.VerificationError("merkle inclusion proof is invalid");
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
/** The refusals of verifyTransparencyHead and verifyTransparencyInclusion under
|
|
937
|
+
* core.TransparencyResult's names, for the verifiers that report a pack or a
|
|
938
|
+
* run record beside Go's verdicts rather than by throwing. The key verdicts
|
|
939
|
+
* collapse to KEY_UNUSABLE as they do in Go, whose transparency result does not
|
|
940
|
+
* say which key problem. Anything unmatched is a field this SDK could not read:
|
|
941
|
+
* UNREADABLE_DOCUMENT, never a tampering claim. */
|
|
942
|
+
const TRANSPARENCY_VERDICTS = [
|
|
943
|
+
["issuer key is compromised:", "KEY_UNUSABLE"],
|
|
944
|
+
["issuer key was not valid when it signed:", "KEY_UNUSABLE"],
|
|
945
|
+
["issuer key has an unusable status", "KEY_UNUSABLE"],
|
|
946
|
+
["tree head signature is invalid", "INVALID_TREE_HEAD_SIGNATURE"],
|
|
947
|
+
["transparency.tree_size (", "INVALID_INCLUSION_PROOF"],
|
|
948
|
+
["transparency.log_id (", "INVALID_INCLUSION_PROOF"],
|
|
949
|
+
["merkle inclusion proof is invalid", "INVALID_INCLUSION_PROOF"],
|
|
950
|
+
];
|
|
951
|
+
function transparencyVerdictName(e) {
|
|
952
|
+
for (const [prefix, verdict] of TRANSPARENCY_VERDICTS) {
|
|
953
|
+
if (e.message.startsWith(prefix))
|
|
954
|
+
return verdict;
|
|
955
|
+
}
|
|
956
|
+
return "UNREADABLE_DOCUMENT";
|
|
801
957
|
}
|
|
802
958
|
// ---------------------------------------------------------------------------
|
|
803
959
|
// Issuance bytes — matches Go's core.IssuanceBytes(cert)
|
|
@@ -890,6 +1046,45 @@ function arrayAt(parent, key, path) {
|
|
|
890
1046
|
throw new errors_js_1.VerificationError(`${path} is not an array`);
|
|
891
1047
|
return Array.from(value);
|
|
892
1048
|
}
|
|
1049
|
+
/** A UUID in the forms Go's uuid.Parse accepts: 8-4-4-4-12 hex, the same in
|
|
1050
|
+
* braces or behind "urn:uuid:", or 32 bare hex digits. */
|
|
1051
|
+
const UUID_RE = /^(?:urn:uuid:)?\{?([0-9a-fA-F]{8})-([0-9a-fA-F]{4})-([0-9a-fA-F]{4})-([0-9a-fA-F]{4})-([0-9a-fA-F]{12})\}?$/;
|
|
1052
|
+
const UUID_BARE_RE = /^[0-9a-fA-F]{32}$/;
|
|
1053
|
+
/** The 16 bytes of a UUID, in any form Go's uuid.Parse reads. Throws TypeError
|
|
1054
|
+
* for anything else: a UUID reaches a signed payload only as its canonical
|
|
1055
|
+
* string, and an unreadable one must not be signed as whatever text arrived. */
|
|
1056
|
+
function parseUuid(text) {
|
|
1057
|
+
if (typeof text !== "string")
|
|
1058
|
+
throw new TypeError(`uuid is not a string: ${typeof text}`);
|
|
1059
|
+
const m = UUID_RE.exec(text);
|
|
1060
|
+
const hex = m !== null ? m.slice(1).join("") : UUID_BARE_RE.test(text) ? text : null;
|
|
1061
|
+
if (hex === null)
|
|
1062
|
+
throw new TypeError(`not a UUID: ${JSON.stringify(text)}`);
|
|
1063
|
+
return hexToBytes(hex);
|
|
1064
|
+
}
|
|
1065
|
+
/** The canonical lowercase 8-4-4-4-12 form, as Go's uuid.UUID.String prints:
|
|
1066
|
+
* the only form a UUID takes inside a signed payload. */
|
|
1067
|
+
function formatUuid(bytes) {
|
|
1068
|
+
if (bytes.length !== 16)
|
|
1069
|
+
throw new TypeError(`uuid is ${bytes.length} bytes, expected 16`);
|
|
1070
|
+
const h = bytesToHex(bytes);
|
|
1071
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
|
|
1072
|
+
}
|
|
1073
|
+
/** Read a UUID field as Go's uuid.UUID unmarshals it, refusing with a
|
|
1074
|
+
* VerificationError what encoding/json would refuse to parse. */
|
|
1075
|
+
function uuidAt(parent, key, path) {
|
|
1076
|
+
const v = parent[key];
|
|
1077
|
+
if (typeof v !== "string")
|
|
1078
|
+
throw new errors_js_1.VerificationError(`${path} is not a string`);
|
|
1079
|
+
try {
|
|
1080
|
+
return formatUuid(parseUuid(v));
|
|
1081
|
+
}
|
|
1082
|
+
catch (e) {
|
|
1083
|
+
if (e instanceof TypeError)
|
|
1084
|
+
throw new errors_js_1.VerificationError(`${path} is not a UUID: ${JSON.stringify(v)}`);
|
|
1085
|
+
throw e;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
893
1088
|
const HEX_TEXT_RE = /^(?:[0-9a-fA-F]{2})*$/;
|
|
894
1089
|
const BASE64_TEXT_RE = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
895
1090
|
/**
|
|
@@ -905,6 +1100,8 @@ const BASE64_TEXT_RE = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/
|
|
|
905
1100
|
* leniently instead wrapped 300 to 44 inside a Uint8Array, produced bytes no
|
|
906
1101
|
* signature covers and blamed the signature — or threw from inside atob.
|
|
907
1102
|
*/
|
|
1103
|
+
/** Exported for run-record.ts, whose digests and roots arrive in the same
|
|
1104
|
+
* shapes; not published from index.ts. */
|
|
908
1105
|
function decodeFixed(value, path, length) {
|
|
909
1106
|
if (value === undefined || value === null)
|
|
910
1107
|
throw new errors_js_1.VerificationError(`${path} is missing`);
|
|
@@ -1142,6 +1339,8 @@ function requireMeasured(value, field, systemName, version) {
|
|
|
1142
1339
|
/** Domain separator for the record itself, by certificate format version. */
|
|
1143
1340
|
function certificatePayloadType(version) {
|
|
1144
1341
|
checkFormatVersion(version);
|
|
1342
|
+
if (version === FORMAT_VERSION_V10)
|
|
1343
|
+
return PAYLOAD_TYPE_VERIFICATION_RECORD_V10;
|
|
1145
1344
|
if (version === FORMAT_VERSION_V9)
|
|
1146
1345
|
return PAYLOAD_TYPE_VERIFICATION_RECORD_V9;
|
|
1147
1346
|
if (version === FORMAT_VERSION_V8)
|
|
@@ -1167,6 +1366,8 @@ function attestationPayloadType(version) {
|
|
|
1167
1366
|
// Equality here, deliberately: each format has its OWN separator, so this is
|
|
1168
1367
|
// a lookup rather than a "this version onward" question. v3 and v4 share one
|
|
1169
1368
|
// because their attestation bytes are identical.
|
|
1369
|
+
if (version === FORMAT_VERSION_V10)
|
|
1370
|
+
return PAYLOAD_TYPE_ATTESTATION_V10;
|
|
1170
1371
|
if (version === FORMAT_VERSION_V9)
|
|
1171
1372
|
return PAYLOAD_TYPE_ATTESTATION_V9;
|
|
1172
1373
|
if (version === FORMAT_VERSION_V8)
|
|
@@ -1296,11 +1497,25 @@ function buildCertificatePayload(cert) {
|
|
|
1296
1497
|
version: stringAt(scope, "version", "scope.version"),
|
|
1297
1498
|
};
|
|
1298
1499
|
}
|
|
1500
|
+
// Signed from v10 when present. On an older format the field is refused
|
|
1501
|
+
// rather than dropped from the bytes: core.BuildCertificatePayload returns an
|
|
1502
|
+
// error there and the verdict is INVALID_CERTIFICATE_SIGNATURE, so the same
|
|
1503
|
+
// words open this message for the audit pack verifier to read it back as
|
|
1504
|
+
// that verdict. A JSON null is an absent run, as Go's *uuid.UUID reads it.
|
|
1505
|
+
if (cert.run_id != null) {
|
|
1506
|
+
if (!signatureCoversRunId(version)) {
|
|
1507
|
+
throw new errors_js_1.VerificationError(`certificate signature is invalid: the record carries run_id, which format ${version} does not ` +
|
|
1508
|
+
`sign (run_id is signed from ${FORMAT_VERSION_V10}), so no signature covers the document as presented`);
|
|
1509
|
+
}
|
|
1510
|
+
payload.run_id = uuidAt(cert, "run_id", "run_id");
|
|
1511
|
+
}
|
|
1299
1512
|
return canonicalJson(payload);
|
|
1300
1513
|
}
|
|
1301
|
-
/** Port of Go's BuildLogLeafPayload (ADR-016 §4).
|
|
1514
|
+
/** Port of Go's BuildLogLeafPayload (ADR-016 §4). A RUN_RECORD leaf names its
|
|
1515
|
+
* run id where the other two name a certificate id, under the same key
|
|
1516
|
+
* (ADR-031). Exported for run-record.ts; not published from index.ts. */
|
|
1302
1517
|
function buildLogLeafPayload(entryType, certificateId, certificateHash, appendedAt) {
|
|
1303
|
-
if (entryType !== "CERTIFICATE" && entryType !== "REVOCATION") {
|
|
1518
|
+
if (entryType !== "CERTIFICATE" && entryType !== "REVOCATION" && entryType !== "RUN_RECORD") {
|
|
1304
1519
|
throw new errors_js_1.VerificationError(`invalid log entry_type: ${String(entryType)}`);
|
|
1305
1520
|
}
|
|
1306
1521
|
const payload = {
|
|
@@ -1368,9 +1583,21 @@ function buildTreeHeadPayload(head) {
|
|
|
1368
1583
|
// ---------------------------------------------------------------------------
|
|
1369
1584
|
// Merkle tree (RFC 6962) — ports of core/merkle.go
|
|
1370
1585
|
// ---------------------------------------------------------------------------
|
|
1586
|
+
/** Exported for run-record.ts, whose roots are trees over these leaves. */
|
|
1371
1587
|
async function hashLeaf(crypto, data) {
|
|
1372
1588
|
return crypto.sha256(concatBytes(new Uint8Array([0x00]), data));
|
|
1373
1589
|
}
|
|
1590
|
+
/** The RFC 6962 root over already-hashed leaves, in the order given. Port of
|
|
1591
|
+
* Go's treeHash, which like it takes at least one leaf: the empty tree has no
|
|
1592
|
+
* root here, and a caller with nothing to commit to says so itself. */
|
|
1593
|
+
async function merkleRoot(crypto, leaves) {
|
|
1594
|
+
if (leaves.length === 0)
|
|
1595
|
+
throw new RangeError("merkleRoot: no leaves");
|
|
1596
|
+
if (leaves.length === 1)
|
|
1597
|
+
return leaves[0];
|
|
1598
|
+
const k = splitPoint(leaves.length);
|
|
1599
|
+
return hashNode(crypto, await merkleRoot(crypto, leaves.slice(0, k)), await merkleRoot(crypto, leaves.slice(k)));
|
|
1600
|
+
}
|
|
1374
1601
|
async function hashNode(crypto, left, right) {
|
|
1375
1602
|
const prefix = new Uint8Array([0x01]);
|
|
1376
1603
|
return crypto.sha256(concatBytes(prefix, concatBytes(left, right)));
|
|
@@ -1429,7 +1656,14 @@ async function chainInclusion(crypto, leaf, index, n, proof) {
|
|
|
1429
1656
|
return [await hashNode(crypto, proof[used], inner), used + 1];
|
|
1430
1657
|
}
|
|
1431
1658
|
function isPow2(n) {
|
|
1432
|
-
|
|
1659
|
+
// Arithmetic, not `n & (n - 1)` (R12-2): JS bitwise operators coerce to 32-bit
|
|
1660
|
+
// signed integers, so for a tree size >= 2^31 the bit test gives the wrong
|
|
1661
|
+
// answer. This holds for every safe integer.
|
|
1662
|
+
if (n < 1)
|
|
1663
|
+
return false;
|
|
1664
|
+
while (n % 2 === 0)
|
|
1665
|
+
n /= 2;
|
|
1666
|
+
return n === 1;
|
|
1433
1667
|
}
|
|
1434
1668
|
/** Verify a Merkle consistency proof (RFC 6962 Section 2.1.4).
|
|
1435
1669
|
*
|
|
@@ -1457,9 +1691,14 @@ async function verifyConsistency(crypto, oldSize, newSize, oldRoot, newRoot, pro
|
|
|
1457
1691
|
}
|
|
1458
1692
|
let fn = oldSize - 1;
|
|
1459
1693
|
let sn = newSize - 1;
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1694
|
+
// Arithmetic throughout (R12-2). `& 1` and `>>= 1` coerce to 32-bit signed
|
|
1695
|
+
// integers, so for tree sizes >= 2^31 fn and sn are truncated and this
|
|
1696
|
+
// consistency check silently returned the wrong answer (CONFIRMED where Go
|
|
1697
|
+
// says INCONSISTENT). `% 2` and Math.floor(/2) are exact for every safe
|
|
1698
|
+
// integer, which requireUint already bounds the inputs to.
|
|
1699
|
+
while (fn % 2 === 1) {
|
|
1700
|
+
fn = Math.floor(fn / 2);
|
|
1701
|
+
sn = Math.floor(sn / 2);
|
|
1463
1702
|
}
|
|
1464
1703
|
// Drive the walk from the tree, not from the proof's length. Looping on
|
|
1465
1704
|
// `pIdx < proof.length` let the prover choose how many steps ran, so a log
|
|
@@ -1473,19 +1712,19 @@ async function verifyConsistency(crypto, oldSize, newSize, oldRoot, newRoot, pro
|
|
|
1473
1712
|
return false;
|
|
1474
1713
|
const c = proof[pIdx];
|
|
1475
1714
|
pIdx++;
|
|
1476
|
-
if (
|
|
1715
|
+
if (fn % 2 === 1 || fn === sn) {
|
|
1477
1716
|
fr = await hashNode(crypto, c, fr);
|
|
1478
1717
|
sr = await hashNode(crypto, c, sr);
|
|
1479
|
-
while (fn !== 0 &&
|
|
1480
|
-
fn
|
|
1481
|
-
sn
|
|
1718
|
+
while (fn !== 0 && fn % 2 === 0) {
|
|
1719
|
+
fn = Math.floor(fn / 2);
|
|
1720
|
+
sn = Math.floor(sn / 2);
|
|
1482
1721
|
}
|
|
1483
1722
|
}
|
|
1484
1723
|
else {
|
|
1485
1724
|
sr = await hashNode(crypto, sr, c);
|
|
1486
1725
|
}
|
|
1487
|
-
fn
|
|
1488
|
-
sn
|
|
1726
|
+
fn = Math.floor(fn / 2);
|
|
1727
|
+
sn = Math.floor(sn / 2);
|
|
1489
1728
|
}
|
|
1490
1729
|
return pIdx === proof.length && bytesEqual(fr, oldRoot) && bytesEqual(sr, newRoot);
|
|
1491
1730
|
}
|