Удаленная аттестация vTPM на конфиденциальной виртуальной машине

1. Обзор

Конфиденциальные виртуальные машины (CVM) — это тип виртуальных машин Compute Engine , которые используют аппаратное шифрование памяти. Это помогает гарантировать, что ваши данные и приложения не смогут быть прочитаны или изменены во время использования. В этой лаборатории кода вам показана удаленная аттестация, которая позволяет удаленной стороне проверять узлы CVM. Кроме того, вы изучите облачное ведение журналов для дальнейшего аудита.

fcc043c716119bd6.png

Как показано на рисунке выше, эта кодовая лаборатория включает в себя следующие шаги:

  • Настройка CVM и удаленная аттестация
  • Исследование облачных журналов при удаленной аттестации CVM
  • Настройка веб-сервера секретного выпуска

Компоненты на рисунке выше включают инструмент go-tpm и Google Cloud Attestation:

  • Инструмент go-tpm : инструмент с открытым исходным кодом для получения доказательств аттестации из vTPM (модуля виртуальной доверенной платформы) на CVM и отправки их в Google Cloud Attestation для получения токена аттестации.
  • Аттестация Google Cloud: проверьте и подтвердите полученные доказательства аттестации и верните токен аттестации, отражающий факты о 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) на 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). Добавьте в роль учетную запись службы виртуальной машины по умолчанию.

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. Подключитесь к ЦВМ.
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 чтобы позже получить секрет. Вы можете декодировать токен подтверждения в 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 содержит проверенный журнал событий. Вы можете использовать предпочитаемый вами редактор, чтобы просмотреть его. Обратите внимание, что команда проверки не проверяет сертификат ключа подтверждения предложения.

4. Включите облачное ведение журнала и изучите журнал удаленной аттестации.

Вы можете запустить следующую команду в своем CVM cvm-attestation-codelab . На этот раз он регистрирует активность в облачном журнале.

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)'

Чтобы изучить облачную регистрацию, откройте следующий URL-адрес: 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 — уникальный идентификатор экземпляра.

Вы сможете найти токен аттестации, его утверждения, необработанные доказательства и одноразовые данные, отправленные в Google Cloud Attestation.

5. Настройка секретного веб-сервера выпуска

На этом этапе вы выходите из предыдущего сеанса SSH и настраиваете другую виртуальную машину. На этой виртуальной машине вы настраиваете веб-сервер секретного выпуска. Веб-сервер проверяет полученный токен аттестации и его утверждения. Если проверки успешны, секрет передается запрашивающей стороне.

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 к вашей новой виртуальной машине.

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). Создайте следующие два файла для хранения исходного кода веб-сервера секретного выпуска (скопируйте и вставьте с помощью 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). Выполните следующие команды, чтобы создать веб-сервер и запустить его. Это запустит веб-сервер секретного выпуска на порту :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 к вашему экземпляру виртуальной машины 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 — это внутренний IP-адрес экземпляра виртуальной машины cvm-attestation-codelab-web-server.

Вы увидите «Это сверхсекретная информация!» на вашем экране.

Если вы введете неверный или просроченный attestation-token , вы увидите «curl: (52) Пустой ответ от сервера». Вы также увидите «Token valid: false» в журнале веб-сервера секретного выпуска в экземпляре виртуальной машины cvm-attestation-codelab-web-server .

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. Что дальше

Узнайте больше о конфиденциальных виртуальных машинах и Compute Engine .