Utiliser Confidential Space avec des ressources protégées qui ne sont pas stockées auprès d'un fournisseur cloud

1. Présentation

Confidential Space offre un partage et une collaboration de données sécurisés entre plusieurs parties, tout en permettant aux entreprises de préserver la confidentialité de leurs données. Cela signifie que les entreprises peuvent collaborer tout en gardant le contrôle de leurs données et en les protégeant contre tout accès non autorisé.

Confidential Space vous permet de dégager mutuellement de la valeur en agrégeant et en analysant des données sensibles, souvent réglementées, tout en conservant un contrôle total dessus. Avec Confidential Space, les entreprises peuvent dégager mutuellement de la valeur en agrégeant et en analysant des données sensibles telles que des informations permettant d'identifier personnellement l'utilisateur, des données de santé protégées, des droits de propriété intellectuelle et des secrets cryptographiques, tout en gardant un contrôle total dessus.

Prérequis

Points abordés

  • Configurer les ressources Cloud nécessaires pour exécuter Confidential Space
  • Exécuter une charge de travail dans une VM Confidential exécutant l'image Confidential Space
  • Autoriser l'accès aux ressources protégées en fonction des attributs du code de la charge de travail (quoi), de l'environnement Confidential Space () et du compte qui exécute la charge de travail (qui).

Cet atelier de programmation explique comment utiliser l'espace confidentiel avec des ressources protégées hébergées ailleurs que sur Google Cloud. Vous allez apprendre à demander un jeton personnalisé et autonome au service d'attestation Google en fournissant un nonce, une audience et le type de jeton PKI.

