Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

crates/nornir-tpm/src/lib.rs

//! **A pure-Rust TPM 2.0 client** — enough of the wire protocol to *seal* the
//! appliance's WireGuard private key to the platform's boot state and *unseal*
//! it again on a boot that measures the same.
//!
//! # Why this exists at all
//!
//! Until 2026-08-16 the appliance's WireGuard private key was base64 on an
//! unencrypted FAT partition, protected by `0600` and nothing else. Pull the
//! drive and you *are* that appliance to the hub — the real key, no forgery
//! needed. The design notes had always called for a TPM seal; nothing in the
//! tree had ever spoken to a TPM. This module is that missing half.
//!
//! # Why it is hand-marshalled
//!
//! Every TPM 2.0 binding in the Rust ecosystem is a wrapper around the C
//! `tss2` stack. This crate hand-rolls Authenticode, GPT, FAT, md RAID1
//! superblocks and ISO-9660 precisely so the appliance links **no C** it did
//! not write; adding `tss2` for six commands would be the largest foreign
//! surface in the whole boot path. The TPM interface is a byte protocol over a
//! character device — `write(2)` a command, `read(2)` a response — and the
//! command set below is six kilobytes of struct marshalling.
//!
//! ```text
//!   command   tag(u16) size(u32) code(u32) [handles] [authArea] [parameters]
//!   response  tag(u16) size(u32) rc(u32)   [handles] [paramSize] [parameters] [authArea]
//! ```
//!
//! # The seal, in one paragraph
//!
//! `CreatePrimary` on the **owner hierarchy** derives a storage root key
//! deterministically from the TPM's own seed and the template — the same
//! template on the same chip always yields the same SRK, and *no other chip*
//! can produce it, which is what makes a stolen blob inert. A **trial**
//! session plus `PolicyPCR` computes the policy digest for the chosen PCR set
//! without authorising anything; that digest becomes the sealed object's
//! `authPolicy`. `Create` seals the secret under the SRK with that policy and
//! hands back an encrypted `outPrivate` + a plaintext `outPublic` — the only
//! two things that go to disk. To get the secret back, a **policy** session
//! runs the same `PolicyPCR` against the *live* PCRs and `Unseal` succeeds
//! only if the resulting digest equals the one baked into the object. A PCR
//! that moved is a policy digest that does not match, and the TPM refuses.
//!
//! # What this module deliberately does NOT do
//!
//! * No HMAC sessions, no parameter encryption. The secret crosses the
//!   kernel/TPM boundary in the clear on a bus an attacker with that access
//!   has already lost to. Salted-session encryption is a real hardening and it
//!   is a separate, provable change.
//! * No persistent handles (`EvictControl`). The SRK is re-derived on every
//!   call, which costs one `CreatePrimary` and owes the NV store nothing.
//! * No `Startup`/`Shutdown`. `/dev/tpmrm0` is the kernel's resource manager;
//!   the chip is already started by the time anything here runs.

use std::io::{Read, Write};
use std::path::{Path, PathBuf};

// ── the constants, verbatim from TPM 2.0 Part 2 ─────────────────────────────

/// `TPM_ST_NO_SESSIONS` — a command carrying no authorisation area.
const TPM_ST_NO_SESSIONS: u16 = 0x8001;
/// `TPM_ST_SESSIONS` — a command carrying one or more authorisation areas.
const TPM_ST_SESSIONS: u16 = 0x8002;

/// `TPM_RH_OWNER` — the storage hierarchy the SRK is derived under.
const TPM_RH_OWNER: u32 = 0x4000_0001;
/// `TPM_RH_NULL` — "no object", used for the unsalted/unbound session.
const TPM_RH_NULL: u32 = 0x4000_0007;
/// `TPM_RS_PW` — the password "session" that is not a session at all.
const TPM_RS_PW: u32 = 0x4000_0009;

const CC_CREATE_PRIMARY: u32 = 0x0000_0131;
const CC_CREATE: u32 = 0x0000_0153;
const CC_LOAD: u32 = 0x0000_0157;
/// `TPM2_Quote` — the chip SIGNS a statement about its own PCRs. The whole of
/// remote attestation is this one command plus a key the verifier already
/// knows.
const CC_QUOTE: u32 = 0x0000_0158;
/// `TPM2_NV_UndefineSpace`.
const CC_NV_UNDEFINE_SPACE: u32 = 0x0000_0122;
/// `TPM2_NV_DefineSpace`.
const CC_NV_DEFINE_SPACE: u32 = 0x0000_012A;
/// `TPM2_NV_Increment` — the monotonic step. The chip owns the "never goes
/// backwards" property; nothing above it has to.
const CC_NV_INCREMENT: u32 = 0x0000_0134;
/// `TPM2_NV_Read`.
const CC_NV_READ: u32 = 0x0000_014E;
/// `TPM2_NV_ReadPublic` — an index's declared shape, needed before an existing
/// index is adopted as somebody's rollback floor.
const CC_NV_READ_PUBLIC: u32 = 0x0000_0169;
const CC_UNSEAL: u32 = 0x0000_015E;
const CC_PCR_RESET: u32 = 0x0000_013D;
const CC_FLUSH_CONTEXT: u32 = 0x0000_0165;
const CC_START_AUTH_SESSION: u32 = 0x0000_0176;
const CC_GET_RANDOM: u32 = 0x0000_017B;
const CC_PCR_READ: u32 = 0x0000_017E;
const CC_POLICY_PCR: u32 = 0x0000_017F;
const CC_PCR_EXTEND: u32 = 0x0000_0182;
const CC_POLICY_GET_DIGEST: u32 = 0x0000_0189;

const ALG_SHA1: u16 = 0x0004;
const ALG_AES: u16 = 0x0006;
const ALG_KEYEDHASH: u16 = 0x0008;
/// `TPM_ALG_SHA256` — the one PCR bank and the one name algorithm this module
/// speaks. SHA-1 banks exist on every chip and are addressable by
/// [`PcrSelection`], but the policy this module seals under is SHA-256.
pub const ALG_SHA256: u16 = 0x000B;
const ALG_NULL: u16 = 0x0010;
/// `TPM_ALG_ECDSA` — the attestation key's signing scheme.
pub const ALG_ECDSA: u16 = 0x0018;
const ALG_ECC: u16 = 0x0023;
const ALG_CFB: u16 = 0x0043;

/// `TPM_GENERATED_VALUE` — the four bytes every structure the TPM *itself*
/// produced begins with, and the reason a restricted signing key cannot be
/// tricked into signing an attacker's blob: the chip refuses to sign external
/// data that starts with this, so anything carrying it was signed as an
/// attestation and nothing else. A verifier that does not check it has thrown
/// the property away.
pub const TPM_GENERATED_VALUE: u32 = 0xFF54_4347;
/// `TPM_ST_ATTEST_QUOTE` — the `type` field of a quote's `TPMS_ATTEST`.
pub const ST_ATTEST_QUOTE: u16 = 0x8018;

/// `TPM_ECC_NIST_P256`.
const ECC_NIST_P256: u16 = 0x0003;

/// `TPM_SE_POLICY` — a session whose digest actually authorises.
const SE_POLICY: u8 = 0x01;
/// `TPM_SE_TRIAL` — a session that computes a policy digest and authorises
/// nothing. This is how a policy is *learned* before anything is sealed to it.
const SE_TRIAL: u8 = 0x03;

/// The sealed object's attributes: `fixedTPM | fixedParent`.
///
/// `userWithAuth` is deliberately **CLEAR**. With it set, the USER role could
/// be satisfied by the object's (empty) auth value and the PCR policy would be
/// decoration — anyone could unseal by presenting no password at all. Clear,
/// the only road to USER authorisation is the policy session, which is the
/// entire security property. `sensitiveDataOrigin` is clear too, because the
/// caller supplies the data rather than the TPM generating it.
const SEALED_OBJECT_ATTRS: u32 = 0x0000_0002 | 0x0000_0010;

/// The SRK template's attributes: `fixedTPM | fixedParent |
/// sensitiveDataOrigin | userWithAuth | noDA | restricted | decrypt` — the
/// TCG-profile storage-key shape.
const SRK_ATTRS: u32 = 0x0003_0472;

/// The attestation key's attributes: the SRK's shape with **`sign` where the
/// SRK has `decrypt`** — `fixedTPM | fixedParent | sensitiveDataOrigin |
/// userWithAuth | noDA | restricted | sign`.
///
/// `restricted` is the bit that matters. A restricted signing key will only
/// sign data the TPM produced itself (external input beginning with
/// [`TPM_GENERATED_VALUE`] is refused outright), so a quote signed by this key
/// cannot be a blob an attacker handed the chip and asked it to sign. Clearing
/// `restricted` would leave a key that signs anything, and every verification
/// in [`crate::attest`] would still pass.
///
/// `fixedTPM | fixedParent` means the private half can never leave this chip
/// and cannot be duplicated to another — which is what makes "this key" and
/// "this machine" the same statement.
const AK_ATTRS: u32 = 0x0005_0472;

/// The largest response `/dev/tpmrm0` will hand back in one read.
const MAX_RESPONSE: usize = 4096;

/// The default character device: the kernel's **resource manager**, which
/// virtualises handles so a caller cannot exhaust the chip's slots.
pub const TPM_RM_DEVICE: &str = "/dev/tpmrm0";
/// The raw device, tried only when the resource manager is absent.
pub const TPM_RAW_DEVICE: &str = "/dev/tpm0";

// ── errors ──────────────────────────────────────────────────────────────────

/// Why a TPM operation refused. Every variant names the device, the command or
/// the field that failed, because the operator's next move is always "go look
/// at exactly that".
#[derive(Debug)]
pub enum TpmError {
    /// No TPM character device could be opened. Carries every path tried.
    NoDevice {
        /// The paths that were tried, in order.
        tried: Vec<PathBuf>,
        /// The error from the last attempt.
        last: std::io::Error,
    },
    /// The character device refused a read or a write.
    Io {
        /// What was being attempted.
        op: &'static str,
        /// The device.
        path: PathBuf,
        /// The OS error, verbatim.
        source: std::io::Error,
    },
    /// The chip answered with a non-zero response code.
    Rc {
        /// The command that was rejected, by name.
        command: &'static str,
        /// The raw `TPM_RC`.
        rc: u32,
        /// The spec's name for it, where this module knows one.
        name: &'static str,
    },
    /// The response was structurally wrong — truncated, a bad tag, a length
    /// field that runs past the buffer. Never a panic: this module parses
    /// bytes that a broken chip or a hostile emulator supplies.
    Malformed {
        /// The command whose response could not be parsed.
        command: &'static str,
        /// What was wrong.
        reason: String,
    },
}

impl std::fmt::Display for TpmError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            TpmError::NoDevice { tried, last } => {
                let list: Vec<String> = tried.iter().map(|p| p.display().to_string()).collect();
                write!(
                    f,
                    "no TPM 2.0 device: tried {} ({last}) — this machine has no TPM, or its \
                     driver is not in the kernel that is running",
                    list.join(", ")
                )
            }
            TpmError::Io { op, path, source } => {
                write!(f, "tpm: {op} {}: {source}", path.display())
            }
            TpmError::Rc { command, rc, name } => {
                write!(f, "tpm: {command} refused with TPM_RC {rc:#010x} ({name})")
            }
            TpmError::Malformed { command, reason } => {
                write!(f, "tpm: {command} response is malformed: {reason}")
            }
        }
    }
}

impl std::error::Error for TpmError {}

impl TpmError {
    /// `true` when the chip said the policy did not match — the *expected*
    /// refusal on a machine whose boot state moved, and the one a caller wants
    /// to tell apart from "no TPM" or "corrupt blob".
    ///
    /// `TPM_RC_POLICY_FAIL` is the digest mismatch itself; `TPM_RC_INTEGRITY`
    /// is what a blob sealed under a *different* SRK produces at `Load`, which
    /// is the stolen-disk case.
    pub fn is_policy_refusal(&self) -> bool {
        matches!(self, TpmError::Rc { rc, .. } if rc_base(*rc) == 0x09D || rc_base(*rc) == 0x09F || rc_base(*rc) == 0x0A2)
    }

    /// `true` when the chip said there is no such NV index — the ordinary
    /// answer on a machine that has never had one defined, and the ONE case
    /// [`Tpm::nv_ensure_counter`] may treat as "create it". Every other refusal
    /// (a chip in lockout, an authorisation failure) must not look like a fresh
    /// machine, or a locked-out TPM would silently get a brand-new floor at
    /// zero.
    pub fn is_undefined_nv_index(&self) -> bool {
        matches!(self, TpmError::Rc { rc, .. } if rc_base(*rc) == 0x08B)
    }
}

