diff --git a/README.md b/README.md index de0c6e0..1653747 100644 --- a/README.md +++ b/README.md @@ -125,12 +125,20 @@ nix eval --raw -f examples/trinity_quine/node___NODE3.nix; echo sha256sum examples/trinity_quine/*.nix ``` -### mutual_quine_ne — mutual quine as AWS Nitro Enclaves +### mutual_quine_ne_sha — mutual quine as AWS Nitro Enclaves (source digests) -Two **Nitro Enclave images** (EIFs), built reproducibly with [monzo/aws-nitro-util](https://github.com/monzo/aws-nitro-util), each of which *rebuilds the other enclave's image inside itself at runtime* and prints the peer's reference **PCR values** — the measurements the Nitro hypervisor attests — derived intrinsically, with no network and no shared state. +Two **Nitro Enclave images** (EIFs), built with [monzo/aws-nitro-util](https://github.com/monzo/aws-nitro-util), that differ in exactly one file — the quine node — and at runtime *each reconstructs the other's exact Nix source and prints its SHA-384 digest*, with no network and no shared state. +Nothing is rebuilt inside the enclave: this is the Kleene fixed point of `mutual_quine`, demonstrated in isolation on Nitro hardware, and the gentle on-ramp to the full PCR reconstruction below. -Unlike the examples above, running this one requires an **EC2 instance with AWS Nitro Enclaves enabled** and ample resources — it builds the EIFs on the host and each enclave rebuilds its peer's EIF in RAM (the tested setups are `m6a.xlarge` for x86\_64 and `m6g.xlarge` for AArch64: 4 vCPUs / 16 GiB, of which 2 vCPUs / 8 GiB go to the enclave). -See the full tutorial in [`examples/mutual_quine_ne/`](examples/mutual_quine_ne/README.md) for the instance prerequisites, host setup, and a step-by-step walk-through. +Unlike the examples above, running it requires an **EC2 instance with AWS Nitro Enclaves enabled**, but only modest resources (~2 GiB of enclave memory). +See the tutorial in [`examples/mutual_quine_ne_sha/`](examples/mutual_quine_ne_sha/README.md). + +### mutual_quine_ne_pcrs — mutual quine as AWS Nitro Enclaves (reference PCRs) + +The full scheme: the two EIFs are built *reproducibly*, and each enclave *rebuilds the other enclave's image inside itself at runtime* and prints the peer's reference **PCR values** — the measurements the Nitro hypervisor attests — derived intrinsically, with no network and no shared state. + +Running this one requires an **EC2 instance with AWS Nitro Enclaves enabled** and ample resources — it builds the EIFs on the host and each enclave rebuilds its peer's EIF in RAM (the tested setups are `m6a.xlarge` for x86\_64 and `m6g.xlarge` for AArch64: 4 vCPUs / 16 GiB, of which 2 vCPUs / 8 GiB go to the enclave). +See the full tutorial in [`examples/mutual_quine_ne_pcrs/`](examples/mutual_quine_ne_pcrs/README.md) for the instance prerequisites, host setup, and a step-by-step walk-through. ## Tests @@ -141,4 +149,5 @@ nix flake check -L ``` For each example this transpiles the template from scratch, asserts the output is byte-identical to the committed node files, evaluates the emitted nodes and asserts their quine property (self-reproduction / peer digests). -For `mutual_quine_ne` the `mutual-quine-ne-verify-*` checks additionally re-run the enclave entrypoint against each image's rootfs and demand that the EIF it reconstructs for its peer matches the peer's actual build — the mutual-quine property is proven without Nitro hardware. + +For the Nitro Enclave examples the `mutual-quine-ne-sha-verify-*` and `mutual-quine-ne-pcrs-verify-*` checks additionally re-run the enclave entrypoint against each image's rootfs and demand that what it reconstructs for its peer — the peer's source and SHA-384 digest (`_sha`), or the peer's EIF and PCRs (`_pcrs`) — matches the peer's actual build; the mutual-quine property is proven without Nitro hardware. diff --git a/examples/checks.nix b/examples/checks.nix index 43b12bf..cd027e1 100644 --- a/examples/checks.nix +++ b/examples/checks.nix @@ -4,11 +4,12 @@ # (drift detection), # 3. evaluate the emitted nodes with nix-instantiate and assert the # quine property they claim (self-reproduction / peer digests). -# The mutual_quine_ne example has its own, much stronger checks in -# ./mutual_quine_ne/default.nix (EIF reconstruction). +# The mutual_quine_ne_{sha,pcrs} examples have their own, much stronger +# checks in ./mutual_quine_ne_sha/default.nix (peer source digest) and +# ./mutual_quine_ne_pcrs/default.nix (EIF reconstruction). { pkgs }: let - # >= 3.12 for PEP 695; keep in sync with mutual_quine_ne/default.nix + # >= 3.12 for PEP 695; keep in sync with mutual_quine_ne_{sha,pcrs}/default.nix transpiled = name: pkgs.runCommand "nixreflect-${name}-transpiled" { nativeBuildInputs = [ pkgs.python312 ]; } '' export PYTHONPATH=${../src} @@ -17,7 +18,7 @@ let ''; # nix-instantiate performs pure evaluation only, but still wants writable - # state and cache locations (same trick as mutual_quine_ne/run.sh) + # state and cache locations (same trick as mutual_quine_ne_pcrs/run.sh) check = name: script: pkgs.runCommand "nixreflect-e2e-${name}" { nativeBuildInputs = [ pkgs.nix pkgs.jq ]; } '' export HOME="$TMPDIR/home" XDG_CACHE_HOME="$TMPDIR/cache" \ diff --git a/examples/mutual_quine_ne/README.md b/examples/mutual_quine_ne_pcrs/README.md similarity index 96% rename from examples/mutual_quine_ne/README.md rename to examples/mutual_quine_ne_pcrs/README.md index 99cf48b..d366866 100644 --- a/examples/mutual_quine_ne/README.md +++ b/examples/mutual_quine_ne_pcrs/README.md @@ -1,4 +1,6 @@ -# Mutual Quine as two AWS Nitro Enclaves +# Mutual Quine as two AWS Nitro Enclaves — PCRs edition + +> Looking for a gentler start? [`mutual_quine_ne_sha`](../mutual_quine_ne_sha/README.md) is the lighter sibling of this example: the enclaves only compute the SHA-384 digest of each other's *source*, without the in-enclave EIF rebuild performed here. This tutorial builds **two Nitro Enclave image files (EIFs)** such that, at runtime, *each enclave prints the reference PCR values of the other* — without ever talking to the other enclave, the host, or the network. Everything an enclave needs to know about its peer is embedded in its own image. @@ -161,14 +163,14 @@ A different lock — like a different architecture — will produce different (s From the repo root: ```bash -nix build .#mutual-quine-ne-nodes -o nodes +nix build .#mutual-quine-ne-pcrs-nodes -o nodes cat nodes/node___ENCLAVE1.nix ``` You can also run the transpiler directly, without Nix: ```bash -PYTHONPATH=src python3 -m nixreflect examples/mutual_quine_ne/template.json out/ +PYTHONPATH=src python3 -m nixreflect examples/mutual_quine_ne_pcrs/template.json out/ ``` Each node evaluates to `{ self, peer, selfSource, peerSource, peerSourceSha256 }`, reconstructed entirely from the JSON blob embedded in the node itself. @@ -176,8 +178,8 @@ Each node evaluates to `{ self, peer, selfSource, peerSource, peerSourceSha256 } ### Step 2 — EIF build ```bash -nix build .#mutual-quine-ne-eif1 -o eif1 -nix build .#mutual-quine-ne-eif2 -o eif2 +nix build .#mutual-quine-ne-pcrs-eif1 -o eif1 +nix build .#mutual-quine-ne-pcrs-eif2 -o eif2 cat eif1/pcr.json # node1's reference PCRs cat eif2/pcr.json # node2's reference PCRs @@ -191,8 +193,8 @@ Note `nix build` will always reproduce the same `image.eif` and `pcr.json` — t The flake ships checks that re-run **the exact enclave entrypoint** against each image's pristine rootfs and compare the PCRs it reconstructs for its peer with the peer's actual build: ```bash -nix build .#checks.$(nix eval --raw --impure --expr builtins.currentSystem).mutual-quine-ne-verify-1-rebuilds-2 -nix build .#checks.$(nix eval --raw --impure --expr builtins.currentSystem).mutual-quine-ne-verify-2-rebuilds-1 +nix build .#checks.$(nix eval --raw --impure --expr builtins.currentSystem).mutual-quine-ne-pcrs-verify-1-rebuilds-2 +nix build .#checks.$(nix eval --raw --impure --expr builtins.currentSystem).mutual-quine-ne-pcrs-verify-2-rebuilds-1 # or simply: nix flake check ``` @@ -216,7 +218,7 @@ nitro-cli console --enclave-id "$(nitro-cli describe-enclaves | jq -r '.[0].Encl After boot, enclave 1 evaluates its quine, rebuilds its peer, and prints: -``` +```console ==[ NixReflect mutual quine -- Nitro Enclave edition ]== warning: the group 'nixbld' specified in 'build-users-group' does not exist self: __ENCLAVE1 @@ -252,7 +254,7 @@ nitro-cli run-enclave \ nitro-cli console --enclave-id "$(nitro-cli describe-enclaves | jq -r '.[0].EnclaveID')" ``` -``` +```console ==[ NixReflect mutual quine -- Nitro Enclave edition ]== warning: the group 'nixbld' specified in 'build-users-group' does not exist self: __ENCLAVE2 diff --git a/examples/mutual_quine_ne/default.nix b/examples/mutual_quine_ne_pcrs/default.nix similarity index 98% rename from examples/mutual_quine_ne/default.nix rename to examples/mutual_quine_ne_pcrs/default.nix index 572fd44..cd703ea 100644 --- a/examples/mutual_quine_ne/default.nix +++ b/examples/mutual_quine_ne_pcrs/default.nix @@ -1,4 +1,4 @@ -# NixReflect mutual quine on AWS Nitro Enclaves. +# NixReflect mutual quine on AWS Nitro Enclaves -- PCRs edition. # # Builds two enclave images whose node-specific parts are /app/node.nix (user # ramdisk) and /node-id (bootstrap ramdisk); every other build input is diff --git a/examples/mutual_quine_ne/run.sh b/examples/mutual_quine_ne_pcrs/run.sh similarity index 100% rename from examples/mutual_quine_ne/run.sh rename to examples/mutual_quine_ne_pcrs/run.sh diff --git a/examples/mutual_quine_ne/scripts/setup-docker.sh b/examples/mutual_quine_ne_pcrs/scripts/setup-docker.sh similarity index 100% rename from examples/mutual_quine_ne/scripts/setup-docker.sh rename to examples/mutual_quine_ne_pcrs/scripts/setup-docker.sh diff --git a/examples/mutual_quine_ne/scripts/setup-nitro-cli.sh b/examples/mutual_quine_ne_pcrs/scripts/setup-nitro-cli.sh similarity index 97% rename from examples/mutual_quine_ne/scripts/setup-nitro-cli.sh rename to examples/mutual_quine_ne_pcrs/scripts/setup-nitro-cli.sh index db373c3..eacbee5 100755 --- a/examples/mutual_quine_ne/scripts/setup-nitro-cli.sh +++ b/examples/mutual_quine_ne_pcrs/scripts/setup-nitro-cli.sh @@ -17,7 +17,8 @@ # # Tunables (env vars): # ALLOCATOR_MEMORY_MIB memory reserved for enclaves (default 8192 -- the -# mutual_quine_ne enclaves rebuild an EIF in RAM) +# mutual_quine_ne_pcrs enclaves rebuild an EIF in RAM; +# mutual_quine_ne_sha gets by with far less, e.g. 2048) # ALLOCATOR_CPU_COUNT CPUs reserved for enclaves (default 2) set -euo pipefail diff --git a/examples/mutual_quine_ne/template.json b/examples/mutual_quine_ne_pcrs/template.json similarity index 100% rename from examples/mutual_quine_ne/template.json rename to examples/mutual_quine_ne_pcrs/template.json diff --git a/examples/mutual_quine_ne_sha/README.md b/examples/mutual_quine_ne_sha/README.md new file mode 100644 index 0000000..6f9318b --- /dev/null +++ b/examples/mutual_quine_ne_sha/README.md @@ -0,0 +1,172 @@ +# Mutual Quine as two AWS Nitro Enclaves — SHA-384 edition + +This tutorial builds **two Nitro Enclave image files (EIFs)** such that, at runtime, *each enclave reconstructs the exact Nix source of the other and prints its SHA-384 digest* — without ever talking to the other enclave, the host, or the network. +Everything an enclave needs to know about its peer is embedded in its own image. + +It is the **lighter sibling of [`mutual_quine_ne_pcrs`](../mutual_quine_ne_pcrs/README.md)**: real Nitro Enclaves are still built and booted, but *nothing is rebuilt inside the enclave*. +Where `mutual_quine_ne_pcrs` re-stages the peer's rootfs, re-packs its ramdisk, and re-runs `eif_build` in RAM to obtain the peer's reference **PCRs**, this example stops after the quine step: evaluate `/app/node.nix`, reconstruct the peer's source, hash it. +No `eif_build`, no payload of EIF build inputs, far less enclave memory. + +## What this shows — and what it deliberately doesn't + +The [`mutual_quine_ne_pcrs`](../mutual_quine_ne_pcrs/README.md) scheme is two ideas composed: + +1. **Kleene's second recursion theorem** (via NixReflect) — each node can reconstruct the exact source of every node, itself included, from data embedded in itself alone; +2. **reproducible builds** (via [monzo/aws-nitro-util](https://github.com/monzo/aws-nitro-util)) — a pure, pinned function from that source to the peer's EIF, and hence to its reference PCRs. + +This example demonstrates idea 1 **in isolation** on Nitro hardware. +The printed digest is over the peer's *source*, not over its *image*: it is **not** a measurement the Nitro hypervisor attests, so it cannot be checked against an attestation document. +Turning "I know my peer's source" into "I know my peer's reference PCRs" is exactly the reproducible-build step that `mutual_quine_ne_pcrs` adds on top. + +SHA-384 is used (rather than the SHA-256 of the plain [`mutual_quine`](../mutual_quine) example) to match the digest family of Nitro PCRs. +Nix's `builtins.hashString` does not support SHA-384, so the hash is computed with coreutils' `sha384sum` over the reconstructed source file. + +## Image layout + +Both EIFs are assembled by `aws-nitro-util` from: + +| EIF section | content | node1 vs node2 | +| --- | --- | --- | +| kernel + cmdline | AWS-provided blob | identical | +| bootstrap ramdisk | `init` (compiled from source) + `nsm.ko` | **identical** (the enclaves share PCR1) | +| user ramdisk | `/env`, `/cmd`, `/rootfs/**` | **differs in exactly one file** | + +Unlike `mutual_quine_ne_pcrs` there is no `/node-id` in the bootstrap ramdisk (nothing selects a peer ramdisk at runtime, so nothing needs one) and no payload of EIF build inputs in the rootfs. + +The user ramdisk's rootfs contains: + +- `/nix/store/**` — the closure of the tool set the enclave needs at runtime (`bash`, `coreutils`, `diffutils`, `jq`, `nix`). Identical for both. +- `/app/run` — the entrypoint (identical), +- `/app/node.nix` — the NixReflect-transpiled quine node. + **The only file that differs between the two images.** + +At runtime `/app/run`: + +1. evaluates `/app/node.nix` with `nix-instantiate` — the quine yields `{ self, peer, selfSource, peerSource }`; +2. sanity-checks that `selfSource` is byte-identical to its own `/app/node.nix`; +3. runs `sha384sum` over `peerSource` and prints the digest: an intrinsically derived digest of the peer's exact source. + +## Expected digests + +The node sources — and therefore the digests — depend only on [`template.json`](template.json) and the transpiler in this repo: unlike the PCRs of `mutual_quine_ne_pcrs`, they are independent of `flake.lock`'s tool pins and of the architecture. +With the committed template and transpiler: + +| file | SHA-384 | +| --- | --- | +| `node___ENCLAVE1.nix` | `1b9ac63487aae18376a2dfae69db04e526495c4c9a2892e4528fe36941a14f2c0a16a81e7b5e3fb30609c81c9b19b699` | +| `node___ENCLAVE2.nix` | `b0458a2fa18fef7ccd62fc80eb8e49dc489a4408920ad4905b4b6037db30b1a4357ec558502b601f5eda17691b5a744f` | + +So enclave 1 prints node 2's digest (`b0458a…`) and enclave 2 prints node 1's (`1b9ac6…`). + +## Prerequisites + +Same as [`mutual_quine_ne_pcrs`](../mutual_quine_ne_pcrs/README.md#prerequisites): a build machine with Nix + flakes, and an EC2 instance with Nitro Enclaves enabled whose architecture matches the EIF's. +The host setup scripts live in the sibling example: + +```bash +ALLOCATOR_MEMORY_MIB=2048 ../mutual_quine_ne_pcrs/scripts/setup-nitro-cli.sh +``` + +Since nothing is rebuilt in RAM here, a much smaller allocator suffices — 2048 MiB is plenty for the tool closure plus `nix-instantiate` (raise it if `nitro-cli run-enclave` reports insufficient memory). + +### Tested environment + +Last verified end to end on **2026-08-07**, on the **AArch64** host of [`mutual_quine_ne_pcrs`'s tested environment](../mutual_quine_ne_pcrs/README.md#tested-environment) (`m6g.xlarge`, Ubuntu 26.04 LTS, nitro-cli 1.4.5, Determinate Nix 3.21.8): both enclaves booted with 2048 MiB / 2 CPUs on real Nitro hardware, each printed exactly the SHA-384 digest of its peer's node file, and for a non-debug run of each image the PCR0–2 reported by the hypervisor (`nitro-cli describe-enclaves --metadata`) matched the image's build-time `pcr.json`. +Unlike the PCRs, the [expected digests](#expected-digests) are architecture-independent — an x86\_64 run prints the same values. + +## Run + +### Step 1 — transpilation (optional) + +From the repo root: + +```bash +nix build .#mutual-quine-ne-sha-nodes -o nodes +cat nodes/node___ENCLAVE1.nix +sha384sum nodes/node___ENCLAVE*.nix # the digests the enclaves will print +``` + +You can also run the transpiler directly, without Nix: + +```bash +PYTHONPATH=src python3 -m nixreflect examples/mutual_quine_ne_sha/template.json out/ +``` + +Each node evaluates to `{ self, peer, selfSource, peerSource }`, reconstructed entirely from the JSON blob embedded in the node itself. + +### Step 2 — EIF build + +```bash +nix build .#mutual-quine-ne-sha-eif1 -o eif1 +nix build .#mutual-quine-ne-sha-eif2 -o eif2 +``` + +### Step 3 — hardware-free verification + +The flake ships checks that re-run **the exact enclave entrypoint** against each image's pristine rootfs, and demand that both the source and the SHA-384 digest it reconstructs for its peer match the peer's actual node file: + +```bash +nix build .#checks.$(nix eval --raw --impure --expr builtins.currentSystem).mutual-quine-ne-sha-verify-1-hashes-2 +nix build .#checks.$(nix eval --raw --impure --expr builtins.currentSystem).mutual-quine-ne-sha-verify-2-hashes-1 +# or simply: +nix flake check +``` + +If these pass, what the enclaves will print on real hardware is already determined to be correct. + +### Step 4 — run on Nitro Enclaves + +**Enclave 1** — boot it and attach to its console: + +```bash +nitro-cli run-enclave \ + --eif-path eif1/image.eif \ + --memory 2048 --cpu-count 2 \ + --debug-mode + +nitro-cli console --enclave-id "$(nitro-cli describe-enclaves | jq -r '.[0].EnclaveID')" +``` + +After boot, enclave 1 evaluates its quine and prints: + +```console +==[ NixReflect mutual quine -- Nitro Enclave SHA-384 edition ]== +warning: the group 'nixbld' specified in 'build-users-group' does not exist +self: __ENCLAVE1 +peer: __ENCLAVE2 +self-render is byte-identical to /app/node.nix + +==[ SHA-384 of the source of peer enclave __ENCLAVE2 ]== +b0458a2fa18fef7ccd62fc80eb8e49dc489a4408920ad4905b4b6037db30b1a4357ec558502b601f5eda17691b5a744f + +compare against sha384sum of the peer's /app/node.nix. +``` + +— exactly `sha384sum` of node 2's `/app/node.nix`. +Terminate it, then repeat with `eif2/image.eif`: enclave 2 prints node 1's digest (`1b9ac6…`). + +```bash +nitro-cli terminate-enclave --all +``` + +## Comparison with `mutual_quine_ne_pcrs` + +| | `mutual_quine_ne_sha` (this) | [`mutual_quine_ne_pcrs`](../mutual_quine_ne_pcrs/README.md) | +| --- | --- | --- | +| printed for the peer | SHA-384 of its *source* | its reference *PCRs* (SHA-384 of its EIF sections) | +| in-enclave work | `nix-instantiate` + `sha384sum` | `nix-instantiate` + rootfs re-stage + `cpio\|gzip` re-pack + `eif_build` | +| tools packed into the image | `bash`, `coreutils`, `diffutils`, `jq`, `nix` | those + `findutils`, `cpio`, `gzip`, `eif_build`, and byte-copies of every EIF build input | +| bootstrap ramdisk | shared (PCR1 equal) | per-node `/node-id` (PCR1 differs) | +| enclave memory | ~2 GiB | ~8 GiB | +| output depends on | template + transpiler only | additionally `flake.lock` and architecture | +| usable against an attestation document | no — sources are not measured | **yes** — PCRs are what the NSM attests | + +## Notes + +- **`--debug-mode`** lets you read the console; run without it for real attestation experiments (irrelevant here — this example never touches the NSM). +- The digest values above are pure functions of the committed template and transpiler; if either changes, re-derive them with `sha384sum nodes/node___ENCLAVE*.nix` (Step 1). +- Hardware-free `verify` checks (Step 3) prove the printed output correct without an EC2 instance; booting the enclaves only demonstrates that the same computation runs under the Nitro hypervisor. + +## References + +See [`mutual_quine_ne_pcrs`'s references](../mutual_quine_ne_pcrs/README.md#references); background on Nitro Enclaves attestation and the TTP-free mutual-attestation motivation is there. diff --git a/examples/mutual_quine_ne_sha/default.nix b/examples/mutual_quine_ne_sha/default.nix new file mode 100644 index 0000000..1ccd03d --- /dev/null +++ b/examples/mutual_quine_ne_sha/default.nix @@ -0,0 +1,115 @@ +# NixReflect mutual quine on AWS Nitro Enclaves -- SHA-384 edition. +# +# Builds two enclave images that differ in exactly one file, /app/node.nix. +# At runtime each enclave evaluates its quine node, reconstructs the *peer's* +# node.nix source from data embedded in itself alone, and prints its SHA-384 +# digest. Unlike the mutual_quine_ne_pcrs sibling, nothing is rebuilt inside +# the enclave -- no eif_build, no payload of EIF build inputs, far less memory +# -- so this isolates the Kleene fixed point on Nitro hardware from the +# reproducible-build machinery that turns source digests into PCRs. +{ nitro, eifInit, pkgs ? nitro.pkgs }: +# `pkgs` defaults to nitro-util's own nixpkgs, matching mutual_quine_ne_pcrs; +# here that is mere consistency (nothing is re-packed inside the enclave). +let + arch = pkgs.stdenv.hostPlatform.uname.processor; + + eifName = "mutual-quine-ne-sha"; + eifVersion = "0.1.0"; + cmdline = "reboot=k panic=30 pci=off nomodules console=ttyS0 random.trust_cpu=on root=/dev/ram0"; + + # transpile the template with NixReflect at build time (>= 3.12 for PEP 695) + nodes = pkgs.runCommand "nixreflect-mq-ne-sha-nodes" + { nativeBuildInputs = [ pkgs.python312 ]; } '' + export PYTHONPATH=${../../src} + mkdir -p $out + python3 -m nixreflect ${./template.json} $out + ''; + + nodeId1 = "__ENCLAVE1"; + nodeId2 = "__ENCLAVE2"; + + # nothing in the bootstrap ramdisk is node-specific (no in-enclave rebuild, + # so no need to pick a peer ramdisk by /node-id): one ramdisk serves both + # images, and the two enclaves share PCR1 + sysRamdisk = nitro.mkSysRamdisk { + init = eifInit + "/bin/init"; + nsmKo = nitro.blobs.${arch}.nsmKo; + }; + + # every tool the in-enclave evaluation needs, identical for both enclaves: + # nix evaluates the quine, jq picks fields out of the eval JSON, coreutils + # provides sha384sum, diffutils provides cmp for the self-render check + appEnv = pkgs.buildEnv { + name = "mq-ne-sha-tools"; + paths = [ + pkgs.bash + pkgs.coreutils + pkgs.diffutils + pkgs.jq + pkgs.nix + ]; + }; + + env = "PATH=${appEnv}/bin"; + entrypoint = "/app/run"; + + runScript = pkgs.writeShellScript "mq-ne-sha-run" (builtins.replaceStrings + [ "@appEnv@" ] + [ "${appEnv}" ] + (builtins.readFile ./run.sh)); + + closureList = pkgs.closureInfo { rootPaths = [ appEnv ]; }; + + rootfsFor = label: nodeFile: pkgs.runCommand "mq-ne-sha-rootfs-${label}" { } '' + mkdir -p $out/nix/store $out/app + for p in $(cat ${closureList}/store-paths); do + cp -r $p $out/nix/store/ + done + cp ${runScript} $out/app/run + cp ${nodeFile} $out/app/node.nix + ''; + + eifFor = rootfs: nitro.mkEif { + name = eifName; + version = eifVersion; + inherit arch cmdline; + kernel = nitro.blobs.${arch}.kernel; + kernelConfig = nitro.blobs.${arch}.kernelConfig; + ramdisks = [ + sysRamdisk + (nitro.mkUserRamdisk { inherit env entrypoint rootfs; }) + ]; + }; + + rootfs1 = rootfsFor "node1" "${nodes}/node_${nodeId1}.nix"; + rootfs2 = rootfsFor "node2" "${nodes}/node_${nodeId2}.nix"; + eif1 = eifFor rootfs1; + eif2 = eifFor rootfs2; + + # Re-run the exact enclave entrypoint against a node's pristine rootfs and + # demand that the source it reconstructs for its peer -- and the SHA-384 + # digest it prints -- match the peer's actual node file. This proves the + # mutual-quine property without Nitro hardware. + verifyFor = label: rootfs: peerNodeFile: pkgs.runCommand "mq-ne-sha-verify-${label}" { } '' + export NIXREFLECT_ROOT=${rootfs} + export NIXREFLECT_OUT=$PWD/out + export NIXREFLECT_HOLD=0 + ${runScript} + + echo "--- comparing the runtime-reconstructed peer source against ${peerNodeFile}" + cmp out/peer-node.nix ${peerNodeFile} + expected=$(sha384sum ${peerNodeFile} | cut -d" " -f1) + [ "$(cat out/peer-sha384)" = "$expected" ] + echo "peer source and SHA-384 digest match." + touch $out + ''; +in +{ + inherit nodes appEnv runScript; + rootfs = { node1 = rootfs1; node2 = rootfs2; }; + eifs = { node1 = eif1; node2 = eif2; }; + verify = { + node1 = verifyFor "node1" rootfs1 "${nodes}/node_${nodeId2}.nix"; + node2 = verifyFor "node2" rootfs2 "${nodes}/node_${nodeId1}.nix"; + }; +} diff --git a/examples/mutual_quine_ne_sha/run.sh b/examples/mutual_quine_ne_sha/run.sh new file mode 100644 index 0000000..a14e0ff --- /dev/null +++ b/examples/mutual_quine_ne_sha/run.sh @@ -0,0 +1,68 @@ +#!/bin/bash +# SPDX-License-Identifier: MIT +# +# NixReflect mutual quine, Nitro Enclave SHA-384 edition -- enclave entrypoint. +# +# Runs inside the enclave (NIXREFLECT_ROOT unset, i.e. against /) or inside the +# `verify` derivation sandbox (NIXREFLECT_ROOT=). +# Reconstructs the peer's node.nix source from data embedded in this image +# alone -- no network, no files from outside -- and prints its SHA-384 digest. +# Unlike mutual_quine_ne_pcrs, nothing is rebuilt here: the peer's EIF (and +# hence its PCRs) is never assembled inside the enclave. +# +# @appEnv@ is substituted by default.nix and is identical between the two +# enclave images; the images differ only in /app/node.nix. +set -eu +umask 0022 +export LC_ALL=C + +ROOT="${NIXREFLECT_ROOT:-}" +OUT="${NIXREFLECT_OUT:-/tmp/nixreflect}" +HOLD="${NIXREFLECT_HOLD:-1}" + +export PATH="@appEnv@/bin" + +work="$(mktemp -d)" +mkdir -p "$OUT" + +# nix only evaluates a single self-contained file (nothing is built or +# fetched), but it still wants writable state and cache locations. +export HOME="$work/home" +export XDG_CACHE_HOME="$work/cache" +export NIX_STATE_DIR="$work/nix/state" +export NIX_LOG_DIR="$work/nix/log" +export NIX_CONF_DIR="$work/nix/conf" +mkdir -p "$HOME" "$XDG_CACHE_HOME" "$NIX_STATE_DIR" "$NIX_LOG_DIR" "$NIX_CONF_DIR" + +echo "==[ NixReflect mutual quine -- Nitro Enclave SHA-384 edition ]==" + +# 1. Evaluate our own node file (a Nix Quine). +nix-instantiate --eval --strict --json "$ROOT/app/node.nix" > "$work/eval.json" +SELF_ID="$(jq -r .self "$work/eval.json")" +PEER_ID="$(jq -r .peer "$work/eval.json")" +echo "self: $SELF_ID" +echo "peer: $PEER_ID" + +# -j, not -r: the rendered sources already end in a newline. +jq -j .selfSource "$work/eval.json" > "$work/node-self.nix" +jq -j .peerSource "$work/eval.json" > "$OUT/peer-node.nix" + +# Quine sanity check: the source this image reconstructs for itself must be +# exactly the file it reconstructs it from. +cmp "$work/node-self.nix" "$ROOT/app/node.nix" +echo "self-render is byte-identical to /app/node.nix" + +# 2. Hash the reconstructed peer source. SHA-384 to match the digest family +# Nitro PCRs use (builtins.hashString cannot do SHA-384; coreutils can). +sha384sum "$OUT/peer-node.nix" | cut -d" " -f1 > "$OUT/peer-sha384" + +echo +echo "==[ SHA-384 of the source of peer enclave $PEER_ID ]==" +cat "$OUT/peer-sha384" +echo +echo "compare against sha384sum of the peer's /app/node.nix." + +if [ "$HOLD" = "1" ]; then + # keep the enclave alive so the console output can be read at leisure + while true; do sleep 3600; done +fi diff --git a/examples/mutual_quine_ne_sha/template.json b/examples/mutual_quine_ne_sha/template.json new file mode 100644 index 0000000..a8660eb --- /dev/null +++ b/examples/mutual_quine_ne_sha/template.json @@ -0,0 +1,10 @@ +[ + { + "node-id": "__ENCLAVE1", + "code": "# ========== BODY ========== \nlet\n peers = builtins.filter (n: n != __nixreflect_self_id__) __nixreflect_node_ids__;\n peer = builtins.head peers;\nin\n{\n self = __nixreflect_self_id__;\n inherit peer;\n selfSource = __nixreflect_render__ __nixreflect_self_id__;\n peerSource = __nixreflect_render__ peer;\n}" + }, + { + "node-id": "__ENCLAVE2", + "code": "# ========== BODY ========== \nlet\n peers = builtins.filter (n: n != __nixreflect_self_id__) __nixreflect_node_ids__;\n peer = builtins.head peers;\nin\n{\n self = __nixreflect_self_id__;\n inherit peer;\n selfSource = __nixreflect_render__ __nixreflect_self_id__;\n peerSource = __nixreflect_render__ peer;\n}" + } +] diff --git a/flake.nix b/flake.nix index 600d6f3..90f3335 100644 --- a/flake.nix +++ b/flake.nix @@ -5,10 +5,11 @@ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; flake-utils.url = "github:numtide/flake-utils"; # NOTE: nitro-util's own nixpkgs pin is deliberately NOT overridden with - # `follows`: the mutual_quine_ne example takes every tool that ends up - # inside the enclaves from nitro-util's pkgs, so the binaries that pack the - # ramdisks at build time and the binaries that re-pack them inside the - # enclaves are the exact same store paths. + # `follows`: the mutual_quine_ne_{sha,pcrs} examples take every tool that + # ends up inside the enclaves from nitro-util's pkgs, so the binaries that + # pack the ramdisks at build time and the binaries that re-pack them + # inside the enclaves (mutual_quine_ne_pcrs) are the exact same store + # paths. nitro-util.url = "github:monzo/aws-nitro-util"; }; @@ -39,21 +40,32 @@ # the Linux system matching the enclave's architecture (flake-utils.lib.eachSystem [ "x86_64-linux" "aarch64-linux" ] (system: let - mq = import ./examples/mutual_quine_ne { - nitro = nitro-util.lib.${system}; + nitro = nitro-util.lib.${system}; + eifInit = nitro-util.packages.${system}.eif-init; + # in-enclave SHA-384 of the peer's source only; no rebuild inside + mqSha = import ./examples/mutual_quine_ne_sha { + inherit nitro eifInit; + }; + # full in-enclave rebuild of the peer's EIF, yielding its PCRs + mqPcrs = import ./examples/mutual_quine_ne_pcrs { + inherit nitro eifInit; eifBuild = nitro-util.packages.${system}.eif_build; - eifInit = nitro-util.packages.${system}.eif-init; }; in { packages = { - mutual-quine-ne-nodes = mq.nodes; - mutual-quine-ne-eif1 = mq.eifs.node1; - mutual-quine-ne-eif2 = mq.eifs.node2; + mutual-quine-ne-sha-nodes = mqSha.nodes; + mutual-quine-ne-sha-eif1 = mqSha.eifs.node1; + mutual-quine-ne-sha-eif2 = mqSha.eifs.node2; + mutual-quine-ne-pcrs-nodes = mqPcrs.nodes; + mutual-quine-ne-pcrs-eif1 = mqPcrs.eifs.node1; + mutual-quine-ne-pcrs-eif2 = mqPcrs.eifs.node2; }; checks = { - mutual-quine-ne-verify-1-rebuilds-2 = mq.verify.node1; - mutual-quine-ne-verify-2-rebuilds-1 = mq.verify.node2; + mutual-quine-ne-sha-verify-1-hashes-2 = mqSha.verify.node1; + mutual-quine-ne-sha-verify-2-hashes-1 = mqSha.verify.node2; + mutual-quine-ne-pcrs-verify-1-rebuilds-2 = mqPcrs.verify.node1; + mutual-quine-ne-pcrs-verify-2-rebuilds-1 = mqPcrs.verify.node2; }; })); }