Korzystaj z Poufnej przestrzeni z chronionymi zasobami, które nie są przechowywane u dostawcy chmury

1. Przegląd

Confidential Space umożliwia bezpieczne udostępnianie danych wielu stronom i współpracę, a jednocześnie pozwala organizacjom zachować poufność danych. Oznacza to, że organizacje mogą ze sobą współpracować, zachowując kontrolę nad swoimi danymi i chroniąc je przed nieuprawnionym dostępem.

Confidential Space umożliwia scenariusze, w których chcesz uzyskać wzajemną korzyść z agregowania i analizowania danych wrażliwych, często podlegających regulacjom, przy jednoczesnym zachowaniu pełnej kontroli nad nimi. Dzięki Confidential Space organizacje mogą czerpać wzajemne korzyści z agregowania i analizowania danych wrażliwych, takich jak informacje umożliwiające identyfikację, chronione informacje zdrowotne, własność intelektualna i tajemnice kryptograficzne, zachowując przy tym pełną kontrolę nad tymi danymi.

Czego potrzebujesz

Czego się nauczysz

  • Konfigurowanie zasobów Cloud niezbędnych do uruchomienia przestrzeni poufnej
  • Jak uruchomić zadanie na poufnej maszynie wirtualnej z obrazem Poufnej przestrzeni
  • Jak autoryzować dostęp do chronionych zasobów na podstawie atrybutów kodu zadania (co), środowiska Poufnej przestrzeni (gdzie) i konta, na którym jest uruchomione zadanie (kto).

Ten moduł skupia się na tym, jak korzystać z usługi Confidential Space w przypadku chronionych zasobów hostowanych poza Google Cloud. Dowiesz się, jak poprosić o niestandardowy, samodzielny token z usługi atestowania Google, podając wartość nonce, odbiorców i typ tokena PKI.

W tym laboratorium kodowym skonfigurujesz przestrzeń poufną między fikcyjnym produktem USleep (aplikacją w kontenerze) a fikcyjnym produktem UWear (połączonym urządzeniem do noszenia) w celu obliczenia jakości snu. UWear będzie udostępniać chronione informacje zdrowotne (PHI) USleep w bezpiecznym i odizolowanym środowisku (zwanym zaufanym środowiskiem wykonawczym lub TEE), tak aby właściciele danych zachowali pełną poufność.

UWear jest zarówno audytorem obciążenia, jak i właścicielem danych. Jako audytor zadań sprawdza kod w uruchamianym zadaniu i zapisuje skrót obrazu. Jako właściciel danych UWear tworzy logikę weryfikacji, aby sprawdzić ważność tokena i jego podpisu. Tworzy ona zasadę weryfikacji, używając skrótu obrazu zweryfikowanych zadań, która zezwala na dostęp do danych wrażliwych tylko konkretnemu skrótowi obrazu w określonym środowisku.

W tym laboratorium USleep wdraża aplikację w kontenerze. USleep nie ma dostępu do danych wrażliwych, ale uruchamia zatwierdzone zadanie, które ma dostęp do tych danych.

Codelab obejmuje te kroki:

  • Krok 1. Skonfiguruj niezbędne zasoby w chmurze na potrzeby ćwiczeń z programowania. Skonfiguruj projekty, płatności i uprawnienia. Pobierz kod źródłowy ćwiczeń z programowania i ustaw zmienne środowiskowe.
  • Krok 2. Pobierz certyfikat główny i zapisz go razem z kodem źródłowym UWear.
  • Krok 3. Utwórz oddzielne konta usługi zadań, które będą używane przez maszynę wirtualną wykonującą zadania w przypadku USleep i UWear.
  • Krok 4. Utwórz zadanie USleep, które udostępnia token atestu.
  • Krok 5. Utwórz zadanie UWear, które weryfikuje token atestu i wysyła dane wrażliwe, jeśli token zostanie zatwierdzony.
  • Krok 6. Uruchom obciążenia USleep i UWear. UWear dostarczy dane wrażliwe, a USleep uruchomi na nich algorytm snu i wygeneruje wynik.
  • Krok 7. (Opcjonalnie) Uruchom nieautoryzowane zadanie USleep i sprawdź, czy z UWear nie zostały odebrane żadne dane wrażliwe.
  • Krok 8. Usuń wszystkie zasoby.

Interpretowanie przepływu pracy

USleep będzie uruchamiać zadanie w Poufnej przestrzeni. Aby uruchomić zadanie, musi mieć dostęp do informacji chronionych UWear. Aby uzyskać dostęp, zbiór zadań USleep najpierw tworzy bezpieczną sesję TLS. USleep poprosi też usługę atestowania Google o token atestu z ładunkiem.

