שימוש ב-Confidential Space עם משאבים מוגנים שלא מאוחסנים אצל ספק ענן

1. סקירה כללית

Confidential Space מאפשר שיתוף נתונים מאובטח בין מספר צדדים ועבודה משותפת, תוך שמירה על סודיות הנתונים של הארגון. כלומר, ארגונים יכולים לשתף פעולה ביניהם ועדיין לשמור על השליטה בנתונים שלהם ולהגן עליהם מפני גישה בלתי מורשית.

בעזרת Confidential Space אפשר ליצור תרחישים שבהם רוצים להפיק ערך הדדי מהצטברות וניתוח של נתונים רגישים, לרוב כאלה שמוגדרים ברגולציה, תוך שמירה על שליטה מלאה בהם. בעזרת Confidential Space, ארגונים יכולים ליהנות מהערך המשותף של צבירת נתונים רגישים כמו פרטים אישיים מזהים (PII), מידע רפואי מוגן (PHI), קניין רוחני וסודות קריפטוגרפיים, תוך שמירה על שליטה מלאה בהם.

מה צריך להכין

מה תלמדו

  • איך מגדירים את משאבי Cloud הנדרשים להרצת Confidential Space
  • איך מפעילים עומס עבודה במכונה וירטואלית סודית (Confidential VM) שמריצה את התמונה של Confidential Space
  • איך להעניק הרשאת גישה למשאבים מוגנים על סמך המאפיינים של קוד עומס העבודה (מה), הסביבה של Confidential Space (איפה) והחשבון שבו פועל עומס העבודה (מי).

בקודלאב הזה נסביר איך להשתמש ב-Confidential Space עם משאבים מוגנים שמתארחים במקום אחר מאשר Google Cloud. במאמר הזה תלמדו איך לבקש אסימון מותאם אישית ועצמאי מ-Google Attestation Service, על ידי ציון חד-פעמי, קהל וסוג אסימון PKI.

בסדנת הקוד הזו תלמדו להגדיר מרחב סודי בין מוצר בדיוני – USleep, אפליקציה בקונטיינרים – לבין מוצר בדיוני – UWear, מכשיר לבישה מחובר – כדי לחשב את איכות השינה שלכם. UWear ישתף מידע רפואי מוגן (PHI) עם USleep בסביבה בטוחה, מאובטחת ומבודדת (נקראת גם Trusted Execution Environment או TEE), כך שבעלי הנתונים יוכלו לשמור על סודיות מלאה.

UWear הוא גם מבקר עומסי העבודה וגם בעל הנתונים. בתור מבקר עומסי עבודה,הוא בודק את הקוד של עומס העבודה שפועל ומתעדה את הסיכום של התמונה. כבעלי הנתונים, UWear כותבים את לוגיקת האימות כדי לבדוק את התוקף של האסימון ואת החתימה שלו. המערכת כותבת מדיניות אימות, באמצעות סיכום התמונה של עומסי העבודה שנבדקו, שמאפשרת רק לסיכום התמונה הספציפי, בסביבה ספציפית, לקבל גישה לנתונים הרגישים.

ב-codelab הזה, USleep פורסת את האפליקציה בקונטיינרים. ל-USleep אין גישה למידע הרגיש, אבל הוא מפעיל את עומס העבודה המאושר שיש לו גישה למידע הרגיש.

הקודלה כוללת את השלבים הבאים:

  • שלב 1: מגדירים את המשאבים הנדרשים בענן ל-codelab. הגדרת פרויקטים, חיוב והרשאות. מורידים את קוד המקור של codelab ומגדירים משתני סביבה.
  • שלב 2: מורידים את אישור הבסיס ושומרים אותו עם קוד המקור של UWear.
  • שלב 3: יוצרים חשבונות שירות נפרדים של כוח עבודה, שבהם ה-VM של כוח העבודה ישתמש ל-USleep ול-UWear.
  • שלב 4: יוצרים את עומס העבודה USleep שמספק אסימון אימות (attestation).
  • שלב 5: יוצרים את עומס העבודה של UWear שמאמת את אסימון האימות ושולח את הנתונים הרגישים אם האסימון מאושר.
  • שלב 6: מריצים את עומסי העבודה USleep ו-UWear. UWear יספק את המידע הרגיש, ו-USleep ירוץ אלגוריתם שינה על הנתונים ויפיק תוצאה.
  • שלב 7: (אופציונלי) מריצים עומס עבודה לא מורשה של USleep ומוודאים שלא התקבל מידע רגיש מ-UWear.
  • שלב 8: מוחקים את כל המשאבים.

הסבר על תהליך העבודה

USleep ירוץ את עומס העבודה ב-Confidential Space. כדי להפעיל את עומס העבודה, צריכה להיות לו גישה למידע הרפואי האישי (PHI) של UWear. כדי לקבל גישה, עומס העבודה USleep יוצר קודם סשן TLS מאובטח. לאחר מכן, USleep יבקש גם אסימון אימות משירות האימות של Google עם מטען שימושי.

