1. Przegląd
Poufne maszyny wirtualne (CVM) to typ maszyn wirtualnych Compute Engine, które korzystają z szyfrowania pamięci opartego na sprzęcie. Dzięki temu dane i aplikacje nie mogą być odczytywane ani modyfikowane podczas używania. W tym laboratorium kodowania dowiesz się, jak działa poświadczenie zdalne, które umożliwia zdalnej stronie weryfikację węzłów CVM. Oprócz tego możesz użyć Cloud Logging do dalszej kontroli.

Zgodnie z rysunkiem powyżej ten codelab obejmuje te kroki:
- Konfigurowanie CVM i zdalny atest
- Eksploracja Cloud Logging na potrzeby zdalnego atestowania CVM
- Konfigurowanie serwera WWW do udostępniania kluczy tajnych
Komponenty na powyższym rysunku to narzędzie go-tpm i usługa Google Cloud Attestation:
- Narzędzie go-tpm: narzędzie open source do pobierania dowodów atestu z modułu vTPM (Virtual Trusted Platform Module) na maszynie CVM i wysyłania ich do usługi Google Cloud Attestation w celu uzyskania tokena atestu.
- Usługa Google Cloud Attestation: weryfikuje i zatwierdza otrzymane dowody atestu oraz zwraca token atestu odzwierciedlający fakty dotyczące maszyny CVM wnioskodawcy.
Czego się nauczysz
- Jak przeprowadzić poświadczenie zdalne za pomocą interfejsów API Confidential Computing na poufnej maszynie wirtualnej
- Korzystanie z Cloud Logging do monitorowania zdalnego potwierdzania tożsamości CVM
Czego potrzebujesz
- Projekt Google Cloud Platform
- przeglądarkę, np. Chrome lub Firefox;
- Podstawowa wiedza o Google Compute Engine i maszynach wirtualnych poufnych
2. Konfiguracja i wymagania
Aby włączyć niezbędne interfejsy API, uruchom to polecenie w konsoli Google Cloud lub w lokalnym środowisku programistycznym:
gcloud auth login
gcloud services enable \
cloudapis.googleapis.com \
cloudshell.googleapis.com \
confidentialcomputing.googleapis.com \
compute.googleapis.com
3. Konfigurowanie zdalnego potwierdzania tożsamości CVM i vTPM
W tym kroku utworzysz maszynę wirtualną CVM i przeprowadzisz zdalne potwierdzenie vTPM, aby pobrać na niej token potwierdzenia (token OIDC).
Otwórz konsolę Google Cloud lub lokalne środowisko programistyczne. Utwórz CVM w ten sposób (patrz Tworzenie instancji poufnej maszyny wirtualnej | Google Cloud). Zakresy są potrzebne do uzyskania dostępu do interfejsów API usługi Confidential Computing.
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
Autoryzuj domyślne konto usługi CVM do uzyskiwania dostępu do interfejsów API Confidential Computing (maszyny CVM muszą mieć te uprawnienia, aby pobierać token potwierdzający z interfejsów API Confidential Computing):
1) Utwórz rolę, która umożliwi dostęp do interfejsów API Confidential Computing.
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
Zastąp następujące elementy:
project-idto unikalny identyfikator projektu.
2) Dodaj do roli domyślne konto usługi maszyny wirtualnej.
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"
Zastąp następujące elementy:
project-idto unikalny identyfikator projektu.
3) Połącz się z CVM i skonfiguruj plik binarny narzędzia go-tpm, aby pobierać token potwierdzenia z interfejsów API Confidential Computing udostępnianych przez Google Cloud Attestation.
- Połącz się z CVM.
gcloud compute ssh --zone us-central1-c cvm-attestation-codelab
- Skonfiguruj środowisko 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
- Skompiluj plik binarny narzędzia go-tpm. Plik binarny narzędzia go-tpm pobiera dowód atestu z vTPM na CVM i wysyła go do usługi Google Cloud Attestation w celu uzyskania tokena atestu.
git clone https://github.com/google/go-tpm-tools.git --depth 1 cd go-tpm-tools/cmd/gotpm/ go build
- Polecenie narzędzia go-tpm wyodrębnia z vTPM dowód atestu CVM i wysyła go do usługi Google Cloud Attestation. Usługa Google Cloud Attestation weryfikuje i potwierdza dowody atestu oraz zwraca token atestu. Polecenie tworzy plik attestation_token, który zawiera
attestation-token. Później użyjeszattestation-tokendo pobrania klucza tajnego. Aby wyświetlić roszczenia, możesz zdekodować token atestu na stronie jwt.io.
sudo ./gotpm token > attestation_token
- (Opcjonalnie) Zamiast przeprowadzać zdalne potwierdzanie za pomocą narzędzia go-tpm i usługi Google Cloud Attestation, pokazujemy polecenia do pobierania dowodów potwierdzających vTPM. W ten sposób możesz utworzyć usługę taką jak Google Cloud Attestation, aby weryfikować i zatwierdzać dowody atestu:
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 zawiera zweryfikowany dziennik zdarzeń. Możesz go wyświetlić w dowolnym edytorze. Pamiętaj, że polecenie verify nie sprawdza certyfikatu klucza atestu w przypadku cytatu.
4. Włącz Cloud Logging i zapoznaj się z logiem atestu zdalnego
Możesz uruchomić to polecenie na cvm-attestation-codelab CVM. Tym razem rejestruje aktywność w Cloud Logging.
sudo ./gotpm token --cloud-log --audience "https://api.cvm-attestation-codelab.com"
Uzyskaj cvm-attestation-codelab <instance-id> w konsoli Google Cloud lub lokalnym środowisku programistycznym.
gcloud compute instances describe cvm-attestation-codelab --zone us-central1-c --format='value(id)'
Aby zapoznać się z Cloud Logging, otwórz ten adres URL: https://console.cloud.google.com/logs. W polu zapytania wpisz:
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
Zastąp następujące elementy:
project-idto unikalny identyfikator projektu.instance-idto unikalny identyfikator instancji.
Powinny się na nim znajdować token atestu, jego roszczenia, surowe dowody i wartość nonce wysłane do usługi Google Cloud Attestation.
5. Konfigurowanie serwera WWW do publikowania tajnych informacji
W tym kroku zakończysz poprzednią sesję SSH i skonfigurujesz kolejną maszynę wirtualną. Na tej maszynie wirtualnej skonfigurujesz serwer WWW z tajną wersją. Serwer WWW weryfikuje otrzymany token atestu i jego roszczenia. Jeśli weryfikacja się powiedzie, usługa udostępni obiekt tajny osobie, która o to poprosiła.
1) Otwórz konsolę Google Cloud lub lokalne środowisko programistyczne. Utwórz maszynę wirtualną.
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
Zastąp następujące elementy:
project-idto unikalny identyfikator projektu.
2) Połącz się z nową maszyną wirtualną za pomocą SSH.
gcloud compute ssh --zone us-central1-c cvm-attestation-codelab-web-server
3) Skonfiguruj środowisko 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). Utwórz te 2 pliki, aby przechowywać kod źródłowy serwera WWW udostępniającego obiekt tajny (skopiuj i wklej za pomocą 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). Aby utworzyć serwer WWW i go uruchomić, wykonaj te polecenia. Spowoduje to uruchomienie serwera WWW udostępniającego obiekt tajny na porcie :8080.
go mod init google.com/codelab go mod tidy go get github.com/golang-jwt/jwt/v4 go build ./codelab
Rozwiązywanie problemów: podczas uruchamiania może pojawić się to ostrzeżenie, które można zignorować: 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). Teraz otwórz kolejną kartę konsoli w chmurze lub sesję lokalnego środowiska programistycznego i uruchom to polecenie. W ten sposób uzyskasz <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). Połącz się z instancją maszyny wirtualnej cvm-attestation-codelab przez SSH.
gcloud compute ssh --zone "us-central1-c" "cvm-attestation-codelab"
8). Poniższe polecenie zastępuje uzyskany wcześniej klucz attestation-token (w sekcji ~/go-tpm-tools/cmd/gotpm/). Dzięki temu uzyskasz tajny kod przechowywany na serwerze WWW udostępniającym tajne kody.
cd ~/go-tpm-tools/cmd/gotpm/ curl http://<cvm-attestation-codelab-web-server-internal-ip>:8080 -H "Authorization: Bearer $(cat ./attestation_token)"
Zastąp następujące elementy:
cvm-attestation-codelab-web-server-internal-ipto wewnętrzny adres IP instancji maszyny wirtualnej cvm-attestation-codelab-web-server.
Na ekranie pojawi się komunikat „This is the super secret information!”.
Jeśli wpiszesz nieprawidłowy lub wygasły attestation-token, zobaczysz komunikat „curl: (52) Empty reply from server” (curl: (52) Pusta odpowiedź z serwera). W logu serwera WWW z wersją tajną w instancji maszyny wirtualnej cvm-attestation-codelab-web-server zobaczysz też komunikat „Token valid: false”.
6. Czyszczenie
Uruchom te polecenia w konsoli Google Cloud lub w lokalnym środowisku programistycznym:
# 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
Zastąp następujące elementy:
project-idto unikalny identyfikator projektu.
7. Co dalej?
Dowiedz się więcej o poufnych maszynach wirtualnych i Compute Engine.