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/src/verify.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
import type { CryptoOps } from "./crypto.js";
|
|
20
20
|
import {
|
|
21
|
+
AUTHORIZATION_POLICY_FAILED,
|
|
21
22
|
CERTIFICATE_REVOKED,
|
|
22
23
|
UNSUPPORTED_ALGORITHM,
|
|
23
24
|
UNSUPPORTED_FORMAT_VERSION,
|
|
@@ -74,8 +75,9 @@ const PAYLOAD_TYPE_ATTESTATION_V8 = "burnledger.attestation.v8";
|
|
|
74
75
|
const PAYLOAD_TYPE_VERIFICATION_RECORD_V8 = "burnledger.verification_record.v8";
|
|
75
76
|
const FORMAT_VERSION_V8 = "8.0";
|
|
76
77
|
// v9 adds two per-system fields, authorization and customer_key_id: whether the
|
|
77
|
-
// enclave checked this system against a
|
|
78
|
-
//
|
|
78
|
+
// enclave checked this system against a registration certificate (ADR-025 §4),
|
|
79
|
+
// and under which customer key group if it did. That the key group SIGNED the
|
|
80
|
+
// registration holds only from enclave protocol 12: see PRE_PROTOCOL_12_IMAGES.
|
|
79
81
|
//
|
|
80
82
|
// The attestation gains NO field at v9. Its tag still moves, because the tag
|
|
81
83
|
// follows the record's version and the record's bytes changed — a v9
|
|
@@ -246,25 +248,28 @@ const PAYLOAD_TYPE_LOG_LEAF = "burnledger.log_leaf.v3";
|
|
|
246
248
|
const PAYLOAD_TYPE_CERTIFICATE_STATUS = "burnledger.certificate_status.v3";
|
|
247
249
|
const PAYLOAD_TYPE_KEY_LIST = "burnledger.key_list.v3";
|
|
248
250
|
|
|
251
|
+
/** Decode hex: whole bytes, digits in either case, nothing else. Anything else
|
|
252
|
+
* throws TypeError. It is exported, so its input is a boundary: reading a stray
|
|
253
|
+
* character as 0, as it once did, turned a typo into different bytes. */
|
|
249
254
|
export function hexToBytes(hex: string): Uint8Array {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
out[i] = (unhex(hi) << 4) | unhex(lo);
|
|
255
|
+
if (typeof hex !== "string") throw new TypeError(`hex is not a string: ${typeof hex}`);
|
|
256
|
+
if (hex.length % 2 !== 0) throw new TypeError(`hex has an odd number of digits: ${hex.length}`);
|
|
257
|
+
const out = new Uint8Array(hex.length / 2);
|
|
258
|
+
for (let i = 0; i < out.length; i++) {
|
|
259
|
+
out[i] = (unhex(hex, i * 2) << 4) | unhex(hex, i * 2 + 1);
|
|
256
260
|
}
|
|
257
261
|
return out;
|
|
258
262
|
}
|
|
259
263
|
|
|
260
|
-
function unhex(
|
|
264
|
+
function unhex(hex: string, at: number): number {
|
|
265
|
+
const c = hex.charCodeAt(at);
|
|
261
266
|
// 0-9
|
|
262
267
|
if (c >= 48 && c <= 57) return c - 48;
|
|
263
268
|
// a-f
|
|
264
269
|
if (c >= 97 && c <= 102) return c - 87;
|
|
265
270
|
// A-F
|
|
266
271
|
if (c >= 65 && c <= 70) return c - 55;
|
|
267
|
-
|
|
272
|
+
throw new TypeError(`hex has a non-hex character ${JSON.stringify(hex[at])} at ${at}`);
|
|
268
273
|
}
|
|
269
274
|
|
|
270
275
|
export function bytesToHex(bytes: Uint8Array): string {
|
|
@@ -362,6 +367,13 @@ export async function publicKeyFromHex(
|
|
|
362
367
|
hexKey: string,
|
|
363
368
|
opts?: PublicKeyOptions,
|
|
364
369
|
): Promise<PublicKeyInfo> {
|
|
370
|
+
// Refused here, as the VerificationError callers are told to catch, rather
|
|
371
|
+
// than left to hexToBytes's TypeError. The alphabet is the one
|
|
372
|
+
// core.KeyEntry.PublicKeyInfo takes through hex.DecodeString: either case,
|
|
373
|
+
// whole bytes, nothing else.
|
|
374
|
+
if (typeof hexKey !== "string" || !HEX_TEXT_RE.test(hexKey)) {
|
|
375
|
+
throw new VerificationError(`public key is not hex: ${JSON.stringify(hexKey)}`);
|
|
376
|
+
}
|
|
365
377
|
const raw = hexToBytes(hexKey);
|
|
366
378
|
if (raw.length !== 32) {
|
|
367
379
|
throw new VerificationError(`public key must be 32 bytes, got ${raw.length}`);
|
|
@@ -469,8 +481,8 @@ export function evaluateKey(pki: PublicKeyInfo, anchor: number | null): string {
|
|
|
469
481
|
export function certificateAnchor(certificate: Record<string, unknown>): number | null {
|
|
470
482
|
const transparency = certificate.transparency as Record<string, unknown> | null | undefined;
|
|
471
483
|
if (transparency == null) return null;
|
|
472
|
-
const sth = transparency.signed_tree_head as Record<string, unknown> | undefined;
|
|
473
|
-
if (sth
|
|
484
|
+
const sth = transparency.signed_tree_head as Record<string, unknown> | null | undefined;
|
|
485
|
+
if (sth == null || typeof sth.timestamp !== "string") return null;
|
|
474
486
|
try {
|
|
475
487
|
return parseRfc3339(sth.timestamp);
|
|
476
488
|
} catch {
|
|
@@ -503,13 +515,19 @@ async function verifiedCertificateAnchor(
|
|
|
503
515
|
|
|
504
516
|
const transparency = certificate.transparency as Record<string, unknown>;
|
|
505
517
|
const sth = transparency.signed_tree_head as Record<string, unknown>;
|
|
518
|
+
// Only a refusal of the head's own fields means "no anchor". A bare catch
|
|
519
|
+
// here gave a defect in this library — or a crypto provider that threw — the
|
|
520
|
+
// same quiet answer as a malformed document.
|
|
521
|
+
let headPayload: Uint8Array;
|
|
522
|
+
let headSig: Uint8Array;
|
|
506
523
|
try {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
524
|
+
headPayload = buildTreeHeadPayload(sth);
|
|
525
|
+
headSig = decodeFixed(sth.signature, "signed_tree_head.signature", 64);
|
|
526
|
+
} catch (e) {
|
|
527
|
+
if (e instanceof VerificationError) return null;
|
|
528
|
+
throw e;
|
|
512
529
|
}
|
|
530
|
+
if (!(await crypto.ed25519Verify(pki.keyBytes, headPayload, headSig))) return null;
|
|
513
531
|
return anchor;
|
|
514
532
|
}
|
|
515
533
|
|
|
@@ -559,21 +577,246 @@ function requireUsableKey(
|
|
|
559
577
|
|
|
560
578
|
type Cert = Record<string, unknown>;
|
|
561
579
|
|
|
580
|
+
/**
|
|
581
|
+
* Require what ADR-025 lets a record prove: that a key group you name signed the
|
|
582
|
+
* registration the enclave checked each system against before measuring it.
|
|
583
|
+
*
|
|
584
|
+
* A pass means each required system is `certified` under one of
|
|
585
|
+
* `customerKeyIds`, in a record from an enclave image of wire protocol 12 or
|
|
586
|
+
* later — the first that registers a system only with that key group's
|
|
587
|
+
* signatures. A record from an earlier image (PRE_PROTOCOL_12_IMAGES), or one
|
|
588
|
+
* that names no image, fails: those images could say `certified` with no
|
|
589
|
+
* customer signature at all.
|
|
590
|
+
*
|
|
591
|
+
* Off unless asked for, because a record that says `authorization: none` is
|
|
592
|
+
* still a genuine record. Whoever relays requests to the enclave can always
|
|
593
|
+
* leave a registration out (ADR-025 §5, §8), and the enclave cannot refuse that
|
|
594
|
+
* for want of durable state. So "my systems must be certified" can be enforced
|
|
595
|
+
* in one place only: here, by the party relying on the record.
|
|
596
|
+
*/
|
|
597
|
+
export interface AuthorizationPolicy {
|
|
598
|
+
/** The system_ids that must be certified. Omitted: every system the record covers. */
|
|
599
|
+
readonly systems?: readonly string[];
|
|
600
|
+
/**
|
|
601
|
+
* The key groups you accept. Required: `certified` under a key id you did not
|
|
602
|
+
* name proves nothing to you, because the relay can enroll a key group of its
|
|
603
|
+
* own and the record would name that one.
|
|
604
|
+
*/
|
|
605
|
+
readonly customerKeyIds: readonly string[];
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
export interface VerifyOptions {
|
|
609
|
+
/**
|
|
610
|
+
* Refuse the record, with a VerificationError whose code is
|
|
611
|
+
* AUTHORIZATION_POLICY_FAILED, unless it meets this policy. Omitted: no
|
|
612
|
+
* requirement, and `authorization: none` verifies as it always has.
|
|
613
|
+
*/
|
|
614
|
+
readonly requireAuthorization?: AuthorizationPolicy;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function policySet(values: readonly string[] | undefined, field: string, lower: boolean): Set<string> | undefined {
|
|
618
|
+
if (values === undefined) return undefined;
|
|
619
|
+
if (!Array.isArray(values) || !values.every((v) => typeof v === "string" && v !== "")) {
|
|
620
|
+
throw new TypeError(`requireAuthorization.${field} must be an array of non-empty strings`);
|
|
621
|
+
}
|
|
622
|
+
if (values.length === 0) {
|
|
623
|
+
// An empty requirement requires nothing, which is never what a caller who
|
|
624
|
+
// built a policy meant.
|
|
625
|
+
throw new Error(`an empty requireAuthorization.${field} requires nothing; omit it instead`);
|
|
626
|
+
}
|
|
627
|
+
return new Set(values.map((v) => (lower ? v.toLowerCase() : v)));
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
interface ParsedPolicy {
|
|
631
|
+
readonly systems: Set<string> | undefined;
|
|
632
|
+
readonly customerKeyIds: Set<string>;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/** Check a policy before anything is verified, so a caller's mistake surfaces as
|
|
636
|
+
* one whatever the record turns out to be. */
|
|
637
|
+
function parsePolicy(policy: AuthorizationPolicy): ParsedPolicy {
|
|
638
|
+
const keys = policy.customerKeyIds;
|
|
639
|
+
if (keys === undefined || (Array.isArray(keys) && keys.length === 0)) {
|
|
640
|
+
throw new TypeError(
|
|
641
|
+
"requireAuthorization.customerKeyIds is required, with at least one key id: `certified` under " +
|
|
642
|
+
"a key id you did not name proves nothing to you, because the relay can enroll a key group of its own",
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
return {
|
|
646
|
+
systems: policySet(policy.systems, "systems", true),
|
|
647
|
+
customerKeyIds: policySet(policy.customerKeyIds, "customerKeyIds", false)!,
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* The enclave images that issued format 9.0 while running wire protocol 11 or
|
|
653
|
+
* earlier, by PCR0, as the published measurement history records them
|
|
654
|
+
* (website/docs/enclave-measurements/, docs/CONNECTOR-STATUS.md).
|
|
655
|
+
*
|
|
656
|
+
* Their `certified` is not evidence of a customer signature. Before protocol 12
|
|
657
|
+
* the enclave enrolled a key group with no proof that anyone held its keys, and
|
|
658
|
+
* registered a system for whoever relayed the authorization — so the host could
|
|
659
|
+
* make a genuine 9.0 record say `certified`, under a key id it chose, with no
|
|
660
|
+
* customer signature anywhere (reproduced by an independent re-review,
|
|
661
|
+
* 2026-09-13).
|
|
662
|
+
*
|
|
663
|
+
* THE SET IS CLOSED BY POLICY, NOT BY ENFORCEMENT: the release runbook forbids
|
|
664
|
+
* admitting or rolling back to an image of protocol 11 or earlier once any
|
|
665
|
+
* customer key is enrolled, and admitting one takes a KMS re-pin, which ADR-027
|
|
666
|
+
* leaves to the owner. The protocol-12 cutover's KMS tighten drops every
|
|
667
|
+
* measurement here from the signing key's policy, after which none of them can
|
|
668
|
+
* sign. v42 ran for seven minutes and is recorded as issuing nothing; it is here
|
|
669
|
+
* because it held the signing key while it ran.
|
|
670
|
+
*
|
|
671
|
+
* The Python SDK carries the same list, in _verify.py.
|
|
672
|
+
*/
|
|
673
|
+
export const PRE_PROTOCOL_12_IMAGES: ReadonlyMap<string, string> = new Map([
|
|
674
|
+
["0e79f985c287fbad3ed9aa1d5443189c45428929086dc4d4312ba71c4039742a8bca02f4ea15ab318d82e1d86ff883d3", "EIF v39"],
|
|
675
|
+
["665864551aeb57705c3b3721018112cbd0e2be98de2e0ff9142a6c0abf366c72c6ba6cdcd31cd6ed8471862e1b9187ce", "EIF v40"],
|
|
676
|
+
["7a61327a0ee11508764a8ad03d68e81e3b374e4e605a284e9e815ec9cf57de8497e59d57048977441f9c5c98bee1d14b", "EIF v41"],
|
|
677
|
+
["f1e02ace0011bfc0d60d6dd76da0078b7706f3165eff6904a36e97e5d2029246099ef20bf15f87d0263dd843f878731c", "EIF v42"],
|
|
678
|
+
["10ed72207ffd6a14f5913130eca69e9f4f9de0447b360e9cfd8aa8e349681c6fe98c515499a38a347d67932a6201a9b4", "EIF v43"],
|
|
679
|
+
["0bfbd2251cb9e138491c6ce424ad132b28be152e1486aa7100acabce0201f3f5d500eb47dc00184de4eebc93fe4dc695", "EIF v44"],
|
|
680
|
+
["4d68b3ef6b77418f2827964400241ee9db7e101ce53666b75cc6cf7aa17d7c9f3a1b9055bafec4f799beee6e00227546", "EIF v45 and v46"],
|
|
681
|
+
]);
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* What a record's signed issuer.enclave_pcr0 makes of `certified`: whether the
|
|
685
|
+
* image that signed it could mark a system certified without the customer's
|
|
686
|
+
* signature. "protocol-12-or-later" is the inference the closed list licenses:
|
|
687
|
+
* a genuine 9.0 record under any other measurement was signed by a later image.
|
|
688
|
+
*/
|
|
689
|
+
export type IssuingImage =
|
|
690
|
+
| { readonly kind: "unnamed" }
|
|
691
|
+
| { readonly kind: "before-protocol-12"; readonly name: string; readonly pcr0: string }
|
|
692
|
+
| { readonly kind: "protocol-12-or-later"; readonly pcr0: string };
|
|
693
|
+
|
|
694
|
+
export function issuingImage(certificate: Cert): IssuingImage {
|
|
695
|
+
const issuer = certificate.issuer as Cert | undefined;
|
|
696
|
+
const pcr0 = String(issuer?.enclave_pcr0 ?? "").toLowerCase();
|
|
697
|
+
if (pcr0 === "") return { kind: "unnamed" };
|
|
698
|
+
const name = PRE_PROTOCOL_12_IMAGES.get(pcr0);
|
|
699
|
+
return name === undefined
|
|
700
|
+
? { kind: "protocol-12-or-later", pcr0 }
|
|
701
|
+
: { kind: "before-protocol-12", name, pcr0 };
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* What `certified` is evidence of, given the image that signed the record: the
|
|
706
|
+
* words every verifier prints after "certified under <key id> —". The Go CLI
|
|
707
|
+
* (cmd/cli/registration_image.go), both SDK CLIs and the /verify page state it
|
|
708
|
+
* identically, and testdata/registration_line_cases.json holds them to it.
|
|
709
|
+
*/
|
|
710
|
+
export function registrationQualifier(image: IssuingImage): string {
|
|
711
|
+
switch (image.kind) {
|
|
712
|
+
case "unnamed":
|
|
713
|
+
return (
|
|
714
|
+
"not evidence here: this record names no enclave image, so it may come from one " +
|
|
715
|
+
"that could mark a system certified without the customer's signature."
|
|
716
|
+
);
|
|
717
|
+
case "before-protocol-12":
|
|
718
|
+
return (
|
|
719
|
+
`not evidence here: this record comes from ${image.name}, an enclave image of ` +
|
|
720
|
+
"protocol 11 or earlier, which could mark a system certified without the customer's signature."
|
|
721
|
+
);
|
|
722
|
+
case "protocol-12-or-later":
|
|
723
|
+
return (
|
|
724
|
+
"the key group with that id signed this system's registration, if that key id is yours: " +
|
|
725
|
+
"this record comes from an enclave image of protocol 12 or later. If that key id is not yours, " +
|
|
726
|
+
"someone else authorized this verification."
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Throw unless a record that has already verified meets `policy`.
|
|
733
|
+
*
|
|
734
|
+
* From 9.0 every field read here is under the certificate signature, the
|
|
735
|
+
* issuer's enclave_pcr0 included. Below 9.0
|
|
736
|
+
* nothing signs `authorization` — those records carry the field outside the
|
|
737
|
+
* signed bytes — so one that says `certified` says only what its last holder
|
|
738
|
+
* typed. It fails on the format, before the field is read.
|
|
739
|
+
*/
|
|
740
|
+
function enforceAuthorization(certificate: Cert, formatVersion: string, policy: ParsedPolicy): void {
|
|
741
|
+
const systemsWanted = policy.systems;
|
|
742
|
+
if (!signatureCoversAuthorization(formatVersion)) {
|
|
743
|
+
throw new VerificationError(
|
|
744
|
+
`authorization required, but this record is format ${formatVersion}, which predates ` +
|
|
745
|
+
`customer-signed registration (${FORMAT_VERSION_V9}); it cannot show that any system was certified`,
|
|
746
|
+
AUTHORIZATION_POLICY_FAILED,
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
const systems = (certificate.systems as Record<string, unknown>[] | null | undefined) ?? [];
|
|
750
|
+
let required = systems;
|
|
751
|
+
if (systemsWanted !== undefined) {
|
|
752
|
+
const byId = new Map(systems.map((s) => [String(s.system_id).toLowerCase(), s]));
|
|
753
|
+
const missing = [...systemsWanted].filter((id) => !byId.has(id)).sort();
|
|
754
|
+
if (missing.length > 0) {
|
|
755
|
+
throw new VerificationError(
|
|
756
|
+
`authorization required for system ${missing[0]}, which this record does not cover`,
|
|
757
|
+
AUTHORIZATION_POLICY_FAILED,
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
required = [...systemsWanted].sort().map((id) => byId.get(id)!);
|
|
761
|
+
}
|
|
762
|
+
const labelOf = (s: Record<string, unknown>): string =>
|
|
763
|
+
`system ${JSON.stringify(s.system_name)} (${String(s.system_id)})`;
|
|
764
|
+
for (const s of required) {
|
|
765
|
+
if (s.authorization !== AUTHORIZATION_CERTIFIED) {
|
|
766
|
+
throw new VerificationError(
|
|
767
|
+
`authorization required, but ${labelOf(s)} is ${JSON.stringify(s.authorization ?? null)}: ` +
|
|
768
|
+
"the enclave did not check it against a registration",
|
|
769
|
+
AUTHORIZATION_POLICY_FAILED,
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
// Asked once, for the record, after every required system has said certified:
|
|
774
|
+
// which image said it decides whether `certified` means a customer signed.
|
|
775
|
+
const image = issuingImage(certificate);
|
|
776
|
+
if (image.kind === "unnamed") {
|
|
777
|
+
throw new VerificationError(
|
|
778
|
+
"authorization required, but this record names no enclave image (issuer.enclave_pcr0 is " +
|
|
779
|
+
"absent), so nothing shows it came from one that registers a system only with the " +
|
|
780
|
+
"customer's signature",
|
|
781
|
+
AUTHORIZATION_POLICY_FAILED,
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
if (image.kind === "before-protocol-12") {
|
|
785
|
+
throw new VerificationError(
|
|
786
|
+
`authorization required, but this record was signed by ${image.name} (PCR0 ${image.pcr0.slice(0, 16)}…), ` +
|
|
787
|
+
"an enclave image of wire protocol 11 or earlier, which could register a system without " +
|
|
788
|
+
"the customer's signature; its `certified` does not show that any key group signed anything",
|
|
789
|
+
AUTHORIZATION_POLICY_FAILED,
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
for (const s of required) {
|
|
793
|
+
if (!policy.customerKeyIds.has(s.customer_key_id as string)) {
|
|
794
|
+
throw new VerificationError(
|
|
795
|
+
`${labelOf(s)} is certified under ${String(s.customer_key_id)}, which is not a key you ` +
|
|
796
|
+
"accept; if it is not yours, someone else authorized this verification",
|
|
797
|
+
AUTHORIZATION_POLICY_FAILED,
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
562
803
|
/** Verify all signatures on a deletion certificate offline. */
|
|
563
804
|
export async function verifyCertificate(
|
|
564
805
|
crypto: CryptoOps,
|
|
565
806
|
certificate: Cert,
|
|
566
807
|
publicKeys: Map<string, PublicKeyInfo>,
|
|
808
|
+
options?: VerifyOptions,
|
|
567
809
|
): Promise<VerificationResult> {
|
|
568
810
|
// Step 0: can this SDK read the format at all? Every check below rebuilds
|
|
569
811
|
// signed bytes from the version, so an unreadable version makes all of them
|
|
570
812
|
// meaningless - and "unknown issuer key" would be the wrong thing to tell
|
|
571
813
|
// someone holding a record that is merely newer than this library.
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
814
|
+
requireObject(certificate, "certificate");
|
|
815
|
+
const policy =
|
|
816
|
+
options?.requireAuthorization === undefined ? undefined : parsePolicy(options.requireAuthorization);
|
|
817
|
+
const formatVersion = checkFormatVersion(certificate.certificate_format_version);
|
|
575
818
|
|
|
576
|
-
const issuer = certificate
|
|
819
|
+
const issuer = objectAt(certificate, "issuer", "issuer");
|
|
577
820
|
// Step 0b: can this SDK check the algorithm the record names? Asked before
|
|
578
821
|
// any signature, because verifying an Ed25519 signature over a record that
|
|
579
822
|
// says it was signed with something else answers a question nobody asked.
|
|
@@ -587,7 +830,7 @@ export async function verifyCertificate(
|
|
|
587
830
|
UNSUPPORTED_ALGORITHM,
|
|
588
831
|
);
|
|
589
832
|
}
|
|
590
|
-
const keyId = issuer.key_id
|
|
833
|
+
const keyId = stringAt(issuer, "key_id", "issuer.key_id");
|
|
591
834
|
|
|
592
835
|
const pki = publicKeys.get(keyId);
|
|
593
836
|
if (pki === undefined) throw new VerificationError(`unknown issuer key: ${keyId}`);
|
|
@@ -602,7 +845,7 @@ export async function verifyCertificate(
|
|
|
602
845
|
// 1. Certificate signature FIRST — nothing below may trust a field until the
|
|
603
846
|
// bytes carrying it are covered by a verified signature.
|
|
604
847
|
const certPayload = buildCertificatePayload(certificate);
|
|
605
|
-
const certSig =
|
|
848
|
+
const certSig = decodeFixed(certificate.certificate_signature, "certificate_signature", 64);
|
|
606
849
|
if (!(await crypto.ed25519Verify(pki.keyBytes, certPayload, certSig))) {
|
|
607
850
|
throw new VerificationError("certificate signature is invalid");
|
|
608
851
|
}
|
|
@@ -621,7 +864,7 @@ export async function verifyCertificate(
|
|
|
621
864
|
},
|
|
622
865
|
certificate.certificate_format_version as string,
|
|
623
866
|
);
|
|
624
|
-
const attSig =
|
|
867
|
+
const attSig = decodeFixed(att.attestation_signature, "attestation.attestation_signature", 64);
|
|
625
868
|
if (!(await crypto.ed25519Verify(pki.keyBytes, attPayload, attSig))) {
|
|
626
869
|
throw new VerificationError("attestation signature is invalid");
|
|
627
870
|
}
|
|
@@ -682,6 +925,10 @@ export async function verifyCertificate(
|
|
|
682
925
|
);
|
|
683
926
|
}
|
|
684
927
|
|
|
928
|
+
if (policy !== undefined) {
|
|
929
|
+
enforceAuthorization(certificate, formatVersion, policy);
|
|
930
|
+
}
|
|
931
|
+
|
|
685
932
|
// A soft key verdict is the result, not a footnote: VALID_KEY_WINDOW_UNKNOWN
|
|
686
933
|
// and VALID_KEY_COMPROMISED_LATER mean this document still needs a human, and
|
|
687
934
|
// reporting VALID here would be the lying by omission ADR-017 exists to stop.
|
|
@@ -697,16 +944,15 @@ export async function verifyTransparency(
|
|
|
697
944
|
certificate: Cert,
|
|
698
945
|
publicKeys: Map<string, PublicKeyInfo>,
|
|
699
946
|
): Promise<TransparencyResult> {
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
| undefined;
|
|
703
|
-
if (transparency == null) {
|
|
947
|
+
requireObject(certificate, "certificate");
|
|
948
|
+
if (certificate.transparency == null) {
|
|
704
949
|
return "NOT_AVAILABLE" as TransparencyResult;
|
|
705
950
|
}
|
|
951
|
+
const transparency = objectAt(certificate, "transparency", "transparency");
|
|
706
952
|
|
|
707
|
-
const sth = transparency
|
|
708
|
-
const issuer = certificate
|
|
709
|
-
const keyId = issuer.key_id
|
|
953
|
+
const sth = objectAt(transparency, "signed_tree_head", "transparency.signed_tree_head");
|
|
954
|
+
const issuer = objectAt(certificate, "issuer", "issuer");
|
|
955
|
+
const keyId = stringAt(issuer, "key_id", "issuer.key_id");
|
|
710
956
|
|
|
711
957
|
const pki = publicKeys.get(keyId);
|
|
712
958
|
if (pki === undefined) throw new VerificationError(`unknown issuer key: ${keyId}`);
|
|
@@ -715,7 +961,7 @@ export async function verifyTransparency(
|
|
|
715
961
|
|
|
716
962
|
// 1. Tree head signature
|
|
717
963
|
const headPayload = buildTreeHeadPayload(sth);
|
|
718
|
-
const headSig =
|
|
964
|
+
const headSig = decodeFixed(sth.signature, "signed_tree_head.signature", 64);
|
|
719
965
|
if (!(await crypto.ed25519Verify(pki.keyBytes, headPayload, headSig))) {
|
|
720
966
|
throw new VerificationError("tree head signature is invalid");
|
|
721
967
|
}
|
|
@@ -731,16 +977,16 @@ export async function verifyTransparency(
|
|
|
731
977
|
const issuanceData = issuanceBytes(certificate);
|
|
732
978
|
const leafPayload = buildLogLeafPayload(
|
|
733
979
|
transparency.entry_type,
|
|
734
|
-
certificate
|
|
980
|
+
stringAt(certificate, "certificate_id", "certificate_id"),
|
|
735
981
|
await crypto.sha256(issuanceData),
|
|
736
982
|
transparency.appended_at,
|
|
737
983
|
);
|
|
738
984
|
const leaf = await hashLeaf(crypto, leafPayload);
|
|
739
985
|
|
|
740
|
-
const proofHashes = (
|
|
741
|
-
(transparency.inclusion_proof
|
|
742
|
-
)
|
|
743
|
-
const root =
|
|
986
|
+
const proofHashes = arrayAt(transparency, "inclusion_proof", "transparency.inclusion_proof").map(
|
|
987
|
+
(h, i) => decodeFixed(h, `transparency.inclusion_proof[${i}]`, 32),
|
|
988
|
+
);
|
|
989
|
+
const root = decodeFixed(sth.root_hash, "signed_tree_head.root_hash", 32);
|
|
744
990
|
// `?? 0` is not a convenience: encoding/json leaves 0 in Go's uint64 fields
|
|
745
991
|
// for an explicit null and for an absent key rather than failing, so refusing
|
|
746
992
|
// either would reject documents the reference accepts — the same class of
|
|
@@ -811,7 +1057,10 @@ export function issuanceBytes(certificate: Cert): Uint8Array {
|
|
|
811
1057
|
// RFC 8785 Canonical JSON
|
|
812
1058
|
// ---------------------------------------------------------------------------
|
|
813
1059
|
|
|
814
|
-
|
|
1060
|
+
/** Exported for tests/verify.test.ts, which keeps a second implementation of
|
|
1061
|
+
* this and must be able to prove the two agree. Not re-exported from index.ts,
|
|
1062
|
+
* so it stays off the published API. */
|
|
1063
|
+
export function canonicalJson(obj: unknown): Uint8Array {
|
|
815
1064
|
return textToBytes(canonicalStringify(obj));
|
|
816
1065
|
}
|
|
817
1066
|
|
|
@@ -835,48 +1084,96 @@ function canonicalStringify(val: unknown): string {
|
|
|
835
1084
|
}
|
|
836
1085
|
|
|
837
1086
|
// ---------------------------------------------------------------------------
|
|
838
|
-
//
|
|
1087
|
+
// Reading the document
|
|
839
1088
|
// ---------------------------------------------------------------------------
|
|
1089
|
+
//
|
|
1090
|
+
// A certificate or status statement handed to this verifier is untrusted JSON.
|
|
1091
|
+
// A key that is missing, a value of the wrong JSON type or bytes that will not
|
|
1092
|
+
// decode is a fact about the DOCUMENT, so every field is read through one of
|
|
1093
|
+
// these and refused with a VerificationError naming it. Casting and reading
|
|
1094
|
+
// instead threw TypeError from `undefined.x` and a DOMException from atob,
|
|
1095
|
+
// which a caller cannot tell apart from a bug in this library (BL-4-001-B).
|
|
1096
|
+
//
|
|
1097
|
+
// Absent and null are the same thing, as they are to Go's encoding/json.
|
|
840
1098
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
if (Array.isArray(value)) {
|
|
848
|
-
return bytesToHex(new Uint8Array(value as number[]));
|
|
849
|
-
}
|
|
850
|
-
const str = value as string;
|
|
851
|
-
if (/^[0-9a-fA-F]+$/.test(str) && str.length % 2 === 0) {
|
|
852
|
-
return str.toLowerCase();
|
|
1099
|
+
type Doc = Record<string, unknown>;
|
|
1100
|
+
|
|
1101
|
+
function requireObject(value: unknown, path: string): Doc {
|
|
1102
|
+
if (value === undefined || value === null) throw new VerificationError(`${path} is missing`);
|
|
1103
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
1104
|
+
throw new VerificationError(`${path} is not an object`);
|
|
853
1105
|
}
|
|
854
|
-
return
|
|
1106
|
+
return value as Doc;
|
|
855
1107
|
}
|
|
856
1108
|
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
const
|
|
863
|
-
if (
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
}
|
|
867
|
-
return base64ToBytes(str);
|
|
1109
|
+
function objectAt(parent: Doc, key: string, path: string): Doc {
|
|
1110
|
+
return requireObject(parent[key], path);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
function stringAt(parent: Doc, key: string, path: string): string {
|
|
1114
|
+
const value = parent[key];
|
|
1115
|
+
if (value === undefined || value === null) throw new VerificationError(`${path} is missing`);
|
|
1116
|
+
if (typeof value !== "string") throw new VerificationError(`${path} is not a string`);
|
|
1117
|
+
return value;
|
|
868
1118
|
}
|
|
869
1119
|
|
|
870
|
-
|
|
871
|
-
|
|
1120
|
+
function optionalStringAt(parent: Doc, key: string, path: string): string | undefined {
|
|
1121
|
+
const value = parent[key];
|
|
1122
|
+
if (value === undefined || value === null) return undefined;
|
|
1123
|
+
if (typeof value !== "string") throw new VerificationError(`${path} is not a string`);
|
|
1124
|
+
return value;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
/**
|
|
1128
|
+
* An array field. Absent and null read as empty: Go leaves a nil slice.
|
|
1129
|
+
*
|
|
1130
|
+
* Returned dense. JSON cannot express a hole, but a caller building the object
|
|
1131
|
+
* in JavaScript can, and `.map` skips holes — so one reached the Merkle code as
|
|
1132
|
+
* `undefined` and threw a TypeError. Array.from reads a hole as undefined,
|
|
1133
|
+
* which the element's own reader then refuses as missing.
|
|
1134
|
+
*/
|
|
1135
|
+
function arrayAt(parent: Doc, key: string, path: string): unknown[] {
|
|
1136
|
+
const value = parent[key];
|
|
1137
|
+
if (value === undefined || value === null) return [];
|
|
1138
|
+
if (!Array.isArray(value)) throw new VerificationError(`${path} is not an array`);
|
|
1139
|
+
return Array.from(value);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
const HEX_TEXT_RE = /^(?:[0-9a-fA-F]{2})*$/;
|
|
1143
|
+
const BASE64_TEXT_RE = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
1144
|
+
|
|
1145
|
+
/**
|
|
1146
|
+
* Decode a fixed-size byte field — a hash, a key, a signature.
|
|
1147
|
+
*
|
|
1148
|
+
* Go marshals `[N]byte` as an array of numbers; hand-written documents carry
|
|
1149
|
+
* hex or standard base64. A well-formed value of the right size is never
|
|
1150
|
+
* ambiguous between the two text forms, because base64 of 32 or 64 bytes ends
|
|
1151
|
+
* in padding hex cannot contain.
|
|
1152
|
+
*
|
|
1153
|
+
* Everything else is refused here, by name: a number, an array element that is
|
|
1154
|
+
* not a byte, text that is neither encoding, the wrong length. Decoding it
|
|
1155
|
+
* leniently instead wrapped 300 to 44 inside a Uint8Array, produced bytes no
|
|
1156
|
+
* signature covers and blamed the signature — or threw from inside atob.
|
|
1157
|
+
*/
|
|
1158
|
+
function decodeFixed(value: unknown, path: string, length: number): Uint8Array {
|
|
1159
|
+
if (value === undefined || value === null) throw new VerificationError(`${path} is missing`);
|
|
1160
|
+
let raw: Uint8Array;
|
|
872
1161
|
if (Array.isArray(value)) {
|
|
873
|
-
|
|
1162
|
+
if (!value.every((b) => Number.isInteger(b) && b >= 0 && b <= 255)) {
|
|
1163
|
+
throw new VerificationError(`${path} is not an array of bytes`);
|
|
1164
|
+
}
|
|
1165
|
+
raw = new Uint8Array(value as number[]);
|
|
1166
|
+
} else if (typeof value === "string") {
|
|
1167
|
+
if (HEX_TEXT_RE.test(value)) raw = hexToBytes(value);
|
|
1168
|
+
else if (BASE64_TEXT_RE.test(value)) raw = base64ToBytes(value);
|
|
1169
|
+
else throw new VerificationError(`${path} is neither hex nor base64`);
|
|
1170
|
+
} else {
|
|
1171
|
+
throw new VerificationError(`${path} is not a byte string`);
|
|
874
1172
|
}
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
return hexToBytes(str);
|
|
1173
|
+
if (raw.length !== length) {
|
|
1174
|
+
throw new VerificationError(`${path} is ${raw.length} bytes, expected ${length}`);
|
|
878
1175
|
}
|
|
879
|
-
return
|
|
1176
|
+
return raw;
|
|
880
1177
|
}
|
|
881
1178
|
|
|
882
1179
|
/** Go's zero time.Time — what encoding/json leaves in a non-pointer time.Time
|
|
@@ -909,13 +1206,16 @@ const RFC3339_RE =
|
|
|
909
1206
|
* Exported from this module so the timestamp_vectors.json suite can diff it
|
|
910
1207
|
* against Go directly. It is deliberately not re-exported from index.ts, so it
|
|
911
1208
|
* is not part of the published package surface.
|
|
1209
|
+
*
|
|
1210
|
+
* `field` names the value in a refusal. It defaults to the word these messages
|
|
1211
|
+
* always used, for callers normalizing a time that is not a field of a document.
|
|
912
1212
|
*/
|
|
913
|
-
export function formatTimestamp(ts: unknown): string {
|
|
1213
|
+
export function formatTimestamp(ts: unknown, field = "timestamp"): string {
|
|
914
1214
|
if (ts === null || ts === undefined) return ZERO_INSTANT;
|
|
915
1215
|
if (typeof ts !== "string") {
|
|
916
|
-
throw new VerificationError(
|
|
1216
|
+
throw new VerificationError(`${field} is not a string: ${JSON.stringify(ts)}`);
|
|
917
1217
|
}
|
|
918
|
-
return formatEpochSeconds(parseRfc3339(ts));
|
|
1218
|
+
return formatEpochSeconds(parseRfc3339(ts, field));
|
|
919
1219
|
}
|
|
920
1220
|
|
|
921
1221
|
/** Parse a strict RFC 3339 timestamp to seconds since 1970-01-01T00:00:00Z.
|
|
@@ -927,19 +1227,19 @@ export function formatTimestamp(ts: unknown): string {
|
|
|
927
1227
|
* `+24:00` and `+00:60` parse, while `+25:00` and `+00:61` do not. Sub-second
|
|
928
1228
|
* digits are dropped, matching Format's truncation.
|
|
929
1229
|
*/
|
|
930
|
-
function parseRfc3339(ts: string): number {
|
|
1230
|
+
function parseRfc3339(ts: string, field = "timestamp"): number {
|
|
931
1231
|
const m = RFC3339_RE.exec(ts);
|
|
932
|
-
if (m === null) throw new VerificationError(
|
|
1232
|
+
if (m === null) throw new VerificationError(`${field} is not RFC 3339: ${ts}`);
|
|
933
1233
|
const [year, month, day, hour, minute, second] = m
|
|
934
1234
|
.slice(1, 7)
|
|
935
1235
|
.map((v) => Number.parseInt(v!, 10)) as [number, number, number, number, number, number];
|
|
936
1236
|
|
|
937
|
-
if (month < 1 || month > 12) throw new VerificationError(
|
|
1237
|
+
if (month < 1 || month > 12) throw new VerificationError(`${field} month out of range: ${ts}`);
|
|
938
1238
|
if (day < 1 || day > daysInMonth(year, month)) {
|
|
939
|
-
throw new VerificationError(
|
|
1239
|
+
throw new VerificationError(`${field} day out of range: ${ts}`);
|
|
940
1240
|
}
|
|
941
1241
|
if (hour > 23 || minute > 59 || second > 59) {
|
|
942
|
-
throw new VerificationError(
|
|
1242
|
+
throw new VerificationError(`${field} time of day out of range: ${ts}`);
|
|
943
1243
|
}
|
|
944
1244
|
|
|
945
1245
|
let offset = 0;
|
|
@@ -947,8 +1247,8 @@ function parseRfc3339(ts: string): number {
|
|
|
947
1247
|
if (sign !== undefined) {
|
|
948
1248
|
const offHour = Number.parseInt(m[8]!, 10);
|
|
949
1249
|
const offMin = Number.parseInt(m[9]!, 10);
|
|
950
|
-
if (offHour > 24) throw new VerificationError(
|
|
951
|
-
if (offMin > 60) throw new VerificationError(
|
|
1250
|
+
if (offHour > 24) throw new VerificationError(`${field} zone offset hour out of range: ${ts}`);
|
|
1251
|
+
if (offMin > 60) throw new VerificationError(`${field} zone offset minute out of range: ${ts}`);
|
|
952
1252
|
offset = (offHour * 3600 + offMin * 60) * (sign === "-" ? -1 : 1);
|
|
953
1253
|
}
|
|
954
1254
|
|
|
@@ -1048,17 +1348,18 @@ function buildAttestationPayload(
|
|
|
1048
1348
|
// payload and call every genuine v8 record a forgery.
|
|
1049
1349
|
const coversMeasured = signatureCoversMeasuredTransport(certFormatVersion);
|
|
1050
1350
|
const coversRecoverable = signatureCoversRecoverableState(certFormatVersion);
|
|
1051
|
-
const systems = (att.systems as Record<string, unknown>[]).map((s) => {
|
|
1351
|
+
const systems = (att.systems as Record<string, unknown>[]).map((s, i) => {
|
|
1052
1352
|
const sys: Record<string, unknown> = {
|
|
1053
1353
|
canonical_version: (s.canonical_version as string | null) ?? null,
|
|
1054
1354
|
connector_type: s.connector_type as string,
|
|
1055
1355
|
hash_scope: s.hash_scope as string,
|
|
1056
|
-
merkle_root:
|
|
1057
|
-
|
|
1058
|
-
|
|
1356
|
+
merkle_root:
|
|
1357
|
+
s.merkle_root == null
|
|
1358
|
+
? null
|
|
1359
|
+
: bytesToHex(decodeFixed(s.merkle_root, `systems[${i}].merkle_root`, 32)),
|
|
1059
1360
|
// The system's own observation time, not the envelope's.
|
|
1060
|
-
observed_at: formatTimestamp(s.observed_at),
|
|
1061
|
-
query_hash:
|
|
1361
|
+
observed_at: formatTimestamp(s.observed_at, `systems[${i}].attested_at`),
|
|
1362
|
+
query_hash: bytesToHex(decodeFixed(s.query_hash, `systems[${i}].query_hash`, 32)),
|
|
1062
1363
|
record_count: s.record_count as number,
|
|
1063
1364
|
system_id: s.system_id as string,
|
|
1064
1365
|
system_name: s.system_name as string,
|
|
@@ -1092,7 +1393,7 @@ function buildAttestationPayload(
|
|
|
1092
1393
|
attested_at: attestedAt,
|
|
1093
1394
|
payload_type: attestationPayloadType(certFormatVersion),
|
|
1094
1395
|
proof_mode: att.proof_mode as string,
|
|
1095
|
-
subject_hash:
|
|
1396
|
+
subject_hash: bytesToHex(decodeFixed(subject.identifier_hash, "subject.identifier_hash", 32)),
|
|
1096
1397
|
systems,
|
|
1097
1398
|
};
|
|
1098
1399
|
return canonicalJson(payload);
|
|
@@ -1158,9 +1459,9 @@ function attestationPayloadType(version: string): string {
|
|
|
1158
1459
|
}
|
|
1159
1460
|
|
|
1160
1461
|
function buildCertificatePayload(cert: Record<string, unknown>): Uint8Array {
|
|
1161
|
-
const att = cert
|
|
1162
|
-
const issuer = cert
|
|
1163
|
-
const subject = cert
|
|
1462
|
+
const att = objectAt(cert, "attestation", "attestation");
|
|
1463
|
+
const issuer = objectAt(cert, "issuer", "issuer");
|
|
1464
|
+
const subject = objectAt(cert, "subject", "subject");
|
|
1164
1465
|
|
|
1165
1466
|
// One list, keyed by system_id (ADR-016 §2). v2 signed an attestation list
|
|
1166
1467
|
// and a verification list joined only on the human-editable system_name,
|
|
@@ -1169,19 +1470,27 @@ function buildCertificatePayload(cert: Record<string, unknown>): Uint8Array {
|
|
|
1169
1470
|
const coversMeasured = signatureCoversMeasuredTransport(version);
|
|
1170
1471
|
const coversRecoverable = signatureCoversRecoverableState(version);
|
|
1171
1472
|
const coversAuthorization = signatureCoversAuthorization(version);
|
|
1172
|
-
const systems = (
|
|
1473
|
+
const systems = arrayAt(cert, "systems", "systems").map((entry, i) => {
|
|
1474
|
+
const where = `systems[${i}]`;
|
|
1475
|
+
const s = requireObject(entry, where);
|
|
1173
1476
|
const sys: Record<string, unknown> = {
|
|
1174
|
-
attested_at: formatTimestamp(s.attested_at),
|
|
1175
|
-
attested_count: s.attested_count
|
|
1176
|
-
canonical_version:
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1477
|
+
attested_at: formatTimestamp(s.attested_at, `${where}.attested_at`),
|
|
1478
|
+
attested_count: requireUint(s.attested_count, `${where}.attested_count`),
|
|
1479
|
+
canonical_version:
|
|
1480
|
+
optionalStringAt(s, "canonical_version", `${where}.canonical_version`) ?? null,
|
|
1481
|
+
connector_type: stringAt(s, "connector_type", `${where}.connector_type`),
|
|
1482
|
+
hash_scope: stringAt(s, "hash_scope", `${where}.hash_scope`),
|
|
1483
|
+
// Absent is a system with no Merkle root; anything present must be one.
|
|
1484
|
+
// Go's *[32]byte is nil or 32 bytes, never "" or [].
|
|
1485
|
+
merkle_root:
|
|
1486
|
+
s.merkle_root == null
|
|
1487
|
+
? null
|
|
1488
|
+
: bytesToHex(decodeFixed(s.merkle_root, `${where}.merkle_root`, 32)),
|
|
1489
|
+
query_hash: bytesToHex(decodeFixed(s.query_hash, `${where}.query_hash`, 32)),
|
|
1490
|
+
system_id: stringAt(s, "system_id", `${where}.system_id`),
|
|
1491
|
+
system_name: stringAt(s, "system_name", `${where}.system_name`),
|
|
1492
|
+
verified_at: formatTimestamp(s.verified_at, `${where}.verified_at`),
|
|
1493
|
+
verified_count: requireUint(s.verified_count, `${where}.verified_count`),
|
|
1185
1494
|
};
|
|
1186
1495
|
if (coversMeasured) {
|
|
1187
1496
|
sys.read_only_enforcement = requireMeasured(
|
|
@@ -1216,9 +1525,11 @@ function buildCertificatePayload(cert: Record<string, unknown>): Uint8Array {
|
|
|
1216
1525
|
// The attestation block carries no system list of its own: the merged list
|
|
1217
1526
|
// above is a superset of it. verification_signature is gone entirely.
|
|
1218
1527
|
const attObj = {
|
|
1219
|
-
attestation_signature:
|
|
1220
|
-
|
|
1221
|
-
|
|
1528
|
+
attestation_signature: bytesToHex(
|
|
1529
|
+
decodeFixed(att.attestation_signature, "attestation.attestation_signature", 64),
|
|
1530
|
+
),
|
|
1531
|
+
attested_at: formatTimestamp(att.attested_at, "attestation.attested_at"),
|
|
1532
|
+
proof_mode: stringAt(att, "proof_mode", "attestation.proof_mode"),
|
|
1222
1533
|
};
|
|
1223
1534
|
|
|
1224
1535
|
// Fields added after v3 are gated on the certificate's OWN version. A v3
|
|
@@ -1232,9 +1543,9 @@ function buildCertificatePayload(cert: Record<string, unknown>): Uint8Array {
|
|
|
1232
1543
|
const isV4Plus = formatAtLeast(version, FORMAT_VERSION_V4);
|
|
1233
1544
|
|
|
1234
1545
|
const issuerObj: Record<string, unknown> = {
|
|
1235
|
-
key_id: issuer.key_id
|
|
1236
|
-
name: issuer.name
|
|
1237
|
-
public_key:
|
|
1546
|
+
key_id: stringAt(issuer, "key_id", "issuer.key_id"),
|
|
1547
|
+
name: stringAt(issuer, "name", "issuer.name"),
|
|
1548
|
+
public_key: bytesToHex(decodeFixed(issuer.public_key, "issuer.public_key", 32)),
|
|
1238
1549
|
};
|
|
1239
1550
|
// v7 signs the algorithm, and does so unconditionally: unlike legal_entity
|
|
1240
1551
|
// and enclave_pcr0, "which scheme signed this" is never unknown to a signer,
|
|
@@ -1247,26 +1558,31 @@ function buildCertificatePayload(cert: Record<string, unknown>): Uint8Array {
|
|
|
1247
1558
|
version,
|
|
1248
1559
|
);
|
|
1249
1560
|
}
|
|
1250
|
-
|
|
1251
|
-
|
|
1561
|
+
// A non-string here used to be dropped silently, so a junk value added to a
|
|
1562
|
+
// record that never carried one still verified. Python refused it; so does
|
|
1563
|
+
// Go, at parse. Now this does too.
|
|
1564
|
+
if (isV4Plus) {
|
|
1565
|
+
const legalEntity = optionalStringAt(issuer, "legal_entity", "issuer.legal_entity");
|
|
1566
|
+
if (legalEntity) issuerObj.legal_entity = legalEntity;
|
|
1252
1567
|
}
|
|
1253
1568
|
// Signed from v5, and omitted when absent or empty: a build with no
|
|
1254
1569
|
// measurement signs none, and "" is a value a reader could mistake for one.
|
|
1255
|
-
if (
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
issuer.enclave_pcr0 !== ""
|
|
1259
|
-
) {
|
|
1260
|
-
issuerObj.enclave_pcr0 = issuer.enclave_pcr0;
|
|
1570
|
+
if (signatureCoversEnclavePcr0(version)) {
|
|
1571
|
+
const pcr0 = optionalStringAt(issuer, "enclave_pcr0", "issuer.enclave_pcr0");
|
|
1572
|
+
if (pcr0) issuerObj.enclave_pcr0 = pcr0;
|
|
1261
1573
|
}
|
|
1262
1574
|
|
|
1263
1575
|
// v5 drops identifier_type_hint: signed, but always the constant "custom",
|
|
1264
1576
|
// so it was never evidence. v3 and v4 keep it or they stop verifying.
|
|
1265
1577
|
const subjectObj: Record<string, unknown> = {
|
|
1266
|
-
identifier_hash:
|
|
1578
|
+
identifier_hash: bytesToHex(decodeFixed(subject.identifier_hash, "subject.identifier_hash", 32)),
|
|
1267
1579
|
};
|
|
1268
1580
|
if (!isV5Plus) {
|
|
1269
|
-
subjectObj.identifier_type_hint =
|
|
1581
|
+
subjectObj.identifier_type_hint = stringAt(
|
|
1582
|
+
subject,
|
|
1583
|
+
"identifier_type_hint",
|
|
1584
|
+
"subject.identifier_type_hint",
|
|
1585
|
+
);
|
|
1270
1586
|
}
|
|
1271
1587
|
|
|
1272
1588
|
// status and revocation are deliberately absent (ADR-016 §3): a signature
|
|
@@ -1274,20 +1590,20 @@ function buildCertificatePayload(cert: Record<string, unknown>): Uint8Array {
|
|
|
1274
1590
|
// travels as a separate short-lived signed status statement.
|
|
1275
1591
|
const payload: Record<string, unknown> = {
|
|
1276
1592
|
attestation: attObj,
|
|
1277
|
-
attestation_id: cert
|
|
1278
|
-
certificate_format_version:
|
|
1279
|
-
certificate_id: cert
|
|
1280
|
-
issued_at: formatTimestamp(cert.issued_at),
|
|
1593
|
+
attestation_id: stringAt(cert, "attestation_id", "attestation_id"),
|
|
1594
|
+
certificate_format_version: version,
|
|
1595
|
+
certificate_id: stringAt(cert, "certificate_id", "certificate_id"),
|
|
1596
|
+
issued_at: formatTimestamp(cert.issued_at, "issued_at"),
|
|
1281
1597
|
issuer: issuerObj,
|
|
1282
1598
|
payload_type: certificatePayloadType(version),
|
|
1283
1599
|
subject: subjectObj,
|
|
1284
1600
|
systems,
|
|
1285
1601
|
};
|
|
1286
1602
|
if (isV4Plus && cert.scope != null) {
|
|
1287
|
-
const scope = cert
|
|
1603
|
+
const scope = objectAt(cert, "scope", "scope");
|
|
1288
1604
|
payload.scope = {
|
|
1289
|
-
text_sha256:
|
|
1290
|
-
version: scope.version
|
|
1605
|
+
text_sha256: bytesToHex(decodeFixed(scope.text_sha256, "scope.text_sha256", 32)),
|
|
1606
|
+
version: stringAt(scope, "version", "scope.version"),
|
|
1291
1607
|
};
|
|
1292
1608
|
}
|
|
1293
1609
|
return canonicalJson(payload);
|
|
@@ -1304,7 +1620,7 @@ function buildLogLeafPayload(
|
|
|
1304
1620
|
throw new VerificationError(`invalid log entry_type: ${String(entryType)}`);
|
|
1305
1621
|
}
|
|
1306
1622
|
const payload = {
|
|
1307
|
-
appended_at: formatTimestamp(appendedAt),
|
|
1623
|
+
appended_at: formatTimestamp(appendedAt, "transparency.appended_at"),
|
|
1308
1624
|
certificate_hash: bytesToHex(certificateHash),
|
|
1309
1625
|
certificate_id: certificateId,
|
|
1310
1626
|
entry_type: entryType,
|
|
@@ -1355,13 +1671,13 @@ function attestationSystems(cert: Record<string, unknown>): Record<string, unkno
|
|
|
1355
1671
|
* between two modules of this package, not published surface.
|
|
1356
1672
|
*/
|
|
1357
1673
|
export function buildTreeHeadPayload(head: Record<string, unknown>): Uint8Array {
|
|
1358
|
-
const logId = head.log_id;
|
|
1359
|
-
const named =
|
|
1674
|
+
const logId = optionalStringAt(head, "log_id", "signed_tree_head.log_id");
|
|
1675
|
+
const named = logId !== undefined && logId !== "";
|
|
1360
1676
|
const payload: Record<string, unknown> = {
|
|
1361
1677
|
payload_type: named ? PAYLOAD_TYPE_TREE_HEAD_V7 : PAYLOAD_TYPE_TREE_HEAD,
|
|
1362
|
-
root_hash:
|
|
1363
|
-
timestamp: formatTimestamp(head.timestamp),
|
|
1364
|
-
tree_size: head.tree_size
|
|
1678
|
+
root_hash: bytesToHex(decodeFixed(head.root_hash, "signed_tree_head.root_hash", 32)),
|
|
1679
|
+
timestamp: formatTimestamp(head.timestamp, "signed_tree_head.timestamp"),
|
|
1680
|
+
tree_size: requireUint(head.tree_size ?? 0, "signed_tree_head.tree_size"),
|
|
1365
1681
|
};
|
|
1366
1682
|
if (named) payload.log_id = logId;
|
|
1367
1683
|
return canonicalJson(payload);
|
|
@@ -1532,21 +1848,24 @@ export function buildCertificateStatusPayload(
|
|
|
1532
1848
|
): Uint8Array {
|
|
1533
1849
|
const payload: Record<string, unknown> = {
|
|
1534
1850
|
payload_type: PAYLOAD_TYPE_CERTIFICATE_STATUS,
|
|
1535
|
-
certificate_id: stmt.certificate_id
|
|
1536
|
-
statement_expires_at: formatTimestamp(stmt.statement_expires_at),
|
|
1537
|
-
statement_issued_at: formatTimestamp(stmt.statement_issued_at),
|
|
1538
|
-
status: stmt.status
|
|
1539
|
-
sth_root_hash:
|
|
1540
|
-
sth_tree_size: stmt.sth_tree_size
|
|
1851
|
+
certificate_id: stringAt(stmt, "certificate_id", "status.certificate_id"),
|
|
1852
|
+
statement_expires_at: formatTimestamp(stmt.statement_expires_at, "status.statement_expires_at"),
|
|
1853
|
+
statement_issued_at: formatTimestamp(stmt.statement_issued_at, "status.statement_issued_at"),
|
|
1854
|
+
status: stringAt(stmt, "status", "status.status"),
|
|
1855
|
+
sth_root_hash: bytesToHex(decodeFixed(stmt.sth_root_hash, "status.sth_root_hash", 32)),
|
|
1856
|
+
sth_tree_size: requireUint(stmt.sth_tree_size ?? 0, "status.sth_tree_size"),
|
|
1541
1857
|
};
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1858
|
+
const replacement = optionalStringAt(
|
|
1859
|
+
stmt,
|
|
1860
|
+
"replacement_certificate_id",
|
|
1861
|
+
"status.replacement_certificate_id",
|
|
1862
|
+
);
|
|
1863
|
+
if (replacement !== undefined) payload.replacement_certificate_id = replacement;
|
|
1545
1864
|
if (stmt.revocation_log_index != null) {
|
|
1546
|
-
payload.revocation_log_index = stmt.revocation_log_index
|
|
1865
|
+
payload.revocation_log_index = requireUint(stmt.revocation_log_index, "status.revocation_log_index");
|
|
1547
1866
|
}
|
|
1548
1867
|
if (stmt.revoked_at != null) {
|
|
1549
|
-
payload.revoked_at = formatTimestamp(stmt.revoked_at);
|
|
1868
|
+
payload.revoked_at = formatTimestamp(stmt.revoked_at, "status.revoked_at");
|
|
1550
1869
|
}
|
|
1551
1870
|
return canonicalJson(payload);
|
|
1552
1871
|
}
|
|
@@ -1579,7 +1898,7 @@ export function buildKeyListPayload(doc: Record<string, unknown>): Uint8Array {
|
|
|
1579
1898
|
keys: entries,
|
|
1580
1899
|
statement_expires_at: formatTimestamp(doc.statement_expires_at),
|
|
1581
1900
|
statement_issued_at: formatTimestamp(doc.statement_issued_at),
|
|
1582
|
-
sth_root_hash:
|
|
1901
|
+
sth_root_hash: bytesToHex(decodeFixed(doc.sth_root_hash, "sth_root_hash", 32)),
|
|
1583
1902
|
sth_tree_size: doc.sth_tree_size as number,
|
|
1584
1903
|
};
|
|
1585
1904
|
return canonicalJson(payload);
|
|
@@ -1607,6 +1926,8 @@ export const VALID_REVOCATION_UNKNOWN = "VALID_REVOCATION_UNKNOWN";
|
|
|
1607
1926
|
*
|
|
1608
1927
|
* The third row is the point: `verifyCertificate` answers VALID there, which
|
|
1609
1928
|
* reads as "not revoked" and is not something it checked.
|
|
1929
|
+
*
|
|
1930
|
+
* `options` is passed to {@link verifyCertificate} unchanged.
|
|
1610
1931
|
*/
|
|
1611
1932
|
export async function verifyCertificateWithStatus(
|
|
1612
1933
|
crypto: CryptoOps,
|
|
@@ -1614,18 +1935,49 @@ export async function verifyCertificateWithStatus(
|
|
|
1614
1935
|
publicKeys: Map<string, PublicKeyInfo>,
|
|
1615
1936
|
status?: Record<string, unknown> | null,
|
|
1616
1937
|
now?: Date,
|
|
1938
|
+
options?: VerifyOptions,
|
|
1617
1939
|
): Promise<string> {
|
|
1618
1940
|
// A refusal throws out of verifyCertificate, so `base` is VALID or one of the
|
|
1619
1941
|
// two soft verdicts. Returning a soft verdict here skipped every check below
|
|
1620
1942
|
// it: a revoked certificate signed by a compromised-later key answered
|
|
1621
1943
|
// VALID_KEY_COMPROMISED_LATER and its statement was never authenticated. The
|
|
1622
1944
|
// verdict is held instead, and resolved against what the statement says.
|
|
1623
|
-
const base = await verifyCertificate(crypto, certificate, publicKeys);
|
|
1945
|
+
const base = await verifyCertificate(crypto, certificate, publicKeys, options);
|
|
1624
1946
|
if (status == null) {
|
|
1625
1947
|
return resolveWithBase(base, VALID_REVOCATION_UNKNOWN);
|
|
1626
1948
|
}
|
|
1627
1949
|
|
|
1628
|
-
|
|
1950
|
+
return resolveWithBase(
|
|
1951
|
+
base,
|
|
1952
|
+
await verifyStatusStatement(
|
|
1953
|
+
crypto, status, publicKeys, (certificate as Record<string, unknown>).certificate_id, now,
|
|
1954
|
+
),
|
|
1955
|
+
);
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
/**
|
|
1959
|
+
* The statement half of {@link verifyCertificateWithStatus}: authenticate a
|
|
1960
|
+
* status statement about `certificateId` against the published keys, and read
|
|
1961
|
+
* it at `now`. The /verify page's QR lookup has a record id and no record, and
|
|
1962
|
+
* uses this so it shows what a signed, fresh statement says, never what the
|
|
1963
|
+
* status endpoint claims.
|
|
1964
|
+
*
|
|
1965
|
+
* | input | result |
|
|
1966
|
+
* |---|---|
|
|
1967
|
+
* | fresh, REVOKED | throws VerificationError, code CERTIFICATE_REVOKED |
|
|
1968
|
+
* | fresh, ACTIVE | the statement key's verdict: `"VALID"`, or a soft one |
|
|
1969
|
+
* | stale, or a status this SDK does not know | `"VALID_REVOCATION_UNKNOWN"` |
|
|
1970
|
+
* | unknown or unusable key, bad signature, another certificate | throws VerificationError |
|
|
1971
|
+
*/
|
|
1972
|
+
export async function verifyStatusStatement(
|
|
1973
|
+
crypto: CryptoOps,
|
|
1974
|
+
status: Record<string, unknown>,
|
|
1975
|
+
publicKeys: Map<string, PublicKeyInfo>,
|
|
1976
|
+
certificateId: unknown,
|
|
1977
|
+
now?: Date,
|
|
1978
|
+
): Promise<string> {
|
|
1979
|
+
requireObject(status, "status");
|
|
1980
|
+
const keyId = optionalStringAt(status, "key_id", "status.key_id");
|
|
1629
1981
|
const info = keyId ? publicKeys.get(keyId) : undefined;
|
|
1630
1982
|
if (!info) {
|
|
1631
1983
|
throw new VerificationError(`status statement signed by unknown key: ${keyId}`);
|
|
@@ -1653,10 +2005,10 @@ export async function verifyCertificateWithStatus(
|
|
|
1653
2005
|
);
|
|
1654
2006
|
|
|
1655
2007
|
const payload = buildCertificateStatusPayload(status);
|
|
1656
|
-
//
|
|
2008
|
+
// decodeFixed, not hexToBytes: signatures arrive as hex, base64 or a byte
|
|
1657
2009
|
// array depending on the producer, and every other signature on this path
|
|
1658
2010
|
// goes through the same decoder.
|
|
1659
|
-
const sig =
|
|
2011
|
+
const sig = decodeFixed(status.signature, "status.signature", 64);
|
|
1660
2012
|
if (!(await crypto.ed25519Verify(info.keyBytes, payload, sig))) {
|
|
1661
2013
|
throw new VerificationError("status statement signature is invalid");
|
|
1662
2014
|
}
|
|
@@ -1674,8 +2026,7 @@ export async function verifyCertificateWithStatus(
|
|
|
1674
2026
|
// Every signature still verified; the binding was the only forged part.
|
|
1675
2027
|
// core/verify.go never had the fallback, so Go said STATUS_STATEMENT_MISMATCH
|
|
1676
2028
|
// while both SDKs and the browser bundle said VALID.
|
|
1677
|
-
|
|
1678
|
-
if (status.certificate_id !== certId) {
|
|
2029
|
+
if (status.certificate_id !== certificateId) {
|
|
1679
2030
|
throw new VerificationError("status statement is about a different certificate");
|
|
1680
2031
|
}
|
|
1681
2032
|
|
|
@@ -1686,7 +2037,7 @@ export async function verifyCertificateWithStatus(
|
|
|
1686
2037
|
if (at < issued || at >= expires) {
|
|
1687
2038
|
// Stale is not a weaker answer, it is no answer — including for a REVOKED
|
|
1688
2039
|
// statement, which must never decay into VALID.
|
|
1689
|
-
return
|
|
2040
|
+
return VALID_REVOCATION_UNKNOWN;
|
|
1690
2041
|
}
|
|
1691
2042
|
|
|
1692
2043
|
if (status.status === "REVOKED") {
|
|
@@ -1702,12 +2053,12 @@ export async function verifyCertificateWithStatus(
|
|
|
1702
2053
|
// case — was reported as revocation CHECKED AND PASSED. Mirrors
|
|
1703
2054
|
// core.ValidCertificateStatusValue.
|
|
1704
2055
|
if (!KNOWN_STATUS_VALUES.has(status.status as string)) {
|
|
1705
|
-
return
|
|
2056
|
+
return VALID_REVOCATION_UNKNOWN;
|
|
1706
2057
|
}
|
|
1707
2058
|
// A soft verdict on the status key is the result, not a footnote — but it is
|
|
1708
2059
|
// reported only once the statement has authenticated and been read, so it can
|
|
1709
2060
|
// neither speak for an unverified statement nor suppress a revocation.
|
|
1710
|
-
return
|
|
2061
|
+
return statusKeyVerdict;
|
|
1711
2062
|
}
|
|
1712
2063
|
|
|
1713
2064
|
/**
|