From 8c87bba755c81417d19f94775c9964dfb63c5e95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Wei=C3=9Fe?= <66256922+daniel-weisse@users.noreply.github.com> Date: Thu, 9 Mar 2023 11:22:58 +0100 Subject: [PATCH] Add measurement reader (#1381) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Weiße --- CMakeLists.txt | 11 ++- Dockerfile.build | 8 ++ image/Makefile | 3 + image/mkosi.conf.d/secure-boot-tpm.conf | 1 - .../system-preset/30-constellation.preset | 2 +- ...{tpm-pcrs.service => measurements.service} | 3 +- .../usr/libexec/constellation-pcrs | 6 +- internal/oid/oid.go | 4 +- measurement-reader/cmd/main.go | 45 +++++++++++ measurement-reader/internal/sorted/sorted.go | 14 ++++ measurement-reader/internal/tpm/tpm.go | 50 ++++++++++++ measurement-reader/internal/tpm/tpm_test.go | 78 +++++++++++++++++++ 12 files changed, 216 insertions(+), 9 deletions(-) rename image/mkosi.skeleton/usr/lib/systemd/system/{tpm-pcrs.service => measurements.service} (66%) create mode 100644 measurement-reader/cmd/main.go create mode 100644 measurement-reader/internal/sorted/sorted.go create mode 100644 measurement-reader/internal/tpm/tpm.go create mode 100644 measurement-reader/internal/tpm/tpm_test.go diff --git a/CMakeLists.txt b/CMakeLists.txt index d05824ea0..b26903584 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,7 +16,7 @@ set(CLI_BUILD_TAGS "" CACHE STRING "Tags passed to go build of Constellation CLI enable_testing() # -# core-os disk-mapper +# disk-mapper # add_custom_target(disk-mapper ALL DOCKER_BUILDKIT=1 docker build -o ${CMAKE_BINARY_DIR} --build-arg PROJECT_VERSION="${PROJECT_VERSION}" -f Dockerfile.build --target disk-mapper . @@ -24,6 +24,15 @@ add_custom_target(disk-mapper ALL BYPRODUCTS disk-mapper ) +# +# measurement-reader +# +add_custom_target(measurement-reader ALL + DOCKER_BUILDKIT=1 docker build -o ${CMAKE_BINARY_DIR} --build-arg PROJECT_VERSION="${PROJECT_VERSION}" -f Dockerfile.build --target measurement-reader . + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + BYPRODUCTS measurement-reader +) + # # bootstrapper # diff --git a/Dockerfile.build b/Dockerfile.build index 1aaae49bd..2257f4d65 100644 --- a/Dockerfile.build +++ b/Dockerfile.build @@ -42,6 +42,11 @@ WORKDIR /constellation/upgrade-agent/ ARG PROJECT_VERSION RUN --mount=type=cache,target=/root/.cache/go-build go build -o upgrade-agent -ldflags "-s -w -buildid='' -X github.com/edgelesssys/constellation/v2/internal/constants.versionInfo=${PROJECT_VERSION}" ./cmd/ +FROM build AS build-measurement-reader +WORKDIR /constellation/measurement-reader/ + +RUN --mount=type=cache,target=/root/.cache/go-build go build -o measurement-reader -ldflags "-s -w -buildid=''" ./cmd/ + FROM scratch AS bootstrapper COPY --from=build-bootstrapper /constellation/bootstrapper/bootstrapper / @@ -50,3 +55,6 @@ COPY --from=build-disk-mapper /constellation/disk-mapper/disk-mapper / FROM scratch AS upgrade-agent COPY --from=build-upgrade-agent /constellation/upgrade-agent/upgrade-agent / + +FROM scratch AS measurement-reader +COPY --from=build-measurement-reader /constellation/measurement-reader/measurement-reader / diff --git a/image/Makefile b/image/Makefile index 010acb7b5..dd019b1cf 100644 --- a/image/Makefile +++ b/image/Makefile @@ -5,6 +5,7 @@ BOOTSTRAPPER_BINARY ?= $(BASE_PATH)/../build/bootstrapper DISK_MAPPER_BINARY ?= $(BASE_PATH)/../build/disk-mapper UPGRADE_AGENT_BINARY ?= $(BASE_PATH)/../build/upgrade-agent DEBUGD_BINARY ?= $(BASE_PATH)/../build/debugd +MEASUREMENT_READER_BINARY ?= $(BASE_PATH)/../build/measurement-reader PKI ?= $(BASE_PATH)/pki MKOSI_EXTRA ?= $(BASE_PATH)/mkosi.extra IMAGE_VERSION ?= v0.0.0 @@ -65,9 +66,11 @@ inject-bins: $(PREBUILD_RPMS_SYSTEMD) $(PREBUILT_RPMS_AZURE) $(PREBUILT_RPMS_GCP mkdir -p $(MKOSI_EXTRA)/usr/sbin cp $(UPGRADE_AGENT_BINARY) $(MKOSI_EXTRA)/usr/bin/upgrade-agent cp $(DISK_MAPPER_BINARY) $(MKOSI_EXTRA)/usr/sbin/disk-mapper + cp $(MEASUREMENT_READER_BINARY) $(MKOSI_EXTRA)/usr/sbin/measurement-reader if [ "$(DEBUG)" = "true" ]; then \ cp $(DEBUGD_BINARY) $(MKOSI_EXTRA)/usr/bin/debugd; \ rm -f $(MKOSI_EXTRA)/usr/bin/bootstrapper; \ + rm -f $(MKOSI_EXTRA)/usr/bin/upgrade-agent; \ else \ cp $(BOOTSTRAPPER_BINARY) $(MKOSI_EXTRA)/usr/bin/bootstrapper; \ rm -f $(MKOSI_EXTRA)/usr/bin/debugd; \ diff --git a/image/mkosi.conf.d/secure-boot-tpm.conf b/image/mkosi.conf.d/secure-boot-tpm.conf index fb52e6d5e..b72e0c47c 100644 --- a/image/mkosi.conf.d/secure-boot-tpm.conf +++ b/image/mkosi.conf.d/secure-boot-tpm.conf @@ -5,4 +5,3 @@ Packages= sbsigntools, efitools, mokutil, - tpm2-tools, diff --git a/image/mkosi.skeleton/usr/lib/systemd/system-preset/30-constellation.preset b/image/mkosi.skeleton/usr/lib/systemd/system-preset/30-constellation.preset index 8af180f88..d969344af 100644 --- a/image/mkosi.skeleton/usr/lib/systemd/system-preset/30-constellation.preset +++ b/image/mkosi.skeleton/usr/lib/systemd/system-preset/30-constellation.preset @@ -4,5 +4,5 @@ enable constellation-bootstrapper.service enable containerd.service enable kubelet.service enable systemd-networkd.service -enable tpm-pcrs.service +enable measurements.service enable export_constellation_debug.service diff --git a/image/mkosi.skeleton/usr/lib/systemd/system/tpm-pcrs.service b/image/mkosi.skeleton/usr/lib/systemd/system/measurements.service similarity index 66% rename from image/mkosi.skeleton/usr/lib/systemd/system/tpm-pcrs.service rename to image/mkosi.skeleton/usr/lib/systemd/system/measurements.service index 87800121c..e99020e3a 100644 --- a/image/mkosi.skeleton/usr/lib/systemd/system/tpm-pcrs.service +++ b/image/mkosi.skeleton/usr/lib/systemd/system/measurements.service @@ -1,10 +1,11 @@ [Unit] -Description=Print PCR state on startup +Description=Print image measurements on startup Before=constellation-bootstrapper.service [Service] Type=oneshot RemainAfterExit=yes +EnvironmentFile=/run/constellation.env ExecStart=/usr/libexec/constellation-pcrs [Install] diff --git a/image/mkosi.skeleton/usr/libexec/constellation-pcrs b/image/mkosi.skeleton/usr/libexec/constellation-pcrs index ad942d8ac..1d9abc3a7 100755 --- a/image/mkosi.skeleton/usr/libexec/constellation-pcrs +++ b/image/mkosi.skeleton/usr/libexec/constellation-pcrs @@ -3,12 +3,12 @@ # # SPDX-License-Identifier: AGPL-3.0-only -# This script reads the PCR state of the system +# This script reads the measurements of the system # and prints the message to the serial console main() { - pcr_state="$(tpm2_pcrread sha256)" - echo -e "PCR state:\n${pcr_state}\n" > /run/issue.d/35_constellation_pcrs.issue + pcr_state="$(/usr/sbin/measurement-reader)" + echo -e "${pcr_state}\n" > /run/issue.d/35_constellation_pcrs.issue } main diff --git a/internal/oid/oid.go b/internal/oid/oid.go index a2df0a71b..8ad94bb0a 100644 --- a/internal/oid/oid.go +++ b/internal/oid/oid.go @@ -25,7 +25,7 @@ package oid import ( "encoding/asn1" - "errors" + "fmt" ) // Getter returns an ASN.1 Object Identifier. @@ -49,7 +49,7 @@ func FromString(oid string) (Getter, error) { case qemuVTPM: return QEMUVTPM{}, nil } - return nil, errors.New("unknown OID") + return nil, fmt.Errorf("unknown OID: %q", oid) } // Dummy OID for testing. diff --git a/measurement-reader/cmd/main.go b/measurement-reader/cmd/main.go new file mode 100644 index 000000000..83754dfb5 --- /dev/null +++ b/measurement-reader/cmd/main.go @@ -0,0 +1,45 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package main + +import ( + "fmt" + "os" + + "github.com/edgelesssys/constellation/v2/internal/constants" + "github.com/edgelesssys/constellation/v2/internal/logger" + "github.com/edgelesssys/constellation/v2/internal/oid" + "github.com/edgelesssys/constellation/v2/measurement-reader/internal/sorted" + "github.com/edgelesssys/constellation/v2/measurement-reader/internal/tpm" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +func main() { + log := logger.New(logger.JSONLog, zapcore.InfoLevel) + variant := os.Getenv(constants.AttestationVariant) + attestationVariant, err := oid.FromString(variant) + if err != nil { + log.With(zap.Error(err)).Fatalf("Failed to parse attestation variant") + } + + var m []sorted.Measurement + switch attestationVariant { + case oid.AWSNitroTPM{}, oid.AzureSEVSNP{}, oid.AzureTrustedLaunch{}, oid.GCPSEVES{}, oid.QEMUVTPM{}: + m, err = tpm.Measurements() + if err != nil { + log.With(zap.Error(err)).Fatalf("Failed to read TPM measurements") + } + default: + log.With(zap.String("attestationVariant", variant)).Fatalf("Unsupported attestation variant") + } + + fmt.Println("Measurements:") + for _, measurement := range m { + fmt.Printf("\t%s : 0x%0X\n", measurement.Index, measurement.Value) + } +} diff --git a/measurement-reader/internal/sorted/sorted.go b/measurement-reader/internal/sorted/sorted.go new file mode 100644 index 000000000..523103913 --- /dev/null +++ b/measurement-reader/internal/sorted/sorted.go @@ -0,0 +1,14 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +// Type definition for sorted measurements. +package sorted + +// Measurement wraps a measurement custom index and value. +type Measurement struct { + Index string + Value []byte +} diff --git a/measurement-reader/internal/tpm/tpm.go b/measurement-reader/internal/tpm/tpm.go new file mode 100644 index 000000000..4bfe91754 --- /dev/null +++ b/measurement-reader/internal/tpm/tpm.go @@ -0,0 +1,50 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +// Package tpm reads measurements from a TPM. +package tpm + +import ( + "fmt" + "sort" + + "github.com/edgelesssys/constellation/v2/internal/attestation/measurements" + "github.com/edgelesssys/constellation/v2/internal/attestation/vtpm" + "github.com/edgelesssys/constellation/v2/measurement-reader/internal/sorted" + tpmClient "github.com/google/go-tpm-tools/client" + "github.com/google/go-tpm/tpm2" +) + +// Measurements returns a sorted list of TPM PCR measurements. +func Measurements() ([]sorted.Measurement, error) { + m, err := vtpm.GetSelectedMeasurements(vtpm.OpenVTPM, tpmClient.FullPcrSel(tpm2.AlgSHA256)) + if err != nil { + return nil, err + } + + return sortMeasurements(m), nil +} + +func sortMeasurements(m measurements.M) []sorted.Measurement { + keys := make([]uint32, 0, len(m)) + for idx := range m { + keys = append(keys, idx) + } + sort.Slice(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + + var measurements []sorted.Measurement + for _, idx := range keys { + expected := m[idx].Expected + measurements = append(measurements, sorted.Measurement{ + Index: fmt.Sprintf("PCR[%02d]", idx), + Value: expected[:], + }) + } + + return measurements +} diff --git a/measurement-reader/internal/tpm/tpm_test.go b/measurement-reader/internal/tpm/tpm_test.go new file mode 100644 index 000000000..3206b4b0e --- /dev/null +++ b/measurement-reader/internal/tpm/tpm_test.go @@ -0,0 +1,78 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package tpm + +import ( + "bytes" + "testing" + + "github.com/edgelesssys/constellation/v2/internal/attestation/measurements" + "github.com/edgelesssys/constellation/v2/measurement-reader/internal/sorted" + "github.com/stretchr/testify/assert" +) + +func TestSortMeasurements(t *testing.T) { + testCases := map[string]struct { + input measurements.M + want []sorted.Measurement + }{ + "pre sorted": { + input: measurements.M{ + 0: measurements.WithAllBytes(0x11, false), + 1: measurements.WithAllBytes(0x22, false), + 2: measurements.WithAllBytes(0x33, false), + }, + want: []sorted.Measurement{ + { + Index: "PCR[00]", + Value: bytes.Repeat([]byte{0x11}, 32), + }, + { + Index: "PCR[01]", + Value: bytes.Repeat([]byte{0x22}, 32), + }, + { + Index: "PCR[02]", + Value: bytes.Repeat([]byte{0x33}, 32), + }, + }, + }, + "unsorted": { + input: measurements.M{ + 1: measurements.WithAllBytes(0x22, false), + 0: measurements.WithAllBytes(0x11, false), + 2: measurements.WithAllBytes(0x33, false), + }, + want: []sorted.Measurement{ + { + Index: "PCR[00]", + Value: bytes.Repeat([]byte{0x11}, 32), + }, + { + Index: "PCR[01]", + Value: bytes.Repeat([]byte{0x22}, 32), + }, + { + Index: "PCR[02]", + Value: bytes.Repeat([]byte{0x33}, 32), + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + got := sortMeasurements(tc.input) + for i := range got { + assert.Equal(got[i].Index, tc.want[i].Index) + assert.Equal(got[i].Value, tc.want[i].Value) + } + }) + } +}