Mengakses resource yang tidak dikelola oleh IAM Google Cloud


Jika akses ke resource yang dilindungi tidak dikelola oleh IAM Google Cloud—misalnya, resource disimpan di layanan cloud lain, di lokasi, atau di perangkat lokal seperti ponsel—Anda tetap dapat mengautentikasi beban kerja Ruang Rahasia ke perangkat atau sistem yang menyediakan akses ke resource tersebut, yang juga dikenal sebagai pihak tepercaya.

Untuk melakukannya, pihak tepercaya harus meminta token pengesahan dari layanan pengesahan Confidential Space dengan audiens kustom dan nonce opsional. Saat meminta token pengesahan seperti ini, Anda harus melakukan validasi token sendiri sebelum memberikan akses ke resource.

Dokumentasi berikut mencakup konsep yang terlibat dalam penggunaan Confidential Space dengan resource di luar Google Cloud, termasuk petunjuk untuk mengintegrasikan workload Confidential Space Anda dengan resource AWS secara lancar. Untuk panduan menyeluruh, lihat codelab.

Alur token pengesahan

Token pengesahan diminta oleh beban kerja atas nama pihak tepercaya dan ditampilkan oleh layanan pengesahan. Bergantung pada kebutuhan Anda, Anda dapat menentukan audiens kustom dan secara opsional memberikan nonce.

Tidak dienkripsi

Untuk memudahkan pemahaman proses pengambilan token, alur yang ditampilkan di sini tidak menggunakan enkripsi. Dalam praktiknya, sebaiknya Anda mengenkripsi komunikasi dengan TLS.

Diagram berikut menunjukkan alurnya:

Diagram alur dari alur pembuatan token pengesahan

  1. Pihak tepercaya mengirim permintaan token ke workload, dengan nonce opsional yang telah dibuatnya.

  2. Workload menentukan audiens, menambahkan audiens ke permintaan, dan mengirim permintaan ke peluncur Ruang Rahasia.

  3. Peluncur mengirimkan permintaan ke layanan pengesahan.

  4. Layanan pengesahan membuat token yang berisi audiens yang ditentukan dan nonce opsional.

  5. Layanan pengesahan menampilkan token ke peluncur.

  6. Peluncur menampilkan token ke workload.

  7. Workload menampilkan token ke pihak tepercaya.

  8. Pihak tepercaya memverifikasi klaim, termasuk audiens dan nonce opsional.

Dienkripsi dengan TLS

Alur yang tidak dienkripsi membuat permintaan rentan terhadap serangan man-in-the-middle. Karena nonce tidak terikat ke output data atau sesi TLS, penyerang dapat mencegat permintaan dan meniru identitas workload.

Untuk membantu mencegah jenis serangan ini, Anda dapat menyiapkan sesi TLS antara pihak tepercaya dan workload serta menggunakan materi kunci yang diekspor TLS (EKM) sebagai nonce. Materi kunci yang diekspor TLS mengikat pengesahan ke sesi TLS dan mengonfirmasi bahwa permintaan pengesahan dikirim melalui saluran yang aman. Proses ini juga dikenal sebagai pengikatan channel.

Diagram berikut menunjukkan alur menggunakan pengikatan saluran:

Diagram alur dari alur pembuatan token pengikatan channel

  1. Pihak tepercaya menyiapkan sesi TLS yang aman dengan Confidential VM yang menjalankan workload.

  2. Pihak tepercaya mengirim permintaan token menggunakan sesi TLS yang aman.

  3. Beban kerja menentukan audiens dan membuat nonce menggunakan materi kunci yang diekspor TLS.

  4. Workload mengirimkan permintaan ke peluncur Confidential Space.

  5. Peluncur mengirimkan permintaan ke layanan pengesahan.

  6. Layanan pengesahan membuat token yang berisi audiens dan nonce yang ditentukan.

  7. Layanan pengesahan menampilkan token ke peluncur.

  8. Peluncur menampilkan token ke workload.

  9. Workload menampilkan token ke pihak tepercaya.

  10. Pihak tepercaya membuat ulang nonce menggunakan materi kunci yang diekspor TLS.

  11. Pihak tepercaya memverifikasi klaim, termasuk audiens dan nonce. Nonce dalam token harus cocok dengan nonce yang dibuat ulang oleh pihak tepercaya.

Struktur token pengesahan

