Gizli Sanal Makinede vTPM Uzaktan Onayı

1. Genel Bakış

Gizli Sanal Makineler (CVM), donanım tabanlı bellek şifrelemesi kullanan bir tür Compute Engine sanal makinesidir. Bu sayede, verileriniz ve uygulamalarınız kullanım sırasında okunamaz veya değiştirilemez. Bu codelab'de, uzak bir tarafın CVM düğümlerini doğrulamasına olanak tanıyan uzaktan doğrulama gösterilmektedir. Ayrıca, daha fazla denetleme için bulut günlüğünü keşfedeceksiniz.

fcc043c716119bd6.png

Yukarıdaki şekilde gösterildiği gibi, bu kod laboratuvarı aşağıdaki adımları içerir:

  • CVM Kurulumu ve Uzaktan Onay
  • CVM uzaktan doğrulamayla ilgili Cloud Logging keşfi
  • Gizli Sürüm Web Sunucusu Kurulumu

Yukarıdaki şekildeki bileşenler arasında bir go-tpm aracı ve Google Cloud Attestation yer alır:

  • go-tpm aracı: CVM'deki vTPM (Sanal Güvenilir Platform Modülü)'den doğrulama kanıtını alan ve doğrulama jetonu için Google Cloud Attestation'a gönderen açık kaynak bir araçtır.
  • Google Cloud Attestation: Alınan doğrulama kanıtını doğrulayın ve istek sahibinin CVM'si hakkındaki gerçekleri yansıtan bir doğrulama jetonu döndürün.

Neler öğreneceksiniz?

  • CVM'de Gizli Bilişim API'leri aracılığıyla uzaktan doğrulama yapma
  • CVM uzaktan kimlik doğrulamasını izlemek için Cloud Logging'i kullanma

İhtiyacınız olanlar

2. Kurulum ve Gereksinimler

Gerekli API'leri etkinleştirmek için Cloud Console'da veya yerel geliştirme ortamınızda aşağıdaki komutu çalıştırın:

gcloud auth login

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

3. CVM ve vTPM uzaktan doğrulamasını ayarlama

Bu adımda, CVM'de bir Attestation Token'ı (OIDC Token'ı) almak için bir CVM oluşturacak ve vTPM uzaktan doğrulama işlemi gerçekleştireceksiniz.

Cloud Console'a veya yerel geliştirme ortamınıza gidin. Aşağıdaki gibi bir CVM oluşturun (Gizli Sanal Makine örneği oluşturma | Google Cloud başlıklı makaleyi inceleyin). Gizli Bilgi İşleme API'lerine erişmek için kapsamlar gerekir.

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

CVM varsayılan hizmet hesabını Confidential Computing API'lerine erişmesi için yetkilendirin (CVM'lerin Confidential Computing API'lerinden Attestation Token'u almak için aşağıdaki izinlere ihtiyacı vardır):

1). Gizli Bilişim API'lerine erişim izni vermek için bir rol oluşturun.

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

Aşağıdakini değiştirin:

  • project-id, projenin benzersiz tanımlayıcısıdır.

2). Sanal makinenin varsayılan hizmet hesabını role ekleyin.

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"

Aşağıdakini değiştirin:

  • project-id, projenin benzersiz tanımlayıcısıdır.

3). CVM'ye bağlanın ve Google Cloud Attestation tarafından sağlanan Gizli Bilgi İşleme API'lerinden Attestation Token'ı almak için go-tpm aracı ikilisini ayarlayın.

  1. CVM'ye bağlanın.
gcloud compute ssh --zone us-central1-c cvm-attestation-codelab
  1. Go ortamı oluşturma:
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. go-tpm aracı ikilisini derleyin. go-tpm aracı ikili dosyası, CVM'deki vTPM'den doğrulama kanıtını alır ve doğrulama jetonu için Google Cloud Attestation'a gönderir.
git clone https://github.com/google/go-tpm-tools.git --depth 1
cd go-tpm-tools/cmd/gotpm/
go build
  1. go-tpm araç komutu, CVM'nin doğrulama kanıtını vTPM'den alıp Google Cloud Attestation'a gönderir. Google Cloud Attestation, onay kanıtını doğrular ve bir onay jetonu döndürür. Komut, attestation-token değerinizi içeren bir attestation_token dosyası oluşturur. Daha sonra gizli anahtar almak için attestation-token değerinizi kullanacaksınız. İddiaları görüntülemek için jwt.io'da doğrulama jetonunun kodunu çözebilirsiniz.
