hack: remove unused tools (#1387)

* Remove unused pcr-compare tool
* Remove unused pcr-reader tool
* Remove obsolete image-measurement tool

Signed-off-by: Daniel Weiße <dw@edgeless.systems>
This commit is contained in:
Daniel Weiße 2023-03-09 16:59:33 +01:00 committed by GitHub
parent bdba9d8ba6
commit 83d10b0e70
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 5 additions and 1755 deletions

View File

@ -17,9 +17,6 @@
/hack/check-licenses.sh @thomasten /hack/check-licenses.sh @thomasten
/hack/clidocgen @thomasten /hack/clidocgen @thomasten
/hack/fetch-broken-e2e @katexochen /hack/fetch-broken-e2e @katexochen
/hack/image-measurement @daniel-weisse
/hack/pcr-compare @nirusu
/hack/pcr-reader @daniel-weisse
/hack/pseudo-version @malt3 /hack/pseudo-version @malt3
/hack/qemu-metadata-api @daniel-weisse /hack/qemu-metadata-api @daniel-weisse
/hack/remove-tf-providers @katexochen /hack/remove-tf-providers @katexochen

View File

@ -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. 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. 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. 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.
To run the tool execute the following command in `/hack/image-measurement`:
```sh
go run . -path <image_path> -type <image_type>
```
`<image_path>` 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 `<image_type>` 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.
# Dependency management # Dependency management

View File

@ -25,8 +25,6 @@ go_library(
"//internal/logger", "//internal/logger",
"//internal/oid", "//internal/oid",
"//internal/role", "//internal/role",
"@com_github_google_go_tpm//tpm2",
"@com_github_google_go_tpm_tools//client",
"@com_github_spf13_afero//:afero", "@com_github_spf13_afero//:afero",
"@org_uber_go_zap//:zap", "@org_uber_go_zap//:zap",
], ],

View File

@ -7,13 +7,9 @@ SPDX-License-Identifier: AGPL-3.0-only
package main package main
import ( import (
"bytes"
"context" "context"
"encoding/json"
"flag" "flag"
"net" "net"
"net/http"
"net/url"
"os" "os"
"path/filepath" "path/filepath"
@ -36,8 +32,6 @@ import (
"github.com/edgelesssys/constellation/v2/internal/logger" "github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/internal/oid" "github.com/edgelesssys/constellation/v2/internal/oid"
"github.com/edgelesssys/constellation/v2/internal/role" "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" "github.com/spf13/afero"
"go.uber.org/zap" "go.uber.org/zap"
) )
@ -78,7 +72,6 @@ func main() {
// using udev rules, a symlink for our disk is created at /dev/sdb // using udev rules, a symlink for our disk is created at /dev/sdb
diskPath, err = filepath.EvalSymlinks(awsStateDiskPath) diskPath, err = filepath.EvalSymlinks(awsStateDiskPath)
if err != nil { if err != nil {
_ = exportPCRs()
log.With(zap.Error(err)).Fatalf("Unable to resolve Azure state disk path") log.With(zap.Error(err)).Fatalf("Unable to resolve Azure state disk path")
} }
metadataClient, err = awscloud.New(context.Background()) metadataClient, err = awscloud.New(context.Background())
@ -89,7 +82,6 @@ func main() {
case cloudprovider.Azure: case cloudprovider.Azure:
diskPath, err = filepath.EvalSymlinks(azureStateDiskPath) diskPath, err = filepath.EvalSymlinks(azureStateDiskPath)
if err != nil { if err != nil {
_ = exportPCRs()
log.With(zap.Error(err)).Fatalf("Unable to resolve Azure state disk path") log.With(zap.Error(err)).Fatalf("Unable to resolve Azure state disk path")
} }
metadataClient, err = azurecloud.New(context.Background()) metadataClient, err = azurecloud.New(context.Background())
@ -100,7 +92,6 @@ func main() {
case cloudprovider.GCP: case cloudprovider.GCP:
diskPath, err = filepath.EvalSymlinks(gcpStateDiskPath) diskPath, err = filepath.EvalSymlinks(gcpStateDiskPath)
if err != nil { if err != nil {
_ = exportPCRs()
log.With(zap.Error(err)).Fatalf("Unable to resolve GCP state disk path") log.With(zap.Error(err)).Fatalf("Unable to resolve GCP state disk path")
} }
gcpMeta, err := gcpcloud.New(context.Background()) gcpMeta, err := gcpcloud.New(context.Background())
@ -116,12 +107,10 @@ func main() {
if err != nil { if err != nil {
log.With(zap.Error).Fatalf("Failed to create OpenStack metadata client") log.With(zap.Error).Fatalf("Failed to create OpenStack metadata client")
} }
_ = exportPCRs()
case cloudprovider.QEMU: case cloudprovider.QEMU:
diskPath = qemuStateDiskPath diskPath = qemuStateDiskPath
metadataClient = qemucloud.New() metadataClient = qemucloud.New()
_ = exportPCRs()
default: default:
log.Fatalf("CSP %s is not supported by Constellation", *csp) 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") 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
}

View File

@ -38,19 +38,13 @@ replace (
require ( require (
github.com/edgelesssys/constellation/v2 v2.5.2 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/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/spf13/cobra v1.6.1
github.com/stretchr/testify v1.8.2 github.com/stretchr/testify v1.8.2
go.uber.org/goleak v1.2.1
go.uber.org/zap v1.24.0 go.uber.org/zap v1.24.0
golang.org/x/mod v0.8.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/square/go-jose.v2 v2.6.0
gopkg.in/yaml.v3 v3.0.1
libvirt.org/go/libvirt v1.8010.0 libvirt.org/go/libvirt v1.8010.0
libvirt.org/go/libvirtxml v1.8009.0
) )
require ( require (
@ -112,6 +106,7 @@ require (
github.com/emirpasic/gods v1.18.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect
github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/evanphx/json-patch v5.6.0+incompatible // indirect
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // 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-chi/chi v4.1.2+incompatible // indirect
github.com/go-errors/errors v1.4.2 // indirect github.com/go-errors/errors v1.4.2 // indirect
github.com/go-git/gcfg v1.5.0 // 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-containerregistry v0.13.0 // indirect
github.com/google/go-sev-guest v0.4.1 // 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 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/go-tspi v0.3.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect github.com/google/gofuzz v1.2.0 // indirect
github.com/google/logger v1.1.1 // 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/api v0.110.0 // indirect
google.golang.org/appengine v1.6.7 // indirect google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc // 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 google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // 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 v2.17.0+incompatible // indirect
helm.sh/helm/v3 v3.11.1 // indirect helm.sh/helm/v3 v3.11.1 // indirect
k8s.io/api v0.26.2 // indirect k8s.io/api v0.26.2 // indirect

View File

@ -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/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.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 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.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.4.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= 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 h1:q4UeuyzSp7GiYUB5T3ytzeXh41hp9JqNQu150NkO+7A=
libvirt.org/go/libvirt v1.8010.0/go.mod h1:1WiFE8EjZfq+FCVog+rvr1yatKbKZ9FaFMZgEqxEJqQ= 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 h1:0E9tOHUfrNH7TCDk5KU0jVBEzCqbfdyuVfGmJ7ZeRPE=
oras.land/oras-go v1.2.2/go.mod h1:Apa81sKoZPpP7CDciE006tSZ0x3Q3+dOoBcMZ/aNxvw= 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= pack.ag/amqp v0.11.2/go.mod h1:4/cbmt4EJXSKlG6LCfWHoqmN0uFdy5i/+YFz+fTfhV4=

View File

@ -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"],
)

View File

@ -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",
}
)

View File

@ -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")
}
}
}

View File

@ -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",
],
)