Token pengesahan adalah token web JSON dengan struktur berikut:

  • Header: Mendeskripsikan algoritma penandatanganan. Token PKI juga menyimpan rantai sertifikat di header dalam kolom x5c.

  • Payload data JSON bertanda tangan: Berisi klaim tentang beban kerja untuk pihak tepercaya, seperti subjek, penerbit, audiens, nonce, dan waktu habis masa berlaku.

  • Tanda tangan: Memberikan validasi bahwa token tidak berubah selama transit. Untuk mengetahui informasi selengkapnya tentang penggunaan tanda tangan, lihat Cara Memvalidasi Token ID OpenID Connect.

Contoh kode berikut adalah contoh token pengesahan yang dienkode yang dibuat dalam image 240500 Confidential Space. Gambar yang lebih baru mungkin berisi kolom tambahan. Anda dapat menggunakan https://jwt.io/ untuk mendekodenya (tanda tangan disamarkan).

eyJhbGciOiJIUzI1NiIsImtpZCI6IjEyMzQ1IiwidHlwIjoiSldUIn0.eyJhdWQiOiJBVURJRU5DRV9OQU1FIiwiZGJnc3RhdCI6ImRpc2FibGVkLXNpbmNlLWJvb3QiLCJlYXRfbm9uY2UiOlsiTk9OQ0VfMSIsIk5PTkNFXzIiXSwiZWF0X3Byb2ZpbGUiOiJodHRwczovL2Nsb3VkLmdvb2dsZS5jb20vY29uZmlkZW50aWFsLWNvbXB1dGluZy9jb25maWRlbnRpYWwtc3BhY2UvZG9jcy9yZWZlcmVuY2UvdG9rZW4tY2xhaW1zIiwiZXhwIjoxNzIxMzMwMDc1LCJnb29nbGVfc2VydmljZV9hY2NvdW50cyI6WyJQUk9KRUNUX0lELWNvbXB1dGVAZGV2ZWxvcGVyLmdzZXJ2aWNlYWNjb3VudC5jb20iXSwiaHdtb2RlbCI6IkdDUF9BTURfU0VWIiwiaWF0IjoxNzIxMzI2NDc1LCJpc3MiOiJodHRwczovL2NvbmZpZGVudGlhbGNvbXB1dGluZy5nb29nbGVhcGlzLmNvbSIsIm5iZiI6MTcyMTMyNjQ3NSwib2VtaWQiOjExMTI5LCJzZWNib290Ijp0cnVlLCJzdWIiOiJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9jb21wdXRlL3YxL3Byb2plY3RzL1BST0pFQ1RfSUQvem9uZXMvdXMtY2VudHJhbDEtYS9pbnN0YW5jZXMvSU5TVEFOQ0VfTkFNRSIsInN1Ym1vZHMiOnsiY29uZmlkZW50aWFsX3NwYWNlIjp7Im1vbml0b3JpbmdfZW5hYmxlZCI6eyJtZW1vcnkiOmZhbHNlfSwic3VwcG9ydF9hdHRyaWJ1dGVzIjpbIkxBVEVTVCIsIlNUQUJMRSIsIlVTQUJMRSJdfSwiY29udGFpbmVyIjp7ImFyZ3MiOlsiL2N1c3RvbW5vbmNlIiwiL2RvY2tlci1lbnRyeXBvaW50LnNoIiwibmdpbngiLCItZyIsImRhZW1vbiBvZmY7Il0sImVudiI6eyJIT1NUTkFNRSI6IkhPU1RfTkFNRSIsIk5HSU5YX1ZFUlNJT04iOiIxLjI3LjAiLCJOSlNfUkVMRUFTRSI6IjJ-Ym9va3dvcm0iLCJOSlNfVkVSU0lPTiI6IjAuOC40IiwiUEFUSCI6Ii91c3IvbG9jYWwvc2JpbjovdXNyL2xvY2FsL2JpbjovdXNyL3NiaW46L3Vzci9iaW46L3NiaW46L2JpbiIsIlBLR19SRUxFQVNFIjoiMn5ib29rd29ybSJ9LCJpbWFnZV9kaWdlc3QiOiJzaGEyNTY6Njc2ODJiZGE3NjlmYWUxY2NmNTE4MzE5MmI4ZGFmMzdiNjRjYWU5OWM2YzMzMDI2NTBmNmY4YmY1ZjBmOTVkZiIsImltYWdlX2lkIjoic2hhMjU2OmZmZmZmYzkwZDM0M2NiY2IwMWE1MDMyZWRhYzg2ZGI1OTk4YzUzNmNkMGEzNjY1MTQxMjFhNDVjNjcyMzc2NWMiLCJpbWFnZV9yZWZlcmVuY2UiOiJkb2NrZXIuaW8vbGlicmFyeS9uZ2lueDpsYXRlc3QiLCJpbWFnZV9zaWduYXR1cmVzIjpbeyJrZXlfaWQiOiI8aGV4YWRlY2ltYWwtc2hhMjU2LWZpbmdlcnByaW50LXB1YmxpYy1rZXkxPiIsInNpZ25hdHVyZSI6IjxiYXNlNjQtZW5jb2RlZC1zaWduYXR1cmU-Iiwic2lnbmF0dXJlX2FsZ29yaXRobSI6IlJTQVNTQV9QU1NfU0hBMjU2In0seyJrZXlfaWQiOiI8aGV4YWRlY2ltYWwtc2hhMjU2LWZpbmdlcnByaW50LXB1YmxpYy1rZXkyPiIsInNpZ25hdHVyZSI6IjxiYXNlNjQtZW5jb2RlZC1zaWduYXR1cmU-Iiwic2lnbmF0dXJlX2FsZ29yaXRobSI6IlJTQVNTQV9QU1NfU0hBMjU2In0seyJrZXlfaWQiOiI8aGV4YWRlY2ltYWwtc2hhMjU2LWZpbmdlcnByaW50LXB1YmxpYy1rZXkzPiIsInNpZ25hdHVyZSI6IjxiYXNlNjQtZW5jb2RlZC1zaWduYXR1cmU-Iiwic2lnbmF0dXJlX2FsZ29yaXRobSI6IkVDRFNBX1AyNTZfU0hBMjU2In1dLCJyZXN0YXJ0X3BvbGljeSI6Ik5ldmVyIn0sImdjZSI6eyJpbnN0YW5jZV9pZCI6IklOU1RBTkNFX0lEIiwiaW5zdGFuY2VfbmFtZSI6IklOU1RBTkNFX05BTUUiLCJwcm9qZWN0X2lkIjoiUFJPSkVDVF9JRCIsInByb2plY3RfbnVtYmVyIjoiUFJPSkVDVF9OVU1CRVIiLCJ6b25lIjoidXMtY2VudHJhbDEtYSJ9fSwic3duYW1lIjoiQ09ORklERU5USUFMX1NQQUNFIiwic3d2ZXJzaW9uIjpbIjI0MDUwMCJdfQ.29V71ymnt7LY5Ny6OJFb9AClT4XNLPi0TIcddKDp5pk<SIGNATURE>

