クラウド プロバイダに保存されていない保護されたリソースで Confidential Space を使用する

1. 概要

Confidential Space は、安全なマルチパーティ データ共有とコラボレーションを提供し、組織がデータの機密性を維持できるようにします。つまり、組織はデータを制御し、不正アクセスから保護しながら、相互に連携できます。

Confidential Space は、機密性の高いデータ(多くの場合、規制対象データ)を集約して分析することで相互に価値を得ながら、そのデータを完全に管理したい場合に役立ちます。Confidential Space では、個人を特定できる情報(PII)、保護医療情報(PHI)、知的財産、暗号シークレットなどの機密データを完全に管理しながら集約、分析することで、共同作業を行う企業は相互に価値を得ることができます。

必要なもの

学習内容

  • Confidential Space の実行に必要な Cloud リソースを構成する方法
  • Confidential Space イメージを実行する Confidential VM でワークロードを実行する方法
  • ワークロード コードの属性(what)、Confidential Space 環境(where)、ワークロードを実行しているアカウント(who)に基づいて、保護されたリソースへのアクセスを承認する方法。

この Codelab では、Google Cloud 以外の場所にホストされている保護されたリソースで Confidential Space を使用する方法について説明します。ノンス、オーディエンス、PKI トークンタイプを指定して、Google Attestation Service からカスタムの自己完結型トークンをリクエストする方法について説明します。

この Codelab では、架空のプロダクトである USleep(コンテナ化されたアプリケーション)と UWear(接続されたウェアラブル デバイス)の間に Confidential Space を設定して、睡眠の質を計算します。UWear は、安全でセキュアな隔離環境(高信頼実行環境(TEE)とも呼ばれます)で保護医療情報(PHI)を USleep と共有します。これにより、データの所有者は完全な機密性を保持できます。

UWear は、ワークロード監査役データ所有者の両方です。ワークロード監査役として、実行中のワークロードのコードをレビューし、イメージ ダイジェストをメモします。データ所有者として、UWear はトークンとその署名の有効性をチェックする検証ロジックを記述します。監査されたワークロード イメージのダイジェストを使用して、特定の環境で特定のイメージのダイジェストのみが機密データにアクセスできるようにする検証ポリシーを書き込みます。

この Codelab では、USleep はコンテナ化されたアプリケーションをデプロイしています。USleep は機密データにアクセスできませんが、機密データへのアクセスが許可されている承認済みのワークロードを実行します。

この Codelab では、次の手順を行います。

  • ステップ 1: Codelab に必要なクラウド リソースを設定します。プロジェクト、請求、権限を設定します。Codelab のソースコードをダウンロードして、環境変数を設定します。
  • ステップ 2: ルート証明書をダウンロードし、UWear ソースコードとともに保存します。
  • ステップ 3: USleep と UWear のワークロード VM で使用される個別のワークロード サービス アカウントを作成します。
  • ステップ 4: 構成証明トークンを提供する USleep ワークロードを作成します。
  • ステップ 5: 証明書トークンを検証し、トークンが承認された場合に機密データを送信する UWear ワークロードを作成します。
  • ステップ 6: USleep ワークロードと UWear ワークロードを実行します。UWear が機密データを提供し、USleep がデータに対して睡眠アルゴリズムを実行して結果を出力します。
  • ステップ 7:(省略可)不正な USleep ワークロードを実行し、UWear から機密データが受信されていないことを確認します。
  • ステップ 8: すべてのリソースをクリーンアップします。

ワークフローを理解する

USleep は Confidential Space でワークロードを実行します。ワークロードを実行するには、UWear の PHI にアクセスする必要があります。アクセス権を取得するために、USleep ワークロードはまず安全な TLS セッションを作成します。USleep は、ペイロードとともに Google Attestation Service から構成証明トークンもリクエストします。

USleep は、次の 3 つの要素を含む JSON ペイロードで構成される証明書トークンをリクエストします。

  1. TLS セッションにバインドされた構成証明トークン。構成証明トークンを TLS セッションにバインドするために、nonce 値は TLS エクスポート鍵マテリアルのハッシュになります。トークンを TLS セッションにバインドすることで、TLS セッションに関与する 2 つのパーティのみが nonce 値を生成できるようになるため、中間者攻撃が発生しないことが保証されます。
  2. 「uwear」のオーディエンスが提供されます。UWear は、証明書トークンの対象となるオーディエンスであることを確認します。
  3. トークン タイプ「PKI」。トークンタイプ「PKI」は、USleep が自己完結型トークンをリクエストすることを意味します。自己完結型トークンは、Confidential Space の既知の PKI エンドポイントからダウンロードしたルートを使用して、Google によって署名されていることを検証できます。これは、署名が定期的にローテーションされる公開鍵を使用して検証されるデフォルトの OIDC トークンタイプとは対照的です。

bb013916a3222ce7.png

USleep ワークロードが認証トークンを受け取ります。次に、UWear は USleep との TLS 接続に参加し、USleep の構成証明トークンを取得します。UWear は、ルート証明書に対して x5c クレームをチェックしてトークンを検証します。