/// Strip the format-1 handle/parameter/session decoration off a response code
/// so the underlying error can be compared. Format-1 codes set bit 7.
fn rc_base(rc: u32) -> u32 {
    if rc & 0x80 != 0 {
        rc & 0x0BF
    } else {
        rc & 0x0FFF
    }
}

/// The spec's name for a response code, for the ones this path can actually
/// produce. Anything unknown renders as the raw hex, never as a guess.
fn rc_name(rc: u32) -> &'static str {
    match rc_base(rc) {
        0x081 => "TPM_RC_ASYMMETRIC",
        0x082 => "TPM_RC_ATTRIBUTES",
        0x083 => "TPM_RC_HASH",
        0x084 => "TPM_RC_VALUE",
        0x085 => "TPM_RC_HIERARCHY",
        0x08A => "TPM_RC_TYPE",
        0x08B => "TPM_RC_HANDLE",
        0x08E => "TPM_RC_AUTH_FAIL",
        0x08F => "TPM_RC_NONCE",
        0x092 => "TPM_RC_SCHEME",
        0x095 => "TPM_RC_SIZE",
        0x097 => "TPM_RC_TAG",
        0x098 => "TPM_RC_SELECTOR",
        0x09B => "TPM_RC_SIGNATURE",
        0x09C => "TPM_RC_KEY",
        0x09D => "TPM_RC_POLICY_FAIL",
        0x09F => "TPM_RC_INTEGRITY",
        0x0A1 => "TPM_RC_RESERVED_BITS",
        0x0A2 => "TPM_RC_BAD_AUTH",
        0x100 => "TPM_RC_INITIALIZE",
        0x101 => "TPM_RC_FAILURE",
        0x103 => "TPM_RC_SEQUENCE",
        0x10B => "TPM_RC_PRIVATE",
        0x119 => "TPM_RC_HMAC",
        0x120 => "TPM_RC_DISABLED",
        0x124 => "TPM_RC_AUTH_TYPE",
        0x125 => "TPM_RC_AUTH_MISSING",
        0x126 => "TPM_RC_POLICY",
        0x127 => "TPM_RC_PCR",
        0x128 => "TPM_RC_PCR_CHANGED",
        0x142 => "TPM_RC_COMMAND_SIZE",
        0x143 => "TPM_RC_COMMAND_CODE",
        0x144 => "TPM_RC_AUTHSIZE",
        0x145 => "TPM_RC_AUTH_CONTEXT",
        0x146 => "TPM_RC_NV_RANGE",
        0x147 => "TPM_RC_NV_SIZE",
        0x148 => "TPM_RC_NV_LOCKED",
        0x149 => "TPM_RC_NV_AUTHORIZATION",
        0x14A => "TPM_RC_NV_UNINITIALIZED",
        0x14B => "TPM_RC_NV_SPACE",
        0x14C => "TPM_RC_NV_DEFINED",
        0x901 => "TPM_RC_CONTEXT_GAP",
        0x902 => "TPM_RC_OBJECT_MEMORY",
        0x903 => "TPM_RC_SESSION_MEMORY",
        0x904 => "TPM_RC_MEMORY",
        0x905 => "TPM_RC_SESSION_HANDLES",
        0x906 => "TPM_RC_OBJECT_HANDLES",
        0x921 => "TPM_RC_LOCKOUT",
        0x922 => "TPM_RC_RETRY",
        _ => "unnamed in this client",
    }
}

// ── marshalling ─────────────────────────────────────────────────────────────

/// A big-endian command builder. TPM structures are all big-endian, so every
/// integer goes out through here rather than through a `to_be_bytes` scattered
/// across nine call sites.
#[derive(Default)]
struct Buf(Vec<u8>);

impl Buf {
    fn new() -> Buf {
        Buf(Vec::with_capacity(256))
    }
    fn u8(&mut self, v: u8) -> &mut Buf {
        self.0.push(v);
        self
    }
    fn u16(&mut self, v: u16) -> &mut Buf {
        self.0.extend_from_slice(&v.to_be_bytes());
        self
    }
    fn u32(&mut self, v: u32) -> &mut Buf {
        self.0.extend_from_slice(&v.to_be_bytes());
        self
    }
    fn raw(&mut self, v: &[u8]) -> &mut Buf {
        self.0.extend_from_slice(v);
        self
    }
    /// A `TPM2B_*`: a `u16` byte count followed by that many bytes.
    fn tpm2b(&mut self, v: &[u8]) -> &mut Buf {
        // A TPM2B longer than u16::MAX cannot be expressed; nothing this module
        // sends comes close (the biggest is a 32-byte digest), so a truncating
        // cast here would be a silent corruption of a field nobody would
        // suspect. Refuse to build it instead.
        assert!(v.len() <= u16::MAX as usize, "TPM2B of {} bytes cannot be marshalled", v.len());
        self.u16(v.len() as u16);
        self.raw(v)
    }
    /// An empty `TPM2B_*` (`size = 0`) — the shape of "no nonce", "no auth",
    /// "no outside info".
    fn empty2b(&mut self) -> &mut Buf {
        self.u16(0)
    }
    /// The nine-byte password authorisation area: `TPM_RS_PW`, an empty nonce,
    /// `continueSession`, an empty HMAC — wrapped in its own `u32` size, which
    /// is what makes it an *area* rather than a session.
    fn password_auth(&mut self) -> &mut Buf {
        self.u32(9);
        self.u32(TPM_RS_PW);
        self.empty2b(); // nonceCaller
        self.u8(0x01); // sessionAttributes: continueSession
        self.empty2b() // hmac
    }
    fn into_vec(self) -> Vec<u8> {
        self.0
    }
    fn len(&self) -> usize {
        self.0.len()
    }
}

/// A bounds-checked big-endian reader. Every `take` is checked, so a truncated
/// or hostile response yields a named refusal instead of a slice panic — the
/// same discipline `loader::decode_hex32` was fixed into on 2026-08-16, and for
/// the same reason: this code runs as PID 1, where a panic is a kernel panic.
///
/// `pub` because `korp-installer`'s `attest` module parses a `TPMS_ATTEST` the
/// chip signed — the same big-endian, length-prefixed grammar, arriving from
/// the same place — and a second bounds-checked reader would be forty lines of
/// twin whose two copies could drift apart on exactly the check that matters.
/// It was `pub(crate)` while `attest` lived in the same crate; the 2026-09-20
/// move to `nornir-tpm` is what widened it, and the reason is unchanged.
pub struct Reader<'a> {
    bytes: &'a [u8],
    pos: usize,
    command: &'static str,
}

impl<'a> Reader<'a> {
    pub fn new(bytes: &'a [u8], command: &'static str) -> Reader<'a> {
        Reader { bytes, pos: 0, command }
    }
    pub fn err<T>(&self, reason: String) -> Result<T, TpmError> {
        Err(TpmError::Malformed { command: self.command, reason })
    }
    /// How many bytes have not been consumed. A caller that must refuse
    /// TRAILING bytes needs this: a `TPMS_ATTEST` with an extra field appended
    /// parses cleanly and means something else.
    pub fn remaining(&self) -> usize {
        self.bytes.len().saturating_sub(self.pos)
    }
    pub fn u64(&mut self) -> Result<u64, TpmError> {
        let b = self.take(8)?;
        Ok(u64::from_be_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]]))
    }
    fn take(&mut self, n: usize) -> Result<&'a [u8], TpmError> {
        let end = self.pos.checked_add(n).unwrap_or(usize::MAX);
        if end > self.bytes.len() {
            return self.err(format!(
                "wanted {n} bytes at offset {} but the response is {} bytes",
                self.pos,
                self.bytes.len()
            ));
        }
        let out = &self.bytes[self.pos..end];
        self.pos = end;
        Ok(out)
    }
    pub fn u8(&mut self) -> Result<u8, TpmError> {
        Ok(self.take(1)?[0])
    }
    pub fn u16(&mut self) -> Result<u16, TpmError> {
        let b = self.take(2)?;
        Ok(u16::from_be_bytes([b[0], b[1]]))
    }
    pub fn u32(&mut self) -> Result<u32, TpmError> {
        let b = self.take(4)?;
        Ok(u32::from_be_bytes([b[0], b[1], b[2], b[3]]))
    }
    /// Read a `TPM2B_*` and hand back its body.
    pub fn tpm2b(&mut self) -> Result<&'a [u8], TpmError> {
        let n = self.u16()? as usize;
        self.take(n)
    }
}

// ── PCR selection ───────────────────────────────────────────────────────────

/// Which PCRs a seal is bound to, in which bank.
///
/// The mask is a bitmap over PCR indices 0–23, bit *n* being PCR *n*, which is
/// exactly how `TPML_PCR_SELECTION` spells it on the wire — so the value that
/// is written into a sealed blob's header and the value that goes to the chip
/// are the same integer, and there is no second encoding to disagree with.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PcrSelection {
    /// The bank — `TPM_ALG_SHA256` in every shape this crate ships.
    pub alg: u16,
    /// Bitmap over PCR 0–23.
    pub mask: u32,
}

impl PcrSelection {
    /// The default binding: **PCR 0 and PCR 7** in the SHA-256 bank.
    ///
    /// PCR 0 is the firmware code — a stolen disk in a different machine
    /// measures a different one. PCR 7 is the Secure Boot policy — turning
    /// Secure Boot off, or enrolling a foreign key, moves it.
    ///
    /// # This choice is MEASURED, not reasoned
    ///
    /// The appliance seals while running off the *installer ISO* and unseals
    /// while running off the *installed disk*. Two boot chains, one firmware. A
    /// PCR that differs between them makes every machine install cleanly and
    /// then refuse its own identity on first boot. `tests/tpm_kvm_pcrs.rs`
    /// boots OVMF twice against the same emulated chip and reads the values
    /// back; on oden, 2026-08-16 (OVMF_CODE_4M, swtpm 0.10.1):
    ///
    /// ```text
    ///             from a CD-ROM        from a hard disk
    ///   PCR 0  012103E0F956C315…    012103E0F956C315…   SAME
    ///   PCR 1  91FE52953A8C078E…    A339966DDE8F6FE2…   DIFFERS
    ///   PCR 2  0AF5E579EDCC84BE…    0AF5E579EDCC84BE…   same
    ///   PCR 7  B5710BF57D25623E…    B5710BF57D25623E…   SAME
    /// ```
    ///
    /// **PCR 1 is why this is not "PCRs 0–7".** It is the platform
    /// *configuration* PCR and the attached media is part of that
    /// configuration, so the binding that reads like the careful choice is the
    /// one that breaks every appliance.
    ///
    /// # PCR 4 is deliberately absent, and that is a real limitation
    ///
    /// PCR 4 measures the boot-loader code. The two boots above agree on it
    /// only because neither found anything to boot, so that agreement proves
    /// nothing; on a real install-then-boot transition it will differ. Sealing
    /// to 4 needs a **re-seal on the first successful boot**, which is a
    /// separate, provable change. The honest statement today is that this seal
    /// binds to *this machine's firmware and Secure Boot state*, and not to
    /// *which loader ran*.
    pub const DEFAULT: PcrSelection = PcrSelection { alg: ALG_SHA256, mask: (1 << 0) | (1 << 7) };

    /// The PCR indices this selection covers, ascending.
    pub fn indices(&self) -> Vec<u32> {
        (0..24).filter(|i| self.mask & (1 << i) != 0).collect()
    }

    /// `true` when no PCR at all is selected — a "policy" that binds to
    /// nothing and would unseal in any state. Refused by [`Tpm::seal`].
    pub fn is_empty(&self) -> bool {
        self.mask & 0x00FF_FFFF == 0
    }

