访问不受 Google Cloud IAM 管理的资源


如果对受保护资源的访问权限不是由 Google Cloud的 IAM 管理(例如,资源存储在其他云服务、本地或移动设备等本地设备上),您仍然可以向提供对这些资源的访问权限的设备或系统(也称为依赖方)对 Confidential Space 工作负载进行身份验证。

为此,依赖方必须向 Confidential Space 认证服务请求包含自定义受众群体和可选 Nonce 的认证令牌。请求此类证明令牌时,您需要先执行自己的令牌验证,然后才能授予对资源的访问权限。

以下文档介绍了将 Confidential Space 与 Google Cloud以外的资源搭配使用时涉及的概念,包括有关将 Confidential Space 工作负载与 AWS 资源无缝集成的说明。如需查看完整的演示步骤,请参阅 Codelab

认证令牌流程

工作负载代表依赖方请求认证令牌,并由认证服务返回。根据您的需求,您可以定义自定义受众群体,并酌情提供 Nonce。

未加密

为方便您理解令牌检索流程,此处介绍的流程不使用加密。在实践中,我们建议您使用 TLS 对通信进行加密。

下图显示了该流程:

认证令牌生成流程的流程图

  1. 依赖方向工作负载发送令牌请求,并附上其生成的可选 Nonce。

  2. 工作负载确定受众群体,将受众群体添加到请求中,并将请求发送到 Confidential Space 启动器。

  3. 启动器将请求发送到认证服务。

  4. 认证服务会生成一个包含指定受众群体和可选 Nonce 的令牌。

  5. 认证服务将令牌返回给启动器。

  6. 启动器将令牌返回给工作负载。

  7. 工作负载将令牌返回给依赖方。

  8. 依赖方会验证声明,包括受众群体和可选的 Nonce。

使用 TLS 加密

未加密的流会使请求容易受到中间人攻击。由于 Nonce 未绑定到数据输出或 TLS 会话,因此攻击者可以拦截请求并冒充工作负载。

为帮助防范此类攻击,您可以在依赖方和工作负载之间设置 TLS 会话,并使用 TLS 导出的密钥材料 (EKM) 作为 Nonce。TLS 导出的密钥材料会将认证绑定到 TLS 会话,并确认认证请求是通过安全通道发送的。此过程也称为“通道绑定”。

下图显示了使用通道绑定的流程:

渠道绑定令牌生成流程的流程图

  1. 依赖方与运行工作负载的机密虚拟机设置安全 TLS 会话。

  2. 依赖方使用安全的 TLS 会话发送令牌请求。

  3. 工作负载确定受众群体,并使用 TLS 导出的密钥材料生成 Nonce。

  4. 工作负载将请求发送到 Confidential Space 启动器。

  5. 启动器将请求发送到认证服务。

  6. 认证服务会生成一个包含指定受众群体和 Nonce 的令牌。

  7. 认证服务将令牌返回给启动器。

  8. 启动器将令牌返回给工作负载。

  9. 工作负载将令牌返回给依赖方。

  10. 依赖方使用 TLS 导出的密钥材料重新生成 Nonce。

  11. 依赖方会验证声明,包括目标对象和 Nonce。令牌中的 Nonce 必须与依赖方重新生成的 Nonce 一致。

认证令牌结构

认证令牌是采用以下结构的 JSON Web 令牌

  • 标头:描述签名算法。PKI 令牌还会将证书链存储在标头的 x5c 字段中。

  • 已签名 JSON 数据载荷:包含与依赖方工作负载相关的声明,例如主题、签发者、受众群体、Nonce 和到期时间。

  • 签名:用于验证令牌在传输过程中是否未发生更改。如需详细了解如何使用签名,请参阅如何验证 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 域套接字向监听网址 http://localhost/v1/token 发出 HTTP 请求。套接字文件位于 /run/container_launcher/teeserver.sock

当有请求发送到监听网址时,Confidential Space 启动器会管理证明证据收集,从证明服务请求证明令牌(传递任何自定义参数),然后将生成的令牌返回给工作负载。

Go 中的以下代码示例演示了如何通过 IPC 与启动器的 HTTP 服务器通信。

