vTPM تایید از راه دور در ماشین مجازی محرمانه

۱. مرور کلی

ماشین‌های مجازی محرمانه (CVM) نوعی از ماشین‌های مجازی موتور محاسباتی هستند که از رمزگذاری حافظه مبتنی بر سخت‌افزار استفاده می‌کنند. این امر به شما اطمینان می‌دهد که داده‌ها و برنامه‌های شما در حین استفاده قابل خواندن یا تغییر نیستند. در این آزمایشگاه کد، گواهی از راه دور به شما نشان داده می‌شود که به یک طرف از راه دور اجازه می‌دهد تا گره‌های CVM را تأیید کند. علاوه بر این، برای حسابرسی بیشتر، ثبت وقایع ابری را بررسی خواهید کرد.

fcc043c716119bd6.png

همانطور که در شکل بالا نشان داده شده است، این codelab شامل مراحل زیر است:

  • راه‌اندازی CVM و تأیید از راه دور
  • کاوش در ثبت وقایع ابری در گواهی از راه دور CVM
  • راه‌اندازی سرور وب با انتشار مخفی

اجزای موجود در شکل بالا شامل یک ابزار go-tpm و Google Cloud Attestation است:

  • ابزار go-tpm : یک ابزار متن‌باز برای دریافت شواهد گواهی از vTPM (ماژول پلتفرم مجازی مورد اعتماد) در CVM و ارسال آن به Google Cloud Attestation برای دریافت توکن گواهی.
  • گواهی گوگل کلود: شواهد گواهی دریافتی را تأیید و اعتبارسنجی کنید و یک توکن گواهی که منعکس‌کننده حقایق مربوط به CVM درخواست‌کننده است را برگردانید.

آنچه یاد خواهید گرفت

  • نحوه انجام گواهی از راه دور از طریق API های محاسبات محرمانه در CVM
  • نحوه استفاده از Cloud Logging برای نظارت بر گواهی از راه دور CVM

آنچه نیاز دارید

۲. تنظیمات و الزامات

برای فعال کردن API های لازم، دستور زیر را در کنسول ابری یا محیط توسعه محلی خود اجرا کنید:

gcloud auth login

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

۳. راه‌اندازی گواهی از راه دور CVM و vTPM

در این مرحله، شما یک CVM ایجاد می‌کنید و یک گواهی از راه دور vTPM را برای بازیابی توکن گواهی (OIDC Token) در CVM انجام می‌دهید.

به کنسول ابری یا محیط توسعه محلی خود بروید. یک CVM به صورت زیر ایجاد کنید (به بخش ایجاد یک نمونه ماشین مجازی محرمانه | Google Cloud مراجعه کنید). برای دسترسی به APIهای محاسبات محرمانه، به Scopeها نیاز است.

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 اجازه دسترسی به APIهای محاسبات محرمانه را بدهید (CVMها برای دریافت توکن گواهی از APIهای محاسبات محرمانه به مجوزهای زیر نیاز دارند):

۱) نقشی برای دسترسی به 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

موارد زیر را جایگزین کنید:

  • project-id شناسه منحصر به فرد پروژه است.

۲) حساب کاربری سرویس پیش‌فرض ماشین مجازی را به این نقش اضافه کنید.

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"

موارد زیر را جایگزین کنید:

  • project-id شناسه منحصر به فرد پروژه است.

۳) به CVM متصل شوید و فایل باینری ابزار go-tpm را برای دریافت توکن گواهی از APIهای محاسبات محرمانه ارائه شده توسط Google Cloud Attestation تنظیم کنید.

  1. به CVM متصل شوید.
gcloud compute ssh --zone us-central1-c cvm-attestation-codelab
  1. یک محیط 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. فایل باینری ابزار go-tpm را بسازید. فایل باینری ابزار go-tpm، شواهد گواهی را از vTPM موجود در CVM دریافت کرده و آن را برای دریافت توکن گواهی به Google Cloud Attestation ارسال می‌کند.
git clone https://github.com/google/go-tpm-tools.git --depth 1
cd go-tpm-tools/cmd/gotpm/
go build
  1. دستور ابزار go-tpm شواهد گواهی CVM را از vTPM استخراج کرده و آن را به Google Cloud Attestation ارسال می‌کند. Google Cloud Attestation شواهد گواهی را تأیید و اعتبارسنجی می‌کند و یک Attestation Token برمی‌گرداند. این دستور یک فایل attestation_token ایجاد می‌کند که حاوی attestation-token شما است. شما بعداً attestation-token خود برای دریافت یک راز استفاده خواهید کرد. می‌توانید attestation token را در jwt.io رمزگشایی کنید تا ادعاها را مشاهده کنید.
