Pengesahan Jarak Jauh vTPM pada Confidential Virtual Machine

1. Ringkasan

Confidential Virtual Machine (CVM) adalah jenis Virtual Machine Compute Engine yang menggunakan enkripsi memori berbasis hardware. Hal ini membantu memastikan data dan aplikasi Anda tidak dapat dibaca atau diubah saat digunakan. Dalam codelab ini, Anda akan melihat pengesahan jarak jauh yang memungkinkan pihak jarak jauh memverifikasi node CVM. Selain itu, Anda akan mempelajari logging cloud untuk audit lebih lanjut.

fcc043c716119bd6.png

Seperti yang digambarkan pada gambar di atas, codelab ini mencakup langkah-langkah berikut:

  • Penyiapan CVM dan Pengesahan jarak jauh
  • Eksplorasi Cloud Logging pada pengesahan jarak jauh CVM
  • Penyiapan Server Web Rilis Secret

Komponen dalam gambar di atas mencakup alat go-tpm dan Google Cloud Attestation:

  • Alat go-tpm: Alat open source untuk mengambil bukti pengesahan dari vTPM (Virtual Trusted Platform Module) di CVM dan mengirimkannya ke Google Cloud Attestation untuk mendapatkan token pengesahan.
  • Pengesahan Google Cloud: Memverifikasi dan memvalidasi bukti pengesahan yang diterima dan menampilkan token pengesahan yang mencerminkan fakta tentang CVM pemohon.

Yang akan Anda pelajari

  • Cara melakukan pengesahan jarak jauh melalui Confidential Computing API di CVM
  • Cara menggunakan Cloud Logging untuk memantau pengesahan jarak jauh CVM

Yang Anda butuhkan

2. Penyiapan dan Persyaratan

Untuk mengaktifkan API yang diperlukan, jalankan perintah berikut di konsol cloud atau lingkungan pengembangan lokal Anda:

gcloud auth login

gcloud services enable \
    cloudapis.googleapis.com \
    cloudshell.googleapis.com \
    confidentialcomputing.googleapis.com \
    compute.googleapis.com

3. Menyiapkan pengesahan jarak jauh CVM dan vTPM

Pada langkah ini, Anda akan membuat CVM dan melakukan pengesahan jarak jauh vTPM untuk mengambil Token Pengesahan (Token OIDC) di CVM.

Buka konsol cloud atau lingkungan pengembangan lokal Anda. Buat CVM sebagai berikut (lihat Membuat instance Confidential VM | Google Cloud). Cakupan diperlukan untuk mengakses Confidential Computing API.

gcloud config set project <project-id>

gcloud compute instances create cvm-attestation-codelab \
    --machine-type=n2d-standard-2 \
    --min-cpu-platform="AMD Milan" \
    --zone=us-central1-c \
    --confidential-compute \
    --image=ubuntu-2204-jammy-v20240228 \
    --image-project=ubuntu-os-cloud \
    --scopes https://www.googleapis.com/auth/cloud-platform

Beri otorisasi akun layanan default CVM untuk mengakses Confidential Computing API (CVM memerlukan izin berikut untuk mengambil Token Pengesahan dari Confidential Computing API):

1.) Buat peran untuk mengizinkan akses ke Confidential Computing API.

gcloud iam roles create Confidential_Computing_User --project=<project-id> \
    --title="CVM User" --description="Grants the ability to generate an attestation token in a CVM." \
 --permissions="confidentialcomputing.challenges.create,confidentialcomputing.challenges.verify,confidentialcomputing.locations.get,confidentialcomputing.locations.list" --stage=GA

Ganti kode berikut:

  • project-id adalah ID unik project.

2.) Tambahkan akun layanan default VM ke peran.

gcloud projects add-iam-policy-binding <project-id> \
    --member serviceAccount:$(gcloud iam service-accounts list --filter="email ~ compute@developer.gserviceaccount.com$" --format='value(email)'
) \
    --role "projects/<project-id>/roles/Confidential_Computing_User"

Ganti kode berikut:

  • project-id adalah ID unik project.

3.) Terhubung ke CVM dan siapkan biner alat go-tpm untuk mengambil Token Pengesahan dari Confidential Computing API yang disediakan oleh Google Cloud Attestation.

  1. Hubungkan ke CVM.
gcloud compute ssh --zone us-central1-c cvm-attestation-codelab
  1. Menyiapkan lingkungan Go:
wget https://go.dev/dl/go1.22.0.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.22.0.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin
  1. Build biner alat go-tpm. Biner alat go-tpm mengambil bukti pengesahan dari vTPM di CVM dan mengirimkannya ke Google Cloud Attestation untuk mendapatkan token pengesahan.