func getCustomTokenBytes(body string) ([]byte, error) {
  httpClient := http.Client{
    Transport: &http.Transport{
      // Set the DialContext field to a function that creates
      // a new network connection to a Unix domain socket
      DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
        return net.Dial("unix", "/run/container_launcher/teeserver.sock")
      },
    },
  }

  // Get the token from the IPC endpoint
  url := "http://localhost/v1/token"

  resp, err := httpClient.Post(url, "application/json", strings.NewReader(body))
  if err != nil {
    return nil, fmt.Errorf("failed to get raw token response: %w", err)
  }
  tokenbytes, err := io.ReadAll(resp.Body)
  if err != nil {
    return nil, fmt.Errorf("failed to read token body: %w", err)
  }
  fmt.Println(string(tokenbytes))
  return tokenbytes, nil
}

使用自定义受众群体请求认证令牌

HTTP 方法和网址:

POST http://localhost/v1/token

请求 JSON 正文:

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

请提供以下值:

  • AUDIENCE_NAME:必填。受众群体价值,即您为依赖方指定的名称。此值由工作负载设置。

    对于没有自定义受众群体的令牌,此字段默认为 https://sts.google.com。设置自定义受众群体时,不能使用值 https://sts.google.com。长度上限为 512 个字节。

    如需在令牌中添加自定义受众群体,工作负载(而非依赖方)必须先将其添加到认证令牌请求中,然后才能将请求发送到 Confidential Space 认证服务。这有助于防止依赖方为其不应访问的受保护资源请求令牌。

  • TOKEN_TYPE:必填。要返回的令牌的类型。 选择以下任一类型:

    • OIDC:这些令牌会根据 OIDC 令牌验证端点jwks_uri 字段中指定的公钥进行验证。公钥会定期轮替。

    • PKI:这些令牌会根据 PKI 令牌验证端点root_ca_uri 字段中指定的根证书进行验证。您需要自行存储此证书。证书每 10 年轮替一次。

    由于系统会使用有效期较长的证书(而非有效期较短的公钥)进行令牌验证,因此您的 IP 地址不会频繁向 Google 服务器公开。这意味着,与 OIDC 令牌相比,PKI 令牌可提供更高的隐私保护。

    您可以使用 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:可选。唯一的随机不透明值,可确保令牌只能使用一次。该值由依赖方设置。最多允许 6 个 Nonce。每个 Nonce 必须介于 10 到 74 字节之间(包括这两个数值)。

    添加 Nonce 时,依赖方必须验证在认证令牌请求中发送的 Nonce 是否与返回的令牌中的 Nonce 相同。如果两者不同,则依赖方必须拒绝相应令牌。

解析和验证认证令牌

以下 Go 代码示例展示了如何验证认证令牌。

OIDC 认证令牌

package main

import (
  "context"
  "crypto/rsa"
  "encoding/base64"
  "encoding/json"
  "errors"
  "fmt"
  "io"
  "math/big"
  "net"
  "net/http"
  "strings"

  "github.com/golang-jwt/jwt/v4"
)

const (
  socketPath     = "/run/container_launcher/teeserver.sock"
  expectedIssuer = "https://confidentialcomputing.googleapis.com"
  wellKnownPath  = "/.well-known/openid-configuration"
)

type jwksFile struct {
  Keys []jwk `json:"keys"`
}

type jwk struct {
  N   string `json:"n"`   // "nMMTBwJ7H6Id8zUCZd-L7uoNyz9b7lvoyse9izD9l2rtOhWLWbiG-7pKeYJyHeEpilHP4KdQMfUo8JCwhd-OMW0be_XtEu3jXEFjuq2YnPSPFk326eTfENtUc6qJohyMnfKkcOcY_kTE11jM81-fsqtBKjO_KiSkcmAO4wJJb8pHOjue3JCP09ZANL1uN4TuxbM2ibcyf25ODt3WQn54SRQTV0wn098Y5VDU-dzyeKYBNfL14iP0LiXBRfHd4YtEaGV9SBUuVhXdhx1eF0efztCNNz0GSLS2AEPLQduVuFoUImP4s51YdO9TPeeQ3hI8aGpOdC0syxmZ7LsL0rHE1Q",
  E   string `json:"e"`   // "AQAB" or 65537 as an int
  Kid string `json:"kid"` // "1f12fa916c3a0ef585894b4b420ad17dc9d6cdf5",

  // Unused fields:
  // Alg string `json:"alg"` // "RS256",
  // Kty string `json:"kty"` // "RSA",
  // Use string `json:"use"` // "sig",
}