UWear は、次の条件を満たす場合に USleep ワークロードを承認します。

  1. トークンが PKI 検証ロジックに合格する。
  2. UWear は、x5c クレームとルート証明書を照合してトークンを検証し、トークンがリーフ証明書で署名されていることを確認し、最後にダウンロードしたルート証明書が x5c クレームのルートと同じであることを確認します。
  3. トークンのワークロード測定クレームが、OPA ポリシーで指定された属性条件と一致する。OPA は、スタック全体でポリシーの適用を統合するオープンソースの汎用ポリシー エンジンです。OPA は、JSON と同様の構文のドキュメントを使用して、ポリシーの検証対象となるベースライン値を設定します。ポリシーがチェックする値の例については、OPA ベースライン値をご覧ください。
  4. ノンスが想定されるノンス(TLS Exported Keying Material)と一致します。これは、上記の OPA ポリシーで検証されます。

これらのチェックがすべて完了し、合格すると、UWear はデータが安全に送信され、処理されることを確認できます。その後、UWear は同じ TLS セッションで機密性の高い PHI を返信し、USleep はそのデータを使用してお客様の睡眠の質を計算できるようになります。

2. クラウド リソースを設定する

始める前に

  1. USleep 用と UWear 用の 2 つの Google Cloud プロジェクトを設定します。Google Cloud プロジェクトの作成の詳細については、「最初の Google プロジェクトを設定して操作する」の Codelab をご覧ください。プロジェクト ID の取得方法と、プロジェクト名やプロジェクト番号との違いについては、プロジェクトの作成と管理をご覧ください。
  2. プロジェクトに対する課金を有効にします
  3. Google プロジェクトの Cloud Shell のいずれかで、次のように必要なプロジェクト環境変数を設定します。
export UWEAR_PROJECT_ID=<Google Cloud project id of UWear>
export USLEEP_PROJECT_ID=<Google Cloud project id of USleep>
  1. 両方のプロジェクトで Confidential Computing API と次の API を有効にします。
gcloud config set project $UWEAR_PROJECT_ID
gcloud services enable \
    cloudapis.googleapis.com \
    cloudshell.googleapis.com \
    container.googleapis.com \
    containerregistry.googleapis.com \
    confidentialcomputing.googleapis.com

gcloud config set project $USLEEP_PROJECT_ID
gcloud services enable \
    cloudapis.googleapis.com \
    cloudshell.googleapis.com \
    container.googleapis.com \
    containerregistry.googleapis.com \
    confidentialcomputing.googleapis.com
  1. 次のコマンドを使用してプリンシパル ID を取得します。
gcloud auth list

# Output should contain
# ACCOUNT: <Principal Identifier>

# Set your member variable
export MEMBER='user:<Principal Identifier>'
  1. これらの 2 つのプロジェクトの権限を追加します。権限を追加するには、IAM ロールを付与するウェブページの詳細に沿って操作します。
gcloud config set project $UWEAR_PROJECT_ID

# Add Artifact Registry Administrator role
gcloud projects add-iam-policy-binding $UWEAR_PROJECT_ID --member=$MEMBER --role='roles/iam.serviceAccountAdmin'

# Add Service Account Administrator role
gcloud projects add-iam-policy-binding $UWEAR_PROJECT_ID --member=$MEMBER --role='roles/artifactregistry.admin'
gcloud config set project $USLEEP_PROJECT_ID

# Add Service Account Administrator role
gcloud projects add-iam-policy-binding $USLEEP_PROJECT_ID --member=$MEMBER --role='roles/iam.serviceAccountAdmin'

# Add Artifact Registry Administrator role
gcloud projects add-iam-policy-binding $USLEEP_PROJECT_ID --member=$MEMBER --role='roles/artifactregistry.admin'

# Add Compute Administrator role
gcloud projects add-iam-policy-binding $USLEEP_PROJECT_ID --member=$MEMBER --role='roles/compute.admin'

# Add Storage Administrator role
gcloud projects add-iam-policy-binding $USLEEP_PROJECT_ID --member=$MEMBER --role='roles/compute.storageAdmin'
  1. Google Cloud プロジェクトのいずれかの Cloud Shell で、次のコマンドを使用して Confidential Space Codelab Github リポジトリのクローンを作成し、この Codelab の一部として使用される必要なスクリプトを取得します。
git clone https://github.com/GoogleCloudPlatform/confidential-space.git
  1. ディレクトリをヘルスデータ Codelab のスクリプト ディレクトリに変更します。
cd confidential-space/codelabs/health_data_analysis_codelab/scripts
  1. codelabs/health_data_analysis_codelab/scripts ディレクトリにある config_env.sh スクリプトの次の 2 行を更新します。プロジェクト ID を USleep と UWear のプロジェクト ID に置き換えます。行の先頭にあるコメント記号「#」は必ず削除してください。
