Skip to content

Instantly share code, notes, and snippets.

@salrashid123
Last active March 5, 2025 15:18
Show Gist options
  • Save salrashid123/8ff84299fd5be1c85a8d00c5a89dd716 to your computer and use it in GitHub Desktop.
Save salrashid123/8ff84299fd5be1c85a8d00c5a89dd716 to your computer and use it in GitHub Desktop.
GCE Attestation Key based authentication
package main
/*
Authenticate to GCP using the GCP embedded vTPM AttestationKey
this specific implementation acquires a JWTAccessToken with scopes
https://github.com/salrashid123/gcp-vtpm-ek-ak/tree/main?tab=readme-ov-file#sign-jwt-with-tpm
1. first create a gce instance with confidentialcompute and vtpm enabled
$ gcloud compute instances describe attestor
confidentialInstanceConfig:
confidentialInstanceType: SEV
minCpuPlatform: AMD Milan
2. acqurie the attestation x509
$ gcloud compute instances get-shielded-identity attestor --format=json --zone=us-central1-a | jq -r '.signingKey.ekCert' > akcert.pem
$ openssl x509 -inform pem -text -in akcert.pem
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
90:f2:9a:7d:b5:0b:b0:bb:7d:04:cc:58:69:de:9b:dc:be:6f:78
Signature Algorithm: sha256WithRSAEncryption
Issuer: C=US, ST=California, L=Mountain View, O=Google LLC, OU=Google Cloud, CN=EK/AK CA Intermediate
Validity
Not Before: Jan 19 02:54:26 2025 GMT
Not After : Jan 12 02:54:25 2055 GMT
Subject: L=us-central1-a, O=Google Compute Engine, OU=core-eso, CN=2003763118985041850
3. upload the x509
$ gcloud iam service-accounts keys upload akcert.pem [email protected]
keyAlgorithm: KEY_ALG_RSA_2048
keyOrigin: USER_PROVIDED
keyType: USER_MANAGED
name: projects/core-eso/serviceAccounts/[email protected]/keys/2c579e335fe9ea470ab1c57f1f20fcaecb9f8e09
validAfterTime: '2025-01-19T02:54:26Z'
validBeforeTime: '2055-01-12T02:54:25Z'
note the x509 is visible externally...
https://www.googleapis.com/service_accounts/v1/metadata/x509/[email protected]
4. On the VM, run this app
go run main.go
you should see the JWT Token, export that and access a gcp resource
export TOKEN="eyJhbGciOiJSUzI1NiIsImtpZCI6IjAwMGJjOTIyNDIyNWJkNjFmNzg2M2NmNjQyMmEzMGI2MWVlODAzNzZkOTkwOTA4MzU1MWNlNjIwMzkwOWY3MjA3Y2ZlIiwidHlwIjoiSldUIn0.eyJzY29wZSI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaX..."
curl -H "Authorization: Bearer $TOKEN" https://storage.googleapis.com/storage/v1/b/core-eso-bucket/o/foo.txt
{
"kind": "storage#object",
"id": "core-eso-bucket/foo.txt/1730576248703829",
also see https://github.com/salrashid123/oauth2
*/
// [go-tpm-tools/client](https://pkg.go.dev/github.com/google/go-tpm-tools/client#pkg-constants)
// // AK (signing)
// GceAKCertNVIndexRSA uint32 = 0x01c10000
// // EK (encryption)
// EKCertNVIndexRSA uint32 = 0x01c00002
// github.com/google/[email protected]/client/handles.go
// [go-tpm-tools/client](https://pkg.go.dev/github.com/google/go-tpm-tools/client#pkg-constants)
// GCE Attestation Key NV Indices
import (
"context"
"crypto/x509"
"encoding/hex"
"encoding/pem"
"flag"
"fmt"
"io"
"log"
"net"
"slices"
"strings"
"time"
"github.com/google/go-tpm-tools/simulator"
"github.com/google/go-tpm/tpm2"
"github.com/google/go-tpm/tpm2/transport"
"github.com/google/go-tpm/tpmutil"
jwt "github.com/golang-jwt/jwt/v5"
tpmjwt "github.com/salrashid123/golang-jwt-tpm"
)
type oauthJWT struct {
Scope string `json:"scope"`
jwt.RegisteredClaims
}
const (
// RSA 2048 AK.
GceAKCertNVIndexRSA uint32 = 0x01c10000
GceAKTemplateNVIndexRSA uint32 = 0x01c10001
// ECC P256 AK.
GceAKCertNVIndexECC uint32 = 0x01c10002
GceAKTemplateNVIndexECC uint32 = 0x01c10003
// RSA 2048 EK Cert.
EKCertNVIndexRSA uint32 = 0x01c00002
// ECC P256 EK Cert.
EKCertNVIndexECC uint32 = 0x01c0000a
)
var (
tpmPath = flag.String("tpm-path", "/dev/tpmrm0", "Path to the TPM device (character device or a Unix socket).")
svcAccountEmail = flag.String("svcAccountEmail", "", "Service Account Email")
expireIn = flag.Int("expireIn", 3600, "Token expires in seconds")
scopes = flag.String("scopes", "https://www.googleapis.com/auth/cloud-platform", "comma separated scopes")
)
var TPMDEVICES = []string{"/dev/tpm0", "/dev/tpmrm0"}
func OpenTPM(path string) (io.ReadWriteCloser, error) {
if slices.Contains(TPMDEVICES, path) {
return tpmutil.OpenTPM(path)
} else if path == "simulator" {
return simulator.Get()
} else {
return net.Dial("tcp", path)
}
}
func main() {
flag.Parse()
log.Println("======= Init ========")
rwc, err := OpenTPM(*tpmPath)
if err != nil {
log.Fatalf("can't open TPM %q: %v", *tpmPath, err)
}
defer func() {
rwc.Close()
}()
rwr := transport.FromReadWriter(rwc)
log.Printf("======= createPrimary RSAEKTemplate ========")
// read from template
// cCreateGCEEK, err := tpm2.CreatePrimary{
// PrimaryHandle: tpm2.TPMRHEndorsement,
// InPublic: tpm2.New2B(tpm2.RSAEKTemplate),
// }.Execute(rwr)
// if err != nil {
// log.Fatalf("can't create object TPM %q: %v", *tpmPath, err)
// }
akTemplatebytes, err := nvReadEX(rwr, tpmutil.Handle(GceAKTemplateNVIndexRSA))
if err != nil {
log.Fatalf("ERROR: could not read nv index for GceAKTemplateNVIndexRSA: %v", err)
}
tb := tpm2.BytesAs2B[tpm2.TPMTPublic, *tpm2.TPMTPublic](akTemplatebytes)
cCreateGCEAK, err := tpm2.CreatePrimary{
PrimaryHandle: tpm2.TPMRHEndorsement,
InPublic: tb,
}.Execute(rwr)
if err != nil {
log.Fatalf("can't create object TPM %q: %v", *tpmPath, err)
}
defer func() {
flushContextCmd := tpm2.FlushContext{
FlushHandle: cCreateGCEAK.ObjectHandle,
}
_, err := flushContextCmd.Execute(rwr)
if err != nil {
log.Fatalf("can't close TPM %q: %v", *tpmPath, err)
}
}()
log.Printf("Name %s\n", hex.EncodeToString(cCreateGCEAK.Name.Buffer))
pub, err := cCreateGCEAK.OutPublic.Contents()
if err != nil {
log.Fatalf("Failed to get rsa public: %v", err)
}
rsaDetail, err := pub.Parameters.RSADetail()
if err != nil {
log.Fatalf("Failed to get rsa details: %v", err)
}
rsaUnique, err := pub.Unique.RSA()
if err != nil {
log.Fatalf("Failed to get rsa unique: %v", err)
}
rsaGCEAKPub, err := tpm2.RSAPub(rsaDetail, rsaUnique)
if err != nil {
log.Fatalf("can't read rsapub unique %q: %v", *tpmPath, err)
}
b2, err := x509.MarshalPKIXPublicKey(rsaGCEAKPub)
if err != nil {
log.Fatalf("Unable to convert rsaGCEAKPub: %v", err)
}
akGCEPubPEM := pem.EncodeToMemory(
&pem.Block{
Type: "PUBLIC KEY",
Bytes: b2,
},
)
log.Printf("GCE AKPublic: \n%v", string(akGCEPubPEM))
// GET certificate
log.Printf(" Load SigningKey and Cert ")
// read direct from nv template
readPubRsp, err := tpm2.NVReadPublic{
NVIndex: tpm2.TPMHandle(GceAKCertNVIndexRSA),
}.Execute(rwr)
if err != nil {
log.Fatalf("Calling TPM2_NV_ReadPublic: %v", err)
}
log.Printf("Name: %x", readPubRsp.NVName.Buffer)
c, err := readPubRsp.NVPublic.Contents()
if err != nil {
log.Fatalf("Calling TPM2_NV_ReadPublic Contents: %v", err)
}
// get nv max buffer
// tpm2_getcap properties-fixed | grep -A 1 TPM2_PT_NV_BUFFER_MAX
// TPM2_PT_NV_BUFFER_MAX:
// raw: 0x800 <<<<< 2048
getCmd := tpm2.GetCapability{
Capability: tpm2.TPMCapTPMProperties,
Property: uint32(tpm2.TPMPTNVBufferMax),
PropertyCount: 1,
}
getRsp, err := getCmd.Execute(rwr)
if err != nil {
log.Fatalf("errpr Calling GetCapability: %v", err)
}
tp, err := getRsp.CapabilityData.Data.TPMProperties()
if err != nil {
log.Fatalf("error Calling TPMProperties: %v", err)
}
blockSize := int(tp.TPMProperty[0].Value)
outBuff := make([]byte, 0, int(c.DataSize))
for len(outBuff) < int(c.DataSize) {
readSize := blockSize
if readSize > (int(c.DataSize) - len(outBuff)) {
readSize = int(c.DataSize) - len(outBuff)
}
readRsp, err := tpm2.NVRead{
AuthHandle: tpm2.AuthHandle{
Handle: tpm2.TPMRHOwner,
Name: tpm2.HandleName(tpm2.TPMRHOwner),
Auth: tpm2.HMAC(tpm2.TPMAlgSHA256, 16, tpm2.Auth([]byte{})),
},
NVIndex: tpm2.NamedHandle{
Handle: tpm2.TPMHandle(GceAKCertNVIndexRSA),
Name: readPubRsp.NVName,
},
Size: uint16(readSize),
Offset: uint16(len(outBuff)),
}.Execute(rwr)
if err != nil {
log.Fatalf("Calling NV Read: %v", err)
}
data := readRsp.Data.Buffer
outBuff = append(outBuff, data...)
}
signCert, err := x509.ParseCertificate(outBuff)
if err != nil {
log.Printf("ERROR: error parsing AK singing cert : %v", err)
return
}
akCertPEM := pem.EncodeToMemory(
&pem.Block{
Type: "CERTIFICATE",
Bytes: signCert.Raw,
},
)
log.Printf(" Signing Certificate \n%s", string(akCertPEM))
// ******************
ctx := context.Background()
iat := time.Now()
exp := iat.Add(time.Duration(*expireIn) * time.Second)
claims := &oauthJWT{
Scope: strings.Replace(*scopes, ",", " ", -1),
RegisteredClaims: jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(iat),
ExpiresAt: jwt.NewNumericDate(exp),
Issuer: *svcAccountEmail,
Subject: *svcAccountEmail,
},
}
tpmjwt.SigningMethodTPMRS256.Override()
token := jwt.NewWithClaims(tpmjwt.SigningMethodTPMRS256, claims)
config := &tpmjwt.TPMConfig{
TPMDevice: rwc,
NamedHandle: tpm2.NamedHandle{
Handle: cCreateGCEAK.ObjectHandle,
Name: cCreateGCEAK.Name,
},
KeyID: hex.EncodeToString(cCreateGCEAK.Name.Buffer),
}
keyctx, err := tpmjwt.NewTPMContext(ctx, config)
if err != nil {
log.Fatalf("Unable to initialize tpmJWT: %v", err)
}
token.Header["kid"] = config.GetKeyID()
tokenString, err := token.SignedString(keyctx)
if err != nil {
log.Fatalf("Error signing %v", err)
}
fmt.Printf("TOKEN: %s\n", tokenString)
// verify with TPM based publicKey
keyFunc, err := tpmjwt.TPMVerfiyKeyfunc(ctx, config)
if err != nil {
log.Fatalf("could not get keyFunc: %v", err)
}
vtoken, err := jwt.Parse(tokenString, keyFunc)
if err != nil {
log.Fatalf("Error verifying token %v", err)
}
if vtoken.Valid {
log.Println(" verified with Signer PublicKey")
}
}
func nvReadEX(rwr transport.TPM, index tpmutil.Handle) ([]byte, error) {
readPubRsp, err := tpm2.NVReadPublic{
NVIndex: tpm2.TPMHandle(index),
}.Execute(rwr)
if err != nil {
return nil, err
}
c, err := readPubRsp.NVPublic.Contents()
if err != nil {
return nil, err
}
getCmd := tpm2.GetCapability{
Capability: tpm2.TPMCapTPMProperties,
Property: uint32(tpm2.TPMPTNVBufferMax),
PropertyCount: 1,
}
getRsp, err := getCmd.Execute(rwr)
if err != nil {
return nil, err
}
tp, err := getRsp.CapabilityData.Data.TPMProperties()
if err != nil {
return nil, err
}
blockSize := int(tp.TPMProperty[0].Value)
outBuff := make([]byte, 0, int(c.DataSize))
for len(outBuff) < int(c.DataSize) {
readSize := blockSize
if readSize > (int(c.DataSize) - len(outBuff)) {
readSize = int(c.DataSize) - len(outBuff)
}
readRsp, err := tpm2.NVRead{
AuthHandle: tpm2.AuthHandle{
Handle: tpm2.TPMRHOwner,
Name: tpm2.HandleName(tpm2.TPMRHOwner),
Auth: tpm2.HMAC(tpm2.TPMAlgSHA256, 16, tpm2.Auth([]byte{})),
},
NVIndex: tpm2.NamedHandle{
Handle: tpm2.TPMHandle(index),
Name: readPubRsp.NVName,
},
Size: uint16(readSize),
Offset: uint16(len(outBuff)),
}.Execute(rwr)
if err != nil {
return nil, err
}
data := readRsp.Data.Buffer
outBuff = append(outBuff, data...)
}
return outBuff, nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment