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

1. Übersicht

Confidential Space bietet eine sichere Freigabe und Zusammenarbeit mit 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.

Mit Confidential Space können Sie Szenarien nutzen, in denen Sie durch die Zusammenführung und Analyse sensibler, oft regulierter Daten einen gemeinsamen Nutzen erzielen und gleichzeitig die volle Kontrolle darüber behalten. Mit Confidential Space können Organisationen gegenseitigen Nutzen aus der Zusammenführung und Analyse sensibler Daten wie personenidentifizierbarer Informationen (PII), geschützter Gesundheitsdaten (PHI), geistigen Eigentums und kryptografischer Geheimnisse ziehen und dabei die volle Kontrolle darüber behalten.

Voraussetzungen

Aufgaben in diesem Lab

  • Erforderliche Cloud-Ressourcen für den Betrieb von vertraulichen Gruppenbereichen 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 (what), der Confidential Space-Umgebung (where) und des Kontos, in dem die Arbeitslast ausgeführt wird (who).

In diesem Codelab geht es darum, wie Sie einen vertraulichen Bereich mit geschützten Ressourcen verwenden, die nicht in Google Cloud gehostet werden. Sie erfahren, wie Sie ein benutzerdefiniertes, eigenständiges Token vom Google Attestation Service anfordern, indem Sie einen Nonce, eine Zielgruppe und den PKI-Tokentyp angeben.

In diesem Codelab richten Sie einen vertraulichen Bereich 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, geschützten und abgeschotteten Umgebung (sogenannte Trusted Execution Environment, TEE) an USleep weiter, sodass die Inhaber der Daten die Vertraulichkeit vollständig wahren können.

UWear ist sowohl Arbeitslastprüfer als auch Dateninhaber. Als Arbeitslastprüfer prüft er den Code in der ausgeführten Arbeitslast und notiert sich 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 geschrieben, die nur dem bestimmten Image-Digest in einer bestimmten Umgebung den Zugriff auf die sensiblen 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 des Codelabs herunter und legen Sie Umgebungsvariablen fest.
  • Schritt 2: Laden Sie das Stammzertifikat herunter und speichern Sie es zusammen mit Ihrem UWear-Quellcode.
  • Schritt 3: Separate Arbeitslast-Dienstkonten erstellen, 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 wurde.
  • Schritt 6: Die Arbeitslasten „USleep“ und „UWear“ ausführen UWear stellt die vertraulichen Daten bereit und USleep führt einen Schlafalgorithmus auf den Daten aus und gibt ein Ergebnis aus.
  • Schritt 7 (optional): Eine nicht autorisierte USleep-Arbeitslast ausführen und prüfen, ob keine sensiblen Daten von UWear empfangen wurden
  • Schritt 8: Alle Ressourcen bereinigen

Workflow

USleep führt die Arbeitslast in Confidential Space aus. Zum Ausführen der Arbeitslast benötigt es Zugriff auf die PHI von UWear. Um auf die Daten zuzugreifen, 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. Um das Attestierungstoken an die TLS-Sitzung zu binden, ist der Nonce-Wert der Hash des exportierten TLS-Schlüsselmaterials. Durch die Bindung des Tokens an die TLS-Sitzung wird verhindert, dass es zu Machine-in-the-Middle-Angriffen kommt, da nur die beiden an der TLS-Sitzung beteiligten Parteien den Nonce-Wert generieren können.
  2. Die Zielgruppe „uwear“ wird bereitgestellt. UWear prüft, ob es sich um die beabsichtigte Zielgruppe für das Attestierungstoken handelt.
  3. Ein Tokentyp von „PKI“. Ein Tokentyp von „PKI“ bedeutet, dass USleep ein eigenständiges Token anfordern möchte. Ob das selbstsignierte Token von Google signiert wurde, lässt sich mithilfe des Root-Zertifikats überprüfen, das vom bekannten PKI-Endpunkt von Confidential Space heruntergeladen 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 verbindet dann die TLS-Verbindung mit USleep und ruft das Attestationstoken von USleep ab. UWear prüft das Token, indem der X5C-Claim mit dem Stammzertifikat verglichen wird.

