attest

package module
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 5, 2026 License: BSD-3-Clause Imports: 11 Imported by: 0

README

go-tpm2/attest

go-tpm2/attest

CI Go Reference Coverage License

Pure-Go TPM 2.0 remote-attestation protocol: a Verifier (control plane) and a Node (agent) that together implement node-admission-on-Quote. A node joins a fleet only if it proves — via a TPM Quote over a fresh verifier nonce, signed by an EK-bound Attestation Key — that it booted an approved stack.

It sits one layer above github.com/go-tpm2/tpm2: the Verifier is pure Go and never touches a TPM (MakeCredential + VerifyQuote are off-TPM crypto); the Node drives a real *tpm2.TPM (CreateEK/CreatePrimary/ActivateCredential/Quote).

Install

go get github.com/go-tpm2/attest

Protocol

Two phases. Enrolment (once per node) binds an AK to a trusted EK; Admission (every join) proves an approved boot.

Node                                   Verifier
 ── EnrollRequest{EKPub,EKCert,          ──▶  Trusted(EKPub)? MakeCredential
                  AKPub,AKName}                (off-TPM) → blob+secret
 ◀─ EnrollChallenge{CredentialBlob,Secret} ──
 ActivateCredential(AK,EK,…) → secret
 ── EnrollProof{ActivationSecret}        ──▶  const-time compare → BindAK
 ─────────────────────────────────────────────────────────────────────────
 ── AdmissionRequest{AKName}             ──▶  AK bound? fresh Nonce
 ◀─ AdmissionChallenge{Nonce}            ──
 Quote(AK,pcrSel,Nonce) + PCRRead
 ── AdmissionResponse{Quoted,Signature,  ──▶  VerifyQuote (sig+pcrDigest),
                      PCRs,EventLog}            extraData==Nonce (anti-replay),
                                               Policy.Matches(PCRs) → granted

Usage — the two-phase handshake

// --- Verifier (control plane, pure Go, no TPM). ---
reg := attest.NewMemRegistry()
reg.TrustEK(ekPub)                        // or chain the EK cert to a root
v := attest.NewVerifier(reg, attest.GoldenPolicy{ /* set after first boot */ },
	attest.RandNonce)

// --- Node (agent, on the attesting platform). ---
node, _ := attest.NewNode(tpm2.New(transport),
	[]tpm2.PCRSelection{{Hash: uint16(common.AlgSHA256), PCRs: []int{0, 7}}})

// Enrolment: bind the AK to a trusted EK.
chal, _ := v.Enroll(node.EnrollRequest(ekCert))
proof, _ := node.RespondEnroll(chal)      // TPM2_ActivateCredential
if err := v.CompleteEnroll(node.AKName(), proof); err != nil {
	log.Fatal(err)                        // attest.ErrActivationFailed
}

// Admission: prove an approved boot on each join.
adChal, _ := v.Challenge(node.AdmissionRequest())
resp, _ := node.RespondAdmission(adChal)  // TPM2_Quote + PCR_Read
granted, err := v.Admit(node.AKName(), resp)
// err is one of: ErrUnboundAK, ErrStaleNonce, ErrQuoteSignature,
// ErrPCRDigestMismatch, ErrUntrustedBoot (errors.As → *UntrustedBootError),
// ErrEventLogMismatch, ErrUnapprovedMeasurement (errors.As →
// *UnapprovedMeasurementError) when an EventLogPolicy is installed.
Event-log replay policy (v0.2.0)
// Approve individual measurements instead of a whole-PCR golden digest.
elp := attest.NewEventLogPolicy().
    RestrictPCRs(16).                      // gate only on PCR 16
    AllowMeasurement(16, shimDigest).      // each is one allowlist entry
    AllowMeasurement(16, grubDigest).
    AllowMeasurement(16, kernelDigest)
v.SetPolicy(elp)

// The node attaches its measured-boot log; Admit replays it, requires the
// replay to equal the verified PCRs, and requires every event allowlisted.
resp, _ := node.RespondAdmission(adChal)
resp.EventLog = eventLog                   // crypto-agile TCG log
granted, err := v.Admit(node.AKName(), resp)
Pluggable challenge state for HA (v0.3.0)

The Verifier's single-use challenge state — the enrolment activation secret and the admission nonce — defaults to an in-memory MemPendingStore. For a multi-replica (HA) control plane, back it with a shared PendingStore (e.g. etcd) so a Challenge served by one replica is admissible by another:

type PendingStore interface {
    PutEnroll(akName, secret []byte) error            // stash; overwrite prior
    TakeEnroll(akName []byte) (secret []byte, ok bool) // consume, single-use
    PutNonce(akName []byte, nonce []byte) error
    TakeNonce(akName []byte) (nonce []byte, ok bool)   // consume, single-use
}

// Default (in-memory) — NewVerifier uses this:
v := attest.NewVerifier(reg, policy, attest.RandNonce)
// HA — back the challenge state with a shared store:
v := attest.NewVerifierWithStore(reg, policy, attest.RandNonce, etcdStore)

The interface is deliberately time/TTL-agnostic: the backing store owns expiry (e.g. etcd leases), so attest imports no clock and the Node side stays import-clean for GOOS=tamago. Take* is read-and-remove, so a challenge is consumed exactly once regardless of which replica serves the follow-up. This is fully backward-compatible — NewVerifier(reg, policy, nonce) is unchanged and uses the in-memory default.

Wire format