    /// The bank's name, for console lines.
    pub fn bank(&self) -> &'static str {
        match self.alg {
            ALG_SHA256 => "sha256",
            ALG_SHA1 => "sha1",
            _ => "unknown-bank",
        }
    }

    /// [`PcrSelection::DEFAULT`] **plus PCR 4** — the binding an *installed,
    /// running* appliance holds its identity under.
    ///
    /// PCR 4 is where the firmware measures the PE it loaded, so this is the
    /// difference between "this machine's firmware and Secure Boot state" and
    /// "**this loader** on this machine". Without it a substituted korp-boot,
    /// signed by a key the firmware still trusts or run with Secure Boot
    /// satisfied some other way, unseals the appliance's identity.
    ///
    /// It cannot be the install-time binding, because the installer measures
    /// its OWN loader and the installed system measures a different one. It is
    /// reached by a re-seal on the first successful boot — see
    /// [`crate::wgseal::reseal_to`].
    ///
    /// PCR 1 stays out for the reason [`PcrSelection::DEFAULT`] documents, and
    /// that is measured, not reasoned.
    pub const WITH_LOADER: PcrSelection =
        PcrSelection { alg: ALG_SHA256, mask: (1 << 0) | (1 << 4) | (1 << 7) };

    /// Marshal as `TPML_PCR_SELECTION` (one selection, `sizeofSelect = 3`).
    fn marshal(&self, b: &mut Buf) {
        b.u32(1);
        b.u16(self.alg);
        b.u8(3);
        b.u8((self.mask & 0xFF) as u8);
        b.u8(((self.mask >> 8) & 0xFF) as u8);
        b.u8(((self.mask >> 16) & 0xFF) as u8);
    }

    /// Parse the operator's spelling: `sha256:0,7` or bare `0,7` (SHA-256
    /// assumed). Refuses by name — this string can arrive off the kernel
    /// command line, i.e. off the plaintext ESP.
    pub fn parse(s: &str) -> Result<PcrSelection, String> {
        let s = s.trim();
        let (alg, list) = match s.split_once(':') {
            Some((bank, rest)) => {
                let alg = match bank.trim().to_ascii_lowercase().as_str() {
                    "sha256" => ALG_SHA256,
                    "sha1" => ALG_SHA1,
                    other => {
                        return Err(format!(
                            "'{other}' is not a PCR bank this client speaks (sha256, sha1)"
                        ))
                    }
                };
                (alg, rest)
            }
            None => (ALG_SHA256, s),
        };
        let mut mask = 0u32;
        let list = list.trim();
        if list.is_empty() {
            return Err("no PCR indices given — a seal bound to no PCR unseals in any state".into());
        }
        for part in list.split(',') {
            let part = part.trim();
            let n: u32 = part
                .parse()
                .map_err(|_| format!("'{part}' is not a PCR index (0–23)"))?;
            if n > 23 {
                return Err(format!("PCR {n} is out of range — the platform has 0–23"));
            }
            mask |= 1 << n;
        }
        Ok(PcrSelection { alg, mask })
    }
}

impl std::fmt::Display for PcrSelection {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let idx: Vec<String> = self.indices().iter().map(|i| i.to_string()).collect();
        write!(f, "{}:{}", self.bank(), idx.join(","))
    }
}

// ── the sealed blob, as it lands on disk ────────────────────────────────────

/// The magic that opens a sealed-key file. Eight bytes, so a file that is not
/// one is refused before a single length field is believed.
pub const SEAL_MAGIC: &[u8; 8] = b"KORPSEAL";
/// The on-disk format version. Bumping it is a breaking change to every
/// installed appliance, so it is checked exactly, never `>=`.
pub const SEAL_VERSION: u16 = 1;

/// What `Create` handed back, in the form that goes on the partition.
///
/// `private` is encrypted **to this chip's SRK**; `public` is the object's
/// public area including its `authPolicy`. Neither is a secret — the whole
/// point of a seal is that the file is inert off the machine that made it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SealedBlob {
    /// The PCR set the seal is bound to, carried so the unsealer does not have
    /// to be told separately — a blob and its policy travel together or the
    /// policy is a second source of truth waiting to disagree.
    pub pcrs: PcrSelection,
    /// `TPM2B_PRIVATE` body: the encrypted sensitive area.
    pub private: Vec<u8>,
    /// `TPM2B_PUBLIC` body: the `TPMT_PUBLIC`, carrying the `authPolicy`.
    pub public: Vec<u8>,
}

/// Why a sealed-key file was refused. Distinct from [`TpmError`] on purpose:
/// "this file is not a seal" and "this chip will not open this seal" are
/// different operator problems with different next moves.
#[derive(Debug, PartialEq, Eq)]
pub enum BlobError {
    /// The file does not begin with [`SEAL_MAGIC`].
    NotASeal {
        /// The first bytes found, rendered readably.
        found: String,
    },
    /// A format version this build does not speak.
    Version {
        /// What the file claims.
        found: u16,
    },
    /// A length field runs past the end of the file, or the file is short.
    Truncated {
        /// The field being read.
        field: &'static str,
        /// How many bytes it wanted.
        wanted: usize,
        /// How many were left.
        available: usize,
    },
    /// The blake3 checksum over the body does not match the header's.
    Checksum,
    /// The header's PCR selection is empty or in an unknown bank.
    BadSelection {
        /// Why.
        reason: String,
    },
}

impl std::fmt::Display for BlobError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            BlobError::NotASeal { found } => write!(
                f,
                "not a sealed key file: expected the magic KORPSEAL, found {found} — refusing \
                 to guess at bytes that are not this format"
            ),
            BlobError::Version { found } => write!(
                f,
                "sealed key file is format version {found}, this build speaks {SEAL_VERSION} \
                 — refusing rather than mis-parsing a layout it does not know"
            ),
            BlobError::Truncated { field, wanted, available } => write!(
                f,
                "sealed key file is truncated: field '{field}' wants {wanted} bytes, {available} \
                 remain"
            ),
            BlobError::Checksum => write!(
                f,
                "sealed key file fails its own blake3 checksum — the bytes changed since they \
                 were sealed; this is corruption, not a policy refusal"
            ),
            BlobError::BadSelection { reason } => {
                write!(f, "sealed key file's PCR selection is unusable: {reason}")
            }
        }
    }
}

impl std::error::Error for BlobError {}

impl SealedBlob {
    /// Render the blob as the bytes that go on the partition.
    ///
    /// ```text
    ///   0  8   magic "KORPSEAL"
    ///   8  2   version (u16 BE)
    ///  10  2   pcr bank alg (u16 BE)
    ///  12  4   pcr mask (u32 BE)
    ///  16 32   blake3 of everything from offset 48 on
    ///  48  2   private length      50 ..  private
    ///   +  2   public  length       +  ..  public
    /// ```
    ///
    /// The checksum is not security — a `private` an attacker rewrites is
    /// refused by the *chip*, at `Load`, with `TPM_RC_INTEGRITY`. It is here so
    /// that "this file rotted on a FAT partition" and "this chip is not the one
    /// that sealed it" are told apart by name instead of both arriving as one
    /// opaque TPM refusal.
    pub fn encode(&self) -> Vec<u8> {
        let mut body = Buf::new();
        body.tpm2b(&self.private);
        body.tpm2b(&self.public);
        let body = body.into_vec();
        let mut out = Buf::new();
        out.raw(SEAL_MAGIC);
        out.u16(SEAL_VERSION);
        out.u16(self.pcrs.alg);
        out.u32(self.pcrs.mask);
        out.raw(blake3::hash(&body).as_bytes());
        out.raw(&body);
        out.into_vec()
    }

    /// Parse a sealed-key file. **Every** length is bounds-checked and the
    /// function is total — `korp-start` feeds it a file off a FAT partition
    /// while running as PID 1, where a panic is a kernel panic.
    pub fn decode(bytes: &[u8]) -> Result<SealedBlob, BlobError> {
        let need = |field: &'static str, wanted: usize, available: usize| BlobError::Truncated {
            field,
            wanted,
            available,
        };
        if bytes.len() < 8 || &bytes[..8] != SEAL_MAGIC {
            let head: String = bytes
                .iter()
                .take(8)
                .map(|b| {
                    if b.is_ascii_graphic() {
                        (*b as char).to_string()
                    } else {
                        format!("\\x{b:02x}")
                    }
                })
                .collect();
            return Err(BlobError::NotASeal { found: format!("'{head}'") });
        }
        if bytes.len() < 48 {
            return Err(need("header", 48, bytes.len()));
        }
        let version = u16::from_be_bytes([bytes[8], bytes[9]]);
        if version != SEAL_VERSION {
            return Err(BlobError::Version { found: version });
        }
        let alg = u16::from_be_bytes([bytes[10], bytes[11]]);
        let mask = u32::from_be_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]);
        let sum: [u8; 32] = bytes[16..48].try_into().expect("48 - 16 is 32");
        let body = &bytes[48..];
        if blake3::hash(body).as_bytes() != &sum {
            return Err(BlobError::Checksum);
        }
        // Bounds-checked walk of the two TPM2Bs.
        let mut pos = 0usize;
        let field = |name: &'static str, pos: &mut usize| -> Result<Vec<u8>, BlobError> {
            if body.len() < *pos + 2 {
                return Err(need(name, 2, body.len().saturating_sub(*pos)));
            }
            let n = u16::from_be_bytes([body[*pos], body[*pos + 1]]) as usize;
            *pos += 2;
            if body.len() < *pos + n {
                return Err(need(name, n, body.len().saturating_sub(*pos)));
            }
            let out = body[*pos..*pos + n].to_vec();
            *pos += n;
            Ok(out)
        };
        let private = field("private", &mut pos)?;
        let public = field("public", &mut pos)?;
        let pcrs = PcrSelection { alg, mask };
        if pcrs.is_empty() {
            return Err(BlobError::BadSelection {
                reason: "no PCR is selected — a seal bound to nothing would unseal in any boot \
                         state, which is the failure this whole mechanism exists to prevent"
                    .into(),
            });
        }
        if alg != ALG_SHA256 && alg != ALG_SHA1 {
            return Err(BlobError::BadSelection {
                reason: format!("bank {alg:#06x} is not one this client speaks"),
            });
        }
        Ok(SealedBlob { pcrs, private, public })
    }
}

/// A secret that zeroes itself. Not a substitute for a real secrets crate, but
/// it means the unsealed WireGuard private key is not left lying in a freed
/// heap page for the rest of the boot.
pub struct Secret(Vec<u8>);

impl Secret {
    /// The bytes, for as long as the guard lives.
    pub fn as_bytes(&self) -> &[u8] {
        &self.0
    }
    /// Length, without exposing the bytes.
    pub fn len(&self) -> usize {
        self.0.len()
    }
    /// `true` when the unsealed payload was empty.
    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }
}

impl Drop for Secret {
    fn drop(&mut self) {
        // A plain loop, through a volatile write so the optimiser may not
        // decide that storing into a dying allocation is dead code.
        for b in self.0.iter_mut() {
            unsafe { std::ptr::write_volatile(b, 0) };
        }
    }
}

impl std::fmt::Debug for Secret {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Secret({} bytes, redacted)", self.0.len())
    }
}

// ── attestation types ───────────────────────────────────────────────────────

/// The attributes an NV **monotonic counter** is defined with: `OWNERWRITE |
/// OWNERREAD | TPM_NT_COUNTER | NO_DA`.
///
/// `TPM_NT_COUNTER` (the 4-bit type field at bits 4–7) is the whole point — the
/// chip refuses any write other than an increment, so a rollback floor kept
/// here cannot be walked backwards by anyone who holds the disk. `NO_DA` keeps
/// a failed authorisation from arming the dictionary-attack lockout and
/// bricking the boot path for an hour.
const NV_COUNTER_ATTRS: u32 = 0x0000_0002 | 0x0002_0000 | (1 << 4) | 0x0200_0000;

/// A P-256 public point — the attestation key as a verifier sees it.
///
/// This is the ONLY thing about the AK that ever leaves the chip. The private
/// half is `fixedTPM`, so it has no on-disk form to steal and no way to be
/// duplicated onto a second machine.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AkPublic {
    /// The affine x coordinate, 32 bytes big-endian.
    pub x: [u8; 32],
    /// The affine y coordinate, 32 bytes big-endian.
    pub y: [u8; 32],
}

impl AkPublic {
    /// SEC1 uncompressed encoding (`0x04 ‖ x ‖ y`) — what every ECDSA verifier
    /// takes, and the form the hub pins.
    pub fn sec1(&self) -> [u8; 65] {
        let mut out = [0u8; 65];
        out[0] = 0x04;
        out[1..33].copy_from_slice(&self.x);
        out[33..65].copy_from_slice(&self.y);
        out
    }

    /// Parse a SEC1 uncompressed point back, refusing anything else by name —
    /// these bytes come off an operator's clipboard.
    pub fn from_sec1(bytes: &[u8]) -> Result<AkPublic, String> {
        if bytes.len() != 65 || bytes[0] != 0x04 {
            return Err(format!(
                "an attestation key is 65 bytes beginning 0x04 (SEC1 uncompressed); this is {} \
                 bytes beginning {:#04x}",
                bytes.len(),
                bytes.first().copied().unwrap_or(0)
            ));
        }
        let mut k = AkPublic { x: [0; 32], y: [0; 32] };
        k.x.copy_from_slice(&bytes[1..33]);
        k.y.copy_from_slice(&bytes[33..65]);
        Ok(k)
    }

    /// The 130-hex-character spelling an operator carries to the hub: one line,
    /// no whitespace, nothing secret.
    pub fn to_hex(&self) -> String {
        hex(&self.sec1())
    }

