Google Cloud IAM で管理されていないリソースにアクセスする


保護されたリソースへのアクセスが Google Cloudの IAM によって管理されていない場合(リソースが別のクラウド サービス、オンプレミス、またはモバイル デバイスなどのローカル デバイスに保存されている場合など)でも、それらのリソースへのアクセスを提供するデバイスまたはシステム(信頼できるパーティ)に対して Confidential Space ワークロードを認証できます。

これを行うには、信頼する当事者が、カスタム オーディエンスとオプションのノンスを使用して、Confidential Space 構成証明サービスから構成証明トークンをリクエストする必要があります。このように構成証明トークンをリクエストする場合は、リソースへのアクセスを許可する前に、独自のトークン検証を行う必要があります。

以降のドキュメントでは、 Google Cloud以外のリソースで Confidential Space を使用する際に関連するコンセプトについて説明します。これには、Confidential Space ワークロードを AWS リソースとシームレスに統合する手順も含まれます。エンドツーエンドのチュートリアルについては、Codelab をご覧ください。

構成証明トークンのフロー

構成証明トークンは、構成証明サービスによって返されます。必要に応じて、カスタム オーディエンスを定義し、必要に応じてノンスを指定できます。

暗号化なし

トークンの取得プロセスを簡単に理解できるように、ここで説明するフローは暗号化を使用しません。実際には、TLS で通信を暗号化することをおすすめします。

次の図は、このフローを模式的に示したものです。

構成証明トークンの生成フローのフロー図

  1. リレーリング パーティは、生成したノンス(省略可)とともに、ワークロードにトークン リクエストを送信します。

  2. ワークロードはオーディエンスを決定し、オーディエンスをリクエストに追加して、リクエストを Confidential Space ランチャーに送信します。

  3. ランチャーは、構成証明サービスにリクエストを送信します。

  4. 構成証明サービスは、指定されたオーディエンスとオプションのノンスを含むトークンを生成します。

  5. 構成証明サービスは、トークンをランチャーに返します。

  6. ランチャーはトークンをワークロードに返します。

  7. ワークロードはトークンを利用者に返します。

  8. 信頼する当事者は、オーディエンスやオプションのノンスなどのクレームを検証します。

TLS で暗号化されている

暗号化されていないフローでは、リクエストが中間攻撃に対して脆弱になります。ノンスはデータ出力または TLS セッションにバインドされていないため、攻撃者はリクエストをインターセプトしてワークロードの権限を借用できます。

このタイプの攻撃を防ぐには、リレーリング パーティとワークロードの間に TLS セッションを設定し、TLS エクスポート鍵マテリアル(EKM)をノンスとして使用します。TLS でエクスポートされた鍵マテリアルは、構成証明を TLS セッションにバインドし、構成証明リクエストが保護されたチャネルを介して送信されたことを確認します。このプロセスはチャンネル バインディングとも呼ばれます。

次の図は、チャネル バインディングを使用したフローを示しています。

チャネル バインディング トークンの生成フローのフロー図

  1. リレーリング パーティは、ワークロードを実行している Confidential VM と安全な TLS セッションを設定します。

  2. 信頼できるパーティは、安全な TLS セッションを使用してトークン リクエストを送信します。

  3. ワークロードはオーディエンスを決定し、TLS エクスポートされた鍵マテリアルを使用してノンスを生成します。

  4. ワークロードは、リクエストを Confidential Space ランチャーに送信します。

  5. ランチャーは、構成証明サービスにリクエストを送信します。

  6. 構成証明サービスは、指定されたオーディエンスとノンスを含むトークンを生成します。

  7. 構成証明サービスは、トークンをランチャーに返します。

  8. ランチャーはトークンをワークロードに返します。

  9. ワークロードはトークンを利用者に返します。

  10. 信頼するパーティは、TLS でエクスポートされた鍵マテリアルを使用してノンスを再生成します。

  11. 信頼する当事者は、オーディエンスやノンスなどのクレームを確認します。トークンのノンスは、リライング パーティによって再生成されたノンスと一致する必要があります。

