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

1. Omówienie

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

Środowisko poufne umożliwia tworzenie scenariuszy, w których chcesz czerpać korzyści z zbierania i analizowania danych wrażliwych, często podlegających przepisom, przy jednoczesnym zachowaniu nad nimi pełnej kontroli. Dzięki platformie Confidential Space organizacje mogą osiągnąć wzajemną wartość dzięki agregowaniu i analizowaniu 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

  • Jak skonfigurować niezbędne zasoby Cloud do obsługi Confidential Space
  • Jak uruchomić zbiory zadań w poufnej maszynie wirtualnej z obrazem Poufnej przestrzeni
  • Jak autoryzować dostęp do zasobów chronionych na podstawie atrybutów kodu zbioru zadań (co), środowiska Confidential Space (gdzie) i konta, które wykonuje zbiór zadań (kto).

Ten warsztat programistyczny koncentruje się na używaniu poufnej przestrzeni w przypadku zasobów chronionych hostowanych poza Google Cloud. Dowiedz się, jak poprosić o niestandardowy, samodzielny token z usługi Google Attestation Service, podając losowy ciąg znaków, grupę odbiorców i typ tokena PKI.

W tym ćwiczeniu z programowania skonfigurujesz przestrzeń poufną między fikcyjnym produktem USleep, czyli aplikacją w kontenerze, a fikcyjnym urządzeniem UWear, czyli połączonym urządzeniem do noszenia, aby obliczyć jakość snu. UWear udostępnia chronione informacje zdrowotne (PHI) USleep w bezpiecznym, bezpiecznym i odizolowanym środowisku (zaufane środowisko wykonawcze, TEE), aby właściciele danych zachowali ich pełną poufność.

UWear jest zarówno audytor zadań, jak i właścicielem danych. Jako audytor zadań sprawdza kod w bieżącym zadaniu i zapisuje skrót obrazu. Jako właściciel danych firma UWear tworzy logikę weryfikacji, która sprawdza ważność tokena i jego sygnatury. Zapisuje on zasadę weryfikacji, korzystając z obrazu podsumowania kontrolowanych zadań, która zezwala tylko na dostęp do danych poufnych w konkretnym środowisku.

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

W ramach tego Codelab wykonasz te czynności:

  • Krok 1. Skonfiguruj niezbędne zasoby chmury na potrzeby ćwiczenia z programowania. Skonfiguruj projekty, rozliczenia i uprawnienia. Pobierz kod źródłowy ćwiczenia i ustaw zmienne środowiskowe.
  • Krok 2. Pobierz certyfikat głównego urzędu certyfikacji i przechowuj go razem z kodem źródłowym UWear.
  • Krok 3. Utwórz oddzielne konta usługi dotyczące zadań, których będzie używać maszyna wirtualna wykonująca zadania w przypadku USleep i UWear.
  • Krok 4. Utwórz zadanie USleep, które udostępnia token atestacji.
  • Krok 5. Utwórz zadanie UWear, które weryfikuje token uwierzytelniający i wysyła dane wrażliwe, jeśli token zostanie zatwierdzony.
  • Krok 6. Uruchom obciążenia USleep i UWear. UWear udostępni dane wrażliwe, a USleep uruchomi algorytm snu na tych danych i wyświetli wynik.
  • Krok 7. (Opcjonalnie) Uruchom nieautoryzowany zbiór zadań USleep i sprawdź, czy z UWear nie zostały otrzymane dane wrażliwe.
  • Krok 8. Wyczyść wszystkie zasoby.

Omówienie przepływu pracy

