Utilizza Confidential Space con risorse protette che non sono archiviate con un cloud provider

1. Panoramica

Confidential Space offre collaborazione e condivisione di dati tra più parti in modo sicuro, consentendo al contempo alle organizzazioni di preservare la riservatezza dei propri dati. Ciò significa che le organizzazioni possono collaborare tra loro mantenendo il controllo sui propri dati e proteggendoli da accessi non autorizzati.

Confidential Space sblocca scenari in cui vuoi ottenere un valore reciproco dall'aggregazione e dall'analisi di dati sensibili, spesso regolamentati, mantenendone il pieno controllo. Con Confidential Space, le organizzazioni possono ottenere un valore reciproco dall'aggregazione e dall'analisi di dati sensibili come informazioni che consentono l'identificazione personale (PII), informazioni sanitarie protette (PHI), proprietà intellettuale e secret crittografici, mantenendone il pieno controllo.

Che cosa ti serve

Cosa imparerai a fare

  • Come configurare le risorse Cloud necessarie per l'esecuzione di Spazio riservato
  • Come eseguire un workload in una VM Confidential che esegue l'immagine Confidential Space
  • Come autorizzare l'accesso alle risorse protette in base agli attributi del codice del workload (cosa), all'ambiente Confidential Space (dove) e all'account che esegue il workload (chi).

Questo codelab si concentra su come utilizzare Spazio riservato con risorse protette ospitate in un luogo diverso da Google Cloud. Scoprirai come richiedere un token autonomo personalizzato dal servizio di attestazione di Google fornendo un nonce, un segmento di pubblico e il tipo di token PKI.

In questo codelab, configurerai uno spazio riservato tra un prodotto immaginario, USleep, un'applicazione in container, e un altro prodotto immaginario, UWear, un dispositivo indossabile connesso, per calcolare la qualità del sonno. UWear condividerà i dati sanitari protetti (PHI) con USleep in un ambiente sicuro, protetto e isolato (ovvero un Trusted Execution Environment o TEE) in modo che i proprietari dei dati mantengano la massima riservatezza.

UWear è sia l'auditor del carico di lavoro sia il proprietario dei dati. In qualità di revisore del carico di lavoro,esamina il codice nel carico di lavoro in esecuzione e prende nota del digest dell'immagine. In qualità di proprietario dei dati, UWear scrive la logica di verifica per controllare la validità del token e della relativa firma. Scrive un criterio di convalida, utilizzando il digest dell'immagine dei workload sottoposti a controllo, che consente solo al digest dell'immagine specifico, in un ambiente specifico, di accedere ai dati sensibili.

In questo codelab, USleep esegue il deployment dell'applicazione containerizzata. USleep non ha accesso ai dati sensibili, ma esegue il carico di lavoro approvato a cui è consentito l'accesso ai dati sensibili.

Il codelab prevede i seguenti passaggi:

  • Passaggio 1: configura le risorse cloud necessarie per il codelab. Configura progetti, fatturazione e autorizzazioni. Scarica il codice sorgente del codelab e imposta le variabili di ambiente.
  • Passaggio 2: scarica il certificato radice e memorizzalo con il codice sorgente di UWear.
  • Passaggio 3: crea account di servizio per i workload separati che verranno utilizzati dalla VM del workload per USleep e UWear.
  • Passaggio 4: crea il workload USleep che fornisce un token di attestazione.
  • Passaggio 5: crea il carico di lavoro UWear che convalida il token di attestazione e invia i dati sensibili se il token viene approvato.
  • Passaggio 6: esegui i carichi di lavoro USleep e UWear. UWear fornirà i dati sensibili e USleep eseguirà un algoritmo per il sonno sui dati e produrrà un risultato.
  • (Facoltativo) Passaggio 7: esegui un carico di lavoro USleep non autorizzato e verifica che i dati sensibili non siano stati ricevuti da UWear.
  • Passaggio 8: ripulisci tutte le risorse.

Informazioni sul flusso di lavoro

USleep eseguirà il workload in uno spazio riservato. Per eseguire il carico di lavoro, deve accedere ai dati sanitari personali di UWear. Per ottenere l'accesso, il carico di lavoro USleep crea innanzitutto una sessione TLS sicura. USleep richiederà anche un token di attestazione dal servizio di attestazione di Google con un payload.

