1. Ringkasan
Confidential Virtual Machine (CVM) adalah jenis Virtual Machine Compute Engine yang menggunakan enkripsi memori berbasis hardware. Hal ini membantu memastikan data dan aplikasi Anda tidak dapat dibaca atau diubah saat digunakan. Dalam codelab ini, Anda akan melihat pengesahan jarak jauh yang memungkinkan pihak jarak jauh memverifikasi node CVM. Selain itu, Anda akan mempelajari logging cloud untuk audit lebih lanjut.
Seperti yang digambarkan pada gambar di atas, codelab ini mencakup langkah-langkah berikut:
- Penyiapan CVM dan Pengesahan jarak jauh
- Eksplorasi Cloud Logging pada pengesahan jarak jauh CVM
- Penyiapan Server Web Rilis Secret
Komponen dalam gambar di atas mencakup alat go-tpm dan Google Cloud Attestation:
- Alat go-tpm: Alat open source untuk mengambil bukti pengesahan dari vTPM (Virtual Trusted Platform Module) di CVM dan mengirimkannya ke Google Cloud Attestation untuk mendapatkan token pengesahan.
- Pengesahan Google Cloud: Memverifikasi dan memvalidasi bukti pengesahan yang diterima dan menampilkan token pengesahan yang mencerminkan fakta tentang CVM pemohon.
Yang akan Anda pelajari
- Cara melakukan pengesahan jarak jauh melalui Confidential Computing API di CVM
- Cara menggunakan Cloud Logging untuk memantau pengesahan jarak jauh CVM
Yang Anda butuhkan
- Project Google Cloud Platform
- Browser, seperti Chrome atau Firefox
- Pengetahuan dasar tentang Google Compute Engine, Confidential VM
2. Penyiapan dan Persyaratan
Untuk mengaktifkan API yang diperlukan, jalankan perintah berikut di konsol cloud atau lingkungan pengembangan lokal Anda:
gcloud auth login gcloud services enable \ cloudapis.googleapis.com \ cloudshell.googleapis.com \ confidentialcomputing.googleapis.com \ compute.googleapis.com
3. Menyiapkan pengesahan jarak jauh CVM dan vTPM
Pada langkah ini, Anda akan membuat CVM dan melakukan pengesahan jarak jauh vTPM untuk mengambil Token Pengesahan (Token OIDC) di CVM.
Buka konsol cloud atau lingkungan pengembangan lokal Anda. Buat CVM sebagai berikut (lihat Membuat instance Confidential VM | Google Cloud). Cakupan diperlukan untuk mengakses Confidential Computing API.
gcloud config set project <project-id> gcloud compute instances create cvm-attestation-codelab \ --machine-type=n2d-standard-2 \ --min-cpu-platform="AMD Milan" \ --zone=us-central1-c \ --confidential-compute \ --image=ubuntu-2204-jammy-v20240228 \ --image-project=ubuntu-os-cloud \ --scopes https://www.googleapis.com/auth/cloud-platform
Beri otorisasi akun layanan default CVM untuk mengakses Confidential Computing API (CVM memerlukan izin berikut untuk mengambil Token Pengesahan dari Confidential Computing API):
1.) Buat peran untuk mengizinkan akses ke Confidential Computing API.
gcloud iam roles create Confidential_Computing_User --project=<project-id> \ --title="CVM User" --description="Grants the ability to generate an attestation token in a CVM." \ --permissions="confidentialcomputing.challenges.create,confidentialcomputing.challenges.verify,confidentialcomputing.locations.get,confidentialcomputing.locations.list" --stage=GA
Ganti kode berikut:
project-id
adalah ID unik project.
2.) Tambahkan akun layanan default VM ke peran.
gcloud projects add-iam-policy-binding <project-id> \ --member serviceAccount:$(gcloud iam service-accounts list --filter="email ~ compute@developer.gserviceaccount.com$" --format='value(email)' ) \ --role "projects/<project-id>/roles/Confidential_Computing_User"
Ganti kode berikut:
project-id
adalah ID unik project.
3.) Terhubung ke CVM dan siapkan biner alat go-tpm untuk mengambil Token Pengesahan dari Confidential Computing API yang disediakan oleh Google Cloud Attestation.
- Hubungkan ke CVM.
gcloud compute ssh --zone us-central1-c cvm-attestation-codelab
- Menyiapkan lingkungan Go:
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
- Build biner alat go-tpm. Biner alat go-tpm mengambil bukti pengesahan dari vTPM di CVM dan mengirimkannya ke Google Cloud Attestation untuk mendapatkan token pengesahan.
git clone https://github.com/google/go-tpm-tools.git --depth 1 cd go-tpm-tools/cmd/gotpm/ go build
- Perintah alat go-tpm mengekstrak bukti pengesahan CVM dari vTPM dan mengirimkannya ke Google Cloud Attestation. Pengesahan Google Cloud memverifikasi dan memvalidasi bukti pengesahan serta menampilkan Token Pengesahan. Perintah ini akan membuat file attestation_token yang berisi
attestation-token
Anda. Anda akan menggunakanattestation-token
untuk mengambil secret nanti. Anda dapat mendekode token pengesahan di jwt.io untuk melihat klaim.
sudo ./gotpm token > attestation_token
- (Opsional) Atau, untuk melakukan pengesahan jarak jauh dengan alat go-tpm dan Google Cloud Attestation, kami menampilkan perintah untuk mengambil bukti pengesahan vTPM. Dengan cara ini, Anda dapat membuat layanan seperti Google Cloud Attestation untuk memverifikasi dan memvalidasi bukti pengesahan:
nonce=$(head -c 16 /dev/urandom | xxd -p) sudo ./gotpm attest --nonce $nonce --format textproto --output quote.dat sudo ./gotpm verify debug --nonce $nonce --format textproto --input quote.dat --output vtpm_report
vtpm_report
berisi log peristiwa terverifikasi. Anda dapat menggunakan editor pilihan untuk melihatnya. Perhatikan bahwa perintah verifikasi tidak memeriksa sertifikat kunci pengesahan dari kutipan.
4. Mengaktifkan Cloud Logging dan menjelajahi log pengesahan jarak jauh
Anda dapat menjalankan perintah berikut di CVM cvm-attestation-codelab
. Kali ini, fungsi ini mencatat aktivitas di cloud logging.
sudo ./gotpm token --cloud-log --audience "https://api.cvm-attestation-codelab.com"
Dapatkan cvm-attestation-codelab
<instance-id>
di konsol cloud atau lingkungan pengembangan lokal Anda.
gcloud compute instances describe cvm-attestation-codelab --zone us-central1-c --format='value(id)'
Untuk menjelajahi Cloud Logging, buka URL berikut: https://console.cloud.google.com/logs. Di kolom kueri, masukkan kode berikut:
resource.type="gce_instance" resource.labels.zone="us-central1-c" resource.labels.instance_id=<instance-id> log_name="projects/<project-id>/logs/gotpm" severity>=DEFAULT
Ganti kode berikut:
project-id
adalah ID unik project.instance-id
adalah ID unik instance.
Anda akan dapat menemukan Token Pengesahan, Klaimnya, bukti mentah, dan nonce yang dikirim ke Google Cloud Attestation.
5. Menyiapkan Server Web Rilis Rahasia
Pada langkah ini, Anda akan keluar dari sesi SSH sebelumnya dan menyiapkan VM lain. Di VM ini, Anda menyiapkan server web rilis rahasia. Server web memvalidasi Token Pengesahan yang diterima dan klaimnya. Jika validasi berhasil, secret akan dirilis kepada pemohon.
1.) Buka konsol cloud atau lingkungan pengembangan lokal Anda. Buat virtual machine.
gcloud config set project <project-id> gcloud compute instances create cvm-attestation-codelab-web-server \ --machine-type=n2d-standard-2 \ --zone=us-central1-c \ --image=ubuntu-2204-jammy-v20240228 \ --image-project=ubuntu-os-cloud
Ganti kode berikut:
project-id
adalah ID unik project.
2.) Gunakan SSH untuk terhubung ke VM baru Anda.
gcloud compute ssh --zone us-central1-c cvm-attestation-codelab-web-server
3.) Siapkan lingkungan Go.
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). Buat dua file berikut untuk menyimpan kode sumber server web rilis rahasia (salin/tempel dengan 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). Jalankan perintah berikut untuk mem-build server web dan menjalankannya. Tindakan ini akan memulai server web rilis rahasia di port :8080
.
go mod init google.com/codelab go mod tidy go get github.com/golang-jwt/jwt/v4 go build ./codelab
Pemecahan masalah: Anda mungkin melihat peringatan berikut yang dapat diabaikan saat menjalankan 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). Sekarang, mulai tab konsol cloud atau sesi lingkungan pengembangan lokal lainnya dan jalankan perintah berikut. Tindakan ini akan memberi Anda <cvm-attestation-codelab-web-server-internal-ip
>.
gcloud compute instances describe cvm-attestation-codelab-web-server --format='get(networkInterfaces[0].networkIP)' --zone=us-central1-c
7). Gunakan SSH untuk terhubung ke instance VM cvm-attestation-codelab
Anda.
gcloud compute ssh --zone "us-central1-c" "cvm-attestation-codelab"
8). Perintah berikut akan mengganti attestation-token
yang diperoleh sebelumnya (di bagian ~/go-tpm-tools/cmd/gotpm/
). Tindakan ini akan mengambil secret yang disimpan oleh server web rilis secret.
cd ~/go-tpm-tools/cmd/gotpm/ curl http://<cvm-attestation-codelab-web-server-internal-ip>:8080 -H "Authorization: Bearer $(cat ./attestation_token)"
Ganti kode berikut:
cvm-attestation-codelab-web-server-internal-ip
adalah IP internal instance VM cvm-attestation-codelab-web-server.
Anda akan melihat "Ini adalah informasi super rahasia!" di layar.
Jika memasukkan attestation-token
yang salah atau sudah tidak berlaku, Anda akan melihat "curl: (52) Empty reply from server". Anda juga akan melihat "Token valid: false" di log server web rilis rahasia di instance VM cvm-attestation-codelab-web-server
.
6. Pembersihan
Jalankan perintah berikut di konsol cloud atau lingkungan pengembangan lokal Anda:
# Delete the role binding gcloud projects remove-iam-policy-binding <project-id> \ --member serviceAccount:$(gcloud iam service-accounts list --filter="email ~ compute@developer.gserviceaccount.com$" --format='value(email)' ) \ --role "projects/<project-id>/roles/Confidential_Computing_User" # Delete the role gcloud iam roles delete Confidential_Computing_User --project=<project-id> # Delete the web server VM instance gcloud compute instances delete cvm-attestation-codelab-web-server --zone=us-central1-c # Delete the CVM instance gcloud compute instances delete cvm-attestation-codelab --zone=us-central1-c
Ganti kode berikut:
project-id
adalah ID unik project.
7. Langkah berikutnya
Pelajari Confidential VM dan Compute Engine lebih lanjut.