Berikut adalah versi yang didekode dari contoh sebelumnya:

{
  "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"
  ]
}

Untuk penjelasan yang lebih mendetail tentang kolom token pengesahan, lihat Klaim token pengesahan.

Mengambil token pengesahan

Selesaikan langkah-langkah berikut untuk menerapkan token pengesahan di lingkungan Confidential Space Anda:

  1. Siapkan klien HTTP di beban kerja Anda.

  2. Dalam beban kerja Anda, gunakan klien HTTP untuk membuat permintaan HTTP ke URL yang mendengarkan, http://localhost/v1/token, melalui socket domain Unix. File socket terletak di /run/container_launcher/teeserver.sock.

Anda.

Saat permintaan dibuat ke URL yang mendengarkan, peluncur Confidential Space akan mengelola pengumpulan bukti pengesahan, meminta token pengesahan dari layanan pengesahan (meneruskan parameter kustom), lalu menampilkan token yang dihasilkan ke beban kerja.

Contoh kode berikut di Go menunjukkan cara berkomunikasi dengan server HTTP peluncur melalui IPC.

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
}

Meminta token pengesahan dengan audiens kustom

Metode HTTP dan URL:

POST http://localhost/v1/token

Meminta isi JSON:

{
  "audience": "AUDIENCE_NAME",
  "token_type": "TOKEN_TYPE",
  "nonces": [
      "NONCE_1",
      "NONCE_2",
      ...
  ]
}

