feat(tpm): add read-only TPM validation
This commit is contained in:
@@ -52,6 +52,7 @@ func Run(_ runtimeenv.Mode) schema.HardwareIngestRequest {
|
||||
snap.PowerSupplies = enrichPSUsWithTelemetry(snap.PowerSupplies, sensorDoc)
|
||||
snap.Sensors = mergeIPMISensors(buildSensorsFromDoc(sensorDoc), collectIPMISensors())
|
||||
snap.EventLogs = append(collectIPMISEL(), collectDmesgErrors()...)
|
||||
snap.PlatformConfig = collectTPMPlatformConfig()
|
||||
finalizeSnapshot(&snap, collectedAt)
|
||||
|
||||
// remaining collectors added in steps 1.8 – 1.10
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
tpmGlob = filepath.Glob
|
||||
tpmStat = os.Stat
|
||||
tpmRun = func(name string, args ...string) ([]byte, error) {
|
||||
return exec.Command(name, args...).Output()
|
||||
}
|
||||
)
|
||||
|
||||
// collectTPMPlatformConfig records TPM presence and read-only identity data.
|
||||
// It never provisions the TPM, changes ownership, writes NV storage, or runs
|
||||
// TPM2_SelfTest.
|
||||
func collectTPMPlatformConfig() *json.RawMessage {
|
||||
config := map[string]any{"TpmPresent": false, "TpmEnabled": false}
|
||||
devices, _ := tpmGlob("/sys/class/tpm/tpm*")
|
||||
if len(devices) == 0 {
|
||||
return rawPlatformConfig(config)
|
||||
}
|
||||
|
||||
config["TpmPresent"] = true
|
||||
config["TpmEnabled"] = true
|
||||
config["TpmDevice"] = filepath.Base(devices[0])
|
||||
for _, path := range []string{"/dev/tpmrm0", "/dev/tpm0"} {
|
||||
if _, err := tpmStat(path); err == nil {
|
||||
config["TpmInterface"] = path
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
out, err := tpmRun("tpm2_getcap", "properties-fixed")
|
||||
if err != nil {
|
||||
return rawPlatformConfig(config)
|
||||
}
|
||||
properties := parseTPMProperties(string(out))
|
||||
if value := tpmPropertyValue(properties, "TPM2_PT_FAMILY_INDICATOR"); value != "" {
|
||||
config["TpmVersion"] = value
|
||||
}
|
||||
if value := tpmManufacturer(properties["TPM2_PT_MANUFACTURER"]); value != "" {
|
||||
config["TpmManufacturer"] = value
|
||||
}
|
||||
firmware1 := tpmPropertyRaw(properties, "TPM2_PT_FIRMWARE_VERSION_1")
|
||||
firmware2 := tpmPropertyRaw(properties, "TPM2_PT_FIRMWARE_VERSION_2")
|
||||
if firmware1 != "" || firmware2 != "" {
|
||||
config["TpmFirmwareVersion"] = strings.Trim(strings.Join([]string{firmware1, firmware2}, "/"), "/")
|
||||
}
|
||||
return rawPlatformConfig(config)
|
||||
}
|
||||
|
||||
type tpmProperty struct {
|
||||
raw string
|
||||
value string
|
||||
}
|
||||
|
||||
func parseTPMProperties(input string) map[string]tpmProperty {
|
||||
properties := make(map[string]tpmProperty)
|
||||
current := ""
|
||||
for _, line := range strings.Split(input, "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "TPM2_PT_") && strings.HasSuffix(trimmed, ":") {
|
||||
current = strings.TrimSuffix(trimmed, ":")
|
||||
continue
|
||||
}
|
||||
if current == "" {
|
||||
continue
|
||||
}
|
||||
property := properties[current]
|
||||
switch {
|
||||
case strings.HasPrefix(trimmed, "raw:"):
|
||||
property.raw = strings.TrimSpace(strings.TrimPrefix(trimmed, "raw:"))
|
||||
case strings.HasPrefix(trimmed, "value:"):
|
||||
property.value = strings.Trim(strings.TrimSpace(strings.TrimPrefix(trimmed, "value:")), `"`)
|
||||
default:
|
||||
continue
|
||||
}
|
||||
properties[current] = property
|
||||
}
|
||||
return properties
|
||||
}
|
||||
|
||||
func tpmPropertyValue(properties map[string]tpmProperty, name string) string {
|
||||
property := properties[name]
|
||||
if property.value != "" {
|
||||
return property.value
|
||||
}
|
||||
return property.raw
|
||||
}
|
||||
|
||||
func tpmPropertyRaw(properties map[string]tpmProperty, name string) string {
|
||||
return properties[name].raw
|
||||
}
|
||||
|
||||
func tpmManufacturer(property tpmProperty) string {
|
||||
if property.value != "" {
|
||||
return property.value
|
||||
}
|
||||
raw := strings.TrimPrefix(property.raw, "0x")
|
||||
value, err := strconv.ParseUint(raw, 16, 32)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
bytes := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(bytes, uint32(value))
|
||||
for _, b := range bytes {
|
||||
if b != 0 && (b < 0x20 || b > 0x7e) {
|
||||
return property.raw
|
||||
}
|
||||
}
|
||||
return strings.TrimRight(string(bytes), "\x00 ")
|
||||
}
|
||||
|
||||
func rawPlatformConfig(config map[string]any) *json.RawMessage {
|
||||
raw, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
message := json.RawMessage(raw)
|
||||
return &message
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseTPMProperties(t *testing.T) {
|
||||
properties := parseTPMProperties(`TPM2_PT_FAMILY_INDICATOR:
|
||||
raw: 0x322E3000
|
||||
value: "2.0"
|
||||
TPM2_PT_MANUFACTURER:
|
||||
raw: 0x49465800
|
||||
value: "IFX"
|
||||
TPM2_PT_FIRMWARE_VERSION_1:
|
||||
raw: 0x0007003F
|
||||
TPM2_PT_FIRMWARE_VERSION_2:
|
||||
raw: 0x00100023
|
||||
`)
|
||||
|
||||
if got := tpmPropertyValue(properties, "TPM2_PT_FAMILY_INDICATOR"); got != "2.0" {
|
||||
t.Fatalf("family indicator = %q, want 2.0", got)
|
||||
}
|
||||
if got := tpmManufacturer(properties["TPM2_PT_MANUFACTURER"]); got != "IFX" {
|
||||
t.Fatalf("manufacturer = %q, want IFX", got)
|
||||
}
|
||||
if got := tpmPropertyRaw(properties, "TPM2_PT_FIRMWARE_VERSION_1"); got != "0x0007003F" {
|
||||
t.Fatalf("firmware version 1 = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTPMManufacturerDecodesRawVendorID(t *testing.T) {
|
||||
if got := tpmManufacturer(tpmProperty{raw: "0x49465800"}); got != "IFX" {
|
||||
t.Fatalf("manufacturer = %q, want IFX", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectTPMPlatformConfig(t *testing.T) {
|
||||
originalGlob, originalStat, originalRun := tpmGlob, tpmStat, tpmRun
|
||||
t.Cleanup(func() {
|
||||
tpmGlob, tpmStat, tpmRun = originalGlob, originalStat, originalRun
|
||||
})
|
||||
|
||||
tpmGlob = func(string) ([]string, error) { return []string{"/sys/class/tpm/tpm0"}, nil }
|
||||
tpmStat = func(path string) (os.FileInfo, error) {
|
||||
if path == "/dev/tpmrm0" {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
tpmRun = func(name string, args ...string) ([]byte, error) {
|
||||
if name != "tpm2_getcap" || !reflect.DeepEqual(args, []string{"properties-fixed"}) {
|
||||
t.Fatalf("unexpected command: %s %v", name, args)
|
||||
}
|
||||
return []byte(`TPM2_PT_FAMILY_INDICATOR:
|
||||
value: "2.0"
|
||||
TPM2_PT_MANUFACTURER:
|
||||
raw: 0x49465800
|
||||
TPM2_PT_FIRMWARE_VERSION_1:
|
||||
raw: 0x0007003F
|
||||
TPM2_PT_FIRMWARE_VERSION_2:
|
||||
raw: 0x00100023
|
||||
`), nil
|
||||
}
|
||||
|
||||
got := decodeTPMConfig(t, collectTPMPlatformConfig())
|
||||
want := map[string]any{
|
||||
"TpmPresent": true,
|
||||
"TpmEnabled": true,
|
||||
"TpmDevice": "tpm0",
|
||||
"TpmInterface": "/dev/tpmrm0",
|
||||
"TpmVersion": "2.0",
|
||||
"TpmManufacturer": "IFX",
|
||||
"TpmFirmwareVersion": "0x0007003F/0x00100023",
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("config = %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectTPMPlatformConfigAbsent(t *testing.T) {
|
||||
originalGlob := tpmGlob
|
||||
t.Cleanup(func() { tpmGlob = originalGlob })
|
||||
tpmGlob = func(string) ([]string, error) { return nil, errors.New("not found") }
|
||||
|
||||
got := decodeTPMConfig(t, collectTPMPlatformConfig())
|
||||
want := map[string]any{"TpmPresent": false, "TpmEnabled": false}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("config = %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func decodeTPMConfig(t *testing.T, raw *json.RawMessage) map[string]any {
|
||||
t.Helper()
|
||||
if raw == nil {
|
||||
t.Fatal("platform config is nil")
|
||||
}
|
||||
var config map[string]any
|
||||
if err := json.Unmarshal(*raw, &config); err != nil {
|
||||
t.Fatalf("unmarshal config: %v", err)
|
||||
}
|
||||
return config
|
||||
}
|
||||
Reference in New Issue
Block a user