構成証明トークンの構造

構成証明トークンは、次の構造の JSON Web Token です。

  • Header: 署名アルゴリズムを表します。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 クライアントを使用して、Unix ドメイン ソケット経由でリッスン URL http://localhost/v1/tokenHTTP リクエストを送信します。ソケット ファイルは /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:21
    
  • NONCE: 省略可。一意のランダムな不透明な値。トークンが 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 構成証明トークン

トークンを検証するには、リライング パーティは次の手順を完了する必要があります。

  1. トークンのヘッダーを解析して証明書チェーンを取得します。

  2. 保存されているルートに対して証明書チェーンを検証します。PKI トークン検証エンドポイントで返された root_ca_uri フィールドで指定された URL からルート証明書を事前にダウンロードしておく必要があります。

  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 構成証明は、Confidential Space ワークロードの完全性と構成に関するクレームを含む検証可能な ID トークンを生成します。これらのクレームのサブセットは 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 ポリシーを作成する際は、Key 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)は、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 リソースを構成する前に、Confidential Space を連携 OIDC プロバイダとして確立するように AWS IAM を構成し、必要な AWS IAM ロールを作成する必要があります。

AWS IAM を構成する

  1. AWS IAM で Google Cloud 構成証明サービスを ID プロバイダとして追加するには、次の操作を行います。

    1. AWS コンソールで、[ID プロバイダ] ページに移動します。

      AWS コンソールに移動

    2. [Provider type] で [OpenID Connect] を選択します。

    3. [Provider URL] に「https://confidentialcomputing.googleapis.com」と入力します。

    4. [Audience] に、ID プロバイダに登録し、AWS にリクエストを行う URL を入力します。例: https://example.com。

    5. [プロバイダを追加] をクリックします。

  2. Confidential Space トークンの AWS IAM ロールを作成するには、次の操作を行います。

    1. AWS コンソールで、[ロール] ページに移動します。
    2. [Create role] をクリックします。
    3. [信頼できるエンティティ] の種類で、[ウェブ ID] を選択します。
    4. [ウェブ ID] セクションで、前の手順に基づいて ID プロバイダとオーディエンスを指定します。
    5. [次へ] をクリックします。この手順では、AWS ポリシーの編集をスキップできます。
    6. [次へ] をクリックし、必要に応じてタグを追加します。
    7. [ロール名] にロール名を入力します。
    8. (省略可)[説明] に、新しいロールの説明を入力します。
    9. 詳細を確認し、[ロールを作成] をクリックします。
  3. 作成したロールの AWS ポリシーを編集して、選択したワークロードへのアクセスのみを許可します。

    この AWS ポリシーを使用すると、トークンの特定のクレーム(以下に例を示します)を確認できます。

    • ワークロードのコンテナ イメージ ダイジェスト。
    • トークンの対象。
    • この CONFIDENTIAL_SPACE は、VM で実行されているソフトウェアです。詳細については、構成証明トークンのクレームswname をご覧ください。
    • 本番環境の Confidential Space イメージのサポート属性。詳細については、confidential_space.support_attributes をご覧ください。

    次の AWS ポリシーの例では、指定されたダイジェストとオーディエンスを持つワークロードへのアクセスを許可します。VM インスタンスで実行されているソフトウェアとして 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 統合のトークン リクエストでは、ノンス フィールドは省略可能です。
  • AWS リソースの構成: 証明書利用者で構成したオーディエンスを含めます。
  • token_type を AWS_PRINCIPALTAGS に設定します。

AWS_PrincipalTag 申し立てリクエスト本文の例を次に示します。

  body := `{
    "audience": "https://example.com",
    "token_type": "AWS_PRINCIPALTAGS",
      }`

次のステップ