Confidential Space mit geschützten Ressourcen verwenden, die nicht bei einem Cloud-Anbieter gespeichert sind

1. Übersicht

Confidential Space bietet sicheren Datenaustausch und Zusammenarbeit zwischen mehreren Parteien und ermöglicht es Organisationen, die Vertraulichkeit ihrer Daten zu wahren. So können Organisationen zusammenarbeiten und gleichzeitig die Kontrolle über ihre Daten behalten und sie vor unbefugtem Zugriff schützen.

Confidential Space ermöglicht Szenarien, in denen Sie durch das Aggregieren und Analysieren sensibler, oft regulierter Daten einen gegenseitigen Mehrwert erzielen und gleichzeitig die volle Kontrolle darüber behalten. Mit Confidential Space können Organisationen durch das Aggregieren und Analysieren sensibler Daten wie personenidentifizierbarer Informationen (PII), geschützter Gesundheitsdaten (PHI), geistigen Eigentums und kryptografischer Secrets einen gegenseitigen Mehrwert erzielen und gleichzeitig die volle Kontrolle darüber behalten.

Voraussetzungen

Lerninhalte

  • Erforderliche Cloud-Ressourcen für die Ausführung von Confidential Space konfigurieren
  • Arbeitslast in einer Confidential VM mit dem Confidential Space-Image ausführen
  • So autorisieren Sie den Zugriff auf geschützte Ressourcen basierend auf den Attributen des Arbeitslastcodes (was), der Confidential Space-Umgebung (wo) und des Kontos, mit dem die Arbeitslast ausgeführt wird (wer).

In diesem Codelab wird beschrieben, wie Sie Confidential Space mit geschützten Ressourcen verwenden, die an einem anderen Ort als in Google Cloud gehostet werden. Sie erfahren, wie Sie ein benutzerdefiniertes, in sich geschlossenes Token vom Google Attestation Service anfordern, indem Sie eine Nonce, eine Zielgruppe und den PKI-Tokentyp angeben.

In diesem Codelab richten Sie einen Confidential Space zwischen einem fiktiven Produkt – USleep, einer containerisierten Anwendung – und einem fiktiven Produkt – UWear, einem verbundenen Wearable – ein, um Ihre Schlafqualität zu berechnen. UWear gibt geschützte Gesundheitsdaten (Protected Health Information, PHI) in einer sicheren und isolierten Umgebung (Trusted Execution Environment, TEE) an USleep weiter, sodass die Inhaber der Daten die vollständige Vertraulichkeit behalten.

UWear ist sowohl Workload-Auditor als auch Dateninhaber. Als Workload-Auditor prüft er den Code in der ausgeführten Arbeitslast und notiert den Image-Digest. Als Dateninhaber schreibt UWear die Bestätigungslogik, um die Gültigkeit des Tokens und seiner Signatur zu prüfen. Es wird eine Validierungsrichtlinie mit dem Image-Digest der geprüften Arbeitslasten erstellt, die nur dem spezifischen Image-Digest in einer bestimmten Umgebung den Zugriff auf die vertraulichen Daten erlaubt.

In diesem Codelab stellt USleep die containerisierte Anwendung bereit. USleep hat keinen Zugriff auf die vertraulichen Daten, führt aber die genehmigte Arbeitslast aus, die Zugriff auf die vertraulichen Daten hat.

Das Codelab umfasst die folgenden Schritte:

  • Schritt 1: Erforderliche Cloud-Ressourcen für das Codelab einrichten Projekte, Abrechnung und Berechtigungen einrichten Laden Sie den Quellcode für das Codelab herunter und legen Sie Umgebungsvariablen fest.
  • Schritt 2: Root-Zertifikat herunterladen und mit dem UWear-Quellcode speichern.
  • Schritt 3: Erstellen Sie separate Dienstkonten für Arbeitslasten, die von der Arbeitslast-VM für USleep und UWear verwendet werden.
  • Schritt 4: USleep-Arbeitslast erstellen, die ein Attestierungstoken bereitstellt
  • Schritt 5: Erstellen Sie die UWear-Arbeitslast, die das Attestierungstoken validiert und die vertraulichen Daten sendet, wenn das Token genehmigt wird.
  • Schritt 6: USleep- und UWear-Arbeitslasten ausführen UWear stellt die vertraulichen Daten bereit und USleep führt einen Schlafalgorithmus für die Daten aus und gibt ein Ergebnis aus.
  • Schritt 7 (optional): Führen Sie eine nicht autorisierte USleep-Arbeitslast aus und bestätigen Sie, dass keine vertraulichen Daten von UWear empfangen wurden.
  • Schritt 8: Alle Ressourcen bereinigen.

Workflow

USleep führt die Arbeitslast in Confidential Space aus. Damit der Arbeitslast ausgeführt werden kann, muss er Zugriff auf die vertraulichen Gesundheitsinformationen von UWear haben. Um Zugriff zu erhalten, erstellt die USleep-Arbeitslast zuerst eine sichere TLS-Sitzung. USleep fordert dann auch ein Attestierungstoken vom Google Attestation Service mit einer Nutzlast an.

USleep fordert ein Attestierungstoken mit einer JSON-Nutzlast an, die drei Dinge enthält:

  1. Ein Attestierungstoken, das an die TLS-Sitzung gebunden ist. Damit das Attestierungstoken an die TLS-Sitzung gebunden wird, ist der Nonce-Wert der Hash des TLS Exported Keying Material. Durch die Bindung des Tokens an die TLS-Sitzung wird sichergestellt, dass keine Man-in-the-Middle-Angriffe stattfinden, da nur die beiden an der TLS-Sitzung beteiligten Parteien den Nonce-Wert generieren können.
  2. Es wird eine Zielgruppe mit dem Namen „uwear“ bereitgestellt. UWear prüft, ob es die beabsichtigte Zielgruppe für das Attestierungstoken ist.
  3. Ein Token vom Typ „PKI“ Ein Tokentyp vom Typ „PKI“ bedeutet, dass USleep ein in sich geschlossenes Token anfordern möchte. Das in sich geschlossene Token kann mit dem Root, der vom bekannten PKI-Endpunkt von Confidential Space heruntergeladen wurde, daraufhin überprüft werden, ob es von Google signiert wurde. Dies steht im Gegensatz zum Standard-OIDC-Tokentyp, dessen Signatur mit einem öffentlichen Schlüssel verifiziert wird, der regelmäßig rotiert.