UWear genehmigt die USleep-Arbeitslast, wenn:

  1. Das Token besteht die PKI-Validierungslogik.
  2. UWear validiert das Token, indem es den X5C-Claim mit dem Stammzertifikat vergleicht, prüft, ob das Token vom untergeordneten Zertifikat signiert ist, und zuletzt, ob das heruntergeladene Stammzertifikat mit dem Stammzertifikat im X5C-Claim übereinstimmt.
  3. Die Ansprüche zur Arbeitslastmessung im Token stimmen mit den Attributbedingungen überein, die in der OPA-Richtlinie angegeben sind. OPA ist eine Open-Source-Richtlinien-Engine für allgemeine Zwecke, die die Richtliniendurchsetzung über den gesamten Stack hinweg vereinheitlicht. OPA verwendet Dokumente mit einer ähnlichen Syntax wie JSON, um Baseline-Werte festzulegen, anhand derer die Richtlinie validiert wird. Ein Beispiel für die Werte, auf die die Richtlinie überprüft, finden Sie unter OPA-Referenzwerte.
  4. Die Nonce stimmt mit der erwarteten exportierten Schlüsselmaterial (TLS) überein. Dies wird in der oben genannten OPA-Richtlinie 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 PHI 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 verwenden“. Unter Projekte erstellen und verwalten finden Sie Details dazu, wie Sie die Projekt-ID abrufen und wie sie sich von Projektnamen und Projektnummer unterscheidet.
  2. Aktivieren Sie die Abrechnung für Ihre Projekte.
  3. Legen Sie in der Cloud Shell Ihres Google-Projekts 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. Rufen Sie Ihre Hauptkonto-ID mithilfe von
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. Folgen Sie dazu der Anleitung auf der Webseite zum Gewähren einer IAM-Rolle.
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 einem Ihrer Google Cloud-Projekte in Cloud Shell das GitHub-Repository des Codelabs zu vertraulichen Gruppenbereichen mit dem folgenden Befehl, um die erforderlichen Scripts zu erhalten, die in diesem Codelab verwendet werden.
git clone https://github.com/GoogleCloudPlatform/confidential-space.git
  1. Wechseln Sie in das Scripts-Verzeichnis für das Codelab zu Gesundheitsdaten.
cd confidential-space/codelabs/health_data_analysis_codelab/scripts
  1. Aktualisieren Sie die zwei Zeilen im Script „config_env.sh“ im Verzeichnis „codelabs/health_data_analysis_codelab/scripts“. Ersetzen Sie die Projekt-IDs durch Ihre 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 alle vorhandenen Variablen fest. Sie können die Ressourcennamen mithilfe dieser Variablen (z. B. export UWEAR_ARTIFACT_REPOSITORY='my-artifact-repository') überschreiben.
  • Sie können die folgenden Variablen mit vorhandenen Namen von Cloud-Ressourcen 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 Script config_env.sh generiert.
  1. Führen Sie das Script config_env.sh aus, um die verbleibenden Variablennamen auf Werte basierend auf Ihrer Projekt-ID für Ressourcennamen festzulegen.
# 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 eigenständige Token zu validieren, muss UWear die Signatur mit dem Root-Zertifikat des vertraulichen Bereichs validieren. UWear muss das Root-Zertifikat herunterladen und lokal speichern. Führen Sie in der Console 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 Stammzertifikats 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

Jetzt erstellen Sie zwei Dienstkonten: eines für die USleep- und eines für die UWear-Arbeitslasten. Führen Sie das Script create_service_accounts.sh aus, um Arbeitslastdienstkonten in den Projekten „USleep“ und „UWear“ zu erstellen. Diese Dienstkonten werden von den VMs verwendet, auf denen die Arbeitslasten ausgeführt werden.

# 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 Script:

  • Weist die Rolle iam.serviceAccountUser zu, wodurch das Dienstkonto der Arbeitslast zugeordnet wird.
  • Weist dem Dienstkonto für die Arbeitslast die Rolle confidentialcomputing.workloadUser zu . Dadurch kann das Nutzerkonto ein Attestierungstoken generieren.
  • Weist dem Dienstkonto für die Arbeitslast die Rolle logging.logWriter zu. So können in der Confidential Space-Umgebung zusätzlich zur seriellen Konsole Logs in Cloud Logging geschrieben werden, 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. Die USleep-Arbeitslast ist eine einfache Golang-Anwendung, die anhand personenbezogener Gesundheitsdaten auf einem Wearable die Schlafqualität eines Kunden bestimmt.

USleep-Arbeitslast

Die USleep-Arbeitslast ist eine einfache Golang-Anwendung, die anhand persönlicher Gesundheitsdaten auf einem Wearable die Qualität des Schlafs eines Kunden bestimmt. Die USleep-Arbeitslast besteht aus drei Hauptelementen:

  1. TLS-Sitzung einrichten und exportiertes Schlüsselmaterial 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 mit einer Zielgruppe, einem Nonce und einem PKI-Tokentyp vom Attestation Service 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. Empfang der sensiblen Daten und Berechnung der Schlafqualität des Nutzers
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 Script create_usleep_workload.sh aus, um die USleep-Arbeitslast zu erstellen. Mit diesem Script wird Folgendes ausgeführt:
  • Erstellt die Artifact Registry ($USLEEP_ARTIFACT_REPOSITORY) im Besitz von UWear, in der die Arbeitslast veröffentlicht werden soll.
  • Erstellt den Code usleep/workload.go und verpackt ihn in einem Docker-Image. Weitere Informationen finden Sie in der Dockerfile 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 die Artifact Registry ($USLEEP_ARTIFACT_REPOSITORY).
./create_usleep_workload.sh
  1. Wichtig: Extrahieren Sie in den Ausgabeprotokollen 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 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

