如果受保護資源的存取權不是由 Google Cloud的 IAM 管理,例如資源儲存在其他雲端服務、內部部署環境或本機裝置 (例如手機),您仍可向提供這些資源存取權的裝置或系統 (又稱為信賴方) 驗證 Confidential Space 工作負載。
為此,信賴方必須向 Confidential Space 驗證服務要求驗證權杖,並提供自訂目標對象和選用隨機數。要求這類認證權杖時,您必須先自行驗證權杖,才能授予資源存取權。
以下說明文件涵蓋使用 Confidential Space 和 Google Cloud以外資源的相關概念,包括如何順暢整合 Confidential Space 工作負載與 AWS 資源的說明。如需完整逐步說明,請參閱codelab。
認證權杖流程
工作負載會代表信賴方要求認證權杖,並由認證服務傳回。您可以視需求定義自訂目標對象,並視需要提供隨機碼。
未加密
為方便瞭解權杖擷取程序,這裡顯示的流程不會使用加密功能。實務上,建議您使用 TLS 加密通訊。
下圖顯示流程:
信賴方會將權杖要求傳送至工作負載,並視需要傳送產生的隨機值。
工作負載會決定目標對象、將目標對象新增至要求,並將要求傳送至 Confidential Space 啟動器。
啟動器會將要求傳送至認證服務。
認證服務會產生權杖,其中包含指定的對象和選用的隨機數。
認證服務會將權杖傳回啟動器。
啟動器會將權杖傳回工作負載。
工作負載會將權杖傳回給信任方。
信賴方會驗證聲明,包括目標對象和選用的隨機數。
透過 TLS 加密
未加密的流程會讓要求容易受到中間人攻擊。由於隨機數未繫結至資料輸出內容或 TLS 工作階段,攻擊者可以攔截要求並模擬工作負載。
為防範這類攻擊,您可以在憑證授權單位和工作負載之間設定 TLS 工作階段,並使用 TLS 匯出金鑰材料 (EKM) 做為隨機數。TLS 匯出的金鑰內容會將認證繫結至 TLS 工作階段,並確認認證要求是透過安全管道傳送。這項程序也稱為「管道繫結」。
下圖顯示使用管道繫結的流程:
憑證核發機構會與執行工作負載的機密 VM 建立安全的 TLS 工作階段。
憑證核發機構會透過安全的 TLS 工作階段傳送符記要求。
工作負載會決定目標對象,並使用 TLS 匯出的金鑰內容產生隨機數。
工作負載會將要求傳送至 Confidential Space 啟動器。
啟動器會將要求傳送至認證服務。
認證服務會產生權杖,其中包含指定的對象和隨機數。
認證服務會將權杖傳回啟動器。
啟動器會將權杖傳回工作負載。
工作負載會將權杖傳回給信任方。
憑證核發機構會使用 TLS 匯出的金鑰材料重新產生隨機數。
信賴方會驗證聲明,包括目標對象和隨機值。權杖中的隨機碼必須與依賴方重新產生的隨機碼相符。
認證權杖結構
認證權杖是 JSON Web Token,結構如下:
標頭:說明簽署演算法。PKI 權杖也會將憑證鏈儲存在
x5c
欄位的標頭中。已簽署的 JSON 資料酬載:包含工作負載的聲明,供信賴方使用,例如主體、簽發者、對象、隨機數和到期時間。
簽章:驗證權杖在傳輸過程中未變更。如要進一步瞭解如何使用簽章,請參閱「如何驗證 OpenID Connect ID 權杖」。
下列程式碼範例是在 Confidential Space 240500 映像檔中產生的編碼認證權杖。較新的圖片可能包含其他欄位。您可以使用 https://jwt.io/ 解碼 (簽章已經過編輯)。
eyJhbGciOiJIUzI1NiIsImtpZCI6IjEyMzQ1IiwidHlwIjoiSldUIn0.eyJhdWQiOiJBVURJRU5DRV9OQU1FIiwiZGJnc3RhdCI6ImRpc2FibGVkLXNpbmNlLWJvb3QiLCJlYXRfbm9uY2UiOlsiTk9OQ0VfMSIsIk5PTkNFXzIiXSwiZWF0X3Byb2ZpbGUiOiJodHRwczovL2Nsb3VkLmdvb2dsZS5jb20vY29uZmlkZW50aWFsLWNvbXB1dGluZy9jb25maWRlbnRpYWwtc3BhY2UvZG9jcy9yZWZlcmVuY2UvdG9rZW4tY2xhaW1zIiwiZXhwIjoxNzIxMzMwMDc1LCJnb29nbGVfc2VydmljZV9hY2NvdW50cyI6WyJQUk9KRUNUX0lELWNvbXB1dGVAZGV2ZWxvcGVyLmdzZXJ2aWNlYWNjb3VudC5jb20iXSwiaHdtb2RlbCI6IkdDUF9BTURfU0VWIiwiaWF0IjoxNzIxMzI2NDc1LCJpc3MiOiJodHRwczovL2NvbmZpZGVudGlhbGNvbXB1dGluZy5nb29nbGVhcGlzLmNvbSIsIm5iZiI6MTcyMTMyNjQ3NSwib2VtaWQiOjExMTI5LCJzZWNib290Ijp0cnVlLCJzdWIiOiJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9jb21wdXRlL3YxL3Byb2plY3RzL1BST0pFQ1RfSUQvem9uZXMvdXMtY2VudHJhbDEtYS9pbnN0YW5jZXMvSU5TVEFOQ0VfTkFNRSIsInN1Ym1vZHMiOnsiY29uZmlkZW50aWFsX3NwYWNlIjp7Im1vbml0b3JpbmdfZW5hYmxlZCI6eyJtZW1vcnkiOmZhbHNlfSwic3VwcG9ydF9hdHRyaWJ1dGVzIjpbIkxBVEVTVCIsIlNUQUJMRSIsIlVTQUJMRSJdfSwiY29udGFpbmVyIjp7ImFyZ3MiOlsiL2N1c3RvbW5vbmNlIiwiL2RvY2tlci1lbnRyeXBvaW50LnNoIiwibmdpbngiLCItZyIsImRhZW1vbiBvZmY7Il0sImVudiI6eyJIT1NUTkFNRSI6IkhPU1RfTkFNRSIsIk5HSU5YX1ZFUlNJT04iOiIxLjI3LjAiLCJOSlNfUkVMRUFTRSI6IjJ-Ym9va3dvcm0iLCJOSlNfVkVSU0lPTiI6IjAuOC40IiwiUEFUSCI6Ii91c3IvbG9jYWwvc2JpbjovdXNyL2xvY2FsL2JpbjovdXNyL3NiaW46L3Vzci9iaW46L3NiaW46L2JpbiIsIlBLR19SRUxFQVNFIjoiMn5ib29rd29ybSJ9LCJpbWFnZV9kaWdlc3QiOiJzaGEyNTY6Njc2ODJiZGE3NjlmYWUxY2NmNTE4MzE5MmI4ZGFmMzdiNjRjYWU5OWM2YzMzMDI2NTBmNmY4YmY1ZjBmOTVkZiIsImltYWdlX2lkIjoic2hhMjU2OmZmZmZmYzkwZDM0M2NiY2IwMWE1MDMyZWRhYzg2ZGI1OTk4YzUzNmNkMGEzNjY1MTQxMjFhNDVjNjcyMzc2NWMiLCJpbWFnZV9yZWZlcmVuY2UiOiJkb2NrZXIuaW8vbGlicmFyeS9uZ2lueDpsYXRlc3QiLCJpbWFnZV9zaWduYXR1cmVzIjpbeyJrZXlfaWQiOiI8aGV4YWRlY2ltYWwtc2hhMjU2LWZpbmdlcnByaW50LXB1YmxpYy1rZXkxPiIsInNpZ25hdHVyZSI6IjxiYXNlNjQtZW5jb2RlZC1zaWduYXR1cmU-Iiwic2lnbmF0dXJlX2FsZ29yaXRobSI6IlJTQVNTQV9QU1NfU0hBMjU2In0seyJrZXlfaWQiOiI8aGV4YWRlY2ltYWwtc2hhMjU2LWZpbmdlcnByaW50LXB1YmxpYy1rZXkyPiIsInNpZ25hdHVyZSI6IjxiYXNlNjQtZW5jb2RlZC1zaWduYXR1cmU-Iiwic2lnbmF0dXJlX2FsZ29yaXRobSI6IlJTQVNTQV9QU1NfU0hBMjU2In0seyJrZXlfaWQiOiI8aGV4YWRlY2ltYWwtc2hhMjU2LWZpbmdlcnByaW50LXB1YmxpYy1rZXkzPiIsInNpZ25hdHVyZSI6IjxiYXNlNjQtZW5jb2RlZC1zaWduYXR1cmU-Iiwic2lnbmF0dXJlX2FsZ29yaXRobSI6IkVDRFNBX1AyNTZfU0hBMjU2In1dLCJyZXN0YXJ0X3BvbGljeSI6Ik5ldmVyIn0sImdjZSI6eyJpbnN0YW5jZV9pZCI6IklOU1RBTkNFX0lEIiwiaW5zdGFuY2VfbmFtZSI6IklOU1RBTkNFX05BTUUiLCJwcm9qZWN0X2lkIjoiUFJPSkVDVF9JRCIsInByb2plY3RfbnVtYmVyIjoiUFJPSkVDVF9OVU1CRVIiLCJ6b25lIjoidXMtY2VudHJhbDEtYSJ9fSwic3duYW1lIjoiQ09ORklERU5USUFMX1NQQUNFIiwic3d2ZXJzaW9uIjpbIjI0MDUwMCJdfQ.29V71ymnt7LY5Ny6OJFb9AClT4XNLPi0TIcddKDp5pk<SIGNATURE>
以下是先前範例的解碼版本:
{
"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"
]
}
如要進一步瞭解認證權杖欄位,請參閱「認證權杖聲明」。
擷取認證權杖
如要在 Confidential Space 環境中實作認證權杖,請完成下列步驟:
在工作負載中設定 HTTP 用戶端。
在工作負載中,使用 HTTP 用戶端發出 HTTP 要求至接聽網址
http://localhost/v1/token
,透過 Unix 網域通訊端。 通訊端檔案位於/run/container_launcher/teeserver.sock
。
當對監聽網址提出要求時,Confidential Space 啟動器會管理認證證據收集作業、向認證服務要求認證權杖 (傳遞任何自訂參數),然後將產生的權杖傳回給工作負載。
下列 Go 程式碼範例說明如何透過 IPC 與啟動器的 HTTP 伺服器通訊。
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
}
要求自訂目標對象的認證權杖
HTTP 方法和網址:
POST http://localhost/v1/token
JSON 要求內文:
{
"audience": "AUDIENCE_NAME",
"token_type": "TOKEN_TYPE",
"nonces": [
"NONCE_1",
"NONCE_2",
...
]
}
提供以下這些值:
AUDIENCE_NAME
:必填。您的目標對象值,也就是您為信賴方指定的名稱。這項設定是由工作負載指定。如果權杖沒有自訂目標對象,這個欄位預設為
https://sts.google.com
。設定自訂目標對象時,無法使用https://sts.google.com
值。長度上限為 512 個位元組。如要在權杖中加入自訂目標對象,工作負載 (而非信賴方) 必須先將目標對象加入驗證權杖要求,再將要求傳送至 Confidential Space 驗證服務。這有助於防止信賴方要求存取不應存取的受保護資源權杖。
TOKEN_TYPE
:必填。要傳回的權杖類型。 選擇下列其中一種類型:OIDC
:系統會根據 OIDC 權杖驗證端點的jwks_uri
欄位中指定的公開金鑰驗證這些權杖。公開金鑰會定期輪替。PKI
:系統會根據 PKI 權杖驗證端點的root_ca_uri
欄位中指定的根憑證,驗證這些權杖。您必須自行儲存這份憑證。憑證每 10 年輪換一次。
由於系統會使用長期有效的憑證,而非短期有效的公開金鑰來驗證權杖,因此您的 IP 位址不會經常暴露在 Google 伺服器中。也就是說,PKI 權杖比 OIDC 權杖更能保護隱私。
您可以使用 OpenSSL 驗證憑證的指紋:
openssl x509 -fingerprint -in confidential_space_root.crt
指紋應符合下列 SHA-1 摘要:
B9:51:20:74:2C:24:E3:AA:34:04:2E:1C:3B:A3:AA:D2:8B:21:23:21
NONCE
:選用。不重複的隨機不透明值,確保權杖只能使用一次。此值由信賴方設定。最多可使用六個隨機碼。每個隨機數必須介於 10 到 74 個位元組之間 (含首尾)。加入隨機值時,依賴方必須驗證認證權杖要求中傳送的隨機值,是否與傳回權杖中的隨機值相同。如果不一致,信賴方必須拒絕符記。
剖析及驗證認證權杖
下列 Go 程式碼範例說明如何驗證認證權杖。
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))
}
PKI 認證權杖
如要驗證權杖,依賴方必須完成下列步驟:
剖析權杖的標頭,取得憑證鏈。
根據儲存的根憑證驗證憑證鏈結。您必須事先從 PKI 權杖驗證端點傳回的
root_ca_uri
欄位指定網址下載根憑證。檢查葉子憑證的有效性。
使用葉子憑證驗證權杖簽章,並在標頭中使用
alg
金鑰中指定的演算法。
權杖通過驗證後,信任方就能剖析權杖的聲明。
// 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 資源
您可以使用 AWS 主體標記,將 Confidential Space 工作負載與 AWS 資源 (例如金鑰或資料) 整合。這項整合功能會使用 Confidential Space 提供的安全認證,授予 AWS 資源的精細存取權。
AWS 主體標記聲明
Google Cloud 驗證會產生可驗證的身分識別權杖,其中包含機密空間工作負載完整性和設定的聲明。其中一部分聲明與 AWS 相容,可讓您控管 AWS 資源的存取權。這些聲明會放在認證權杖的 principal_tags
物件中,也就是 https://aws.amazon.com/tags
聲明。詳情請參閱「AWS 主體標記聲明」。
以下是 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"
]
}
}
}
含有容器映像檔簽章聲明的 AWS 政策
AWS 權杖也支援容器映像檔簽章聲明。如果工作負載變更頻率很高,或是需要與多位協作者或信賴方合作,這些聲明就非常實用。
容器映像檔簽章聲明包含以分隔符號分隔的金鑰 ID。如要在 AWS 權杖中加入這些聲明,您需要在權杖要求中提供這些金鑰 ID 的允許清單,做為額外參數。
只有與用於簽署工作負載的金鑰相符的金鑰 ID,才會新增至權杖。確保系統只接受授權簽章。
編寫 AWS 政策時,請注意金鑰 ID 會以單一字串的形式新增至權杖,並以分隔字元分隔。您需要依字母順序排序預期的金鑰 ID 清單,並建構字串值。舉例來說,如果您有金鑰 ID aKey1
、zKey2
和 bKey3
,政策中的對應聲明值應為 aKey1=bKey3=zKey2
。
如要支援多組鍵,您可以選擇在政策中新增多個值。
"aws:RequestTag/container.signatures.key_ids": [
"aKey1=bKey3=zKey2",
"aKey1=bKey3",
"zKey2"
]
容器映像檔簽章聲明 (container.signatures.key_ids
) 和容器映像檔摘要聲明 (container.image_digest
) 不會同時出現在單一權杖中。如果您使用 container.signatures.key_ids
,請務必從 AWS 政策中移除所有對 container.image_digest
的參照。
以下是包含 container.signatures.key_ids
的 https://aws.amazon.com/tags
聲明結構範例:
{
"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"
]
}
}
}
如要進一步瞭解驗證權杖欄位,請參閱「驗證權杖聲明」。
設定 AWS 資源:信賴憑證者
信賴方必須先設定 AWS IAM,將 Confidential Space 設為同盟 OIDC 提供者,並建立必要的 AWS IAM 角色,才能設定 AWS 資源。
設定 AWS IAM
如要在 AWS IAM 中新增 Google Cloud Attestation 服務做為身分識別提供者,請按照下列步驟操作:
在 AWS 控制台中,前往「Identity providers」(身分識別提供者) 頁面。
在「供應商類型」部分,選取「OpenID Connect」。
在「供應商網址」中,輸入 https://confidentialcomputing.googleapis.com。
在「Audience」中,輸入您向身分識別提供者註冊的網址,該網址會向 AWS 發出要求。例如:https://example.com。
按一下「新增提供者」。
如要為 Confidential Space 權杖建立 AWS IAM 角色,請按照下列步驟操作:
- 前往 AWS 控制台的「Roles」(角色) 頁面。
- 按一下「建立角色」。
- 在「受信任的實體」類型中,選取「網路身分」。
- 在「Web identity」部分,根據上一個步驟選取身分識別提供者和目標對象。
- 點選「下一步」。您可以略過這個步驟,稍後再編輯 AWS 政策。
- 按一下「下一步」,然後視需要新增標記。
- 在「角色名稱」中輸入角色名稱。
- (選用) 輸入新角色的說明。
- 檢查詳細資料,然後按一下「建立角色」。
編輯您建立的角色 AWS 政策,僅授予所選工作負載的存取權。
這項 AWS 政策可讓您檢查權杖中的特定聲明,例如:
- 工作負載的容器映像檔摘要。
- 權杖的目標對象。
CONFIDENTIAL_SPACE
:這是 VM 上執行的軟體。詳情請參閱「swname
」的「認證權杖聲明」。- 生產機密空間圖片支援屬性。詳情請參閱
confidential_space.support_attributes
。
以下是 AWS 政策範例,可授予工作負載存取權,並指定摘要和對象,
CONFIDENTIAL_SPACE
做為 VM 執行個體上執行的軟體,STABLE
做為支援屬性:{ "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 資源
整合完成後,請設定 AWS 資源。這個步驟取決於您的具體用途。舉例來說,您可能會建立 S3 值區、KMS 金鑰或其他 AWS 資源。請務必授予先前建立的 AWS IAM 角色存取這些資源的必要權限。
設定 Confidential Space 工作負載:工作負載作者
如要建立權杖要求,請按照「使用自訂目標對象要求認證權杖」一文的說明操作。
AWS_PrincipalTag
項聲明:
- 在 AWS 整合的權杖要求中,Nonce 欄位為選填。
- 在「Configure AWS Resources: Relying Party」(設定 AWS 資源:信賴憑證者) 中,加入您設定的目標對象。
- 將 token_type 設為
AWS_PRINCIPALTAGS
。
以下是 AWS_PrincipalTag
聲明要求主體的範例:
body := `{
"audience": "https://example.com",
"token_type": "AWS_PRINCIPALTAGS",
}`
後續步驟
- 如要進一步瞭解認證權杖聲明,請參閱認證權杖聲明。