Dans cet atelier de programmation, vous allez configurer un espace confidentiel entre un produit fictif (USleep, une application conteneurisée) et un autre produit fictif (UWear, un appareil connecté) pour calculer la qualité de votre sommeil. UWear partagera des données de santé protégées avec USleep dans un environnement sûr, sécurisé et isolé (également appelé environnement d'exécution sécurisé ou TEE) afin que les propriétaires des données conservent une confidentialité totale.

UWear est à la fois l'auditeur de la charge de travail et le propriétaire des données. En tant que auditeur de charge de travail,il examine le code de la charge de travail en cours d'exécution et prend note du condensé de l'image. En tant que propriétaire des données, UWear écrit la logique de validation pour vérifier la validité du jeton et de sa signature. Il écrit une règle de validation, à l'aide du condensé d'image des charges de travail auditées, qui n'autorise que le condensé d'image spécifique, dans un environnement spécifique, à accéder aux données sensibles.

Dans cet atelier de programmation, USleep déploie l'application conteneurisée. USleep n'a pas accès aux données sensibles, mais exécute la charge de travail approuvée qui est autorisée à y accéder.

Cet atelier de programmation comprend les étapes suivantes:

  • Étape 1: Configurez les ressources cloud nécessaires pour l'atelier de programmation. Configurez des projets, la facturation et les autorisations. Téléchargez le code source de l'atelier de programmation et définissez les variables d'environnement.
  • Étape 2: Téléchargez le certificat racine et stockez-le avec votre code source UWear.
  • Étape 3: Créez des comptes de service de charge de travail distincts qui seront utilisés par la VM de charge de travail pour USleep et UWear.
  • Étape 4: Créez la charge de travail USleep, qui fournit un jeton d'attestation.
  • Étape 5: Créez la charge de travail UWear qui valide le jeton d'attestation et envoie les données sensibles si le jeton est approuvé.
  • Étape 6: Exécutez les charges de travail USleep et UWear. UWear fournira les données sensibles, et USleep exécutera un algorithme de sommeil sur les données et fournira un résultat.
  • Étape 7 (facultatif) : Exécutez une charge de travail USleep non autorisée et vérifiez que des données sensibles n'ont pas été reçues d'UWear.
  • Étape 8: Nettoyez toutes les ressources.

Comprendre le workflow

USleep exécutera la charge de travail dans Confidential Space. Pour exécuter la charge de travail, il a besoin d'accéder aux informations médicales de UWear. Pour y accéder, la charge de travail USleep crée d'abord une session TLS sécurisée. USleep demande ensuite un jeton d'attestation au service d'attestation Google avec une charge utile.

USleep demande un jeton d'attestation avec une charge utile JSON qui contient trois éléments:

  1. Jeton d'attestation lié à la session TLS Pour lier le jeton d'attestation à la session TLS, la valeur nonce correspond au hachage du matériel de clé exporté TLS. Lier le jeton à la session TLS garantit qu'aucune attaque de l'intercepteur ne se produit, car seules les deux parties impliquées dans la session TLS peuvent générer la valeur nonce.
  2. Une audience "uwear" sera fournie. UWear vérifiera qu'il s'agit bien de l'audience visée par le jeton d'attestation.
  3. Type de jeton "PKI". Un type de jeton "PKI" signifie que USleep souhaite demander un jeton autonome. Vous pouvez vérifier que le jeton autosuffisant est signé par Google à l'aide de la racine téléchargée à partir du point de terminaison PKI connu de Confidential Space. Contrairement au type de jeton OIDC par défaut, dont la signature est validée à l'aide d'une clé publique qui est régulièrement remplacée,

bb013916a3222ce7.png

La charge de travail USleep reçoit le jeton d'attestation. UWear rejoint ensuite la connexion TLS avec USleep et récupère le jeton d'attestation d'USleep. UWear valide le jeton en vérifiant la revendication x5c par rapport au certificat racine.

UWear approuve la charge de travail USleep si:

  1. Le jeton passe la logique de validation PKI.
  2. UWear valide le jeton en vérifiant la revendication x5c par rapport au certificat racine, en vérifiant que le jeton est signé par le certificat de feuille et, enfin, que le certificat racine téléchargé est le même que celui de la revendication x5c.
  3. Les revendications de mesure de la charge de travail dans le jeton correspondent aux conditions d'attribut spécifiées dans la règle OPA. OPA est un moteur de règles Open Source à usage général qui unifie l'application des règles dans la pile. OPA utilise des documents, dont la syntaxe est semblable à celle du JSON, pour définir des valeurs de référence contre lesquelles la règle est validée. Pour obtenir un exemple des valeurs que la règle vérifie, consultez la section Valeurs de référence OPA.
  4. Le nonce correspond au nonce attendu (matériel de clé exporté TLS). Cela est confirmé dans la règle OPA ci-dessus.

Une fois que toutes ces vérifications ont été effectuées et réussies, UWear peut confirmer que les données seront envoyées et traitées de manière sécurisée. UWear répondra ensuite avec les informations PHI sensibles via la même session TLS, et USleep pourra utiliser ces données pour calculer la qualité du sommeil du client.

2. Configurer des ressources Cloud

Avant de commencer

  1. Configurez deux projets Google Cloud, l'un pour USleep et l'autre pour UWear. Pour en savoir plus sur la création d'un projet Google Cloud, consultez l'atelier de programmation Configurer et parcourir votre premier projet Google. Pour savoir comment récupérer l'ID du projet et en quoi il diffère du nom et du numéro du projet, consultez la section Créer et gérer des projets.
  2. Activez la facturation pour vos projets.
  3. Dans l'un des Cloud Shell de votre projet Google, définissez les variables d'environnement de projet requises, comme indiqué ci-dessous.
export UWEAR_PROJECT_ID=<Google Cloud project id of UWear>
export USLEEP_PROJECT_ID=<Google Cloud project id of USleep>
  1. Activez l'API Confidential Computing et les API suivantes pour les deux projets.
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. Récupérez votre identifiant principal à l'aide de
gcloud auth list

# Output should contain
# ACCOUNT: <Principal Identifier>

# Set your member variable
export MEMBER='user:<Principal Identifier>'
  1. Ajoutez des autorisations pour ces deux projets. Pour ajouter des autorisations, suivez les instructions de la page Accorder un rôle 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. Dans l'un de vos projets Google Cloud Cloud Shell, clonez le dépôt GitHub de l'atelier de programmation Confidential Space à l'aide de la commande ci-dessous pour obtenir les scripts requis utilisés dans cet atelier de programmation.
git clone https://github.com/GoogleCloudPlatform/confidential-space.git
  1. Accédez au répertoire des scripts pour l'atelier de programmation sur les données de santé.
cd confidential-space/codelabs/health_data_analysis_codelab/scripts
  1. Mettez à jour ces deux lignes dans le script config_env.sh, situé dans le répertoire codelabs/health_data_analysis_codelab/scripts. Remplacez les ID de projet par les vôtres pour USleep et UWear. Veillez à supprimer le symbole de commentaire "#" au début de la ligne.
# TODO: Populate UWear and USleep Project IDs
export UWEAR_PROJECT_ID=your-uwear-project-id
export USLEEP_PROJECT_ID=your-usleep-project-id
  1. Facultatif: Définissez les variables préexistantes. Vous pouvez remplacer les noms de ressources à l'aide de ces variables (par exemple, export UWEAR_ARTIFACT_REPOSITORY='my-artifact-repository').
  • Vous pouvez définir les variables suivantes avec des noms de ressources cloud existants. Si la variable est définie, la ressource cloud existante correspondante du projet est utilisée. Si la variable n'est pas définie, le nom de la ressource cloud est généré à partir des valeurs du script config_env.sh.
  1. Exécutez le script config_env.sh pour définir les noms de variable restants sur des valeurs basées sur votre ID de projet pour les noms de ressources.
# 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. Télécharger le certificat racine

  1. Pour valider le jeton autonome renvoyé par le service d'attestation, UWear doit valider la signature par rapport au certificat racine de l'espace confidentiel. UWear doit télécharger le certificat racine et le stocker localement. Dans la console de l'un de vos projets Google Cloud, exécutez les commandes suivantes:
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. Générer l'empreinte du certificat racine téléchargé
openssl x509 -fingerprint -in confidential_space_root.pem -noout
  1. Vérifiez que l'empreinte correspond au récapitulatif SHA-1 suivant:
B9:51:20:74:2C:24:E3:AA:34:04:2E:1C:3B:A3:AA:D2:8B:21:23:21

4. Créer un compte de service de charge de travail

Vous allez maintenant créer deux comptes de service : un pour les charges de travail USleep et un pour les charges de travail UWear. Exécutez le script create_service_accounts.sh pour créer des comptes de service de charge de travail dans les projets USleep et UWear. Les VM qui exécutent les charges de travail utiliseraient ces comptes de service.

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

# Run the create_service_accounts script
./create_service_accounts.sh

Le script:

  • Attribue le rôle iam.serviceAccountUser, qui associe le compte de service à la charge de travail.
  • Attribue le rôle confidentialcomputing.workloadUser au compte de service de la charge de travail . Le compte utilisateur pourra ainsi générer un jeton d'attestation.
  • Attribue le rôle logging.logWriter à l'autorisation du compte de service de la charge de travail. Cela permet à l'environnement Confidential Space d'écrire des journaux dans Cloud Logging en plus de la console série. Les journaux sont donc disponibles une fois la VM arrêtée.

5. Créer une charge de travail USleep

Au cours de cette étape, vous allez créer des images Docker pour les charges de travail utilisées dans cet atelier de programmation. La charge de travail USleep est une application Golang simple qui détermine la qualité du sommeil d'un client à l'aide d'informations de santé personnelles sur un appareil connecté.

À propos de la charge de travail USleep

La charge de travail USleep est une application Golang simple qui détermine la qualité du sommeil d'un client à l'aide d'informations de santé personnelles sur un appareil connecté. La charge de travail USleep se compose de trois éléments principaux:

  1. Configurer une session TLS et extraire le matériau de clé exporté
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. Demander un jeton au service d'attestation avec une audience, un nonce et un type de jeton 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. Recevoir les données sensibles et calculer la qualité du sommeil de l'utilisateur
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
  ...
}