sudo ./gotpm token > attestation_token
  1. (İsteğe bağlı) go-tpm aracı ve Google Cloud Attestation ile uzaktan doğrulama yapmak için alternatif olarak, vTPM doğrulama kanıtını getirme komutlarını gösteririz. Bu sayede, onay kanıtını doğrulamak ve doğrulamak için Google Cloud Attestation gibi bir hizmet oluşturabilirsiniz:
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, doğrulanmış günlük kaydını içerir. Tercih ettiğiniz düzenleyiciyi kullanarak bu dosyayı inceleyebilirsiniz. Doğrulama komutunun, teklifin tasdik anahtarı sertifikasını kontrol etmediğini unutmayın.

4. Cloud Logging'i etkinleştirme ve uzaktan doğrulama günlüğünü keşfetme

cvm-attestation-codelab CVM'nizde aşağıdaki komutu çalıştırabilirsiniz. Bu kez, etkinliği Cloud Logging'a kaydeder.

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

Cloud Console'unuzda veya yerel geliştirme ortamınızda cvm-attestation-codelab <instance-id>'i edinin.

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

Cloud Logging'u keşfetmek için şu URL'yi açın: https://console.cloud.google.com/logs. Sorgu alanına şunları girin:

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

Aşağıdakini değiştirin:

  • project-id, projenin benzersiz tanımlayıcısıdır.
  • instance-id, örneğin benzersiz tanımlayıcısıdır.

Google Cloud Attestation'a gönderilen Attestation jetonunu, iddialarını, ham kanıtlarını ve tek seferlik kimliğini bulabilirsiniz.

5. Gizli Sürüm Web Sunucusu Kurulumu

Bu adımda, önceki SSH oturumunuzdan çıkar ve başka bir sanal makine oluşturursunuz. Bu sanal makinede gizli sürüm web sunucusu oluşturursunuz. Web sunucusu, alınan Attestation Token'u ve iddialarını doğrular. Doğrulamalar başarılı olursa gizli anahtar istemciye verilir.

1). Cloud Console'a veya yerel geliştirme ortamınıza gidin. Sanal makine oluşturun.

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

Aşağıdakini değiştirin:

  • project-id, projenin benzersiz tanımlayıcısıdır.

2). Yeni sanal makinenize SSH ile bağlanın.

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

3). Go ortamını ayarlayın.

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). Gizli sürüm web sunucusunun kaynak kodunu depolamak için aşağıdaki iki dosyayı oluşturun (nano ile kopyala/yapıştır).

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). Web sunucusunu derlemek ve çalıştırmak için aşağıdaki komutları çalıştırın. Bu işlem, gizli sürüm web sunucusunu :8080 bağlantı noktasında başlatır.

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

Sorun giderme: Aşağıdaki uyarıyı görebilirsiniz. Bu uyarı, çalıştırıldığında yoksayılabilir. 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). Şimdi başka bir Cloud Console sekmesi veya yerel geliştirme ortamı oturumu başlatın ve aşağıdaki komutu çalıştırın. Bu işlem, <cvm-attestation-codelab-web-server-internal-ip> değerini döndürür.

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

7). cvm-attestation-codelab sanal makine örneğinize SSH ile bağlanın.

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

8). Aşağıdaki komut, daha önce elde edilen attestation-token değerini (~/go-tpm-tools/cmd/gotpm/ altında) değiştirir. Bu komut, gizli sürüm web sunucusunun sakladığı gizli anahtarı getirir.

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

Aşağıdakini değiştirin:

  • cvm-attestation-codelab-web-server-internal-ip, cvm-attestation-codelab-web-server sanal makine örneğinin dahili IP adresidir.

Ekranınızda "Bu çok gizli bir bilgidir" ifadesini görürsünüz.

Yanlış veya süresi dolmuş bir attestation-token girerseniz "curl: (52) Empty reply from server" (curl: (52) Sunucudan boş yanıt) mesajını görürsünüz. Ayrıca, cvm-attestation-codelab-web-server sanal makine örneğindeki gizli sürüm web sunucusu günlükünüzde "Jeton geçerli: yanlış" ifadesini görürsünüz.

6. Temizleme

Cloud Console'da veya yerel geliştirme ortamınızda aşağıdaki komutları çalıştırın:

# 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

Aşağıdakini değiştirin:

  • project-id, projenin benzersiz tanımlayıcısıdır.

7. Sırada ne var?

Gizli Sanal Makineler ve Compute Engine hakkında daha fazla bilgi edinin.