bb013916a3222ce7.png

Die USleep-Arbeitslast empfängt das Attestierungstoken. UWear stellt dann eine Verbindung zur TLS-Verbindung mit USleep her und ruft das Attestierungstoken von USleep ab. UWear validiert das Token, indem es den x5c-Anspruch mit dem Root-Zertifikat vergleicht.

UWear genehmigt die USleep-Arbeitslast, wenn:

  1. Das Token besteht die PKI-Validierungslogik.
  2. UWear validiert das Token, indem es die x5c-Anforderung mit dem Root-Zertifikat vergleicht, prüft, ob das Token mit dem Leaf-Zertifikat signiert ist, und schließlich, ob das heruntergeladene Root-Zertifikat mit dem Root-Zertifikat in der x5c-Anforderung übereinstimmt.
  3. Die Ansprüche zur Messung der Arbeitslast im Token stimmen mit den Attributbedingungen überein, die in der OPA-Richtlinie angegeben sind. OPA ist ein Open-Source-Richtlinienmodul für allgemeine Zwecke, das die Richtliniendurchsetzung im gesamten Stack vereinheitlicht. OPA verwendet Dokumente mit einer ähnlichen Syntax wie JSON, um Basiswerte festzulegen, anhand derer die Richtlinie validiert wird. Ein Beispiel dafür, welche Werte in der Richtlinie geprüft werden, finden Sie unter OPA-Referenzwerte.
  4. Die Nonce entspricht der erwarteten Nonce (TLS Exported Keying Material). Das wird in der OPA-Richtlinie oben bestätigt.

Sobald alle diese Prüfungen abgeschlossen und bestanden wurden, kann UWear bestätigen, dass die Daten sicher gesendet und verarbeitet werden. UWear antwortet dann über dieselbe TLS-Sitzung mit den vertraulichen vertraulichen Gesundheitsinformationen und USleep kann diese Daten verwenden, um die Schlafqualität des Kunden zu berechnen.

2. Cloud-Ressourcen einrichten

Hinweis

  1. Richten Sie zwei Google Cloud-Projekte ein, eines für USleep und eines für UWear. Weitere Informationen zum Erstellen eines Google Cloud-Projekts finden Sie im Codelab „Erstes Google-Projekt einrichten und darin navigieren“. Weitere Informationen zum Abrufen der Projekt-ID und zum Unterschied zwischen Projekt-ID, Projektname und Projektnummer finden Sie unter Projekte erstellen und verwalten.
  2. Aktivieren Sie die Abrechnung für Ihre Projekte.
  3. Legen Sie in der Cloud Shell eines Ihrer Google-Projekte die erforderlichen Projektumgebungsvariablen wie unten gezeigt fest.
export UWEAR_PROJECT_ID=<Google Cloud project id of UWear>
export USLEEP_PROJECT_ID=<Google Cloud project id of USleep>
  1. Aktivieren Sie die Confidential Computing API und die folgenden APIs für beide Projekte.
gcloud config set project $UWEAR_PROJECT_ID
gcloud services enable \
    cloudapis.googleapis.com \
    cloudshell.googleapis.com \
    container.googleapis.com \
    containerregistry.googleapis.com \
    confidentialcomputing.googleapis.com

gcloud config set project $USLEEP_PROJECT_ID
gcloud services enable \
    cloudapis.googleapis.com \
    cloudshell.googleapis.com \
    container.googleapis.com \
    containerregistry.googleapis.com \
    confidentialcomputing.googleapis.com
  1. Hauptkonto-ID abrufen mit
gcloud auth list

# Output should contain
# ACCOUNT: <Principal Identifier>

# Set your member variable
export MEMBER='user:<Principal Identifier>'
  1. Fügen Sie Berechtigungen für diese beiden Projekte hinzu. Berechtigungen können hinzugefügt werden, indem Sie der Anleitung zum Zuweisen einer IAM-Rolle folgen.
gcloud config set project $UWEAR_PROJECT_ID

# Add Artifact Registry Administrator role
gcloud projects add-iam-policy-binding $UWEAR_PROJECT_ID --member=$MEMBER --role='roles/iam.serviceAccountAdmin'

# Add Service Account Administrator role
gcloud projects add-iam-policy-binding $UWEAR_PROJECT_ID --member=$MEMBER --role='roles/artifactregistry.admin'
gcloud config set project $USLEEP_PROJECT_ID

# Add Service Account Administrator role
gcloud projects add-iam-policy-binding $USLEEP_PROJECT_ID --member=$MEMBER --role='roles/iam.serviceAccountAdmin'

# Add Artifact Registry Administrator role
gcloud projects add-iam-policy-binding $USLEEP_PROJECT_ID --member=$MEMBER --role='roles/artifactregistry.admin'

# Add Compute Administrator role
gcloud projects add-iam-policy-binding $USLEEP_PROJECT_ID --member=$MEMBER --role='roles/compute.admin'

# Add Storage Administrator role
gcloud projects add-iam-policy-binding $USLEEP_PROJECT_ID --member=$MEMBER --role='roles/compute.storageAdmin'
  1. Klonen Sie in Cloud Shell eines Ihrer Google Cloud-Projekte das Confidential Space Codelab-GitHub-Repository mit dem folgenden Befehl, um die erforderlichen Skripts zu erhalten, die in diesem Codelab verwendet werden.
git clone https://github.com/GoogleCloudPlatform/confidential-space.git
  1. Wechseln Sie in das Verzeichnis mit den Skripts für das Codelab zu Gesundheitsdaten.
