Jika akses ke resource yang dilindungi tidak dikelola oleh Google Cloud's IAM—misalnya, resource disimpan di layanan cloud lain, di lokasi, atau di perangkat lokal seperti ponsel—Anda tetap dapat mengautentikasi workload Confidential Space ke perangkat atau sistem yang menyediakan akses ke resource tersebut, yang juga dikenal sebagai relying party.
Untuk melakukannya, relying party harus menerima token pengesahan dari layanan pengesahan, seperti Google Cloud Attestation, dengan audiens kustom dan nonce opsional. Saat meminta token pengesahan seperti ini, relying party mungkin perlu melakukan validasi tokennya sendiri sebelum memberikan akses ke resource.
Dokumentasi berikut mencakup konsep yang terlibat dalam penggunaan Confidential Space dengan resource di luar Google Cloud, termasuk petunjuk untuk mengintegrasikan workload Confidential Space dengan resource AWS. Untuk panduan lengkap, lihat codelab.
Alur token pengesahan
Token pengesahan diminta oleh workload atas nama relying party dan ditampilkan oleh layanan pengesahan. Bergantung pada kebutuhan Anda, Anda dapat menentukan audiens kustom dan secara opsional memberikan nonce.
Tidak dienkripsi
Untuk memudahkan pemahaman proses pengambilan token, alur yang ditampilkan di sini tidak menggunakan enkripsi. Dalam praktiknya, sebaiknya enkripsi komunikasi dengan TLS.
Diagram berikut menunjukkan alurnya:
Relying party mengirimkan permintaan token ke workload, dengan nonce opsional yang telah dibuatnya.
Workload menentukan audiens, menambahkan audiens ke permintaan, dan mengirimkan permintaan ke peluncur Confidential Space.
Peluncur mengirimkan permintaan ke layanan pengesahan.
Layanan pengesahan membuat token yang berisi audiens yang ditentukan dan nonce opsional.
Layanan pengesahan menampilkan token ke peluncur.
Peluncur menampilkan token ke workload.
Workload menampilkan token ke relying party.
Relying party memverifikasi klaim, termasuk audiens dan nonce opsional.
Dienkripsi dengan TLS
Alur yang tidak dienkripsi membuat permintaan rentan terhadap serangan man-in-the-middle. Karena nonce tidak terikat pada output data atau sesi TLS, penyerang dapat mencegat permintaan dan meniru workload.
Untuk membantu mencegah jenis serangan ini, Anda dapat menyiapkan sesi TLS antara relying party dan workload serta menggunakan materi kunci yang diekspor TLS (EKM) sebagai nonce. Materi kunci yang diekspor TLS mengikat pengesahan ke sesi TLS dan mengonfirmasi bahwa permintaan pengesahan dikirim melalui channel yang aman. Proses ini juga dikenal sebagai pengikatan channel.
Diagram berikut menunjukkan alur menggunakan pengikatan channel:
Relying party menyiapkan sesi TLS yang aman dengan Confidential VM yang menjalankan workload.
Relying party mengirimkan permintaan token menggunakan sesi TLS yang aman.
Workload menentukan audiens dan membuat nonce menggunakan materi kunci yang diekspor TLS.
Workload mengirimkan permintaan ke peluncur Confidential Space.
Peluncur mengirimkan permintaan ke layanan pengesahan.
Layanan pengesahan membuat token yang berisi audiens dan nonce yang ditentukan.
Layanan pengesahan menampilkan token ke peluncur.
Peluncur menampilkan token ke workload.
Workload menampilkan token ke relying party.
Relying party membuat ulang nonce menggunakan materi kunci yang diekspor TLS.
Relying party memverifikasi klaim, termasuk audiens dan nonce. Nonce dalam token harus cocok dengan nonce yang dibuat ulang oleh relying party.
Struktur token pengesahan
Token pengesahan adalah token web JSON dengan struktur berikut:
Header: Menjelaskan algoritma penandatanganan. Token PKI juga menyimpan rantai sertifikat di header dalam kolom
x5c.Payload data JSON yang ditandatangani: Berisi klaim tentang workload untuk relying party, seperti subjek, penerbit, audiens, nonce, dan waktu habis masa berlaku.
Tanda tangan: Memberikan validasi bahwa token tidak berubah selama transit. Untuk mengetahui informasi selengkapnya tentang penggunaan tanda tangan, lihat Cara Memvalidasi Token ID OpenID Connect.
Contoh kode berikut menunjukkan token pengesahan yang dienkode dengan tanda tangan dihapus. Anda dapat menggunakan https://jwt.io/ untuk mendekode token.
Token dibuat oleh image Confidential Space 240500 dan Google Cloud Attestation. Image yang lebih baru mungkin berisi kolom tambahan.
eyJhbGciOiJIUzI1NiIsImtpZCI6IjEyMzQ1IiwidHlwIjoiSldUIn0.eyJhdWQiOiJBVURJRU5DRV9OQU1FIiwiZGJnc3RhdCI6ImRpc2FibGVkLXNpbmNlLWJvb3QiLCJlYXRfbm9uY2UiOlsiTk9OQ0VfMSIsIk5PTkNFXzIiXSwiZWF0X3Byb2ZpbGUiOiJodHRwczovL2Nsb3VkLmdvb2dsZS5jb20vY29uZmlkZW50aWFsLWNvbXB1dGluZy9jb25maWRlbnRpYWwtc3BhY2UvZG9jcy9yZWZlcmVuY2UvdG9rZW4tY2xhaW1zIiwiZXhwIjoxNzIxMzMwMDc1LCJnb29nbGVfc2VydmljZV9hY2NvdW50cyI6WyJQUk9KRUNUX0lELWNvbXB1dGVAZGV2ZWxvcGVyLmdzZXJ2aWNlYWNjb3VudC5jb20iXSwiaHdtb2RlbCI6IkdDUF9BTURfU0VWIiwiaWF0IjoxNzIxMzI2NDc1LCJpc3MiOiJodHRwczovL2NvbmZpZGVudGlhbGNvbXB1dGluZy5nb29nbGVhcGlzLmNvbSIsIm5iZiI6MTcyMTMyNjQ3NSwib2VtaWQiOjExMTI5LCJzZWNib290Ijp0cnVlLCJzdWIiOiJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9jb21wdXRlL3YxL3Byb2plY3RzL1BST0pFQ1RfSUQvem9uZXMvdXMtY2VudHJhbDEtYS9pbnN0YW5jZXMvSU5TVEFOQ0VfTkFNRSIsInN1Ym1vZHMiOnsiY29uZmlkZW50aWFsX3NwYWNlIjp7Im1vbml0b3JpbmdfZW5hYmxlZCI6eyJtZW1vcnkiOmZhbHNlfSwic3VwcG9ydF9hdHRyaWJ1dGVzIjpbIkxBVEVTVCIsIlNUQUJMRSIsIlVTQUJMRSJdfSwiY29udGFpbmVyIjp7ImFyZ3MiOlsiL2N1c3RvbW5vbmNlIiwiL2RvY2tlci1lbnRyeXBvaW50LnNoIiwibmdpbngiLCItZyIsImRhZW1vbiBvZmY7Il0sImVudiI6eyJIT1NUTkFNRSI6IkhPU1RfTkFNRSIsIk5HSU5YX1ZFUlNJT04iOiIxLjI3LjAiLCJOSlNfUkVMRUFTRSI6IjJ-Ym9va3dvcm0iLCJOSlNfVkVSU0lPTiI6IjAuOC40IiwiUEFUSCI6Ii91c3IvbG9jYWwvc2JpbjovdXNyL2xvY2FsL2JpbjovdXNyL3NiaW46L3Vzci9iaW46L3NiaW46L2JpbiIsIlBLR19SRUxFQVNFIjoiMn5ib29rd29ybSJ9LCJpbWFnZV9kaWdlc3QiOiJzaGEyNTY6Njc2ODJiZGE3NjlmYWUxY2NmNTE4MzE5MmI4ZGFmMzdiNjRjYWU5OWM2YzMzMDI2NTBmNmY4YmY1ZjBmOTVkZiIsImltYWdlX2lkIjoic2hhMjU2OmZmZmZmYzkwZDM0M2NiY2IwMWE1MDMyZWRhYzg2ZGI1OTk4YzUzNmNkMGEzNjY1MTQxMjFhNDVjNjcyMzc2NWMiLCJpbWFnZV9yZWZlcmVuY2UiOiJkb2NrZXIuaW8vbGlicmFyeS9uZ2lueDpsYXRlc3QiLCJpbWFnZV9zaWduYXR1cmVzIjpbeyJrZXlfaWQiOiI8aGV4YWRlY2ltYWwtc2hhMjU2LWZpbmdlcnByaW50LXB1YmxpYy1rZXkxPiIsInNpZ25hdHVyZSI6IjxiYXNlNjQtZW5jb2RlZC1zaWduYXR1cmU-Iiwic2lnbmF0dXJlX2FsZ29yaXRobSI6IlJTQVNTQV9QU1NfU0hBMjU2In0seyJrZXlfaWQiOiI8aGV4YWRlY2ltYWwtc2hhMjU2LWZpbmdlcnByaW50LXB1YmxpYy1rZXkyPiIsInNpZ25hdHVyZSI6IjxiYXNlNjQtZW5jb2RlZC1zaWduYXR1cmU-Iiwic2lnbmF0dXJlX2FsZ29yaXRobSI6IlJTQVNTQV9QU1NfU0hBMjU2In0seyJrZXlfaWQiOiI8aGV4YWRlY2ltYWwtc2hhMjU2LWZpbmdlcnByaW50LXB1YmxpYy1rZXkzPiIsInNpZ25hdHVyZSI6IjxiYXNlNjQtZW5jb2RlZC1zaWduYXR1cmU-Iiwic2lnbmF0dXJlX2FsZ29yaXRobSI6IkVDRFNBX1AyNTZfU0hBMjU2In1dLCJyZXN0YXJ0X3BvbGljeSI6Ik5ldmVyIn0sImdjZSI6eyJpbnN0YW5jZV9pZCI6IklOU1RBTkNFX0lEIiwiaW5zdGFuY2VfbmFtZSI6IklOU1RBTkNFX05BTUUiLCJwcm9qZWN0X2lkIjoiUFJPSkVDVF9JRCIsInByb2plY3RfbnVtYmVyIjoiUFJPSkVDVF9OVU1CRVIiLCJ6b25lIjoidXMtY2VudHJhbDEtYSJ9fSwic3duYW1lIjoiQ09ORklERU5USUFMX1NQQUNFIiwic3d2ZXJzaW9uIjpbIjI0MDUwMCJdfQ.29V71ymnt7LY5Ny6OJFb9AClT4XNLPi0TIcddKDp5pk
Berikut adalah versi yang didekode dari contoh sebelumnya:
{
"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"
]
}
Untuk penjelasan yang lebih mendetail tentang kolom token pengesahan, lihat Klaim token pengesahan.
Mengambil token pengesahan
Selesaikan langkah-langkah berikut untuk menerapkan token pengesahan di lingkungan Confidential Space Anda:
Siapkan klien HTTP di workload Anda.
Di workload Anda, gunakan klien HTTP untuk membuat permintaan HTTP ke URL yang dipantau, melalui soket domain Unix yang terletak di
/run/container_launcher/teeserver.sock. URL yang dipantau berubah bergantung pada layanan pengesahan yang Anda gunakan:Google Cloud Attestation:
http://localhost/v1/tokenIntel Trust Authority:
http://localhost/v1/intel/token
Saat permintaan dibuat ke URL yang dipantau, peluncur Confidential Space mengelola pengumpulan bukti pengesahan, meminta token pengesahan dari layanan pengesahan (meneruskan parameter kustom apa pun), lalu menampilkan token yang dibuat ke workload.
Contoh kode berikut di Go menunjukkan cara berkomunikasi dengan server HTTP peluncur melalui IPC, berdasarkan layanan pengesahan yang Anda gunakan.
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 hanya mendukung instance VM yang menjalankan 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
}
Meminta token pengesahan dengan audiens kustom
Metode HTTP dan URL:
POST http://localhost/v1/token
Meminta isi JSON:
{
"audience": "AUDIENCE_NAME",
"token_type": "TOKEN_TYPE",
"nonces": [
"NONCE_1",
"NONCE_2",
...
]
}
Berikan nilai berikut:
AUDIENCE_NAME: Wajib diisi. Nilai audiens Anda, yang merupakan nama yang Anda berikan kepada relying party. Nilai ini ditetapkan oleh workload.Kolom ini secara default ditetapkan ke
https://sts.google.comuntuk token tanpa audiens kustom. Nilaihttps://sts.google.comtidak dapat digunakan saat menetapkan audiens kustom. Panjang maksimum adalah 512 byte.Untuk menyertakan audiens kustom dalam token, workload—bukan relying party—harus menambahkannya ke permintaan token pengesahan sebelum mengirimkan permintaan ke layanan pengesahan. Hal ini membantu mencegah relying party meminta token untuk resource yang dilindungi yang seharusnya tidak dapat diakses.
TOKEN_TYPE: Wajib diisi. Jenis token yang akan ditampilkan. Pilih salah satu jenis berikut:OIDC: Token ini divalidasi terhadap kunci publik yang dirotasi secara berkala yang ditentukan di kolomjwks_uridi endpoint validasi token OIDC. Endpoint dibuat dengan menambahkan.well-known/openid-configurationke URI penerbit.Google Cloud Attestation:
https://confidentialcomputing.googleapis.com/.well-known/openid-configuration.Intel Trust Authority:
https://portal.trustauthority.intel.com/.well-known/openid-configuration.
PKI: Token ini divalidasi terhadap root certificate yang ditentukan di kolomroot_ca_uridi endpoint validasi token PKI. Anda harus menyimpan sertifikat ini sendiri. Sertifikat dirotasi setiap 10 tahun.
Karena sertifikat yang masa berlakunya lama digunakan, bukan kunci publik yang masa berlakunya singkat untuk validasi token, alamat IP Anda tidak terlalu sering diekspos ke server Google. Artinya, token PKI menawarkan privasi yang lebih tinggi daripada token OIDC.
Anda dapat memvalidasi sidik jari sertifikat dengan OpenSSL:
openssl x509 -fingerprint -in confidential_space_root.crtSidik jari harus cocok dengan ringkasan SHA-1 berikut:
B9:51:20:74:2C:24:E3:AA:34:04:2E:1C:3B:A3:AA:D2:8B:21:23:21NONCE: Opsional. Nilai unik, acak, dan buram yang memverifikasi bahwa token hanya dapat digunakan satu kali. Nilai ini ditetapkan oleh relying party. Maksimal enam nonce diizinkan. Setiap nonce harus antara 10 dan 74 byte, inklusif.Saat menyertakan nonce, relying party harus memverifikasi bahwa nonce yang dikirim dalam permintaan token pengesahan sama dengan nonce dalam token yang ditampilkan. Jika berbeda, relying party harus menolak token.
Mengurai dan memvalidasi token pengesahan
Contoh kode berikut di Go menunjukkan cara memvalidasi token pengesahan.
Token pengesahan 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))
}
Token pengesahan PKI
Untuk memvalidasi token, relying party harus menyelesaikan langkah-langkah berikut:
Mengurai header token untuk mendapatkan rantai sertifikat.
Memvalidasi rantai sertifikat terhadap root yang disimpan. Anda harus mendownload root certificate sebelumnya dari URL yang ditentukan di kolom yang ditampilkan di endpoint validasi token PKI.
root_ca_uriMemeriksa validitas sertifikat leaf.
Menggunakan sertifikat leaf untuk memvalidasi tanda tangan token, menggunakan algoritma yang ditentukan dalam kunci
algdi header.
Setelah token divalidasi, relying party dapat mengurai klaim 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
}
Mengintegrasikan resource AWS
Anda dapat mengintegrasikan workload Confidential Space dengan resource AWS (seperti kunci atau data) menggunakan tag utama AWS. Integrasi ini menggunakan pengesahan aman yang disediakan oleh Confidential Space untuk memberikan akses terperinci ke resource AWS Anda.
Klaim tag utama AWS
Google Cloud Attestation membuat
token identitas yang dapat diverifikasi yang berisi klaim tentang integritas dan konfigurasi workload Confidential Space. Sebagian klaim ini
kompatibel dengan
AWS, sehingga Anda dapat mengontrol akses ke resource AWS. Klaim ini ditempatkan di klaim https://aws.amazon.com/tags, dalam objek principal_tags di token pengesahan. Untuk mengetahui informasi selengkapnya, lihat
Klaim tag utama AWS.
Berikut adalah contoh struktur klaim 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"
]
}
}
}
Kebijakan AWS dengan klaim tanda tangan image container
Token AWS juga mendukung klaim tanda tangan image container. Klaim ini cocok untuk perubahan workload frekuensi tinggi dan menangani beberapa kolaborator atau relying party.
Klaim tanda tangan image container terdiri dari ID kunci, yang dipisahkan oleh pembatas. Untuk menyertakan klaim ini dalam token AWS, Anda harus memberikan daftar yang diizinkan dari ID kunci ini sebagai parameter tambahan dalam permintaan token Anda.
Hanya ID kunci yang cocok dengan kunci yang digunakan untuk menandatangani workload Anda yang ditambahkan ke token. Proses ini membantu memastikan bahwa hanya tanda tangan yang diotorisasi yang diterima.
Saat menulis kebijakan AWS, ingatlah bahwa ID kunci ditambahkan ke token sebagai satu string dengan karakter pembatas. Anda harus mengurutkan daftar ID kunci yang diharapkan secara alfabetis dan membuat nilai string. Misalnya, jika Anda
memiliki ID kunci aKey1, zKey2, dan bKey3, nilai klaim yang sesuai dalam
kebijakan Anda harus aKey1=bKey3=zKey2.
Untuk mendukung beberapa kumpulan kunci, Anda dapat secara opsional menambahkan beberapa nilai ke kebijakan Anda.
"aws:RequestTag/container.signatures.key_ids": [
"aKey1=bKey3=zKey2",
"aKey1=bKey3",
"zKey2"
]
Klaim tanda tangan image container (container.signatures.key_ids) dan klaim ringkasan image container (container.image_digest) tidak muncul bersama dalam satu token. Jika Anda menggunakan container.signatures.key_ids, hapus semua referensi ke container.image_digest dari kebijakan AWS Anda.
Berikut adalah contoh struktur klaim https://aws.amazon.com/tags yang berisi 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"
]
}
}
}
Untuk penjelasan yang lebih mendetail tentang kolom token pengesahan, lihat Klaim token pengesahan.
Mengonfigurasi resource AWS: relying party
Sebelum dapat mengonfigurasi resource AWS, relying party harus mengonfigurasi AWS IAM untuk menetapkan Confidential Space sebagai penyedia OIDC gabungan dan membuat peran AWS IAM yang diperlukan.
Mengonfigurasi AWS IAM
Untuk menambahkan layanan Google Cloud Attestation sebagai penyedia identitas di AWS IAM, lakukan hal berikut:
Di konsol AWS, buka halaman Identity providers.
Untuk Provider type, pilih OpenID Connect.
Untuk Provider URL, masukkan https://confidentialcomputing.googleapis.com.
Untuk Audience, masukkan URL yang Anda daftarkan dengan penyedia identitas dan yang membuat permintaan ke AWS. Misalnya, https://example.com.
Klik Add provider.
Untuk membuat peran AWS IAM untuk token Confidential Space, lakukan hal berikut:
Di konsol AWS, buka halaman Roles.
Klik Create role.
Untuk jenis Trusted entity, pilih Web identity.
Di bagian Web identity, pilih penyedia identitas dan audiens berdasarkan langkah sebelumnya.
Klik Next. Anda dapat melewati pengeditan kebijakan AWS pada langkah ini.
Klik Next, dan tambahkan tag jika diperlukan.
Untuk Role name, masukkan nama peran.
Opsional: Untuk Description, masukkan deskripsi untuk peran baru.
Tinjau detailnya, lalu klik Create role.
Edit kebijakan AWS dari peran yang Anda buat untuk hanya memberikan akses ke workload pilihan Anda.
Kebijakan AWS ini memungkinkan Anda memeriksa klaim tertentu dalam token, seperti:
Ringkasan image container workload.
Target audiens token.
CONFIDENTIAL_SPACEadalah software yang berjalan di VM. Untuk mengetahui informasi selengkapnya, lihatswnamedi Klaim token pengesahan.Atribut dukungan image Confidential Space produksi. Untuk mengetahui informasi selengkapnya, lihat
confidential_space.support_attributes.
Berikut adalah contoh kebijakan AWS yang memberikan akses ke workload dengan ringkasan dan audiens yang ditentukan,
CONFIDENTIAL_SPACEsebagai software yang berjalan di instance VM, danSTABLEsebagai atribut dukungan:{ "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*" } } } ] }
Mengonfigurasi resource AWS
Setelah integrasi selesai, konfigurasikan resource AWS Anda. Langkah ini bergantung pada kasus penggunaan spesifik Anda. Misalnya, Anda dapat membuat bucket S3, kunci KMS, atau resource AWS lainnya. Pastikan untuk memberikan peran AWS IAM yang Anda buat sebelumnya izin yang diperlukan untuk mengakses resource ini.
Mengonfigurasi workload Confidential Space: penulis workload
Untuk membuat permintaan token, ikuti petunjuk di bagian meminta token pengesahan dengan audiens kustom.
Untuk klaim AWS_PrincipalTag:
Kolom nonce bersifat opsional dalam permintaan token Anda untuk integrasi AWS.
Sertakan audiens yang Anda konfigurasi di Mengonfigurasi Resource AWS: Relying Party.
Tetapkan token_type ke
AWS_PRINCIPALTAGS.
Berikut adalah contoh isi permintaan klaim AWS_PrincipalTag:
body := `{
"audience": "https://example.com",
"token_type": "AWS_PRINCIPALTAGS",
}`
Langkah berikutnya
Lihat Klaim token pengesahan untuk mengetahui informasi selengkapnya tentang klaim token pengesahan.