# TODO: Populate UWear and USleep Project IDs
export UWEAR_PROJECT_ID=your-uwear-project-id
export USLEEP_PROJECT_ID=your-usleep-project-id
  1. 省略可: 既存の変数を設定します。これらの変数(export UWEAR_ARTIFACT_REPOSITORY='my-artifact-repository' など)を使用して、リソース名をオーバーライドできます。
  • 次の変数は、既存のクラウド リソース名で設定できます。変数が設定されている場合、プロジェクトの対応する既存のクラウド リソースが使用されます。変数が設定されていない場合、クラウド リソース名は config_env.sh スクリプトの値から生成されます。
  1. config_env.sh スクリプトを実行して、残りの変数名をリソース名のプロジェクト ID に基づく値に設定します。
# Navigate to the scripts folder
cd ~/confidential-space/codelabs/health_data_analysis_codelab/scripts

# Run the config_env script
source config_env.sh

# Verify the variables were set
# Expected output for default variable should be `workload-sa`
echo $USLEEP_WORKLOAD_SERVICE_ACCOUNT

3. ルート証明書をダウンロードする

  1. 構成証明サービスから返された自己完結型トークンを検証するために、UWear は Confidential Space ルート証明書に対して署名を検証する必要があります。UWear は、ルート証明書をダウンロード してローカルに保存する必要があります。Google Cloud プロジェクトのコンソールのいずれかで、次のコマンドを実行します。
cd ~/confidential-space/codelabs/health_data_analysis_codelab/src/uwear

wget https://confidentialcomputing.googleapis.com/.well-known/confidential_space_root.crt -O confidential_space_root.pem
  1. ダウンロードしたルート証明書のフィンガープリントを生成する
openssl x509 -fingerprint -in confidential_space_root.pem -noout
  1. フィンガープリントが次の SHA-1 ダイジェストと一致していることを確認します。
B9:51:20:74:2C:24:E3:AA:34:04:2E:1C:3B:A3:AA:D2:8B:21:23:21

4. ワークロード サービス アカウントを作成する

次に、USleep ワークロード用と UWear ワークロード用の 2 つのサービス アカウントを作成します。create_service_accounts.sh スクリプトを実行して、USleep プロジェクトと UWear プロジェクトにワークロード サービス アカウントを作成します。ワークロードを実行する VM は、これらのサービス アカウントを使用します。

# Navigate to the scripts folder
cd ~/confidential-space/codelabs/health_data_analysis_codelab/scripts

# Run the create_service_accounts script
./create_service_accounts.sh

スクリプト:

  • サービス アカウントをワークロードに接続する iam.serviceAccountUser ロールを付与します。
  • ワークロード サービス アカウントに confidentialcomputing.workloadUser ロールを付与します。これにより、ユーザー アカウントで構成証明トークンを生成できるようになります。
  • ワークロード サービス アカウントに logging.logWriter ロールを付与します。これにより、Confidential Space 環境はシリアル コンソールに加えて Cloud Logging にログを書き込むことができるため、VM が終了した後もログを利用できます。ワークロードを作成する

5. USleep ワークロードを作成します。

このステップでは、この Codelab で使用するワークロードの Docker イメージを作成します。USleep ワークロードは、ウェアラブル デバイスの個人的な健康情報を使用して、お客様の睡眠の質を判断するシンプルな Golang アプリケーションです。

USleep ワークロードについて

USleep ワークロードは、ウェアラブル デバイスの個人的な健康情報を使用して、顧客の睡眠の質を判断するシンプルな Golang アプリケーションです。USleep ワークロードは、主に次の 3 つの部分で構成されています。

  1. TLS セッションを設定し、エクスポートされたキーイング マテリアルを抽出する
func handleConnectionRequest(w http.ResponseWriter, r *http.Request) {
  // Upgrade HTTP Connection to a websocket.
  conn, err := upgrader.Upgrade(w, r, nil)
  if err != nil {
    fmt.Printf("failed to upgrade connection to a websocket with err: %v\n", err)
    return
  }
  defer conn.Close()

  // Get EKM
  hash, err := getEKMHashFromRequest(r)
  if err != nil {
    fmt.Printf("Failed to get EKM: %v", err)
  }
  ...
}

func getEKMHashFromRequest(r *http.Request) (string, error) {
  ekm, err := r.TLS.ExportKeyingMaterial("testing_nonce", nil, 32)
  if err != nil {
    err := fmt.Errorf("failed to get EKM from inbound http request: %w", err)
    return "", err
  }

  sha := sha256.New()
  sha.Write(ekm)
  hash := base64.StdEncoding.EncodeToString(sha.Sum(nil))

  fmt.Printf("EKM: %v\nSHA hash: %v", ekm, hash)
  return hash, nil
}
  1. オーディエンス、ノンス、PKI トークン タイプを使用して Attestation Service からトークンをリクエストします。
func handleConnectionRequest(w http.ResponseWriter, r *http.Request) {
  ...

  // Request token with TLS Exported Keying Material (EKM) hashed.
  token, err := getCustomToken(hash)
  if err != nil {
    fmt.Printf("failed to get custom token from token endpoint: %v", err)
    return
  }

  // Respond to the client with the token.
  conn.WriteMessage(websocket.TextMessage, token)

  ...
}

