Aceder a recursos não geridos pela IAM do Google Cloud


Se o acesso aos seus recursos protegidos não for gerido pelo IAM da Google Cloud, por exemplo, os recursos estiverem armazenados noutro serviço na nuvem, no local ou num dispositivo local, como um telemóvel, pode continuar a autenticar uma carga de trabalho do espaço confidencial no dispositivo ou no sistema que fornece acesso a esses recursos, também conhecido como parte fidedigna. Google Cloud

Para tal, a parte fidedigna tem de pedir um token de atestação ao serviço de atestação do espaço confidencial com um público-alvo personalizado e números aleatórios opcionais. Quando pede um token de atestação como este, tem de fazer a sua própria validação do token antes de conceder acesso aos recursos.

A documentação que se segue aborda os conceitos envolvidos na utilização do espaço confidencial com recursos fora do Google Cloud, incluindo instruções para integrar as suas cargas de trabalho do espaço confidencial com recursos da AWS. Para ver um passo a passo completo, consulte o codelab.

Fluxo de tokens de atestação

Os tokens de atestação são pedidos pela carga de trabalho em nome de uma parte fidedigna e devolvidos pelo serviço de atestação. Consoante as suas necessidades, pode definir um público-alvo personalizado e, opcionalmente, fornecer números aleatórios únicos.

Sem encriptação

Para facilitar a compreensão do processo de obtenção de tokens, o fluxo apresentado aqui não usa encriptação. Na prática, recomendamos que encriptem as comunicações com TLS.

O diagrama seguinte mostra o fluxo:

Um diagrama de fluxo do fluxo de geração de tokens de atestação

  1. A parte fidedigna envia um pedido de token para a carga de trabalho, com valores únicos opcionais que gerou.

  2. A carga de trabalho determina o público-alvo, adiciona o público-alvo ao pedido e envia o pedido para o iniciador do espaço confidencial.

  3. O iniciador envia o pedido ao serviço de atestação.

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

  5. O serviço de atestação devolve o token ao launcher.

  6. O iniciador devolve o token à carga de trabalho.

  7. A carga de trabalho devolve o token à parte fidedigna.

  8. A parte fidedigna valida as reivindicações, incluindo o público-alvo e os valores únicos opcionais.

Encriptada com TLS

Um fluxo não encriptado deixa o pedido vulnerável a ataques de máquina no meio. Uma vez que um nonce não está associado à saída de dados nem a uma sessão TLS, um atacante pode intercetar o pedido e roubar a identidade da carga de trabalho.

Para ajudar a evitar este tipo de ataque, pode configurar uma sessão TLS entre a parte fidedigna e a carga de trabalho e usar o material de chave exportado (EKM) do TLS como um nonce. O material da chave exportada TLS associa a atestação à sessão TLS e confirma que o pedido de atestação foi enviado através de um canal seguro. Este processo também é conhecido como associação de canais.

O diagrama seguinte mostra o fluxo através da vinculação de canal:

Um diagrama de fluxo do fluxo de geração de tokens de associação de canais

  1. A parte fidedigna configura uma sessão TLS segura com a Confidential VM que está a executar a carga de trabalho.

  2. A parte fidedigna envia um pedido de token através da sessão TLS segura.

  3. A carga de trabalho determina o público-alvo e gera um nonce através do material de chave exportado do TLS.

  4. A carga de trabalho envia o pedido para o iniciador do espaço confidencial.

  5. O iniciador envia o pedido ao serviço de atestação.

  6. O serviço de atestação gera um token que contém o público-alvo e o nonce especificados.

  7. O serviço de atestação devolve o token ao launcher.

  8. O iniciador devolve o token à carga de trabalho.

  9. A carga de trabalho devolve o token à parte fidedigna.

  10. A parte fidedigna volta a gerar o nonce através do material da chave exportada do TLS.

  11. A parte fidedigna valida as reivindicações, incluindo o público-alvo e o número aleatório. O nonce no token tem de corresponder ao nonce que é regenerado pela parte fidedigna.

Estrutura do token de atestação

Os tokens de atestação são símbolos 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 afirmações sobre a carga de trabalho para a parte fidedigna, como o assunto, o emissor, o público-alvo, os números aleatórios e a hora de validade.

  • Assinatura: fornece a validação de que o token não foi alterado durante a transmissão. Para mais informações sobre a utilização da assinatura, consulte o artigo Como validar um token de ID do OpenID Connect.

