Accedere alle risorse non gestite da IAM di Google Cloud


Se l'accesso alle risorse protette non è gestito da IAM di Google Cloud, ad esempio le risorse sono archiviate in un altro servizio cloud, on-premise o su un dispositivo locale come un cellulare, puoi comunque autenticare un workload Confidential Space sul dispositivo o sul sistema che fornisce l'accesso a queste risorse, altrimenti noto come relying party.

A questo scopo, la relying party deve richiedere un token di attestazione al servizio di attestazione Confidential Space con un pubblico personalizzato e nonce facoltativi. Quando richiedi un token di attestazione come questo, devi eseguire la tua convalida del token prima di concedere l'accesso alle risorse.

La documentazione che segue illustra i concetti relativi all'utilizzo di Confidential Space con risorse esterne a Google Cloud, incluse istruzioni per integrare facilmente i carichi di lavoro di Confidential Space con le risorse AWS. Per una procedura dettagliata end-to-end, consulta il codelab.

Flusso del token di attestazione

I token di attestazione vengono richiesti dal workload per conto di una relying party e restituiti dal servizio di attestazione. A seconda delle tue esigenze, puoi definire un segmento di pubblico personalizzato e, facoltativamente, fornire nonce.

Non criptato

Per facilitare la comprensione del processo di recupero dei token, il flusso presentato qui non utilizza la crittografia. In pratica, ti consigliamo di criptare le comunicazioni con TLS.

Il seguente diagramma mostra il flusso:

Un diagramma di flusso del flusso di generazione dei token di attestazione

  1. La relying party invia una richiesta di token al workload, con nonce facoltativi che ha generato.

  2. Il workload determina il segmento di pubblico, lo aggiunge alla richiesta e invia la richiesta al launcher di Confidential Space.

  3. Il launcher invia la richiesta al servizio di attestazione.

  4. Il servizio di attestazione genera un token che contiene il pubblico specificato e nonce facoltativi.

  5. Il servizio di attestazione restituisce il token al launcher.

  6. Il launcher restituisce il token al workload.

  7. Il workload restituisce il token alla parte autorizzata.

  8. La relying party verifica le rivendicazioni, inclusi il pubblico e i nonce facoltativi.

Criptato con TLS

Un flusso non criptato lascia la richiesta vulnerabile agli attacchi man-in-the-middle. Poiché un nonce non è associato all'output dei dati o a una sessione TLS, un utente malintenzionato può intercettare la richiesta e impersonare il workload.

Per contribuire a prevenire questo tipo di attacco, puoi configurare una sessione TLS tra la relying party e il workload e utilizzare il materiale della chiave esportata (EKM) TLS come nonce. Il materiale delle chiavi esportato TLS associa l'attestazione alla sessione TLS e conferma che la richiesta di attestazione è stata inviata tramite un canale protetto. Questa procedura è nota anche come associazione del canale.

Il seguente diagramma mostra il flusso che utilizza il binding del canale:

Un diagramma di flusso del flusso di generazione del token di binding del canale

  1. La relying party configura una sessione TLS sicura con la Confidential VM che esegue il workload.

  2. La relying party invia una richiesta di token utilizzando la sessione TLS sicura.

  3. Il workload determina il segmento di pubblico e genera un nonce utilizzando il materiale delle chiavi esportato TLS.

  4. Il workload invia la richiesta al launcher di Confidential Space.

  5. Il launcher invia la richiesta al servizio di attestazione.

  6. Il servizio di attestazione genera un token che contiene il nonce e il pubblico specificati.

  7. Il servizio di attestazione restituisce il token al launcher.

  8. Il launcher restituisce il token al workload.

  9. Il workload restituisce il token alla parte autorizzata.

  10. La relying party rigenera il nonce utilizzando il materiale della chiave esportata TLS.

  11. La relying party verifica le rivendicazioni, inclusi il pubblico e il nonce. Il nonce nel token deve corrispondere a quello rigenerato dalla relying party.

Struttura del token di attestazione

I token di attestazione sono token web JSON con la seguente struttura:

  • Intestazione: descrive l'algoritmo di firma. I token PKI archiviano anche la catena di certificati nell'intestazione nel campo x5c.

  • Payload di dati JSON firmati: contiene rivendicazioni sul carico di lavoro per la relying party, come soggetto, emittente, pubblico, nonce e ora di scadenza.

  • Firma: fornisce la convalida che il token non è cambiato durante il transito. Per ulteriori informazioni sull'utilizzo della firma, vedi Come convalidare un token ID OpenID Connect.

Il seguente esempio di codice è un esempio di token di attestazione codificato generato nell'immagine 240500 di Confidential Space. Le immagini più recenti potrebbero contenere campi aggiuntivi. Puoi utilizzare https://jwt.io/ per decodificarlo (la firma è oscurata).

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

Ecco la versione decodificata dell'esempio precedente:

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

Per una spiegazione più dettagliata dei campi del token di attestazione, vedi Attestazione delle rivendicazioni del token.

Recupera i token di attestazione