A small, versioned, big-endian, length-prefixed codec (codec.go): every frame is magic("ATT2") | version | kind | body; []byte is u32 len | bytes; the 32-byte admission nonce is fixed-width; PCR maps are emitted in ascending key order so the encoding is byte-stable. Decoders reject foreign magic, wrong version, wrong message kind, and trailing bytes; every message round-trips.

Security model

  • EK enrolment / identity. MakeCredential binds the activation secret to the AK's Name and the EK's public key; only a TPM holding the EK private key and an AK with that Name can recover it via ActivateCredential. A successful CompleteEnroll proves the AK and a trusted EK live on the same TPM.
  • Nonce anti-replay. Every admission uses a fresh nonce folded into the quote's extraData; Admit checks it and consumes it, so a captured quote cannot be replayed.
  • Golden PCR policy. GoldenPolicy admits only if every required PCR equals its golden digest, naming the first mismatch in *UntrustedBootError.
  • Event-log replay policy (v0.2.0). EventLogPolicy admits by replaying the node's TCG measured-boot event log (the crypto-agile TCG_PCR_EVENT2 stream, TCG PC Client Platform Firmware Profile) instead of matching whole-PCR golden digests. Matches parses the log, folds each event into a virtual PCR (pcr = SHA256(pcr ‖ digest) from all-zero), requires the replayed PCRs to equal the attested PCRs (ErrEventLogMismatch otherwise — this binds the otherwise-untrusted log to the verified quote), and requires every event to be on an allowlist of approved (PCR, digest) measurements (ErrUnapprovedMeasurement, naming the offending event, otherwise). Rolling out a new image is one allowlist entry, not a per-platform golden-PCR recompute. See Policy.Matches(pcrs, eventLog) (the v0.2.0 signature).
  • TOCTOU caveat. A Quote attests boot-time measurements, not runtime state; attestation gates admission, it is not a continuous integrity monitor. Pair it with short admission leases / re-attestation.

Conventions

Pure Go, CGO_ENABLED=0, no workspace (GOWORK=off), BSD-3 (SPDX per file), 100 % statement coverage (CI gate). Validated end-to-end against a real swtpm in go-tpm2/validate: the full handshake runs in a tamago/amd64 guest against live TPM crypto. cmd/attestvalidate asserts ADMITTED and rejects bad-PCR, stale-nonce, and wrong-AK; cmd/attesteventlog (v0.2.0) does several real PCR_Extends, builds a crypto-agile TCG event log of them, and asserts the EventLogPolicy replay equals the real swtpm PCRs and is allowlisted (ADMITTED), then rejects an unapproved measurement (ErrUnapprovedMeasurement) and a tampered log (ErrEventLogMismatch).

Sibling repos: common, tpm2, crb, tis, validate.

Documentation

Overview

Package attest is a reusable TPM 2.0 remote-attestation protocol: a Verifier (control-plane) side and a Node (agent) side that together implement NODE-ADMISSION-ON-QUOTE. A node joins a fleet only if it proves, via a TPM Quote over a fresh verifier nonce signed by an EK-bound Attestation Key, that it booted an approved stack. It builds on github.com/go-tpm2/tpm2: the Verifier is pure Go and off-TPM (MakeCredential + VerifyQuote); the Node drives a real *tpm2.TPM (CreateEK/CreatePrimary/ActivateCredential/Quote).

Protocol

The handshake has two phases. ENROLMENT runs once per node and binds an AK to a trusted EK; ADMISSION runs on every join and proves an approved boot.

Enrolment (proves AK and a trusted EK live on the SAME TPM):

Node                                  Verifier
  -- EnrollRequest{EKPub,EKCert,         -->  Trusted(EKPub)? pick random
                   AKPub,AKName}              activation secret; MakeCredential
                                             (off-TPM) -> blob+secret
  <-- EnrollChallenge{CredentialBlob,    --
                      Secret}
  ActivateCredential(AK,EK,blob,secret)
  -> recovered secret
  -- EnrollProof{ActivationSecret}       -->  constant-time compare to pending;
                                             on match BindAK(AKName, AKPub)

Admission (proves an approved boot, fresh each join):

Node                                  Verifier
  -- AdmissionRequest{AKName}            -->  AK bound? fresh Nonce; stash
  <-- AdmissionChallenge{Nonce}          --
  Quote(AK, pcrSel, Nonce) + PCRRead
  -- AdmissionResponse{Quoted,Signature, -->  VerifyQuote (sig + pcrDigest);
                       PCRs,EventLog}         extraData==Nonce (anti-replay);
                                             Policy.Matches(PCRs) -> granted