type wellKnown struct {
  JwksURI string `json:"jwks_uri"` // "https://www.googleapis.com/service_accounts/v1/metadata/jwk/signer@confidentialspace-sign.iam.gserviceaccount.com"

  // Unused fields:
  // Iss                                   string `json:"issuer"`                                // "https://confidentialcomputing.googleapis.com"
  // Subject_types_supported               string `json:"subject_types_supported"`               // [ "public" ]
  // Response_types_supported              string `json:"response_types_supported"`              // [ "id_token" ]
  // Claims_supported                      string `json:"claims_supported"`                      // [ "sub", "aud", "exp", "iat", "iss", "jti", "nbf", "dbgstat", "eat_nonce", "google_service_accounts", "hwmodel", "oemid", "secboot", "submods", "swname", "swversion" ]
  // Id_token_signing_alg_values_supported string `json:"id_token_signing_alg_values_supported"` // [ "RS256" ]
  // Scopes_supported                      string `json:"scopes_supported"`                      // [ "openid" ]
}

func getWellKnownFile() (wellKnown, error) {
  httpClient := http.Client{}
  resp, err := httpClient.Get(expectedIssuer + wellKnownPath)
  if err != nil {
    return wellKnown{}, fmt.Errorf("failed to get raw .well-known response: %w", err)
  }

  wellKnownJSON, err := io.ReadAll(resp.Body)
  if err != nil {
    return wellKnown{}, fmt.Errorf("failed to read .well-known response: %w", err)
  }

  wk := wellKnown{}
  json.Unmarshal(wellKnownJSON, &wk)
  return wk, nil
}

func getJWKFile() (jwksFile, error) {
  wk, err := getWellKnownFile()
  if err != nil {
    return jwksFile{}, fmt.Errorf("failed to get .well-known json: %w", err)
  }

  // Get JWK URI from .wellknown
  uri := wk.JwksURI
  fmt.Printf("jwks URI: %v\n", uri)

  httpClient := http.Client{}
  resp, err := httpClient.Get(uri)
  if err != nil {
    return jwksFile{}, fmt.Errorf("failed to get raw JWK response: %w", err)
  }

  jwkbytes, err := io.ReadAll(resp.Body)
  if err != nil {
    return jwksFile{}, fmt.Errorf("failed to read JWK body: %w", err)
  }

  file := jwksFile{}
  err = json.Unmarshal(jwkbytes, &file)
  if err != nil {
    return jwksFile{}, fmt.Errorf("failed to unmarshall JWK content: %w", err)
  }

  return file, nil
}

// N and E are 'base64urlUInt' encoded: https://www.rfc-editor.org/rfc/rfc7518#section-6.3
func base64urlUIntDecode(s string) (*big.Int, error) {
  b, err := base64.RawURLEncoding.DecodeString(s)
  if err != nil {
    return nil, err
  }
  z := new(big.Int)
  z.SetBytes(b)
  return z, nil
}

func getRSAPublicKeyFromJWKsFile(t *jwt.Token) (any, error) {
  keysfile, err := getJWKFile()
  if err != nil {
    return nil, fmt.Errorf("failed to fetch the JWK file: %w", err)
  }

  // Multiple keys are present in this endpoint to allow for key rotation.
  // This method finds the key that was used for signing to pass to the validator.
  kid := t.Header["kid"]
  for _, key := range keysfile.Keys {
    if key.Kid != kid {
      continue // Select the key used for signing
    }

    n, err := base64urlUIntDecode(key.N)
    if err != nil {
      return nil, fmt.Errorf("failed to decode key.N %w", err)
    }
    e, err := base64urlUIntDecode(key.E)
    if err != nil {
      return nil, fmt.Errorf("failed to decode key.E %w", err)
    }

    // The parser expects an rsa.PublicKey: https://github.com/golang-jwt/jwt/blob/main/rsa.go#L53
    // or an array of keys. We chose to show passing a single key in this example as its possible
    // not all validators accept multiple keys for validation.
    return &rsa.PublicKey{
      N: n,
      E: int(e.Int64()),
    }, nil
  }

  return nil, fmt.Errorf("failed to find key with kid '%v' from well-known endpoint", kid)
}