sudo ./gotpm token > attestation_token
  1. (اختیاری) به عنوان یک روش جایگزین برای انجام گواهی از راه دور با ابزار go-tpm و Google Cloud Attestation، دستوراتی را برای دریافت شواهد گواهی vTPM ارائه می‌دهیم. به این ترتیب، می‌توانید سرویسی مانند Google Cloud Attestation ایجاد کنید تا شواهد گواهی را تأیید و اعتبارسنجی کند:
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 شامل گزارش رویداد تأیید شده است. می‌توانید از ویرایشگر دلخواه خود برای مشاهده آن استفاده کنید. توجه داشته باشید که دستور verify، گواهی کلید تأیید نقل قول را بررسی نمی‌کند.

۴. فعال کردن Cloud Logging و بررسی گزارش گواهی از راه دور

می‌توانید دستور زیر را در ماشین مجازی cvm-attestation-codelab خود اجرا کنید. این بار، فعالیت‌ها روی ابر ثبت می‌شوند.

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

<instance-id> به cvm-attestation-codelab در کنسول ابری یا محیط توسعه محلی خود دریافت کنید.

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

برای کاوش در Cloud Logging، آدرس اینترنتی زیر را باز کنید: https://console.cloud.google.com/logs . در فیلد جستجو، موارد زیر را وارد کنید:

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

موارد زیر را جایگزین کنید:

  • project-id شناسه منحصر به فرد پروژه است.
  • instance-id شناسه منحصر به فرد نمونه است.

شما باید بتوانید توکن گواهی، ادعاهای آن، شواهد خام و nonce ارسال شده به Google Cloud Attestation را پیدا کنید.

۵. راه‌اندازی یک وب سرور انتشار مخفی

در این مرحله، از جلسه SSH قبلی خود خارج می‌شوید و یک ماشین مجازی دیگر راه‌اندازی می‌کنید. روی این ماشین مجازی، یک وب سرور انتشار محرمانه راه‌اندازی می‌کنید. وب سرور، توکن گواهی دریافتی و ادعاهای آن را اعتبارسنجی می‌کند. اگر اعتبارسنجی‌ها موفقیت‌آمیز باشند، آنگاه راز را برای درخواست‌کننده آزاد می‌کند.

۱) به کنسول ابری یا محیط توسعه محلی خود بروید. یک ماشین مجازی ایجاد کنید.

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

موارد زیر را جایگزین کنید:

  • project-id شناسه منحصر به فرد پروژه است.

۲) به ماشین مجازی جدید خود SSH کنید.

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

۳) محیط 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

۴) دو فایل زیر را برای ذخیره کد منبع وب سرور انتشار مخفی ایجاد کنید (با 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)
}

۵) دستورات زیر را برای ساخت و اجرای وب سرور اجرا کنید. این کار وب سرور نسخه مخفی را در پورت :8080 شروع می‌کند.

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

عیب‌یابی: ممکن است هنگام اجرای دستور 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

۶) حالا یک تب کنسول ابری دیگر یا یک جلسه محیط توسعه محلی را باز کنید و دستور زیر را اجرا کنید. این دستور 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

۷) به نمونه ماشین مجازی cvm-attestation-codelab خود از طریق SSH متصل شوید.

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

۸). دستور زیر جایگزین attestation-token که قبلاً (در مسیر ~/go-tpm-tools/cmd/gotpm/ ) دریافت شده بود، می‌شود. این دستور، رمزی را که توسط وب سرور انتشار رمز نگهداری می‌شود، برای شما بازیابی می‌کند!

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

موارد زیر را جایگزین کنید:

  • cvm-attestation-codelab-web-server-internal-ip آدرس IP داخلی ماشین مجازی cvm-attestation-codelab-web-server است.

روی صفحه نمایش خود عبارت «این اطلاعات فوق‌العاده سری است!» را خواهید دید.

اگر یک attestation-token نادرست یا منقضی شده وارد کنید، عبارت "curl: (52) Empty reply from server" را مشاهده خواهید کرد. همچنین در لاگ وب سرور secret release خود در نمونه ماشین مجازی cvm-attestation-codelab-web-server عبارت "Token valid: false" را مشاهده خواهید کرد.

۶. پاکسازی

دستورات زیر را در کنسول ابری یا محیط توسعه محلی خود اجرا کنید:

# 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

موارد زیر را جایگزین کنید:

  • project-id شناسه منحصر به فرد پروژه است.

۷. قدم بعدی چیست؟

درباره ماشین‌های مجازی محرمانه و موتور محاسباتی بیشتر بدانید.