1. Overview
Confidential GKE (CGKE) nodes assure data in workloads is encrypted-in-use. Exposing the vTPM device to CGKE workloads allows workloads to use vTPM features. In this codelab, you are showcased with two features of vTPM.
- vTPM remote attestation allows a remote party to verify that the CGKE nodes hosting workloads are running on Confidential VMs (CVM).
- vTPM authorization and vTPM sealing.
As depicted in the figure above, the first part of this codelab includes following steps:
- CGKE nodes setup and expose the vTPM device to selected workloads.
- Deploy a workload and remote attest the CGKE node hosting the workload.
- Secret Release Web Server setup.
As depicted in the figure above, the second part of this codelab includes:
- vTPM authorization setup and vTPM sealing on the CGKE nodes.
What you'll learn
- How to expose the vTPM device to CGKE workloads.
- How to remote attest through Confidential Computing API (Attestation Verifier service) on CGKE workloads.
- How to set up vTPM authorization and perform vTPM sealing.
What you'll need
- A Google Cloud Platform Project
- A Browser, such Chrome or Firefox
- Basic knowledge of Google Compute Engine ( codelab), Confidential VM, Confidential GKE nodes and Artifact Registry
2. Setup and Requirements:
To enable the necessary APIs, run the following command in cloud console or your local development environment:
gcloud auth login gcloud services enable \ cloudapis.googleapis.com \ cloudshell.googleapis.com \ container.googleapis.com \ containerregistry.googleapis.com \ confidentialcomputing.googleapis.com \ iamcredentials.googleapis.com \ compute.googleapis.com
3. Setting up CGKE nodes and exposing the vTPM device to selected workloads
This step starts the first part of this codelab. In this step, you start a CGKE cluster and apply a device plugin to expose the CVM vTPM device to workloads. Go to the cloud console or your local development environment for running the commands.
1). Create a CGKE cluster, use workload identity pool to allow CGKE workloads to use GCP confidential computing API. Workload identity pool is necessary because the CGKE workloads need to access GCP resources. And to access GCP resources, CGKE workloads need to have an identity.
gcloud container clusters create cgke-attestation-codelab \ --machine-type=n2d-standard-2 \ --enable-confidential-nodes \ --zone us-central1-c \ --workload-pool=${PROJECT_ID}.svc.id.goog \ --workload-metadata=GKE_METADATA
Replace the following:
project-id
is the unique identifier of the project.
2). Start the device plugin to allow CGKE cluster to expose vTPM device to workloads. We use a kubernetes device plugin to create a new resource - google.com/cc
. Any workload associated with the new resource will be able to see the vTPM device on the worker node.
gcloud container clusters get-credentials cgke-attestation-codelab --zone us-central1-c --project ${PROJECT_ID} kubectl create -f https://raw.githubusercontent.com/google/cc-device-plugin/main/manifests/cc-device-plugin.yaml
Replace the following:
project-id
is the unique identifier of the project.
Following command allows you to see the deployed cc-device-plugin.
kubectl get pods -A | grep "cc-device-plugin"
Note: In case of a mixed mode GKE cluster (with both Confidential and non-Confidential GKE worker nodes), it's recommended that the operator only deploys cc-device-plugin on to the Confidential GKE worker nodes.
(Optional). Apply the CGKE pod Prometheus monitoring. Turning on the monitoring allows you to observe the device plugin status.
kubectl apply -f https://raw.githubusercontent.com/google/cc-device-plugin/main/manifests/cc-device-plugin-pod-monitoring.yaml
Go to https://console.cloud.google.com/monitoring/metrics-explorer and find the cc-device-plugin metrics or use PROMQL. e.g. The following PROMQL command shows cpu seconds for each cc-device-plugin process.
rate(process_cpu_seconds_total[${__interval}])
4. Deploying a workload and performing remote attestation on the workload
In this step, you create and deploy a workload to the CGKE cluster you created in the previous step and perform a vTPM remote attestation to retrieve an Attestation Token (OIDC Token) on the worker node.
1). Create the application container image and push it to the Artifact Registry. The application container image contains the go-tpm tool, which can collect attestation evidence and send it to Attestation Verifier service for an Attestation Token (an OIDC Token).
- Create the Dockerfile for the application container image.
Dockerfile
FROM golang:1.21.0 as builder
WORKDIR /
RUN git clone https://github.com/google/go-tpm-tools.git
WORKDIR /go-tpm-tools/cmd/gotpm
RUN CGO_ENABLED=0 GOOS=linux go build -o /gotpm
FROM debian:trixie
WORKDIR /
RUN apt-get update -y
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y ca-certificates
RUN rm -rf /etc/apt/sources.list.d
COPY --from=builder /gotpm /gotpm
CMD ["tail", "-f", "/dev/null"]
- Create an Artifact Registry.
gcloud artifacts repositories create codelab-repo \ --repository-format=docker \ --location=us
- Push the application container image to the Artifact Registry.
docker build -t us-docker.pkg.dev/${PROJECT_ID}/codelab-repo/go-tpm:latest . docker push us-docker.pkg.dev/${PROJECT_ID}/codelab-repo/go-tpm:latest
2). Set up a Kubernetes service account to inherit a GCP service account's permissions on GCP resources.
- Create a Kubernetes service account
codelab-ksa
.
kubectl create serviceaccount codelab-ksa \ --namespace default
- Create a role
Confidential_Computing_Workload_User
and grants the role permissions to access Confidential Computing APIs.
gcloud iam roles create Confidential_Computing_Workload_User --project=<project-id> \ --title="CGKE Workload User" --description="Grants the ability to generate an attestation token in a GKE workload." \ --permissions="confidentialcomputing.challenges.create,confidentialcomputing.challenges.verify,confidentialcomputing.locations.get,confidentialcomputing.locations.list" --stage=GA
Replace the following:
project-id
is the unique identifier of the project.
- Create a GCP service account
codelab-csa
and bind it with the roleConfidential_Computing_Workload_User. So that codelab-csa
has permissions to access Confidential Computing APIs.
gcloud iam service-accounts create codelab-csa \ --project=<project-id> gcloud projects add-iam-policy-binding <project-id> \ --member "serviceAccount:codelab-csa@<project-id>.iam.gserviceaccount.com" \ --role "projects/<project-id>/roles/Confidential_Computing_Workload_User" gcloud iam service-accounts add-iam-policy-binding codelab-csa@<project-id>.iam.gserviceaccount.com \ --role roles/iam.workloadIdentityUser \ --member "serviceAccount:<project-id>.svc.id.goog[default/codelab-ksa]"
Replace the following:
project-id
is the unique identifier of the project.
- Bind the Kubernetes service account
codelab-ksa
with GCP service accountcodelab-csa
. So thatcodelab-ksa
has permissions to access Confidential Computing APIs.
kubectl annotate serviceaccount codelab-ksa \ --namespace default \ iam.gke.io/gcp-service-account=codelab-csa@<project-id>.iam.gserviceaccount.com
Replace the following:
project-id
is the unique identifier of the project.
3). Create the application deployment yaml for the demo application. Assign the Kubernetes service account codelab-ksa
to selected workloads.
deploy.yaml
apiVersion: v1
kind: Pod
metadata:
name: go-tpm-demo
labels:
app.kubernetes.io/name: go-tpm-demo
spec:
serviceAccountName: codelab-ksa
nodeSelector:
iam.gke.io/gke-metadata-server-enabled: "true"
containers:
- name: go-tpm
image: us-docker.pkg.dev/<project-id>/codelab-repo/go-tpm:latest
resources:
limits:
google.com/cc: 1
Replace the following:
project-id
is the unique identifier of the project.
4). Apply the deployment to the CGKE cluster.
kubectl create -f deploy.yaml
5). Connect to the workload and launch remote attestation to fetch an Attestation Token (an OIDC Token)
kubectl exec -it go-tpm-demo -- /bin/bash ./gotpm token --event-log=/run/cc-device-plugin/binary_bios_measurements > attestation_token
You may decode the attestation token in jwt.io to view the claims!
5. Setting up Secret Release Web Server
In this step, you exit the previous SSH session and set up another VM. On this VM, you set up a secret release web server. The web server validates the received Attestation Token and its claims. If the validations succeed, then it passes secret to the requester.
1). Go to the cloud console or your local development environment. Create a virtual machine.
gcloud config set project <project-id> gcloud compute instances create cgke-attestation-codelab-web-server \ --machine-type=n2d-standard-2 \ --zone=us-central1-c \ --image=ubuntu-2204-jammy-v20240228 \ --image-project=ubuntu-os-cloud
Replace the following:
project-id
is the unique identifier of the project.
2). SSH to your new VM.
gcloud compute ssh --zone us-central1-c cgke-attestation-codelab-web-server
3). Set up the Go environment.
wget https://go.dev/dl/go1.22.0.linux-amd64.tar.gz sudo tar -C /usr/local -xzf go1.22.0.linux-amd64.tar.gz export PATH=$PATH:/usr/local/go/bin
4). Create the following two files storing the source code of the secret release web server (copy paste with nano).
main.go
package main
import (
"fmt"
"net/http"
"strings"
"time"
"log"
"github.com/golang-jwt/jwt/v4"
)
const (
theSecret = "This is the super secret information!"
)
func homePage(w http.ResponseWriter, r *http.Request) {
tokenString := r.Header.Get("Authorization")
if tokenString != "" {
tokenString, err := extractToken(tokenString)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
}
tokenBytes := []byte(tokenString)
// A method to return a public key from the well-known endpoint
keyFunc := getRSAPublicKeyFromJWKsFile
token, err := decodeAndValidateToken(tokenBytes, keyFunc)
if err != nil {
http.Error(w, "Invalid JWT Token", http.StatusUnauthorized)
}
if ok, err := isValid(token.Claims.(jwt.MapClaims)); ok {
fmt.Fprintln(w, theSecret)
} else {
if err != nil {
http.Error(w, "Error validating JWT claims: "+err.Error(), http.StatusUnauthorized)
} else {
http.Error(w, "Invalid JWT token Claims", http.StatusUnauthorized)
}
}
} else {
http.Error(w, "Authorization token required", http.StatusUnauthorized)
}
}
func extractToken(tokenString string) (string, error) {
if strings.HasPrefix(tokenString, "Bearer ") {
return strings.TrimPrefix(tokenString, "Bearer "), nil
}
return "", fmt.Errorf("invalid token format")
}
func isValid(claims jwt.MapClaims) (bool, error) {
// 1. Evaluating Standard Claims:
subject, ok := claims["sub"].(string)
if !ok {
return false, fmt.Errorf("missing or invalid 'sub' claim")
}
fmt.Println("Subject:", subject)
// e.g. "sub":"https://www.googleapis.com/compute/v1/projects/<project_id>/zones/<project_zone>/instances/<instance_name>"
issuedAt, ok := claims["iat"].(float64)
if !ok {
return false, fmt.Errorf("missing or invalid 'iat' claim")
}
fmt.Println("Issued At:", time.Unix(int64(issuedAt), 0))
// 2. Evaluating Remote Attestation Claims:
hwModel, ok := claims["hwmodel"].(string)
if !ok || hwModel != "GCP_AMD_SEV" {
return false, fmt.Errorf("missing or invalid 'hwModel'")
}
fmt.Println("hwmodel:", hwModel)
swName, ok := claims["swname"].(string)
if !ok || swName != "GCE" {
return false, fmt.Errorf("missing or invalid 'hwModel'")
}
fmt.Println("swname:", swName)
return true, nil
}
func main() {
http.HandleFunc("/", homePage)
fmt.Println("Server listening on :8080")
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatalf("Server failed to start: %v", err)
}
}
helper.go
package main
import (
"crypto/rsa"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"math/big"
"net/http"
"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\n", 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)
}
5). Run the following commands to build the web server and run it. This starts the secret release webserver at :8080
port.
go mod init google.com/codelab go mod tidy go get github.com/golang-jwt/jwt/v4 go build ./codelab
Troubleshooting: you might see the following warning which can be ignored when run go mod tidy:
go: finding module for package github.com/golang-jwt/jwt/v4 go: downloading github.com/golang-jwt/jwt v3.2.2+incompatible go: downloading github.com/golang-jwt/jwt/v4 v4.5.0 go: found github.com/golang-jwt/jwt/v4 in github.com/golang-jwt/jwt/v4 v4.5.0 go: google.com/codelab/go/pkg/mod/github.com/golang-jwt/jwt@v3.2.2+incompatible: import path "google.com/codelab/go/pkg/mod/github.com/golang-jwt/jwt@v3.2.2+incompatible" should not have @version go: google.com/codelab/go/pkg/mod/github.com/golang-jwt/jwt@v3.2.2+incompatible/cmd/jwt: import path "google.com/codelab/go/pkg/mod/github.com/golang-jwt/jwt@v3.2.2+incompatible/cmd/jwt" should not have @version go: google.com/codelab/go/pkg/mod/github.com/golang-jwt/jwt@v3.2.2+incompatible/request: import path "google.com/codelab/go/pkg/mod/github.com/golang-jwt/jwt@v3.2.2+incompatible/request" should not have @version go: google.com/codelab/go/pkg/mod/github.com/golang-jwt/jwt@v3.2.2+incompatible/test: import path "google.com/codelab/go/pkg/mod/github.com/golang-jwt/jwt@v3.2.2+incompatible/test" should not have @version
6). Start another cloud console tab or local development environment session and run the following command. This will get you the cgke-attestation-codelab-web-server-internal-ip
.
gcloud compute instances describe cgke-attestation-codelab-web-server --format='get(networkInterfaces[0].networkIP)' --zone=us-central1-c
7). Connect to your CGKE workload and launch remote attestation to fetch an Attestation Token (an OIDC Token). Then embed the content of the attestation-token
and cgke-attestation-codelab-web-server-internal-ip
in the following command. This will fetch you the secret held by the secret release web server!
kubectl exec -it go-tpm-demo -- /bin/bash ./gotpm token --event-log=/run/cc-device-plugin/binary_bios_measurements > attestation_token curl http://<cgke-attestation-codelab-web-server-internal-ip>:8080 -H "Authorization: Bearer $(cat ./attestation_token)"
Replace the following:
cgke-attestation-codelab-web-server-internal-ip
is the internal ip of thecgke-attestation-codelab-web-server
VM instance.
6. vTPM Sealing on the CGKE nodes
This step starts the second part of this codelab. In this step, you set up vTPM owner authorization on the CGKE nodes and deploy a workload with the vTPM owner passphrase. Afterwards, you create a vTPM primary key to seal and unseal data in the workload with the vTPM sealing capability.
1). Set up vTPM owner authorization on the CGKE nodes.
- Create a one time job container image. The one time job sets the owner password for all vTPMs. The following is the dockerfile to create the container image.
Dockerfile
FROM debian:latest
RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
RUN apt-get update
RUN apt -y install \
autoconf-archive \
libcmocka0 \
libcmocka-dev \
net-tools \
build-essential \
git \
pkg-config \
gcc \
g++ \
m4 \
libtool \
automake \
libgcrypt20-dev \
libssl-dev \
uthash-dev \
autoconf \
uuid-dev \
libcurl4-openssl-dev \
libjson-c-dev
RUN mkdir /src
WORKDIR /src
RUN git clone https://github.com/tpm2-software/tpm2-tss
WORKDIR /src/tpm2-tss
RUN ./bootstrap
RUN ./configure --prefix=/usr/local
RUN make all install
WORKDIR /src
RUN git clone https://github.com/tpm2-software/tpm2-tools
WORKDIR /src/tpm2-tools
RUN apt-get -y install libcurl4 libcurl4-openssl-dev pandoc man-db
RUN ./bootstrap
RUN ./configure --prefix=/usr/local
RUN make all install
RUN apt-get -y install vim
ENTRYPOINT ["/bin/bash"]
- Build and push the one time job container image to the artifact registry.
docker build -t us-docker.pkg.dev/<project-id>/codelab-repo/tpm-tools:latest . docker push us-docker.pkg.dev/<project-id>/codelab-repo/tpm-tools:latest
Replace the following:
project-id
is the unique identifier of the project.
- Execute the one-time job through a Kubernetes job. (WARNING: this job clears vTPM on each CVM, if your CVM uses vTPM to encrypt disk, this job would make your CVM unusable after reboot. You could check if your disk has FSTYPE
crypto_LUKS
withlsblk -f
command)
tpm-tools-task.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: tpm-tools-task
spec:
template:
spec:
containers:
- name: tpm-tools
image: us-docker.pkg.dev/<project-id>/codelab-repo/tpm-tools:latest
command: ["/bin/sh", "-c"]
args: ["tpm2_clear; tpm2_changeauth -c owner this_is_passphrase"]
resources:
limits:
google.com/cc: 1
restartPolicy: Never
Replace the following:
project-id
is the unique identifier of the project.
- Launch the one time job. This job sets the vTPM owner passphrase on all the worker nodes.
kubectl create -f tpm-tools-task.yaml
2). Create a Kubernetes secret to hold the vTPM owner passphrase.
kubectl create secret generic tpm-secret --from-literal=passphrase='this_is_passphrase'
3). Create a demo application container and pass the passphrase to it. The demo application container contains tpm2 tools to interact with the vTPM.
- Create the deployment yaml file for the demo application container.
deploy_demo.yaml
apiVersion: v1
kind: Pod
metadata:
name: tpm-tools-demo
labels:
app.kubernetes.io/name: tpm-tools-demo
spec:
containers:
- name: tpm-tools
image: us-docker.pkg.dev/<project-id>/codelab-repo/tpm-tools:latest
command: ["tail", "-f", "/dev/null"]
resources:
limits:
google.com/cc: 1
volumeMounts:
- name: secret-volume
mountPath: "/etc/tpmsecret"
readOnly: true
volumes:
- name: secret-volume
secret:
secretName: tpm-secret
Replace the following:
project-id
is the unique identifier of the project.
- Deploy the demo application.
kubectl create -f deploy_demo.yaml
4). Perform vTPM sealing in the demo application container.
- Connect to the demo application container and set a primary key with passphrase.
kubectl exec -it tpm-tools-demo -- /bin/bash tpm2_createprimary -C o -c primary.ctx -P $(cat /etc/tpmsecret/passphrase)
tpm2_createprimary
interacts with the vTPM to generate the primary object based on the specified hierarchy and template.
- -C o: Indicates the primary key will be created under the TPM's owner hierarchy.
- -c primary.ctx: Saves the context (handle and associated data) of the created primary object to the file primary.ctx. This context is essential for later operations.
The workload cannot use the wrong owner passphrase to create a primary key.
tpm2_createprimary -C o -P wrong_passphrase
The command returns the following errors:
WARNING:esys:src/tss2-esys/api/Esys_CreatePrimary.c:401:Esys_CreatePrimary_Finish() Received TPM Error ERROR:esys:src/tss2-esys/api/Esys_CreatePrimary.c:135:Esys_CreatePrimary() Esys Finish ErrorCode (0x000009a2) ERROR: Esys_CreatePrimary(0x9A2) - tpm:session(1):authorization failure without DA implications ERROR: Unable to run tpm2_createprimary
- The created primary key could then be used to seal and unseal data.
echo "This is my secret message" > secret.txt tpm2_create -C primary.ctx -u sealed.pub -r sealed.priv -i secret.txt tpm2_load -C primary.ctx -u sealed.pub -r sealed.priv -c sealed.ctx tpm2_unseal -c sealed.ctx -o unsealed.txt
tpm2_create
interacts with the vTPM to generate the desired cryptographic object.
- -C primary.ctx: Uses the primary key context we created earlier.
- -u sealed.pub: Stores the public portion of the sealing key (needed for unsealing) in sealed.pub.
- -r sealed.priv: Stores the private portion of the sealing key in sealed.priv.
- -i secret.txt: The file containing the secret to be sealed.
tpm2_load
: Loads the sealing key into the TPM using the public and private portions (sealed.pub, sealed.priv) and saves its context to sealed.ctx.
tpm2_unseal
: decrypt (unseal) data that was previously encrypted (sealed) using a vTPM sealing object.
Note that: The primary.ctx
, sealed.priv
files are only usable on one vTPM device. And anyone with access to the vTPM device and these files can access the sealed data. You could further use policy on PCR values to seal data, but it is out of scope for this codelab.
7. Cleanup
Run the following commands in cloud console or your local development environment:
gcloud config set project <project-id> # Delete the CGKE cluster gcloud container clusters delete cgke-attestation-codelab --zone us-central1-c # Delete the Artifact Registry gcloud artifacts repositories delete codelab-repo --location=us # Delete the web server VM instance gcloud compute instances delete cgke-attestation-codelab-web-server --zone=us-central1-c # Delete the GCP service account gcloud iam service-accounts delete codelab-csa@<project-id>.iam.gserviceaccount.com # Delete the role gcloud iam roles delete Confidential_Computing_Workload_User
Replace the following:
project-id
is the unique identifier of the project.
8. What's next
Learn more about Confidential GKE Nodes.