feat(tpm): add read-only TPM validation

This commit is contained in:
Mikhail Chusavitin
2026-08-26 17:13:50 +03:00
parent 9e466b8a70
commit af95216c6f
27 changed files with 434 additions and 10 deletions
+1
View File
@@ -1542,6 +1542,7 @@ func (h *handler) handleAPISystemTimeSync(w http.ResponseWriter, r *http.Request
var standardTools = []string{
"dmidecode", "smartctl", "nvme", "lspci", "ipmitool",
"tpm2_getcap", "tpm2_pcrread", "tpm2_gettestresult",
"nvidia-smi", "dcgmi", "nv-hostengine", "memtester", "stress-ng", "nvtop",
"mstflint", "saa",
}
+44 -4
View File
@@ -22,6 +22,7 @@ type validateInventory struct {
CPU string
Memory string
Storage string
TPM string
NVIDIA string
AMD string
NvidiaGPUCount int
@@ -42,6 +43,7 @@ func validateTotalValidateSec(n int) int {
}
total := platform.SATEstimatedCPUValidateSec +
platform.SATEstimatedMemoryValidateSec +
platform.SATEstimatedTPMValidateSec +
platform.SATEstimatedNvidiaInterconnectSec +
platform.SATEstimatedNvidiaBandwidthSec
if n > 0 {
@@ -147,6 +149,12 @@ func renderValidateMode(opts HandlerOptions, stressDefault bool) string {
`<code>lsblk</code>; NVMe: <code>nvme id-ctrl</code>, <code>nvme smart-log</code>, <code>nvme device-self-test -s 1</code>; SATA/SAS: <code>smartctl -H -A</code>, <code>smartctl -t short</code>`,
`~2 min per device (NVMe short self-test; SATA/SAS short self-test — duration device-dependent).`,
)) +
renderSATCard("tpm", "TPM", "runSAT('tpm')", "", renderValidateCardBody(
inv.TPM,
`Checks TPM 2.0 communication and reports its fixed properties, allocated PCR banks, current PCR values, and the result of self-tests already performed by the TPM. It does not start a new self-test or change TPM state.`,
`<code>tpm2_getcap properties-fixed</code>, <code>tpm2_getcap pcrs</code>, <code>tpm2_pcrread</code>, <code>tpm2_gettestresult</code>`,
`Seconds - read-only queries; no ownership, NV, PCR, or key changes.`,
)) +
`</div>
<div style="height:1px;background:var(--border);margin:16px 0"></div>
<div class="card" style="margin-bottom:16px">
@@ -211,7 +219,7 @@ func renderValidateMode(opts HandlerOptions, stressDefault bool) string {
let satES = null;
` + satStressModeJS + `
function satLabels() {
return {nvidia:'Validate GPU', 'nvidia-targeted-stress':'NVIDIA Targeted Stress (dcgmi diag targeted_stress)', 'nvidia-targeted-power':'NVIDIA Targeted Power (dcgmi diag targeted_power)', 'nvidia-pulse':'NVIDIA PSU Pulse Test (dcgmi diag pulse_test)', 'nvidia-interconnect':'NVIDIA Interconnect (NCCL all_reduce_perf)', 'nvidia-bandwidth':'NVIDIA Bandwidth (NVBandwidth)', memory:'Validate Memory', storage:'Validate Storage', cpu:'Validate CPU', amd:'Validate AMD GPU', 'amd-mem':'AMD GPU MEM Integrity', 'amd-bandwidth':'AMD GPU MEM Bandwidth'};
return {nvidia:'Validate GPU', 'nvidia-targeted-stress':'NVIDIA Targeted Stress (dcgmi diag targeted_stress)', 'nvidia-targeted-power':'NVIDIA Targeted Power (dcgmi diag targeted_power)', 'nvidia-pulse':'NVIDIA PSU Pulse Test (dcgmi diag pulse_test)', 'nvidia-interconnect':'NVIDIA Interconnect (NCCL all_reduce_perf)', 'nvidia-bandwidth':'NVIDIA Bandwidth (NVBandwidth)', memory:'Validate Memory', storage:'Validate Storage', tpm:'Validate TPM (read-only)', cpu:'Validate CPU', amd:'Validate AMD GPU', 'amd-mem':'AMD GPU MEM Integrity', 'amd-bandwidth':'AMD GPU MEM Bandwidth'};
}
let satNvidiaGPUsPromise = null;
function loadSatNvidiaGPUs() {
@@ -416,7 +424,7 @@ function runAllSAT() {
const status = document.getElementById('sat-all-status');
status.textContent = 'Enqueuing...';
const stressOnlyTargets = ['nvidia-targeted-stress', 'nvidia-targeted-power', 'nvidia-pulse'];
const baseTargets = ['nvidia','nvidia-targeted-stress','nvidia-targeted-power','nvidia-pulse','nvidia-interconnect','nvidia-bandwidth','memory','storage','cpu'].concat(selectedAMDValidateTargets());
const baseTargets = ['nvidia','nvidia-targeted-stress','nvidia-targeted-power','nvidia-pulse','nvidia-interconnect','nvidia-bandwidth','memory','storage','tpm','cpu'].concat(selectedAMDValidateTargets());
const activeTargets = baseTargets.filter(target => {
if (stressOnlyTargets.indexOf(target) >= 0 && !satStressMode()) return false;
const btn = document.getElementById('sat-btn-' + target);
@@ -498,6 +506,7 @@ func loadValidateInventory(opts HandlerOptions) validateInventory {
CPU: unknown,
Memory: unknown,
Storage: unknown,
TPM: unknown,
NVIDIA: unknown,
AMD: unknown,
}
@@ -561,6 +570,7 @@ func loadValidateInventory(opts HandlerOptions) validateInventory {
out.CPU = formatValidateDeviceSummary(cpuTotal, cpuCounts, "CPU")
out.Memory = formatValidateDeviceSummary(memTotal, memCounts, "module")
out.Storage = formatValidateDeviceSummary(storageTotal, storageCounts, "device")
out.TPM = formatValidateTPMSummary(snap.Hardware.PlatformConfig)
out.NVIDIA = formatValidateDeviceSummary(nvidiaTotal, nvidiaCounts, "GPU")
out.AMD = formatValidateDeviceSummary(amdTotal, amdCounts, "GPU")
out.NvidiaGPUCount = nvidiaTotal
@@ -568,6 +578,30 @@ func loadValidateInventory(opts HandlerOptions) validateInventory {
return out
}
func formatValidateTPMSummary(raw *json.RawMessage) string {
if raw == nil {
return "TPM presence was not collected."
}
var config map[string]any
if err := json.Unmarshal(*raw, &config); err != nil {
return "TPM presence was not collected."
}
present, ok := config["TpmPresent"].(bool)
if !ok {
return "TPM presence was not collected."
}
if !present {
return "No TPM detected."
}
parts := []string{"TPM detected"}
for _, key := range []string{"TpmVersion", "TpmManufacturer", "TpmFirmwareVersion", "TpmInterface"} {
if value, ok := config[key].(string); ok && strings.TrimSpace(value) != "" {
parts = append(parts, html.EscapeString(value))
}
}
return strings.Join(parts, " / ")
}
func renderValidateCardBody(devices, description, commands, settings string) string {
return `<div class="validate-card-section"><div style="font-size:13px;color:var(--muted)">` + devices + `</div></div>` +
`<div class="validate-card-section"><div style="font-size:13px">` + description + `</div></div>` +
@@ -665,6 +699,12 @@ func renderCheck(opts HandlerOptions) string {
`<code>lsblk</code>; NVMe: <code>nvme id-ctrl</code>, <code>nvme smart-log</code>; SATA/SAS: <code>smartctl -H -A</code>`,
`Seconds — instantaneous device query, no wear counters incremented.`,
)) +
renderSATCard("tpm", "TPM", "runSAT('tpm')", "", renderValidateCardBody(
inv.TPM,
`Checks TPM 2.0 communication and reads fixed properties, allocated PCR banks, current PCR values, and the result of self-tests already performed by the TPM. It never starts a new self-test.`,
`<code>tpm2_getcap properties-fixed</code>, <code>tpm2_getcap pcrs</code>, <code>tpm2_pcrread</code>, <code>tpm2_gettestresult</code>`,
`Seconds - read-only queries; no ownership, NV, PCR, or key changes.`,
)) +
renderSATCard("nvidia-config", "GPU Config & NVLink", "runSAT('nvidia-config')", "", renderValidateCardBody(
inv.NVIDIA,
`Checks GPU configuration and NVLink topology that DCGM diag does not cover: ECC/MIG/power-limit drift from factory default, NVLink-bonded pair link count and error counters, and (informational) NVIDIA Confidential Computing readiness (CPU TEE support + GPU firmware CC capability). Read-only — changes nothing.`,
@@ -744,7 +784,7 @@ func renderCheck(opts HandlerOptions) string {
<script>
let satES = null;
function satLabels() {
return {nvidia:'Check GPU (DCGM L2)', 'nvidia-interconnect':'NVIDIA Interconnect (NCCL)', 'nvidia-bandwidth':'NVIDIA Bandwidth (NVBandwidth)', memory:'Check Memory', storage:'Check Storage', cpu:'Check CPU', amd:'Check AMD GPU', 'amd-mem':'AMD GPU MEM Integrity', 'amd-bandwidth':'AMD GPU MEM Bandwidth', 'nvidia-config':'Check GPU Config & NVLink', 'pcie-link':'PCIe Link Check', 'nvidia-pcie-bandwidth':'GPU PCIe Bandwidth Check'};
return {nvidia:'Check GPU (DCGM L2)', 'nvidia-interconnect':'NVIDIA Interconnect (NCCL)', 'nvidia-bandwidth':'NVIDIA Bandwidth (NVBandwidth)', memory:'Check Memory', storage:'Check Storage', tpm:'Check TPM (read-only)', cpu:'Check CPU', amd:'Check AMD GPU', 'amd-mem':'AMD GPU MEM Integrity', 'amd-bandwidth':'AMD GPU MEM Bandwidth', 'nvidia-config':'Check GPU Config & NVLink', 'pcie-link':'PCIe Link Check', 'nvidia-pcie-bandwidth':'GPU PCIe Bandwidth Check'};
}
let satNvidiaGPUsPromise = null;
function loadSatNvidiaGPUs() {
@@ -880,7 +920,7 @@ function runAllCheckSAT() {
status.textContent = 'Enqueuing...';
const nvidiaIndices = satSelectedGPUIndices();
const nvidiaAllTargets = ['nvidia', 'nvidia-interconnect', 'nvidia-bandwidth', 'nvidia-pcie-bandwidth'];
const baseTargets = ['cpu', 'memory', 'storage', 'nvidia-config', 'pcie-link'];
const baseTargets = ['cpu', 'memory', 'storage', 'tpm', 'nvidia-config', 'pcie-link'];
const amdTargets = selectedAMDValidateTargets();
const expanded = [];
baseTargets.forEach(t => expanded.push({target: t}));
@@ -0,0 +1,49 @@
package webui
import (
"encoding/json"
"strings"
"testing"
)
func TestFormatValidateTPMSummary(t *testing.T) {
t.Parallel()
tests := []struct {
name string
raw string
want string
}{
{name: "absent", raw: `{"TpmPresent":false}`, want: "No TPM detected."},
{name: "present", raw: `{"TpmPresent":true,"TpmVersion":"2.0","TpmManufacturer":"IFX","TpmFirmwareVersion":"7.63","TpmInterface":"/dev/tpmrm0"}`, want: "TPM detected / 2.0 / IFX / 7.63 / /dev/tpmrm0"},
{name: "legacy snapshot", raw: `{}`, want: "TPM presence was not collected."},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
raw := json.RawMessage(tt.raw)
if got := formatValidateTPMSummary(&raw); got != tt.want {
t.Fatalf("summary=%q want %q", got, tt.want)
}
})
}
}
func TestRenderCheckIncludesReadOnlyTPMValidation(t *testing.T) {
t.Parallel()
page := renderCheck(HandlerOptions{})
for _, want := range []string{
`id="sat-btn-tpm"`,
`tpm2_getcap properties-fixed`,
`tpm2_pcrread`,
`tpm2_gettestresult`,
`'storage', 'tpm', 'nvidia-config'`,
} {
if !strings.Contains(page, want) {
t.Fatalf("check page does not contain %q", want)
}
}
if strings.Contains(page, "tpm2_selftest") {
t.Fatal("check page must not offer tpm2_selftest")
}
}
+1
View File
@@ -264,6 +264,7 @@ func NewHandler(opts HandlerOptions) http.Handler {
mux.HandleFunc("POST /api/sat/nvidia-stress/run", h.handleAPISATRun("nvidia-stress"))
mux.HandleFunc("POST /api/sat/memory/run", h.handleAPISATRun("memory"))
mux.HandleFunc("POST /api/sat/storage/run", h.handleAPISATRun("storage"))
mux.HandleFunc("POST /api/sat/tpm/run", h.handleAPISATRun("tpm"))
mux.HandleFunc("POST /api/sat/nvidia-config/run", h.handleAPISATRun("nvidia-config"))
mux.HandleFunc("POST /api/sat/pcie-link/run", h.handleAPISATRun("pcie-link"))
mux.HandleFunc("POST /api/sat/nvidia-pcie-bandwidth/run", h.handleAPISATRun("nvidia-pcie-bandwidth"))
+6
View File
@@ -288,6 +288,12 @@ func executeTaskWithOptions(opts *HandlerOptions, t *Task, j *jobState, ctx cont
break
}
archive, err = runStorageAcceptancePackCtx(a, ctx, "", t.params.StressMode, j.append)
case "tpm":
if a == nil {
err = fmt.Errorf("app not configured")
break
}
archive, err = runTPMValidationPackCtx(a, ctx, "", j.append)
case "nvidia-config":
if a == nil {
err = fmt.Errorf("app not configured")
+4
View File
@@ -45,6 +45,7 @@ var taskNames = map[string]string{
"nvidia-stress": "NVIDIA GPU Stress",
"memory": "Memory SAT",
"storage": "Storage SAT",
"tpm": "TPM Read-only Check",
"nvidia-config": "GPU Config & NVLink Check",
"pcie-link": "PCIe Link Check (forced retrain)",
"nvidia-pcie-bandwidth": "NVIDIA GPU PCIe Bandwidth Check",
@@ -316,6 +317,9 @@ var (
runStorageAcceptancePackCtx = func(a *app.App, ctx context.Context, baseDir string, extended bool, logFunc func(string)) (string, error) {
return a.RunStorageAcceptancePackCtx(ctx, baseDir, extended, logFunc)
}
runTPMValidationPackCtx = func(a *app.App, ctx context.Context, baseDir string, logFunc func(string)) (string, error) {
return a.RunTPMValidationPackCtx(ctx, baseDir, logFunc)
}
runNvidiaConfigCheckPackCtx = func(a *app.App, ctx context.Context, baseDir string, logFunc func(string)) (string, error) {
return a.RunNvidiaConfigCheckPackCtx(ctx, baseDir, logFunc)
}