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

1. بررسی اجمالی

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

fcc043c716119bd6.png

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

  • راه اندازی CVM و گواهی از راه دور
  • کاوش Cloud Logging در گواهی از راه دور CVM
  • راه اندازی وب سرور انتشار مخفی

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

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

چیزی که یاد خواهید گرفت

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

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

2. راه اندازی و الزامات

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

gcloud auth login

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

3. تنظیم گواهی از راه دور CVM و vTPM

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

به کنسول ابری یا محیط توسعه محلی خود بروید. یک CVM به شکل زیر ایجاد کنید (به ایجاد نمونه مجازی مجازی محرمانه | Google Cloud مراجعه کنید). برای دسترسی به 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

مجوز حساب سرویس پیش‌فرض CVM برای دسترسی به APIهای محاسباتی محرمانه (CVMها برای واکشی نشانه تأیید از APIهای محاسباتی محرمانه به مجوزهای زیر نیاز دارند):

1). نقشی برای اجازه دسترسی به 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 شناسه منحصر به فرد پروژه است.

2). حساب سرویس پیش فرض VM را به نقش اضافه کنید.

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 شناسه منحصر به فرد پروژه است.

3). به 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 Testation برای یک نشانه گواهی می‌فرستد.
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 Atestation ارسال می کند. Google Cloud Atestation شواهد تأیید را تأیید و تأیید می کند و یک نشانه تأیید را برمی گرداند. این دستور یک فایل attestation_token ایجاد می کند که حاوی attestation-token شما است. شما attestation-token خود برای واکشی راز بعدا استفاده خواهید کرد. برای مشاهده ادعاها، می توانید رمز تأیید را در jwt.io رمزگشایی کنید.
sudo ./gotpm token > attestation_token
  1. (اختیاری) یا برای انجام تأیید از راه دور با ابزار go-tpm و Google Cloud Atestation، دستورات را برای واکشی شواهد تأیید 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 گواهینامه کلید گواهی نقل قول را بررسی نمی کند.

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

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

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

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

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

برای کاوش در Cloud Logging، URL زیر را باز کنید: https://console.cloud.google.com/logs . در قسمت query عبارت زیر را وارد کنید:

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 شناسه منحصر به فرد نمونه است.

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

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

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

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

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 شناسه منحصر به فرد پروژه است.

2). SSH به VM جدید شما.

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

3). محیط 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). دو فایل زیر را برای ذخیره کد منبع وب سرور انتشار مخفی ایجاد کنید (کپی/پیست با نانو).

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). دستورات زیر را برای ساخت وب سرور اجرا کنید و آن را اجرا کنید. این سرور وب سرور مخفی را در پورت :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

6). حالا یک تب کنسول ابری یا جلسه محیط توسعه محلی دیگر را شروع کنید و دستور زیر را اجرا کنید. با این کار < 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). SSH به نمونه VM cvm-attestation-codelab شما.

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

8). دستور زیر جایگزین 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 داخلی نمونه cvm-attestation-codelab-web-server VM است.

خواهید دید "این اطلاعات فوق سری است!" روی صفحه نمایش شما

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

6. پاکسازی

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

# 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 شناسه منحصر به فرد پروژه است.

7. بعدش چیه

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