burnledger 0.8.1 → 0.9.1
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 +144 -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 +175 -54
- package/dist/cjs/client.js.map +1 -1
- package/dist/cjs/customer-keys.d.ts +73 -1
- package/dist/cjs/customer-keys.d.ts.map +1 -1
- package/dist/cjs/customer-keys.js +329 -3
- package/dist/cjs/customer-keys.js.map +1 -1
- package/dist/cjs/enclave-registration.d.ts +165 -0
- package/dist/cjs/enclave-registration.d.ts.map +1 -0
- package/dist/cjs/enclave-registration.js +287 -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 +22 -10
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +75 -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/node-runtime.d.ts +40 -0
- package/dist/cjs/node-runtime.d.ts.map +1 -0
- package/dist/cjs/node-runtime.js +42 -0
- package/dist/cjs/node-runtime.js.map +1 -0
- package/dist/cjs/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 +108 -3
- package/dist/cjs/verify.d.ts.map +1 -1
- package/dist/cjs/verify.js +451 -171
- 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/cjs/webhooks.d.ts +9 -2
- package/dist/cjs/webhooks.d.ts.map +1 -1
- package/dist/cjs/webhooks.js +29 -11
- package/dist/cjs/webhooks.js.map +1 -1
- package/dist/esm/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 +30 -17
- package/dist/esm/cli.d.ts.map +1 -1
- package/dist/esm/cli.js +142 -83
- 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 +175 -21
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/customer-keys.d.ts +73 -1
- package/dist/esm/customer-keys.d.ts.map +1 -1
- package/dist/esm/customer-keys.js +323 -3
- package/dist/esm/customer-keys.js.map +1 -1
- package/dist/esm/enclave-registration.d.ts +165 -0
- package/dist/esm/enclave-registration.d.ts.map +1 -0
- package/dist/esm/enclave-registration.js +277 -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 +22 -10
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +30 -8
- 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/node-runtime.d.ts +40 -0
- package/dist/esm/node-runtime.d.ts.map +1 -0
- package/dist/esm/node-runtime.js +38 -0
- package/dist/esm/node-runtime.js.map +1 -0
- package/dist/esm/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 +108 -3
- package/dist/esm/verify.d.ts.map +1 -1
- package/dist/esm/verify.js +448 -171
- 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/dist/esm/webhooks.d.ts +9 -2
- package/dist/esm/webhooks.d.ts.map +1 -1
- package/dist/esm/webhooks.js +29 -11
- package/dist/esm/webhooks.js.map +1 -1
- package/package.json +1 -1
- package/src/anchor.ts +10 -4
- package/src/cli.ts +145 -78
- package/src/client.ts +241 -25
- package/src/customer-keys.ts +373 -3
- package/src/enclave-registration.ts +402 -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/node-runtime.ts +57 -0
- package/src/status-document.ts +59 -0
- package/src/verify.ts +565 -177
- package/src/web-verifier.ts +36 -3
- package/src/webhooks.ts +28 -11
package/dist/cjs/verify.js
CHANGED
|
@@ -17,13 +17,15 @@
|
|
|
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;
|
|
@@ -34,6 +36,7 @@ exports.verifyConsistency = verifyConsistency;
|
|
|
34
36
|
exports.buildCertificateStatusPayload = buildCertificateStatusPayload;
|
|
35
37
|
exports.buildKeyListPayload = buildKeyListPayload;
|
|
36
38
|
exports.verifyCertificateWithStatus = verifyCertificateWithStatus;
|
|
39
|
+
exports.verifyStatusStatement = verifyStatusStatement;
|
|
37
40
|
const errors_js_1 = require("./errors.js");
|
|
38
41
|
// ---------------------------------------------------------------------------
|
|
39
42
|
// Pure byte helpers (no Buffer, no node:crypto)
|
|
@@ -82,8 +85,9 @@ const PAYLOAD_TYPE_ATTESTATION_V8 = "burnledger.attestation.v8";
|
|
|
82
85
|
const PAYLOAD_TYPE_VERIFICATION_RECORD_V8 = "burnledger.verification_record.v8";
|
|
83
86
|
const FORMAT_VERSION_V8 = "8.0";
|
|
84
87
|
// v9 adds two per-system fields, authorization and customer_key_id: whether the
|
|
85
|
-
// enclave checked this system against a
|
|
86
|
-
//
|
|
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.
|
|
87
91
|
//
|
|
88
92
|
// The attestation gains NO field at v9. Its tag still moves, because the tag
|
|
89
93
|
// follows the record's version and the record's bytes changed — a v9
|
|
@@ -227,17 +231,22 @@ const PAYLOAD_TYPE_LOG_LEAF = "burnledger.log_leaf.v3";
|
|
|
227
231
|
// in the same enclave call that signs the certificate (ADR-016 §2).
|
|
228
232
|
const PAYLOAD_TYPE_CERTIFICATE_STATUS = "burnledger.certificate_status.v3";
|
|
229
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. */
|
|
230
237
|
function hexToBytes(hex) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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);
|
|
237
245
|
}
|
|
238
246
|
return out;
|
|
239
247
|
}
|
|
240
|
-
function unhex(
|
|
248
|
+
function unhex(hex, at) {
|
|
249
|
+
const c = hex.charCodeAt(at);
|
|
241
250
|
// 0-9
|
|
242
251
|
if (c >= 48 && c <= 57)
|
|
243
252
|
return c - 48;
|
|
@@ -247,7 +256,7 @@ function unhex(c) {
|
|
|
247
256
|
// A-F
|
|
248
257
|
if (c >= 65 && c <= 70)
|
|
249
258
|
return c - 55;
|
|
250
|
-
|
|
259
|
+
throw new TypeError(`hex has a non-hex character ${JSON.stringify(hex[at])} at ${at}`);
|
|
251
260
|
}
|
|
252
261
|
function bytesToHex(bytes) {
|
|
253
262
|
let out = "";
|
|
@@ -287,6 +296,13 @@ function textToBytes(s) {
|
|
|
287
296
|
return new TextEncoder().encode(s);
|
|
288
297
|
}
|
|
289
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
|
+
}
|
|
290
306
|
const raw = hexToBytes(hexKey);
|
|
291
307
|
if (raw.length !== 32) {
|
|
292
308
|
throw new errors_js_1.VerificationError(`public key must be 32 bytes, got ${raw.length}`);
|
|
@@ -357,6 +373,18 @@ function evaluateKey(pki, anchor) {
|
|
|
357
373
|
if (pki.keyStatus === "compromised") {
|
|
358
374
|
if (anchor === null || pki.compromisedFrom === undefined)
|
|
359
375
|
return exports.KEY_COMPROMISED;
|
|
376
|
+
// The validity window is tested before the compromise anchor (R03-1). A
|
|
377
|
+
// signature made outside the stated interval was never authorized, whatever
|
|
378
|
+
// the key's later disposition, so an anchor outside the window is
|
|
379
|
+
// KEY_OUTSIDE_VALIDITY even when it predates compromisedFrom. Skipping the
|
|
380
|
+
// window handed the soft, usable VALID_KEY_COMPROMISED_LATER to a signature
|
|
381
|
+
// the window alone already refuses.
|
|
382
|
+
if (pki.notBefore !== undefined && anchor < parseRfc3339(pki.notBefore)) {
|
|
383
|
+
return exports.KEY_OUTSIDE_VALIDITY;
|
|
384
|
+
}
|
|
385
|
+
if (pki.notAfter !== undefined && anchor >= parseRfc3339(pki.notAfter)) {
|
|
386
|
+
return exports.KEY_OUTSIDE_VALIDITY;
|
|
387
|
+
}
|
|
360
388
|
return anchor < parseRfc3339(pki.compromisedFrom)
|
|
361
389
|
? exports.VALID_KEY_COMPROMISED_LATER
|
|
362
390
|
: exports.KEY_COMPROMISED;
|
|
@@ -394,7 +422,7 @@ function certificateAnchor(certificate) {
|
|
|
394
422
|
if (transparency == null)
|
|
395
423
|
return null;
|
|
396
424
|
const sth = transparency.signed_tree_head;
|
|
397
|
-
if (sth
|
|
425
|
+
if (sth == null || typeof sth.timestamp !== "string")
|
|
398
426
|
return null;
|
|
399
427
|
try {
|
|
400
428
|
return parseRfc3339(sth.timestamp);
|
|
@@ -404,34 +432,43 @@ function certificateAnchor(certificate) {
|
|
|
404
432
|
}
|
|
405
433
|
}
|
|
406
434
|
/**
|
|
407
|
-
* The anchor, but only once
|
|
408
|
-
*
|
|
435
|
+
* The anchor, but only once this record has been PROVEN to be in the tree head
|
|
436
|
+
* carrying it — its inclusion proof verified against that head, not merely the
|
|
437
|
+
* head's own signature checked.
|
|
409
438
|
*
|
|
410
|
-
* The bypass this closes: for a key declared compromised,
|
|
411
|
-
* KEY_COMPROMISED with no anchor and
|
|
412
|
-
* anchor predates the compromise — and the
|
|
413
|
-
* passes through as the RESULT of
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
439
|
+
* The bypass this closes (R12-1/R14-1): for a key declared compromised,
|
|
440
|
+
* evaluateKey answers KEY_COMPROMISED with no anchor and
|
|
441
|
+
* VALID_KEY_COMPROMISED_LATER when the anchor predates the compromise — and the
|
|
442
|
+
* second is a verdict requireUsableKey passes through as the RESULT of
|
|
443
|
+
* verifyCertificate. A signed tree head is a public artifact: any genuine older
|
|
444
|
+
* head the issuer ever published can be stapled onto a record it never
|
|
445
|
+
* contained, and its signature still verifies. Checking only the head signature
|
|
446
|
+
* let a holder of a certificate signed after the compromise attach a
|
|
447
|
+
* pre-compromise head and have the forgery reported as verified-with-a-caveat.
|
|
448
|
+
* The head's timestamp is an honest anchor for THIS record only once THIS record
|
|
449
|
+
* is shown to be committed to under it.
|
|
417
450
|
*
|
|
418
|
-
*
|
|
419
|
-
*
|
|
451
|
+
* checkCertificateInclusion is exactly that proof; it deliberately does NOT test
|
|
452
|
+
* the key's usability at head time — that is the verdict this anchor exists to
|
|
453
|
+
* compute, so gating the anchor on it would drop the anchor for an out-of-window
|
|
454
|
+
* key and soften KEY_OUTSIDE_VALIDITY to VALID_KEY_WINDOW_UNKNOWN. Anything short
|
|
455
|
+
* of a held inclusion proof yields no anchor, the same conservative answer as a
|
|
456
|
+
* certificate with no transparency block at all.
|
|
420
457
|
*/
|
|
421
458
|
async function verifiedCertificateAnchor(crypto, certificate, pki) {
|
|
422
459
|
const anchor = certificateAnchor(certificate);
|
|
423
460
|
if (anchor === null)
|
|
424
461
|
return null;
|
|
425
|
-
|
|
426
|
-
|
|
462
|
+
// Only a refusal of the proof means "no anchor". A defect in this library, or
|
|
463
|
+
// a crypto provider that threw, must not get the same quiet answer as a
|
|
464
|
+
// document whose inclusion proof does not verify.
|
|
427
465
|
try {
|
|
428
|
-
|
|
429
|
-
const headSig = decodeSignature(sth.signature);
|
|
430
|
-
if (!(await crypto.ed25519Verify(pki.keyBytes, headPayload, headSig)))
|
|
431
|
-
return null;
|
|
466
|
+
await checkCertificateInclusion(crypto, certificate, pki);
|
|
432
467
|
}
|
|
433
|
-
catch {
|
|
434
|
-
|
|
468
|
+
catch (e) {
|
|
469
|
+
if (e instanceof errors_js_1.VerificationError)
|
|
470
|
+
return null;
|
|
471
|
+
throw e;
|
|
435
472
|
}
|
|
436
473
|
return anchor;
|
|
437
474
|
}
|
|
@@ -468,14 +505,155 @@ role = "issuer key") {
|
|
|
468
505
|
}
|
|
469
506
|
return verdict;
|
|
470
507
|
}
|
|
508
|
+
function policySet(values, field, lower) {
|
|
509
|
+
if (values === undefined)
|
|
510
|
+
return undefined;
|
|
511
|
+
if (!Array.isArray(values) || !values.every((v) => typeof v === "string" && v !== "")) {
|
|
512
|
+
throw new TypeError(`requireAuthorization.${field} must be an array of non-empty strings`);
|
|
513
|
+
}
|
|
514
|
+
if (values.length === 0) {
|
|
515
|
+
// An empty requirement requires nothing, which is never what a caller who
|
|
516
|
+
// built a policy meant.
|
|
517
|
+
throw new Error(`an empty requireAuthorization.${field} requires nothing; omit it instead`);
|
|
518
|
+
}
|
|
519
|
+
return new Set(values.map((v) => (lower ? v.toLowerCase() : v)));
|
|
520
|
+
}
|
|
521
|
+
/** Check a policy before anything is verified, so a caller's mistake surfaces as
|
|
522
|
+
* one whatever the record turns out to be. */
|
|
523
|
+
function parsePolicy(policy) {
|
|
524
|
+
const keys = policy.customerKeyIds;
|
|
525
|
+
if (keys === undefined || (Array.isArray(keys) && keys.length === 0)) {
|
|
526
|
+
throw new TypeError("requireAuthorization.customerKeyIds is required, with at least one key id: `certified` under " +
|
|
527
|
+
"a key id you did not name proves nothing to you, because the relay can enroll a key group of its own");
|
|
528
|
+
}
|
|
529
|
+
return {
|
|
530
|
+
systems: policySet(policy.systems, "systems", true),
|
|
531
|
+
customerKeyIds: policySet(policy.customerKeyIds, "customerKeyIds", false),
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* The enclave images that issued format 9.0 while running wire protocol 11 or
|
|
536
|
+
* earlier, by PCR0, as the published measurement history records them
|
|
537
|
+
* (website/docs/enclave-measurements/, docs/CONNECTOR-STATUS.md).
|
|
538
|
+
*
|
|
539
|
+
* Their `certified` is not evidence of a customer signature. Before protocol 12
|
|
540
|
+
* the enclave enrolled a key group with no proof that anyone held its keys, and
|
|
541
|
+
* registered a system for whoever relayed the authorization — so the host could
|
|
542
|
+
* make a genuine 9.0 record say `certified`, under a key id it chose, with no
|
|
543
|
+
* customer signature anywhere (reproduced by an independent re-review,
|
|
544
|
+
* 2026-09-13).
|
|
545
|
+
*
|
|
546
|
+
* THE SET IS CLOSED BY POLICY, NOT BY ENFORCEMENT: the release runbook forbids
|
|
547
|
+
* admitting or rolling back to an image of protocol 11 or earlier once any
|
|
548
|
+
* customer key is enrolled, and admitting one takes a KMS re-pin, which ADR-027
|
|
549
|
+
* leaves to the owner. The protocol-12 cutover's KMS tighten drops every
|
|
550
|
+
* measurement here from the signing key's policy, after which none of them can
|
|
551
|
+
* sign. v42 ran for seven minutes and is recorded as issuing nothing; it is here
|
|
552
|
+
* because it held the signing key while it ran.
|
|
553
|
+
*
|
|
554
|
+
* The Python SDK carries the same list, in _verify.py.
|
|
555
|
+
*/
|
|
556
|
+
exports.PRE_PROTOCOL_12_IMAGES = new Map([
|
|
557
|
+
["0e79f985c287fbad3ed9aa1d5443189c45428929086dc4d4312ba71c4039742a8bca02f4ea15ab318d82e1d86ff883d3", "EIF v39"],
|
|
558
|
+
["665864551aeb57705c3b3721018112cbd0e2be98de2e0ff9142a6c0abf366c72c6ba6cdcd31cd6ed8471862e1b9187ce", "EIF v40"],
|
|
559
|
+
["7a61327a0ee11508764a8ad03d68e81e3b374e4e605a284e9e815ec9cf57de8497e59d57048977441f9c5c98bee1d14b", "EIF v41"],
|
|
560
|
+
["f1e02ace0011bfc0d60d6dd76da0078b7706f3165eff6904a36e97e5d2029246099ef20bf15f87d0263dd843f878731c", "EIF v42"],
|
|
561
|
+
["10ed72207ffd6a14f5913130eca69e9f4f9de0447b360e9cfd8aa8e349681c6fe98c515499a38a347d67932a6201a9b4", "EIF v43"],
|
|
562
|
+
["0bfbd2251cb9e138491c6ce424ad132b28be152e1486aa7100acabce0201f3f5d500eb47dc00184de4eebc93fe4dc695", "EIF v44"],
|
|
563
|
+
["4d68b3ef6b77418f2827964400241ee9db7e101ce53666b75cc6cf7aa17d7c9f3a1b9055bafec4f799beee6e00227546", "EIF v45 and v46"],
|
|
564
|
+
]);
|
|
565
|
+
function issuingImage(certificate) {
|
|
566
|
+
const issuer = certificate.issuer;
|
|
567
|
+
const pcr0 = String(issuer?.enclave_pcr0 ?? "").toLowerCase();
|
|
568
|
+
if (pcr0 === "")
|
|
569
|
+
return { kind: "unnamed" };
|
|
570
|
+
const name = exports.PRE_PROTOCOL_12_IMAGES.get(pcr0);
|
|
571
|
+
return name === undefined
|
|
572
|
+
? { kind: "protocol-12-or-later", pcr0 }
|
|
573
|
+
: { kind: "before-protocol-12", name, pcr0 };
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* What `certified` is evidence of, given the image that signed the record: the
|
|
577
|
+
* words every verifier prints after "certified under <key id> —". The Go CLI
|
|
578
|
+
* (cmd/cli/registration_image.go), both SDK CLIs and the /verify page state it
|
|
579
|
+
* identically, and testdata/registration_line_cases.json holds them to it.
|
|
580
|
+
*/
|
|
581
|
+
function registrationQualifier(image) {
|
|
582
|
+
switch (image.kind) {
|
|
583
|
+
case "unnamed":
|
|
584
|
+
return ("not evidence here: this record names no enclave image, so it may come from one " +
|
|
585
|
+
"that could mark a system certified without the customer's signature.");
|
|
586
|
+
case "before-protocol-12":
|
|
587
|
+
return (`not evidence here: this record comes from ${image.name}, an enclave image of ` +
|
|
588
|
+
"protocol 11 or earlier, which could mark a system certified without the customer's signature.");
|
|
589
|
+
case "protocol-12-or-later":
|
|
590
|
+
return ("the key group with that id signed this system's registration, if that key id is yours: " +
|
|
591
|
+
"this record comes from an enclave image of protocol 12 or later. If that key id is not yours, " +
|
|
592
|
+
"someone else authorized this verification.");
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Throw unless a record that has already verified meets `policy`.
|
|
597
|
+
*
|
|
598
|
+
* From 9.0 every field read here is under the certificate signature, the
|
|
599
|
+
* issuer's enclave_pcr0 included. Below 9.0
|
|
600
|
+
* nothing signs `authorization` — those records carry the field outside the
|
|
601
|
+
* signed bytes — so one that says `certified` says only what its last holder
|
|
602
|
+
* typed. It fails on the format, before the field is read.
|
|
603
|
+
*/
|
|
604
|
+
function enforceAuthorization(certificate, formatVersion, policy) {
|
|
605
|
+
const systemsWanted = policy.systems;
|
|
606
|
+
if (!signatureCoversAuthorization(formatVersion)) {
|
|
607
|
+
throw new errors_js_1.VerificationError(`authorization required, but this record is format ${formatVersion}, which predates ` +
|
|
608
|
+
`customer-signed registration (${FORMAT_VERSION_V9}); it cannot show that any system was certified`, errors_js_1.AUTHORIZATION_POLICY_FAILED);
|
|
609
|
+
}
|
|
610
|
+
const systems = certificate.systems ?? [];
|
|
611
|
+
let required = systems;
|
|
612
|
+
if (systemsWanted !== undefined) {
|
|
613
|
+
const byId = new Map(systems.map((s) => [String(s.system_id).toLowerCase(), s]));
|
|
614
|
+
const missing = [...systemsWanted].filter((id) => !byId.has(id)).sort();
|
|
615
|
+
if (missing.length > 0) {
|
|
616
|
+
throw new errors_js_1.VerificationError(`authorization required for system ${missing[0]}, which this record does not cover`, errors_js_1.AUTHORIZATION_POLICY_FAILED);
|
|
617
|
+
}
|
|
618
|
+
required = [...systemsWanted].sort().map((id) => byId.get(id));
|
|
619
|
+
}
|
|
620
|
+
const labelOf = (s) => `system ${JSON.stringify(s.system_name)} (${String(s.system_id)})`;
|
|
621
|
+
for (const s of required) {
|
|
622
|
+
if (s.authorization !== AUTHORIZATION_CERTIFIED) {
|
|
623
|
+
throw new errors_js_1.VerificationError(`authorization required, but ${labelOf(s)} is ${JSON.stringify(s.authorization ?? null)}: ` +
|
|
624
|
+
"the enclave did not check it against a registration", errors_js_1.AUTHORIZATION_POLICY_FAILED);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
// Asked once, for the record, after every required system has said certified:
|
|
628
|
+
// which image said it decides whether `certified` means a customer signed.
|
|
629
|
+
const image = issuingImage(certificate);
|
|
630
|
+
if (image.kind === "unnamed") {
|
|
631
|
+
throw new errors_js_1.VerificationError("authorization required, but this record names no enclave image (issuer.enclave_pcr0 is " +
|
|
632
|
+
"absent), so nothing shows it came from one that registers a system only with the " +
|
|
633
|
+
"customer's signature", errors_js_1.AUTHORIZATION_POLICY_FAILED);
|
|
634
|
+
}
|
|
635
|
+
if (image.kind === "before-protocol-12") {
|
|
636
|
+
throw new errors_js_1.VerificationError(`authorization required, but this record was signed by ${image.name} (PCR0 ${image.pcr0.slice(0, 16)}…), ` +
|
|
637
|
+
"an enclave image of wire protocol 11 or earlier, which could register a system without " +
|
|
638
|
+
"the customer's signature; its `certified` does not show that any key group signed anything", errors_js_1.AUTHORIZATION_POLICY_FAILED);
|
|
639
|
+
}
|
|
640
|
+
for (const s of required) {
|
|
641
|
+
if (!policy.customerKeyIds.has(s.customer_key_id)) {
|
|
642
|
+
throw new errors_js_1.VerificationError(`${labelOf(s)} is certified under ${String(s.customer_key_id)}, which is not a key you ` +
|
|
643
|
+
"accept; if it is not yours, someone else authorized this verification", errors_js_1.AUTHORIZATION_POLICY_FAILED);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
471
647
|
/** Verify all signatures on a deletion certificate offline. */
|
|
472
|
-
async function verifyCertificate(crypto, certificate, publicKeys) {
|
|
648
|
+
async function verifyCertificate(crypto, certificate, publicKeys, options) {
|
|
473
649
|
// Step 0: can this SDK read the format at all? Every check below rebuilds
|
|
474
650
|
// signed bytes from the version, so an unreadable version makes all of them
|
|
475
651
|
// meaningless - and "unknown issuer key" would be the wrong thing to tell
|
|
476
652
|
// someone holding a record that is merely newer than this library.
|
|
653
|
+
requireObject(certificate, "certificate");
|
|
654
|
+
const policy = options?.requireAuthorization === undefined ? undefined : parsePolicy(options.requireAuthorization);
|
|
477
655
|
const formatVersion = checkFormatVersion(certificate.certificate_format_version);
|
|
478
|
-
const issuer = certificate
|
|
656
|
+
const issuer = objectAt(certificate, "issuer", "issuer");
|
|
479
657
|
// Step 0b: can this SDK check the algorithm the record names? Asked before
|
|
480
658
|
// any signature, because verifying an Ed25519 signature over a record that
|
|
481
659
|
// says it was signed with something else answers a question nobody asked.
|
|
@@ -486,7 +664,7 @@ async function verifyCertificate(crypto, certificate, publicKeys) {
|
|
|
486
664
|
`this version of the SDK verifies ${ALGORITHM_ED25519}. ` +
|
|
487
665
|
"The record may be genuine and simply signed with a scheme this library does not implement.", errors_js_1.UNSUPPORTED_ALGORITHM);
|
|
488
666
|
}
|
|
489
|
-
const keyId = issuer.key_id;
|
|
667
|
+
const keyId = stringAt(issuer, "key_id", "issuer.key_id");
|
|
490
668
|
const pki = publicKeys.get(keyId);
|
|
491
669
|
if (pki === undefined)
|
|
492
670
|
throw new errors_js_1.VerificationError(`unknown issuer key: ${keyId}`);
|
|
@@ -496,7 +674,7 @@ async function verifyCertificate(crypto, certificate, publicKeys) {
|
|
|
496
674
|
// 1. Certificate signature FIRST — nothing below may trust a field until the
|
|
497
675
|
// bytes carrying it are covered by a verified signature.
|
|
498
676
|
const certPayload = buildCertificatePayload(certificate);
|
|
499
|
-
const certSig =
|
|
677
|
+
const certSig = decodeFixed(certificate.certificate_signature, "certificate_signature", 64);
|
|
500
678
|
if (!(await crypto.ed25519Verify(pki.keyBytes, certPayload, certSig))) {
|
|
501
679
|
throw new errors_js_1.VerificationError("certificate signature is invalid");
|
|
502
680
|
}
|
|
@@ -510,7 +688,7 @@ async function verifyCertificate(crypto, certificate, publicKeys) {
|
|
|
510
688
|
attested_at: att.attested_at,
|
|
511
689
|
systems: attestationSystems(certificate),
|
|
512
690
|
}, certificate.certificate_format_version);
|
|
513
|
-
const attSig =
|
|
691
|
+
const attSig = decodeFixed(att.attestation_signature, "attestation.attestation_signature", 64);
|
|
514
692
|
if (!(await crypto.ed25519Verify(pki.keyBytes, attPayload, attSig))) {
|
|
515
693
|
throw new errors_js_1.VerificationError("attestation signature is invalid");
|
|
516
694
|
}
|
|
@@ -559,6 +737,9 @@ async function verifyCertificate(crypto, certificate, publicKeys) {
|
|
|
559
737
|
if (!anyAttested) {
|
|
560
738
|
throw new errors_js_1.VerificationError("incomplete verification: no system held any records at attestation time");
|
|
561
739
|
}
|
|
740
|
+
if (policy !== undefined) {
|
|
741
|
+
enforceAuthorization(certificate, formatVersion, policy);
|
|
742
|
+
}
|
|
562
743
|
// A soft key verdict is the result, not a footnote: VALID_KEY_WINDOW_UNKNOWN
|
|
563
744
|
// and VALID_KEY_COMPROMISED_LATER mean this document still needs a human, and
|
|
564
745
|
// reporting VALID here would be the lying by omission ADR-017 exists to stop.
|
|
@@ -567,21 +748,34 @@ async function verifyCertificate(crypto, certificate, publicKeys) {
|
|
|
567
748
|
const NIL_UUID = "00000000-0000-0000-0000-000000000000";
|
|
568
749
|
/** Verify the transparency proof embedded in a certificate. */
|
|
569
750
|
async function verifyTransparency(crypto, certificate, publicKeys) {
|
|
570
|
-
|
|
571
|
-
if (transparency == null) {
|
|
751
|
+
requireObject(certificate, "certificate");
|
|
752
|
+
if (certificate.transparency == null) {
|
|
572
753
|
return "NOT_AVAILABLE";
|
|
573
754
|
}
|
|
574
|
-
const
|
|
575
|
-
const
|
|
576
|
-
const keyId = issuer.key_id;
|
|
755
|
+
const issuer = objectAt(certificate, "issuer", "issuer");
|
|
756
|
+
const keyId = stringAt(issuer, "key_id", "issuer.key_id");
|
|
577
757
|
const pki = publicKeys.get(keyId);
|
|
578
758
|
if (pki === undefined)
|
|
579
759
|
throw new errors_js_1.VerificationError(`unknown issuer key: ${keyId}`);
|
|
580
760
|
// The tree head carries its own timestamp, so this path always has an anchor.
|
|
581
761
|
requireUsableKey(pki, certificateAnchor(certificate), keyId);
|
|
762
|
+
await checkCertificateInclusion(crypto, certificate, pki);
|
|
763
|
+
return "INCLUDED";
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Verify the head signature and the Merkle inclusion of a certificate's proof —
|
|
767
|
+
* everything about whether this record is committed to under its head, but NOT
|
|
768
|
+
* whether the key was usable when it signed. That last question is the caller's;
|
|
769
|
+
* certificateAnchor needs membership, not authority, and folding authority in
|
|
770
|
+
* here made the anchor for an out-of-window key collapse to "no anchor" and the
|
|
771
|
+
* verdict soften. Throws VerificationError on any inclusion failure.
|
|
772
|
+
*/
|
|
773
|
+
async function checkCertificateInclusion(crypto, certificate, pki) {
|
|
774
|
+
const transparency = objectAt(certificate, "transparency", "transparency");
|
|
775
|
+
const sth = objectAt(transparency, "signed_tree_head", "transparency.signed_tree_head");
|
|
582
776
|
// 1. Tree head signature
|
|
583
777
|
const headPayload = buildTreeHeadPayload(sth);
|
|
584
|
-
const headSig =
|
|
778
|
+
const headSig = decodeFixed(sth.signature, "signed_tree_head.signature", 64);
|
|
585
779
|
if (!(await crypto.ed25519Verify(pki.keyBytes, headPayload, headSig))) {
|
|
586
780
|
throw new errors_js_1.VerificationError("tree head signature is invalid");
|
|
587
781
|
}
|
|
@@ -594,10 +788,10 @@ async function verifyTransparency(crypto, certificate, publicKeys) {
|
|
|
594
788
|
// revocation of the same certificate produced identical leaves and the tree
|
|
595
789
|
// committed to neither the entry type nor when it happened.
|
|
596
790
|
const issuanceData = issuanceBytes(certificate);
|
|
597
|
-
const leafPayload = buildLogLeafPayload(transparency.entry_type, certificate
|
|
791
|
+
const leafPayload = buildLogLeafPayload(transparency.entry_type, stringAt(certificate, "certificate_id", "certificate_id"), await crypto.sha256(issuanceData), transparency.appended_at);
|
|
598
792
|
const leaf = await hashLeaf(crypto, leafPayload);
|
|
599
|
-
const proofHashes = (transparency.inclusion_proof
|
|
600
|
-
const root =
|
|
793
|
+
const proofHashes = arrayAt(transparency, "inclusion_proof", "transparency.inclusion_proof").map((h, i) => decodeFixed(h, `transparency.inclusion_proof[${i}]`, 32));
|
|
794
|
+
const root = decodeFixed(sth.root_hash, "signed_tree_head.root_hash", 32);
|
|
601
795
|
// `?? 0` is not a convenience: encoding/json leaves 0 in Go's uint64 fields
|
|
602
796
|
// for an explicit null and for an absent key rather than failing, so refusing
|
|
603
797
|
// either would reject documents the reference accepts — the same class of
|
|
@@ -629,7 +823,6 @@ async function verifyTransparency(crypto, certificate, publicKeys) {
|
|
|
629
823
|
if (!(await verifyInclusion(crypto, leaf, index, treeSize, proofHashes, root))) {
|
|
630
824
|
throw new errors_js_1.VerificationError("merkle inclusion proof is invalid");
|
|
631
825
|
}
|
|
632
|
-
return "INCLUDED";
|
|
633
826
|
}
|
|
634
827
|
// ---------------------------------------------------------------------------
|
|
635
828
|
// Issuance bytes — matches Go's core.IssuanceBytes(cert)
|
|
@@ -679,47 +872,89 @@ function canonicalStringify(val) {
|
|
|
679
872
|
}
|
|
680
873
|
return JSON.stringify(val);
|
|
681
874
|
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
* - base64 string (from Go's json.Marshal of []byte slices)
|
|
688
|
-
* - number[] (from Go's json.Marshal of [N]byte fixed arrays)
|
|
689
|
-
*/
|
|
690
|
-
function toHex(value) {
|
|
691
|
-
if (Array.isArray(value)) {
|
|
692
|
-
return bytesToHex(new Uint8Array(value));
|
|
875
|
+
function requireObject(value, path) {
|
|
876
|
+
if (value === undefined || value === null)
|
|
877
|
+
throw new errors_js_1.VerificationError(`${path} is missing`);
|
|
878
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
879
|
+
throw new errors_js_1.VerificationError(`${path} is not an object`);
|
|
693
880
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
return bytesToHex(base64ToBytes(str));
|
|
881
|
+
return value;
|
|
882
|
+
}
|
|
883
|
+
function objectAt(parent, key, path) {
|
|
884
|
+
return requireObject(parent[key], path);
|
|
699
885
|
}
|
|
700
|
-
|
|
701
|
-
|
|
886
|
+
function stringAt(parent, key, path) {
|
|
887
|
+
const value = parent[key];
|
|
888
|
+
if (value === undefined || value === null)
|
|
889
|
+
throw new errors_js_1.VerificationError(`${path} is missing`);
|
|
890
|
+
if (typeof value !== "string")
|
|
891
|
+
throw new errors_js_1.VerificationError(`${path} is not a string`);
|
|
892
|
+
return value;
|
|
893
|
+
}
|
|
894
|
+
function optionalStringAt(parent, key, path) {
|
|
895
|
+
const value = parent[key];
|
|
896
|
+
if (value === undefined || value === null)
|
|
897
|
+
return undefined;
|
|
898
|
+
if (typeof value !== "string")
|
|
899
|
+
throw new errors_js_1.VerificationError(`${path} is not a string`);
|
|
900
|
+
return value;
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* An array field. Absent and null read as empty: Go leaves a nil slice.
|
|
904
|
+
*
|
|
905
|
+
* Returned dense. JSON cannot express a hole, but a caller building the object
|
|
906
|
+
* in JavaScript can, and `.map` skips holes — so one reached the Merkle code as
|
|
907
|
+
* `undefined` and threw a TypeError. Array.from reads a hole as undefined,
|
|
908
|
+
* which the element's own reader then refuses as missing.
|
|
909
|
+
*/
|
|
910
|
+
function arrayAt(parent, key, path) {
|
|
911
|
+
const value = parent[key];
|
|
912
|
+
if (value === undefined || value === null)
|
|
913
|
+
return [];
|
|
914
|
+
if (!Array.isArray(value))
|
|
915
|
+
throw new errors_js_1.VerificationError(`${path} is not an array`);
|
|
916
|
+
return Array.from(value);
|
|
917
|
+
}
|
|
918
|
+
const HEX_TEXT_RE = /^(?:[0-9a-fA-F]{2})*$/;
|
|
919
|
+
const BASE64_TEXT_RE = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
920
|
+
/**
|
|
921
|
+
* Decode a fixed-size byte field — a hash, a key, a signature.
|
|
922
|
+
*
|
|
923
|
+
* Go marshals `[N]byte` as an array of numbers; hand-written documents carry
|
|
924
|
+
* hex or standard base64. A well-formed value of the right size is never
|
|
925
|
+
* ambiguous between the two text forms, because base64 of 32 or 64 bytes ends
|
|
926
|
+
* in padding hex cannot contain.
|
|
927
|
+
*
|
|
928
|
+
* Everything else is refused here, by name: a number, an array element that is
|
|
929
|
+
* not a byte, text that is neither encoding, the wrong length. Decoding it
|
|
930
|
+
* leniently instead wrapped 300 to 44 inside a Uint8Array, produced bytes no
|
|
931
|
+
* signature covers and blamed the signature — or threw from inside atob.
|
|
932
|
+
*/
|
|
933
|
+
function decodeFixed(value, path, length) {
|
|
934
|
+
if (value === undefined || value === null)
|
|
935
|
+
throw new errors_js_1.VerificationError(`${path} is missing`);
|
|
936
|
+
let raw;
|
|
702
937
|
if (Array.isArray(value)) {
|
|
703
|
-
|
|
938
|
+
if (!value.every((b) => Number.isInteger(b) && b >= 0 && b <= 255)) {
|
|
939
|
+
throw new errors_js_1.VerificationError(`${path} is not an array of bytes`);
|
|
940
|
+
}
|
|
941
|
+
raw = new Uint8Array(value);
|
|
704
942
|
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
if (
|
|
709
|
-
|
|
943
|
+
else if (typeof value === "string") {
|
|
944
|
+
if (HEX_TEXT_RE.test(value))
|
|
945
|
+
raw = hexToBytes(value);
|
|
946
|
+
else if (BASE64_TEXT_RE.test(value))
|
|
947
|
+
raw = base64ToBytes(value);
|
|
948
|
+
else
|
|
949
|
+
throw new errors_js_1.VerificationError(`${path} is neither hex nor base64`);
|
|
710
950
|
}
|
|
711
|
-
|
|
712
|
-
}
|
|
713
|
-
/** Decode a hash/bytes field to raw bytes. Same format handling as toHex. */
|
|
714
|
-
function decodeBytes(value) {
|
|
715
|
-
if (Array.isArray(value)) {
|
|
716
|
-
return new Uint8Array(value);
|
|
951
|
+
else {
|
|
952
|
+
throw new errors_js_1.VerificationError(`${path} is not a byte string`);
|
|
717
953
|
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
return hexToBytes(str);
|
|
954
|
+
if (raw.length !== length) {
|
|
955
|
+
throw new errors_js_1.VerificationError(`${path} is ${raw.length} bytes, expected ${length}`);
|
|
721
956
|
}
|
|
722
|
-
return
|
|
957
|
+
return raw;
|
|
723
958
|
}
|
|
724
959
|
/** Go's zero time.Time — what encoding/json leaves in a non-pointer time.Time
|
|
725
960
|
* field for an explicit JSON null or an absent key. */
|
|
@@ -748,14 +983,17 @@ const RFC3339_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?
|
|
|
748
983
|
* Exported from this module so the timestamp_vectors.json suite can diff it
|
|
749
984
|
* against Go directly. It is deliberately not re-exported from index.ts, so it
|
|
750
985
|
* is not part of the published package surface.
|
|
986
|
+
*
|
|
987
|
+
* `field` names the value in a refusal. It defaults to the word these messages
|
|
988
|
+
* always used, for callers normalizing a time that is not a field of a document.
|
|
751
989
|
*/
|
|
752
|
-
function formatTimestamp(ts) {
|
|
990
|
+
function formatTimestamp(ts, field = "timestamp") {
|
|
753
991
|
if (ts === null || ts === undefined)
|
|
754
992
|
return ZERO_INSTANT;
|
|
755
993
|
if (typeof ts !== "string") {
|
|
756
|
-
throw new errors_js_1.VerificationError(
|
|
994
|
+
throw new errors_js_1.VerificationError(`${field} is not a string: ${JSON.stringify(ts)}`);
|
|
757
995
|
}
|
|
758
|
-
return formatEpochSeconds(parseRfc3339(ts));
|
|
996
|
+
return formatEpochSeconds(parseRfc3339(ts, field));
|
|
759
997
|
}
|
|
760
998
|
/** Parse a strict RFC 3339 timestamp to seconds since 1970-01-01T00:00:00Z.
|
|
761
999
|
*
|
|
@@ -766,20 +1004,20 @@ function formatTimestamp(ts) {
|
|
|
766
1004
|
* `+24:00` and `+00:60` parse, while `+25:00` and `+00:61` do not. Sub-second
|
|
767
1005
|
* digits are dropped, matching Format's truncation.
|
|
768
1006
|
*/
|
|
769
|
-
function parseRfc3339(ts) {
|
|
1007
|
+
function parseRfc3339(ts, field = "timestamp") {
|
|
770
1008
|
const m = RFC3339_RE.exec(ts);
|
|
771
1009
|
if (m === null)
|
|
772
|
-
throw new errors_js_1.VerificationError(
|
|
1010
|
+
throw new errors_js_1.VerificationError(`${field} is not RFC 3339: ${ts}`);
|
|
773
1011
|
const [year, month, day, hour, minute, second] = m
|
|
774
1012
|
.slice(1, 7)
|
|
775
1013
|
.map((v) => Number.parseInt(v, 10));
|
|
776
1014
|
if (month < 1 || month > 12)
|
|
777
|
-
throw new errors_js_1.VerificationError(
|
|
1015
|
+
throw new errors_js_1.VerificationError(`${field} month out of range: ${ts}`);
|
|
778
1016
|
if (day < 1 || day > daysInMonth(year, month)) {
|
|
779
|
-
throw new errors_js_1.VerificationError(
|
|
1017
|
+
throw new errors_js_1.VerificationError(`${field} day out of range: ${ts}`);
|
|
780
1018
|
}
|
|
781
1019
|
if (hour > 23 || minute > 59 || second > 59) {
|
|
782
|
-
throw new errors_js_1.VerificationError(
|
|
1020
|
+
throw new errors_js_1.VerificationError(`${field} time of day out of range: ${ts}`);
|
|
783
1021
|
}
|
|
784
1022
|
let offset = 0;
|
|
785
1023
|
const sign = m[7];
|
|
@@ -787,9 +1025,9 @@ function parseRfc3339(ts) {
|
|
|
787
1025
|
const offHour = Number.parseInt(m[8], 10);
|
|
788
1026
|
const offMin = Number.parseInt(m[9], 10);
|
|
789
1027
|
if (offHour > 24)
|
|
790
|
-
throw new errors_js_1.VerificationError(
|
|
1028
|
+
throw new errors_js_1.VerificationError(`${field} zone offset hour out of range: ${ts}`);
|
|
791
1029
|
if (offMin > 60)
|
|
792
|
-
throw new errors_js_1.VerificationError(
|
|
1030
|
+
throw new errors_js_1.VerificationError(`${field} zone offset minute out of range: ${ts}`);
|
|
793
1031
|
offset = (offHour * 3600 + offMin * 60) * (sign === "-" ? -1 : 1);
|
|
794
1032
|
}
|
|
795
1033
|
return daysFromCivil(year, month, day) * 86400 + hour * 3600 + minute * 60 + second - offset;
|
|
@@ -873,17 +1111,17 @@ function buildAttestationPayload(subject, att, certFormatVersion) {
|
|
|
873
1111
|
// payload and call every genuine v8 record a forgery.
|
|
874
1112
|
const coversMeasured = signatureCoversMeasuredTransport(certFormatVersion);
|
|
875
1113
|
const coversRecoverable = signatureCoversRecoverableState(certFormatVersion);
|
|
876
|
-
const systems = att.systems.map((s) => {
|
|
1114
|
+
const systems = att.systems.map((s, i) => {
|
|
877
1115
|
const sys = {
|
|
878
1116
|
canonical_version: s.canonical_version ?? null,
|
|
879
1117
|
connector_type: s.connector_type,
|
|
880
1118
|
hash_scope: s.hash_scope,
|
|
881
|
-
merkle_root: s.merkle_root
|
|
882
|
-
?
|
|
883
|
-
:
|
|
1119
|
+
merkle_root: s.merkle_root == null
|
|
1120
|
+
? null
|
|
1121
|
+
: bytesToHex(decodeFixed(s.merkle_root, `systems[${i}].merkle_root`, 32)),
|
|
884
1122
|
// The system's own observation time, not the envelope's.
|
|
885
|
-
observed_at: formatTimestamp(s.observed_at),
|
|
886
|
-
query_hash:
|
|
1123
|
+
observed_at: formatTimestamp(s.observed_at, `systems[${i}].attested_at`),
|
|
1124
|
+
query_hash: bytesToHex(decodeFixed(s.query_hash, `systems[${i}].query_hash`, 32)),
|
|
887
1125
|
record_count: s.record_count,
|
|
888
1126
|
system_id: s.system_id,
|
|
889
1127
|
system_name: s.system_name,
|
|
@@ -901,7 +1139,7 @@ function buildAttestationPayload(subject, att, certFormatVersion) {
|
|
|
901
1139
|
attested_at: attestedAt,
|
|
902
1140
|
payload_type: attestationPayloadType(certFormatVersion),
|
|
903
1141
|
proof_mode: att.proof_mode,
|
|
904
|
-
subject_hash:
|
|
1142
|
+
subject_hash: bytesToHex(decodeFixed(subject.identifier_hash, "subject.identifier_hash", 32)),
|
|
905
1143
|
systems,
|
|
906
1144
|
};
|
|
907
1145
|
return canonicalJson(payload);
|
|
@@ -967,9 +1205,9 @@ function attestationPayloadType(version) {
|
|
|
967
1205
|
return PAYLOAD_TYPE_ATTESTATION;
|
|
968
1206
|
}
|
|
969
1207
|
function buildCertificatePayload(cert) {
|
|
970
|
-
const att = cert
|
|
971
|
-
const issuer = cert
|
|
972
|
-
const subject = cert
|
|
1208
|
+
const att = objectAt(cert, "attestation", "attestation");
|
|
1209
|
+
const issuer = objectAt(cert, "issuer", "issuer");
|
|
1210
|
+
const subject = objectAt(cert, "subject", "subject");
|
|
973
1211
|
// One list, keyed by system_id (ADR-016 §2). v2 signed an attestation list
|
|
974
1212
|
// and a verification list joined only on the human-editable system_name,
|
|
975
1213
|
// which made a partial deletion indistinguishable from a complete one.
|
|
@@ -977,19 +1215,25 @@ function buildCertificatePayload(cert) {
|
|
|
977
1215
|
const coversMeasured = signatureCoversMeasuredTransport(version);
|
|
978
1216
|
const coversRecoverable = signatureCoversRecoverableState(version);
|
|
979
1217
|
const coversAuthorization = signatureCoversAuthorization(version);
|
|
980
|
-
const systems = (cert
|
|
1218
|
+
const systems = arrayAt(cert, "systems", "systems").map((entry, i) => {
|
|
1219
|
+
const where = `systems[${i}]`;
|
|
1220
|
+
const s = requireObject(entry, where);
|
|
981
1221
|
const sys = {
|
|
982
|
-
attested_at: formatTimestamp(s.attested_at),
|
|
983
|
-
attested_count: s.attested_count,
|
|
984
|
-
canonical_version: s.canonical_version ?? null,
|
|
985
|
-
connector_type: s.connector_type,
|
|
986
|
-
hash_scope: s.hash_scope,
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1222
|
+
attested_at: formatTimestamp(s.attested_at, `${where}.attested_at`),
|
|
1223
|
+
attested_count: requireUint(s.attested_count, `${where}.attested_count`),
|
|
1224
|
+
canonical_version: optionalStringAt(s, "canonical_version", `${where}.canonical_version`) ?? null,
|
|
1225
|
+
connector_type: stringAt(s, "connector_type", `${where}.connector_type`),
|
|
1226
|
+
hash_scope: stringAt(s, "hash_scope", `${where}.hash_scope`),
|
|
1227
|
+
// Absent is a system with no Merkle root; anything present must be one.
|
|
1228
|
+
// Go's *[32]byte is nil or 32 bytes, never "" or [].
|
|
1229
|
+
merkle_root: s.merkle_root == null
|
|
1230
|
+
? null
|
|
1231
|
+
: bytesToHex(decodeFixed(s.merkle_root, `${where}.merkle_root`, 32)),
|
|
1232
|
+
query_hash: bytesToHex(decodeFixed(s.query_hash, `${where}.query_hash`, 32)),
|
|
1233
|
+
system_id: stringAt(s, "system_id", `${where}.system_id`),
|
|
1234
|
+
system_name: stringAt(s, "system_name", `${where}.system_name`),
|
|
1235
|
+
verified_at: formatTimestamp(s.verified_at, `${where}.verified_at`),
|
|
1236
|
+
verified_count: requireUint(s.verified_count, `${where}.verified_count`),
|
|
993
1237
|
};
|
|
994
1238
|
if (coversMeasured) {
|
|
995
1239
|
sys.read_only_enforcement = requireMeasured(s.read_only_enforcement, "read_only_enforcement", s.system_name, version);
|
|
@@ -1009,9 +1253,9 @@ function buildCertificatePayload(cert) {
|
|
|
1009
1253
|
// The attestation block carries no system list of its own: the merged list
|
|
1010
1254
|
// above is a superset of it. verification_signature is gone entirely.
|
|
1011
1255
|
const attObj = {
|
|
1012
|
-
attestation_signature:
|
|
1013
|
-
attested_at: formatTimestamp(att.attested_at),
|
|
1014
|
-
proof_mode: att.proof_mode,
|
|
1256
|
+
attestation_signature: bytesToHex(decodeFixed(att.attestation_signature, "attestation.attestation_signature", 64)),
|
|
1257
|
+
attested_at: formatTimestamp(att.attested_at, "attestation.attested_at"),
|
|
1258
|
+
proof_mode: stringAt(att, "proof_mode", "attestation.proof_mode"),
|
|
1015
1259
|
};
|
|
1016
1260
|
// Fields added after v3 are gated on the certificate's OWN version. A v3
|
|
1017
1261
|
// certificate must reconstruct to the same bytes forever; reading these
|
|
@@ -1023,9 +1267,9 @@ function buildCertificatePayload(cert) {
|
|
|
1023
1267
|
const isV5Plus = formatAtLeast(version, FORMAT_VERSION_V5);
|
|
1024
1268
|
const isV4Plus = formatAtLeast(version, FORMAT_VERSION_V4);
|
|
1025
1269
|
const issuerObj = {
|
|
1026
|
-
key_id: issuer.key_id,
|
|
1027
|
-
name: issuer.name,
|
|
1028
|
-
public_key:
|
|
1270
|
+
key_id: stringAt(issuer, "key_id", "issuer.key_id"),
|
|
1271
|
+
name: stringAt(issuer, "name", "issuer.name"),
|
|
1272
|
+
public_key: bytesToHex(decodeFixed(issuer.public_key, "issuer.public_key", 32)),
|
|
1029
1273
|
};
|
|
1030
1274
|
// v7 signs the algorithm, and does so unconditionally: unlike legal_entity
|
|
1031
1275
|
// and enclave_pcr0, "which scheme signed this" is never unknown to a signer,
|
|
@@ -1033,43 +1277,48 @@ function buildCertificatePayload(cert) {
|
|
|
1033
1277
|
if (signatureCoversAlgorithm(version)) {
|
|
1034
1278
|
issuerObj.algorithm = requireMeasured(issuer.algorithm, "issuer.algorithm", "issuer", version);
|
|
1035
1279
|
}
|
|
1036
|
-
|
|
1037
|
-
|
|
1280
|
+
// A non-string here used to be dropped silently, so a junk value added to a
|
|
1281
|
+
// record that never carried one still verified. Python refused it; so does
|
|
1282
|
+
// Go, at parse. Now this does too.
|
|
1283
|
+
if (isV4Plus) {
|
|
1284
|
+
const legalEntity = optionalStringAt(issuer, "legal_entity", "issuer.legal_entity");
|
|
1285
|
+
if (legalEntity)
|
|
1286
|
+
issuerObj.legal_entity = legalEntity;
|
|
1038
1287
|
}
|
|
1039
1288
|
// Signed from v5, and omitted when absent or empty: a build with no
|
|
1040
1289
|
// measurement signs none, and "" is a value a reader could mistake for one.
|
|
1041
|
-
if (signatureCoversEnclavePcr0(version)
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1290
|
+
if (signatureCoversEnclavePcr0(version)) {
|
|
1291
|
+
const pcr0 = optionalStringAt(issuer, "enclave_pcr0", "issuer.enclave_pcr0");
|
|
1292
|
+
if (pcr0)
|
|
1293
|
+
issuerObj.enclave_pcr0 = pcr0;
|
|
1045
1294
|
}
|
|
1046
1295
|
// v5 drops identifier_type_hint: signed, but always the constant "custom",
|
|
1047
1296
|
// so it was never evidence. v3 and v4 keep it or they stop verifying.
|
|
1048
1297
|
const subjectObj = {
|
|
1049
|
-
identifier_hash:
|
|
1298
|
+
identifier_hash: bytesToHex(decodeFixed(subject.identifier_hash, "subject.identifier_hash", 32)),
|
|
1050
1299
|
};
|
|
1051
1300
|
if (!isV5Plus) {
|
|
1052
|
-
subjectObj.identifier_type_hint = subject.identifier_type_hint;
|
|
1301
|
+
subjectObj.identifier_type_hint = stringAt(subject, "identifier_type_hint", "subject.identifier_type_hint");
|
|
1053
1302
|
}
|
|
1054
1303
|
// status and revocation are deliberately absent (ADR-016 §3): a signature
|
|
1055
1304
|
// commits to bytes at an instant, revocation is discovered later, so it
|
|
1056
1305
|
// travels as a separate short-lived signed status statement.
|
|
1057
1306
|
const payload = {
|
|
1058
1307
|
attestation: attObj,
|
|
1059
|
-
attestation_id: cert
|
|
1060
|
-
certificate_format_version:
|
|
1061
|
-
certificate_id: cert
|
|
1062
|
-
issued_at: formatTimestamp(cert.issued_at),
|
|
1308
|
+
attestation_id: stringAt(cert, "attestation_id", "attestation_id"),
|
|
1309
|
+
certificate_format_version: version,
|
|
1310
|
+
certificate_id: stringAt(cert, "certificate_id", "certificate_id"),
|
|
1311
|
+
issued_at: formatTimestamp(cert.issued_at, "issued_at"),
|
|
1063
1312
|
issuer: issuerObj,
|
|
1064
1313
|
payload_type: certificatePayloadType(version),
|
|
1065
1314
|
subject: subjectObj,
|
|
1066
1315
|
systems,
|
|
1067
1316
|
};
|
|
1068
1317
|
if (isV4Plus && cert.scope != null) {
|
|
1069
|
-
const scope = cert
|
|
1318
|
+
const scope = objectAt(cert, "scope", "scope");
|
|
1070
1319
|
payload.scope = {
|
|
1071
|
-
text_sha256:
|
|
1072
|
-
version: scope.version,
|
|
1320
|
+
text_sha256: bytesToHex(decodeFixed(scope.text_sha256, "scope.text_sha256", 32)),
|
|
1321
|
+
version: stringAt(scope, "version", "scope.version"),
|
|
1073
1322
|
};
|
|
1074
1323
|
}
|
|
1075
1324
|
return canonicalJson(payload);
|
|
@@ -1080,7 +1329,7 @@ function buildLogLeafPayload(entryType, certificateId, certificateHash, appended
|
|
|
1080
1329
|
throw new errors_js_1.VerificationError(`invalid log entry_type: ${String(entryType)}`);
|
|
1081
1330
|
}
|
|
1082
1331
|
const payload = {
|
|
1083
|
-
appended_at: formatTimestamp(appendedAt),
|
|
1332
|
+
appended_at: formatTimestamp(appendedAt, "transparency.appended_at"),
|
|
1084
1333
|
certificate_hash: bytesToHex(certificateHash),
|
|
1085
1334
|
certificate_id: certificateId,
|
|
1086
1335
|
entry_type: entryType,
|
|
@@ -1129,13 +1378,13 @@ function attestationSystems(cert) {
|
|
|
1129
1378
|
* between two modules of this package, not published surface.
|
|
1130
1379
|
*/
|
|
1131
1380
|
function buildTreeHeadPayload(head) {
|
|
1132
|
-
const logId = head.log_id;
|
|
1133
|
-
const named =
|
|
1381
|
+
const logId = optionalStringAt(head, "log_id", "signed_tree_head.log_id");
|
|
1382
|
+
const named = logId !== undefined && logId !== "";
|
|
1134
1383
|
const payload = {
|
|
1135
1384
|
payload_type: named ? PAYLOAD_TYPE_TREE_HEAD_V7 : PAYLOAD_TYPE_TREE_HEAD,
|
|
1136
|
-
root_hash:
|
|
1137
|
-
timestamp: formatTimestamp(head.timestamp),
|
|
1138
|
-
tree_size: head.tree_size,
|
|
1385
|
+
root_hash: bytesToHex(decodeFixed(head.root_hash, "signed_tree_head.root_hash", 32)),
|
|
1386
|
+
timestamp: formatTimestamp(head.timestamp, "signed_tree_head.timestamp"),
|
|
1387
|
+
tree_size: requireUint(head.tree_size ?? 0, "signed_tree_head.tree_size"),
|
|
1139
1388
|
};
|
|
1140
1389
|
if (named)
|
|
1141
1390
|
payload.log_id = logId;
|
|
@@ -1205,7 +1454,14 @@ async function chainInclusion(crypto, leaf, index, n, proof) {
|
|
|
1205
1454
|
return [await hashNode(crypto, proof[used], inner), used + 1];
|
|
1206
1455
|
}
|
|
1207
1456
|
function isPow2(n) {
|
|
1208
|
-
|
|
1457
|
+
// Arithmetic, not `n & (n - 1)` (R12-2): JS bitwise operators coerce to 32-bit
|
|
1458
|
+
// signed integers, so for a tree size >= 2^31 the bit test gives the wrong
|
|
1459
|
+
// answer. This holds for every safe integer.
|
|
1460
|
+
if (n < 1)
|
|
1461
|
+
return false;
|
|
1462
|
+
while (n % 2 === 0)
|
|
1463
|
+
n /= 2;
|
|
1464
|
+
return n === 1;
|
|
1209
1465
|
}
|
|
1210
1466
|
/** Verify a Merkle consistency proof (RFC 6962 Section 2.1.4).
|
|
1211
1467
|
*
|
|
@@ -1233,9 +1489,14 @@ async function verifyConsistency(crypto, oldSize, newSize, oldRoot, newRoot, pro
|
|
|
1233
1489
|
}
|
|
1234
1490
|
let fn = oldSize - 1;
|
|
1235
1491
|
let sn = newSize - 1;
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1492
|
+
// Arithmetic throughout (R12-2). `& 1` and `>>= 1` coerce to 32-bit signed
|
|
1493
|
+
// integers, so for tree sizes >= 2^31 fn and sn are truncated and this
|
|
1494
|
+
// consistency check silently returned the wrong answer (CONFIRMED where Go
|
|
1495
|
+
// says INCONSISTENT). `% 2` and Math.floor(/2) are exact for every safe
|
|
1496
|
+
// integer, which requireUint already bounds the inputs to.
|
|
1497
|
+
while (fn % 2 === 1) {
|
|
1498
|
+
fn = Math.floor(fn / 2);
|
|
1499
|
+
sn = Math.floor(sn / 2);
|
|
1239
1500
|
}
|
|
1240
1501
|
// Drive the walk from the tree, not from the proof's length. Looping on
|
|
1241
1502
|
// `pIdx < proof.length` let the prover choose how many steps ran, so a log
|
|
@@ -1249,19 +1510,19 @@ async function verifyConsistency(crypto, oldSize, newSize, oldRoot, newRoot, pro
|
|
|
1249
1510
|
return false;
|
|
1250
1511
|
const c = proof[pIdx];
|
|
1251
1512
|
pIdx++;
|
|
1252
|
-
if (
|
|
1513
|
+
if (fn % 2 === 1 || fn === sn) {
|
|
1253
1514
|
fr = await hashNode(crypto, c, fr);
|
|
1254
1515
|
sr = await hashNode(crypto, c, sr);
|
|
1255
|
-
while (fn !== 0 &&
|
|
1256
|
-
fn
|
|
1257
|
-
sn
|
|
1516
|
+
while (fn !== 0 && fn % 2 === 0) {
|
|
1517
|
+
fn = Math.floor(fn / 2);
|
|
1518
|
+
sn = Math.floor(sn / 2);
|
|
1258
1519
|
}
|
|
1259
1520
|
}
|
|
1260
1521
|
else {
|
|
1261
1522
|
sr = await hashNode(crypto, sr, c);
|
|
1262
1523
|
}
|
|
1263
|
-
fn
|
|
1264
|
-
sn
|
|
1524
|
+
fn = Math.floor(fn / 2);
|
|
1525
|
+
sn = Math.floor(sn / 2);
|
|
1265
1526
|
}
|
|
1266
1527
|
return pIdx === proof.length && bytesEqual(fr, oldRoot) && bytesEqual(sr, newRoot);
|
|
1267
1528
|
}
|
|
@@ -1275,21 +1536,21 @@ async function verifyConsistency(crypto, oldSize, newSize, oldRoot, newRoot, pro
|
|
|
1275
1536
|
function buildCertificateStatusPayload(stmt) {
|
|
1276
1537
|
const payload = {
|
|
1277
1538
|
payload_type: PAYLOAD_TYPE_CERTIFICATE_STATUS,
|
|
1278
|
-
certificate_id: stmt.certificate_id,
|
|
1279
|
-
statement_expires_at: formatTimestamp(stmt.statement_expires_at),
|
|
1280
|
-
statement_issued_at: formatTimestamp(stmt.statement_issued_at),
|
|
1281
|
-
status: stmt.status,
|
|
1282
|
-
sth_root_hash:
|
|
1283
|
-
sth_tree_size: stmt.sth_tree_size,
|
|
1539
|
+
certificate_id: stringAt(stmt, "certificate_id", "status.certificate_id"),
|
|
1540
|
+
statement_expires_at: formatTimestamp(stmt.statement_expires_at, "status.statement_expires_at"),
|
|
1541
|
+
statement_issued_at: formatTimestamp(stmt.statement_issued_at, "status.statement_issued_at"),
|
|
1542
|
+
status: stringAt(stmt, "status", "status.status"),
|
|
1543
|
+
sth_root_hash: bytesToHex(decodeFixed(stmt.sth_root_hash, "status.sth_root_hash", 32)),
|
|
1544
|
+
sth_tree_size: requireUint(stmt.sth_tree_size ?? 0, "status.sth_tree_size"),
|
|
1284
1545
|
};
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1546
|
+
const replacement = optionalStringAt(stmt, "replacement_certificate_id", "status.replacement_certificate_id");
|
|
1547
|
+
if (replacement !== undefined)
|
|
1548
|
+
payload.replacement_certificate_id = replacement;
|
|
1288
1549
|
if (stmt.revocation_log_index != null) {
|
|
1289
|
-
payload.revocation_log_index = stmt.revocation_log_index;
|
|
1550
|
+
payload.revocation_log_index = requireUint(stmt.revocation_log_index, "status.revocation_log_index");
|
|
1290
1551
|
}
|
|
1291
1552
|
if (stmt.revoked_at != null) {
|
|
1292
|
-
payload.revoked_at = formatTimestamp(stmt.revoked_at);
|
|
1553
|
+
payload.revoked_at = formatTimestamp(stmt.revoked_at, "status.revoked_at");
|
|
1293
1554
|
}
|
|
1294
1555
|
return canonicalJson(payload);
|
|
1295
1556
|
}
|
|
@@ -1324,7 +1585,7 @@ function buildKeyListPayload(doc) {
|
|
|
1324
1585
|
keys: entries,
|
|
1325
1586
|
statement_expires_at: formatTimestamp(doc.statement_expires_at),
|
|
1326
1587
|
statement_issued_at: formatTimestamp(doc.statement_issued_at),
|
|
1327
|
-
sth_root_hash:
|
|
1588
|
+
sth_root_hash: bytesToHex(decodeFixed(doc.sth_root_hash, "sth_root_hash", 32)),
|
|
1328
1589
|
sth_tree_size: doc.sth_tree_size,
|
|
1329
1590
|
};
|
|
1330
1591
|
return canonicalJson(payload);
|
|
@@ -1350,18 +1611,38 @@ exports.VALID_REVOCATION_UNKNOWN = "VALID_REVOCATION_UNKNOWN";
|
|
|
1350
1611
|
*
|
|
1351
1612
|
* The third row is the point: `verifyCertificate` answers VALID there, which
|
|
1352
1613
|
* reads as "not revoked" and is not something it checked.
|
|
1614
|
+
*
|
|
1615
|
+
* `options` is passed to {@link verifyCertificate} unchanged.
|
|
1353
1616
|
*/
|
|
1354
|
-
async function verifyCertificateWithStatus(crypto, certificate, publicKeys, status, now) {
|
|
1617
|
+
async function verifyCertificateWithStatus(crypto, certificate, publicKeys, status, now, options) {
|
|
1355
1618
|
// A refusal throws out of verifyCertificate, so `base` is VALID or one of the
|
|
1356
1619
|
// two soft verdicts. Returning a soft verdict here skipped every check below
|
|
1357
1620
|
// it: a revoked certificate signed by a compromised-later key answered
|
|
1358
1621
|
// VALID_KEY_COMPROMISED_LATER and its statement was never authenticated. The
|
|
1359
1622
|
// verdict is held instead, and resolved against what the statement says.
|
|
1360
|
-
const base = await verifyCertificate(crypto, certificate, publicKeys);
|
|
1623
|
+
const base = await verifyCertificate(crypto, certificate, publicKeys, options);
|
|
1361
1624
|
if (status == null) {
|
|
1362
1625
|
return resolveWithBase(base, exports.VALID_REVOCATION_UNKNOWN);
|
|
1363
1626
|
}
|
|
1364
|
-
|
|
1627
|
+
return resolveWithBase(base, await verifyStatusStatement(crypto, status, publicKeys, certificate.certificate_id, now));
|
|
1628
|
+
}
|
|
1629
|
+
/**
|
|
1630
|
+
* The statement half of {@link verifyCertificateWithStatus}: authenticate a
|
|
1631
|
+
* status statement about `certificateId` against the published keys, and read
|
|
1632
|
+
* it at `now`. The /verify page's QR lookup has a record id and no record, and
|
|
1633
|
+
* uses this so it shows what a signed, fresh statement says, never what the
|
|
1634
|
+
* status endpoint claims.
|
|
1635
|
+
*
|
|
1636
|
+
* | input | result |
|
|
1637
|
+
* |---|---|
|
|
1638
|
+
* | fresh, REVOKED | throws VerificationError, code CERTIFICATE_REVOKED |
|
|
1639
|
+
* | fresh, ACTIVE | the statement key's verdict: `"VALID"`, or a soft one |
|
|
1640
|
+
* | stale, or a status this SDK does not know | `"VALID_REVOCATION_UNKNOWN"` |
|
|
1641
|
+
* | unknown or unusable key, bad signature, another certificate | throws VerificationError |
|
|
1642
|
+
*/
|
|
1643
|
+
async function verifyStatusStatement(crypto, status, publicKeys, certificateId, now) {
|
|
1644
|
+
requireObject(status, "status");
|
|
1645
|
+
const keyId = optionalStringAt(status, "key_id", "status.key_id");
|
|
1365
1646
|
const info = keyId ? publicKeys.get(keyId) : undefined;
|
|
1366
1647
|
if (!info) {
|
|
1367
1648
|
throw new errors_js_1.VerificationError(`status statement signed by unknown key: ${keyId}`);
|
|
@@ -1382,10 +1663,10 @@ async function verifyCertificateWithStatus(crypto, certificate, publicKeys, stat
|
|
|
1382
1663
|
const at = formatTimestamp(when.toISOString());
|
|
1383
1664
|
const statusKeyVerdict = requireUsableKey(info, parseRfc3339(at), keyId, "status statement key");
|
|
1384
1665
|
const payload = buildCertificateStatusPayload(status);
|
|
1385
|
-
//
|
|
1666
|
+
// decodeFixed, not hexToBytes: signatures arrive as hex, base64 or a byte
|
|
1386
1667
|
// array depending on the producer, and every other signature on this path
|
|
1387
1668
|
// goes through the same decoder.
|
|
1388
|
-
const sig =
|
|
1669
|
+
const sig = decodeFixed(status.signature, "status.signature", 64);
|
|
1389
1670
|
if (!(await crypto.ed25519Verify(info.keyBytes, payload, sig))) {
|
|
1390
1671
|
throw new errors_js_1.VerificationError("status statement signature is invalid");
|
|
1391
1672
|
}
|
|
@@ -1402,8 +1683,7 @@ async function verifyCertificateWithStatus(crypto, certificate, publicKeys, stat
|
|
|
1402
1683
|
// Every signature still verified; the binding was the only forged part.
|
|
1403
1684
|
// core/verify.go never had the fallback, so Go said STATUS_STATEMENT_MISMATCH
|
|
1404
1685
|
// while both SDKs and the browser bundle said VALID.
|
|
1405
|
-
|
|
1406
|
-
if (status.certificate_id !== certId) {
|
|
1686
|
+
if (status.certificate_id !== certificateId) {
|
|
1407
1687
|
throw new errors_js_1.VerificationError("status statement is about a different certificate");
|
|
1408
1688
|
}
|
|
1409
1689
|
// Compared in the normalized form the payload signs, so the freshness check
|
|
@@ -1413,7 +1693,7 @@ async function verifyCertificateWithStatus(crypto, certificate, publicKeys, stat
|
|
|
1413
1693
|
if (at < issued || at >= expires) {
|
|
1414
1694
|
// Stale is not a weaker answer, it is no answer — including for a REVOKED
|
|
1415
1695
|
// statement, which must never decay into VALID.
|
|
1416
|
-
return
|
|
1696
|
+
return exports.VALID_REVOCATION_UNKNOWN;
|
|
1417
1697
|
}
|
|
1418
1698
|
if (status.status === "REVOKED") {
|
|
1419
1699
|
// Coded, because this is the only status-path rejection that is a fact
|
|
@@ -1428,12 +1708,12 @@ async function verifyCertificateWithStatus(crypto, certificate, publicKeys, stat
|
|
|
1428
1708
|
// case — was reported as revocation CHECKED AND PASSED. Mirrors
|
|
1429
1709
|
// core.ValidCertificateStatusValue.
|
|
1430
1710
|
if (!KNOWN_STATUS_VALUES.has(status.status)) {
|
|
1431
|
-
return
|
|
1711
|
+
return exports.VALID_REVOCATION_UNKNOWN;
|
|
1432
1712
|
}
|
|
1433
1713
|
// A soft verdict on the status key is the result, not a footnote — but it is
|
|
1434
1714
|
// reported only once the statement has authenticated and been read, so it can
|
|
1435
1715
|
// neither speak for an unverified statement nor suppress a revocation.
|
|
1436
|
-
return
|
|
1716
|
+
return statusKeyVerdict;
|
|
1437
1717
|
}
|
|
1438
1718
|
/**
|
|
1439
1719
|
* Picks what to report when the certificate's own key returned a soft verdict
|