USleep poprosi o token atestu z ładunkiem JSON, który będzie zawierać 3 elementy:

  1. Token atestu powiązany z sesją TLS. Aby powiązać token atestu z sesją TLS, wartością nonce będzie hash eksportowanego materiału klucza TLS. Powiązanie tokena z sesją TLS zapewnia, że nie dochodzi do ataków typu „maszyna w środku”, ponieważ tylko 2 strony biorące udział w sesji TLS będą mogły wygenerować wartość nonce.
  2. Zostanie podana lista odbiorców „uwear”. UWear sprawdzi, czy jest docelowym odbiorcą tokena atestu.
  3. Typ tokena „PKI”. Typ tokena „PKI” oznacza, że USleep chce poprosić o samodzielny token. Samodzielny token można zweryfikować pod kątem podpisu Google, korzystając z głównego certyfikatu pobranego z znanego punktu końcowego PKI usługi Confidential Space. W przeciwieństwie do domyślnego typu tokena OIDC, którego podpis jest weryfikowany za pomocą regularnie zmienianego klucza publicznego.

bb013916a3222ce7.png

Zadanie USleep otrzymuje token atestu. UWear łączy się następnie z połączeniem TLS z USleep i pobiera token atestu USleep. UWear zweryfikuje token, sprawdzając deklarację x5c w odniesieniu do certyfikatu głównego.

UWear zatwierdzi zadanie USleep, jeśli:

  1. Token przechodzi logikę weryfikacji PKI.
  2. UWear zweryfikuje token, sprawdzając roszczenie x5c w odniesieniu do certyfikatu głównego, sprawdzając, czy token jest podpisany przez certyfikat liścia, a na koniec, czy pobrany certyfikat główny jest taki sam jak certyfikat główny w roszczeniu x5c.
  3. Deklaracje pomiaru obciążenia w tokenie są zgodne z warunkami atrybutów określonymi w zasadach OPA. OPA to silnik zasad ogólnego przeznaczenia typu open source, który ujednolica egzekwowanie zasad w całym stosie. OPA używa dokumentów o składni podobnej do JSON, aby ustawić wartości bazowe, względem których weryfikowane są zasady. Przykład wartości, które są sprawdzane przez zasady, znajdziesz w sekcji Wartości odniesienia OPA.
  4. Wartość nonce jest zgodna z oczekiwaną wartością nonce (eksportowany materiał klucza TLS). Jest to potwierdzone w zasadach OPA powyżej.

Gdy wszystkie te kontrole zostaną zakończone i przejdą pomyślnie, UWear może potwierdzić, że dane będą wysyłane i przetwarzane w bezpieczny sposób. UWear odpowie, przesyłając wrażliwe informacje zdrowotne w ramach tej samej sesji TLS, a USleep będzie mogła wykorzystać te dane do obliczenia jakości snu klienta.

2. Konfigurowanie zasobów w chmurze

Zanim zaczniesz

  1. Skonfiguruj 2 projekty Google Cloud: jeden dla USleep i jeden dla UWear. Więcej informacji o tworzeniu projektu Google Cloud znajdziesz w samouczku „Set up and navigate your first Google project”. Więcej informacji o tym, jak uzyskać identyfikator projektu i czym różni się on od nazwy i numeru projektu, znajdziesz w artykule na temat tworzenia projektów i zarządzania nimi.
  2. Włącz płatności w swoich projektach.
  3. Cloud Shell projektu Google skonfiguruj wymagane zmienne środowiskowe projektu, jak pokazano poniżej.
export UWEAR_PROJECT_ID=<Google Cloud project id of UWear>
export USLEEP_PROJECT_ID=<Google Cloud project id of USleep>
  1. Włącz interfejs Confidential Computing API i poniższe interfejsy API w obu projektach.
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. Pobieranie identyfikatora podmiotu zabezpieczeń za pomocą
gcloud auth list

# Output should contain
# ACCOUNT: <Principal Identifier>

# Set your member variable
export MEMBER='user:<Principal Identifier>'
  1. Dodaj uprawnienia do tych 2 projektów. Uprawnienia można dodać, postępując zgodnie z instrukcjami na stronie internetowej dotyczącej przyznawania ról uprawnień.
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. W Cloud Shell w jednym z projektów Google Cloud sklonuj repozytorium GitHub z ćwiczeniami Confidential Space za pomocą poniższego polecenia, aby uzyskać wymagane skrypty używane w tych ćwiczeniach.
git clone https://github.com/GoogleCloudPlatform/confidential-space.git
  1. Przejdź do katalogu skryptów w samouczku dotyczącym danych o zdrowiu.