USleep יבקש אסימון אימות עם מטען ייעודי (payload) של JSON שיכיל שלושה דברים:

  1. אסימון אימות שמקושר לסשן ה-TLS. כדי לקשר את אסימון האימות לסשן ה-TLS, ערך ה-nonce יהיה הגיבוב של חומר מפתח שיוצא ל-TLS. קישור האסימון לסשן ה-TLS מבטיח שלא מתרחשות מתקפות אדם בתווך, כי רק שני הצדדים המעורבים בסשן ה-TLS יוכלו ליצור את הערך של המזהה החד-פעמי.
  2. המערכת תיצור קהל בשם 'uwear'. UWear יאמת שהוא הקהל המיועד לאסימון האימות.
  3. סוג אסימון 'PKI'. אם סוג האסימון הוא 'PKI', המשמעות היא ש-USleep רוצה לבקש אסימון עצמאי. אפשר לאמת שהאסימון העצמאי נחתם על ידי Google באמצעות ה-root שהורדתם מנקודת הקצה הידועה של PKI ב-Confidential Space. בניגוד לסוג ברירת המחדל של אסימון OIDC, שבו החתימה מאומתת באמצעות מפתח ציבורי שמתבצעת בו רוטציה באופן קבוע.

bb013916a3222ce7.png

עומס העבודה USleep מקבל את אסימון האימות. לאחר מכן, UWear מצטרף לחיבור ה-TLS עם USleep ומאחזר את אסימון האימות של USleep. UWear יאמת את האסימון על ידי בדיקת הצהרת x5c באישור הבסיס.

צוות UWear יאשר את עומס העבודה USleep אם:

  1. האסימון עובר את הלוגיקת האימות של PKI.
  2. UWear יאמת את האסימון על ידי בדיקת הצהרת ה-x5c מול אישור הבסיס, בדיקה שהאסימון נחתם על ידי אישור העלה ולבסוף בדיקה שאישור הבסיס שהורדתם הוא אותו אישור בסיס שמופיע בהצהרת ה-x5c.
  3. ההצהרות על מדידת עומסי העבודה באסימון תואמות לתנאי המאפיינים שצוינו במדיניות OPA. OPA הוא מנוע מדיניות בקוד פתוח למטרות כלליות, שמאחד את אכיפת המדיניות בסטאק. מערכת OPA משתמשת במסמכים עם תחביר דומה ל-JSON כדי להגדיר ערכים בסיסיים שביחס אליהם מתבצע אימות המדיניות. ערכים בסיסיים של OPA – דוגמה לערכים שהמדיניות בודקת.
  4. המזהה החד-פעמי תואם למזהה החד-פעמי הצפוי (חומר מפתח מיוצא של TLS). הדבר מאומת במדיניות OPA שלמעלה.

אחרי שכל הבדיקות האלה יושלמו ויעברו, UWear יוכל לאשר שהנתונים יישלחו ויעובדו באופן מאובטח. לאחר מכן, UWear תשיב עם פרטי ה-PHI הרגישים באותו סשן TLS, ו-USleep תוכל להשתמש בנתונים האלה כדי לחשב את איכות השינה של הלקוח.

2. הגדרת משאבי Cloud

לפני שמתחילים

  1. מגדירים שני פרויקטים ב-Google Cloud, אחד ל-USleep ואחד ל-UWear. מידע נוסף על יצירת פרויקט ב-Google Cloud זמין בcodelab 'הגדרה וניווט בפרויקט Google הראשון'. במאמר יצירה וניהול של פרויקטים מוסבר איך לאחזר את מזהה הפרויקט ואיך הוא שונה משם הפרויקט וממספר הפרויקט.
  2. מפעילים את החיוב בפרויקטים.
  3. באחד ממכונות Cloud Shell של הפרויקט ב-Google, מגדירים את משתני הסביבה הנדרשים של הפרויקט כפי שמתואר בהמשך.
export UWEAR_PROJECT_ID=<Google Cloud project id of UWear>
export USLEEP_PROJECT_ID=<Google Cloud project id of USleep>
  1. מפעילים את 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
  1. אחזור המזהה של חשבון המשתמש באמצעות
gcloud auth list

# Output should contain
# ACCOUNT: <Principal Identifier>

# Set your member variable
export MEMBER='user:<Principal Identifier>'
  1. מוסיפים הרשאות לשני הפרויקטים האלה. כדי להוסיף הרשאות, פועלים לפי הפרטים שמפורטים בדף האינטרנט הקצאת תפקיד IAM.
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'
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'
  1. באחד מהפרויקטים שלכם ב-Google Cloud, משכפלים את מאגר ה-GitHub של Confidential Space Codelab באמצעות הפקודה הבאה כדי לקבל את הסקריפטים הנדרשים שנעשה בהם שימוש במסגרת הקודלה הזו.
git clone https://github.com/GoogleCloudPlatform/confidential-space.git
  1. עוברים לספריית הסקריפטים של Codelab בנושא נתוני בריאות.