    /// Parse [`AkPublic::to_hex`] back, refusing by name.
    pub fn from_hex(s: &str) -> Result<AkPublic, String> {
        let s = s.trim();
        if s.len() != 130 || !s.bytes().all(|b| b.is_ascii_hexdigit()) {
            return Err(format!(
                "an attestation key is 130 hex characters; got {} characters",
                s.len()
            ));
        }
        let mut bytes = [0u8; 65];
        for (i, b) in bytes.iter_mut().enumerate() {
            *b = u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).map_err(|e| e.to_string())?;
        }
        AkPublic::from_sec1(&bytes)
    }
}

/// An NV index's declared shape, as `TPM2_NV_ReadPublic` states it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NvPublic {
    /// The index.
    pub index: u32,
    /// The name algorithm.
    pub name_alg: u16,
    /// `TPMA_NV`. Bits 4–7 are the type — 1 is `TPM_NT_COUNTER`.
    pub attributes: u32,
    /// How many bytes the index holds. A counter is 8.
    pub data_size: u16,
}

impl NvPublic {
    /// `true` when this index is a monotonic counter — the only shape a
    /// rollback floor may live in.
    pub fn is_counter(&self) -> bool {
        (self.attributes >> 4) & 0xF == 1 && self.data_size == 8
    }
}

/// A monotonic counter, and whether this call is what created it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NvCounter {
    /// The NV index it lives at.
    pub index: u32,
    /// Its current value.
    pub value: u64,
    /// `true` when this call defined it — a FIRST install, not a later boot.
    /// A caller that treats every boot as a first install would reset the
    /// meaning of the floor it is protecting.
    pub created: bool,
}

/// A loaded attestation key: the chip-side handle plus the point that
/// identifies it. The handle must be flushed; the point is what travels.
#[derive(Debug)]
pub struct AttestationKey {
    /// The transient object handle inside the chip.
    pub handle: u32,
    /// The public point.
    pub public: AkPublic,
}

/// What `TPM2_Quote` handed back: the signed structure and the signature over
/// it. `quoted` is the marshalled `TPMS_ATTEST` **body** — the exact bytes the
/// signature covers, so a verifier hashes those and never something it
/// re-derived.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QuoteOutput {
    /// `TPM2B_ATTEST` body — a `TPMS_ATTEST`.
    pub quoted: Vec<u8>,
    /// ECDSA `r`, 32 bytes big-endian.
    pub sig_r: [u8; 32],
    /// ECDSA `s`, 32 bytes big-endian.
    pub sig_s: [u8; 32],
}

/// What a seal produced, with the evidence a console line needs.
#[derive(Debug)]
pub struct SealReport {
    /// The blob to write to disk.
    pub blob: SealedBlob,
    /// The policy digest the object was sealed under — the one number that
    /// says "this is the boot state".
    pub policy_digest: [u8; 32],
    /// The live PCR values at seal time, for the log.
    pub pcrs: Vec<(u32, Vec<u8>)>,
}

// ── the client ──────────────────────────────────────────────────────────────

/// Anything a TPM command can be written to and a response read back from.
///
/// Two shapes exist and they behave differently, which is the whole reason this
/// is a trait: a character device hands back exactly one response per `read(2)`,
/// while a socket is a byte stream that may deliver a response in pieces. The
/// framing loop in [`Tpm::transmit`] handles both by trusting only the size
/// field in the response header.
trait Wire: Read + Write + Send {}
impl<T: Read + Write + Send> Wire for T {}

/// An open TPM 2.0 device.
pub struct Tpm {
    dev: Box<dyn Wire>,
    path: PathBuf,
}

impl Tpm {
    /// Open a specific device node.
    pub fn open(path: &Path) -> Result<Tpm, TpmError> {
        let dev = std::fs::OpenOptions::new()
            .read(true)
            .write(true)
            .open(path)
            .map_err(|last| TpmError::NoDevice { tried: vec![path.to_path_buf()], last })?;
        Ok(Tpm { dev: Box::new(dev), path: path.to_path_buf() })
    }

    /// Connect to a TPM behind a **unix socket** — what `swtpm socket` presents,
    /// and therefore what a QEMU guest's emulated chip is on the host side.
    ///
    /// This is what makes the KVM proof possible at all: the *same* client code
    /// that runs on the appliance can talk to the *same* emulated chip a guest
    /// just booted against, so a key sealed while one thing was booted can be
    /// tested against the PCR state left by booting another.
    #[cfg(unix)]
    pub fn connect_unix(path: &Path) -> Result<Tpm, TpmError> {
        let sock = std::os::unix::net::UnixStream::connect(path)
            .map_err(|last| TpmError::NoDevice { tried: vec![path.to_path_buf()], last })?;
        // Without a timeout a client that mis-frames a command waits forever,
        // and a test that hangs is worse than one that fails.
        let _ = sock.set_read_timeout(Some(std::time::Duration::from_secs(30)));
        Ok(Tpm { dev: Box::new(sock), path: path.to_path_buf() })
    }

    /// Open the resource manager, falling back to the raw device.
    ///
    /// The fallback is not superstition: an initramfs that has `tpm_crb` but no
    /// `tpm_vtpm_proxy`-backed `/dev/tpmrm0` is a real kernel configuration,
    /// and refusing to seal on such a machine over a device *name* would be
    /// trading one distro assumption for another.
    pub fn open_default() -> Result<Tpm, TpmError> {
        let mut tried = Vec::new();
        let mut last = None;
        for p in [TPM_RM_DEVICE, TPM_RAW_DEVICE] {
            let path = PathBuf::from(p);
            match Tpm::open(&path) {
                Ok(t) => return Ok(t),
                Err(TpmError::NoDevice { last: e, .. }) => {
                    tried.push(path);
                    last = Some(e);
                }
                Err(other) => return Err(other),
            }
        }
        Err(TpmError::NoDevice {
            tried,
            last: last.unwrap_or_else(|| std::io::Error::other("no attempt was made")),
        })
    }

    /// Which device this client is talking to.
    pub fn device(&self) -> &Path {
        &self.path
    }

    /// Send one command, read one response, check the header and the response
    /// code. Returns the response **body** — everything after `rc`.
    fn transmit(&mut self, command: &'static str, cmd: Vec<u8>) -> Result<Vec<u8>, TpmError> {
        self.dev.write_all(&cmd).map_err(|source| TpmError::Io {
            op: "write a command to",
            path: self.path.clone(),
            source,
        })?;
        // Frame the response by its own size field rather than by "one read is
        // one message". That is true of a character device and NOT true of a
        // socket, where `swtpm` may hand back the header and the body in
        // separate segments — a client that assumed otherwise would report a
        // malformed response for a perfectly good one, intermittently.
        let mut buf: Vec<u8> = Vec::with_capacity(1024);
        let mut chunk = [0u8; MAX_RESPONSE];
        let declared = loop {
            if buf.len() >= 6 {
                let size = u32::from_be_bytes([buf[2], buf[3], buf[4], buf[5]]) as usize;
                if size < 10 || size > MAX_RESPONSE {
                    return Reader::new(&buf, command)
                        .err(format!("header declares a {size}-byte response, which is not one"));
                }
                if buf.len() >= size {
                    break size;
                }
            }
            let n = self.dev.read(&mut chunk).map_err(|source| TpmError::Io {
                op: "read a response from",
                path: self.path.clone(),
                source,
            })?;
            if n == 0 {
                return Reader::new(&buf, command).err(format!(
                    "the device closed after {} bytes without completing a response",
                    buf.len()
                ));
            }
            buf.extend_from_slice(&chunk[..n]);
        };
        if buf.len() != declared {
            return Reader::new(&buf, command).err(format!(
                "header declares {declared} bytes, the device handed back {}",
                buf.len()
            ));
        }
        let mut r = Reader::new(&buf, command);
        let tag = r.u16()?;
        if tag != TPM_ST_NO_SESSIONS && tag != TPM_ST_SESSIONS {
            return r.err(format!("response tag {tag:#06x} is neither NO_SESSIONS nor SESSIONS"));
        }
        let _size = r.u32()?; // already checked against the framing above
        let rc = r.u32()?;
        if rc != 0 {
            return Err(TpmError::Rc { command, rc, name: rc_name(rc) });
        }
        Ok(buf[10..].to_vec())
    }

    /// Build a command header around an already-marshalled tail.
    fn command(tag: u16, code: u32, tail: Buf) -> Vec<u8> {
        let mut out = Buf::new();
        out.u16(tag);
        out.u32((10 + tail.len()) as u32);
        out.u32(code);
        out.raw(&tail.into_vec());
        out.into_vec()
    }

    /// `TPM2_GetRandom` — used for session nonces, and the cheapest possible
    /// proof that the marshalling reaches a live chip.
    pub fn get_random(&mut self, n: u16) -> Result<Vec<u8>, TpmError> {
        let mut t = Buf::new();
        t.u16(n);
        let body = self.transmit("GetRandom", Tpm::command(TPM_ST_NO_SESSIONS, CC_GET_RANDOM, t))?;
        let mut r = Reader::new(&body, "GetRandom");
        Ok(r.tpm2b()?.to_vec())
    }

    /// `TPM2_PCR_Read` — the diagnostic that makes a policy refusal legible:
    /// the operator can see *which* PCR moved.
    pub fn pcr_read(&mut self, sel: &PcrSelection) -> Result<Vec<(u32, Vec<u8>)>, TpmError> {
        let mut t = Buf::new();
        sel.marshal(&mut t);
        let body = self.transmit("PCR_Read", Tpm::command(TPM_ST_NO_SESSIONS, CC_PCR_READ, t))?;
        let mut r = Reader::new(&body, "PCR_Read");
        let _update_counter = r.u32()?;
        // pcrSelectionOut — the chip echoes what it actually read, which can be
        // a SUBSET when a selection spans more than one read. Trust the echo,
        // not the request: pairing requested indices with returned digests
        // would silently mislabel every value after the first gap.
        let count = r.u32()?;
        let mut indices: Vec<u32> = Vec::new();
        for _ in 0..count {
            let _alg = r.u16()?;
            let size_of_select = r.u8()? as usize;
            let bits = r.take(size_of_select)?;
            for (byte, b) in bits.iter().enumerate() {
                for bit in 0..8u32 {
                    if b & (1 << bit) != 0 {
                        indices.push(byte as u32 * 8 + bit);
                    }
                }
            }
        }
        let digest_count = r.u32()? as usize;
        if digest_count != indices.len() {
            return r.err(format!(
                "the chip returned {digest_count} digests for {} selected PCRs",
                indices.len()
            ));
        }
        let mut out = Vec::with_capacity(digest_count);
        for idx in indices {
            out.push((idx, r.tpm2b()?.to_vec()));
        }
        Ok(out)
    }

    /// `TPM2_PCR_Extend` in the SHA-256 bank — how a test *moves* the boot
    /// state so a seal can be watched refusing.
    ///
    /// This is a real, irreversible change to the running platform's PCR: a PCR
    /// only resets on a platform reset. Nothing in the appliance's boot path
    /// calls it; it exists so the guard can be seen red.
    pub fn pcr_extend(&mut self, index: u32, digest: &[u8; 32]) -> Result<(), TpmError> {
        let mut t = Buf::new();
        t.u32(index);
        t.password_auth();
        t.u32(1); // one digest
        t.u16(ALG_SHA256);
        t.raw(digest);
        self.transmit("PCR_Extend", Tpm::command(TPM_ST_SESSIONS, CC_PCR_EXTEND, t))?;
        Ok(())
    }

    /// `TPM2_PCR_Reset` — put a *resettable* PCR back to zero.
    ///
    /// Only PCRs 16 and 23 are resettable from locality 0; the platform PCRs
    /// (0–7) are not, by design, and asking gets `TPM_RC_LOCALITY`. This exists
    /// so a red probe that extends a PCR to watch a seal refuse can leave the
    /// machine exactly as it found it — a proof that permanently changes the
    /// box it runs on is a proof nobody runs twice.
    pub fn pcr_reset(&mut self, index: u32) -> Result<(), TpmError> {
        let mut t = Buf::new();
        t.u32(index);
        t.password_auth();
        self.transmit("PCR_Reset", Tpm::command(TPM_ST_SESSIONS, CC_PCR_RESET, t))?;
        Ok(())
    }