cd confidential-space/codelabs/health_data_analysis_codelab/scripts
  1. Zaktualizuj te 2 wiersze w skrypcie config_env.sh, który znajduje się w katalogu codelabs/health_data_analysis_codelab/scripts. Zastąp identyfikatory projektów identyfikatorami projektów USleep i UWear. Pamiętaj, aby usunąć symbol komentarza „#” na początku wiersza.
# TODO: Populate UWear and USleep Project IDs
export UWEAR_PROJECT_ID=your-uwear-project-id
export USLEEP_PROJECT_ID=your-usleep-project-id
  1. Opcjonalnie: skonfiguruj dowolne wcześniej utworzone zmienne. Nazwy zasobów możesz zastąpić za pomocą tych zmiennych (np. export UWEAR_ARTIFACT_REPOSITORY='my-artifact-repository').
  • Możesz ustawić te zmienne za pomocą nazw istniejących zasobów w chmurze. Jeśli zmienna jest ustawiona, zostanie użyty odpowiedni istniejący zasób w chmurze z projektu. Jeśli zmienna nie jest ustawiona, nazwa zasobu w chmurze zostanie wygenerowana na podstawie wartości w skrypcie config_env.sh.
  1. Uruchom skrypt config_env.sh, aby ustawić pozostałe nazwy zmiennych na wartości oparte na identyfikatorze projektu dla nazw zasobów.
# 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. Pobieranie certyfikatu głównego

  1. Aby zweryfikować samodzielny token zwrócony przez usługę atestowania, UWear musi sprawdzić sygnaturę względem certyfikatu głównego przestrzeni poufnej. UWear musi pobrać certyfikat główny i zapisać go lokalnie. W konsoli jednego z projektów Google Cloud uruchom te polecenia:
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. Wygeneruj odcisk cyfrowy pobranego certyfikatu głównego.
openssl x509 -fingerprint -in confidential_space_root.pem -noout
  1. Sprawdź, czy odcisk cyfrowy pasuje do tego skrótu SHA-1:
B9:51:20:74:2C:24:E3:AA:34:04:2E:1C:3B:A3:AA:D2:8B:21:23:21

4. Tworzenie konta usługi zadania

Teraz utworzysz 2 konta usługi: jedno dla obciążeń USleep i jedno dla obciążeń UWear. Uruchom skrypt create_service_accounts.sh, aby utworzyć konta usługi obciążenia w projektach USleep i UWear. Maszyny wirtualne, na których działają obciążenia, będą używać tych kont usługi.

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

# Run the create_service_accounts script
./create_service_accounts.sh

Skrypt:

  • Przyznaje rolę iam.serviceAccountUser, która dołącza konto usługi do zadania.
  • Przypisuje rolę confidentialcomputing.workloadUser do konta usługi zadania . Umożliwi to generowanie tokena atestu przez konto użytkownika.
  • Przyznaje uprawnienia do roli logging.logWriter kontu usługi obciążenia. Dzięki temu środowisko Poufnej przestrzeni może zapisywać logi w Cloud Logging oprócz konsoli szeregowej, więc logi są dostępne po zakończeniu działania maszyny wirtualnej.Tworzenie zbiorów zadań

5. Tworzenie zadania USleep

W ramach tego kroku utworzysz obrazy Dockera dla zadań używanych w tym laboratorium. Obciążenie USleep to prosta aplikacja w języku Golang, która określa jakość snu klienta na podstawie informacji o zdrowiu osobistym na urządzeniu do noszenia.

Informacje o zadaniu USleep

Obciążenie USleep to prosta aplikacja w języku Golang, która określa jakość snu klienta na podstawie informacji o zdrowiu osobistym na urządzeniu do noszenia. Zbiór zadań USleep składa się z 3 głównych części:

  1. Konfigurowanie sesji TLS i wyodrębnianie wyeksportowanego materiału klucza
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. Żądanie tokena z usługi atestowania z użyciem odbiorców, wartości nonce i typu tokena PKI.
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. Otrzymywanie danych wrażliwych i obliczanie jakości snu użytkownika
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
  ...
}

Procedura tworzenia zadania USleep

  1. Uruchom skrypt create_usleep_workload.sh, aby utworzyć zadanie USleep. Ten skrypt:
  • Tworzy repozytorium Artifact Registry ($USLEEP_ARTIFACT_REPOSITORY) należące do UWear, w którym będzie publikowane zadanie.
  • Kompiluje kod usleep/workload.go i pakuje go w obrazie Dockera. Zobacz konfigurację Dockerfile dla USleep.
  • Publikuje obraz Dockera w Artifact Registry ($USLEEP_ARTIFACT_REPOSITORY) należącym do UWear.
  • Przyznaje kontu usługi $USLEEP_WORKLOAD_SERVICE_ACCOUNT uprawnienia do odczytu w Artifact Registry ($USLEEP_ARTIFACT_REPOSITORY).