View File

@ -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
}

View File

@ -1 +0,0 @@
pcr-compare

View File

@ -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"],
)

View File

@ -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", "<expected-measurements> <actual-measurements>")
} else {
fmt.Println("Usage:", os.Args[0], "<expected-measurements> <actual-measurements>")
}
fmt.Println("<expected-measurements> is supposed to be a JSON file from the 'Build OS image' pipeline.")
fmt.Println("<actual-measurements> 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
}

View File

@ -1 +0,0 @@
pcr-reader

View File

@ -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",
],
)

View File

@ -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 <NODE_IP> -constell-port <VERIFY_SERVICE_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.

View File

@ -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
}

View File

@ -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[:]))
}
})
}
}

View File

@ -8,7 +8,6 @@ go_library(
visibility = ["//visibility:public"], visibility = ["//visibility:public"],
deps = [ deps = [
"//hack/qemu-metadata-api/virtwrapper", "//hack/qemu-metadata-api/virtwrapper",
"//internal/attestation/measurements",
"//internal/cloud/metadata", "//internal/cloud/metadata",
"//internal/logger", "//internal/logger",
"//internal/role", "//internal/role",
@ -24,7 +23,6 @@ go_test(
tags = ["manual"], tags = ["manual"],
deps = [ deps = [
"//hack/qemu-metadata-api/virtwrapper", "//hack/qemu-metadata-api/virtwrapper",
"//internal/attestation/measurements",
"//internal/cloud/metadata", "//internal/cloud/metadata",
"//internal/logger", "//internal/logger",
"@com_github_stretchr_testify//assert", "@com_github_stretchr_testify//assert",

View File

@ -15,7 +15,6 @@ import (
"strings" "strings"
"github.com/edgelesssys/constellation/v2/hack/qemu-metadata-api/virtwrapper" "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/cloud/metadata"
"github.com/edgelesssys/constellation/v2/internal/logger" "github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/internal/role" "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("/self", http.HandlerFunc(s.listSelf))
mux.Handle("/peers", http.HandlerFunc(s.listPeers)) mux.Handle("/peers", http.HandlerFunc(s.listPeers))
mux.Handle("/log", http.HandlerFunc(s.postLog)) mux.Handle("/log", http.HandlerFunc(s.postLog))
mux.Handle("/pcrs", http.HandlerFunc(s.exportPCRs))
mux.Handle("/endpoint", http.HandlerFunc(s.getEndpoint)) mux.Handle("/endpoint", http.HandlerFunc(s.getEndpoint))
mux.Handle("/initsecrethash", http.HandlerFunc(s.initSecretHash)) 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") 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. // listAll returns a list of all active peers.
func (s *Server) listAll() ([]metadata.InstanceMetadata, error) { func (s *Server) listAll() ([]metadata.InstanceMetadata, error) {
net, err := s.virt.LookupNetworkByName(s.network) net, err := s.virt.LookupNetworkByName(s.network)

View File

@ -17,7 +17,6 @@ import (
"testing" "testing"
"github.com/edgelesssys/constellation/v2/hack/qemu-metadata-api/virtwrapper" "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/cloud/metadata"
"github.com/edgelesssys/constellation/v2/internal/logger" "github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/stretchr/testify/assert" "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) { func TestInitSecretHash(t *testing.T) {
defaultConnect := &stubConnect{ defaultConnect := &stubConnect{
network: stubNetwork{ 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 { type stubConnect struct {
network stubNetwork network stubNetwork
getNetworkErr error getNetworkErr error