Si el acceso a tus recursos protegidos no se gestiona mediante la gestión de identidades y accesos de Google Cloud, por ejemplo, si los recursos se almacenan en otro servicio en la nube, en un entorno local o en un dispositivo local, como un teléfono móvil, puedes autenticar una carga de trabajo de Espacio Confidencial en el dispositivo o sistema que proporcione acceso a esos recursos, también conocido como tercero de confianza.
Para ello, la parte verificadora debe solicitar un token de certificación al servicio de certificación de Confidential Space con una audiencia personalizada y nonces opcionales. Cuando solicites un token de certificación como este, debes realizar tu propia validación del token antes de conceder acceso a los recursos.
En la documentación que se incluye a continuación se explican los conceptos relacionados con el uso de Confidential Space con recursos externos a Google Cloud, incluidas las instrucciones para integrar tus cargas de trabajo de Confidential Space con recursos de AWS. Para ver una guía completa, consulta el codelab.
Flujo de tokens de atestación
La carga de trabajo solicita tokens de certificación en nombre de una parte verificadora y el servicio de certificación los devuelve. En función de tus necesidades, puedes definir una audiencia personalizada y, opcionalmente, proporcionar nonces.
Sin encriptar
Para que sea más fácil entender el proceso de obtención de tokens, el flujo que se muestra aquí no usa cifrado. En la práctica, te recomendamos que cifres las comunicaciones con TLS.
En el siguiente diagrama se muestra el flujo:
La parte verificadora envía una solicitud de token a la carga de trabajo, con nonces opcionales que ha generado.
La carga de trabajo determina la audiencia, la añade a la solicitud y envía la solicitud al launcher de Confidential Space.
El launcher envía la solicitud al servicio de certificación.
El servicio de certificación genera un token que contiene la audiencia especificada y nonces opcionales.
El servicio de certificación devuelve el token al launcher.
El lanzador devuelve el token a la carga de trabajo.
La carga de trabajo devuelve el token a la entidad dependiente.
La parte verificadora comprueba las reclamaciones, incluida la audiencia y los nonces opcionales.
Cifrado con TLS
Si el flujo no está cifrado, la solicitud será vulnerable a los ataques de intermediario. Como un nonce no está vinculado a la salida de datos ni a una sesión TLS, un atacante puede interceptar la solicitud y suplantar la carga de trabajo.
Para evitar este tipo de ataque, puedes configurar una sesión TLS entre la parte verificadora y la carga de trabajo, y usar el material de clave exportada (EKM) de TLS como nonce. El material de la clave exportada de TLS vincula la certificación a la sesión de TLS y confirma que la solicitud de certificación se ha enviado a través de un canal seguro. Este proceso también se conoce como vinculación de canales.
En el siguiente diagrama se muestra el flujo mediante el enlace de canal:
La parte verificadora configura una sesión TLS segura con la máquina virtual confidencial que ejecuta la carga de trabajo.
La parte verificadora envía una solicitud de token mediante la sesión TLS segura.
La carga de trabajo determina la audiencia y genera un nonce mediante el material de clave exportado de TLS.
La carga de trabajo envía la solicitud al launcher de Confidential Space.
El launcher envía la solicitud al servicio de certificación.
El servicio de certificación genera un token que contiene la audiencia y el nonce especificados.
El servicio de certificación devuelve el token al launcher.
El lanzador devuelve el token a la carga de trabajo.
La carga de trabajo devuelve el token a la entidad dependiente.
La parte verificadora vuelve a generar el nonce con el material de la clave exportada de TLS.
La parte verificadora comprueba las reclamaciones, incluidas la audiencia y el nonce. El nonce del token debe coincidir con el nonce que regenera la parte verificadora.
Estructura del token de atestación
Los tokens de certificación son tokens web JSON con la siguiente estructura:
Encabezado: describe el algoritmo de firma. Los tokens de PKI también almacenan la cadena de certificados en el encabezado en el campo
x5c
.Carga útil de datos JSON firmados: contiene afirmaciones sobre la carga de trabajo para la parte verificadora, como el asunto, el emisor, la audiencia, los nonces y la hora de vencimiento.
Firma: valida que el token no ha cambiado durante el tránsito. Para obtener más información sobre cómo usar la firma, consulta Cómo validar un token de ID de OpenID Connect.
El siguiente código de muestra es un ejemplo de un token de certificación codificado generado en el espacio confidencial imagen 240500. Las imágenes más recientes pueden contener campos adicionales. Puedes usar https://jwt.io/ para descodificarlo (la firma se ha ocultado).
eyJhbGciOiJIUzI1NiIsImtpZCI6IjEyMzQ1IiwidHlwIjoiSldUIn0.eyJhdWQiOiJBVURJRU5DRV9OQU1FIiwiZGJnc3RhdCI6ImRpc2FibGVkLXNpbmNlLWJvb3QiLCJlYXRfbm9uY2UiOlsiTk9OQ0VfMSIsIk5PTkNFXzIiXSwiZWF0X3Byb2ZpbGUiOiJodHRwczovL2Nsb3VkLmdvb2dsZS5jb20vY29uZmlkZW50aWFsLWNvbXB1dGluZy9jb25maWRlbnRpYWwtc3BhY2UvZG9jcy9yZWZlcmVuY2UvdG9rZW4tY2xhaW1zIiwiZXhwIjoxNzIxMzMwMDc1LCJnb29nbGVfc2VydmljZV9hY2NvdW50cyI6WyJQUk9KRUNUX0lELWNvbXB1dGVAZGV2ZWxvcGVyLmdzZXJ2aWNlYWNjb3VudC5jb20iXSwiaHdtb2RlbCI6IkdDUF9BTURfU0VWIiwiaWF0IjoxNzIxMzI2NDc1LCJpc3MiOiJodHRwczovL2NvbmZpZGVudGlhbGNvbXB1dGluZy5nb29nbGVhcGlzLmNvbSIsIm5iZiI6MTcyMTMyNjQ3NSwib2VtaWQiOjExMTI5LCJzZWNib290Ijp0cnVlLCJzdWIiOiJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9jb21wdXRlL3YxL3Byb2plY3RzL1BST0pFQ1RfSUQvem9uZXMvdXMtY2VudHJhbDEtYS9pbnN0YW5jZXMvSU5TVEFOQ0VfTkFNRSIsInN1Ym1vZHMiOnsiY29uZmlkZW50aWFsX3NwYWNlIjp7Im1vbml0b3JpbmdfZW5hYmxlZCI6eyJtZW1vcnkiOmZhbHNlfSwic3VwcG9ydF9hdHRyaWJ1dGVzIjpbIkxBVEVTVCIsIlNUQUJMRSIsIlVTQUJMRSJdfSwiY29udGFpbmVyIjp7ImFyZ3MiOlsiL2N1c3RvbW5vbmNlIiwiL2RvY2tlci1lbnRyeXBvaW50LnNoIiwibmdpbngiLCItZyIsImRhZW1vbiBvZmY7Il0sImVudiI6eyJIT1NUTkFNRSI6IkhPU1RfTkFNRSIsIk5HSU5YX1ZFUlNJT04iOiIxLjI3LjAiLCJOSlNfUkVMRUFTRSI6IjJ-Ym9va3dvcm0iLCJOSlNfVkVSU0lPTiI6IjAuOC40IiwiUEFUSCI6Ii91c3IvbG9jYWwvc2JpbjovdXNyL2xvY2FsL2JpbjovdXNyL3NiaW46L3Vzci9iaW46L3NiaW46L2JpbiIsIlBLR19SRUxFQVNFIjoiMn5ib29rd29ybSJ9LCJpbWFnZV9kaWdlc3QiOiJzaGEyNTY6Njc2ODJiZGE3NjlmYWUxY2NmNTE4MzE5MmI4ZGFmMzdiNjRjYWU5OWM2YzMzMDI2NTBmNmY4YmY1ZjBmOTVkZiIsImltYWdlX2lkIjoic2hhMjU2OmZmZmZmYzkwZDM0M2NiY2IwMWE1MDMyZWRhYzg2ZGI1OTk4YzUzNmNkMGEzNjY1MTQxMjFhNDVjNjcyMzc2NWMiLCJpbWFnZV9yZWZlcmVuY2UiOiJkb2NrZXIuaW8vbGlicmFyeS9uZ2lueDpsYXRlc3QiLCJpbWFnZV9zaWduYXR1cmVzIjpbeyJrZXlfaWQiOiI8aGV4YWRlY2ltYWwtc2hhMjU2LWZpbmdlcnByaW50LXB1YmxpYy1rZXkxPiIsInNpZ25hdHVyZSI6IjxiYXNlNjQtZW5jb2RlZC1zaWduYXR1cmU-Iiwic2lnbmF0dXJlX2FsZ29yaXRobSI6IlJTQVNTQV9QU1NfU0hBMjU2In0seyJrZXlfaWQiOiI8aGV4YWRlY2ltYWwtc2hhMjU2LWZpbmdlcnByaW50LXB1YmxpYy1rZXkyPiIsInNpZ25hdHVyZSI6IjxiYXNlNjQtZW5jb2RlZC1zaWduYXR1cmU-Iiwic2lnbmF0dXJlX2FsZ29yaXRobSI6IlJTQVNTQV9QU1NfU0hBMjU2In0seyJrZXlfaWQiOiI8aGV4YWRlY2ltYWwtc2hhMjU2LWZpbmdlcnByaW50LXB1YmxpYy1rZXkzPiIsInNpZ25hdHVyZSI6IjxiYXNlNjQtZW5jb2RlZC1zaWduYXR1cmU-Iiwic2lnbmF0dXJlX2FsZ29yaXRobSI6IkVDRFNBX1AyNTZfU0hBMjU2In1dLCJyZXN0YXJ0X3BvbGljeSI6Ik5ldmVyIn0sImdjZSI6eyJpbnN0YW5jZV9pZCI6IklOU1RBTkNFX0lEIiwiaW5zdGFuY2VfbmFtZSI6IklOU1RBTkNFX05BTUUiLCJwcm9qZWN0X2lkIjoiUFJPSkVDVF9JRCIsInByb2plY3RfbnVtYmVyIjoiUFJPSkVDVF9OVU1CRVIiLCJ6b25lIjoidXMtY2VudHJhbDEtYSJ9fSwic3duYW1lIjoiQ09ORklERU5USUFMX1NQQUNFIiwic3d2ZXJzaW9uIjpbIjI0MDUwMCJdfQ.29V71ymnt7LY5Ny6OJFb9AClT4XNLPi0TIcddKDp5pk<SIGNATURE>
Esta es la versión decodificada del ejemplo anterior:
{
"alg": "HS256",
"kid": "12345",
"typ": "JWT"
}.
{
"aud": "AUDIENCE_NAME",
"dbgstat": "disabled-since-boot",
"eat_nonce": [
"NONCE_1",
"NONCE_2"
],
"eat_profile": "https://cloud.google.com/confidential-computing/confidential-space/docs/reference/token-claims",
"exp": 1721330075,
"google_service_accounts": [
"PROJECT_ID-compute@developer.gserviceaccount.com"
],
"hwmodel": "GCP_AMD_SEV",
"iat": 1721326475,
"iss": "https://confidentialcomputing.googleapis.com",
"nbf": 1721326475,
"oemid": 11129,
"secboot": true,
"sub": "https://www.googleapis.com/compute/v1/projects/PROJECT_ID/zones/us-central1-a/instances/INSTANCE_NAME",
"submods": {
"confidential_space": {
"monitoring_enabled": {
"memory": false
},
"support_attributes": [
"LATEST",
"STABLE",
"USABLE"
]
},
"container": {
"args": [
"/customnonce",
"/docker-entrypoint.sh",
"nginx",
"-g",
"daemon off;"
],
"env": {
"HOSTNAME": "HOST_NAME",
"NGINX_VERSION": "1.27.0",
"NJS_RELEASE": "2~bookworm",
"NJS_VERSION": "0.8.4",
"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"PKG_RELEASE": "2~bookworm"
},
"image_digest": "sha256:67682bda769fae1ccf5183192b8daf37b64cae99c6c3302650f6f8bf5f0f95df",
"image_id": "sha256:fffffc90d343cbcb01a5032edac86db5998c536cd0a366514121a45c6723765c",
"image_reference": "docker.io/library/nginx:latest",
"image_signatures": [
{
"key_id": "<hexadecimal-sha256-fingerprint-public-key1>",
"signature": "<base64-encoded-signature>",
"signature_algorithm": "RSASSA_PSS_SHA256"
},
{
"key_id": "<hexadecimal-sha256-fingerprint-public-key2>",
"signature": "<base64-encoded-signature>",
"signature_algorithm": "RSASSA_PSS_SHA256"
},
{
"key_id": "<hexadecimal-sha256-fingerprint-public-key3>",
"signature": "<base64-encoded-signature>",
"signature_algorithm": "ECDSA_P256_SHA256"
}
],
"restart_policy": "Never"
},
"gce": {
"instance_id": "INSTANCE_ID",
"instance_name": "INSTANCE_NAME",
"project_id": "PROJECT_ID",
"project_number": "PROJECT_NUMBER",
"zone": "us-central1-a"
}
},
"swname": "CONFIDENTIAL_SPACE",
"swversion": [
"240500"
]
}
Para obtener una explicación más detallada de los campos del token de certificación, consulta Reclamaciones del token de certificación.
Obtener tokens de atestación
Sigue estos pasos para implementar tokens de certificación en tu entorno de Confidential Space:
Configura un cliente HTTP en tu carga de trabajo.
En tu carga de trabajo, usa el cliente HTTP para hacer una solicitud HTTP a la URL de escucha,
http://localhost/v1/token
, a través de un socket de dominio Unix. El archivo de socket se encuentra en/run/container_launcher/teeserver.sock
.
Cuando se envía una solicitud a la URL de escucha, el launcher de Confidential Space gestiona la recogida de pruebas de certificación, solicita un token de certificación al servicio de certificación (transfiriendo los parámetros personalizados) y, a continuación, devuelve el token generado a la carga de trabajo.
En el siguiente ejemplo de código de Go se muestra cómo comunicarse con el servidor HTTP del launcher a través de IPC.
func getCustomTokenBytes(body 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", "/run/container_launcher/teeserver.sock")
},
},
}
// Get the token from the IPC endpoint
url := "http://localhost/v1/token"
resp, err := httpClient.Post(url, "application/json", strings.NewReader(body))
if err != nil {
return nil, fmt.Errorf("failed to get raw token response: %w", err)
}
tokenbytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read token body: %w", err)
}
fmt.Println(string(tokenbytes))
return tokenbytes, nil
}
Solicitar un token de certificación con una audiencia personalizada
Método HTTP y URL:
POST http://localhost/v1/token
Cuerpo JSON de la solicitud:
{
"audience": "AUDIENCE_NAME",
"token_type": "TOKEN_TYPE",
"nonces": [
"NONCE_1",
"NONCE_2",
...
]
}
Proporciona los siguientes valores:
AUDIENCE_NAME
: obligatorio. El valor de tu audiencia, que es el nombre que le has dado a tu tercero de confianza. Este valor lo establece la carga de trabajo.El valor predeterminado de este campo es
https://sts.google.com
para los tokens sin audiencia personalizada. El valorhttps://sts.google.com
no se puede usar al definir una audiencia personalizada. La longitud máxima es de 512 bytes.Para incluir una audiencia personalizada en un token, la carga de trabajo (no la tercera parte de confianza) debe añadirla a la solicitud de token de certificación antes de enviar la solicitud al servicio de certificación de Confidential Space. De esta forma, se evita que la parte verificadora solicite un token para un recurso protegido al que no debería tener acceso.
TOKEN_TYPE
: obligatorio. El tipo de token que se va a devolver. Elige uno de los siguientes tipos:OIDC
: Estos tokens se validan con una clave pública especificada en el campojwks_uri
del endpoint de validación de tokens de OIDC. La clave pública rota periódicamente.PKI
: estos tokens se validan con un certificado raíz especificado en el camporoot_ca_uri
del endpoint de validación de tokens de PKI. Debes almacenar este certificado por tu cuenta. El certificado rota cada 10 años.
Como se usan certificados con un periodo de validez largo en lugar de claves públicas con un periodo de validez corto para validar los tokens, tus direcciones IP no se exponen a los servidores de Google con tanta frecuencia. Esto significa que los tokens de PKI ofrecen una mayor privacidad que los tokens de OIDC.
Puedes validar la huella digital del certificado con OpenSSL:
openssl x509 -fingerprint -in confidential_space_root.crt
La huella digital debe coincidir 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
NONCE
: opcional. Un valor único, aleatorio y opaco que asegura que un token solo se pueda usar una vez. El valor lo define la parte relevante. Se permiten hasta seis nonces. Cada nonce debe tener entre 10 y 74 bytes, ambos incluidos.Cuando se incluye un nonce, la parte verificadora debe comprobar que los nonces enviados en la solicitud de token de atestación son los mismos que los nonces del token devuelto. Si no coinciden, la parte verificadora debe rechazar el token.
Analizar y validar tokens de certificación
En los siguientes ejemplos de código de Go se muestra cómo validar tokens de certificación.
Tokens de certificación de OIDC
package main
import (
"context"
"crypto/rsa"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"math/big"
"net"
"net/http"
"strings"
"github.com/golang-jwt/jwt/v4"
)
const (
socketPath = "/run/container_launcher/teeserver.sock"
expectedIssuer = "https://confidentialcomputing.googleapis.com"
wellKnownPath = "/.well-known/openid-configuration"
)
type jwksFile struct {
Keys []jwk `json:"keys"`
}
type jwk struct {
N string `json:"n"` // "nMMTBwJ7H6Id8zUCZd-L7uoNyz9b7lvoyse9izD9l2rtOhWLWbiG-7pKeYJyHeEpilHP4KdQMfUo8JCwhd-OMW0be_XtEu3jXEFjuq2YnPSPFk326eTfENtUc6qJohyMnfKkcOcY_kTE11jM81-fsqtBKjO_KiSkcmAO4wJJb8pHOjue3JCP09ZANL1uN4TuxbM2ibcyf25ODt3WQn54SRQTV0wn098Y5VDU-dzyeKYBNfL14iP0LiXBRfHd4YtEaGV9SBUuVhXdhx1eF0efztCNNz0GSLS2AEPLQduVuFoUImP4s51YdO9TPeeQ3hI8aGpOdC0syxmZ7LsL0rHE1Q",
E string `json:"e"` // "AQAB" or 65537 as an int
Kid string `json:"kid"` // "1f12fa916c3a0ef585894b4b420ad17dc9d6cdf5",
// Unused fields:
// Alg string `json:"alg"` // "RS256",
// Kty string `json:"kty"` // "RSA",
// Use string `json:"use"` // "sig",
}
type wellKnown struct {
JwksURI string `json:"jwks_uri"` // "https://www.googleapis.com/service_accounts/v1/metadata/jwk/signer@confidentialspace-sign.iam.gserviceaccount.com"
// Unused fields:
// Iss string `json:"issuer"` // "https://confidentialcomputing.googleapis.com"
// Subject_types_supported string `json:"subject_types_supported"` // [ "public" ]
// Response_types_supported string `json:"response_types_supported"` // [ "id_token" ]
// Claims_supported string `json:"claims_supported"` // [ "sub", "aud", "exp", "iat", "iss", "jti", "nbf", "dbgstat", "eat_nonce", "google_service_accounts", "hwmodel", "oemid", "secboot", "submods", "swname", "swversion" ]
// Id_token_signing_alg_values_supported string `json:"id_token_signing_alg_values_supported"` // [ "RS256" ]
// Scopes_supported string `json:"scopes_supported"` // [ "openid" ]
}
func getWellKnownFile() (wellKnown, error) {
httpClient := http.Client{}
resp, err := httpClient.Get(expectedIssuer + wellKnownPath)
if err != nil {
return wellKnown{}, fmt.Errorf("failed to get raw .well-known response: %w", err)
}
wellKnownJSON, err := io.ReadAll(resp.Body)
if err != nil {
return wellKnown{}, fmt.Errorf("failed to read .well-known response: %w", err)
}
wk := wellKnown{}
json.Unmarshal(wellKnownJSON, &wk)
return wk, nil
}
func getJWKFile() (jwksFile, error) {
wk, err := getWellKnownFile()
if err != nil {
return jwksFile{}, fmt.Errorf("failed to get .well-known json: %w", err)
}
// Get JWK URI from .wellknown
uri := wk.JwksURI
fmt.Printf("jwks URI: %v\n", uri)
httpClient := http.Client{}
resp, err := httpClient.Get(uri)
if err != nil {
return jwksFile{}, fmt.Errorf("failed to get raw JWK response: %w", err)
}
jwkbytes, err := io.ReadAll(resp.Body)
if err != nil {
return jwksFile{}, fmt.Errorf("failed to read JWK body: %w", err)
}
file := jwksFile{}
err = json.Unmarshal(jwkbytes, &file)
if err != nil {
return jwksFile{}, fmt.Errorf("failed to unmarshall JWK content: %w", err)
}
return file, nil
}
// N and E are 'base64urlUInt' encoded: https://www.rfc-editor.org/rfc/rfc7518#section-6.3
func base64urlUIntDecode(s string) (*big.Int, error) {
b, err := base64.RawURLEncoding.DecodeString(s)
if err != nil {
return nil, err
}
z := new(big.Int)
z.SetBytes(b)
return z, nil
}
func getRSAPublicKeyFromJWKsFile(t *jwt.Token) (any, error) {
keysfile, err := getJWKFile()
if err != nil {
return nil, fmt.Errorf("failed to fetch the JWK file: %w", err)
}
// Multiple keys are present in this endpoint to allow for key rotation.
// This method finds the key that was used for signing to pass to the validator.
kid := t.Header["kid"]
for _, key := range keysfile.Keys {
if key.Kid != kid {
continue // Select the key used for signing
}
n, err := base64urlUIntDecode(key.N)
if err != nil {
return nil, fmt.Errorf("failed to decode key.N %w", err)
}
e, err := base64urlUIntDecode(key.E)
if err != nil {
return nil, fmt.Errorf("failed to decode key.E %w", err)
}
// The parser expects an rsa.PublicKey: https://github.com/golang-jwt/jwt/blob/main/rsa.go#L53
// or an array of keys. We chose to show passing a single key in this example as its possible
// not all validators accept multiple keys for validation.
return &rsa.PublicKey{
N: n,
E: int(e.Int64()),
}, nil
}
return nil, fmt.Errorf("failed to find key with kid '%v' from well-known endpoint", kid)
}
func decodeAndValidateToken(tokenBytes []byte, keyFunc func(t *jwt.Token) (any, error)) (*jwt.Token, error) {
var err error
fmt.Println("Unmarshalling token and checking its validity...")
token, err := jwt.NewParser().Parse(string(tokenBytes), keyFunc)
fmt.Printf("Token valid: %v", token.Valid)
if token.Valid {
return token, nil
}
if ve, ok := err.(*jwt.ValidationError); ok {
if ve.Errors&jwt.ValidationErrorMalformed != 0 {
return nil, fmt.Errorf("token format invalid. Please contact the Confidential Space team for assistance")
}
if ve.Errors&(jwt.ValidationErrorNotValidYet) != 0 {
// If device time is not synchronized with the Attestation Service you may need to account for that here.
return nil, errors.New("token is not active yet")
}
if ve.Errors&(jwt.ValidationErrorExpired) != 0 {
return nil, fmt.Errorf("token is expired")
}
return nil, fmt.Errorf("unknown validation error: %v", err)
}
return nil, fmt.Errorf("couldn't handle this token or couldn't read a validation error: %v", err)
}
func main() {
// Get a token from a workload running in Confidential Space
tokenbytes, err := getTokenBytesFromWorkload()
// Write a method to return a public key from the well-known endpoint
keyFunc := getRSAPublicKeyFromJWKsFile
// Verify properties of the original Confidential Space workload that generated the attestation
// using the token claims.
token, err := decodeAndValidateToken(tokenbytes, keyFunc)
if err != nil {
panic(err)
}
claimsString, err := json.MarshalIndent(token.Claims, "", " ")
if err != nil {
panic(err)
}
fmt.Println(string(claimsString))
}
Tokens de certificación de PKI
Para validar el token, la parte verificadora debe completar los siguientes pasos:
Analiza el encabezado del token para obtener la cadena de certificados.
Valida la cadena de certificados con la raíz almacenada. Debes haber descargado previamente el certificado raíz de la URL especificada en el campo
root_ca_uri
devuelto en el endpoint de validación de tokens de PKI.Comprueba la validez del certificado de hoja.
Usa el certificado de hoja para validar la firma del token con el algoritmo especificado en la clave
alg
del encabezado.
Una vez que se haya validado el token, la entidad dependiente podrá analizar las reclamaciones del token.
// This code is an example of how to validate a PKI token. This library is not an official library,
// nor is it endorsed by Google.
// ValidatePKIToken validates the PKI token returned from the attestation service is valid.
// Returns a valid jwt.Token or returns an error if invalid.
func ValidatePKIToken(storedRootCertificate x509.Certificate, attestationToken string) (jwt.Token, error) {
// IMPORTANT: The attestation token should be considered untrusted until the certificate chain and
// the signature is verified.
jwtHeaders, err := ExtractJWTHeaders(attestationToken)
if err != nil {
return jwt.Token{}, fmt.Errorf("ExtractJWTHeaders(token) returned error: %v", err)
}
if jwtHeaders["alg"] != "RS256" {
return jwt.Token{}, fmt.Errorf("ValidatePKIToken(string, *attestpb.Attestation, *v1mainpb.VerifyAttestationRequest) - 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 golangs jwt.Parse function
x5cHeaders := jwtHeaders["x5c"].([]any)
certificates, err := ExtractCertificatesFromX5CHeader(x5cHeaders)
if err != nil {
return jwt.Token{}, fmt.Errorf("ExtractCertificatesFromX5CHeader(x5cHeaders) returned error: %v", 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(storedRootCertificate, *certificates.RootCert)
if err != nil {
return jwt.Token{}, fmt.Errorf("failed to verify certificate chain: %v", err)
}
err = VerifyCertificateChain(certificates)
if err != nil {
return jwt.Token{}, fmt.Errorf("VerifyCertificateChain(string, *attestpb.Attestation, *v1mainpb.VerifyAttestationRequest) - 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
}
// ExtractJWTHeaders parses the JWT and returns the headers.
func ExtractJWTHeaders(token string) (map[string]any, error) {
parser := &jwt.Parser{}
// The claims returned from the token are unverified at this point
// Do not use the claims until the algorithm, certificate chain verification and root certificate
// comparison is successful
unverifiedClaims := &jwt.MapClaims{}
parsedToken, _, err := parser.ParseUnverified(token, unverifiedClaims)
if err != nil {
return nil, fmt.Errorf("Failed to parse claims token: %v", err)
}
return parsedToken.Header, nil
}
// PKICertificates contains the certificates extracted from the x5c header.
type PKICertificates struct {
LeafCert *x509.Certificate
IntermediateCert *x509.Certificate
RootCert *x509.Certificate
}
// ExtractCertificatesFromX5CHeader extracts the certificates from the given x5c header.
func ExtractCertificatesFromX5CHeader(x5cHeaders []any) (PKICertificates, error) {
if x5cHeaders == nil {
return PKICertificates{}, fmt.Errorf("VerifyAttestation(string, *attestpb.Attestation, *v1mainpb.VerifyAttestationRequest) - x5c header not set")
}
x5c := []string{}
for _, header := range x5cHeaders {
x5c = append(x5c, header.(string))
}
// The PKI token x5c header should have 3 certificates - leaf, intermediate and root
if len(x5c) != 3 {
return PKICertificates{}, fmt.Errorf("incorrect number of certificates in x5c header, expected 3 certificates, but got %v", len(x5c))
}
leafCert, err := DecodeAndParseDERCertificate(x5c[0])
if err != nil {
return PKICertificates{}, fmt.Errorf("cannot parse leaf certificate: %v", err)
}
intermediateCert, err := DecodeAndParseDERCertificate(x5c[1])
if err != nil {
return PKICertificates{}, fmt.Errorf("cannot parse intermediate certificate: %v", err)
}
rootCert, err := DecodeAndParseDERCertificate(x5c[2])
if err != nil {
return PKICertificates{}, fmt.Errorf("cannot parse root certificate: %v", err)
}
certificates := PKICertificates{
LeafCert: leafCert,
IntermediateCert: intermediateCert,
RootCert: rootCert,
}
return certificates, nil
}
// DecodeAndParseDERCertificate decodes the given DER certificate string and parses it into an x509 certificate.
func DecodeAndParseDERCertificate(certificate string) (*x509.Certificate, error) {
bytes, _ := base64.StdEncoding.DecodeString(certificate)
cert, err := x509.ParseCertificate(bytes)
if err != nil {
return nil, fmt.Errorf("cannot parse certificate: %v", err)
}
return cert, nil
}
// DecodeAndParsePEMCertificate decodes the given PEM certificate string and parses it into an x509 certificate.
func DecodeAndParsePEMCertificate(certificate string) (*x509.Certificate, error) {
block, _ := pem.Decode([]byte(certificate))
if block == nil {
return nil, fmt.Errorf("cannot decode certificate")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, fmt.Errorf("cannot parse certificate: %v", err)
}
return cert, nil
}
// VerifyCertificateChain verifies the certificate chain from leaf to root.
// It also checks that all certificate lifetimes are valid.
func VerifyCertificateChain(certificates PKICertificates) error {
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
}
func isCertificateLifetimeValid(certificate *x509.Certificate) bool {
currentTime := time.Now()
// check the current time is after the certificate NotBefore time
if !currentTime.After(certificate.NotBefore) {
return false
}
// check the current time is before the certificate NotAfter time
if currentTime.Before(certificate.NotAfter) {
return false
}
return true
}
// CompareCertificates compares two certificate fingerprints.
func CompareCertificates(cert1 x509.Certificate, cert2 x509.Certificate) error {
fingerprint1 := sha256.Sum256(cert1.Raw)
fingerprint2 := sha256.Sum256(cert2.Raw)
if fingerprint1 != fingerprint2 {
return fmt.Errorf("certificate fingerprint mismatch")
}
return nil
}
Integrar recursos de AWS
Puedes integrar tus cargas de trabajo de Confidential Space con recursos de AWS (como claves o datos) mediante etiquetas de principales de AWS. Esta integración usa la certificación segura proporcionada por Confidential Space para conceder acceso granular a tus recursos de AWS.
Reivindicaciones de etiquetas principales de AWS
Google Cloud Attestation genera tokens de identidad verificables que contienen reclamaciones sobre la integridad y la configuración de la carga de trabajo de Confidential Space. Un subconjunto de estas reclamaciones son compatibles con AWS, lo que te permite controlar el acceso a tus recursos de AWS. Estas reclamaciones se incluyen en las reclamaciones https://aws.amazon.com/tags
, en el objeto principal_tags
del token de certificación. Para obtener más información, consulta las reclamaciones de etiquetas principales de AWS.
A continuación se muestra un ejemplo de estructura de reclamación https://aws.amazon.com/tags
:
{
"https://aws.amazon.com/tags": {
"principal_tags": {
"confidential_space.support_attributes": [
"LATEST=STABLE=USABLE"
],
"container.image_digest": [
"sha256:6eccbcf1a1de8bf50aefbb37e8c3600d5b59f4a12cf7d964b6f8ef964b782eb2"
],
"gce.project_id": [
"confidentialcomputing-e2e"
],
"gce.zone": [
"us-west1-a"
],
"hwmodel": [
"GCP_AMD_SEV"
],
"swname": [
"CONFIDENTIAL_SPACE"
],
"swversion": [
"250101"
]
}
}
}
Políticas de AWS con reclamaciones de firma de imágenes de contenedor
Los tokens de AWS también admiten reclamaciones de firma de imágenes de contenedor. Estas reclamaciones son útiles en caso de que las cargas de trabajo cambien con frecuencia o cuando se trate de varios colaboradores o terceros de confianza.
Las reclamaciones de firma de imágenes de contenedor constan de IDs de clave, que están separados por un delimitador. Para incluir estas reclamaciones en el token de AWS, debe proporcionar una lista de permitidos de estos IDs de clave como parámetro adicional en su solicitud de token.
Solo se añaden al token los IDs de clave que coincidan con las claves usadas para firmar tu carga de trabajo. De esta forma, solo se aceptan las firmas autorizadas.
Cuando escribas tu política de AWS, recuerda que los IDs de clave se añaden al token como una sola cadena con caracteres delimitadores. Debe ordenar alfabéticamente la lista de IDs de clave que espera y crear el valor de cadena. Por ejemplo, si tienes los IDs de clave aKey1
, zKey2
y bKey3
, el valor de la reclamación correspondiente en tu política debe ser aKey1=bKey3=zKey2
.
Para admitir varios conjuntos de claves, puedes añadir varios valores a tu política.
"aws:RequestTag/container.signatures.key_ids": [
"aKey1=bKey3=zKey2",
"aKey1=bKey3",
"zKey2"
]
Las reclamaciones de firma de imagen de contenedor (container.signatures.key_ids
) y de digest de imagen de contenedor (container.image_digest
) no aparecerán juntas en un solo token. Si usas container.signatures.key_ids
, asegúrate de eliminar cualquier referencia a container.image_digest
de tus políticas de AWS.
A continuación se muestra un ejemplo de estructura de reclamación https://aws.amazon.com/tags
que contiene container.signatures.key_ids
:
{
"https://aws.amazon.com/tags": {
"principal_tags": {
"confidential_space.support_attributes": [
"LATEST=STABLE=USABLE"
],
"container.signatures.key_ids": [
"keyid1=keyid2=keyid3"
],
"gce.project_id": [
"confidentialcomputing-e2e"
],
"gce.zone": [
"us-west1-a"
],
"hwmodel": [
"GCP_AMD_SEV"
],
"swname": [
"CONFIDENTIAL_SPACE"
],
"swversion": [
"250101"
]
}
}
}
Para obtener una explicación más detallada de los campos del token de certificación, consulta Reclamaciones del token de certificación.
Configurar recursos de AWS: usuario autenticado
Para que la parte de confianza pueda configurar sus recursos de AWS, debe configurar AWS IAM para establecer Confidential Space como proveedor de OIDC federado y crear el rol de AWS IAM necesario.
Configurar la gestión de identidades y accesos de AWS
Para añadir el servicio de certificación de Google Cloud como proveedor de identidades en AWS IAM, haz lo siguiente:
En la consola de AWS, ve a la página Proveedores de identidades.
En Tipo de proveedor, selecciona OpenID Connect.
En URL del proveedor, introduce https://confidentialcomputing.googleapis.com.
En Audiencia, introduce la URL que has registrado con el proveedor de identidades y que envía solicitudes a AWS. Por ejemplo, https://example.com.
Haz clic en Añadir proveedor.
Para crear un rol de gestión de identidades y accesos de AWS para tokens de Confidential Space, sigue estos pasos:
En la consola de AWS, ve a la página Roles.
Haz clic en Crear rol.
En el tipo Entidad de confianza, selecciona Identidad web.
En la sección Identidad web, selecciona el proveedor de identidades y la audiencia según el paso anterior.
Haz clic en Siguiente. Puedes saltarte la edición de la política de AWS en este paso.
Haz clic en Siguiente y añade etiquetas si es necesario.
En Nombre del rol, escribe el nombre del rol.
(Opcional) En Descripción, escribe una descripción del nuevo rol.
Revisa los detalles y haz clic en Crear rol.
Edita la política de AWS del rol que has creado para conceder acceso únicamente a la carga de trabajo que elijas.
Esta política de AWS te permite comprobar reclamaciones específicas en el token, como las siguientes:
Digest de la imagen de contenedor de la carga de trabajo.
La audiencia prevista del token.
Ese
CONFIDENTIAL_SPACE
es el software que se ejecuta en la VM. Para obtener más información, consultaswname
en Reclamaciones de tokens de certificación.Atributo de asistencia de imagen de Confidential Space de producción. Para obtener más información, consulta
confidential_space.support_attributes
.
A continuación, se muestra un ejemplo de una política de AWS que concede acceso a una carga de trabajo con un digest y una audiencia especificados,
CONFIDENTIAL_SPACE
como software que se ejecuta en la instancia de VM ySTABLE
como atributo de asistencia:{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::232510754029:oidc-provider/confidentialcomputing.googleapis.com" }, "Action": [ "sts:AssumeRoleWithWebIdentity", "sts:TagSession" ], "Condition": { "StringEquals": { "confidentialcomputing.googleapis.com:aud": "https://integration.test", "aws:RequestTag/swname": "CONFIDENTIAL_SPACE", "aws:RequestTag/container.image_digest": "sha256:ac74cbeca443e36325bad15a7c28f2598b22966aa94681a444553f0b838717cf" }, "StringLike": { "aws:RequestTag/confidential_space.support_attributes": "*STABLE*" } } } ] }
Configurar recursos de AWS
Una vez que hayas completado la integración, configura tus recursos de AWS. Este paso depende de tu caso práctico específico. Por ejemplo, puede crear un segmento de S3, una clave de KMS u otros recursos de AWS. Concede al rol de gestión de identidades y accesos de AWS que has creado anteriormente los permisos necesarios para acceder a estos recursos.
Configurar la carga de trabajo de Confidential Space: autor de la carga de trabajo
Para crear solicitudes de tokens, sigue las instrucciones que se indican en el artículo sobre cómo solicitar un token de certificación con una audiencia personalizada.
En el caso de las reclamaciones de AWS_PrincipalTag
:
El campo nonce es opcional en la solicitud de token para la integración con AWS.
Incluya la audiencia que ha configurado en Configurar recursos de AWS: usuario autenticado.
Asigna el valor
AWS_PRINCIPALTAGS
a token_type.
A continuación se muestra un ejemplo de cuerpo de solicitud de reclamación AWS_PrincipalTag
:
body := `{
"audience": "https://example.com",
"token_type": "AWS_PRINCIPALTAGS",
}`
Siguientes pasos
Consulta Reivindicaciones de tokens de atestación para obtener más información sobre las reivindicaciones de tokens de atestación.