O seguinte exemplo de código é um exemplo de um token de atestação codificado gerado no espaço confidencial imagem 240500. As imagens mais recentes podem conter campos adicionais. Pode usar https://jwt.io/ para descodificá-lo (a assinatura é ocultada).

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

Segue-se a versão descodificada 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 do token de atestação, consulte o artigo Reivindicações do token de atestação.

Obtenha tokens de atestação

Conclua os passos seguintes para implementar tokens de atestação 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 um pedido HTTP ao URL de escuta, http://localhost/v1/token, através de um socket de domínio Unix. O ficheiro de entrada está localizado em /run/container_launcher/teeserver.sock.

.

Quando é feito um pedido ao URL de escuta, o iniciador do espaço confidencial gere a recolha de provas de atestação, pede um token de atestação ao serviço de atestação (transmitindo quaisquer parâmetros personalizados) e, em seguida, devolve o token gerado à carga de trabalho.

O seguinte exemplo de código em Go demonstra como comunicar com o servidor HTTP do iniciador através de 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
}

Peça um token de atestação com um público-alvo personalizado

Método HTTP e URL:

POST http://localhost/v1/token

Corpo JSON do pedido:

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

Indique os seguintes valores:

  • AUDIENCE_NAME: obrigatório. O valor do público-alvo, que é o nome que atribuiu à parte fidedigna. Esta opção é definida pela carga de trabalho.

    A predefinição deste campo é https://sts.google.com para tokens sem um público-alvo personalizado. Não é possível usar o valor https://sts.google.com quando define um público-alvo personalizado. O comprimento máximo é de 512 bytes.

    Para incluir um público-alvo personalizado num token, a carga de trabalho, e não a parte fidedigna, tem de o adicionar ao pedido de token de atestação antes de enviar o pedido ao serviço de atestação do espaço confidencial. Isto ajuda a impedir que a parte fidedigna solicite um token para um recurso protegido ao qual não deve ter acesso.

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

    Uma vez que são usados certificados com um longo período de validade em vez de chaves públicas com um curto período de validade para a validação de tokens, os seus endereços IP não são expostos aos servidores da Google com tanta frequência. Isto significa que os tokens de ICP oferecem maior privacidade do que os tokens de OIDC.

    Pode validar a impressão digital do certificado com o OpenSSL:

    openssl x509 -fingerprint -in confidential_space_root.crt
    

    A impressão digital deve 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 único, aleatório e opaco que garante que um token só pode ser usado uma vez. O valor é definido pela parte fidedigna. São permitidos até seis números únicos. Cada nonce tem de ter entre 10 e 74 bytes, inclusive.

    Quando inclui um nonce, a parte fidedigna tem de verificar se os nonces enviados no pedido de token de atestação são os mesmos que os nonces no token devolvido. Se forem diferentes, a parte fidedigna tem de rejeitar o token.

Analise e valide tokens de atestação

Os seguintes exemplos de código em Go mostram como validar tokens de atestação.

Tokens de atestação 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 atestação de PKI

Para validar o token, a parte fidedigna tem de concluir os seguintes passos:

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

  2. Validar a cadeia de certificados em relação à raiz armazenada. Tem de ter transferido anteriormente o certificado de raiz do URL especificado no campo root_ca_uri devolvido no ponto final de validação do token de ICP.

  3. Verifique a validade do certificado de folha.

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

Depois de o token ser validado, a parte fidedigna pode analisar as reivindicaçõ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
}

Integre recursos da AWS

Pode integrar as suas cargas de trabalho do Confidential Space com recursos da AWS (como chaves ou dados) através de etiquetas principais da AWS. Esta integração usa a atestação segura fornecida pelo Confidential Space para conceder acesso detalhado aos seus recursos da AWS.

Reivindicações de etiquetas principais da AWS

A atestação do Google Cloud gera tokens de identidade validáveis que contêm reivindicações sobre a integridade e a configuração da carga de trabalho do espaço confidencial. Um subconjunto destas reivindicações é compatível com a AWS, o que lhe permite controlar o acesso aos seus recursos da AWS. Estas reivindicações são colocadas nas reivindicações https://aws.amazon.com/tags, no objeto principal_tags no token de atestação. Para mais informações, consulte o artigo Reivindicações de etiquetas principais da AWS.

Segue-se um exemplo de uma estrutura de reivindicaçã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 reivindicações de assinatura de imagens de contentores

