diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index a4321cee..5ea679f5 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -6,9 +6,12 @@ on: branches: [ "master" ] jobs: tests: - runs-on: ubuntu-24.04 + runs-on: ${{ matrix.runner }} strategy: matrix: + runner: + - ubuntu-24.04 + - ubuntu-24.04-arm goversion: - 1.18 # The unit tests currently fail against the new stable go diff --git a/efi/preinstall/check_host_security.go b/efi/preinstall/check_host_security.go index 35567ac3..7e96fd46 100644 --- a/efi/preinstall/check_host_security.go +++ b/efi/preinstall/check_host_security.go @@ -166,6 +166,8 @@ func checkHostSecurity(env internal_efi.HostEnvironment, log *tcglog.Log) (platf switch runtimeGOARCH { case "amd64": return checkHostSecurityAMD64(env, log) + case "arm64": + return checkHostSecurityARM64(env, log) default: return platformFirmwareIntegrityNone, &UnsupportedPlatformError{fmt.Errorf("checking host security is not implemented on %s", runtimeGOARCH)} } @@ -178,6 +180,8 @@ func checkDiscreteTPMPartialResetAttackMitigationStatus(env internal_efi.HostEnv switch runtimeGOARCH { case "amd64": return checkDiscreteTPMPartialResetAttackMitigationStatusAMD64(env, logResults) + case "arm64": + return checkDiscreteTPMPartialResetAttackMitigationStatusARM64(env, logResults) default: return dtpmPartialResetAttackMitigationNotRequired, nil } @@ -295,3 +299,72 @@ func checkDiscreteTPMPartialResetAttackMitigationStatusAMD64(env internal_efi.Ho // PCR0 can be recreated from the OS. return dtpmPartialResetAttackMitigationUnavailable, nil } + +// checkHostSecurityARM64Platform selects the platform-specific firmware +// integrity check. Tests replace this to supply synthetic platforms. +var checkHostSecurityARM64Platform = func(env internal_efi.HostEnvironmentARM64, cpuManufacturer string) (platformFirmwareIntegrityConfig, error) { + return platformFirmwareIntegrityNone, &UnsupportedPlatformError{fmt.Errorf("unsupported CPU manufacturer: %s", cpuManufacturer)} +} + +func checkHostSecurityARM64(env internal_efi.HostEnvironment, log *tcglog.Log) (platformFirmwareIntegrityConfig, error) { + arm64Env, err := env.ARM64() + if err != nil { + return platformFirmwareIntegrityNone, &UnsupportedPlatformError{fmt.Errorf("cannot obtain ARM64 environment: %w", err)} + } + + cpuManufacturer, err := arm64Env.CPUManufacturer() + if err != nil { + return platformFirmwareIntegrityNone, &UnsupportedPlatformError{fmt.Errorf("cannot determine CPU manufacturer: %w", err)} + } + + integrity, err := checkHostSecurityARM64Platform(arm64Env, cpuManufacturer) + if err != nil { + return platformFirmwareIntegrityNone, err + } + + return checkHostSecurityARM64Generic(env, log, integrity) +} + +func checkHostSecurityARM64Generic(env internal_efi.HostEnvironment, log *tcglog.Log, integrity platformFirmwareIntegrityConfig) (platformFirmwareIntegrityConfig, error) { + var errs []error + + if err := checkSecureBootPolicyPCRForDegradedFirmwareSettings(log); err != nil { + var ce CompoundError + if !errors.As(err, &ce) { + return platformFirmwareIntegrityNone, fmt.Errorf("encountered an error whilst checking the TCG log for degraded firmware settings: %w", err) + } + errs = append(errs, ce.Unwrap()...) + } + + if err := checkForKernelIOMMU(env); err != nil { + switch { + case errors.Is(err, ErrNoKernelIOMMU): + errs = append(errs, err) + default: + return platformFirmwareIntegrityNone, fmt.Errorf("encountered an error whilst checking sysfs to determine that kernel IOMMU support is enabled: %w", err) + } + } + + if len(errs) > 0 { + return integrity, joinErrors(errs...) + } + + return integrity, nil +} + +// checkDiscreteTPMPartialResetAttackMitigationStatusARM64 determines whether a partial mitigation +// against discrete TPM reset attacks should be enabled. +func checkDiscreteTPMPartialResetAttackMitigationStatusARM64(env internal_efi.HostEnvironment, _ *pcrBankResults) (discreteTPMPartialResetAttackMitigationStatus, error) { + discreteTPM, err := isTPMDiscrete(env) + if err != nil { + return dtpmPartialResetAttackMitigationUnknown, &TPM2DeviceError{err} + } + if !discreteTPM { + return dtpmPartialResetAttackMitigationNotRequired, nil + } + + // ARM64 has no generic mechanism to establish that the TPM startup locality + // is protected by the hardware root of trust, so PCR0 binding cannot be relied + // on to mitigate an independent reset of a discrete TPM. + return dtpmPartialResetAttackMitigationUnavailable, nil +} diff --git a/efi/preinstall/check_host_security_test.go b/efi/preinstall/check_host_security_test.go index 1ca94879..670cf328 100644 --- a/efi/preinstall/check_host_security_test.go +++ b/efi/preinstall/check_host_security_test.go @@ -535,6 +535,154 @@ func (s *hostSecurityAMD64Suite) TestCheckDiscreteTPMPartialResetAttackMitigatio c.Check(errors.As(err, &upe), testutil.IsTrue) } +type hostSecurityARM64Suite struct { + snapd_testutil.BaseTest +} + +func (s *hostSecurityARM64Suite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + s.AddCleanup(MockRuntimeGOARCH("arm64")) +} + +var _ = Suite(&hostSecurityARM64Suite{}) + +type hostSecurityARM64ErrorEnv struct { + cpuManufacturer string + cpuVersion string +} + +func (e *hostSecurityARM64ErrorEnv) CPUManufacturer() (string, error) { + return e.cpuManufacturer, nil +} + +func (e *hostSecurityARM64ErrorEnv) CPUVersion() (string, error) { + return e.cpuVersion, nil +} + +func makeArm64IOMMUDevices() []internal_efi.SysfsDevice { + return []internal_efi.SysfsDevice{ + efitest.NewMockSysfsDevice("/sys/devices/platform/soc@0/8000000.iommu", nil, "iommu", nil, nil), + } +} + +func makeArm64PCRResults(c *C) *PCRBankResults { + return NewPCRBankResults(tpm2.HashAlgorithmSHA256, 0, [8]PcrResults{ + MakePCRResults( + false, + make(tpm2.Digest, 32), + testutil.DecodeHexString(c, "a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5"), + testutil.DecodeHexString(c, "a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5"), + nil, + ), + }) +} + +func (s *hostSecurityARM64Suite) TestCheckHostSecurityErrNotARM64(c *C) { + env := efitest.NewMockHostEnvironmentWithOpts() + + _, err := CheckHostSecurity(env, nil) + c.Check(err, ErrorMatches, `unsupported platform: cannot obtain ARM64 environment: not a ARM64 host`) + var upe *UnsupportedPlatformError + c.Check(errors.As(err, &upe), testutil.IsTrue) +} + +func (s *hostSecurityARM64Suite) TestCheckHostSecurityErrUnknownCPUManufacturer(c *C) { + env := efitest.NewMockHostEnvironmentWithOpts(efitest.WithARM64Environment("ACME", exampleARM64CPUVersion)) + + _, err := CheckHostSecurity(env, nil) + c.Check(err, ErrorMatches, `unsupported platform: unsupported CPU manufacturer: ACME`) + var upe *UnsupportedPlatformError + c.Check(errors.As(err, &upe), testutil.IsTrue) +} + +func (s *hostSecurityARM64Suite) TestCheckHostSecurityUEFIDebuggerFinding(c *C) { + env := efitest.NewMockHostEnvironmentWithOpts( + efitest.WithARM64Environment(exampleARM64CPUManufacturer, exampleARM64CPUVersion), + efitest.WithSysfsDevices(makeArm64IOMMUDevices()...), + ) + log := efitest.NewLog(c, &efitest.LogOptions{FirmwareDebugger: true}) + + integrity, err := CheckHostSecurity(env, log) + c.Check(integrity, Equals, PlatformFirmwareIntegrityMeasured) + c.Check(err, ErrorMatches, `the platform firmware contains a debugging endpoint enabled`) + var tmpl CompoundError + c.Assert(err, Implements, &tmpl) + c.Check(err.(CompoundError).Unwrap(), DeepEquals, []error{ErrUEFIDebuggingEnabled}) +} + +func (s *hostSecurityARM64Suite) TestCheckHostSecurityInsufficientDMAProtection(c *C) { + env := efitest.NewMockHostEnvironmentWithOpts( + efitest.WithARM64Environment(exampleARM64CPUManufacturer, exampleARM64CPUVersion), + efitest.WithSysfsDevices(makeArm64IOMMUDevices()...), + ) + log := efitest.NewLog(c, &efitest.LogOptions{DMAProtection: efitest.DMAProtectionDisabled}) + + integrity, err := CheckHostSecurity(env, log) + c.Check(integrity, Equals, PlatformFirmwareIntegrityMeasured) + c.Check(err, ErrorMatches, `the platform firmware indicates that DMA protections are insufficient`) + var tmpl CompoundError + c.Assert(err, Implements, &tmpl) + c.Check(err.(CompoundError).Unwrap(), DeepEquals, []error{ErrInsufficientDMAProtection}) +} + +func (s *hostSecurityARM64Suite) TestCheckHostSecurityNoIOMMU(c *C) { + env := efitest.NewMockHostEnvironmentWithOpts( + efitest.WithARM64Environment(exampleARM64CPUManufacturer, exampleARM64CPUVersion), + efitest.WithSysfsDevices(), + ) + log := efitest.NewLog(c, &efitest.LogOptions{}) + + integrity, err := CheckHostSecurity(env, log) + c.Check(integrity, Equals, PlatformFirmwareIntegrityMeasured) + c.Check(err, ErrorMatches, `no kernel IOMMU support was detected`) + var tmpl CompoundError + c.Assert(err, Implements, &tmpl) + c.Check(err.(CompoundError).Unwrap(), DeepEquals, []error{ErrNoKernelIOMMU}) +} + +func (s *hostSecurityARM64Suite) TestCheckHostSecurityMultipleRecoverableErrors(c *C) { + env := efitest.NewMockHostEnvironmentWithOpts( + efitest.WithARM64Environment(exampleARM64CPUManufacturer, exampleARM64CPUVersion), + efitest.WithSysfsDevices(), + ) + log := efitest.NewLog(c, &efitest.LogOptions{FirmwareDebugger: true}) + + integrity, err := CheckHostSecurity(env, log) + c.Check(integrity, Equals, PlatformFirmwareIntegrityMeasured) + c.Check(err, ErrorMatches, `2 errors detected: +- the platform firmware contains a debugging endpoint enabled +- no kernel IOMMU support was detected +`) + var tmpl CompoundError + c.Assert(err, Implements, &tmpl) + c.Check(err.(CompoundError).Unwrap(), DeepEquals, []error{ErrUEFIDebuggingEnabled, ErrNoKernelIOMMU}) +} + +func (s *hostSecurityARM64Suite) TestCheckDiscreteTPMPartialResetAttackMitigationStatusUnknownForUnsupportedCPUManufacturer(c *C) { + env := efitest.NewMockHostEnvironmentWithOpts( + efitest.WithARM64Environment(exampleARM64CPUManufacturer, exampleARM64CPUVersion), + efitest.WithSysfsDevices(makeArm64TPMDevice("tpm_crb")), + ) + + status, err := CheckDiscreteTPMPartialResetAttackMitigationStatus(env, makeArm64PCRResults(c)) + c.Check(status, Equals, DtpmPartialResetAttackMitigationUnknown) + c.Check(err, ErrorMatches, `error with TPM2 device: unsupported platform: unsupported CPU manufacturer: `+exampleARM64CPUManufacturer) + var tpmErr *TPM2DeviceError + c.Check(errors.As(err, &tpmErr), testutil.IsTrue) + c.Check(status, Equals, DtpmPartialResetAttackMitigationUnknown) +} + +func (s *hostSecurityARM64Suite) TestCheckDiscreteTPMPartialResetAttackMitigationStatusNotRequiredForOPTEE(c *C) { + env := efitest.NewMockHostEnvironmentWithOpts( + efitest.WithARM64Environment(exampleARM64CPUManufacturer, exampleARM64CPUVersion), + efitest.WithSysfsDevices(makeArm64TPMDevice("optee-ftpm")), + ) + + status, err := CheckDiscreteTPMPartialResetAttackMitigationStatus(env, makeArm64PCRResults(c)) + c.Check(err, IsNil) + c.Check(status, Equals, DtpmPartialResetAttackMitigationNotRequired) +} + func (s *hostSecuritySuite) TestCheckHostSecurityUnsupportedArchitecture(c *C) { restore := MockRuntimeGOARCH("ppc64le") defer restore() diff --git a/efi/preinstall/check_tpm.go b/efi/preinstall/check_tpm.go index 4a39e7b1..72593954 100644 --- a/efi/preinstall/check_tpm.go +++ b/efi/preinstall/check_tpm.go @@ -25,6 +25,7 @@ import ( "fmt" "github.com/canonical/go-tpm2" + "github.com/pilebones/go-udev/netlink" internal_efi "github.com/snapcore/secboot/internal/efi" ) @@ -478,6 +479,8 @@ func isTPMDiscrete(env internal_efi.HostEnvironment) (bool, error) { switch runtimeGOARCH { case "amd64": return isTPMDiscreteAMD64(env) + case "arm64": + return isTPMDiscreteARM64(env) default: return false, &UnsupportedPlatformError{fmt.Errorf("checking for TPM discreteness is not implemented on %s", runtimeGOARCH)} } @@ -507,3 +510,60 @@ func isTPMDiscreteAMD64(env internal_efi.HostEnvironment) (bool, error) { panic("not reached") } } + +// isTPMDiscreteARM64 determines whether the default TPM is discrete. OP-TEE firmware +// TPMs are identified by their backing kernel driver. Other implementations use +// platform-specific knowledge. +func isTPMDiscreteARM64(env internal_efi.HostEnvironment) (bool, error) { + isOpteefTPM, err := isTPMFirmwareOptee(env) + if err != nil { + return false, err + } + if isOpteefTPM { + return false, nil + } + + arm64Env, err := env.ARM64() + if err != nil { + return false, err + } + + cpuManufacturer, err := arm64Env.CPUManufacturer() + if err != nil { + return false, &UnsupportedPlatformError{fmt.Errorf("cannot determine CPU manufacturer: %w", err)} + } + + return false, &UnsupportedPlatformError{fmt.Errorf("unsupported CPU manufacturer: %s", cpuManufacturer)} +} + +// isTPMFirmwareOptee determines whether the default TPM is an OP-TEE firmware TPM, +// as identified by its backing kernel driver +func isTPMFirmwareOptee(env internal_efi.HostEnvironment) (bool, error) { + devices, err := env.EnumerateDevices(&netlink.RuleDefinition{ + Env: map[string]string{ + "SUBSYSTEM": "tpm", + "DEVNAME": "tpm0", + }, + }) + if err != nil { + return false, fmt.Errorf("cannot enumerate TPM devices: %w", err) + } + if len(devices) != 1 { + return false, fmt.Errorf("internal error: expected one tpm0 device, found %d", len(devices)) + } + + parent, err := devices[0].Parent() + if err != nil { + return false, fmt.Errorf("cannot obtain parent of tpm0 device: %w", err) + } + if parent == nil { + return false, fmt.Errorf("internal error: tpm0 device has no parent") + } + + switch parent.Properties()["DRIVER"] { + case "optee-ftpm", "ftpm-tee": + return true, nil + default: + return false, nil + } +} diff --git a/efi/preinstall/check_tpm_test.go b/efi/preinstall/check_tpm_test.go index bdeb03b5..b242ac18 100644 --- a/efi/preinstall/check_tpm_test.go +++ b/efi/preinstall/check_tpm_test.go @@ -27,6 +27,7 @@ import ( "github.com/canonical/go-tpm2/objectutil" tpm2_testutil "github.com/canonical/go-tpm2/testutil" . "github.com/snapcore/secboot/efi/preinstall" + internal_efi "github.com/snapcore/secboot/internal/efi" "github.com/snapcore/secboot/internal/efitest" "github.com/snapcore/secboot/internal/testutil" "github.com/snapcore/secboot/internal/tpm2_device" @@ -1207,6 +1208,56 @@ func (s *tpmIntelSuite) TestIsTPMDiscreteUnrecognizedCPUVendor(c *C) { c.Check(errors.As(err, &upe), testutil.IsTrue) } +type tpmARM64Suite struct { + snapd_testutil.BaseTest +} + +func (s *tpmARM64Suite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + s.AddCleanup(MockRuntimeGOARCH("arm64")) +} + +var _ = Suite(&tpmARM64Suite{}) + +func makeArm64TPMDevice(driver string) internal_efi.SysfsDevice { + parent := efitest.NewMockSysfsDevice( + "/sys/devices/platform/firmware-tpm", + map[string]string{"DRIVER": driver}, + "platform", + nil, + nil, + ) + return efitest.NewMockSysfsDevice( + "/sys/devices/platform/firmware-tpm/tpm/tpm0", + map[string]string{"DEVNAME": "tpm0"}, + "tpm", + nil, + parent, + ) +} + +func (s *tpmARM64Suite) TestIsTPMDiscreteOPTEE(c *C) { + for _, driver := range []string{"optee-ftpm", "ftpm-tee"} { + env := efitest.NewMockHostEnvironmentWithOpts(efitest.WithSysfsDevices(makeArm64TPMDevice(driver))) + + discrete, err := IsTPMDiscrete(env) + c.Check(err, IsNil, Commentf("driver %q", driver)) + c.Check(discrete, testutil.IsFalse, Commentf("driver %q", driver)) + } +} + +func (s *tpmARM64Suite) TestIsTPMDiscreteUnsupportedCPUManufacturer(c *C) { + env := efitest.NewMockHostEnvironmentWithOpts( + efitest.WithARM64Environment("ACME", "Unknown"), + efitest.WithSysfsDevices(makeArm64TPMDevice("tpm_crb")), + ) + + _, err := IsTPMDiscrete(env) + c.Check(err, ErrorMatches, `unsupported platform: unsupported CPU manufacturer: ACME`) + var upe *UnsupportedPlatformError + c.Check(errors.As(err, &upe), testutil.IsTrue) +} + func (s *tpmSuite) TestIsTPMDiscreteUnsupportedArchitecture(c *C) { restore := MockRuntimeGOARCH("ppc64le") defer restore() diff --git a/efi/preinstall/checks_fixture_test.go b/efi/preinstall/checks_fixture_test.go index d40de2de..51e9297f 100644 --- a/efi/preinstall/checks_fixture_test.go +++ b/efi/preinstall/checks_fixture_test.go @@ -109,6 +109,33 @@ func (f *runChecksHostFixture) mockRuntimeGOARCH(s interface{ AddCleanup(func()) s.AddCleanup(MockRuntimeGOARCH(f.arch)) } +const ( + exampleARM64CPUManufacturer = "Example Manufacturer" + exampleARM64CPUVersion = "Example OP-TEE SoC" +) + +func init() { + RegisterARM64TestPlatform(exampleARM64CPUManufacturer, exampleARM64CPUVersion) +} + +func runChecksArm64TPMDevice(driver string) internal_efi.SysfsDevice { + parent := efitest.NewMockSysfsDevice( + "/sys/devices/platform/firmware-tpm", + map[string]string{"DRIVER": driver}, + "platform", + nil, + nil, + ) + + return efitest.NewMockSysfsDevice( + "/sys/devices/platform/firmware-tpm/tpm/tpm0", + map[string]string{"DEVNAME": "tpm0"}, + "tpm", + nil, + parent, + ) +} + // runChecksPlatformHostFixtures returns an array of mock platform host configurations // for testing. Each fixture can define an EFI environment with CPU features, a set of sysfs // devices, virtualization information, and host ISA. Each fixture also includes a set of @@ -139,6 +166,14 @@ func runChecksPlatformHostFixtures() []runChecksHostFixture { ))) } + newDevices := func(tpmDriver string, withIOMMU bool) []internal_efi.SysfsDevice { + devices := []internal_efi.SysfsDevice{runChecksArm64TPMDevice(tpmDriver)} + if withIOMMU { + devices = append([]internal_efi.SysfsDevice{efitest.NewMockSysfsDevice("/sys/devices/platform/smmu0", nil, "iommu", nil, nil)}, devices...) + } + return devices + } + return []runChecksHostFixture{ // amd64 fixtures { @@ -209,5 +244,28 @@ func runChecksPlatformHostFixtures() []runChecksHostFixture { virtualizationDetection: internal_efi.DetectVirtModeVM, arch: "amd64", }, + // arm64 fixtures + { + name: "example-arm64-optee-ftpm", + capabilities: runChecksHostCapabilityValid | + runChecksHostCapabilityNotVirtualMachine | + runChecksHostCapabilityFirmwareTPM, + environment: efitest.WithARM64Environment(exampleARM64CPUManufacturer, exampleARM64CPUVersion), + virtualizationMode: internal_efi.VirtModeNone, + virtualizationDetection: internal_efi.DetectVirtModeAll, + sysfsDevices: newDevices("optee-ftpm", true), + additionalExpectedFlags: RequireLockToPlatformFirmware, + arch: "arm64", + }, + { + name: "example-arm64-optee-ftpm-no-kernel-iommu", + capabilities: runChecksHostCapabilityNotVirtualMachine | runChecksHostCapabilityNoKernelIOMMU | runChecksHostCapabilityFirmwareTPM, + environment: efitest.WithARM64Environment(exampleARM64CPUManufacturer, exampleARM64CPUVersion), + virtualizationMode: internal_efi.VirtModeNone, + virtualizationDetection: internal_efi.DetectVirtModeAll, + sysfsDevices: newDevices("optee-ftpm", false), + additionalExpectedFlags: RequireLockToPlatformFirmware, + arch: "arm64", + }, } } diff --git a/efi/preinstall/export_test.go b/efi/preinstall/export_test.go index 7c3273a9..762599b0 100644 --- a/efi/preinstall/export_test.go +++ b/efi/preinstall/export_test.go @@ -22,6 +22,7 @@ package preinstall import ( "crypto" "encoding/json" + "fmt" "io" efi "github.com/canonical/go-efilib" @@ -44,6 +45,7 @@ type ( HfstsRegistersCsme18 = hfstsRegistersCsme18 JoinError = joinError MeVersion = meVersion + PCRBankResults = pcrBankResults PcrResults = pcrResults SecureBootPolicyResult = secureBootPolicyResult SecureBootPolicyResultFlags = secureBootPolicyResultFlags @@ -98,6 +100,7 @@ var ( ClearTPM = clearTPM DetermineCPUVendor = determineCPUVendor DetectVirtualization = detectVirtualization + DtpmPartialResetAttackMitigationUnknown = dtpmPartialResetAttackMitigationUnknown ErrInvalidLockoutAuthValueSupplied = errInvalidLockoutAuthValueSupplied InsertActionProceed = insertActionProceed IsLaunchedFromLoadOption = isLaunchedFromLoadOption @@ -188,3 +191,22 @@ func MockRuntimeGOARCH(arch string) (restore func()) { runtimeGOARCH = arch return func() { runtimeGOARCH = orig } } + +func RegisterARM64TestPlatform(cpuManufacturer, cpuVersion string) { + previous := checkHostSecurityARM64Platform + checkHostSecurityARM64Platform = func(env internal_efi.HostEnvironmentARM64, manufacturer string) (platformFirmwareIntegrityConfig, error) { + if manufacturer != cpuManufacturer { + return previous(env, manufacturer) + } + + version, err := env.CPUVersion() + if err != nil { + return platformFirmwareIntegrityNone, &UnsupportedPlatformError{fmt.Errorf("cannot determine CPU version: %w", err)} + } + if version != cpuVersion { + return previous(env, manufacturer) + } + + return platformFirmwareIntegrityMeasured, nil + } +} diff --git a/internal/efi/default_env.go b/internal/efi/default_env.go index cbbc55fd..892d57f1 100644 --- a/internal/efi/default_env.go +++ b/internal/efi/default_env.go @@ -31,6 +31,7 @@ import ( "path/filepath" "runtime" "strconv" + "strings" "syscall" efi "github.com/canonical/go-efilib" @@ -50,10 +51,15 @@ var ( osOpen = os.Open osReadFile = os.ReadFile osReadlink = os.Readlink + // runtimeGOARCH is the architecture that host security checks are performed + // for. It is a variable so that tests can run the checks for architectures + // other than the one the test binary was built for. runtimeGOARCH = runtime.GOARCH tpm2_deviceDefaultDevice = tpm2_device.DefaultDevice eventLogPath = "/sys/kernel/security/tpm0/binary_bios_measurements" // Path of the TCG event log for the default TPM, in binary form + + dmiProcessorInfoPath = "/sys/firmware/dmi/entries/4-0/raw" ) func SetEventLogPath(path string) { @@ -336,3 +342,92 @@ func (defaultEnvImpl) AMD64() (HostEnvironmentAMD64, error) { } return defaultEnvAMD64Impl{}, nil } + +type defaultEnvARM64Impl struct{} + +// smbiosType4ManufacturerOffset is the byte offset of the Manufacturer string +// index within an SMBIOS type 4 (Processor Information) formatted area. +const smbiosType4ManufacturerOffset = 0x07 + +// smbiosType4VersionOffset is the byte offset of the Version string index +// within an SMBIOS type 4 (Processor Information) formatted area. +const smbiosType4VersionOffset = 0x10 + +// decodeSMBIOSType4Field decodes a string field from an SMBIOS type 4 +// (Processor Information) structure blob. data is the raw binary blob read +// from the kernel's DMI entries sysfs interface. fieldOffset is the byte +// offset within the formatted area that holds the 1-based string index. +// +// Layout (DMTF SMBIOS specification): +// - byte 0: structure type (must be 4) +// - byte 1: length of the formatted area including the header +// - bytes 2-3: handle +// - bytes 4…: remaining formatted area +// - after the formatted area: NUL-terminated strings; string set ends +// with an additional NUL (empty string sentinel) +func decodeSMBIOSType4Field(data []byte, fieldOffset uint8) (string, error) { + if len(data) < 4 { + return "", fmt.Errorf("SMBIOS structure too short for header: have %d bytes", len(data)) + } + structType := data[0] + formattedLen := data[1] + if structType != 4 { + return "", fmt.Errorf("unexpected SMBIOS structure type %d (expected 4)", structType) + } + if int(formattedLen) <= int(fieldOffset) { + return "", fmt.Errorf("SMBIOS structure too short to contain field at offset 0x%02x: formatted area length is %d", fieldOffset, formattedLen) + } + if len(data) < int(formattedLen) { + return "", fmt.Errorf("SMBIOS structure data truncated: have %d bytes, formatted area length is %d", len(data), formattedLen) + } + strIdx := data[fieldOffset] + if strIdx == 0 { + return "", fmt.Errorf("SMBIOS field at offset 0x%02x is unset", fieldOffset) + } + stringsData := data[formattedLen:] + var n uint8 + for i := 0; i < len(stringsData); { + end := i + for end < len(stringsData) && stringsData[end] != 0 { + end++ + } + if i == end { + // Empty string: end-of-string-set sentinel. + break + } + n++ + if n == strIdx { + return strings.TrimSpace(string(stringsData[i:end])), nil + } + i = end + 1 + } + return "", fmt.Errorf("SMBIOS string index %d is out of range", strIdx) +} + +// CPUManufacturer implements [HostEnvironmentARM64.CPUManufacturer]. +func (defaultEnvARM64Impl) CPUManufacturer() (string, error) { + data, err := osReadFile(dmiProcessorInfoPath) + if err != nil { + return "", fmt.Errorf("cannot read %s: %w", dmiProcessorInfoPath, err) + } + return decodeSMBIOSType4Field(data, smbiosType4ManufacturerOffset) +} + +// CPUVersion implements [HostEnvironmentARM64.CPUVersion]. +func (defaultEnvARM64Impl) CPUVersion() (string, error) { + data, err := osReadFile(dmiProcessorInfoPath) + if err != nil { + return "", fmt.Errorf("cannot read %s: %w", dmiProcessorInfoPath, err) + } + return decodeSMBIOSType4Field(data, smbiosType4VersionOffset) +} + +// ARM64 implements [HostEnvironment.ARM64]. +// The architecture is checked at runtime (rather than by build constraint) so +// that this implementation is compiled and unit tested on every architecture. +func (defaultEnvImpl) ARM64() (HostEnvironmentARM64, error) { + if runtimeGOARCH != "arm64" { + return nil, ErrNotARM64Host + } + return defaultEnvARM64Impl{}, nil +} diff --git a/internal/efi/default_env_test.go b/internal/efi/default_env_test.go index e6b2dd67..0f81540f 100644 --- a/internal/efi/default_env_test.go +++ b/internal/efi/default_env_test.go @@ -23,6 +23,7 @@ import ( "context" "encoding/binary" "errors" + "fmt" "io" "os" "path/filepath" @@ -765,3 +766,326 @@ func (s *defaultEnvAMD64Suite) TestNotAMD64Host(c *C) { _, err := DefaultEnv.AMD64() c.Check(err, Equals, ErrNotAMD64Host) } + +type defaultEnvARM64Suite struct { + snapd_testutil.BaseTest +} + +var _ = Suite(&defaultEnvARM64Suite{}) + +func (s *defaultEnvARM64Suite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + s.AddCleanup(MockRuntimeGOARCH("arm64")) +} + +// buildSMBIOSType4 constructs a synthetic SMBIOS type 4 (Processor Information) +// blob for testing. formattedArea contains the raw formatted area bytes +// (including the 4-byte header: type, length, handle×2); formattedArea[0] +// should be set to the desired type byte and formattedArea[1] is overwritten +// with len(formattedArea). strs are the string-set entries appended in order. +func buildSMBIOSType4(formattedArea []byte, strs ...string) []byte { + fa := make([]byte, len(formattedArea)) + copy(fa, formattedArea) + if len(fa) >= 2 { + fa[1] = byte(len(fa)) + } + out := append([]byte(nil), fa...) + for _, s := range strs { + out = append(out, s...) + out = append(out, 0) + } + out = append(out, 0) // end-of-string-set sentinel + return out +} + +// makeValidType4Blob returns a minimal valid SMBIOS type 4 blob whose +// Manufacturer field (0x07) points to string 1 and Version field (0x10) +// points to string 2. +func makeValidType4Blob(manufacturer, version string) []byte { + fa := make([]byte, 0x11) // 17 bytes: includes offsets 0x07 and 0x10 + fa[0] = 4 // structure type 4 + fa[0x07] = 1 // Manufacturer = string 1 + fa[0x10] = 2 // Version = string 2 + return buildSMBIOSType4(fa, manufacturer, version) +} + +func (s *defaultEnvARM64Suite) TestCPUManufacturer(c *C) { + restore := MockOsReadFile(func(path string) ([]byte, error) { + if path == "/sys/firmware/dmi/entries/4-0/raw" { + return makeValidType4Blob("NVIDIA", "GB10"), nil + } + return nil, fmt.Errorf("unexpected path: %s", path) + }) + defer restore() + + arm64, err := DefaultEnv.ARM64() + c.Assert(err, IsNil) + + manufacturer, err := arm64.CPUManufacturer() + c.Check(err, IsNil) + c.Check(manufacturer, Equals, "NVIDIA") +} + +func (s *defaultEnvARM64Suite) TestCPUVersion(c *C) { + restore := MockOsReadFile(func(path string) ([]byte, error) { + if path == "/sys/firmware/dmi/entries/4-0/raw" { + return makeValidType4Blob("NVIDIA", "GB10"), nil + } + return nil, fmt.Errorf("unexpected path: %s", path) + }) + defer restore() + + arm64, err := DefaultEnv.ARM64() + c.Assert(err, IsNil) + + version, err := arm64.CPUVersion() + c.Check(err, IsNil) + c.Check(version, Equals, "GB10") +} + +func (s *defaultEnvARM64Suite) TestCPUManufacturerWhitespaceTrimming(c *C) { + restore := MockOsReadFile(func(path string) ([]byte, error) { + if path == "/sys/firmware/dmi/entries/4-0/raw" { + return makeValidType4Blob("\t Example Manufacturer \t", "Example Version"), nil + } + return nil, fmt.Errorf("unexpected path: %s", path) + }) + defer restore() + + arm64, err := DefaultEnv.ARM64() + c.Assert(err, IsNil) + + manufacturer, err := arm64.CPUManufacturer() + c.Check(err, IsNil) + c.Check(manufacturer, Equals, "Example Manufacturer") +} + +func (s *defaultEnvARM64Suite) TestCPUVersionWhitespaceTrimming(c *C) { + restore := MockOsReadFile(func(path string) ([]byte, error) { + if path == "/sys/firmware/dmi/entries/4-0/raw" { + return makeValidType4Blob("Example Manufacturer", "\t Example Version \t"), nil + } + return nil, fmt.Errorf("unexpected path: %s", path) + }) + defer restore() + + arm64, err := DefaultEnv.ARM64() + c.Assert(err, IsNil) + + version, err := arm64.CPUVersion() + c.Check(err, IsNil) + c.Check(version, Equals, "Example Version") +} + +func (s *defaultEnvARM64Suite) TestCPUManufacturerReadError(c *C) { + restore := MockOsReadFile(func(path string) ([]byte, error) { + return nil, errors.New("some error") + }) + defer restore() + + arm64, err := DefaultEnv.ARM64() + c.Assert(err, IsNil) + + _, err = arm64.CPUManufacturer() + c.Check(err, ErrorMatches, "cannot read /sys/firmware/dmi/entries/4-0/raw: some error") +} + +func (s *defaultEnvARM64Suite) TestCPUVersionReadError(c *C) { + restore := MockOsReadFile(func(path string) ([]byte, error) { + return nil, errors.New("some error") + }) + defer restore() + + arm64, err := DefaultEnv.ARM64() + c.Assert(err, IsNil) + + _, err = arm64.CPUVersion() + c.Check(err, ErrorMatches, "cannot read /sys/firmware/dmi/entries/4-0/raw: some error") +} + +func (s *defaultEnvARM64Suite) TestCPUManufacturerWrongStructureType(c *C) { + restore := MockOsReadFile(func(path string) ([]byte, error) { + if path == "/sys/firmware/dmi/entries/4-0/raw" { + fa := make([]byte, 0x11) + fa[0] = 1 // wrong type (System Information, not Processor Information) + fa[0x07] = 1 // Manufacturer = string 1 + fa[0x10] = 2 // Version = string 2 + return buildSMBIOSType4(fa, "NVIDIA", "GB10"), nil + } + return nil, fmt.Errorf("unexpected path: %s", path) + }) + defer restore() + + arm64, err := DefaultEnv.ARM64() + c.Assert(err, IsNil) + + _, err = arm64.CPUManufacturer() + c.Check(err, ErrorMatches, "unexpected SMBIOS structure type 1 \\(expected 4\\)") +} + +func (s *defaultEnvARM64Suite) TestCPUManufacturerTooShortForHeader(c *C) { + restore := MockOsReadFile(func(path string) ([]byte, error) { + if path == "/sys/firmware/dmi/entries/4-0/raw" { + return []byte{4, 3, 0}, nil // only 3 bytes, not enough for header + } + return nil, fmt.Errorf("unexpected path: %s", path) + }) + defer restore() + + arm64, err := DefaultEnv.ARM64() + c.Assert(err, IsNil) + + _, err = arm64.CPUManufacturer() + c.Check(err, ErrorMatches, "SMBIOS structure too short for header: have 3 bytes") +} + +func (s *defaultEnvARM64Suite) TestCPUManufacturerFormattedAreaTooShort(c *C) { + restore := MockOsReadFile(func(path string) ([]byte, error) { + if path == "/sys/firmware/dmi/entries/4-0/raw" { + // Formatted area is only 7 bytes; Manufacturer is at 0x07, + // so formattedLen (7) <= fieldOffset (7): field not present. + fa := make([]byte, 7) + fa[0] = 4 + fa[0x06] = 1 // not the manufacturer field, just filler + return buildSMBIOSType4(fa, "NVIDIA"), nil + } + return nil, fmt.Errorf("unexpected path: %s", path) + }) + defer restore() + + arm64, err := DefaultEnv.ARM64() + c.Assert(err, IsNil) + + _, err = arm64.CPUManufacturer() + c.Check(err, ErrorMatches, "SMBIOS structure too short to contain field at offset 0x07: formatted area length is 7") +} + +func (s *defaultEnvARM64Suite) TestCPUManufacturerDataTruncated(c *C) { + restore := MockOsReadFile(func(path string) ([]byte, error) { + if path == "/sys/firmware/dmi/entries/4-0/raw" { + // Length field says 0x11 bytes but we only provide 8. + data := make([]byte, 8) + data[0] = 4 + data[1] = 0x11 // claims 17-byte formatted area + data[0x07] = 1 + return data, nil + } + return nil, fmt.Errorf("unexpected path: %s", path) + }) + defer restore() + + arm64, err := DefaultEnv.ARM64() + c.Assert(err, IsNil) + + _, err = arm64.CPUManufacturer() + c.Check(err, ErrorMatches, "SMBIOS structure data truncated: have 8 bytes, formatted area length is 17") +} + +func (s *defaultEnvARM64Suite) TestCPUManufacturerOutOfRangeStringIndex(c *C) { + restore := MockOsReadFile(func(path string) ([]byte, error) { + if path == "/sys/firmware/dmi/entries/4-0/raw" { + fa := make([]byte, 0x11) + fa[0] = 4 + fa[0x07] = 3 // Manufacturer = string 3, but only 2 strings exist + fa[0x10] = 2 + return buildSMBIOSType4(fa, "NVIDIA", "GB10"), nil + } + return nil, fmt.Errorf("unexpected path: %s", path) + }) + defer restore() + + arm64, err := DefaultEnv.ARM64() + c.Assert(err, IsNil) + + _, err = arm64.CPUManufacturer() + c.Check(err, ErrorMatches, "SMBIOS string index 3 is out of range") +} + +func (s *defaultEnvARM64Suite) TestCPUManufacturerUnsetStringIndex(c *C) { + restore := MockOsReadFile(func(path string) ([]byte, error) { + if path == "/sys/firmware/dmi/entries/4-0/raw" { + fa := make([]byte, 0x11) + fa[0] = 4 + fa[0x07] = 0 // Manufacturer unset (index 0) + fa[0x10] = 1 + return buildSMBIOSType4(fa, "GB10"), nil + } + return nil, fmt.Errorf("unexpected path: %s", path) + }) + defer restore() + + arm64, err := DefaultEnv.ARM64() + c.Assert(err, IsNil) + + _, err = arm64.CPUManufacturer() + c.Check(err, ErrorMatches, "SMBIOS field at offset 0x07 is unset") +} + +// makeRealisticType4Blob returns a SMBIOS type 4 blob laid out like the ones +// observed on real NVIDIA Spark hardware: a 0x32 byte formatted area, with +// Socket Designation as string 1, Manufacturer as string 2, Version as string +// 3, and further strings following Version. +func makeRealisticType4Blob(manufacturer, version string) []byte { + fa := make([]byte, 0x32) // 50 bytes, as reported by dmidecode on DGX Spark and RTX Spark + fa[0] = 4 // structure type 4 + fa[0x04] = 1 // Socket Designation = string 1 + fa[0x07] = 2 // Manufacturer = string 2 + fa[0x10] = 3 // Version = string 3 + return buildSMBIOSType4(fa, "CPU01", manufacturer, version, "NA", "NA", "Spark") +} + +func (s *defaultEnvARM64Suite) TestCPUManufacturerAndVersionDGXSparkLayout(c *C) { + // Regression test using the structure shape reported by a real DGX Spark, + // where Version is string 3 and is followed by more strings. + restore := MockOsReadFile(func(path string) ([]byte, error) { + if path == "/sys/firmware/dmi/entries/4-0/raw" { + return makeRealisticType4Blob("NVIDIA", "GB10"), nil + } + return nil, fmt.Errorf("unexpected path: %s", path) + }) + defer restore() + + arm64, err := DefaultEnv.ARM64() + c.Assert(err, IsNil) + + manufacturer, err := arm64.CPUManufacturer() + c.Check(err, IsNil) + c.Check(manufacturer, Equals, "NVIDIA") + + version, err := arm64.CPUVersion() + c.Check(err, IsNil) + c.Check(version, Equals, "GB10") +} + +func (s *defaultEnvARM64Suite) TestCPUManufacturerAndVersionRTXSparkLayout(c *C) { + // Regression test using the structure shape and version string reported by + // a real RTX Spark. + restore := MockOsReadFile(func(path string) ([]byte, error) { + if path == "/sys/firmware/dmi/entries/4-0/raw" { + return makeRealisticType4Blob("NVIDIA", "NVIDIA RTX Spark N1X (5120-core GPU, 18-core CPU)"), nil + } + return nil, fmt.Errorf("unexpected path: %s", path) + }) + defer restore() + + arm64, err := DefaultEnv.ARM64() + c.Assert(err, IsNil) + + manufacturer, err := arm64.CPUManufacturer() + c.Check(err, IsNil) + c.Check(manufacturer, Equals, "NVIDIA") + + version, err := arm64.CPUVersion() + c.Check(err, IsNil) + c.Check(version, Equals, "NVIDIA RTX Spark N1X (5120-core GPU, 18-core CPU)") +} + +func (s *defaultEnvARM64Suite) TestNotARM64Host(c *C) { + // Override the arm64 mock installed by SetUpTest: on a non-arm64 host + // ARM64() must return ErrNotARM64Host. + restore := MockRuntimeGOARCH("amd64") + defer restore() + + _, err := DefaultEnv.ARM64() + c.Check(err, Equals, ErrNotARM64Host) +} diff --git a/internal/efi/env.go b/internal/efi/env.go index 15ff4889..043d9596 100644 --- a/internal/efi/env.go +++ b/internal/efi/env.go @@ -93,6 +93,18 @@ type HostEnvironmentAMD64 interface { ReadMSRs(msr uint32) (map[uint32]uint64, error) } +// HostEnvironmentARM64 is an interface that abstracts out a host environment specific +// to ARM64 platforms. +type HostEnvironmentARM64 interface { + // CPUManufacturer returns the processor manufacturer from the SMBIOS + // type 4 (Processor Information) structure. + CPUManufacturer() (string, error) + + // CPUVersion returns the processor version from the SMBIOS + // type 4 (Processor Information) structure. + CPUVersion() (string, error) +} + // DetectVirtMode controls what type of virtualization to test for. type DetectVirtMode int @@ -123,6 +135,10 @@ var ( // are not AMD64. ErrNotAMD64Host = errors.New("not a AMD64 host") + // ErrNotARM64Host is returned from HostEnvironment.ARM64 on environments that + // are not ARM64. + ErrNotARM64Host = errors.New("not a ARM64 host") + // ErrNoKernelMSRSupport is returned from HostEnvironmentAMD64.ReadMSRs if there is // no support for reading MSRs. ErrNoKernelMSRSupport = errors.New("missing kernel support for reading MSRs") @@ -152,4 +168,8 @@ type HostEnvironment interface { // AMD64 returns an interface that can be used to mock some parts of an AMD64 platform. // This will return ErrNotAMD64Host on non-AMD64 platforms. AMD64() (HostEnvironmentAMD64, error) + + // ARM64 returns an interface that can be used to mock some parts of an ARM64 platform. + // This will return ErrNotARM64Host on non-ARM64 platforms. + ARM64() (HostEnvironmentARM64, error) } diff --git a/internal/efitest/hostenv.go b/internal/efitest/hostenv.go index a1773682..bd2f8a45 100644 --- a/internal/efitest/hostenv.go +++ b/internal/efitest/hostenv.go @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2021 Canonical Ltd + * Copyright (C) 2021-2026 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -48,6 +48,7 @@ type MockHostEnvironment struct { Devices map[string]internal_efi.SysfsDevice AMD64Env internal_efi.HostEnvironmentAMD64 + ARM64Env internal_efi.HostEnvironmentARM64 } func NewMockHostEnvironment(vars MockVars, log *tcglog.Log) *MockHostEnvironment { @@ -149,6 +150,19 @@ func (e *mockHostEnvironmentAMD64) ReadMSRs(msr uint32) (map[uint32]uint64, erro return out, nil } +type mockHostEnvironmentARM64 struct { + cpuManufacturer string + cpuVersion string +} + +func (e *mockHostEnvironmentARM64) CPUManufacturer() (string, error) { + return e.cpuManufacturer, nil +} + +func (e *mockHostEnvironmentARM64) CPUVersion() (string, error) { + return e.cpuVersion, nil +} + // MockSysfsDevice is a mock implementation of [internal_efi.SysfsDevice]. type MockSysfsDevice struct { DevicePath string @@ -221,6 +235,16 @@ func WithAMD64Environment(cpuVendorIdentificator string, family uint32, cpuidFea } } +// WithARM64Environment adds a [github.com/snapcore/secboot/efi/internal.HostEnvironmentARM64] to the [MockHostEnvironment]. +func WithARM64Environment(cpuManufacturer, cpuVersion string) MockHostEnvironmentOption { + return func(env *MockHostEnvironment) { + env.ARM64Env = &mockHostEnvironmentARM64{ + cpuManufacturer: cpuManufacturer, + cpuVersion: cpuVersion, + } + } +} + // NewMockHostEnvironmentWithOpts returns a new MockHostEnvironment. func NewMockHostEnvironmentWithOpts(options ...MockHostEnvironmentOption) *MockHostEnvironment { env := &MockHostEnvironment{ @@ -308,3 +332,11 @@ func (e *MockHostEnvironment) AMD64() (internal_efi.HostEnvironmentAMD64, error) } return e.AMD64Env, nil } + +// ARM64 implements [github.com/snapcore/secboot/internal/efi.HostEnvironment.ARM64]. +func (e *MockHostEnvironment) ARM64() (internal_efi.HostEnvironmentARM64, error) { + if e.ARM64Env == nil { + return nil, internal_efi.ErrNotARM64Host + } + return e.ARM64Env, nil +}