cd confidential-space/codelabs/health_data_analysis_codelab/scripts
  1. Aktualisieren Sie diese beiden Zeilen im Skript „config_env.sh“ im Verzeichnis „codelabs/health_data_analysis_codelab/scripts“. Aktualisieren Sie die Projekt-IDs mit Ihren Projekt-IDs für USleep und UWear. Entfernen Sie das Kommentarsymbol „#“ am Anfang der Zeile.
# TODO: Populate UWear and USleep Project IDs
export UWEAR_PROJECT_ID=your-uwear-project-id
export USLEEP_PROJECT_ID=your-usleep-project-id
  1. Optional: Legen Sie bereits vorhandene Variablen fest. Sie können die Ressourcennamen mit diesen Variablen überschreiben, z. B. export UWEAR_ARTIFACT_REPOSITORY='my-artifact-repository'.
  • Sie können die folgenden Variablen mit vorhandenen Cloud-Ressourcennamen festlegen. Wenn die Variable festgelegt ist, wird die entsprechende vorhandene Cloud-Ressource aus dem Projekt verwendet. Wenn die Variable nicht festgelegt ist, wird der Name der Cloud-Ressource aus den Werten im Skript config_env.sh generiert.
  1. Führen Sie das config_env.sh-Skript aus, um die verbleibenden Variablennamen auf Werte festzulegen, die auf Ihrer Projekt-ID für Ressourcennamen basieren.
# Navigate to the scripts folder
cd ~/confidential-space/codelabs/health_data_analysis_codelab/scripts

# Run the config_env script
source config_env.sh

# Verify the variables were set
# Expected output for default variable should be `workload-sa`
echo $USLEEP_WORKLOAD_SERVICE_ACCOUNT

3. Root-Zertifikat herunterladen

  1. Um das vom Attestierungsservice zurückgegebene in sich geschlossene Token zu validieren, muss UWear die Signatur anhand des Confidential Space-Root-Zertifikats validieren. UWear muss das Root-Zertifikat herunterladen und lokal speichern. Führen Sie in der Konsole eines Ihrer Google Cloud-Projekte die folgenden Befehle aus:
cd ~/confidential-space/codelabs/health_data_analysis_codelab/src/uwear

wget https://confidentialcomputing.googleapis.com/.well-known/confidential_space_root.crt -O confidential_space_root.pem
  1. Fingerabdruck des heruntergeladenen Root-Zertifikats generieren
openssl x509 -fingerprint -in confidential_space_root.pem -noout
  1. Prüfen Sie, ob der Fingerabdruck mit dem folgenden SHA-1-Digest übereinstimmt:
B9:51:20:74:2C:24:E3:AA:34:04:2E:1C:3B:A3:AA:D2:8B:21:23:21

4. Dienstkonto für Arbeitslast erstellen

Als Nächstes erstellen Sie zwei Dienstkonten: eines für die USleep- und eines für die UWear-Arbeitslasten. Führen Sie das Skript create_service_accounts.sh aus, um Dienstkonten für Arbeitslasten in den Projekten „USleep“ und „UWear“ zu erstellen. Die VMs, auf denen die Arbeitslasten ausgeführt werden, würden diese Dienstkonten verwenden.

# Navigate to the scripts folder
cd ~/confidential-space/codelabs/health_data_analysis_codelab/scripts

# Run the create_service_accounts script
./create_service_accounts.sh

Das Skript:

  • Weist die Rolle iam.serviceAccountUser zu, mit der das Dienstkonto an die Arbeitslast angehängt wird.
  • Weist dem Dienstkonto der Arbeitslast die Rolle confidentialcomputing.workloadUser zu . Dadurch kann mit dem Nutzerkonto ein Attestierungstoken generiert werden.
  • Gewährt dem Dienstkonto der Arbeitslast die Rolle logging.logWriter. Dadurch kann die Confidential Space-Umgebung zusätzlich zur seriellen Konsole Logs in Cloud Logging schreiben, sodass Logs auch nach dem Beenden der VM verfügbar sind.

5. USleep-Arbeitslast erstellen

In diesem Schritt erstellen Sie Docker-Images für die in diesem Codelab verwendeten Arbeitslasten. Der USleep-Arbeitslast ist eine einfache Golang-Anwendung, die die Schlafqualität eines Kunden anhand persönlicher Gesundheitsinformationen auf einem Wearable bestimmt.

Informationen zur USleep-Arbeitslast

Der USleep-Arbeitslast ist eine einfache Golang-Anwendung, die anhand persönlicher Gesundheitsinformationen auf einem Wearable die Schlafqualität eines Kunden ermittelt. Der USleep-Arbeitslast besteht aus drei Hauptteilen:

  1. TLS-Sitzung einrichten und Exported Keying Material extrahieren
func handleConnectionRequest(w http.ResponseWriter, r *http.Request) {
  // Upgrade HTTP Connection to a websocket.
  conn, err := upgrader.Upgrade(w, r, nil)
  if err != nil {
    fmt.Printf("failed to upgrade connection to a websocket with err: %v\n", err)
    return
  }
  defer conn.Close()

  // Get EKM
  hash, err := getEKMHashFromRequest(r)
  if err != nil {
    fmt.Printf("Failed to get EKM: %v", err)
  }
  ...
}

func getEKMHashFromRequest(r *http.Request) (string, error) {
  ekm, err := r.TLS.ExportKeyingMaterial("testing_nonce", nil, 32)
  if err != nil {
    err := fmt.Errorf("failed to get EKM from inbound http request: %w", err)
    return "", err
  }

  sha := sha256.New()
  sha.Write(ekm)
  hash := base64.StdEncoding.EncodeToString(sha.Sum(nil))

  fmt.Printf("EKM: %v\nSHA hash: %v", ekm, hash)
  return hash, nil
}
  1. Ein Token vom Attestation Service mit einer Zielgruppe, einem Nonce und einem PKI-Tokentyp anfordern.
