Usa Confidential Space con recursos protegidos que no estén almacenados con un proveedor de servicios en la nube

1. Descripción general

Confidencial Space ofrece colaboración y uso compartido de datos seguros entre varias partes, al tiempo que permite a las organizaciones preservar la confidencialidad de sus datos. Esto significa que las organizaciones pueden colaborar entre sí y, al mismo tiempo, mantener el control sobre sus datos y protegerlos del acceso no autorizado.

Confidential Space abre situaciones en las que deseas obtener valor mutuo a partir de la agregación y el análisis de datos sensibles, a menudo regulados, y, al mismo tiempo, conservar el control total sobre ellos. Con Confidential Space, las organizaciones pueden obtener valor mutuo a partir de la agregación y el análisis de datos sensibles, como información de identificación personal (PII), información de salud protegida (PHI), propiedad intelectual y secretos criptográficos, y, al mismo tiempo, conservar el control total sobre ellos.

Requisitos

Qué aprenderás

  • Cómo configurar los recursos de Cloud necesarios para ejecutar Confidential Space
  • Cómo ejecutar una carga de trabajo en una Confidential VM que ejecuta la imagen de Confidential Space
  • Cómo autorizar el acceso a recursos protegidos según los atributos del código de la carga de trabajo (qué), el entorno de Confidential Space (dónde) y la cuenta que ejecuta la carga de trabajo (quién).

En este codelab, se explica cómo usar el Espacio confidencial con recursos protegidos que se alojan en otro lugar que no sea Google Cloud. Aprenderás a solicitar un token personalizado y autónomo del servicio de certificación de Google proporcionando un nonce, un público y el tipo de token de PKI.

En este codelab, configurarás un espacio confidencial entre un producto ficticio (USleep, una aplicación en contenedor) y un producto ficticio (UWear, un dispositivo wearable conectado) para calcular la calidad de tu sueño. UWear compartirá información de salud protegida (PHI) con USleep en un entorno seguro, aislado y protegido (también conocido como entorno de ejecución confiable o TEE) para que los propietarios de los datos conserven la confidencialidad completa.

UWear es el auditor de cargas de trabajo y el propietario de los datos. Como auditor de cargas de trabajo, revisa el código de la carga de trabajo que se está ejecutando y toma nota del resumen de la imagen. Como propietario de los datos, UWear escribe la lógica de verificación para verificar la validez del token y su firma. Escribe una política de validación, con el resumen de imágenes de las cargas de trabajo auditadas, que solo permite que el resumen de imágenes específico, en un entorno específico, obtenga acceso a los datos sensibles.

En este codelab, USleep implementa la aplicación alojada en contenedores. USleep no tiene acceso a los datos sensibles, pero ejecuta la carga de trabajo aprobada que tiene acceso a ellos.

El codelab incluye los siguientes pasos:

  • Paso 1: Configura los recursos de nube necesarios para el codelab. Configura proyectos, facturación y permisos. Descarga el código fuente del codelab y establece las variables de entorno.
  • Paso 2: Descarga el certificado raíz y guárdalo con tu código fuente de UWear.
  • Paso 3: Crea cuentas de servicio de carga de trabajo independientes que usará la VM de carga de trabajo para USleep y UWear.
  • Paso 4: Crea la carga de trabajo de USleep, que proporciona un token de certificación.
  • Paso 5: Crea la carga de trabajo de UWear que valida el token de certificación y envía los datos sensibles si se aprueba el token.
  • Paso 6: Ejecuta las cargas de trabajo de USleep y UWear. UWear proporcionará los datos sensibles, y USleep ejecutará un algoritmo de sueño en los datos y mostrará un resultado.
  • Paso 7: (Opcional) Ejecuta una carga de trabajo de USleep no autorizada y confirma que no se hayan recibido datos sensibles de UWear.
  • Paso 8: Limpia todos los recursos.

Comprende el flujo de trabajo

USleep ejecutará la carga de trabajo en Confidential Space. Para ejecutar la carga de trabajo, necesita acceso a la PHI de UWear. Para obtener acceso, la carga de trabajo de USleep primero crea una sesión TLS segura. Luego, USleep también solicitará un token de certificación al servicio de certificación de Google con una carga útil.