var (
        socketPath    = "/run/container_launcher/teeserver.sock"
        tokenEndpoint = "http://localhost/v1/token"
        contentType   = "application/json"
)


func getCustomToken(nonce 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", socketPath)
      },
    },
  }

  body := fmt.Sprintf(`{
    "audience": "uwear",
    "nonces": ["%s"],
    "token_type": "PKI"
  }`, nonce)

  resp, err := httpClient.Post(tokenEndpoint, contentType, strings.NewReader(body))
  if err != nil {
    return nil, err
  }

  fmt.Printf("Response from launcher: %v\n", resp)
  text, err := io.ReadAll(resp.Body)
  if err != nil {
    return nil, fmt.Errorf("Failed to read resp.Body: %w", err)
  }
  fmt.Printf("Token from the attestation service: %s\n", text)

  return text, nil
}
  1. 機密データを受信してユーザーの睡眠の質を計算する
func handleConnectionRequest(w http.ResponseWriter, r *http.Request) {
  ...

  // Read the sensitive data
  _, content, err := conn.ReadMessage()
  if err != nil {
    fmt.Printf("failed to read message from the connection: %v\n", err)
  }
  fmt.Printf("Received content from other side, %v\n", string(content))

 // TODO: Handle sensitive data
  ...
}

USleep ワークロードを作成する手順

  1. create_usleep_workload.sh スクリプトを実行して、USleep ワークロードを作成します。このスクリプトは次のようになります。
  • ワークロードが公開される UWear が所有する Artifact Registry($USLEEP_ARTIFACT_REPOSITORY)を作成します。
  • usleep/workload.go コードをビルドして、Docker イメージにパッケージ化します。USleep の Dockerfile 構成をご覧ください。
  • UWear が所有する Artifact Registry($USLEEP_ARTIFACT_REPOSITORY)に Docker イメージを公開します。
  • サービス アカウントに Artifact Registry($USLEEP_ARTIFACT_REPOSITORY)の読み取り権限 $USLEEP_WORKLOAD_SERVICE_ACCOUNT を付与します。
./create_usleep_workload.sh
  1. 重要: 出力ログで、USleep のイメージ ダイジェストを抽出します。
latest: digest: sha256:<USLEEP_IMAGE_DIGEST> size: 945
  1. UWear ディレクトリに移動します。
cd ~/confidential-space/codelabs/health_data_analysis_codelab/src/uwear
  1. opa_validation_values.json の「allowed_submods_container_image_digest」の値を USLEEP_IMAGE_DIGEST に置き換えます。
# Replace the image digest
sed -i 's/sha256:bc4c32cb2ca046ba07dcd964b07a320b7d0ca88a5cf8e979da15cae68a2103ee/sha256:<USLEEP_IMAGE_DIGEST>/' ~/confidential-space/codelabs/health_data_analysis_codelab/src/uwear/opa_validation_values.json

6. UWear ワークロードを作成する

UWear Workload について

UWear ワークロードには、次の 4 つの主要な部分があります。

  1. USleep ワークロードで作成された同じ TLS セッションに参加し、安全な TLS セッションを介して USleep から構成証明トークンを取得します。
func main() {
  fmt.Println("Initializing client...")

  tlsconfig := &tls.Config{
    // Skipping client verification of the server's certificate chain and host name since we are
    // doing custom verification using the attestation token.
    InsecureSkipVerify: true,
  }

  dialer := websocket.Dialer{
    TLSClientConfig:  tlsconfig,
    HandshakeTimeout: 5 * time.Second,
  }

  ipAddress := os.Getenv(ipAddrEnvVar)
  url := fmt.Sprintf("wss://%s:8081/connection", ipAddress)

  fmt.Printf("Attempting to dial to url %v...\n", url)
  conn, _, err := dialer.Dial(url, nil)
  if err != nil {
    fmt.Printf("Failed to dial to url %s, err %v\n", url, err)
    return
  }

  defer conn.Close()

  tokenString, ekm, err := retrieveTokenAndEKMFromConn(conn)
  if err != nil {
    fmt.Printf("Failed to retrieve token and EKM from connection: %v\n", err)
    return
  }

  fmt.Printf("token: %v\n", tokenString)

  ...
}
  1. 自己完結型トークンを検証します。
  • x5c クレームに、リーフ証明書から中間証明書、そしてルート証明書へと正しくチェーンされている証明書チェーンが含まれていることを確認します。
  • トークンが x5c クレームに含まれるリーフ証明書によって署名されていることを確認します。
  • ダウンロードまたは保存されたルート証明書が x5c クレームのルートと同じであることを確認します。
func main() {
  ...

  token, err := validatePKIToken(tokenString)
  if err != nil {
    fmt.Printf("Failed to validate PKI token, err: %v\n.", err)
    return
  }
  fmt.Println("PKI token validated successfully")
 
  ...
}