func decodeAndValidateToken(tokenBytes []byte, keyFunc func(t *jwt.Token) (any, error)) (*jwt.Token, error) {
  var err error
  fmt.Println("Unmarshalling token and checking its validity...")
  token, err := jwt.NewParser().Parse(string(tokenBytes), keyFunc)

  fmt.Printf("Token valid: %v", token.Valid)
  if token.Valid {
    return token, nil
  }
  if ve, ok := err.(*jwt.ValidationError); ok {
    if ve.Errors&jwt.ValidationErrorMalformed != 0 {
      return nil, fmt.Errorf("token format invalid. Please contact the Confidential Space team for assistance")
    }
    if ve.Errors&(jwt.ValidationErrorNotValidYet) != 0 {
      // If device time is not synchronized with the Attestation Service you may need to account for that here.
      return nil, errors.New("token is not active yet")
    }
    if ve.Errors&(jwt.ValidationErrorExpired) != 0 {
      return nil, fmt.Errorf("token is expired")
    }
    return nil, fmt.Errorf("unknown validation error: %v", err)
  }

  return nil, fmt.Errorf("couldn't handle this token or couldn't read a validation error: %v", err)
}

func main() {
  // Get a token from a workload running in Confidential Space
  tokenbytes, err := getTokenBytesFromWorkload()

  // Write a method to return a public key from the well-known endpoint
  keyFunc := getRSAPublicKeyFromJWKsFile

  // Verify properties of the original Confidential Space workload that generated the attestation
  // using the token claims.
  token, err := decodeAndValidateToken(tokenbytes, keyFunc)
  if err != nil {
    panic(err)
  }

  claimsString, err := json.MarshalIndent(token.Claims, "", "  ")
  if err != nil {
    panic(err)
  }
  fmt.Println(string(claimsString))
}

PKI 认证令牌

如需验证令牌,依赖方必须完成以下步骤:

  1. 解析令牌的标头以获取证书链。

  2. 对照存储的根证书验证证书链。您必须先从 PKI 令牌验证端点返回的 root_ca_uri 字段中指定的网址下载根证书。

  3. 检查叶证书的有效性。

  4. 使用叶证书验证令牌签名,使用标头中的 alg 键中指定的算法。

令牌验证完毕后,依赖方便可以解析令牌的声明。

// This code is an example of how to validate a PKI token. This library is not an official library,
// nor is it endorsed by Google.

// ValidatePKIToken validates the PKI token returned from the attestation service is valid.
// Returns a valid jwt.Token or returns an error if invalid.
func ValidatePKIToken(storedRootCertificate x509.Certificate, attestationToken string) (jwt.Token, error) {
  // IMPORTANT: The attestation token should be considered untrusted until the certificate chain and
  // the signature is verified.

  jwtHeaders, err := ExtractJWTHeaders(attestationToken)
  if err != nil {
    return jwt.Token{}, fmt.Errorf("ExtractJWTHeaders(token) returned error: %v", err)
  }

  if jwtHeaders["alg"] != "RS256" {
    return jwt.Token{}, fmt.Errorf("ValidatePKIToken(string, *attestpb.Attestation, *v1mainpb.VerifyAttestationRequest) - got Alg: %v, want: %v", jwtHeaders["alg"], "RS256")
  }

  // Additional Check: Validate the ALG in the header matches the certificate SPKI.
  // https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.7
  // This is included in golangs jwt.Parse function

  x5cHeaders := jwtHeaders["x5c"].([]any)
  certificates, err := ExtractCertificatesFromX5CHeader(x5cHeaders)
  if err != nil {
    return jwt.Token{}, fmt.Errorf("ExtractCertificatesFromX5CHeader(x5cHeaders) returned error: %v", err)
  }

  // Verify the leaf certificate signature algorithm is an RSA key
  if certificates.LeafCert.SignatureAlgorithm != x509.SHA256WithRSA {
    return jwt.Token{}, fmt.Errorf("leaf certificate signature algorithm is not SHA256WithRSA")
  }

  // Verify the leaf certificate public key algorithm is RSA
  if certificates.LeafCert.PublicKeyAlgorithm != x509.RSA {
    return jwt.Token{}, fmt.Errorf("leaf certificate public key algorithm is not RSA")
  }

  // Verify the storedRootCertificate is the same as the root certificate returned in the token.
  // storedRootCertificate is downloaded from the confidential computing well known endpoint
  // https://confidentialcomputing.googleapis.com/.well-known/attestation-pki-root
  err = CompareCertificates(storedRootCertificate, *certificates.RootCert)
  if err != nil {
    return jwt.Token{}, fmt.Errorf("failed to verify certificate chain: %v", err)
  }

  err = VerifyCertificateChain(certificates)
  if err != nil {
    return jwt.Token{}, fmt.Errorf("VerifyCertificateChain(string, *attestpb.Attestation, *v1mainpb.VerifyAttestationRequest) - error verifying x5c chain: %v", err)
  }

  keyFunc := func(token *jwt.Token) (any, error) {
    return certificates.LeafCert.PublicKey, nil
  }

  verifiedJWT, err := jwt.Parse(attestationToken, keyFunc)
  return *verifiedJWT, err
}

