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. 証明書利用者(RP)は、ワークロードを実行している Confidential VM と安全な TLS セッションを確立します。

  2. 証明書利用者は、安全な TLS セッションを使用してトークン リクエストを送信します。

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

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

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

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

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

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

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

  10. 証明書利用者側は、TLS エクスポート鍵マテリアルを使用してノンスを再生成します。

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

証明書トークンの構造

構成証明トークンは、次の構造の 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 環境に構成証明トークンを実装する手順は次のとおりです。

  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 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 が 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 リソースを構成する前に、AWS IAM を構成して Confidential Space をフェデレーション OIDC プロバイダとして確立し、必要な AWS IAM ロールを作成する必要があります。

AWS IAM を構成する

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

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

      AWS コンソールに移動

    2. [プロバイダ タイプ] で、[OpenID Connect] を選択します。

    3. [プロバイダ 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 ポリシーの例です。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",
}`

次のステップ

証明書トークン クレームの詳細については、証明書トークン クレームをご覧ください。