burnledger 0.8.0 → 0.9.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 +139 -2
- package/dist/cjs/anchor.d.ts +5 -3
- package/dist/cjs/anchor.d.ts.map +1 -1
- package/dist/cjs/anchor.js +10 -4
- package/dist/cjs/anchor.js.map +1 -1
- package/dist/cjs/client.d.ts +95 -0
- package/dist/cjs/client.d.ts.map +1 -1
- package/dist/cjs/client.js +133 -2
- package/dist/cjs/client.js.map +1 -1
- package/dist/cjs/customer-keys.d.ts +146 -0
- package/dist/cjs/customer-keys.d.ts.map +1 -0
- package/dist/cjs/customer-keys.js +465 -0
- package/dist/cjs/customer-keys.js.map +1 -0
- package/dist/cjs/enclave-registration.d.ts +153 -0
- package/dist/cjs/enclave-registration.d.ts.map +1 -0
- package/dist/cjs/enclave-registration.js +275 -0
- package/dist/cjs/enclave-registration.js.map +1 -0
- package/dist/cjs/enclave-seal.d.ts +43 -0
- package/dist/cjs/enclave-seal.d.ts.map +1 -1
- package/dist/cjs/enclave-seal.js +62 -1
- package/dist/cjs/enclave-seal.js.map +1 -1
- package/dist/cjs/errors.d.ts +10 -0
- package/dist/cjs/errors.d.ts.map +1 -1
- package/dist/cjs/errors.js +11 -1
- package/dist/cjs/errors.js.map +1 -1
- package/dist/cjs/index.browser.d.ts +7 -3
- package/dist/cjs/index.browser.d.ts.map +1 -1
- package/dist/cjs/index.browser.js +6 -3
- package/dist/cjs/index.browser.js.map +1 -1
- package/dist/cjs/index.d.ts +23 -9
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +40 -6
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/key-group.d.ts +80 -0
- package/dist/cjs/key-group.d.ts.map +1 -0
- package/dist/cjs/key-group.js +136 -0
- package/dist/cjs/key-group.js.map +1 -0
- package/dist/cjs/models.d.ts +60 -0
- package/dist/cjs/models.d.ts.map +1 -1
- package/dist/cjs/models.js +63 -0
- package/dist/cjs/models.js.map +1 -1
- package/dist/cjs/status-document.d.ts +25 -0
- package/dist/cjs/status-document.d.ts.map +1 -0
- package/dist/cjs/status-document.js +62 -0
- package/dist/cjs/status-document.js.map +1 -0
- package/dist/cjs/verify.d.ts +112 -3
- package/dist/cjs/verify.d.ts.map +1 -1
- package/dist/cjs/verify.js +393 -146
- package/dist/cjs/verify.js.map +1 -1
- package/dist/cjs/web-verifier.d.ts +23 -3
- package/dist/cjs/web-verifier.d.ts.map +1 -1
- package/dist/cjs/web-verifier.js +31 -5
- package/dist/cjs/web-verifier.js.map +1 -1
- package/dist/esm/anchor.d.ts +5 -3
- package/dist/esm/anchor.d.ts.map +1 -1
- package/dist/esm/anchor.js +10 -4
- package/dist/esm/anchor.js.map +1 -1
- package/dist/esm/cli.d.ts +13 -19
- package/dist/esm/cli.d.ts.map +1 -1
- package/dist/esm/cli.js +109 -81
- package/dist/esm/cli.js.map +1 -1
- package/dist/esm/client.d.ts +95 -0
- package/dist/esm/client.d.ts.map +1 -1
- package/dist/esm/client.js +133 -2
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/customer-keys.d.ts +146 -0
- package/dist/esm/customer-keys.d.ts.map +1 -0
- package/dist/esm/customer-keys.js +450 -0
- package/dist/esm/customer-keys.js.map +1 -0
- package/dist/esm/enclave-registration.d.ts +153 -0
- package/dist/esm/enclave-registration.d.ts.map +1 -0
- package/dist/esm/enclave-registration.js +265 -0
- package/dist/esm/enclave-registration.js.map +1 -0
- package/dist/esm/enclave-seal.d.ts +43 -0
- package/dist/esm/enclave-seal.d.ts.map +1 -1
- package/dist/esm/enclave-seal.js +61 -1
- package/dist/esm/enclave-seal.js.map +1 -1
- package/dist/esm/errors.d.ts +10 -0
- package/dist/esm/errors.d.ts.map +1 -1
- package/dist/esm/errors.js +10 -0
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.browser.d.ts +7 -3
- package/dist/esm/index.browser.d.ts.map +1 -1
- package/dist/esm/index.browser.js +6 -3
- package/dist/esm/index.browser.js.map +1 -1
- package/dist/esm/index.d.ts +23 -9
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +18 -7
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/key-group.d.ts +80 -0
- package/dist/esm/key-group.d.ts.map +1 -0
- package/dist/esm/key-group.js +130 -0
- package/dist/esm/key-group.js.map +1 -0
- package/dist/esm/models.d.ts +60 -0
- package/dist/esm/models.d.ts.map +1 -1
- package/dist/esm/models.js +59 -0
- package/dist/esm/models.js.map +1 -1
- package/dist/esm/status-document.d.ts +25 -0
- package/dist/esm/status-document.d.ts.map +1 -0
- package/dist/esm/status-document.js +59 -0
- package/dist/esm/status-document.js.map +1 -0
- package/dist/esm/verify.d.ts +112 -3
- package/dist/esm/verify.d.ts.map +1 -1
- package/dist/esm/verify.js +390 -147
- package/dist/esm/verify.js.map +1 -1
- package/dist/esm/web-verifier.d.ts +23 -3
- package/dist/esm/web-verifier.d.ts.map +1 -1
- package/dist/esm/web-verifier.js +27 -5
- package/dist/esm/web-verifier.js.map +1 -1
- package/package.json +1 -1
- package/src/anchor.ts +10 -4
- package/src/cli.ts +122 -79
- package/src/client.ts +198 -6
- package/src/customer-keys.ts +550 -0
- package/src/enclave-registration.ts +390 -0
- package/src/enclave-seal.ts +108 -1
- package/src/errors.ts +11 -0
- package/src/index.browser.ts +9 -3
- package/src/index.ts +48 -6
- package/src/key-group.ts +181 -0
- package/src/models.ts +131 -0
- package/src/status-document.ts +59 -0
- package/src/verify.ts +503 -152
- package/src/web-verifier.ts +36 -3
package/dist/cjs/verify.js
CHANGED
|
@@ -17,22 +17,26 @@
|
|
|
17
17
|
* output at issuance time (transparency_status=PENDING, no transparency).
|
|
18
18
|
*/
|
|
19
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
-
exports.VALID_REVOCATION_UNKNOWN = exports.VALID_KEY_WINDOW_UNKNOWN = exports.VALID_KEY_COMPROMISED_LATER = exports.KEY_COMPROMISED = exports.KEY_OUTSIDE_VALIDITY = exports.KNOWN_FORMAT_VERSIONS = void 0;
|
|
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
21
|
exports.hexToBytes = hexToBytes;
|
|
22
22
|
exports.bytesToHex = bytesToHex;
|
|
23
23
|
exports.publicKeyFromHex = publicKeyFromHex;
|
|
24
24
|
exports.keyIsUsable = keyIsUsable;
|
|
25
25
|
exports.evaluateKey = evaluateKey;
|
|
26
26
|
exports.certificateAnchor = certificateAnchor;
|
|
27
|
+
exports.issuingImage = issuingImage;
|
|
28
|
+
exports.registrationQualifier = registrationQualifier;
|
|
27
29
|
exports.verifyCertificate = verifyCertificate;
|
|
28
30
|
exports.verifyTransparency = verifyTransparency;
|
|
29
31
|
exports.issuanceBytes = issuanceBytes;
|
|
32
|
+
exports.canonicalJson = canonicalJson;
|
|
30
33
|
exports.formatTimestamp = formatTimestamp;
|
|
31
34
|
exports.buildTreeHeadPayload = buildTreeHeadPayload;
|
|
32
35
|
exports.verifyConsistency = verifyConsistency;
|
|
33
36
|
exports.buildCertificateStatusPayload = buildCertificateStatusPayload;
|
|
34
37
|
exports.buildKeyListPayload = buildKeyListPayload;
|
|
35
38
|
exports.verifyCertificateWithStatus = verifyCertificateWithStatus;
|
|
39
|
+
exports.verifyStatusStatement = verifyStatusStatement;
|
|
36
40
|
const errors_js_1 = require("./errors.js");
|
|
37
41
|
// ---------------------------------------------------------------------------
|
|
38
42
|
// Pure byte helpers (no Buffer, no node:crypto)
|
|
@@ -81,8 +85,9 @@ const PAYLOAD_TYPE_ATTESTATION_V8 = "burnledger.attestation.v8";
|
|
|
81
85
|
const PAYLOAD_TYPE_VERIFICATION_RECORD_V8 = "burnledger.verification_record.v8";
|
|
82
86
|
const FORMAT_VERSION_V8 = "8.0";
|
|
83
87
|
// v9 adds two per-system fields, authorization and customer_key_id: whether the
|
|
84
|
-
// enclave checked this system against a
|
|
85
|
-
//
|
|
88
|
+
// enclave checked this system against a registration certificate (ADR-025 §4),
|
|
89
|
+
// and under which customer key group if it did. That the key group SIGNED the
|
|
90
|
+
// registration holds only from enclave protocol 12: see PRE_PROTOCOL_12_IMAGES.
|
|
86
91
|
//
|
|
87
92
|
// The attestation gains NO field at v9. Its tag still moves, because the tag
|
|
88
93
|
// follows the record's version and the record's bytes changed — a v9
|
|
@@ -226,17 +231,22 @@ const PAYLOAD_TYPE_LOG_LEAF = "burnledger.log_leaf.v3";
|
|
|
226
231
|
// in the same enclave call that signs the certificate (ADR-016 §2).
|
|
227
232
|
const PAYLOAD_TYPE_CERTIFICATE_STATUS = "burnledger.certificate_status.v3";
|
|
228
233
|
const PAYLOAD_TYPE_KEY_LIST = "burnledger.key_list.v3";
|
|
234
|
+
/** Decode hex: whole bytes, digits in either case, nothing else. Anything else
|
|
235
|
+
* throws TypeError. It is exported, so its input is a boundary: reading a stray
|
|
236
|
+
* character as 0, as it once did, turned a typo into different bytes. */
|
|
229
237
|
function hexToBytes(hex) {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
238
|
+
if (typeof hex !== "string")
|
|
239
|
+
throw new TypeError(`hex is not a string: ${typeof hex}`);
|
|
240
|
+
if (hex.length % 2 !== 0)
|
|
241
|
+
throw new TypeError(`hex has an odd number of digits: ${hex.length}`);
|
|
242
|
+
const out = new Uint8Array(hex.length / 2);
|
|
243
|
+
for (let i = 0; i < out.length; i++) {
|
|
244
|
+
out[i] = (unhex(hex, i * 2) << 4) | unhex(hex, i * 2 + 1);
|
|
236
245
|
}
|
|
237
246
|
return out;
|
|
238
247
|
}
|
|
239
|
-
function unhex(
|
|
248
|
+
function unhex(hex, at) {
|
|
249
|
+
const c = hex.charCodeAt(at);
|
|
240
250
|
// 0-9
|
|
241
251
|
if (c >= 48 && c <= 57)
|
|
242
252
|
return c - 48;
|
|
@@ -246,7 +256,7 @@ function unhex(c) {
|
|
|
246
256
|
// A-F
|
|
247
257
|
if (c >= 65 && c <= 70)
|
|
248
258
|
return c - 55;
|
|
249
|
-
|
|
259
|
+
throw new TypeError(`hex has a non-hex character ${JSON.stringify(hex[at])} at ${at}`);
|
|
250
260
|
}
|
|
251
261
|
function bytesToHex(bytes) {
|
|
252
262
|
let out = "";
|
|
@@ -286,6 +296,13 @@ function textToBytes(s) {
|
|
|
286
296
|
return new TextEncoder().encode(s);
|
|
287
297
|
}
|
|
288
298
|
async function publicKeyFromHex(crypto, hexKey, opts) {
|
|
299
|
+
// Refused here, as the VerificationError callers are told to catch, rather
|
|
300
|
+
// than left to hexToBytes's TypeError. The alphabet is the one
|
|
301
|
+
// core.KeyEntry.PublicKeyInfo takes through hex.DecodeString: either case,
|
|
302
|
+
// whole bytes, nothing else.
|
|
303
|
+
if (typeof hexKey !== "string" || !HEX_TEXT_RE.test(hexKey)) {
|
|
304
|
+
throw new errors_js_1.VerificationError(`public key is not hex: ${JSON.stringify(hexKey)}`);
|
|
305
|
+
}
|
|
289
306
|
const raw = hexToBytes(hexKey);
|
|
290
307
|
if (raw.length !== 32) {
|
|
291
308
|
throw new errors_js_1.VerificationError(`public key must be 32 bytes, got ${raw.length}`);
|
|
@@ -393,7 +410,7 @@ function certificateAnchor(certificate) {
|
|
|
393
410
|
if (transparency == null)
|
|
394
411
|
return null;
|
|
395
412
|
const sth = transparency.signed_tree_head;
|
|
396
|
-
if (sth
|
|
413
|
+
if (sth == null || typeof sth.timestamp !== "string")
|
|
397
414
|
return null;
|
|
398
415
|
try {
|
|
399
416
|
return parseRfc3339(sth.timestamp);
|
|
@@ -423,15 +440,22 @@ async function verifiedCertificateAnchor(crypto, certificate, pki) {
|
|
|
423
440
|
return null;
|
|
424
441
|
const transparency = certificate.transparency;
|
|
425
442
|
const sth = transparency.signed_tree_head;
|
|
443
|
+
// Only a refusal of the head's own fields means "no anchor". A bare catch
|
|
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;
|
|
426
448
|
try {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
449
|
+
headPayload = buildTreeHeadPayload(sth);
|
|
450
|
+
headSig = decodeFixed(sth.signature, "signed_tree_head.signature", 64);
|
|
451
|
+
}
|
|
452
|
+
catch (e) {
|
|
453
|
+
if (e instanceof errors_js_1.VerificationError)
|
|
430
454
|
return null;
|
|
455
|
+
throw e;
|
|
431
456
|
}
|
|
432
|
-
|
|
457
|
+
if (!(await crypto.ed25519Verify(pki.keyBytes, headPayload, headSig)))
|
|
433
458
|
return null;
|
|
434
|
-
}
|
|
435
459
|
return anchor;
|
|
436
460
|
}
|
|
437
461
|
/** Throws for a key that cannot be used at all; returns the verdict otherwise.
|
|
@@ -467,14 +491,155 @@ role = "issuer key") {
|
|
|
467
491
|
}
|
|
468
492
|
return verdict;
|
|
469
493
|
}
|
|
494
|
+
function policySet(values, field, lower) {
|
|
495
|
+
if (values === undefined)
|
|
496
|
+
return undefined;
|
|
497
|
+
if (!Array.isArray(values) || !values.every((v) => typeof v === "string" && v !== "")) {
|
|
498
|
+
throw new TypeError(`requireAuthorization.${field} must be an array of non-empty strings`);
|
|
499
|
+
}
|
|
500
|
+
if (values.length === 0) {
|
|
501
|
+
// An empty requirement requires nothing, which is never what a caller who
|
|
502
|
+
// built a policy meant.
|
|
503
|
+
throw new Error(`an empty requireAuthorization.${field} requires nothing; omit it instead`);
|
|
504
|
+
}
|
|
505
|
+
return new Set(values.map((v) => (lower ? v.toLowerCase() : v)));
|
|
506
|
+
}
|
|
507
|
+
/** Check a policy before anything is verified, so a caller's mistake surfaces as
|
|
508
|
+
* one whatever the record turns out to be. */
|
|
509
|
+
function parsePolicy(policy) {
|
|
510
|
+
const keys = policy.customerKeyIds;
|
|
511
|
+
if (keys === undefined || (Array.isArray(keys) && keys.length === 0)) {
|
|
512
|
+
throw new TypeError("requireAuthorization.customerKeyIds is required, with at least one key id: `certified` under " +
|
|
513
|
+
"a key id you did not name proves nothing to you, because the relay can enroll a key group of its own");
|
|
514
|
+
}
|
|
515
|
+
return {
|
|
516
|
+
systems: policySet(policy.systems, "systems", true),
|
|
517
|
+
customerKeyIds: policySet(policy.customerKeyIds, "customerKeyIds", false),
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* The enclave images that issued format 9.0 while running wire protocol 11 or
|
|
522
|
+
* earlier, by PCR0, as the published measurement history records them
|
|
523
|
+
* (website/docs/enclave-measurements/, docs/CONNECTOR-STATUS.md).
|
|
524
|
+
*
|
|
525
|
+
* Their `certified` is not evidence of a customer signature. Before protocol 12
|
|
526
|
+
* the enclave enrolled a key group with no proof that anyone held its keys, and
|
|
527
|
+
* registered a system for whoever relayed the authorization — so the host could
|
|
528
|
+
* make a genuine 9.0 record say `certified`, under a key id it chose, with no
|
|
529
|
+
* customer signature anywhere (reproduced by an independent re-review,
|
|
530
|
+
* 2026-09-13).
|
|
531
|
+
*
|
|
532
|
+
* THE SET IS CLOSED BY POLICY, NOT BY ENFORCEMENT: the release runbook forbids
|
|
533
|
+
* admitting or rolling back to an image of protocol 11 or earlier once any
|
|
534
|
+
* customer key is enrolled, and admitting one takes a KMS re-pin, which ADR-027
|
|
535
|
+
* leaves to the owner. The protocol-12 cutover's KMS tighten drops every
|
|
536
|
+
* measurement here from the signing key's policy, after which none of them can
|
|
537
|
+
* sign. v42 ran for seven minutes and is recorded as issuing nothing; it is here
|
|
538
|
+
* because it held the signing key while it ran.
|
|
539
|
+
*
|
|
540
|
+
* The Python SDK carries the same list, in _verify.py.
|
|
541
|
+
*/
|
|
542
|
+
exports.PRE_PROTOCOL_12_IMAGES = new Map([
|
|
543
|
+
["0e79f985c287fbad3ed9aa1d5443189c45428929086dc4d4312ba71c4039742a8bca02f4ea15ab318d82e1d86ff883d3", "EIF v39"],
|
|
544
|
+
["665864551aeb57705c3b3721018112cbd0e2be98de2e0ff9142a6c0abf366c72c6ba6cdcd31cd6ed8471862e1b9187ce", "EIF v40"],
|
|
545
|
+
["7a61327a0ee11508764a8ad03d68e81e3b374e4e605a284e9e815ec9cf57de8497e59d57048977441f9c5c98bee1d14b", "EIF v41"],
|
|
546
|
+
["f1e02ace0011bfc0d60d6dd76da0078b7706f3165eff6904a36e97e5d2029246099ef20bf15f87d0263dd843f878731c", "EIF v42"],
|
|
547
|
+
["10ed72207ffd6a14f5913130eca69e9f4f9de0447b360e9cfd8aa8e349681c6fe98c515499a38a347d67932a6201a9b4", "EIF v43"],
|
|
548
|
+
["0bfbd2251cb9e138491c6ce424ad132b28be152e1486aa7100acabce0201f3f5d500eb47dc00184de4eebc93fe4dc695", "EIF v44"],
|
|
549
|
+
["4d68b3ef6b77418f2827964400241ee9db7e101ce53666b75cc6cf7aa17d7c9f3a1b9055bafec4f799beee6e00227546", "EIF v45 and v46"],
|
|
550
|
+
]);
|
|
551
|
+
function issuingImage(certificate) {
|
|
552
|
+
const issuer = certificate.issuer;
|
|
553
|
+
const pcr0 = String(issuer?.enclave_pcr0 ?? "").toLowerCase();
|
|
554
|
+
if (pcr0 === "")
|
|
555
|
+
return { kind: "unnamed" };
|
|
556
|
+
const name = exports.PRE_PROTOCOL_12_IMAGES.get(pcr0);
|
|
557
|
+
return name === undefined
|
|
558
|
+
? { kind: "protocol-12-or-later", pcr0 }
|
|
559
|
+
: { kind: "before-protocol-12", name, pcr0 };
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* What `certified` is evidence of, given the image that signed the record: the
|
|
563
|
+
* words every verifier prints after "certified under <key id> —". The Go CLI
|
|
564
|
+
* (cmd/cli/registration_image.go), both SDK CLIs and the /verify page state it
|
|
565
|
+
* identically, and testdata/registration_line_cases.json holds them to it.
|
|
566
|
+
*/
|
|
567
|
+
function registrationQualifier(image) {
|
|
568
|
+
switch (image.kind) {
|
|
569
|
+
case "unnamed":
|
|
570
|
+
return ("not evidence here: this record names no enclave image, so it may come from one " +
|
|
571
|
+
"that could mark a system certified without the customer's signature.");
|
|
572
|
+
case "before-protocol-12":
|
|
573
|
+
return (`not evidence here: this record comes from ${image.name}, an enclave image of ` +
|
|
574
|
+
"protocol 11 or earlier, which could mark a system certified without the customer's signature.");
|
|
575
|
+
case "protocol-12-or-later":
|
|
576
|
+
return ("the key group with that id signed this system's registration, if that key id is yours: " +
|
|
577
|
+
"this record comes from an enclave image of protocol 12 or later. If that key id is not yours, " +
|
|
578
|
+
"someone else authorized this verification.");
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Throw unless a record that has already verified meets `policy`.
|
|
583
|
+
*
|
|
584
|
+
* From 9.0 every field read here is under the certificate signature, the
|
|
585
|
+
* issuer's enclave_pcr0 included. Below 9.0
|
|
586
|
+
* nothing signs `authorization` — those records carry the field outside the
|
|
587
|
+
* signed bytes — so one that says `certified` says only what its last holder
|
|
588
|
+
* typed. It fails on the format, before the field is read.
|
|
589
|
+
*/
|
|
590
|
+
function enforceAuthorization(certificate, formatVersion, policy) {
|
|
591
|
+
const systemsWanted = policy.systems;
|
|
592
|
+
if (!signatureCoversAuthorization(formatVersion)) {
|
|
593
|
+
throw new errors_js_1.VerificationError(`authorization required, but this record is format ${formatVersion}, which predates ` +
|
|
594
|
+
`customer-signed registration (${FORMAT_VERSION_V9}); it cannot show that any system was certified`, errors_js_1.AUTHORIZATION_POLICY_FAILED);
|
|
595
|
+
}
|
|
596
|
+
const systems = certificate.systems ?? [];
|
|
597
|
+
let required = systems;
|
|
598
|
+
if (systemsWanted !== undefined) {
|
|
599
|
+
const byId = new Map(systems.map((s) => [String(s.system_id).toLowerCase(), s]));
|
|
600
|
+
const missing = [...systemsWanted].filter((id) => !byId.has(id)).sort();
|
|
601
|
+
if (missing.length > 0) {
|
|
602
|
+
throw new errors_js_1.VerificationError(`authorization required for system ${missing[0]}, which this record does not cover`, errors_js_1.AUTHORIZATION_POLICY_FAILED);
|
|
603
|
+
}
|
|
604
|
+
required = [...systemsWanted].sort().map((id) => byId.get(id));
|
|
605
|
+
}
|
|
606
|
+
const labelOf = (s) => `system ${JSON.stringify(s.system_name)} (${String(s.system_id)})`;
|
|
607
|
+
for (const s of required) {
|
|
608
|
+
if (s.authorization !== AUTHORIZATION_CERTIFIED) {
|
|
609
|
+
throw new errors_js_1.VerificationError(`authorization required, but ${labelOf(s)} is ${JSON.stringify(s.authorization ?? null)}: ` +
|
|
610
|
+
"the enclave did not check it against a registration", errors_js_1.AUTHORIZATION_POLICY_FAILED);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
// Asked once, for the record, after every required system has said certified:
|
|
614
|
+
// which image said it decides whether `certified` means a customer signed.
|
|
615
|
+
const image = issuingImage(certificate);
|
|
616
|
+
if (image.kind === "unnamed") {
|
|
617
|
+
throw new errors_js_1.VerificationError("authorization required, but this record names no enclave image (issuer.enclave_pcr0 is " +
|
|
618
|
+
"absent), so nothing shows it came from one that registers a system only with the " +
|
|
619
|
+
"customer's signature", errors_js_1.AUTHORIZATION_POLICY_FAILED);
|
|
620
|
+
}
|
|
621
|
+
if (image.kind === "before-protocol-12") {
|
|
622
|
+
throw new errors_js_1.VerificationError(`authorization required, but this record was signed by ${image.name} (PCR0 ${image.pcr0.slice(0, 16)}…), ` +
|
|
623
|
+
"an enclave image of wire protocol 11 or earlier, which could register a system without " +
|
|
624
|
+
"the customer's signature; its `certified` does not show that any key group signed anything", errors_js_1.AUTHORIZATION_POLICY_FAILED);
|
|
625
|
+
}
|
|
626
|
+
for (const s of required) {
|
|
627
|
+
if (!policy.customerKeyIds.has(s.customer_key_id)) {
|
|
628
|
+
throw new errors_js_1.VerificationError(`${labelOf(s)} is certified under ${String(s.customer_key_id)}, which is not a key you ` +
|
|
629
|
+
"accept; if it is not yours, someone else authorized this verification", errors_js_1.AUTHORIZATION_POLICY_FAILED);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
470
633
|
/** Verify all signatures on a deletion certificate offline. */
|
|
471
|
-
async function verifyCertificate(crypto, certificate, publicKeys) {
|
|
634
|
+
async function verifyCertificate(crypto, certificate, publicKeys, options) {
|
|
472
635
|
// Step 0: can this SDK read the format at all? Every check below rebuilds
|
|
473
636
|
// signed bytes from the version, so an unreadable version makes all of them
|
|
474
637
|
// meaningless - and "unknown issuer key" would be the wrong thing to tell
|
|
475
638
|
// someone holding a record that is merely newer than this library.
|
|
639
|
+
requireObject(certificate, "certificate");
|
|
640
|
+
const policy = options?.requireAuthorization === undefined ? undefined : parsePolicy(options.requireAuthorization);
|
|
476
641
|
const formatVersion = checkFormatVersion(certificate.certificate_format_version);
|
|
477
|
-
const issuer = certificate
|
|
642
|
+
const issuer = objectAt(certificate, "issuer", "issuer");
|
|
478
643
|
// Step 0b: can this SDK check the algorithm the record names? Asked before
|
|
479
644
|
// any signature, because verifying an Ed25519 signature over a record that
|
|
480
645
|
// says it was signed with something else answers a question nobody asked.
|
|
@@ -485,7 +650,7 @@ async function verifyCertificate(crypto, certificate, publicKeys) {
|
|
|
485
650
|
`this version of the SDK verifies ${ALGORITHM_ED25519}. ` +
|
|
486
651
|
"The record may be genuine and simply signed with a scheme this library does not implement.", errors_js_1.UNSUPPORTED_ALGORITHM);
|
|
487
652
|
}
|
|
488
|
-
const keyId = issuer.key_id;
|
|
653
|
+
const keyId = stringAt(issuer, "key_id", "issuer.key_id");
|
|
489
654
|
const pki = publicKeys.get(keyId);
|
|
490
655
|
if (pki === undefined)
|
|
491
656
|
throw new errors_js_1.VerificationError(`unknown issuer key: ${keyId}`);
|
|
@@ -495,7 +660,7 @@ async function verifyCertificate(crypto, certificate, publicKeys) {
|
|
|
495
660
|
// 1. Certificate signature FIRST — nothing below may trust a field until the
|
|
496
661
|
// bytes carrying it are covered by a verified signature.
|
|
497
662
|
const certPayload = buildCertificatePayload(certificate);
|
|
498
|
-
const certSig =
|
|
663
|
+
const certSig = decodeFixed(certificate.certificate_signature, "certificate_signature", 64);
|
|
499
664
|
if (!(await crypto.ed25519Verify(pki.keyBytes, certPayload, certSig))) {
|
|
500
665
|
throw new errors_js_1.VerificationError("certificate signature is invalid");
|
|
501
666
|
}
|
|
@@ -509,7 +674,7 @@ async function verifyCertificate(crypto, certificate, publicKeys) {
|
|
|
509
674
|
attested_at: att.attested_at,
|
|
510
675
|
systems: attestationSystems(certificate),
|
|
511
676
|
}, certificate.certificate_format_version);
|
|
512
|
-
const attSig =
|
|
677
|
+
const attSig = decodeFixed(att.attestation_signature, "attestation.attestation_signature", 64);
|
|
513
678
|
if (!(await crypto.ed25519Verify(pki.keyBytes, attPayload, attSig))) {
|
|
514
679
|
throw new errors_js_1.VerificationError("attestation signature is invalid");
|
|
515
680
|
}
|
|
@@ -558,6 +723,9 @@ async function verifyCertificate(crypto, certificate, publicKeys) {
|
|
|
558
723
|
if (!anyAttested) {
|
|
559
724
|
throw new errors_js_1.VerificationError("incomplete verification: no system held any records at attestation time");
|
|
560
725
|
}
|
|
726
|
+
if (policy !== undefined) {
|
|
727
|
+
enforceAuthorization(certificate, formatVersion, policy);
|
|
728
|
+
}
|
|
561
729
|
// A soft key verdict is the result, not a footnote: VALID_KEY_WINDOW_UNKNOWN
|
|
562
730
|
// and VALID_KEY_COMPROMISED_LATER mean this document still needs a human, and
|
|
563
731
|
// reporting VALID here would be the lying by omission ADR-017 exists to stop.
|
|
@@ -566,13 +734,14 @@ async function verifyCertificate(crypto, certificate, publicKeys) {
|
|
|
566
734
|
const NIL_UUID = "00000000-0000-0000-0000-000000000000";
|
|
567
735
|
/** Verify the transparency proof embedded in a certificate. */
|
|
568
736
|
async function verifyTransparency(crypto, certificate, publicKeys) {
|
|
569
|
-
|
|
570
|
-
if (transparency == null) {
|
|
737
|
+
requireObject(certificate, "certificate");
|
|
738
|
+
if (certificate.transparency == null) {
|
|
571
739
|
return "NOT_AVAILABLE";
|
|
572
740
|
}
|
|
573
|
-
const
|
|
574
|
-
const
|
|
575
|
-
const
|
|
741
|
+
const transparency = objectAt(certificate, "transparency", "transparency");
|
|
742
|
+
const sth = objectAt(transparency, "signed_tree_head", "transparency.signed_tree_head");
|
|
743
|
+
const issuer = objectAt(certificate, "issuer", "issuer");
|
|
744
|
+
const keyId = stringAt(issuer, "key_id", "issuer.key_id");
|
|
576
745
|
const pki = publicKeys.get(keyId);
|
|
577
746
|
if (pki === undefined)
|
|
578
747
|
throw new errors_js_1.VerificationError(`unknown issuer key: ${keyId}`);
|
|
@@ -580,7 +749,7 @@ async function verifyTransparency(crypto, certificate, publicKeys) {
|
|
|
580
749
|
requireUsableKey(pki, certificateAnchor(certificate), keyId);
|
|
581
750
|
// 1. Tree head signature
|
|
582
751
|
const headPayload = buildTreeHeadPayload(sth);
|
|
583
|
-
const headSig =
|
|
752
|
+
const headSig = decodeFixed(sth.signature, "signed_tree_head.signature", 64);
|
|
584
753
|
if (!(await crypto.ed25519Verify(pki.keyBytes, headPayload, headSig))) {
|
|
585
754
|
throw new errors_js_1.VerificationError("tree head signature is invalid");
|
|
586
755
|
}
|
|
@@ -593,10 +762,10 @@ async function verifyTransparency(crypto, certificate, publicKeys) {
|
|
|
593
762
|
// revocation of the same certificate produced identical leaves and the tree
|
|
594
763
|
// committed to neither the entry type nor when it happened.
|
|
595
764
|
const issuanceData = issuanceBytes(certificate);
|
|
596
|
-
const leafPayload = buildLogLeafPayload(transparency.entry_type, certificate
|
|
765
|
+
const leafPayload = buildLogLeafPayload(transparency.entry_type, stringAt(certificate, "certificate_id", "certificate_id"), await crypto.sha256(issuanceData), transparency.appended_at);
|
|
597
766
|
const leaf = await hashLeaf(crypto, leafPayload);
|
|
598
|
-
const proofHashes = (transparency.inclusion_proof
|
|
599
|
-
const root =
|
|
767
|
+
const proofHashes = arrayAt(transparency, "inclusion_proof", "transparency.inclusion_proof").map((h, i) => decodeFixed(h, `transparency.inclusion_proof[${i}]`, 32));
|
|
768
|
+
const root = decodeFixed(sth.root_hash, "signed_tree_head.root_hash", 32);
|
|
600
769
|
// `?? 0` is not a convenience: encoding/json leaves 0 in Go's uint64 fields
|
|
601
770
|
// for an explicit null and for an absent key rather than failing, so refusing
|
|
602
771
|
// either would reject documents the reference accepts — the same class of
|
|
@@ -652,6 +821,9 @@ function issuanceBytes(certificate) {
|
|
|
652
821
|
// ---------------------------------------------------------------------------
|
|
653
822
|
// RFC 8785 Canonical JSON
|
|
654
823
|
// ---------------------------------------------------------------------------
|
|
824
|
+
/** Exported for tests/verify.test.ts, which keeps a second implementation of
|
|
825
|
+
* this and must be able to prove the two agree. Not re-exported from index.ts,
|
|
826
|
+
* so it stays off the published API. */
|
|
655
827
|
function canonicalJson(obj) {
|
|
656
828
|
return textToBytes(canonicalStringify(obj));
|
|
657
829
|
}
|
|
@@ -675,47 +847,89 @@ function canonicalStringify(val) {
|
|
|
675
847
|
}
|
|
676
848
|
return JSON.stringify(val);
|
|
677
849
|
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
* - base64 string (from Go's json.Marshal of []byte slices)
|
|
684
|
-
* - number[] (from Go's json.Marshal of [N]byte fixed arrays)
|
|
685
|
-
*/
|
|
686
|
-
function toHex(value) {
|
|
687
|
-
if (Array.isArray(value)) {
|
|
688
|
-
return bytesToHex(new Uint8Array(value));
|
|
850
|
+
function requireObject(value, path) {
|
|
851
|
+
if (value === undefined || value === null)
|
|
852
|
+
throw new errors_js_1.VerificationError(`${path} is missing`);
|
|
853
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
854
|
+
throw new errors_js_1.VerificationError(`${path} is not an object`);
|
|
689
855
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
856
|
+
return value;
|
|
857
|
+
}
|
|
858
|
+
function objectAt(parent, key, path) {
|
|
859
|
+
return requireObject(parent[key], path);
|
|
860
|
+
}
|
|
861
|
+
function stringAt(parent, key, path) {
|
|
862
|
+
const value = parent[key];
|
|
863
|
+
if (value === undefined || value === null)
|
|
864
|
+
throw new errors_js_1.VerificationError(`${path} is missing`);
|
|
865
|
+
if (typeof value !== "string")
|
|
866
|
+
throw new errors_js_1.VerificationError(`${path} is not a string`);
|
|
867
|
+
return value;
|
|
868
|
+
}
|
|
869
|
+
function optionalStringAt(parent, key, path) {
|
|
870
|
+
const value = parent[key];
|
|
871
|
+
if (value === undefined || value === null)
|
|
872
|
+
return undefined;
|
|
873
|
+
if (typeof value !== "string")
|
|
874
|
+
throw new errors_js_1.VerificationError(`${path} is not a string`);
|
|
875
|
+
return value;
|
|
695
876
|
}
|
|
696
|
-
/**
|
|
697
|
-
|
|
877
|
+
/**
|
|
878
|
+
* An array field. Absent and null read as empty: Go leaves a nil slice.
|
|
879
|
+
*
|
|
880
|
+
* Returned dense. JSON cannot express a hole, but a caller building the object
|
|
881
|
+
* in JavaScript can, and `.map` skips holes — so one reached the Merkle code as
|
|
882
|
+
* `undefined` and threw a TypeError. Array.from reads a hole as undefined,
|
|
883
|
+
* which the element's own reader then refuses as missing.
|
|
884
|
+
*/
|
|
885
|
+
function arrayAt(parent, key, path) {
|
|
886
|
+
const value = parent[key];
|
|
887
|
+
if (value === undefined || value === null)
|
|
888
|
+
return [];
|
|
889
|
+
if (!Array.isArray(value))
|
|
890
|
+
throw new errors_js_1.VerificationError(`${path} is not an array`);
|
|
891
|
+
return Array.from(value);
|
|
892
|
+
}
|
|
893
|
+
const HEX_TEXT_RE = /^(?:[0-9a-fA-F]{2})*$/;
|
|
894
|
+
const BASE64_TEXT_RE = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
895
|
+
/**
|
|
896
|
+
* Decode a fixed-size byte field — a hash, a key, a signature.
|
|
897
|
+
*
|
|
898
|
+
* Go marshals `[N]byte` as an array of numbers; hand-written documents carry
|
|
899
|
+
* hex or standard base64. A well-formed value of the right size is never
|
|
900
|
+
* ambiguous between the two text forms, because base64 of 32 or 64 bytes ends
|
|
901
|
+
* in padding hex cannot contain.
|
|
902
|
+
*
|
|
903
|
+
* Everything else is refused here, by name: a number, an array element that is
|
|
904
|
+
* not a byte, text that is neither encoding, the wrong length. Decoding it
|
|
905
|
+
* leniently instead wrapped 300 to 44 inside a Uint8Array, produced bytes no
|
|
906
|
+
* signature covers and blamed the signature — or threw from inside atob.
|
|
907
|
+
*/
|
|
908
|
+
function decodeFixed(value, path, length) {
|
|
909
|
+
if (value === undefined || value === null)
|
|
910
|
+
throw new errors_js_1.VerificationError(`${path} is missing`);
|
|
911
|
+
let raw;
|
|
698
912
|
if (Array.isArray(value)) {
|
|
699
|
-
|
|
913
|
+
if (!value.every((b) => Number.isInteger(b) && b >= 0 && b <= 255)) {
|
|
914
|
+
throw new errors_js_1.VerificationError(`${path} is not an array of bytes`);
|
|
915
|
+
}
|
|
916
|
+
raw = new Uint8Array(value);
|
|
700
917
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
if (
|
|
705
|
-
|
|
918
|
+
else if (typeof value === "string") {
|
|
919
|
+
if (HEX_TEXT_RE.test(value))
|
|
920
|
+
raw = hexToBytes(value);
|
|
921
|
+
else if (BASE64_TEXT_RE.test(value))
|
|
922
|
+
raw = base64ToBytes(value);
|
|
923
|
+
else
|
|
924
|
+
throw new errors_js_1.VerificationError(`${path} is neither hex nor base64`);
|
|
706
925
|
}
|
|
707
|
-
|
|
708
|
-
}
|
|
709
|
-
/** Decode a hash/bytes field to raw bytes. Same format handling as toHex. */
|
|
710
|
-
function decodeBytes(value) {
|
|
711
|
-
if (Array.isArray(value)) {
|
|
712
|
-
return new Uint8Array(value);
|
|
926
|
+
else {
|
|
927
|
+
throw new errors_js_1.VerificationError(`${path} is not a byte string`);
|
|
713
928
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
return hexToBytes(str);
|
|
929
|
+
if (raw.length !== length) {
|
|
930
|
+
throw new errors_js_1.VerificationError(`${path} is ${raw.length} bytes, expected ${length}`);
|
|
717
931
|
}
|
|
718
|
-
return
|
|
932
|
+
return raw;
|
|
719
933
|
}
|
|
720
934
|
/** Go's zero time.Time — what encoding/json leaves in a non-pointer time.Time
|
|
721
935
|
* field for an explicit JSON null or an absent key. */
|
|
@@ -744,14 +958,17 @@ const RFC3339_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?
|
|
|
744
958
|
* Exported from this module so the timestamp_vectors.json suite can diff it
|
|
745
959
|
* against Go directly. It is deliberately not re-exported from index.ts, so it
|
|
746
960
|
* is not part of the published package surface.
|
|
961
|
+
*
|
|
962
|
+
* `field` names the value in a refusal. It defaults to the word these messages
|
|
963
|
+
* always used, for callers normalizing a time that is not a field of a document.
|
|
747
964
|
*/
|
|
748
|
-
function formatTimestamp(ts) {
|
|
965
|
+
function formatTimestamp(ts, field = "timestamp") {
|
|
749
966
|
if (ts === null || ts === undefined)
|
|
750
967
|
return ZERO_INSTANT;
|
|
751
968
|
if (typeof ts !== "string") {
|
|
752
|
-
throw new errors_js_1.VerificationError(
|
|
969
|
+
throw new errors_js_1.VerificationError(`${field} is not a string: ${JSON.stringify(ts)}`);
|
|
753
970
|
}
|
|
754
|
-
return formatEpochSeconds(parseRfc3339(ts));
|
|
971
|
+
return formatEpochSeconds(parseRfc3339(ts, field));
|
|
755
972
|
}
|
|
756
973
|
/** Parse a strict RFC 3339 timestamp to seconds since 1970-01-01T00:00:00Z.
|
|
757
974
|
*
|
|
@@ -762,20 +979,20 @@ function formatTimestamp(ts) {
|
|
|
762
979
|
* `+24:00` and `+00:60` parse, while `+25:00` and `+00:61` do not. Sub-second
|
|
763
980
|
* digits are dropped, matching Format's truncation.
|
|
764
981
|
*/
|
|
765
|
-
function parseRfc3339(ts) {
|
|
982
|
+
function parseRfc3339(ts, field = "timestamp") {
|
|
766
983
|
const m = RFC3339_RE.exec(ts);
|
|
767
984
|
if (m === null)
|
|
768
|
-
throw new errors_js_1.VerificationError(
|
|
985
|
+
throw new errors_js_1.VerificationError(`${field} is not RFC 3339: ${ts}`);
|
|
769
986
|
const [year, month, day, hour, minute, second] = m
|
|
770
987
|
.slice(1, 7)
|
|
771
988
|
.map((v) => Number.parseInt(v, 10));
|
|
772
989
|
if (month < 1 || month > 12)
|
|
773
|
-
throw new errors_js_1.VerificationError(
|
|
990
|
+
throw new errors_js_1.VerificationError(`${field} month out of range: ${ts}`);
|
|
774
991
|
if (day < 1 || day > daysInMonth(year, month)) {
|
|
775
|
-
throw new errors_js_1.VerificationError(
|
|
992
|
+
throw new errors_js_1.VerificationError(`${field} day out of range: ${ts}`);
|
|
776
993
|
}
|
|
777
994
|
if (hour > 23 || minute > 59 || second > 59) {
|
|
778
|
-
throw new errors_js_1.VerificationError(
|
|
995
|
+
throw new errors_js_1.VerificationError(`${field} time of day out of range: ${ts}`);
|
|
779
996
|
}
|
|
780
997
|
let offset = 0;
|
|
781
998
|
const sign = m[7];
|
|
@@ -783,9 +1000,9 @@ function parseRfc3339(ts) {
|
|
|
783
1000
|
const offHour = Number.parseInt(m[8], 10);
|
|
784
1001
|
const offMin = Number.parseInt(m[9], 10);
|
|
785
1002
|
if (offHour > 24)
|
|
786
|
-
throw new errors_js_1.VerificationError(
|
|
1003
|
+
throw new errors_js_1.VerificationError(`${field} zone offset hour out of range: ${ts}`);
|
|
787
1004
|
if (offMin > 60)
|
|
788
|
-
throw new errors_js_1.VerificationError(
|
|
1005
|
+
throw new errors_js_1.VerificationError(`${field} zone offset minute out of range: ${ts}`);
|
|
789
1006
|
offset = (offHour * 3600 + offMin * 60) * (sign === "-" ? -1 : 1);
|
|
790
1007
|
}
|
|
791
1008
|
return daysFromCivil(year, month, day) * 86400 + hour * 3600 + minute * 60 + second - offset;
|
|
@@ -869,17 +1086,17 @@ function buildAttestationPayload(subject, att, certFormatVersion) {
|
|
|
869
1086
|
// payload and call every genuine v8 record a forgery.
|
|
870
1087
|
const coversMeasured = signatureCoversMeasuredTransport(certFormatVersion);
|
|
871
1088
|
const coversRecoverable = signatureCoversRecoverableState(certFormatVersion);
|
|
872
|
-
const systems = att.systems.map((s) => {
|
|
1089
|
+
const systems = att.systems.map((s, i) => {
|
|
873
1090
|
const sys = {
|
|
874
1091
|
canonical_version: s.canonical_version ?? null,
|
|
875
1092
|
connector_type: s.connector_type,
|
|
876
1093
|
hash_scope: s.hash_scope,
|
|
877
|
-
merkle_root: s.merkle_root
|
|
878
|
-
?
|
|
879
|
-
:
|
|
1094
|
+
merkle_root: s.merkle_root == null
|
|
1095
|
+
? null
|
|
1096
|
+
: bytesToHex(decodeFixed(s.merkle_root, `systems[${i}].merkle_root`, 32)),
|
|
880
1097
|
// The system's own observation time, not the envelope's.
|
|
881
|
-
observed_at: formatTimestamp(s.observed_at),
|
|
882
|
-
query_hash:
|
|
1098
|
+
observed_at: formatTimestamp(s.observed_at, `systems[${i}].attested_at`),
|
|
1099
|
+
query_hash: bytesToHex(decodeFixed(s.query_hash, `systems[${i}].query_hash`, 32)),
|
|
883
1100
|
record_count: s.record_count,
|
|
884
1101
|
system_id: s.system_id,
|
|
885
1102
|
system_name: s.system_name,
|
|
@@ -897,7 +1114,7 @@ function buildAttestationPayload(subject, att, certFormatVersion) {
|
|
|
897
1114
|
attested_at: attestedAt,
|
|
898
1115
|
payload_type: attestationPayloadType(certFormatVersion),
|
|
899
1116
|
proof_mode: att.proof_mode,
|
|
900
|
-
subject_hash:
|
|
1117
|
+
subject_hash: bytesToHex(decodeFixed(subject.identifier_hash, "subject.identifier_hash", 32)),
|
|
901
1118
|
systems,
|
|
902
1119
|
};
|
|
903
1120
|
return canonicalJson(payload);
|
|
@@ -963,9 +1180,9 @@ function attestationPayloadType(version) {
|
|
|
963
1180
|
return PAYLOAD_TYPE_ATTESTATION;
|
|
964
1181
|
}
|
|
965
1182
|
function buildCertificatePayload(cert) {
|
|
966
|
-
const att = cert
|
|
967
|
-
const issuer = cert
|
|
968
|
-
const subject = cert
|
|
1183
|
+
const att = objectAt(cert, "attestation", "attestation");
|
|
1184
|
+
const issuer = objectAt(cert, "issuer", "issuer");
|
|
1185
|
+
const subject = objectAt(cert, "subject", "subject");
|
|
969
1186
|
// One list, keyed by system_id (ADR-016 §2). v2 signed an attestation list
|
|
970
1187
|
// and a verification list joined only on the human-editable system_name,
|
|
971
1188
|
// which made a partial deletion indistinguishable from a complete one.
|
|
@@ -973,19 +1190,25 @@ function buildCertificatePayload(cert) {
|
|
|
973
1190
|
const coversMeasured = signatureCoversMeasuredTransport(version);
|
|
974
1191
|
const coversRecoverable = signatureCoversRecoverableState(version);
|
|
975
1192
|
const coversAuthorization = signatureCoversAuthorization(version);
|
|
976
|
-
const systems = (cert
|
|
1193
|
+
const systems = arrayAt(cert, "systems", "systems").map((entry, i) => {
|
|
1194
|
+
const where = `systems[${i}]`;
|
|
1195
|
+
const s = requireObject(entry, where);
|
|
977
1196
|
const sys = {
|
|
978
|
-
attested_at: formatTimestamp(s.attested_at),
|
|
979
|
-
attested_count: s.attested_count,
|
|
980
|
-
canonical_version: s.canonical_version ?? null,
|
|
981
|
-
connector_type: s.connector_type,
|
|
982
|
-
hash_scope: s.hash_scope,
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1197
|
+
attested_at: formatTimestamp(s.attested_at, `${where}.attested_at`),
|
|
1198
|
+
attested_count: requireUint(s.attested_count, `${where}.attested_count`),
|
|
1199
|
+
canonical_version: optionalStringAt(s, "canonical_version", `${where}.canonical_version`) ?? null,
|
|
1200
|
+
connector_type: stringAt(s, "connector_type", `${where}.connector_type`),
|
|
1201
|
+
hash_scope: stringAt(s, "hash_scope", `${where}.hash_scope`),
|
|
1202
|
+
// Absent is a system with no Merkle root; anything present must be one.
|
|
1203
|
+
// Go's *[32]byte is nil or 32 bytes, never "" or [].
|
|
1204
|
+
merkle_root: s.merkle_root == null
|
|
1205
|
+
? null
|
|
1206
|
+
: bytesToHex(decodeFixed(s.merkle_root, `${where}.merkle_root`, 32)),
|
|
1207
|
+
query_hash: bytesToHex(decodeFixed(s.query_hash, `${where}.query_hash`, 32)),
|
|
1208
|
+
system_id: stringAt(s, "system_id", `${where}.system_id`),
|
|
1209
|
+
system_name: stringAt(s, "system_name", `${where}.system_name`),
|
|
1210
|
+
verified_at: formatTimestamp(s.verified_at, `${where}.verified_at`),
|
|
1211
|
+
verified_count: requireUint(s.verified_count, `${where}.verified_count`),
|
|
989
1212
|
};
|
|
990
1213
|
if (coversMeasured) {
|
|
991
1214
|
sys.read_only_enforcement = requireMeasured(s.read_only_enforcement, "read_only_enforcement", s.system_name, version);
|
|
@@ -1005,9 +1228,9 @@ function buildCertificatePayload(cert) {
|
|
|
1005
1228
|
// The attestation block carries no system list of its own: the merged list
|
|
1006
1229
|
// above is a superset of it. verification_signature is gone entirely.
|
|
1007
1230
|
const attObj = {
|
|
1008
|
-
attestation_signature:
|
|
1009
|
-
attested_at: formatTimestamp(att.attested_at),
|
|
1010
|
-
proof_mode: att.proof_mode,
|
|
1231
|
+
attestation_signature: bytesToHex(decodeFixed(att.attestation_signature, "attestation.attestation_signature", 64)),
|
|
1232
|
+
attested_at: formatTimestamp(att.attested_at, "attestation.attested_at"),
|
|
1233
|
+
proof_mode: stringAt(att, "proof_mode", "attestation.proof_mode"),
|
|
1011
1234
|
};
|
|
1012
1235
|
// Fields added after v3 are gated on the certificate's OWN version. A v3
|
|
1013
1236
|
// certificate must reconstruct to the same bytes forever; reading these
|
|
@@ -1019,9 +1242,9 @@ function buildCertificatePayload(cert) {
|
|
|
1019
1242
|
const isV5Plus = formatAtLeast(version, FORMAT_VERSION_V5);
|
|
1020
1243
|
const isV4Plus = formatAtLeast(version, FORMAT_VERSION_V4);
|
|
1021
1244
|
const issuerObj = {
|
|
1022
|
-
key_id: issuer.key_id,
|
|
1023
|
-
name: issuer.name,
|
|
1024
|
-
public_key:
|
|
1245
|
+
key_id: stringAt(issuer, "key_id", "issuer.key_id"),
|
|
1246
|
+
name: stringAt(issuer, "name", "issuer.name"),
|
|
1247
|
+
public_key: bytesToHex(decodeFixed(issuer.public_key, "issuer.public_key", 32)),
|
|
1025
1248
|
};
|
|
1026
1249
|
// v7 signs the algorithm, and does so unconditionally: unlike legal_entity
|
|
1027
1250
|
// and enclave_pcr0, "which scheme signed this" is never unknown to a signer,
|
|
@@ -1029,43 +1252,48 @@ function buildCertificatePayload(cert) {
|
|
|
1029
1252
|
if (signatureCoversAlgorithm(version)) {
|
|
1030
1253
|
issuerObj.algorithm = requireMeasured(issuer.algorithm, "issuer.algorithm", "issuer", version);
|
|
1031
1254
|
}
|
|
1032
|
-
|
|
1033
|
-
|
|
1255
|
+
// A non-string here used to be dropped silently, so a junk value added to a
|
|
1256
|
+
// record that never carried one still verified. Python refused it; so does
|
|
1257
|
+
// Go, at parse. Now this does too.
|
|
1258
|
+
if (isV4Plus) {
|
|
1259
|
+
const legalEntity = optionalStringAt(issuer, "legal_entity", "issuer.legal_entity");
|
|
1260
|
+
if (legalEntity)
|
|
1261
|
+
issuerObj.legal_entity = legalEntity;
|
|
1034
1262
|
}
|
|
1035
1263
|
// Signed from v5, and omitted when absent or empty: a build with no
|
|
1036
1264
|
// measurement signs none, and "" is a value a reader could mistake for one.
|
|
1037
|
-
if (signatureCoversEnclavePcr0(version)
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1265
|
+
if (signatureCoversEnclavePcr0(version)) {
|
|
1266
|
+
const pcr0 = optionalStringAt(issuer, "enclave_pcr0", "issuer.enclave_pcr0");
|
|
1267
|
+
if (pcr0)
|
|
1268
|
+
issuerObj.enclave_pcr0 = pcr0;
|
|
1041
1269
|
}
|
|
1042
1270
|
// v5 drops identifier_type_hint: signed, but always the constant "custom",
|
|
1043
1271
|
// so it was never evidence. v3 and v4 keep it or they stop verifying.
|
|
1044
1272
|
const subjectObj = {
|
|
1045
|
-
identifier_hash:
|
|
1273
|
+
identifier_hash: bytesToHex(decodeFixed(subject.identifier_hash, "subject.identifier_hash", 32)),
|
|
1046
1274
|
};
|
|
1047
1275
|
if (!isV5Plus) {
|
|
1048
|
-
subjectObj.identifier_type_hint = subject.identifier_type_hint;
|
|
1276
|
+
subjectObj.identifier_type_hint = stringAt(subject, "identifier_type_hint", "subject.identifier_type_hint");
|
|
1049
1277
|
}
|
|
1050
1278
|
// status and revocation are deliberately absent (ADR-016 §3): a signature
|
|
1051
1279
|
// commits to bytes at an instant, revocation is discovered later, so it
|
|
1052
1280
|
// travels as a separate short-lived signed status statement.
|
|
1053
1281
|
const payload = {
|
|
1054
1282
|
attestation: attObj,
|
|
1055
|
-
attestation_id: cert
|
|
1056
|
-
certificate_format_version:
|
|
1057
|
-
certificate_id: cert
|
|
1058
|
-
issued_at: formatTimestamp(cert.issued_at),
|
|
1283
|
+
attestation_id: stringAt(cert, "attestation_id", "attestation_id"),
|
|
1284
|
+
certificate_format_version: version,
|
|
1285
|
+
certificate_id: stringAt(cert, "certificate_id", "certificate_id"),
|
|
1286
|
+
issued_at: formatTimestamp(cert.issued_at, "issued_at"),
|
|
1059
1287
|
issuer: issuerObj,
|
|
1060
1288
|
payload_type: certificatePayloadType(version),
|
|
1061
1289
|
subject: subjectObj,
|
|
1062
1290
|
systems,
|
|
1063
1291
|
};
|
|
1064
1292
|
if (isV4Plus && cert.scope != null) {
|
|
1065
|
-
const scope = cert
|
|
1293
|
+
const scope = objectAt(cert, "scope", "scope");
|
|
1066
1294
|
payload.scope = {
|
|
1067
|
-
text_sha256:
|
|
1068
|
-
version: scope.version,
|
|
1295
|
+
text_sha256: bytesToHex(decodeFixed(scope.text_sha256, "scope.text_sha256", 32)),
|
|
1296
|
+
version: stringAt(scope, "version", "scope.version"),
|
|
1069
1297
|
};
|
|
1070
1298
|
}
|
|
1071
1299
|
return canonicalJson(payload);
|
|
@@ -1076,7 +1304,7 @@ function buildLogLeafPayload(entryType, certificateId, certificateHash, appended
|
|
|
1076
1304
|
throw new errors_js_1.VerificationError(`invalid log entry_type: ${String(entryType)}`);
|
|
1077
1305
|
}
|
|
1078
1306
|
const payload = {
|
|
1079
|
-
appended_at: formatTimestamp(appendedAt),
|
|
1307
|
+
appended_at: formatTimestamp(appendedAt, "transparency.appended_at"),
|
|
1080
1308
|
certificate_hash: bytesToHex(certificateHash),
|
|
1081
1309
|
certificate_id: certificateId,
|
|
1082
1310
|
entry_type: entryType,
|
|
@@ -1125,13 +1353,13 @@ function attestationSystems(cert) {
|
|
|
1125
1353
|
* between two modules of this package, not published surface.
|
|
1126
1354
|
*/
|
|
1127
1355
|
function buildTreeHeadPayload(head) {
|
|
1128
|
-
const logId = head.log_id;
|
|
1129
|
-
const named =
|
|
1356
|
+
const logId = optionalStringAt(head, "log_id", "signed_tree_head.log_id");
|
|
1357
|
+
const named = logId !== undefined && logId !== "";
|
|
1130
1358
|
const payload = {
|
|
1131
1359
|
payload_type: named ? PAYLOAD_TYPE_TREE_HEAD_V7 : PAYLOAD_TYPE_TREE_HEAD,
|
|
1132
|
-
root_hash:
|
|
1133
|
-
timestamp: formatTimestamp(head.timestamp),
|
|
1134
|
-
tree_size: head.tree_size,
|
|
1360
|
+
root_hash: bytesToHex(decodeFixed(head.root_hash, "signed_tree_head.root_hash", 32)),
|
|
1361
|
+
timestamp: formatTimestamp(head.timestamp, "signed_tree_head.timestamp"),
|
|
1362
|
+
tree_size: requireUint(head.tree_size ?? 0, "signed_tree_head.tree_size"),
|
|
1135
1363
|
};
|
|
1136
1364
|
if (named)
|
|
1137
1365
|
payload.log_id = logId;
|
|
@@ -1271,21 +1499,21 @@ async function verifyConsistency(crypto, oldSize, newSize, oldRoot, newRoot, pro
|
|
|
1271
1499
|
function buildCertificateStatusPayload(stmt) {
|
|
1272
1500
|
const payload = {
|
|
1273
1501
|
payload_type: PAYLOAD_TYPE_CERTIFICATE_STATUS,
|
|
1274
|
-
certificate_id: stmt.certificate_id,
|
|
1275
|
-
statement_expires_at: formatTimestamp(stmt.statement_expires_at),
|
|
1276
|
-
statement_issued_at: formatTimestamp(stmt.statement_issued_at),
|
|
1277
|
-
status: stmt.status,
|
|
1278
|
-
sth_root_hash:
|
|
1279
|
-
sth_tree_size: stmt.sth_tree_size,
|
|
1502
|
+
certificate_id: stringAt(stmt, "certificate_id", "status.certificate_id"),
|
|
1503
|
+
statement_expires_at: formatTimestamp(stmt.statement_expires_at, "status.statement_expires_at"),
|
|
1504
|
+
statement_issued_at: formatTimestamp(stmt.statement_issued_at, "status.statement_issued_at"),
|
|
1505
|
+
status: stringAt(stmt, "status", "status.status"),
|
|
1506
|
+
sth_root_hash: bytesToHex(decodeFixed(stmt.sth_root_hash, "status.sth_root_hash", 32)),
|
|
1507
|
+
sth_tree_size: requireUint(stmt.sth_tree_size ?? 0, "status.sth_tree_size"),
|
|
1280
1508
|
};
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1509
|
+
const replacement = optionalStringAt(stmt, "replacement_certificate_id", "status.replacement_certificate_id");
|
|
1510
|
+
if (replacement !== undefined)
|
|
1511
|
+
payload.replacement_certificate_id = replacement;
|
|
1284
1512
|
if (stmt.revocation_log_index != null) {
|
|
1285
|
-
payload.revocation_log_index = stmt.revocation_log_index;
|
|
1513
|
+
payload.revocation_log_index = requireUint(stmt.revocation_log_index, "status.revocation_log_index");
|
|
1286
1514
|
}
|
|
1287
1515
|
if (stmt.revoked_at != null) {
|
|
1288
|
-
payload.revoked_at = formatTimestamp(stmt.revoked_at);
|
|
1516
|
+
payload.revoked_at = formatTimestamp(stmt.revoked_at, "status.revoked_at");
|
|
1289
1517
|
}
|
|
1290
1518
|
return canonicalJson(payload);
|
|
1291
1519
|
}
|
|
@@ -1320,7 +1548,7 @@ function buildKeyListPayload(doc) {
|
|
|
1320
1548
|
keys: entries,
|
|
1321
1549
|
statement_expires_at: formatTimestamp(doc.statement_expires_at),
|
|
1322
1550
|
statement_issued_at: formatTimestamp(doc.statement_issued_at),
|
|
1323
|
-
sth_root_hash:
|
|
1551
|
+
sth_root_hash: bytesToHex(decodeFixed(doc.sth_root_hash, "sth_root_hash", 32)),
|
|
1324
1552
|
sth_tree_size: doc.sth_tree_size,
|
|
1325
1553
|
};
|
|
1326
1554
|
return canonicalJson(payload);
|
|
@@ -1346,18 +1574,38 @@ exports.VALID_REVOCATION_UNKNOWN = "VALID_REVOCATION_UNKNOWN";
|
|
|
1346
1574
|
*
|
|
1347
1575
|
* The third row is the point: `verifyCertificate` answers VALID there, which
|
|
1348
1576
|
* reads as "not revoked" and is not something it checked.
|
|
1577
|
+
*
|
|
1578
|
+
* `options` is passed to {@link verifyCertificate} unchanged.
|
|
1349
1579
|
*/
|
|
1350
|
-
async function verifyCertificateWithStatus(crypto, certificate, publicKeys, status, now) {
|
|
1580
|
+
async function verifyCertificateWithStatus(crypto, certificate, publicKeys, status, now, options) {
|
|
1351
1581
|
// A refusal throws out of verifyCertificate, so `base` is VALID or one of the
|
|
1352
1582
|
// two soft verdicts. Returning a soft verdict here skipped every check below
|
|
1353
1583
|
// it: a revoked certificate signed by a compromised-later key answered
|
|
1354
1584
|
// VALID_KEY_COMPROMISED_LATER and its statement was never authenticated. The
|
|
1355
1585
|
// verdict is held instead, and resolved against what the statement says.
|
|
1356
|
-
const base = await verifyCertificate(crypto, certificate, publicKeys);
|
|
1586
|
+
const base = await verifyCertificate(crypto, certificate, publicKeys, options);
|
|
1357
1587
|
if (status == null) {
|
|
1358
1588
|
return resolveWithBase(base, exports.VALID_REVOCATION_UNKNOWN);
|
|
1359
1589
|
}
|
|
1360
|
-
|
|
1590
|
+
return resolveWithBase(base, await verifyStatusStatement(crypto, status, publicKeys, certificate.certificate_id, now));
|
|
1591
|
+
}
|
|
1592
|
+
/**
|
|
1593
|
+
* The statement half of {@link verifyCertificateWithStatus}: authenticate a
|
|
1594
|
+
* status statement about `certificateId` against the published keys, and read
|
|
1595
|
+
* it at `now`. The /verify page's QR lookup has a record id and no record, and
|
|
1596
|
+
* uses this so it shows what a signed, fresh statement says, never what the
|
|
1597
|
+
* status endpoint claims.
|
|
1598
|
+
*
|
|
1599
|
+
* | input | result |
|
|
1600
|
+
* |---|---|
|
|
1601
|
+
* | fresh, REVOKED | throws VerificationError, code CERTIFICATE_REVOKED |
|
|
1602
|
+
* | fresh, ACTIVE | the statement key's verdict: `"VALID"`, or a soft one |
|
|
1603
|
+
* | stale, or a status this SDK does not know | `"VALID_REVOCATION_UNKNOWN"` |
|
|
1604
|
+
* | unknown or unusable key, bad signature, another certificate | throws VerificationError |
|
|
1605
|
+
*/
|
|
1606
|
+
async function verifyStatusStatement(crypto, status, publicKeys, certificateId, now) {
|
|
1607
|
+
requireObject(status, "status");
|
|
1608
|
+
const keyId = optionalStringAt(status, "key_id", "status.key_id");
|
|
1361
1609
|
const info = keyId ? publicKeys.get(keyId) : undefined;
|
|
1362
1610
|
if (!info) {
|
|
1363
1611
|
throw new errors_js_1.VerificationError(`status statement signed by unknown key: ${keyId}`);
|
|
@@ -1378,10 +1626,10 @@ async function verifyCertificateWithStatus(crypto, certificate, publicKeys, stat
|
|
|
1378
1626
|
const at = formatTimestamp(when.toISOString());
|
|
1379
1627
|
const statusKeyVerdict = requireUsableKey(info, parseRfc3339(at), keyId, "status statement key");
|
|
1380
1628
|
const payload = buildCertificateStatusPayload(status);
|
|
1381
|
-
//
|
|
1629
|
+
// decodeFixed, not hexToBytes: signatures arrive as hex, base64 or a byte
|
|
1382
1630
|
// array depending on the producer, and every other signature on this path
|
|
1383
1631
|
// goes through the same decoder.
|
|
1384
|
-
const sig =
|
|
1632
|
+
const sig = decodeFixed(status.signature, "status.signature", 64);
|
|
1385
1633
|
if (!(await crypto.ed25519Verify(info.keyBytes, payload, sig))) {
|
|
1386
1634
|
throw new errors_js_1.VerificationError("status statement signature is invalid");
|
|
1387
1635
|
}
|
|
@@ -1398,8 +1646,7 @@ async function verifyCertificateWithStatus(crypto, certificate, publicKeys, stat
|
|
|
1398
1646
|
// Every signature still verified; the binding was the only forged part.
|
|
1399
1647
|
// core/verify.go never had the fallback, so Go said STATUS_STATEMENT_MISMATCH
|
|
1400
1648
|
// while both SDKs and the browser bundle said VALID.
|
|
1401
|
-
|
|
1402
|
-
if (status.certificate_id !== certId) {
|
|
1649
|
+
if (status.certificate_id !== certificateId) {
|
|
1403
1650
|
throw new errors_js_1.VerificationError("status statement is about a different certificate");
|
|
1404
1651
|
}
|
|
1405
1652
|
// Compared in the normalized form the payload signs, so the freshness check
|
|
@@ -1409,7 +1656,7 @@ async function verifyCertificateWithStatus(crypto, certificate, publicKeys, stat
|
|
|
1409
1656
|
if (at < issued || at >= expires) {
|
|
1410
1657
|
// Stale is not a weaker answer, it is no answer — including for a REVOKED
|
|
1411
1658
|
// statement, which must never decay into VALID.
|
|
1412
|
-
return
|
|
1659
|
+
return exports.VALID_REVOCATION_UNKNOWN;
|
|
1413
1660
|
}
|
|
1414
1661
|
if (status.status === "REVOKED") {
|
|
1415
1662
|
// Coded, because this is the only status-path rejection that is a fact
|
|
@@ -1424,12 +1671,12 @@ async function verifyCertificateWithStatus(crypto, certificate, publicKeys, stat
|
|
|
1424
1671
|
// case — was reported as revocation CHECKED AND PASSED. Mirrors
|
|
1425
1672
|
// core.ValidCertificateStatusValue.
|
|
1426
1673
|
if (!KNOWN_STATUS_VALUES.has(status.status)) {
|
|
1427
|
-
return
|
|
1674
|
+
return exports.VALID_REVOCATION_UNKNOWN;
|
|
1428
1675
|
}
|
|
1429
1676
|
// A soft verdict on the status key is the result, not a footnote — but it is
|
|
1430
1677
|
// reported only once the statement has authenticated and been read, so it can
|
|
1431
1678
|
// neither speak for an unverified statement nor suppress a revocation.
|
|
1432
|
-
return
|
|
1679
|
+
return statusKeyVerdict;
|
|
1433
1680
|
}
|
|
1434
1681
|
/**
|
|
1435
1682
|
* Picks what to report when the certificate's own key returned a soft verdict
|