./create_usleep_workload.sh
  1. Ważne: w logach wyjściowych wyodrębnij skrót obrazu dla USleep.
latest: digest: sha256:<USLEEP_IMAGE_DIGEST> size: 945
  1. Przejdź do katalogu UWear
cd ~/confidential-space/codelabs/health_data_analysis_codelab/src/uwear
  1. Zastąp wartość w sekcji „allowed_submods_container_image_digest” w pliku opa_validation_values.json wartością 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. Tworzenie zadania UWear

Informacje o zadaniu UWear

Zbiór zadań UWear składa się z 4 głównych części:

  1. Dołączanie do tej samej sesji TLS, która została utworzona w przypadku zadania USleep, i pobieranie tokena atestu z USleep przez bezpieczną sesję TLS.
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. Weryfikacja samodzielnego tokena przez:
  • Sprawdzenie, czy roszczenie x5c zawiera łańcuch certyfikatów, który jest prawidłowo połączony od certyfikatu liścia przez certyfikat pośredni do certyfikatu głównego.
  • Sprawdzenie, czy token jest podpisany przez certyfikat liścia zawarty w deklaracji x5c.
  • Sprawdź, czy pobrany lub zapisany certyfikat główny jest taki sam jak certyfikat główny w deklaracji x5c.
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. Zadanie UWear sprawdzi następnie, czy roszczenia dotyczące pomiaru obciążenia w tokenie są zgodne z warunkami atrybutów określonymi w zasadach OPA. OPA to silnik zasad ogólnego przeznaczenia typu open source, który ujednolica egzekwowanie zasad w całym stosie. OPA używa dokumentów o składni podobnej do JSON, aby ustawić wartości bazowe, względem których weryfikowane są zasady.
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"
  ]
}
  • Przykład zasad OPA napisanych w języku Rego.
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"
}
  • Przykładowe zapytanie Rego.
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
"

Przykładowy kod do pobierania skrótu EKM:

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. Gdy wszystkie te kontrole zostaną zakończone i przejdą pomyślnie, UWear może potwierdzić, że dane będą wysyłane i przetwarzane w bezpieczny sposób. UWear odpowie, przesyłając wrażliwe informacje zdrowotne w ramach tej samej sesji TLS, a USleep będzie mogła wykorzystać te dane do obliczenia jakości snu klienta.
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()
  
  ...
}

Procedura tworzenia zadania USleep

  1. Przejdź do katalogu skryptów
cd ~/confidential-space/codelabs/health_data_analysis_codelab/scripts
  1. Uruchom skrypt create_uwear_workload.sh, aby utworzyć zadanie UWear:
  • Tworzy repozytorium Artifact Registry ($UWEAR_ARTIFACT_REPOSITORY) należące do UWear, w którym będzie publikowane zadanie.
  • Kompiluje kod uwear/workload.go i pakuje go w obrazie Dockera. Zobacz konfigurację Dockerfile dla USleep.
  • Publikuje obraz Dockera w Artifact Registry ($UWEAR_ARTIFACT_REPOSITORY) należącym do UWear.
  • Przyznaje kontu usługi $UWEAR_WORKLOAD_SERVICE_ACCOUNT uprawnienia do odczytu w Artifact Registry ($UWEAR_ARTIFACT_REPOSITORY).
./create_uwear_workload.sh

7. Uruchamianie zbiorów zadań USleep i UWear

Uruchamianie zadania USleep

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

Odpowiedź powinna zawierać STATUS: RUNNING i EXTERNAL_IP, podobnie jak w tym przykładzie:

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

Zapisywanie zewnętrznego adresu IP w zmiennej

export USLEEP_EXTERNAL_IP=<add your external IP> 

Sprawdzanie, czy zadanie USleep zostało uruchomione prawidłowo

Aby sprawdzić, czy zadanie USleep działa prawidłowo, otwórz stronę instancji maszyn wirtualnych w projekcie USleep. Kliknij instancję „usleep” i w sekcji Logi naciśnij „Port szeregowy 1(konsola)”. Gdy serwer będzie działać, u dołu dzienników powinny się wyświetlać informacje podobne do tych poniżej.

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

Uruchamianie zadania UWear

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

Sprawdzanie, czy zadanie UWear zostało uruchomione prawidłowo

