From ac140324ebfb54f580c9b9bbbb55549bd5ffa11e Mon Sep 17 00:00:00 2001 From: Noel Georgi Date: Mon, 5 May 2025 10:26:24 +0530 Subject: [PATCH] fix: skip PCR extension if TPM1.2 is found When extending PCR or trying to seed entropy pool from TPM if the found device is a TPM1.2 device, skip it, since Talos only supports TPM2.0 Fixes: #10847 Signed-off-by: Noel Georgi --- .github/workflows/ci.yaml | 10 ++++++- .../workflows/integration-misc-2-cron.yaml | 10 ++++++- .kres.yaml | 8 ++++++ .../cmd/mgmt/cluster/create/command.go | 7 ++++- .../cmd/mgmt/cluster/create/create.go | 1 + hack/test/e2e-qemu.sh | 8 ++++++ internal/pkg/rng/tpm.go | 12 +++++--- internal/pkg/secureboot/tpm2/pcr.go | 19 +++++++++++-- pkg/provision/options.go | 15 ++++++++-- pkg/provision/providers/qemu/destroy.go | 2 +- pkg/provision/providers/qemu/launch.go | 28 ++++++++++++------- pkg/provision/providers/qemu/node.go | 8 +++--- .../providers/qemu/preflight_linux.go | 2 +- pkg/provision/providers/qemu/tpm2.go | 26 +++++++++-------- pkg/provision/result.go | 4 +-- website/content/v1.11/reference/cli.md | 3 +- 16 files changed, 120 insertions(+), 43 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2fc403560..5ad17fdb1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,6 +1,6 @@ # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. # -# Generated on 2025-05-04T13:37:33Z by kres 6cbcbd1. +# Generated on 2025-05-05T04:55:03Z by kres 6cbcbd1. name: default concurrency: @@ -2392,6 +2392,14 @@ jobs: WITH_CONFIG_PATCH: '@hack/test/patches/node-address-v2.yaml' run: | sudo -E make e2e-qemu + - name: e2e-tpm1_2 + env: + GITHUB_STEP_NAME: ${{ github.job}}-e2e-tpm1_2 + IMAGE_REGISTRY: registry.dev.siderolabs.io + SHORT_INTEGRATION_TEST: "yes" + WITH_TPM1_2: "true" + run: | + sudo -E make e2e-qemu - name: save artifacts if: always() uses: actions/upload-artifact@v4 diff --git a/.github/workflows/integration-misc-2-cron.yaml b/.github/workflows/integration-misc-2-cron.yaml index 4aa9eb38e..bd10ba0a9 100644 --- a/.github/workflows/integration-misc-2-cron.yaml +++ b/.github/workflows/integration-misc-2-cron.yaml @@ -1,6 +1,6 @@ # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. # -# Generated on 2025-04-04T12:28:10Z by kres d903dae. +# Generated on 2025-05-05T04:55:03Z by kres 6cbcbd1. name: integration-misc-2-cron concurrency: @@ -149,6 +149,14 @@ jobs: WITH_CONFIG_PATCH: '@hack/test/patches/node-address-v2.yaml' run: | sudo -E make e2e-qemu + - name: e2e-tpm1_2 + env: + GITHUB_STEP_NAME: ${{ github.job}}-e2e-tpm1_2 + IMAGE_REGISTRY: registry.dev.siderolabs.io + SHORT_INTEGRATION_TEST: "yes" + WITH_TPM1_2: "true" + run: | + sudo -E make e2e-qemu - name: save artifacts if: always() uses: actions/upload-artifact@v4 diff --git a/.kres.yaml b/.kres.yaml index f69d787e3..59f8289ae 100644 --- a/.kres.yaml +++ b/.kres.yaml @@ -1246,6 +1246,14 @@ spec: SHORT_INTEGRATION_TEST: yes WITH_CONFIG_PATCH: "@hack/test/patches/node-address-v2.yaml" IMAGE_REGISTRY: registry.dev.siderolabs.io + - name: e2e-tpm1_2 + command: e2e-qemu + withSudo: true + environment: + GITHUB_STEP_NAME: ${{ github.job}}-e2e-tpm1_2 + SHORT_INTEGRATION_TEST: yes + WITH_TPM1_2: true + IMAGE_REGISTRY: registry.dev.siderolabs.io - name: save-talos-logs conditions: - always diff --git a/cmd/talosctl/cmd/mgmt/cluster/create/command.go b/cmd/talosctl/cmd/mgmt/cluster/create/command.go index 88b2e8cce..d190c6c18 100644 --- a/cmd/talosctl/cmd/mgmt/cluster/create/command.go +++ b/cmd/talosctl/cmd/mgmt/cluster/create/command.go @@ -37,6 +37,7 @@ const ( bootloaderEnabledFlag = "with-bootloader" controlPlanePortFlag = "control-plane-port" firewallFlag = "with-firewall" + tpmEnabledFlag = "with-tpm1_2" tpm2EnabledFlag = "with-tpm2" withDebugShellFlag = "with-debug-shell" withIOMMUFlag = "with-iommu" @@ -117,6 +118,7 @@ type qemuOps struct { nodeIPXEBootScript string bootloaderEnabled bool uefiEnabled bool + tpm1_2Enabled bool tpm2Enabled bool extraUEFISearchPaths []string networkNoMasqueradeCIDRs []string @@ -249,7 +251,8 @@ func init() { createCmd.Flags().StringVar(&ops.qemu.nodeIPXEBootScript, "ipxe-boot-script", "", "iPXE boot script (URL) to use") createCmd.Flags().BoolVar(&ops.qemu.bootloaderEnabled, bootloaderEnabledFlag, true, "enable bootloader to load kernel and initramfs from disk image after install") createCmd.Flags().BoolVar(&ops.qemu.uefiEnabled, "with-uefi", true, "enable UEFI on x86_64 architecture") - createCmd.Flags().BoolVar(&ops.qemu.tpm2Enabled, tpm2EnabledFlag, false, "enable TPM2 emulation support using swtpm") + createCmd.Flags().BoolVar(&ops.qemu.tpm1_2Enabled, tpmEnabledFlag, false, "enable TPM 1.2 emulation support using swtpm") + createCmd.Flags().BoolVar(&ops.qemu.tpm2Enabled, tpm2EnabledFlag, false, "enable TPM 2.0 emulation support using swtpm") createCmd.Flags().BoolVar(&ops.qemu.debugShellEnabled, withDebugShellFlag, false, "drop talos into a maintenance shell on boot, this is for advanced debugging for developers only") createCmd.Flags().BoolVar(&ops.qemu.withIOMMU, withIOMMUFlag, false, "enable IOMMU support, this also add a new PCI root port and an interface attached to it (qemu only)") createCmd.Flags().MarkHidden("with-debug-shell") //nolint:errcheck @@ -317,5 +320,7 @@ func init() { createCmd.MarkFlagsMutuallyExclusive(inputDirFlag, kubePrismFlag) createCmd.MarkFlagsMutuallyExclusive(inputDirFlag, diskEncryptionKeyTypesFlag) + createCmd.MarkFlagsMutuallyExclusive(tpmEnabledFlag, tpm2EnabledFlag) + clustercmd.Cmd.AddCommand(createCmd) } diff --git a/cmd/talosctl/cmd/mgmt/cluster/create/create.go b/cmd/talosctl/cmd/mgmt/cluster/create/create.go index 6935750c8..638aae4b1 100644 --- a/cmd/talosctl/cmd/mgmt/cluster/create/create.go +++ b/cmd/talosctl/cmd/mgmt/cluster/create/create.go @@ -417,6 +417,7 @@ func create(ctx context.Context, ops createOps) error { provision.WithDockerPortsHostIP(dockerOps.dockerHostIP), provision.WithBootlader(qOps.bootloaderEnabled), provision.WithUEFI(qOps.uefiEnabled), + provision.WithTPM1_2(qOps.tpm1_2Enabled), provision.WithTPM2(qOps.tpm2Enabled), provision.WithDebugShell(qOps.debugShellEnabled), provision.WithIOMMU(qOps.withIOMMU), diff --git a/hack/test/e2e-qemu.sh b/hack/test/e2e-qemu.sh index 8f8b2daba..c5339d14a 100755 --- a/hack/test/e2e-qemu.sh +++ b/hack/test/e2e-qemu.sh @@ -187,6 +187,14 @@ case "${WITH_TRUSTED_BOOT_ISO:-false}" in ;; esac +case "${WITH_TPM1_2:-false}" in + false) + ;; + *) + QEMU_FLAGS+=("--with-tpm1_2") + ;; +esac + case "${WITH_SIDEROLINK_AGENT:-false}" in false) ;; diff --git a/internal/pkg/rng/tpm.go b/internal/pkg/rng/tpm.go index 5e08d2255..5f7c34d8b 100644 --- a/internal/pkg/rng/tpm.go +++ b/internal/pkg/rng/tpm.go @@ -8,7 +8,6 @@ import ( "fmt" "log" "os" - "strings" "time" "github.com/google/go-tpm/tpm2" @@ -22,8 +21,8 @@ import ( func TPMSeed() error { t, err := tpm.Open() if err != nil { - // if the TPM is not available or not a TPM 2.0, we can skip the PCR extension - if os.IsNotExist(err) || strings.Contains(err.Error(), "device is not a TPM 2.0") { + // if the TPM is not available we can skip seeding random pool + if os.IsNotExist(err) { log.Printf("TPM device is not available") return nil @@ -34,13 +33,18 @@ func TPMSeed() error { defer t.Close() //nolint:errcheck + // now we need to check if the TPM is a 2.0 device + // we can do this by checking the manufacturer, + // if it fails, we can skip trying to seed the pool caps, err := tpm2.GetCapability{ Capability: tpm2.TPMCapTPMProperties, Property: uint32(tpm2.TPMPTManufacturer), PropertyCount: 1, }.Execute(t) if err != nil { - return fmt.Errorf("error getting TPM capabilities: %w", err) + log.Printf("TPM device is not a TPM 2.0, skipping seeding entropy pool from TPM") + + return nil } props, err := caps.CapabilityData.Data.TPMProperties() diff --git a/internal/pkg/secureboot/tpm2/pcr.go b/internal/pkg/secureboot/tpm2/pcr.go index e63e81679..bf236544f 100644 --- a/internal/pkg/secureboot/tpm2/pcr.go +++ b/internal/pkg/secureboot/tpm2/pcr.go @@ -11,7 +11,6 @@ import ( "fmt" "log" "os" - "strings" "github.com/google/go-tpm/tpm2" "github.com/google/go-tpm/tpm2/transport" @@ -67,8 +66,8 @@ func ReadPCR(t transport.TPM, pcr int) ([]byte, error) { func PCRExtend(pcr int, data []byte) error { t, err := tpm.Open() if err != nil { - // if the TPM is not available or not a TPM 2.0, we can skip the PCR extension - if os.IsNotExist(err) || strings.Contains(err.Error(), "device is not a TPM 2.0") { + // if the TPM is not available we can skip the PCR extension + if os.IsNotExist(err) { log.Printf("TPM device is not available, skipping PCR extension") return nil @@ -79,6 +78,20 @@ func PCRExtend(pcr int, data []byte) error { defer t.Close() //nolint:errcheck + // now we need to check if the TPM is a 2.0 device + // we can do this by checking the manufacturer, + // if it fails, we can skip the PCR extension + _, err = tpm2.GetCapability{ + Capability: tpm2.TPMCapTPMProperties, + Property: uint32(tpm2.TPMPTManufacturer), + PropertyCount: 1, + }.Execute(t) + if err != nil { + log.Printf("TPM device is not a TPM 2.0, skipping PCR extension") + + return nil + } + // since we are using SHA256, we can assume that the PCR bank is SHA256 digest := sha256.Sum256(data) diff --git a/pkg/provision/options.go b/pkg/provision/options.go index c3d0b6771..855776417 100644 --- a/pkg/provision/options.go +++ b/pkg/provision/options.go @@ -70,7 +70,16 @@ func WithUEFI(enabled bool) Option { } } -// WithTPM2 enables or disables TPM2 emulation. +// WithTPM1_2 enables or disables TPM1.2 emulation. +func WithTPM1_2(enabled bool) Option { + return func(o *Options) error { + o.TPM1_2Enabled = enabled + + return nil + } +} + +// WithTPM2 enables or disables TPM2.0 emulation. func WithTPM2(enabled bool) Option { return func(o *Options) error { o.TPM2Enabled = enabled @@ -200,7 +209,9 @@ type Options struct { // Enable UEFI (for amd64), arm64 can only boot UEFI UEFIEnabled bool - // Enable TPM2 emulation using swtpm. + // Enable TPM 1.2 emulation using swtpm. + TPM1_2Enabled bool + // Enable TPM 2.0 emulation using swtpm. TPM2Enabled bool // Enable debug shell in the bootloader. WithDebugShell bool diff --git a/pkg/provision/providers/qemu/destroy.go b/pkg/provision/providers/qemu/destroy.go index 50b3348eb..836e9669a 100644 --- a/pkg/provision/providers/qemu/destroy.go +++ b/pkg/provision/providers/qemu/destroy.go @@ -56,7 +56,7 @@ func (p *provisioner) Destroy(ctx context.Context, cluster provision.Cluster, op return err } - if err := p.destroyVirtualTPM2s(cluster.Info()); err != nil { + if err := p.destroyVirtualTPMs(cluster.Info()); err != nil { return err } diff --git a/pkg/provision/providers/qemu/launch.go b/pkg/provision/providers/qemu/launch.go index 350079839..0911200b3 100644 --- a/pkg/provision/providers/qemu/launch.go +++ b/pkg/provision/providers/qemu/launch.go @@ -45,7 +45,7 @@ type LaunchConfig struct { MonitorPath string DefaultBootOrder string BootloaderEnabled bool - TPM2Config tpm2Config + TPMConfig tpmConfig NodeUUID uuid.UUID BadRTC bool ArchitectureData Arch @@ -91,9 +91,11 @@ type LaunchConfig struct { controller *Controller } -type tpm2Config struct { +type tpmConfig struct { NodeName string StateDir string + + TPM2 bool } // launchVM runs qemu with args built based on config. @@ -235,21 +237,27 @@ func launchVM(config *LaunchConfig) error { return err } - if config.TPM2Config.NodeName != "" { - tpm2SocketPath := filepath.Join(config.TPM2Config.StateDir, "swtpm.sock") + if config.TPMConfig.NodeName != "" { + tpm2SocketPath := filepath.Join(config.TPMConfig.StateDir, "swtpm.sock") - cmd := exec.Command("swtpm", []string{ + swtpmArgs := []string{ "socket", "--tpmstate", - fmt.Sprintf("dir=%s,mode=0644", config.TPM2Config.StateDir), + fmt.Sprintf("dir=%s,mode=0644", config.TPMConfig.StateDir), "--ctrl", fmt.Sprintf("type=unixio,path=%s", tpm2SocketPath), - "--tpm2", + // "--tpm2", "--pid", - fmt.Sprintf("file=%s", filepath.Join(config.TPM2Config.StateDir, "swtpm.pid")), + fmt.Sprintf("file=%s", filepath.Join(config.TPMConfig.StateDir, "swtpm.pid")), "--log", - fmt.Sprintf("file=%s,level=20", filepath.Join(config.TPM2Config.StateDir, "swtpm.log")), - }...) + fmt.Sprintf("file=%s,level=20", filepath.Join(config.TPMConfig.StateDir, "swtpm.log")), + } + + if config.TPMConfig.TPM2 { + swtpmArgs = append(swtpmArgs, "--tpm2") + } + + cmd := exec.Command("swtpm", swtpmArgs...) log.Printf("starting swtpm: %s", cmd.String()) diff --git a/pkg/provision/providers/qemu/node.go b/pkg/provision/providers/qemu/node.go index dc7570086..2a7682649 100644 --- a/pkg/provision/providers/qemu/node.go +++ b/pkg/provision/providers/qemu/node.go @@ -210,14 +210,14 @@ func (p *provisioner) createNode(state *vm.State, clusterReq provision.ClusterRe APIPort: apiPort, } - if opts.TPM2Enabled { - tpm2, tpm2Err := p.createVirtualTPM2State(state, nodeReq.Name) + if opts.TPM1_2Enabled || opts.TPM2Enabled { + tpmConfig, tpm2Err := p.createVirtualTPMState(state, nodeReq.Name, opts.TPM2Enabled) if tpm2Err != nil { return provision.NodeInfo{}, tpm2Err } - launchConfig.TPM2Config = tpm2 - nodeInfo.TPM2StateDir = tpm2.StateDir + launchConfig.TPMConfig = tpmConfig + nodeInfo.TPMStateDir = tpmConfig.StateDir } if !clusterReq.Network.DHCPSkipHostname { diff --git a/pkg/provision/providers/qemu/preflight_linux.go b/pkg/provision/providers/qemu/preflight_linux.go index e8a307a34..6a137e981 100644 --- a/pkg/provision/providers/qemu/preflight_linux.go +++ b/pkg/provision/providers/qemu/preflight_linux.go @@ -125,7 +125,7 @@ func (check *preflightCheckContext) checkIptables(ctx context.Context) error { } func (check *preflightCheckContext) swtpmExecutable(ctx context.Context) error { - if check.options.TPM2Enabled { + if check.options.TPM1_2Enabled || check.options.TPM2Enabled { if _, err := exec.LookPath("swtpm"); err != nil { return fmt.Errorf("swtpm not found in PATH, please install swtpm-tools with the package manager: %w", err) } diff --git a/pkg/provision/providers/qemu/tpm2.go b/pkg/provision/providers/qemu/tpm2.go index 6a61fdc6e..33ce9f89c 100644 --- a/pkg/provision/providers/qemu/tpm2.go +++ b/pkg/provision/providers/qemu/tpm2.go @@ -15,40 +15,42 @@ import ( "github.com/siderolabs/talos/pkg/provision/providers/vm" ) -func (p *provisioner) createVirtualTPM2State(state *vm.State, nodeName string) (tpm2Config, error) { - tpm2StateDir := state.GetRelativePath(fmt.Sprintf("%s-tpm2", nodeName)) +func (p *provisioner) createVirtualTPMState(state *vm.State, nodeName string, tpm2Enabled bool) (tpmConfig, error) { + tpmStateDir := state.GetRelativePath(fmt.Sprintf("%s-tpm", nodeName)) - if err := os.MkdirAll(tpm2StateDir, 0o755); err != nil { - return tpm2Config{}, err + if err := os.MkdirAll(tpmStateDir, 0o755); err != nil { + return tpmConfig{}, err } - return tpm2Config{ + return tpmConfig{ NodeName: nodeName, - StateDir: tpm2StateDir, + StateDir: tpmStateDir, + + TPM2: tpm2Enabled, }, nil } -func (p *provisioner) destroyVirtualTPM2s(cluster provision.ClusterInfo) error { +func (p *provisioner) destroyVirtualTPMs(cluster provision.ClusterInfo) error { errCh := make(chan error) nodes := append([]provision.NodeInfo{}, cluster.Nodes...) for _, node := range nodes { - if node.TPM2StateDir == "" { + if node.TPMStateDir == "" { continue } - tpm2PidPath := filepath.Join(node.TPM2StateDir, "swtpm.pid") + tpm2PidPath := filepath.Join(node.TPMStateDir, "swtpm.pid") go func() { - errCh <- p.destroyVirtualTPM2(tpm2PidPath) + errCh <- p.destroyVirtualTPM(tpm2PidPath) }() } var multiErr *multierror.Error for _, node := range nodes { - if node.TPM2StateDir == "" { + if node.TPMStateDir == "" { continue } @@ -58,6 +60,6 @@ func (p *provisioner) destroyVirtualTPM2s(cluster provision.ClusterInfo) error { return multiErr.ErrorOrNil() } -func (p *provisioner) destroyVirtualTPM2(pid string) error { +func (p *provisioner) destroyVirtualTPM(pid string) error { return vm.StopProcessByPidfile(pid) } diff --git a/pkg/provision/result.go b/pkg/provision/result.go index f46e33044..42b9164f9 100644 --- a/pkg/provision/result.go +++ b/pkg/provision/result.go @@ -61,6 +61,6 @@ type NodeInfo struct { IPs []netip.Addr - APIPort int - TPM2StateDir string + APIPort int + TPMStateDir string } diff --git a/website/content/v1.11/reference/cli.md b/website/content/v1.11/reference/cli.md index 7bf43cd8b..f03cea43a 100644 --- a/website/content/v1.11/reference/cli.md +++ b/website/content/v1.11/reference/cli.md @@ -225,7 +225,8 @@ talosctl cluster create [flags] --with-network-packet-loss float specify percent of packet loss on the bridge interface when creating a qemu cluster. e.g. 50% = 0.50 (default: 0.0) --with-network-packet-reorder float specify percent of reordered packets on the bridge interface when creating a qemu cluster. e.g. 50% = 0.50 (default: 0.0) --with-siderolink true enables the use of siderolink agent as configuration apply mechanism. true or `wireguard` enables the agent, `tunnel` enables the agent with grpc tunneling (default none) - --with-tpm2 enable TPM2 emulation support using swtpm + --with-tpm1_2 enable TPM 1.2 emulation support using swtpm + --with-tpm2 enable TPM 2.0 emulation support using swtpm --with-uefi enable UEFI on x86_64 architecture (default true) --with-uuid-hostnames use machine UUIDs as default hostnames (QEMU only) --workers int the number of workers to create (default 1)