USleep solicitará un token de certificación con una carga útil de JSON que contendrá tres elementos:

  1. Un token de certificación vinculado a la sesión de TLS Para vincular el token de certificación a la sesión de TLS, el valor del nonce será el hash del material de claves exportado de TLS. La vinculación del token a la sesión de TLS garantiza que no se produzcan ataques de intermediario, ya que solo las dos partes involucradas en la sesión de TLS podrán generar el valor de nonce.
  2. Se proporcionará un público de "uwear". UWear verificará que es el público previsto para el token de certificación.
  3. Un tipo de token de "PKI". Un tipo de token de "PKI" significa que USleep desea solicitar un token independiente. Se puede verificar que Google firmó el token independiente con la raíz descargada del extremo de PKI conocido de Confidential Space. Esto contrasta con el tipo de token de OIDC predeterminado, cuya firma se verifica con una clave pública que rota con regularidad.

bb013916a3222ce7.png

La carga de trabajo de USleep recibe el token de certificación. Luego, UWear se une a la conexión TLS con USleep y recupera el token de certificación de USleep. UWear validará el token verificando el reclamación x5c en el certificado raíz.

UWear aprobará la carga de trabajo de USleep en los siguientes casos:

  1. El token pasa la lógica de validación de PKI.
  2. UWear validará el token verificando el reclamo x5c en el certificado raíz, verificando que el token esté firmado por el certificado de entidad final y, por último, que el certificado raíz descargado sea el mismo que el de la reclamación x5c.
  3. Los reclamos de medición de la carga de trabajo en el token coinciden con las condiciones de los atributos especificados en la política de OPA. OPA es un motor de políticas de código abierto y de uso general que unifica la aplicación de políticas en toda la pila. OPA usa documentos, con una sintaxis similar a JSON, para establecer valores de referencia con los que se valida la política. Consulta Valores de referencia de la OPA para ver un ejemplo de los valores que verifica la política.
  4. El nonce coincide con el nonce esperado (material de claves exportado de TLS). Esto se verifica en la política de OPA anterior.

Una vez que se completen y se aprueben todas esas verificaciones, UWear podrá confirmar que los datos se enviarán y procesarán de forma segura. Luego, UWear responderá con la PHI sensible a través de la misma sesión de TLS, y USleep podrá usar esos datos para calcular la calidad del sueño del cliente.

2. Configura recursos de Cloud

Antes de comenzar

  1. Configura dos proyectos de Google Cloud, uno para USleep y otro para UWear. Para obtener más información sobre cómo crear un proyecto de Google Cloud, consulta el codelab"Configura y navega por tu primer proyecto de Google". Puedes consultar cómo crear y administrar proyectos para obtener detalles sobre cómo recuperar el ID del proyecto y en qué se diferencia del nombre y el número del proyecto.
  2. Habilita la facturación para tus proyectos.
  3. En una de las secciones de Cloud Shell de tu proyecto de Google, configura las variables de entorno del proyecto requeridas como se muestra a continuación.
export UWEAR_PROJECT_ID=<Google Cloud project id of UWear>
export USLEEP_PROJECT_ID=<Google Cloud project id of USleep>
  1. Habilita la API de Confidential Computing y las siguientes APIs para ambos proyectos.
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 tu identificador de principal con
gcloud auth list

# Output should contain
# ACCOUNT: <Principal Identifier>

# Set your member variable
export MEMBER='user:<Principal Identifier>'
  1. Agrega permisos para estos dos proyectos. Para agregar permisos, sigue los detalles que se indican en la página web para otorgar un rol de 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. En uno de tus proyectos de Google Cloud en Cloud Shell, clona el repositorio de GitHub del codelab de espacio confidencial con el siguiente comando para obtener las secuencias de comandos necesarias que se usan como parte de este codelab.
git clone https://github.com/GoogleCloudPlatform/confidential-space.git
  1. Cambia al directorio de secuencias de comandos del codelab de datos de salud.
cd confidential-space/codelabs/health_data_analysis_codelab/scripts
  1. Actualiza estas dos líneas en la secuencia de comandos config_env.sh, que se encuentra en el directorio codelabs/health_data_analysis_codelab/scripts. Actualiza los IDs de los proyectos con los IDs de USleep y UWear. Asegúrate de quitar el símbolo de comentario “#” al comienzo de la línea.
