Use resources outside of Google Cloud


If your protected resources are hosted somewhere other than Google Cloud—whether on another cloud service, on-premises, or on a local device such as a mobile phone—you can still authenticate with a Confidential Space workload to exchange sensitive data securely. The system making a request to Confidential Space from outside of Google Cloud is known as an external relying party.

External relying parties can make use of custom attestation tokens to authenticate with a Confidential Space workload. Custom attestation tokens are JSON web tokens with the following structure:

  • Header: Describes the signing algorithm and token type.

  • Signed JSON data payload: Contains claims about the relying party, such as subject, issuer, audience, nonce, and expiration time.

  • Signature: Provides validation that the token wasn't changed during transit. For more information about using the signature, see How to Validate an OpenID Connect ID Token.

Custom and default tokens

A custom attestation token is different from a default attestation token:

  • A custom token lets a Confidential Space workload operate on resources stored outside of Google Cloud.

  • A default token is what's used to access resources inside Google Cloud through workload identity federation. Unlike the custom token, it doesn't support custom audience or nonces fields.

When working with relying parties, depending on your workflow you might need both tokens. To learn how to retrieve tokens, see Implement custom attestation tokens.

To view all the token claims, see Token claims.

Custom token payload

To authenticate the workload to a relying party outside of Google Cloud, two parameters are used with the custom token:

  • An audience: Required. Identifies the relying party the token is intended for, and limits the usage to that party.

  • Noncese: Optional, but recommended. A nonce is a unique, random, and opaque value. It makes sure a custom token can only be used once.

The relying party must verify that a nonce sent in an attestation token request is the same as a nonce in the returned token. If they are different, the relying party must reject the token.

To include a custom audience in a token, the workload—not the relying party—must add it to the attestation token request before sending the request to the Confidential Space attestation service. This helps to prevent the relying party from requesting a token for a protected resource that it shouldn't have access to.

Custom token generation process

This section describes how you can generate a custom attestation token with a custom audience and nonce.

Unencrypted

This flow describes how you can pass the nonce and audience from the relying party to the attestation service to receive a token. It's presented here without encryption for ease of understanding the process. In practice, we recommend you encrypt communications with TLS.

The following diagram shows the flow:

A flow diagram of the custom token generation flow

  1. The relying party sends a token request with a nonce that it has generated to the workload.

  2. The workload determines the audience, adds the audience to the request, and sends the request to the Confidential Space launcher.

  3. The launcher sends the request to the attestation service.

  4. The attestation service generates a token that contains the specified audience and nonce.

  5. The attestation service returns the token to the launcher.

  6. The launcher returns the token to the workload.

  7. The workload returns the token to the relying party.

  8. The relying party verifies the claims, including the audience and nonce.

With TLS

The flow without TLS leaves the request vulnerable to machine in the middle attacks. Because the nonce isn't bound to the data output or a TLS session, an attacker can intercept the request and impersonate the workload.

To help prevent this type of attack, you can set up a TLS session between the relying party and workload and use the TLS exported key material (EKM) as the nonce. The TLS exported key material binds the attestation to the TLS session and confirms that the attestation request was sent through a secured channel. This process is also known as channel binding.

The following diagram shows the flow using channel binding:

A flow diagram of the channel binding token generation flow

  1. The relying party sets up a secure TLS session with the Confidential VM that is running the workload.

  2. The relying party sends a token request using the secure TLS session.

  3. The workload determines the audience and generates a nonce using the TLS exported key material.

  4. The workload sends the request to the Confidential Space launcher.

  5. The launcher sends the request to the attestation service.

  6. The attestation service generates a token that contains the specified audience and nonce.

  7. The attestation service returns the token to the launcher.

  8. The launcher returns the token to the workload.

  9. The workload returns the token to the relying party.

  10. The relying party re-generates the nonce using the TLS exported key material.

  11. The relying party verifies the claims, including the audience and nonce. The nonce in the token must match the nonce that is regenerated by the relying party.

Implement attestation tokens

This section describes how you can implement custom attestation tokens in Confidential Space.

Set up an HTTP client to make requests

Set up an HTTP client in the workload. The workload requests tokens from the Confidential Space launcher using an HTTP request over a Unix domain socket, with a socket file at /run/container_launcher/teeserver.sock. The launcher listens to requests at the http://localhost/v1/token URL.

Requests are limited to 5 queries per second per project per region. If you need to increase the Confidential Space attestation quota, contact your Google Cloud account manager.

Retrieve tokens

To retrieve the default attestation token, make a GET request to the listening URL.

To retrieve the custom attestation token, you need to make a POST request with a JSON body. The following table describes the supported custom attestation token fields.

Name Type Value
audience String Required. Your audience value, which is the name that you've given your relying party. You can't set this to sts.googleapis.com, as it's used for the default token. The maximum length is 512 bytes.
token_type String Required. The type of token to receive. Only standard OIDC tokens are available. The value must be OIDC.
nonces String array Optional. The list of nonces to place into the token. A maximum of six nonces is allowed. Each nonce must be between 10 and 74 bytes, inclusive.

Here's an example request:

"audience": "string",
"token_type": "OIDC",
"nonces": [
    "thisIsACustomNonce",
    "thisIsAMuchLongerCustomNonceWithPaddingFor74Bytes0000000000000000000000000"
]

When a request is made to the listening URL, the Confidential Space launcher manages the attestation evidence collection, requests an attestation token from the attestation service (passing along any custom parameters), and then returns the generated token to the workload.

The following code sample in Go demonstrates how to communicate with the launcher's HTTP server over 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 custom token response: %w", err)
  }
  tokenbytes, err := io.ReadAll(resp.Body)
  if err != nil {
    return nil, fmt.Errorf("failed to read custom token body: %w", err)
  }
  fmt.Println(string(tokenbytes))
  return tokenbytes, nil
}

Parse custom tokens

The following code sample in Go shows how to parse a custom attestation token:

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

What's next