{"id":50495346,"url":"https://github.com/johnforfar/xnode-tpm-attest","last_synced_at":"2026-06-02T06:30:29.477Z","repository":{"id":355878048,"uuid":"1230038293","full_name":"johnforfar/xnode-tpm-attest","owner":"johnforfar","description":"TPM2 remote-attestation self-test as a Nix flake. Runs the canonical 7-step quote/seal/unseal/credential-activation flow, produces a human-readable log. Deploys as an xnode app or runs anywhere via 'nix run'.","archived":false,"fork":false,"pushed_at":"2026-05-05T16:45:15.000Z","size":26,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-05T18:14:00.019Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Shell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/johnforfar.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-05T16:05:57.000Z","updated_at":"2026-05-05T16:45:20.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/johnforfar/xnode-tpm-attest","commit_stats":null,"previous_names":["johnforfar/xnode-tpm-attest"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/johnforfar/xnode-tpm-attest","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/johnforfar%2Fxnode-tpm-attest","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/johnforfar%2Fxnode-tpm-attest/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/johnforfar%2Fxnode-tpm-attest/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/johnforfar%2Fxnode-tpm-attest/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/johnforfar","download_url":"https://codeload.github.com/johnforfar/xnode-tpm-attest/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/johnforfar%2Fxnode-tpm-attest/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33810341,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-02T02:00:07.132Z","response_time":109,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-02T06:30:28.577Z","updated_at":"2026-06-02T06:30:29.469Z","avatar_url":"https://github.com/johnforfar.png","language":"Shell","funding_links":[],"categories":[],"sub_categories":[],"readme":"# xnode-tpm-attest\n\nA self-contained TPM 2.0 remote-attestation tool. Runs the canonical\nseven-step quote / seal / unseal / credential-activation flow, plus an\n**end-to-end orchestrator** that talks to a separate verifier service\n([xnode-tpm-verify](https://github.com/johnforfar/xnode-tpm-verify))\nto prove that a real app produced a real output on a real attested\nmachine.\n\nThree modes from one flake:\n\n```sh\n# 1. Standalone protocol self-test (no verifier needed)\nnix run github:johnforfar/xnode-tpm-attest\n\n# 2. Standalone, against a software TPM (no hardware needed)\nnix run github:johnforfar/xnode-tpm-attest -- --emulator\n\n# 3. End-to-end attested-app orchestrator (talks to a verifier)\nAPP_NAME=hello-attested \\\nVERIFIER_URL=https://attest.build.openmesh.cloud \\\nTASK_INPUT=\"hello\" \\\nnix run github:johnforfar/xnode-tpm-attest#run-attested-app\n```\n\n## Drop-in NixOS module for any xnode-app or own1-app\n\nThe fastest path to attesting an existing service. Add five lines to your\nflake and the module wires PCR-extending the binary's hash on service\nstart (Layer 2) plus a heartbeat timer (Layer 4):\n\n```nix\n# in your app's flake.nix\ninputs.xnode-tpm-attest.url = \"github:johnforfar/xnode-tpm-attest\";\n# ...\nmodules = [\n  inputs.xnodeos.nixosModules.app\n  inputs.xnode-tpm-attest.nixosModules.appAttestation\n  ({ pkgs, ... }: {\n    services.ollama.enable = true;\n    services.xnode-app-attestation = {\n      enable     = true;\n      appName    = \"ollama\";\n      service    = \"ollama\";\n      execPath   = \"${pkgs.ollama}/bin/ollama\";\n      pcr        = 16;\n      heartbeatInterval = \"5min\";\n      verifierUrl = \"https://attest.build.openmesh.cloud\";\n    };\n  })\n];\n```\n\nWhat gets auto-wired:\n\n- `ExecStartPre` on the named systemd unit → extends `pcr` with `sha256(execPath)` before the unit starts\n- A systemd timer (`xnode-attest-heartbeat`) that re-quotes and POSTs `/heartbeat` every `heartbeatInterval`\n- `DeviceAllow` for `/dev/tpm*` on both units\n- `failClosed = true` (default false): refuses to start if PCR-extend fails\n\nOperator side, register the app once with the verifier:\n\n```sh\nEXEC_HASH=$(sha256sum /run/current-system/sw/bin/ollama | cut -d' ' -f1)\ncurl -fsS -X POST -H \"authorization: Bearer $OPERATOR_TOKEN\" \\\n  -H 'content-type: application/json' \\\n  -d \"{\\\"app_name\\\":\\\"ollama\\\",\\\"version\\\":\\\"v0.5.7\\\",\\\"closure_hash\\\":\\\"$EXEC_HASH\\\",\\\"expected_pcrs\\\":{}}\" \\\n  https://attest.build.openmesh.cloud/register-app\n```\n\nAfter the first quote lands, capture the live PCR values from the receipt\nand re-register with them pinned to enable drift detection.\n\n## Tested on / not tested on\n\n| Vendor | Class | Status | Notes |\n|---|---|---|---|\n| **Intel** PTT (firmware TPM) | silicon | ✅ **Fully tested** | All 7 steps verified end-to-end on Beelink GTi15 Ultra (Meteor Lake) 2026-05-05 |\n| **Cloud xnodes** with no TPM passthrough | n/a | ✅ **Verified diagnostic** | Reports \"no TPM device\" cleanly; doesn't crash. Tested on xnode-1. |\n| **Infineon** OPTIGA TPM | silicon discrete | ⚠️ **Experimental** | RSA + ECC root + intermediate CAs bundled (publicly fetchable from `pki.infineon.com`); chain validation logic wired but **not verified on real Infineon hardware**. The script will try the standard flow and label vendor-specific output `EXPERIMENTAL`. |\n| **AMD** fTPM (PSP) | silicon | ⚠️ **Experimental** | Vendor detection wired; **CA root not bundled** — AMD ships fTPM roots out-of-band and the URL moves between security advisories. Resolve by AIA-walking from a real AMD-fTPM EK cert when first AMD node enrols. |\n| **Nuvoton** NPCT75x | silicon discrete | ⚠️ **Experimental** | Vendor detection wired; **CA root not bundled** — `developer.nuvoton.com` not reachable from build host. |\n| **STMicro** ST33TP* | silicon discrete | ⚠️ **Experimental** | Vendor detection wired; **CA root not bundled** — `sw-center.st.com` endpoints 404. |\n| Microsoft / Google / SwTPM (vTPM) | virtual | ✅ **Detection verified** | Identified correctly; chain validation skipped (vTPMs need cloud-provider attestation API, not silicon CA). |\n\n**What \"experimental\" means here:** the code paths exist, the vendor will be identified correctly, the script will attempt the standard TPM2 protocol — but specific firmware quirks (different NV-index layouts, different EK key templates, different `tpm2_createek` invocations, different policy session requirements) may cause individual steps to fail in vendor-specific ways. The first operator to run this on real AMD/Nuvoton/STMicro hardware will produce useful telemetry; treat their first run as a debug session, not a trust signal.\n\n## Test emulation (no hardware needed)\n\nPass `--emulator` to run the protocol against a software TPM 2.0\n([`swtpm`](https://github.com/stefanberger/swtpm)) instead of real silicon.\nLets you run the full protocol test on **any Linux machine** — Own1,\nxnode-1, workstation, CI runner — even ones with no TPM, with a vTPM\nalready in use, or in containers without `/dev/tpm*` passthrough.\n\n```sh\nnix run github:johnforfar/xnode-tpm-attest -- --emulator\n```\n\n| What `--emulator` mode covers | What it does NOT cover |\n|---|---|\n| Quote signature correctness | Silicon authenticity — swtpm's EK chains to a self-signed CA, not Intel/AMD/Infineon |\n| PCR golden-digest computation | Real boot-time PCR measurements (swtpm starts blank; PCRs are zero unless you extend them yourself) |\n| Seal-to-PCR + unseal flow | Vendor-specific firmware quirks (e.g. the Intel PTT persistent-handle bug we hit) — swtpm is a clean reference impl, doesn't reproduce them |\n| Negative test (wrong policy → unseal fails) | Hardware fingerprinting / fleet-uniqueness properties (each swtpm instance has a unique state, but vendor identity is uniformly \"swtpm\") |\n| Credential activation / AK ↔ EK binding | Side-channel resistance, physical-presence operations, anti-tamper |\n| Verifier code paths, parallel attestation, replay-attack handling | Real EFI / TPM event log replay (swtpm has no kernel event log) |\n\n**Use it for:** developing the verifier side, load-testing parallel\nattestation across N nodes (run N swtpm instances), CI checks that\nprotocol changes don't regress the standard flow, debugging registry\nintegration without burning out a real TPM's DA counter.\n\n**Don't use it for:** any production trust decision, \"verify this is\ngenuine Intel/AMD silicon,\" or anything that needs to look at PCR 7\n(Secure Boot state) — swtpm has no firmware to measure.\n\n## What it does\n\nThree deploy modes from one flake:\n\n1. **As an xnode app** — `om app deploy --flake github:johnforfar/xnode-tpm-attest \u003cname\u003e` — runs as a systemd oneshot, output in the journal, retrieve via `om app logs \u003cname\u003e`\n2. **Direct on any Linux + Nix host** — `nix run github:johnforfar/xnode-tpm-attest` — runs the probe and prints to stdout\n3. **Via the Claude relay or any HTTP fetch** — `curl -fsS https://raw.githubusercontent.com/johnforfar/xnode-tpm-attest/main/scripts/attest.sh | bash` — runs the script directly, no Nix needed (assumes `tpm2-tools`, `openssl`, etc. on PATH)\n\nSame script everywhere. The output adapts: present TPMs run all 7 steps and print pass/fail per step; absent TPMs print a clean \"no TPM available\" diagnostic.\n\n## What it actually does\n\nRuns the canonical seven-step TPM2 attestation flow:\n\n1. **Quote PCRs.** Generate a fresh attestation key (AK) under the\n   manufacturer's endorsement key (EK), sign a quote of selected PCRs\n   (default: 0, 4, 7, 9, 11) with a verifier-supplied nonce.\n2. **Verify TPM provenance.** Read the EK certificate from NV, decode\n   it, identify the manufacturer from the certificate's SAN, select the\n   matching CA bundle, run `openssl verify` against the bundled root.\n3. **Compare PCR golden values.** Compute `sha256(concat(selected PCRs))`\n   from the live PCR readings and confirm it equals the `pcrDigest`\n   field inside the signed quote — proving the quote is for *these* PCR\n   values, not stale or forged ones.\n4. **Seal a secret to the PCR policy.** Build a policy digest from the\n   live PCR values, then create a sealed object whose unseal\n   authorisation requires that exact policy.\n5. **Unseal on the same machine.** Open a policy session matching the\n   committed policy and unseal — should succeed. Repeat with a\n   deliberately wrong policy — should fail with a policy-check error.\n6. **Bind AK to EK (credential activation).** Run a self-test of the\n   `tpm2_makecredential` / `tpm2_activatecredential` pair: the verifier\n   wraps a one-shot secret to the EK pubkey + AK name, the prover\n   activates and recovers the secret. Proves the AK is TPM-resident\n   (not a software RSA key) and that the AK and EK live in the same\n   chip.\n7. **Replay the boot event log.** Parse\n   `/sys/kernel/security/tpm0/binary_bios_measurements`, replay each\n   extend operation, confirm the replayed digest matches the live PCR\n   value, and surface specific events of interest (Secure Boot variable,\n   kernel/initrd hashes, UKI measurements).\n\nEach step's pass/fail status, the supporting hex/text artifacts, and the\nfull quote bundle are written to a single HTML page and a parallel JSON\nreport.\n\n## Why it exists\n\nMost TPM2 attestation tutorials are a wall of `tpm2_*` invocations with\nno sense of why they're there or what they prove. This tool is the\nopposite: every command's purpose is annotated, every output cross-checked,\nand the report tells you not just *what* the digest is but *whether* it\nmatches the expected boot-integrity story.\n\nParticularly useful for:\n\n- **Operators bringing up new hardware** — prove the TPM is present,\n  enrolled, and producing valid quotes before relying on it for fleet\n  membership.\n- **Verifying boot-integrity changes** — flip Secure Boot, swap the\n  kernel, and immediately see which PCRs moved and what the new\n  golden-digest is.\n- **Comparing platforms** — the same report layout for Intel, AMD,\n  Infineon, and virtualised TPMs lets you see the trust differences\n  directly.\n\n## Output: what the report looks like\n\nEach step in the report has:\n\n- A traffic-light status (✓ pass / ✗ fail / ⚠ warning / ⏭ skipped)\n- A one-line summary of what was checked\n- The raw hex/text artifacts for inspection\n- A \"what this means\" annotation explaining the implication\n\nThe full bundle (quote.msg, quote.sig, EK cert, AK pubkey, event log) is\nalso exposed as base64 download links for the verifier-side workflow.\n\n## Quick start\n\n### As an xnode app\n\n```sh\nom app deploy --flake github:johnforfar/xnode-tpm-attest xnode-tpm-attest --wait true\nom app expose xnode-tpm-attest --port 80 --domain xnode-tpm-attest.\u003cyour-xnode-domain\u003e\n# report at https://xnode-tpm-attest.\u003cyour-xnode-domain\u003e/\n# refresh re-runs attestation (cached for 60s to avoid TPM load)\n```\n\nThe app declares the bind-mounts it needs (`/dev/tpm0`, `/dev/tpmrm0`,\n`/sys/kernel/security/tpm0`, `/sys/firmware/efi/efivars`) in its NixOS\nmodule. If your `xnode-manager` build doesn't pass through container\ndevice bind-mounts, the app boots and reports \"TPM not visible from\ncontainer\" — useful diagnostic, not a crash.\n\n### As a workstation / bare-metal tool\n\n```sh\nnix run github:johnforfar/xnode-tpm-attest\n# writes report to ./xnode-tpm-attest-report-\u003chostname\u003e-\u003ctimestamp\u003e/index.html\n# add --serve to start a localhost:8080 web server\n```\n\nRequires root (or `CAP_SYS_RAWIO` + access to `/dev/tpmrm0`) because the\nTPM resource manager device is owned by root by default.\n\n## Trust model\n\nThis is an *attestation* tool, not a *security boundary*. The report it\ngenerates is meant to be **consumed by a verifier elsewhere** — a Pythia\nregistry, a fleet operator, a peer node — that decides what to do with\nthe claims. The report includes everything a verifier needs to make\nthat decision independently: signed quote bundle, EK cert chain\nartifacts, event log, raw PCR values.\n\nWhat the tool **does not** do:\n\n- Make trust decisions on behalf of the verifier (no \"✓ this machine is\n  trusted\" verdict — only \"✓ all attestation steps cryptographically\n  consistent\")\n- Pin or persist anything to the network (the entire protocol is local\n  + bundle-based)\n- Replace the verifier — the verifier needs the same bundled CA roots\n  and the same golden-PCR allowlist to make a real decision\n\n## Configuration\n\nDefaults are sensible; everything is override-able via flake options or\nenvironment variables:\n\n| Option | Default | Effect |\n|---|---|---|\n| `pcrs` | `0,4,7,9,11` | PCR set to quote |\n| `bank` | `sha256` | PCR bank algorithm (rejects sha1) |\n| `nonce_length` | `16` | bytes of verifier nonce |\n| `secret_to_seal` | random 32 bytes | what step 4 seals |\n| `ca_bundle_path` | `./ca-bundle/` | trust roots for chain validation |\n| `port` | `80` (xnode) / `8080` (workstation) | HTTP listen |\n| `cache_ttl_seconds` | `60` | min interval between TPM-touching runs |\n\n## CA bundle: what's required to ship\n\nThe `ca-bundle/` directory holds trust roots for verifying EK certs from\neach TPM vendor. The application loads these at runtime; **no network\naccess** is needed for chain validation once the bundle is shipped.\n\nThe bundle is built by `tools/bundle-tpm-roots.sh` (run once, results\ncommitted). See [`ca-bundle/README.md`](./ca-bundle/README.md) for what's\ncurrently bundled, what's still TODO, and how to refresh.\n\nFor now: the Intel On-Die CA root is bundled. Other vendors require\ncase-by-case sourcing — covered in the bundle README.\n\n## Architecture\n\n```\n┌─ machine under attestation ─────────────────────────────────┐\n│                                                              │\n│  /dev/tpm0  /dev/tpmrm0   /sys/kernel/security/tpm0          │\n│       ▲           ▲              ▲                            │\n│       │           │              │                            │\n│  ┌─── attest.sh ──────────────────────────────┐              │\n│  │  step 1 → quote                            │              │\n│  │  step 2 → read EK cert + verify chain      │              │\n│  │  step 3 → compare PCR digest               │              │\n│  │  step 4 → seal-to-policy                   │              │\n│  │  step 5 → unseal (positive + negative)     │              │\n│  │  step 6 → makecredential / activate        │              │\n│  │  step 7 → event log replay                 │              │\n│  └──────────────────────┬──────────────────────┘              │\n│                         ▼                                     │\n│      ./xnode-tpm-attest-report/index.html + report.json             │\n│                         ▼                                     │\n│                  nginx :80 (or python -m http.server)         │\n│                                                                │\n└──────────────────────┬───────────────────────────────────────┘\n                       │  HTTPS\n                       ▼\n              browser / verifier / Pythia registry\n```\n\nThree layers, each ~100–300 lines:\n\n- **`scripts/attest.sh`** — the protocol implementation. Pure bash +\n  tpm2-tools + openssl. Reads from `/dev/tpmrm0`, writes report files.\n- **`nix/module.nix`** — NixOS module that runs `attest.sh` as a\n  systemd timer, bind-mounts the right device paths, wires nginx to\n  serve the report.\n- **`flake.nix`** — entry point for both `nix run` and `om app deploy`.\n  Pins `tpm2-tools`, `openssl`, `nginx` versions.\n\n## Comparison with safeboot.dev/attestation\n\nThe [Safe Boot project's attestation page](https://safeboot.dev/attestation/)\ndocuments the same underlying protocol — this tool is one packaged\nimplementation with extra steps (sealing, event-log replay, JSON output,\nweb report). If you're learning the protocol, read Safe Boot first.\nIf you want a runnable artifact for an xnode/Own1 fleet, this is that.\n\n## Status\n\n- [x] Step 1 (quote) — verified live on Intel firmware-TPM\n- [x] Step 2 (EK cert + chain) — verified; partial-trust chain check against bundled Intel root\n- [x] Step 3 (PCR golden) — verified\n- [x] Step 4 (seal) — verified\n- [x] Step 5 (unseal positive + negative) — verified\n- [x] Step 6 (activatecredential) — verified end-to-end on Intel PTT\n- [x] Step 7 (event log replay) — `tpm2_eventlog` parses cleanly; explicit Secure Boot check works\n- [x] Workstation / direct-host mode — runs anywhere a Linux + TPM2 + Nix stack exists\n- [x] xnode-app deployment — deploys cleanly via `om app deploy`; reports \"no TPM\" diagnostic when container has no `/dev/tpm*` access\n- [ ] Multi-vendor CA bundle — Intel root only; other vendor roots pending\n\n## Related\n\n- Safe Boot's attestation reference: https://safeboot.dev/attestation/\n- `tpm2-tools` upstream: https://github.com/tpm2-software/tpm2-tools\n- TCG TPM 2.0 specification: https://trustedcomputinggroup.org/resource/tpm-library-specification/\n\n## License\n\nMIT.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjohnforfar%2Fxnode-tpm-attest","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjohnforfar%2Fxnode-tpm-attest","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjohnforfar%2Fxnode-tpm-attest/lists"}