git clone https://github.com/google/go-tpm-tools.git --depth 1
cd go-tpm-tools/cmd/gotpm/
go build
  1. Perintah alat go-tpm mengekstrak bukti pengesahan CVM dari vTPM dan mengirimkannya ke Google Cloud Attestation. Pengesahan Google Cloud memverifikasi dan memvalidasi bukti pengesahan serta menampilkan Token Pengesahan. Perintah ini akan membuat file attestation_token yang berisi attestation-token Anda. Anda akan menggunakan attestation-token untuk mengambil secret nanti. Anda dapat mendekode token pengesahan di jwt.io untuk melihat klaim.
sudo ./gotpm token > attestation_token
  1. (Opsional) Atau, untuk melakukan pengesahan jarak jauh dengan alat go-tpm dan Google Cloud Attestation, kami menampilkan perintah untuk mengambil bukti pengesahan vTPM. Dengan cara ini, Anda dapat membuat layanan seperti Google Cloud Attestation untuk memverifikasi dan memvalidasi bukti pengesahan:
nonce=$(head -c 16 /dev/urandom | xxd -p)
sudo ./gotpm attest --nonce $nonce --format textproto --output quote.dat
sudo ./gotpm verify debug --nonce $nonce --format textproto --input quote.dat --output vtpm_report

vtpm_report berisi log peristiwa terverifikasi. Anda dapat menggunakan editor pilihan untuk melihatnya. Perhatikan bahwa perintah verifikasi tidak memeriksa sertifikat kunci pengesahan dari kutipan.

4. Mengaktifkan Cloud Logging dan menjelajahi log pengesahan jarak jauh

Anda dapat menjalankan perintah berikut di CVM cvm-attestation-codelab. Kali ini, fungsi ini mencatat aktivitas di cloud logging.

sudo ./gotpm token --cloud-log --audience "https://api.cvm-attestation-codelab.com"

Dapatkan cvm-attestation-codelab <instance-id> di konsol cloud atau lingkungan pengembangan lokal Anda.

gcloud compute instances describe cvm-attestation-codelab --zone us-central1-c --format='value(id)'

Untuk menjelajahi Cloud Logging, buka URL berikut: https://console.cloud.google.com/logs. Di kolom kueri, masukkan kode berikut:

resource.type="gce_instance" resource.labels.zone="us-central1-c" resource.labels.instance_id=<instance-id> log_name="projects/<project-id>/logs/gotpm" severity>=DEFAULT

Ganti kode berikut:

  • project-id adalah ID unik project.
  • instance-id adalah ID unik instance.

Anda akan dapat menemukan Token Pengesahan, Klaimnya, bukti mentah, dan nonce yang dikirim ke Google Cloud Attestation.

5. Menyiapkan Server Web Rilis Rahasia

Pada langkah ini, Anda akan keluar dari sesi SSH sebelumnya dan menyiapkan VM lain. Di VM ini, Anda menyiapkan server web rilis rahasia. Server web memvalidasi Token Pengesahan yang diterima dan klaimnya. Jika validasi berhasil, secret akan dirilis kepada pemohon.

1.) Buka konsol cloud atau lingkungan pengembangan lokal Anda. Buat virtual machine.

gcloud config set project <project-id>

gcloud compute instances create cvm-attestation-codelab-web-server \
    --machine-type=n2d-standard-2 \
    --zone=us-central1-c \
    --image=ubuntu-2204-jammy-v20240228 \
    --image-project=ubuntu-os-cloud

Ganti kode berikut:

  • project-id adalah ID unik project.

2.) Gunakan SSH untuk terhubung ke VM baru Anda.

gcloud compute ssh --zone us-central1-c cvm-attestation-codelab-web-server

3.) Siapkan lingkungan Go.

wget https://go.dev/dl/go1.22.0.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.22.0.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin

4). Buat dua file berikut untuk menyimpan kode sumber server web rilis rahasia (salin/tempel dengan nano).

main.go

package main

import (
        "fmt"
        "net/http"
        "strings"
        "time"
        "log"

        "github.com/golang-jwt/jwt/v4"
)

const (
        theSecret = "This is the super secret information!"
)

