1. نظرة عامة
الأجهزة الافتراضية السرية (CVM) هي نوع من الأجهزة الافتراضية في Compute Engine تستخدم تشفير الذاكرة المستند إلى الأجهزة. يساعد ذلك في ضمان عدم إمكانية قراءة بياناتك وتطبيقاتك أو تعديلها أثناء استخدامها. في هذا الدرس التطبيقي حول الترميز، سيتم عرض مصادقة عن بعد تتيح لجهة خارجية التحقّق من عُقد طريقة التحقق من هوية حامل البطاقة (CVM). بالإضافة إلى ذلك، ستستكشف تسجيل البيانات في السحابة الإلكترونية لإجراء المزيد من عمليات التدقيق.

كما هو موضّح في الشكل أعلاه، يتضمّن هذا الدرس التطبيقي العملي الخطوات التالية:
- إعداد CVM والتأكيد عن بُعد
- استكشاف Cloud Logging بشأن المصادقة عن بُعد على CVM
- إعداد خادم الويب لإصدار Secret
تتضمّن المكوّنات في الشكل أعلاه أداة go-tpm وخدمة Google Cloud Attestation:
- أداة go-tpm: هي أداة مفتوحة المصدر لاسترداد مستندات الإثبات من وحدة الأنظمة الأساسية الموثوقة الافتراضية (vTPM) على الجهاز الظاهري لآلة CVM وإرسالها إلى خدمة Google Cloud Attestation للحصول على رمز مميّز للإثبات.
- شهادة المصادقة من Google Cloud: يتم التحقّق من صحة دليل شهادة المصادقة المستلَم والتحقّق منه، ثم يتم إرجاع رمز مميّز لشهادة المصادقة يعكس الحقائق حول الجهاز الظاهري المحمي (CVM) الخاص بالجهة الطالبة.
أهداف الدورة التعليمية
- كيفية إجراء التأكيد عن بُعد من خلال واجهات برمجة التطبيقات للحوسبة السرية على CVM
- كيفية استخدام Cloud Logging لرصد المصادقة عن بعد في الآلة الافتراضية المحمية
المتطلبات
- مشروع على Google Cloud Platform
- متصفّح، مثل Chrome أو Firefox
- معرفة أساسية بخدمتَي Google Compute Engine وConfidential VM
2. الإعداد والمتطلبات
لتفعيل واجهات برمجة التطبيقات اللازمة، نفِّذ الأمر التالي في "وحدة تحكّم السحابة الإلكترونية" أو بيئة التطوير المحلية:
gcloud auth login
gcloud services enable \
cloudapis.googleapis.com \
cloudshell.googleapis.com \
confidentialcomputing.googleapis.com \
compute.googleapis.com
3- إعداد التأكيد عن بُعد باستخدام CVM وvTPM
في هذه الخطوة، ستنشئ جهازًا افتراضيًا محميًا من خلال التشفير (CVM) وتنفّذ عملية إثبات صحة عن بُعد لجهاز vTPM من أجل استرداد رمز إثبات الصحة (رمز OIDC) على الجهاز الافتراضي المحمي من خلال التشفير.
انتقِل إلى وحدة تحكّم السحابة الإلكترونية أو بيئة التطوير المحلية. أنشئ جهاز CVM على النحو التالي (راجِع إنشاء مثيل Confidential VM | Google Cloud). يجب استخدام النطاقات للوصول إلى واجهات برمجة التطبيقات الخاصة بـ حوسبة سرية.
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
امنح حساب الخدمة التلقائي في CVM الإذن بالوصول إلى واجهات برمجة تطبيقات الحوسبة السرية (تحتاج الأجهزة الافتراضية CVM إلى الأذونات التالية لاسترداد رمز الإثبات من واجهات برمجة تطبيقات الحوسبة السرية):
1. أنشئ دورًا للسماح بالوصول إلى واجهات برمجة التطبيقات للحوسبة السرية.
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
غيِّر القيم في السلسلة على الشكل التالي:
-
project-idهو المعرّف الفريد للمشروع.
2. أضِف حساب الخدمة التلقائي للجهاز الظاهري إلى الدور.
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"
غيِّر القيم في السلسلة على الشكل التالي:
-
project-idهو المعرّف الفريد للمشروع.
3. اتّصِل بجهاز CVM وأعِدّ ملف go-tpm الثنائي لجلب رمز المصادقة من واجهات برمجة التطبيقات للحوسبة السرية التي توفّرها خدمة Google Cloud Attestation.
- الاتصال بـ CVM
gcloud compute ssh --zone us-central1-c cvm-attestation-codelab
- إعداد بيئة 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
- إنشاء ملف go-tpm الثنائي للأداة يجلب ملف go-tpm الثنائي دليل الإقرار من وحدة TPM الافتراضية على الجهاز الظاهري للعميل (CVM) ويرسله إلى خدمة Google Cloud Attestation للحصول على رمز إقرار.
git clone https://github.com/google/go-tpm-tools.git --depth 1 cd go-tpm-tools/cmd/gotpm/ go build
- يستخرج أمر أداة go-tpm دليل الإقرار الخاص بـ CVM من vTPM ويرسله إلى خدمة Google Cloud Attestation. تتحقّق خدمة Google Cloud Attestation من صحة أدلة المصادقة وتُرجع رمز مصادقة. ينشئ الأمر ملف attestation_token يحتوي على
attestation-token. ستستخدِمattestation-tokenلاسترداد سرّ لاحقًا. يمكنك فك ترميز رمز الإثبات في jwt.io لعرض المطالبات.
sudo ./gotpm token > attestation_token
- (اختياري) بدلاً من إجراء التصديق عن بُعد باستخدام أداة go-tpm وخدمة Google Cloud Attestation، نعرض الأوامر اللازمة لجلب مستندات التصديق على وحدة TPM الافتراضية. بهذه الطريقة، يمكنك إنشاء خدمة مثل Google Cloud Attestation للتحقّق من صحة مستندات المصادقة والتحقق منها:
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 على سجلّ الأحداث الذي تم التحقّق منه. يمكنك استخدام المحرّر المفضّل لديك للاطّلاع عليه. يُرجى العِلم أنّ الأمر verify لا يتحقّق من شهادة مفتاح المصادقة الخاصة بالاقتباس.
4. تفعيل Cloud Logging واستكشاف سجلّ التأكيد عن بُعد
يمكنك تنفيذ الأمر التالي في cvm-attestation-codelab CVM. في هذه المرة، يتم تسجيل النشاط في خدمة تسجيل الدخول السحابية.
sudo ./gotpm token --cloud-log --audience "https://api.cvm-attestation-codelab.com"
احصل على cvm-attestation-codelab <instance-id> في وحدة تحكّم السحابة الإلكترونية أو بيئة التطوير المحلية.
gcloud compute instances describe cvm-attestation-codelab --zone us-central1-c --format='value(id)'
لاستكشاف Cloud Logging، افتح عنوان URL التالي: https://console.cloud.google.com/logs. في حقل طلب البحث، أدخِل ما يلي:
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
غيِّر القيم في السلسلة على الشكل التالي:
-
project-idهو المعرّف الفريد للمشروع. -
instance-idهو المعرّف الفريد للآلة الافتراضية.
من المفترض أن تتمكّن من العثور على رمز الإثبات ومطالباته والأدلة الأولية والرقم العشوائي الذي تم إرساله إلى خدمة Google Cloud Attestation.
5- إعداد خادم ويب لإصدار سري
في هذه الخطوة، عليك الخروج من جلسة SSH السابقة وإعداد جهاز افتراضي آخر. على هذا الجهاز الافتراضي، يمكنك إعداد خادم ويب لإصدار سري. يتحقّق خادم الويب من صحة رمز الإثبات والبيانات الواردة فيه. إذا نجحت عمليات التحقّق، سيتم إرسال الرقم السري إلى مقدّم الطلب.
1. انتقِل إلى وحدة تحكّم السحابة الإلكترونية أو بيئة التطوير المحلية. أنشئ جهازًا افتراضيًا.
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
غيِّر القيم في السلسلة على الشكل التالي:
-
project-idهو المعرّف الفريد للمشروع.
2. استخدِم بروتوكول SSH للوصول إلى جهازك الافتراضي الجديد.
gcloud compute ssh --zone us-central1-c cvm-attestation-codelab-web-server
3. إعداد بيئة 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. أنشئ الملفَين التاليَين لتخزين الرمز المصدر لخادم الويب الخاص بإصدار التطبيق السري (يمكنك نسخ المحتوى ولصقه باستخدام 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). نفِّذ الأوامر التالية لإنشاء خادم الويب وتشغيله. سيؤدي ذلك إلى بدء تشغيل خادم الويب للإصدار السري على المنفذ :8080.
go mod init google.com/codelab go mod tidy go get github.com/golang-jwt/jwt/v4 go build ./codelab
تحديد المشاكل وحلّها: قد يظهر لك التحذير التالي الذي يمكن تجاهله عند تنفيذ 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. الآن، ابدأ جلسة أخرى في علامة تبويب Cloud Console أو بيئة تطوير محلية ونفِّذ الأمر التالي. سيؤدي ذلك إلى الحصول على <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) اتّصِل بمثيل الجهاز الافتراضي cvm-attestation-codelab باستخدام بروتوكول SSH.
gcloud compute ssh --zone "us-central1-c" "cvm-attestation-codelab"
8). يستبدل الأمر التالي attestation-token الذي تم الحصول عليه سابقًا (ضمن ~/go-tpm-tools/cmd/gotpm/). سيؤدي ذلك إلى جلب الرمز السري الذي يحتفظ به خادم الويب لإصدار الرمز السري.
cd ~/go-tpm-tools/cmd/gotpm/ curl http://<cvm-attestation-codelab-web-server-internal-ip>:8080 -H "Authorization: Bearer $(cat ./attestation_token)"
غيِّر القيم في السلسلة على الشكل التالي:
-
cvm-attestation-codelab-web-server-internal-ipهو عنوان IP الداخلي لمثيل الجهاز الافتراضي cvm-attestation-codelab-web-server.
ستظهر لك العبارة "هذه هي المعلومات السرية للغاية!" على الشاشة.
إذا أدخلت attestation-token غير صحيح أو منتهي الصلاحية، ستظهر لك الرسالة "curl: (52) Empty reply from server". سيظهر لك أيضًا "Token valid: false" (الرمز المميز غير صالح) في سجل خادم الويب لإصدار التطبيق السري في مثيل الجهاز الافتراضي cvm-attestation-codelab-web-server.
6. تنظيف
نفِّذ الأوامر التالية في وحدة تحكّم السحابة الإلكترونية أو بيئة التطوير المحلية:
# 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
غيِّر القيم في السلسلة على الشكل التالي:
-
project-idهو المعرّف الفريد للمشروع.
7. الخطوات التالية
مزيد من المعلومات عن الأجهزة الافتراضية السرية وCompute Engine