// ExtractJWTHeaders parses the JWT and returns the headers.
func ExtractJWTHeaders(token string) (map[string]any, error) {
  parser := &jwt.Parser{}

  // The claims returned from the token are unverified at this point
  // Do not use the claims until the algorithm, certificate chain verification and root certificate
  // comparison is successful
  unverifiedClaims := &jwt.MapClaims{}
  parsedToken, _, err := parser.ParseUnverified(token, unverifiedClaims)
  if err != nil {
    return nil, fmt.Errorf("Failed to parse claims token: %v", err)
  }

  return parsedToken.Header, nil
}

// PKICertificates contains the certificates extracted from the x5c header.
type PKICertificates struct {
  LeafCert         *x509.Certificate
  IntermediateCert *x509.Certificate
  RootCert         *x509.Certificate
}

// ExtractCertificatesFromX5CHeader extracts the certificates from the given x5c header.
func ExtractCertificatesFromX5CHeader(x5cHeaders []any) (PKICertificates, error) {
  if x5cHeaders == nil {
    return PKICertificates{}, fmt.Errorf("VerifyAttestation(string, *attestpb.Attestation, *v1mainpb.VerifyAttestationRequest) - x5c header not set")
  }

  x5c := []string{}
  for _, header := range x5cHeaders {
    x5c = append(x5c, header.(string))
  }

  // The PKI token x5c header should have 3 certificates - leaf, intermediate and root
  if len(x5c) != 3 {
    return PKICertificates{}, fmt.Errorf("incorrect number of certificates in x5c header, expected 3 certificates, but got %v", len(x5c))
  }

  leafCert, err := DecodeAndParseDERCertificate(x5c[0])
  if err != nil {
    return PKICertificates{}, fmt.Errorf("cannot parse leaf certificate: %v", err)
  }

  intermediateCert, err := DecodeAndParseDERCertificate(x5c[1])
  if err != nil {
    return PKICertificates{}, fmt.Errorf("cannot parse intermediate certificate: %v", err)
  }

  rootCert, err := DecodeAndParseDERCertificate(x5c[2])
  if err != nil {
    return PKICertificates{}, fmt.Errorf("cannot parse root certificate: %v", err)
  }

  certificates := PKICertificates{
    LeafCert:         leafCert,
    IntermediateCert: intermediateCert,
    RootCert:         rootCert,
  }
  return certificates, nil
}

// DecodeAndParseDERCertificate decodes the given DER certificate string and parses it into an x509 certificate.
func DecodeAndParseDERCertificate(certificate string) (*x509.Certificate, error) {
  bytes, _ := base64.StdEncoding.DecodeString(certificate)

  cert, err := x509.ParseCertificate(bytes)
  if err != nil {
    return nil, fmt.Errorf("cannot parse certificate: %v", err)
  }

  return cert, nil
}

// DecodeAndParsePEMCertificate decodes the given PEM certificate string and parses it into an x509 certificate.
func DecodeAndParsePEMCertificate(certificate string) (*x509.Certificate, error) {
  block, _ := pem.Decode([]byte(certificate))
  if block == nil {
    return nil, fmt.Errorf("cannot decode certificate")
  }

  cert, err := x509.ParseCertificate(block.Bytes)
  if err != nil {
    return nil, fmt.Errorf("cannot parse certificate: %v", err)
  }

  return cert, nil
}