func homePage(w http.ResponseWriter, r *http.Request) {
        tokenString := r.Header.Get("Authorization")
        if tokenString != "" {
                tokenString, err := extractToken(tokenString)
                if err != nil {
                        http.Error(w, err.Error(), http.StatusUnauthorized)
                }

                tokenBytes := []byte(tokenString)
                // A method to return a public key from the well-known endpoint
                keyFunc := getRSAPublicKeyFromJWKsFile

                token, err := decodeAndValidateToken(tokenBytes, keyFunc)
                if err != nil {
                        http.Error(w, "Invalid JWT Token", http.StatusUnauthorized)
                }

                if ok, err := isValid(token.Claims.(jwt.MapClaims)); ok {
                        fmt.Fprintln(w, theSecret)
                } else {
                        if err != nil {
                                http.Error(w, "Error validating JWT claims: "+err.Error(), http.StatusUnauthorized)
                        } else {
                                http.Error(w, "Invalid JWT token Claims", http.StatusUnauthorized)
                        }
                }
        } else {
                http.Error(w, "Authorization token required", http.StatusUnauthorized)
        }
}

func extractToken(tokenString string) (string, error) {
        if strings.HasPrefix(tokenString, "Bearer ") {
                return strings.TrimPrefix(tokenString, "Bearer "), nil
        }
        return "", fmt.Errorf("invalid token format")
}

func isValid(claims jwt.MapClaims) (bool, error) {
        // 1. Evaluating Standard Claims:
        subject, ok := claims["sub"].(string)
        if !ok {
                return false, fmt.Errorf("missing or invalid 'sub' claim")
        }
        fmt.Println("Subject:", subject)
        // e.g. "sub":"https://www.googleapis.com/compute/v1/projects/<project_id>/zones/<project_zone>/instances/<instance_name>"

        issuedAt, ok := claims["iat"].(float64)
        if !ok {
                return false, fmt.Errorf("missing or invalid 'iat' claim")
        }
        fmt.Println("Issued At:", time.Unix(int64(issuedAt), 0))

        // 2. Evaluating Remote Attestation Claims:
        hwModel, ok := claims["hwmodel"].(string)
        if !ok || hwModel != "GCP_AMD_SEV" {
                return false, fmt.Errorf("missing or invalid 'hwModel'")
        }
        fmt.Println("hwmodel:", hwModel)

        swName, ok := claims["swname"].(string)
        if !ok || swName != "GCE" {
                return false, fmt.Errorf("missing or invalid 'hwModel'")
        }
        fmt.Println("swname:", swName)

        return true, nil
}

func main() {
        http.HandleFunc("/", homePage)
        fmt.Println("Server listening on :8080")
        err := http.ListenAndServe(":8080", nil)
        if err != nil {
                log.Fatalf("Server failed to start: %v", err)
        }
}

helper.go

package main

import (
        "crypto/rsa"
        "encoding/base64"
        "encoding/json"
        "errors"
        "fmt"
        "io"
        "math/big"
        "net/http"

        "github.com/golang-jwt/jwt/v4"
)

const (
        socketPath     = "/run/container_launcher/teeserver.sock"
        expectedIssuer = "https://confidentialcomputing.googleapis.com"
        wellKnownPath  = "/.well-known/openid-configuration"
)

type jwksFile struct {
        Keys []jwk `json:"keys"`
}

type jwk struct {
        N   string `json:"n"`   // "nMMTBwJ7H6Id8zUCZd-L7uoNyz9b7lvoyse9izD9l2rtOhWLWbiG-7pKeYJyHeEpilHP4KdQMfUo8JCwhd-OMW0be_XtEu3jXEFjuq2YnPSPFk326eTfENtUc6qJohyMnfKkcOcY_kTE11jM81-fsqtBKjO_KiSkcmAO4wJJb8pHOjue3JCP09ZANL1uN4TuxbM2ibcyf25ODt3WQn54SRQTV0wn098Y5VDU-dzyeKYBNfL14iP0LiXBRfHd4YtEaGV9SBUuVhXdhx1eF0efztCNNz0GSLS2AEPLQduVuFoUImP4s51YdO9TPeeQ3hI8aGpOdC0syxmZ7LsL0rHE1Q",
        E   string `json:"e"`   // "AQAB" or 65537 as an int
        Kid string `json:"kid"` // "1f12fa916c3a0ef585894b4b420ad17dc9d6cdf5",

        // Unused fields:
        // Alg string `json:"alg"` // "RS256",
        // Kty string `json:"kty"` // "RSA",
        // Use string `json:"use"` // "sig",
}

