// SPDX-License-Identifier: Apache-2.0 // Package keylime implements attestation status checking against // the Keylime verifier or the posture-current ConfigMap. // // This is the data source for the Keylime NodeAttestor SPIRE plugin. // Keylime remains the single TPM authority — this package queries // attestation results, never touches hardware. package keylime import ( "encoding/json" "fmt" "net/http" "os" "path/filepath" "time" ) // AttestationStatus represents the Keylime verdict for a node. type AttestationStatus struct { Verdict string `json:"verdict"` // Attested, Pending, Failed, Unknown PostureLevel uint8 `json:"posture_level"` // 1=Lockdown..5=Normal LastSuccessful string `json:"last_successful"` Reason string `json:"reason"` } // IsAttested returns true if the node is currently attested. func (s *AttestationStatus) IsAttested() bool { return s.Verdict == "Attested" } // Client queries Keylime attestation status from one of two sources. type Client struct { source string // "configmap" or "verifier" filePath string // ConfigMap mount path verifier string // Keylime verifier URL apiVer string // API version (default "2.1") http *http.Client maxAgeSec int } // Config for the Keylime client. type Config struct { Source string `hcl:"source"` PostureConfigMapPath string `hcl:"posture_configmap_path"` VerifierURL string `hcl:"verifier_url"` APIVersion string `hcl:"api_version"` TimeoutMs int `hcl:"timeout_ms"` MaxAttestationAge int `hcl:"max_attestation_age_secs"` } // DefaultConfig returns sensible defaults. func DefaultConfig() *Config { return &Config{ Source: "configmap", PostureConfigMapPath: "/var/run/posture/posture-current", APIVersion: "2.1", TimeoutMs: 5000, MaxAttestationAge: 600, } } // NewClient creates a Keylime attestation client. func NewClient(cfg *Config) *Client { if cfg == nil { cfg = DefaultConfig() } return &Client{ source: cfg.Source, filePath: cfg.PostureConfigMapPath, verifier: cfg.VerifierURL, apiVer: cfg.APIVersion, maxAgeSec: cfg.MaxAttestationAge, http: &http.Client{ Timeout: time.Duration(cfg.TimeoutMs) * time.Millisecond, }, } } // GetStatus queries attestation status for a node. func (c *Client) GetStatus(nodeID string) (*AttestationStatus, error) { switch c.source { case "verifier": return c.getFromVerifier(nodeID) default: return c.getFromConfigMap(nodeID) } } func (c *Client) getFromConfigMap(nodeID string) (*AttestationStatus, error) { // posture-current ConfigMap contains a "level" key with the // cluster-wide posture level. Per-node keying is future work; // for now use the global level for all nodes. levelPath := filepath.Join(c.filePath, "level") data, err := os.ReadFile(levelPath) if err != nil { // Try reading the node-specific key nodePath := filepath.Join(c.filePath, nodeID) data, err = os.ReadFile(nodePath) if err != nil { return &AttestationStatus{ Verdict: "Unknown", Reason: fmt.Sprintf("posture data not found: %v", err), }, nil } } level := string(data) // The level key contains "1"-"5" as a string switch level { case "5": return &AttestationStatus{Verdict: "Attested", PostureLevel: 5}, nil case "4": return &AttestationStatus{Verdict: "Attested", PostureLevel: 4, Reason: "elevated"}, nil case "3": return &AttestationStatus{Verdict: "Pending", PostureLevel: 3, Reason: "restricted"}, nil case "2": return &AttestationStatus{Verdict: "Failed", PostureLevel: 2, Reason: "critical"}, nil case "1": return &AttestationStatus{Verdict: "Failed", PostureLevel: 1, Reason: "lockdown"}, nil default: // Try parsing as JSON (per-node format) var status AttestationStatus if err := json.Unmarshal([]byte(level), &status); err == nil { return &status, nil } return &AttestationStatus{Verdict: "Unknown", Reason: "unparseable level: " + level}, nil } } func (c *Client) getFromVerifier(nodeID string) (*AttestationStatus, error) { url := fmt.Sprintf("%s/v%s/agents/%s", c.verifier, c.apiVer, nodeID) resp, err := c.http.Get(url) if err != nil { return nil, fmt.Errorf("verifier request failed: %w", err) } defer resp.Body.Close() if resp.StatusCode == http.StatusNotFound { return &AttestationStatus{ Verdict: "Unknown", Reason: "agent not registered with Keylime verifier", }, nil } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("verifier returned %d", resp.StatusCode) } var vr struct { Results struct { OperationalState *int `json:"operational_state"` LastSuccessful *int64 `json:"last_successful_attestation"` } `json:"results"` } if err := json.NewDecoder(resp.Body).Decode(&vr); err != nil { return nil, fmt.Errorf("verifier response parse: %w", err) } verdict := mapOpState(vr.Results.OperationalState) var lastStr string if ts := vr.Results.LastSuccessful; ts != nil && *ts > 0 { lastStr = time.Unix(*ts, 0).UTC().Format(time.RFC3339) } return &AttestationStatus{ Verdict: verdict, LastSuccessful: lastStr, }, nil } func mapOpState(state *int) string { if state == nil { return "Pending" // push model — no operational_state } switch *state { case 7: return "Attested" case 0, 1, 2, 3, 4, 5, 6: return "Pending" case 8, 9, 10: return "Failed" default: return "Unknown" } }