Acessar recursos não gerenciados pelo IAM do Google Cloud


Se o acesso aos seus recursos protegidos não for gerenciado pelo IAM do Google Cloud, por exemplo, se os recursos estiverem armazenados em outro serviço de nuvem, no local ou em um dispositivo local, como um smartphone, ainda será possível autenticar uma carga de trabalho do Confidential Space no dispositivo ou sistema que fornece acesso a esses recursos, também conhecido como parte confiável.

Para isso, a parte confiável precisa solicitar um token de comprovação do serviço de comprovação do Confidential Space com um público-alvo personalizado e nonces opcionais. Ao solicitar um token de atestado como este, é necessário realizar sua própria validação de token antes de conceder acesso aos recursos.

A documentação a seguir aborda os conceitos envolvidos no uso do Confidential Space com recursos fora de Google Cloud, incluindo instruções para integrar perfeitamente suas cargas de trabalho do Confidential Space com recursos da AWS. Para um tutorial completo, consulte o codelab.

Fluxo de token de atestado

Os tokens de comprovação são solicitados pela carga de trabalho em nome de uma parte confiável e retornados pelo serviço de comprovação. Dependendo das suas necessidades, é possível definir um público-alvo personalizado e fornecer nonces.

Não criptografado

Para facilitar a compreensão do processo de recuperação de token, o fluxo apresentado aqui não usa criptografia. Na prática, recomendamos criptografar as comunicações com TLS.

O diagrama a seguir mostra o fluxo:

Um diagrama de fluxo do fluxo de geração de token de comprovação

  1. A parte confiável envia uma solicitação de token para a carga de trabalho, com nonces opcionais que ela gerou.

  2. A carga de trabalho determina o público-alvo, adiciona o público à solicitação e envia a solicitação para o iniciador do Confidential Space.

  3. O iniciador envia a solicitação ao serviço de atestado.

  4. O serviço de atestado gera um token que contém o público-alvo especificado e nonces opcionais.

  5. O serviço de atestado retorna o token ao iniciador.

  6. O acesso rápido retorna o token para a carga de trabalho.

  7. A carga de trabalho retorna o token para a parte confiável.

  8. A parte confiável verifica as declarações, incluindo o público e os valores de uso único opcionais.

Criptografada com TLS

Um fluxo não criptografado deixa a solicitação vulnerável a ataques de máquina no meio. Como um nonce não está vinculado à saída de dados ou a uma sessão TLS, um invasor pode interceptar a solicitação e se passar pela carga de trabalho.

Para evitar esse tipo de ataque, configure uma sessão TLS entre a parte confiável e a carga de trabalho e use o material de chave exportado (EKM) do TLS como um nonce. O material de chave exportado do TLS vincula o atestado à sessão do TLS e confirma que a solicitação de atestado foi enviada por um canal seguro. Esse processo também é conhecido como vinculação de canal.

O diagrama a seguir mostra o fluxo usando a vinculação de canais:

Um diagrama de fluxo do fluxo de geração de token de vinculação de canal

  1. A parte confiante configura uma sessão TLS segura com a VM confidencial que está executando a carga de trabalho.

  2. A parte confiável envia uma solicitação de token usando a sessão TLS segura.

  3. A carga de trabalho determina o público-alvo e gera um valor de uso único usando o material de chave exportado do TLS.

  4. A carga de trabalho envia a solicitação para o iniciador do Confidential Space.

  5. O iniciador envia a solicitação ao serviço de atestado.

  6. O serviço de atestado gera um token que contém o público-alvo e o valor de uso único especificados.

  7. O serviço de atestado retorna o token ao iniciador.

  8. O acesso rápido retorna o token para a carga de trabalho.

  9. A carga de trabalho retorna o token para a parte confiável.

  10. A parte confiável regenera o nonce usando o material da chave exportada do TLS.

  11. A parte confiável verifica as declarações, incluindo o público-alvo e o nonce. O valor de uso único no token precisa corresponder ao valor regenerado pela parte confiável.

Estrutura do token de atestado

Os tokens de comprovação são tokens da Web JSON com a seguinte estrutura:

  • Cabeçalho: descreve o algoritmo de assinatura. Os tokens de PKI também armazenam a cadeia de certificados no cabeçalho no campo x5c.

  • Payload de dados JSON assinado: contém declarações sobre a carga de trabalho para a parte confiante, como assunto, emissor, público, nonces e tempo de expiração.

  • Assinatura: fornece validação de que o token não mudou durante o trânsito. Para mais informações sobre como usar a assinatura, consulte Como validar um token de ID do OpenID Connect.

O exemplo de código a seguir é um exemplo de um token de comprovação codificado gerado na imagem 240500 do Confidential Space. As imagens mais recentes podem conter campos adicionais. Use https://jwt.io/ para decodificar (a assinatura é editada).

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

Confira a versão decodificada do exemplo anterior:

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