type wellKnown struct {
        JwksURI string `json:"jwks_uri"` // "https://www.googleapis.com/service_accounts/v1/metadata/jwk/signer@confidentialspace-sign.iam.gserviceaccount.com"

        // Unused fields:
        // Iss                                   string `json:"issuer"`                                // "https://confidentialcomputing.googleapis.com"
        // Subject_types_supported               string `json:"subject_types_supported"`               // [ "public" ]
        // Response_types_supported              string `json:"response_types_supported"`              // [ "id_token" ]
        // Claims_supported                      string `json:"claims_supported"`                      // [ "sub", "aud", "exp", "iat", "iss", "jti", "nbf", "dbgstat", "eat_nonce", "google_service_accounts", "hwmodel", "oemid", "secboot", "submods", "swname", "swversion" ]
        // Id_token_signing_alg_values_supported string `json:"id_token_signing_alg_values_supported"` // [ "RS256" ]
        // Scopes_supported                      string `json:"scopes_supported"`                      // [ "openid" ]
}

func getWellKnownFile() (wellKnown, error) {
        httpClient := http.Client{}
        resp, err := httpClient.Get(expectedIssuer + wellKnownPath)
        if err != nil {
                return wellKnown{}, fmt.Errorf("failed to get raw .well-known response: %w", err)
        }

        wellKnownJSON, err := io.ReadAll(resp.Body)
        if err != nil {
                return wellKnown{}, fmt.Errorf("failed to read .well-known response: %w", err)
        }

        wk := wellKnown{}
        json.Unmarshal(wellKnownJSON, &wk)
        return wk, nil
}

func getJWKFile() (jwksFile, error) {
        wk, err := getWellKnownFile()
        if err != nil {
                return jwksFile{}, fmt.Errorf("failed to get .well-known json: %w", err)
        }

        // Get JWK URI from .wellknown
        uri := wk.JwksURI
        fmt.Printf("jwks URI: %v\n", uri)

        httpClient := http.Client{}
        resp, err := httpClient.Get(uri)
        if err != nil {
                return jwksFile{}, fmt.Errorf("failed to get raw JWK response: %w", err)
        }

        jwkbytes, err := io.ReadAll(resp.Body)
        if err != nil {
                return jwksFile{}, fmt.Errorf("failed to read JWK body: %w", err)
        }

        file := jwksFile{}
        err = json.Unmarshal(jwkbytes, &file)
        if err != nil {
                return jwksFile{}, fmt.Errorf("failed to unmarshall JWK content: %w", err)
        }

        return file, nil
}

// N and E are 'base64urlUInt' encoded: https://www.rfc-editor.org/rfc/rfc7518#section-6.3
func base64urlUIntDecode(s string) (*big.Int, error) {
        b, err := base64.RawURLEncoding.DecodeString(s)
        if err != nil {
                return nil, err
        }
        z := new(big.Int)
        z.SetBytes(b)
        return z, nil
}

func getRSAPublicKeyFromJWKsFile(t *jwt.Token) (any, error) {
        keysfile, err := getJWKFile()
        if err != nil {
                return nil, fmt.Errorf("failed to fetch the JWK file: %w", err)
        }

        // Multiple keys are present in this endpoint to allow for key rotation.
        // This method finds the key that was used for signing to pass to the validator.
        kid := t.Header["kid"]
        for _, key := range keysfile.Keys {
                if key.Kid != kid {
                        continue // Select the key used for signing
                }

                n, err := base64urlUIntDecode(key.N)
                if err != nil {
                        return nil, fmt.Errorf("failed to decode key.N %w", err)
                }
                e, err := base64urlUIntDecode(key.E)
                if err != nil {
                        return nil, fmt.Errorf("failed to decode key.E %w", err)
                }

                // The parser expects an rsa.PublicKey: https://github.com/golang-jwt/jwt/blob/main/rsa.go#L53
                // or an array of keys. We chose to show passing a single key in this example as its possible
                // not all validators accept multiple keys for validation.
                return &rsa.PublicKey{
                        N: n,
                        E: int(e.Int64()),
                }, nil
        }

        return nil, fmt.Errorf("failed to find key with kid '%v' from well-known endpoint", kid)
}

func decodeAndValidateToken(tokenBytes []byte, keyFunc func(t *jwt.Token) (any, error)) (*jwt.Token, error) {
        var err error
        fmt.Println("Unmarshalling token and checking its validity...")
        token, err := jwt.NewParser().Parse(string(tokenBytes), keyFunc)

        fmt.Printf("Token valid: %v\n", token.Valid)
        if token.Valid {
                return token, nil
        }
        if ve, ok := err.(*jwt.ValidationError); ok {
                if ve.Errors&jwt.ValidationErrorMalformed != 0 {
                        return nil, fmt.Errorf("token format invalid. Please contact the Confidential Space team for assistance")
                }
                if ve.Errors&(jwt.ValidationErrorNotValidYet) != 0 {
                        // If device time is not synchronized with the Attestation Service you may need to account for that here.
                        return nil, errors.New("token is not active yet")
                }
                if ve.Errors&(jwt.ValidationErrorExpired) != 0 {
                        return nil, fmt.Errorf("token is expired")
                }
                return nil, fmt.Errorf("unknown validation error: %v", err)
        }

        return nil, fmt.Errorf("couldn't handle this token or couldn't read a validation error: %v", err)
}