Aby wyświetlić logi zadania UWear, otwórz stronę Instancje maszyn wirtualnych w projekcie UWear. Kliknij instancję „uwear” i w sekcji Logi naciśnij „Port szeregowy 1(konsola)”.

Dane wyjściowe dziennika po pełnym uruchomieniu instancji powinny wyglądać tak:

W projekcie UWear logi szeregowe powinny wyglądać podobnie do tego:

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

Jeśli obciążenie UWear nie wygląda tak, jak na ilustracji, zapoznaj się z instrukcjami w uwagach poniżej.

Wyświetlanie wyników USleep

Aby wyświetlić wyniki, wróć na stronę Instancje maszyn wirtualnych w projekcie USleep. Kliknij instancję „usleep” i w sekcji Logi naciśnij „Port szeregowy 1(konsola)”. Wyniki zadania znajdziesz na dole logów. Powinny wyglądać podobnie do przykładu poniżej.

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

Wynik powinien wyglądać tak: "total sleep time is less than 8 hours".

Gratulacje! Udało Ci się utworzyć przestrzeń poufną między UWear a USleep, aby udostępniać informacje poufne.

8. (Opcjonalnie) Uruchomienie nieautoryzowanego zadania

W kolejnym scenariuszu USleep aktualizuje kod i uruchamia inne zadanie na podstawie danych o śnie dostarczonych przez UWear. Firma UWear nie zaakceptowała tego nowego zadania i nie zaktualizowała swoich zasad OPA, aby zezwolić na nowy skrót obrazu. Sprawdzimy, czy UWear nie wysyła danych wrażliwych do nieautoryzowanego zbioru zadań.

USleep modyfikuje zbiór zadań

  1. Ustaw projekt na $USLEEP_PROJECT_ID.
gcloud config set project $USLEEP_PROJECT_ID
  1. Usuń instancję maszyny wirtualnej USleep.
gcloud compute instances delete usleep --zone $USLEEP_PROJECT_ZONE
  1. Przejdź do katalogu usleep/workload.go.
cd ~/confidential-space/codelabs/health_data_analysis_codelab/src/usleep
  1. W pliku usleep/workload.go. Zaktualizuj wiersz "audience": "uwear". W tym przykładzie, aby zmienić skrót obrazu, zaktualizujemy odbiorców, podając inną wartość, która nie została zatwierdzona przez UWear. Dlatego UWear powinien odrzucić ten produkt z 2 powodów: niezatwierdzony skrót obrazu i nieprawidłowi odbiorcy.
"audience": "anotherCompany.com",
  1. Tworzenie nowego zadania USleep
cd ~/confidential-space/codelabs/health_data_analysis_codelab/scripts

./create_usleep_workload.sh
  1. Utwórz nową instancję maszyny wirtualnej USleep i uruchom zadanie
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. Wyodrębnij nowy zewnętrzny adres IP USleep do późniejszego wykorzystania.
export USLEEP_EXTERNAL_IP=<add your external IP>

Ponowne uruchomienie zadania

  1. Usuwanie instancji maszyny wirtualnej UWear
gcloud config set project $UWEAR_PROJECT_ID

gcloud compute instances delete uwear --zone $UWEAR_PROJECT_ZONE
  1. Ponownie utwórz instancję maszyny wirtualnej UWear, używając nowego zewnętrznego adresu IP.
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. W logach seryjnych UWear powinien pojawić się ten komunikat, a maszyna wirtualna USleep nie powinna otrzymywać żadnych danych wrażliwych.
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. Czyszczenie

Do usunięcia zasobów utworzonych w ramach tych ćwiczeń z programowania możesz użyć skryptu czyszczącego. W ramach tego czyszczenia zostaną usunięte te zasoby:

  • Konto usługi UWear ($UWEAR_SERVICE_ACCOUNT).
  • Rejestr artefaktów UWear ($UWEAR_ARTIFACT_REPOSITORY).
  • Instancja Compute UWear
  • Konto usługi USleep ($USLEEP_SERVICE_ACCOUNT).
  • Rejestr artefaktów USleep ($USLEEP_ARTIFACT_REPOSITORY).
  • Instancja Compute USleep
./cleanup.sh

Jeśli skończysz już eksperymentować, możesz usunąć projekt, postępując zgodnie z tymi instrukcjami.

Gratulacje

Gratulacje! Codelab został ukończony.

Dowiedzieliśmy się, jak bezpiecznie udostępniać dane, zachowując ich poufność, za pomocą przestrzeni poufnej.

Co dalej?

Wypróbuj te podobne codelaby:

Więcej informacji