1. Tổng quan
Máy ảo bảo mật (CVM) là một loại máy ảo Compute Engine sử dụng tính năng mã hoá bộ nhớ dựa trên phần cứng. Điều này giúp đảm bảo dữ liệu và ứng dụng của bạn không thể bị đọc hoặc sửa đổi trong khi sử dụng. Trong lớp học lập trình này, bạn sẽ thấy quy trình chứng thực từ xa cho phép một bên từ xa xác minh các nút CVM. Ngoài ra, bạn sẽ khám phá tính năng ghi nhật ký trên đám mây để kiểm tra thêm.

Như minh hoạ trong hình trên, lớp học lập trình này bao gồm các bước sau:
- Thiết lập CVM và chứng thực từ xa
- Khám phá Cloud Logging trên quy trình chứng thực từ xa CVM
- Thiết lập máy chủ web phát hành bí mật
Các thành phần trong hình trên bao gồm một công cụ go-tpm và Google Cloud Attestation:
- Công cụ go-tpm: Một công cụ nguồn mở để tìm nạp bằng chứng chứng thực từ vTPM (Mô-đun nền tảng đáng tin cậy ảo) trên CVM và gửi bằng chứng đó đến dịch vụ Chứng thực của Google Cloud để lấy mã thông báo chứng thực.
- Chứng thực Google Cloud: Xác minh và xác thực bằng chứng chứng thực đã nhận được, đồng thời trả về một mã thông báo chứng thực phản ánh các thông tin về CVM của người yêu cầu.
Kiến thức bạn sẽ học được
- Cách thực hiện chứng thực từ xa thông qua API Điện toán bảo mật trên CVM
- Cách sử dụng Cloud Logging để giám sát chứng thực từ xa CVM
Bạn cần có
- Một dự án trên Google Cloud Platform
- Một trình duyệt, chẳng hạn như Chrome hoặc Firefox
- Kiến thức cơ bản về Google Compute Engine, VM bảo mật
2. Thiết lập và yêu cầu
Để bật các API cần thiết, hãy chạy lệnh sau trong bảng điều khiển đám mây hoặc môi trường phát triển cục bộ:
gcloud auth login
gcloud services enable \
cloudapis.googleapis.com \
cloudshell.googleapis.com \
confidentialcomputing.googleapis.com \
compute.googleapis.com
3. Thiết lập chứng thực từ xa CVM và vTPM
Ở bước này, bạn sẽ tạo một CVM và thực hiện chứng thực từ xa vTPM để truy xuất Mã thông báo chứng thực (Mã thông báo OIDC) trên CVM.
Truy cập vào bảng điều khiển đám mây hoặc môi trường phát triển cục bộ. Tạo một CVM như sau (tham khảo bài viết Tạo một phiên bản máy ảo bảo mật | Google Cloud). Bạn cần có phạm vi để truy cập vào API Điện toán bảo mật.
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
Uỷ quyền cho tài khoản dịch vụ mặc định của CVM truy cập vào các API Điện toán bảo mật (CVM cần có các quyền sau để tìm nạp Mã thông báo chứng thực từ các API Điện toán bảo mật):
1). Tạo một vai trò để cho phép truy cập vào API Điện toán bảo mật.
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
Thay thế nội dung sau:
project-idlà giá trị nhận dạng duy nhất của dự án.
2). Thêm tài khoản dịch vụ mặc định của VM vào vai trò.
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"
Thay thế nội dung sau:
project-idlà giá trị nhận dạng duy nhất của dự án.
3). Kết nối với CVM và thiết lập tệp nhị phân công cụ go-tpm để tìm nạp Mã chứng thực từ các API Điện toán bảo mật do Google Cloud Attestation cung cấp.
- Kết nối với CVM.
gcloud compute ssh --zone us-central1-c cvm-attestation-codelab
- Thiết lập môi trường 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
- Tạo tệp nhị phân công cụ go-tpm. Tệp nhị phân của công cụ go-tpm sẽ tìm nạp bằng chứng chứng thực từ vTPM trên CVM và gửi bằng chứng đó đến Chứng thực Google Cloud để lấy mã thông báo chứng thực.
git clone https://github.com/google/go-tpm-tools.git --depth 1 cd go-tpm-tools/cmd/gotpm/ go build
- Lệnh go-tpm trích xuất bằng chứng chứng thực của CVM từ vTPM và gửi đến Google Cloud Attestation. Google Cloud Attestation xác minh và xác thực bằng chứng chứng thực, đồng thời trả về một Mã thông báo chứng thực. Lệnh này sẽ tạo một tệp attestation_token chứa
attestation-tokencủa bạn. Sau này, bạn sẽ dùngattestation-tokenđể tìm nạp một khoá bí mật. Bạn có thể giải mã mã thông báo chứng thực trong jwt.io để xem các yêu cầu.
sudo ./gotpm token > attestation_token
- (Không bắt buộc) Ngoài việc thực hiện chứng thực từ xa bằng công cụ go-tpm và Google Cloud Attestation, chúng tôi giới thiệu các lệnh để tìm nạp bằng chứng chứng thực vTPM. Bằng cách này, bạn có thể tạo một dịch vụ như Google Cloud Attestation để xác minh và xác thực bằng chứng chứng thực:
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 chứa nhật ký sự kiện đã xác minh. Bạn có thể dùng trình chỉnh sửa mà mình muốn để xem tệp này. Xin lưu ý rằng lệnh xác minh không kiểm tra chứng chỉ khoá chứng thực của trích dẫn.
4. Bật Cloud Logging và khám phá nhật ký chứng thực từ xa
Bạn có thể chạy lệnh sau trong CVM cvm-attestation-codelab. Lần này, hoạt động sẽ được ghi vào Cloud Logging.
sudo ./gotpm token --cloud-log --audience "https://api.cvm-attestation-codelab.com"
Nhận cvm-attestation-codelab <instance-id> trong Cloud Console hoặc môi trường phát triển cục bộ.
gcloud compute instances describe cvm-attestation-codelab --zone us-central1-c --format='value(id)'
Để khám phá Cloud Logging, hãy mở URL sau: https://console.cloud.google.com/logs. Trong trường truy vấn, hãy nhập nội dung sau:
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
Thay thế nội dung sau:
project-idlà giá trị nhận dạng duy nhất của dự án.instance-idlà giá trị nhận dạng duy nhất của thực thể.
Bạn có thể tìm thấy Mã chứng thực, các Xác nhận quyền sở hữu, bằng chứng thô và số chỉ dùng một lần được gửi đến Dịch vụ chứng thực của Google Cloud.
5. Thiết lập một máy chủ web phát hành bí mật
Trong bước này, bạn sẽ thoát khỏi phiên SSH trước đó và thiết lập một máy ảo khác. Trên VM này, bạn thiết lập một máy chủ web phát hành bí mật. Máy chủ web xác thực Attestation Token đã nhận và các xác nhận quyền sở hữu của mã này. Nếu quá trình xác thực thành công, thì nó sẽ phát hành khoá bí mật cho người yêu cầu.
1). Truy cập vào bảng điều khiển đám mây hoặc môi trường phát triển cục bộ. Tạo một máy ảo.
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
Thay thế nội dung sau:
project-idlà giá trị nhận dạng duy nhất của dự án.
2). Tạo kết nối SSH đến máy ảo mới.
gcloud compute ssh --zone us-central1-c cvm-attestation-codelab-web-server
3). Thiết lập môi trường 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). Tạo 2 tệp sau đây để lưu trữ mã nguồn của máy chủ web phát hành bí mật (sao chép/dán bằng 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). Chạy các lệnh sau để tạo và chạy máy chủ web. Thao tác này sẽ khởi động máy chủ web phát hành bí mật tại cổng :8080.
go mod init google.com/codelab go mod tidy go get github.com/golang-jwt/jwt/v4 go build ./codelab
Khắc phục sự cố: bạn có thể thấy cảnh báo sau đây và có thể bỏ qua cảnh báo này khi chạy 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). Bây giờ, hãy bắt đầu một phiên bảng điều khiển đám mây khác hoặc phiên môi trường phát triển cục bộ rồi chạy lệnh sau. Thao tác này sẽ giúp bạn nhận được <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). Kết nối SSH đến phiên bản máy ảo cvm-attestation-codelab của bạn.
gcloud compute ssh --zone "us-central1-c" "cvm-attestation-codelab"
8). Lệnh sau đây sẽ thay thế attestation-token đã nhận được trước đó (trong ~/go-tpm-tools/cmd/gotpm/). Lệnh này sẽ tìm nạp cho bạn khoá bí mật do máy chủ web phát hành khoá bí mật nắm giữ!
cd ~/go-tpm-tools/cmd/gotpm/ curl http://<cvm-attestation-codelab-web-server-internal-ip>:8080 -H "Authorization: Bearer $(cat ./attestation_token)"
Thay thế nội dung sau:
cvm-attestation-codelab-web-server-internal-iplà ip nội bộ của phiên bản VM cvm-attestation-codelab-web-server.
Bạn sẽ thấy thông báo "Đây là thông tin siêu bí mật!" trên màn hình.
Nếu nhập attestation-token không chính xác hoặc đã hết hạn, bạn sẽ thấy thông báo "curl: (52) Empty reply from server" (curl: (52) Máy chủ không phản hồi). Bạn cũng sẽ thấy "Token valid: false" (Mã thông báo không hợp lệ) trên nhật ký máy chủ web phát hành bí mật trong phiên bản máy ảo cvm-attestation-codelab-web-server.
6. Dọn dẹp
Chạy các lệnh sau trong bảng điều khiển đám mây hoặc môi trường phát triển cục bộ:
# 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
Thay thế nội dung sau:
project-idlà giá trị nhận dạng duy nhất của dự án.
7. Bước tiếp theo
Tìm hiểu thêm về Máy ảo bảo mật và Compute Engine.