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
- func JoinSignature(sig tpm2.ECDSASignature) []byte
- func RandNonce() ([32]byte, error)
- func ReplayPCRs(events []Event, alg uint16) (map[int][]byte, error)
- type AdmissionChallenge
- type AdmissionRequest
- type AdmissionResponse
- type EKRegistry
- type EnrollChallenge
- type EnrollProof
- type EnrollRequest
- type Event
- type EventLogPolicy
- type GoldenPolicy
- type LogBuilder
- type MemPendingStore
- type MemRegistry
- type Node
- type Nonce
- type PendingStore
- type Policy
- type UnapprovedMeasurementError
- type UntrustedBootError
- type Verifier
- func (v *Verifier) Admit(akName []byte, resp AdmissionResponse) (bool, error)
- func (v *Verifier) Challenge(req AdmissionRequest) (AdmissionChallenge, error)
- func (v *Verifier) CompleteEnroll(akName []byte, proof EnrollProof) error
- func (v *Verifier) Enroll(req EnrollRequest) (EnrollChallenge, error)
- func (v *Verifier) SetPolicy(p Policy)
Constants ¶
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 ==).
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 ==).
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 ==).
const ErrBadPublic = common.Error("attest: malformed ECC TPMT_PUBLIC")
ErrBadPublic is returned when a public area is not a parseable ECC TPMT_PUBLIC.
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.
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 ReplayPCRs ¶ added in v0.2.0
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 ¶
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 ¶
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
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:
- parses the crypto-agile log (ParseEventLog) and replays it into virtual PCRs (ReplayPCRs over the SHA-256 bank);
- 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;
- 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 ¶
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).
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.
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 ¶
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 ¶
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 ¶
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
func (e *UnapprovedMeasurementError) Error() string
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:
- 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);
- 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);
- 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.