Security model

  • EK enrolment / identity. MakeCredential binds an activation secret to the AK's Name AND the EK's public key; only a TPM holding the EK private key and an AK with that exact Name can recover it via ActivateCredential. A successful CompleteEnroll therefore proves the AK and a TRUSTED EK live on the same TPM. Which EKs are trusted is the EKRegistry's decision (an allowlist, or an EK-certificate chain to a manufacturer root).

  • Nonce anti-replay. Every admission uses a fresh verifier nonce, folded into the quote's extraData (qualifyingData). Admit checks the signed attest's extraData equals exactly the issued nonce and consumes the pending nonce, so a captured quote cannot be replayed.

  • Golden PCR policy. GoldenPolicy admits a node only if every required PCR equals its expected ("golden") digest, naming the first mismatch in a typed *UntrustedBootError (errors.Is ErrUntrustedBoot). The PCR values are trusted only because the signature and the quoted pcrDigest were verified first.

  • Event-log replay policy (v0.2.0). EventLogPolicy admits a node by REPLAYING its TCG measured-boot event log (the crypto-agile TCG_PCR_EVENT2 stream, TCG PC Client Platform Firmware Profile) rather than matching whole-PCR golden digests. Its Matches: (1) parses the log (ParseEventLog) and folds each event's digest into a virtual PCR — pcr = SHA256(pcr || digest), starting from all-zero (ReplayPCRs); (2) requires the replayed PCRs to EQUAL the attested PCRs (ErrEventLogMismatch otherwise) — this is what binds the otherwise-untrusted log to the cryptographically-verified quote, so a tampered log whose replay diverges is rejected; (3) requires every event to be on an allowlist of approved (PCRindex, digest) measurements (ErrUnapprovedMeasurement, naming the offending event, otherwise).

  • Allowlist vs. golden tradeoff. A GoldenPolicy entry is the FINAL PCR value, i.e. the hash chain over every measurement that PCR accumulated; changing any one component (a kernel, a shim, a config blob) changes the final digest, so the operator must recompute and redeploy the golden PCR for every platform/image permutation. An EventLogPolicy allowlist instead approves INDIVIDUAL measurements: rolling out a new kernel image is ONE new allowlist entry (its measurement), and any platform whose log is built from approved components — in any valid order the replay reproduces — is admitted. The cost is that the verifier now parses and replays an attacker-influenced log; the replay-equality check (2) is the mitigation that keeps the log honest. Use GoldenPolicy for a small, frozen fleet; EventLogPolicy when images update independently and often.

  • TOCTOU caveat. A Quote attests the platform's BOOT-TIME measurements (the PCR state at quote time), NOT its runtime state. An approved boot says nothing about post-boot compromise; attestation gates admission, it is not a continuous runtime-integrity monitor. Pair it with short admission leases / re-attestation for stronger guarantees.

tpm2 wiring

The Node calls tpm2.CreateEK, tpm2.CreatePrimaryPublic, tpm2.ObjectName, tpm2.StartAuthSession, tpm2.PolicySecret, tpm2.ActivateCredential, tpm2.Quote and tpm2.PCRRead. The Verifier calls tpm2.MakeCredential and tpm2.VerifyQuote. VerifyQuote checks the ECDSA signature and the pcrDigest but does NOT compare extraData to a nonce, so Admit performs the nonce (anti-replay) check itself against the parsed AttestInfo.ExtraData.

Index

Constants

View Source
const (
	// ErrShortBuffer is returned when a buffer ends before a field could be
	// fully decoded.
	ErrShortBuffer = common.Error("attest: short buffer")
	// ErrBadMagic is returned when a frame does not begin with wireMagic.
	ErrBadMagic = common.Error("attest: bad frame magic")
	// ErrBadVersion is returned when a frame's version != wireVersion.
	ErrBadVersion = common.Error("attest: unsupported codec version")
	// ErrWrongKind is returned when a frame's kind tag does not match the
	// message type being decoded into.
	ErrWrongKind = common.Error("attest: wrong message kind")
	// ErrTrailingBytes is returned when a frame has bytes left over after the
	// message was fully decoded (a strict, non-malleable decode).
	ErrTrailingBytes = common.Error("attest: trailing bytes after message")
)

Codec error sentinels (typed and constant for ==).

View Source
const (
	// ErrMalformedLog is returned when the event log is truncated or otherwise
	// cannot be parsed (a short buffer, a digest of an unknown algorithm, etc.).
	ErrMalformedLog = common.Error("attest: malformed TCG event log")
	// ErrBadSpecID is returned when the first event is not a well-formed
	// TCG_EfiSpecIdEvent (wrong signature, or no algorithms declared).
	ErrBadSpecID = common.Error("attest: missing or bad Spec ID Event03 header")
	// ErrUnknownDigestAlg is returned when a TCG_PCR_EVENT2 carries a digest
	// whose algorithm was not declared in the spec-ID header.
	ErrUnknownDigestAlg = common.Error("attest: event-log digest of undeclared algorithm")
	// ErrEventLogMismatch is returned by EventLogPolicy when replaying the log
	// does not reproduce the attested PCR values.
	ErrEventLogMismatch = common.Error("attest: event-log replay != attested PCRs")
)

Event-log error sentinels (typed and constant for ==).

View Source
const (
	// ErrUnknownEK is returned by Enroll when the presented EK is not trusted.
	ErrUnknownEK = common.Error("attest: unknown or untrusted EK")
	// ErrActivationFailed is returned by CompleteEnroll when the returned
	// activation secret does not match the issued one (or no enrolment is
	// pending for the AK).
	ErrActivationFailed = common.Error("attest: credential activation failed")
	// ErrUnboundAK is returned when an admission step references an AK that was
	// never bound via a successful enrolment.
	ErrUnboundAK = common.Error("attest: AK is not bound")
	// ErrStaleNonce is returned by Admit when no challenge is pending for the
	// AK or the quote's extraData does not equal the issued nonce (anti-replay).
	ErrStaleNonce = common.Error("attest: stale or replayed nonce")
	// ErrQuoteSignature is returned when the quote's ECDSA signature does not
	// verify under the bound AK public key.
	ErrQuoteSignature = common.Error("attest: quote signature invalid")
	// ErrPCRDigestMismatch is returned when the quoted pcrDigest does not equal
	// the digest recomputed over the claimed PCR values.
	ErrPCRDigestMismatch = common.Error("attest: quoted pcrDigest != claimed PCRs")
	// ErrMalformedQuote is returned when the quote or signature cannot be
	// parsed (e.g. a signature that is not the expected r||s width).
	ErrMalformedQuote = common.Error("attest: malformed quote or signature")
)