Completa i seguenti passaggi per implementare i token di attestazione nel tuo ambiente Confidential Space:

  1. Configura un client HTTP nel tuo workload.

  2. Nel tuo workload, utilizza il client HTTP per inviare una richiesta HTTP all'URL di ascolto, http://localhost/v1/token, tramite un socket di dominio Unix. Il file socket si trova in /run/container_launcher/teeserver.sock.

Quando viene effettuata una richiesta all'URL di ascolto, il launcher di Confidential Space gestisce la raccolta delle prove di attestazione, richiede un token di attestazione al servizio di attestazione (trasmettendo eventuali parametri personalizzati) e poi restituisce il token generato al workload.

Il seguente esempio di codice in Go mostra come comunicare con il server HTTP del launcher tramite 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
}

Richiedere un token di attestazione con un segmento di pubblico personalizzato

Metodo HTTP e URL:

POST http://localhost/v1/token

Corpo JSON della richiesta:

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

Fornisci i seguenti valori:

  • AUDIENCE_NAME: obbligatorio. Il valore del pubblico, ovvero il nome che hai dato alla relying party. Questa impostazione è definita dal carico di lavoro.

    Il valore predefinito di questo campo è https://sts.google.com per i token senza un pubblico personalizzato. Il valore https://sts.google.com non può essere utilizzato quando imposti un segmento di pubblico personalizzato. La lunghezza massima è di 512 byte.

    Per includere un segmento di pubblico personalizzato in un token, il workload, non la relying party, deve aggiungerlo alla richiesta di token di attestazione prima di inviare la richiesta al servizio di attestazione Confidential Space. In questo modo, la relying party non può richiedere un token per una risorsa protetta a cui non dovrebbe avere accesso.

  • TOKEN_TYPE: obbligatorio. Il tipo di token da restituire. Scegli uno dei seguenti tipi:

    • OIDC: questi token vengono convalidati in base a una chiave pubblica specificata nel campo jwks_uri nell'endpoint di convalida dei token OIDC. La chiave pubblica viene ruotata regolarmente.

    • PKI: questi token vengono convalidati in base a un certificato radice specificato nel campo root_ca_uri dell'endpoint di convalida dei token PKI. Devi archiviare questo certificato personalmente. Il certificato ruota ogni 10 anni.

    Poiché per la convalida dei token vengono utilizzati certificati con scadenza lunga anziché chiavi pubbliche con scadenza breve, i tuoi indirizzi IP non vengono esposti ai server di Google con la stessa frequenza. Ciò significa che i token PKI offrono una maggiore privacy rispetto ai token OIDC.

    Puoi convalidare l'impronta del certificato con OpenSSL:

    openssl x509 -fingerprint -in confidential_space_root.crt
    

    L'impronta deve corrispondere al seguente digest SHA-1:

    B9:51:20:74:2C:24:E3:AA:34:04:2E:1C:3B:A3:AA:D2:8B:21:23:21
    
  • NONCE: (Facoltativo). Un valore univoco, casuale e opaco, che garantisce che un token possa essere utilizzato una sola volta. Il valore è impostato dalla relying party. Sono consentiti fino a sei nonce. Ogni nonce deve essere compreso tra 10 e 74 byte inclusi.

    Quando includi un nonce, la relying party deve verificare che i nonce inviati nella richiesta del token di attestazione siano gli stessi dei nonce nel token restituito. In caso contrario, la relying party deve rifiutare il token.

Analizzare e convalidare i token di attestazione

I seguenti esempi di codice in Go mostrano come convalidare i token di attestazione.

Token di attestazione OIDC

package main

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  return file, nil
}

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

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

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

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

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

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

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

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

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

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

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

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

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

Token di attestazione PKI

Per convalidare il token, la relying party deve completare i seguenti passaggi:

  1. Analizza l'intestazione del token per ottenere la catena di certificati.

  2. Convalida la catena di certificati rispetto alla radice archiviata. Devi aver precedentemente scaricato il certificato radice dall'URL specificato nel campo root_ca_uri restituito nell'endpoint di convalida del token PKI.

  3. Controlla la validità del certificato foglia.

  4. Utilizza il certificato foglia per convalidare la firma del token utilizzando l'algoritmo specificato nella chiave alg nell'intestazione.

Una volta convalidato il token, la parte autorizzata può analizzare le rivendicazioni del 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
}

Integrare le risorse AWS

Puoi integrare i tuoi carichi di lavoro Confidential Space con le risorse AWS (come chiavi o dati) utilizzando i tag principal AWS. Questa integrazione utilizza l'attestazione sicura fornita da Confidential Space per concedere l'accesso granulare alle tue risorse AWS.

Rivendicazioni dei tag principal AWS

Google Cloud Attestation genera token di identità verificabili contenenti attestazioni sull'integrità e la configurazione del workload Confidential Space. Un sottoinsieme di queste rivendicazioni è compatibile con AWS, consentendoti di controllare l'accesso alle tue risorse AWS. Queste rivendicazioni vengono inserite nelle rivendicazioni https://aws.amazon.com/tags, nell'oggetto principal_tags nel token di attestazione. Per maggiori informazioni, consulta la sezione Rivendicazioni dei tag principali di AWS.

