1. סקירה כללית
Confidential Space מאפשר שיתוף נתונים ושיתוף פעולה מאובטח בין כמה צדדים, תוך שמירה על סודיות הנתונים של הארגונים. המשמעות היא שארגונים יכולים לשתף פעולה ביניהם ועדיין לשמור על שליטה בנתונים שלהם ולהגן עליהם מפני גישה לא מורשית.
בעזרת Confidential Space אפשר לנתח נתונים רגישים, שלעתים קרובות כפופים לרגולציה, ולקבל מהם ערך משותף, תוך שמירה על שליטה מלאה בהם. בעזרת Confidential Space, ארגונים יכולים להפיק ערך משותף מצבירה וניתוח של נתונים רגישים כמו פרטים אישיים מזהים (PII), מידע רפואי מוגן (PHI), קניין רוחני וסודות קריפטוגרפיים – תוך שמירה על שליטה מלאה בנתונים.
מה תצטרכו
- שני פרויקטים נפרדים ב-Google Cloud Platform
- דפדפן, כמו Chrome או Firefox
- ידע בסיסי ב-Google Compute Engine, ב-Confidential VM, ב-Containers ובמאגרי מידע מרוחקים, באישורים ובשרשראות אישורים.
- ידע בסיסי בחשבונות שירות, ב-Open Policy Agent, ב-Rego וב-Public Key Infrastructure
מה תלמדו
- איך מגדירים את משאבי הענן שדרושים להרצת Confidential Space
- איך מריצים עומס עבודה במכונה וירטואלית חסויה שמופעלת באמצעות התמונה של Confidential Space
- איך מאשרים גישה למשאבים מוגנים על סמך המאפיינים של קוד עומס העבודה (מה), סביבת Confidential Space (איפה) והחשבון שמריץ את עומס העבודה (מי).
ב-codelab הזה נתמקד בשימוש ב-Confidential Space עם משאבים מוגנים שמארחים במקום אחר ולא ב-Google Cloud. תלמדו איך לבקש אסימון מותאם אישית ועצמאי משירות האימות של Google על ידי ציון ערך חד-פעמי, קהל ויעד ואת סוג האסימון של PKI.
ב-codelab הזה תגדירו מרחב נתונים חסוי בין מוצר פיקטיבי – USleep, אפליקציה מבוססת-קונטיינר, לבין מוצר פיקטיבי – UWear, מכשיר לביש מקושר, כדי לחשב את איכות השינה שלכם. UWear ישתף מידע רפואי מוגן (PHI) עם USleep בסביבה בטוחה, מאובטחת ומבודדת (שנקראת גם סביבת מחשוב אמינה או TEE), כך שבעלי הנתונים ישמרו על סודיות מלאה.
UWear הוא גם בודק עומסי העבודה וגם בעל הנתונים. בתור בודק עומסי עבודה,הוא בודק את הקוד בעומס העבודה שמופעל ורושם את הגיבוב של התמונה. בתור בעלים של הנתונים, UWear כותבת את לוגיקת האימות כדי לבדוק את התוקף של האסימון והחתימה שלו. הוא כותב מדיניות אימות, באמצעות תקציר התמונה של עומסי העבודה שנבדקו, שמאפשרת רק לתקציר התמונה הספציפי, בסביבה ספציפית, לקבל גישה לנתונים הרגישים.
במעבדת הקוד הזו, USleep פורסת את האפליקציה שמוכלת בקונטיינר. ל-USleep אין גישה לנתונים הרגישים, אבל היא מריצה את עומס העבודה שאושר ושיש לו גישה לנתונים הרגישים.
ה-codelab כולל את השלבים הבאים:
- שלב 1: מגדירים את משאבי הענן הנדרשים ל-codelab. הגדרת פרויקטים, חיוב והרשאות. מורידים את קוד המקור של ה-codelab ומגדירים משתני סביבה.
- שלב 2: מורידים את אישור הבסיס ומאחסנים אותו עם קוד המקור של UWear.
- שלב 3: יוצרים חשבונות שירות נפרדים לעומסי העבודה, שישמשו את המכונה הווירטואלית של עומס העבודה ל-USleep ול-UWear.
- שלב 4: יוצרים את עומס העבודה USleep שמספק אסימון אימות.
- שלב 5: יוצרים את עומס העבודה של UWear שמאמת את אסימון האישור ושולח את הנתונים הרגישים אם האסימון מאושר.
- שלב 6: מריצים את עומסי העבודה של USleep ו-UWear. אפליקציית UWear תספק את הנתונים הרגישים, ואפליקציית USleep תריץ אלגוריתם שינה על הנתונים ותציג תוצאה.
- שלב 7: (אופציונלי) מריצים עומס עבודה לא מורשה של USleep ומוודאים שלא התקבלו נתונים רגישים מ-UWear.
- שלב 8: מוחקים את כל המשאבים.
הסבר על תהליך העבודה
USleep תריץ את עומס העבודה ב-Confidential Space. כדי להריץ את עומס העבודה, צריך גישה ל-PHI של UWear. כדי לקבל גישה, עומס העבודה של USleep יוצר קודם סשן TLS מאובטח. בשלב הבא, אפליקציית USleep תבקש גם אסימון אישור מ-Google Attestation Service עם מטען ייעודי (payload).
USleep ישלח בקשה לאסימון אימות עם מטען ייעודי (payload) בפורמט JSON שיכיל שלושה דברים:
- אסימון אימות שמשויך לסשן TLS. כדי לקשור את אסימון האימות לסשן TLS, ערך ה-nonce יהיה הגיבוב של חומר המפתח המיוצא של TLS. קישור הטוקן לסשן TLS מבטיח שלא יתרחשו מתקפות מסוג 'מחשב באמצע', כי רק שני הצדדים שמעורבים בסשן TLS יוכלו ליצור את ערך ה-nonce.
- יוצג קהל של משתמשי uwear. מערכת UWear תאמת שהקהל שאליו מיועד טוקן האימות הוא הקהל הנכון.
- סוג האסימון הוא PKI. סוג האסימון 'PKI' מציין ש-USleep רוצה לבקש אסימון עצמאי. אפשר לאמת שהאסימון העצמאי נחתם על ידי Google באמצעות שורש שהורד מנקודת הקצה הידועה של PKI של Confidential Space. זאת בניגוד לסוג ברירת המחדל של אסימון OIDC, שהחתימה שלו מאומתת באמצעות מפתח ציבורי שמתחלף באופן קבוע.