USleep richiederà un token di attestazione con un payload JSON contenente tre elementi:

  1. Un token di attestazione associato alla sessione TLS. Per associare il token di attestazione alla sessione TLS, il valore nonce sarà l'hash del materiale di crittografia esportato TLS. L'associazione del token alla sessione TLS garantisce che non si verifichino attacchi man-in-the-middle, poiché solo le due parti coinvolte nella sessione TLS potranno generare il valore nonce.
  2. Verrà fornito un segmento di pubblico di "uwear". UWear verificherà che si tratti del pubblico previsto per il token di attestazione.
  3. Un tipo di token "PKI". Un tipo di token "PKI" indica che USleep vuole richiedere un token autonomo. È possibile verificare che il token autonomo sia firmato da Google utilizzando il token principale scaricato dall'endpoint PKI noto di Confidential Space. Ciò è in contrasto con il tipo di token OIDC predefinito, la cui firma viene verificata utilizzando una chiave pubblica che ruota regolarmente.

bb013916a3222ce7.png

Il workload USleep riceve il token di attestazione. UWear si connette quindi alla connessione TLS con USleep e recupera il token di attestazione di USleep. UWear convaliderà il token controllando il claim x5c rispetto al certificato principale.

UWear approverà il carico di lavoro USleep se:

  1. Il token supera la logica di convalida PKI.
  2. UWear convaliderà il token controllando la rivendicazione x5c rispetto al certificato radice, verificando che il token sia firmato dal certificato secondario e infine che il certificato radice scaricato sia lo stesso del certificato radice nella rivendicazione x5c.
  3. Le rivendicazioni di misurazione del carico di lavoro nel token corrispondono alle condizioni degli attributi specificate nel criterio OPA. OPA è un motore di criteri open source generico che unifica l'applicazione dei criteri nell'intero stack. OPA utilizza documenti con una sintassi simile a JSON per impostare i valori di riferimento rispetto ai quali viene convalidata la norma. Consulta la sezione Valori di base OPA per un esempio dei valori controllati dal criterio.
  4. Il nonce corrisponde al nonce previsto (Materiale di crittografia esportato TLS). Questo viene verificato nelle norme OPA riportate sopra.

Una volta completati e superati tutti questi controlli, UWear può confermare che i dati verranno inviati ed elaborati in modo sicuro. UWear risponderà con i dati sanitari sensibili sulla stessa sessione TLS e USleep potrà utilizzare questi dati per calcolare la qualità del sonno del cliente.

2. Configurare le risorse cloud

Prima di iniziare

  1. Configura due progetti Google Cloud, uno per USleep e uno per UWear. Per ulteriori informazioni sulla creazione di un progetto Google Cloud, consulta il codelab"Configurare e navigare nel tuo primo progetto Google". Per informazioni dettagliate su come recuperare l'ID progetto e sulle differenze tra questo, il nome e il numero del progetto, consulta la sezione Creare e gestire i progetti.
  2. Attiva la fatturazione per i tuoi progetti.
  3. In uno dei Cloud Shell del tuo progetto Google, imposta le variabili di ambiente del progetto richieste come mostrato di seguito.
export UWEAR_PROJECT_ID=<Google Cloud project id of UWear>
export USLEEP_PROJECT_ID=<Google Cloud project id of USleep>
  1. Abilita l'API Confidential Computing e le API seguenti per entrambi i progetti.
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. Recupera l'identificatore principale utilizzando
gcloud auth list

# Output should contain
# ACCOUNT: <Principal Identifier>

# Set your member variable
export MEMBER='user:<Principal Identifier>'
  1. Aggiungi le autorizzazioni per questi due progetti. Le autorizzazioni possono essere aggiunte seguendo i dettagli nella pagina web Concedi un ruolo IAM.
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. In uno dei tuoi progetti Google Cloud Cloud Shell, clona il repository GitHub del Codelab Spazio riservato utilizzando il comando riportato di seguito per ottenere gli script richiesti utilizzati nell'ambito di questo codelab.
git clone https://github.com/GoogleCloudPlatform/confidential-space.git
  1. Passa alla directory degli script per il codelab relativo ai dati sanitari.