    /// `TPM2_StartAuthSession` — unsalted (`tpmKey = TPM_RH_NULL`), unbound
    /// (`bind = TPM_RH_NULL`), no symmetric algorithm, SHA-256.
    fn start_auth_session(&mut self, kind: u8) -> Result<u32, TpmError> {
        // A caller nonce of at least the hash size, from the chip's own RNG.
        // The value does not carry weight without an HMAC session, but sending
        // a fixed one would make every session on the fleet identical, and the
        // spec requires a non-empty caller nonce here.
        let nonce = self.get_random(32)?;
        let mut t = Buf::new();
        t.u32(TPM_RH_NULL); // tpmKey — no salt
        t.u32(TPM_RH_NULL); // bind — no bound entity
        t.tpm2b(&nonce);
        t.empty2b(); // encryptedSalt
        t.u8(kind);
        t.u16(ALG_NULL); // symmetric: TPMT_SYM_DEF of TPM_ALG_NULL ends here
        t.u16(ALG_SHA256); // authHash
        let name = if kind == SE_TRIAL { "StartAuthSession(TRIAL)" } else { "StartAuthSession(POLICY)" };
        let body = self.transmit(name, Tpm::command(TPM_ST_NO_SESSIONS, CC_START_AUTH_SESSION, t))?;
        let mut r = Reader::new(&body, name);
        let handle = r.u32()?;
        let _nonce_tpm = r.tpm2b()?;
        Ok(handle)
    }

    /// `TPM2_PolicyPCR` with an **empty** `pcrDigest`, which means "bind to the
    /// PCRs as they are right now" — in a trial session that computes the
    /// digest, in a policy session that checks it.
    fn policy_pcr(&mut self, session: u32, sel: &PcrSelection) -> Result<(), TpmError> {
        let mut t = Buf::new();
        t.u32(session);
        t.empty2b(); // pcrDigest: empty ⇒ use the current values
        sel.marshal(&mut t);
        self.transmit("PolicyPCR", Tpm::command(TPM_ST_NO_SESSIONS, CC_POLICY_PCR, t))?;
        Ok(())
    }

    /// `TPM2_PolicyGetDigest` — read the digest a session has accumulated.
    fn policy_get_digest(&mut self, session: u32) -> Result<[u8; 32], TpmError> {
        let mut t = Buf::new();
        t.u32(session);
        let body =
            self.transmit("PolicyGetDigest", Tpm::command(TPM_ST_NO_SESSIONS, CC_POLICY_GET_DIGEST, t))?;
        let mut r = Reader::new(&body, "PolicyGetDigest");
        let d = r.tpm2b()?;
        <[u8; 32]>::try_from(d).map_err(|_| TpmError::Malformed {
            command: "PolicyGetDigest",
            reason: format!("policy digest is {} bytes, not the 32 SHA-256 must be", d.len()),
        })
    }

    /// `TPM2_FlushContext`. The handle is a **parameter**, not a handle-area
    /// entry — the one command in this module where that is true, and getting
    /// it wrong costs a `TPM_RC_COMMAND_SIZE` that looks like a marshalling bug
    /// somewhere else entirely.
    pub fn flush(&mut self, handle: u32) -> Result<(), TpmError> {
        let mut t = Buf::new();
        t.u32(handle);
        self.transmit("FlushContext", Tpm::command(TPM_ST_NO_SESSIONS, CC_FLUSH_CONTEXT, t))?;
        Ok(())
    }

    /// Flush without caring — for cleanup paths where the interesting error has
    /// already been captured and a second one would only bury it.
    fn flush_quietly(&mut self, handle: u32) {
        let _ = self.flush(handle);
    }

    /// Compute the policy digest for a PCR selection using a **trial** session,
    /// which authorises nothing and exists exactly for this.
    pub fn policy_digest_for(&mut self, sel: &PcrSelection) -> Result<[u8; 32], TpmError> {
        let session = self.start_auth_session(SE_TRIAL)?;
        let out = (|| {
            self.policy_pcr(session, sel)?;
            self.policy_get_digest(session)
        })();
        self.flush_quietly(session);
        out
    }

    /// `TPM2_CreatePrimary` on the owner hierarchy, ECC P-256 storage key.
    ///
    /// **Deterministic**: the TPM derives a primary key from its own hierarchy
    /// seed and the exact template bytes, so the same template on the same chip
    /// always yields the same key, and no other chip can. That is what makes a
    /// sealed blob machine-bound without any of it being written down. ECC
    /// rather than RSA because an RSA-2048 primary costs seconds of key
    /// generation on real hardware and this runs on every boot.
    fn create_primary_srk(&mut self) -> Result<u32, TpmError> {
        Ok(self.create_primary_srk_full()?.0)
    }

    /// [`Tpm::create_primary_srk`], keeping the `outPublic` the chip returned.
    fn create_primary_srk_full(&mut self) -> Result<(u32, Vec<u8>), TpmError> {
        let mut public = Buf::new();
        public.u16(ALG_ECC);
        public.u16(ALG_SHA256); // nameAlg
        public.u32(SRK_ATTRS);
        public.empty2b(); // authPolicy: none, the SRK is used with its (empty) auth
        // TPMS_ECC_PARMS
        public.u16(ALG_AES); // symmetric.algorithm
        public.u16(128); // symmetric.keyBits
        public.u16(ALG_CFB); // symmetric.mode
        public.u16(ALG_NULL); // scheme
        public.u16(ECC_NIST_P256); // curveID
        public.u16(ALG_NULL); // kdf
        // TPMS_ECC_POINT unique — empty, the TPM fills it in
        public.empty2b();
        public.empty2b();
        let public = public.into_vec();
        self.create_primary(&public, "CreatePrimary(SRK)")
    }

    /// The shared `TPM2_CreatePrimary` tail: send a marshalled `TPMT_PUBLIC` on
    /// the owner hierarchy and hand back the handle **and** the `outPublic` the
    /// chip filled in.
    ///
    /// The public area is not decoration: for an attestation key it carries the
    /// point a verifier checks signatures against, and it is the only way that
    /// point ever leaves the chip.
    fn create_primary(
        &mut self,
        public: &[u8],
        what: &'static str,
    ) -> Result<(u32, Vec<u8>), TpmError> {
        let mut t = Buf::new();
        t.u32(TPM_RH_OWNER);
        t.password_auth();
        // inSensitive: TPM2B_SENSITIVE_CREATE wrapping two empty TPM2Bs
        t.u16(4);
        t.empty2b(); // userAuth
        t.empty2b(); // data
        t.tpm2b(public); // inPublic
        t.empty2b(); // outsideInfo
        t.u32(0); // creationPCR: none
        let body = self.transmit(what, Tpm::command(TPM_ST_SESSIONS, CC_CREATE_PRIMARY, t))?;
        let mut r = Reader::new(&body, what);
        let handle = r.u32()?;
        let _parameter_size = r.u32()?;
        let out_public = r.tpm2b()?.to_vec();
        Ok((handle, out_public))
    }

    /// Seal `secret` under a PCR policy.
    ///
    /// The secret goes into a KEYEDHASH object whose `authPolicy` is the PCR
    /// policy digest and whose `userWithAuth` is clear, so the only way back to
    /// it is a policy session over the same PCRs.
    pub fn seal(&mut self, secret: &[u8], sel: &PcrSelection) -> Result<SealReport, TpmError> {
        if sel.is_empty() {
            return Err(TpmError::Malformed {
                command: "seal",
                reason: "the PCR selection is empty — a seal bound to no PCR unseals in ANY \
                         boot state, which is not a seal"
                    .into(),
            });
        }
        if secret.is_empty() || secret.len() > 128 {
            return Err(TpmError::Malformed {
                command: "seal",
                reason: format!(
                    "a sealed data object carries 1–128 bytes; {} were offered",
                    secret.len()
                ),
            });
        }
        let policy_digest = self.policy_digest_for(sel)?;
        let pcrs = self.pcr_read(sel)?;
        let srk = self.create_primary_srk()?;

        let mut public = Buf::new();
        public.u16(ALG_KEYEDHASH);
        public.u16(ALG_SHA256); // nameAlg
        public.u32(SEALED_OBJECT_ATTRS);
        public.tpm2b(&policy_digest); // authPolicy — THE binding
        public.u16(ALG_NULL); // TPMS_KEYEDHASH_PARMS.scheme: plain sealed data
        public.empty2b(); // unique
        let public = public.into_vec();

        let mut t = Buf::new();
        t.u32(srk);
        t.password_auth();
        // inSensitive: userAuth empty, data = the secret
        let mut sensitive = Buf::new();
        sensitive.empty2b();
        sensitive.tpm2b(secret);
        let sensitive = sensitive.into_vec();
        t.tpm2b(&sensitive);
        t.tpm2b(&public);
        t.empty2b(); // outsideInfo
        t.u32(0); // creationPCR

        let result = self.transmit("Create", Tpm::command(TPM_ST_SESSIONS, CC_CREATE, t));
        self.flush_quietly(srk);
        let body = result?;
        let mut r = Reader::new(&body, "Create");
        let _parameter_size = r.u32()?;
        let out_private = r.tpm2b()?.to_vec();
        let out_public = r.tpm2b()?.to_vec();
        Ok(SealReport {
            blob: SealedBlob { pcrs: *sel, private: out_private, public: out_public },
            policy_digest,
            pcrs,
        })
    }

    /// Unseal a blob: re-derive the SRK, `Load` the object under it, run a
    /// **policy** session over the blob's own PCR selection, and `Unseal`.
    ///
    /// Three distinct refusals live here, and they are different problems:
    /// `Load` failing with `TPM_RC_INTEGRITY` means the blob was sealed by a
    /// different chip (or edited); `Unseal` failing with `TPM_RC_POLICY_FAIL`
    /// means this chip made it but the boot state has moved; anything else is
    /// the chip or the wiring.
    pub fn unseal(&mut self, blob: &SealedBlob) -> Result<Secret, TpmError> {
        let srk = self.create_primary_srk()?;
        let loaded = self.load(srk, blob);
        self.flush_quietly(srk);
        let object = loaded?;

        let out = (|| {
            let session = self.start_auth_session(SE_POLICY)?;
            let unsealed = (|| {
                self.policy_pcr(session, &blob.pcrs)?;
                let mut t = Buf::new();
                t.u32(object);
                // The authorisation area is the POLICY session, not a password:
                // empty nonce and empty HMAC, because the object carries no auth
                // value and the policy contains no PolicyAuthValue.
                t.u32(9);
                t.u32(session);
                t.empty2b(); // nonceCaller
                t.u8(0x01); // continueSession — this module flushes explicitly
                t.empty2b(); // hmac
                let body = self.transmit("Unseal", Tpm::command(TPM_ST_SESSIONS, CC_UNSEAL, t))?;
                let mut r = Reader::new(&body, "Unseal");
                let _parameter_size = r.u32()?;
                Ok::<Vec<u8>, TpmError>(r.tpm2b()?.to_vec())
            })();
            self.flush_quietly(session);
            unsealed
        })();
        self.flush_quietly(object);
        Ok(Secret(out?))
    }

    /// `TPM2_Load` — bring a sealed object into the chip under its parent.
    fn load(&mut self, parent: u32, blob: &SealedBlob) -> Result<u32, TpmError> {
        let mut t = Buf::new();
        t.u32(parent);
        t.password_auth();
        t.tpm2b(&blob.private);
        t.tpm2b(&blob.public);
        let body = self.transmit("Load", Tpm::command(TPM_ST_SESSIONS, CC_LOAD, t))?;
        let mut r = Reader::new(&body, "Load");
        r.u32()
    }

    // ── attestation ─────────────────────────────────────────────────────────

    /// **Derive this machine's attestation key.** ECC P-256, ECDSA-SHA256,
    /// `restricted | sign`, primary on the owner hierarchy.
    ///
    /// Deterministic in exactly the way the SRK is: the template plus the
    /// chip's own hierarchy seed produce the same key on every boot of this
    /// machine, and no other chip can produce it. That is what lets the hub
    /// *pin* the public point once, at enrolment, and treat every later quote
    /// signed by it as coming from the same physical machine — with no key
    /// material stored anywhere and nothing to steal off the disk.
    ///
    /// # What this is NOT
    ///
    /// It is not an EK-certified AK. A full TCG enrolment proves the AK lives
    /// in a genuine TPM by `MakeCredential`/`ActivateCredential` against the
    /// manufacturer-certified endorsement key. This does not, so the hub's
    /// trust in the point is **trust on first sight**: it learns the AK in the
    /// same operator-carried ceremony that hands it the WireGuard public key,
    /// and from then on it can tell "the same machine as before" from "someone
    /// else". Saying that plainly is the point; a comment claiming otherwise
    /// would be the lie.
    pub fn attestation_key(&mut self) -> Result<AttestationKey, TpmError> {
        let mut public = Buf::new();
        public.u16(ALG_ECC);
        public.u16(ALG_SHA256); // nameAlg
        public.u32(AK_ATTRS);
        public.empty2b(); // authPolicy: none — userWithAuth + the empty auth value
        // TPMS_ECC_PARMS. A signing key carries NO symmetric algorithm; the SRK
        // does because it is a restricted DECRYPT parent and has to wrap.
        public.u16(ALG_NULL); // symmetric.algorithm
        public.u16(ALG_ECDSA); // scheme.scheme
        public.u16(ALG_SHA256); // scheme.details.hashAlg
        public.u16(ECC_NIST_P256); // curveID
        public.u16(ALG_NULL); // kdf
        public.empty2b(); // unique.x — the TPM fills it in
        public.empty2b(); // unique.y
        let public = public.into_vec();
        let (handle, out_public) = self.create_primary(&public, "CreatePrimary(AK)")?;
        match parse_ecc_public(&out_public) {
            Ok(point) => Ok(AttestationKey { handle, public: point }),
            Err(e) => {
                // A handle that cannot be described is still a handle the chip
                // is holding; leaking it would exhaust the object slots on a
                // machine that quotes every boot.
                self.flush_quietly(handle);
                Err(e)
            }
        }
    }