Verifier error sentinels (typed and constant for ==).

View Source
const ErrBadPublic = common.Error("attest: malformed ECC TPMT_PUBLIC")

ErrBadPublic is returned when a public area is not a parseable ECC TPMT_PUBLIC.

View Source
const ErrUnapprovedMeasurement = common.Error("attest: event log contains an unapproved measurement")

ErrUnapprovedMeasurement is the sentinel every allowlist rejection satisfies under errors.Is. A concrete *UnapprovedMeasurementError additionally names the offending event.

View Source
const ErrUntrustedBoot = common.Error("attest: untrusted boot (PCR policy mismatch)")

ErrUntrustedBoot is the sentinel every Policy mismatch satisfies under errors.Is. A concrete *UntrustedBootError additionally names the PCR.

Variables

This section is empty.

Functions

func JoinSignature

func JoinSignature(sig tpm2.ECDSASignature) []byte

JoinSignature is the inverse of splitSignature: it renders a tpm2.ECDSASignature (as Quote returns it) into the fixed-width r||s wire form AdmissionResponse carries. r and s are left-padded to p256HalfLen bytes.

func RandNonce

func RandNonce() ([32]byte, error)

RandNonce is the default Nonce source, drawing from crypto/rand.

func ReplayPCRs added in v0.2.0

func ReplayPCRs(events []Event, alg uint16) (map[int][]byte, error)

ReplayPCRs recomputes the PCR bank for algorithm alg by folding each event's digest into a virtual PCR exactly as the TPM does: a PCR starts at all-zero and each extend sets pcr = H(pcr || eventDigest), where H is the bank's hash. For the SHA-256 bank (the only bank this stack replays) H is SHA-256 and the PCR width is 32 bytes. An event missing a digest for alg is an inconsistent log. PFP clause 7.2 ("PCR Usage") / TCG "Part 1", "PCR Extend". Only common.AlgSHA256 is supported as the replay bank.

Types

type AdmissionChallenge

type AdmissionChallenge struct {
	Nonce [32]byte
}

AdmissionChallenge is the verifier's fresh 32-byte anti-replay nonce, which the node folds into the Quote's extraData (qualifyingData). The verifier later checks the quoted attest's extraData equals exactly this nonce.

func (AdmissionChallenge) Marshal

func (m AdmissionChallenge) Marshal() []byte

Marshal encodes the AdmissionChallenge to a frame.

func (*AdmissionChallenge) Unmarshal

func (m *AdmissionChallenge) Unmarshal(buf []byte) error

Unmarshal decodes a frame into the AdmissionChallenge.

type AdmissionRequest

type AdmissionRequest struct {
	AKName []byte
}

AdmissionRequest is the node's request to join the fleet, naming the AK it enrolled. The verifier looks the AK Name up in its registry and refuses unbound AKs.

func (AdmissionRequest) Marshal

func (m AdmissionRequest) Marshal() []byte

Marshal encodes the AdmissionRequest to a frame.

func (*AdmissionRequest) Unmarshal

func (m *AdmissionRequest) Unmarshal(buf []byte) error

Unmarshal decodes a frame into the AdmissionRequest.

type AdmissionResponse

type AdmissionResponse struct {
	Quoted    []byte
	Signature []byte
	PCRs      map[int][]byte
	EventLog  []byte
}

AdmissionResponse carries the node's TPM Quote: the raw TPM2B_ATTEST data (Quoted, the signed bytes), the ECDSA signature over it (Signature, as a concatenated r||s of equal halves), the claimed PCR values (PCRs, by index), and an optional measured-boot event log (EventLog, reserved for the v0.2 EventLogPolicy). The verifier reconstructs the (r,s) split from the signed curve (P-256 => 32-byte halves).

func (AdmissionResponse) Marshal

func (m AdmissionResponse) Marshal() []byte

Marshal encodes the AdmissionResponse to a frame.

func (*AdmissionResponse) Unmarshal

func (m *AdmissionResponse) Unmarshal(buf []byte) error

Unmarshal decodes a frame into the AdmissionResponse.

type EKRegistry

type EKRegistry interface {
	// Trusted reports whether the given EK public area is acceptable for
	// enrolment. ekPub is the TPMT_PUBLIC of the EK.
	Trusted(ekPub []byte) bool
	// BindAK records that akName (the AK's Name) is bound to a trusted EK and
	// stores akPub (the AK's TPMT_PUBLIC) for later signature verification. It
	// is called only after a successful credential activation.
	BindAK(akName, akPub []byte) error
	// AKPub returns the stored AK public area for a bound AK Name, and whether
	// the AK is bound at all.
	AKPub(akName []byte) (akPub []byte, ok bool)
}

EKRegistry is the verifier's trust store for Endorsement Keys and the EK->AK bindings established at enrolment. It is the policy boundary for "which TPMs may ever join": Trusted decides whether an EK public is acceptable (e.g. its certificate chains to a known manufacturer root, or it is on an explicit allowlist), and BindAK records that an AK Name has been cryptographically tied — via ActivateCredential — to such an EK.

Implementations MUST be safe for concurrent use by multiple goroutines.

type EnrollChallenge

type EnrollChallenge struct {
	CredentialBlob []byte
	Secret         []byte
}