func handleConnectionRequest(w http.ResponseWriter, r *http.Request) {
  ...

  // Request token with TLS Exported Keying Material (EKM) hashed.
  token, err := getCustomToken(hash)
  if err != nil {
    fmt.Printf("failed to get custom token from token endpoint: %v", err)
    return
  }

  // Respond to the client with the token.
  conn.WriteMessage(websocket.TextMessage, token)

  ...
}

var (
        socketPath    = "/run/container_launcher/teeserver.sock"
        tokenEndpoint = "http://localhost/v1/token"
        contentType   = "application/json"
)


func getCustomToken(nonce string) ([]byte, error) {
  httpClient := http.Client{
    Transport: &http.Transport{
      // Set the DialContext field to a function that creates
      // a new network connection to a Unix domain socket
      DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
        return net.Dial("unix", socketPath)
      },
    },
  }

  body := fmt.Sprintf(`{
    "audience": "uwear",
    "nonces": ["%s"],
    "token_type": "PKI"
  }`, nonce)

  resp, err := httpClient.Post(tokenEndpoint, contentType, strings.NewReader(body))
  if err != nil {
    return nil, err
  }

  fmt.Printf("Response from launcher: %v\n", resp)
  text, err := io.ReadAll(resp.Body)
  if err != nil {
    return nil, fmt.Errorf("Failed to read resp.Body: %w", err)
  }
  fmt.Printf("Token from the attestation service: %s\n", text)

  return text, nil
}
  1. Sensible Daten empfangen und die Schlafqualität des Nutzers berechnen
func handleConnectionRequest(w http.ResponseWriter, r *http.Request) {
  ...

  // Read the sensitive data
  _, content, err := conn.ReadMessage()
  if err != nil {
    fmt.Printf("failed to read message from the connection: %v\n", err)
  }
  fmt.Printf("Received content from other side, %v\n", string(content))

 // TODO: Handle sensitive data
  ...
}

Schritte zum Erstellen der USleep-Arbeitslast

  1. Führen Sie das Skript create_usleep_workload.sh aus, um die USleep-Arbeitslast zu erstellen. Mit diesem Skript wird Folgendes ausgeführt:
  • Erstellt ein Artifact Registry-Repository ($USLEEP_ARTIFACT_REPOSITORY), das UWear gehört und in dem die Arbeitslast veröffentlicht wird.
  • Erstellt den Code usleep/workload.go und verpackt ihn in einem Docker-Image. Dockerfile-Konfiguration für USleep
  • Das Docker-Image wird in der Artifact Registry ($USLEEP_ARTIFACT_REPOSITORY) von UWear veröffentlicht.
  • Gewährt dem Dienstkonto $USLEEP_WORKLOAD_SERVICE_ACCOUNT die Leseberechtigung für Artifact Registry ($USLEEP_ARTIFACT_REPOSITORY).
./create_usleep_workload.sh
  1. Wichtig: Extrahieren Sie im Ausgabelog den Image-Digest für USleep.
latest: digest: sha256:<USLEEP_IMAGE_DIGEST> size: 945
  1. Rufen Sie das UWear-Verzeichnis auf.
cd ~/confidential-space/codelabs/health_data_analysis_codelab/src/uwear
  1. Ersetzen Sie den Wert unter „allowed_submods_container_image_digest“ in der Datei opa_validation_values.json durch den USLEEP_IMAGE_DIGEST.
# Replace the image digest
sed -i 's/sha256:bc4c32cb2ca046ba07dcd964b07a320b7d0ca88a5cf8e979da15cae68a2103ee/sha256:<USLEEP_IMAGE_DIGEST>/' ~/confidential-space/codelabs/health_data_analysis_codelab/src/uwear/opa_validation_values.json

6. UWear-Arbeitslast erstellen

Informationen zur UWear-Arbeitslast

Die UWear-Arbeitslast besteht aus vier Hauptteilen:

  1. Der Dienst tritt derselben TLS-Sitzung bei, die in der USleep-Arbeitslast erstellt wurde, und ruft das Attestierungstoken von USleep über die sichere TLS-Sitzung ab.
func main() {
  fmt.Println("Initializing client...")

  tlsconfig := &tls.Config{
    // Skipping client verification of the server's certificate chain and host name since we are
    // doing custom verification using the attestation token.
    InsecureSkipVerify: true,
  }

  dialer := websocket.Dialer{
    TLSClientConfig:  tlsconfig,
    HandshakeTimeout: 5 * time.Second,
  }

  ipAddress := os.Getenv(ipAddrEnvVar)
  url := fmt.Sprintf("wss://%s:8081/connection", ipAddress)

  fmt.Printf("Attempting to dial to url %v...\n", url)
  conn, _, err := dialer.Dial(url, nil)
  if err != nil {
    fmt.Printf("Failed to dial to url %s, err %v\n", url, err)
    return
  }

  defer conn.Close()

  tokenString, ekm, err := retrieveTokenAndEKMFromConn(conn)
  if err != nil {
    fmt.Printf("Failed to retrieve token and EKM from connection: %v\n", err)
    return
  }

  fmt.Printf("token: %v\n", tokenString)

  ...
}
  1. Das in sich geschlossene Token wird validiert, indem
  • Beim Prüfen der x5c-Anforderung wird geprüft, ob die Zertifikatskette korrekt vom Blattzertifikat über das Zwischenzertifikat zum Stammzertifikat führt.
  • Prüfen, ob das Token mit dem im Anspruch „x5c“ enthaltenen Blattzertifikat signiert ist.
  • Prüfen Sie, ob das heruntergeladene / gespeicherte Stammzertifikat mit dem Stammzertifikat in der x5c-Anforderung übereinstimmt.
func main() {
  ...

  token, err := validatePKIToken(tokenString)
  if err != nil {
    fmt.Printf("Failed to validate PKI token, err: %v\n.", err)
    return
  }
  fmt.Println("PKI token validated successfully")
 
  ...
}