    /// **`TPM2_Quote`** — the chip signs *(these PCRs are in this state, and
    /// here is the caller's qualifying data)* with the attestation key.
    ///
    /// `qualifying_data` is the verifier's anti-replay handle: it goes into the
    /// signed structure as `extraData` and comes back out of it, so a quote
    /// that does not carry the challenge the hub issued is a recording of an
    /// older one. [`crate::attest`] puts a hash of the hub's nonce **and the
    /// appliance's WireGuard public key** there, which is what turns "some
    /// machine has a TPM" into "this key belongs to a machine in this state".
    ///
    /// The scheme sent is `TPM_ALG_NULL`, meaning *use the key's own* — the AK
    /// template fixes ECDSA-SHA256, so a caller cannot downgrade the hash by
    /// asking for a weaker one.
    pub fn quote(
        &mut self,
        ak: &AttestationKey,
        qualifying_data: &[u8],
        sel: &PcrSelection,
    ) -> Result<QuoteOutput, TpmError> {
        if sel.is_empty() {
            return Err(TpmError::Malformed {
                command: "Quote",
                reason: "the PCR selection is empty — a quote over no PCR says nothing about \
                         the boot state, which is the only thing a quote is for"
                    .into(),
            });
        }
        if qualifying_data.is_empty() || qualifying_data.len() > 64 {
            return Err(TpmError::Malformed {
                command: "Quote",
                reason: format!(
                    "qualifyingData is {} bytes; a challenge is 1–64, and an EMPTY one would \
                     make every quote this machine ever produced interchangeable — which is \
                     the replay this field exists to stop",
                    qualifying_data.len()
                ),
            });
        }
        let mut t = Buf::new();
        t.u32(ak.handle);
        t.password_auth();
        t.tpm2b(qualifying_data);
        t.u16(ALG_NULL); // inScheme: use the key's own scheme
        sel.marshal(&mut t);
        let body = self.transmit("Quote", Tpm::command(TPM_ST_SESSIONS, CC_QUOTE, t))?;
        let mut r = Reader::new(&body, "Quote");
        let _parameter_size = r.u32()?;
        let quoted = r.tpm2b()?.to_vec();
        // TPMT_SIGNATURE
        let sig_alg = r.u16()?;
        if sig_alg != ALG_ECDSA {
            return r.err(format!(
                "the chip signed with algorithm {sig_alg:#06x}, not ECDSA — this client can \
                 only hand a verifier an ECDSA signature"
            ));
        }
        let hash_alg = r.u16()?;
        if hash_alg != ALG_SHA256 {
            return r.err(format!(
                "the quote is hashed with {hash_alg:#06x}, not SHA-256 — a verifier that \
                 assumed SHA-256 would reject every signature and never say why"
            ));
        }
        let sig_r = pad32(r.tpm2b()?, "signatureR")?;
        let sig_s = pad32(r.tpm2b()?, "signatureS")?;
        Ok(QuoteOutput { quoted, sig_r, sig_s })
    }

    /// Derive the AK, quote, and flush it again — the one-shot every caller
    /// outside a test wants, because the AK is re-derived rather than stored.
    pub fn quote_now(
        &mut self,
        qualifying_data: &[u8],
        sel: &PcrSelection,
    ) -> Result<(AkPublic, QuoteOutput), TpmError> {
        let ak = self.attestation_key()?;
        let out = self.quote(&ak, qualifying_data, sel);
        self.flush_quietly(ak.handle);
        Ok((ak.public, out?))
    }

    // ── NV monotonic counters ───────────────────────────────────────────────
    //
    // Not used by the seal or the quote. They are here because a rollback floor
    // needs a number that CANNOT be decremented by anyone holding the disk, and
    // the chip is the only thing on this machine that can promise that. The
    // alternative — a version in a file — is exactly the record an attacker who
    // rolls the image back also rolls back.

    /// `TPM2_NV_DefineSpace` for an 8-byte **monotonic counter** at `index`,
    /// owner-readable and owner-writable, `noDA`.
    ///
    /// A freshly defined counter cannot be read until it has been incremented
    /// once (`TPM_RC_NV_UNINITIALIZED`), so this does that first increment
    /// itself — a "defined" counter that refuses to be read is a trap for the
    /// next caller, not a state worth exposing.
    ///
    /// Defining an index that already exists is `TPM_RC_NV_DEFINED`, and it is
    /// RETURNED rather than swallowed: quietly accepting it would let a caller
    /// believe it had created a fresh counter when it had adopted somebody
    /// else's, at whatever value that one happens to hold.
    pub fn nv_define_counter(&mut self, index: u32) -> Result<(), TpmError> {
        self.nv_define(index, NV_COUNTER_ATTRS, 8)?;
        self.nv_increment(index)
    }

    /// `TPM2_NV_DefineSpace` with the attributes and size spelled out. The
    /// general door; [`Tpm::nv_define_counter`] is the one shape this crate
    /// uses, and the reason this is public is that a proof needs to define an
    /// index that is *not* a counter in order to watch
    /// [`Tpm::nv_ensure_counter`] refuse to adopt it.
    pub fn nv_define(&mut self, index: u32, attrs: u32, size: u16) -> Result<(), TpmError> {
        let mut nv_public = Buf::new();
        nv_public.u32(index);
        nv_public.u16(ALG_SHA256); // nameAlg
        nv_public.u32(attrs);
        nv_public.empty2b(); // authPolicy
        nv_public.u16(size);
        let nv_public = nv_public.into_vec();

        let mut t = Buf::new();
        t.u32(TPM_RH_OWNER);
        t.password_auth();
        t.empty2b(); // auth: the index carries no auth value of its own
        t.tpm2b(&nv_public);
        self.transmit("NV_DefineSpace", Tpm::command(TPM_ST_SESSIONS, CC_NV_DEFINE_SPACE, t))?;
        Ok(())
    }

    /// **The idempotent door a rollback floor actually needs**: define the
    /// counter if this machine has none, adopt it if it already has one, and
    /// hand back its value either way.
    ///
    /// A floor is defined by one install and read by every boot afterwards, so
    /// "already defined" is the NORMAL case, not an error. But adopting an
    /// index because it exists is exactly where this kind of code goes blind:
    /// an index at the same address could have been defined by anything, as
    /// **ordinary** NV storage that is freely writable to any value — and a
    /// floor stored there can be walked backwards by whoever holds the machine,
    /// which is the entire property it was supposed to have.
    ///
    /// So an existing index is READ BACK ([`Tpm::nv_read_public`]) and its
    /// attributes checked before a single value is believed. Wrong type, wrong
    /// size, or no owner access, and this refuses by name rather than returning
    /// a number that looks exactly like a working floor.
    pub fn nv_ensure_counter(&mut self, index: u32) -> Result<NvCounter, TpmError> {
        match self.nv_read_public(index) {
            Ok(public) => {
                // Adopting: prove it is what a floor needs BEFORE trusting it.
                let nt = (public.attributes >> 4) & 0xF;
                if nt != 1 {
                    return Err(TpmError::Malformed {
                        command: "NV_ReadPublic",
                        reason: format!(
                            "NV index {index:#010x} already exists but its type is {nt} \
                             (TPM_NT_COUNTER is 1) — this is ORDINARY storage, writable to any \
                             value, so a rollback floor kept here could be walked backwards by \
                             anyone holding the machine. Refusing to adopt it as a counter"
                        ),
                    });
                }
                if public.data_size != 8 {
                    return Err(TpmError::Malformed {
                        command: "NV_ReadPublic",
                        reason: format!(
                            "NV index {index:#010x} is {} bytes, not the 8 a counter is",
                            public.data_size
                        ),
                    });
                }
                if public.attributes & 0x0002_0000 == 0 || public.attributes & 0x0000_0002 == 0 {
                    return Err(TpmError::Malformed {
                        command: "NV_ReadPublic",
                        reason: format!(
                            "NV index {index:#010x} is a counter but not owner-readable and \
                             owner-writable (attributes {:#010x}) — this build authorises with \
                             the owner hierarchy and could not step it",
                            public.attributes
                        ),
                    });
                }
                let value = self.nv_read_counter_initialised(index)?;
                Ok(NvCounter { index, value, created: false })
            }
            // TPM_RC_HANDLE is what a chip says about an index that was never
            // defined. Every other refusal is propagated: a chip in lockout, or
            // a client bug, must not look like a fresh machine.
            Err(e) if e.is_undefined_nv_index() => {
                self.nv_define(index, NV_COUNTER_ATTRS, 8)?;
                self.nv_increment(index)?;
                let value = self.nv_read_counter(index)?;
                Ok(NvCounter { index, value, created: true })
            }
            Err(e) => Err(e),
        }
    }

    /// [`Tpm::nv_read_counter`], stepping the counter once first if the chip
    /// says it has never been written.
    ///
    /// A counter that was defined and then lost power before its first
    /// increment answers `TPM_RC_NV_UNINITIALIZED` for ever until something
    /// increments it. Treating that as a hard failure would strand a machine on
    /// a state that a single command fixes and that no operator can diagnose.
    fn nv_read_counter_initialised(&mut self, index: u32) -> Result<u64, TpmError> {
        match self.nv_read_counter(index) {
            Ok(v) => Ok(v),
            Err(TpmError::Rc { rc, .. }) if rc_base(rc) == 0x14A => {
                self.nv_increment(index)?;
                self.nv_read_counter(index)
            }
            Err(e) => Err(e),
        }
    }

    /// `TPM2_NV_ReadPublic` — the index's declared shape, with no authorisation
    /// at all. This is how "an index exists here" is told apart from "an index
    /// that can hold a monotonic floor exists here".
    pub fn nv_read_public(&mut self, index: u32) -> Result<NvPublic, TpmError> {
        let mut t = Buf::new();
        t.u32(index);
        let body =
            self.transmit("NV_ReadPublic", Tpm::command(TPM_ST_NO_SESSIONS, CC_NV_READ_PUBLIC, t))?;
        let mut r = Reader::new(&body, "NV_ReadPublic");
        let nv_public = r.tpm2b()?;
        let mut p = Reader::new(nv_public, "TPMS_NV_PUBLIC");
        let found_index = p.u32()?;
        let name_alg = p.u16()?;
        let attributes = p.u32()?;
        let _auth_policy = p.tpm2b()?;
        let data_size = p.u16()?;
        if found_index != index {
            return p.err(format!(
                "asked about NV index {index:#010x} and the chip described {found_index:#010x}"
            ));
        }
        Ok(NvPublic { index, name_alg, attributes, data_size })
    }

    /// `TPM2_NV_Increment` — step the counter. The chip guarantees this is the
    /// only direction it moves, across power cycles, for the life of the index.
    pub fn nv_increment(&mut self, index: u32) -> Result<(), TpmError> {
        let mut t = Buf::new();
        t.u32(TPM_RH_OWNER); // authHandle
        t.u32(index); // nvIndex
        t.password_auth();
        self.transmit("NV_Increment", Tpm::command(TPM_ST_SESSIONS, CC_NV_INCREMENT, t))?;
        Ok(())
    }

