Pengesahan Jarak Jauh vTPM pada Confidential Virtual Machine

1. Ringkasan

Confidential Virtual Machines (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 Rahasia

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

  • Alat go-tpm: Alat open source untuk mengambil bukti pengesahan dari vTPM (Virtual Trusted Platform Module) di CVM dan mengirimkannya ke Pengesahan Google Cloud untuk mendapatkan token pengesahan.
  • Pengesahan Google Cloud: Verifikasi dan validasi bukti pengesahan yang diterima, lalu kembalikan 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

Memberi 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.) Hubungkan ke CVM dan siapkan biner alat go-tpm untuk mengambil Token Pengesahan dari API Confidential Computing yang disediakan oleh Pengesahan Google Cloud.

  1. Hubungkan ke CVM.
gcloud compute ssh --zone us-central1-c cvm-attestation-codelab
  1. 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
  1. Buat 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) Sebagai alternatif untuk melakukan pengesahan jarak jauh dengan alat go-tpm dan Pengesahan Google Cloud, kami menunjukkan perintah untuk mengambil bukti pengesahan vTPM. Dengan cara ini, Anda dapat membuat layanan seperti Pengesahan Google Cloud 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 yang telah diverifikasi. Anda dapat menggunakan editor pilihan Anda untuk melihatnya. Perhatikan bahwa perintah verifikasi tidak memeriksa sertifikat kunci pengesahan penawaran harga.

4. Mengaktifkan Cloud Logging dan menjelajahi log pengesahan jarak jauh

Anda dapat menjalankan perintah berikut di CVM cvm-attestation-codelab. Kali ini, aktivitas tersebut dicatat 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 Pengesahan Google Cloud.

5. Menyiapkan Server Web Rilis Rahasia

Pada langkah ini, Anda keluar dari sesi SSH sebelumnya dan menyiapkan VM lain. Di VM ini, Anda akan menyiapkan server web rilis rahasia. Server web memvalidasi Token Pengesahan yang diterima dan klaimnya. Jika validasi berhasil, rahasia akan dirilis ke 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 membangun 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 sesi lingkungan pengembangan lokal atau tab konsol cloud 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.

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

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

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 Anda 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 Anda 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 lebih lanjut Confidential VM dan Compute Engine.