Berikan nilai berikut:

  • AUDIENCE_NAME: Wajib diisi. Nilai audiens Anda, yaitu nama yang telah Anda berikan kepada pihak tepercaya Anda. Ini ditetapkan oleh beban kerja.

    Kolom ini secara default adalah https://sts.google.com untuk token tanpa audiens kustom. Nilai https://sts.google.com tidak dapat digunakan saat menetapkan audiens kustom. Panjang maksimum adalah 512 byte.

    Untuk menyertakan audiens kustom dalam token, beban kerja—bukan pihak tepercaya—harus menambahkannya ke permintaan token pengesahan sebelum mengirimkan permintaan ke layanan pengesahan Confidential Space. Hal ini membantu mencegah pihak tepercaya meminta token untuk resource yang dilindungi yang seharusnya tidak dapat diaksesnya.

  • TOKEN_TYPE: Wajib diisi. Jenis token yang akan ditampilkan. Pilih salah satu jenis berikut:

    • OIDC: Token ini divalidasi terhadap kunci publik yang ditentukan di kolom jwks_uri di endpoint validasi token OIDC. Kunci publik dirotasi secara rutin.

    • PKI: Token ini divalidasi terhadap sertifikat root yang ditentukan di bidang root_ca_uri di endpoint validasi token PKI. Anda harus menyimpan sertifikat ini sendiri. Sertifikat dirotasi setiap 10 tahun.

    Karena sertifikat dengan masa berlaku lama digunakan, bukan kunci publik dengan masa berlaku singkat untuk validasi token, alamat IP Anda tidak terlalu sering diekspos ke server Google. Artinya, token PKI menawarkan privasi yang lebih tinggi daripada token OIDC.

    Anda dapat memvalidasi sidik jari sertifikat dengan OpenSSL:

    openssl x509 -fingerprint -in confidential_space_root.crt
    

    Sidik jari harus cocok dengan ringkasan SHA-1 berikut:

    B9:51:20:74:2C:24:E3:AA:34:04:2E:1C:3B:A3:AA:D2:8B:21:23:21
    
  • NONCE: Opsional. Nilai unik, acak, dan buram, memastikan bahwa token hanya dapat digunakan satu kali. Nilai ditetapkan oleh pihak yang mengandalkan. Maksimal enam nonce diizinkan. Setiap nonce harus berukuran antara 10 dan 74 byte, inklusif.

    Saat menyertakan nonce, pihak tepercaya harus memverifikasi bahwa nonce yang dikirim dalam permintaan token pengesahan sama dengan nonce dalam token yang ditampilkan. Jika berbeda, pihak tepercaya harus menolak token.

Mengurai dan memvalidasi token pengesahan

Contoh kode berikut di Go menunjukkan cara memvalidasi token pengesahan.

Token pengesahan 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))
}

Token pengesahan PKI

Untuk memvalidasi token, pihak tepercaya harus menyelesaikan langkah-langkah berikut:

  1. Parse header token untuk mendapatkan rantai sertifikat.

  2. Memvalidasi rantai sertifikat terhadap root yang disimpan. Anda harus sebelumnya mendownload sertifikat root dari URL yang ditentukan di kolom root_ca_uri yang ditampilkan di endpoint validasi token PKI.

  3. Periksa validitas sertifikat leaf.

  4. Gunakan sertifikat leaf untuk memvalidasi tanda tangan token, menggunakan algoritma yang ditentukan dalam kunci alg di header.

Setelah token divalidasi, {i>relying party<i} kemudian dapat mem-parsing klaim token.

// 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
}

Mengintegrasikan resource AWS

Anda dapat mengintegrasikan workload Confidential Space dengan resource AWS (seperti kunci atau data) menggunakan tag principal AWS. Integrasi ini menggunakan pengesahan aman yang disediakan oleh Confidential Space untuk memberikan akses terperinci ke resource AWS Anda.

Klaim tag principal AWS

Pengesahan Google Cloud membuat token identitas yang dapat diverifikasi yang berisi klaim tentang integritas dan konfigurasi beban kerja Confidential Space. Sebagian kecil dari klaim ini kompatibel dengan AWS, sehingga Anda dapat mengontrol akses ke resource AWS Anda. Klaim ini ditempatkan di klaim https://aws.amazon.com/tags, dalam objek principal_tags di token pengesahan. Untuk mengetahui informasi selengkapnya, lihat Klaim tag principal AWS.

Berikut adalah contoh struktur klaim 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"
      ]
    }
  }
}

Kebijakan AWS dengan klaim tanda tangan image container

Token AWS juga mendukung klaim tanda tangan image container. Klaim ini berguna jika terjadi perubahan beban kerja frekuensi tinggi atau saat berurusan dengan beberapa kolaborator atau pihak tepercaya.

Klaim tanda tangan image container terdiri dari ID kunci, yang dipisahkan oleh pembatas. Untuk menyertakan klaim ini dalam token AWS, Anda harus memberikan daftar yang diizinkan untuk ID kunci ini sebagai parameter tambahan dalam permintaan token Anda.

Hanya ID kunci yang cocok dengan kunci yang digunakan untuk menandatangani beban kerja Anda yang ditambahkan ke token. Hal ini memastikan bahwa hanya tanda tangan yang sah yang diterima.

