保護されたリソースへのアクセスが Google Cloudの IAM によって管理されていない場合(リソースが別のクラウド サービス、オンプレミス、または携帯電話などのローカル デバイスに保存されている場合など)、Confidential Space ワークロードを、これらのリソースへのアクセスを提供するデバイスまたはシステム(証明書利用者とも呼ばれます)に対して認証できます。
これを行うには、カスタム オーディエンスとオプションのノンスを使用して、Confidential Space 構成証明サービスから構成証明トークンをリクエストする必要があります。このような証明書トークンをリクエストする場合は、リソースへのアクセスを許可する前に、独自のトークン検証を行う必要があります。
以降のドキュメントでは、 Google Cloud以外のリソースで Confidential Space を使用する際のコンセプトについて説明します。これには、Confidential Space ワークロードを AWS リソースと統合する手順も含まれます。エンドツーエンドのチュートリアルについては、Codelab をご覧ください。
証明書トークン フロー
構成証明トークンは、依存関係パーティに代わってワークロードによってリクエストされ、構成証明サービスによって返されます。必要に応じて、カスタム オーディエンスを定義し、必要に応じてノンスを指定できます。
暗号化なし
トークン取得プロセスをわかりやすくするために、ここで示すフローでは暗号化を使用していません。実際には、TLS で通信を暗号化することをおすすめします。
次の図は、このフローを示しています。
証明書利用者は、生成したオプションのノンスとともに、トークン リクエストをワークロードに送信します。
ワークロードはオーディエンスを決定し、リクエストにオーディエンスを追加して、リクエストを Confidential Space ランチャーに送信します。
ランチャーが構成証明サービスにリクエストを送信します。
構成証明サービスは、指定されたオーディエンスとオプションのノンスを含むトークンを生成します。
構成証明サービスがランチャーにトークンを返します。
ランチャーはトークンをワークロードに返します。
ワークロードは、証明書利用者にトークンを返します。
証明書利用者は、オーディエンスやオプションのノンスなどのクレームを検証します。
TLS で暗号化されている
暗号化されていないフローでは、リクエストが中間者攻撃に対して脆弱になります。ノンスはデータ出力や TLS セッションにバインドされていないため、攻撃者はリクエストを傍受してワークロードを偽装できます。
この種の攻撃を防ぐには、証明書利用者とワークロードの間に TLS セッションを設定し、TLS 鍵マテリアル(EKM)を nonce として使用します。TLS エクスポート鍵マテリアルは、構成証明を TLS セッションにバインドし、構成証明リクエストが安全なチャネル経由で送信されたことを確認します。このプロセスは、チャンネル バインディングとも呼ばれます。
次の図は、チャネル バインディングを使用したフローを示しています。
証明書利用者は、ワークロードを実行している Confidential VM と安全な TLS セッションを確立します。
証明書利用者は、安全な TLS セッションを使用してトークン リクエストを送信します。
ワークロードは、TLS エクスポート鍵マテリアルを使用してオーディエンスを決定し、ノンスを生成します。
ワークロードは Confidential Space ランチャーにリクエストを送信します。
ランチャーが構成証明サービスにリクエストを送信します。
構成証明サービスは、指定されたオーディエンスとノンスを含むトークンを生成します。
構成証明サービスがランチャーにトークンを返します。
ランチャーはトークンをワークロードに返します。
ワークロードは、証明書利用者にトークンを返します。
証明書利用者側は、TLS エクスポート鍵マテリアルを使用して nonce を再生成します。
証明書利用者は、オーディエンスやノンスなどのクレームを検証します。トークンのノンスは、証明書利用者が再生成したノンスと一致する必要があります。
構成証明トークンの構造
構成証明トークンは、次の構造の JSON ウェブトークンです。
ヘッダー: 署名アルゴリズムを記述します。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 クライアントを使用して、Unix ドメイン ソケット経由でリスニング URL
http://localhost/v1/tokenに HTTP リクエストを送信します。ソケット ファイルは/run/container_launcher/teeserver.sockにあります。
リスニング URL にリクエストが送信されると、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 メソッドと URL:
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:21NONCE: 省略可。トークンが 1 回しか使用されないようにする、一意のランダムな不透明な値。値は証明書利用者によって設定されます。最大 6 つのノンスを使用できます。各ノンスは 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フィールドで指定された URL から、ルート証明書を事前にダウンロードしておく必要があります。リーフ証明書の有効性を確認します。
リーフ証明書を使用して、ヘッダーの
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 Attestation は、Confidential Space ワークロードの完全性と構成に関するクレームを含む検証可能な ID トークンを生成します。これらのクレームのサブセットは 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)が 1 つのトークンに同時に表示されることはありません。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 サービスを ID プロバイダとして追加するには、次の操作を行います。
AWS コンソールで、[ID プロバイダ] ページに移動します。
[プロバイダ タイプ] で、[OpenID Connect] を選択します。
[プロバイダ URL] に「https://confidentialcomputing.googleapis.com」と入力します。
[Audience] に、ID プロバイダに登録され、AWS にリクエストを行う URL を入力します。例: https://example.com。
[プロバイダを追加] をクリックします。
Confidential Space トークンの AWS IAM ロールを作成するには、次の操作を行います。
AWS コンソールで、[ロール] ページに移動します。
[ロールを作成] をクリックします。
[信頼できるエンティティ] タイプで、[ウェブ ID] を選択します。
[ウェブ ID] セクションで、前の手順に基づいて ID プロバイダとオーディエンスを選択します。
[次へ] をクリックします。この手順では、AWS ポリシーの編集をスキップできます。
[次へ] をクリックし、必要に応じてタグを追加します。
[ロール名] にロール名を入力します。
(省略可)[説明] に、新しいロールの説明を入力します。
詳細を確認し、[ロールを作成] をクリックします。
作成したロールの AWS ポリシーを編集して、選択したワークロードへのアクセス権のみを付与します。
この AWS ポリシーを使用すると、トークン内の特定のクレームを確認できます。
ワークロードのコンテナ イメージ ダイジェスト。
トークンの対象オーディエンス。
この
CONFIDENTIAL_SPACEは、VM で実行されているソフトウェアです。詳細については、証明書トークン クレームのswnameをご覧ください。本番環境の Confidential Space イメージのサポート属性。詳細については、
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 フィールドは省略可能です。
AWS リソースを構成する: 信頼当事者で構成したオーディエンスを含めます。
token_type を
AWS_PRINCIPALTAGSに設定します。
AWS_PrincipalTag クレーム リクエスト本文の例を次に示します。
body := `{
"audience": "https://example.com",
"token_type": "AWS_PRINCIPALTAGS",
}`
次のステップ
証明書トークン クレームの詳細については、証明書トークン クレームをご覧ください。