USleep będzie wykonywać zadanie w Poufnej przestrzeni. Aby uruchomić obciążenie, potrzebuje dostępu do informacji o pacjentach w UWear. Aby uzyskać dostęp, zadanie USleep najpierw tworzy bezpieczną sesję TLS. USleep poprosi też o token uwierzytelniający z danymi z usługi Google Attestation Service.

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

  1. Token uwierzytelniający powiązany z sesją TLS. Aby powiązać token uwierzytelniający z sesją TLS, wartość nonce będzie hashem wyeksportowanego materiału klucza TLS. Powiązanie tokena z sesją TLS zapewnia, że nie dochodzi do ataków typu „man-in-the-middle”, ponieważ tylko 2 strony biorące udział w sesji TLS będą mogły wygenerować wartość nonce.
  2. Będzie to grupa odbiorców „odzież”. UWear sprawdzi, czy jest to właściwa grupa odbiorców tokena uwierzytelniania.
  3. Typ tokena „PKI”. Typ tokena „PKI” oznacza, że USleep chce poprosić o token samodzielny. Można sprawdzić, czy samodzielny token został podpisany przez Google, używając głównego certyfikatu pobranego z dobrze znanego punktu końcowego PKI Confidential Space. W odróżnieniu od domyślnego typu tokena OIDC, którego podpis jest weryfikowany za pomocą klucza publicznego, który jest regularnie rotowany.

bb013916a3222ce7.png

Zadanie USleep otrzymuje token atestacji. Następnie UWear łączy się z USleep za pomocą połączenia TLS i pobiera token uwierzytelniający USleep. UWear zweryfikuje token, sprawdzając oświadczenie x5c w odniesieniu do głównego certyfikatu.

UWear zatwierdzi zadanie USleep, jeśli:

  1. Token spełnia logikę weryfikacji PKI.
  2. UWear zweryfikuje token, sprawdzając roszczenie x5c w odniesieniu do certyfikatu głównego, sprawdzając, czy token jest podpisany certyfikatem liściastym, i sprawdzając, czy pobrany certyfikat główny jest taki sam jak w rośnięciu x5c.
  3. roszczenia dotyczące pomiaru obciążenia w tokenie są zgodne z warunkami atrybutu określonymi w zasadach dotyczących pomiaru obciążenia; OPA to ogólny mechanizm zasad typu open source, który umożliwia spójne egzekwowanie zasad w całym stosie. OPA używa dokumentów o składni podobnej do JSON do ustawiania wartości bazowych, na podstawie których weryfikowana jest zasada. Przykładowe wartości, które są sprawdzane przez tę zasadę, znajdziesz w sekcji Wartości odniesienia OPA.
  4. Wartość nonce jest zgodna z oczekiwaną (TLS Eksportowany materiał kluczowy). Jest to weryfikowane w ramach zasad dotyczących umowy o przeniesienie praw autorskich.

Gdy wszystkie te kontrole zostaną przeprowadzone i zaliczone, UWear może potwierdzić, że dane zostaną wysłane i przetworzone w bezpieczny sposób. UWear odpowie z danymi PHI w ramach tej samej sesji TLS, a USleep będzie mógł użyć tych danych do obliczenia jakości snu klienta.

2. Konfigurowanie zasobów Cloud

Zanim zaczniesz

  1. Skonfiguruj 2 projekty Google Cloud: jeden dla USleep, a drugi dla UWear. Więcej informacji o tworzeniu projektu Google Cloud znajdziesz w laboratorium kodu „Konfigurowanie pierwszego projektu Google i poruszanie się po nim”. Więcej informacji o tym, jak pobrać identyfikator projektu i czym różni się on od nazwy i numeru projektu, znajdziesz w artykule Tworzenie projektów i zarządzanie nimi.
  2. Włącz płatności w przypadku swoich projektów.
  3. W Cloud Shell w projekcie Google ustaw 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 te 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 dla tych 2 projektów. Aby dodać uprawnienia, postępuj zgodnie ze wskazówkami na stronie przyznawania roli 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 jednej z sesji Cloud Shell w projektach Google Cloud sklonuj repozytorium GitHub Codelab Confidential Space, używając tego polecenia, aby uzyskać wymagane skrypty używane w ramach tego Codelab.
git clone https://github.com/GoogleCloudPlatform/confidential-space.git
  1. Zmień katalog na katalog skryptów dla Codelab danych dotyczących zdrowia.
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. Zaktualizuj identyfikatory projektów, podając identyfikatory swoich 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 dotychczasowe zmienne. Możesz zastąpić nazwy zasobów za pomocą tych zmiennych (np. export UWEAR_ARTIFACT_REPOSITORY='my-artifact-repository').
  • Te zmienne możesz ustawić 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.