Saat menulis kebijakan AWS, ingatlah bahwa ID kunci ditambahkan ke token sebagai satu string dengan karakter pembatas. Anda harus mengurutkan daftar ID kunci yang diharapkan menurut abjad dan membuat nilai string. Misalnya, jika Anda memiliki ID kunci aKey1, zKey2, dan bKey3, nilai klaim yang sesuai dalam kebijakan Anda harus aKey1=bKey3=zKey2.

Untuk mendukung beberapa set kunci, Anda dapat menambahkan beberapa nilai ke kebijakan Anda secara opsional.

"aws:RequestTag/container.signatures.key_ids": [
  "aKey1=bKey3=zKey2",
  "aKey1=bKey3",
  "zKey2"
]

Klaim tanda tangan image container (container.signatures.key_ids) dan klaim ringkasan image container (container.image_digest) tidak akan muncul bersama dalam satu token. Jika Anda menggunakan container.signatures.key_ids, pastikan Anda menghapus semua referensi ke container.image_digest dari kebijakan AWS Anda.

Berikut adalah contoh struktur klaim https://aws.amazon.com/tags yang berisi container.signatures.key_ids:

{
  "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"
      ]
    }
  }
}

Untuk penjelasan yang lebih mendetail tentang kolom token pengesahan, lihat Klaim token pengesahan.

Mengonfigurasi resource AWS: pihak tepercaya

Sebelum pihak tepercaya dapat mengonfigurasi resource AWS, pihak tepercaya harus mengonfigurasi AWS IAM untuk menetapkan Confidential Space sebagai penyedia OIDC gabungan dan membuat peran AWS IAM yang diperlukan.

Mengonfigurasi AWS IAM

  1. Untuk menambahkan layanan Pengesahan Google Cloud sebagai penyedia identitas di AWS IAM, lakukan hal berikut:

    1. Di konsol AWS, buka halaman Identity providers.

      Buka konsol AWS

    2. Untuk Provider type, pilih OpenID Connect.

    3. Untuk Provider URL, masukkan https://confidentialcomputing.googleapis.com.

    4. Untuk Audiens, masukkan URL yang Anda daftarkan dengan penyedia identitas dan yang membuat permintaan ke AWS. Misalnya, https://example.com.

    5. Klik Tambahkan penyedia.

  2. Untuk membuat peran AWS IAM bagi token Confidential Space, lakukan hal berikut:

    1. Di konsol AWS, buka halaman Roles.
    2. Klik Buat peran.
    3. Untuk jenis Entitas tepercaya, pilih Identitas web.
    4. Di bagian Web identity, pilih penyedia identitas dan audiens berdasarkan langkah sebelumnya.
    5. Klik Berikutnya. Anda dapat melewati pengeditan kebijakan AWS pada langkah ini.
    6. Klik Berikutnya, lalu tambahkan tag jika diperlukan.
    7. Untuk Nama peran, masukkan nama peran.
    8. (Opsional) Untuk Deskripsi, masukkan deskripsi untuk peran baru.
    9. Tinjau detailnya, lalu klik Buat peran.
  3. Edit kebijakan AWS peran yang Anda buat agar hanya memberikan akses ke beban kerja pilihan Anda.

    Kebijakan AWS ini memungkinkan Anda memeriksa klaim tertentu dalam token, seperti:

    • Ringkasan image container workload.
    • Target audiens token.
    • CONFIDENTIAL_SPACE adalah software yang berjalan di VM. Untuk mengetahui informasi selengkapnya, lihat swname di Klaim token pengesahan.
    • Atribut dukungan image Confidential Space produksi. Untuk mengetahui informasi selengkapnya, lihat confidential_space.support_attributes.

    Berikut adalah contoh kebijakan AWS yang memberikan akses ke workload dengan ringkasan dan audiens tertentu, CONFIDENTIAL_SPACE sebagai software yang berjalan di instance VM, dan STABLE sebagai atribut dukungan:

    {
      "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*"
            }
          }
        }
      ]
    }
    

Mengonfigurasi resource AWS

Setelah integrasi selesai, konfigurasikan resource AWS Anda. Langkah ini bergantung pada kasus penggunaan spesifik Anda. Misalnya, Anda dapat membuat bucket S3, kunci KMS, atau resource AWS lainnya. Pastikan untuk memberikan izin yang diperlukan kepada peran IAM AWS yang Anda buat sebelumnya untuk mengakses resource ini.

Mengonfigurasi workload Confidential Space Anda: pembuat workload

Untuk membuat permintaan token, ikuti petunjuk dalam artikel meminta token pengesahan dengan audiens kustom.

Untuk AWS_PrincipalTag klaim:

Berikut adalah contoh isi permintaan klaim AWS_PrincipalTag:

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

Langkah berikutnya