mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-12-23 14:39:40 -05:00
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:
parent
bdba9d8ba6
commit
83d10b0e70
@ -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
|
||||
|
@ -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 <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.
|
||||
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
|
||||
|
||||
|
@ -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",
|
||||
],
|
||||
|
@ -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
|
||||
}
|
||||
|
10
hack/go.mod
10
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
|
||||
|
@ -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=
|
||||
|
@ -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"],
|
||||
)
|
@ -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",
|
||||
}
|
||||
)
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
@ -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",
|
||||
],
|
||||
)
|
@ -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
|
||||
}
|
1
hack/pcr-compare/.gitignore
vendored
1
hack/pcr-compare/.gitignore
vendored
@ -1 +0,0 @@
|
||||
pcr-compare
|
@ -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"],
|
||||
)
|
@ -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
|
||||
}
|
1
hack/pcr-reader/.gitignore
vendored
1
hack/pcr-reader/.gitignore
vendored
@ -1 +0,0 @@
|
||||
pcr-reader
|
@ -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",
|
||||
],
|
||||
)
|
@ -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.
|
@ -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
|
||||
}
|
@ -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[:]))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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",
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user