diff --git a/CODEOWNERS b/CODEOWNERS index afebd3b12..ef345aaa1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -17,9 +17,6 @@ /hack/check-licenses.sh @thomasten /hack/clidocgen @thomasten /hack/fetch-broken-e2e @katexochen -/hack/image-measurement @daniel-weisse -/hack/pcr-compare @nirusu -/hack/pcr-reader @daniel-weisse /hack/pseudo-version @malt3 /hack/qemu-metadata-api @daniel-weisse /hack/remove-tf-providers @katexochen diff --git a/dev-docs/workflows/build-test-run.md b/dev-docs/workflows/build-test-run.md index 0963609f0..f538a4ea3 100644 --- a/dev-docs/workflows/build-test-run.md +++ b/dev-docs/workflows/build-test-run.md @@ -82,22 +82,7 @@ Instructions on how to set it up can be found in the [QEMU README](qemu.md). In order to verify your cluster we describe a [verification workflow](https://docs.edgeless.systems/constellation/workflows/verify-cluster) in our official docs. Apart from that you can also reproduce some of the measurements described in the [docs](https://docs.edgeless.systems/constellation/architecture/attestation#runtime-measurements) locally. -To do so we built a tool that creates a VM, collects the PCR values and reports them to you. -To run the tool execute the following command in `/hack/image-measurement`: - -```sh -go run . -path -type -``` - -`` needs to point to a valid image file. -The image can be either in raw or QEMU's `qcow2` format. -This format is specified in the `` argument. - -You can compare the values of PCR 4, 8 and 9 to the ones you are seeing in your `constellation-conf.yaml`. -The PCR values depend on the image you specify in the `path` argument. -Therefore, if you want to verify a cluster deployed with a release image you will have to download the images first. - -After collecting the measurements you can put them into your `constellation-conf.yaml` under the `measurements` key in order to enforce them. +Use the provided scripts in `/image/measured-boot` to generated measurements for a built image. Measurements for release images are also available in our image API. # Dependency management diff --git a/disk-mapper/cmd/BUILD.bazel b/disk-mapper/cmd/BUILD.bazel index f39360c6f..93e87730a 100644 --- a/disk-mapper/cmd/BUILD.bazel +++ b/disk-mapper/cmd/BUILD.bazel @@ -25,8 +25,6 @@ go_library( "//internal/logger", "//internal/oid", "//internal/role", - "@com_github_google_go_tpm//tpm2", - "@com_github_google_go_tpm_tools//client", "@com_github_spf13_afero//:afero", "@org_uber_go_zap//:zap", ], diff --git a/disk-mapper/cmd/main.go b/disk-mapper/cmd/main.go index a610be2ff..f02bbee04 100644 --- a/disk-mapper/cmd/main.go +++ b/disk-mapper/cmd/main.go @@ -7,13 +7,9 @@ SPDX-License-Identifier: AGPL-3.0-only package main import ( - "bytes" "context" - "encoding/json" "flag" "net" - "net/http" - "net/url" "os" "path/filepath" @@ -36,8 +32,6 @@ import ( "github.com/edgelesssys/constellation/v2/internal/logger" "github.com/edgelesssys/constellation/v2/internal/oid" "github.com/edgelesssys/constellation/v2/internal/role" - tpmClient "github.com/google/go-tpm-tools/client" - "github.com/google/go-tpm/tpm2" "github.com/spf13/afero" "go.uber.org/zap" ) @@ -78,7 +72,6 @@ func main() { // using udev rules, a symlink for our disk is created at /dev/sdb diskPath, err = filepath.EvalSymlinks(awsStateDiskPath) if err != nil { - _ = exportPCRs() log.With(zap.Error(err)).Fatalf("Unable to resolve Azure state disk path") } metadataClient, err = awscloud.New(context.Background()) @@ -89,7 +82,6 @@ func main() { case cloudprovider.Azure: diskPath, err = filepath.EvalSymlinks(azureStateDiskPath) if err != nil { - _ = exportPCRs() log.With(zap.Error(err)).Fatalf("Unable to resolve Azure state disk path") } metadataClient, err = azurecloud.New(context.Background()) @@ -100,7 +92,6 @@ func main() { case cloudprovider.GCP: diskPath, err = filepath.EvalSymlinks(gcpStateDiskPath) if err != nil { - _ = exportPCRs() log.With(zap.Error(err)).Fatalf("Unable to resolve GCP state disk path") } gcpMeta, err := gcpcloud.New(context.Background()) @@ -116,12 +107,10 @@ func main() { if err != nil { log.With(zap.Error).Fatalf("Failed to create OpenStack metadata client") } - _ = exportPCRs() case cloudprovider.QEMU: diskPath = qemuStateDiskPath metadataClient = qemucloud.New() - _ = exportPCRs() default: log.Fatalf("CSP %s is not supported by Constellation", *csp) @@ -179,38 +168,3 @@ func main() { log.With(zap.Error(err)).Fatalf("Failed to prepare state disk") } } - -// exportPCRs tries to export the node's PCRs to QEMU's metadata API. -// This function is called when an Azure or GCP image boots, but is unable to find a state disk. -// This happens when we boot such an image in QEMU. -// We can use this to calculate the PCRs of the image locally. -func exportPCRs() error { - // get TPM state - pcrs, err := vtpm.GetSelectedMeasurements(vtpm.OpenVTPM, tpmClient.FullPcrSel(tpm2.AlgSHA256)) - if err != nil { - return err - } - pcrsPretty, err := json.Marshal(pcrs) - if err != nil { - return err - } - - // send PCRs to metadata API - url := &url.URL{ - Scheme: "http", - Host: "10.42.0.1:8080", // QEMU metadata endpoint - Path: "/pcrs", - } - req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, url.String(), bytes.NewBuffer(pcrsPretty)) - if err != nil { - return err - } - req.Header.Set("Content-Type", "application/json") - resp, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - return nil -} diff --git a/hack/go.mod b/hack/go.mod index 3f29eae0e..3cafd160c 100644 --- a/hack/go.mod +++ b/hack/go.mod @@ -38,19 +38,13 @@ replace ( require ( github.com/edgelesssys/constellation/v2 v2.5.2 - github.com/fatih/color v1.14.1 github.com/go-git/go-git/v5 v5.5.2 - github.com/google/go-tpm-tools v0.3.10 github.com/spf13/cobra v1.6.1 github.com/stretchr/testify v1.8.2 - go.uber.org/goleak v1.2.1 go.uber.org/zap v1.24.0 golang.org/x/mod v0.8.0 - google.golang.org/grpc v1.53.0 gopkg.in/square/go-jose.v2 v2.6.0 - gopkg.in/yaml.v3 v3.0.1 libvirt.org/go/libvirt v1.8010.0 - libvirt.org/go/libvirtxml v1.8009.0 ) require ( @@ -112,6 +106,7 @@ require ( github.com/emirpasic/gods v1.18.1 // indirect 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.14.1 // indirect github.com/go-chi/chi v4.1.2+incompatible // indirect github.com/go-errors/errors v1.4.2 // indirect github.com/go-git/gcfg v1.5.0 // indirect @@ -143,6 +138,7 @@ require ( github.com/google/go-containerregistry v0.13.0 // indirect github.com/google/go-sev-guest v0.4.1 // indirect github.com/google/go-tpm v0.3.3 // indirect + github.com/google/go-tpm-tools v0.3.10 // indirect github.com/google/go-tspi v0.3.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/logger v1.1.1 // indirect @@ -255,10 +251,12 @@ require ( google.golang.org/api v0.110.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc // indirect + google.golang.org/grpc v1.53.0 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect helm.sh/helm v2.17.0+incompatible // indirect helm.sh/helm/v3 v3.11.1 // indirect k8s.io/api v0.26.2 // indirect diff --git a/hack/go.sum b/hack/go.sum index 03e19fe78..7529644e7 100644 --- a/hack/go.sum +++ b/hack/go.sum @@ -1369,7 +1369,6 @@ go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= -go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.4.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= @@ -2043,8 +2042,6 @@ k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5 h1:kmDqav+P+/5e1i9tFfHq1qcF3sOrD k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= libvirt.org/go/libvirt v1.8010.0 h1:q4UeuyzSp7GiYUB5T3ytzeXh41hp9JqNQu150NkO+7A= libvirt.org/go/libvirt v1.8010.0/go.mod h1:1WiFE8EjZfq+FCVog+rvr1yatKbKZ9FaFMZgEqxEJqQ= -libvirt.org/go/libvirtxml v1.8009.0 h1:29wyVe4m08S3JBSJnMJ/0WCWWcq8Xl5zF5NIa2RtXdI= -libvirt.org/go/libvirtxml v1.8009.0/go.mod h1:7Oq2BLDstLr/XtoQD8Fr3mfDNrzlI3utYKySXF2xkng= oras.land/oras-go v1.2.2 h1:0E9tOHUfrNH7TCDk5KU0jVBEzCqbfdyuVfGmJ7ZeRPE= oras.land/oras-go v1.2.2/go.mod h1:Apa81sKoZPpP7CDciE006tSZ0x3Q3+dOoBcMZ/aNxvw= pack.ag/amqp v0.11.2/go.mod h1:4/cbmt4EJXSKlG6LCfWHoqmN0uFdy5i/+YFz+fTfhV4= diff --git a/hack/image-measurement/BUILD.bazel b/hack/image-measurement/BUILD.bazel deleted file mode 100644 index 8adb32c56..000000000 --- a/hack/image-measurement/BUILD.bazel +++ /dev/null @@ -1,27 +0,0 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") - -go_library( - name = "image-measurement_lib", - srcs = [ - "definitions.go", - "main.go", - ], - importpath = "github.com/edgelesssys/constellation/v2/hack/image-measurement", - visibility = ["//visibility:private"], - deps = [ - "//hack/image-measurement/server", - "//internal/attestation/measurements", - "//internal/logger", - "@in_gopkg_yaml_v3//:yaml_v3", - "@org_libvirt_go_libvirt//:libvirt", - "@org_libvirt_go_libvirtxml//:libvirtxml", - "@org_uber_go_zap//:zap", - "@org_uber_go_zap//zapcore", - ], -) - -go_binary( - name = "image-measurement", - embed = [":image-measurement_lib"], - visibility = ["//visibility:public"], -) diff --git a/hack/image-measurement/definitions.go b/hack/image-measurement/definitions.go deleted file mode 100644 index e5bd63aaa..000000000 --- a/hack/image-measurement/definitions.go +++ /dev/null @@ -1,317 +0,0 @@ -/* -Copyright (c) Edgeless Systems GmbH - -SPDX-License-Identifier: AGPL-3.0-only -*/ - -package main - -import ( - "libvirt.org/go/libvirtxml" -) - -var ( - libvirtImagePath = "/var/lib/libvirt/images/" - baseDiskName = "constellation-measurement" - stateDiskName = "constellation-measurement-state" - bootDiskName = "constellation-measurement-boot" - diskPoolName = "constellation-measurement-pool" - domainName = "constellation-measurement-vm" - networkName = "constellation-measurement-net" - - networkXMLConfig = libvirtxml.Network{ - Name: networkName, - Forward: &libvirtxml.NetworkForward{ - Mode: "nat", - NAT: &libvirtxml.NetworkForwardNAT{ - Ports: []libvirtxml.NetworkForwardNATPort{ - { - Start: 1024, - End: 65535, - }, - }, - }, - }, - Bridge: &libvirtxml.NetworkBridge{ - Name: "virbr1", - STP: "on", - Delay: "0", - }, - DNS: &libvirtxml.NetworkDNS{ - Enable: "yes", - }, - IPs: []libvirtxml.NetworkIP{ - { - Family: "ipv4", - Address: "10.42.0.1", - Prefix: 16, - DHCP: &libvirtxml.NetworkDHCP{ - Ranges: []libvirtxml.NetworkDHCPRange{ - { - Start: "10.42.0.2", - End: "10.42.255.254", - }, - }, - }, - }, - }, - } - poolXMLConfig = libvirtxml.StoragePool{ - Name: diskPoolName, - Type: "dir", - Source: &libvirtxml.StoragePoolSource{}, - Target: &libvirtxml.StoragePoolTarget{ - Path: libvirtImagePath, - Permissions: &libvirtxml.StoragePoolTargetPermissions{ - Owner: "0", - Group: "0", - Mode: "0711", - }, - }, - } - volumeBootXMLConfig = libvirtxml.StorageVolume{ - Type: "file", - Name: bootDiskName, - Target: &libvirtxml.StorageVolumeTarget{ - Path: libvirtImagePath + bootDiskName, - Format: &libvirtxml.StorageVolumeTargetFormat{ - Type: "qcow2", - }, - }, - BackingStore: &libvirtxml.StorageVolumeBackingStore{ - Path: libvirtImagePath + baseDiskName, - Format: &libvirtxml.StorageVolumeTargetFormat{}, - }, - Capacity: &libvirtxml.StorageVolumeSize{ - Unit: "GiB", - Value: uint64(10), - }, - } - - volumeBaseXMLConfig = libvirtxml.StorageVolume{ - Type: "file", - Name: baseDiskName, - Target: &libvirtxml.StorageVolumeTarget{ - Path: libvirtImagePath + baseDiskName, - Format: &libvirtxml.StorageVolumeTargetFormat{ - Type: "qcow2", - }, - }, - Capacity: &libvirtxml.StorageVolumeSize{ - Unit: "GiB", - Value: uint64(10), - }, - } - - volumeStateXMLConfig = libvirtxml.StorageVolume{ - Type: "file", - Name: stateDiskName, - Target: &libvirtxml.StorageVolumeTarget{ - Path: libvirtImagePath + stateDiskName, - Format: &libvirtxml.StorageVolumeTargetFormat{ - Type: "qcow2", - }, - }, - Capacity: &libvirtxml.StorageVolumeSize{ - Unit: "GiB", - Value: uint64(10), - }, - } - - port = uint(0) - domainXMLConfig = libvirtxml.Domain{ - Title: "measurement-VM", - Name: domainName, - Type: "kvm", - Memory: &libvirtxml.DomainMemory{ - Value: 2, - Unit: "GiB", - }, - Resource: &libvirtxml.DomainResource{ - Partition: "/machine", - }, - VCPU: &libvirtxml.DomainVCPU{ - Placement: "static", - Current: 2, - Value: 2, - }, - CPU: &libvirtxml.DomainCPU{ - Mode: "custom", - Model: &libvirtxml.DomainCPUModel{ - Fallback: "forbid", - Value: "qemu64", - }, - Features: []libvirtxml.DomainCPUFeature{ - { - Policy: "require", - Name: "x2apic", - }, - { - Policy: "require", - Name: "hypervisor", - }, - { - Policy: "require", - Name: "lahf_lm", - }, - { - Policy: "disable", - Name: "svm", - }, - }, - }, - Features: &libvirtxml.DomainFeatureList{ - ACPI: &libvirtxml.DomainFeature{}, - PAE: &libvirtxml.DomainFeature{}, - SMM: &libvirtxml.DomainFeatureSMM{ - State: "on", - }, - APIC: &libvirtxml.DomainFeatureAPIC{}, - }, - - OS: &libvirtxml.DomainOS{ - // If Firmware is set, Loader and NVRam will be chosen automatically - Firmware: "efi", - Type: &libvirtxml.DomainOSType{ - Arch: "x86_64", - Machine: "q35", - Type: "hvm", - }, - BootDevices: []libvirtxml.DomainBootDevice{ - { - Dev: "hd", - }, - }, - }, - Devices: &libvirtxml.DomainDeviceList{ - Emulator: "/usr/bin/qemu-system-x86_64", - Disks: []libvirtxml.DomainDisk{ - { - Device: "disk", - Driver: &libvirtxml.DomainDiskDriver{ - Name: "qemu", - Type: "qcow2", - }, - Target: &libvirtxml.DomainDiskTarget{ - Dev: "sda", - Bus: "scsi", - }, - Source: &libvirtxml.DomainDiskSource{ - Index: 2, - Volume: &libvirtxml.DomainDiskSourceVolume{ - Pool: diskPoolName, - Volume: bootDiskName, - }, - }, - }, - { - Device: "disk", - Driver: &libvirtxml.DomainDiskDriver{ - Name: "qemu", - }, - Target: &libvirtxml.DomainDiskTarget{ - Dev: "vda", - Bus: "virtio", - }, - Source: &libvirtxml.DomainDiskSource{ - Index: 1, - Volume: &libvirtxml.DomainDiskSourceVolume{ - Pool: diskPoolName, - Volume: stateDiskName, - }, - }, - Alias: &libvirtxml.DomainAlias{ - Name: "virtio-disk1", - }, - }, - }, - Controllers: []libvirtxml.DomainController{ - { - Type: "scsi", - Model: "virtio-scsi", - }, - }, - TPMs: []libvirtxml.DomainTPM{ - { - Model: "tpm-tis", - Backend: &libvirtxml.DomainTPMBackend{ - Emulator: &libvirtxml.DomainTPMBackendEmulator{ - Version: "2.0", - ActivePCRBanks: &libvirtxml.DomainTPMBackendPCRBanks{ - SHA1: &libvirtxml.DomainTPMBackendPCRBank{}, - SHA256: &libvirtxml.DomainTPMBackendPCRBank{}, - SHA384: &libvirtxml.DomainTPMBackendPCRBank{}, - SHA512: &libvirtxml.DomainTPMBackendPCRBank{}, - }, - }, - }, - }, - }, - Interfaces: []libvirtxml.DomainInterface{ - { - Model: &libvirtxml.DomainInterfaceModel{ - Type: "virtio", - }, - Source: &libvirtxml.DomainInterfaceSource{ - Network: &libvirtxml.DomainInterfaceSourceNetwork{ - Network: networkName, - Bridge: "virbr1", - }, - }, - Alias: &libvirtxml.DomainAlias{ - Name: "net0", - }, - }, - }, - Serials: []libvirtxml.DomainSerial{ - { - Source: &libvirtxml.DomainChardevSource{ - Pty: &libvirtxml.DomainChardevSourcePty{ - Path: "/dev/pts/4", - }, - }, - Target: &libvirtxml.DomainSerialTarget{ - Type: "isa-serial", - Port: &port, - Model: &libvirtxml.DomainSerialTargetModel{ - Name: "isa-serial", - }, - }, - Log: &libvirtxml.DomainChardevLog{ - File: "/tmp/libvirt.log", - }, - }, - }, - Consoles: []libvirtxml.DomainConsole{ - { - TTY: "/dev/pts/4", - Source: &libvirtxml.DomainChardevSource{ - Pty: &libvirtxml.DomainChardevSourcePty{ - Path: "/dev/pts/4", - }, - }, - Target: &libvirtxml.DomainConsoleTarget{ - Type: "serial", - Port: &port, - }, - }, - }, - RNGs: []libvirtxml.DomainRNG{ - { - Model: "virtio", - Backend: &libvirtxml.DomainRNGBackend{ - Random: &libvirtxml.DomainRNGBackendRandom{ - Device: "/dev/urandom", - }, - }, - Alias: &libvirtxml.DomainAlias{ - Name: "rng0", - }, - }, - }, - }, - OnPoweroff: "destroy", - OnCrash: "destroy", - OnReboot: "restart", - } -) diff --git a/hack/image-measurement/main.go b/hack/image-measurement/main.go deleted file mode 100644 index 9fac91a4d..000000000 --- a/hack/image-measurement/main.go +++ /dev/null @@ -1,373 +0,0 @@ -/* -Copyright (c) Edgeless Systems GmbH - -SPDX-License-Identifier: AGPL-3.0-only -*/ - -package main - -import ( - "errors" - "flag" - "fmt" - "io" - "net/http" - "os" - "os/signal" - "syscall" - - "github.com/edgelesssys/constellation/v2/hack/image-measurement/server" - "github.com/edgelesssys/constellation/v2/internal/attestation/measurements" - "github.com/edgelesssys/constellation/v2/internal/logger" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" - "gopkg.in/yaml.v3" - "libvirt.org/go/libvirt" -) - -// Usage: -// go build -//./image-measurement --path=disk.raw --type=raw - -type libvirtInstance struct { - conn *libvirt.Connect - log *logger.Logger - imagePath string -} - -func (l *libvirtInstance) uploadBaseImage(baseVolume *libvirt.StorageVol) (retErr error) { - stream, err := l.conn.NewStream(libvirt.STREAM_NONBLOCK) - if err != nil { - return err - } - defer func() { _ = stream.Free() }() - file, err := os.Open(l.imagePath) - if err != nil { - return fmt.Errorf("error while opening %s: %s", l.imagePath, err) - } - defer func() { - retErr = errors.Join(err, file.Close()) - }() - - fi, err := file.Stat() - if err != nil { - return err - } - if err := baseVolume.Upload(stream, 0, uint64(fi.Size()), 0); err != nil { - return err - } - transferredBytes := 0 - buffer := make([]byte, 4*1024*1024) - for { - _, err := file.Read(buffer) - if err != nil && err != io.EOF { - return err - } - if err == io.EOF { - break - } - num, err := stream.Send(buffer) - if err != nil { - return err - } - transferredBytes += num - - } - if transferredBytes < int(fi.Size()) { - return fmt.Errorf("only send %d out of %d bytes", transferredBytes, fi.Size()) - } - return nil -} - -func (l *libvirtInstance) createLibvirtInstance() error { - domainXMLString, err := domainXMLConfig.Marshal() - if err != nil { - return err - } - poolXMLString, err := poolXMLConfig.Marshal() - if err != nil { - return err - } - volumeBootXMLString, err := volumeBootXMLConfig.Marshal() - if err != nil { - return err - } - volumeBaseXMLString, err := volumeBaseXMLConfig.Marshal() - if err != nil { - return err - } - volumeStateXMLString, err := volumeStateXMLConfig.Marshal() - if err != nil { - return err - } - networkXMLString, err := networkXMLConfig.Marshal() - if err != nil { - return err - } - l.log.Infof("creating network") - network, err := l.conn.NetworkCreateXML(networkXMLString) - if err != nil { - return err - } - defer func() { _ = network.Free() }() - - l.log.Infof("creating storage pool") - poolObject, err := l.conn.StoragePoolDefineXML(poolXMLString, libvirt.STORAGE_POOL_DEFINE_VALIDATE) - if err != nil { - return fmt.Errorf("error defining libvirt storage pool: %s", err) - } - defer func() { _ = poolObject.Free() }() - if err := poolObject.Build(libvirt.STORAGE_POOL_BUILD_NEW); err != nil { - return fmt.Errorf("error building libvirt storage pool: %s", err) - } - if err := poolObject.Create(libvirt.STORAGE_POOL_CREATE_NORMAL); err != nil { - return fmt.Errorf("error creating libvirt storage pool: %s", err) - } - volumeBaseObject, err := poolObject.StorageVolCreateXML(volumeBaseXMLString, 0) - if err != nil { - return fmt.Errorf("error creating libvirt storage volume 'base': %s", err) - } - defer func() { _ = volumeBaseObject.Free() }() - - l.log.Infof("uploading image to libvirt") - if err := l.uploadBaseImage(volumeBaseObject); err != nil { - return err - } - - l.log.Infof("creating storage volume 'boot'") - bootVol, err := poolObject.StorageVolCreateXML(volumeBootXMLString, 0) - if err != nil { - return fmt.Errorf("error creating libvirt storage volume 'boot': %s", err) - } - defer func() { _ = bootVol.Free() }() - - l.log.Infof("creating storage volume 'state'") - stateVol, err := poolObject.StorageVolCreateXML(volumeStateXMLString, 0) - if err != nil { - return fmt.Errorf("error creating libvirt storage volume 'state': %s", err) - } - defer func() { _ = stateVol.Free() }() - - l.log.Infof("creating domain") - domain, err := l.conn.DomainCreateXML(domainXMLString, libvirt.DOMAIN_NONE) - if err != nil { - return fmt.Errorf("error creating libvirt domain: %s", err) - } - defer func() { _ = domain.Free() }() - return nil -} - -func (l *libvirtInstance) deleteNetwork() error { - nets, err := l.conn.ListAllNetworks(libvirt.CONNECT_LIST_NETWORKS_ACTIVE) - if err != nil { - return err - } - defer func() { - for _, net := range nets { - _ = net.Free() - } - }() - for _, net := range nets { - name, err := net.GetName() - if err != nil { - return err - } - if name != networkName { - continue - } - if err := net.Destroy(); err != nil { - return err - } - } - return nil -} - -func (l *libvirtInstance) deleteDomain() error { - doms, err := l.conn.ListAllDomains(libvirt.CONNECT_LIST_DOMAINS_ACTIVE) - if err != nil { - return err - } - defer func() { - for _, dom := range doms { - _ = dom.Free() - } - }() - for _, dom := range doms { - name, err := dom.GetName() - if err != nil { - return err - } - if name != domainName { - continue - } - if err := dom.Destroy(); err != nil { - return err - } - } - return nil -} - -func (l *libvirtInstance) deleteVolume(pool *libvirt.StoragePool) error { - volumes, err := pool.ListAllStorageVolumes(0) - if err != nil { - return err - } - defer func() { - for _, volume := range volumes { - _ = volume.Free() - } - }() - for _, volume := range volumes { - name, err := volume.GetName() - if err != nil { - return err - } - if name != stateDiskName && name != bootDiskName && name != baseDiskName { - continue - } - if err := volume.Delete(libvirt.STORAGE_VOL_DELETE_NORMAL); err != nil { - return err - } - } - return nil -} - -func (l *libvirtInstance) deletePool() error { - pools, err := l.conn.ListAllStoragePools(libvirt.CONNECT_LIST_STORAGE_POOLS_DIR) - if err != nil { - return err - } - defer func() { - for _, pool := range pools { - _ = pool.Free() - } - }() - for _, pool := range pools { - name, err := pool.GetName() - if err != nil { - return err - } - if name != diskPoolName { - continue - } - active, err := pool.IsActive() - if err != nil { - return err - } - if active { - if err := l.deleteVolume(&pool); err != nil { - return err - } - if err := pool.Destroy(); err != nil { - return err - } - if err := pool.Delete(libvirt.STORAGE_POOL_DELETE_NORMAL); err != nil { - return err - } - } - // If something fails and the Pool becomes inactive, we cannot delete/destroy it anymore. - // We have to undefine it in this case - if err := pool.Undefine(); err != nil { - return err - } - } - return nil -} - -func (l *libvirtInstance) deleteLibvirtInstance() error { - var err error - err = errors.Join(err, l.deleteNetwork()) - err = errors.Join(err, l.deleteDomain()) - err = errors.Join(err, l.deletePool()) - return err -} - -func (l *libvirtInstance) obtainMeasurements() (measurements measurements.M, retErr error) { - // sanity check - if err := l.deleteLibvirtInstance(); err != nil { - return nil, err - } - done := make(chan struct{}, 1) - serv := server.New(l.log, done) - go func() { - if err := serv.ListenAndServe("8080"); err != http.ErrServerClosed { - l.log.With(zap.Error(err)).Fatalf("Failed to serve") - } - }() - defer func() { - retErr = errors.Join(retErr, l.deleteLibvirtInstance()) - }() - if err := l.createLibvirtInstance(); err != nil { - return nil, err - } - sigs := make(chan os.Signal, 1) - signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) - l.log.Infof("waiting for PCRs or CTRL+C") - select { - case <-done: - break - case <-sigs: - break - } - signal.Stop(sigs) - close(sigs) - if err := serv.Shutdown(); err != nil { - return nil, err - } - close(done) - - return serv.GetMeasurements(), nil -} - -func main() { - var imageLocation, imageType, outFile string - var verboseLog bool - flag.StringVar(&imageLocation, "path", "", "path to the image to measure (required)") - flag.StringVar(&imageType, "type", "", "type of the image. One of 'qcow2' or 'raw' (required)") - flag.StringVar(&outFile, "file", "-", "path to output file, or '-' for stdout") - flag.BoolVar(&verboseLog, "v", false, "verbose logging") - - flag.Parse() - log := logger.New(logger.JSONLog, zapcore.DebugLevel) - if !verboseLog { - log = log.WithIncreasedLevel(zapcore.FatalLevel) // Only print fatal errors in non-verbose mode - } - - if imageLocation == "" || imageType == "" { - flag.Usage() - os.Exit(1) - } - volumeBootXMLConfig.BackingStore.Format.Type = imageType - domainXMLConfig.Devices.Disks[1].Driver.Type = imageType - - conn, err := libvirt.NewConnect("qemu:///system") - if err != nil { - log.With(zap.Error(err)).Fatalf("Failed to connect to libvirt") - } - defer conn.Close() - - lInstance := libvirtInstance{ - conn: conn, - log: log, - imagePath: imageLocation, - } - - measurements, err := lInstance.obtainMeasurements() - if err != nil { - log.With(zap.Error(err)).Fatalf("Failed to obtain PCR measurements") - } - log.Infof("instances terminated successfully") - - output, err := yaml.Marshal(measurements) - if err != nil { - log.With(zap.Error(err)).Fatalf("Failed to marshal measurements") - } - - if outFile == "-" { - fmt.Println(string(output)) - } else { - if err := os.WriteFile(outFile, output, 0o644); err != nil { - log.With(zap.Error(err)).Fatalf("Failed to write measurements to file") - } - } -} diff --git a/hack/image-measurement/server/BUILD.bazel b/hack/image-measurement/server/BUILD.bazel deleted file mode 100644 index 48a743d84..000000000 --- a/hack/image-measurement/server/BUILD.bazel +++ /dev/null @@ -1,13 +0,0 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_library") - -go_library( - name = "server", - srcs = ["server.go"], - importpath = "github.com/edgelesssys/constellation/v2/hack/image-measurement/server", - visibility = ["//visibility:public"], - deps = [ - "//internal/attestation/measurements", - "//internal/logger", - "@org_uber_go_zap//:zap", - ], -) diff --git a/hack/image-measurement/server/server.go b/hack/image-measurement/server/server.go deleted file mode 100644 index adfd6f606..000000000 --- a/hack/image-measurement/server/server.go +++ /dev/null @@ -1,95 +0,0 @@ -/* -Copyright (c) Edgeless Systems GmbH - -SPDX-License-Identifier: AGPL-3.0-only -*/ - -package server - -import ( - "context" - "encoding/json" - "net" - "net/http" - - "github.com/edgelesssys/constellation/v2/internal/attestation/measurements" - "github.com/edgelesssys/constellation/v2/internal/logger" - "go.uber.org/zap" -) - -// Server provides measurements. -type Server struct { - log *logger.Logger - server http.Server - measurements measurements.M - done chan<- struct{} -} - -// New creates a new Server. -func New(log *logger.Logger, done chan<- struct{}) *Server { - return &Server{ - log: log, - done: done, - } -} - -// ListenAndServe on given port. -func (s *Server) ListenAndServe(port string) error { - mux := http.NewServeMux() - mux.Handle("/pcrs", http.HandlerFunc(s.logPCRs)) - - s.server = http.Server{ - Handler: mux, - } - - lis, err := net.Listen("tcp", net.JoinHostPort("", port)) - if err != nil { - return err - } - s.log.Infof("Starting QEMU metadata API on %s", lis.Addr()) - return s.server.Serve(lis) -} - -// Shutdown server. -func (s *Server) Shutdown() error { - return s.server.Shutdown(context.Background()) -} - -// logPCRs allows QEMU instances to export their TPM state during boot. -func (s *Server) logPCRs(w http.ResponseWriter, r *http.Request) { - log := s.log.With(zap.String("peer", r.RemoteAddr)) - if r.Method != http.MethodPost { - log.With(zap.String("method", r.Method)).Errorf("Invalid method for /log") - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - log.Infof("Serving POST request for /pcrs") - - if r.Body == nil { - log.Errorf("Request body is empty") - http.Error(w, "Request body is empty", http.StatusBadRequest) - return - } - - // unmarshal the request body into a map of PCRs - var pcrs measurements.M - if err := json.NewDecoder(r.Body).Decode(&pcrs); err != nil { - log.With(zap.Error(err)).Errorf("Failed to read request body") - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - log.Infof("PCR 4 %x", pcrs[4]) - log.Infof("PCR 8 %x", pcrs[8]) - log.Infof("PCR 9 %x", pcrs[9]) - - s.measurements = pcrs - - s.done <- struct{}{} -} - -// GetMeasurements returns the static measurements for QEMU environment. -func (s *Server) GetMeasurements() measurements.M { - return s.measurements -} diff --git a/hack/pcr-compare/.gitignore b/hack/pcr-compare/.gitignore deleted file mode 100644 index 6c6d80b92..000000000 --- a/hack/pcr-compare/.gitignore +++ /dev/null @@ -1 +0,0 @@ -pcr-compare diff --git a/hack/pcr-compare/BUILD.bazel b/hack/pcr-compare/BUILD.bazel deleted file mode 100644 index d2259a42e..000000000 --- a/hack/pcr-compare/BUILD.bazel +++ /dev/null @@ -1,18 +0,0 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") - -go_library( - name = "pcr-compare_lib", - srcs = ["main.go"], - importpath = "github.com/edgelesssys/constellation/v2/hack/pcr-compare", - visibility = ["//visibility:private"], - deps = [ - "//internal/attestation/measurements", - "@com_github_fatih_color//:color", - ], -) - -go_binary( - name = "pcr-compare", - embed = [":pcr-compare_lib"], - visibility = ["//visibility:public"], -) diff --git a/hack/pcr-compare/main.go b/hack/pcr-compare/main.go deleted file mode 100644 index 02da45878..000000000 --- a/hack/pcr-compare/main.go +++ /dev/null @@ -1,143 +0,0 @@ -/* -Copyright (c) Edgeless Systems GmbH - -SPDX-License-Identifier: AGPL-3.0-only -*/ - -package main - -import ( - "encoding/hex" - "encoding/json" - "fmt" - "os" - "sort" - - "github.com/edgelesssys/constellation/v2/internal/attestation/measurements" - "github.com/fatih/color" -) - -func main() { - if len(os.Args) < 3 { - if len(os.Args) == 0 { - fmt.Println("Usage:", "pcr-compare", " ") - } else { - fmt.Println("Usage:", os.Args[0], " ") - } - fmt.Println(" is supposed to be a JSON file from the 'Build OS image' pipeline.") - fmt.Println(" in supposed to be a JSON file with metadata from the PCR reader which is supposed to be verified.") - os.Exit(1) - } - - parsedExpectedMeasurements, err := parseMeasurements(os.Args[1]) - if err != nil { - panic(err) - } - - parsedActualMeasurements, err := parseMeasurements(os.Args[2]) - if err != nil { - panic(err) - } - - // Extract the PCR values we care about. - strippedActualMeasurements := stripMeasurements(parsedActualMeasurements) - strippedExpectedMeasurements := stripMeasurements(parsedExpectedMeasurements) - - // Do the check early. - areEqual := strippedExpectedMeasurements.EqualTo(strippedActualMeasurements) - - // Print values and similarities / differences in addition. - compareMeasurements(strippedExpectedMeasurements, strippedActualMeasurements) - - if !areEqual { - os.Exit(1) - } -} - -// parseMeasurements unmarshals a JSON file containing the expected or actual measurements. -func parseMeasurements(filename string) (measurements.M, error) { - fileData, err := os.ReadFile(filename) - if err != nil { - return measurements.M{}, err - } - - // Technically the expected version does not hold metadata, but we can use the same struct as both hold the measurements in `measurements`. - // This uses the fallback mechanism of the Measurements unmarshaller which accepts strings without the full struct, defaulting to warnOnly = false. - // warnOnly = false is expected for the expected measurements, so that's fine. - // We don't verify metadata here, the CLI has to do that. - var parsedMeasurements measurements.WithMetadata - if err := json.Unmarshal(fileData, &parsedMeasurements); err != nil { - return measurements.M{}, err - } - - return parsedMeasurements.Measurements, nil -} - -// stripMeasurements extracts only the measurements we want to compare. -// This excludes PCR 15 since the actual measurements come from an initialized cluster, but the expected measurements are supposed to be from an uninitialized state. -func stripMeasurements(input measurements.M) measurements.M { - toBeChecked := []uint32{4, 8, 9, 11, 12, 13} - - strippedMeasurements := make(measurements.M, len(toBeChecked)) - for _, pcr := range toBeChecked { - if _, ok := input[pcr]; ok { - strippedMeasurements[pcr] = input[pcr] - } - } - - return strippedMeasurements -} - -// compareMeasurements compares the expected PCRs with the actual PCRs. -func compareMeasurements(expectedMeasurements, actualMeasurements measurements.M) { - redPrint := color.New(color.FgRed).SprintFunc() - greenPrint := color.New(color.FgGreen).SprintFunc() - - expectedPCRs := getSortedKeysOfMap(expectedMeasurements) - for _, pcr := range expectedPCRs { - if _, ok := actualMeasurements[pcr]; !ok { - color.Magenta("Expected PCR %d not found in the calculated measurements.\n", pcr) - continue - } - - actualValue := actualMeasurements[pcr] - expectedValue := expectedMeasurements[pcr] - - fmt.Printf("Expected PCR %02d: %s (warnOnly: %t)\n", pcr, hex.EncodeToString(expectedValue.Expected[:]), expectedValue.WarnOnly) - - var foundMismatch bool - var coloredValue string - var coloredWarnOnly string - if actualValue.Expected == expectedValue.Expected { - coloredValue = greenPrint(hex.EncodeToString(actualValue.Expected[:])) - } else { - coloredValue = redPrint(hex.EncodeToString(actualValue.Expected[:])) - foundMismatch = true - } - - if actualValue.WarnOnly == expectedValue.WarnOnly { - coloredWarnOnly = greenPrint(fmt.Sprintf("%t", actualValue.WarnOnly)) - } else { - coloredWarnOnly = redPrint(fmt.Sprintf("%t", actualValue.WarnOnly)) - foundMismatch = true - } - - fmt.Printf("Measured PCR %02d: %s (warnOnly: %s)\n", pcr, coloredValue, coloredWarnOnly) - if !foundMismatch { - color.Green("PCR %02d matches.\n", pcr) - } else { - color.Red("PCR %02d does not match.\n", pcr) - } - } -} - -// getSortedKeysOfMap returns the sorted keys of a map to allow the PCR output to be ordered in the output. -func getSortedKeysOfMap(inputMap measurements.M) []uint32 { - keys := make([]uint32, 0, len(inputMap)) - for singleKey := range inputMap { - keys = append(keys, singleKey) - } - sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] }) - - return keys -} diff --git a/hack/pcr-reader/.gitignore b/hack/pcr-reader/.gitignore deleted file mode 100644 index 33db2649b..000000000 --- a/hack/pcr-reader/.gitignore +++ /dev/null @@ -1 +0,0 @@ -pcr-reader diff --git a/hack/pcr-reader/BUILD.bazel b/hack/pcr-reader/BUILD.bazel deleted file mode 100644 index 1e8271726..000000000 --- a/hack/pcr-reader/BUILD.bazel +++ /dev/null @@ -1,43 +0,0 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") -load("//bazel/go:go_test.bzl", "go_test") - -go_library( - name = "pcr-reader_lib", - srcs = ["main.go"], - importpath = "github.com/edgelesssys/constellation/v2/hack/pcr-reader", - visibility = ["//visibility:private"], - deps = [ - "//internal/attestation/measurements", - "//internal/attestation/vtpm", - "//internal/cloud/cloudprovider", - "//internal/constants", - "//internal/crypto", - "//verify/verifyproto", - "@in_gopkg_yaml_v3//:yaml_v3", - "@org_golang_google_grpc//:go_default_library", - "@org_golang_google_grpc//credentials/insecure", - ], -) - -go_binary( - name = "pcr-reader", - embed = [":pcr-reader_lib"], - visibility = ["//visibility:public"], -) - -go_test( - name = "pcr-reader_test", - srcs = ["main_test.go"], - embed = [":pcr-reader_lib"], - deps = [ - "//internal/attestation/measurements", - "//internal/attestation/vtpm", - "//internal/cloud/cloudprovider", - "@com_github_google_go_tpm_tools//proto/attest", - "@com_github_google_go_tpm_tools//proto/tpm", - "@com_github_stretchr_testify//assert", - "@com_github_stretchr_testify//require", - "@in_gopkg_yaml_v3//:yaml_v3", - "@org_uber_go_goleak//:goleak", - ], -) diff --git a/hack/pcr-reader/README.md b/hack/pcr-reader/README.md deleted file mode 100644 index e748df4ef..000000000 --- a/hack/pcr-reader/README.md +++ /dev/null @@ -1,88 +0,0 @@ -# PCR-updater - -New images result in different PCR values for the image. -This utility program makes it simple to update the expected PCR values of the CLI. - -## Usage - -To read the PCR state of any running Constellation node, run the following: - -```shell -go run main.go -constell-ip -constell-port -``` - -The output is similar to the following: - -```shell -$ go run main.go -constell-ip 192.0.2.3 -constell-port 30081 -connecting to verification service at 192.0.2.3:30081 -PCRs: -{ - "0": "DzXCFGCNk8em5ornNZtKi+Wg6Z7qkQfs5CfE3qTkOc8=", - "1": "XBoRlWuQx6nIDr5vgUL0DlJHy6H6u1dPU3qK2NyToc8=", - "10": "WLmYFRmDft/ajZJ056CAhpheU6Vbt73aR8eIQpLRGq0=", - "11": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", - "12": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", - "13": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", - "14": "4tPyJd6A5g09KduV3+nWZQCiEzHAiRT5DulmAqlvpZU=", - "15": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", - "16": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", - "17": "//////////////////////////////////////////8=", - "18": "//////////////////////////////////////////8=", - "19": "//////////////////////////////////////////8=", - "2": "PUWM/lXMA+ofRD8VYr7sjfUcdeFKn8+acjShPxmOeWk=", - "20": "//////////////////////////////////////////8=", - "21": "//////////////////////////////////////////8=", - "22": "//////////////////////////////////////////8=", - "23": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", - "3": "PUWM/lXMA+ofRD8VYr7sjfUcdeFKn8+acjShPxmOeWk=", - "4": "MmkueFj1rP2seH+bjeIRsO4dUnLnMdl7QgtGoAtQH7M=", - "5": "ExaiapuIfo0KMBo8wj6kPDORLocgnH1C0G/KY8DcV3A=", - "6": "PUWM/lXMA+ofRD8VYr7sjfUcdeFKn8+acjShPxmOeWk=", - "7": "UZcW+fhFRMpFkgU+EfKG2s3KdmgEA+TD2quLmthQHbo=", - "8": "KLSMootYaHBjysWKq9CAYXkXpeYx9PUBimlSEZGJqUM=", - "9": "gse53SjsqREEdOpImJH4KAb0b8PqIgwI+Ps/XSiFnN4=" -} -``` - -### Extend Config - -To set measurement values in Constellation config, use `yaml` format option. -Optionally filter down results measurements per cloud provider: - -Azure - -```bash -./pcr-reader --constell-ip ${CONSTELLATION_IP} --format yaml | yq e 'del(.[0,6,10,11,12,13,14,15,16,17,18,19,20,21,22,23])' - -``` - -## Meaning of PCR values - -An overview about what data is measured into the different registers can be found [in the TPM spec](https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClient_PFP_r1p05_v23_pub.pdf#%5B%7B%22num%22%3A157%2C%22gen%22%3A0%7D%2C%7B%22name%22%3A%22XYZ%22%7D%2C33%2C400%2C0%5D). - -We use the TPM and its PCRs to verify all nodes of a Constellation run with the same firmware and OS software. - -### Azure trusted launch - -PCR[0] measures the firmware volume (FV). Changes to FV also change PCR[0], making it unreliable for attestation. -PCR[6] measures the VM ID. This is unusable for cluster attestation for two reasons: - -1. The verification service does not know the VM ID of nodes wanting to join the cluster, so it can not compute the expected PCR[6] for the joining VM -2. A user may attest any node of the cluster without knowing the VM ID - -PCR[10] is used by Linux Integrity Measurement Architecture (IMA). -IMA creates runtime measurements based on a measurement policy (which is obsolete for Constellation, since we use dm-verity). -The first entry of the runtime measurements is the `boot_aggregate`. It is a SHA1 hash over PCRs 0 to 7. -As detailed earlier, PCR[6] is different for every VM in Azure, therefore PCR[10] will also be different since it includes PCR[6], meaning we can not use it for attestation. -IMA writing its measurements into PCR[10] can not be disabled without rebuilding the kernel. - -### Azure flexible deployment and attestation (FDA) - -With FDA CVMs measuring all of the firmware, it should be possible to use all PCRs for attestation since we know, and can choose, what firmware is running. - -### GCP confidential VM - -GCP uses confidential VMs based on AMD SEV-ES with a vTPM interface. - -PCR[0] contains the measurement of a string marking the VM as using ADM SEV-ES. -All firmware measurements seem to be constant. diff --git a/hack/pcr-reader/main.go b/hack/pcr-reader/main.go deleted file mode 100644 index 135f94f8b..000000000 --- a/hack/pcr-reader/main.go +++ /dev/null @@ -1,202 +0,0 @@ -/* -Copyright (c) Edgeless Systems GmbH - -SPDX-License-Identifier: AGPL-3.0-only -*/ - -package main - -import ( - "context" - "encoding/json" - "errors" - "flag" - "fmt" - "io" - "log" - "net" - "os" - "strconv" - "strings" - "time" - - "github.com/edgelesssys/constellation/v2/internal/attestation/measurements" - "github.com/edgelesssys/constellation/v2/internal/attestation/vtpm" - "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" - "github.com/edgelesssys/constellation/v2/internal/constants" - "github.com/edgelesssys/constellation/v2/internal/crypto" - "github.com/edgelesssys/constellation/v2/verify/verifyproto" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" - "gopkg.in/yaml.v3" -) - -func main() { - coordIP := flag.String("constell-ip", "", "Public IP of the Constellation") - port := flag.String("constell-port", strconv.Itoa(constants.VerifyServiceNodePortGRPC), "NodePort of the Constellation's verification service") - format := flag.String("format", "json", "Output format: json, yaml (default json)") - quiet := flag.Bool("q", false, "Set to disable output") - timeout := flag.Duration("timeout", 2*time.Minute, "Wait this duration for the verification service to become available") - metadata := flag.Bool("metadata", false, "Include image metadata (CSP, image UID) for publishing") - csp := flag.String("csp", "", "Define CSP for metadata") - image := flag.String("image", "", "Define image UID for metadata from which image the PCRs are taken from") - - flag.Parse() - - if *coordIP == "" || *port == "" { - flag.Usage() - os.Exit(1) - } - - if *metadata && (*csp == "" || *image == "") { - fmt.Println("If you enable metadata, you also need to define a CSP and an image to include from as arguments.") - flag.Usage() - os.Exit(1) - } - - addr := net.JoinHostPort(*coordIP, *port) - ctx, cancel := context.WithTimeout(context.Background(), *timeout) - defer cancel() - - attDocRaw, err := getAttestation(ctx, addr) - if err != nil { - log.Fatal(err) - } - - pcrs, err := validatePCRAttDoc(attDocRaw) - if err != nil { - log.Fatal(err) - } - - if *quiet { - return - } - - if *metadata { - outputWithMetadata := measurements.WithMetadata{ - CSP: cloudprovider.FromString(*csp), - Image: strings.ToLower(*image), - Measurements: pcrs, - } - err = printPCRsWithMetadata(os.Stdout, outputWithMetadata, *format) - } else { - err = printPCRs(os.Stdout, pcrs, *format) - } - - if err != nil { - log.Fatal(err) - } -} - -// getAttestation connects to the Constellation verification service and returns its attestation document. -func getAttestation(ctx context.Context, addr string) ([]byte, error) { - conn, err := grpc.DialContext( - ctx, addr, grpc.WithTransportCredentials(insecure.NewCredentials()), - ) - if err != nil { - return nil, fmt.Errorf("unable to connect to verification service: %w", err) - } - defer conn.Close() - - nonce, err := crypto.GenerateRandomBytes(32) - if err != nil { - return nil, err - } - - client := verifyproto.NewAPIClient(conn) - res, err := client.GetAttestation(ctx, &verifyproto.GetAttestationRequest{Nonce: nonce}) - if err != nil { - return nil, err - } - return res.Attestation, nil -} - -// validatePCRAttDoc parses and validates PCRs of an attestation document. -func validatePCRAttDoc(attDocRaw []byte) (measurements.M, error) { - attDoc := vtpm.AttestationDocument{} - if err := json.Unmarshal(attDocRaw, &attDoc); err != nil { - return nil, err - } - if attDoc.Attestation == nil { - return nil, errors.New("empty attestation") - } - qIdx, err := vtpm.GetSHA256QuoteIndex(attDoc.Attestation.Quotes) - if err != nil { - return nil, err - } - - m := measurements.M{} - for idx, pcr := range attDoc.Attestation.Quotes[qIdx].Pcrs.Pcrs { - if len(pcr) != 32 { - return nil, fmt.Errorf("incomplete PCR at index: %d", idx) - } - - m[idx] = measurements.Measurement{ - Expected: *(*[32]byte)(pcr), - WarnOnly: true, - } - } - return m, nil -} - -// printPCRs formats and prints PCRs to the given writer. -// format can be one of 'json' or 'yaml'. If it doesn't match defaults to 'json'. -func printPCRs(w io.Writer, pcrs measurements.M, format string) error { - switch format { - case "json": - return printPCRsJSON(w, pcrs) - case "yaml": - return printPCRsYAML(w, pcrs) - default: - return printPCRsJSON(w, pcrs) - } -} - -// printPCRs formats and prints PCRs to the given writer. -// format can be one of 'json' or 'yaml'. If it doesn't match defaults to 'json'. -func printPCRsWithMetadata(w io.Writer, outputWithMetadata measurements.WithMetadata, format string) error { - switch format { - case "json": - return printPCRsJSONWithMetadata(w, outputWithMetadata) - case "yaml": - return printPCRsYAMLWithMetadata(w, outputWithMetadata) - default: - return printPCRsJSONWithMetadata(w, outputWithMetadata) - } -} - -func printPCRsYAML(w io.Writer, pcrs measurements.M) error { - pcrYAML, err := yaml.Marshal(pcrs) - if err != nil { - return err - } - fmt.Fprintf(w, "%s", string(pcrYAML)) - return nil -} - -func printPCRsYAMLWithMetadata(w io.Writer, outputWithMetadata measurements.WithMetadata) error { - pcrYAML, err := yaml.Marshal(outputWithMetadata) - if err != nil { - return err - } - fmt.Fprintf(w, "%s", string(pcrYAML)) - return nil -} - -func printPCRsJSON(w io.Writer, pcrs measurements.M) error { - pcrJSON, err := json.MarshalIndent(pcrs, "", " ") - if err != nil { - return err - } - fmt.Fprintf(w, "%s", string(pcrJSON)) - return nil -} - -func printPCRsJSONWithMetadata(w io.Writer, outputWithMetadata measurements.WithMetadata) error { - pcrJSON, err := json.MarshalIndent(outputWithMetadata, "", " ") - if err != nil { - return err - } - fmt.Fprintf(w, "%s", string(pcrJSON)) - return nil -} diff --git a/hack/pcr-reader/main_test.go b/hack/pcr-reader/main_test.go deleted file mode 100644 index 33de77de0..000000000 --- a/hack/pcr-reader/main_test.go +++ /dev/null @@ -1,216 +0,0 @@ -/* -Copyright (c) Edgeless Systems GmbH - -SPDX-License-Identifier: AGPL-3.0-only -*/ - -package main - -import ( - "bytes" - "encoding/hex" - "encoding/json" - "fmt" - "testing" - - "github.com/edgelesssys/constellation/v2/internal/attestation/measurements" - "github.com/edgelesssys/constellation/v2/internal/attestation/vtpm" - "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" - "github.com/google/go-tpm-tools/proto/attest" - "github.com/google/go-tpm-tools/proto/tpm" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.uber.org/goleak" - "gopkg.in/yaml.v3" -) - -func TestMain(m *testing.M) { - goleak.VerifyTestMain(m) -} - -func TestValidatePCRAttDoc(t *testing.T) { - testCases := map[string]struct { - attDocRaw []byte - wantErr bool - }{ - "invalid attestation document": { - attDocRaw: []byte{0x1, 0x2, 0x3}, - wantErr: true, - }, - "nil attestation": { - attDocRaw: mustMarshalAttDoc(t, vtpm.AttestationDocument{}), - wantErr: true, - }, - "nil quotes": { - attDocRaw: mustMarshalAttDoc(t, vtpm.AttestationDocument{ - Attestation: &attest.Attestation{}, - }), - wantErr: true, - }, - "invalid PCRs": { - attDocRaw: mustMarshalAttDoc(t, vtpm.AttestationDocument{ - Attestation: &attest.Attestation{ - Quotes: []*tpm.Quote{ - { - Pcrs: &tpm.PCRs{ - Hash: tpm.HashAlgo_SHA256, - Pcrs: map[uint32][]byte{ - 0: {0x1, 0x2, 0x3}, - }, - }, - }, - }, - }, - }), - wantErr: true, - }, - "valid PCRs": { - attDocRaw: mustMarshalAttDoc(t, vtpm.AttestationDocument{ - Attestation: &attest.Attestation{ - Quotes: []*tpm.Quote{ - { - Pcrs: &tpm.PCRs{ - Hash: tpm.HashAlgo_SHA256, - Pcrs: map[uint32][]byte{ - 0: bytes.Repeat([]byte{0xAA}, 32), - }, - }, - }, - }, - }, - }), - wantErr: false, - }, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - pcrs, err := validatePCRAttDoc(tc.attDocRaw) - if tc.wantErr { - assert.Error(err) - } else { - require.NoError(err) - - attDoc := vtpm.AttestationDocument{} - require.NoError(json.Unmarshal(tc.attDocRaw, &attDoc)) - qIdx, err := vtpm.GetSHA256QuoteIndex(attDoc.Attestation.Quotes) - require.NoError(err) - - for pcrIdx, pcrVal := range pcrs { - assert.Equal(pcrVal.Expected[:], attDoc.Attestation.Quotes[qIdx].Pcrs.Pcrs[pcrIdx]) - } - } - }) - } -} - -func mustMarshalAttDoc(t *testing.T, attDoc vtpm.AttestationDocument) []byte { - attDocRaw, err := json.Marshal(attDoc) - require.NoError(t, err) - return attDocRaw -} - -func TestPrintPCRs(t *testing.T) { - testCases := map[string]struct { - format string - }{ - "json": { - format: "json", - }, - "empty format": { - format: "", - }, - "yaml": { - format: "yaml", - }, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - assert := assert.New(t) - - pcrs := measurements.M{ - 0: measurements.WithAllBytes(0xAA, true), - 1: measurements.WithAllBytes(0xBB, true), - 2: measurements.WithAllBytes(0xCC, true), - } - - var out bytes.Buffer - err := printPCRs(&out, pcrs, tc.format) - assert.NoError(err) - - for idx, pcr := range pcrs { - assert.Contains(out.String(), fmt.Sprintf("%d", idx)) - assert.Contains(out.String(), hex.EncodeToString(pcr.Expected[:])) - } - }) - } -} - -func TestPrintPCRsWithMetadata(t *testing.T) { - testCases := map[string]struct { - format string - csp cloudprovider.Provider - image string - }{ - "json": { - format: "json", - csp: cloudprovider.Azure, - image: "v2.0.0", - }, - "yaml": { - csp: cloudprovider.GCP, - image: "v2.0.0-testimage", - format: "yaml", - }, - "empty format": { - format: "", - csp: cloudprovider.QEMU, - image: "v2.0.0-testimage", - }, - "empty": {}, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - pcrs := measurements.M{ - 0: measurements.WithAllBytes(0xAA, true), - 1: measurements.WithAllBytes(0xBB, true), - 2: measurements.WithAllBytes(0xCC, true), - } - - outputWithMetadata := measurements.WithMetadata{ - CSP: tc.csp, - Image: tc.image, - Measurements: pcrs, - } - - var out bytes.Buffer - err := printPCRsWithMetadata(&out, outputWithMetadata, tc.format) - assert.NoError(err) - - var unmarshalledOutput measurements.WithMetadata - if tc.format == "" || tc.format == "json" { - require.NoError(json.Unmarshal(out.Bytes(), &unmarshalledOutput)) - } else if tc.format == "yaml" { - require.NoError(yaml.Unmarshal(out.Bytes(), &unmarshalledOutput)) - } - - assert.NotNil(unmarshalledOutput.CSP) - assert.NotNil(unmarshalledOutput.Image) - assert.Equal(tc.csp, unmarshalledOutput.CSP) - assert.Equal(tc.image, unmarshalledOutput.Image) - - for idx, pcr := range pcrs { - assert.Contains(out.String(), fmt.Sprintf("%d", idx)) - assert.Contains(out.String(), hex.EncodeToString(pcr.Expected[:])) - } - }) - } -} diff --git a/hack/qemu-metadata-api/server/BUILD.bazel b/hack/qemu-metadata-api/server/BUILD.bazel index eb4dc66d9..846b44c47 100644 --- a/hack/qemu-metadata-api/server/BUILD.bazel +++ b/hack/qemu-metadata-api/server/BUILD.bazel @@ -8,7 +8,6 @@ go_library( visibility = ["//visibility:public"], deps = [ "//hack/qemu-metadata-api/virtwrapper", - "//internal/attestation/measurements", "//internal/cloud/metadata", "//internal/logger", "//internal/role", @@ -24,7 +23,6 @@ go_test( tags = ["manual"], deps = [ "//hack/qemu-metadata-api/virtwrapper", - "//internal/attestation/measurements", "//internal/cloud/metadata", "//internal/logger", "@com_github_stretchr_testify//assert", diff --git a/hack/qemu-metadata-api/server/server.go b/hack/qemu-metadata-api/server/server.go index 9468d0cd9..c60bdae30 100644 --- a/hack/qemu-metadata-api/server/server.go +++ b/hack/qemu-metadata-api/server/server.go @@ -15,7 +15,6 @@ import ( "strings" "github.com/edgelesssys/constellation/v2/hack/qemu-metadata-api/virtwrapper" - "github.com/edgelesssys/constellation/v2/internal/attestation/measurements" "github.com/edgelesssys/constellation/v2/internal/cloud/metadata" "github.com/edgelesssys/constellation/v2/internal/logger" "github.com/edgelesssys/constellation/v2/internal/role" @@ -46,7 +45,6 @@ func (s *Server) ListenAndServe(port string) error { mux.Handle("/self", http.HandlerFunc(s.listSelf)) mux.Handle("/peers", http.HandlerFunc(s.listPeers)) mux.Handle("/log", http.HandlerFunc(s.postLog)) - mux.Handle("/pcrs", http.HandlerFunc(s.exportPCRs)) mux.Handle("/endpoint", http.HandlerFunc(s.getEndpoint)) mux.Handle("/initsecrethash", http.HandlerFunc(s.initSecretHash)) @@ -202,55 +200,6 @@ func (s *Server) postLog(w http.ResponseWriter, r *http.Request) { log.With(zap.String("message", string(msg))).Infof("Cloud-logging entry") } -// exportPCRs allows QEMU instances to export their TPM state during boot. -// This can be used to check expected PCRs for GCP/Azure cloud images locally. -func (s *Server) exportPCRs(w http.ResponseWriter, r *http.Request) { - log := s.log.With(zap.String("peer", r.RemoteAddr)) - if r.Method != http.MethodPost { - log.With(zap.String("method", r.Method)).Errorf("Invalid method for /log") - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - log.Infof("Serving POST request for /pcrs") - - if r.Body == nil { - log.Errorf("Request body is empty") - http.Error(w, "Request body is empty", http.StatusBadRequest) - return - } - - // unmarshal the request body into a map of PCRs - var pcrs measurements.M - if err := json.NewDecoder(r.Body).Decode(&pcrs); err != nil { - log.With(zap.Error(err)).Errorf("Failed to read request body") - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - // get name of the node sending the export request - var nodeName string - peers, err := s.listAll() - if err != nil { - log.With(zap.Error(err)).Errorf("Failed to list peer metadata") - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - remoteIP, _, err := net.SplitHostPort(r.RemoteAddr) - if err != nil { - log.With(zap.Error(err)).Errorf("Failed to parse remote address") - http.Error(w, fmt.Sprintf("Failed to parse remote address: %s\n", err), http.StatusInternalServerError) - return - } - for _, peer := range peers { - if peer.VPCIP == remoteIP { - nodeName = peer.Name - } - } - - log.With(zap.String("node", nodeName)).With(zap.Any("pcrs", pcrs)).Infof("Received PCRs from node") -} - // listAll returns a list of all active peers. func (s *Server) listAll() ([]metadata.InstanceMetadata, error) { net, err := s.virt.LookupNetworkByName(s.network) diff --git a/hack/qemu-metadata-api/server/server_test.go b/hack/qemu-metadata-api/server/server_test.go index 4ff1ba37e..f9c5a6d7d 100644 --- a/hack/qemu-metadata-api/server/server_test.go +++ b/hack/qemu-metadata-api/server/server_test.go @@ -17,7 +17,6 @@ import ( "testing" "github.com/edgelesssys/constellation/v2/hack/qemu-metadata-api/virtwrapper" - "github.com/edgelesssys/constellation/v2/internal/attestation/measurements" "github.com/edgelesssys/constellation/v2/internal/cloud/metadata" "github.com/edgelesssys/constellation/v2/internal/logger" "github.com/stretchr/testify/assert" @@ -284,87 +283,6 @@ func TestPostLog(t *testing.T) { } } -func TestExportPCRs(t *testing.T) { - defaultConnect := &stubConnect{ - network: stubNetwork{ - leases: []libvirt.NetworkDHCPLease{ - { - IPaddr: "192.0.100.1", - Hostname: "control-plane-0", - }, - }, - }, - } - - testCases := map[string]struct { - remoteAddr string - connect *stubConnect - message string - method string - wantErr bool - }{ - "success": { - remoteAddr: "192.0.100.1:1234", - connect: defaultConnect, - method: http.MethodPost, - message: mustMarshal(t, measurements.M{0: measurements.WithAllBytes(0xAA, false)}), - }, - "incorrect method": { - remoteAddr: "192.0.100.1:1234", - connect: defaultConnect, - message: mustMarshal(t, measurements.M{0: measurements.WithAllBytes(0xAA, false)}), - method: http.MethodGet, - wantErr: true, - }, - "listAll error": { - remoteAddr: "192.0.100.1:1234", - connect: &stubConnect{ - getNetworkErr: errors.New("error"), - }, - message: mustMarshal(t, measurements.M{0: measurements.WithAllBytes(0xAA, false)}), - method: http.MethodPost, - wantErr: true, - }, - "invalid message": { - remoteAddr: "192.0.100.1:1234", - connect: defaultConnect, - method: http.MethodPost, - message: "message", - wantErr: true, - }, - "invalid remote address": { - remoteAddr: "localhost", - connect: defaultConnect, - method: http.MethodPost, - message: mustMarshal(t, measurements.M{0: measurements.WithAllBytes(0xAA, false)}), - wantErr: true, - }, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - server := New(logger.NewTest(t), "test", "initSecretHash", tc.connect) - - req, err := http.NewRequestWithContext(context.Background(), tc.method, "http://192.0.0.1/pcrs", strings.NewReader(tc.message)) - require.NoError(err) - req.RemoteAddr = tc.remoteAddr - - w := httptest.NewRecorder() - server.exportPCRs(w, req) - - if tc.wantErr { - assert.NotEqual(http.StatusOK, w.Code) - return - } - - assert.Equal(http.StatusOK, w.Code) - }) - } -} - func TestInitSecretHash(t *testing.T) { defaultConnect := &stubConnect{ network: stubNetwork{ @@ -417,13 +335,6 @@ func TestInitSecretHash(t *testing.T) { } } -func mustMarshal(t *testing.T, v any) string { - t.Helper() - b, err := json.Marshal(v) - require.NoError(t, err) - return string(b) -} - type stubConnect struct { network stubNetwork getNetworkErr error