5). Jalankan perintah berikut untuk mem-build server web dan menjalankannya. Tindakan ini akan memulai server web rilis rahasia di port :8080.

go mod init google.com/codelab
go mod tidy
go get github.com/golang-jwt/jwt/v4
go build
./codelab

Pemecahan masalah: Anda mungkin melihat peringatan berikut yang dapat diabaikan saat menjalankan go mod tidy:

go: finding module for package github.com/golang-jwt/jwt/v4
go: downloading github.com/golang-jwt/jwt v3.2.2+incompatible
go: downloading github.com/golang-jwt/jwt/v4 v4.5.0
go: found github.com/golang-jwt/jwt/v4 in github.com/golang-jwt/jwt/v4 v4.5.0
go: google.com/codelab/go/pkg/mod/github.com/golang-jwt/jwt@v3.2.2+incompatible: import path "google.com/codelab/go/pkg/mod/github.com/golang-jwt/jwt@v3.2.2+incompatible" should not have @version
go: google.com/codelab/go/pkg/mod/github.com/golang-jwt/jwt@v3.2.2+incompatible/cmd/jwt: import path "google.com/codelab/go/pkg/mod/github.com/golang-jwt/jwt@v3.2.2+incompatible/cmd/jwt" should not have @version
go: google.com/codelab/go/pkg/mod/github.com/golang-jwt/jwt@v3.2.2+incompatible/request: import path "google.com/codelab/go/pkg/mod/github.com/golang-jwt/jwt@v3.2.2+incompatible/request" should not have @version
go: google.com/codelab/go/pkg/mod/github.com/golang-jwt/jwt@v3.2.2+incompatible/test: import path "google.com/codelab/go/pkg/mod/github.com/golang-jwt/jwt@v3.2.2+incompatible/test" should not have @version

6). Sekarang, mulai tab konsol cloud atau sesi lingkungan pengembangan lokal lainnya dan jalankan perintah berikut. Tindakan ini akan memberi Anda <cvm-attestation-codelab-web-server-internal-ip>.

gcloud compute instances describe cvm-attestation-codelab-web-server --format='get(networkInterfaces[0].networkIP)' --zone=us-central1-c

7). Gunakan SSH untuk terhubung ke instance VM cvm-attestation-codelab Anda.

gcloud compute ssh --zone "us-central1-c" "cvm-attestation-codelab"

8). Perintah berikut akan mengganti attestation-token yang diperoleh sebelumnya (di bagian ~/go-tpm-tools/cmd/gotpm/). Tindakan ini akan mengambil secret yang disimpan oleh server web rilis secret.

cd ~/go-tpm-tools/cmd/gotpm/
curl http://<cvm-attestation-codelab-web-server-internal-ip>:8080 -H "Authorization: Bearer $(cat ./attestation_token)"

Ganti kode berikut:

  • cvm-attestation-codelab-web-server-internal-ip adalah IP internal instance VM cvm-attestation-codelab-web-server.

Anda akan melihat "Ini adalah informasi super rahasia!" di layar.

Jika memasukkan attestation-token yang salah atau sudah tidak berlaku, Anda akan melihat "curl: (52) Empty reply from server". Anda juga akan melihat "Token valid: false" di log server web rilis rahasia di instance VM cvm-attestation-codelab-web-server.

6. Pembersihan

Jalankan perintah berikut di konsol cloud atau lingkungan pengembangan lokal Anda:

# Delete the role binding
gcloud projects remove-iam-policy-binding <project-id> \
    --member serviceAccount:$(gcloud iam service-accounts list --filter="email ~ compute@developer.gserviceaccount.com$" --format='value(email)'
) \
    --role "projects/<project-id>/roles/Confidential_Computing_User"

# Delete the role
gcloud iam roles delete Confidential_Computing_User --project=<project-id>

# Delete the web server VM instance
gcloud compute instances delete cvm-attestation-codelab-web-server --zone=us-central1-c

# Delete the CVM instance
gcloud compute instances delete cvm-attestation-codelab --zone=us-central1-c

Ganti kode berikut:

  • project-id adalah ID unik project.

7. Langkah berikutnya

Pelajari Confidential VM dan Compute Engine lebih lanjut.