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
- 2 oddzielne projekty Google Cloud Platform
- przeglądarka, np. Chrome lub Firefox;
- Podstawowa znajomość Google Compute Engine, poufalnej maszyny wirtualnej, kontenerów oraz repozytoriów zdalnych, certyfikatów i łańcuchów certyfikatów.
- Podstawowa znajomość kont usługi, otwartego agenta zasad, Rego i infrastruktury kluczy publicznych.
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:
- 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.
- Będzie to grupa odbiorców „odzież”. UWear sprawdzi, czy jest to właściwa grupa odbiorców tokena uwierzytelniania.
- 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.
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:
- Token spełnia logikę weryfikacji PKI.
- 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.
- 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.
- 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
- 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.
- Włącz płatności w przypadku swoich projektów.
- 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>
- 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
- Pobieranie identyfikatora podmiotu zabezpieczeń za pomocą
gcloud auth list
# Output should contain
# ACCOUNT: <Principal Identifier>
# Set your member variable
export MEMBER='user:<Principal Identifier>'
- Dodaj uprawnienia dla tych 2 projektów. Aby dodać uprawnienia, postępuj zgodnie ze wskazówkami na stronie przyznawania roli uprawnień.
- W przypadku
$UWEAR_PROJECT_ID
musisz mieć uprawnienia Administratora rejestru artefaktów i administratora konta usługi.
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'
- Aby korzystać z
$USLEEP_PROJECT_ID
, musisz mieć uprawnienia administracyjne w Compute, administracyjne w pamięci masowej, administracyjne w rejestrze artefaktów oraz administracyjne w koncie usługi.
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'
- 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
- Zmień katalog na katalog skryptów dla Codelab danych dotyczących zdrowia.
cd confidential-space/codelabs/health_data_analysis_codelab/scripts
- 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
- 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.
- 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
- 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
- wygenerować odcisk cyfrowy pobranego certyfikatu głównego.
openssl x509 -fingerprint -in confidential_space_root.pem -noout
- 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:
- 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
}
- 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
}
- 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
- 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
- Ważne: w logach wyjściowych wyodrębnij podsumowanie obrazu dla USleep.
latest: digest: sha256:<USLEEP_IMAGE_DIGEST> size: 945
- Przejdź do katalogu UWear
cd ~/confidential-space/codelabs/health_data_analysis_codelab/src/uwear
- 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:
- 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)
...
}
- 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
}
- 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
}
- Przykładowe wartości odniesienia OPA:
{
"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 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ł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
"
- Podczas weryfikacji OPA zadanie UWear sprawdza też, czy losowa wartość (nonce) jest zgodna z oczekiwaną losową wartością (eksportowany materiał klucza TLS – EKM). Numer losowy jest weryfikowany w zasadach OPA za pomocą klucza EKM przekazanego do oceniacza zasad.
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
}
- 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
- Przejdź do katalogu skryptów
cd ~/confidential-space/codelabs/health_data_analysis_codelab/scripts
- 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ń
- Ustaw projekt na $USLEEP_PROJECT_ID.
gcloud config set project $USLEEP_PROJECT_ID
- Usuń instancję maszyny wirtualnej USleep.
gcloud compute instances delete usleep --zone $USLEEP_PROJECT_ZONE
- Przejdź do katalogu usleep/workload.go.
cd ~/confidential-space/codelabs/health_data_analysis_codelab/src/usleep
- 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",
- Tworzenie nowego zadania USleep
cd ~/confidential-space/codelabs/health_data_analysis_codelab/scripts
./create_usleep_workload.sh
- 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
- 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
- Usuń instancję maszyny wirtualnej UWear
gcloud config set project $UWEAR_PROJECT_ID
gcloud compute instances delete uwear --zone $UWEAR_PROJECT_ZONE
- 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
- 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...
- Bezpieczeństwo modeli uczenia maszynowego i własności intelektualnej dzięki Confidential Space
- Jak przeprowadzać transakcje z zasobami cyfrowymi przy użyciu obliczeń wielostronnych i prywatnych pokoi
- Analizowanie poufnych danych za pomocą obszarów poufnych
Więcej informacji
- Czujesz się odizolowany? Confidential Computing na ratunek
- Poufna obróbka danych w GCP
- Poufna przestrzeń: przyszłość współpracy z zachowaniem prywatności
- Jak Google i Intel zwiększają bezpieczeństwo poufnego przetwarzania danych
- Prywatność a postęp – zwiększanie bezpieczeństwa dzięki szyfrowaniu danych w Google Cloud