Procédure pour créer la charge de travail USleep

  1. Exécutez le script create_usleep_workload.sh pour créer la charge de travail USleep. Ce script:
  • Crée un dépôt Artifact Registry ($USLEEP_ARTIFACT_REPOSITORY) appartenant à UWear dans lequel la charge de travail sera publiée.
  • Crée le code usleep/workload.go et l'empaquette dans une image Docker. Consultez la configuration du Dockerfile pour USleep.
  • Publie l'image Docker dans Artifact Registry ($USLEEP_ARTIFACT_REPOSITORY) appartenant à UWear.
  • Accorde au compte de service $USLEEP_WORKLOAD_SERVICE_ACCOUNT l'autorisation de lecture pour Artifact Registry ($USLEEP_ARTIFACT_REPOSITORY).
./create_usleep_workload.sh
  1. Important: Dans les journaux de sortie, extrayez le récapitulatif d'image pour USleep.
latest: digest: sha256:<USLEEP_IMAGE_DIGEST> size: 945
  1. Accédez au répertoire UWear.
cd ~/confidential-space/codelabs/health_data_analysis_codelab/src/uwear
  1. Remplacez la valeur sous "allowed_submods_container_image_digest" dans le fichier opa_validation_values.json par 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. Créer une charge de travail UWear

