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

Как показано на рисунке выше, данная практическая работа включает следующие шаги:
- Настройка CVM и удаленная аттестация
- Исследование Cloud Logging в рамках удаленной аттестации CVM.
- Настройка веб-сервера для выпуска секретных файлов
На рисунке выше показаны компоненты, включающие инструмент go-tpm и Google Cloud Attestation:
- go-tpm tool : Инструмент с открытым исходным кодом для получения подтверждения аттестации от vTPM (Virtual Trusted Platform Module) на CVM и отправки его в Google Cloud Attestation для получения токена аттестации.
- Google Cloud Attestation: Проверить и подтвердить полученные данные аттестации и вернуть токен аттестации, отражающий информацию о CVM запрашивающего.
Что вы узнаете
- Как выполнить удалённую аттестацию с помощью API конфиденциальных вычислений на CVM
- Как использовать Cloud Logging для мониторинга удаленной аттестации CVM
Что вам понадобится
- Проект Google Cloud Platform
- Браузер, например Chrome или Firefox.
- Базовые знания Google Compute Engine , Confidential VM.
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 конфиденциальных вычислений необходимы области действия (Positions).
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 конфиденциальных вычислений (для получения токена подтверждения от API конфиденциальных вычислений CVM требуются следующие разрешения):
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.
- Подключитесь к CVM.
gcloud compute ssh --zone us-central1-c cvm-attestation-codelab
- Настройте среду 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
- Соберите исполняемый файл инструмента 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
- Команда инструмента go-tpm извлекает подтверждение аттестации CVM из vTPM и отправляет его в Google Cloud Attestation. Google Cloud Attestation проверяет и подтверждает подтверждение аттестации и возвращает токен аттестации. Команда создает файл attestation_token, который содержит ваш
attestation-token. Вы будете использовать свойattestation-tokenдля получения секрета позже. Вы можете расшифровать токен аттестации на jwt.io , чтобы просмотреть утверждения.
sudo ./gotpm token > attestation_token
- (Необязательно) В качестве альтернативы для выполнения удаленной аттестации с помощью инструмента 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 не проверяет сертификат ключа аттестации цитаты.
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)'
Чтобы изучить Cloud Logging, откройте следующий 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 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) Пустой ответ от сервера". Также в журнале веб-сервера для выпуска секретов в экземпляре виртуальной машины cvm-attestation-codelab-web-server вы увидите сообщение "Token valid: false".
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 .