UWear-Arbeitslast

Die UWear-Arbeitslast besteht aus vier Hauptelementen:

  1. Sie treten derselben TLS-Sitzung bei, die in der USleep-Arbeitslast erstellt wurde, und rufen das Attestationstoken über die sichere TLS-Sitzung von USleep 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. Validieren Sie das eigenständige Token:
  • Die Überprüfung des X5C-Anspruchs enthält eine Zertifikatskette, die vom untergeordneten Zertifikat zum Zwischenzertifikat und schließlich zum Stammzertifikat korrekt verkettet ist.
  • Prüfen, ob das Token vom Blattzertifikat im X5C-Claim signiert ist.
  • Prüfen, ob das heruntergeladene / gespeicherte Stammzertifikat mit dem Stammzertifikat im X5C-Anspruch ü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 Ansprüche zur Arbeitslastmessung im Token mit den Attributbedingungen übereinstimmen, die in der OPA-Richtlinie angegeben sind. OPA ist eine Open-Source-Richtlinien-Engine für allgemeine Zwecke, die die Richtliniendurchsetzung über den gesamten Stack hinweg vereinheitlicht. OPA verwendet Dokumente mit einer ähnlichen Syntax wie JSON, um Baseline-Werte 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-Hashes:

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 PHI 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 Script create_uwear_workload.sh aus, um die UWear-Arbeitslast zu erstellen:
  • Erstellt die Artifact Registry ($UWEAR_ARTIFACT_REPOSITORY) im Besitz von UWear, in der die Arbeitslast veröffentlicht werden soll.
  • Erstellt den Code uwear/workload.go und verpackt ihn in einem Docker-Image. Weitere Informationen finden Sie in der Dockerfile 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 die 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 den STATUS: RUNNING enthalten und die EXTERNAL_IP sollte in etwa so aussehen:

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

Rufen Sie im USleep-Projekt die Seite VM-Instanzen auf, um zu prüfen, ob die USleep-Arbeitslast richtig ausgeführt wird. Klicken Sie auf die Instanz „usleep“ und dann unter „Protokolle“ auf „Serieller Port 1(Konsole)“. Sobald der Server läuft, sollte unten in den Protokollen ungefähr Folgendes 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

Die Logs der UWear-Arbeitslast finden Sie im UWear-Projekt auf der Seite VM-Instanzen. Klicken Sie auf die Instanz „uwear“ und dann unter „Protokolle“ auf „Serieller Port 1(Konsole)“.

Die Protokollausgabe sollte nach dem vollständigen Start der Instanz so aussehen:

Im UWear-Projekt sollten die seriellen Protokolle in etwa so aussehen:

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 in den Anmerkungen unten eine Anleitung.

USleep-Ergebnisse ansehen

Rufen Sie die Ergebnisse auf der Seite VM-Instanzen im USleep-Projekt auf. Klicken Sie auf die Instanz „usleep“ und dann unter „Protokolle“ auf „Serieller Port 1(Konsole)“. Die Ergebnisse der Arbeitslast finden Sie unten in den Protokollen. Sie sollten in etwa so aussehen wie im Beispiel unten.

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.

Herzlichen Glückwunsch! Sie haben einen vertraulichen Bereich zwischen UWear und USleep zum Teilen vertraulicher Informationen erstellt.

8. (Optional) Nicht autorisierte Arbeitslast ausführen

Im nächsten Szenario aktualisiert USleep den Code und führt eine andere 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 keine vertraulichen Daten an die nicht autorisierte Arbeitslast sendet.

USleep ändert die 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. Rufen Sie das Verzeichnis usleep/workload.go auf.
cd ~/confidential-space/codelabs/health_data_analysis_codelab/src/usleep
  1. In der Datei usleep/workload.go Zeile aktualisieren "audience": "uwear". In diesem Beispiel ändern wir die Zielgruppe in einen anderen Wert, der von UWear nicht genehmigt wurde, um den Bild-Digest zu ändern. UWear sollte es also aus zwei Gründen ablehnen: unzulässiger 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 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 UWear-Serienprotokollen sollte die folgende Meldung angezeigt werden und die USleep-VM sollte keine vertraulichen Daten erhalten.
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).
  • UWear-Compute-Instanz
  • Das USleep-Dienstkonto ($USLEEP_SERVICE_ACCOUNT)
  • Die USleep-Artifact-Registry ($USLEEP_ARTIFACT_REPOSITORY)
  • USleep-Compute-Instanz
./cleanup.sh

Wenn Sie mit der explorativen Datenanalyse fertig sind, können Sie Ihr Projekt löschen. Folgen Sie dazu dieser Anleitung.

Glückwunsch

Herzlichen Glückwunsch, Sie haben das Codelab erfolgreich abgeschlossen.

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

Was liegt als Nächstes an?

Sehen Sie sich diese ähnlichen Codelabs an:

Weitere Informationen