Skip to content

[Guardian] Pin attestation PCR0 via a per-build allowlist#675

Draft
mskd12 wants to merge 3 commits into
deepakmaram/guardian-attestation-verifyfrom
deepakmaram/guardian-pcr-allowlist
Draft

[Guardian] Pin attestation PCR0 via a per-build allowlist#675
mskd12 wants to merge 3 commits into
deepakmaram/guardian-attestation-verifyfrom
deepakmaram/guardian-pcr-allowlist

Conversation

@mskd12

@mskd12 mskd12 commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

⚠️ Draft / do not merge. Stacked on #666 (base = its branch) — review/merge #666 first; this rebases onto main once it lands. #666 itself is blocked on fastcrypto #971.

Replaces #666's single config PCR0 with a per-build allowlist, completing IOP-225 check C. Verification resolves the build named by the session's signature-verified untrusted_git_revision and pins the attestation's PCR0 to it; an unknown build is rejected (we don't know its measurements, so we can't trust it).

Allowlist — capped at two builds

struct PcrAllowlist { expected_build: BuildPcrs, prev_build: Option<BuildPcrs> }
  • expected_build is the build we run; prev_build is the outgoing image, set only during an upgrade window so its still-running sessions verify until they rotate.
  • The 1..=2 cap is a type invariant (one mandatory + one optional) — 0 or 3+ builds are unrepresentable, so allowlist rot is impossible: a retired, possibly-broken PCR can't linger across upgrades.

Targeted by source

resolve(revision, source) — the live enclave must be expected_build; historical S3 logs may also come from prev_build:

  • GetGuardianInfoResponse::verify (relay GetGuardianInfo, hashi-node queries) → LiveEnclave, so you can never provision/act on a stale enclave.
  • get_enclave_identity (the GuardianReader S3 path: prior withdrawals, old committee) → HistoricalLog.

The lookup happens inside verify_enclave_attestation (prod branch), so non-enclave-dev/test stays a complete no-op — dev configs never enumerate a revision.

Read-path consolidation

  • get_verified_enclave_pubkeyget_enclave_identity: reads the AWS-self-signed attestation (anchoring the signing pubkey), then the signed GuardianInfo (sig-checked under that pubkey → reported build), then pins PCR0.
  • The session cache (GuardianSessionKeyCacheGuardianSessionCache) holds the whole EnclaveIdentity { signing_pubkey, info }, so get_info is a cache hit instead of a second S3 read.

Config

expected_pcr0: Stringexpected_build (+ optional prev_build), each { git_revision, pcr0 }, via the shared BuildPcrsConfig; a flat map couldn't label which entry is expected vs outgoing. Threaded through provisioner + monitor (configs + sample YAMLs + README). Dropped the now-unused hex dep from hashi-monitor.

Trust ordering

Attestation cert-chain → anchor pubkey → info signature under it → PCR0 pinned to the named build. Selecting the build by the self-reported revision can't be gamed: PCR0 is the real attested image hash, so a wrong revision resolves to a PCR0 the real image won't match. Side effect: after this, untrusted_git_revision is bound to the attested image.

Notes

  • Kept OIAttestationUnsigned and OIGuardianInfo as separate logs — the attestation is AWS-self-signed (its own root of trust), so it stays unsigned.
  • No test/e2e path drives the reader (e2e uses in-process install_operator_init_for_testing); bootstrap_guardian uses signed_info.verify directly. Every session logs OIGuardianInfo before any traffic, so the commit is always available.
  • The provisioner's current-session get_info is HistoricalLog (permissive); the authoritative "expected build" gate is the relay precheck it submits the share through.

🤖 Generated with Claude Code

mskd12 and others added 3 commits June 9, 2026 17:04
Replace the single config PCR0 with a PcrAllowlist (git revision -> BuildPcrs):
verification resolves the entry named by the session's signature-verified
untrusted_git_revision and pins the attestation's PCR0 to it. Listing multiple
builds lets an upgrade's old+new images both verify; an unknown build is rejected.

- verify_enclave_attestation takes (allowlist, git_revision) and does the lookup
  inside the prod branch, so non-enclave-dev stays a complete no-op.
- Consolidate the S3 read path: get_verified_enclave_pubkey -> get_enclave_identity,
  returning a verified EnclaveIdentity { signing_pubkey, info }. The session cache
  holds the identity, so get_info is now a cache hit instead of a second read.
- Config: expected_pcr0 (String) -> expected_builds (git rev -> PCR0 hex), with a
  pcr_allowlist() accessor; provisioner + monitor + sample YAMLs + README updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the HashMap with { expected_build, prev_build: Option }, type-encoding
the 1..=2 cap (one mandatory + one optional) so 0 or 3+ builds are unrepresentable
and rot is impossible — a retired build can't linger across upgrades. expected_build
is the build we run; prev_build is the outgoing image during an upgrade window.

- BuildPcrs carries its git_revision; resolve() scans the two fields.
- New BuildPcrsConfig (git_revision + pcr0 hex) shared by both configs; YAML gains
  expected_build/prev_build named fields (a flat map couldn't label which is which).
- Drop now-unused hex dep from hashi-monitor (hex decode lives in hashi-types).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
resolve() takes an AttestationSource: the live enclave (relay GetGuardianInfo,
hashi-node queries) must run expected_build, while historical S3 logs may also
come from the outgoing prev_build during an upgrade. So you can never provision
or act on a stale enclave, but prior state written by the outgoing build still
verifies. The source is consumed inside verify_enclave_attestation, so dev stays
a complete no-op.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant