Si el acceso a tus recursos protegidos no está administrado por Google Cloud's IAM—por ejemplo, los recursos se almacenan en otro servicio en la nube, en un entorno local o en un dispositivo local, como un teléfono celular—puedes autenticar una carga de trabajo de Confidential Space en el dispositivo o sistema que proporciona acceso a esos recursos, también conocido como entidad emisora.
Para ello, la entidad emisora debe recibir un token de certificación de un servicio de certificación, como Google Cloud Attestation, con un público personalizado y nonces opcionales. Cuando se solicita un token de certificación como este, es posible que la entidad emisora deba realizar su propia validación de token antes de otorgar acceso a los recursos.
En la documentación que sigue, se abarcan los conceptos relacionados con el uso de Confidential Space con recursos externos Google Cloud, incluidas las instrucciones para integrar tus cargas de trabajo de Confidential Space con recursos de AWS. Para obtener un instructivo de extremo a extremo, consulta el codelab.
Flujo de token de certificación
La carga de trabajo solicita los tokens de certificación en nombre de una entidad emisora, y un servicio de certificación los devuelve. Según tus necesidades, puedes definir un público personalizado y, de manera opcional, proporcionar nonces.
Sin encriptación
Para facilitar la comprensión del proceso de recuperación de tokens, el flujo que se presenta aquí no usa encriptación. En la práctica, te recomendamos que encriptes las comunicaciones con TLS.
En el siguiente diagrama, se muestra el flujo:
La entidad emisora envía una solicitud de token a la carga de trabajo, con nonces opcionales que generó.
La carga de trabajo determina el público, lo agrega a la solicitud y la envía al iniciador de Confidential Space.
El iniciador envía la solicitud al servicio de certificación.
El servicio de certificación genera un token que contiene el público especificado y nonces opcionales.
El servicio de certificación devuelve el token al iniciador.
El iniciador devuelve el token a la carga de trabajo.
La carga de trabajo devuelve el token a la entidad emisora.
La entidad emisora verifica las reclamaciones, incluido el público y los nonces opcionales.
Encriptado con TLS
Un flujo sin encriptar deja la solicitud vulnerable a ataques de intermediario. Debido a que un nonce no está vinculado a la salida de datos ni a una sesión de TLS, un atacante puede interceptar la solicitud y suplantar la carga de trabajo.
Para evitar este tipo de ataque, puedes configurar una sesión de TLS entre la entidad emisora y la carga de trabajo, y usar el material de claves exportado (EKM) de TLS como nonce. El material de claves exportado de TLS vincula la certificación a la sesión de TLS y confirma que la solicitud de certificación se envió 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 con la vinculación de canales:
La entidad emisora configura una sesión de TLS segura con la Confidential VM que ejecuta la carga de trabajo.
La entidad emisora envía una solicitud de token con la sesión de TLS segura.
La carga de trabajo determina el público y genera un nonce con el material de claves exportado de TLS.
La carga de trabajo envía la solicitud al iniciador de Confidential Space.
El iniciador envía la solicitud al servicio de certificación.
El servicio de certificación genera un token que contiene el público y el nonce especificados.
El servicio de certificación devuelve el token al iniciador.
El iniciador devuelve el token a la carga de trabajo.
La carga de trabajo devuelve el token a la entidad emisora.
La entidad emisora vuelve a generar el nonce con el material de claves exportado de TLS.
La entidad emisora verifica las reclamaciones, incluido el público y el nonce. El nonce del token debe coincidir con el que vuelve a generar la entidad emisora.
Estructura del token de certificació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 firmada: Contiene reclamaciones sobre la carga de trabajo para la entidad emisora, como el asunto, la entidad emisora, el público, los nonces y la hora de vencimiento.
Firma: Proporciona validación de que el token no cambió durante el tránsito. Para obtener más información sobre el uso de la firma, consulta Cómo validar un token de ID de OpenID Connect.
En el siguiente muestra de código, se muestra un token de certificación codificado con la firma quitada. Puedes usar https://jwt.io/ para decodificar el token.
El token se generó con la imagen de Confidential Space 240500 y Google Cloud Attestation. Es posible que las imágenes más recientes contengan campos adicionales.
eyJhbGciOiJIUzI1NiIsImtpZCI6IjEyMzQ1IiwidHlwIjoiSldUIn0.eyJhdWQiOiJBVURJRU5DRV9OQU1FIiwiZGJnc3RhdCI6ImRpc2FibGVkLXNpbmNlLWJvb3QiLCJlYXRfbm9uY2UiOlsiTk9OQ0VfMSIsIk5PTkNFXzIiXSwiZWF0X3Byb2ZpbGUiOiJodHRwczovL2Nsb3VkLmdvb2dsZS5jb20vY29uZmlkZW50aWFsLWNvbXB1dGluZy9jb25maWRlbnRpYWwtc3BhY2UvZG9jcy9yZWZlcmVuY2UvdG9rZW4tY2xhaW1zIiwiZXhwIjoxNzIxMzMwMDc1LCJnb29nbGVfc2VydmljZV9hY2NvdW50cyI6WyJQUk9KRUNUX0lELWNvbXB1dGVAZGV2ZWxvcGVyLmdzZXJ2aWNlYWNjb3VudC5jb20iXSwiaHdtb2RlbCI6IkdDUF9BTURfU0VWIiwiaWF0IjoxNzIxMzI2NDc1LCJpc3MiOiJodHRwczovL2NvbmZpZGVudGlhbGNvbXB1dGluZy5nb29nbGVhcGlzLmNvbSIsIm5iZiI6MTcyMTMyNjQ3NSwib2VtaWQiOjExMTI5LCJzZWNib290Ijp0cnVlLCJzdWIiOiJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9jb21wdXRlL3YxL3Byb2plY3RzL1BST0pFQ1RfSUQvem9uZXMvdXMtY2VudHJhbDEtYS9pbnN0YW5jZXMvSU5TVEFOQ0VfTkFNRSIsInN1Ym1vZHMiOnsiY29uZmlkZW50aWFsX3NwYWNlIjp7Im1vbml0b3JpbmdfZW5hYmxlZCI6eyJtZW1vcnkiOmZhbHNlfSwic3VwcG9ydF9hdHRyaWJ1dGVzIjpbIkxBVEVTVCIsIlNUQUJMRSIsIlVTQUJMRSJdfSwiY29udGFpbmVyIjp7ImFyZ3MiOlsiL2N1c3RvbW5vbmNlIiwiL2RvY2tlci1lbnRyeXBvaW50LnNoIiwibmdpbngiLCItZyIsImRhZW1vbiBvZmY7Il0sImVudiI6eyJIT1NUTkFNRSI6IkhPU1RfTkFNRSIsIk5HSU5YX1ZFUlNJT04iOiIxLjI3LjAiLCJOSlNfUkVMRUFTRSI6IjJ-Ym9va3dvcm0iLCJOSlNfVkVSU0lPTiI6IjAuOC40IiwiUEFUSCI6Ii91c3IvbG9jYWwvc2JpbjovdXNyL2xvY2FsL2JpbjovdXNyL3NiaW46L3Vzci9iaW46L3NiaW46L2JpbiIsIlBLR19SRUxFQVNFIjoiMn5ib29rd29ybSJ9LCJpbWFnZV9kaWdlc3QiOiJzaGEyNTY6Njc2ODJiZGE3NjlmYWUxY2NmNTE4MzE5MmI4ZGFmMzdiNjRjYWU5OWM2YzMzMDI2NTBmNmY4YmY1ZjBmOTVkZiIsImltYWdlX2lkIjoic2hhMjU2OmZmZmZmYzkwZDM0M2NiY2IwMWE1MDMyZWRhYzg2ZGI1OTk4YzUzNmNkMGEzNjY1MTQxMjFhNDVjNjcyMzc2NWMiLCJpbWFnZV9yZWZlcmVuY2UiOiJkb2NrZXIuaW8vbGlicmFyeS9uZ2lueDpsYXRlc3QiLCJpbWFnZV9zaWduYXR1cmVzIjpbeyJrZXlfaWQiOiI8aGV4YWRlY2ltYWwtc2hhMjU2LWZpbmdlcnByaW50LXB1YmxpYy1rZXkxPiIsInNpZ25hdHVyZSI6IjxiYXNlNjQtZW5jb2RlZC1zaWduYXR1cmU-Iiwic2lnbmF0dXJlX2FsZ29yaXRobSI6IlJTQVNTQV9QU1NfU0hBMjU2In0seyJrZXlfaWQiOiI8aGV4YWRlY2ltYWwtc2hhMjU2LWZpbmdlcnByaW50LXB1YmxpYy1rZXkyPiIsInNpZ25hdHVyZSI6IjxiYXNlNjQtZW5jb2RlZC1zaWduYXR1cmU-Iiwic2lnbmF0dXJlX2FsZ29yaXRobSI6IlJTQVNTQV9QU1NfU0hBMjU2In0seyJrZXlfaWQiOiI8aGV4YWRlY2ltYWwtc2hhMjU2LWZpbmdlcnByaW50LXB1YmxpYy1rZXkzPiIsInNpZ25hdHVyZSI6IjxiYXNlNjQtZW5jb2RlZC1zaWduYXR1cmU-Iiwic2lnbmF0dXJlX2FsZ29yaXRobSI6IkVDRFNBX1AyNTZfU0hBMjU2In1dLCJyZXN0YXJ0X3BvbGljeSI6Ik5ldmVyIn0sImdjZSI6eyJpbnN0YW5jZV9pZCI6IklOU1RBTkNFX0lEIiwiaW5zdGFuY2VfbmFtZSI6IklOU1RBTkNFX05BTUUiLCJwcm9qZWN0X2lkIjoiUFJPSkVDVF9JRCIsInByb2plY3RfbnVtYmVyIjoiUFJPSkVDVF9OVU1CRVIiLCJ6b25lIjoidXMtY2VudHJhbDEtYSJ9fSwic3duYW1lIjoiQ09ORklERU5USUFMX1NQQUNFIiwic3d2ZXJzaW9uIjpbIjI0MDUwMCJdfQ.29V71ymnt7LY5Ny6OJFb9AClT4XNLPi0TIcddKDp5pk
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 de token de certificación, consulta Reclamaciones de token de certificación.
Recupera tokens de certificación
Completa los siguientes 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 realizar una solicitud HTTP a la URL de escucha, a través de un socket de dominio de Unix ubicado en
/run/container_launcher/teeserver.sock. La URL de escucha cambia según el servicio de certificación que uses:Google Cloud Attestation:
http://localhost/v1/tokenIntel Trust Authority:
http://localhost/v1/intel/token
Cuando se realiza una solicitud a la URL de escucha, el iniciador de Confidential Space administra la recopilación de pruebas de certificación, solicita un token de certificación de un servicio de certificación (y pasa los parámetros personalizados) y, luego, devuelve el token generado a la carga de trabajo.
En el siguiente muestra de código en Go, se muestra cómo comunicarse con el servidor HTTP del iniciador a través de IPC, según el servicio de certificación que uses.
Google Cloud Attestation
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
}
Intel Trust Authority
Intel Trust Authority solo admite instancias de VM que ejecutan Intel TDX.
type tokenRequest struct {
Audience string `json:"audience"`
Nonces []string `json:"nonces"`
TokenType string `json:"token_type"`
TokenTypeOptions tokenTypeOptions `json:"aws_principal_tag_options"`
}
func getITAToken(body string) (string, 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"
)
},
},
}
contentType := "application/json"
itaTokenEndpoint := "http://localhost/v1/intel/token"
resp, err := httpClient.Post(itaTokenEndpoint, contentType, strings.NewReader(body))
if err != nil { ... }
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf(...)
}
tokenbytes, err := io.ReadAll(resp.Body)
if err != nil { ... }
return string(tokenbytes), nil
}
func main() {
// Get token from the Confidential Space launcher
body := tokenRequest{
Audience: "http://test.audience",
TokenType: "OIDC", // Set this to AWS_PRINCIPAL_TAGS for AWS tokens.
}
val, err := json.Marshal(body)
if err != nil {...}
token, err := getITAToken(string(val))
if err != nil {...}
fmt.Printf("Token received: %v", token)
// Use your ITA token to gain access to resources on AWS or Google Cloud
}
Solicita un token de certificación con un público personalizado
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",
...
]
}
Ingresa los siguientes valores:
AUDIENCE_NAME: Obligatorio. El valor de tu público, que es el nombre que le asignaste a tu entidad emisora. La carga de trabajo lo establece.Este campo tiene el valor predeterminado
https://sts.google.compara los tokens sin un público personalizado. El valorhttps://sts.google.comno se puede usar cuando se configura un público personalizado. La longitud máxima es de 512 bytes.Para incluir un público personalizado en un token, la carga de trabajo, no la entidad emisora, debe agregarlo a la solicitud de token de certificación antes de enviar la solicitud a un servicio de certificación. Esto ayuda a evitar que la entidad emisora solicite un token para un recurso protegido al que no debería tener acceso.
TOKEN_TYPE: Obligatorio. El tipo de token que se mostrará. Elige uno de los siguientes tipos:OIDC: Estos tokens se validan con una clave pública que rota con regularidad y que se especifica en el campojwks_urien el extremo de validación de tokens de OIDC. El extremo se construye agregando.well-known/openid-configurational URI de la entidad emisora.Google Cloud Attestation:
https://confidentialcomputing.googleapis.com/.well-known/openid-configuration.Intel Trust Authority:
https://portal.trustauthority.intel.com/.well-known/openid-configuration.
PKI: Estos tokens se validan con un certificado raíz especificado en el camporoot_ca_urien el extremo de validación de tokens de PKI. Debes almacenar este certificado por tu cuenta. El certificado rota cada 10 años.
Debido a que se usan certificados de vencimiento prolongado en lugar de claves públicas de vencimiento corto para la validación de tokens, tus direcciones IP no se exponen a los servidores de Google con tanta frecuencia. Esto significa que los tokens de PKI ofrecen mayor privacidad que los tokens de OIDC.
Puedes validar la huella digital del certificado con OpenSSL:
openssl x509 -fingerprint -in confidential_space_root.crtLa 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:21NONCE: Opcional Un valor único, aleatorio y opaco que verifica que un token solo se pueda usar una vez. La entidad emisora establece el valor. Se permiten hasta seis nonces. Cada nonce debe tener entre 10 y 74 bytes, inclusive.Cuando se incluye un nonce, la entidad emisora debe verificar que los nonces enviados en la solicitud de token de certificación sean los mismos que los del token devuelto. Si son diferentes, la entidad emisora debe rechazar el token.
Analiza y valida tokens de certificación
En los siguientes ejemplos de código en 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 entidad emisora 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
root_ca_uricampo que se muestra en el extremo de validación de tokens de PKI.Verifica la validez del certificado de hoja.
Usa el certificado de hoja para validar la firma del token con el algoritmo especificado en la clave
algdel encabezado.
Después de validar el token, la entidad emisora puede 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 that the PKI token returned from an 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
}
Integra recursos de AWS
Puedes integrar tus cargas de trabajo de Confidential Space con recursos de AWS (como claves o datos) mediante etiquetas principales de AWS. Esta integración usa la certificación segura que proporciona Confidential Space para otorgar acceso detallado a tus recursos de AWS.
Reclamaciones 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 colocan 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
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 imagen de contenedor
Los tokens de AWS también admiten reclamaciones de firma de imagen de contenedor. Estas reclamaciones son adecuadas para cambios de carga de trabajo de alta frecuencia y para tratar con varios colaboradores o entidades emisoras.
Las reclamaciones de firma de imagen de contenedor constan de IDs de clave, que están separados por un delimitador. Para incluir estas reclamaciones en el token de AWS, debes proporcionar una lista de entidades permitidas de estos IDs de clave como parámetro adicional en tu solicitud de token.
Solo los IDs de clave que coinciden con las claves que se usan para firmar tu carga de trabajo se agregan al token. Este proceso ayuda a garantizar que solo se acepten las firmas autorizadas.
Cuando escribas tu política de AWS, recuerda que los IDs de clave se agregan al token como una sola cadena con caracteres delimitadores. Debes ordenar alfabéticamente la lista de IDs de clave que esperas y construir el valor de cadena. Por ejemplo, si tienes IDs de clave aKey1, zKey2, y bKey3, el valor de reclamación correspondiente en tu política debe ser aKey1=bKey3=zKey2.
Para admitir varios conjuntos de claves, puedes agregar varios valores a tu política de manera opcional.
"aws:RequestTag/container.signatures.key_ids": [
"aKey1=bKey3=zKey2",
"aKey1=bKey3",
"zKey2"
]
La reclamación de firmas de imagen de contenedor (container.signatures.key_ids) y la reclamación de resumen de imagen de contenedor (container.image_digest) no aparecen juntas en un solo token. Si usas container.signatures.key_ids, quita todas las referencias 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 de token de certificación, consulta Reclamaciones de token de certificación.
Configura recursos de AWS: entidad emisora
Antes de que la entidad emisora pueda configurar sus recursos de AWS, debe configurar AWS IAM para establecer Confidential Space como un proveedor de OIDC federado y crear el rol de IAM de AWS necesario.
Configura AWS IAM
Para agregar el servicio de Google Cloud Attestation como proveedor de identidad en AWS IAM, haz lo siguiente:
En la consola de AWS, ve a la página Proveedores de identidad.
En Tipo de proveedor, selecciona OpenID Connect.
En URL del proveedor, ingresa https://confidentialcomputing.googleapis.com.
En Público, ingresa la URL que registraste con el proveedor de identidad y que realiza solicitudes a AWS. Por ejemplo, https://example.com.
Haz clic en Agregar proveedor.
Para crear un rol de IAM de AWS para tokens de Confidential Space, haz lo siguiente:
En la consola de AWS, ve a la página Roles.
Haz clic en Crear rol.
En el tipo de Entidad de confianza, selecciona Identidad web.
En la sección Identidad web, selecciona el proveedor de identidad y el público según el paso anterior.
Haz clic en Siguiente. Puedes omitir la edición de la política de AWS en este paso.
Haz clic en Siguiente y agrega etiquetas si es necesario.
En Nombre del rol, ingresa el nombre del rol.
Opcional: En Descripción, ingresa una descripción para el rol nuevo.
Revisa los detalles y, luego, haz clic en Crear rol.
Edita la política de AWS del rol que creaste para otorgar acceso solo a la carga de trabajo que elijas.
Esta política de AWS te permite verificar reclamaciones específicas en el token, como las siguientes:
El resumen de la imagen de contenedor de la carga de trabajo
El público objetivo del token
Que
CONFIDENTIAL_SPACEes el software que se ejecuta en la VM Para obtener más información, consultaswnameen Reclamaciones de token de certificación.El atributo de asistencia de la 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 política de AWS que otorga acceso a una carga de trabajo con un resumen y un público especificados,
CONFIDENTIAL_SPACEcomo el software que se ejecuta en la instancia de VM ySTABLEcomo el 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*" } } } ] }
Configura tus recursos de AWS
Una vez que se complete la integración, configura tus recursos de AWS. Este paso depende de tu caso de uso específico. Por ejemplo, puedes crear un bucket de S3, una clave de KMS, o cualquier otro recurso de AWS. Asegúrate de otorgar al rol de IAM de AWS que creaste anteriormente los permisos necesarios para acceder a estos recursos.
Configura tu carga de trabajo de Confidential Space: autor de la carga de trabajo
Para crear solicitudes de token, sigue las instrucciones que se indican en Solicita un token de certificación con un público personalizado.
Para las reclamaciones de AWS_PrincipalTag:
Un campo nonce es opcional en tu solicitud de token para la integración de AWS.
Incluye el público que configuraste en Configura recursos de AWS: entidad emisora.
Establece token_type en
AWS_PRINCIPALTAGS.
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",
}`
¿Qué sigue?
Consulta Reclamaciones de token de certificación para obtener más información sobre las reclamaciones de token de certificación.