Para uma explicação mais detalhada dos campos de token de atestado, consulte Declarações de token de atestado.

Recuperar tokens de atestado

Conclua as etapas a seguir para implementar tokens de atestado no seu ambiente do Confidential Space:

  1. Configure um cliente HTTP na sua carga de trabalho.

  2. Na sua carga de trabalho, use o cliente HTTP para fazer uma solicitação HTTP ao URL de escuta, http://localhost/v1/token, em um socket de domínio Unix. O arquivo de soquete está localizado em /run/container_launcher/teeserver.sock.

.

Quando uma solicitação é feita ao URL de escuta, o iniciador do Confidential Space gerencia a coleta de evidências de comprovação, solicita um token de comprovação do serviço de comprovação (transmitindo parâmetros personalizados) e retorna o token gerado para a carga de trabalho.

O exemplo de código a seguir em Go demonstra como se comunicar com o servidor HTTP do iniciador por 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
}

Solicitar um token de atestado com um público-alvo personalizado

Método HTTP e URL:

POST http://localhost/v1/token

Solicitar corpo JSON:

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

Forneça os valores a seguir:

  • AUDIENCE_NAME: obrigatório. O valor do público-alvo, que é o nome que você deu à parte confiável. Isso é definido pela carga de trabalho.

    O padrão desse campo é https://sts.google.com para tokens sem um público-alvo personalizado. O valor https://sts.google.com não pode ser usado ao definir um público-alvo personalizado. O comprimento máximo é de 512 bytes.

    Para incluir um público-alvo personalizado em um token, a carga de trabalho, e não a parte confiável, precisa adicioná-lo à solicitação de token de comprovação antes de enviar a solicitação ao serviço de comprovação do Confidential Space. Isso ajuda a evitar que a parte confiável solicite um token para um recurso protegido a que ela não deveria ter acesso.

  • TOKEN_TYPE: obrigatório. O tipo de token a ser retornado. Escolha um dos seguintes tipos:

    • OIDC: esses tokens são validados com uma chave pública especificada no campo jwks_uri no endpoint de validação de token do OIDC. A chave pública é alternada regularmente.

    • PKI: esses tokens são validados em relação a um certificado raiz especificado no campo root_ca_uri no endpoint de validação de token PKI. Você precisa armazenar esse certificado por conta própria. O certificado é alternado a cada 10 anos.

    Como certificados de longa duração são usados em vez de chaves públicas de curta duração para validação de token, seus endereços IP não são expostos aos servidores do Google com tanta frequência. Isso significa que os tokens de PKI oferecem mais privacidade do que os tokens do OIDC.

    Você pode validar a impressão digital do certificado com o OpenSSL:

    openssl x509 -fingerprint -in confidential_space_root.crt
    

    A impressão digital precisa corresponder ao seguinte resumo SHA-1:

    B9:51:20:74:2C:24:E3:AA:34:04:2E:1C:3B:A3:AA:D2:8B:21:23:21
    
  • NONCE: opcional. Um valor exclusivo, aleatório e opaco que garante que um token só possa ser usado uma vez. O valor é definido pela parte confiável. São permitidos até seis nonces. Cada nonce precisa ter entre 10 e 74 bytes, incluindo esses dois valores.

    Ao incluir um valor de uso único, a parte confiável precisa verificar se os valores enviados na solicitação de token de comprovação são os mesmos do token retornado. Se forem diferentes, a parte confiável precisará rejeitar o token.

Analisar e validar tokens de atestado

Os exemplos de código a seguir em Go mostram como validar tokens de comprovação.

Tokens de atestado do 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))
}

Tokens de comprovação da ICP

Para validar o token, a parte confiável precisa concluir as seguintes etapas:

  1. Analise o cabeçalho do token para receber a cadeia de certificados.

  2. Valide a cadeia de certificados em relação à raiz armazenada. Você precisa ter baixado o certificado raiz da URL especificada no campo root_ca_uri retornado no endpoint de validação do token de ICP.

  3. Verifique a validade do certificado folha.

  4. Use o certificado folha para validar a assinatura do token usando o algoritmo especificado na chave alg no cabeçalho.

Depois que o token é validado, a parte confiante pode analisar as declarações do 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
}

Integrar recursos da AWS

É possível integrar suas cargas de trabalho do Confidential Space com recursos da AWS (como chaves ou dados) usando tags principais da AWS. Essa integração usa a declaração segura fornecida pelo Confidential Space para conceder acesso granular aos seus recursos da AWS.

Declarações de tags principais da AWS

O Google Cloud Attestation gera tokens de identidade verificáveis que contêm declarações sobre a integridade e a configuração da carga de trabalho do Confidential Space. Um subconjunto dessas reivindicações é compatível com a AWS, permitindo controlar o acesso aos recursos da AWS. Essas declarações são colocadas nas declarações https://aws.amazon.com/tags, no objeto principal_tags no token de atestado. Para mais informações, consulte Declarações de tag principal da AWS.