À propos de la charge de travail UWear

La charge de travail UWear se compose de quatre éléments principaux:

  1. Rejoindre la même session TLS créée dans la charge de travail USleep et récupérer le jeton d'attestation d'USleep via la session TLS sécurisée.
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. Valider le jeton autonome en:
  • La vérification de la revendication x5c contient une chaîne de certificats qui se lie correctement du certificat de feuille au certificat intermédiaire, puis au certificat racine.
  • Vérifiez que le jeton est signé par le certificat feuille contenu dans la revendication x5c.
  • Vérifier que le certificat racine téléchargé / stocké est le même que celui de la revendication 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. La charge de travail UWear vérifie ensuite si les revendications de mesure de la charge de travail dans le jeton correspondent aux conditions d'attribut spécifiées dans la règle OPA. OPA est un moteur de règles Open Source à usage général qui unifie l'application des règles dans la pile. OPA utilise des documents, dont la syntaxe est semblable à celle du JSON, pour définir des valeurs de référence contre lesquelles la règle est validée.
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"
}
  • Exemple de requête 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
"

Exemple de code pour obtenir le hachage 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. Une fois que toutes ces vérifications ont été effectuées et réussies, UWear peut confirmer que les données seront envoyées et traitées de manière sécurisée. UWear répondra ensuite avec les informations PHI sensibles via la même session TLS, et USleep pourra utiliser ces données pour calculer la qualité du sommeil du client.
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()
  
  ...
}

Procédure pour créer la charge de travail USleep

  1. Accédez au répertoire des scripts
cd ~/confidential-space/codelabs/health_data_analysis_codelab/scripts
  1. Exécutez le script create_uwear_workload.sh pour créer la charge de travail UWear:
  • Crée un dépôt Artifact Registry ($UWEAR_ARTIFACT_REPOSITORY) appartenant à UWear dans lequel la charge de travail sera publiée.
  • Crée le code uwear/workload.go et l'empaquette dans une image Docker. Consultez la configuration du Dockerfile pour USleep.
  • Publie l'image Docker dans Artifact Registry ($UWEAR_ARTIFACT_REPOSITORY) appartenant à UWear.
  • Accorde au compte de service $UWEAR_WORKLOAD_SERVICE_ACCOUNT l'autorisation de lecture pour Artifact Registry ($UWEAR_ARTIFACT_REPOSITORY).
./create_uwear_workload.sh

7. Exécuter les charges de travail USleep et UWear

Exécuter la charge de travail 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 réponse doit renvoyer un STATUS: RUNNING, et l'adresse IP EXTERNE doit également être renvoyée, comme suit:

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

Stocker l'adresse IP externe dans une variable

export USLEEP_EXTERNAL_IP=<add your external IP> 

Vérifier que la charge de travail USleep s'est exécutée correctement

Pour vérifier que la charge de travail USleep s'exécute correctement, accédez à la page Instances de VM du projet USleep. Cliquez sur l'instance "usleep", puis sur "Port série 1(console)" dans la section "Journaux". Une fois le serveur opérationnel, les journaux devraient afficher quelque chose de semblable à ce qui suit en bas.

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

Exécuter la charge de travail 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

Vérifier que la charge de travail UWear s'est exécutée correctement

