1. Übersicht
Confidential VMs (CVMs) sind eine Art von Compute Engine-VMs, die hardwarebasierte Speicherverschlüsselung verwenden. So wird sichergestellt, dass Ihre Daten und Anwendungen während der Nutzung nicht gelesen oder geändert werden können. In diesem Codelab wird gezeigt, wie mit Remote Attestation eine Remote-Partei die CVM-Knoten überprüfen kann. Außerdem sehen Sie sich das Cloud-Logging für weitere Audits an.

Wie in der Abbildung oben dargestellt, umfasst dieses Codelab die folgenden Schritte:
- CVM-Einrichtung und Remote-Bestätigung
- Cloud Logging-Untersuchung der CVM-Remote-Attestierung
- Webserver für die Secret-Freigabe einrichten
Die Komponenten im obigen Bild umfassen ein go-tpm-Tool und Google Cloud Attestation:
- go-tpm-Tool: Ein Open-Source-Tool zum Abrufen von Attestierungsnachweisen vom vTPM (Virtual Trusted Platform Module) auf der CVM und zum Senden an Google Cloud Attestation für ein Attestierungstoken.
- Google Cloud-Attestierung: Die empfangenen Attestierungsnachweise werden geprüft und validiert und ein Attestierungstoken wird zurückgegeben, das die Fakten zum CVM des Anfragenden widerspiegelt.
Lerninhalte
- Remote Attestation über Confidential Computing APIs auf CVM ausführen
- Cloud Logging zum Überwachen der Remote-Attestierung von Confidential VMs verwenden
Voraussetzungen
- Google Cloud Platform-Projekt
- Ein Browser, z. B. Chrome oder Firefox
- Grundkenntnisse zu Google Compute Engine und Confidential VMs
2. Einrichtung und Anforderungen
Führen Sie den folgenden Befehl in der Cloud Console oder in Ihrer lokalen Entwicklungsumgebung aus, um die erforderlichen APIs zu aktivieren:
gcloud auth login
gcloud services enable \
cloudapis.googleapis.com \
cloudshell.googleapis.com \
confidentialcomputing.googleapis.com \
compute.googleapis.com
3. Remote-Bestätigung für CVM und vTPM einrichten
In diesem Schritt erstellen Sie eine CVM und führen eine vTPM-Remote-Attestierung durch, um ein Attestierungs-Token (OIDC-Token) auf der CVM abzurufen.
Rufen Sie die Cloud Console oder Ihre lokale Entwicklungsumgebung auf. Erstellen Sie eine CVM wie folgt (siehe Confidential VM-Instanz erstellen | Google Cloud). Für den Zugriff auf Confidential Computing APIs sind Bereiche erforderlich.
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
Autorisieren Sie das CVM-Standarddienstkonto für den Zugriff auf Confidential Computing APIs. CVMs benötigen die folgenden Berechtigungen, um das Attestation Token aus Confidential Computing APIs abzurufen:
1). Erstellen Sie eine Rolle, um den Zugriff auf Confidential Computing APIs zu ermöglichen.
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
Ersetzen Sie Folgendes:
project-idist die eindeutige Kennung des Projekts.
2). Fügen Sie der Rolle das Standarddienstkonto der VM hinzu.
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"
Ersetzen Sie Folgendes:
project-idist die eindeutige Kennung des Projekts.
3). Stellen Sie eine Verbindung zur CVM her und richten Sie die go-tpm-Tool-Binärdatei ein, um das Attestierungstoken von den Confidential Computing APIs abzurufen, die von Google Cloud Attestation bereitgestellt werden.
- Stellen Sie eine Verbindung zur CVM her.
gcloud compute ssh --zone us-central1-c cvm-attestation-codelab
- Go-Umgebung einrichten:
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
- Erstellen Sie das Binärprogramm für das go-tpm-Tool. Die Binärdatei des Tools „go-tpm“ ruft Attestierungsnachweise vom vTPM auf der CVM ab und sendet sie an Google Cloud Attestation, um ein Attestierungstoken zu erhalten.
git clone https://github.com/google/go-tpm-tools.git --depth 1 cd go-tpm-tools/cmd/gotpm/ go build
- Mit dem Befehl des Tools „go-tpm“ werden die Attestierungsnachweise der CVM aus dem vTPM extrahiert und an Google Cloud Attestation gesendet. Google Cloud Attestation prüft und validiert die Attestierungsnachweise und gibt ein Attestierungstoken zurück. Mit dem Befehl wird eine Datei namens „attestation_token“ erstellt, die Ihre
attestation-tokenenthält. Sie verwendenattestation-tokenspäter, um ein Secret abzurufen. Sie können das Attestierungstoken unter jwt.io decodieren, um die Ansprüche aufzurufen.
sudo ./gotpm token > attestation_token
- (Optional) Alternativ zur Durchführung der Remote-Attestierung mit dem go-tpm-Tool und Google Cloud Attestation zeigen wir die Befehle zum Abrufen von vTPM-Attestierungsnachweisen. So können Sie einen Dienst wie Google Cloud Attestation erstellen, um die Attestierungsnachweise zu überprüfen und zu validieren:
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 enthält das bestätigte Ereignisprotokoll. Sie können sich die Datei mit Ihrem bevorzugten Editor ansehen. Mit dem Befehl „verify“ wird das Attestschlüsselzertifikat des Angebots nicht geprüft.
4. Cloud Logging aktivieren und das Log für die Remote-Attestierung ansehen
Sie können den folgenden Befehl in Ihrer cvm-attestation-codelab-CVM ausführen. Dieses Mal wird die Aktivität in Cloud Logging protokolliert.
sudo ./gotpm token --cloud-log --audience "https://api.cvm-attestation-codelab.com"
Rufen Sie cvm-attestation-codelab <instance-id> in Ihrer Cloud Console oder lokalen Entwicklungsumgebung ab.
gcloud compute instances describe cvm-attestation-codelab --zone us-central1-c --format='value(id)'
Wenn Sie Cloud Logging ausprobieren möchten, öffnen Sie die folgende URL: https://console.cloud.google.com/logs. Geben Sie im Abfragefeld Folgendes ein:
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
Ersetzen Sie Folgendes:
project-idist die eindeutige Kennung des Projekts.instance-idist die eindeutige Kennung der Instanz.
Sie sollten das Attestierungs-Token, die zugehörigen Claims, die Rohdaten und die an Google Cloud Attestation gesendete Nonce finden können.
5. Webserver für die Secret-Freigabe einrichten
In diesem Schritt beenden Sie die vorherige SSH-Sitzung und richten eine weitere VM ein. Auf dieser VM richten Sie einen Webserver für die Freigabe von Secrets ein. Der Webserver validiert das empfangene Attestierungstoken und seine Behauptungen. Wenn die Validierungen erfolgreich sind, wird das Secret für den Anfragenden freigegeben.
1). Rufen Sie die Cloud Console oder Ihre lokale Entwicklungsumgebung auf. Erstellen Sie eine virtuelle Maschine.
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
Ersetzen Sie Folgendes:
project-idist die eindeutige Kennung des Projekts.
2). Stellen Sie eine SSH-Verbindung zu Ihrer neuen VM her.
gcloud compute ssh --zone us-central1-c cvm-attestation-codelab-web-server
3). Richten Sie die Go-Umgebung ein.
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). Erstellen Sie die folgenden zwei Dateien, um den Quellcode des Webservers für die geheime Freigabe zu speichern (Kopieren/Einfügen mit 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). Führen Sie die folgenden Befehle aus, um den Webserver zu erstellen und auszuführen. Dadurch wird der Webserver für die Secret-Freigabe am Port :8080 gestartet.
go mod init google.com/codelab go mod tidy go get github.com/golang-jwt/jwt/v4 go build ./codelab
Fehlerbehebung: Möglicherweise wird die folgende Warnung angezeigt, die Sie ignorieren können, wenn Sie go mod tidy: ausführen.
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). Starten Sie nun einen weiteren Cloud Console-Tab oder eine weitere Sitzung der lokalen Entwicklungsumgebung und führen Sie den folgenden Befehl aus. Dadurch erhalten Sie das <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). Stellen Sie eine SSH-Verbindung zu Ihrer cvm-attestation-codelab-VM-Instanz her.
gcloud compute ssh --zone "us-central1-c" "cvm-attestation-codelab"
8). Mit dem folgenden Befehl wird das zuvor abgerufene attestation-token (unter ~/go-tpm-tools/cmd/gotpm/) ersetzt. Dadurch wird das Secret abgerufen, das vom Webserver für die Secret-Freigabe gespeichert wird.
cd ~/go-tpm-tools/cmd/gotpm/ curl http://<cvm-attestation-codelab-web-server-internal-ip>:8080 -H "Authorization: Bearer $(cat ./attestation_token)"
Ersetzen Sie Folgendes:
cvm-attestation-codelab-web-server-internal-ipist die interne IP-Adresse der VM-Instanz cvm-attestation-codelab-web-server.
Auf dem Bildschirm wird „Das sind die streng geheimen Informationen!“ angezeigt.
Wenn Sie ein falsches oder abgelaufenes attestation-token eingeben, wird die Meldung „curl: (52) Empty reply from server“ angezeigt. Im Webserverlog für die geheime Freigabe auf der cvm-attestation-codelab-web-server-VM-Instanz wird auch „Token valid: false“ angezeigt.
6. Bereinigen
Führen Sie die folgenden Befehle in der Cloud Console oder in Ihrer lokalen Entwicklungsumgebung aus:
# 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
Ersetzen Sie Folgendes:
project-idist die eindeutige Kennung des Projekts.
7. Nächste Schritte
Weitere Informationen zu Confidential VMs und Compute Engine