1. Обзор
Конфиденциальные виртуальные машины (CVM) — это тип виртуальных машин Compute Engine , которые используют аппаратное шифрование памяти. Это помогает гарантировать, что ваши данные и приложения не смогут быть прочитаны или изменены во время использования. В этой лаборатории кода вам показана удаленная аттестация, которая позволяет удаленной стороне проверять узлы CVM. Кроме того, вы изучите облачное ведение журналов для дальнейшего аудита.
Как показано на рисунке выше, эта кодовая лаборатория включает в себя следующие шаги:
- Настройка CVM и удаленная аттестация
- Исследование облачных журналов при удаленной аттестации CVM
- Настройка веб-сервера секретного выпуска
Компоненты на рисунке выше включают инструмент go-tpm и Google Cloud Attestation:
- Инструмент go-tpm : инструмент с открытым исходным кодом для получения доказательств аттестации из vTPM (модуля виртуальной доверенной платформы) на CVM и отправки их в Google Cloud Attestation для получения токена аттестации.
- Аттестация Google Cloud: проверьте и подтвердите полученные доказательства аттестации и верните токен аттестации, отражающий факты о CVM запрашивающей стороны.
Что вы узнаете
- Как выполнить удаленную аттестацию с помощью API конфиденциальных вычислений на CVM
- Как использовать Cloud Logging для мониторинга удаленной аттестации CVM
Что вам понадобится
- Проект облачной платформы Google
- Браузер, например Chrome или Firefox.
- Базовые знания Google Compute Engine , конфиденциальная виртуальная машина
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.
- Подключитесь к ЦВМ.
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. Двоичный файл инструмента 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
содержит проверенный журнал событий. Вы можете использовать предпочитаемый вами редактор, чтобы просмотреть его. Обратите внимание, что команда проверки не проверяет сертификат ключа подтверждения предложения.
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 .