Di seguito è riportato un esempio di struttura di rivendicazione 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"
      ]
    }
  }
}

Policy AWS con rivendicazioni della firma dell'immagine container

I token AWS supportano anche le attestazioni della firma dell'immagine container. Queste rivendicazioni sono utili in caso di modifiche del carico di lavoro ad alta frequenza o quando si ha a che fare con più collaboratori o relying party.

Le attestazioni della firma dell'immagine container sono costituite da ID chiave, separati da un delimitatore. Per includere queste rivendicazioni nel token AWS, devi fornire un elenco consentito di questi ID chiave come parametro aggiuntivo nella richiesta di token.

Al token vengono aggiunti solo gli ID chiave che corrispondono alle chiavi utilizzate per firmare il workload. In questo modo, vengono accettate solo le firme autorizzate.

Quando scrivi la policy AWS, ricorda che gli ID chiave vengono aggiunti al token come una singola stringa con caratteri delimitatori. Devi ordinare alfabeticamente l'elenco degli ID chiave previsti e creare il valore della stringa. Ad esempio, se hai gli ID chiave aKey1, zKey2 e bKey3, il valore dell'attestazione corrispondente nelle norme deve essere aKey1=bKey3=zKey2.

Per supportare più set di chiavi, puoi facoltativamente aggiungere più valori alla norma.

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

L'attestazione delle firme dell'immagine container (container.signatures.key_ids) e l'attestazione del digest dell'immagine container (container.image_digest) non verranno visualizzate insieme in un unico token. Se utilizzi container.signatures.key_ids, assicurati di rimuovere tutti i riferimenti a container.image_digest dai tuoi criteri AWS.

Di seguito è riportato un esempio di struttura di rivendicazione https://aws.amazon.com/tags contenente 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"
      ]
    }
  }
}

Per una spiegazione più dettagliata dei campi del token di attestazione, vedi Attestazione delle rivendicazioni del token.

Configura le risorse AWS: relying party

Prima che la relying party possa configurare le proprie risorse AWS, deve configurare AWS IAM per stabilire Confidential Space come provider OIDC federato e creare il ruolo AWS IAM necessario.

Configura AWS IAM

  1. Per aggiungere il servizio Google Cloud Attestation come provider di identità in AWS IAM, svolgi le seguenti operazioni:

    1. Nella console AWS, vai alla pagina Provider di identità.

      Vai alla console AWS

    2. Per Tipo di provider, seleziona OpenID Connect.

    3. Per URL del fornitore, inserisci https://confidentialcomputing.googleapis.com.

    4. In Pubblico, inserisci l'URL che hai registrato con il provider di identità e che effettua richieste ad AWS. Ad esempio, https://example.com.

    5. Fai clic su Aggiungi provider.

  2. Per creare un ruolo AWS IAM per i token Confidential Space:

    1. Nella console AWS, vai alla pagina Ruoli.
    2. Fai clic su Crea ruolo.
    3. Per il tipo Entità attendibile, seleziona Identità web.
    4. Nella sezione Identità web, seleziona il provider di identità e il pubblico in base al passaggio precedente.
    5. Fai clic su Avanti. Puoi saltare la modifica della policy AWS in questo passaggio.
    6. Fai clic su Avanti e aggiungi i tag, se necessario.
    7. In Nome ruolo, inserisci il nome del ruolo.
    8. (Facoltativo) In Descrizione, inserisci una descrizione per il nuovo ruolo.
    9. Rivedi i dettagli e fai clic su Crea ruolo.
  3. Modifica la policy AWS del ruolo che hai creato per concedere l'accesso solo al workload di tua scelta.

    Questa norma AWS consente di controllare rivendicazioni specifiche nel token, ad esempio:

    Di seguito è riportato un esempio di policy AWS che concede l'accesso a un workload con un digest e un pubblico specificati, CONFIDENTIAL_SPACE come software in esecuzione sull'istanza VM e STABLE come attributo di supporto:

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

Configura le risorse AWS

Una volta completata l'integrazione, configura le risorse AWS. Questo passaggio dipende dal tuo caso d'uso specifico. Ad esempio, potresti creare un bucket S3, una chiave KMS o altre risorse AWS. Assicurati di concedere al ruolo AWS IAM creato in precedenza le autorizzazioni necessarie per accedere a queste risorse.

Configura il tuo workload Confidential Space: autore del workload

Per creare richieste di token, segui le istruzioni riportate in Richiedere un token di attestazione con un segmento di pubblico personalizzato.

Per le richieste AWS_PrincipalTag:

  • Un campo nonce è facoltativo nella richiesta di token per l'integrazione AWS.
  • Includi il pubblico che hai configurato in Configura risorse AWS: componente.
  • Imposta token_type su AWS_PRINCIPALTAGS.

Di seguito è riportato un esempio di corpo della richiesta di rivendicazione AWS_PrincipalTag:

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

Passaggi successivi