EnrollChallenge is the verifier's MakeCredential response: the credentialBlob (TPM2B_ID_OBJECT) and secret (TPM2B_ENCRYPTED_SECRET) the node feeds to TPM2_ActivateCredential. Only a TPM holding the EK private key AND an AK with the committed Name can recover the embedded activation secret.

func (EnrollChallenge) Marshal

func (m EnrollChallenge) Marshal() []byte

Marshal encodes the EnrollChallenge to a frame.

func (*EnrollChallenge) Unmarshal

func (m *EnrollChallenge) Unmarshal(buf []byte) error

Unmarshal decodes a frame into the EnrollChallenge.

type EnrollProof

type EnrollProof struct {
	ActivationSecret []byte
}

EnrollProof is the node's proof of possession: the activation secret it recovered from ActivateCredential. The verifier constant-time-compares it to the secret it embedded; equality binds the AK to the trusted EK.

func (EnrollProof) Marshal

func (m EnrollProof) Marshal() []byte

Marshal encodes the EnrollProof to a frame.

func (*EnrollProof) Unmarshal

func (m *EnrollProof) Unmarshal(buf []byte) error

Unmarshal decodes a frame into the EnrollProof.

type EnrollRequest

type EnrollRequest struct {
	EKPub  []byte
	EKCert []byte
	AKPub  []byte
	AKName []byte
}

EnrollRequest is the node's opening enrolment message: its Endorsement Key public area, the EK certificate (carried opaquely; trust is decided by the EKRegistry), the AK public area, and the AK Name (the value MakeCredential must commit to). EKPub is the TPMT_PUBLIC of the EK; AKPub the TPMT_PUBLIC of the AK; AKName the bare (alg||digest) Name from tpm2.ObjectName.

func (EnrollRequest) Marshal

func (m EnrollRequest) Marshal() []byte

Marshal encodes the EnrollRequest to a self-describing frame.

func (*EnrollRequest) Unmarshal

func (m *EnrollRequest) Unmarshal(buf []byte) error

Unmarshal decodes a frame into the EnrollRequest.

type Event added in v0.2.0

type Event struct {
	// PCR is the PCR index this event extended.
	PCR int
	// Type is the TCG event type (EV_*). It is carried verbatim; this package
	// does not interpret it (the allowlist keys on PCR + digest).
	Type uint32
	// Digests maps TPM_ALG_ID -> the digest that was extended for that bank.
	Digests map[uint16][]byte
	// Data is the opaque event payload (EventSize bytes).
	Data []byte
}

Event is one parsed measurement from the log: the PCR it extended, the TCG event type, the per-algorithm digests that were folded into that PCR, and the opaque event data. Digests is keyed by TPM_ALG_ID (e.g. common.AlgSHA256) so a replay can pick the bank it is reconstructing. PFP clause 10.2.1.

func ParseEventLog added in v0.2.0

func ParseEventLog(log []byte) ([]Event, error)

ParseEventLog parses a crypto-agile TCG event log into its events. The first event MUST be the legacy TCG_PCR_EVENT carrying a TCG_EfiSpecIdEvent header (which declares the per-algorithm digest sizes used to parse the rest); every subsequent event is a crypto-agile TCG_PCR_EVENT2. The header event itself is NOT returned (it is metadata, EV_NO_ACTION, never extended into a PCR). PFP clauses 9.4.5.1 and 10.2.1.

type EventLogPolicy

type EventLogPolicy struct {
	// contains filtered or unexported fields
}

EventLogPolicy is the v0.2.0 Policy that REPLAYS a TCG measured-boot event log against the attested PCRs and checks every measured event against an allowlist of approved individual measurements — closing the gap that GoldenPolicy needs a precomputed golden digest PER platform. Its Matches:

  1. parses the crypto-agile log (ParseEventLog) and replays it into virtual PCRs (ReplayPCRs over the SHA-256 bank);
  2. confirms every replayed PCR equals the attested value for that index (ErrEventLogMismatch otherwise) — this is what binds the log to the cryptographically-verified quote, so a tampered log cannot be trusted;
  3. confirms every event's SHA-256 digest is on the allowlist (ErrUnapprovedMeasurement, naming the offending event, otherwise).

The allowlist is a set of approved (PCRindex, SHA-256 digest) measurements, so a single image/component update is ONE new allowlist entry rather than a full per-platform PCR-digest recompute (see doc.go for the tradeoff).

func NewEventLogPolicy added in v0.2.0

func NewEventLogPolicy() *EventLogPolicy

NewEventLogPolicy builds an empty EventLogPolicy whose allowlist is then populated with AllowMeasurement. With an empty allowlist, any non-empty log is rejected at the first event (so a policy must be explicitly provisioned).

func (*EventLogPolicy) AllowMeasurement added in v0.2.0

func (p *EventLogPolicy) AllowMeasurement(pcr int, digest []byte) *EventLogPolicy

AllowMeasurement adds one approved measurement — the SHA-256 digest extended into PCR pcr — to the allowlist. It returns the policy for chaining. The digest is copied; the key combines the PCR index and the digest so the same digest in a different PCR is a distinct (and separately-approved) measurement.

func (*EventLogPolicy) Matches added in v0.2.0

func (p *EventLogPolicy) Matches(pcrs map[int][]byte, eventLog []byte) error

Matches replays eventLog into virtual PCRs, requires them to equal the attested PCRs (proving the log is consistent with the verified quote), and requires every event to be on the allowlist. It satisfies Policy.