Pour afficher les journaux de la charge de travail UWear, accédez à la page Instances de VM du projet UWear. Cliquez sur l'instance "uwear", puis sur "Port série 1(console)" dans la section "Journaux".

Une fois l'instance démarrée, la sortie du journal doit ressembler à ceci :

Dans le projet UWear, les journaux sériels doivent se présenter comme suit :

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

Si votre charge de travail UWear ne ressemble pas à celle-ci, consultez les notes ci-dessous pour obtenir des instructions.

Afficher les résultats de USleep

Pour afficher les résultats, revenez à la page Instances de VM du projet USleep. Cliquez sur l'instance "usleep", puis sur "Port série 1(console)" dans la section "Journaux". Consultez les résultats de la charge de travail en bas des journaux. Elles doivent ressembler à l'exemple ci-dessous.

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

Le résultat doit être "total sleep time is less than 8 hours"..

Félicitations ! Vous avez créé un espace confidentiel entre UWear et USleep pour partager des informations sensibles.

8. (Facultatif) Exécuter une charge de travail non autorisée

Dans le scénario suivant, USleep met à jour le code et exécute une charge de travail différente sur les données de sommeil fournies par UWear. UWear n'a pas accepté cette nouvelle charge de travail et n'a pas mis à jour sa règle OPA pour autoriser le nouveau condensé d'image. Nous vérifierons que UWear n'envoie pas ses données sensibles à la charge de travail non autorisée.

USleep modifie sa charge de travail

  1. Définissez le projet sur $USLEEP_PROJECT_ID.
gcloud config set project $USLEEP_PROJECT_ID
  1. Supprimez l'instance de VM USleep.
gcloud compute instances delete usleep --zone $USLEEP_PROJECT_ZONE
  1. Accédez au répertoire usleep/workload.go.
cd ~/confidential-space/codelabs/health_data_analysis_codelab/src/usleep
  1. Dans le fichier usleep/workload.go. Modifier la ligne "audience": "uwear". Dans cet exemple, afin de modifier le récapitulatif des images, nous allons remplacer l'audience par une valeur différente que UWear n'a pas approuvée. UWear doit donc le refuser pour deux raisons : condensé d'images non approuvé et audience incorrecte.
"audience": "anotherCompany.com",
  1. Créer une charge de travail USleep
cd ~/confidential-space/codelabs/health_data_analysis_codelab/scripts

./create_usleep_workload.sh
  1. Créer l'instance de VM USleep et exécuter la charge de travail
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. Extraire la nouvelle adresse IP externe USleep pour une utilisation ultérieure
export USLEEP_EXTERNAL_IP=<add your external IP>

Réexécuter la charge de travail

  1. Supprimer l'instance de VM UWear
gcloud config set project $UWEAR_PROJECT_ID

gcloud compute instances delete uwear --zone $UWEAR_PROJECT_ZONE
  1. Recréer l'instance de VM UWear à l'aide de la nouvelle adresse IP externe
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. Dans les journaux de série UWear, le message suivant doit s'afficher et la VM USleep ne doit pas recevoir de données sensibles.
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. Effectuer un nettoyage

Le script de nettoyage permet de nettoyer les ressources que nous avons créées dans cet atelier de programmation. Lors de ce nettoyage, les ressources suivantes seront supprimées:

  • Le compte de service UWear ($UWEAR_SERVICE_ACCOUNT)
  • Le dépôt d'artefacts UWear ($UWEAR_ARTIFACT_REPOSITORY)
  • Instance de calcul UWear
  • Compte de service USleep ($USLEEP_SERVICE_ACCOUNT)
  • Le registre d'artefacts USleep ($USLEEP_ARTIFACT_REPOSITORY)
  • Instance de calcul USleep
./cleanup.sh

Si vous avez terminé votre exploration, vous pouvez supprimer votre projet en suivant ces instructions.

Félicitations

Félicitations, vous avez terminé l'atelier de programmation.

Vous avez appris à partager des données de manière sécurisée tout en préservant leur confidentialité à l'aide de Confidential Space.

Et ensuite ?

Découvrez ces ateliers de programmation similaires :

Complément d'informations