    /// `TPM2_NV_Read` of the 8 counter bytes, big-endian.
    pub fn nv_read_counter(&mut self, index: u32) -> Result<u64, TpmError> {
        let mut t = Buf::new();
        t.u32(TPM_RH_OWNER); // authHandle
        t.u32(index); // nvIndex
        t.password_auth();
        t.u16(8); // size
        t.u16(0); // offset
        let body = self.transmit("NV_Read", Tpm::command(TPM_ST_SESSIONS, CC_NV_READ, t))?;
        let mut r = Reader::new(&body, "NV_Read");
        let _parameter_size = r.u32()?;
        let data = r.tpm2b()?;
        let bytes: [u8; 8] = data.try_into().map_err(|_| TpmError::Malformed {
            command: "NV_Read",
            reason: format!("a counter is 8 bytes; the index returned {}", data.len()),
        })?;
        Ok(u64::from_be_bytes(bytes))
    }

    /// `TPM2_NV_UndefineSpace` — remove the index. This DESTROYS the floor, so
    /// it exists for tests and for a deliberate operator reset, never for a
    /// boot path.
    pub fn nv_undefine(&mut self, index: u32) -> Result<(), TpmError> {
        let mut t = Buf::new();
        t.u32(TPM_RH_OWNER);
        t.u32(index);
        t.password_auth();
        self.transmit("NV_UndefineSpace", Tpm::command(TPM_ST_SESSIONS, CC_NV_UNDEFINE_SPACE, t))?;
        Ok(())
    }
}

/// Left-pad a curve parameter the chip may have handed back with leading
/// zeroes stripped, refusing anything that cannot be a P-256 scalar.
fn pad32(bytes: &[u8], field: &'static str) -> Result<[u8; 32], TpmError> {
    if bytes.len() > 32 || bytes.is_empty() {
        return Err(TpmError::Malformed {
            command: "Quote",
            reason: format!("{field} is {} bytes, which is not a P-256 scalar", bytes.len()),
        });
    }
    let mut out = [0u8; 32];
    out[32 - bytes.len()..].copy_from_slice(bytes);
    Ok(out)
}

/// Parse a `TPMT_PUBLIC` carrying an ECC key and hand back its point.
///
/// Total and bounds-checked like everything else here that reads bytes off the
/// wire: this parses a response, and a chip or an emulator that answers short
/// must produce a named refusal rather than a panic.
fn parse_ecc_public(public: &[u8]) -> Result<AkPublic, TpmError> {
    let mut r = Reader::new(public, "CreatePrimary outPublic");
    let kind = r.u16()?;
    if kind != ALG_ECC {
        return r.err(format!("the primary is type {kind:#06x}, not ECC"));
    }
    let _name_alg = r.u16()?;
    let _attrs = r.u32()?;
    let _auth_policy = r.tpm2b()?;
    // TPMS_ECC_PARMS — the symmetric field is a TPMT_SYM_DEF_OBJECT, which
    // carries keyBits and mode ONLY when its algorithm is not NULL. The AK has
    // none and the SRK has AES-128-CFB, so a parser that assumed either shape
    // would read the point out of the wrong offset on the other.
    let symmetric = r.u16()?;
    if symmetric != ALG_NULL {
        let _key_bits = r.u16()?;
        let _mode = r.u16()?;
    }
    let scheme = r.u16()?;
    match scheme {
        ALG_NULL => {}
        ALG_ECDSA => {
            let _hash = r.u16()?;
        }
        other => {
            return r.err(format!(
                "signing scheme {other:#06x} is not one this client can hand a verifier"
            ))
        }
    }
    let curve = r.u16()?;
    if curve != ECC_NIST_P256 {
        return r.err(format!("curve {curve:#06x} is not NIST P-256"));
    }
    let kdf = r.u16()?;
    if kdf != ALG_NULL {
        let _hash = r.u16()?;
    }
    let x = pad32(r.tpm2b()?, "unique.x")?;
    let y = pad32(r.tpm2b()?, "unique.y")?;
    Ok(AkPublic { x, y })
}

/// Render a digest as lowercase hex — console lines and log records only.
pub fn hex(bytes: &[u8]) -> String {
    bytes.iter().map(|b| format!("{b:02x}")).collect()
}

#[cfg(test)]
mod marshalling_tests {
    use super::*;

    /// The wire encoding of the pieces every command is built from. These run
    /// with no TPM anywhere: a marshalling bug is a bug on a machine that has
    /// no chip to catch it.
    #[test]
    fn the_primitives_marshal_big_endian_and_the_2b_carries_its_length() {
        let mut b = Buf::new();
        b.u8(0x01).u16(0x0203).u32(0x0405_0607);
        assert_eq!(b.into_vec(), vec![0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]);

        let mut b = Buf::new();
        b.tpm2b(&[0xAA, 0xBB, 0xCC]);
        assert_eq!(b.into_vec(), vec![0x00, 0x03, 0xAA, 0xBB, 0xCC]);

        let mut b = Buf::new();
        b.empty2b();
        assert_eq!(b.into_vec(), vec![0x00, 0x00]);
    }

    /// The nine-byte password authorisation area, byte for byte. A tenth byte
    /// here is a `TPM_RC_AUTHSIZE` that looks like a command-code bug.
    #[test]
    fn the_password_auth_area_is_exactly_nine_bytes_of_declared_shape() {
        let mut b = Buf::new();
        b.password_auth();
        let got = b.into_vec();
        assert_eq!(
            got,
            vec![
                0x00, 0x00, 0x00, 0x09, // authSize = 9
                0x40, 0x00, 0x00, 0x09, // TPM_RS_PW
                0x00, 0x00, // nonce: empty
                0x01, // continueSession
                0x00, 0x00, // hmac: empty
            ]
        );
        assert_eq!(got.len(), 13, "4 bytes of size plus the 9 it declares");
    }

    /// The command header: tag, then the TOTAL size including the header, then
    /// the code. A size that counts only the tail is the classic first bug.
    #[test]
    fn the_command_header_counts_itself_in_the_size() {
        let mut tail = Buf::new();
        tail.u16(32);
        let cmd = Tpm::command(TPM_ST_NO_SESSIONS, CC_GET_RANDOM, tail);
        assert_eq!(cmd[0..2], [0x80, 0x01], "TPM_ST_NO_SESSIONS");
        assert_eq!(cmd[2..6], [0, 0, 0, 12], "10 header + 2 tail");
        assert_eq!(cmd[6..10], [0x00, 0x00, 0x01, 0x7B], "TPM2_CC_GetRandom");
        assert_eq!(cmd.len(), 12, "the declared size IS the buffer length");
    }

    /// `TPML_PCR_SELECTION`: one selection, `sizeofSelect = 3`, little-endian
    /// BITS inside big-endian bytes — PCR 0 is bit 0 of byte 0, PCR 7 is bit 7
    /// of byte 0, PCR 8 is bit 0 of byte 1.
    #[test]
    fn a_pcr_selection_marshals_as_a_three_byte_bitmap() {
        let mut b = Buf::new();
        PcrSelection::DEFAULT.marshal(&mut b);
        assert_eq!(
            b.into_vec(),
            vec![
                0x00, 0x00, 0x00, 0x01, // count = 1
                0x00, 0x0B, // sha256
                0x03, // sizeofSelect
                0x81, 0x00, 0x00, // PCR 0 (bit 0) and PCR 7 (bit 7)
            ]
        );
        let mut b = Buf::new();
        PcrSelection { alg: ALG_SHA256, mask: 1 << 8 }.marshal(&mut b);
        assert_eq!(b.into_vec()[7..], [0x00, 0x01, 0x00], "PCR 8 is bit 0 of the SECOND byte");
        let mut b = Buf::new();
        PcrSelection { alg: ALG_SHA256, mask: 1 << 23 }.marshal(&mut b);
        assert_eq!(b.into_vec()[7..], [0x00, 0x00, 0x80], "PCR 23 is bit 7 of the THIRD byte");
    }

    /// The reader refuses past its end instead of panicking. Its input is a
    /// device response; a chip or an emulator that answers short must produce a
    /// named refusal, not a slice index panic in PID 1.
    #[test]
    fn the_reader_refuses_a_short_response_by_name() {
        let mut r = Reader::new(&[0x00, 0x01], "TestCommand");
        assert!(r.u16().is_ok());
        let e = r.u32().unwrap_err();
        let msg = e.to_string();
        assert!(msg.contains("TestCommand"), "the refusal names the command: {msg}");
        assert!(msg.contains("malformed"), "{msg}");

        // A TPM2B whose length runs past the buffer is refused, not sliced.
        let mut r = Reader::new(&[0xFF, 0xFF, 0x01], "TestCommand");
        assert!(r.tpm2b().is_err(), "a 65535-byte field in a 3-byte buffer");

        // And the control: a well-formed one reads.
        let mut r = Reader::new(&[0x00, 0x02, 0xAB, 0xCD], "TestCommand");
        assert_eq!(r.tpm2b().unwrap(), &[0xAB, 0xCD]);
    }

    /// The selection grammar the kernel command line can carry. This string
    /// arrives off the plaintext ESP, so every refusal is by name.
    #[test]
    fn the_pcr_selection_grammar_refuses_by_name() {
        assert_eq!(PcrSelection::parse("0,7").unwrap(), PcrSelection::DEFAULT);
        assert_eq!(PcrSelection::parse("sha256:0,7").unwrap(), PcrSelection::DEFAULT);
        assert_eq!(PcrSelection::parse(" sha256 : 0 , 7 ").unwrap(), PcrSelection::DEFAULT);
        assert_eq!(PcrSelection::parse("sha1:0").unwrap().alg, ALG_SHA1);
        assert_eq!(PcrSelection::parse("23").unwrap().mask, 1 << 23);

        for (bad, must_say) in [
            ("", "no PCR indices"),
            ("sha256:", "no PCR indices"),
            ("24", "out of range"),
            ("99", "out of range"),
            ("sha384:0", "not a PCR bank"),
            ("0,x", "not a PCR index"),
            ("-1", "not a PCR index"),
        ] {
            let e = PcrSelection::parse(bad).unwrap_err();
            assert!(e.contains(must_say), "{bad:?} must say {must_say:?}, said: {e}");
        }
        // The control: the guard above is not simply "refuse everything".
        assert!(PcrSelection::parse("0,2,4,7").is_ok());
    }

    /// Display round-trips through the parser — one spelling, both directions.
    #[test]
    fn a_selection_renders_the_way_it_parses() {
        for s in ["sha256:0,7", "sha256:0", "sha1:0,1,2,3", "sha256:0,2,4,7,23"] {
            let sel = PcrSelection::parse(s).unwrap();
            assert_eq!(sel.to_string(), s, "the rendering is the parser's input");
        }
    }

    /// The on-disk blob survives a round trip, and its magic/version/mask land
    /// where the layout comment says.
    #[test]
    fn a_sealed_blob_round_trips_and_its_header_is_where_it_says() {
        let blob = SealedBlob {
            pcrs: PcrSelection::DEFAULT,
            private: vec![0x11; 70],
            public: vec![0x22; 48],
        };
        let bytes = blob.encode();
        assert_eq!(&bytes[..8], SEAL_MAGIC);
        assert_eq!(u16::from_be_bytes([bytes[8], bytes[9]]), SEAL_VERSION);
        assert_eq!(u16::from_be_bytes([bytes[10], bytes[11]]), ALG_SHA256);
        assert_eq!(u32::from_be_bytes(bytes[12..16].try_into().unwrap()), 0x81);
        assert_eq!(bytes.len(), 48 + 2 + 70 + 2 + 48);
        assert_eq!(SealedBlob::decode(&bytes).unwrap(), blob);
    }