# TODO: Populate UWear and USleep Project IDs
export UWEAR_PROJECT_ID=your-uwear-project-id
export USLEEP_PROJECT_ID=your-usleep-project-id
  1. Opcional: Establece cualquier variable preexistente. Puedes anular los nombres de los recursos con estas variables (p. ej., export UWEAR_ARTIFACT_REPOSITORY='my-artifact-repository').
  • Puedes configurar las siguientes variables con nombres de recursos de nube existentes. Si se configura la variable, se usará el recurso de nube existente correspondiente del proyecto. Si no se establece la variable, el nombre del recurso de Cloud se generará a partir de los valores de la secuencia de comandos config_env.sh.
  1. Ejecuta la secuencia de comandos config_env.sh para establecer los nombres de las variables restantes en valores basados en el ID de tu proyecto para los nombres de los recursos.
# 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. Descarga el certificado raíz

  1. Para validar el token independiente que se muestra desde el servicio de certificación, UWear deberá validar la firma en función del certificado raíz de Confidential Space. UWear deberá descargar el certificado raíz y almacenarlo de forma local. En la consola de uno de tus proyectos de Google Cloud, ejecuta los siguientes comandos:
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 la huella digital del certificado raíz que se descargó
openssl x509 -fingerprint -in confidential_space_root.pem -noout
  1. Verifica que la huella digital coincida con el siguiente resumen SHA-1:
B9:51:20:74:2C:24:E3:AA:34:04:2E:1C:3B:A3:AA:D2:8B:21:23:21

4. Crea una cuenta de servicio de carga de trabajo

Ahora, crearás dos cuentas de servicio: una para USleep y otra para las cargas de trabajo de UWear. Ejecuta la secuencia de comandos create_service_accounts.sh para crear cuentas de servicio de cargas de trabajo en los proyectos USleep y UWear. Las VMs que ejecutan las cargas de trabajo usarían estas cuentas de servicio.

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

# Run the create_service_accounts script
./create_service_accounts.sh

La secuencia de comandos hace lo siguiente:

  • Otorga el rol iam.serviceAccountUser, que adjunta la cuenta de servicio a la carga de trabajo.
  • Otorga el rol confidentialcomputing.workloadUser a la cuenta de servicio de la carga de trabajo . Esto permitirá que la cuenta de usuario genere un token de certificación.
  • Otorga el rol logging.logWriter al permiso de la cuenta de servicio de la carga de trabajo. Esto permite que el entorno de Confidential Space escriba registros en Cloud Logging, además de la consola en serie, de modo que los registros estén disponibles después de que se cierre la VM.Crea cargas de trabajo

5. Crea una carga de trabajo de USleep

Como parte de este paso, crearás imágenes de Docker para las cargas de trabajo que se usan en este codelab. La carga de trabajo de USleep es una aplicación simple de Golang que determina la calidad del sueño de un cliente con información de salud personal en un dispositivo portátil.

Acerca de la carga de trabajo de USleep

La carga de trabajo de USleep es una aplicación simple de Golang que determina la calidad del sueño de un cliente con información de salud personal en un dispositivo portátil. La carga de trabajo de USleep tiene tres partes principales:

  1. Cómo configurar una sesión de TLS y extraer el material de claves exportado
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. Solicita un token al servicio de certificación con un público, un nonce y un tipo de token de 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. Recibir los datos sensibles y calcular la calidad del sueño del usuario
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
  ...
}

Pasos para crear la carga de trabajo de USleep

  1. Ejecuta la secuencia de comandos create_usleep_workload.sh para crear la carga de trabajo de USleep. Esta secuencia de comandos hace lo siguiente:
  • Crea un Artifact Registry ($USLEEP_ARTIFACT_REPOSITORY) propiedad de UWear en el que se publicará la carga de trabajo.
  • Compila el código usleep/workload.go y lo empaqueta en una imagen de Docker. Consulta la configuración del Dockerfile para USleep.
  • Publica la imagen de Docker en Artifact Registry ($USLEEP_ARTIFACT_REPOSITORY) que pertenece a UWear.
  • Otorga a la cuenta de servicio $USLEEP_WORKLOAD_SERVICE_ACCOUNT permiso de lectura para Artifact Registry ($USLEEP_ARTIFACT_REPOSITORY).