cd confidential-space/codelabs/health_data_analysis_codelab/scripts
  1. Aggiorna queste due righe nello script config_env.sh, che si trova nella directory codelabs/health_data_analysis_codelab/scripts. Aggiorna gli ID progetto con i tuoi ID progetto per USleep e UWear. Assicurati di rimuovere il simbolo di commento "#" all'inizio della riga.
# TODO: Populate UWear and USleep Project IDs
export UWEAR_PROJECT_ID=your-uwear-project-id
export USLEEP_PROJECT_ID=your-usleep-project-id
  1. (Facoltativo) Imposta eventuali variabili preesistenti. Puoi eseguire l'override dei nomi delle risorse utilizzando queste variabili (ad es.export UWEAR_ARTIFACT_REPOSITORY='my-artifact-repository')
  • Puoi impostare le seguenti variabili con i nomi delle risorse cloud esistenti. Se la variabile è impostata, verrà utilizzata la risorsa cloud esistente corrispondente del progetto. Se la variabile non è impostata, il nome della risorsa cloud viene generato dai valori nello script config_env.sh.
  1. Esegui lo script config_env.sh per impostare i nomi delle variabili rimanenti su valori basati sull'ID progetto per i nomi delle risorse.
# 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. Scarica il certificato radice

  1. Per convalidare il token autonomo restituito dal servizio di attestazione, UWear dovrà convalidare la firma rispetto al certificato radice di Confidential Space. UWear dovrà scaricare il certificato radice e archiviarlo localmente. Nella console di uno dei tuoi progetti Google Cloud, esegui i seguenti comandi:
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. Genera l'impronta del certificato radice scaricato
openssl x509 -fingerprint -in confidential_space_root.pem -noout
  1. Verifica che l'impronta corrisponda al seguente digest SHA-1:
B9:51:20:74:2C:24:E3:AA:34:04:2E:1C:3B:A3:AA:D2:8B:21:23:21

4. Crea un account di servizio per il workload

Ora crea due account di servizio: uno per i carichi di lavoro USleep e uno per i carichi di lavoro UWear. Esegui lo script create_service_accounts.sh per creare account di servizio per i carichi di lavoro nei progetti USleep e UWear. Le VM che eseguono i carichi di lavoro utilizzeranno questi account di servizio.

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

# Run the create_service_accounts script
./create_service_accounts.sh

Lo script:

  • Concedi il ruolo iam.serviceAccountUser che collega l'account di servizio al carico di lavoro.
  • Concedi il ruolo confidentialcomputing.workloadUser all'account di servizio del carico di lavoro . In questo modo l'account utente potrà generare un token di attestazione.
  • Concedi il ruolo logging.logWriter all'autorizzazione dell'account di servizio del carico di lavoro. In questo modo, l'ambiente Confidential Space può scrivere i log in Cloud Logging, oltre che nella console seriale, in modo che siano disponibili dopo l'interruzione della VM.Crea i workload

5. Crea un workload USleep

In questo passaggio, creerai immagini Docker per i carichi di lavoro utilizzati in questo codelab. Il carico di lavoro USleep è una semplice applicazione Golang che determina la qualità del sonno di un cliente utilizzando informazioni sanitarie personali su un dispositivo indossabile.

Informazioni sul workload USleep

Il carico di lavoro USleep è una semplice applicazione Golang che determina la qualità del sonno di un cliente utilizzando informazioni sanitarie personali su un dispositivo indossabile. Il carico di lavoro USleep è composto da tre elementi principali:

  1. Configurazione di una sessione TLS ed estrazione del materiale di crittografia esportato
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. Richiesta di un token dal servizio di attestazione con un segmento di pubblico, un nonce e un tipo di token 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. Ricevere i dati sensibili e calcolare la qualità del sonno dell'utente
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
  ...
}