# 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ć token samodzielny zwrócony przez usługę uwierzytelniania, UWear musi zweryfikować podpis za pomocą certyfikatu głównego Confidential Space. Użytkownik 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. wygenerować odcisk cyfrowy pobranego certyfikatu głównego.
openssl x509 -fingerprint -in confidential_space_root.pem -noout
  1. Sprawdź, czy odcisk cyfrowy jest zgodny z tym zestawem danych 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 dotyczącego obciążenia

Teraz utwórz 2 konta usługi: jedno dla USleep, a drugie dla zadań UWear. Uruchom skrypt create_service_accounts.sh, aby utworzyć konta usługi obciążenia w projektach USleep i UWear. Maszyny wirtualne, na których są wykonywane 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:

  • Przypisuje rolę iam.serviceAccountUser, która umożliwia przyłączenie konta usługi do zadania.
  • Przypisuje rolę confidentialcomputing.workloadUser do konta usługi zadania . Pozwoli to na wygenerowanie tokena weryfikacji przez konto użytkownika.
  • Przypisuje uprawnienia konta usługi do zadań do roli logging.logWriter. Dzięki temu środowisko Confidential Space może zapisywać dzienniki nie tylko w konsoli szeregowej, ale też w Cloud Logging, dzięki czemu dzienniki są dostępne po zakończeniu działania maszyny wirtualnej.Tworzenie zbiorów zadań

5. Tworzenie zadania USleep

Na tym etapie utworzysz obrazy Dockera dla obciążeń używanych w tym ćwiczeniu. USleep to prosta aplikacja w języku Golang, która określa jakość snu klienta na podstawie jego osobistych informacji zdrowotnych na urządzeniu do noszenia.

Informacje o obciążeniu USleep

USleep to prosta aplikacja w Golangu, która określa jakość snu klienta na podstawie osobistych informacji zdrowotnych na urządzeniu do noszenia. Zbiór zadań USleep składa się z 3 głównych elementów:

  1. Konfigurowanie sesji TLS i wyodrębnianie wyeksportowanych materiałów kluczy
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. Prośba o token od usługi uwierzytelniania z odbiorcami, nonce i typem 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. Odbieranie 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
  ...
}

Tworzenie zadania USleep

  1. Uruchom skrypt create_usleep_workload.sh, aby utworzyć zadanie USleep. Ten skrypt:
  • Tworzy repozytorium artefaktów ($USLEEP_ARTIFACT_REPOSITORY) należące do UWear, w którym zadanie zostanie opublikowane.
  • Kompiluje kod usleep/workload.go i pakuje go w obraz Dockera. Zapoznaj się z konfiguracją Dockerfile dla USleep.
  • Publikuje obraz Dockera w usłudze Artifact Registry ($USLEEP_ARTIFACT_REPOSITORY) należącej do UWear.
  • Przyznanie kontu usługi $USLEEP_WORKLOAD_SERVICE_ACCOUNT uprawnienia odczytu do repozytorium Artifact Registry ($USLEEP_ARTIFACT_REPOSITORY).
./create_usleep_workload.sh
  1. Ważne: w logach wyjściowych wyodrębnij podsumowanie 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

Zadanie UWear składa się z 4 głównych elementów:

  1. Dołączanie do tej samej sesji TLS, która została utworzona w zbiorze zadań USleep, i pobieranie tokena uwierzytelniania 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:
  • Sprawdzanie roszczenia x5c zawiera łańcuch certyfikatów, który prawidłowo łączy certyfikat liścia z certyfikatem przejściowym, a następnie z certyfikatem głównym.
  • Sprawdzanie, czy token jest podpisany certyfikatem wierzchołka ścieżki zawartym w roszczeniu x5c.
  • Sprawdzanie, czy pobrany / zarchiwizowany certyfikat główny jest taki sam jak w twierdzeniu 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. Następnie zadanie UWear sprawdzi, czy roszczenia dotyczące pomiaru zadań w tokenie są zgodne z warunkami atrybutu określonymi w zasadach dotyczących pomiaru zadań. OPA to ogólny mechanizm zasad typu open source, który umożliwia spójne egzekwowanie zasad w całym stosie. OPA używa dokumentów o składni podobnej do JSON do ustawiania wartości bazowych, na podstawie których weryfikowana jest zasada.
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"
}
  • Przykład zapytania dotyczącego rejestru.
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 służący do uzyskiwania hasha 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ą przeprowadzone i zaliczone, UWear może potwierdzić, że dane zostaną wysłane i przetworzone w bezpieczny sposób. UWear odpowie z danymi PHI w ramach tej samej sesji TLS, a USleep będzie mógł użyć tych danych 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()
  
  ...
}