./create_usleep_workload.sh
  1. Importante: En los registros de salida, extrae el resumen de imágenes de USleep.
latest: digest: sha256:<USLEEP_IMAGE_DIGEST> size: 945
  1. Navega al directorio UWear
cd ~/confidential-space/codelabs/health_data_analysis_codelab/src/uwear
  1. Reemplaza el valor de "allowed_submods_container_image_digest" en opa_validation_values.json por 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. Crea una carga de trabajo de UWear

Información acerca de la carga de trabajo de UWear

La carga de trabajo de UWear tiene 4 partes principales:

  1. Unirse a la misma sesión TLS que se creó en la carga de trabajo de USleep y recuperar el token de certificación de USleep a través de la sesión TLS segura
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. Valida el token independiente de la siguiente manera:
  • La verificación del reclamo x5c contiene una cadena de certificados que se conecta correctamente desde el certificado de entidad final al intermedio y, por último, al certificado raíz.
  • Verifica que el token esté firmado por el certificado de entidad final contenido en el reclamo x5c.
  • Verifica que el certificado raíz descargado o almacenado sea la misma raíz que en el reclamo 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. Luego, la carga de trabajo de UWear verificará si los reclamos de medición de la carga de trabajo en el token coinciden con las condiciones de los atributos especificadas en la política de OPA. OPA es un motor de políticas de código abierto y de uso general que unifica la aplicación de políticas en toda la pila. OPA usa documentos, con una sintaxis similar a JSON, para establecer valores de referencia con los que se valida la política.
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"
}
  • Ejemplo de consulta de 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
"

Código de ejemplo para obtener el hash de 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 vez que se completen y se aprueben todas esas verificaciones, UWear podrá confirmar que los datos se enviarán y procesarán de forma segura. Luego, UWear responderá con la PHI sensible a través de la misma sesión de TLS, y USleep podrá usar esos datos para calcular la calidad del sueño 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()
  
  ...
}

Pasos para crear la carga de trabajo de USleep

  1. Navega al directorio de secuencias de comandos
cd ~/confidential-space/codelabs/health_data_analysis_codelab/scripts
  1. Ejecuta la secuencia de comandos create_uwear_workload.sh para crear la carga de trabajo de UWear:
  • Crea un Artifact Registry ($UWEAR_ARTIFACT_REPOSITORY) propiedad de UWear en el que se publicará la carga de trabajo.
  • Compila el código uwear/workload.go y lo empaqueta en una imagen de Docker. Consulta la configuración del Dockerfile para USleep.
  • Publica la imagen de Docker en Artifact Registry ($UWEAR_ARTIFACT_REPOSITORY) que pertenece a UWear.
  • Otorga a la cuenta de servicio $UWEAR_WORKLOAD_SERVICE_ACCOUNT permiso de lectura para Artifact Registry ($UWEAR_ARTIFACT_REPOSITORY).
./create_uwear_workload.sh

7. Ejecuta las cargas de trabajo de USleep y UWear

Ejecuta la carga de trabajo de 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 respuesta debe mostrar un STATUS: RUNNING y EXTERNAL_IP también debe mostrarse de forma similar a esta:

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

Almacena la IP externa en una variable

export USLEEP_EXTERNAL_IP=<add your external IP> 

Verifica que la carga de trabajo de USleep se haya ejecutado correctamente

Para verificar que la carga de trabajo de USleep se ejecute correctamente, navega a la página Instancias de VM en el proyecto de USleep. Haz clic en la instancia de "usleep" y presiona "Puerto en serie 1(consola)" en la sección Registros. Una vez que el servidor esté en funcionamiento, en la parte inferior de los registros, estos deberían mostrar algo similar a lo siguiente.

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

Ejecuta la carga de trabajo de 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

Verifica que la carga de trabajo de UWear se haya ejecutado correctamente

