Wenn der Zugriff auf Ihre geschützten Ressourcen nicht über die IAM von Google Cloudverwaltet wird, z. B. wenn die Ressourcen in einem anderen Cloud-Dienst, lokal oder auf einem lokalen Gerät wie einem Mobiltelefon gespeichert sind, können Sie trotzdem eine Confidential Space-Arbeitslast für das Gerät oder System authentifizieren, das Zugriff auf diese Ressourcen bietet. Dieses wird auch als Vertrauensseite bezeichnet.
Dazu muss die vertrauende Partei ein Attestierungstoken vom Confidential Space-Attestierungsdienst mit einer benutzerdefinierten Zielgruppe und optionalen Nonces anfordern. Wenn Sie ein Attestierungstoken auf diese Weise anfordern, müssen Sie das Token selbst validieren, bevor Sie Zugriff auf Ressourcen gewähren.
In der folgenden Dokumentation werden die Konzepte für die Verwendung von Confidential Space mit Ressourcen außerhalb von Google Cloudbehandelt, einschließlich Anleitungen zur nahtlosen Integration Ihrer Confidential Space-Arbeitslasten in AWS-Ressourcen. Eine umfassende Anleitung finden Sie im codelab.
Ablauf des Attestierungstokens
Attestierungstokens werden von der Arbeitslast im Namen einer vertrauenden Partei angefordert und vom Attestierungsdienst zurückgegeben. Je nach Bedarf können Sie eine benutzerdefinierte Zielgruppe definieren und optional Nounces angeben.
Unverschlüsselt
Um den Prozess des Abrufens von Tokens besser zu verstehen, wird im hier dargestellten Ablauf keine Verschlüsselung verwendet. In der Praxis empfehlen wir, die Kommunikation mit TLS zu verschlüsseln.
Das folgende Diagramm zeigt den Ablauf:
Die vertrauende Partei sendet eine Tokenanfrage an die Arbeitslast mit optionalen Nonces, die sie generiert hat.
Die Arbeitslast bestimmt die Zielgruppe, fügt sie der Anfrage hinzu und sendet die Anfrage an den Confidential Space-Launcher.
Der Launcher sendet die Anfrage an den Attestierungsdienst.
Der Attestierungsdienst generiert ein Token, das die angegebene Zielgruppe und optionale Nonces enthält.
Der Attestierungsdienst gibt das Token an den Launcher zurück.
Der Launcher gibt das Token an die Arbeitslast zurück.
Die Arbeitslast gibt das Token an die vertrauende Partei zurück.
Die vertrauende Partei überprüft die Ansprüche, einschließlich der Zielgruppe und optionalen Nonces.
Mit TLS verschlüsselt
Bei einem unverschlüsselten Ablauf ist die Anfrage anfällig für Man-in-the-Middle-Angriffe. Da eine Nonce nicht an die Datenausgabe oder eine TLS-Sitzung gebunden ist, kann ein Angreifer die Anfrage abfangen und sich als die Arbeitslast ausgeben.
Um diese Art von Angriff zu verhindern, können Sie eine TLS-Sitzung zwischen der vertrauenden Partei und der Arbeitslast einrichten und das TLS-exportierte Schlüsselmaterial (EKM) als Nonce verwenden. Das exportierte TLS-Schlüsselmaterial bindet die Attestierung an die TLS-Sitzung und bestätigt, dass die Attestierungsanfrage über einen sicheren Kanal gesendet wurde. Dieser Vorgang wird auch als Channel-Binding bezeichnet.
Das folgende Diagramm zeigt den Ablauf bei Verwendung der Channel-Bindung:
Die vertrauende Partei richtet eine sichere TLS-Sitzung mit der Confidential VM ein, auf der die Arbeitslast ausgeführt wird.
Die vertrauende Partei sendet eine Tokenanfrage über die sichere TLS-Sitzung.
Die Arbeitslast bestimmt die Zielgruppe und generiert eine Nonce mit dem exportierten TLS-Schlüsselmaterial.
Die Arbeitslast sendet die Anfrage an den Confidential Space-Launcher.
Der Launcher sendet die Anfrage an den Attestierungsdienst.
Der Attestierungsdienst generiert ein Token, das die angegebene Zielgruppe und Nonce enthält.
Der Attestierungsdienst gibt das Token an den Launcher zurück.
Der Launcher gibt das Token an die Arbeitslast zurück.
Die Arbeitslast gibt das Token an die vertrauende Partei zurück.
Die vertrauende Partei generiert die Nonce mit dem exportierten TLS-Schlüsselmaterial neu.
Die vertrauende Partei überprüft die Ansprüche, einschließlich der Zielgruppe und der Einmal-ID. Die Nonce im Token muss mit der Nonce übereinstimmen, die von der vertrauenden Partei neu generiert wird.
Struktur von Attestierungstokens
Bestätigungstokens sind JSON-Webtokens mit der folgenden Struktur:
Header: Beschreibt den Signieralgorithmus. In PKI-Tokens wird die Zertifikatskette auch im Header im Feld
x5c
gespeichert.Signierte JSON-Daten-Nutzlast: Enthält Ansprüche zur Arbeitslast für die vertrauende Partei, z. B. Betreff, Aussteller, Zielgruppe, Einmalcodes und Ablaufzeit.
Signatur: Bietet eine Validierung, dass sich das Token während der Übertragung nicht geändert hat. Weitere Informationen zur Verwendung der Signatur finden Sie unter OpenID Connect-ID-Token validieren.
Das folgende Codebeispiel ist ein Beispiel für ein codiertes Attest-Token, das im 240500-Image für Confidential Space generiert wurde. Neuere Bilder enthalten möglicherweise zusätzliche Felder. Sie können https://jwt.io/ verwenden, um es zu decodieren (die Signatur wird entfernt).
eyJhbGciOiJIUzI1NiIsImtpZCI6IjEyMzQ1IiwidHlwIjoiSldUIn0.eyJhdWQiOiJBVURJRU5DRV9OQU1FIiwiZGJnc3RhdCI6ImRpc2FibGVkLXNpbmNlLWJvb3QiLCJlYXRfbm9uY2UiOlsiTk9OQ0VfMSIsIk5PTkNFXzIiXSwiZWF0X3Byb2ZpbGUiOiJodHRwczovL2Nsb3VkLmdvb2dsZS5jb20vY29uZmlkZW50aWFsLWNvbXB1dGluZy9jb25maWRlbnRpYWwtc3BhY2UvZG9jcy9yZWZlcmVuY2UvdG9rZW4tY2xhaW1zIiwiZXhwIjoxNzIxMzMwMDc1LCJnb29nbGVfc2VydmljZV9hY2NvdW50cyI6WyJQUk9KRUNUX0lELWNvbXB1dGVAZGV2ZWxvcGVyLmdzZXJ2aWNlYWNjb3VudC5jb20iXSwiaHdtb2RlbCI6IkdDUF9BTURfU0VWIiwiaWF0IjoxNzIxMzI2NDc1LCJpc3MiOiJodHRwczovL2NvbmZpZGVudGlhbGNvbXB1dGluZy5nb29nbGVhcGlzLmNvbSIsIm5iZiI6MTcyMTMyNjQ3NSwib2VtaWQiOjExMTI5LCJzZWNib290Ijp0cnVlLCJzdWIiOiJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9jb21wdXRlL3YxL3Byb2plY3RzL1BST0pFQ1RfSUQvem9uZXMvdXMtY2VudHJhbDEtYS9pbnN0YW5jZXMvSU5TVEFOQ0VfTkFNRSIsInN1Ym1vZHMiOnsiY29uZmlkZW50aWFsX3NwYWNlIjp7Im1vbml0b3JpbmdfZW5hYmxlZCI6eyJtZW1vcnkiOmZhbHNlfSwic3VwcG9ydF9hdHRyaWJ1dGVzIjpbIkxBVEVTVCIsIlNUQUJMRSIsIlVTQUJMRSJdfSwiY29udGFpbmVyIjp7ImFyZ3MiOlsiL2N1c3RvbW5vbmNlIiwiL2RvY2tlci1lbnRyeXBvaW50LnNoIiwibmdpbngiLCItZyIsImRhZW1vbiBvZmY7Il0sImVudiI6eyJIT1NUTkFNRSI6IkhPU1RfTkFNRSIsIk5HSU5YX1ZFUlNJT04iOiIxLjI3LjAiLCJOSlNfUkVMRUFTRSI6IjJ-Ym9va3dvcm0iLCJOSlNfVkVSU0lPTiI6IjAuOC40IiwiUEFUSCI6Ii91c3IvbG9jYWwvc2JpbjovdXNyL2xvY2FsL2JpbjovdXNyL3NiaW46L3Vzci9iaW46L3NiaW46L2JpbiIsIlBLR19SRUxFQVNFIjoiMn5ib29rd29ybSJ9LCJpbWFnZV9kaWdlc3QiOiJzaGEyNTY6Njc2ODJiZGE3NjlmYWUxY2NmNTE4MzE5MmI4ZGFmMzdiNjRjYWU5OWM2YzMzMDI2NTBmNmY4YmY1ZjBmOTVkZiIsImltYWdlX2lkIjoic2hhMjU2OmZmZmZmYzkwZDM0M2NiY2IwMWE1MDMyZWRhYzg2ZGI1OTk4YzUzNmNkMGEzNjY1MTQxMjFhNDVjNjcyMzc2NWMiLCJpbWFnZV9yZWZlcmVuY2UiOiJkb2NrZXIuaW8vbGlicmFyeS9uZ2lueDpsYXRlc3QiLCJpbWFnZV9zaWduYXR1cmVzIjpbeyJrZXlfaWQiOiI8aGV4YWRlY2ltYWwtc2hhMjU2LWZpbmdlcnByaW50LXB1YmxpYy1rZXkxPiIsInNpZ25hdHVyZSI6IjxiYXNlNjQtZW5jb2RlZC1zaWduYXR1cmU-Iiwic2lnbmF0dXJlX2FsZ29yaXRobSI6IlJTQVNTQV9QU1NfU0hBMjU2In0seyJrZXlfaWQiOiI8aGV4YWRlY2ltYWwtc2hhMjU2LWZpbmdlcnByaW50LXB1YmxpYy1rZXkyPiIsInNpZ25hdHVyZSI6IjxiYXNlNjQtZW5jb2RlZC1zaWduYXR1cmU-Iiwic2lnbmF0dXJlX2FsZ29yaXRobSI6IlJTQVNTQV9QU1NfU0hBMjU2In0seyJrZXlfaWQiOiI8aGV4YWRlY2ltYWwtc2hhMjU2LWZpbmdlcnByaW50LXB1YmxpYy1rZXkzPiIsInNpZ25hdHVyZSI6IjxiYXNlNjQtZW5jb2RlZC1zaWduYXR1cmU-Iiwic2lnbmF0dXJlX2FsZ29yaXRobSI6IkVDRFNBX1AyNTZfU0hBMjU2In1dLCJyZXN0YXJ0X3BvbGljeSI6Ik5ldmVyIn0sImdjZSI6eyJpbnN0YW5jZV9pZCI6IklOU1RBTkNFX0lEIiwiaW5zdGFuY2VfbmFtZSI6IklOU1RBTkNFX05BTUUiLCJwcm9qZWN0X2lkIjoiUFJPSkVDVF9JRCIsInByb2plY3RfbnVtYmVyIjoiUFJPSkVDVF9OVU1CRVIiLCJ6b25lIjoidXMtY2VudHJhbDEtYSJ9fSwic3duYW1lIjoiQ09ORklERU5USUFMX1NQQUNFIiwic3d2ZXJzaW9uIjpbIjI0MDUwMCJdfQ.29V71ymnt7LY5Ny6OJFb9AClT4XNLPi0TIcddKDp5pk<SIGNATURE>
Hier ist die decodierte Version des vorherigen Beispiels:
{
"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"
]
}
Eine ausführlichere Erläuterung der Felder für Attestierungstokens finden Sie unter Attestierungstoken-Claims.
Attestierungstokens abrufen
Führen Sie die folgenden Schritte aus, um Attestierungstokens in Ihrer Confidential Space-Umgebung zu implementieren:
Richten Sie einen HTTP-Client in Ihrer Arbeitslast ein.
Verwenden Sie in Ihrem Arbeitslast den HTTP-Client, um eine HTTP-Anfrage über einen Unix-Domain-Socket an die Listening-URL
http://localhost/v1/token
zu senden. Die Socket-Datei befindet sich unter/run/container_launcher/teeserver.sock
.
Wenn eine Anfrage an die Listening-URL gesendet wird, verwaltet der Confidential Space-Launcher die Erfassung der Attestierungsnachweise, fordert ein Attestierungstoken vom Attestierungsdienst an (und übergibt alle benutzerdefinierten Parameter) und gibt das generierte Token dann an die Arbeitslast zurück.
Das folgende Codebeispiel in Go zeigt, wie über IPC mit dem HTTP-Server des Launchers kommuniziert wird.
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
}
Attestierungs-Token mit einer benutzerdefinierten Zielgruppe anfordern
HTTP-Methode und URL:
POST http://localhost/v1/token
JSON-Text anfordern:
{
"audience": "AUDIENCE_NAME",
"token_type": "TOKEN_TYPE",
"nonces": [
"NONCE_1",
"NONCE_2",
...
]
}
Geben Sie folgende Werte an:
AUDIENCE_NAME
: erforderlich. Der Wert Ihrer Zielgruppe, also der Name, den Sie Ihrer vertrauenden Partei gegeben haben. Dies wird von der Arbeitslast festgelegt.Der Standardwert für dieses Feld ist
https://sts.google.com
für Tokens ohne benutzerdefinierte Zielgruppe. Der Werthttps://sts.google.com
kann nicht verwendet werden, wenn eine benutzerdefinierte Zielgruppe festgelegt wird. Die maximale Länge beträgt 512 Bytes.Wenn Sie eine benutzerdefinierte Zielgruppe in ein Token einfügen möchten, muss die Arbeitslast – nicht die vertrauende Partei – sie der Anfrage für das Attestierungstoken hinzufügen, bevor die Anfrage an den Confidential Space-Attestierungsdienst gesendet wird. So wird verhindert, dass die vertrauende Partei ein Token für eine geschützte Ressource anfordert, auf die sie keinen Zugriff haben sollte.
TOKEN_TYPE
: erforderlich. Der Typ des zurückzugebenden Tokens. Wählen Sie einen der folgenden Typen aus:OIDC
: Diese Tokens werden anhand eines öffentlichen Schlüssels validiert, der im Feldjwks_uri
am OIDC-Token-Validierungs-Endpunkt angegeben ist. Der öffentliche Schlüssel wird regelmäßig rotiert.PKI
: Diese Tokens werden anhand eines Root-Zertifikats validiert, das im Feldroot_ca_uri
am PKI-Token-Validierungsendpunkt angegeben ist. Sie müssen dieses Zertifikat selbst speichern. Das Zertifikat wird alle 10 Jahre rotiert.
Da für die Tokenvalidierung Zertifikate mit langer Ablaufzeit anstelle von öffentlichen Schlüsseln mit kurzer Ablaufzeit verwendet werden, werden Ihre IP-Adressen nicht so oft an Google-Server gesendet. Das bedeutet, dass PKI-Tokens einen höheren Datenschutz bieten als OIDC-Tokens.
Sie können den Fingerabdruck des Zertifikats mit OpenSSL validieren:
openssl x509 -fingerprint -in confidential_space_root.crt
Der Fingerabdruck sollte dem folgenden SHA‑1-Digest entsprechen:
B9:51:20:74:2C:24:E3:AA:34:04:2E:1C:3B:A3:AA:D2:8B:21:23:21
NONCE
: Optional. Ein eindeutiger, zufälliger und opaker Wert, der dafür sorgt, dass ein Token nur einmal verwendet werden kann. Der Wert wird von der vertrauenden Partei festgelegt. Es sind bis zu sechs Nonces zulässig. Jeder Nonce muss zwischen 10 und 74 Bytes (einschließlich) lang sein.Wenn ein Einmal-Code enthalten ist, muss die vertrauende Partei überprüfen, ob die in der Anfrage für das Attestierungstoken gesendeten Einmal-Codes mit den Einmal-Codes im zurückgegebenen Token übereinstimmen. Wenn sie sich unterscheiden, muss die vertrauende Partei das Token ablehnen.
Attestierungstokens parsen und validieren
Die folgenden Codebeispiele in Go zeigen, wie Sie Attestierungstokens validieren.
OIDC-Attestierungstokens
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))
}
PKI-Bestätigungstokens
Um das Token zu validieren, muss die vertrauende Partei die folgenden Schritte ausführen:
Parsen Sie den Header des Tokens, um die Zertifikatskette abzurufen.
Die Zertifikatskette wird anhand des gespeicherten Stamms validiert. Sie müssen das Root-Zertifikat zuvor über die URL heruntergeladen haben, die im Feld
root_ca_uri
zurückgegeben wurde, das am PKI-Token-Validierungsendpunkt zurückgegeben wurde.Prüfen Sie die Gültigkeit des Leaf-Zertifikats.
Verwenden Sie das Endzertifikat, um die Tokensignatur mit dem im Schlüssel
alg
im Header angegebenen Algorithmus zu validieren.
Nachdem das Token validiert wurde, kann die vertrauende Partei die Ansprüche des Tokens parsen.
// 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
}
AWS-Ressourcen einbinden
Sie können Ihre Confidential Space-Arbeitslasten mithilfe von AWS-Principal-Tags in AWS-Ressourcen (z. B. Schlüssel oder Daten) einbinden. Bei dieser Integration wird die sichere Attestierung von Confidential Space verwendet, um detaillierten Zugriff auf Ihre AWS-Ressourcen zu gewähren.
AWS-Haupt-Tag-Ansprüche
Google Cloud Attestation generiert überprüfbare Identitätstokens, die Behauptungen zur Integrität und Konfiguration der Confidential Space-Arbeitslast enthalten. Eine Teilmenge dieser Claims ist mit AWS kompatibel, sodass Sie den Zugriff auf Ihre AWS-Ressourcen steuern können. Diese Ansprüche werden in den https://aws.amazon.com/tags
-Ansprüchen im principal_tags
-Objekt im Attestierungstoken platziert. Weitere Informationen finden Sie unter AWS-Principal-Tag-Claims.
Hier sehen Sie ein Beispiel für die Struktur einer https://aws.amazon.com/tags
-Anfrage:
{
"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"
]
}
}
}
AWS-Richtlinien mit Ansprüchen zur Signatur von Container-Images
AWS-Tokens unterstützen auch Ansprüche für Container-Image-Signaturen. Diese Ansprüche sind nützlich, wenn sich die Arbeitslast häufig ändert oder wenn Sie mit mehreren Mitbearbeitern oder Drittparteien zusammenarbeiten.
Container-Image-Signaturansprüche bestehen aus Schlüssel-IDs, die durch ein Trennzeichen getrennt sind. Damit diese Ansprüche im AWS-Token enthalten sind, müssen Sie eine Zulassungsliste dieser Schlüssel-IDs als zusätzlichen Parameter in Ihrer Tokenanfrage angeben.
Dem Token werden nur die Schlüssel-IDs hinzugefügt, die mit den Schlüsseln übereinstimmen, die zum Signieren Ihrer Arbeitslast verwendet wurden. So wird sichergestellt, dass nur autorisierte Signaturen akzeptiert werden.
Denken Sie beim Schreiben Ihrer AWS-Richtlinie daran, dass Schlüssel-IDs dem Token als einzelner String mit Trennzeichen hinzugefügt werden. Sie müssen die Liste der erwarteten Schlüssel-IDs alphabetisch sortieren und den Stringwert erstellen. Wenn Sie beispielsweise die Schlüssel-IDs aKey1
, zKey2
und bKey3
haben, sollte der entsprechende Anspruchswert in Ihrer Richtlinie aKey1=bKey3=zKey2
sein.
Wenn Sie mehrere Schlüsselsätze unterstützen möchten, können Sie Ihrer Richtlinie optional mehrere Werte hinzufügen.
"aws:RequestTag/container.signatures.key_ids": [
"aKey1=bKey3=zKey2",
"aKey1=bKey3",
"zKey2"
]
Der Anspruch für die Signatur des Container-Images (container.signatures.key_ids
) und der Anspruch für den Digest des Container-Images (container.image_digest
) werden nicht zusammen in einem einzelnen Token angezeigt. Wenn Sie container.signatures.key_ids
verwenden, müssen Sie alle Verweise auf container.image_digest
aus Ihren AWS-Richtlinien entfernen.
Hier sehen Sie ein Beispiel für eine https://aws.amazon.com/tags
-Anspruchsstruktur mit 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"
]
}
}
}
Eine ausführlichere Erläuterung der Felder für Attestierungstokens finden Sie unter Attestierungstoken-Claims.
AWS-Ressourcen konfigurieren: vertrauende Partei
Bevor die vertrauende Partei ihre AWS-Ressourcen konfigurieren kann, muss sie AWS IAM so konfigurieren, dass Confidential Space als föderierter OIDC-Anbieter eingerichtet wird, und die erforderliche AWS IAM-Rolle erstellen.
AWS IAM konfigurieren
So fügen Sie den Google Cloud Attestation-Dienst als Identitätsanbieter in AWS IAM hinzu:
Rufen Sie in der AWS Console die Seite Identitätsanbieter auf.
Wählen Sie als Provider type (Anbietertyp) die Option OpenID Connect aus.
Geben Sie für Provider URL (Anbieter-URL) https://confidentialcomputing.googleapis.com ein.
Geben Sie unter Zielgruppe die URL ein, die Sie beim Identitätsanbieter registriert haben und mit der Anfragen an AWS gesendet werden. Beispiel: https://beispiel.de.
Klicken Sie auf Anbieter hinzufügen.
So erstellen Sie eine AWS-IAM-Rolle für Confidential Space-Tokens:
- Rufen Sie in der AWS Console die Seite Rollen auf.
- Klicken Sie auf Rolle erstellen.
- Wählen Sie als Typ der vertrauenswürdigen Entität die Option Webidentität aus.
- Wählen Sie im Bereich Webidentität den Identitätsanbieter und die Zielgruppe basierend auf dem vorherigen Schritt aus.
- Klicken Sie auf Weiter. Sie können das Bearbeiten der AWS-Richtlinie in diesem Schritt überspringen.
- Klicken Sie auf Weiter und fügen Sie bei Bedarf Tags hinzu.
- Geben Sie unter Rollenname den Rollennamen ein.
- Optional: Geben Sie unter Beschreibung eine Beschreibung für die neue Rolle ein.
- Prüfen Sie die Details und klicken Sie auf Rolle erstellen.
Bearbeiten Sie die AWS-Richtlinie der von Ihnen erstellten Rolle, um nur Zugriff auf die gewünschte Arbeitslast zu gewähren.
Mit dieser AWS-Richtlinie können Sie bestimmte Ansprüche im Token prüfen, z. B.:
- Der Container-Image-Digest der Arbeitslast.
- Die Zielgruppe des Tokens.
CONFIDENTIAL_SPACE
ist die Software, die auf der VM ausgeführt wird. Weitere Informationen finden Sie unterswname
in Attestierungstoken-Claims.- Das Attribut „support“ für das Confidential Space-Produktions-Image. Weitere Informationen finden Sie unter
confidential_space.support_attributes
.
Das folgende Beispiel zeigt eine AWS-Richtlinie, die einer Arbeitslast mit einem angegebenen Digest und einer angegebenen Zielgruppe Zugriff gewährt. Dabei ist
CONFIDENTIAL_SPACE
die Software, die auf der VM-Instanz ausgeführt wird, undSTABLE
das Support-Attribut:{ "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*" } } } ] }
AWS-Ressourcen konfigurieren
Nachdem Sie die Integration abgeschlossen haben, konfigurieren Sie Ihre AWS-Ressourcen. Dieser Schritt hängt von Ihrem jeweiligen Anwendungsfall ab. Sie können beispielsweise einen S3-Bucket, einen KMS-Schlüssel oder andere AWS-Ressourcen erstellen. Gewähren Sie der AWS IAM-Rolle, die Sie zuvor erstellt haben, die erforderlichen Berechtigungen für den Zugriff auf diese Ressourcen.
Confidential Space-Arbeitslast konfigurieren: Arbeitslastautor
Folgen Sie der Anleitung unter Attestierungs-Token mit einer benutzerdefinierten Zielgruppe anfordern, um Token-Anfragen zu erstellen.
Für AWS_PrincipalTag
-Ansprüche:
- Ein Nonce-Feld ist in Ihrer Tokenanfrage für die AWS-Integration optional.
- Fügen Sie die Zielgruppe ein, die Sie in AWS-Ressourcen konfigurieren: Vertrauende Seite konfiguriert haben.
- Legen Sie „token_type“ auf
AWS_PRINCIPALTAGS
fest.
Hier sehen Sie ein Beispiel für den Text einer AWS_PrincipalTag
-Anfrage für einen Anspruch:
body := `{
"audience": "https://example.com",
"token_type": "AWS_PRINCIPALTAGS",
}`
Nächste Schritte
- Weitere Informationen zu Attestierungstoken-Claims finden Sie unter Attestierungstoken-Claims.