Tworzenie 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 artefaktów ($UWEAR_ARTIFACT_REPOSITORY) należące do UWear, w którym zadanie zostanie opublikowane.
  • Kompiluje kod uwear/workload.go i pakuje go w obraz Dockera. Zapoznaj się z konfiguracją Dockerfile dla USleep.
  • Publikuje obraz Dockera w usłudze Artifact Registry ($UWEAR_ARTIFACT_REPOSITORY) należącej do UWear.
  • Przyznanie kontu usługi $UWEAR_WORKLOAD_SERVICE_ACCOUNT uprawnienia odczytu do repozytorium 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, a także wartość EXTERNAL_IP podobną do tej:

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

Przechowuj zewnętrzny adres 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 maszyny wirtualnej w projekcie USleep. Kliknij wystąpienie „usleep” i w sekcji Logi kliknij „Port szeregowy 1(konsola)”. Gdy serwer będzie działać, na dole dzienników powinien wyświetlić się komunikat podobny do tego.

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ę Instance maszyn wirtualnych w projekcie UWear. Kliknij instancję „uwear” i w sekcji Logi kliknij „Port szeregowy 1(konsola)”.

Wyjście z dziennika po uruchomieniu instancji powinno wyglądać tak

W projekcie UWear dzienniki szeregowe powinny zawierać informacje podobne do tych:

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 informacjami poniżej.

Wyświetlanie wyników USleep

Aby wyświetlić wyniki, wróć na stronę Instancji maszyn wirtualnych w projekcie USleep. Kliknij wystąpienie „usleep” i w sekcji Logi kliknij „Port szeregowy 1(konsola)”. Wyniki zadań znajdziesz na dole logów. Powinny wyglądać podobnie do tych na przykładzie 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 być "total sleep time is less than 8 hours".

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

8. (Opcjonalnie) Uruchamianie nieautoryzowanego zadania

W kolejnym scenariuszu USleep aktualizuje kod i uruchamia inny zestaw zadań na podstawie danych o śnie dostarczonych przez UWear. Firma UWear nie zgodziła się na to nowe zadanie i nie zaktualizowała zasad dotyczących OPA, aby zezwolić na nowy podgląd obrazu. Sprawdzamy, czy UWear nie wysyła danych wrażliwych do nieautoryzowanego zadania.

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. Zmień wiersz "audience": "uwear". W tym przykładzie, aby zmienić podsumowanie obrazu, zaktualizujemy listę odbiorców, podając inną wartość, której UWear nie zatwierdził. UWear powinien odrzucić tę reklamę z 2 powodów: nie zatwierdzony skrót obrazu i nieprawidłowa grupa odbiorców.
"audience": "anotherCompany.com",
  1. Tworzenie nowego zadania USleep
cd ~/confidential-space/codelabs/health_data_analysis_codelab/scripts

./create_usleep_workload.sh
  1. Tworzenie nowej instancji maszyny wirtualnej USleep i uruchamianie zadania
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 na potrzeby późniejszego użycia
export USLEEP_EXTERNAL_IP=<add your external IP>

Ponownie uruchom zadanie

  1. Usuń instancję 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 przy użyciu 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

Skrypt czyszczenia możesz użyć do usunięcia zasobów utworzonych w ramach tego ćwiczenia z programowania. W ramach tego czyszczenia zostaną usunięte te zasoby:

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

Gdy skończysz eksplorować projekt, możesz go usunąć, postępując zgodnie z tymi instrukcjami.

Gratulacje

Gratulacje! Udało Ci się ukończyć zajęcia.

Wiesz już, jak bezpiecznie udostępniać dane, zachowując ich poufność, za pomocą przestrzeni poufnej.

Co dalej?

Sprawdź te podobne laboratoria programistyczne...

Więcej informacji