diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7853bfc32..7cd0d447e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,17 +71,21 @@ jobs: - name: Install Windows packages run: choco install openssl if: runner.os == 'Windows' - - name: Build all modules - run: go build -v ./... ./cmd/... ./launcher/... ./verifier/... + - name: Build all modules except launcher + run: go build -v ./... ./cmd/... ./verifier/... + - name: Build launcher module + run: go build -v ./launcher/... + if: runner.os == 'Linux' - name: Run specific tests under root permission run: | GO_EXECUTABLE_PATH=$(which go) sudo $GO_EXECUTABLE_PATH test -v -run "TestFetchImageSignaturesDockerPublic" ./launcher + if: runner.os == 'Linux' - name: Run all tests in launcher to capture potential data race run: go test -v -race ./launcher/... - if: (runner.os == 'Linux' || runner.os == 'macOS') && matrix.architecture == 'x64' - - name: Test all modules - run: go test -v ./... ./cmd/... ./launcher/... ./verifier/... -skip='TestCacheConcurrentSetGet|TestHwAttestationPass|TestHardwareAttestationPass' + if: (runner.os == 'Linux') && matrix.architecture == 'x64' + - name: Test all modules except launcher + run: go test -v ./... ./cmd/... ./verifier/... -skip='TestCacheConcurrentSetGet|TestHwAttestationPass|TestHardwareAttestationPass' lint: strategy: diff --git a/client/attest.go b/client/attest.go index 3ea022917..8e7be72a3 100644 --- a/client/attest.go +++ b/client/attest.go @@ -1,9 +1,7 @@ package client import ( - "crypto/x509" "fmt" - "io" "net/http" sabi "github.com/google/go-sev-guest/abi" @@ -11,14 +9,10 @@ import ( tg "github.com/google/go-tdx-guest/client" tabi "github.com/google/go-tdx-guest/client/linuxabi" tpb "github.com/google/go-tdx-guest/proto/tdx" + "github.com/google/go-tpm-tools/internal" pb "github.com/google/go-tpm-tools/proto/attest" ) -const ( - maxIssuingCertificateURLs = 3 - maxCertChainLength = 4 -) - // TEEDevice is an interface to add an attestation report from a TEE technology's // attestation driver or quote provider. type TEEDevice interface { @@ -49,6 +43,7 @@ type AttestOpts struct { // Currently, we only support PCR replay for PCRs orthogonal to those in the // firmware event log, where PCRs 0-9 and 14 are often measured. If the two // logs overlap, server-side verification using this library may fail. + // Deprecated: Manually populate the pb.Attestation instead. CanonicalEventLog []byte // If non-nil, will be used to fetch the AK certificate chain for validation. // Key.Attest() will construct the certificate chain by making GET requests to @@ -66,77 +61,6 @@ type AttestOpts struct { TEENonce []byte } -// Given a certificate, iterates through its IssuingCertificateURLs and returns -// the certificate that signed it. If the certificate lacks an -// IssuingCertificateURL, return nil. If fetching the certificates fails or the -// cert chain is malformed, return an error. -func fetchIssuingCertificate(client *http.Client, cert *x509.Certificate) (*x509.Certificate, error) { - // Check if we should event attempt fetching. - if cert == nil || len(cert.IssuingCertificateURL) == 0 { - return nil, nil - } - // For each URL, fetch and parse the certificate, then verify whether it signed cert. - // If successful, return the parsed certificate. If any step in this process fails, try the next url. - // If all the URLs fail, return the last error we got. - // TODO(Issue #169): Return a multi-error here - var lastErr error - for i, url := range cert.IssuingCertificateURL { - // Limit the number of attempts. - if i >= maxIssuingCertificateURLs { - break - } - resp, err := client.Get(url) - if err != nil { - lastErr = fmt.Errorf("failed to retrieve certificate at %v: %w", url, err) - continue - } - - if resp.StatusCode != http.StatusOK { - lastErr = fmt.Errorf("certificate retrieval from %s returned non-OK status: %v", url, resp.StatusCode) - continue - } - certBytes, err := io.ReadAll(resp.Body) - resp.Body.Close() - if err != nil { - lastErr = fmt.Errorf("failed to read response body from %s: %w", url, err) - continue - } - - parsedCert, err := x509.ParseCertificate(certBytes) - if err != nil { - lastErr = fmt.Errorf("failed to parse response from %s into a certificate: %w", url, err) - continue - } - - // Check if the parsed certificate signed the current one. - if err = cert.CheckSignatureFrom(parsedCert); err != nil { - lastErr = fmt.Errorf("parent certificate from %s did not sign child: %w", url, err) - continue - } - return parsedCert, nil - } - return nil, lastErr -} - -// Constructs the certificate chain for the key's certificate. -// If an error is encountered in the process, return what has been constructed so far. -func (k *Key) getCertificateChain(client *http.Client) ([][]byte, error) { - var certs [][]byte - currentCert := k.cert - for len(certs) <= maxCertChainLength { - issuingCert, err := fetchIssuingCertificate(client, currentCert) - if err != nil { - return nil, err - } - if issuingCert == nil { - return certs, nil - } - certs = append(certs, issuingCert.Raw) - currentCert = issuingCert - } - return nil, fmt.Errorf("max certificate chain length (%v) exceeded", maxCertChainLength) -} - // SevSnpQuoteProvider encapsulates the SEV-SNP attestation device to add its attestation report // to a pb.Attestation. type SevSnpQuoteProvider struct { @@ -383,12 +307,13 @@ func (k *Key) Attest(opts AttestOpts) (*pb.Attestation, error) { // Attempt to construct certificate chain. fetchIssuingCertificate checks if // AK cert is present and contains intermediate cert URLs. if opts.CertChainFetcher != nil { - attestation.IntermediateCerts, err = k.getCertificateChain(opts.CertChainFetcher) + attestation.IntermediateCerts, err = internal.GetCertificateChain(k.cert, opts.CertChainFetcher) if err != nil { return nil, fmt.Errorf("fetching certificate chain: %w", err) } } + // TODO: issues/504 this should be outside of this function, not related to TPM attestation if err := getTEEAttestationReport(&attestation, opts); err != nil { return nil, fmt.Errorf("collecting TEE attestation report: %w", err) } diff --git a/client/attest_network_test.go b/client/attest_network_test.go index 83a461c19..2f225c2e7 100644 --- a/client/attest_network_test.go +++ b/client/attest_network_test.go @@ -5,6 +5,7 @@ import ( "net/http" "testing" + "github.com/google/go-tpm-tools/internal" "github.com/google/go-tpm-tools/internal/test" pb "github.com/google/go-tpm-tools/proto/attest" "google.golang.org/protobuf/proto" @@ -24,9 +25,7 @@ func TestNetworkFetchIssuingCertificate(t *testing.T) { t.Fatalf("Error parsing AK Cert: %v", err) } - key := &Key{cert: akCert} - - certChain, err := key.getCertificateChain(externalClient) + certChain, err := internal.GetCertificateChain(akCert, externalClient) if err != nil { t.Error(err) } diff --git a/client/attest_test.go b/client/attest_test.go index 84b98ef2c..5da979080 100644 --- a/client/attest_test.go +++ b/client/attest_test.go @@ -2,10 +2,7 @@ package client import ( "bytes" - "crypto/rand" - "crypto/rsa" "crypto/x509" - "math/big" "net/http" "net/http/httptest" "strings" @@ -23,112 +20,8 @@ import ( var localClient = http.DefaultClient -// Returns an x509 Certificate with the provided issuingURL and signed with the provided parent certificate and key. -// If parentCert and parentKey are nil, the certificate will be self-signed. -func getTestCert(t *testing.T, issuingURL []string, parentCert *x509.Certificate, parentKey *rsa.PrivateKey) (*x509.Certificate, *rsa.PrivateKey) { - t.Helper() - - certKey, _ := rsa.GenerateKey(rand.Reader, 2048) - - template := &x509.Certificate{ - SerialNumber: big.NewInt(1), - NotBefore: time.Now(), - NotAfter: time.Now().AddDate(10, 0, 0), - KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, - BasicConstraintsValid: true, - IsCA: true, - MaxPathLenZero: true, - IssuingCertificateURL: issuingURL, - } - - if parentCert == nil && parentKey == nil { - parentCert = template - parentKey = certKey - } - - certBytes, err := x509.CreateCertificate(rand.Reader, template, parentCert, certKey.Public(), parentKey) - if err != nil { - t.Fatalf("Unable to create test certificate: %v", err) - } - - cert, err := x509.ParseCertificate(certBytes) - if err != nil { - t.Fatalf("Unable to parse test certificate: %v", err) - } - - return cert, certKey -} - -func TestFetchIssuingCertificateSucceeds(t *testing.T) { - testCA, caKey := getTestCert(t, nil, nil, nil) - - ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { - rw.WriteHeader(http.StatusOK) - rw.Write(testCA.Raw) - })) - defer ts.Close() - - leafCert, _ := getTestCert(t, []string{"invalid.URL", ts.URL}, testCA, caKey) - - cert, err := fetchIssuingCertificate(localClient, leafCert) - if err != nil || cert == nil { - t.Errorf("fetchIssuingCertificate() did not find valid intermediate cert: %v", err) - } -} - -func TestFetchIssuingCertificateReturnsErrorIfMalformedCertificateFound(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { - rw.WriteHeader(http.StatusOK) - rw.Write([]byte("these are some random bytes")) - })) - defer ts.Close() - - testCA, caKey := getTestCert(t, nil, nil, nil) - leafCert, _ := getTestCert(t, []string{ts.URL}, testCA, caKey) - - _, err := fetchIssuingCertificate(localClient, leafCert) - if err == nil { - t.Fatal("expected fetchIssuingCertificate to fail with malformed cert") - } -} - -func TestGetCertificateChainSucceeds(t *testing.T) { - // Create CA and corresponding server. - testCA, caKey := getTestCert(t, nil, nil, nil) - - caServer := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { - rw.WriteHeader(http.StatusOK) - rw.Write(testCA.Raw) - })) - - defer caServer.Close() - - // Create intermediate cert and corresponding server. - intermediateCert, intermediateKey := getTestCert(t, []string{caServer.URL}, testCA, caKey) - - intermediateServer := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { - rw.WriteHeader(http.StatusOK) - rw.Write(intermediateCert.Raw) - })) - defer intermediateServer.Close() - - // Create leaf cert. - leafCert, _ := getTestCert(t, []string{intermediateServer.URL}, intermediateCert, intermediateKey) - - key := &Key{cert: leafCert} - - certChain, err := key.getCertificateChain(localClient) - if err != nil { - t.Fatal(err) - } - if len(certChain) != 2 { - t.Fatalf("getCertificateChain did not return the expected number of certificates: got %v, want 2", len(certChain)) - } -} - func TestKeyAttestSucceedsWithCertChainRetrieval(t *testing.T) { - testCA, caKey := getTestCert(t, nil, nil, nil) + testCA, caKey := test.GetTestCert(t, nil, nil, nil) caServer := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { rw.WriteHeader(http.StatusOK) @@ -137,7 +30,7 @@ func TestKeyAttestSucceedsWithCertChainRetrieval(t *testing.T) { defer caServer.Close() - leafCert, _ := getTestCert(t, []string{caServer.URL}, testCA, caKey) + leafCert, _ := test.GetTestCert(t, []string{caServer.URL}, testCA, caKey) rwc := test.GetTPM(t) defer CheckedClose(t, rwc) @@ -173,7 +66,7 @@ func TestKeyAttestGetCertificateChainConditions(t *testing.T) { t.Fatalf("Failed to generate test AK: %v", err) } - akCert, _ := getTestCert(t, nil, nil, nil) + akCert, _ := test.GetTestCert(t, nil, nil, nil) testcases := []struct { name string diff --git a/cmd/go.mod b/cmd/go.mod index 1674fc3f4..1d5e3bbab 100644 --- a/cmd/go.mod +++ b/cmd/go.mod @@ -8,8 +8,8 @@ require ( github.com/containerd/containerd v1.7.16 github.com/golang-jwt/jwt/v4 v4.5.0 github.com/golang/protobuf v1.5.4 - github.com/google/go-sev-guest v0.11.1 - github.com/google/go-tdx-guest v0.3.1 + github.com/google/go-sev-guest v0.11.2-0.20241009005433-de2ac900e958 + github.com/google/go-tdx-guest v0.3.2-0.20240902060211-1f7f7b9b42b9 github.com/google/go-tpm v0.9.0 github.com/google/go-tpm-tools v0.4.4 github.com/google/go-tpm-tools/verifier v0.4.4 @@ -34,7 +34,8 @@ require ( github.com/google/certificate-transparency-go v1.1.2 // indirect github.com/google/gce-tcb-verifier v0.2.3-0.20240905212129-12f728a62786 // indirect github.com/google/go-attestation v0.5.1 // indirect - github.com/google/go-configfs-tsm v0.2.2 // indirect + github.com/google/go-configfs-tsm v0.3.3-0.20240919001351-b4b5b84fdcbc // indirect + github.com/google/go-eventlog v0.0.2-0.20241003021507-01bb555f7cba // indirect github.com/google/go-tspi v0.3.0 // indirect github.com/google/logger v1.1.1 // indirect github.com/google/s2a-go v0.1.7 // indirect @@ -42,7 +43,6 @@ require ( github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/googleapis/gax-go/v2 v2.13.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/pborman/uuid v1.2.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/pflag v1.0.5 // indirect diff --git a/cmd/go.sum b/cmd/go.sum index a95fc24a0..9a4d65d17 100644 --- a/cmd/go.sum +++ b/cmd/go.sum @@ -336,17 +336,19 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-configfs-tsm v0.2.2 h1:YnJ9rXIOj5BYD7/0DNnzs8AOp7UcvjfTvt215EWcs98= -github.com/google/go-configfs-tsm v0.2.2/go.mod h1:EL1GTDFMb5PZQWDviGfZV9n87WeGTR/JUg13RfwkgRo= +github.com/google/go-configfs-tsm v0.3.3-0.20240919001351-b4b5b84fdcbc h1:SG12DWUUM5igxm+//YX5Yq4vhdoRnOG9HkCodkOn+YU= +github.com/google/go-configfs-tsm v0.3.3-0.20240919001351-b4b5b84fdcbc/go.mod h1:EL1GTDFMb5PZQWDviGfZV9n87WeGTR/JUg13RfwkgRo= +github.com/google/go-eventlog v0.0.2-0.20241003021507-01bb555f7cba h1:05m5+kgZjxYUZrx3bZfkKHl6wkch+Khao6N21rFHInk= +github.com/google/go-eventlog v0.0.2-0.20241003021507-01bb555f7cba/go.mod h1:7huE5P8w2NTObSwSJjboHmB7ioBNblkijdzoVa2skfQ= github.com/google/go-github/v28 v28.1.1/go.mod h1:bsqJWQX05omyWVmc00nEUql9mhQyv38lDZ8kPZcQVoM= github.com/google/go-licenses v0.0.0-20210329231322-ce1d9163b77d/go.mod h1:+TYOmkVoJOpwnS0wfdsJCV9CoD5nJYsHoFk/0CrTK4M= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-replayers/grpcreplay v0.1.0/go.mod h1:8Ig2Idjpr6gifRd6pNVggX6TC1Zw6Jx74AKp7QNH2QE= github.com/google/go-replayers/httpreplay v0.1.0/go.mod h1:YKZViNhiGgqdBlUbI2MwGpq4pXxNmhJLPHQ7cv2b5no= -github.com/google/go-sev-guest v0.11.1 h1:gnww4U8fHV5DCPz4gykr1s8SEX1fFNcxCBy+vvXN24k= -github.com/google/go-sev-guest v0.11.1/go.mod h1:qBOfb+JmgsUI3aUyzQoGC13Kpp9zwLeWvuyXmA9q77w= -github.com/google/go-tdx-guest v0.3.1 h1:gl0KvjdsD4RrJzyLefDOvFOUH3NAJri/3qvaL5m83Iw= -github.com/google/go-tdx-guest v0.3.1/go.mod h1:/rc3d7rnPykOPuY8U9saMyEps0PZDThLk/RygXm04nE= +github.com/google/go-sev-guest v0.11.2-0.20241009005433-de2ac900e958 h1:GfnkFZNr80qFGLR/EY75zwk8puz8+frGj4iwPwnJbSU= +github.com/google/go-sev-guest v0.11.2-0.20241009005433-de2ac900e958/go.mod h1:8+UOtSaqVIZjJJ9DDmgRko3J/kNc6jI5KLHxoeao7cA= +github.com/google/go-tdx-guest v0.3.2-0.20240902060211-1f7f7b9b42b9 h1:hk7vjuJgvYnHMZYI0cIDSXiC5XBmOlzRotA5bJ7nb+c= +github.com/google/go-tdx-guest v0.3.2-0.20240902060211-1f7f7b9b42b9/go.mod h1:g/n8sKITIT9xRivBUbizo34DTsUm2nN2uU3A662h09g= 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= diff --git a/go.mod b/go.mod index d80707976..7530075eb 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/google/go-attestation v0.5.1 github.com/google/go-cmp v0.6.0 github.com/google/go-configfs-tsm v0.3.3-0.20240910040719-1cc7e25d9272 - github.com/google/go-sev-guest v0.11.1 + github.com/google/go-sev-guest v0.11.2-0.20241009005433-de2ac900e958 github.com/google/go-tdx-guest v0.3.2-0.20240902060211-1f7f7b9b42b9 github.com/google/go-tpm v0.9.0 github.com/google/logger v1.1.1 @@ -21,7 +21,6 @@ require ( github.com/google/go-tspi v0.3.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/pborman/uuid v1.2.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/spf13/cobra v1.8.0 // indirect github.com/spf13/pflag v1.0.5 // indirect diff --git a/go.sum b/go.sum index a1fa1a630..4a55b9d2e 100644 --- a/go.sum +++ b/go.sum @@ -315,8 +315,8 @@ github.com/google/go-licenses v0.0.0-20210329231322-ce1d9163b77d/go.mod h1:+TYOm github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-replayers/grpcreplay v0.1.0/go.mod h1:8Ig2Idjpr6gifRd6pNVggX6TC1Zw6Jx74AKp7QNH2QE= github.com/google/go-replayers/httpreplay v0.1.0/go.mod h1:YKZViNhiGgqdBlUbI2MwGpq4pXxNmhJLPHQ7cv2b5no= -github.com/google/go-sev-guest v0.11.1 h1:gnww4U8fHV5DCPz4gykr1s8SEX1fFNcxCBy+vvXN24k= -github.com/google/go-sev-guest v0.11.1/go.mod h1:qBOfb+JmgsUI3aUyzQoGC13Kpp9zwLeWvuyXmA9q77w= +github.com/google/go-sev-guest v0.11.2-0.20241009005433-de2ac900e958 h1:GfnkFZNr80qFGLR/EY75zwk8puz8+frGj4iwPwnJbSU= +github.com/google/go-sev-guest v0.11.2-0.20241009005433-de2ac900e958/go.mod h1:8+UOtSaqVIZjJJ9DDmgRko3J/kNc6jI5KLHxoeao7cA= github.com/google/go-tdx-guest v0.3.2-0.20240902060211-1f7f7b9b42b9 h1:hk7vjuJgvYnHMZYI0cIDSXiC5XBmOlzRotA5bJ7nb+c= github.com/google/go-tdx-guest v0.3.2-0.20240902060211-1f7f7b9b42b9/go.mod h1:g/n8sKITIT9xRivBUbizo34DTsUm2nN2uU3A662h09g= github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk= @@ -550,8 +550,6 @@ github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= -github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= -github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= diff --git a/go.work.sum b/go.work.sum index 73dd10603..b007031c8 100644 --- a/go.work.sum +++ b/go.work.sum @@ -694,6 +694,7 @@ github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZat github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-configfs-tsm v0.2.2/go.mod h1:EL1GTDFMb5PZQWDviGfZV9n87WeGTR/JUg13RfwkgRo= github.com/google/go-configfs-tsm v0.3.2/go.mod h1:EL1GTDFMb5PZQWDviGfZV9n87WeGTR/JUg13RfwkgRo= github.com/google/go-containerregistry v0.14.0/go.mod h1:aiJ2fp/SXvkWgmYHioXnbMdlgB8eXiiYOY55gfN91Wk= github.com/google/go-eventlog v0.0.1/go.mod h1:7huE5P8w2NTObSwSJjboHmB7ioBNblkijdzoVa2skfQ= diff --git a/internal/cert.go b/internal/cert.go new file mode 100644 index 000000000..a4a2cbb7c --- /dev/null +++ b/internal/cert.go @@ -0,0 +1,84 @@ +package internal + +import ( + "crypto/x509" + "fmt" + "io" + "net/http" +) + +const ( + maxIssuingCertificateURLs = 3 + maxCertChainLength = 4 +) + +// GetCertificateChain constructs the certificate chain for the key's certificate. +// If an error is encountered in the process, return what has been constructed so far. +func GetCertificateChain(cert *x509.Certificate, client *http.Client) ([][]byte, error) { + var certs [][]byte + currentCert := cert + for len(certs) <= maxCertChainLength { + issuingCert, err := fetchIssuingCertificate(client, currentCert) + if err != nil { + return nil, err + } + if issuingCert == nil { + return certs, nil + } + certs = append(certs, issuingCert.Raw) + currentCert = issuingCert + } + return nil, fmt.Errorf("max certificate chain length (%v) exceeded", maxCertChainLength) +} + +// Given a certificate, iterates through its IssuingCertificateURLs and returns +// the certificate that signed it. If the certificate lacks an +// IssuingCertificateURL, return nil. If fetching the certificates fails or the +// cert chain is malformed, return an error. +func fetchIssuingCertificate(client *http.Client, cert *x509.Certificate) (*x509.Certificate, error) { + // Check if we should event attempt fetching. + if cert == nil || len(cert.IssuingCertificateURL) == 0 { + return nil, nil + } + // For each URL, fetch and parse the certificate, then verify whether it signed cert. + // If successful, return the parsed certificate. If any step in this process fails, try the next url. + // If all the URLs fail, return the last error we got. + // TODO(Issue #169): Return a multi-error here + var lastErr error + for i, url := range cert.IssuingCertificateURL { + // Limit the number of attempts. + if i >= maxIssuingCertificateURLs { + break + } + resp, err := client.Get(url) + if err != nil { + lastErr = fmt.Errorf("failed to retrieve certificate at %v: %w", url, err) + continue + } + + if resp.StatusCode != http.StatusOK { + lastErr = fmt.Errorf("certificate retrieval from %s returned non-OK status: %v", url, resp.StatusCode) + continue + } + certBytes, err := io.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + lastErr = fmt.Errorf("failed to read response body from %s: %w", url, err) + continue + } + + parsedCert, err := x509.ParseCertificate(certBytes) + if err != nil { + lastErr = fmt.Errorf("failed to parse response from %s into a certificate: %w", url, err) + continue + } + + // Check if the parsed certificate signed the current one. + if err = cert.CheckSignatureFrom(parsedCert); err != nil { + lastErr = fmt.Errorf("parent certificate from %s did not sign child: %w", url, err) + continue + } + return parsedCert, nil + } + return nil, lastErr +} diff --git a/internal/cert_test.go b/internal/cert_test.go new file mode 100644 index 000000000..9a502b36e --- /dev/null +++ b/internal/cert_test.go @@ -0,0 +1,76 @@ +package internal + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/google/go-tpm-tools/internal/test" +) + +var localClient = http.DefaultClient + +func TestFetchIssuingCertificateSucceeds(t *testing.T) { + testCA, caKey := test.GetTestCert(t, nil, nil, nil) + + ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { + rw.WriteHeader(http.StatusOK) + rw.Write(testCA.Raw) + })) + defer ts.Close() + + leafCert, _ := test.GetTestCert(t, []string{"invalid.URL", ts.URL}, testCA, caKey) + + cert, err := fetchIssuingCertificate(localClient, leafCert) + if err != nil || cert == nil { + t.Errorf("fetchIssuingCertificate() did not find valid intermediate cert: %v", err) + } +} + +func TestFetchIssuingCertificateReturnsErrorIfMalformedCertificateFound(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { + rw.WriteHeader(http.StatusOK) + rw.Write([]byte("these are some random bytes")) + })) + defer ts.Close() + + testCA, caKey := test.GetTestCert(t, nil, nil, nil) + leafCert, _ := test.GetTestCert(t, []string{ts.URL}, testCA, caKey) + + _, err := fetchIssuingCertificate(localClient, leafCert) + if err == nil { + t.Fatal("expected fetchIssuingCertificate to fail with malformed cert") + } +} + +func TestGetCertificateChainSucceeds(t *testing.T) { + // Create CA and corresponding server. + testCA, caKey := test.GetTestCert(t, nil, nil, nil) + + caServer := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { + rw.WriteHeader(http.StatusOK) + rw.Write(testCA.Raw) + })) + + defer caServer.Close() + + // Create intermediate cert and corresponding server. + intermediateCert, intermediateKey := test.GetTestCert(t, []string{caServer.URL}, testCA, caKey) + + intermediateServer := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { + rw.WriteHeader(http.StatusOK) + rw.Write(intermediateCert.Raw) + })) + defer intermediateServer.Close() + + // Create leaf cert. + leafCert, _ := test.GetTestCert(t, []string{intermediateServer.URL}, intermediateCert, intermediateKey) + + certChain, err := GetCertificateChain(leafCert, localClient) + if err != nil { + t.Fatal(err) + } + if len(certChain) != 2 { + t.Fatalf("GetCertificateChain did not return the expected number of certificates: got %v, want 2", len(certChain)) + } +} diff --git a/internal/test/test_cert.go b/internal/test/test_cert.go new file mode 100644 index 000000000..0f45a277a --- /dev/null +++ b/internal/test/test_cert.go @@ -0,0 +1,47 @@ +package test + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "math/big" + "testing" + "time" +) + +// GetTestCert returns an x509 Certificate with the provided issuingURL and signed with the provided parent certificate and key. +// If parentCert and parentKey are nil, the certificate will be self-signed. +func GetTestCert(t *testing.T, issuingURL []string, parentCert *x509.Certificate, parentKey *rsa.PrivateKey) (*x509.Certificate, *rsa.PrivateKey) { + t.Helper() + + certKey, _ := rsa.GenerateKey(rand.Reader, 2048) + + template := &x509.Certificate{ + SerialNumber: big.NewInt(1), + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + IsCA: true, + MaxPathLenZero: true, + IssuingCertificateURL: issuingURL, + } + + if parentCert == nil && parentKey == nil { + parentCert = template + parentKey = certKey + } + + certBytes, err := x509.CreateCertificate(rand.Reader, template, parentCert, certKey.Public(), parentKey) + if err != nil { + t.Fatalf("Unable to create test certificate: %v", err) + } + + cert, err := x509.ParseCertificate(certBytes) + if err != nil { + t.Fatalf("Unable to parse test certificate: %v", err) + } + + return cert, certKey +} diff --git a/launcher/agent/agent.go b/launcher/agent/agent.go index 28aa4b49b..6a9c14ef1 100644 --- a/launcher/agent/agent.go +++ b/launcher/agent/agent.go @@ -13,12 +13,20 @@ import ( "io" "log" "net/http" + "os" "sync" "time" "github.com/cenkalti/backoff/v4" + "github.com/google/go-configfs-tsm/configfs/configfsi" + + "github.com/google/go-configfs-tsm/configfs/linuxtsm" + tg "github.com/google/go-tdx-guest/client" + tlabi "github.com/google/go-tdx-guest/client/linuxabi" + "github.com/google/go-tpm-tools/cel" "github.com/google/go-tpm-tools/client" + "github.com/google/go-tpm-tools/internal" "github.com/google/go-tpm-tools/launcher/internal/signaturediscovery" "github.com/google/go-tpm-tools/launcher/spec" pb "github.com/google/go-tpm-tools/proto/attest" @@ -37,10 +45,18 @@ type principalIDTokenFetcher func(audience string) ([][]byte, error) type AttestationAgent interface { MeasureEvent(cel.Content) error Attest(context.Context, AttestAgentOpts) ([]byte, error) + AttestationEvidence([]byte, string) (*evidence, error) Refresh(context.Context) error Close() error } +type attestRoot interface { + // Extend measures the cel content into a measurement register and appends to the CEL. + Extend(cel.Content, *cel.CEL) error + // Attest fetches a technology-specific quote from the root of trust. + Attest(nonce []byte) (any, error) +} + // AttestAgentOpts contains user generated options when calling the // VerifyAttestation API type AttestAgentOpts struct { @@ -50,13 +66,12 @@ type AttestAgentOpts struct { } type agent struct { - tpm io.ReadWriteCloser - tpmMu sync.Mutex + ar attestRoot + cosCel cel.CEL fetchedAK *client.Key client verifier.Client principalFetcher principalIDTokenFetcher sigsFetcher signaturediscovery.Fetcher - cosCel cel.CEL launchSpec spec.LaunchSpec logger *log.Logger sigsCache *sigsCache @@ -75,8 +90,8 @@ func CreateAttestationAgent(tpm io.ReadWriteCloser, akFetcher util.TpmKeyFetcher if err != nil { return nil, fmt.Errorf("failed to create an Attestation Agent: %w", err) } - return &agent{ - tpm: tpm, + + attestAgent := &agent{ client: verifierClient, fetchedAK: ak, principalFetcher: principalFetcher, @@ -84,7 +99,32 @@ func CreateAttestationAgent(tpm io.ReadWriteCloser, akFetcher util.TpmKeyFetcher launchSpec: launchSpec, logger: logger, sigsCache: &sigsCache{}, - }, nil + } + + // check if is a TDX machine + qp, err := tg.GetQuoteProvider() + if err != nil || qp.IsSupported() != nil { + logger.Println("Using TPM PCRs for measurement.") + // by default using TPM + attestAgent.ar = &tpmAttestRoot{ + fetchedAK: ak, + tpm: tpm, + } + } else { + logger.Println("Using TDX RTMRs for measurement.") + // try to create tsm client for tdx rtmr + tsm, err := linuxtsm.MakeClient() + if err != nil { + return nil, fmt.Errorf("failed to create TSM for TDX: %v", err) + } + + attestAgent.ar = &tdxAttestRoot{ + qp: qp, + tsmClient: tsm, + } + } + + return attestAgent, nil } // Close cleans up the agent @@ -96,9 +136,64 @@ func (a *agent) Close() error { // MeasureEvent takes in a cel.Content and appends it to the CEL eventlog // under the attestation agent. func (a *agent) MeasureEvent(event cel.Content) error { - a.tpmMu.Lock() - defer a.tpmMu.Unlock() - return a.cosCel.AppendEvent(a.tpm, cel.CosEventPCR, defaultCELHashAlgo, event) + return a.ar.Extend(event, &a.cosCel) +} + +type evidence struct { + TPMAttestation *pb.Attestation + TDXAttestation *verifier.TDCCELAttestation + PrincipalTokens [][]byte + ContainerSignatures []oci.Signature +} + +func (a *agent) AttestationEvidence(nonce []byte, principalAud string) (*evidence, error) { + attEvidence := &evidence{} + + var err error + attEvidence.PrincipalTokens, err = a.principalFetcher(principalAud) + if err != nil { + return nil, fmt.Errorf("failed to get principal tokens: %w", err) + } + + attResult, err := a.ar.Attest(nonce) + if err != nil { + return nil, fmt.Errorf("failed to attest: %v", err) + } + + var cosCel bytes.Buffer + if err := a.cosCel.EncodeCEL(&cosCel); err != nil { + return nil, err + } + + switch v := attResult.(type) { + case *pb.Attestation: + a.logger.Println("attestation through TPM quote") + + v.CanonicalEventLog = cosCel.Bytes() + attEvidence.TPMAttestation = v + case *verifier.TDCCELAttestation: + a.logger.Println("attestation through TDX quote") + + certChain, err := internal.GetCertificateChain(a.fetchedAK.Cert(), http.DefaultClient) + if err != nil { + return nil, fmt.Errorf("failed when fetching certificate chain: %w", err) + } + + v.CanonicalEventLog = cosCel.Bytes() + v.IntermediateCerts = certChain + v.AkCert = a.fetchedAK.CertDERBytes() + attEvidence.TDXAttestation = v + default: + return nil, fmt.Errorf("received an unsupported attestation type! %v", v) + } + + signatures := a.sigsCache.get() + if len(signatures) > 0 { + attEvidence.ContainerSignatures = signatures + a.logger.Printf("Found container image signatures: %v\n", signatures) + } + + return attEvidence, nil } // Attest fetches the nonce and connection ID from the Attestation Service, @@ -110,25 +205,17 @@ func (a *agent) Attest(ctx context.Context, opts AttestAgentOpts) ([]byte, error return nil, err } - principalTokens, err := a.principalFetcher(challenge.Name) + evidence, err := a.AttestationEvidence(challenge.Nonce, challenge.Name) if err != nil { - return nil, fmt.Errorf("failed to get principal tokens: %w", err) - } - - var buf bytes.Buffer - if err := a.cosCel.EncodeCEL(&buf); err != nil { return nil, err } - attestation, err := a.attest(challenge.Nonce, buf.Bytes()) - if err != nil { - return nil, fmt.Errorf("failed to attest: %v", err) - } - req := verifier.VerifyAttestationRequest{ - Challenge: challenge, - GcpCredentials: principalTokens, - Attestation: attestation, + Challenge: challenge, + GcpCredentials: evidence.PrincipalTokens, + Attestation: evidence.TPMAttestation, + TDCCELAttestation: evidence.TDXAttestation, + ContainerImageSignatures: evidence.ContainerSignatures, TokenOptions: verifier.TokenOptions{ CustomAudience: opts.Aud, CustomNonce: opts.Nonces, @@ -136,12 +223,6 @@ func (a *agent) Attest(ctx context.Context, opts AttestAgentOpts) ([]byte, error }, } - signatures := a.sigsCache.get() - 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 @@ -152,10 +233,62 @@ func (a *agent) Attest(ctx context.Context, opts AttestAgentOpts) ([]byte, error return resp.ClaimsToken, nil } -func (a *agent) attest(nonce []byte, cel []byte) (*pb.Attestation, error) { - a.tpmMu.Lock() - defer a.tpmMu.Unlock() - return a.fetchedAK.Attest(client.AttestOpts{Nonce: nonce, CanonicalEventLog: cel, CertChainFetcher: http.DefaultClient}) +type tpmAttestRoot struct { + tpmMu sync.Mutex + fetchedAK *client.Key + tpm io.ReadWriteCloser +} + +func (t *tpmAttestRoot) Extend(c cel.Content, l *cel.CEL) error { + return l.AppendEventPCR(t.tpm, cel.CosEventPCR, defaultCELHashAlgo, c) +} + +func (t *tpmAttestRoot) Attest(nonce []byte) (any, error) { + t.tpmMu.Lock() + defer t.tpmMu.Unlock() + + return t.fetchedAK.Attest(client.AttestOpts{ + Nonce: nonce, + CertChainFetcher: http.DefaultClient, + }) +} + +type tdxAttestRoot struct { + tdxMu sync.Mutex + qp *tg.LinuxConfigFsQuoteProvider + tsmClient configfsi.Client +} + +func (t *tdxAttestRoot) Extend(c cel.Content, l *cel.CEL) error { + return l.AppendEventRTMR(t.tsmClient, cel.CosRTMR, c) +} + +func (t *tdxAttestRoot) Attest(nonce []byte) (any, error) { + t.tdxMu.Lock() + defer t.tdxMu.Unlock() + + var tdxNonce [tlabi.TdReportDataSize]byte + copy(tdxNonce[:], nonce) + + rawQuote, err := tg.GetRawQuote(t.qp, tdxNonce) + if err != nil { + return nil, err + } + + ccelData, err := os.ReadFile("/sys/firmware/acpi/tables/data/CCEL") + if err != nil { + return nil, err + } + ccelTable, err := os.ReadFile("/sys/firmware/acpi/tables/CCEL") + if err != nil { + return nil, err + } + + return &verifier.TDCCELAttestation{ + CcelAcpiTable: ccelTable, + CcelData: ccelData, + TdQuote: rawQuote, + }, nil } // Refresh refreshes the internal state of the attestation agent. diff --git a/launcher/cloudbuild.yaml b/launcher/cloudbuild.yaml index ed9b2378d..791a90b29 100644 --- a/launcher/cloudbuild.yaml +++ b/launcher/cloudbuild.yaml @@ -1,7 +1,7 @@ substitutions: '_BASE_IMAGE': '' # an empty base image means the build will use the latest image in '_BASE_IMAGE_FAMILY' - '_BASE_IMAGE_FAMILY': 'cos-113-lts' # base image family - '_OUTPUT_IMAGE_PREFIX': 'confidential-space' + '_BASE_IMAGE_FAMILY': 'cos-tdx-113-lts' # base image family + '_OUTPUT_IMAGE_PREFIX': 'confidential-space' # in order '_OUTPUT_IMAGE_SUFFIX': '' '_OUTPUT_IMAGE_FAMILY': '' '_BUCKET_NAME': '${PROJECT_ID}_cloudbuild' @@ -21,7 +21,7 @@ steps: if [ -z ${base_image} ] then echo "getting the latest COS image" - base_image=$(gcloud compute images describe-from-family ${BASE_IMAGE_FAMILY} --project cos-cloud | grep name | cut -d ' ' -f 2) + base_image=$(gcloud compute images describe-from-family ${BASE_IMAGE_FAMILY} --project confidential-vm-images | grep name | cut -d ' ' -f 2) fi echo ${base_image} > /workspace/base_image.txt @@ -30,9 +30,9 @@ steps: id: DebugImageBuild waitFor: ['BaseImageIdent'] env: - - 'OUTPUT_IMAGE_PREFIX=$_OUTPUT_IMAGE_PREFIX' - - 'OUTPUT_IMAGE_SUFFIX=$_OUTPUT_IMAGE_SUFFIX' - - 'OUTPUT_IMAGE_FAMILY=$_OUTPUT_IMAGE_FAMILY' + - 'OUTPUT_IMAGE_PREFIX=${_OUTPUT_IMAGE_PREFIX}' + - 'OUTPUT_IMAGE_SUFFIX=${_OUTPUT_IMAGE_SUFFIX}' + - 'OUTPUT_IMAGE_FAMILY=${_OUTPUT_IMAGE_FAMILY}' - 'BUCKET_NAME=$_BUCKET_NAME' - 'SHORT_SHA=${SHORT_SHA}' script: | @@ -49,9 +49,9 @@ steps: id: HardenedImageBuild waitFor: ['BaseImageIdent'] env: - - 'OUTPUT_IMAGE_PREFIX=$_OUTPUT_IMAGE_PREFIX' - - 'OUTPUT_IMAGE_SUFFIX=$_OUTPUT_IMAGE_SUFFIX' - - 'OUTPUT_IMAGE_FAMILY=$_OUTPUT_IMAGE_FAMILY' + - 'OUTPUT_IMAGE_PREFIX=${_OUTPUT_IMAGE_PREFIX}' + - 'OUTPUT_IMAGE_SUFFIX=${_OUTPUT_IMAGE_SUFFIX}' + - 'OUTPUT_IMAGE_FAMILY=${_OUTPUT_IMAGE_FAMILY}' - 'BUCKET_NAME=$_BUCKET_NAME' - 'SHORT_SHA=${SHORT_SHA}' script: | @@ -68,8 +68,8 @@ steps: id: ExperimentsTests waitFor: ['DebugImageBuild'] env: - - 'OUTPUT_IMAGE_PREFIX=$_OUTPUT_IMAGE_PREFIX' - - 'OUTPUT_IMAGE_SUFFIX=$_OUTPUT_IMAGE_SUFFIX' + - 'OUTPUT_IMAGE_PREFIX=${_OUTPUT_IMAGE_PREFIX}' + - 'OUTPUT_IMAGE_SUFFIX=${_OUTPUT_IMAGE_SUFFIX}' - 'PROJECT_ID=$PROJECT_ID' script: | #!/usr/bin/env bash @@ -84,8 +84,8 @@ steps: id: HttpServerTests waitFor: ['DebugImageBuild'] env: - - 'OUTPUT_IMAGE_PREFIX=$_OUTPUT_IMAGE_PREFIX' - - 'OUTPUT_IMAGE_SUFFIX=$_OUTPUT_IMAGE_SUFFIX' + - 'OUTPUT_IMAGE_PREFIX=${_OUTPUT_IMAGE_PREFIX}' + - 'OUTPUT_IMAGE_SUFFIX=${_OUTPUT_IMAGE_SUFFIX}' - 'PROJECT_ID=$PROJECT_ID' script: | #!/usr/bin/env bash @@ -100,8 +100,8 @@ steps: id: DebugImageTests waitFor: ['DebugImageBuild'] env: - - 'OUTPUT_IMAGE_PREFIX=$_OUTPUT_IMAGE_PREFIX' - - 'OUTPUT_IMAGE_SUFFIX=$_OUTPUT_IMAGE_SUFFIX' + - 'OUTPUT_IMAGE_PREFIX=${_OUTPUT_IMAGE_PREFIX}' + - 'OUTPUT_IMAGE_SUFFIX=${_OUTPUT_IMAGE_SUFFIX}' - 'PROJECT_ID=$PROJECT_ID' script: | #!/usr/bin/env bash @@ -116,8 +116,8 @@ steps: id: HardenedImageTests waitFor: ['HardenedImageBuild'] env: - - 'OUTPUT_IMAGE_PREFIX=$_OUTPUT_IMAGE_PREFIX' - - 'OUTPUT_IMAGE_SUFFIX=$_OUTPUT_IMAGE_SUFFIX' + - 'OUTPUT_IMAGE_PREFIX=${_OUTPUT_IMAGE_PREFIX}' + - 'OUTPUT_IMAGE_SUFFIX=${_OUTPUT_IMAGE_SUFFIX}' - 'PROJECT_ID=$PROJECT_ID' script: | #!/usr/bin/env bash @@ -131,8 +131,8 @@ steps: id: LaunchPolicyTests waitFor: ['HardenedImageBuild'] env: - - 'OUTPUT_IMAGE_PREFIX=$_OUTPUT_IMAGE_PREFIX' - - 'OUTPUT_IMAGE_SUFFIX=$_OUTPUT_IMAGE_SUFFIX' + - 'OUTPUT_IMAGE_PREFIX=${_OUTPUT_IMAGE_PREFIX}' + - 'OUTPUT_IMAGE_SUFFIX=${_OUTPUT_IMAGE_SUFFIX}' - 'PROJECT_ID=$PROJECT_ID' script: | #!/usr/bin/env bash @@ -147,8 +147,8 @@ steps: id: HardenedNetworkIngressTests waitFor: ['HardenedImageBuild'] env: - - 'OUTPUT_IMAGE_PREFIX=$_OUTPUT_IMAGE_PREFIX' - - 'OUTPUT_IMAGE_SUFFIX=$_OUTPUT_IMAGE_SUFFIX' + - 'OUTPUT_IMAGE_PREFIX=${_OUTPUT_IMAGE_PREFIX}' + - 'OUTPUT_IMAGE_SUFFIX=${_OUTPUT_IMAGE_SUFFIX}' - 'PROJECT_ID=$PROJECT_ID' script: | #!/usr/bin/env bash @@ -161,8 +161,8 @@ steps: id: DebugNetworkIngressTests waitFor: ['DebugImageBuild'] env: - - 'OUTPUT_IMAGE_PREFIX=$_OUTPUT_IMAGE_PREFIX' - - 'OUTPUT_IMAGE_SUFFIX=$_OUTPUT_IMAGE_SUFFIX' + - 'OUTPUT_IMAGE_PREFIX=${_OUTPUT_IMAGE_PREFIX}' + - 'OUTPUT_IMAGE_SUFFIX=${_OUTPUT_IMAGE_SUFFIX}' - 'PROJECT_ID=$PROJECT_ID' script: | #!/usr/bin/env bash @@ -175,8 +175,8 @@ steps: id: LogRedirectionTests waitFor: ['HardenedImageBuild'] env: - - 'OUTPUT_IMAGE_PREFIX=$_OUTPUT_IMAGE_PREFIX' - - 'OUTPUT_IMAGE_SUFFIX=$_OUTPUT_IMAGE_SUFFIX' + - 'OUTPUT_IMAGE_PREFIX=${_OUTPUT_IMAGE_PREFIX}' + - 'OUTPUT_IMAGE_SUFFIX=${_OUTPUT_IMAGE_SUFFIX}' - 'PROJECT_ID=$PROJECT_ID' script: | #!/usr/bin/env bash @@ -190,8 +190,8 @@ steps: id: HardenedDiscoverContainerSignatureTests waitFor: ['HardenedImageBuild'] env: - - 'OUTPUT_IMAGE_PREFIX=$_OUTPUT_IMAGE_PREFIX' - - 'OUTPUT_IMAGE_SUFFIX=$_OUTPUT_IMAGE_SUFFIX' + - 'OUTPUT_IMAGE_PREFIX=${_OUTPUT_IMAGE_PREFIX}' + - 'OUTPUT_IMAGE_SUFFIX=${_OUTPUT_IMAGE_SUFFIX}' - 'PROJECT_ID=$PROJECT_ID' script: | #!/usr/bin/env bash @@ -204,8 +204,8 @@ steps: id: DebugDiscoverContainerSignatureTests waitFor: ['DebugImageBuild'] env: - - 'OUTPUT_IMAGE_PREFIX=$_OUTPUT_IMAGE_PREFIX' - - 'OUTPUT_IMAGE_SUFFIX=$_OUTPUT_IMAGE_SUFFIX' + - 'OUTPUT_IMAGE_PREFIX=${_OUTPUT_IMAGE_PREFIX}' + - 'OUTPUT_IMAGE_SUFFIX=${_OUTPUT_IMAGE_SUFFIX}' - 'PROJECT_ID=$PROJECT_ID' script: | #!/usr/bin/env bash @@ -218,8 +218,8 @@ steps: id: MemoryMonitoringTests waitFor: ['HardenedImageBuild'] env: - - 'OUTPUT_IMAGE_PREFIX=$_OUTPUT_IMAGE_PREFIX' - - 'OUTPUT_IMAGE_SUFFIX=$_OUTPUT_IMAGE_SUFFIX' + - 'OUTPUT_IMAGE_PREFIX=${_OUTPUT_IMAGE_PREFIX}' + - 'OUTPUT_IMAGE_SUFFIX=${_OUTPUT_IMAGE_SUFFIX}' - 'PROJECT_ID=$PROJECT_ID' script: | #!/usr/bin/env bash @@ -232,8 +232,8 @@ steps: id: ODAWithSignedContainerTest waitFor: ['HardenedImageBuild'] env: - - 'OUTPUT_IMAGE_PREFIX=$_OUTPUT_IMAGE_PREFIX' - - 'OUTPUT_IMAGE_SUFFIX=$_OUTPUT_IMAGE_SUFFIX' + - 'OUTPUT_IMAGE_PREFIX=${_OUTPUT_IMAGE_PREFIX}' + - 'OUTPUT_IMAGE_SUFFIX=${_OUTPUT_IMAGE_SUFFIX}' - 'PROJECT_ID=$PROJECT_ID' script: | #!/usr/bin/env bash @@ -246,8 +246,8 @@ steps: id: MountTests waitFor: ['HardenedImageBuild'] env: - - 'OUTPUT_IMAGE_PREFIX=$_OUTPUT_IMAGE_PREFIX' - - 'OUTPUT_IMAGE_SUFFIX=$_OUTPUT_IMAGE_SUFFIX' + - 'OUTPUT_IMAGE_PREFIX=${_OUTPUT_IMAGE_PREFIX}' + - 'OUTPUT_IMAGE_SUFFIX=${_OUTPUT_IMAGE_SUFFIX}' - 'PROJECT_ID=$PROJECT_ID' script: | #!/usr/bin/env bash diff --git a/launcher/go.mod b/launcher/go.mod index a7da7f3be..48ec21ca7 100644 --- a/launcher/go.mod +++ b/launcher/go.mod @@ -4,11 +4,14 @@ go 1.21 require ( cloud.google.com/go/compute/metadata v0.5.0 + cloud.google.com/go/logging v1.10.0 github.com/cenkalti/backoff/v4 v4.2.1 github.com/containerd/containerd v1.7.16 github.com/coreos/go-systemd/v22 v22.5.0 github.com/golang-jwt/jwt/v4 v4.5.0 github.com/google/go-cmp v0.6.0 + github.com/google/go-configfs-tsm v0.3.3-0.20240919001351-b4b5b84fdcbc + github.com/google/go-tdx-guest v0.3.2-0.20241009005452-097ee70d0843 github.com/google/go-tpm v0.9.0 github.com/google/go-tpm-tools v0.4.4 github.com/google/go-tpm-tools/verifier v0.4.4 @@ -21,9 +24,11 @@ require ( ) require ( + cloud.google.com/go v0.115.0 // indirect cloud.google.com/go/auth v0.7.2 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.3 // indirect cloud.google.com/go/confidentialcomputing v1.6.0 // indirect + cloud.google.com/go/longrunning v0.5.9 // indirect github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect @@ -45,10 +50,8 @@ require ( github.com/google/certificate-transparency-go v1.1.2 // indirect github.com/google/gce-tcb-verifier v0.2.3-0.20240905212129-12f728a62786 // indirect github.com/google/go-attestation v0.5.1 // indirect - github.com/google/go-configfs-tsm v0.3.3-0.20240919001351-b4b5b84fdcbc // indirect github.com/google/go-eventlog v0.0.2-0.20241003021507-01bb555f7cba // indirect - github.com/google/go-sev-guest v0.11.1 // indirect - github.com/google/go-tdx-guest v0.3.2-0.20240902060211-1f7f7b9b42b9 // indirect + github.com/google/go-sev-guest v0.11.2-0.20241009005433-de2ac900e958 // indirect github.com/google/go-tspi v0.3.0 // indirect github.com/google/logger v1.1.1 // indirect github.com/google/s2a-go v0.1.7 // indirect @@ -62,7 +65,6 @@ require ( github.com/moby/sys/signal v0.7.0 // indirect github.com/moby/sys/user v0.1.0 // indirect github.com/opencontainers/selinux v1.11.0 // indirect - github.com/pborman/uuid v1.2.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/sirupsen/logrus v1.9.3 // indirect go.opencensus.io v0.24.0 // indirect diff --git a/launcher/go.sum b/launcher/go.sum index 784880023..2f946a815 100644 --- a/launcher/go.sum +++ b/launcher/go.sum @@ -30,6 +30,8 @@ cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aD cloud.google.com/go v0.92.2/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= cloud.google.com/go v0.92.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14= +cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU= cloud.google.com/go/auth v0.7.2 h1:uiha352VrCDMXg+yoBtaD0tUF4Kv9vrtrWPYXwutnDE= cloud.google.com/go/auth v0.7.2/go.mod h1:VEc4p5NNxycWQTMQEDQF0bd6aTMb6VgYDXEwiJJQAbs= cloud.google.com/go/auth/oauth2adapt v0.2.3 h1:MlxF+Pd3OmSudg/b1yZ5lJwoXCEaeedAguodky1PcKI= @@ -47,6 +49,12 @@ cloud.google.com/go/confidentialcomputing v1.6.0/go.mod h1:0Y5aQEtvVIUIkFYDwqdc/ cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/iam v1.1.10 h1:ZSAr64oEhQSClwBL670MsJAW5/RLiC6kfw3Bqmd5ZDI= +cloud.google.com/go/iam v1.1.10/go.mod h1:iEgMq62sg8zx446GCaijmA2Miwg5o3UbO+nI47WHJps= +cloud.google.com/go/logging v1.10.0 h1:f+ZXMqyrSJ5vZ5pE/zr0xC8y/M9BLNzQeLBwfeZ+wY4= +cloud.google.com/go/logging v1.10.0/go.mod h1:EHOwcxlltJrYGqMGfghSet736KR3hX1MAj614mrMk9I= +cloud.google.com/go/longrunning v0.5.9 h1:haH9pAuXdPAMqHvzX0zlWQigXT7B0+CL4/2nXXdBo5k= +cloud.google.com/go/longrunning v0.5.9/go.mod h1:HD+0l9/OOW0za6UWdKJtXoFAX/BGg/3Wj8p10NeWF7c= cloud.google.com/go/monitoring v0.1.0/go.mod h1:Hpm3XfzJv+UTiXzCG5Ffp0wijzHTC7Cv4eR7o3x/fEE= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= @@ -363,10 +371,10 @@ github.com/google/go-licenses v0.0.0-20210329231322-ce1d9163b77d/go.mod h1:+TYOm github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-replayers/grpcreplay v0.1.0/go.mod h1:8Ig2Idjpr6gifRd6pNVggX6TC1Zw6Jx74AKp7QNH2QE= github.com/google/go-replayers/httpreplay v0.1.0/go.mod h1:YKZViNhiGgqdBlUbI2MwGpq4pXxNmhJLPHQ7cv2b5no= -github.com/google/go-sev-guest v0.11.1 h1:gnww4U8fHV5DCPz4gykr1s8SEX1fFNcxCBy+vvXN24k= -github.com/google/go-sev-guest v0.11.1/go.mod h1:qBOfb+JmgsUI3aUyzQoGC13Kpp9zwLeWvuyXmA9q77w= -github.com/google/go-tdx-guest v0.3.2-0.20240902060211-1f7f7b9b42b9 h1:hk7vjuJgvYnHMZYI0cIDSXiC5XBmOlzRotA5bJ7nb+c= -github.com/google/go-tdx-guest v0.3.2-0.20240902060211-1f7f7b9b42b9/go.mod h1:g/n8sKITIT9xRivBUbizo34DTsUm2nN2uU3A662h09g= +github.com/google/go-sev-guest v0.11.2-0.20241009005433-de2ac900e958 h1:GfnkFZNr80qFGLR/EY75zwk8puz8+frGj4iwPwnJbSU= +github.com/google/go-sev-guest v0.11.2-0.20241009005433-de2ac900e958/go.mod h1:8+UOtSaqVIZjJJ9DDmgRko3J/kNc6jI5KLHxoeao7cA= +github.com/google/go-tdx-guest v0.3.2-0.20241009005452-097ee70d0843 h1:+MoPobRN9HrDhGyn6HnF5NYo4uMBKaiFqAtf/D/OB4A= +github.com/google/go-tdx-guest v0.3.2-0.20241009005452-097ee70d0843/go.mod h1:g/n8sKITIT9xRivBUbizo34DTsUm2nN2uU3A662h09g= 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= @@ -624,8 +632,6 @@ github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= -github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= -github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= @@ -831,6 +837,8 @@ go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGX go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw= go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc= +go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= +go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= go.opentelemetry.io/otel/sdk/export/metric v0.20.0/go.mod h1:h7RBNMsDJ5pmI1zExLi+bJK+Dr8NQCh0qGhm1KDnNlE= go.opentelemetry.io/otel/sdk/metric v0.20.0/go.mod h1:knxiS8Xd4E/N+ZqKmUPf3gTTZ4/0TjTXukfxjzSTpHE= go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw= diff --git a/launcher/image/cloudbuild.yaml b/launcher/image/cloudbuild.yaml index 1ac42a381..fad9f0bbc 100644 --- a/launcher/image/cloudbuild.yaml +++ b/launcher/image/cloudbuild.yaml @@ -2,7 +2,7 @@ substitutions: '_BASE_IMAGE': '' '_OUTPUT_IMAGE_NAME': '' '_OUTPUT_IMAGE_FAMILY': '' - '_BASE_IMAGE_PROJECT': 'cos-cloud' + '_BASE_IMAGE_PROJECT': 'confidential-vm-images' '_IMAGE_ENV': '' '_BUCKET_NAME': '${PROJECT_ID}_cloudbuild' '_CS_LICENSE': '' diff --git a/launcher/launcher/main.go b/launcher/launcher/main.go index 9181d4e61..a9a9899ea 100644 --- a/launcher/launcher/main.go +++ b/launcher/launcher/main.go @@ -161,7 +161,7 @@ func startLauncher(ctx context.Context, launchSpec spec.LaunchSpec, serialConsol } defer tpm.Close() - // check AK (EK signing) cert + // check if TPM AK (EK signing) cert is present gceAk, err := client.GceAttestationKeyECC(tpm) if err != nil { return err diff --git a/launcher/teeserver/evidence.go b/launcher/teeserver/evidence.go new file mode 100644 index 000000000..a1adea33e --- /dev/null +++ b/launcher/teeserver/evidence.go @@ -0,0 +1,51 @@ +package teeserver + +type tdxEvidence struct { + CcelAcpiTable []byte `json:"ccel_table,omitempty"` + CcelData []byte `json:"event_log,omitempty"` + CanonicalEventLog []byte `json:"canonical_event_log"` + Quote []byte `json:"quote"` +} + +type containerSignature struct { + Payload []byte `json:"payload"` + Signature []byte `json:"signature"` +} + +type containerImageSignatures struct { + KeyIDs []string `json:"key_ids"` +} + +type principalTags struct { + ContainerImageSigs containerImageSignatures `json:"container_image_signatures"` +} + +type tokenTypeOptions struct { + AllowedPrincipalTags principalTags `json:"allowed_principal_tags"` +} + +type tokenOptions struct { + Audience string `json:"audience"` + Nonces []string `json:"nonces"` + TokenType string `json:"token_type"` + TokenTypeOpts tokenTypeOptions `json:"token_type_options"` +} + +type confidentialSpaceInfo struct { + SignedEntities []containerSignature `json:"signed_entities"` + TokenOpts tokenOptions `json:"token_options"` +} + +type gcpData struct { + GcpCredentials []string `json:"gcp_credentials"` + AKCert []byte `json:"ak_cert"` + IntermediateCerts [][]byte `json:"intermediate_certs"` + CSInfo confidentialSpaceInfo `json:"confidential_space_info"` +} + +type tokenRequest struct { + PolicyMatch bool `json:"policy_must_match"` + TDX tdxEvidence `json:"tdx"` + SigAlg string `json:"token_signing_alg"` + GCP gcpData `json:"gcpcs"` +} diff --git a/launcher/teeserver/tee_server.go b/launcher/teeserver/tee_server.go index 05f415ec5..1c5b7228b 100644 --- a/launcher/teeserver/tee_server.go +++ b/launcher/teeserver/tee_server.go @@ -4,7 +4,10 @@ package teeserver import ( "context" + "crypto/sha512" + "encoding/base64" "encoding/json" + "errors" "fmt" "log" "net" @@ -67,6 +70,7 @@ func (a *attestHandler) Handler() http.Handler { // --unix-socket /tmp/container_launcher/teeserver.sock http://localhost/v1/token mux.HandleFunc("/v1/token", a.getToken) + mux.HandleFunc("/v1/evidence", a.getEvidence) return mux } @@ -138,6 +142,208 @@ func (a *attestHandler) getToken(w http.ResponseWriter, r *http.Request) { w.Write([]byte("TEE server received invalid request")) } +type itaNonce struct { + Val []byte `json:"val"` + Iat []byte `json:"iat"` + Signature []byte `json:"signature"` +} + +type evidenceRequest struct { + Nonce itaNonce `json:"nonce"` +} + +func processITANonce(input itaNonce) ([]byte, error) { + if len(input.Val) == 0 { + return nil, fmt.Errorf("no value in nonce") + } + + if len(input.Iat) == 0 { + return nil, fmt.Errorf("no iat in nonce") + } + + nonce := append(input.Val, input.Iat...) + + hash := sha512.New() + _, err := hash.Write(nonce) + if err != nil { + return nil, err + } + + return hash.Sum(nil), nil +} + +func trimCCELData(data []byte) []byte { + trimIndex := len(data) + for ; trimIndex >= 0; trimIndex-- { + c := data[trimIndex-1] + // Proceed until 0xFF padding ends. + if c != byte(255) { + break + } + } + + return data[:trimIndex] +} + +func (a *attestHandler) getEvidence(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + switch r.Method { + case "GET": + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("use POST request for evidence")) + return + case "POST": + var evidenceReq evidenceRequest + decoder := json.NewDecoder(r.Body) + decoder.DisallowUnknownFields() + + err := decoder.Decode(&evidenceReq) + if err != nil { + a.logger.Printf(err.Error()) + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + + nonce, err := processITANonce(evidenceReq.Nonce) + if err != nil { + a.logger.Print(err.Error()) + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + + evidence, err := a.attestAgent.AttestationEvidence(nonce, "ita://"+string(evidenceReq.Nonce.Val)) + if err != nil { + a.logger.Print(err.Error()) + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + + // Check for TDX Attestation. + if evidence.TDXAttestation == nil { + err_msg := "getEvidence is only supported for TDX Attestation" + a.logger.Print(err_msg) + w.WriteHeader(http.StatusPreconditionFailed) + w.Write([]byte(err_msg)) + return + } + + tdxEvi := &tokenRequest{ + PolicyMatch: true, + TDX: tdxEvidence{ + Quote: evidence.TDXAttestation.TdQuote, + CcelData: trimCCELData(evidence.TDXAttestation.CcelData), + CanonicalEventLog: evidence.TDXAttestation.CanonicalEventLog, + }, + SigAlg: "RS256", + GCP: gcpData{ + GcpCredentials: []string{}, + AKCert: evidence.TDXAttestation.AkCert, + IntermediateCerts: evidence.TDXAttestation.IntermediateCerts, + CSInfo: confidentialSpaceInfo{ + SignedEntities: make([]containerSignature, len(evidence.ContainerSignatures)), + TokenOpts: tokenOptions{ + Audience: "custom-audience", + Nonces: []string{"nonce1", "nonce2"}, + TokenType: "OIDC", + TokenTypeOpts: tokenTypeOptions{ + AllowedPrincipalTags: principalTags{ + ContainerImageSigs: containerImageSignatures{ + KeyIDs: []string{"kid1", "kid2"}, + }, + }, + }, + }, + }, + }, + } + + for _, token := range evidence.PrincipalTokens { + tdxEvi.GCP.GcpCredentials = append(tdxEvi.GCP.GcpCredentials, string(token)) + } + + for i, sig := range evidence.ContainerSignatures { + sigPayload, err := sig.Payload() + if err != nil { + a.logger.Print(err.Error()) + w.WriteHeader(http.StatusPreconditionFailed) + w.Write([]byte(err.Error())) + return + } + + b64Sig, err := sig.Base64Encoded() + if err != nil { + a.logger.Print(err.Error()) + w.WriteHeader(http.StatusPreconditionFailed) + w.Write([]byte(err.Error())) + return + } + + sigBytes, err := base64.StdEncoding.DecodeString(b64Sig) + if err != nil { + a.logger.Print(err.Error()) + w.WriteHeader(http.StatusPreconditionFailed) + w.Write([]byte(err.Error())) + return + } + + tdxEvi.GCP.CSInfo.SignedEntities[i] = containerSignature{sigPayload, sigBytes} + } + + a.logger.Printf("%+v\n", tdxEvi) + + jsonData, err := json.Marshal(tdxEvi) + if err != nil { + err_msg := "error marshalling response" + a.logger.Print(err_msg) + w.WriteHeader(http.StatusPreconditionFailed) + w.Write([]byte(err_msg)) + return + } + + // Check if output file exists. + filename := "/tmp/container_launcher/ita_evidence" + _, err = os.Stat(filename) + if err != nil && !errors.Is(err, os.ErrNotExist) { + w.WriteHeader(http.StatusPreconditionFailed) + w.Write([]byte(err.Error())) + return + } else if err == nil { + os.Remove(filename) + } + + // Create output file. + f, err := os.Create(filename) + if err != nil { + fmt.Printf("failed to create output file: %v", err) + w.WriteHeader(http.StatusPreconditionFailed) + w.Write([]byte(err.Error())) + return + } + defer f.Close() + + // Write to output file. + _, err = f.WriteString(string(jsonData)) + if err != nil { + fmt.Printf("failed to write to output file: %v", err) + w.WriteHeader(http.StatusPreconditionFailed) + w.Write([]byte(err.Error())) + return + } + + w.WriteHeader(http.StatusOK) + w.Write(jsonData) + return + } + + w.WriteHeader(http.StatusBadRequest) + // TODO: add an url pointing to the REST API document + w.Write([]byte("TEE server received invalid request")) +} + // Serve starts the server, will block until the server shutdown. func (s *TeeServer) Serve() error { return s.server.Serve(s.netListener) diff --git a/verifier/client.go b/verifier/client.go index 0af28ae1a..7b8783391 100644 --- a/verifier/client.go +++ b/verifier/client.go @@ -37,11 +37,24 @@ type TokenOptions struct { // Challenge from CreateChallenge, optional GcpCredentials linked to the // attestation, the Attestation generated from the TPM, and optional container image signatures associated with the workload. type VerifyAttestationRequest struct { - Challenge *Challenge - GcpCredentials [][]byte + Challenge *Challenge + GcpCredentials [][]byte + // Attestation is for TPM attestation Attestation *attestpb.Attestation ContainerImageSignatures []oci.Signature TokenOptions TokenOptions + // TDCCELAttestation is for TDX CCEL RTMR attestation + TDCCELAttestation *TDCCELAttestation +} + +type TDCCELAttestation struct { + CcelAcpiTable []byte + CcelData []byte + CanonicalEventLog []byte + TdQuote []byte + // still needs following two for GCE info + AkCert []byte + IntermediateCerts [][]byte } // VerifyAttestationResponse is the response from a successful diff --git a/verifier/go.mod b/verifier/go.mod index 717c00e0e..5c0f2569c 100644 --- a/verifier/go.mod +++ b/verifier/go.mod @@ -9,13 +9,13 @@ require ( cloud.google.com/go/confidentialcomputing v1.6.0 github.com/golang-jwt/jwt/v4 v4.5.0 github.com/google/go-cmp v0.6.0 - github.com/google/go-sev-guest v0.11.1 + github.com/google/go-sev-guest v0.11.2-0.20241009005433-de2ac900e958 github.com/google/go-tdx-guest v0.3.2-0.20240902060211-1f7f7b9b42b9 github.com/google/go-tpm v0.9.0 github.com/google/go-tpm-tools v0.4.4 + github.com/google/uuid v1.6.0 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.0 - github.com/pborman/uuid v1.2.1 go.uber.org/multierr v1.11.0 golang.org/x/net v0.27.0 golang.org/x/oauth2 v0.21.0 @@ -41,7 +41,6 @@ require ( github.com/google/go-tspi v0.3.0 // indirect github.com/google/logger v1.1.1 // indirect github.com/google/s2a-go v0.1.7 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/googleapis/gax-go/v2 v2.13.0 // indirect github.com/pkg/errors v0.9.1 // indirect diff --git a/verifier/go.sum b/verifier/go.sum index 6a69d1572..8974f960f 100644 --- a/verifier/go.sum +++ b/verifier/go.sum @@ -332,8 +332,8 @@ github.com/google/go-licenses v0.0.0-20210329231322-ce1d9163b77d/go.mod h1:+TYOm github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-replayers/grpcreplay v0.1.0/go.mod h1:8Ig2Idjpr6gifRd6pNVggX6TC1Zw6Jx74AKp7QNH2QE= github.com/google/go-replayers/httpreplay v0.1.0/go.mod h1:YKZViNhiGgqdBlUbI2MwGpq4pXxNmhJLPHQ7cv2b5no= -github.com/google/go-sev-guest v0.11.1 h1:gnww4U8fHV5DCPz4gykr1s8SEX1fFNcxCBy+vvXN24k= -github.com/google/go-sev-guest v0.11.1/go.mod h1:qBOfb+JmgsUI3aUyzQoGC13Kpp9zwLeWvuyXmA9q77w= +github.com/google/go-sev-guest v0.11.2-0.20241009005433-de2ac900e958 h1:GfnkFZNr80qFGLR/EY75zwk8puz8+frGj4iwPwnJbSU= +github.com/google/go-sev-guest v0.11.2-0.20241009005433-de2ac900e958/go.mod h1:8+UOtSaqVIZjJJ9DDmgRko3J/kNc6jI5KLHxoeao7cA= github.com/google/go-tdx-guest v0.3.2-0.20240902060211-1f7f7b9b42b9 h1:hk7vjuJgvYnHMZYI0cIDSXiC5XBmOlzRotA5bJ7nb+c= github.com/google/go-tdx-guest v0.3.2-0.20240902060211-1f7f7b9b42b9/go.mod h1:g/n8sKITIT9xRivBUbizo34DTsUm2nN2uU3A662h09g= github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk= @@ -577,8 +577,6 @@ github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= -github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= -github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= diff --git a/verifier/rest/rest.go b/verifier/rest/rest.go index 1b20d2764..561a4079e 100644 --- a/verifier/rest/rest.go +++ b/verifier/rest/rest.go @@ -109,11 +109,17 @@ func (c *restClient) CreateChallenge(ctx context.Context) (*verifier.Challenge, // VerifyAttestation implements verifier.Client func (c *restClient) VerifyAttestation(ctx context.Context, request verifier.VerifyAttestationRequest) (*verifier.VerifyAttestationResponse, error) { - if request.Challenge == nil || request.Attestation == nil { + if request.Challenge == nil { return nil, fmt.Errorf("nil value provided in challenge") } + + if request.Attestation == nil && request.TDCCELAttestation == nil { + return nil, fmt.Errorf("neither TPM nor TDX attestation is present") + } + req := convertRequestToREST(request) req.Challenge = request.Challenge.Name + response, err := c.v1Client.VerifyAttestation(ctx, req) if err != nil { return nil, fmt.Errorf("calling v1.VerifyAttestation: %w", err) @@ -140,36 +146,6 @@ func convertRequestToREST(request verifier.VerifyAttestationRequest) *confidenti idTokens[i] = string(token) } - quotes := make([]*confidentialcomputingpb.TpmAttestation_Quote, len(request.Attestation.GetQuotes())) - for i, quote := range request.Attestation.GetQuotes() { - pcrVals := map[int32][]byte{} - for idx, val := range quote.GetPcrs().GetPcrs() { - pcrVals[int32(idx)] = val - } - - quotes[i] = &confidentialcomputingpb.TpmAttestation_Quote{ - RawQuote: quote.GetQuote(), - RawSignature: quote.GetRawSig(), - HashAlgo: int32(quote.GetPcrs().GetHash()), - PcrValues: pcrVals, - } - } - - certs := make([][]byte, len(request.Attestation.GetIntermediateCerts())) - for i, cert := range request.Attestation.GetIntermediateCerts() { - 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 - } - var tokenType confidentialcomputingpb.TokenType switch request.TokenOptions.TokenType { case "OIDC": @@ -182,17 +158,21 @@ func convertRequestToREST(request verifier.VerifyAttestationRequest) *confidenti tokenType = confidentialcomputingpb.TokenType_TOKEN_TYPE_UNSPECIFIED } + 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 + } + verifyReq := &confidentialcomputingpb.VerifyAttestationRequest{ GcpCredentials: &confidentialcomputingpb.GcpCredentials{ ServiceAccountIdTokens: idTokens, }, - TpmAttestation: &confidentialcomputingpb.TpmAttestation{ - Quotes: quotes, - TcgEventLog: request.Attestation.GetEventLog(), - CanonicalEventLog: request.Attestation.GetCanonicalEventLog(), - AkCert: request.Attestation.GetAkCert(), - CertChain: certs, - }, + TpmAttestation: nil, ConfidentialSpaceInfo: &confidentialcomputingpb.ConfidentialSpaceInfo{ SignedEntities: []*confidentialcomputingpb.SignedEntity{{ContainerImageSignatures: signatures}}, }, @@ -203,20 +183,67 @@ func convertRequestToREST(request verifier.VerifyAttestationRequest) *confidenti }, } - if request.Attestation.GetSevSnpAttestation() != nil { - sevsnp, err := convertSEVSNPProtoToREST(request.Attestation.GetSevSnpAttestation()) - if err != nil { - log.Fatalf("Failed to convert SEVSNP proto to API proto: %v", err) + if request.Attestation != nil { + // TPM attestation route + quotes := make([]*confidentialcomputingpb.TpmAttestation_Quote, len(request.Attestation.GetQuotes())) + for i, quote := range request.Attestation.GetQuotes() { + pcrVals := map[int32][]byte{} + for idx, val := range quote.GetPcrs().GetPcrs() { + pcrVals[int32(idx)] = val + } + + quotes[i] = &confidentialcomputingpb.TpmAttestation_Quote{ + RawQuote: quote.GetQuote(), + RawSignature: quote.GetRawSig(), + HashAlgo: int32(quote.GetPcrs().GetHash()), + PcrValues: pcrVals, + } } - verifyReq.TeeAttestation = sevsnp - } - if request.Attestation.GetTdxAttestation() != nil { - tdx, err := convertTDXProtoToREST(request.Attestation.GetTdxAttestation()) - if err != nil { - log.Fatalf("Failed to convert TD quote proto to API proto: %v", err) + certs := make([][]byte, len(request.Attestation.GetIntermediateCerts())) + for i, cert := range request.Attestation.GetIntermediateCerts() { + certs[i] = cert + } + + verifyReq.TpmAttestation = &confidentialcomputingpb.TpmAttestation{ + Quotes: quotes, + TcgEventLog: request.Attestation.GetEventLog(), + CanonicalEventLog: request.Attestation.GetCanonicalEventLog(), + AkCert: request.Attestation.GetAkCert(), + CertChain: certs, + } + + if request.Attestation.GetSevSnpAttestation() != nil { + sevsnp, err := convertSEVSNPProtoToREST(request.Attestation.GetSevSnpAttestation()) + if err != nil { + log.Fatalf("Failed to convert SEVSNP proto to API proto: %v", err) + } + verifyReq.TeeAttestation = sevsnp + } + + if request.Attestation.GetTdxAttestation() != nil { + tdx, err := convertTDXProtoToREST(request.Attestation.GetTdxAttestation()) + if err != nil { + log.Fatalf("Failed to convert TD quote proto to API proto: %v", err) + } + verifyReq.TeeAttestation = tdx + } + } else if request.TDCCELAttestation != nil { + // TDX attestation route + // still need AK for GCE info! + verifyReq.TpmAttestation = &confidentialcomputingpb.TpmAttestation{ + AkCert: request.TDCCELAttestation.AkCert, + CertChain: request.TDCCELAttestation.IntermediateCerts, + } + + verifyReq.TeeAttestation = &confidentialcomputingpb.VerifyAttestationRequest_TdCcel{ + TdCcel: &confidentialcomputingpb.TdxCcelAttestation{ + TdQuote: request.TDCCELAttestation.TdQuote, + CcelAcpiTable: request.TDCCELAttestation.CcelAcpiTable, + CcelData: request.TDCCELAttestation.CcelData, + CanonicalEventLog: request.TDCCELAttestation.CanonicalEventLog, + }, } - verifyReq.TeeAttestation = tdx } return verifyReq diff --git a/verifier/rest/rest_test.go b/verifier/rest/rest_test.go index eaf6946fa..02464f4c0 100644 --- a/verifier/rest/rest_test.go +++ b/verifier/rest/rest_test.go @@ -11,7 +11,7 @@ import ( tpb "github.com/google/go-tdx-guest/proto/tdx" tgtestdata "github.com/google/go-tdx-guest/testing/testdata" "github.com/google/go-tpm-tools/verifier" - "github.com/pborman/uuid" + "github.com/google/uuid" "google.golang.org/protobuf/encoding/prototext" "google.golang.org/protobuf/testing/protocmp" ) @@ -95,23 +95,41 @@ func testRawCertTable(t testing.TB) *testCertTable { vcekraw := []byte("vcek") vlekraw := []byte("vlek") extraraw := []byte("extra") - headers[0].GUID = uuid.Parse(sabi.ArkGUID) + + var err error + headers[0].GUID, err = uuid.Parse(sabi.ArkGUID) + if err != nil { + t.Fatalf("cannot parse uuid: %v", err) + } headers[0].Offset = uint32(len(headers) * sabi.CertTableEntrySize) headers[0].Length = uint32(len(arkraw)) - headers[1].GUID = uuid.Parse(sabi.AskGUID) + headers[1].GUID, err = uuid.Parse(sabi.AskGUID) + + if err != nil { + t.Fatalf("cannot parse uuid: %v", err) + } headers[1].Offset = headers[0].Offset + headers[0].Length headers[1].Length = uint32(len(askraw)) - headers[2].GUID = uuid.Parse(sabi.VcekGUID) + headers[2].GUID, err = uuid.Parse(sabi.VcekGUID) + if err != nil { + t.Fatalf("cannot parse uuid: %v", err) + } headers[2].Offset = headers[1].Offset + headers[1].Length headers[2].Length = uint32(len(vcekraw)) - headers[3].GUID = uuid.Parse(sabi.VlekGUID) + headers[3].GUID, err = uuid.Parse(sabi.VlekGUID) + if err != nil { + t.Fatalf("cannot parse uuid: %v", err) + } headers[3].Offset = headers[2].Offset + headers[2].Length headers[3].Length = uint32(len(vlekraw)) - headers[4].GUID = uuid.Parse(extraGUID) + headers[4].GUID, err = uuid.Parse(extraGUID) + if err != nil { + t.Fatalf("cannot parse uuid: %v", err) + } headers[4].Offset = headers[3].Offset + headers[3].Length headers[4].Length = uint32(len(extraraw))