עומס העבודה של USleep מקבל את אסימון האימות. לאחר מכן, UWear מצטרף לחיבור ה-TLS עם USleep ומאחזר את אסימון האימות של USleep. מערכת UWear תאמת את האסימון על ידי בדיקת התביעה x5c מול אישור הבסיס.
מערכת UWear תאשר את עומס העבודה USleep אם:
- האסימון עובר את הלוגיקה של אימות PKI.
- UWear יאמת את האסימון על ידי בדיקת הטענה x5c מול אישור הבסיס, בדיקה שהאסימון חתום על ידי אישור העלה, ולבסוף בדיקה שאישור הבסיס שהורד זהה לבסיס בטענה x5c.
- ההצהרות לגבי מדידת עומס העבודה באסימון תואמות לתנאי המאפיינים שצוינו במדיניות OPA. OPA הוא מנוע מדיניות בקוד פתוח לשימוש כללי, שמאחד את אכיפת המדיניות בכל הערימה. OPA משתמש במסמכים עם תחביר דומה ל-JSON כדי להגדיר ערכי בסיס שהמדיניות מאומתת מולם. בדוגמה של ערכי בסיס של OPA אפשר לראות אילו ערכים המדיניות בודקת.
- ה-nonce תואם ל-nonce הצפוי (TLS Exported Keying Material). האימות הזה מופיע במדיניות OPA שלמעלה.
אחרי שכל הבדיקות האלה יושלמו ויעברו בהצלחה, UWear תוכל לאשר שהנתונים יישלחו ויעברו עיבוד בצורה מאובטחת. אחרי כן, UWear יגיב עם נתוני ה-PHI הרגישים באותו סשן TLS, ו-USleep יוכל להשתמש בנתונים האלה כדי לחשב את איכות השינה של הלקוח.
2. הגדרת משאבי ענן
לפני שמתחילים
- מגדירים שני פרויקטים ב-Google Cloud, אחד ל-USleep ואחד ל-UWear. מידע נוסף על יצירת פרויקט ב-Google Cloud זמין ב-codelab בנושא הגדרה וניווט בפרויקט הראשון ב-Google. במאמר יצירה וניהול של פרויקטים מוסבר איך לאחזר את מזהה הפרויקט ומה ההבדל בינו לבין שם הפרויקט ומספר הפרויקט.
- מפעילים את החיוב בפרויקטים.
- באחד מפרויקטים של Google ב-Cloud Shell, מגדירים את משתני הסביבה הנדרשים של הפרויקט כמו שמוצג בהמשך.
export UWEAR_PROJECT_ID=<Google Cloud project id of UWear>
export USLEEP_PROJECT_ID=<Google Cloud project id of USleep>
- מפעילים את Confidential Computing API ואת ממשקי ה-API הבאים בשני הפרויקטים.
gcloud config set project $UWEAR_PROJECT_ID
gcloud services enable \
cloudapis.googleapis.com \
cloudshell.googleapis.com \
container.googleapis.com \
containerregistry.googleapis.com \
confidentialcomputing.googleapis.com
gcloud config set project $USLEEP_PROJECT_ID
gcloud services enable \
cloudapis.googleapis.com \
cloudshell.googleapis.com \
container.googleapis.com \
containerregistry.googleapis.com \
confidentialcomputing.googleapis.com
- אחזור המזהה של חשבון המשתמש באמצעות
gcloud auth list
# Output should contain
# ACCOUNT: <Principal Identifier>
# Set your member variable
export MEMBER='user:<Principal Identifier>'
- מוסיפים הרשאות לשני הפרויקטים האלה. אפשר להוסיף הרשאות לפי ההוראות שבדף האינטרנט בנושא הענקת תפקיד IAM.
- במקרה של
$UWEAR_PROJECT_ID, תצטרכו את התפקידים Artifact Registry Administrator ו-Service Account Admin.
gcloud config set project $UWEAR_PROJECT_ID
# Add Artifact Registry Administrator role
gcloud projects add-iam-policy-binding $UWEAR_PROJECT_ID --member=$MEMBER --role='roles/iam.serviceAccountAdmin'
# Add Service Account Administrator role
gcloud projects add-iam-policy-binding $UWEAR_PROJECT_ID --member=$MEMBER --role='roles/artifactregistry.admin'
- כדי להשתמש ב-
$USLEEP_PROJECT_ID, תצטרכו את התפקידים אדמין Compute, אדמין אחסון, אדמין Artifact Registry ואדמין של חשבון שירות.
gcloud config set project $USLEEP_PROJECT_ID
# Add Service Account Administrator role
gcloud projects add-iam-policy-binding $USLEEP_PROJECT_ID --member=$MEMBER --role='roles/iam.serviceAccountAdmin'
# Add Artifact Registry Administrator role
gcloud projects add-iam-policy-binding $USLEEP_PROJECT_ID --member=$MEMBER --role='roles/artifactregistry.admin'
# Add Compute Administrator role
gcloud projects add-iam-policy-binding $USLEEP_PROJECT_ID --member=$MEMBER --role='roles/compute.admin'
# Add Storage Administrator role
gcloud projects add-iam-policy-binding $USLEEP_PROJECT_ID --member=$MEMBER --role='roles/compute.storageAdmin'
- באחד מהפרויקטים שלכם ב-Google Cloud, מריצים את הפקודה הבאה ב-Cloud Shell כדי לשכפל את מאגר ה-GitHub של Codelab בנושא Confidential Space ולקבל את הסקריפטים הנדרשים שמשמשים כחלק מה-Codelab הזה.
git clone https://github.com/GoogleCloudPlatform/confidential-space.git
- עוברים לספריית הסקריפטים של ה-codelab בנושא נתוני בריאות.
cd confidential-space/codelabs/health_data_analysis_codelab/scripts
- מעדכנים את שתי השורות האלה בתסריט config_env.sh, שנמצא בספרייה codelabs/health_data_analysis_codelab/scripts. מעדכנים את מזהי הפרויקטים במזהי הפרויקטים שלכם ב-USleep וב-UWear. חשוב להסיר את סמל התגובה '#' בתחילת השורה.
# TODO: Populate UWear and USleep Project IDs
export UWEAR_PROJECT_ID=your-uwear-project-id
export USLEEP_PROJECT_ID=your-usleep-project-id
- אופציונלי: מגדירים משתנים קיימים. אפשר לשנות את שמות המשאבים באמצעות המשתנים האלה (למשל,
export UWEAR_ARTIFACT_REPOSITORY='my-artifact-repository')
- אפשר להגדיר את המשתנים הבאים באמצעות שמות קיימים של משאבי ענן. אם המשתנה מוגדר, ייעשה שימוש במשאב הענן הקיים התואם מהפרויקט. אם המשתנה לא מוגדר, שם משאב הענן ייווצר מהערכים בסקריפט config_env.sh.
- מריצים את הסקריפט config_env.sh כדי להגדיר את שמות המשתנים שנותרו לערכים שמבוססים על מזהה הפרויקט שלכם בשמות המשאבים.
# Navigate to the scripts folder
cd ~/confidential-space/codelabs/health_data_analysis_codelab/scripts
# Run the config_env script
source config_env.sh
# Verify the variables were set
# Expected output for default variable should be `workload-sa`
echo $USLEEP_WORKLOAD_SERVICE_ACCOUNT
3. הורדת אישור הבסיס
- כדי לאמת את האסימון העצמאי שמוחזר משירות האימות, UWear יצטרך לאמת את החתימה מול אישור הבסיס של המרחב הסודי. אפליקציית UWear תצטרך להוריד את אישור הבסיס ולאחסן אותו באופן מקומי. באחד מהמסופים של פרויקט Google Cloud, מריצים את הפקודות הבאות:
cd ~/confidential-space/codelabs/health_data_analysis_codelab/src/uwear
wget https://confidentialcomputing.googleapis.com/.well-known/confidential_space_root.crt -O confidential_space_root.pem
- יצירת טביעת האצבע של אישור הבסיס שהורד
openssl x509 -fingerprint -in confidential_space_root.pem -noout
- בודקים שטביעת האצבע תואמת לסיכום הבא של SHA-1:
B9:51:20:74:2C:24:E3:AA:34:04:2E:1C:3B:A3:AA:D2:8B:21:23:21
4. יצירת חשבון שירות לעומס עבודה
עכשיו יוצרים שני חשבונות שירות: אחד לעומס העבודה USleep ואחד לעומס העבודה UWear. מריצים את הסקריפט create_service_accounts.sh כדי ליצור חשבונות שירות של עומסי עבודה בפרויקטים USleep ו-UWear. המכונות הווירטואליות שמריצות את עומסי העבודה ישתמשו בחשבונות השירות האלה.
# Navigate to the scripts folder
cd ~/confidential-space/codelabs/health_data_analysis_codelab/scripts
# Run the create_service_accounts script
./create_service_accounts.sh
הסקריפט:
- מקצים את התפקיד
iam.serviceAccountUserשמקשר את חשבון השירות לעומס העבודה. - מקצה את התפקיד
confidentialcomputing.workloadUserלחשבון השירות של עומס העבודה . כך חשבון המשתמש יוכל ליצור אסימון אימות. - מקצים את ההרשאה לתפקיד
logging.logWriterלחשבון השירות של עומס העבודה. כך סביבת Confidential Space יכולה לכתוב יומנים ל-Cloud Logging בנוסף למסוף הטורי, כדי שהיומנים יהיו זמינים אחרי שהמכונה הווירטואלית מסיימת את הפעולה.
5. יצירת עומס עבודה של USleep
בשלב הזה תיצרו קובצי אימג' של Docker עבור עומסי העבודה שמשמשים בשיעור הזה. עומס העבודה של USleep הוא אפליקציית Golang פשוטה שקובעת את איכות השינה של לקוח באמצעות מידע רפואי אישי במכשיר לביש.
מידע על עומס העבודה של USleep
עומס העבודה של USleep הוא אפליקציית Golang פשוטה שקובעת את איכות השינה של לקוח באמצעות מידע אישי על הבריאות במכשיר לביש. עומס העבודה של USleep מורכב משלושה חלקים עיקריים:
- הגדרת סשן TLS וחילוץ של Exported Keying Material (חומר מפתח שיוצא)
func handleConnectionRequest(w http.ResponseWriter, r *http.Request) {
// Upgrade HTTP Connection to a websocket.
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
fmt.Printf("failed to upgrade connection to a websocket with err: %v\n", err)
return
}
defer conn.Close()
// Get EKM
hash, err := getEKMHashFromRequest(r)
if err != nil {
fmt.Printf("Failed to get EKM: %v", err)
}
...
}
func getEKMHashFromRequest(r *http.Request) (string, error) {
ekm, err := r.TLS.ExportKeyingMaterial("testing_nonce", nil, 32)
if err != nil {
err := fmt.Errorf("failed to get EKM from inbound http request: %w", err)
return "", err
}
sha := sha256.New()
sha.Write(ekm)
hash := base64.StdEncoding.EncodeToString(sha.Sum(nil))
fmt.Printf("EKM: %v\nSHA hash: %v", ekm, hash)
return hash, nil
}
- שליחת בקשה לאסימון משירות האימות עם קהל, מספר חד-פעמי וסוג אסימון PKI.
func handleConnectionRequest(w http.ResponseWriter, r *http.Request) {
...
// Request token with TLS Exported Keying Material (EKM) hashed.
token, err := getCustomToken(hash)
if err != nil {
fmt.Printf("failed to get custom token from token endpoint: %v", err)
return
}
// Respond to the client with the token.
conn.WriteMessage(websocket.TextMessage, token)
...
}
var (
socketPath = "/run/container_launcher/teeserver.sock"
tokenEndpoint = "http://localhost/v1/token"
contentType = "application/json"
)
func getCustomToken(nonce string) ([]byte, error) {
httpClient := http.Client{
Transport: &http.Transport{
// Set the DialContext field to a function that creates
// a new network connection to a Unix domain socket
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
return net.Dial("unix", socketPath)
},
},
}
body := fmt.Sprintf(`{
"audience": "uwear",
"nonces": ["%s"],
"token_type": "PKI"
}`, nonce)
resp, err := httpClient.Post(tokenEndpoint, contentType, strings.NewReader(body))
if err != nil {
return nil, err
}
fmt.Printf("Response from launcher: %v\n", resp)
text, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("Failed to read resp.Body: %w", err)
}
fmt.Printf("Token from the attestation service: %s\n", text)
return text, nil
}
- קבלת הנתונים הרגישים וחישוב איכות השינה של המשתמש
func handleConnectionRequest(w http.ResponseWriter, r *http.Request) {
...
// Read the sensitive data
_, content, err := conn.ReadMessage()
if err != nil {
fmt.Printf("failed to read message from the connection: %v\n", err)
}
fmt.Printf("Received content from other side, %v\n", string(content))
// TODO: Handle sensitive data
...
}
השלבים ליצירת עומס העבודה USleep
- מריצים את הסקריפט create_usleep_workload.sh כדי ליצור את עומס העבודה של USleep. הסקריפט הזה:
- יוצר Artifact Registry (
$USLEEP_ARTIFACT_REPOSITORY) בבעלות UWear, שבו יפורסם עומס העבודה. - יוצר את הקוד usleep/workload.go ואורז אותו בקובץ אימג' של Docker. אפשר לעיין בהגדרות של Dockerfile עבור USleep.
- מפרסם את קובץ האימג' של Docker ב-Artifact Registry (
$USLEEP_ARTIFACT_REPOSITORY) שבבעלות UWear. - נותן לחשבון השירות
$USLEEP_WORKLOAD_SERVICE_ACCOUNTהרשאת קריאה ל-Artifact Registry ($USLEEP_ARTIFACT_REPOSITORY).
./create_usleep_workload.sh
- חשוב: ביומני הפלט, מחלצים את תקציר התמונה של USleep.
latest: digest: sha256:<USLEEP_IMAGE_DIGEST> size: 945
- מנווטים לספרייה UWear
cd ~/confidential-space/codelabs/health_data_analysis_codelab/src/uwear
- מחליפים את הערך בקטע allowed_submods_container_image_digest בקובץ opa_validation_values.json בערך USLEEP_IMAGE_DIGEST.
# Replace the image digest
sed -i 's/sha256:bc4c32cb2ca046ba07dcd964b07a320b7d0ca88a5cf8e979da15cae68a2103ee/sha256:<USLEEP_IMAGE_DIGEST>/' ~/confidential-space/codelabs/health_data_analysis_codelab/src/uwear/opa_validation_values.json
6. יצירת עומס עבודה של UWear
מידע על עומס העבודה של UWear
עומס העבודה של UWear כולל 4 חלקים עיקריים:
- הצטרפות לאותו סשן TLS שנוצר בעומס העבודה של USleep ואחזור אסימון האימות מ-USleep דרך סשן TLS מאובטח.
func main() {
fmt.Println("Initializing client...")
tlsconfig := &tls.Config{
// Skipping client verification of the server's certificate chain and host name since we are
// doing custom verification using the attestation token.
InsecureSkipVerify: true,
}
dialer := websocket.Dialer{
TLSClientConfig: tlsconfig,
HandshakeTimeout: 5 * time.Second,
}
ipAddress := os.Getenv(ipAddrEnvVar)
url := fmt.Sprintf("wss://%s:8081/connection", ipAddress)
fmt.Printf("Attempting to dial to url %v...\n", url)
conn, _, err := dialer.Dial(url, nil)
if err != nil {
fmt.Printf("Failed to dial to url %s, err %v\n", url, err)
return
}
defer conn.Close()
tokenString, ekm, err := retrieveTokenAndEKMFromConn(conn)
if err != nil {
fmt.Printf("Failed to retrieve token and EKM from connection: %v\n", err)
return
}
fmt.Printf("token: %v\n", tokenString)
...
}
- אימות הטוקן העצמאי על ידי:
- בדיקה של טענת x5c מכילה שרשרת אישורים שמתחילה באישור העלה, ממשיכה לאישור הביניים ומסתיימת באישור הבסיס.
- בודקים שהאסימון חתום על ידי אישור הקצה שכלול בהצהרת x5c.
- בודקים שהאישור הבסיסי שהורד או נשמר הוא אותו אישור בסיסי שמופיע בטענת x5c.
func main() {
...
token, err := validatePKIToken(tokenString)
if err != nil {
fmt.Printf("Failed to validate PKI token, err: %v\n.", err)
return
}
fmt.Println("PKI token validated successfully")
...
}
// validatePKIToken validates the PKI token returned from the attestation service.
// It verifies the token the certificate chain and that the token is signed by Google
// Returns a jwt.Token or returns an error if invalid.
func validatePKIToken(attestationToken string) (jwt.Token, error) {
// IMPORTANT: The attestation token should be considered untrusted until the certificate chain and
// the signature is verified.
rawRootCertificate, err := readFile(rootCertificateFile)
if err != nil {
return jwt.Token{}, fmt.Errorf("readFile(%v) - failed to read root certificate: %w", rootCertificateFile, err)
}
storedRootCert, err := decodeAndParsePEMCertificate(string(rawRootCertificate))
if err != nil {
return jwt.Token{}, fmt.Errorf("DecodeAndParsePEMCertificate(string) - failed to decode and parse root certificate: %w", err)
}
jwtHeaders, err := extractJWTHeaders(attestationToken)
if err != nil {
return jwt.Token{}, fmt.Errorf("ExtractJWTHeaders(token) - failed to extract JWT headers: %w", err)
}
if jwtHeaders["alg"] != "RS256" {
return jwt.Token{}, fmt.Errorf("ValidatePKIToken(attestationToken, ekm) - got Alg: %v, want: %v", jwtHeaders["alg"], "RS256")
}
// Additional Check: Validate the ALG in the header matches the certificate SPKI.
// https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.7
// This is included in Golang's jwt.Parse function
x5cHeaders := jwtHeaders["x5c"].([]any)
certificates, err := extractCertificatesFromX5CHeader(x5cHeaders)
if err != nil {
return jwt.Token{}, fmt.Errorf("ExtractCertificatesFromX5CHeader(x5cHeaders) returned error: %w", err)
}
// Verify the leaf certificate signature algorithm is an RSA key
if certificates.LeafCert.SignatureAlgorithm != x509.SHA256WithRSA {
return jwt.Token{}, fmt.Errorf("leaf certificate signature algorithm is not SHA256WithRSA")
}
// Verify the leaf certificate public key algorithm is RSA
if certificates.LeafCert.PublicKeyAlgorithm != x509.RSA {
return jwt.Token{}, fmt.Errorf("leaf certificate public key algorithm is not RSA")
}
// Verify the storedRootCertificate is the same as the root certificate returned in the token
// storedRootCertificate is downloaded from the confidential computing well known endpoint
// https://confidentialcomputing.googleapis.com/.well-known/attestation-pki-root
err = compareCertificates(*storedRootCert, *certificates.RootCert)
if err != nil {
return jwt.Token{}, fmt.Errorf("failed to verify certificate chain: %w", err)
}
err = verifyCertificateChain(certificates)
if err != nil {
return jwt.Token{}, fmt.Errorf("VerifyCertificateChain(CertificateChain) - error verifying x5c chain: %v", err)
}
keyFunc := func(token *jwt.Token) (any, error) {
return certificates.LeafCert.PublicKey, nil
}
verifiedJWT, err := jwt.Parse(attestationToken, keyFunc)
return *verifiedJWT, err
}
// verifyCertificateChain verifies the certificate chain from leaf to root.
// It also checks that all certificate lifetimes are valid.
func verifyCertificateChain(certificates CertificateChain) error {
// Additional check: Verify that all certificates in the cert chain are valid.
// Note: The *x509.Certificate Verify method in Golang already validates this but for other coding
// languages it is important to make sure the certificate lifetimes are checked.
if isCertificateLifetimeValid(certificates.LeafCert) {
return fmt.Errorf("leaf certificate is not valid")
}
if isCertificateLifetimeValid(certificates.IntermediateCert) {
return fmt.Errorf("intermediate certificate is not valid")
}
interPool := x509.NewCertPool()
interPool.AddCert(certificates.IntermediateCert)
if isCertificateLifetimeValid(certificates.RootCert) {
return fmt.Errorf("root certificate is not valid")
}
rootPool := x509.NewCertPool()
rootPool.AddCert(certificates.RootCert)
_, err := certificates.LeafCert.Verify(x509.VerifyOptions{
Intermediates: interPool,
Roots: rootPool,
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
})
if err != nil {
return fmt.Errorf("failed to verify certificate chain: %v", err)
}
return nil
}
- לאחר מכן, עומס העבודה של UWear יבדוק אם הטענות לגבי מדידת עומס העבודה באסימון תואמות לתנאי המאפיינים שצוינו במדיניות OPA. OPA הוא מנוע מדיניות בקוד פתוח לשימוש כללי, שמאחד את אכיפת המדיניות בכל הערימה. OPA משתמש במסמכים עם תחביר דומה ל-JSON כדי להגדיר ערכי בסיס שהמדיניות מאומתת מולם.
func main() {
...
err = validateClaimsAgainstOPAPolicy(token, ekm)
if err != nil {
fmt.Printf("Failed to validate claims against OPA policy: %v\n", err)
return
}
fmt.Println("Validated token and claims. Sending sensitive data")
...
}
// validateClaimsAgainstOPAPolicy validates the claims in the JWT token against the OPA policy.
func validateClaimsAgainstOPAPolicy(token jwt.Token, ekm string) error {
data, err := os.ReadFile("opa_validation_values.json")
authorized, err := evaluateOPAPolicy(context.Background(), token, ekm, string(data))
if err != nil {
fmt.Println("Error evaluating OPA policy:", err)
return fmt.Errorf("failed to evaluate OPA policy: %w", err)
}
if !authorized {
fmt.Println("Remote TEE's JWT failed policy check.")
return fmt.Errorf("remote TEE's JWT failed policy check")
}
fmt.Println("JWT is authorized.")
return nil
}
// evaluateOPAPolicy returns boolean indicating if OPA policy is satisfied or not, or error if occurred
func evaluateOPAPolicy(ctx context.Context, token jwt.Token, ekm string, policyData string) (bool, error) {
var claims jwt.MapClaims
var ok bool
if claims, ok = token.Claims.(jwt.MapClaims); !ok {
return false, fmt.Errorf("failed to get the claims from the JWT")
}
module := fmt.Sprintf(opaPolicy, ekm)
var json map[string]any
err := util.UnmarshalJSON([]byte(policyData), &json)
store := inmem.NewFromObject(json)
// Bind 'allow' to the value of the policy decision
// Bind 'hw_verified', 'image_verified', 'audience_verified, 'nonce_verified' to their respective policy evaluations
query, err := rego.New(
rego.Query(regoQuery), // Argument 1 (Query string)
rego.Store(store), // Argument 2 (Data store)
rego.Module("confidential_space.rego", module), // Argument 3 (Policy module)
).PrepareForEval(ctx)
if err != nil {
fmt.Printf("Error creating query: %v\n", err)
return false, err
}
fmt.Println("Performing OPA query evaluation...")
results, err := query.Eval(ctx, rego.EvalInput(claims))
if err != nil {
fmt.Printf("Error evaluating OPA policy: %v\n", err)
return false, err
} else if len(results) == 0 {
fmt.Println("Undefined result from evaluating OPA policy")
return false, err
} else if result, ok := results[0].Bindings["allow"].(bool); !ok {
fmt.Printf("Unexpected result type: %v\n", ok)
fmt.Printf("Result: %+v\n", result)
return false, err
}
fmt.Println("OPA policy evaluation completed.")
fmt.Println("OPA policy result values:")
for key, value := range results[0].Bindings {
fmt.Printf("[ %s ]: %v\n", key, value)
}
result := results[0].Bindings["allow"]
if result == true {
fmt.Println("Policy check PASSED")
return true, nil
}
fmt.Println("Policy check FAILED")
return false, nil
}
- דוגמה לערכי בסיס של OPA:
{
"allowed_submods_container_image_digest": [
"sha256:<USLEEP_IMAGE_DIGEST>"
],
"allowed_hwmodel": [
"GCP_INTEL_TDX",
"GCP_SHIELDED_VM",
"GCP_AMD_SEV_ES",
"GCP_AMD_SEV"
],
"allowed_aud": [
"uwear"
],
"allowed_issuer": [
"https://confidentialcomputing.googleapis.com"
],
"allowed_secboot": [
true
],
"allowed_sw_name": [
"CONFIDENTIAL_SPACE"
]
}
- דוגמה למדיניות OPA שנכתבה ב-Rego.
package confidential_space
import rego.v1
default allow := false
default hw_verified := false
default image_digest_verified := false
default audience_verified := false
default nonce_verified := false
default issuer_verified := false
default secboot_verified := false
default sw_name_verified := false
allow if {
hw_verified
image_digest_verified
audience_verified
nonce_verified
issuer_verified
secboot_verified
sw_name_verified
}
hw_verified if input.hwmodel in data.allowed_hwmodel
image_digest_verified if input.submods.container.image_digest in data.allowed_submods_container_image_digest
audience_verified if input.aud in data.allowed_aud
issuer_verified if input.iss in data.allowed_issuer
secboot_verified if input.secboot in data.allowed_secboot
sw_name_verified if input.swname in data.allowed_sw_name
nonce_verified if {
input.eat_nonce == "%s"
}
- שאילתת Rego לדוגמה.
regoQuery = "
allow = data.confidential_space.allow;
hw_verified = data.confidential_space.hw_verified;
image__digest_verified = data.confidential_space.image_digest_verified;
audience_verified = data.confidential_space.audience_verified;
nonce_verified = data.confidential_space.nonce_verified;
issuer_verified = data.confidential_space.issuer_verified;
secboot_verified = data.confidential_space.secboot_verified;
sw_name_verified = data.confidential_space.sw_name_verified
"
- במהלך האימות של OPA, עומס העבודה של UWear מאמת גם שהערך החד-פעמי תואם לערך החד-פעמי הצפוי (nonce) (TLS Exported Keying Material – EKM). ה-nonce מאומת במדיניות OPA באמצעות EKM שמועבר למעריך המדיניות.
func getEKMHashFromConn(c *websocket.Conn) (string, error) {
conn, ok := c.NetConn().(*tls.Conn)
if !ok {
return "", fmt.Errorf("failed to cast NetConn to *tls.Conn")
}
state := conn.ConnectionState()
ekm, err := state.ExportKeyingMaterial("testing_nonce", nil, 32)
if err != nil {
return "", fmt.Errorf("failed to get EKM from TLS connection: %w", err)
}
sha := sha256.New()
sha.Write(ekm)
hash := base64.StdEncoding.EncodeToString(sha.Sum(nil))
return hash, nil
}
- אחרי שכל הבדיקות האלה יושלמו ויעברו בהצלחה, UWear תוכל לאשר שהנתונים יישלחו ויעברו עיבוד בצורה מאובטחת. אחרי כן, UWear יגיב עם נתוני ה-PHI הרגישים באותו סשן TLS, ו-USleep יוכל להשתמש בנתונים האלה כדי לחשב את איכות השינה של הלקוח.
func main() {
...
fmt.Println("Validated token and claims. Sending sensitive data")
data, err := readFile(mySensitiveDataFile)
if err != nil {
fmt.Printf("Failed to read data from the file: %v\n", err)
}
conn.WriteMessage(websocket.BinaryMessage, data)
fmt.Println("Sent payload. Closing the connection")
conn.Close()
...
}
השלבים ליצירת עומס העבודה USleep
- ניווט לספריית הסקריפטים
cd ~/confidential-space/codelabs/health_data_analysis_codelab/scripts
- מריצים את הסקריפט create_uwear_workload.sh כדי ליצור את עומס העבודה של UWear:
- יוצר Artifact Registry (
$UWEAR_ARTIFACT_REPOSITORY) בבעלות UWear, שבו יפורסם עומס העבודה. - יוצר את הקוד uwear/workload.go ואורז אותו בקובץ אימג' של Docker. אפשר לעיין בהגדרות של Dockerfile עבור USleep.
- מפרסם את קובץ האימג' של Docker ב-Artifact Registry (
$UWEAR_ARTIFACT_REPOSITORY) שבבעלות UWear. - נותן לחשבון השירות
$UWEAR_WORKLOAD_SERVICE_ACCOUNTהרשאת קריאה ל-Artifact Registry ($UWEAR_ARTIFACT_REPOSITORY).
./create_uwear_workload.sh
7. הפעלת עומסי העבודה USleep ו-UWear
הרצת עומס עבודה של USleep
gcloud config set project $USLEEP_PROJECT_ID
gcloud compute instances create \
--confidential-compute-type=SEV \
--shielded-secure-boot \
--maintenance-policy=MIGRATE \
--scopes=cloud-platform --zone=${USLEEP_PROJECT_ZONE} \
--image-project=confidential-space-images \
--image-family=confidential-space \
--service-account=${USLEEP_WORKLOAD_SERVICE_ACCOUNT}@${USLEEP_PROJECT_ID}.iam.gserviceaccount.com \
--metadata ^~^tee-image-reference=${USLEEP_PROJECT_REPOSITORY_REGION}-docker.pkg.dev/${USLEEP_PROJECT_ID}/${USLEEP_ARTIFACT_REPOSITORY}/${USLEEP_WORKLOAD_IMAGE_NAME}:${USLEEP_WORKLOAD_IMAGE_TAG}~tee-restart-policy=Never~tee-container-log-redirect=true usleep
התגובה צריכה להחזיר STATUS: RUNNING ואת EXTERNAL_IP, כמו בדוגמה הבאה:
NAME: usleep
ZONE: us-west1-b
MACHINE_TYPE: n2d-standard-2
PREEMPTIBLE:
INTERNAL_IP: 10.138.0.6
EXTERNAL_IP: 34.168.56.10
STATUS: RUNNING
אחסון כתובת ה-IP החיצונית במשתנה
export USLEEP_EXTERNAL_IP=<add your external IP>
אימות של עומס העבודה של USleep
כדי לוודא שעומס העבודה של USleep פועל בצורה תקינה, עוברים אל הדף של המכונות הווירטואליות בפרויקט USleep. לוחצים על המופע usleep ועל Serial port 1(console) (יציאה טורית 1 (מסוף)) בקטע Logs (יומני רישום). אחרי שהשרת יפעל, בחלק התחתון של היומנים יופיעו נתונים דומים לאלה שמופיעים בהמשך.
2024/09/13 17:00:00 workload task started
#####----- Local IP Address is <YOUR-LOCAL-IP> -----#####
Starting Server..
הפעלת עומס עבודה של UWear
gcloud config set project $UWEAR_PROJECT_ID
gcloud compute instances create \
--confidential-compute-type=SEV \
--shielded-secure-boot \
--maintenance-policy=MIGRATE \
--scopes=cloud-platform --zone=${UWEAR_PROJECT_ZONE} \
--image-project=confidential-space-images \
--image-family=confidential-space \
--service-account=${UWEAR_WORKLOAD_SERVICE_ACCOUNT}@${UWEAR_PROJECT_ID}.iam.gserviceaccount.com \
--metadata ^~^tee-image-reference=${UWEAR_PROJECT_REPOSITORY_REGION}-docker.pkg.dev/${UWEAR_PROJECT_ID}/${UWEAR_ARTIFACT_REPOSITORY}/${UWEAR_WORKLOAD_IMAGE_NAME}:${UWEAR_WORKLOAD_IMAGE_TAG}~tee-restart-policy=Never~tee-container-log-redirect=true~tee-env-remote_ip_addr=$USLEEP_EXTERNAL_IP uwear
איך מוודאים שעומס העבודה של UWear פעל בצורה תקינה
כדי לראות את היומנים של עומס העבודה UWear, עוברים אל הדף של המכונות הווירטואליות בפרויקט UWear. לוחצים על המופע uwear ועל Serial port 1(console) (יציאה טורית 1 (מסוף)) בקטע Logs (יומני רישום).
פלט היומן אחרי שהמופע יופעל במלואו אמור להיראות כך
בפרויקט UWear, ביומני הנתונים הסידוריים אמור להופיע משהו דומה ל
token: eyJ[...]MrXUg
PKI token validated successfully
Performing OPA query evaluation...
OPA policy evaluation completed.
OPA policy result values:
[ hw_verified ]: true
[ image__digest_verified ]: true
[ audience_verified ]: true
[ nonce_verified ]: true
[ issuer_verified ]: true
[ secboot_verified ]: true
[ sw_name_verified ]: true
[ allow ]: true
Policy check PASSED
JWT is authorized.
Validated token and claims. Sending sensitive data
Sent payload. Closing the connection
אם עומס העבודה של UWear לא נראה כך, אפשר לעיין בהערות שלמטה כדי לקבל הוראות.
צפייה בתוצאות של USleep
כדי לראות את התוצאות, חוזרים אל דף המכונות הווירטואליות בפרויקט USleep. לוחצים על המופע usleep ועל Serial port 1(console) (יציאה טורית 1 (מסוף)) בקטע Logs (יומני רישום). תוכלו לראות את התוצאות של עומס העבודה בתחתית היומנים. הם צריכים להיראות כמו בדוגמה שלמטה.
Token from the attestation service: eyJhbGci...Ii5A3CJBuDM2o5Q
Received content from other side, {
"name": "Amy",
"age": 29,
"sleep": {
"light": {
"minutes": 270
},
"deep": {
"minutes": 135
},
"rem": {
"minutes": 105
}
}
}
Sleep quality result: total sleep time is less than 8 hours
התוצאה צריכה להיות "total sleep time is less than 8 hours".
ברכות, יצרתם בהצלחה מרחב סודי בין UWear ל-USleep כדי לשתף מידע רגיש.
8. (אופציונלי) הפעלת עומס עבודה לא מורשה
בתרחיש הבא, USleep מעדכן את הקוד ומריץ עומס עבודה שונה על נתוני השינה שסופקו על ידי UWear. חברת UWear לא הסכימה לעומס העבודה החדש הזה ולא עדכנה את מדיניות ה-OPA שלה כדי לאפשר את תקציר התמונה החדש. אנחנו נוודא ש-UWear לא ישלח את המידע הרגיש שלו לעומס העבודה הלא מורשה.
USleep משנה את עומס העבודה
- מגדירים את הפרויקט לערך $USLEEP_PROJECT_ID.
gcloud config set project $USLEEP_PROJECT_ID
- מוחקים את מופע ה-VM של USleep.
gcloud compute instances delete usleep --zone $USLEEP_PROJECT_ZONE
- עוברים לספרייה usleep/workload.go.
cd ~/confidential-space/codelabs/health_data_analysis_codelab/src/usleep
- בקובץ usleep/workload.go. מעדכנים את השורה
"audience": "uwear".בדוגמה הזו, כדי לשנות את הגיבוב של התמונה, נעדכן את קהל היעד לערך אחר שלא אושר על ידי UWear. לכן, מערכת UWear צריכה לדחות את הבקשה משתי סיבות – תקציר תמונה לא מאושר וקהל לא מתאים.
"audience": "anotherCompany.com",
- יצירת עומס עבודה חדש של USleep
cd ~/confidential-space/codelabs/health_data_analysis_codelab/scripts
./create_usleep_workload.sh
- יצירת מופע חדש של מכונה וירטואלית של USleep והרצת עומס העבודה
gcloud compute instances create \
--confidential-compute-type=SEV \
--shielded-secure-boot \
--maintenance-policy=MIGRATE \
--scopes=cloud-platform --zone=${USLEEP_PROJECT_ZONE} \
--image-project=confidential-space-images \
--image-family=confidential-space \
--service-account=${USLEEP_WORKLOAD_SERVICE_ACCOUNT}@${USLEEP_PROJECT_ID}.iam.gserviceaccount.com \
--metadata ^~^tee-image-reference=${USLEEP_PROJECT_REPOSITORY_REGION}-docker.pkg.dev/${USLEEP_PROJECT_ID}/${USLEEP_ARTIFACT_REPOSITORY}/${USLEEP_WORKLOAD_IMAGE_NAME}:${USLEEP_WORKLOAD_IMAGE_TAG}~tee-restart-policy=Never~tee-container-log-redirect=true usleep
- צריך לחלץ את כתובת ה-IP החיצונית החדשה של USleep לשימוש במועד מאוחר יותר.
export USLEEP_EXTERNAL_IP=<add your external IP>
הפעלה מחדש של עומס העבודה
- מחיקת מופע ה-VM של UWear
gcloud config set project $UWEAR_PROJECT_ID
gcloud compute instances delete uwear --zone $UWEAR_PROJECT_ZONE
- יוצרים מחדש את המכונה הווירטואלית UWear באמצעות כתובת ה-IP החיצונית החדשה
gcloud compute instances create \
--confidential-compute-type=SEV \
--shielded-secure-boot \
--maintenance-policy=MIGRATE \
--scopes=cloud-platform --zone=${UWEAR_PROJECT_ZONE} \
--image-project=confidential-space-images \
--image-family=confidential-space \
--service-account=${UWEAR_WORKLOAD_SERVICE_ACCOUNT}@${UWEAR_PROJECT_ID}.iam.gserviceaccount.com \
--metadata ^~^tee-image-reference=${UWEAR_PROJECT_REPOSITORY_REGION}-docker.pkg.dev/${UWEAR_PROJECT_ID}/${UWEAR_ARTIFACT_REPOSITORY}/${UWEAR_WORKLOAD_IMAGE_NAME}:${UWEAR_WORKLOAD_IMAGE_TAG}~tee-restart-policy=Never~tee-container-log-redirect=true~tee-env-remote_ip_addr=$USLEEP_EXTERNAL_IP uwear
- במסמכי היומן הטוריים של UWear צריכה להופיע ההודעה הבאה, והמכונה הווירטואלית של USleep לא צריכה לקבל נתונים רגישים
OPA policy result values:
[ nonce_verified ]: true
[ issuer_verified ]: true
[ secboot_verified ]: true
[ sw_name_verified ]: true
[ allow ]: false
[ hw_verified ]: true
[ image__digest_verified ]: false
[ audience_verified ]: false
Policy check FAILED
Remote TEE's JWT failed policy check.
Failed to validate claims against OPA policy: remote TEE's JWT failed policy check
9. הסרת המשאבים
אפשר להשתמש בסקריפט הניקוי כדי לנקות את המשאבים שיצרנו כחלק מה-Codelab הזה. כחלק מהניקוי הזה, המשאבים הבאים יימחקו:
- חשבון השירות של UWear (
$UWEAR_SERVICE_ACCOUNT). - מאגר הארטיפקטים של UWear (
$UWEAR_ARTIFACT_REPOSITORY). - מכונה של UWear
- חשבון השירות של USleep (
$USLEEP_SERVICE_ACCOUNT). - מאגר הארטיפקטים של USleep (
$USLEEP_ARTIFACT_REPOSITORY). - מכונת Compute של USleep
./cleanup.sh
אם סיימתם את הבדיקה, מומלץ למחוק את הפרויקט לפי ההוראות האלה.
מזל טוב
כל הכבוד, סיימתם את ה-Codelab!
למדתם איך לשתף נתונים באופן מאובטח תוך שמירה על הסודיות שלהם באמצעות Confidential Space.
מה השלב הבא?
כדאי לעיין במדריכי Codelab דומים נוספים…
- אבטחת מודלים של למידת מכונה וקניין רוחני באמצעות Confidential Space
- איך מבצעים עסקאות בנכסים דיגיטליים באמצעות חישוב רב-צדדי ומרחבים סודיים
- ניתוח נתונים סודיים באמצעות Confidential spaces