func (*EventLogPolicy) RestrictPCRs added in v0.2.0

func (p *EventLogPolicy) RestrictPCRs(pcrs ...int) *EventLogPolicy

RestrictPCRs limits the replay-equality check to the given PCR indices (the log may legitimately touch PCRs the verifier does not gate on). With no restriction set, every PCR the log extends must reproduce its attested value.

type GoldenPolicy

type GoldenPolicy map[int][]byte

GoldenPolicy is the v0.1.0 Policy: a map of required PCR index -> expected digest. Matches requires every golden PCR to be present in the attested set AND to equal its golden value exactly; the first PCR that is missing or differs is named in the returned *UntrustedBootError. Attested PCRs not named in the golden map are ignored (the policy constrains only the PCRs it lists).

func (GoldenPolicy) Matches

func (g GoldenPolicy) Matches(pcrs map[int][]byte, _ []byte) error

Matches checks every golden PCR against the attested values in ascending index order (so the "first mismatch" reported is deterministic).

type LogBuilder added in v0.2.0

type LogBuilder struct {
	// contains filtered or unexported fields
}

LogBuilder constructs a crypto-agile TCG event log (a spec-ID header followed by TCG_PCR_EVENT2 records) for the SHA-256 bank. A platform's measured-boot firmware emits this log as it extends PCRs; this builder lets a Go node (or a test) record the same SHA-256 measurements it extends so the verifier can replay them. Use NewLogBuilder, Add per measurement, then Bytes.

func NewLogBuilder added in v0.2.0

func NewLogBuilder() *LogBuilder

NewLogBuilder starts an empty crypto-agile log builder for the SHA-256 bank.

func (*LogBuilder) Add added in v0.2.0

func (b *LogBuilder) Add(pcr int, etype uint32, sha256Digest, data []byte) *LogBuilder

Add records one measurement: the SHA-256 digest extended into PCR pcr, with TCG event type etype and opaque event data (which may be nil). It mirrors one TPM2_PCR_Extend and returns the builder for chaining. PFP clause 10.2.1 ("TCG_PCR_EVENT2").

func (*LogBuilder) Bytes added in v0.2.0

func (b *LogBuilder) Bytes() []byte

Bytes renders the full log: the legacy TCG_PCR_EVENT spec-ID header (which declares the SHA-256 bank and its 32-byte digest width) followed by the recorded TCG_PCR_EVENT2 events. PFP clauses 9.4.5.1 and 10.2.1.

type MemPendingStore added in v0.3.0

type MemPendingStore struct {
	// contains filtered or unexported fields
}

MemPendingStore is the default in-memory PendingStore: plain maps guarded by a mutex, no expiry. It is the behavior the Verifier had before PendingStore was introduced, and is used by NewVerifier. It is safe for concurrent use.

Note: MemPendingStore keeps only the activation secret and the nonce — the pending AK public area (needed to BindAK on a successful CompleteEnroll) is retained by the Verifier itself, since it is not single-use challenge state and an HA store need not carry it (a node that fails over re-enrolls).

func NewMemPendingStore added in v0.3.0

func NewMemPendingStore() *MemPendingStore

NewMemPendingStore returns an empty in-memory PendingStore.

func (*MemPendingStore) PutEnroll added in v0.3.0

func (m *MemPendingStore) PutEnroll(akName, secret []byte) error

PutEnroll stashes a copy of secret under akName.

func (*MemPendingStore) PutNonce added in v0.3.0

func (m *MemPendingStore) PutNonce(akName []byte, nonce []byte) error

PutNonce stashes a copy of nonce under akName.

func (*MemPendingStore) TakeEnroll added in v0.3.0

func (m *MemPendingStore) TakeEnroll(akName []byte) ([]byte, bool)

TakeEnroll consumes the pending activation secret for akName.

func (*MemPendingStore) TakeNonce added in v0.3.0

func (m *MemPendingStore) TakeNonce(akName []byte) ([]byte, bool)

TakeNonce consumes the pending admission nonce for akName.

type MemRegistry

type MemRegistry struct {
	// contains filtered or unexported fields
}

MemRegistry is a simple thread-safe in-memory EKRegistry. EKs are trusted by an explicit allowlist keyed on the exact EK public bytes; bound AKs are held in a map from AK Name to AK public. It is intended for tests, single-process control planes, and as a reference implementation.

func NewMemRegistry

func NewMemRegistry() *MemRegistry

NewMemRegistry returns an empty MemRegistry. Use TrustEK to allow EKs before enrolling nodes.

func (*MemRegistry) AKPub

func (r *MemRegistry) AKPub(akName []byte) ([]byte, bool)

AKPub returns the bound AK public for akName, if any.

func (*MemRegistry) BindAK

func (r *MemRegistry) BindAK(akName, akPub []byte) error

BindAK stores akPub under akName. It never fails for MemRegistry (the error is part of the interface so a backing store can report a write failure).

func (*MemRegistry) TrustEK

func (r *MemRegistry) TrustEK(ekPub []byte)

TrustEK adds an EK public area to the allowlist, so a node presenting this exact EK may enrol. A production registry would instead validate the EK certificate against a manufacturer root; this explicit allowlist keeps the reference impl dependency-free.

func (*MemRegistry) Trusted

func (r *MemRegistry) Trusted(ekPub []byte) bool

Trusted reports whether ekPub is on the allowlist.

type Node

type Node struct {
	// contains filtered or unexported fields
}

