存取非由 Google Cloud IAM 管理的資源


如果受保護資源的存取權不是由 Google Cloud的 IAM 管理,例如資源儲存在其他雲端服務、內部部署環境或本機裝置 (例如手機),您仍可向提供這些資源存取權的裝置或系統 (又稱為信賴方) 驗證 Confidential Space 工作負載。

為此,信賴方必須向 Confidential Space 驗證服務要求驗證權杖,並提供自訂目標對象和選用隨機數。要求這類認證權杖時,您必須先自行驗證權杖,才能授予資源存取權。

以下說明文件涵蓋使用 Confidential Space 和 Google Cloud以外資源的相關概念,包括如何順暢整合 Confidential Space 工作負載與 AWS 資源的說明。如需完整逐步說明,請參閱codelab

認證權杖流程

工作負載會代表信賴方要求認證權杖,並由認證服務傳回。您可以視需求定義自訂目標對象,並視需要提供隨機碼。

未加密

為方便瞭解權杖擷取程序,這裡顯示的流程不會使用加密功能。實務上,建議您使用 TLS 加密通訊。

下圖顯示流程:

認證權杖產生流程圖

  1. 信賴方會將權杖要求傳送至工作負載,並視需要傳送產生的隨機值。

  2. 工作負載會決定目標對象、將目標對象新增至要求,並將要求傳送至 Confidential Space 啟動器。

  3. 啟動器會將要求傳送至認證服務。

  4. 認證服務會產生權杖,其中包含指定的對象和選用的隨機數。

  5. 認證服務會將權杖傳回啟動器。

  6. 啟動器會將權杖傳回工作負載。

  7. 工作負載會將權杖傳回給信任方。

  8. 信賴方會驗證聲明,包括目標對象和選用的隨機數。

透過 TLS 加密

未加密的流程會讓要求容易受到中間人攻擊。由於隨機數未繫結至資料輸出內容或 TLS 工作階段,攻擊者可以攔截要求並模擬工作負載。

為防範這類攻擊,您可以在憑證授權單位和工作負載之間設定 TLS 工作階段,並使用 TLS 匯出金鑰材料 (EKM) 做為隨機數。TLS 匯出的金鑰內容會將認證繫結至 TLS 工作階段,並確認認證要求是透過安全管道傳送。這項程序也稱為「管道繫結」

下圖顯示使用管道繫結的流程:

管道繫結權杖產生流程圖

  1. 憑證核發機構會與執行工作負載的機密 VM 建立安全的 TLS 工作階段。

  2. 憑證核發機構會透過安全的 TLS 工作階段傳送符記要求。

  3. 工作負載會決定目標對象,並使用 TLS 匯出的金鑰內容產生隨機數。

  4. 工作負載會將要求傳送至 Confidential Space 啟動器。

  5. 啟動器會將要求傳送至認證服務。

  6. 認證服務會產生權杖,其中包含指定的對象和隨機數。

  7. 認證服務會將權杖傳回啟動器。

  8. 啟動器會將權杖傳回工作負載。

  9. 工作負載會將權杖傳回給信任方。

  10. 憑證核發機構會使用 TLS 匯出的金鑰材料重新產生隨機數。

  11. 信賴方會驗證聲明,包括目標對象和隨機值。權杖中的隨機碼必須與依賴方重新產生的隨機碼相符。

認證權杖結構

認證權杖是 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 環境中實作認證權杖,請完成下列步驟:

  1. 在工作負載中設定 HTTP 用戶端。

  2. 在工作負載中,使用 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 認證權杖

如要驗證權杖,依賴方必須完成下列步驟:

  1. 剖析權杖的標頭,取得憑證鏈。

  2. 根據儲存的根憑證驗證憑證鏈結。您必須事先從 PKI 權杖驗證端點傳回的 root_ca_uri 欄位指定網址下載根憑證。

  3. 檢查葉子憑證的有效性。

  4. 使用葉子憑證驗證權杖簽章,並在標頭中使用 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 aKey1zKey2bKey3,政策中的對應聲明值應為 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_idshttps://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

  1. 如要在 AWS IAM 中新增 Google Cloud Attestation 服務做為身分識別提供者,請按照下列步驟操作:

    1. 在 AWS 控制台中,前往「Identity providers」(身分識別提供者) 頁面

      前往 AWS 控制台

    2. 在「供應商類型」部分,選取「OpenID Connect」

    3. 在「供應商網址」中,輸入 https://confidentialcomputing.googleapis.com。

    4. 在「Audience」中,輸入您向身分識別提供者註冊的網址,該網址會向 AWS 發出要求。例如:https://example.com。

    5. 按一下「新增提供者」

  2. 如要為 Confidential Space 權杖建立 AWS IAM 角色,請按照下列步驟操作:

    1. 前往 AWS 控制台的「Roles」(角色) 頁面。
    2. 按一下「建立角色」
    3. 在「受信任的實體」類型中,選取「網路身分」
    4. 在「Web identity」部分,根據上一個步驟選取身分識別提供者和目標對象。
    5. 點選「下一步」。您可以略過這個步驟,稍後再編輯 AWS 政策。
    6. 按一下「下一步」,然後視需要新增標記。
    7. 在「角色名稱」中輸入角色名稱。
    8. (選用) 輸入新角色的說明
    9. 檢查詳細資料,然後按一下「建立角色」
  3. 編輯您建立的角色 AWS 政策,僅授予所選工作負載的存取權。

    這項 AWS 政策可讓您檢查權杖中的特定聲明,例如:

    以下是 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",
      }`

後續步驟