Os tokens da AWS também suportam reivindicações de assinatura de imagens de contentores. Estas reivindicações são úteis em caso de alterações de carga de trabalho de alta frequência ou quando lida com vários colaboradores ou partes fidedignas.

As reivindicações de assinatura de imagens de contentores consistem em IDs de chaves, que estão separados por um delimitador. Para incluir estas reivindicações no token da AWS, tem de fornecer uma lista de autorizações destes IDs de chaves como um parâmetro adicional no seu pedido de token.

Apenas os IDs das chaves que correspondem às chaves usadas para assinar a sua carga de trabalho são adicionados ao token. Isto garante que apenas são aceites assinaturas autorizadas.

Ao escrever a sua política da AWS, lembre-se de que os IDs das chaves são adicionados ao token como uma única string com carateres delimitadores. Tem de ordenar alfabeticamente a lista de IDs de chaves esperados e criar o valor da string. Por exemplo, se tiver os IDs das chaves aKey1, zKey2 e bKey3, o valor da reivindicação correspondente na sua política deve ser aKey1=bKey3=zKey2.

Para suportar vários conjuntos de chaves, pode adicionar opcionalmente vários valores à sua política.

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

A reivindicação de assinaturas de imagens de contentores (container.signatures.key_ids) e a reivindicação de resumo de imagens de contentores (container.image_digest) não aparecem juntas num único token. Se estiver a usar o container.signatures.key_ids, certifique-se de que remove todas as referências a container.image_digest das suas políticas da AWS.

Segue-se um exemplo de uma estrutura de reivindicaçã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 do token de atestação, consulte o artigo Reivindicações do token de atestação.

Configure recursos da AWS: relying party

Antes de a parte fidedigna poder configurar os respetivos recursos da AWS, tem de configurar o AWS IAM para estabelecer o espaço confidencial como um fornecedor OIDC federado e criar a função do AWS IAM necessária.

Configure o AWS IAM

  1. Para adicionar o serviço de atestação do Google Cloud como um fornecedor de identidade no AWS IAM, faça o seguinte:

    1. Na consola da AWS, aceda à página Fornecedores de identidade.

      Aceder à consola da AWS

    2. Para Tipo de fornecedor, selecione OpenID Connect.

    3. Para o URL do fornecedor, introduza https://confidentialcomputing.googleapis.com.

    4. Em Público-alvo, introduza o URL que registou junto do fornecedor de identidade e que faz pedidos à AWS. Por exemplo, https://example.com.

    5. Clique em Adicionar fornecedor.

  2. Para criar uma função de IAM do AWS para tokens do espaço confidencial, faça o seguinte:

    1. Na consola do AWS, aceda à página Funções.

    2. Clique em Criar função.

    3. Para o tipo de Entidade fidedigna, selecione Identidade na Web.

    4. Na secção Identidade da Web, selecione o fornecedor de identidade e o público-alvo com base no passo anterior.

    5. Clicar em Seguinte. Pode ignorar a edição da política da AWS neste passo.

    6. Clique em Seguinte e adicione etiquetas, se necessário.

    7. Em Nome da função, introduza o nome da função.

    8. (Opcional) Em Descrição, introduza uma descrição para a nova função.

    9. Reveja os detalhes e, de seguida, clique em Criar função.

  3. Edite a política da AWS da função que criou para conceder acesso apenas à carga de trabalho da sua escolha.

    Esta política da AWS permite-lhe verificar reivindicações específicas no token, como:

    Segue-se um exemplo de uma 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 apoio técnico:

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

Configure recursos da AWS

Depois de concluir a integração, configure os seus recursos da AWS. Este passo depende do seu exemplo de utilização específico. Por exemplo, pode criar um contentor do S3, uma chave do KMS ou outros recursos da AWS. Certifique-se de que concede à função de IAM do AWS que criou anteriormente as autorizações necessárias para aceder a estes recursos.

Configure a sua carga de trabalho do espaço confidencial: autor da carga de trabalho

Para criar pedidos de tokens, siga as instruções em peça um token de atestação com um público-alvo personalizado.

Para AWS_PrincipalTag reivindicações:

Segue-se um exemplo do corpo de um pedido de reivindicação AWS_PrincipalTag:

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

O que se segue?

Consulte Declarações de tokens de atestação para ver mais informações sobre as declarações de tokens de atestação.