    /// **A corrupt sealed blob is refused BY NAME, never a panic.**
    ///
    /// `korp-start` reads this file off a FAT partition as PID 1, where a panic
    /// is a kernel panic with no line saying which record was bad — the exact
    /// class fixed in `loader::decode_hex32` (edda 1b95cd4) hours before this
    /// module was written. Every case below is a file an attacker with the disk
    /// can write.
    #[test]
    fn a_corrupt_sealed_blob_is_refused_by_name_and_never_panics() {
        let good = SealedBlob {
            pcrs: PcrSelection::DEFAULT,
            private: vec![0x11; 70],
            public: vec![0x22; 48],
        }
        .encode();

        // Empty, short, and not-a-seal.
        assert!(matches!(SealedBlob::decode(&[]), Err(BlobError::NotASeal { .. })));
        assert!(matches!(SealedBlob::decode(b"KORP"), Err(BlobError::NotASeal { .. })));
        assert!(matches!(
            SealedBlob::decode(b"NOTASEAL and then some"),
            Err(BlobError::NotASeal { .. })
        ));
        // The magic alone, with no header behind it.
        assert!(matches!(SealedBlob::decode(SEAL_MAGIC), Err(BlobError::Truncated { .. })));

        // A wrong version is refused before any length field is believed.
        let mut wrong_version = good.clone();
        wrong_version[9] = 9;
        assert_eq!(SealedBlob::decode(&wrong_version), Err(BlobError::Version { found: 9 }));

        // One flipped payload bit fails the checksum.
        let mut flipped = good.clone();
        let last = flipped.len() - 1;
        flipped[last] ^= 0x01;
        assert_eq!(SealedBlob::decode(&flipped), Err(BlobError::Checksum));

        // A length field claiming more than the file holds — with the checksum
        // recomputed, so the ONLY thing standing between this and a panic is
        // the bounds check.
        let mut body = Buf::new();
        body.u16(0xFFFF); // private claims 65535 bytes
        body.raw(&[0u8; 4]);
        let body = body.into_vec();
        let mut hostile = Buf::new();
        hostile.raw(SEAL_MAGIC);
        hostile.u16(SEAL_VERSION);
        hostile.u16(ALG_SHA256);
        hostile.u32(0x81);
        hostile.raw(blake3::hash(&body).as_bytes());
        hostile.raw(&body);
        let e = SealedBlob::decode(&hostile.into_vec()).unwrap_err();
        assert!(
            matches!(e, BlobError::Truncated { field: "private", wanted: 65535, .. }),
            "a hostile length must be REFUSED, got {e}"
        );

        // A seal declaring NO PCRs — the file shape of "unseal me in any boot
        // state", which is the whole failure this mechanism exists to prevent.
        let empty_mask = SealedBlob {
            pcrs: PcrSelection { alg: ALG_SHA256, mask: 0 },
            private: vec![1],
            public: vec![2],
        }
        .encode();
        assert!(matches!(
            SealedBlob::decode(&empty_mask),
            Err(BlobError::BadSelection { .. })
        ));

        // Every refusal above renders a sentence naming the problem.
        for msg in [
            BlobError::NotASeal { found: "'x'".into() }.to_string(),
            BlobError::Version { found: 9 }.to_string(),
            BlobError::Truncated { field: "public", wanted: 5, available: 1 }.to_string(),
            BlobError::Checksum.to_string(),
            BlobError::BadSelection { reason: "why".into() }.to_string(),
        ] {
            assert!(msg.len() > 30, "a refusal is a sentence, not a code: {msg}");
        }

        // The control: the good bytes still decode, so the loop above was the
        // guard and not a decoder that refuses everything.
        assert!(SealedBlob::decode(&good).is_ok());
    }

    /// Response-code decoding, including the format-1 decoration that makes a
    /// policy failure look like a different number every time.
    #[test]
    fn response_codes_are_named_through_their_format_one_decoration() {
        // TPM_RC_POLICY_FAIL, plain and with a session/parameter number in the
        // high nibbles — the chip decorates format-1 codes with WHERE the fault
        // was, and a client that compares raw values misses its own refusal.
        assert_eq!(rc_name(0x0000_099D), "TPM_RC_POLICY_FAIL");
        assert_eq!(rc_name(0x0000_019D), "TPM_RC_POLICY_FAIL");
        assert_eq!(rc_name(0x0000_09D), "TPM_RC_POLICY_FAIL");
        assert_eq!(rc_name(0x0000_019F), "TPM_RC_INTEGRITY");
        assert_eq!(rc_name(0x0000_0128), "TPM_RC_PCR_CHANGED");
        assert_eq!(rc_name(0x0000_0143), "TPM_RC_COMMAND_CODE");
        assert_eq!(rc_name(0x0000_0DEAD), "unnamed in this client");

        let policy = TpmError::Rc {
            command: "Unseal",
            rc: 0x0000_099D,
            name: rc_name(0x0000_099D),
        };
        assert!(policy.is_policy_refusal(), "a digest mismatch is the EXPECTED refusal");
        assert!(policy.to_string().contains("TPM_RC_POLICY_FAIL"), "{policy}");
        assert!(policy.to_string().contains("Unseal"), "the refusal names the command");

        let integrity = TpmError::Rc { command: "Load", rc: 0x19F, name: rc_name(0x19F) };
        assert!(integrity.is_policy_refusal(), "a foreign blob is a refusal, not a bug");

        // …and the control: a genuine wiring bug is NOT reported as a policy
        // refusal, or a broken client would look like a secure one.
        let wiring = TpmError::Rc { command: "Create", rc: 0x143, name: rc_name(0x143) };
        assert!(!wiring.is_policy_refusal(), "TPM_RC_COMMAND_CODE is a bug, not a boot state");
    }

    /// The sealed object's attributes, spelled out. `userWithAuth` SET here
    /// would let anyone unseal by presenting the empty auth value, and the PCR
    /// policy would be decoration — a green that could never go red.
    #[test]
    fn the_sealed_object_attributes_leave_user_with_auth_clear() {
        assert_eq!(SEALED_OBJECT_ATTRS, 0x12, "fixedTPM | fixedParent, and nothing else");
        assert_eq!(SEALED_OBJECT_ATTRS & 0x0000_0040, 0, "userWithAuth MUST be clear");
        assert_eq!(SEALED_OBJECT_ATTRS & 0x0000_0020, 0, "sensitiveDataOrigin: we supply the data");
        // The SRK's, for contrast: a restricted decryption parent.
        assert_eq!(SRK_ATTRS & 0x0001_0000, 0x0001_0000, "restricted");
        assert_eq!(SRK_ATTRS & 0x0002_0000, 0x0002_0000, "decrypt");
        assert_eq!(SRK_ATTRS & 0x0000_0040, 0x0000_0040, "the SRK IS userWithAuth");
    }

    /// **The attestation key's attributes, spelled out.**
    ///
    /// `restricted` CLEAR would leave a key that signs whatever it is handed —
    /// including a `TPMS_ATTEST` an attacker composed byte by byte — and every
    /// signature check in `attest.rs` would still pass. It is the one bit whose
    /// absence nothing downstream can detect.
    #[test]
    fn the_attestation_key_is_restricted_and_signs_and_never_leaves_the_chip() {
        assert_eq!(AK_ATTRS & 0x0001_0000, 0x0001_0000, "restricted");
        assert_eq!(AK_ATTRS & 0x0004_0000, 0x0004_0000, "sign");
        assert_eq!(AK_ATTRS & 0x0002_0000, 0, "an AK must NOT be a decryption key");
        assert_eq!(AK_ATTRS & 0x0000_0002, 0x0000_0002, "fixedTPM — no copy off this chip");
        assert_eq!(AK_ATTRS & 0x0000_0010, 0x0000_0010, "fixedParent");
        assert_eq!(AK_ATTRS & 0x0000_0040, 0x0000_0040, "userWithAuth — the empty auth signs");
        // The AK and the SRK differ in EXACTLY the two role bits. Identical
        // templates would make the chip derive the same key for both, and the
        // machine would be signing attestations with its storage root.
        assert_eq!(AK_ATTRS ^ SRK_ATTRS, 0x0006_0000, "sign vs decrypt, and nothing else");
    }

    /// The NV counter's attributes. `TPM_NT_COUNTER` in bits 4–7 is what makes
    /// the index monotonic; ordinary NV storage would be freely writable to any
    /// value, i.e. a rollback floor an attacker simply lowers.
    #[test]
    fn the_nv_counter_is_declared_a_counter_and_not_ordinary_storage() {
        assert_eq!((NV_COUNTER_ATTRS >> 4) & 0xF, 1, "TPM_NT_COUNTER");
        assert_eq!(NV_COUNTER_ATTRS & 0x0000_0002, 0x0000_0002, "OWNERWRITE");
        assert_eq!(NV_COUNTER_ATTRS & 0x0002_0000, 0x0002_0000, "OWNERREAD");
        assert_eq!(NV_COUNTER_ATTRS & 0x0200_0000, 0x0200_0000, "NO_DA");
        assert_eq!(NV_COUNTER_ATTRS & 0x4000_0000, 0, "not PLATFORMCREATE — owner defines it");
    }

    /// The AK's public point survives the spelling an operator carries to the
    /// hub, and every malformed spelling is refused by name rather than
    /// truncated into a key that would verify nothing and say nothing.
    #[test]
    fn an_attestation_key_round_trips_through_its_operator_spelling() {
        let ak = AkPublic { x: [0x11; 32], y: [0x22; 32] };
        assert_eq!(ak.sec1()[0], 0x04, "SEC1 uncompressed");
        assert_eq!(ak.to_hex().len(), 130);
        assert_eq!(AkPublic::from_hex(&ak.to_hex()).unwrap(), ak);
        assert_eq!(AkPublic::from_sec1(&ak.sec1()).unwrap(), ak);
        assert_eq!(AkPublic::from_hex(&format!("  {}\n", ak.to_hex())).unwrap(), ak);

        for bad in ["", "04", &"a".repeat(129), &"z".repeat(130)] {
            let e = AkPublic::from_hex(bad).unwrap_err();
            assert!(e.contains("130 hex"), "{bad:?} refused as: {e}");
        }
        // 65 bytes but NOT uncompressed: a verifier handed a compressed point
        // would reject every genuine signature from that machine.
        let mut compressed = ak.sec1();
        compressed[0] = 0x02;
        assert!(AkPublic::from_sec1(&compressed).unwrap_err().contains("0x04"));
    }

    /// `TPMT_PUBLIC` parsing, both shapes: the AK's (no symmetric, ECDSA
    /// scheme) and the SRK's (AES-CFB symmetric, NULL scheme). One reader, two
    /// layouts — a parser that assumed either would read the point out of the
    /// wrong offset on the other and hand a verifier 32 bytes of another field.
    #[test]
    fn the_public_area_parser_finds_the_point_in_both_key_shapes() {
        let x = [0xAB; 32];
        let y = [0xCD; 32];

        let mut ak = Buf::new();
        ak.u16(ALG_ECC);
        ak.u16(ALG_SHA256);
        ak.u32(AK_ATTRS);
        ak.empty2b();
        ak.u16(ALG_NULL); // symmetric
        ak.u16(ALG_ECDSA); // scheme
        ak.u16(ALG_SHA256); // hash
        ak.u16(ECC_NIST_P256);
        ak.u16(ALG_NULL); // kdf
        ak.tpm2b(&x);
        ak.tpm2b(&y);
        assert_eq!(parse_ecc_public(&ak.into_vec()).unwrap(), AkPublic { x, y });

        let mut srk = Buf::new();
        srk.u16(ALG_ECC);
        srk.u16(ALG_SHA256);
        srk.u32(SRK_ATTRS);
        srk.empty2b();
        srk.u16(ALG_AES); // symmetric — three more fields follow
        srk.u16(128);
        srk.u16(ALG_CFB);
        srk.u16(ALG_NULL); // scheme
        srk.u16(ECC_NIST_P256);
        srk.u16(ALG_NULL); // kdf
        srk.tpm2b(&x);
        srk.tpm2b(&y);
        assert_eq!(parse_ecc_public(&srk.into_vec()).unwrap(), AkPublic { x, y });

        // A short buffer is a NAMED refusal, never a panic.
        let mut short = Buf::new();
        short.u16(ALG_ECC);
        short.u16(ALG_SHA256);
        let e = parse_ecc_public(&short.into_vec()).unwrap_err();
        assert!(e.to_string().contains("malformed"), "{e}");

        // An RSA public area is refused by name rather than mis-read as ECC.
        let mut rsa = Buf::new();
        rsa.u16(0x0001); // TPM_ALG_RSA
        let e = parse_ecc_public(&rsa.into_vec()).unwrap_err();
        assert!(e.to_string().contains("not ECC"), "{e}");
    }

    /// A curve parameter the chip stripped leading zeroes from is LEFT-padded,
    /// not right-padded. Right-padding multiplies the scalar by 256^k, and the
    /// signature would fail on roughly one quote in 256 — intermittently,
    /// which is the worst way for a verifier to be wrong.
    #[test]
    fn a_short_curve_parameter_is_left_padded() {
        assert_eq!(pad32(&[0xFF], "r").unwrap()[31], 0xFF);
        assert_eq!(pad32(&[0xFF], "r").unwrap()[0], 0x00);
        assert_eq!(pad32(&[7u8; 32], "r").unwrap(), [7u8; 32]);
        assert!(pad32(&[], "r").is_err(), "an empty scalar is not a signature");
        assert!(pad32(&[0u8; 33], "r").is_err(), "33 bytes is not a P-256 scalar");
    }

    /// A `Secret` redacts itself in `Debug`. A derived impl would put the
    /// WireGuard private key one `{:?}` away from every log line.
    #[test]
    fn a_secret_never_prints_itself() {
        let s = Secret(b"this is the private key".to_vec());
        let rendered = format!("{s:?}");
        assert!(!rendered.contains("private key"), "{rendered}");
        assert!(rendered.contains("redacted"), "{rendered}");
        assert_eq!(s.len(), 23);
    }
}