// VerifyCertificateChain verifies the certificate chain from leaf to root.
// It also checks that all certificate lifetimes are valid.
func VerifyCertificateChain(certificates PKICertificates) error {
  if isCertificateLifetimeValid(certificates.LeafCert) {
    return fmt.Errorf("leaf certificate is not valid")
  }

  if isCertificateLifetimeValid(certificates.IntermediateCert) {
    return fmt.Errorf("intermediate certificate is not valid")
  }
  interPool := x509.NewCertPool()
  interPool.AddCert(certificates.IntermediateCert)

  if isCertificateLifetimeValid(certificates.RootCert) {
    return fmt.Errorf("root certificate is not valid")
  }
  rootPool := x509.NewCertPool()
  rootPool.AddCert(certificates.RootCert)

  _, err := certificates.LeafCert.Verify(x509.VerifyOptions{
    Intermediates: interPool,
    Roots:         rootPool,
    KeyUsages:     []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
  })

  if err != nil {
    return fmt.Errorf("failed to verify certificate chain: %v", err)
  }

  return nil
}

func isCertificateLifetimeValid(certificate *x509.Certificate) bool {
  currentTime := time.Now()
  // check the current time is after the certificate NotBefore time
  if !currentTime.After(certificate.NotBefore) {
    return false
  }

  // check the current time is before the certificate NotAfter time
  if currentTime.Before(certificate.NotAfter) {
    return false
  }

  return true
}

// CompareCertificates compares two certificate fingerprints.
func CompareCertificates(cert1 x509.Certificate, cert2 x509.Certificate) error {
  fingerprint1 := sha256.Sum256(cert1.Raw)
  fingerprint2 := sha256.Sum256(cert2.Raw)
  if fingerprint1 != fingerprint2 {
    return fmt.Errorf("certificate fingerprint mismatch")
  }
  return nil
}

集成 AWS 资源

您可以使用 AWS 正文标记将 Confidential Space 工作负载与 AWS 资源(例如密钥或数据)集成。此集成使用 Confidential Space 提供的安全证明来授予对 AWS 资源的精细访问权限。

AWS 正文标记声明

Google Cloud 认证会生成可验证的身份令牌,其中包含与 Confidential Space 工作负载的完整性和配置相关的声明。其中一部分声明与 AWS 兼容,可让您控制对 AWS 资源的访问权限。这些声明位于 https://aws.amazon.com/tags 声明中,即位于认证令牌中的 principal_tags 对象中。如需了解详情,请参阅 AWS 主标记声明

以下是 https://aws.amazon.com/tags 声明结构示例:

{
  "https://aws.amazon.com/tags": {
    "principal_tags": {
      "confidential_space.support_attributes": [
        "LATEST=STABLE=USABLE"
      ],
      "container.image_digest": [
        "sha256:6eccbcf1a1de8bf50aefbb37e8c3600d5b59f4a12cf7d964b6f8ef964b782eb2"
      ],
      "gce.project_id": [
        "confidentialcomputing-e2e"
      ],
      "gce.zone": [
        "us-west1-a"
      ],
      "hwmodel": [
        "GCP_AMD_SEV"
      ],
      "swname": [
        "CONFIDENTIAL_SPACE"
      ],
      "swversion": [
        "250101"
      ]
    }
  }
}

包含容器映像签名声明的 AWS 政策

AWS 令牌还支持容器映像签名声明。在工作负载频繁发生变化或处理多个协作者或依赖方时,这些断言非常有用。

容器映像签名声明由键 ID 组成,这些 ID 之间由分隔符分隔。如需在 AWS 令牌中包含这些声明,您需要在令牌请求中将这些密钥 ID 的许可名单作为额外参数提供。

只有与用于对工作负载进行签名的密钥匹配的密钥 ID 会添加到令牌中。这样可以确保仅接受已获授权的签名。

编写 AWS 政策时,请注意密钥 ID 会作为包含分隔符字符的单个字符串添加到令牌中。您需要按字母顺序对预期的键 ID 列表进行排序,然后构建字符串值。例如,如果您的键 ID 分别为 aKey1zKey2bKey3,则政策中的相应声明值应为 aKey1=bKey3=zKey2

如需支持多组键,您可以选择向政策添加多个值。

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

容器映像签名声明 (container.signatures.key_ids) 和容器映像摘要声明 (container.image_digest) 不会在单个令牌中同时出现。如果您使用的是 container.signatures.key_ids,请务必从 AWS 政策中移除对 container.image_digest 的所有引用。

以下是一个包含 container.signatures.key_idshttps://aws.amazon.com/tags 声明结构示例:

