create-evda 0.1.10 → 0.1.11
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/CHANGELOG.md +23 -0
- package/README.md +1 -1
- package/bin/evda.mjs +7 -1
- package/create-evda.mjs +3 -8
- package/lib/adopt.mjs +43 -15
- package/lib/attested-host.mjs +25 -7
- package/lib/attested-wrap.mjs +147 -0
- package/lib/checkout-freshness.mjs +88 -0
- package/lib/circles.mjs +4 -2
- package/lib/day-writer.mjs +31 -4
- package/lib/doctor.mjs +28 -2
- package/lib/folders.mjs +2 -1
- package/lib/gather.mjs +4 -3
- package/lib/keep-hook.mjs +23 -4
- package/lib/person-folders.mjs +2 -1
- package/lib/presets.mjs +2 -1
- package/lib/prompt-hook.mjs +115 -4
- package/lib/readme.mjs +145 -0
- package/lib/record-walk.mjs +9 -0
- package/lib/tdx-attestation.mjs +28 -8
- package/package.json +1 -1
package/lib/tdx-attestation.mjs
CHANGED
|
@@ -23,12 +23,19 @@
|
|
|
23
23
|
// 8. the nonce is one this phone issued, unused, and young enough;
|
|
24
24
|
// 9. REPORTDATA equals reportDataFor(nonce, tlsPublicKey), which binds the quote to the TLS key the
|
|
25
25
|
// phone then pins.
|
|
26
|
+
// 10. when the VM names a key-wrapping key (lib/attested-wrap.mjs), REPORTDATA also binds it, so the
|
|
27
|
+
// key a client wraps circle keys to is one the attested VM made, not one the host operator chose.
|
|
26
28
|
//
|
|
27
29
|
// The binding, exactly: REPORTDATA (64 bytes) = SHA-512( "EVDA-TDX-REPORTDATA-v1" || 0x00 ||
|
|
28
30
|
// nonce (32 bytes) || SPKI DER of the TLS public key ). The nonce has a fixed length and SPKI DER
|
|
29
31
|
// carries its own length, so the concatenation is unambiguous. The VM computes the same value with
|
|
30
32
|
// the same function when it asks the TDX module for a quote.
|
|
31
33
|
//
|
|
34
|
+
// With a key-wrapping key, REPORTDATA = SHA-512( "EVDA-TDX-REPORTDATA-v2" || 0x00 || nonce (32 bytes) ||
|
|
35
|
+
// SPKI DER of the TLS public key || the X25519 wrap public key (32 raw bytes) ). The wrap key has a fixed
|
|
36
|
+
// length and comes last, after a self-delimiting SPKI, and the tag differs, so neither form can be read
|
|
37
|
+
// as the other.
|
|
38
|
+
//
|
|
32
39
|
// Not checked here yet: certificate revocation (Intel's PCK and root CRLs), quote v5 (refused as
|
|
33
40
|
// unknown rather than half-checked), the TD relaunch advice v5 quotes carry, and whether the expected
|
|
34
41
|
// measurements are themselves in Sigstore's log (lib/measurement-log.mjs checks a digest; tying the
|
|
@@ -71,11 +78,20 @@ const spkiDer = (key) => {
|
|
|
71
78
|
return createPublicKey(key).export({ type: "spki", format: "der" });
|
|
72
79
|
};
|
|
73
80
|
|
|
74
|
-
/** The
|
|
75
|
-
export
|
|
81
|
+
/** The domain tag when REPORTDATA also binds the VM's key-wrapping key. */
|
|
82
|
+
export const REPORTDATA_WRAP_TAG = "EVDA-TDX-REPORTDATA-v2";
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The 64 bytes a TD must put in REPORTDATA to bind `nonce` (32 bytes) and `tlsPublicKey` to its quote,
|
|
86
|
+
* and, when given, `wrapPublicKey` (the raw 32-byte X25519 key the VM made inside itself).
|
|
87
|
+
*/
|
|
88
|
+
export function reportDataFor(nonce, tlsPublicKey, wrapPublicKey) {
|
|
76
89
|
const n = Buffer.from(nonce);
|
|
77
90
|
if (n.length !== 32) throw new Error("the nonce must be 32 bytes");
|
|
78
|
-
return createHash("sha512").update(Buffer.concat([Buffer.from(REPORTDATA_TAG, "ascii"), Buffer.from([0]), n, spkiDer(tlsPublicKey)])).digest();
|
|
91
|
+
if (wrapPublicKey == null) return createHash("sha512").update(Buffer.concat([Buffer.from(REPORTDATA_TAG, "ascii"), Buffer.from([0]), n, spkiDer(tlsPublicKey)])).digest();
|
|
92
|
+
const w = Buffer.from(wrapPublicKey);
|
|
93
|
+
if (w.length !== 32) throw new Error("the wrap key must be 32 bytes");
|
|
94
|
+
return createHash("sha512").update(Buffer.concat([Buffer.from(REPORTDATA_WRAP_TAG, "ascii"), Buffer.from([0]), n, spkiDer(tlsPublicKey), w])).digest();
|
|
79
95
|
}
|
|
80
96
|
|
|
81
97
|
/**
|
|
@@ -381,8 +397,10 @@ function checkQuote({ quote, collateral, minTcb, now, trust }) {
|
|
|
381
397
|
* nonce: the 32 bytes the phone sent; nonces: the createNonceBook that issued it (spent here, pass or fail).
|
|
382
398
|
* tlsPublicKey: the TLS server key of this connection (KeyObject, PEM, or SPKI DER).
|
|
383
399
|
* expected: { mrtd, rtmr0, rtmr1, rtmr2, rtmr3 } as hex. Every one must be named.
|
|
400
|
+
* wrapPublicKey: the VM's key-wrapping key as the host named it (32 raw bytes), or absent. When given,
|
|
401
|
+
* the quote must bind it too, and the result names it.
|
|
384
402
|
*/
|
|
385
|
-
export function verifyTdxAttestation({ quote, nonce, nonces, tlsPublicKey, expected, minTcb, collateral, now, trust = TDX_TRUST } = {}) {
|
|
403
|
+
export function verifyTdxAttestation({ quote, nonce, nonces, tlsPublicKey, wrapPublicKey, expected, minTcb, collateral, now, trust = TDX_TRUST } = {}) {
|
|
386
404
|
try {
|
|
387
405
|
if (!nonces?.take) return fail("no nonce book was given, so freshness cannot be checked");
|
|
388
406
|
const at = now === undefined ? undefined : new Date(now).getTime();
|
|
@@ -400,12 +418,14 @@ export function verifyTdxAttestation({ quote, nonce, nonces, tlsPublicKey, expec
|
|
|
400
418
|
// 7. The code is the code expected.
|
|
401
419
|
for (const n of MEASUREMENT_NAMES) if (r.body[n] !== expected[n]) return fail(`${n.toUpperCase()} is ${r.body[n].slice(0, 16)}..., not the expected ${expected[n].slice(0, 16)}...`);
|
|
402
420
|
|
|
403
|
-
// 9. The quote is for this nonce and this
|
|
404
|
-
const want = reportDataFor(nonce, tlsPublicKey);
|
|
405
|
-
if (!timingSafeEqual(want, r.body.reportData)) return fail("REPORTDATA does not bind this nonce and this TLS key");
|
|
421
|
+
// 9 and 10. The quote is for this nonce, this TLS key and, when named, this wrap key.
|
|
422
|
+
const want = reportDataFor(nonce, tlsPublicKey, wrapPublicKey);
|
|
423
|
+
if (!timingSafeEqual(want, r.body.reportData)) return fail(wrapPublicKey == null ? "REPORTDATA does not bind this nonce and this TLS key" : "REPORTDATA does not bind this nonce, this TLS key and this wrap key");
|
|
406
424
|
|
|
407
425
|
const measurements = Object.fromEntries(MEASUREMENT_NAMES.map((n) => [n, r.body[n]]));
|
|
408
|
-
|
|
426
|
+
const out = { ok: true, measurements, tcb: r.tcb, tlsPublicKeySha256: sha256(spkiDer(tlsPublicKey)).toString("hex") };
|
|
427
|
+
if (wrapPublicKey != null) out.wrapPublicKey = Buffer.from(wrapPublicKey);
|
|
428
|
+
return out;
|
|
409
429
|
} catch (err) {
|
|
410
430
|
return fail(`the attestation could not be checked: ${err.message}`);
|
|
411
431
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-evda",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The person layer over the substrate: scaffold and operate an EVDA, a directory that knows what it is. Plain files are the truth, the tooling is optional, and there are no runtime dependencies.",
|
|
6
6
|
"bin": {
|