Confira a seguir um exemplo de estrutura de declaração 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"
      ]
    }
  }
}

Políticas da AWS com declarações de assinatura de imagem de contêiner

Os tokens da AWS também aceitam declarações de assinatura de imagens de contêiner. Essas declarações são úteis em caso de mudanças frequentes na carga de trabalho ou ao lidar com vários colaboradores ou partes confiáveis.

As declarações de assinatura de imagem de contêiner consistem em IDs de chave, que são separados por um delimitador. Para incluir essas declarações no token da AWS, forneça uma lista de permissões desses IDs de chave como um parâmetro adicional na solicitação de token.

Somente os IDs de chave que correspondem às chaves usadas para assinar sua carga de trabalho são adicionados ao token. Isso garante que apenas assinaturas autorizadas sejam aceitas.

Ao escrever sua política da AWS, lembre-se de que os IDs de chave são adicionados ao token como uma única string com caracteres delimitadores. É necessário classificar em ordem alfabética a lista de IDs de chave esperados e construir o valor da string. Por exemplo, se você tiver os IDs de chave aKey1, zKey2 e bKey3, o valor de declaração correspondente na sua política será aKey1=bKey3=zKey2.

Para oferecer suporte a vários conjuntos de chaves, adicione vários valores à política.

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

A declaração de assinaturas de imagens de contêiner (container.signatures.key_ids) e a declaração de resumo de imagens de contêiner (container.image_digest) não aparecem juntas em um único token. Se você estiver usando container.signatures.key_ids, remova todas as referências a container.image_digest das políticas da AWS.

Confira a seguir um exemplo de estrutura de declaração https://aws.amazon.com/tags que contém 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"
      ]
    }
  }
}

Para uma explicação mais detalhada dos campos de token de atestado, consulte Declarações de token de atestado.

Configurar recursos da AWS: parte confiável

Antes que a parte confiável possa configurar os recursos da AWS, ela precisa configurar o IAM da AWS para estabelecer o Confidential Space como um provedor OIDC federado e criar a função necessária do IAM da AWS.

Configurar o IAM da AWS

  1. Para adicionar o serviço de atestado do Google Cloud como um provedor de identidade no IAM da AWS, faça o seguinte:

    1. No console da AWS, acesse a página Provedores de identidade.

      Acessar o console da AWS

    2. Em Tipo de provedor, selecione OpenID Connect.

    3. Em URL do provedor, digite https://confidentialcomputing.googleapis.com.

    4. Em Público-alvo, insira o URL registrado no provedor de identidade e que faz solicitações para a AWS. Por exemplo, https://example.com.

    5. Clique em Adicionar provedor.

  2. Para criar um papel do IAM da AWS para tokens do Confidential Space, faça o seguinte:

    1. No console da AWS, acesse a página Funções.
    2. Clique em Criar papel.
    3. Em Tipo de entidade confiável, selecione Identidade da Web.
    4. Na seção Identidade da Web, selecione o provedor de identidade e o público-alvo com base na etapa anterior.
    5. Clique em Próxima. Você pode pular a edição da política da AWS nesta etapa.
    6. Clique em Próxima e adicione tags, se necessário.
    7. Em Nome da função, insira o nome da função.
    8. (Opcional) Em Descrição, insira uma descrição para a nova função.
    9. Revise os detalhes e clique em Criar função.
  3. Edite a política da AWS do papel criado para conceder acesso apenas à carga de trabalho escolhida.

    Essa política da AWS permite verificar declarações específicas no token, como:

    • O resumo da imagem do contêiner da carga de trabalho.
    • O público-alvo do token.
    • Esse CONFIDENTIAL_SPACE é o software em execução na VM. Para mais informações, consulte swname em Declarações de token de atestado.
    • O atributo de suporte à imagem de produção do Confidential Space. Para ver mais informações, consulte confidential_space.support_attributes.

    Confira a seguir um exemplo de política da AWS que concede acesso a uma carga de trabalho com um resumo e um público-alvo especificados, CONFIDENTIAL_SPACE como o software em execução na instância de VM e STABLE como o atributo de suporte:

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

Configurar recursos da AWS

Depois que a integração for concluída, configure os recursos da AWS. Essa etapa depende do seu caso de uso específico. Por exemplo, você pode criar um bucket do S3, uma chave do KMS ou outros recursos da AWS. Conceda ao papel do IAM da AWS criado anteriormente as permissões necessárias para acessar esses recursos.

Configurar sua carga de trabalho do Confidential Space: autor da carga de trabalho

Para criar solicitações de token, siga as instruções em Solicitar um token de comprovação com um público-alvo personalizado.

Para declarações de AWS_PrincipalTag:

Veja a seguir um exemplo de corpo de solicitação de reivindicação AWS_PrincipalTag:

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

A seguir