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.
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)
- type AdmissionChallenge
- type AdmissionRequest
- type AdmissionResponse
- type EKRegistry
- type EnrollChallenge
- type EnrollProof
- type EnrollRequest
- type EventLogPolicy
- type GoldenPolicy
- type MemRegistry
- type Node
- type Nonce
- type Policy
- 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 ( // 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 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.
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 EventLogPolicy ¶
type EventLogPolicy interface {
Policy
// Replay folds the event log into virtual PCRs and reports whether it is
// consistent with the attested PCRs (stub for v0.2).
Replay(eventLog []byte, pcrs map[int][]byte) error
}
EventLogPolicy is the planned v0.2 Policy that REPLAYS a TCG measured-boot event log against the attested PCRs and checks every measured event against an allowlist — closing the gap that GoldenPolicy needs a precomputed golden digest per platform. It is declared here as a documented stub so the interface and intent are stable.
TODO(v0.2): implement TCG event-log (TPM2_EventLog / EFI_TCG2 format) replay:
- parse the event stream (spec-id header + per-event digests),
- fold each event's digest into a virtual PCR bank and assert the result equals the attested PCR value (proves the log is consistent with the quote), and
- check every event against an allowlist of approved measurements,
then satisfy Policy by delegating value-level checks to a GoldenPolicy or to the replayed expectations.
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 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 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.
Matches(pcrs map[int][]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.
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.
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.
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.