cd confidential-space/codelabs/health_data_analysis_codelab/scripts
  1. מעדכנים את שתי השורות האלה בסקריפט 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
  1. אופציונלי: מגדירים משתנים קיימים. אפשר לשנות את שמות המשאבים באמצעות המשתנים האלה (למשל export UWEAR_ARTIFACT_REPOSITORY='my-artifact-repository')
  • אפשר להגדיר את המשתנים הבאים עם שמות של משאבים קיימים בענן. אם המשתנה מוגדר, המערכת תשתמש במשאב הענן הקיים המתאים מהפרויקט. אם המשתנה לא מוגדר, שם המשאב בענן ייווצר מהערכים שבסקריפט config_env.sh.
  1. מריצים את הסקריפט 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. מורידים את אישור הבסיס.

  1. כדי לאמת את האסימון המכיל את עצמו שמוחזר משירות האימות, UWear צריך לאמת את החתימה מול אישור הבסיס של Confidential Space. 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
  1. יצירת טביעת האצבע של אישור הבסיס שהורדתם
openssl x509 -fingerprint -in confidential_space_root.pem -noout
  1. מוודאים שטביעת האצבע תואמת ל-digest 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 מורכב משלושה חלקים עיקריים:

  1. הגדרת סשן TLS וחילוץ חומר מפתחות שיוצא
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
}
  1. שליחת בקשה לאסימון משירות האימות עם קהל, אסימון חד-פעמי וסוג אסימון 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
}
  1. קבלת המידע הרגיש וחישוב איכות השינה של המשתמש
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

  1. מריצים את הסקריפט 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
  1. חשוב: ביומני הפלט, מחלצים את תקציר התמונה של USleep.
latest: digest: sha256:<USLEEP_IMAGE_DIGEST> size: 945
  1. מנווטים לספרייה UWear
cd ~/confidential-space/codelabs/health_data_analysis_codelab/src/uwear
  1. מחליפים את הערך בקטע '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 Workload

עומס העבודה של UWear מורכב מ-4 חלקים עיקריים:

  1. הצטרפות לאותו סשן 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)

  ...
}
  1. אימות האסימון המכיל את כל הפרטים באמצעות:
  • בודקים שההצהרה 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
}
  1. לאחר מכן, עומס העבודה של 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
}
{
  "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"
  ]
}
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
"

קוד לדוגמה לקבלת גיבוב 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
}
  1. אחרי שכל הבדיקות האלה יושלמו ויעברו, 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

  1. מנווטים לספריית הסקריפטים
cd ~/confidential-space/codelabs/health_data_analysis_codelab/scripts
  1. מריצים את הסקריפט 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) בקטע 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) בקטע 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) בקטע 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 משנים את עומס העבודה שלהם

  1. מגדירים את הפרויקט כ-‎ $USLEEP_PROJECT_ID.
gcloud config set project $USLEEP_PROJECT_ID
  1. מוחקים את מכונת ה-VM של USleep.
gcloud compute instances delete usleep --zone $USLEEP_PROJECT_ZONE
  1. עוברים לספרייה usleep/workload.go.
cd ~/confidential-space/codelabs/health_data_analysis_codelab/src/usleep
  1. בקובץ usleep/workload.go. מעדכנים את השורה "audience": "uwear".. בדוגמה הזו, כדי לשנות את הסיכום של התמונה נעדכן את הקהל לערך אחר ש-UWear לא אישר. לכן, UWear צריך לדחות אותו משתי סיבות – תקציר תמונות לא מאושר וקהל שגוי.
"audience": "anotherCompany.com",
  1. יצירת עומס עבודה חדש מסוג USleep
cd ~/confidential-space/codelabs/health_data_analysis_codelab/scripts

./create_usleep_workload.sh
  1. יצירת המכונה הווירטואלית החדשה של 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
  1. חילוץ כתובת ה-IP החיצונית החדשה של USleep לשימוש מאוחר יותר
export USLEEP_EXTERNAL_IP=<add your external IP>

הפעלה מחדש של עומס העבודה

  1. מחיקת מכונת ה-VM של UWear
gcloud config set project $UWEAR_PROJECT_ID

gcloud compute instances delete uwear --zone $UWEAR_PROJECT_ZONE
  1. יוצרים מחדש את המכונה הווירטואלית של 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
  1. ההודעה הבאה אמורה להופיע ביומני הטורי של 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 Compute
  • חשבון השירות USleep‏ ($USLEEP_SERVICE_ACCOUNT).
  • מאגר הארטיפקטים של USleep‏ ($USLEEP_ARTIFACT_REPOSITORY).
  • מכונה של Compute ב-USleep
./cleanup.sh

אחרי שתסיימו לבדוק את האפשרויות, תוכלו למחוק את הפרויקט לפי ההוראות האלה.

מזל טוב

מזל טוב, סיימת את הקודלהב!

למדתם איך לשתף נתונים באופן מאובטח תוך שמירה על הסודיות שלהם באמצעות Confidential Space.

מה השלב הבא?

כדאי לעיין בחלק מהמדריכים הבאים בנושא Codelab…

מקורות מידע נוספים