// validatePKIToken validates the PKI token returned from the attestation service.
// It verifies the token the certificate chain and that the token is signed by Google
// Returns a jwt.Token or returns an error if invalid.
func validatePKIToken(attestationToken string) (jwt.Token, error) {
  // IMPORTANT: The attestation token should be considered untrusted until the certificate chain and
  // the signature is verified.
  rawRootCertificate, err := readFile(rootCertificateFile)
  if err != nil {
    return jwt.Token{}, fmt.Errorf("readFile(%v) - failed to read root certificate: %w", rootCertificateFile, err)
  }

  storedRootCert, err := decodeAndParsePEMCertificate(string(rawRootCertificate))
  if err != nil {
    return jwt.Token{}, fmt.Errorf("DecodeAndParsePEMCertificate(string) - failed to decode and parse root certificate: %w", err)
  }

  jwtHeaders, err := extractJWTHeaders(attestationToken)
  if err != nil {
    return jwt.Token{}, fmt.Errorf("ExtractJWTHeaders(token) - failed to extract JWT headers: %w", err)
  }

  if jwtHeaders["alg"] != "RS256" {
    return jwt.Token{}, fmt.Errorf("ValidatePKIToken(attestationToken, ekm) - got Alg: %v, want: %v", jwtHeaders["alg"], "RS256")
  }

  // Additional Check: Validate the ALG in the header matches the certificate SPKI.
  // https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.7
  // This is included in Golang's jwt.Parse function

  x5cHeaders := jwtHeaders["x5c"].([]any)
  certificates, err := extractCertificatesFromX5CHeader(x5cHeaders)
  if err != nil {
    return jwt.Token{}, fmt.Errorf("ExtractCertificatesFromX5CHeader(x5cHeaders) returned error: %w", err)
  }

  // Verify the leaf certificate signature algorithm is an RSA key
  if certificates.LeafCert.SignatureAlgorithm != x509.SHA256WithRSA {
    return jwt.Token{}, fmt.Errorf("leaf certificate signature algorithm is not SHA256WithRSA")
  }

  // Verify the leaf certificate public key algorithm is RSA
  if certificates.LeafCert.PublicKeyAlgorithm != x509.RSA {
    return jwt.Token{}, fmt.Errorf("leaf certificate public key algorithm is not RSA")
  }

  // Verify the storedRootCertificate is the same as the root certificate returned in the token
  // storedRootCertificate is downloaded from the confidential computing well known endpoint
  // https://confidentialcomputing.googleapis.com/.well-known/attestation-pki-root
  err = compareCertificates(*storedRootCert, *certificates.RootCert)
  if err != nil {
    return jwt.Token{}, fmt.Errorf("failed to verify certificate chain: %w", err)
  }

  err = verifyCertificateChain(certificates)
  if err != nil {
    return jwt.Token{}, fmt.Errorf("VerifyCertificateChain(CertificateChain) - error verifying x5c chain: %v", err)
  }

  keyFunc := func(token *jwt.Token) (any, error) {
    return certificates.LeafCert.PublicKey, nil
  }

  verifiedJWT, err := jwt.Parse(attestationToken, keyFunc)
  return *verifiedJWT, err
}



// verifyCertificateChain verifies the certificate chain from leaf to root.
// It also checks that all certificate lifetimes are valid.
func verifyCertificateChain(certificates CertificateChain) error {
    // Additional check: Verify that all certificates in the cert chain are valid.
    // Note: The *x509.Certificate Verify method in Golang already validates this but for other coding
    // languages it is important to make sure the certificate lifetimes are checked.
    if isCertificateLifetimeValid(certificates.LeafCert) {
        return fmt.Errorf("leaf certificate is not valid")
    }

    if isCertificateLifetimeValid(certificates.IntermediateCert) {
        return fmt.Errorf("intermediate certificate is not valid")
    }
    interPool := x509.NewCertPool()
    interPool.AddCert(certificates.IntermediateCert)

    if isCertificateLifetimeValid(certificates.RootCert) {
        return fmt.Errorf("root certificate is not valid")
    }
    rootPool := x509.NewCertPool()
    rootPool.AddCert(certificates.RootCert)

    _, err := certificates.LeafCert.Verify(x509.VerifyOptions{
        Intermediates: interPool,
        Roots:         rootPool,
        KeyUsages:     []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
    })

    if err != nil {
        return fmt.Errorf("failed to verify certificate chain: %v", err)
    }

    return nil
}
  1. Die UWear-Arbeitslast prüft dann, ob die Behauptungen zur Arbeitslastmessung im Token mit den in der OPA-Richtlinie angegebenen Attributbedingungen übereinstimmen. OPA ist ein Open-Source-Richtlinienmodul für allgemeine Zwecke, das die Richtliniendurchsetzung im gesamten Stack vereinheitlicht. OPA verwendet Dokumente mit einer ähnlichen Syntax wie JSON, um Basiswerte festzulegen, anhand derer die Richtlinie validiert wird.
func main() {
  ...

  err = validateClaimsAgainstOPAPolicy(token, ekm)
  if err != nil {
    fmt.Printf("Failed to validate claims against OPA policy: %v\n", err)
  return
  }

  fmt.Println("Validated token and claims. Sending sensitive data")

  ...
}

// validateClaimsAgainstOPAPolicy validates the claims in the JWT token against the OPA policy.
func validateClaimsAgainstOPAPolicy(token jwt.Token, ekm string) error {
        data, err := os.ReadFile("opa_validation_values.json")
        authorized, err := evaluateOPAPolicy(context.Background(), token, ekm, string(data))
        if err != nil {
                fmt.Println("Error evaluating OPA policy:", err)
                return fmt.Errorf("failed to evaluate OPA policy: %w", err)
        }
        if !authorized {
                fmt.Println("Remote TEE's JWT failed policy check.")
                return fmt.Errorf("remote TEE's JWT failed policy check")
        }
        fmt.Println("JWT is authorized.")
        return nil
}