Node is the agent side of the protocol, running on the attesting platform. It drives a real *tpm2.TPM: it owns the EK and AK transient handles, their public areas, and the AK Name, and it answers the verifier's two challenges — RespondEnroll (ActivateCredential) and RespondAdmission (Quote + PCRRead).

A Node is NOT safe for concurrent use: it serializes onto a single TPM, and credential activation opens a policy session per call.

func FromHandles

func FromHandles(tpm *tpm2.TPM, ekHandle uint32, ekPub []byte, akHandle uint32, akPub, akName []byte, pcrSel []tpm2.PCRSelection) (*Node, error)

FromHandles builds a Node from pre-loaded EK and AK handles and their public areas (the contents of TPM2B_PUBLIC). akName is the AK Name; if nil it is derived from akPub via ObjectName. This is for callers that created or loaded the keys themselves. ekPub must be an ECC TPMT_PUBLIC carrying the EK point.

func NewNode

func NewNode(tpm *tpm2.TPM, pcrSel []tpm2.PCRSelection) (*Node, error)

NewNode creates the EK and the AK on tpm and returns a ready Node. It runs CreateEK (the EK Credential Profile L-2 ECC P-256 key), CreatePrimaryPublic (an ECC P-256 restricted signing AK, returning its TPMT_PUBLIC), and derives the AK Name with ObjectName. pcrSel is the PCR selection the node will quote (e.g. SHA-256 bank, PCRs 0..7).

func (*Node) AKName

func (n *Node) AKName() []byte

AKName returns the node's AK Name (used in AdmissionRequest and as the verifier's lookup key).

func (*Node) AdmissionRequest

func (n *Node) AdmissionRequest() AdmissionRequest

AdmissionRequest returns the node's admission message naming its AK.

func (*Node) EnrollRequest

func (n *Node) EnrollRequest(ekCert []byte) EnrollRequest

EnrollRequest returns the node's enrolment message: its EK public area, an (optional) EK certificate, its AK public area, and its AK Name. ekCert may be nil when the trust decision is made on the EK public alone.

func (*Node) RespondAdmission

func (n *Node) RespondAdmission(ch AdmissionChallenge) (AdmissionResponse, error)

RespondAdmission answers the verifier's nonce challenge with a TPM Quote over the node's PCR selection, folding the nonce into the quote's extraData, plus the current PCR values from PCRRead. The signature is rendered as the fixed-width r||s form the wire carries.

func (*Node) RespondEnroll

func (n *Node) RespondEnroll(ch EnrollChallenge) (EnrollProof, error)

RespondEnroll answers the verifier's MakeCredential challenge by running TPM2_ActivateCredential: it opens a policy session, satisfies the EK policy with PolicySecret(TPM_RH_ENDORSEMENT), and activates the credential with the AK as activateHandle and the EK as keyHandle. The recovered activation secret (which only this TPM can produce) is returned as the proof.

type Nonce

type Nonce func() ([32]byte, error)

Nonce is the verifier's source of fresh 32-byte challenges. It is injected so tests can supply a deterministic source; production code uses RandNonce.

type PendingStore added in v0.3.0

type PendingStore interface {
	// PutEnroll stashes secret for akName, overwriting any prior pending
	// enrolment for akName.
	PutEnroll(akName, secret []byte) error
	// TakeEnroll consumes the pending activation secret for akName. It returns
	// ok=false if none is pending (or it was already taken).
	TakeEnroll(akName []byte) (secret []byte, ok bool)
	// PutNonce stashes nonce for akName, overwriting any prior pending nonce.
	PutNonce(akName []byte, nonce []byte) error
	// TakeNonce consumes the pending admission nonce for akName. It returns
	// ok=false if none is pending (or it was already taken).
	TakeNonce(akName []byte) (nonce []byte, ok bool)
}

PendingStore persists the Verifier's single-use challenge state across replicas. Default is in-memory; back it with a shared store (e.g. etcd) for an HA control plane.

The interface is deliberately time/TTL-agnostic: the backing store owns expiry (e.g. via etcd leases). This keeps attest free of any clock import, so the Node side stays import-clean (it builds for GOOS=tamago, which has no host clock). The in-memory default is plain maps with no expiry.

All four methods are single-purpose:

  • PutEnroll / PutNonce stash a value for an AK Name, overwriting any prior value for that AK.
  • TakeEnroll / TakeNonce consume (read-and-remove) a value: a value may be taken at most once. A missing or already-taken entry returns ok=false.

Implementations MUST be safe for concurrent use by multiple goroutines.

type Policy

type Policy interface {
	// Matches reports nil if the PCRs represent an approved stack, or a typed
	// error (e.g. *UntrustedBootError) naming the first failing PCR otherwise.
	// eventLog is the attested TCG event log (may be nil); value-only policies
	// ignore it.
	Matches(pcrs map[int][]byte, eventLog []byte) error
}

Policy decides whether a set of attested PCR values represents an approved boot. The verifier calls Matches AFTER it has cryptographically established that the PCR values are genuine (signature + nonce + pcrDigest all checked), so a Policy only has to compare values, never crypto.

Matches also receives the attested measured-boot event log (resp.EventLog, possibly nil) so a Policy that replays the log — EventLogPolicy — can verify it against the genuine PCRs. Value-only policies (GoldenPolicy) ignore it. The log is NOT independently trusted: a Policy that uses it MUST prove it is consistent with the cryptographically-verified PCRs (replay == PCRs) before drawing any conclusion from its contents. The v0.1.0 signature Matches(pcrs) gained the eventLog parameter in v0.2.0 (a clean minor change: attest has no external API consumers — the only caller is the in-package Verifier, and the validate harness is updated in lockstep).