Passaggi per creare il workload USleep

  1. Esegui lo script create_usleep_workload.sh per creare il workload USleep. Questo script:
  • Crea Artifact Registry ($USLEEP_ARTIFACT_REPOSITORY) di proprietà di UWear in cui verrà pubblicato il workload.
  • Compila il codice usleep/workload.go e lo pacchettizza in un'immagine Docker. Consulta la configurazione del Dockerfile per USleep.
  • Pubblica l'immagine Docker in Artifact Registry ($USLEEP_ARTIFACT_REPOSITORY) di proprietà di UWear.
  • Concede all'account di servizio $USLEEP_WORKLOAD_SERVICE_ACCOUNT l'autorizzazione di lettura per Artifact Registry ($USLEEP_ARTIFACT_REPOSITORY).
./create_usleep_workload.sh
  1. Importante: nei log di output, estrai il digest dell'immagine per USleep.
latest: digest: sha256:<USLEEP_IMAGE_DIGEST> size: 945
  1. Vai alla directory UWear
cd ~/confidential-space/codelabs/health_data_analysis_codelab/src/uwear
  1. Sostituisci il valore in "allowed_submods_container_image_digest" in opa_validation_values.json con 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. Creare un workload UWear

Informazioni sul workload UWear

Il carico di lavoro UWear è composto da quattro elementi principali:

  1. Unisciti alla stessa sessione TLS creata nel carico di lavoro di USleep e recupera il token di attestazione da USleep tramite la sessione TLS sicura.
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. Convalida del token autonomo:
  • La verifica del claim x5c contiene una catena di certificati che si concatena correttamente dal certificato dell'entità finale a quello intermedio e infine al certificato radice.
  • Il token deve essere firmato dal certificato finale contenuto nel claim x5c.
  • Verificare che il certificato radice scaricato / salvato sia lo stesso radice del claim 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. Il carico di lavoro UWear controllerà quindi se le affermazioni relative alla misurazione del carico di lavoro nel token corrispondono alle condizioni degli attributi specificate nel criterio OPA. OPA è un motore di criteri open source generico che unifica l'applicazione dei criteri nell'intero stack. OPA utilizza documenti con una sintassi simile a JSON per impostare i valori di riferimento rispetto ai quali viene convalidata la norma.
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"
}
  • Esempio di query 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
"

Esempio di codice per ottenere l'hash 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. Una volta completati e superati tutti questi controlli, UWear può confermare che i dati verranno inviati ed elaborati in modo sicuro. UWear risponderà con i dati sanitari sensibili sulla stessa sessione TLS e USleep potrà utilizzare questi dati per calcolare la qualità del sonno del cliente.
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()
  
  ...
}

Passaggi per creare il workload USleep

  1. Vai alla directory degli script
cd ~/confidential-space/codelabs/health_data_analysis_codelab/scripts
  1. Esegui lo script create_uwear_workload.sh per creare il workload UWear:
  • Crea Artifact Registry ($UWEAR_ARTIFACT_REPOSITORY) di proprietà di UWear in cui verrà pubblicato il workload.
  • Compila il codice uwear/workload.go e lo pacchettizza in un'immagine Docker. Consulta la configurazione del Dockerfile per USleep.
  • Pubblica l'immagine Docker in Artifact Registry ($UWEAR_ARTIFACT_REPOSITORY) di proprietà di UWear.
  • Concede all'account di servizio $UWEAR_WORKLOAD_SERVICE_ACCOUNT l'autorizzazione di lettura per Artifact Registry ($UWEAR_ARTIFACT_REPOSITORY).
./create_uwear_workload.sh

7. Esegui i carichi di lavoro USleep e UWear

Esegui il carico di lavoro 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

La risposta deve restituire uno stato STATUS: RUNNING e anche EXTERNAL_IP deve essere restituito in modo simile a questo:

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

Memorizza l'IP esterno in una variabile

export USLEEP_EXTERNAL_IP=<add your external IP> 

Verificare che il carico di lavoro USleep sia stato eseguito correttamente

Per verificare che il carico di lavoro USleep funzioni correttamente, vai alla pagina Istanze VM nel progetto USleep. Fai clic sull'istanza "usleep" e premi "Porta seriale 1(console)" nella sezione Log. Una volta che il server è attivo e funzionante, in fondo ai log dovresti vedere qualcosa di simile al seguente.

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

Esegui il carico di lavoro 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

Verificare che il carico di lavoro UWear sia stato eseguito correttamente