// evaluateOPAPolicy returns boolean indicating if OPA policy is satisfied or not, or error if occurred
func evaluateOPAPolicy(ctx context.Context, token jwt.Token, ekm string, policyData string) (bool, error) {
        var claims jwt.MapClaims
        var ok bool
        if claims, ok = token.Claims.(jwt.MapClaims); !ok {
                return false, fmt.Errorf("failed to get the claims from the JWT")
        }

        module := fmt.Sprintf(opaPolicy, ekm)

        var json map[string]any
        err := util.UnmarshalJSON([]byte(policyData), &json)
        store := inmem.NewFromObject(json)

        // Bind 'allow' to the value of the policy decision
        // Bind 'hw_verified', 'image_verified', 'audience_verified, 'nonce_verified' to their respective policy evaluations
        query, err := rego.New(
                rego.Query(regoQuery),                          // Argument 1 (Query string)
                rego.Store(store),                              // Argument 2 (Data store)
                rego.Module("confidential_space.rego", module), // Argument 3 (Policy module)
        ).PrepareForEval(ctx)

        if err != nil {
                fmt.Printf("Error creating query: %v\n", err)
                return false, err
        }

        fmt.Println("Performing OPA query evaluation...")
        results, err := query.Eval(ctx, rego.EvalInput(claims))

        if err != nil {
                fmt.Printf("Error evaluating OPA policy: %v\n", err)
                return false, err
        } else if len(results) == 0 {
                fmt.Println("Undefined result from evaluating OPA policy")
                return false, err
        } else if result, ok := results[0].Bindings["allow"].(bool); !ok {
                fmt.Printf("Unexpected result type: %v\n", ok)
                fmt.Printf("Result: %+v\n", result)
                return false, err
        }

        fmt.Println("OPA policy evaluation completed.")

        fmt.Println("OPA policy result values:")
        for key, value := range results[0].Bindings {
                fmt.Printf("[ %s ]: %v\n", key, value)
        }
        result := results[0].Bindings["allow"]
        if result == true {
                fmt.Println("Policy check PASSED")
                return true, nil
        }
        fmt.Println("Policy check FAILED")
        return false, nil
}
{
  "allowed_submods_container_image_digest": [
    "sha256:<USLEEP_IMAGE_DIGEST>"
  ],
  "allowed_hwmodel": [
    "GCP_INTEL_TDX",
    "GCP_SHIELDED_VM",
    "GCP_AMD_SEV_ES",
    "GCP_AMD_SEV"
  ],
  "allowed_aud": [
    "uwear"
  ],
  "allowed_issuer": [
    "https://confidentialcomputing.googleapis.com"
  ],
  "allowed_secboot": [
    true
  ],
  "allowed_sw_name": [
    "CONFIDENTIAL_SPACE"
  ]
}
package confidential_space

import rego.v1

default allow := false
default hw_verified := false
default image_digest_verified := false
default audience_verified := false
default nonce_verified := false
default issuer_verified := false
default secboot_verified := false
default sw_name_verified := false

allow if {
  hw_verified
  image_digest_verified
  audience_verified
  nonce_verified
  issuer_verified
  secboot_verified
  sw_name_verified
}

hw_verified if input.hwmodel in data.allowed_hwmodel
image_digest_verified if input.submods.container.image_digest in data.allowed_submods_container_image_digest
audience_verified if input.aud in data.allowed_aud
issuer_verified if input.iss in data.allowed_issuer
secboot_verified if input.secboot in data.allowed_secboot
sw_name_verified if input.swname in data.allowed_sw_name
nonce_verified if {
  input.eat_nonce == "%s"
}
  • Beispiel für eine Rego-Abfrage.
regoQuery = "
    allow = data.confidential_space.allow;
    hw_verified = data.confidential_space.hw_verified;
    image__digest_verified = data.confidential_space.image_digest_verified;
    audience_verified = data.confidential_space.audience_verified;
    nonce_verified = data.confidential_space.nonce_verified;
    issuer_verified = data.confidential_space.issuer_verified;
    secboot_verified = data.confidential_space.secboot_verified;
    sw_name_verified = data.confidential_space.sw_name_verified
"

Beispielcode zum Abrufen des EKM-Hashs:

func getEKMHashFromConn(c *websocket.Conn) (string, error) {
  conn, ok := c.NetConn().(*tls.Conn)
  if !ok {
    return "", fmt.Errorf("failed to cast NetConn to *tls.Conn")
  }

  state := conn.ConnectionState()
  ekm, err := state.ExportKeyingMaterial("testing_nonce", nil, 32)
  if err != nil {
    return "", fmt.Errorf("failed to get EKM from TLS connection: %w", err)
  }

  sha := sha256.New()
  sha.Write(ekm)
  hash := base64.StdEncoding.EncodeToString(sha.Sum(nil))

  return hash, nil
}
  1. Sobald alle diese Prüfungen abgeschlossen und bestanden wurden, kann UWear bestätigen, dass die Daten sicher gesendet und verarbeitet werden. UWear antwortet dann über dieselbe TLS-Sitzung mit den vertraulichen vertraulichen Gesundheitsinformationen und USleep kann diese Daten verwenden, um die Schlafqualität des Kunden zu berechnen.
func main() {
  ...

  fmt.Println("Validated token and claims. Sending sensitive data")

  data, err := readFile(mySensitiveDataFile)
  if err != nil {
    fmt.Printf("Failed to read data from the file: %v\n", err)
  }

  conn.WriteMessage(websocket.BinaryMessage, data)
  fmt.Println("Sent payload. Closing the connection")
  conn.Close()
  
  ...
}

Schritte zum Erstellen der USleep-Arbeitslast

  1. Rufen Sie das Verzeichnis „scripts“ auf:
