Si l'accès à vos ressources protégées n'est pas géré par l'IAM de Google Cloud(par exemple, les ressources sont stockées dans un autre service cloud, sur site ou sur un appareil local tel qu'un téléphone mobile), vous pouvez toujours authentifier une charge de travail Confidential Space sur l'appareil qui fournit l'accès à ces ressources, également appelé partie de confiance.
Pour ce faire, la partie de confiance doit demander un jeton d'attestation au service d'attestation Confidential Space avec une audience personnalisée et des nonces facultatifs. Lorsque vous demandez un jeton d'attestation comme celui-ci, vous devez effectuer votre propre validation de jeton avant d'accorder l'accès aux ressources.
La documentation suivante couvre les concepts liés à l'utilisation de Confidential Space avec des ressources en dehors de Google Cloud. Pour une procédure pas à pas, consultez l'atelier de programmation.
Flux de jeton d'attestation
Les jetons d'attestation sont demandés par la charge de travail au nom d'une partie de confiance et renvoyés par le service d'attestation. Selon vos besoins, vous pouvez définir une audience personnalisée et fournir éventuellement des nonces.
Non chiffré
Pour faciliter la compréhension du processus de récupération du jeton, le flux présenté ici n'utilise pas de chiffrement. En pratique, nous vous recommandons de chiffrer les communications avec TLS.
Le schéma suivant illustre le flux:
La partie de confiance envoie une requête de jeton à la charge de travail, avec des nonces facultatifs qu'elle a générés.
La charge de travail détermine l'audience, l'ajoute à la requête et envoie la requête au lanceur d'espace confidentiel.
Le lanceur d'applications envoie la requête au service d'attestation.
Le service d'attestation génère un jeton contenant l'audience spécifiée et des nonces facultatifs.
Le service d'attestation renvoie le jeton au lanceur d'applications.
Le lanceur renvoie le jeton à la charge de travail.
La charge de travail renvoie le jeton à la partie de confiance.
La partie de confiance vérifie les revendications, y compris l'audience et les nonces facultatifs.
Chiffré avec TLS
Un flux non chiffré rend la requête vulnérable aux attaques de l'intercepteur. Étant donné qu'un nonce n'est pas lié à la sortie de données ni à une session TLS, un pirate informatique peut intercepter la requête et usurper l'identité de la charge de travail.
Pour éviter ce type d'attaque, vous pouvez configurer une session TLS entre la partie de confiance et la charge de travail, et utiliser le matériel de clé exporté (EKM) TLS comme nonce. Le matériel de clé TLS exporté lie l'attestation à la session TLS et confirme que la requête d'attestation a été envoyée via un canal sécurisé. Ce processus est également appelé association de canaux.
Le schéma suivant illustre le flux utilisant la liaison de canaux:
La partie de confiance configure une session TLS sécurisée avec la VM de Confidential VMs qui exécute la charge de travail.
La partie de confiance envoie une requête de jeton à l'aide de la session TLS sécurisée.
La charge de travail détermine l'audience et génère un nonce à l'aide du matériel de clé TLS exporté.
La charge de travail envoie la requête au lanceur d'espace confidentiel.
Le lanceur d'applications envoie la requête au service d'attestation.
Le service d'attestation génère un jeton contenant l'audience et le nonce spécifiés.
Le service d'attestation renvoie le jeton au lanceur d'applications.
Le lanceur renvoie le jeton à la charge de travail.
La charge de travail renvoie le jeton à la partie de confiance.
La partie de confiance génère à nouveau le nonce à l'aide du matériel de clé TLS exporté.
La partie de confiance vérifie les revendications, y compris l'audience et le nonce. Le nonce du jeton doit correspondre au nonce régénéré par le tiers de confiance.
Structure du jeton d'attestation
Les jetons d'attestation sont des jetons Web JSON dont la structure est la suivante:
En-tête: décrit l'algorithme de signature. Les jetons PKI stockent également la chaîne de certificats dans l'en-tête, dans le champ
x5c
.Charge utile de données JSON signée: contient des revendications sur la charge de travail pour la partie de confiance, telles que l'objet, l'émetteur, l'audience, les nonces et la date d'expiration.
Signature: permet de vérifier que le jeton n'a pas été modifié pendant le transfert. Pour en savoir plus sur l'utilisation de la signature, consultez la section Valider un jeton d'identification OpenID Connect.
L'exemple de code suivant est un exemple de jeton d'attestation encodé généré dans l'image 240500 de l'espace confidentiel. Les images plus récentes peuvent contenir des champs supplémentaires. Vous pouvez utiliser https://jwt.io/ pour le décoder (la signature est masquée).
eyJhbGciOiJIUzI1NiIsImtpZCI6IjEyMzQ1IiwidHlwIjoiSldUIn0.eyJhdWQiOiJBVURJRU5DRV9OQU1FIiwiZGJnc3RhdCI6ImRpc2FibGVkLXNpbmNlLWJvb3QiLCJlYXRfbm9uY2UiOlsiTk9OQ0VfMSIsIk5PTkNFXzIiXSwiZWF0X3Byb2ZpbGUiOiJodHRwczovL2Nsb3VkLmdvb2dsZS5jb20vY29uZmlkZW50aWFsLWNvbXB1dGluZy9jb25maWRlbnRpYWwtc3BhY2UvZG9jcy9yZWZlcmVuY2UvdG9rZW4tY2xhaW1zIiwiZXhwIjoxNzIxMzMwMDc1LCJnb29nbGVfc2VydmljZV9hY2NvdW50cyI6WyJQUk9KRUNUX0lELWNvbXB1dGVAZGV2ZWxvcGVyLmdzZXJ2aWNlYWNjb3VudC5jb20iXSwiaHdtb2RlbCI6IkdDUF9BTURfU0VWIiwiaWF0IjoxNzIxMzI2NDc1LCJpc3MiOiJodHRwczovL2NvbmZpZGVudGlhbGNvbXB1dGluZy5nb29nbGVhcGlzLmNvbSIsIm5iZiI6MTcyMTMyNjQ3NSwib2VtaWQiOjExMTI5LCJzZWNib290Ijp0cnVlLCJzdWIiOiJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9jb21wdXRlL3YxL3Byb2plY3RzL1BST0pFQ1RfSUQvem9uZXMvdXMtY2VudHJhbDEtYS9pbnN0YW5jZXMvSU5TVEFOQ0VfTkFNRSIsInN1Ym1vZHMiOnsiY29uZmlkZW50aWFsX3NwYWNlIjp7Im1vbml0b3JpbmdfZW5hYmxlZCI6eyJtZW1vcnkiOmZhbHNlfSwic3VwcG9ydF9hdHRyaWJ1dGVzIjpbIkxBVEVTVCIsIlNUQUJMRSIsIlVTQUJMRSJdfSwiY29udGFpbmVyIjp7ImFyZ3MiOlsiL2N1c3RvbW5vbmNlIiwiL2RvY2tlci1lbnRyeXBvaW50LnNoIiwibmdpbngiLCItZyIsImRhZW1vbiBvZmY7Il0sImVudiI6eyJIT1NUTkFNRSI6IkhPU1RfTkFNRSIsIk5HSU5YX1ZFUlNJT04iOiIxLjI3LjAiLCJOSlNfUkVMRUFTRSI6IjJ-Ym9va3dvcm0iLCJOSlNfVkVSU0lPTiI6IjAuOC40IiwiUEFUSCI6Ii91c3IvbG9jYWwvc2JpbjovdXNyL2xvY2FsL2JpbjovdXNyL3NiaW46L3Vzci9iaW46L3NiaW46L2JpbiIsIlBLR19SRUxFQVNFIjoiMn5ib29rd29ybSJ9LCJpbWFnZV9kaWdlc3QiOiJzaGEyNTY6Njc2ODJiZGE3NjlmYWUxY2NmNTE4MzE5MmI4ZGFmMzdiNjRjYWU5OWM2YzMzMDI2NTBmNmY4YmY1ZjBmOTVkZiIsImltYWdlX2lkIjoic2hhMjU2OmZmZmZmYzkwZDM0M2NiY2IwMWE1MDMyZWRhYzg2ZGI1OTk4YzUzNmNkMGEzNjY1MTQxMjFhNDVjNjcyMzc2NWMiLCJpbWFnZV9yZWZlcmVuY2UiOiJkb2NrZXIuaW8vbGlicmFyeS9uZ2lueDpsYXRlc3QiLCJpbWFnZV9zaWduYXR1cmVzIjpbeyJrZXlfaWQiOiI8aGV4YWRlY2ltYWwtc2hhMjU2LWZpbmdlcnByaW50LXB1YmxpYy1rZXkxPiIsInNpZ25hdHVyZSI6IjxiYXNlNjQtZW5jb2RlZC1zaWduYXR1cmU-Iiwic2lnbmF0dXJlX2FsZ29yaXRobSI6IlJTQVNTQV9QU1NfU0hBMjU2In0seyJrZXlfaWQiOiI8aGV4YWRlY2ltYWwtc2hhMjU2LWZpbmdlcnByaW50LXB1YmxpYy1rZXkyPiIsInNpZ25hdHVyZSI6IjxiYXNlNjQtZW5jb2RlZC1zaWduYXR1cmU-Iiwic2lnbmF0dXJlX2FsZ29yaXRobSI6IlJTQVNTQV9QU1NfU0hBMjU2In0seyJrZXlfaWQiOiI8aGV4YWRlY2ltYWwtc2hhMjU2LWZpbmdlcnByaW50LXB1YmxpYy1rZXkzPiIsInNpZ25hdHVyZSI6IjxiYXNlNjQtZW5jb2RlZC1zaWduYXR1cmU-Iiwic2lnbmF0dXJlX2FsZ29yaXRobSI6IkVDRFNBX1AyNTZfU0hBMjU2In1dLCJyZXN0YXJ0X3BvbGljeSI6Ik5ldmVyIn0sImdjZSI6eyJpbnN0YW5jZV9pZCI6IklOU1RBTkNFX0lEIiwiaW5zdGFuY2VfbmFtZSI6IklOU1RBTkNFX05BTUUiLCJwcm9qZWN0X2lkIjoiUFJPSkVDVF9JRCIsInByb2plY3RfbnVtYmVyIjoiUFJPSkVDVF9OVU1CRVIiLCJ6b25lIjoidXMtY2VudHJhbDEtYSJ9fSwic3duYW1lIjoiQ09ORklERU5USUFMX1NQQUNFIiwic3d2ZXJzaW9uIjpbIjI0MDUwMCJdfQ.29V71ymnt7LY5Ny6OJFb9AClT4XNLPi0TIcddKDp5pk<SIGNATURE>
Voici la version décodée de l'exemple précédent:
{
"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"
]
}
Pour une explication plus détaillée des champs de jeton d'attestation, consultez la section Attestations de jeton d'attestation.
Récupérer des jetons d'attestation
Pour implémenter des jetons d'attestation dans votre environnement Confidential Space, procédez comme suit:
Configurez un client HTTP dans votre charge de travail.
Dans votre charge de travail, utilisez le client HTTP pour envoyer une requête HTTP à l'URL d'écoute,
http://localhost/v1/token
, via un socket de domaine Unix. Le fichier de socket se trouve à l'emplacement/run/container_launcher/teeserver.sock
.
Lorsqu'une requête est envoyée à l'URL d'écoute, le lanceur Confidential Space gère la collecte des preuves d'attestation, demande un jeton d'attestation au service d'attestation (en transmettant les éventuels paramètres personnalisés), puis renvoie le jeton généré à la charge de travail.
L'exemple de code suivant en Go montre comment communiquer avec le serveur HTTP du lanceur via 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
}
Demander un jeton d'attestation avec une audience personnalisée
Méthode HTTP et URL :
POST http://localhost/v1/token
Corps JSON de la requête :
{
"audience": "AUDIENCE_NAME",
"token_type": "TOKEN_TYPE",
"nonces": [
"NONCE_1",
"NONCE_2",
...
]
}
Indiquez les valeurs suivantes :
AUDIENCE_NAME
: valeur obligatoire. La valeur de votre audience, qui correspond au nom que vous avez attribué à votre partie de confiance. Ce paramètre est défini par la charge de travail.La valeur par défaut de ce champ est
https://sts.google.com
pour les jetons sans audience personnalisée. La valeurhttps://sts.google.com
ne peut pas être utilisée lors de la définition d'une audience personnalisée. La longueur maximale est de 512 octets.Pour inclure une audience personnalisée dans un jeton, la charge de travail (et non la partie de confiance) doit l'ajouter à la requête de jeton d'attestation avant d'envoyer la requête au service d'attestation de l'espace confidentiel. Cela permet d'empêcher la partie de confiance de demander un jeton pour une ressource protégée à laquelle elle ne devrait pas avoir accès.
TOKEN_TYPE
: valeur obligatoire. Type de jeton à renvoyer. Sélectionnez l'un des types suivants:OIDC
: ces jetons sont validés par rapport à une clé publique spécifiée dans le champjwks_uri
au point de terminaison de validation des jetons OIDC. La clé publique est régulièrement remplacée.PKI
: ces jetons sont validés par rapport à un certificat racine spécifié dans le champroot_ca_uri
au niveau du point de terminaison de validation des jetons PKI. Vous devez stocker ce certificat vous-même. Le certificat est renouvelé tous les 10 ans.
Étant donné que des certificats à expiration longue sont utilisés au lieu de clés publiques à expiration courte pour la validation des jetons, vos adresses IP ne sont pas exposées aux serveurs Google aussi souvent. Cela signifie que les jetons PKI offrent une confidentialité plus élevée que les jetons OIDC.
Vous pouvez valider l'empreinte du certificat avec OpenSSL:
openssl x509 -fingerprint -in confidential_space_root.crt
L'empreinte doit correspondre au récapitulatif SHA-1 suivant:
B9:51:20:74:2C:24:E3:AA:34:04:2E:1C:3B:A3:AA:D2:8B:21:23:21
NONCE
: facultatif. Valeur unique, aléatoire et opaque, qui garantit qu'un jeton ne peut être utilisé qu'une seule fois. La valeur est définie par la partie de confiance. Vous pouvez indiquer jusqu'à six nonces. Chaque nonce doit être compris entre 10 et 74 octets inclus.Lorsqu'il inclut un nonce, le tiers de confiance doit vérifier que les nonces envoyés dans la requête de jeton d'attestation sont les mêmes que ceux du jeton renvoyé. Si ce n'est pas le cas, la partie de confiance doit refuser le jeton.
Analyser et valider les jetons d'attestation
Les exemples de code suivants en Go montrent comment valider les jetons d'attestation.
Jetons d'attestation 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))
}
Jetons d'attestation PKI
Pour valider le jeton, le tiers de confiance doit suivre les étapes ci-dessous:
Analysez l'en-tête du jeton pour obtenir la chaîne de certificats.
Validez la chaîne de certificats par rapport à la racine stockée. Vous devez avoir précédemment téléchargé le certificat racine à partir de l'URL spécifiée dans le champ
root_ca_uri
renvoyé au point de terminaison de validation du jeton PKI.Vérifiez la validité du certificat de feuille.
Utilisez le certificat de feuille pour valider la signature du jeton, à l'aide de l'algorithme spécifié dans la clé
alg
de l'en-tête.
Une fois le jeton validé, la partie de confiance peut analyser les revendications du jeton.
// 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
}