type UnapprovedMeasurementError added in v0.2.0

type UnapprovedMeasurementError struct {
	// PCR is the PCR index the unapproved event extended.
	PCR int
	// EventType is the TCG event type (EV_*) of the unapproved event.
	EventType uint32
	// Digest is the unapproved SHA-256 measurement.
	Digest []byte
}

UnapprovedMeasurementError is the typed EventLogPolicy failure: an event in the (replay-verified) log whose digest is not on the allowlist. It names the PCR, the TCG event type, and the offending digest. It satisfies the ErrUnapprovedMeasurement sentinel under errors.Is.

func (*UnapprovedMeasurementError) Error added in v0.2.0

Error implements the error interface.

func (*UnapprovedMeasurementError) Is added in v0.2.0

func (e *UnapprovedMeasurementError) Is(target error) bool

Is reports that an *UnapprovedMeasurementError matches the ErrUnapprovedMeasurement sentinel, so callers can write errors.Is(err, ErrUnapprovedMeasurement).

type UntrustedBootError

type UntrustedBootError struct {
	// PCR is the index of the first failing PCR.
	PCR int
	// Reason describes the failure (missing vs. value mismatch).
	Reason string
}

UntrustedBootError is the typed Policy failure: the index of the first PCR that did not match the policy, and a short reason. errors.As lets callers recover the PCR index for diagnostics. It satisfies ErrUntrustedBoot under errors.Is so callers can branch on the sentinel without unwrapping.

func (*UntrustedBootError) Error

func (e *UntrustedBootError) Error() string

Error implements the error interface.

func (*UntrustedBootError) Is

func (e *UntrustedBootError) Is(target error) bool

Is reports that an *UntrustedBootError matches the ErrUntrustedBoot sentinel, so callers can write errors.Is(err, ErrUntrustedBoot).

type Verifier

type Verifier struct {
	// contains filtered or unexported fields
}

Verifier is the control-plane side of the protocol: it admits nodes onto a fleet on proof of an approved boot. It is PURE GO and never touches a TPM — the verifier side of TPM credential protection (MakeCredential) and of quote verification (VerifyQuote) is all off-TPM crypto. A Verifier is safe for concurrent use.

func NewVerifier

func NewVerifier(registry EKRegistry, policy Policy, nonce Nonce) *Verifier

NewVerifier builds a Verifier over a trust registry, an admission policy, and a nonce source. Pass RandNonce for production; a deterministic source for tests. The single-use challenge state is held in an in-memory MemPendingStore — use NewVerifierWithStore to back it with a shared store (e.g. etcd) for a multi-replica control plane.

func NewVerifierWithStore added in v0.3.0

func NewVerifierWithStore(registry EKRegistry, policy Policy, nonce Nonce, store PendingStore) *Verifier

NewVerifierWithStore is NewVerifier with an explicit PendingStore for the single-use enrolment-secret and admission-nonce state. Pass a shared store (e.g. an etcd-backed one) so an Enroll/Challenge served by one replica is visible to the CompleteEnroll/Admit served by another (HA control plane). A nil store falls back to a fresh in-memory MemPendingStore.

func (*Verifier) Admit

func (v *Verifier) Admit(akName []byte, resp AdmissionResponse) (bool, error)

Admit decides whether to grant the node admission. It looks up the bound AK and the pending nonce, then in order:

  1. verifies the Quote's ECDSA signature under the AK public AND that the quoted pcrDigest equals the digest over the claimed PCRs (tpm2.VerifyQuote does both);
  2. checks the attest's extraData equals exactly the issued nonce (anti-replay — VerifyQuote returns the parsed AttestInfo but does NOT check the nonce, so it is checked explicitly here);
  3. applies the boot Policy to the claimed PCRs, passing the attested event log too (an EventLogPolicy replays it and binds it to these PCRs; a GoldenPolicy ignores it).

Each failure returns a precise sentinel. The pending nonce is consumed on every call so a quote cannot be replayed against the same challenge.

func (*Verifier) Challenge

func (v *Verifier) Challenge(req AdmissionRequest) (AdmissionChallenge, error)

Challenge issues a fresh admission nonce for a bound AK and stashes it pending the node's quote. An unbound AK is refused.

func (*Verifier) CompleteEnroll

func (v *Verifier) CompleteEnroll(akName []byte, proof EnrollProof) error

CompleteEnroll finishes enrolment: it constant-time-compares the node's recovered ActivationSecret to the pending one and, on a match, binds the AK to the trusted EK in the registry. The pending entry is dropped either way (so a failed proof cannot be retried against the same challenge).

func (*Verifier) Enroll

func (v *Verifier) Enroll(req EnrollRequest) (EnrollChallenge, error)

Enroll begins enrolment: it rejects an untrusted EK, draws a random activation secret, and runs MakeCredential OFF the TPM (from the EK public point and the AK Name) to produce the credentialBlob/secret challenge. The activation secret is stashed pending the node's proof. Only a TPM holding the EK private key and an AK with this exact Name can recover the secret.

func (*Verifier) SetPolicy

func (v *Verifier) SetPolicy(p Policy)

SetPolicy swaps the admission Policy. It is used after the first boot to install a GoldenPolicy built from the freshly measured PCRs (the common "trust on first attestation" bootstrap). It is safe for concurrent use.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL