Last active
March 5, 2025 15:18
-
-
Save salrashid123/8ff84299fd5be1c85a8d00c5a89dd716 to your computer and use it in GitHub Desktop.
GCE Attestation Key based authentication
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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