cd ~/confidential-space/codelabs/health_data_analysis_codelab/scripts
  1. Führen Sie das Skript create_uwear_workload.sh aus, um die UWear-Arbeitslast zu erstellen:
  • Erstellt ein Artifact Registry-Repository ($UWEAR_ARTIFACT_REPOSITORY), das UWear gehört und in dem die Arbeitslast veröffentlicht wird.
  • Erstellt den Code uwear/workload.go und verpackt ihn in einem Docker-Image. Dockerfile-Konfiguration für USleep
  • Das Docker-Image wird in der Artifact Registry ($UWEAR_ARTIFACT_REPOSITORY) von UWear veröffentlicht.
  • Gewährt dem Dienstkonto $UWEAR_WORKLOAD_SERVICE_ACCOUNT die Leseberechtigung für Artifact Registry ($UWEAR_ARTIFACT_REPOSITORY).
./create_uwear_workload.sh

7. USleep- und UWear-Arbeitslasten ausführen

USleep-Arbeitslast ausführen

gcloud config set project $USLEEP_PROJECT_ID


gcloud compute instances create \
 --confidential-compute-type=SEV \
 --shielded-secure-boot \
 --maintenance-policy=MIGRATE \
 --scopes=cloud-platform --zone=${USLEEP_PROJECT_ZONE} \
 --image-project=confidential-space-images \
 --image-family=confidential-space \
--service-account=${USLEEP_WORKLOAD_SERVICE_ACCOUNT}@${USLEEP_PROJECT_ID}.iam.gserviceaccount.com \
 --metadata ^~^tee-image-reference=${USLEEP_PROJECT_REPOSITORY_REGION}-docker.pkg.dev/${USLEEP_PROJECT_ID}/${USLEEP_ARTIFACT_REPOSITORY}/${USLEEP_WORKLOAD_IMAGE_NAME}:${USLEEP_WORKLOAD_IMAGE_TAG}~tee-restart-policy=Never~tee-container-log-redirect=true usleep

Die Antwort sollte STATUS: RUNNING und die EXTERNAL_IP zurückgeben, die in etwa so aussehen sollte:

NAME: usleep
ZONE: us-west1-b
MACHINE_TYPE: n2d-standard-2
PREEMPTIBLE:
INTERNAL_IP: 10.138.0.6
EXTERNAL_IP: 34.168.56.10
STATUS: RUNNING

Externe IP-Adresse in einer Variablen speichern

export USLEEP_EXTERNAL_IP=<add your external IP> 

Prüfen, ob die USleep-Arbeitslast korrekt ausgeführt wurde

Wenn Sie prüfen möchten, ob die USleep-Arbeitslast korrekt ausgeführt wird, rufen Sie im USleep-Projekt die Seite VM-Instanzen auf. Klicken Sie auf die „usleep“-Instanz und drücken Sie im Bereich „Logs“ auf „Serieller Port 1(Konsole)“. Wenn der Server ausgeführt wird, sollte unten in den Logs etwas Ähnliches wie das Folgende angezeigt werden.

2024/09/13 17:00:00 workload task started
#####----- Local IP Address is <YOUR-LOCAL-IP> -----#####
Starting Server..

UWear-Arbeitslast ausführen

gcloud config set project $UWEAR_PROJECT_ID

gcloud compute instances create \
 --confidential-compute-type=SEV \
 --shielded-secure-boot \
 --maintenance-policy=MIGRATE \
 --scopes=cloud-platform --zone=${UWEAR_PROJECT_ZONE} \
 --image-project=confidential-space-images \
 --image-family=confidential-space \
--service-account=${UWEAR_WORKLOAD_SERVICE_ACCOUNT}@${UWEAR_PROJECT_ID}.iam.gserviceaccount.com \
 --metadata ^~^tee-image-reference=${UWEAR_PROJECT_REPOSITORY_REGION}-docker.pkg.dev/${UWEAR_PROJECT_ID}/${UWEAR_ARTIFACT_REPOSITORY}/${UWEAR_WORKLOAD_IMAGE_NAME}:${UWEAR_WORKLOAD_IMAGE_TAG}~tee-restart-policy=Never~tee-container-log-redirect=true~tee-env-remote_ip_addr=$USLEEP_EXTERNAL_IP uwear

Prüfen, ob die UWear-Arbeitslast korrekt ausgeführt wurde

Wenn Sie die Logs der UWear-Arbeitslast aufrufen möchten, rufen Sie im UWear-Projekt die Seite VM-Instanzen auf. Klicken Sie auf die „uwear“-Instanz und drücken Sie im Bereich „Logs“ auf „Serieller Port 1(Konsole)“.

Die Log-Ausgabe, nachdem die Instanz vollständig gestartet wurde, sollte so aussehen:

In den seriellen Logs des UWear-Projekts sollte etwas Ähnliches wie das Folgende angezeigt werden:

token: eyJ[...]MrXUg
PKI token validated successfully
Performing OPA query evaluation...
OPA policy evaluation completed.
OPA policy result values:
[ hw_verified ]: true
[ image__digest_verified ]: true
[ audience_verified ]: true
[ nonce_verified ]: true
[ issuer_verified ]: true
[ secboot_verified ]: true
[ sw_name_verified ]: true
[ allow ]: true
Policy check PASSED
JWT is authorized.
Validated token and claims. Sending sensitive data
Sent payload. Closing the connection

Wenn Ihre UWear-Arbeitslast nicht so aussieht, finden Sie unten eine Anleitung.

USleep-Ergebnisse ansehen

Rufen Sie die Seite „VM-Instanzen“ im Projekt „USleep“ auf, um die Ergebnisse zu sehen. Klicken Sie auf die „usleep“-Instanz und drücken Sie im Bereich „Logs“ auf „Serieller Port 1(Konsole)“. Die Ergebnisse der Arbeitslast werden unten in den Logs angezeigt. Sie sollten dem Beispiel unten ähneln.

Token from the attestation service: eyJhbGci...Ii5A3CJBuDM2o5Q
Received content from other side, {
  "name": "Amy",
  "age": 29,
  "sleep": {
      "light": {
          "minutes": 270
      },
      "deep": {
          "minutes": 135
      },
      "rem": {
          "minutes": 105
      }
  }
}
Sleep quality result: total sleep time is less than 8 hours

Das Ergebnis sollte "total sleep time is less than 8 hours". sein.

Sie haben erfolgreich einen vertraulichen Bereich zwischen UWear und USleep erstellt, um vertrauliche Informationen zu teilen.

8. Optional: Nicht autorisierte Arbeitslast ausführen

Im nächsten Szenario aktualisiert USleep den Code und führt einen anderen Arbeitslast auf den von UWear bereitgestellten Schlafdaten aus. UWear hat dieser neuen Arbeitslast nicht zugestimmt und seine OPA-Richtlinie nicht aktualisiert, um den neuen Image-Digest zuzulassen. Wir prüfen, ob UWear seine sensiblen Daten an die nicht autorisierte Arbeitslast sendet.

USleep ändert seine Arbeitslast

  1. Legen Sie das Projekt auf $USLEEP_PROJECT_ID fest.
gcloud config set project $USLEEP_PROJECT_ID
  1. Löschen Sie die USleep-VM-Instanz.
gcloud compute instances delete usleep --zone $USLEEP_PROJECT_ZONE
  1. Wechseln Sie zum Verzeichnis usleep/workload.go.
cd ~/confidential-space/codelabs/health_data_analysis_codelab/src/usleep
  1. In der Datei usleep/workload.go. Aktualisieren Sie die Zeile "audience": "uwear".. In diesem Beispiel ändern wir den Zielgruppenwert, um den Bild-Digest zu ändern. Der neue Wert wurde von UWear nicht genehmigt. UWear sollte die Anfrage also aus zwei Gründen ablehnen: nicht genehmigter Bild-Digest und falsche Zielgruppe.
"audience": "anotherCompany.com",
  1. Neue USleep-Arbeitslast erstellen
cd ~/confidential-space/codelabs/health_data_analysis_codelab/scripts

./create_usleep_workload.sh
  1. Neue USleep-VM-Instanz erstellen und Arbeitslast ausführen
gcloud compute instances create \
 --confidential-compute-type=SEV \
 --shielded-secure-boot \
 --maintenance-policy=MIGRATE \
 --scopes=cloud-platform --zone=${USLEEP_PROJECT_ZONE} \
 --image-project=confidential-space-images \
 --image-family=confidential-space \
--service-account=${USLEEP_WORKLOAD_SERVICE_ACCOUNT}@${USLEEP_PROJECT_ID}.iam.gserviceaccount.com \
 --metadata ^~^tee-image-reference=${USLEEP_PROJECT_REPOSITORY_REGION}-docker.pkg.dev/${USLEEP_PROJECT_ID}/${USLEEP_ARTIFACT_REPOSITORY}/${USLEEP_WORKLOAD_IMAGE_NAME}:${USLEEP_WORKLOAD_IMAGE_TAG}~tee-restart-policy=Never~tee-container-log-redirect=true usleep
  1. Neue externe USleep-IP-Adresse für die spätere Verwendung extrahieren
export USLEEP_EXTERNAL_IP=<add your external IP>

Arbeitslast noch einmal ausführen

  1. UWear-VM-Instanz löschen
gcloud config set project $UWEAR_PROJECT_ID

gcloud compute instances delete uwear --zone $UWEAR_PROJECT_ZONE
  1. UWear-VM-Instanz mit der neuen externen IP-Adresse neu erstellen
gcloud compute instances create \
 --confidential-compute-type=SEV \
 --shielded-secure-boot \
 --maintenance-policy=MIGRATE \
 --scopes=cloud-platform --zone=${UWEAR_PROJECT_ZONE} \
 --image-project=confidential-space-images \
 --image-family=confidential-space \
--service-account=${UWEAR_WORKLOAD_SERVICE_ACCOUNT}@${UWEAR_PROJECT_ID}.iam.gserviceaccount.com \
 --metadata ^~^tee-image-reference=${UWEAR_PROJECT_REPOSITORY_REGION}-docker.pkg.dev/${UWEAR_PROJECT_ID}/${UWEAR_ARTIFACT_REPOSITORY}/${UWEAR_WORKLOAD_IMAGE_NAME}:${UWEAR_WORKLOAD_IMAGE_TAG}~tee-restart-policy=Never~tee-container-log-redirect=true~tee-env-remote_ip_addr=$USLEEP_EXTERNAL_IP uwear
  1. In den seriellen Protokollen von UWear sollte die folgende Meldung angezeigt werden und die USleep-VM sollte keine vertraulichen Daten empfangen.
OPA policy result values:
[ nonce_verified ]: true
[ issuer_verified ]: true
[ secboot_verified ]: true
[ sw_name_verified ]: true
[ allow ]: false
[ hw_verified ]: true
[ image__digest_verified ]: false
[ audience_verified ]: false
Policy check FAILED
Remote TEE's JWT failed policy check.
Failed to validate claims against OPA policy: remote TEE's JWT failed policy check

9. Bereinigen

Mit dem Bereinigungsskript können Sie die Ressourcen bereinigen, die wir im Rahmen dieses Codelabs erstellt haben. Im Rahmen dieser Bereinigung werden die folgenden Ressourcen gelöscht:

  • Das UWear-Dienstkonto ($UWEAR_SERVICE_ACCOUNT).
  • Die UWear-Artifact Registry ($UWEAR_ARTIFACT_REPOSITORY).
  • Die UWear Compute-Instanz
  • Das USleep-Dienstkonto ($USLEEP_SERVICE_ACCOUNT).
  • Die USleep-Artefaktregistrierung ($USLEEP_ARTIFACT_REPOSITORY).
  • Die USleep Compute-Instanz
./cleanup.sh

Wenn Sie die Erkundung abgeschlossen haben, sollten Sie Ihr Projekt anhand dieser Anleitung löschen.

Glückwunsch

Sie haben das Codelab erfolgreich abgeschlossen.

Sie haben gelernt, wie Sie Daten sicher teilen und gleichzeitig ihre Vertraulichkeit mit Confidential Space wahren.

Nächste Schritte

Sehen Sie sich einige dieser ähnlichen Codelabs an:

Weitere Informationen