// validatePKIToken validates the PKI token returned from the attestation service.
// It verifies the token the certificate chain and that the token is signed by Google
// Returns a jwt.Token or returns an error if invalid.
func validatePKIToken(attestationToken string) (jwt.Token, error) {
  // IMPORTANT: The attestation token should be considered untrusted until the certificate chain and
  // the signature is verified.
  rawRootCertificate, err := readFile(rootCertificateFile)
  if err != nil {
    return jwt.Token{}, fmt.Errorf("readFile(%v) - failed to read root certificate: %w", rootCertificateFile, err)
  }

  storedRootCert, err := decodeAndParsePEMCertificate(string(rawRootCertificate))
  if err != nil {
    return jwt.Token{}, fmt.Errorf("DecodeAndParsePEMCertificate(string) - failed to decode and parse root certificate: %w", err)
  }

  jwtHeaders, err := extractJWTHeaders(attestationToken)
  if err != nil {
    return jwt.Token{}, fmt.Errorf("ExtractJWTHeaders(token) - failed to extract JWT headers: %w", err)
  }

  if jwtHeaders["alg"] != "RS256" {
    return jwt.Token{}, fmt.Errorf("ValidatePKIToken(attestationToken, ekm) - 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 Golang's jwt.Parse function

  x5cHeaders := jwtHeaders["x5c"].([]any)
  certificates, err := extractCertificatesFromX5CHeader(x5cHeaders)
  if err != nil {
    return jwt.Token{}, fmt.Errorf("ExtractCertificatesFromX5CHeader(x5cHeaders) returned error: %w", 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(*storedRootCert, *certificates.RootCert)
  if err != nil {
    return jwt.Token{}, fmt.Errorf("failed to verify certificate chain: %w", err)
  }

  err = verifyCertificateChain(certificates)
  if err != nil {
    return jwt.Token{}, fmt.Errorf("VerifyCertificateChain(CertificateChain) - 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
}



// verifyCertificateChain verifies the certificate chain from leaf to root.
// It also checks that all certificate lifetimes are valid.
func verifyCertificateChain(certificates CertificateChain) error {
    // Additional check: Verify that all certificates in the cert chain are valid.
    // Note: The *x509.Certificate Verify method in Golang already validates this but for other coding
    // languages it is important to make sure the certificate lifetimes are checked.
    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
}
  1. 次に、UWear ワークロードは、トークンのワークロード測定クレームが OPA ポリシーで指定された属性条件と一致するかどうかを確認します。OPA は、スタック全体でポリシーの適用を統合するオープンソースの汎用ポリシー エンジンです。OPA は、JSON と同様の構文のドキュメントを使用して、ポリシーの検証対象となるベースライン値を設定します。
func main() {
  ...

  err = validateClaimsAgainstOPAPolicy(token, ekm)
  if err != nil {
    fmt.Printf("Failed to validate claims against OPA policy: %v\n", err)
  return
  }

  fmt.Println("Validated token and claims. Sending sensitive data")

  ...
}

// validateClaimsAgainstOPAPolicy validates the claims in the JWT token against the OPA policy.
func validateClaimsAgainstOPAPolicy(token jwt.Token, ekm string) error {
        data, err := os.ReadFile("opa_validation_values.json")
        authorized, err := evaluateOPAPolicy(context.Background(), token, ekm, string(data))
        if err != nil {
                fmt.Println("Error evaluating OPA policy:", err)
                return fmt.Errorf("failed to evaluate OPA policy: %w", err)
        }
        if !authorized {
                fmt.Println("Remote TEE's JWT failed policy check.")
                return fmt.Errorf("remote TEE's JWT failed policy check")
        }
        fmt.Println("JWT is authorized.")
        return nil
}


// evaluateOPAPolicy returns boolean indicating if OPA policy is satisfied or not, or error if occurred
func evaluateOPAPolicy(ctx context.Context, token jwt.Token, ekm string, policyData string) (bool, error) {
        var claims jwt.MapClaims
        var ok bool
        if claims, ok = token.Claims.(jwt.MapClaims); !ok {
                return false, fmt.Errorf("failed to get the claims from the JWT")
        }

        module := fmt.Sprintf(opaPolicy, ekm)

        var json map[string]any
        err := util.UnmarshalJSON([]byte(policyData), &json)
        store := inmem.NewFromObject(json)

        // Bind 'allow' to the value of the policy decision
        // Bind 'hw_verified', 'image_verified', 'audience_verified, 'nonce_verified' to their respective policy evaluations
        query, err := rego.New(
                rego.Query(regoQuery),                          // Argument 1 (Query string)
                rego.Store(store),                              // Argument 2 (Data store)
                rego.Module("confidential_space.rego", module), // Argument 3 (Policy module)
        ).PrepareForEval(ctx)

        if err != nil {
                fmt.Printf("Error creating query: %v\n", err)
                return false, err
        }

        fmt.Println("Performing OPA query evaluation...")
        results, err := query.Eval(ctx, rego.EvalInput(claims))

        if err != nil {
                fmt.Printf("Error evaluating OPA policy: %v\n", err)
                return false, err
        } else if len(results) == 0 {
                fmt.Println("Undefined result from evaluating OPA policy")
                return false, err
        } else if result, ok := results[0].Bindings["allow"].(bool); !ok {
                fmt.Printf("Unexpected result type: %v\n", ok)
                fmt.Printf("Result: %+v\n", result)
                return false, err
        }

        fmt.Println("OPA policy evaluation completed.")

        fmt.Println("OPA policy result values:")
        for key, value := range results[0].Bindings {
                fmt.Printf("[ %s ]: %v\n", key, value)
        }
        result := results[0].Bindings["allow"]
        if result == true {
                fmt.Println("Policy check PASSED")
                return true, nil
        }
        fmt.Println("Policy check FAILED")
        return false, nil
}
{
  "allowed_submods_container_image_digest": [
    "sha256:<USLEEP_IMAGE_DIGEST>"
  ],
  "allowed_hwmodel": [
    "GCP_INTEL_TDX",
    "GCP_SHIELDED_VM",
    "GCP_AMD_SEV_ES",
    "GCP_AMD_SEV"
  ],
  "allowed_aud": [
    "uwear"
  ],
  "allowed_issuer": [
    "https://confidentialcomputing.googleapis.com"
  ],
  "allowed_secboot": [
    true
  ],
  "allowed_sw_name": [
    "CONFIDENTIAL_SPACE"
  ]
}
package confidential_space

import rego.v1

default allow := false
default hw_verified := false
default image_digest_verified := false
default audience_verified := false
default nonce_verified := false
default issuer_verified := false
default secboot_verified := false
default sw_name_verified := false

allow if {
  hw_verified
  image_digest_verified
  audience_verified
  nonce_verified
  issuer_verified
  secboot_verified
  sw_name_verified
}

hw_verified if input.hwmodel in data.allowed_hwmodel
image_digest_verified if input.submods.container.image_digest in data.allowed_submods_container_image_digest
audience_verified if input.aud in data.allowed_aud
issuer_verified if input.iss in data.allowed_issuer
secboot_verified if input.secboot in data.allowed_secboot
sw_name_verified if input.swname in data.allowed_sw_name
nonce_verified if {
  input.eat_nonce == "%s"
}
  • Rego クエリの例。
regoQuery = "
    allow = data.confidential_space.allow;
    hw_verified = data.confidential_space.hw_verified;
    image__digest_verified = data.confidential_space.image_digest_verified;
    audience_verified = data.confidential_space.audience_verified;
    nonce_verified = data.confidential_space.nonce_verified;
    issuer_verified = data.confidential_space.issuer_verified;
    secboot_verified = data.confidential_space.secboot_verified;
    sw_name_verified = data.confidential_space.sw_name_verified
"

EKM ハッシュを取得するコードの例:

func getEKMHashFromConn(c *websocket.Conn) (string, error) {
  conn, ok := c.NetConn().(*tls.Conn)
  if !ok {
    return "", fmt.Errorf("failed to cast NetConn to *tls.Conn")
  }

  state := conn.ConnectionState()
  ekm, err := state.ExportKeyingMaterial("testing_nonce", nil, 32)
  if err != nil {
    return "", fmt.Errorf("failed to get EKM from TLS connection: %w", err)
  }

  sha := sha256.New()
  sha.Write(ekm)
  hash := base64.StdEncoding.EncodeToString(sha.Sum(nil))

  return hash, nil
}
  1. これらのチェックがすべて完了し、合格すると、UWear はデータが安全に送信され、処理されることを確認できます。UWear は同じ TLS セッションで機密性の高い PHI を返信し、USleep はそのデータを使用してお客様の睡眠の質を計算できます。
func main() {
  ...

  fmt.Println("Validated token and claims. Sending sensitive data")

  data, err := readFile(mySensitiveDataFile)
  if err != nil {
    fmt.Printf("Failed to read data from the file: %v\n", err)
  }

  conn.WriteMessage(websocket.BinaryMessage, data)
  fmt.Println("Sent payload. Closing the connection")
  conn.Close()
  
  ...
}

USleep ワークロードを作成する手順

  1. スクリプト ディレクトリに移動する
cd ~/confidential-space/codelabs/health_data_analysis_codelab/scripts
  1. create_uwear_workload.sh スクリプトを実行して、UWear ワークロードを作成します。
  • ワークロードが公開される UWear が所有する Artifact Registry($UWEAR_ARTIFACT_REPOSITORY)を作成します。
  • uwear/workload.go コードをビルドして、Docker イメージにパッケージ化します。USleep の Dockerfile 構成をご覧ください。
  • UWear が所有する Artifact Registry($UWEAR_ARTIFACT_REPOSITORY)に Docker イメージを公開します。
  • サービス アカウントに Artifact Registry($UWEAR_ARTIFACT_REPOSITORY)の読み取り権限($UWEAR_WORKLOAD_SERVICE_ACCOUNT)を付与します。
./create_uwear_workload.sh

7. USleep ワークロードと UWear ワークロードを実行する

USleep ワークロードを実行する

gcloud config set project $USLEEP_PROJECT_ID


gcloud compute instances create \
 --confidential-compute-type=SEV \
 --shielded-secure-boot \
 --maintenance-policy=MIGRATE \
 --scopes=cloud-platform --zone=${USLEEP_PROJECT_ZONE} \
 --image-project=confidential-space-images \
 --image-family=confidential-space \
--service-account=${USLEEP_WORKLOAD_SERVICE_ACCOUNT}@${USLEEP_PROJECT_ID}.iam.gserviceaccount.com \
 --metadata ^~^tee-image-reference=${USLEEP_PROJECT_REPOSITORY_REGION}-docker.pkg.dev/${USLEEP_PROJECT_ID}/${USLEEP_ARTIFACT_REPOSITORY}/${USLEEP_WORKLOAD_IMAGE_NAME}:${USLEEP_WORKLOAD_IMAGE_TAG}~tee-restart-policy=Never~tee-container-log-redirect=true usleep

レスポンスは STATUS: RUNNING を返し、EXTERNAL_IP も次のように返します。

NAME: usleep
ZONE: us-west1-b
MACHINE_TYPE: n2d-standard-2
PREEMPTIBLE:
INTERNAL_IP: 10.138.0.6
EXTERNAL_IP: 34.168.56.10
STATUS: RUNNING

外部 IP を変数に保存する

export USLEEP_EXTERNAL_IP=<add your external IP> 

USleep ワークロードが正しく実行されたことを確認する

USleep ワークロードが正しく実行されていることを確認するには、USleep プロジェクトの [VM インスタンス] ページに移動します。「usleep」インスタンスをクリックし、[ログ] セクションの [シリアルポート 1(コンソール)] を押します。サーバーが起動して実行されると、ログの下部に次のような内容が表示されます。

2024/09/13 17:00:00 workload task started
#####----- Local IP Address is <YOUR-LOCAL-IP> -----#####
Starting Server..

UWear ワークロードを実行する

gcloud config set project $UWEAR_PROJECT_ID

gcloud compute instances create \
 --confidential-compute-type=SEV \
 --shielded-secure-boot \
 --maintenance-policy=MIGRATE \
 --scopes=cloud-platform --zone=${UWEAR_PROJECT_ZONE} \
 --image-project=confidential-space-images \
 --image-family=confidential-space \
--service-account=${UWEAR_WORKLOAD_SERVICE_ACCOUNT}@${UWEAR_PROJECT_ID}.iam.gserviceaccount.com \
 --metadata ^~^tee-image-reference=${UWEAR_PROJECT_REPOSITORY_REGION}-docker.pkg.dev/${UWEAR_PROJECT_ID}/${UWEAR_ARTIFACT_REPOSITORY}/${UWEAR_WORKLOAD_IMAGE_NAME}:${UWEAR_WORKLOAD_IMAGE_TAG}~tee-restart-policy=Never~tee-container-log-redirect=true~tee-env-remote_ip_addr=$USLEEP_EXTERNAL_IP uwear

UWear ワークロードが正しく実行されたことを確認する

UWear ワークロードのログを表示するには、UWear プロジェクトの VM インスタンス ページに移動します。「uwear」インスタンスをクリックし、[ログ] セクションの [シリアルポート 1(コンソール)] を押します。

インスタンスが完全に起動した後のログ出力は次のようになります。

UWear プロジェクトでは、シリアルログに次のような内容が表示されます。

token: eyJ[...]MrXUg
PKI token validated successfully
Performing OPA query evaluation...
OPA policy evaluation completed.
OPA policy result values:
[ hw_verified ]: true
[ image__digest_verified ]: true
[ audience_verified ]: true
[ nonce_verified ]: true
[ issuer_verified ]: true
[ secboot_verified ]: true
[ sw_name_verified ]: true
[ allow ]: true
Policy check PASSED
JWT is authorized.
Validated token and claims. Sending sensitive data
Sent payload. Closing the connection

UWear ワークロードがこのようにならない場合は、以下の注で手順を確認してください。

USleep の結果を表示する

結果を表示するには、USleep プロジェクトの [VM インスタンス] ページに戻ります。「usleep」インスタンスをクリックし、[ログ] セクションの [シリアルポート 1(コンソール)] を押します。ワークロードの結果はログの下部に表示されます。次の例のようになります。

Token from the attestation service: eyJhbGci...Ii5A3CJBuDM2o5Q
Received content from other side, {
  "name": "Amy",
  "age": 29,
  "sleep": {
      "light": {
          "minutes": 270
      },
      "deep": {
          "minutes": 135
      },
      "rem": {
          "minutes": 105
      }
  }
}
Sleep quality result: total sleep time is less than 8 hours

結果は "total sleep time is less than 8 hours". になります。

これで、UWear と USleep の間に機密情報を共有するための Confidential Space が正常に作成されました。

8. (省略可)不正なワークロードを実行する

次のシナリオでは、USleep がコードを更新し、UWear が提供する睡眠データに対して別のワークロードを実行します。UWear はこの新しいワークロードに同意しておらず、新しいイメージ ダイジェストを許可するように OPA ポリシーを更新していません。UWear が機密データを不正なワークロードに送信しないことを確認します。

USleep がワークロードを変更する

  1. プロジェクトを $USLEEP_PROJECT_ID に設定します。
gcloud config set project $USLEEP_PROJECT_ID
  1. USleep VM インスタンスを削除します。
gcloud compute instances delete usleep --zone $USLEEP_PROJECT_ZONE
  1. usleep/workload.go ディレクトリに移動します。
cd ~/confidential-space/codelabs/health_data_analysis_codelab/src/usleep
  1. usleep/workload.go ファイル内。"audience": "uwear". 行を更新します。この例では、イメージ ダイジェストを変更するために、UWear が承認していない別の値にオーディエンスを更新します。そのため、UWear は承認されていないイメージ ダイジェストと誤ったユーザー層という 2 つの理由で拒否する必要があります。
"audience": "anotherCompany.com",
  1. 新しい USleep ワークロードを作成する
cd ~/confidential-space/codelabs/health_data_analysis_codelab/scripts

./create_usleep_workload.sh
  1. 新しい USleep VM インスタンスを作成してワークロードを実行する
gcloud compute instances create \
 --confidential-compute-type=SEV \
 --shielded-secure-boot \
 --maintenance-policy=MIGRATE \
 --scopes=cloud-platform --zone=${USLEEP_PROJECT_ZONE} \
 --image-project=confidential-space-images \
 --image-family=confidential-space \
--service-account=${USLEEP_WORKLOAD_SERVICE_ACCOUNT}@${USLEEP_PROJECT_ID}.iam.gserviceaccount.com \
 --metadata ^~^tee-image-reference=${USLEEP_PROJECT_REPOSITORY_REGION}-docker.pkg.dev/${USLEEP_PROJECT_ID}/${USLEEP_ARTIFACT_REPOSITORY}/${USLEEP_WORKLOAD_IMAGE_NAME}:${USLEEP_WORKLOAD_IMAGE_TAG}~tee-restart-policy=Never~tee-container-log-redirect=true usleep
  1. 後で使用するために、新しい USleep 外部 IP を抽出する
export USLEEP_EXTERNAL_IP=<add your external IP>

ワークロードを再実行する

  1. UWear VM インスタンスを削除する
gcloud config set project $UWEAR_PROJECT_ID

gcloud compute instances delete uwear --zone $UWEAR_PROJECT_ZONE
  1. 新しい外部 IP を使用して UWear VM インスタンスを再作成する
gcloud compute instances create \
 --confidential-compute-type=SEV \
 --shielded-secure-boot \
 --maintenance-policy=MIGRATE \
 --scopes=cloud-platform --zone=${UWEAR_PROJECT_ZONE} \
 --image-project=confidential-space-images \
 --image-family=confidential-space \
--service-account=${UWEAR_WORKLOAD_SERVICE_ACCOUNT}@${UWEAR_PROJECT_ID}.iam.gserviceaccount.com \
 --metadata ^~^tee-image-reference=${UWEAR_PROJECT_REPOSITORY_REGION}-docker.pkg.dev/${UWEAR_PROJECT_ID}/${UWEAR_ARTIFACT_REPOSITORY}/${UWEAR_WORKLOAD_IMAGE_NAME}:${UWEAR_WORKLOAD_IMAGE_TAG}~tee-restart-policy=Never~tee-container-log-redirect=true~tee-env-remote_ip_addr=$USLEEP_EXTERNAL_IP uwear
  1. UWear シリアルログに次のメッセージが表示され、USleep VM が機密データを受信しないようにする必要があります。
OPA policy result values:
[ nonce_verified ]: true
[ issuer_verified ]: true
[ secboot_verified ]: true
[ sw_name_verified ]: true
[ allow ]: false
[ hw_verified ]: true
[ image__digest_verified ]: false
[ audience_verified ]: false
Policy check FAILED
Remote TEE's JWT failed policy check.
Failed to validate claims against OPA policy: remote TEE's JWT failed policy check

9. クリーンアップ

クリーンアップ スクリプトを使用すると、この Codelab の一部として作成したリソースをクリーンアップできます。このクリーンアップの一環として、次のリソースが削除されます。

  • UWear サービス アカウント($UWEAR_SERVICE_ACCOUNT)。
  • UWear アーティファクト レジストリ($UWEAR_ARTIFACT_REPOSITORY)。
  • UWear コンピューティング インスタンス
  • USleep サービス アカウント($USLEEP_SERVICE_ACCOUNT)。
  • USleep アーティファクト レジストリ($USLEEP_ARTIFACT_REPOSITORY)。
  • USleep コンピューティング インスタンス
./cleanup.sh

探索が完了したら、こちらの手順に沿ってプロジェクトの削除をご検討ください。

おめでとうございます!

お疲れさまでした。これでこの Codelab は終了です。

Confidential Space を使用して、機密性を保持しながらデータを安全に共有する方法を学びました。

次のステップ

以下の類似の Codelab をご覧ください。

参考資料