From 79f32ea47bea87c6f432b8a726d7de0075d7bfc2 Mon Sep 17 00:00:00 2001 From: Pranjali-2501 <87357388+Pranjali-2501@users.noreply.github.com> Date: Fri, 15 Sep 2023 16:11:07 +0000 Subject: [PATCH 1/6] import go-tdx-guest v0.2.2 (#359) --- go.mod | 2 +- go.sum | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 8d73a9091..a482c3a4c 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/google/go-attestation v0.5.0 github.com/google/go-cmp v0.5.9 github.com/google/go-sev-guest v0.7.0 - github.com/google/go-tdx-guest v0.2.1-0.20230907045450-944015509c84 + github.com/google/go-tdx-guest v0.2.2 github.com/google/go-tpm v0.9.0 github.com/google/logger v1.1.1 google.golang.org/protobuf v1.31.0 diff --git a/go.sum b/go.sum index 7346ba167..45b93d3da 100644 --- a/go.sum +++ b/go.sum @@ -312,6 +312,8 @@ github.com/google/go-sev-guest v0.7.0 h1:DBCABhTo7WicP27ZH/hwcCdjcmxFkxxMOQXm5hF github.com/google/go-sev-guest v0.7.0/go.mod h1:UEi9uwoPbLdKGl1QHaq1G8pfCbQ4QP0swWX4J0k6r+Q= github.com/google/go-tdx-guest v0.2.1-0.20230907045450-944015509c84 h1:XqVJa7fVU8b+Hlhcvw49qfg0+LYcRI+V+jYUrSek848= github.com/google/go-tdx-guest v0.2.1-0.20230907045450-944015509c84/go.mod h1:a8EIh1l5x7jmIrrOuH//xWn6y4Sk4yupwmMcJE006RI= +github.com/google/go-tdx-guest v0.2.2 h1:MgHcWLCEHVpqR0LXxtgJq2uG0oNNszV8+5XYtKs7+Yg= +github.com/google/go-tdx-guest v0.2.2/go.mod h1:a8EIh1l5x7jmIrrOuH//xWn6y4Sk4yupwmMcJE006RI= github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk= github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU= github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus= @@ -652,6 +654,7 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0= From 12c2c651d678d15f39b85a02496c2b34c7b48b67 Mon Sep 17 00:00:00 2001 From: Alex Wu Date: Fri, 15 Sep 2023 11:02:27 -0700 Subject: [PATCH 2/6] Fix flakey cloud logging test (#361) The basic workload cloud logging test does not have some wait time for the workload to write to cloud logging, so it consistently fails. --- .../image/test/scripts/test_launcher_workload_cloudlogging.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/launcher/image/test/scripts/test_launcher_workload_cloudlogging.sh b/launcher/image/test/scripts/test_launcher_workload_cloudlogging.sh index d5e1b1301..176777111 100644 --- a/launcher/image/test/scripts/test_launcher_workload_cloudlogging.sh +++ b/launcher/image/test/scripts/test_launcher_workload_cloudlogging.sh @@ -2,6 +2,9 @@ set -euo pipefail source util/read_cloud_logging.sh +# Allow VM some time to boot and write to cloud logging. +sleep 120 + # This test requires the workload to run and print # corresponding messages to cloud logging. CLOUD_LOGGING_OUTPUT=$(read_cloud_logging $1) From c2f108fb998e99793a9213cfc895d50123ee71ac Mon Sep 17 00:00:00 2001 From: JoshuaKrstic <80930796+JoshuaKrstic@users.noreply.github.com> Date: Sat, 16 Sep 2023 23:39:19 -0700 Subject: [PATCH 3/6] Add experiment support (#352) Co-authored-by: Joshua Krstic --- launcher/container_runner.go | 2 + launcher/image/cloudbuild.yaml | 7 +++ launcher/image/preload.sh | 9 ++++ launcher/internal/experiments/experiments.go | 42 +++++++++++++++ .../internal/experiments/experiments_test.go | 52 +++++++++++++++++++ launcher/launcher/main.go | 22 ++++++++ launcher/spec/launch_spec.go | 2 + 7 files changed, 136 insertions(+) create mode 100644 launcher/internal/experiments/experiments.go create mode 100644 launcher/internal/experiments/experiments_test.go diff --git a/launcher/container_runner.go b/launcher/container_runner.go index 82eb5933d..173b7cf72 100644 --- a/launcher/container_runner.go +++ b/launcher/container_runner.go @@ -500,6 +500,8 @@ func (r *ContainerRunner) Run(ctx context.Context) error { return fmt.Errorf("failed to fetch and write OIDC token: %v", err) } + r.logger.Printf("EnableTestFeatureForImage is set to %v\n", r.launchSpec.Experiments.EnableTestFeatureForImage) + var streamOpt cio.Opt switch r.launchSpec.LogRedirect { case spec.Nowhere: diff --git a/launcher/image/cloudbuild.yaml b/launcher/image/cloudbuild.yaml index 976812285..5bdf05dfa 100644 --- a/launcher/image/cloudbuild.yaml +++ b/launcher/image/cloudbuild.yaml @@ -15,6 +15,13 @@ steps: - | cd launcher/launcher CGO_ENABLED=0 go build -o ../image/launcher + - name: 'gcr.io/cloud-builders/gcloud' + id: DownloadExpBinary + entrypoint: 'gcloud' + args: ['storage', + 'cp', + 'gs://confidential-space-images_third-party/confidential_space_experiments', + './launcher/image/confidential_space_experiments'] - name: 'gcr.io/cos-cloud/cos-customizer' args: ['start-image-build', '-build-context=launcher/image', diff --git a/launcher/image/preload.sh b/launcher/image/preload.sh index 0a840070b..e1c7d9bec 100644 --- a/launcher/image/preload.sh +++ b/launcher/image/preload.sh @@ -2,11 +2,18 @@ readonly OEM_PATH='/usr/share/oem' readonly CS_PATH="${OEM_PATH}/confidential_space" +readonly EXPERIMENTS_BINARY="confidential_space_experiments" copy_launcher() { cp launcher "${CS_PATH}/cs_container_launcher" } +copy_experiment_client() { + # DownloadExpBinary creates the file at EXPERIMENTS_BINARY. + cp $EXPERIMENTS_BINARY "${CS_PATH}/${EXPERIMENTS_BINARY}" + chmod +x "${CS_PATH}/${EXPERIMENTS_BINARY}" +} + setup_launcher_systemd_unit() { cp container-runner.service "${CS_PATH}/container-runner.service" cp exit_script.sh "${CS_PATH}/exit_script.sh" @@ -90,6 +97,8 @@ main() { # Install container launcher entrypoint. configure_entrypoint "entrypoint.sh" + # Install experiment client. + copy_experiment_client # Install container launcher. copy_launcher setup_launcher_systemd_unit diff --git a/launcher/internal/experiments/experiments.go b/launcher/internal/experiments/experiments.go new file mode 100644 index 000000000..1d6aec365 --- /dev/null +++ b/launcher/internal/experiments/experiments.go @@ -0,0 +1,42 @@ +// Package experiments contains functionalities to retrieve synced experiments +package experiments + +import ( + "encoding/json" + "fmt" + "os" +) + +// Experiments contains the experiments flags this version of the launcher expects to receive. +// Failure to unmarshal the experiment JSON data will result in an empty object being returned +// to treat experiment flags as their default value. The error should still be checked. +type Experiments struct { + EnableTestFeatureForImage bool + EnableSignedContainerImage bool +} + +// New takes a filepath, opens the file, and calls ReadJsonInput with the contents +// of the file. +// If the file cannot be opened, the experiments map is set to an empty map. +func New(fpath string) (Experiments, error) { + f, err := os.ReadFile(fpath) + if err != nil { + // Return default values on failure. + return Experiments{}, err + } + + r, err := readJSONInput(f) + + return r, err +} + +// ReadJSONInput takes a reader and unmarshals the contents into the experiments map. +// If the unmarsahlling fails, the experiments map is set to an empty map. +func readJSONInput(b []byte) (Experiments, error) { + var experiments Experiments + if err := json.Unmarshal(b, &experiments); err != nil { + // Return default values on failure. + return Experiments{}, fmt.Errorf("failed to unmarshal json: %w", err) + } + return experiments, nil +} diff --git a/launcher/internal/experiments/experiments_test.go b/launcher/internal/experiments/experiments_test.go new file mode 100644 index 000000000..656353a08 --- /dev/null +++ b/launcher/internal/experiments/experiments_test.go @@ -0,0 +1,52 @@ +package experiments + +import ( + "testing" +) + +func TestExperiments(t *testing.T) { + tests := []struct { + input string + }{ + {input: "{\"EnableTestFeatureForImage\":true,\"EnableSignedContainerImage\":true}"}, + {input: "{\"EnableTestFeatureForImage\":true,\"EnableSignedContainerImage\":true,\"FloatFeature\":-5.6,\"OtherTestFeatureForImage\":false}"}, + } + + for i, test := range tests { + e, err := readJSONInput([]byte(test.input)) + + if err != nil { + t.Errorf("testcase %d: failed to create experiments object: %v", i, err) + } + + if e.EnableTestFeatureForImage == false { + t.Errorf("testcase %d: expected EnableTestFeatureForImage to be true, got false", i) + } + + if e.EnableSignedContainerImage == false { + t.Errorf("testcase %d: expected EnableSignedContainerImage to be true, got false", i) + } + } +} + +func TestExperimentsBadJson(t *testing.T) { + tests := []struct { + input string + }{ + {input: "{\"EnableTestFeatureForImage\":true,\"EnableSignedContainerImage\":true"}, + {input: "{}"}, + {input: ""}, + } + + for i, test := range tests { + e, _ := readJSONInput([]byte(test.input)) + + if e.EnableTestFeatureForImage == true { + t.Errorf("testcase %d: expected EnableTestFeatureForImage to be false, got true", i) + } + + if e.EnableSignedContainerImage == true { + t.Errorf("testcase %d: expected EnableSignedContainerImage to be false, got true", i) + } + } +} diff --git a/launcher/launcher/main.go b/launcher/launcher/main.go index 636df3663..e68e5293c 100644 --- a/launcher/launcher/main.go +++ b/launcher/launcher/main.go @@ -9,6 +9,7 @@ import ( "log" "os" "os/exec" + "path" "regexp" "strings" @@ -18,6 +19,8 @@ import ( "github.com/containerd/containerd/namespaces" "github.com/google/go-tpm-tools/client" "github.com/google/go-tpm-tools/launcher" + "github.com/google/go-tpm-tools/launcher/internal/experiments" + "github.com/google/go-tpm-tools/launcher/launcherfile" "github.com/google/go-tpm-tools/launcher/spec" "github.com/google/go-tpm/legacy/tpm2" ) @@ -28,6 +31,10 @@ const ( // panic() returns 2 rebootRC = 3 // reboot holdRC = 4 // hold + // experimentDataFile defines where the experiment sync output data is expected to be. + experimentDataFile = "experiment_data" + // binaryPath contains the path to the experiments binary. + binaryPath = "/usr/share/oem/confidential_space/confidential_space_experiments" ) var rcMessage = map[int]string{ @@ -72,6 +79,21 @@ func main() { return } + experimentsFile := path.Join(launcherfile.HostTmpPath, experimentDataFile) + + args := fmt.Sprintf("-output=%s", experimentsFile) + err = exec.Command(binaryPath, args).Run() + if err != nil { + logger.Printf("failure during experiment sync: %v", err) + } + + e, err := experiments.New(experimentsFile) + if err != nil { + logger.Printf("failed to read experiment file %v\n", err) + // do not fail if experiment retrieval fails + } + launchSpec.Experiments = e + defer func() { // Catch panic to attempt to output to Cloud Logging. if r := recover(); r != nil { diff --git a/launcher/spec/launch_spec.go b/launcher/spec/launch_spec.go index 2a84ff093..c275bf5cb 100644 --- a/launcher/spec/launch_spec.go +++ b/launcher/spec/launch_spec.go @@ -9,6 +9,7 @@ import ( "strings" "cloud.google.com/go/compute/metadata" + "github.com/google/go-tpm-tools/launcher/internal/experiments" ) // RestartPolicy is the enum for the container restart policy. @@ -92,6 +93,7 @@ type LaunchSpec struct { Region string Hardened bool LogRedirect LogRedirectLocation + Experiments experiments.Experiments } // UnmarshalJSON unmarshals an instance attributes list in JSON format from the metadata From ee76386d009af76c955bb1462af1b829317c2f17 Mon Sep 17 00:00:00 2001 From: Joshua Krstic Date: Sun, 17 Sep 2023 19:09:24 -0700 Subject: [PATCH 4/6] Change directory that experiments data is saved to. Correct log messages --- launcher/launcher/main.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/launcher/launcher/main.go b/launcher/launcher/main.go index e68e5293c..6c798446a 100644 --- a/launcher/launcher/main.go +++ b/launcher/launcher/main.go @@ -79,17 +79,20 @@ func main() { return } + if err := os.MkdirAll(launcherfile.HostTmpPath, 0744); err != nil { + logger.Printf("failed to create %s: %v", launcherfile.HostTmpPath, err) + } experimentsFile := path.Join(launcherfile.HostTmpPath, experimentDataFile) args := fmt.Sprintf("-output=%s", experimentsFile) err = exec.Command(binaryPath, args).Run() if err != nil { - logger.Printf("failure during experiment sync: %v", err) + logger.Printf("failure during experiment sync: %v\n", err) } e, err := experiments.New(experimentsFile) if err != nil { - logger.Printf("failed to read experiment file %v\n", err) + logger.Printf("failed to read experiment file: %v\n", err) // do not fail if experiment retrieval fails } launchSpec.Experiments = e From cdf30f7cec71785eef41c5f040804320d4fe843a Mon Sep 17 00:00:00 2001 From: Alex Wu Date: Tue, 26 Sep 2023 14:44:44 -0700 Subject: [PATCH 5/6] launcher: Print kernel cmdline on builds (#268) Previously, we would need to pull logs from the server to see how exactly the cmdline gets passed through. Unfortunately, the cmdline passed by GRUB, and as a result, measured in the TCG event log, has surrounding quotes stripped out. So, we mutate the existing command line to what GRUB passes to the kernel. --- launcher/image/fixup_oem.sh | 11 ++++++++++- launcher/image/preload.sh | 4 ++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/launcher/image/fixup_oem.sh b/launcher/image/fixup_oem.sh index da3394799..fe6fbc80a 100644 --- a/launcher/image/fixup_oem.sh +++ b/launcher/image/fixup_oem.sh @@ -14,7 +14,16 @@ main() { sed -i -e 's|,oemroot|;oemroot|g' /mnt/disks/efi/efi/boot/grub.cfg fi - cat /mnt/disks/efi/efi/boot/grub.cfg + # Print grub.cfg's kernel command line. + grep -i '^\s*linux' /mnt/disks/efi/efi/boot/grub.cfg | \ + sed -e 's|.*|[BEGIN_CS_GRUB_CMDLINE]&[END_CS_GRUB_CMDLINE]|g' + + # Convert grub.cfg's kernel command line into what GRUB passes to the kernel. + grep -i '^\s*linux' /mnt/disks/efi/efi/boot/grub.cfg | \ + sed -e "s|'ds=nocloud;s=/usr/share/oem/'|ds=nocloud;s=/usr/share/oem/|g" | \ + sed -e 's|\\"|"|g' | \ + sed -e 's|dm-mod.create="|"dm-mod.create=|g' | \ + sed -e 's|.*|[BEGIN_CS_CMDLINE]&[END_CS_CMDLINE]|g' umount /mnt/disks/efi } diff --git a/launcher/image/preload.sh b/launcher/image/preload.sh index e1c7d9bec..a2759d43b 100644 --- a/launcher/image/preload.sh +++ b/launcher/image/preload.sh @@ -108,10 +108,10 @@ main() { if [[ "${IMAGE_ENV}" == "debug" ]]; then configure_systemd_units_for_debug - append_cmdline "'confidential-space.hardened=false'" + append_cmdline "confidential-space.hardened=false" elif [[ "${IMAGE_ENV}" == "hardened" ]]; then configure_systemd_units_for_hardened - append_cmdline "'confidential-space.hardened=true'" + append_cmdline "confidential-space.hardened=true" else echo "Unknown image env: ${IMAGE_ENV}." \ "Only 'debug' and 'hardened' are supported." From 7f600769f4d4373c1d72ef0868889ddbc8ad88c0 Mon Sep 17 00:00:00 2001 From: yawangwang Date: Tue, 3 Oct 2023 23:01:41 +0000 Subject: [PATCH 6/6] integrate signature discovery client into attestation agent (#343) --- .github/workflows/ci.yml | 4 + go.work.sum | 7 +- launcher/agent/agent.go | 58 +++- launcher/agent/agent_test.go | 261 +++++++++++++++--- launcher/container_runner.go | 13 +- launcher/internal/oci/cosign/fakesignature.go | 43 +++ .../internal/signaturediscovery/client.go | 9 +- .../signaturediscovery/client_test.go | 5 +- .../internal/signaturediscovery/fakeclient.go | 54 ++++ launcher/verifier/client.go | 12 +- launcher/verifier/fake/fakeclaims.go | 25 ++ launcher/verifier/fake/fakeverifier.go | 61 +++- launcher/verifier/rest/rest.go | 35 +++ launcher/verifier/rest/rest_network_test.go | 7 +- 14 files changed, 536 insertions(+), 58 deletions(-) create mode 100644 launcher/internal/oci/cosign/fakesignature.go create mode 100644 launcher/internal/signaturediscovery/fakeclient.go create mode 100644 launcher/verifier/fake/fakeclaims.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c7ca742c1..a30c8082b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,6 +68,10 @@ jobs: if: runner.os == 'Windows' - name: Build all modules run: go build -v ./... ./cmd/... ./launcher/... + - name: Run specific tests under root permission + run: | + GO_EXECUTABLE_PATH=$(which go) + sudo $GO_EXECUTABLE_PATH test -v -run "TestFetchImageSignaturesDockerPublic" ./launcher - name: Test all modules run: go test -v ./... ./cmd/... ./launcher/... diff --git a/go.work.sum b/go.work.sum index eb4d98fb8..9f839a179 100644 --- a/go.work.sum +++ b/go.work.sum @@ -72,6 +72,8 @@ cloud.google.com/go/language v1.9.0/go.mod h1:Ns15WooPM5Ad/5no/0n81yUetis74g3zrb cloud.google.com/go/lifesciences v0.8.0/go.mod h1:lFxiEOMqII6XggGbOnKiyZ7IBwoIqA84ClvoezaA/bo= cloud.google.com/go/logging v1.4.2 h1:Mu2Q75VBDQlW1HlBMjTX4X84UFR73G1TiLlRYc/b7tA= cloud.google.com/go/logging v1.4.2/go.mod h1:jco9QZSx8HiVVqLJReq7z7bVdj0P1Jb9PDFs63T+axo= +cloud.google.com/go/logging v1.7.0 h1:CJYxlNNNNAMkHp9em/YEXcfJg+rPDg7YfwoRpMU+t5I= +cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM= cloud.google.com/go/managedidentities v1.5.0/go.mod h1:+dWcZ0JlUmpuxpIDfyP5pP5y0bLdRwOS4Lp7gMni/LA= cloud.google.com/go/maps v0.7.0/go.mod h1:3GnvVl3cqeSvgMcpRlQidXsPYuDGQ8naBis7MVzpXsY= cloud.google.com/go/mediatranslation v0.7.0/go.mod h1:LCnB/gZr90ONOIQLgSXagp8XUW1ODs2UmUMvcgMfI2I= @@ -163,17 +165,13 @@ github.com/googleapis/go-type-adapters v1.0.0 h1:9XdMn+d/G57qq1s8dNc5IesGCXHf6V2 github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= github.com/josephlr/google-api-go-client v0.86.1 h1:nSNMjyd+GV04cF99zd5r/Ql5sZvA0ehya/Qtj/GbE68= github.com/josephlr/google-api-go-client v0.86.1/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= -github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/opencontainers/runc v1.1.5/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= github.com/prometheus/common v0.30.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= @@ -212,7 +210,6 @@ golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= diff --git a/launcher/agent/agent.go b/launcher/agent/agent.go index 72a2360bc..0e3f157e6 100644 --- a/launcher/agent/agent.go +++ b/launcher/agent/agent.go @@ -11,10 +11,15 @@ import ( "crypto" "fmt" "io" + "log" "net/http" + "sync" "github.com/google/go-tpm-tools/cel" "github.com/google/go-tpm-tools/client" + "github.com/google/go-tpm-tools/launcher/internal/oci" + "github.com/google/go-tpm-tools/launcher/internal/signaturediscovery" + "github.com/google/go-tpm-tools/launcher/spec" "github.com/google/go-tpm-tools/launcher/verifier" pb "github.com/google/go-tpm-tools/proto/attest" ) @@ -37,7 +42,10 @@ type agent struct { akFetcher tpmKeyFetcher client verifier.Client principalFetcher principalIDTokenFetcher + sigsFetcher signaturediscovery.Fetcher cosCel cel.CEL + launchSpec spec.LaunchSpec + logger *log.Logger } // CreateAttestationAgent returns an agent capable of performing remote @@ -45,12 +53,17 @@ type agent struct { // - tpm is a handle to the TPM on the instance // - akFetcher is a func to fetch an attestation key: see go-tpm-tools/client. // - principalFetcher is a func to fetch GCE principal tokens for a given audience. -func CreateAttestationAgent(tpm io.ReadWriteCloser, akFetcher tpmKeyFetcher, verifierClient verifier.Client, principalFetcher principalIDTokenFetcher) AttestationAgent { +// - signaturesFetcher is a func to fetch container image signatures associated with the running workload. +// - logger will log any partial errors returned by VerifyAttestation. +func CreateAttestationAgent(tpm io.ReadWriteCloser, akFetcher tpmKeyFetcher, verifierClient verifier.Client, principalFetcher principalIDTokenFetcher, sigsFetcher signaturediscovery.Fetcher, launchSpec spec.LaunchSpec, logger *log.Logger) AttestationAgent { return &agent{ tpm: tpm, client: verifierClient, akFetcher: akFetcher, principalFetcher: principalFetcher, + sigsFetcher: sigsFetcher, + launchSpec: launchSpec, + logger: logger, } } @@ -79,14 +92,27 @@ func (a *agent) Attest(ctx context.Context) ([]byte, error) { return nil, err } - resp, err := a.client.VerifyAttestation(ctx, verifier.VerifyAttestationRequest{ + req := verifier.VerifyAttestationRequest{ Challenge: challenge, GcpCredentials: principalTokens, Attestation: attestation, - }) + } + + if a.launchSpec.Experiments.EnableSignedContainerImage { + signatures := fetchContainerImageSignatures(ctx, a.sigsFetcher, a.launchSpec.SignedImageRepos, a.logger) + if len(signatures) > 0 { + req.ContainerImageSignatures = signatures + a.logger.Printf("Found container image signatures: %v\n", signatures) + } + } + + resp, err := a.client.VerifyAttestation(ctx, req) if err != nil { return nil, err } + if len(resp.PartialErrs) > 0 { + a.logger.Printf("Partial errors from VerifyAttestation: %v", resp.PartialErrs) + } return resp.ClaimsToken, nil } @@ -108,3 +134,29 @@ func (a *agent) getAttestation(nonce []byte) (*pb.Attestation, error) { } return attestation, nil } + +// TODO: cache signatures so we don't need to fetch every time. +func fetchContainerImageSignatures(ctx context.Context, fetcher signaturediscovery.Fetcher, targetRepos []string, logger *log.Logger) []oci.Signature { + signatures := make([][]oci.Signature, len(targetRepos)) + + var wg sync.WaitGroup + for i, repo := range targetRepos { + wg.Add(1) + go func(targetRepo string, index int) { + defer wg.Done() + sigs, err := fetcher.FetchImageSignatures(ctx, targetRepo) + if err != nil { + logger.Printf("Failed to fetch signatures from the target repo [%s]: %v", targetRepo, err) + } else { + signatures[index] = sigs + } + }(repo, i) + } + wg.Wait() + + var foundSigs []oci.Signature + for _, sigs := range signatures { + foundSigs = append(foundSigs, sigs...) + } + return foundSigs +} diff --git a/launcher/agent/agent_test.go b/launcher/agent/agent_test.go index 20a6b7a40..259b4142f 100644 --- a/launcher/agent/agent_test.go +++ b/launcher/agent/agent_test.go @@ -4,57 +4,254 @@ import ( "context" "crypto/rand" "crypto/rsa" + "encoding/base64" "fmt" + "log" "testing" "github.com/golang-jwt/jwt/v4" + "github.com/google/go-cmp/cmp" "github.com/google/go-tpm-tools/client" "github.com/google/go-tpm-tools/internal/test" + "github.com/google/go-tpm-tools/launcher/internal/experiments" + "github.com/google/go-tpm-tools/launcher/internal/signaturediscovery" + "github.com/google/go-tpm-tools/launcher/spec" + "github.com/google/go-tpm-tools/launcher/verifier" "github.com/google/go-tpm-tools/launcher/verifier/fake" ) func TestAttest(t *testing.T) { - tpm := test.GetTPM(t) - defer client.CheckedClose(t, tpm) - - fakeSigner, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - t.Errorf("Failed to generate signing key %v", err) + testCases := []struct { + name string + launchSpec spec.LaunchSpec + principalIDTokenFetcher func(string) ([][]byte, error) + containerSignaturesFetcher signaturediscovery.Fetcher + }{ + { + name: "all experiment flags disabled", + launchSpec: spec.LaunchSpec{}, + principalIDTokenFetcher: placeholderPrincipalFetcher, + containerSignaturesFetcher: signaturediscovery.NewFakeClient(), + }, + { + name: "enable signed container", + launchSpec: spec.LaunchSpec{ + SignedImageRepos: []string{signaturediscovery.FakeRepoWithSignatures}, + Experiments: experiments.Experiments{EnableSignedContainerImage: true}, + }, + principalIDTokenFetcher: placeholderPrincipalFetcher, + containerSignaturesFetcher: signaturediscovery.NewFakeClient(), + }, } - verifierClient := fake.NewClient(fakeSigner) - agent := CreateAttestationAgent(tpm, client.AttestationKeyECC, verifierClient, placeholderFetcher) - tokenBytes, err := agent.Attest(context.Background()) - if err != nil { - t.Errorf("failed to attest to Attestation Service: %v", err) - } + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() - registeredClaims := &jwt.RegisteredClaims{} - keyFunc := func(token *jwt.Token) (interface{}, error) { return fakeSigner.Public(), nil } - token, err := jwt.ParseWithClaims(string(tokenBytes), registeredClaims, keyFunc) - if err != nil { - t.Errorf("Failed to parse token %s", err) - } + tpm := test.GetTPM(t) + defer client.CheckedClose(t, tpm) - if err = registeredClaims.Valid(); err != nil { - t.Errorf("Invalid exp, iat, or nbf: %s", err) - } + fakeSigner, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Errorf("Failed to generate signing key %v", err) + } - if !registeredClaims.VerifyAudience("https://sts.googleapis.com/", true) { - t.Errorf("Invalid aud") - } + verifierClient := fake.NewClient(fakeSigner) - if !registeredClaims.VerifyIssuer("https://confidentialcomputing.googleapis.com/", true) { - t.Errorf("Invalid iss") - } + agent := CreateAttestationAgent(tpm, client.AttestationKeyECC, verifierClient, tc.principalIDTokenFetcher, tc.containerSignaturesFetcher, tc.launchSpec, log.Default()) - if registeredClaims.Subject != "https://www.googleapis.com/compute/v1/projects/fakeProject/zones/fakeZone/instances/fakeInstance" { - t.Errorf("Invalid sub") - } + tokenBytes, err := agent.Attest(context.Background()) + if err != nil { + t.Errorf("failed to attest to Attestation Service: %v", err) + } + + claims := &fake.Claims{} + keyFunc := func(token *jwt.Token) (interface{}, error) { return fakeSigner.Public(), nil } + token, err := jwt.ParseWithClaims(string(tokenBytes), claims, keyFunc) + if err != nil { + t.Errorf("Failed to parse token %s", err) + } + + if err = claims.Valid(); err != nil { + t.Errorf("Invalid exp, iat, or nbf: %s", err) + } + + if !claims.VerifyAudience("https://sts.googleapis.com/", true) { + t.Errorf("Invalid aud") + } - fmt.Printf("token.Claims: %v\n", token.Claims) + if !claims.VerifyIssuer("https://confidentialcomputing.googleapis.com/", true) { + t.Errorf("Invalid iss") + } + + if claims.Subject != "https://www.googleapis.com/compute/v1/projects/fakeProject/zones/fakeZone/instances/fakeInstance" { + t.Errorf("Invalid sub") + } + if tc.launchSpec.Experiments.EnableSignedContainerImage { + got := claims.ContainerImageSignatures + want := []fake.ContainerImageSignatureClaims{ + { + Payload: "test data", + Signature: base64.StdEncoding.EncodeToString([]byte("test data")), + PubKey: "test data", + SigAlg: "ECDSA_P256_SHA256", + }, + { + Payload: "hello world", + Signature: base64.StdEncoding.EncodeToString([]byte("hello world")), + PubKey: "hello world", + SigAlg: "RSASSA_PKCS1V15_SHA256", + }, + } + if !cmp.Equal(got, want) { + t.Errorf("ContainerImageSignatureClaims does not match expected value: got %v, want %v", got, want) + } + } + fmt.Printf("token.Claims: %v\n", token.Claims) + }) + } } -func placeholderFetcher(_ string) ([][]byte, error) { +func placeholderPrincipalFetcher(_ string) ([][]byte, error) { return [][]byte{}, nil } + +func TestFetchContainerImageSignatures(t *testing.T) { + ctx := context.Background() + + testCases := []struct { + name string + targetRepos []string + wantBase64Sigs []string + wantSignatureClaims []fake.ContainerImageSignatureClaims + wantPartialErrLen int + }{ + { + name: "fetchContainerImageSignatures with repos that have signatures", + targetRepos: []string{signaturediscovery.FakeRepoWithSignatures}, + wantBase64Sigs: []string{ + "dGVzdCBkYXRh", // base64 encoded "test data". + "aGVsbG8gd29ybGQ=", // base64 encoded "hello world". + }, + wantSignatureClaims: []fake.ContainerImageSignatureClaims{ + { + Payload: "test data", + Signature: base64.StdEncoding.EncodeToString([]byte("test data")), + PubKey: "test data", + SigAlg: "ECDSA_P256_SHA256", + }, + { + Payload: "hello world", + Signature: base64.StdEncoding.EncodeToString([]byte("hello world")), + PubKey: "hello world", + SigAlg: "RSASSA_PKCS1V15_SHA256", + }, + }, + wantPartialErrLen: 0, + }, + { + name: "fetchContainerImageSignatures with nil target repos", + targetRepos: nil, + wantBase64Sigs: nil, + wantSignatureClaims: nil, + wantPartialErrLen: 0, + }, + { + name: "fetchContainerImageSignatures with empty target repos", + targetRepos: []string{}, + wantBase64Sigs: nil, + wantSignatureClaims: nil, + wantPartialErrLen: 0, + }, + { + name: "fetchContainerImageSignatures with non exist repos", + targetRepos: []string{signaturediscovery.FakeNonExistRepo}, + wantBase64Sigs: nil, + wantSignatureClaims: nil, + wantPartialErrLen: 0, + }, + { + name: "fetchContainerImageSignatures with repos that don't have signatures", + targetRepos: []string{signaturediscovery.FakeRepoWithNoSignatures}, + wantBase64Sigs: nil, + wantSignatureClaims: nil, + wantPartialErrLen: 0, + }, + { + name: "fetchContainerImageSignatures with repos that have all invalid signatures", + targetRepos: []string{signaturediscovery.FakeRepoWithAllInvalidSignatures}, + wantBase64Sigs: []string{ + "aW52YWxpZCBzaWduYXR1cmU=", // base64 encoded "invalid signature". + "aW52YWxpZCBzaWduYXR1cmU=", // base64 encoded "invalid signature". + }, + wantSignatureClaims: nil, + wantPartialErrLen: 2, + }, + { + name: "fetchContainerImageSignatures with repos that have partial valid signatures", + targetRepos: []string{signaturediscovery.FakeRepoWithPartialValidSignatures}, + wantBase64Sigs: []string{ + "dGVzdCBkYXRh", // base64 encoded "test data". + "aW52YWxpZCBzaWduYXR1cmU=", // base64 encoded "invalid signature". + }, + wantSignatureClaims: []fake.ContainerImageSignatureClaims{ + { + Payload: "test data", + Signature: base64.StdEncoding.EncodeToString([]byte("test data")), + PubKey: "test data", + SigAlg: "ECDSA_P256_SHA256", + }, + }, + wantPartialErrLen: 1, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + sdClient := signaturediscovery.NewFakeClient() + gotSigs := fetchContainerImageSignatures(ctx, sdClient, tc.targetRepos, log.Default()) + if len(gotSigs) != len(tc.wantBase64Sigs) { + t.Errorf("fetchContainerImageSignatures did not return expected signatures for test case %s, got signatures length %d, but want %d", tc.name, len(gotSigs), len(tc.wantBase64Sigs)) + } + var gotBase64Sigs []string + for _, gotSig := range gotSigs { + base64Sig, err := gotSig.Base64Encoded() + if err != nil { + t.Fatalf("fetchContainerImageSignatures did not return expected base64 signatures for test case %s: %v", tc.name, err) + } + gotBase64Sigs = append(gotBase64Sigs, base64Sig) + } + if !cmp.Equal(gotBase64Sigs, tc.wantBase64Sigs) { + t.Errorf("fetchContainerImageSignatures did not return expected signatures for test case %s, got signatures %v, but want %v", tc.name, gotBase64Sigs, tc.wantBase64Sigs) + } + + fakeSigner, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Errorf("Failed to generate signing key %v", err) + } + verifierClient := fake.NewClient(fakeSigner) + req := verifier.VerifyAttestationRequest{ + ContainerImageSignatures: gotSigs, + } + got, err := verifierClient.VerifyAttestation(context.Background(), req) + if err != nil { + t.Fatalf("VerifyAttestation failed: %v", err) + } + claims := &fake.Claims{} + keyFunc := func(token *jwt.Token) (interface{}, error) { return fakeSigner.Public(), nil } + _, err = jwt.ParseWithClaims(string(got.ClaimsToken), claims, keyFunc) + if err != nil { + t.Errorf("Failed to parse token %s", err) + } + + gotSignatureClaims := claims.ContainerImageSignatures + if !cmp.Equal(gotSignatureClaims, tc.wantSignatureClaims) { + t.Errorf("ContainerImageSignatureClaims does not match expected value: got %v, want %v", gotSignatureClaims, tc.wantSignatureClaims) + } + if len(got.PartialErrs) != tc.wantPartialErrLen { + t.Errorf("VerifyAttestation did not return expected partial error length for test case %s, got partial errors length %d, but want %d", tc.name, len(got.ClaimsToken), tc.wantPartialErrLen) + } + }) + } +} diff --git a/launcher/container_runner.go b/launcher/container_runner.go index 173b7cf72..6105230e1 100644 --- a/launcher/container_runner.go +++ b/launcher/container_runner.go @@ -29,6 +29,7 @@ import ( "github.com/google/go-tpm-tools/cel" "github.com/google/go-tpm-tools/client" "github.com/google/go-tpm-tools/launcher/agent" + "github.com/google/go-tpm-tools/launcher/internal/signaturediscovery" "github.com/google/go-tpm-tools/launcher/launcherfile" "github.com/google/go-tpm-tools/launcher/spec" "github.com/google/go-tpm-tools/launcher/verifier" @@ -230,15 +231,25 @@ func NewRunner(ctx context.Context, cdClient *containerd.Client, token oauth2.To return nil, fmt.Errorf("failed to create REST verifier client: %v", err) } + // Create a new signaturediscovery cleint to fetch signatures. + sdClient := getSignatureDiscoveryClient(cdClient, token, image.Target()) return &ContainerRunner{ container, launchSpec, - agent.CreateAttestationAgent(tpm, client.GceAttestationKeyECC, verifierClient, principalFetcher), + agent.CreateAttestationAgent(tpm, client.GceAttestationKeyECC, verifierClient, principalFetcher, sdClient, launchSpec, logger), logger, serialConsole, }, nil } +func getSignatureDiscoveryClient(cdClient *containerd.Client, token oauth2.Token, imageDesc v1.Descriptor) signaturediscovery.Fetcher { + var remoteOpt containerd.RemoteOpt + if token.Valid() { + remoteOpt = containerd.WithResolver(Resolver(token.AccessToken)) + } + return signaturediscovery.New(cdClient, imageDesc, remoteOpt) +} + // getRESTClient returns a REST verifier.Client that points to the given address. // It defaults to the Attestation Verifier instance at // https://confidentialcomputing.googleapis.com. diff --git a/launcher/internal/oci/cosign/fakesignature.go b/launcher/internal/oci/cosign/fakesignature.go new file mode 100644 index 000000000..db16a4ec2 --- /dev/null +++ b/launcher/internal/oci/cosign/fakesignature.go @@ -0,0 +1,43 @@ +package cosign + +import ( + "encoding/base64" + "fmt" + + "github.com/google/go-tpm-tools/launcher/internal/oci" +) + +type fakeSig struct { + data string + sigAlg oci.SigningAlgorithm +} + +// NewFakeSignature constructs a new fake oci.Signature given data and signature algorithm. +func NewFakeSignature(data string, sigAlg oci.SigningAlgorithm) oci.Signature { + return &fakeSig{data, sigAlg} +} + +// Payload returns a fake payload. +func (f fakeSig) Payload() ([]byte, error) { + return []byte(f.data), nil +} + +// Base64Encoded returns a fake base64 encoded signature. +func (f fakeSig) Base64Encoded() (string, error) { + return base64.StdEncoding.EncodeToString([]byte(f.data)), nil +} + +// PublicKey returns a fake public key. +func (f fakeSig) PublicKey() ([]byte, error) { + return []byte(f.data), nil +} + +// SigningAlgorithm returns a fake signature algorithm. +func (f fakeSig) SigningAlgorithm() (oci.SigningAlgorithm, error) { + switch f.sigAlg { + case oci.ECDSAP256SHA256, oci.RSASSAPKCS1V152048SHA256, oci.RSASSAPSS2048SHA256: + return f.sigAlg, nil + default: + return "", fmt.Errorf("unsupported signing algorithm: %v", f.sigAlg) + } +} diff --git a/launcher/internal/signaturediscovery/client.go b/launcher/internal/signaturediscovery/client.go index cdc9741bb..550d62879 100644 --- a/launcher/internal/signaturediscovery/client.go +++ b/launcher/internal/signaturediscovery/client.go @@ -15,6 +15,11 @@ import ( const signatureTagSuffix = "sig" +// Fetcher discovers and fetches OCI signatures from the target repository. +type Fetcher interface { + FetchImageSignatures(ctx context.Context, targetRepository string) ([]oci.Signature, error) +} + // Client is a wrapper of containerd.Client to interact with signed image manifest. type Client struct { cdClient *containerd.Client @@ -22,8 +27,8 @@ type Client struct { RemoteOpts []containerd.RemoteOpt } -// New creates a new client. -func New(cdClient *containerd.Client, originalImageDesc v1.Descriptor, opts ...containerd.RemoteOpt) *Client { +// New creates a new client that implements Fetcher interface. +func New(cdClient *containerd.Client, originalImageDesc v1.Descriptor, opts ...containerd.RemoteOpt) Fetcher { return &Client{ cdClient: cdClient, OriginalImageDesc: originalImageDesc, diff --git a/launcher/internal/signaturediscovery/client_test.go b/launcher/internal/signaturediscovery/client_test.go index 1205eb126..fe740de4a 100644 --- a/launcher/internal/signaturediscovery/client_test.go +++ b/launcher/internal/signaturediscovery/client_test.go @@ -99,5 +99,8 @@ func createTestClient(t *testing.T, originalImageDesc v1.Descriptor) *Client { t.Skipf("test needs containerd daemon: %v", err) } t.Cleanup(func() { containerdClient.Close() }) - return New(containerdClient, originalImageDesc) + return &Client{ + cdClient: containerdClient, + OriginalImageDesc: originalImageDesc, + } } diff --git a/launcher/internal/signaturediscovery/fakeclient.go b/launcher/internal/signaturediscovery/fakeclient.go new file mode 100644 index 000000000..6d9b4da89 --- /dev/null +++ b/launcher/internal/signaturediscovery/fakeclient.go @@ -0,0 +1,54 @@ +package signaturediscovery + +import ( + "context" + "fmt" + + "github.com/google/go-tpm-tools/launcher/internal/oci" + "github.com/google/go-tpm-tools/launcher/internal/oci/cosign" +) + +const ( + // FakeRepoWithSignatures represents an OCI registry with container image signatures for testing. + FakeRepoWithSignatures = "repo with signatures" + // FakeRepoWithNoSignatures represents an OCI registry with no container image signatures for testing. + FakeRepoWithNoSignatures = "repo with no signatures" + // FakeNonExistRepo represents a non-exist OCI registry for testing. + FakeNonExistRepo = "nonexist repo" + // FakeRepoWithAllInvalidSignatures represents an OCI registry with all invalid container image signatures for testing. + FakeRepoWithAllInvalidSignatures = "repo with all invalid signatures" + // FakeRepoWithPartialValidSignatures represents an OCI registry with parital valid container image signatures for testing. + FakeRepoWithPartialValidSignatures = "repo with parital valid signatures" +) + +type fakeClient struct{} + +// NewFakeClient constructs a new fake signature discovery client. +func NewFakeClient() Fetcher { + return &fakeClient{} +} + +// FetchImageSignatures returns hardcoded signatures based on the given target repository. +func (f *fakeClient) FetchImageSignatures(_ context.Context, targetRepository string) ([]oci.Signature, error) { + switch targetRepository { + case FakeRepoWithSignatures: + return []oci.Signature{ + cosign.NewFakeSignature("test data", oci.ECDSAP256SHA256), + cosign.NewFakeSignature("hello world", oci.RSASSAPKCS1V152048SHA256), + }, nil + case FakeRepoWithNoSignatures, FakeNonExistRepo: + return nil, fmt.Errorf("cannot fetch the signature object from target repository [%s]", targetRepository) + case FakeRepoWithAllInvalidSignatures: + return []oci.Signature{ + cosign.NewFakeSignature("invalid signature", "unsupported"), + cosign.NewFakeSignature("invalid signature", "unsupported"), + }, nil + case FakeRepoWithPartialValidSignatures: + return []oci.Signature{ + cosign.NewFakeSignature("test data", oci.ECDSAP256SHA256), + cosign.NewFakeSignature("invalid signature", "unsupported"), + }, nil + default: + return []oci.Signature{}, nil + } +} diff --git a/launcher/verifier/client.go b/launcher/verifier/client.go index c851139ce..15469de5b 100644 --- a/launcher/verifier/client.go +++ b/launcher/verifier/client.go @@ -5,7 +5,9 @@ package verifier import ( "context" + "github.com/google/go-tpm-tools/launcher/internal/oci" attestpb "github.com/google/go-tpm-tools/proto/attest" + "google.golang.org/genproto/googleapis/rpc/status" ) // Client is a common interface to various attestation verifiers. @@ -25,15 +27,17 @@ type Challenge struct { // VerifyAttestationRequest is passed in on VerifyAttestation. It contains the // Challenge from CreateChallenge, optional GcpCredentials linked to the -// attestation, and the Attestation generated from the TPM. +// attestation, the Attestation generated from the TPM, and optional container image signatures associated with the workload. type VerifyAttestationRequest struct { - Challenge *Challenge - GcpCredentials [][]byte - Attestation *attestpb.Attestation + Challenge *Challenge + GcpCredentials [][]byte + Attestation *attestpb.Attestation + ContainerImageSignatures []oci.Signature } // VerifyAttestationResponse is the response from a successful // VerifyAttestation call. type VerifyAttestationResponse struct { ClaimsToken []byte + PartialErrs []*status.Status } diff --git a/launcher/verifier/fake/fakeclaims.go b/launcher/verifier/fake/fakeclaims.go new file mode 100644 index 000000000..7fd538b64 --- /dev/null +++ b/launcher/verifier/fake/fakeclaims.go @@ -0,0 +1,25 @@ +package fake + +import "github.com/golang-jwt/jwt/v4" + +// Verify that Claims implements jwt.Claims. +var _ jwt.Claims = Claims{} + +// Claims contains information to be formatted into a fake JWT. +type Claims struct { + jwt.RegisteredClaims + ContainerImageSignatures []ContainerImageSignatureClaims `json:"container_image_signatures"` +} + +// ContainerImageSignatureClaims contains claims about a container image signature. +type ContainerImageSignatureClaims struct { + Payload string `json:"payload"` + Signature string `json:"signature"` + PubKey string `json:"public_key"` + SigAlg string `json:"signature_algorithm"` +} + +// Valid is necessary to implement the jwt.Claims interface. +func (c Claims) Valid() error { + return nil +} diff --git a/launcher/verifier/fake/fakeverifier.go b/launcher/verifier/fake/fakeverifier.go index 2d56b5b5e..ee28c3ca0 100644 --- a/launcher/verifier/fake/fakeverifier.go +++ b/launcher/verifier/fake/fakeverifier.go @@ -8,7 +8,11 @@ import ( "time" "github.com/golang-jwt/jwt/v4" + "github.com/google/go-tpm-tools/launcher/internal/oci" "github.com/google/go-tpm-tools/launcher/verifier" + "go.uber.org/multierr" + "google.golang.org/genproto/googleapis/rpc/code" + "google.golang.org/genproto/googleapis/rpc/status" ) type fakeClient struct { @@ -37,19 +41,33 @@ func (fc *fakeClient) CreateChallenge(_ context.Context) (*verifier.Challenge, e // // If you have found this method is insufficient for your tests, this class must be updated to // allow for better testing. -func (fc *fakeClient) VerifyAttestation(_ context.Context, _ verifier.VerifyAttestationRequest) (*verifier.VerifyAttestationResponse, error) { +func (fc *fakeClient) VerifyAttestation(_ context.Context, req verifier.VerifyAttestationRequest) (*verifier.VerifyAttestationResponse, error) { // Determine signing algorithm. signingMethod := jwt.SigningMethodRS256 now := jwt.TimeFunc() - claims := jwt.RegisteredClaims{ - IssuedAt: &jwt.NumericDate{Time: now}, - NotBefore: &jwt.NumericDate{Time: now}, - ExpiresAt: &jwt.NumericDate{Time: now.Add(time.Hour)}, - Audience: []string{"https://sts.googleapis.com/"}, - Issuer: "https://confidentialcomputing.googleapis.com/", - Subject: "https://www.googleapis.com/compute/v1/projects/fakeProject/zones/fakeZone/instances/fakeInstance", + claims := Claims{ + RegisteredClaims: jwt.RegisteredClaims{ + IssuedAt: &jwt.NumericDate{Time: now}, + NotBefore: &jwt.NumericDate{Time: now}, + ExpiresAt: &jwt.NumericDate{Time: now.Add(time.Hour)}, + Audience: []string{"https://sts.googleapis.com/"}, + Issuer: "https://confidentialcomputing.googleapis.com/", + Subject: "https://www.googleapis.com/compute/v1/projects/fakeProject/zones/fakeZone/instances/fakeInstance", + }, } + var signatureClaims []ContainerImageSignatureClaims + var partialErrs []*status.Status + for _, signature := range req.ContainerImageSignatures { + sc, err := verifyContainerImageSignature(signature) + if err != nil { + partialErrs = append(partialErrs, &status.Status{Code: int32(code.Code_INVALID_ARGUMENT), Message: err.Error()}) + } else { + signatureClaims = append(signatureClaims, sc) + } + } + claims.ContainerImageSignatures = signatureClaims + token := jwt.NewWithClaims(signingMethod, claims) // Instead of a private key, provide the signer. @@ -60,7 +78,34 @@ func (fc *fakeClient) VerifyAttestation(_ context.Context, _ verifier.VerifyAtte response := verifier.VerifyAttestationResponse{ ClaimsToken: []byte(signed), + PartialErrs: partialErrs, } return &response, nil } + +func verifyContainerImageSignature(signature oci.Signature) (ContainerImageSignatureClaims, error) { + var err error + payload, e := signature.Payload() + if e != nil { + err = multierr.Append(err, e) + } + b64Sig, e := signature.Base64Encoded() + if e != nil { + err = multierr.Append(err, e) + } + pubKey, e := signature.PublicKey() + if e != nil { + err = multierr.Append(err, e) + } + sigAlg, e := signature.SigningAlgorithm() + if e != nil { + err = multierr.Append(err, e) + } + return ContainerImageSignatureClaims{ + Payload: string(payload), + Signature: b64Sig, + PubKey: string(pubKey), + SigAlg: string(sigAlg), + }, err +} diff --git a/launcher/verifier/rest/rest.go b/launcher/verifier/rest/rest.go index 5b492ecee..22fea85a4 100644 --- a/launcher/verifier/rest/rest.go +++ b/launcher/verifier/rest/rest.go @@ -5,8 +5,10 @@ import ( "context" "encoding/base64" "fmt" + "log" "strings" + "github.com/google/go-tpm-tools/launcher/internal/oci" "github.com/google/go-tpm-tools/launcher/verifier" v1 "cloud.google.com/go/confidentialcomputing/apiv1" @@ -154,6 +156,16 @@ func convertRequestToREST(request verifier.VerifyAttestationRequest) *confidenti certs[i] = cert } + signatures := make([]*confidentialcomputingpb.ContainerImageSignature, len(request.ContainerImageSignatures)) + for i, sig := range request.ContainerImageSignatures { + signature, err := convertOCISignatureToREST(sig) + if err != nil { + log.Printf("failed to convert OCI signature [%v] to ContainerImageSignature proto: %v", sig, err) + continue + } + signatures[i] = signature + } + return &confidentialcomputingpb.VerifyAttestationRequest{ GcpCredentials: &confidentialcomputingpb.GcpCredentials{ ServiceAccountIdTokens: idTokens, @@ -165,6 +177,9 @@ func convertRequestToREST(request verifier.VerifyAttestationRequest) *confidenti AkCert: request.Attestation.GetAkCert(), CertChain: certs, }, + ConfidentialSpaceInfo: &confidentialcomputingpb.ConfidentialSpaceInfo{ + SignedEntities: []*confidentialcomputingpb.SignedEntity{{ContainerImageSignatures: signatures}}, + }, } } @@ -172,5 +187,25 @@ func convertResponseFromREST(resp *confidentialcomputingpb.VerifyAttestationResp token := []byte(resp.GetOidcClaimsToken()) return &verifier.VerifyAttestationResponse{ ClaimsToken: token, + PartialErrs: resp.PartialErrors, + }, nil +} + +func convertOCISignatureToREST(signature oci.Signature) (*confidentialcomputingpb.ContainerImageSignature, error) { + payload, err := signature.Payload() + if err != nil { + return nil, err + } + b64Sig, err := signature.Base64Encoded() + if err != nil { + return nil, err + } + sigBytes, err := encoding.DecodeString(b64Sig) + if err != nil { + return nil, err + } + return &confidentialcomputingpb.ContainerImageSignature{ + Payload: payload, + Signature: sigBytes, }, nil } diff --git a/launcher/verifier/rest/rest_network_test.go b/launcher/verifier/rest/rest_network_test.go index b5649c925..bb56688b3 100644 --- a/launcher/verifier/rest/rest_network_test.go +++ b/launcher/verifier/rest/rest_network_test.go @@ -2,11 +2,14 @@ package rest import ( "context" + "log" "testing" "github.com/google/go-tpm-tools/client" "github.com/google/go-tpm-tools/internal/test" "github.com/google/go-tpm-tools/launcher/agent" + "github.com/google/go-tpm-tools/launcher/internal/signaturediscovery" + "github.com/google/go-tpm-tools/launcher/spec" "github.com/google/go-tpm-tools/launcher/verifier" "golang.org/x/oauth2/google" "google.golang.org/api/option" @@ -36,7 +39,7 @@ func testClient(t *testing.T) verifier.Client { return vClient } -func testFetcher(_ string) ([][]byte, error) { +func testPrincipalIDTokenFetcher(_ string) ([][]byte, error) { return [][]byte{}, nil } @@ -46,7 +49,7 @@ func TestWithAgent(t *testing.T) { tpm := test.GetTPM(t) defer client.CheckedClose(t, tpm) - agent := agent.CreateAttestationAgent(tpm, client.AttestationKeyECC, vClient, testFetcher) + agent := agent.CreateAttestationAgent(tpm, client.AttestationKeyECC, vClient, testPrincipalIDTokenFetcher, signaturediscovery.NewFakeClient(), spec.LaunchSpec{}, log.Default()) token, err := agent.Attest(context.Background()) if err != nil { t.Errorf("failed to attest to Attestation Service: %v", err)