{
  "https://aws.amazon.com/tags": {
    "principal_tags": {
      "confidential_space.support_attributes": [
        "LATEST=STABLE=USABLE"
      ],
      "container.signatures.key_ids": [
        "keyid1=keyid2=keyid3"
      ],
      "gce.project_id": [
        "confidentialcomputing-e2e"
      ],
      "gce.zone": [
        "us-west1-a"
      ],
      "hwmodel": [
        "GCP_AMD_SEV"
      ],
      "swname": [
        "CONFIDENTIAL_SPACE"
      ],
      "swversion": [
        "250101"
      ]
    }
  }
}

如需详细了解认证令牌字段,请参阅认证令牌声明

配置 AWS 资源:依赖方

在依赖方能够配置其 AWS 资源之前,需要配置 AWS IAM 以将 Confidential Space 设为联邦 OIDC 提供程序,并创建必要的 AWS IAM 角色。

配置 AWS IAM

  1. 如需在 AWS IAM 中将 Google Cloud Attestation 服务添加为身份提供商,请执行以下操作:

    1. 在 AWS 控制台中,前往身份提供商页面。

      前往 AWS 控制台

    2. 对于提供方类型,请选择 OpenID Connect

    3. 提供商网址中,输入 https://confidentialcomputing.googleapis.com。

    4. 对于受众群体,请输入您在身份提供方处注册的网址,该网址用于向 AWS 发出请求。例如,https://example.com。

    5. 点击添加提供商

  2. 如需为机密空间令牌创建 AWS IAM 角色,请执行以下操作:

    1. 在 AWS 控制台中,前往角色页面。
    2. 点击 Create role
    3. 对于可信实体类型,选择 Web 身份
    4. Web 身份部分下,根据上一步中的选择选择身份提供方和受众群体。
    5. 点击下一步。您可以在此步骤中跳过修改 AWS 政策。
    6. 点击下一步,并根据需要添加标记。
    7. 角色名称中,输入角色名称。
    8. (可选)对于说明,输入新角色的说明。
    9. 查看详细信息,然后点击创建角色
  3. 修改您创建的角色的 AWS 政策,以仅授予对您选择的工作负载的访问权限。

    借助此 AWS 政策,您可以检查令牌中的特定声明,例如:

    • 工作负载的容器映像摘要。
    • 令牌的目标受众群体。
    • CONFIDENTIAL_SPACE 是指在虚拟机上运行的软件。如需了解详情,请参阅证明令牌声明中的 swname
    • 生产 Confidential Space 映像支持特性。如需了解详情,请参阅 confidential_space.support_attributes

    以下 AWS 政策示例会授予对具有指定摘要和受众群体的工作负载的访问权限,其中 CONFIDENTIAL_SPACE 是指在虚拟机实例上运行的软件,STABLE 是指支持属性:

    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Principal": {
            "Federated": "arn:aws:iam::232510754029:oidc-provider/confidentialcomputing.googleapis.com"
          },
          "Action": [
            "sts:AssumeRoleWithWebIdentity",
            "sts:TagSession"
          ],
          "Condition": {
            "StringEquals": {
              "confidentialcomputing.googleapis.com:aud": "https://integration.test",
              "aws:RequestTag/swname": "CONFIDENTIAL_SPACE",
              "aws:RequestTag/container.image_digest": "sha256:ac74cbeca443e36325bad15a7c28f2598b22966aa94681a444553f0b838717cf"
            },
            "StringLike": {
              "aws:RequestTag/confidential_space.support_attributes": "*STABLE*"
            }
          }
        }
      ]
    }
    

配置 AWS 资源

集成完成后,请配置 AWS 资源。此步骤取决于您的具体用例。例如,您可以创建 S3 存储分区KMS 密钥或其他 AWS 资源。请务必向您之前创建的 AWS IAM 角色授予访问这些资源所需的权限。

配置 Confidential Space 工作负载:工作负载作者

如需创建令牌请求,请按照使用自定义受众群体请求认证令牌中的说明操作。

对于 AWS_PrincipalTag 版权主张:

  • 在 AWS 集成的令牌请求中,Nonce 字段是可选字段。
  • 添加您在配置 AWS 资源:信赖方中配置的受众群体。
  • 将 token_type 设置为 AWS_PRINCIPALTAGS

以下是 AWS_PrincipalTag 声明请求正文示例:

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

后续步骤