Per visualizzare i log del carico di lavoro UWear, vai alla pagina Istanze VM nel progetto UWear. Fai clic sull'istanza "uwear" e premi "Porta seriale 1(console)" nella sezione Log.

L'output del log al termine dell'avvio completo dell'istanza dovrebbe avere il seguente aspetto

Nel progetto UWear, i log di serie dovrebbero mostrare qualcosa di simile a

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

Se il tuo carico di lavoro UWear non ha questo aspetto, consulta le note di seguito per le istruzioni.

Visualizzare i risultati di USleep

Per visualizzare i risultati, torna alla pagina Istanze VM nel progetto USleep. Fai clic sull'istanza "usleep" e premi "Porta seriale 1(console)" nella sezione Log. Visualizza i risultati del carico di lavoro nella parte inferiore dei log. Dovrebbero avere un aspetto simile a quello del campione riportato di seguito.

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

Il risultato dovrebbe essere "total sleep time is less than 8 hours".

Congratulazioni, hai creato uno spazio riservato tra UWear e USleep per condividere informazioni sensibili.

8. (Facoltativo) Esegui un carico di lavoro non autorizzato

Nel prossimo scenario, USleep aggiorna il codice ed esegue un carico di lavoro diverso sui dati sul sonno forniti da UWear. UWear non ha accettato questo nuovo carico di lavoro e non ha aggiornato le proprie norme OPA per consentire il nuovo digest delle immagini. Verificheremo che UWear non invii i suoi dati sensibili al carico di lavoro non autorizzato.

USleep modifica il proprio carico di lavoro

  1. Imposta il progetto su $USLEEP_PROJECT_ID.
gcloud config set project $USLEEP_PROJECT_ID
  1. Elimina l'istanza VM USleep.
gcloud compute instances delete usleep --zone $USLEEP_PROJECT_ZONE
  1. Vai alla directory usleep/workload.go.
cd ~/confidential-space/codelabs/health_data_analysis_codelab/src/usleep
  1. Nel file usleep/workload.go. Aggiorna la riga "audience": "uwear". In questo esempio, per modificare il digest delle immagini, aggiorneremo il segmento di pubblico con un valore diverso non approvato da UWear. Pertanto, UWear dovrebbe rifiutarlo per due motivi: digest di immagini non approvato e pubblico errato.
"audience": "anotherCompany.com",
  1. Creare un nuovo workload USleep
cd ~/confidential-space/codelabs/health_data_analysis_codelab/scripts

./create_usleep_workload.sh
  1. Crea la nuova istanza VM USleep ed esegui il carico di lavoro
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. Estrai il nuovo IP esterno di USleep per utilizzarlo in un secondo momento
export USLEEP_EXTERNAL_IP=<add your external IP>

Esegui nuovamente il carico di lavoro

  1. Elimina l'istanza VM UWear
gcloud config set project $UWEAR_PROJECT_ID

gcloud compute instances delete uwear --zone $UWEAR_PROJECT_ZONE
  1. Ricrea l'istanza VM UWear utilizzando il nuovo IP esterno
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. Nei log di serie di UWear dovrebbe essere visualizzato il seguente messaggio e la VM USleep non dovrebbe ricevere dati sensibili
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. Esegui la pulizia

Lo script di pulizia può essere utilizzato per eliminare le risorse che abbiamo creato nell'ambito di questo codelab. Nell'ambito di questa operazione di pulizia, verranno eliminate le seguenti risorse:

  • L'account di servizio UWear ($UWEAR_SERVICE_ACCOUNT).
  • Il registry degli elementi UWear ($UWEAR_ARTIFACT_REPOSITORY).
  • L'istanza Compute UWear
  • Il service account USleep ($USLEEP_SERVICE_ACCOUNT).
  • Il registry degli elementi USleep ($USLEEP_ARTIFACT_REPOSITORY).
  • L'istanza di calcolo USleep
./cleanup.sh

Se hai finito di esplorare, ti consigliamo di eliminare il progetto seguendo queste istruzioni.

Congratulazioni

Complimenti, hai completato il codelab.

Hai imparato a condividere i dati in sicurezza mantenendo la loro riservatezza utilizzando Confidential Space.

Passaggi successivi

Dai un'occhiata ad alcuni di questi codelab simili…

Letture aggiuntive