Para ver los registros de la carga de trabajo de UWear, navega a la página Instancias de VM en el proyecto de UWear. Haz clic en la instancia "uwear" y presiona "Puerto en serie 1(consola)" en la sección Registros.

El resultado del registro una vez que la instancia se haya iniciado por completo debería verse de la siguiente manera:

En el proyecto UWear, los registros de serie deberían mostrar algo similar a lo siguiente:

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 tu carga de trabajo de UWear no se ve de esta manera, consulta las notas que aparecen a continuación para obtener instrucciones.

Cómo ver los resultados de USleep

Para ver los resultados, vuelve a la página Instancias de VM en el proyecto USleep. Haz clic en la instancia de "usleep" y presiona "Puerto en serie 1(consola)" en la sección Registros. Consulta los resultados de la carga de trabajo en la parte inferior de los registros. Deberían verse similares al siguiente ejemplo.

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

El resultado debería ser "total sleep time is less than 8 hours"..

¡Felicitaciones! Creaste correctamente un espacio confidencial entre UWear y USleep para compartir información sensible.

8. Ejecuta una carga de trabajo no autorizada (opcional)

En la siguiente situación, USleep actualiza el código y ejecuta una carga de trabajo diferente en los datos de sueño que proporciona UWear. UWear no aceptó esta nueva carga de trabajo y no actualizó su política de OPA para permitir el nuevo resumen de imágenes. Verificaremos que UWear no envíe sus datos sensibles a la carga de trabajo no autorizada.

USleep modifica su carga de trabajo

  1. Establece el proyecto en $USLEEP_PROJECT_ID.
gcloud config set project $USLEEP_PROJECT_ID
  1. Borra la instancia de VM de USleep.
gcloud compute instances delete usleep --zone $USLEEP_PROJECT_ZONE
  1. Navega al directorio usleep/workload.go.
cd ~/confidential-space/codelabs/health_data_analysis_codelab/src/usleep
  1. En el archivo usleep/workload.go. Actualiza la línea "audience": "uwear".. En este ejemplo, para cambiar el resumen de la imagen, actualizaremos el público a un valor diferente que UWear no haya aprobado. Por lo tanto, UWear debería rechazarlo por dos motivos: resumen de imagen no aprobado y público incorrecto.
"audience": "anotherCompany.com",
  1. Crea una nueva carga de trabajo de USleep
cd ~/confidential-space/codelabs/health_data_analysis_codelab/scripts

./create_usleep_workload.sh
  1. Crea la nueva instancia de VM de USleep y ejecuta la carga de trabajo
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. Extrae la nueva IP externa de USleep para usarla más adelante
export USLEEP_EXTERNAL_IP=<add your external IP>

Vuelve a ejecutar la carga de trabajo

  1. Borra la instancia de VM de UWear
gcloud config set project $UWEAR_PROJECT_ID

gcloud compute instances delete uwear --zone $UWEAR_PROJECT_ZONE
  1. Vuelve a crear la instancia de VM de UWear con la nueva IP externa
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. En los registros de serie de UWear, debería aparecer el siguiente mensaje, y la VM de USleep no debería recibir ningún dato sensible.
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. Realiza una limpieza

La secuencia de comandos de limpieza se puede usar para limpiar los recursos que creamos como parte de este codelab. Como parte de esta limpieza, se borrarán los siguientes recursos:

  • La cuenta de servicio de UWear ($UWEAR_SERVICE_ACCOUNT).
  • El registro de artefactos de UWear ($UWEAR_ARTIFACT_REPOSITORY).
  • La instancia de procesamiento de UWear
  • La cuenta de servicio de USleep ($USLEEP_SERVICE_ACCOUNT).
  • El registro de artefactos de USleep ($USLEEP_ARTIFACT_REPOSITORY).
  • La instancia de procesamiento de USleep
./cleanup.sh

Si ya terminaste de explorar, considera borrar tu proyecto. Para ello, sigue estas instrucciones.

Felicitaciones

¡Felicitaciones! Completaste el codelab correctamente.

Aprendiste a compartir datos de forma segura y, al mismo tiempo, mantener su confidencialidad con Confidential Space.

¿Qué sigue?

Consulta algunos de estos codelabs similares…

Lecturas adicionales