From f6d9f918776fd1c3ecaa653d3df1adbc40ad334f Mon Sep 17 00:00:00 2001 From: Malte Poll Date: Thu, 21 Sep 2023 14:50:18 +0200 Subject: [PATCH] image: reimplement and adapt measurement generation in Go --- bazel/toolchains/go_module_deps.bzl | 16 ++ go.mod | 3 +- go.sum | 3 + image/measured-boot/cmd/BUILD.bazel | 34 +++ image/measured-boot/cmd/main.go | 227 +++++++++++++++++ image/measured-boot/extract/BUILD.bazel | 22 ++ image/measured-boot/extract/extract.go | 116 +++++++++ image/measured-boot/extract/extract_test.go | 239 ++++++++++++++++++ image/measured-boot/extract_authentihash.py | 21 -- image/measured-boot/fixtures/BUILD.bazel | 9 + image/measured-boot/fixtures/fixtures.go | 17 ++ image/measured-boot/fixtures/uki.efi | Bin 0 -> 18432 bytes image/measured-boot/measure/BUILD.bazel | 39 +++ image/measured-boot/measure/authentihash.go | 31 +++ .../measure/authentihash_test.go | 33 +++ image/measured-boot/measure/measure_test.go | 17 ++ image/measured-boot/measure/pcr.go | 120 +++++++++ image/measured-boot/measure/pcr04.go | 59 +++++ image/measured-boot/measure/pcr04_test.go | 51 ++++ image/measured-boot/measure/pcr09.go | 60 +++++ image/measured-boot/measure/pcr09_test.go | 38 +++ image/measured-boot/measure/pcr11.go | 68 +++++ image/measured-boot/measure/pcr11_test.go | 61 +++++ image/measured-boot/measure/pcr_test.go | 49 ++++ image/measured-boot/measure_util.sh | 35 --- image/measured-boot/pcr-stable.json | 16 -- image/measured-boot/pesection/BUILD.bazel | 8 + image/measured-boot/pesection/pesection.go | 24 ++ image/measured-boot/precalculate_pcr_12.sh | 77 ------ image/measured-boot/precalculate_pcr_4.sh | 77 ------ image/measured-boot/precalculate_pcr_9.sh | 59 ----- 31 files changed, 1343 insertions(+), 286 deletions(-) create mode 100644 image/measured-boot/cmd/BUILD.bazel create mode 100644 image/measured-boot/cmd/main.go create mode 100644 image/measured-boot/extract/BUILD.bazel create mode 100644 image/measured-boot/extract/extract.go create mode 100644 image/measured-boot/extract/extract_test.go delete mode 100755 image/measured-boot/extract_authentihash.py create mode 100644 image/measured-boot/fixtures/BUILD.bazel create mode 100644 image/measured-boot/fixtures/fixtures.go create mode 100644 image/measured-boot/fixtures/uki.efi create mode 100644 image/measured-boot/measure/BUILD.bazel create mode 100644 image/measured-boot/measure/authentihash.go create mode 100644 image/measured-boot/measure/authentihash_test.go create mode 100644 image/measured-boot/measure/measure_test.go create mode 100644 image/measured-boot/measure/pcr.go create mode 100644 image/measured-boot/measure/pcr04.go create mode 100644 image/measured-boot/measure/pcr04_test.go create mode 100644 image/measured-boot/measure/pcr09.go create mode 100644 image/measured-boot/measure/pcr09_test.go create mode 100644 image/measured-boot/measure/pcr11.go create mode 100644 image/measured-boot/measure/pcr11_test.go create mode 100644 image/measured-boot/measure/pcr_test.go delete mode 100644 image/measured-boot/measure_util.sh delete mode 100755 image/measured-boot/pcr-stable.json create mode 100644 image/measured-boot/pesection/BUILD.bazel create mode 100644 image/measured-boot/pesection/pesection.go delete mode 100755 image/measured-boot/precalculate_pcr_12.sh delete mode 100755 image/measured-boot/precalculate_pcr_4.sh delete mode 100755 image/measured-boot/precalculate_pcr_9.sh diff --git a/bazel/toolchains/go_module_deps.bzl b/bazel/toolchains/go_module_deps.bzl index aa39c5312..f7ef9f354 100644 --- a/bazel/toolchains/go_module_deps.bzl +++ b/bazel/toolchains/go_module_deps.bzl @@ -142,6 +142,14 @@ def go_dependencies(): sum = "h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0=", version = "v1.4.1", ) + go_repository( + name = "com_github_anatol_vmtest", + build_file_generation = "on", + build_file_proto_mode = "disable_global", + importpath = "github.com/anatol/vmtest", + sum = "h1:t4JGeY9oaF5LB4Rdx9e2wARRRPAYt8Ow4eCf5SwO3fA=", + version = "v0.0.0-20220413190228-7a42f1f6d7b8", + ) go_repository( name = "com_github_andybalholm_brotli", @@ -1720,6 +1728,14 @@ def go_dependencies(): sum = "h1:/l4kBbb4/vGSsdtB5nUe8L7B9mImVMaBPw9L/0TBHU8=", version = "v3.2.5+incompatible", ) + go_repository( + name = "com_github_foxboron_go_uefi", + build_file_generation = "on", + build_file_proto_mode = "disable_global", + importpath = "github.com/foxboron/go-uefi", + sum = "h1:SJMQFT74bCrP+kQ24oWhmuyPFHDTavrd3JMIe//2NhU=", + version = "v0.0.0-20230808201820-18b9ba9cd4c3", + ) go_repository( name = "com_github_foxcpp_go_mockdns", diff --git a/go.mod b/go.mod index d86acbaba..60085b5a4 100644 --- a/go.mod +++ b/go.mod @@ -196,6 +196,7 @@ require ( github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect github.com/fatih/color v1.15.0 // indirect + github.com/foxboron/go-uefi v0.0.0-20230808201820-18b9ba9cd4c3 github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/go-chi/chi v4.1.2+incompatible // indirect github.com/go-errors/errors v1.4.2 // indirect @@ -320,7 +321,7 @@ require ( golang.org/x/oauth2 v0.9.0 // indirect golang.org/x/sync v0.3.0 // indirect golang.org/x/term v0.12.0 // indirect - golang.org/x/text v0.13.0 // indirect + golang.org/x/text v0.13.0 golang.org/x/time v0.3.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/appengine v1.6.7 // indirect diff --git a/go.sum b/go.sum index f41a58acf..524deeaf7 100644 --- a/go.sum +++ b/go.sum @@ -332,6 +332,8 @@ github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYF github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= +github.com/foxboron/go-uefi v0.0.0-20230808201820-18b9ba9cd4c3 h1:SJMQFT74bCrP+kQ24oWhmuyPFHDTavrd3JMIe//2NhU= +github.com/foxboron/go-uefi v0.0.0-20230808201820-18b9ba9cd4c3/go.mod h1:VdozURTQHi5Rs54l+4Szi3yIJQDMfXXYrRLAjKKowWI= github.com/foxcpp/go-mockdns v1.0.0 h1:7jBqxd3WDWwi/6WhDvacvH1XsN3rOLXyHM1uhvIx6FI= github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= @@ -1056,6 +1058,7 @@ go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4x go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8= go.mongodb.org/mongo-driver v1.11.3 h1:Ql6K6qYHEzB6xvu4+AU0BoRoqf9vFPcc4o7MUIdPW8Y= go.mongodb.org/mongo-driver v1.11.3/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= +go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1 h1:A/5uWzF44DlIgdm/PQFwfMkW0JX+cIcQi/SwLAmZP5M= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= diff --git a/image/measured-boot/cmd/BUILD.bazel b/image/measured-boot/cmd/BUILD.bazel new file mode 100644 index 000000000..a3bb2d4f8 --- /dev/null +++ b/image/measured-boot/cmd/BUILD.bazel @@ -0,0 +1,34 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") + +go_library( + name = "cmd_lib", + srcs = ["main.go"], + importpath = "github.com/edgelesssys/constellation/v2/image/measured-boot/cmd", + visibility = ["//visibility:private"], + deps = [ + "//image/measured-boot/extract", + "//image/measured-boot/measure", + "//image/measured-boot/pesection", + "@com_github_spf13_afero//:afero", + ], +) + +go_binary( + name = "cmd", + # keep + data = select({ + "@rules_nixpkgs_core//constraints:support_nix": [ + "@systemd//:bin/systemd-dissect", + ], + "//conditions:default": [], + }), + embed = [":cmd_lib"], + # keep + env = select({ + "@rules_nixpkgs_core//constraints:support_nix": { + "DISSECT_TOOLCHAIN": "$(rootpath @systemd//:bin/systemd-dissect)", + }, + "//conditions:default": {}, + }), + visibility = ["//visibility:public"], +) diff --git a/image/measured-boot/cmd/main.go b/image/measured-boot/cmd/main.go new file mode 100644 index 000000000..e855c2688 --- /dev/null +++ b/image/measured-boot/cmd/main.go @@ -0,0 +1,227 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package main + +import ( + "bytes" + "crypto/sha256" + "encoding/json" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + + "github.com/edgelesssys/constellation/v2/image/measured-boot/extract" + "github.com/edgelesssys/constellation/v2/image/measured-boot/measure" + "github.com/edgelesssys/constellation/v2/image/measured-boot/pesection" + "github.com/spf13/afero" +) + +const ( + ukiPath = "/efi/EFI/BOOT/BOOTX64.EFI" +) + +func precalculatePCRs(fs afero.Fs, dissectToolchain, imageFile string) (*measure.Simulator, error) { + dir, err := afero.TempDir(fs, "", "con-measure") + if err != nil { + return nil, err + } + defer func() { _ = fs.RemoveAll(dir) }() + + simulator := measure.NewDefaultSimulator() + + // extract UKI from raw image + ukiFile := filepath.Join(dir, "uki.efi") + if err := extract.CopyFrom(dissectToolchain, imageFile, ukiPath, ukiFile); err != nil { + return nil, fmt.Errorf("failed to extract UKI: %v", err) + } + + // extract section digests from UKI + ukiReader, err := fs.Open(ukiFile) + if err != nil { + return nil, err + } + defer ukiReader.Close() + + ukiSections, err := extract.PeFileSectionDigests(ukiReader) + if err != nil { + return nil, fmt.Errorf("failed to extract UKI section digests: %v", err) + } + + if err := precalculatePCR4(simulator, fs, ukiFile); err != nil { + return nil, err + } + + if err := precalculatePCR9(simulator, fs, ukiFile); err != nil { + return nil, err + } + + if err := precalculatePCR11(simulator, ukiSections); err != nil { + return nil, err + } + + fmt.Fprintf(os.Stderr, "PCR[ 4]: %x\n", simulator.Bank[4]) + fmt.Fprintf(os.Stderr, "PCR[ 9]: %x\n", simulator.Bank[9]) + fmt.Fprintf(os.Stderr, "PCR[11]: %x\n", simulator.Bank[11]) + // TODO(malt3): with systemd-stub >= 254, PCR[12] will + // contain the "rendered" kernel command line, + // credentials, and sysexts. We should measure these + // values here. + // For now, we expect the PCR to be zero. + fmt.Fprintf(os.Stderr, "PCR[12]: %x\n", simulator.Bank[12]) + // PCR[13] would contain extension images for the initrd + // We enforce the absence of extension images by + // expecting PCR[13] to be zero. + fmt.Fprintf(os.Stderr, "PCR[13]: %x\n", simulator.Bank[13]) + // PCR[15] can be used to measure from userspace (systemd-pcrphase and others) + // We enforce the absence of userspace measurements by + // expecting PCR[15] to be zero at boot. + fmt.Fprintf(os.Stderr, "PCR[15]: %x\n", simulator.Bank[15]) + + return simulator, nil +} + +func measurePE(fs afero.Fs, peFile string) ([]byte, error) { + f, err := fs.Open(peFile) + if err != nil { + return nil, err + } + defer f.Close() + + return measure.Authentihash(f, sha256.New()) +} + +func precalculatePCR4(simulator *measure.Simulator, fs afero.Fs, ukiFile string) error { + ukiMeasurement, err := measurePE(fs, ukiFile) + if err != nil { + return fmt.Errorf("failed to measure UKI: %v", err) + } + + ukiPe, err := fs.Open(ukiFile) + if err != nil { + return err + } + defer ukiPe.Close() + linuxSectionReader, err := extract.PeSectionReader(ukiPe, ".linux") + if err != nil { + return fmt.Errorf("uki does not contain linux kernel image: %v", err) + } + linuxMeasurement, err := measure.Authentihash(linuxSectionReader, sha256.New()) + if err != nil { + return fmt.Errorf("failed to measure linux kernel image: %v", err) + } + + bootStages := []measure.EFIBootStage{ + {Name: "Unified Kernel Image (UKI)", Digest: measure.PCR256(ukiMeasurement)}, + {Name: "Linux", Digest: measure.PCR256(linuxMeasurement)}, + } + + if err := measure.DescribeBootStages(os.Stderr, bootStages); err != nil { + return err + } + + return measure.PredictPCR4(simulator, bootStages) +} + +func precalculatePCR9(simulator *measure.Simulator, fs afero.Fs, ukiFile string) error { + // load cmdline and initrd from UKI + + ukiPe, err := fs.Open(ukiFile) + if err != nil { + return err + } + defer ukiPe.Close() + + cmdlineSectionReader, err := extract.PeSectionReader(ukiPe, ".cmdline") + if err != nil { + return fmt.Errorf("uki does not contain cmdline: %v", err) + } + + cmdline := new(bytes.Buffer) + if _, err := cmdline.ReadFrom(cmdlineSectionReader); err != nil { + return err + } + + initrdSectionReader, err := extract.PeSectionReader(ukiPe, ".initrd") + if err != nil { + return fmt.Errorf("uki does not contain initrd: %v", err) + } + + initrdDigest := sha256.New() + if _, err := io.Copy(initrdDigest, initrdSectionReader); err != nil { + return err + } + + cmdlineBytes := cmdline.Bytes() + initrdDigestBytes := [32]byte(initrdDigest.Sum(nil)) + + if err := measure.DescribeLinuxLoad2(os.Stderr, cmdlineBytes, initrdDigestBytes); err != nil { + return err + } + + return measure.PredictPCR9(simulator, cmdlineBytes, initrdDigestBytes) +} + +func precalculatePCR11(simulator *measure.Simulator, ukiSections []pesection.PESection) error { + if err := measure.DescribeUKISections(os.Stderr, ukiSections); err != nil { + return err + } + + return measure.PredictPCR11(simulator, ukiSections) +} + +func loadToolchain(key, fallback string) string { + toolchain := os.Getenv(key) + if toolchain == "" { + toolchain = fallback + } + toolchain, err := exec.LookPath(toolchain) + if err != nil { + return "" + } + + absolutePath, err := filepath.Abs(toolchain) + if err != nil { + return "" + } + return absolutePath +} + +func writeOutput(fs afero.Fs, outputFile string, simulator *measure.Simulator) error { + out, err := fs.Create(outputFile) + if err != nil { + return err + } + defer out.Close() + + return json.NewEncoder(out).Encode(simulator) +} + +func main() { + if len(os.Args) != 3 { + fmt.Fprintln(os.Stderr, "Usage: measured-boot-precalc ") + os.Exit(1) + } + + imageFile := os.Args[1] + outputFile := os.Args[2] + + fs := afero.NewOsFs() + dissectToolchain := loadToolchain("DISSECT_TOOLCHAIN", "systemd-dissect") + + simulator, err := precalculatePCRs(fs, dissectToolchain, imageFile) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + if err := writeOutput(fs, outputFile, simulator); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} diff --git a/image/measured-boot/extract/BUILD.bazel b/image/measured-boot/extract/BUILD.bazel new file mode 100644 index 000000000..0d8eb013b --- /dev/null +++ b/image/measured-boot/extract/BUILD.bazel @@ -0,0 +1,22 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") +load("//bazel/go:go_test.bzl", "go_test") + +go_library( + name = "extract", + srcs = ["extract.go"], + importpath = "github.com/edgelesssys/constellation/v2/image/measured-boot/extract", + visibility = ["//visibility:public"], + deps = ["//image/measured-boot/pesection"], +) + +go_test( + name = "extract_test", + srcs = ["extract_test.go"], + embed = [":extract"], + deps = [ + "//image/measured-boot/fixtures", + "//image/measured-boot/pesection", + "@com_github_stretchr_testify//assert", + "@org_uber_go_goleak//:goleak", + ], +) diff --git a/image/measured-boot/extract/extract.go b/image/measured-boot/extract/extract.go new file mode 100644 index 000000000..d96c302e0 --- /dev/null +++ b/image/measured-boot/extract/extract.go @@ -0,0 +1,116 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package extract + +import ( + "crypto/sha256" + "debug/pe" + "fmt" + "io" + "os/exec" + "sort" + + "github.com/edgelesssys/constellation/v2/image/measured-boot/pesection" +) + +// CopyFrom is a wrapper for systemd-dissect --copy-from. +func CopyFrom(dissectToolchain, image, path, output string) error { + if dissectToolchain == "" { + dissectToolchain = "systemd-dissect" + } + out, err := exec.Command(dissectToolchain, "--copy-from", image, path, output).CombinedOutput() + if err != nil { + return fmt.Errorf("failed to extract %s from %s: %v\n%s", path, image, err, out) + } + return nil +} + +// PeSectionReader returns a reader for the named section of a PE file. +func PeSectionReader(peFile io.ReaderAt, section string) (io.Reader, error) { + f, err := pe.NewFile(peFile) + if err != nil { + return nil, err + } + defer f.Close() + + for _, s := range f.Sections { + if s.Name == section { + return io.LimitReader(s.Open(), int64(s.VirtualSize)), nil + } + } + + return nil, fmt.Errorf("section %q not found", section) +} + +// PeFileSectionDigests returns the section digests of a PE file. +func PeFileSectionDigests(peFile io.ReaderAt) ([]pesection.PESection, error) { + f, err := pe.NewFile(peFile) + if err != nil { + return nil, err + } + defer f.Close() + + sections := make([]pesection.PESection, len(f.Sections)) + for i, section := range f.Sections { + sectionDigest := sha256.New() + sectionReader := section.Open() + _, err := io.CopyN(sectionDigest, sectionReader, int64(section.VirtualSize)) + if err != nil { + return nil, err + } + + sections[i].Name = section.Name + sections[i].Size = section.VirtualSize + sections[i].Digest = ([32]byte)(sectionDigest.Sum(nil)) + sections[i].Measure = shouldMeasureSection(section.Name) + sections[i].MeasureOrder = sectionMeasureOrder(section.Name) + } + + sort.Slice(sections, func(i, j int) bool { + if sections[i].Measure != sections[j].Measure { + return sections[i].Measure + } + if sections[i].MeasureOrder == sections[j].MeasureOrder { + return sections[i].Name < sections[j].Name + } + return sections[i].MeasureOrder < sections[j].MeasureOrder + }) + + return sections, nil +} + +var ukiSections = []string{ + ".linux", + ".osrel", + ".cmdline", + ".initrd", + ".splash", + ".dtb", + // uanme and sbat will be added in systemd-stub >= 254 + // ".uname", + // ".sbat", + ".pcrsig", + ".pcrkey", +} + +func shouldMeasureSection(name string) bool { + for _, section := range ukiSections { + if name == section && name != ".pcrsig" { + return true + } + } + return false +} + +func sectionMeasureOrder(name string) int { + for i, section := range ukiSections { + if name == section { + return i + } + } + return -1 +} diff --git a/image/measured-boot/extract/extract_test.go b/image/measured-boot/extract/extract_test.go new file mode 100644 index 000000000..4e8e6379c --- /dev/null +++ b/image/measured-boot/extract/extract_test.go @@ -0,0 +1,239 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package extract + +import ( + "bytes" + "io" + "testing" + + "github.com/edgelesssys/constellation/v2/image/measured-boot/fixtures" + "github.com/edgelesssys/constellation/v2/image/measured-boot/pesection" + + "github.com/stretchr/testify/assert" + "go.uber.org/goleak" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m) +} + +func TestPeSectionReader(t *testing.T) { + assert := assert.New(t) + + // can read existing section ".uname" + peReader := bytes.NewReader(fixtures.UKI()) + unameSectionReader, err := PeSectionReader(peReader, ".uname") + assert.NoError(err) + uname, err := io.ReadAll(unameSectionReader) + assert.NoError(err) + assert.Equal("0.0.0-100.constellation.fc0.x86_64", string(uname)) + + // fails to read non-existing section + _, err = PeSectionReader(peReader, ".non-existing") + assert.Error(err) + + // fails to read non-PE file + _, err = PeSectionReader(bytes.NewReader([]byte("not a PE file")), ".uname") + assert.Error(err) +} + +func TestPeFileSectionDigests(t *testing.T) { + assert := assert.New(t) + + // can calculate section digests + peReader := bytes.NewReader(fixtures.UKI()) + sectionDigests, err := PeFileSectionDigests(peReader) + assert.NoError(err) + assert.Equal([]pesection.PESection{ + { + Name: ".linux", Size: 0x1f8, + Digest: [32]uint8{ + 0x01, 0xe5, 0xce, 0xe2, 0xd1, 0x8e, 0xaa, 0xce, + 0x36, 0xb5, 0xbc, 0x39, 0x4f, 0x70, 0x31, 0xaa, + 0xe1, 0x66, 0x8e, 0x4a, 0x7f, 0x7c, 0xc0, 0xe9, + 0x49, 0x52, 0x5e, 0xa6, 0x5c, 0x40, 0xf7, 0x95, + }, + Measure: true, MeasureOrder: 0, + }, + { + Name: ".osrel", Size: 0x2b0, + Digest: [32]uint8{ + 0x65, 0x83, 0x80, 0x1d, 0xa2, 0x9b, 0x3b, 0x74, + 0x0f, 0x0e, 0xb0, 0xc4, 0x27, 0xd5, 0xb8, 0x52, + 0x0b, 0xfb, 0xf7, 0xff, 0x63, 0x69, 0xc2, 0x2e, + 0xf2, 0xf4, 0xc4, 0x80, 0xf0, 0xea, 0x99, 0xfc, + }, + Measure: true, MeasureOrder: 1, + }, + { + Name: ".cmdline", + Size: 0x94, + Digest: [32]uint8{ + 0xf0, 0x47, 0xd0, 0x3a, 0x36, 0xf0, 0xde, 0x1f, + 0x77, 0x91, 0x6c, 0x2a, 0xab, 0x88, 0x77, 0xa9, + 0xd8, 0x80, 0xac, 0xf9, 0x17, 0x68, 0x3c, 0xc7, + 0x7b, 0x7c, 0x01, 0xdf, 0x18, 0xb1, 0x31, 0xc7, + }, + Measure: true, MeasureOrder: 2, + }, + { + Name: ".initrd", + Size: 0x12, + Digest: [32]uint8{ + 0x4e, 0x50, 0x30, 0x6a, 0x07, 0x84, 0x47, 0x1f, + 0x02, 0xde, 0x7e, 0x54, 0xd9, 0x0f, 0xdc, 0xa1, + 0x0e, 0x8e, 0x12, 0xec, 0xcc, 0x2d, 0x7a, 0x9d, + 0x97, 0x02, 0xf6, 0xe7, 0x38, 0xe1, 0xc2, 0xca, + }, + Measure: true, MeasureOrder: 3, + }, + { + Name: ".splash", + Size: 0x12, + Digest: [32]uint8{ + 0x36, 0xb5, 0xf4, 0x82, 0x37, 0x2e, 0x50, 0x49, + 0x83, 0x9d, 0x17, 0x6c, 0xf4, 0xd1, 0x4a, 0xcb, + 0xfd, 0xfe, 0xda, 0xc1, 0xbf, 0x77, 0xea, 0x0e, + 0xa4, 0xb1, 0x72, 0xa8, 0x76, 0xae, 0x2d, 0x2e, + }, + Measure: true, MeasureOrder: 4, + }, + { + Name: ".dtb", + Size: 0xf, + Digest: [32]uint8{ + 0x46, 0xa0, 0x01, 0x53, 0xca, 0xd9, 0x9d, 0x19, + 0x4a, 0xf1, 0x14, 0x48, 0x30, 0x5c, 0x8c, 0xa1, + 0x87, 0x2a, 0xba, 0xe9, 0x20, 0xee, 0x42, 0x3c, + 0x19, 0x35, 0x01, 0x05, 0x0f, 0x36, 0xe7, 0x8d, + }, + Measure: true, MeasureOrder: 5, + }, + { + Name: ".pcrkey", + Size: 0x12, + Digest: [32]uint8{ + 0x35, 0x4b, 0x67, 0xd5, 0xa3, 0xef, 0x2a, 0xff, + 0xda, 0xdb, 0x3d, 0xfc, 0x1f, 0x8b, 0xd0, 0xf6, + 0x69, 0xd0, 0x86, 0xa6, 0xd6, 0x7d, 0x5f, 0xee, + 0x88, 0xdb, 0x21, 0x90, 0xc4, 0xa7, 0x07, 0x26, + }, + Measure: true, MeasureOrder: 7, + }, + { + Name: ".data", + Size: 0x10, + Digest: [32]uint8{ + 0xc3, 0xde, 0x14, 0xca, 0x16, 0x45, 0x87, 0x5e, + 0x3b, 0xb0, 0xdd, 0xab, 0x9f, 0x60, 0x91, 0x46, + 0xf2, 0x1c, 0xc0, 0xeb, 0xd0, 0xea, 0x9b, 0x4f, + 0x22, 0xd3, 0x98, 0x40, 0xc0, 0xea, 0x29, 0xc5, + }, + Measure: false, MeasureOrder: -1, + }, + { + Name: ".dynamic", + Size: 0x13, + Digest: [32]uint8{ + 0x2b, 0x75, 0x29, 0xc8, 0x3a, 0x74, 0xbc, 0xb0, + 0xac, 0x63, 0x15, 0x18, 0xa1, 0x14, 0x95, 0x10, + 0x1a, 0x8d, 0x8e, 0x40, 0x69, 0x93, 0xed, 0x05, + 0xed, 0x8a, 0xcc, 0x2d, 0x88, 0xec, 0x13, 0x79, + }, + Measure: false, MeasureOrder: -1, + }, + { + Name: ".dynsym", + Size: 0x12, + Digest: [32]uint8{ + 0xb6, 0x0a, 0x7d, 0x65, 0x69, 0xeb, 0xa3, 0xd9, + 0x9e, 0xec, 0x13, 0x32, 0x57, 0x2b, 0x61, 0x19, + 0x32, 0x0b, 0x57, 0x1b, 0x43, 0xc1, 0x96, 0x75, + 0x37, 0x5a, 0x85, 0x76, 0xda, 0xf7, 0x81, 0x24, + }, + Measure: false, MeasureOrder: -1, + }, + { + //nolint:misspell + Name: ".rela", + Size: 0x10, + Digest: [32]uint8{ + 0x1c, 0xd6, 0xfb, 0x4f, 0xb8, 0x74, 0xfd, 0xb2, + 0xf3, 0xb7, 0xf5, 0x3d, 0xc1, 0x8c, 0x5b, 0x8e, + 0x5b, 0xa1, 0x4d, 0x00, 0x6c, 0x56, 0x41, 0x5e, + 0x9b, 0x8e, 0x22, 0x1d, 0xbf, 0x59, 0xdd, 0x9d, + }, + Measure: false, MeasureOrder: -1, + }, + { + Name: ".reloc", + Size: 0xa, + Digest: [32]uint8{ + 0x4f, 0xfa, 0xdb, 0x1d, 0xbd, 0xe9, 0x2d, 0xce, + 0x21, 0x37, 0xae, 0x1e, 0x24, 0x74, 0xad, 0x09, + 0xf2, 0x7b, 0x62, 0xe4, 0xbb, 0xa5, 0xcc, 0xc6, + 0x49, 0x0a, 0xb0, 0xda, 0x45, 0xfa, 0x45, 0xc3, + }, + Measure: false, MeasureOrder: -1, + }, + { + Name: ".sbat", Size: 0x10, + Digest: [32]uint8{ + 0x66, 0x30, 0xfb, 0x7d, 0x5b, 0xaf, 0x9d, 0x6c, + 0xd5, 0x1c, 0x9a, 0xc9, 0x54, 0x10, 0xe6, 0x8a, + 0xa3, 0xfe, 0xdb, 0x4a, 0xdd, 0xd4, 0x2b, 0x34, + 0x0e, 0x47, 0x11, 0xe2, 0x3c, 0xcc, 0xd4, 0xb2, + }, + Measure: false, MeasureOrder: -1, + }, + { + Name: ".sdmagic", Size: 0x2d, + Digest: [32]uint8{ + 0xc1, 0x02, 0x12, 0x0d, 0xe9, 0xfa, 0x62, 0x43, + 0xf2, 0x16, 0xdd, 0xb4, 0x58, 0x28, 0xe2, 0xa2, + 0xb6, 0x4a, 0x65, 0x82, 0x30, 0xd0, 0xca, 0xe6, + 0xc2, 0xf2, 0x98, 0x39, 0x67, 0xba, 0xbe, 0x95, + }, + Measure: false, MeasureOrder: -1, + }, + { + Name: ".text", Size: 0x10, + Digest: [32]uint8{ + 0xaf, 0x54, 0x41, 0x9a, 0x3f, 0xbe, 0x76, 0x0c, + 0xf7, 0xd3, 0x6a, 0x86, 0x37, 0xf0, 0x1d, 0x13, + 0xd4, 0x4b, 0xb5, 0xf3, 0x92, 0x15, 0xe2, 0x2e, + 0xad, 0x52, 0x15, 0x51, 0xfa, 0xe4, 0x2f, 0x2d, + }, + Measure: false, MeasureOrder: -1, + }, + { + Name: ".uname", Size: 0x22, + Digest: [32]uint8{ + 0x32, 0xd5, 0x9d, 0x99, 0x0e, 0x9c, 0x1f, 0x7d, + 0xa5, 0x54, 0xcb, 0x88, 0x8e, 0x32, 0x38, 0xac, + 0x61, 0x93, 0xe5, 0xe7, 0x23, 0x0f, 0x99, 0xb1, + 0x97, 0x13, 0x8d, 0xd7, 0x23, 0xc0, 0xeb, 0xb6, + }, + Measure: false, MeasureOrder: -1, + }, + { + Name: ".pcrsig", Size: 0x216, + Digest: [32]uint8{ + 0xcc, 0x41, 0xa5, 0x48, 0xbd, 0x02, 0x03, 0x17, + 0x49, 0x39, 0xf5, 0x0c, 0x3d, 0xf1, 0x77, 0x59, + 0xb8, 0x13, 0xb5, 0x31, 0xb0, 0x56, 0x3e, 0x91, + 0x20, 0x55, 0x6c, 0xf7, 0x25, 0x01, 0xa3, 0x26, + }, + Measure: false, MeasureOrder: 6, + }, + }, sectionDigests) + + // fails to read non-PE file + _, err = PeFileSectionDigests(bytes.NewReader([]byte("not a PE file"))) + assert.Error(err) +} diff --git a/image/measured-boot/extract_authentihash.py b/image/measured-boot/extract_authentihash.py deleted file mode 100755 index c3999ee2d..000000000 --- a/image/measured-boot/extract_authentihash.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) Edgeless Systems GmbH -# -# SPDX-License-Identifier: AGPL-3.0-only - -# This script calculates the authentihash of a PE / EFI binary. -# Install prerequisites: -# pip install lief - -import sys -import lief - -def authentihash(filename): - pe = lief.parse(filename) - return pe.authentihash(lief.PE.ALGORITHMS.SHA_256) - -if __name__ == '__main__': - if len(sys.argv) != 2: - print(f"Usage: {sys.argv[0]} ") - sys.exit(1) - print(authentihash(sys.argv[1]).hex()) diff --git a/image/measured-boot/fixtures/BUILD.bazel b/image/measured-boot/fixtures/BUILD.bazel new file mode 100644 index 000000000..4123024ef --- /dev/null +++ b/image/measured-boot/fixtures/BUILD.bazel @@ -0,0 +1,9 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "fixtures", + srcs = ["fixtures.go"], + embedsrcs = ["uki.efi"], + importpath = "github.com/edgelesssys/constellation/v2/image/measured-boot/fixtures", + visibility = ["//visibility:public"], +) diff --git a/image/measured-boot/fixtures/fixtures.go b/image/measured-boot/fixtures/fixtures.go new file mode 100644 index 000000000..0e9372594 --- /dev/null +++ b/image/measured-boot/fixtures/fixtures.go @@ -0,0 +1,17 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package fixtures + +import _ "embed" + +// UKI returns the UKI EFI binary. +func UKI() []byte { + return ukiEFI[:] +} + +//go:embed uki.efi +var ukiEFI []byte diff --git a/image/measured-boot/fixtures/uki.efi b/image/measured-boot/fixtures/uki.efi new file mode 100644 index 0000000000000000000000000000000000000000..35abc33fbb3a0b30edbc40306528cf443f8ba7e0 GIT binary patch literal 18432 zcmeHP%Wm676lKz)3m0h97U)`Fi#DwzX-G;UDOUycvMh%ZWj*XzMW9f8NVFcLN$Oz- z0lMlsi+(^qAA!q-VQjCcPwswv_wKjUT`*(Cf|eRo;1c$& zZ1~RJp{S1*hK;cOn)1DCd{ZvSp2S>ZXloP| zYoTC1M$CG*mig7=Hh9rXL*K>xd*Izzk0LHi< zLm((hIG*lzvGr4YxwphgxN0AYYI zKo}ql5C#YXZ)QNV$G$Q4Beoe?Z3%cYn0Rg_18=);0g-t z#n!C#B4Ew44bT5_%iWqYvR>(j=YP4Ca=i#x^K8TOzuY3b=8UXY`d@)WWLwn@w>&oO zbi`B#Msye$A~D27QOwjhA%b!1#mstSTg_IA8MzZ?zaus z{*K-#7cxE|R;~`z(OD(06pD)|^hQn2Hn){}1tVzKSniv)J4%}fJoRF;dDyw!UF5uz zY_(DDZ1(J^1@Enme66?)s5wSDZm01e9oIfG zG(RY(p<$u8oo&g@&W_S(E_%l(dSgea6g#cDoT2-^?|A8WJk&a7yS+HHRr0M$vD(Zw zfsl(}-PSz#!sM*KpoKAxwi-d4xQXK4iRT-msH%@(&%Woos&Bhqv^TMI17bH?wVG0I zUW}@GgDXMlja;kLsTZ%JyOZ9jH5{rjW3G;zp%K&U5kyw%r4BS{mt{HIsnwN2E8kp9 z@#}q^)r_N{>1trFX?e*NYPj+gtAz|B@+gYm9Lz?!k}Va3hXLjb*nq+MmOX|iDsZei zrl%+dKT*du;EvlP27UsGE^dn}O}?sl$FC*Vw*5XF0Lg@D{x{|21jjVb$@Gk|;aXZ` zWmA2;Qi7?EPF2@Z$NmZvo%+rw0NO(%)*-7W@T*3Z8GUmh*HT?vwGu4SOImgj#jQgU|wH*CfqL*b7id_EEDWeM5e z_K%Z$mt)*uJ-q&xM?r6JG^FeI%|Lkmmq&cx_Yg>bH^{(K+Uu)qGDW8&KRu=K;cZ-E zn8yzyv@ea_O$sdAWz{6tHI1asvz&o6l@*c6iM)o46eA=Uj_LA9 zN66rkEOaXfDh>t5%W&^Kd%p(e)o~fI;dJM=WI__PYYSOEe@64 zy;*$5)N;J9723?h$!L0d?9UV1O7dX;NEz%m$JK53aP(*_K4fdoak)^OH2s-ci4Pz4 zY78s;T0X@k^PBtLZe_2>4MeZFZOXG;{?zRs&i!O5@61nYu4m46eXr$Aj2z2()fvkl z>^AuProTTvKA7=cH)V;_-K2O_6E}Ar4UPGxGHmY*tsVKaJ#!}QtYFnDdrdT-jzq+$ zgXD>wJFeEIiNh0>5%r#3#pPYg@OqgH{p|6x Z8+;&1*YA~q@cSQDKmL~tMAnFbe*n maxNameLen { + maxNameLen = len(bootStage.Name) + } + } + for i, bootStage := range bootStages { + if _, err := fmt.Fprintf(w, " Stage %d - %-*s:\t%x\n", i+1, maxNameLen, bootStage.Name, bootStage.Digest); err != nil { + return err + } + } + return nil +} + +// PredictPCR4 predicts the PCR4 value based on the EFIBootStages. +func PredictPCR4(simulator *Simulator, efiBootStages []EFIBootStage) error { + // TCG PC Client Platform Firmware Profile Family "2.0 Section" 7.2.4.4.a + if err := simulator.ExtendPCR(4, EVEFIActionPCR256(), nil, "EV_EFI_ACTION: Calling EFI Application from Boot Option"); err != nil { + return err + } + // TCG PC Client Platform Firmware Profile Family "2.0 Section" 7.2.4.4.b + if err := simulator.ExtendPCR(4, EVSeparatorPCR256(), []byte{0x00, 0x00, 0x00, 0x00}, "EV_SEPARATOR"); err != nil { + return err + } + + for i, efiBootStage := range efiBootStages { + // TCG PC Client Platform Firmware Profile Family "2.0 Section" 7.2.4.4.e + err := simulator.ExtendPCR(4, efiBootStage.Digest, nil, fmt.Sprintf("Boot Stage %d: %s", i+1, efiBootStage.Name)) + if err != nil { + return err + } + } + + return nil +} diff --git a/image/measured-boot/measure/pcr04_test.go b/image/measured-boot/measure/pcr04_test.go new file mode 100644 index 000000000..215fb5d8e --- /dev/null +++ b/image/measured-boot/measure/pcr04_test.go @@ -0,0 +1,51 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package measure + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPredictPCR4(t *testing.T) { + assert := assert.New(t) + + sim := NewDefaultSimulator() + + bootstages := []EFIBootStage{ + { + Name: "stage0", + Digest: [32]byte{}, + }, + { + Name: "stage1", + Digest: [32]byte{ + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, + }, + }, + } + + out := bytes.NewBuffer(nil) + assert.NoError(DescribeBootStages(out, bootstages)) + assert.Equal("EFI Boot Stages:\n"+ + " Stage 1 - stage0:\t0000000000000000000000000000000000000000000000000000000000000000\n"+ + " Stage 2 - stage1:\t0101010101010101010101010101010101010101010101010101010101010101\n", + out.String()) + + assert.NoError(PredictPCR4(sim, bootstages)) + assert.Equal(PCR256{ + 0x22, 0x11, 0x6d, 0xee, 0x86, 0x1a, 0xa6, 0xb4, + 0x42, 0x42, 0xac, 0x46, 0x9e, 0xab, 0x24, 0xce, + 0xad, 0x34, 0x4d, 0x52, 0xc7, 0x71, 0x31, 0xf5, + 0x4a, 0xc1, 0xca, 0xc9, 0xd6, 0xa2, 0x40, 0x8e, + }, sim.Bank[4]) +} diff --git a/image/measured-boot/measure/pcr09.go b/image/measured-boot/measure/pcr09.go new file mode 100644 index 000000000..8013f3c39 --- /dev/null +++ b/image/measured-boot/measure/pcr09.go @@ -0,0 +1,60 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package measure + +import ( + "crypto/sha256" + "fmt" + "io" + + "golang.org/x/text/encoding/unicode" +) + +// DescribeLinuxLoad2 describes the expected measurements for the Linux LOAD_FILE2 protocol. +func DescribeLinuxLoad2(w io.Writer, cmdline []byte, initrdDigest [32]byte) error { + if _, err := fmt.Fprintf(w, "Linux LOAD_FILE2 protocol:\n"); err != nil { + return err + } + if _, err := fmt.Fprintf(w, " cmdline: %q\n", cmdline); err != nil { + return err + } + if _, err := fmt.Fprintf(w, " initrd (digest %x)\n", initrdDigest); err != nil { + return err + } + return nil +} + +// PredictPCR9 predicts the PCR9 value based on the kernel command line and initrd. +func PredictPCR9(simulator *Simulator, cmdline []byte, initrdDigest [32]byte) error { + // Linux LOAD_FILE2 protocol + + // Linux LOAD_FILE2 protocol - efi_convert_cmdline + // https://github.com/torvalds/linux/blob/42dc814987c1feb6410904e58cfd4c36c4146150/drivers/firmware/efi/libstub/efi-stub-helper.c#L280 + // kernel cmdline is null terminated utf-8 + // will be loaded / measured as UTF-16LE + if len(cmdline) == 0 || cmdline[len(cmdline)-1] != 0 { + return fmt.Errorf("kernel cmdline must be null terminated") + } + cmdlineUTF16LE, err := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder().Bytes(cmdline) + if err != nil { + return err + } + err = simulator.ExtendPCR(9, sha256.Sum256(cmdlineUTF16LE), cmdlineUTF16LE, fmt.Sprintf("EV_EVENT_TAG: Linux LOAD_FILE2 protocol: cmdline %q", cmdline)) + if err != nil { + return err + } + + // Linux LOAD_FILE2 protocol - efi_load_initrd + // https://github.com/torvalds/linux/blob/42dc814987c1feb6410904e58cfd4c36c4146150/drivers/firmware/efi/libstub/efi-stub-helper.c#L559 + // initrd is hashed as-is and measured + err = simulator.ExtendPCR(9, initrdDigest, nil, fmt.Sprintf("EV_EVENT_TAG: Linux LOAD_FILE2 protocol: initrd (digest %x)", initrdDigest)) + if err != nil { + return err + } + + return nil +} diff --git a/image/measured-boot/measure/pcr09_test.go b/image/measured-boot/measure/pcr09_test.go new file mode 100644 index 000000000..2462605e9 --- /dev/null +++ b/image/measured-boot/measure/pcr09_test.go @@ -0,0 +1,38 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package measure + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPredictPCR9(t *testing.T) { + assert := assert.New(t) + + sim := NewDefaultSimulator() + + cmdline := []byte("console=tty0\x00") + initrdDigest := [32]byte{} + + out := bytes.NewBuffer(nil) + assert.NoError(DescribeLinuxLoad2(out, cmdline, initrdDigest)) + assert.Equal("Linux LOAD_FILE2 protocol:\n"+ + " cmdline: \"console=tty0\\x00\"\n"+ + " initrd (digest 0000000000000000000000000000000000000000000000000000000000000000)\n", + out.String()) + + assert.NoError(PredictPCR9(sim, cmdline, initrdDigest)) + assert.Equal(PCR256{ + 0xeb, 0x4f, 0x7b, 0xca, 0x86, 0x58, 0x07, 0xd3, + 0x16, 0x3b, 0x95, 0x17, 0x4d, 0x6e, 0x66, 0xcf, + 0xc7, 0x4a, 0xcf, 0x8b, 0x93, 0x0a, 0x55, 0x3e, + 0x95, 0xec, 0x94, 0x66, 0x2c, 0xb6, 0xfa, 0xcd, + }, sim.Bank[9]) +} diff --git a/image/measured-boot/measure/pcr11.go b/image/measured-boot/measure/pcr11.go new file mode 100644 index 000000000..1fbc37887 --- /dev/null +++ b/image/measured-boot/measure/pcr11.go @@ -0,0 +1,68 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package measure + +import ( + "crypto/sha256" + "fmt" + "io" + + "github.com/edgelesssys/constellation/v2/image/measured-boot/pesection" +) + +// DescribeUKISections describes the expected measurements for the UKI sections. +func DescribeUKISections(w io.Writer, ukiSections []pesection.PESection) error { + if _, err := fmt.Fprintf(w, "UKI sections:\n"); err != nil { + return err + } + + var maxNameLen int + for _, ukiSection := range ukiSections { + if len(ukiSection.Name) > maxNameLen { + maxNameLen = len(ukiSection.Name) + } + } + for i, ukiSection := range ukiSections { + if ukiSection.Measure { + if _, err := fmt.Fprintf(w, " Section %2d - %-*s (%10d bytes):\t%x, %x\n", i+1, maxNameLen, ukiSection.Name, ukiSection.Size, sha256.Sum256(ukiSection.NullTerminatedName()), ukiSection.Digest); err != nil { + return err + } + continue + } + if _, err := fmt.Fprintf(w, " Section %2d - %-*s:\t%s\n", i+1, maxNameLen, ukiSection.Name, "not measured"); err != nil { + return err + } + } + return nil +} + +// PredictPCR11 predicts the PCR11 value based on the components of unified kernel images. +func PredictPCR11(simulator *Simulator, ukiSections []pesection.PESection) error { + for i, ukiSection := range ukiSections { + // systemd-stub documentation TPM PCR Notes + // https://github.com/systemd/systemd/blob/7c52d5236a3bc85db1755de6a458934be095cd1c/src/boot/efi/stub.c#L409-L441 + + if !ukiSection.Measure { + continue + } + + // first, measure the name + name := ukiSection.NullTerminatedName() + err := simulator.ExtendPCR(11, sha256.Sum256(name), name, fmt.Sprintf("EV_IPL: UKI section %d name: %s", i+1, ukiSection.Name)) + if err != nil { + return err + } + + // then, measure the data + err = simulator.ExtendPCR(11, ukiSection.Digest, nil, fmt.Sprintf("EV_IPL: UKI section %d data: %x", i+1, ukiSection.Digest)) + if err != nil { + return err + } + } + + return nil +} diff --git a/image/measured-boot/measure/pcr11_test.go b/image/measured-boot/measure/pcr11_test.go new file mode 100644 index 000000000..ca94c16de --- /dev/null +++ b/image/measured-boot/measure/pcr11_test.go @@ -0,0 +1,61 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package measure + +import ( + "bytes" + "testing" + + "github.com/edgelesssys/constellation/v2/image/measured-boot/pesection" + "github.com/stretchr/testify/assert" +) + +func TestPredictPCR11(t *testing.T) { + assert := assert.New(t) + + sim := NewDefaultSimulator() + + peSections := []pesection.PESection{ + { + Name: ".text", + Size: 100, + Digest: [32]byte{}, + }, + { + Name: ".linux", + Size: 100, + Digest: [32]byte{}, + Measure: true, + }, + { + Name: ".initrd", + Digest: [32]byte{ + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, + }, + Measure: true, + }, + } + + out := bytes.NewBuffer(nil) + assert.NoError(DescribeUKISections(out, peSections)) + assert.Equal("UKI sections:\n"+ + " Section 1 - .text :\tnot measured\n"+ + " Section 2 - .linux ( 100 bytes):\t0da293e37ad5511c59be47993769aacb91b243f7d010288e118dc90e95aaef5a, 0000000000000000000000000000000000000000000000000000000000000000\n"+ + " Section 3 - .initrd ( 0 bytes):\t15ee37e75f1e8d42080e91fdbbd2560780918c81fe3687ae6d15c472bbdaac75, 0101010101010101010101010101010101010101010101010101010101010101\n", + out.String()) + + assert.NoError(PredictPCR11(sim, peSections)) + assert.Equal(PCR256{ + 0x9d, 0xfe, 0x39, 0x9f, 0xcd, 0x44, 0x32, 0x63, + 0x9f, 0x0e, 0x20, 0xf4, 0x9d, 0xf8, 0x23, 0xaa, + 0x66, 0xb0, 0x95, 0xf0, 0x66, 0x4f, 0x0a, 0x4b, + 0x9f, 0xbd, 0xc1, 0x1e, 0xa6, 0x46, 0x83, 0xe2, + }, sim.Bank[11]) +} diff --git a/image/measured-boot/measure/pcr_test.go b/image/measured-boot/measure/pcr_test.go new file mode 100644 index 000000000..d05f47b69 --- /dev/null +++ b/image/measured-boot/measure/pcr_test.go @@ -0,0 +1,49 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package measure + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestExtendPCR(t *testing.T) { + assert := assert.New(t) + + sim := NewDefaultSimulator() + assert.Equal(ZeroPCR256(), sim.Bank[4]) + + assert.NoError(sim.ExtendPCR(4, EVSeparatorPCR256(), []byte{0x00, 0x00, 0x00, 0x00}, "EV_SEPARATOR")) + assert.Equal(PCR256{ + 0x3d, 0x45, 0x8c, 0xfe, 0x55, 0xcc, 0x03, 0xea, + 0x1f, 0x44, 0x3f, 0x15, 0x62, 0xbe, 0xec, 0x8d, + 0xf5, 0x1c, 0x75, 0xe1, 0x4a, 0x9f, 0xcf, 0x9a, + 0x72, 0x34, 0xa1, 0x3f, 0x19, 0x8e, 0x79, 0x69, + }, sim.Bank[4]) + + assert.NoError(sim.ExtendPCR(4, EVEFIActionPCR256(), nil, "EV_EFI_ACTION: Calling EFI Application from Boot Option")) + assert.Equal(PCR256{ + 0xdd, 0x50, 0xc8, 0xda, 0x0f, 0x89, 0x9f, 0x65, + 0x5b, 0x43, 0x05, 0xd2, 0x43, 0x86, 0x63, 0xc1, + 0xb3, 0xda, 0x6d, 0x19, 0x22, 0xa0, 0xc8, 0x22, + 0x65, 0x33, 0xac, 0x41, 0x7a, 0xbc, 0xd5, 0x23, + }, sim.Bank[4]) + + assert.Equal([]Event{ + { + PCRIndex: 0x4, Digest: Digest256(EVSeparatorPCR256()), + Data: []uint8{0x0, 0x0, 0x0, 0x0}, + Description: "EV_SEPARATOR", + }, + { + PCRIndex: 0x4, Digest: Digest256(EVEFIActionPCR256()), + Data: []uint8(nil), + Description: "EV_EFI_ACTION: Calling EFI Application from Boot Option", + }, + }, sim.EventLog.Events) +} diff --git a/image/measured-boot/measure_util.sh b/image/measured-boot/measure_util.sh deleted file mode 100644 index bfb704838..000000000 --- a/image/measured-boot/measure_util.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash -# Copyright (c) Edgeless Systems GmbH -# -# SPDX-License-Identifier: AGPL-3.0-only - -# This script contains shared functions for pcr calculation. - -set -euo pipefail -shopt -s inherit_errexit - -pcr_extend() { - local CURRENT_PCR="$1" - local EXTEND_WITH="$2" - local HASH_FUNCTION="$3" - ( - echo -n "${CURRENT_PCR}" | xxd -r -p - echo -n "${EXTEND_WITH}" | xxd -r -p - ) | ${HASH_FUNCTION} | cut -d " " -f 1 -} - -extract() { - local image="$1" - local path="$2" - local output="$3" - sudo systemd-dissect --copy-from "${image}" "${path}" "${output}" -} - -mktempdir() { - mktemp -d -} - -cleanup() { - local dir="$1" - rm -rf "${dir}" -} diff --git a/image/measured-boot/pcr-stable.json b/image/measured-boot/pcr-stable.json deleted file mode 100755 index 5530ceabc..000000000 --- a/image/measured-boot/pcr-stable.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "measurements": { - "8": { - "expected": "0000000000000000000000000000000000000000000000000000000000000000" - }, - "11": { - "expected": "0000000000000000000000000000000000000000000000000000000000000000" - }, - "13": { - "expected": "0000000000000000000000000000000000000000000000000000000000000000" - }, - "15": { - "expected": "0000000000000000000000000000000000000000000000000000000000000000" - } - } -} diff --git a/image/measured-boot/pesection/BUILD.bazel b/image/measured-boot/pesection/BUILD.bazel new file mode 100644 index 000000000..1b44204c2 --- /dev/null +++ b/image/measured-boot/pesection/BUILD.bazel @@ -0,0 +1,8 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "pesection", + srcs = ["pesection.go"], + importpath = "github.com/edgelesssys/constellation/v2/image/measured-boot/pesection", + visibility = ["//visibility:public"], +) diff --git a/image/measured-boot/pesection/pesection.go b/image/measured-boot/pesection/pesection.go new file mode 100644 index 000000000..59b849e39 --- /dev/null +++ b/image/measured-boot/pesection/pesection.go @@ -0,0 +1,24 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package pesection + +// PESection describes a PE section. +type PESection struct { + Name string + Size uint32 + Digest [32]byte + Measure bool + MeasureOrder int +} + +// NullTerminatedName returns the name of the section with a null terminator. +func (u PESection) NullTerminatedName() []byte { + if len(u.Name) > 0 && u.Name[len(u.Name)-1] == 0x00 { + return []byte(u.Name) + } + return append([]byte(u.Name), 0x00) +} diff --git a/image/measured-boot/precalculate_pcr_12.sh b/image/measured-boot/precalculate_pcr_12.sh deleted file mode 100755 index d68765aec..000000000 --- a/image/measured-boot/precalculate_pcr_12.sh +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env bash -# Copyright (c) Edgeless Systems GmbH -# -# SPDX-License-Identifier: AGPL-3.0-only - -# This script is used to precalculate the PCR[12] value for a Constellation OS image. -# PCR[12] contains the hash of the kernel command line and is measured by systemd-boot. -# This value was previously measured into PCR[8]. -# This script may produce wrong results for systemd-boot versions < 251. -# Usage: precalculate_pcr_12.sh - -set -euo pipefail -shopt -s inherit_errexit -source "$(dirname "$0")/measure_util.sh" - -get_cmdline_from_uki() { - local uki="$1" - local output="$2" - objcopy -O binary --only-section=.cmdline "${uki}" "${output}" -} - -cmdline_measure() { - local path="$1" - local tmp - tmp=$(mktemp) - # convert to utf-16le - iconv -f utf-8 -t utf-16le "${path}" -o "${tmp}" - sha256sum "${tmp}" | cut -d " " -f 1 - rm "${tmp}" -} - -write_output() { - local out="$1" - cat > "${out}" << EOF -{ - "measurements": { - "12": { - "expected": "${expected_pcr_12}" - } - }, - "cmdline": "${cmdline}", - "cmdline-sha256": "${cmdline_hash}" -} -EOF -} - -IMAGE="$1" -OUT="$2" -CSP="$3" - -DIR=$(mktempdir) -trap 'cleanup "${DIR}"' EXIT - -extract "${IMAGE}" "/efi/EFI/Linux" "${DIR}/uki" -sudo chown -R "${USER}:${USER}" "${DIR}/uki" -cp "${DIR}"/uki/*.efi "${DIR}/03-uki.efi" -get_cmdline_from_uki "${DIR}/03-uki.efi" "${DIR}/cmdline" -cmdline=$(cat "${DIR}/cmdline") - -cmdline_hash=$(cmdline_measure "${DIR}/cmdline") -cleanup "${DIR}" - -expected_pcr_12=0000000000000000000000000000000000000000000000000000000000000000 -expected_pcr_12=$(pcr_extend "${expected_pcr_12}" "${cmdline_hash}" "sha256sum") -if [[ ${CSP} == "azure" ]]; then - # Azure displays the boot menu - # triggering an extra measurement of the kernel command line. - expected_pcr_12=$(pcr_extend "${expected_pcr_12}" "${cmdline_hash}" "sha256sum") -fi - -echo "Kernel commandline: ${cmdline}" -echo "Kernel Commandline measurement ${cmdline_hash}" -echo "" -echo "Expected PCR[12]: ${expected_pcr_12}" -echo "" - -write_output "${OUT}" diff --git a/image/measured-boot/precalculate_pcr_4.sh b/image/measured-boot/precalculate_pcr_4.sh deleted file mode 100755 index c2e04535c..000000000 --- a/image/measured-boot/precalculate_pcr_4.sh +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env bash -# Copyright (c) Edgeless Systems GmbH -# -# SPDX-License-Identifier: AGPL-3.0-only - -# This script is used to precalculate the PCR[4] value for a Constellation OS image. -# Usage: precalculate_pcr_4.sh - -set -euo pipefail -shopt -s inherit_errexit -source "$(dirname "$0")/measure_util.sh" - -ev_efi_action_sha256=3d6772b4f84ed47595d72a2c4c5ffd15f5bb72c7507fe26f2aaee2c69d5633ba -ev_efi_separator_sha256=df3f619804a92fdb4057192dc43dd748ea778adc52bc498ce80524c014b81119 - -authentihash() { - local path="$1" - "$(dirname "$0")/extract_authentihash.py" "${path}" -} - -write_output() { - local out="$1" - cat > "${out}" << EOF -{ - "measurements": { - "4": { - "expected": "${expected_pcr_4}" - } - }, - "efistages": [ - { - "name": "shim", - "sha256": "${shim_authentihash}" - }, - { - "name": "systemd-boot", - "sha256": "${sd_boot_authentihash}" - }, - { - "name": "uki", - "sha256": "${uki_authentihash}" - } - ] -} -EOF -} - -DIR=$(mktempdir) -trap 'cleanup "${DIR}"' EXIT - -extract "$1" "/efi/EFI/BOOT/BOOTX64.EFI" "${DIR}/01-shim.efi" -extract "$1" "/efi/EFI/BOOT/grubx64.efi" "${DIR}/02-sd-boot.efi" -extract "$1" "/efi/EFI/Linux" "${DIR}/uki" -sudo chown -R "${USER}:${USER}" "${DIR}/uki" -cp "${DIR}"/uki/*.efi "${DIR}/03-uki.efi" - -shim_authentihash=$(authentihash "${DIR}/01-shim.efi") -sd_boot_authentihash=$(authentihash "${DIR}/02-sd-boot.efi") -uki_authentihash=$(authentihash "${DIR}/03-uki.efi") -cleanup "${DIR}" - -expected_pcr_4=0000000000000000000000000000000000000000000000000000000000000000 -expected_pcr_4=$(pcr_extend "${expected_pcr_4}" "${ev_efi_action_sha256}" "sha256sum") -expected_pcr_4=$(pcr_extend "${expected_pcr_4}" "${ev_efi_separator_sha256}" "sha256sum") -expected_pcr_4=$(pcr_extend "${expected_pcr_4}" "${shim_authentihash}" "sha256sum") -expected_pcr_4=$(pcr_extend "${expected_pcr_4}" "${sd_boot_authentihash}" "sha256sum") -expected_pcr_4=$(pcr_extend "${expected_pcr_4}" "${uki_authentihash}" "sha256sum") - -echo "Authentihashes:" -echo "Stage 1 - shim: ${shim_authentihash}" -echo "Stage 2 - sd-boot: ${sd_boot_authentihash}" -echo "Stage 3 - Unified Kernel Image (UKI): ${uki_authentihash}" -echo "" -echo "Expected PCR[4]: ${expected_pcr_4}" -echo "" - -write_output "$2" diff --git a/image/measured-boot/precalculate_pcr_9.sh b/image/measured-boot/precalculate_pcr_9.sh deleted file mode 100755 index c0ad8a869..000000000 --- a/image/measured-boot/precalculate_pcr_9.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env bash -# Copyright (c) Edgeless Systems GmbH -# -# SPDX-License-Identifier: AGPL-3.0-only - -# This script is used to precalculate the PCR[9] value for a Constellation OS image. -# PCR[9] contains the hash of the initrd and is measured by the linux kernel after loading the initrd. -# Usage: precalculate_pcr_9.sh - -set -euo pipefail -shopt -s inherit_errexit - -source "$(dirname "$0")/measure_util.sh" - -get_initrd_from_uki() { - local uki="$1" - local output="$2" - objcopy -O binary --only-section=.initrd "${uki}" "${output}" -} - -initrd_measure() { - local path="$1" - sha256sum "${path}" | cut -d " " -f 1 -} - -write_output() { - local out="$1" - cat > "${out}" << EOF -{ - "measurements": { - "9": { - "expected": "${expected_pcr_9}" - } - }, - "initrd-sha256": "${initrd_hash}" -} -EOF -} - -DIR=$(mktempdir) -trap 'cleanup "${DIR}"' EXIT - -extract "$1" "/efi/EFI/Linux" "${DIR}/uki" -sudo chown -R "${USER}:${USER}" "${DIR}/uki" -cp "${DIR}"/uki/*.efi "${DIR}/03-uki.efi" -get_initrd_from_uki "${DIR}/03-uki.efi" "${DIR}/initrd" - -initrd_hash=$(initrd_measure "${DIR}/initrd") -cleanup "${DIR}" - -expected_pcr_9=0000000000000000000000000000000000000000000000000000000000000000 -expected_pcr_9=$(pcr_extend "${expected_pcr_9}" "${initrd_hash}" "sha256sum") - -echo "Initrd measurement ${initrd_hash}" -echo "" -echo "Expected PCR[9]: ${expected_pcr_9}" -echo "" - -write_output "$2"