如果受保护资源的访问权限不是由 Google Cloud的 IAM 管理的(例如,资源存储在其他云服务、本地或本地设备(如手机)上),您仍然可以对提供这些资源访问权限的设备或系统(也称为信赖方)进行 Confidential Space 工作负载身份验证。
为此,信赖方必须从认证服务(例如 Google Cloud Attestation)接收具有自定义受众群体和可选随机数的认证令牌。在请求此类证明令牌时,信赖方可能需要先执行自己的令牌验证,然后才能授予对资源的访问权限。
以下文档介绍了将 Confidential Space 与 Google Cloud外部资源搭配使用所涉及的概念,包括将 Confidential Space 工作负载与 AWS 资源集成在一起的说明。如需查看端到端演练,请参阅 Codelab。
认证令牌流程
证明令牌由工作负载代表信赖方请求,并由证明服务返回。您可以根据需要定义自定义受众群体,并可选择性地提供随机数。
未加密
为了便于理解令牌检索流程,此处显示的流程未使用加密。在实践中,我们建议您使用 TLS 加密通信。
下图展示了该流程:
信赖方将令牌请求发送给工作负载,其中包含它生成的可选随机数。
工作负载确定目标对象,将目标对象添加到请求中,然后将请求发送到 Confidential Space 启动器。
启动器将请求发送到证明服务。
证明服务会生成一个包含指定受众群体和可选 Nonce 的令牌。
证明服务将令牌返回给启动器。
启动器将令牌返回给工作负载。
工作负载将令牌返回给依赖方。
信赖方验证声明,包括目标对象和可选的 nonce。
通过 TLS 加密
未加密的流量会使请求容易受到中间人攻击。由于随机数未绑定到数据输出或 TLS 会话,攻击者可以拦截请求并冒充工作负载。
为帮助防止此类攻击,您可以在信赖方和工作负载之间设置 TLS 会话,并使用 TLS 导出的密钥材料 (EKM) 作为随机数。TLS 导出的密钥材料将证明与 TLS 会话绑定,并确认证明请求是通过安全信道发送的。此过程也称为渠道绑定。
下图显示了使用渠道绑定的流程:
信赖方与运行工作负载的机密虚拟机建立安全的 TLS 会话。
信赖方使用安全的 TLS 会话发送令牌请求。
工作负载确定受众群体,并使用 TLS 导出的密钥材料生成随机数。
工作负载将请求发送到 Confidential Space 启动器。
启动器将请求发送到证明服务。
证明服务会生成一个包含指定受众群体和 nonce 的令牌。
证明服务将令牌返回给启动器。
启动器将令牌返回给工作负载。
工作负载将令牌返回给依赖方。
信赖方使用 TLS 导出的密钥材料重新生成随机数。
信赖方验证声明,包括受众群体和随机数。令牌中的随机数必须与依赖方重新生成的随机数一致。
证明令牌结构
证明令牌是具有以下结构的 JSON Web 令牌:
标头:描述签名算法。PKI 令牌还会将证书链存储在标头的
x5c字段中。已签名的 JSON 数据载荷:包含有关依赖方工作负载的声明,例如主题、签发者、受众群体、随机数和过期时间。
签名:用于验证令牌在传输过程中是否发生变化。如需详细了解如何使用签名,请参阅如何验证 OpenID Connect ID 令牌。
以下代码示例展示了一个已移除签名的编码后的证明令牌。您可以使用 https://jwt.io/ 对令牌进行解码。
该令牌由 Confidential Space 240500 映像和 Google Cloud Attestation 生成。较新的映像可能包含其他字段。
eyJhbGciOiJIUzI1NiIsImtpZCI6IjEyMzQ1IiwidHlwIjoiSldUIn0.eyJhdWQiOiJBVURJRU5DRV9OQU1FIiwiZGJnc3RhdCI6ImRpc2FibGVkLXNpbmNlLWJvb3QiLCJlYXRfbm9uY2UiOlsiTk9OQ0VfMSIsIk5PTkNFXzIiXSwiZWF0X3Byb2ZpbGUiOiJodHRwczovL2Nsb3VkLmdvb2dsZS5jb20vY29uZmlkZW50aWFsLWNvbXB1dGluZy9jb25maWRlbnRpYWwtc3BhY2UvZG9jcy9yZWZlcmVuY2UvdG9rZW4tY2xhaW1zIiwiZXhwIjoxNzIxMzMwMDc1LCJnb29nbGVfc2VydmljZV9hY2NvdW50cyI6WyJQUk9KRUNUX0lELWNvbXB1dGVAZGV2ZWxvcGVyLmdzZXJ2aWNlYWNjb3VudC5jb20iXSwiaHdtb2RlbCI6IkdDUF9BTURfU0VWIiwiaWF0IjoxNzIxMzI2NDc1LCJpc3MiOiJodHRwczovL2NvbmZpZGVudGlhbGNvbXB1dGluZy5nb29nbGVhcGlzLmNvbSIsIm5iZiI6MTcyMTMyNjQ3NSwib2VtaWQiOjExMTI5LCJzZWNib290Ijp0cnVlLCJzdWIiOiJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9jb21wdXRlL3YxL3Byb2plY3RzL1BST0pFQ1RfSUQvem9uZXMvdXMtY2VudHJhbDEtYS9pbnN0YW5jZXMvSU5TVEFOQ0VfTkFNRSIsInN1Ym1vZHMiOnsiY29uZmlkZW50aWFsX3NwYWNlIjp7Im1vbml0b3JpbmdfZW5hYmxlZCI6eyJtZW1vcnkiOmZhbHNlfSwic3VwcG9ydF9hdHRyaWJ1dGVzIjpbIkxBVEVTVCIsIlNUQUJMRSIsIlVTQUJMRSJdfSwiY29udGFpbmVyIjp7ImFyZ3MiOlsiL2N1c3RvbW5vbmNlIiwiL2RvY2tlci1lbnRyeXBvaW50LnNoIiwibmdpbngiLCItZyIsImRhZW1vbiBvZmY7Il0sImVudiI6eyJIT1NUTkFNRSI6IkhPU1RfTkFNRSIsIk5HSU5YX1ZFUlNJT04iOiIxLjI3LjAiLCJOSlNfUkVMRUFTRSI6IjJ-Ym9va3dvcm0iLCJOSlNfVkVSU0lPTiI6IjAuOC40IiwiUEFUSCI6Ii91c3IvbG9jYWwvc2JpbjovdXNyL2xvY2FsL2JpbjovdXNyL3NiaW46L3Vzci9iaW46L3NiaW46L2JpbiIsIlBLR19SRUxFQVNFIjoiMn5ib29rd29ybSJ9LCJpbWFnZV9kaWdlc3QiOiJzaGEyNTY6Njc2ODJiZGE3NjlmYWUxY2NmNTE4MzE5MmI4ZGFmMzdiNjRjYWU5OWM2YzMzMDI2NTBmNmY4YmY1ZjBmOTVkZiIsImltYWdlX2lkIjoic2hhMjU2OmZmZmZmYzkwZDM0M2NiY2IwMWE1MDMyZWRhYzg2ZGI1OTk4YzUzNmNkMGEzNjY1MTQxMjFhNDVjNjcyMzc2NWMiLCJpbWFnZV9yZWZlcmVuY2UiOiJkb2NrZXIuaW8vbGlicmFyeS9uZ2lueDpsYXRlc3QiLCJpbWFnZV9zaWduYXR1cmVzIjpbeyJrZXlfaWQiOiI8aGV4YWRlY2ltYWwtc2hhMjU2LWZpbmdlcnByaW50LXB1YmxpYy1rZXkxPiIsInNpZ25hdHVyZSI6IjxiYXNlNjQtZW5jb2RlZC1zaWduYXR1cmU-Iiwic2lnbmF0dXJlX2FsZ29yaXRobSI6IlJTQVNTQV9QU1NfU0hBMjU2In0seyJrZXlfaWQiOiI8aGV4YWRlY2ltYWwtc2hhMjU2LWZpbmdlcnByaW50LXB1YmxpYy1rZXkyPiIsInNpZ25hdHVyZSI6IjxiYXNlNjQtZW5jb2RlZC1zaWduYXR1cmU-Iiwic2lnbmF0dXJlX2FsZ29yaXRobSI6IlJTQVNTQV9QU1NfU0hBMjU2In0seyJrZXlfaWQiOiI8aGV4YWRlY2ltYWwtc2hhMjU2LWZpbmdlcnByaW50LXB1YmxpYy1rZXkzPiIsInNpZ25hdHVyZSI6IjxiYXNlNjQtZW5jb2RlZC1zaWduYXR1cmU-Iiwic2lnbmF0dXJlX2FsZ29yaXRobSI6IkVDRFNBX1AyNTZfU0hBMjU2In1dLCJyZXN0YXJ0X3BvbGljeSI6Ik5ldmVyIn0sImdjZSI6eyJpbnN0YW5jZV9pZCI6IklOU1RBTkNFX0lEIiwiaW5zdGFuY2VfbmFtZSI6IklOU1RBTkNFX05BTUUiLCJwcm9qZWN0X2lkIjoiUFJPSkVDVF9JRCIsInByb2plY3RfbnVtYmVyIjoiUFJPSkVDVF9OVU1CRVIiLCJ6b25lIjoidXMtY2VudHJhbDEtYSJ9fSwic3duYW1lIjoiQ09ORklERU5USUFMX1NQQUNFIiwic3d2ZXJzaW9uIjpbIjI0MDUwMCJdfQ.29V71ymnt7LY5Ny6OJFb9AClT4XNLPi0TIcddKDp5pk
以下是上一个示例的解码版本:
{
"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 客户端通过位于
/run/container_launcher/teeserver.sock的 Unix 网域套接字向监听网址发出 HTTP 请求。监听网址会根据您使用的证明服务而变化:Google Cloud Attestation:
http://localhost/v1/tokenIntel Trust Authority:
http://localhost/v1/intel/token
当向监听网址发出请求时,Confidential Space 启动器会管理证明证据收集,从证明服务请求证明令牌(传递任何自定义参数),然后将生成的令牌返回给工作负载。
以下 Go 代码示例演示了如何根据您使用的证明服务,通过 IPC 与启动器的 HTTP 服务器进行通信。
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 仅支持运行 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
}
请求具有自定义受众群体的证明令牌
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 个字节。如需在令牌中添加自定义受众群体,工作负载(而非信赖方)必须在将证明令牌请求发送给证明服务之前,将其添加到证明令牌请求中。这有助于防止信赖方请求其不应有权访问的受保护资源的令牌。
TOKEN_TYPE:必填。要返回的令牌类型。 选择以下类型之一:OIDC:这些令牌会根据 OIDC 令牌验证端点jwks_uri字段中指定的定期轮换的公钥进行验证。通过将.well-known/openid-configuration附加到颁发者 URI 来构建端点。Google Cloud Attestation:
https://confidentialcomputing.googleapis.com/.well-known/openid-configuration。Intel Trust Authority:
https://portal.trustauthority.intel.com/.well-known/openid-configuration。
PKI:这些令牌会根据 PKI 令牌验证端点的root_ca_uri字段中指定的根证书进行验证。您需要自行存储此证书。证书每 10 年轮换一次。
由于令牌验证使用的是长期限证书,而不是短期限公钥,因此您的 IP 地址不会经常暴露给 Google 服务器。这意味着,与 OIDC 令牌相比,PKI 令牌可提供更高的隐私保护。
您可以使用 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:21NONCE:可选。一个唯一、随机且不透明的值,用于验证令牌是否只能使用一次。该值由信赖方设置。最多允许 6 个随机数。每个随机数必须介于 10 到 74 字节之间(含 10 和 74 字节)。如果包含 Nonce,依赖方必须验证在证明令牌请求中发送的 Nonce 是否与返回的令牌中的 Nonce 相同。如果两者不同,信赖方必须拒绝相应令牌。
解析和验证证明令牌
以下 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 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
}
集成 AWS 资源
您可以使用 AWS 主账号标签将 Confidential Space 工作负载与 AWS 资源(例如密钥或数据)集成。此集成使用 Confidential Space 提供的安全证明来授予对 AWS 资源的精细访问权限。
AWS 主账号标记声明
Google Cloud Attestation会生成可验证的身份令牌,其中包含有关 Confidential Space 工作负载的完整性和配置的声明。这些声明的一部分与 AWS 兼容,可让您控制对 AWS 资源的访问权限。这些声明位于证明令牌的 https://aws.amazon.com/tags 声明中,即 principal_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 资源之前,他们需要配置 AWS IAM,以将 Confidential Space 建立为联合 OIDC 提供方,并创建必要的 AWS IAM 角色。
配置 AWS IAM
如需在 AWS IAM 中添加 Google Cloud Attestation 服务作为身份提供方,请执行以下操作:
在 AWS 控制台中,前往身份提供商页面。
对于提供方类型,选择 OpenID Connect。
对于提供方网址,请输入 https://confidentialcomputing.googleapis.com。
对于受众群体,请输入您向身份提供方注册的且向 AWS 发出请求的网址。例如,https://example.com。
点击添加提供商。
如需为 Confidential Space 令牌创建 AWS IAM 角色,请执行以下操作:
在 AWS 控制台中,前往角色页面。
点击 Create role。
对于可信实体类型,选择 Web 身份。
在网络身份部分下,根据上一步选择身份提供方和受众群体。
点击下一步。您可以在此步骤中跳过修改 AWS 政策。
点击下一步,然后根据需要添加标记。
在角色名称中,输入角色名称。
可选:在说明中,输入新角色的说明。
查看详细信息,然后点击创建角色。
修改您创建的角色的 AWS 政策,以仅授予对所选工作负载的访问权限。
借助此 AWS 政策,您可以检查令牌中的特定声明,例如:
工作负载的容器映像摘要。
令牌的目标受众群体。
该
CONFIDENTIAL_SPACE是在虚拟机上运行的软件。如需了解详情,请参阅证明令牌声明中的swname。生产 Confidential Space 映像支持特性。如需了解详情,请参阅
confidential_space.support_attributes。
以下是一个 AWS 政策示例,该政策授予对具有指定摘要和受众群体的工作负载的访问权限,其中
CONFIDENTIAL_SPACE是在虚拟机实例上运行的软件,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 字段是可选的。
纳入您在配置 AWS 资源:信赖方中配置的受众群体。
将 token_type 设置为
AWS_PRINCIPALTAGS。
以下是 AWS_PrincipalTag 声明请求正文的示例:
body := `{
"audience": "https://example.com",
"token_type": "AWS_PRINCIPALTAGS",
}`
后续步骤
如需详细了解证明令牌声明,请参阅证明令牌声明。