استخدام "المساحة السرية" مع موارد محمية غير مخزّنة لدى مقدّم خدمات السحابة الإلكترونية

1. نظرة عامة

توفّر المساحة السرية مشاركة البيانات والتعاون الآمنَين بين جهات متعددة، مع السماح للمؤسسات بالحفاظ على سرية بياناتها. وهذا يعني أنّه يمكن للمؤسسات التعاون مع بعضها البعض مع الحفاظ على التحكّم في بياناتها وحمايتها من الوصول غير المصرّح به.

تتيح لك "مساحة العمل السرية" الاستفادة من سيناريوهات تريد فيها تحقيق قيمة متبادلة من تجميع البيانات الحسّاسة التي تخضع للوائح التنظيمية في أغلب الأحيان وتحليلها، مع الاحتفاظ بالسيطرة الكاملة عليها. باستخدام "المساحة السرية"، يمكن للمؤسسات تحقيق قيمة متبادلة من خلال تجميع البيانات الحسّاسة وتحليلها، مثل معلومات تحديد الهوية الشخصية (PII) والمعلومات الصحية المحمية (PHI) والملكية الفكرية والأسرار التشفيرية، مع الاحتفاظ بالسيطرة الكاملة عليها.

المتطلبات

ما ستتعرّف عليه

  • كيفية ضبط موارد Cloud اللازمة لتشغيل "المساحة السرية"
  • كيفية تشغيل حمولة في جهاز افتراضي سري يعمل بنظام التشغيل Confidential Space
  • كيفية تفويض الوصول إلى الموارد المحمية استنادًا إلى سمات رمز "حملات العمل" (ما الذي) وبيئة "مساحة العمل السرية" (أين) والحساب الذي يشغّل "حملات العمل" (من)

تركّز ورشة رموز البرامج هذه على كيفية استخدام Confidential Space مع الموارد المحمية التي يتم استضافتها في مكان آخر غير Google Cloud. ستتعرّف على كيفية طلب رمز مميّز مُخصّص من خدمة إثبات الهوية من Google من خلال تقديم مفتاح متغيّر وشريحة جمهور ونوع رمز مميّز لبنية التحكّم في المفاتيح العامة (PKI).

في هذا الدرس التطبيقي حول الترميز، ستُنشئ "مساحة سرية" بين منتج خيالي، وهو تطبيق مُحاوي يُسمى USleep، ومنتج خيالي آخر، وهو جهاز قابل للارتداء متصل يُسمى UWear، وذلك لاحتساب جودة نومك. ستشارك UWear المعلومات الصحية المحمية (PHI) مع USleep في بيئة آمنة ومعزولة (المعروفة أيضًا باسم بيئة التنفيذ الموثوق أو TEE) حتى يحتفظ مالكو البيانات بسرية تامة.

ويكون UWear هو مدقّق حمولة العمل وصاحب البيانات. وبصفته مدقّقًا للحمولة، يراجع الرمز البرمجي في الحمولة التي يتم تشغيلها ويدوّن خلاصة الصورة. بصفتها مالك البيانات، تكتب UWear منطق التحقّق للتحقّق من صلاحية الرمز المميّز وتوقيعه. ويُنشئ سياسة التحقّق باستخدام خلاصة صورة عمليات العمل التي تمّت التدقيق فيها، والتي لا تسمح إلا لخلاصة الصورة المحدّدة في بيئة معيّنة بالوصول إلى البيانات الحسّاسة.

في هذا الدرس التطبيقي، ينشر فريق USleep التطبيق المُنشئ في حاوية. لا يمكن لتطبيق USleep الوصول إلى البيانات الحسّاسة، ولكنه يشغّل أعباء العمل الموافَق عليها التي يُسمح لها بالوصول إلى البيانات الحسّاسة.

يتضمّن الدرس التطبيقي حول الترميز الخطوات التالية:

  • الخطوة 1: إعداد موارد السحابة الإلكترونية اللازمة لجلسة التعلّم البرمجي إعداد المشاريع والفوترة والأذونات نزِّل رمز المصدر في ورشة التعلم البرمجي واضبط متغيّرات البيئة.
  • الخطوة 2: نزِّل شهادة الجذر واحفظها مع رمز UWear المصدر.
  • الخطوة 3: إنشاء حسابات خدمة منفصلة لوحدة العمل ستستخدمها آلة افتراضية لوحدة العمل في USleep وUWear
  • الخطوة 4: أنشئ معالجة USleep التي توفّر رمزًا مميّزًا لإثبات الهوية.
  • الخطوة 5: أنشئ "وحدة عمل UWear" التي تتحقّق من صحة الرمز المميّز لإثبات الهوية وتُرسِل البيانات الحسّاسة في حال الموافقة على الرمز المميّز.
  • الخطوة 6: تشغيل أحمال العمل USleep وUWear يوفّر UWear البيانات الحسّاسة، ويعمل USleep على تنفيذ خوارزمية النوم على البيانات وعرض النتيجة.
  • الخطوة 7: (اختيارية) تشغيل "وحدة عمل USleep" غير مصرَّح بها والتأكّد من عدم استلام بيانات حسّاسة من UWear
  • الخطوة 8: تنظيف جميع الموارد

فهم سير العمل

سيشغّل USleep حجم العمل في "مساحة سرية". لتشغيل عبء العمل، يجب أن يحصل على إذن الوصول إلى معلومات UWear الصحية الشخصية. للوصول إلى البيانات، تنشئ "وحدة عمل USleep" أولاً جلسة بروتوكول أمان طبقة النقل (TLS) آمنة. سيطلب تطبيق USleep بعد ذلك رمزًا مميّزًا لإثبات الهوية من "خدمة إثبات الهوية من Google" مع حمولة.

سيطلب تطبيق USleep رمز شهادة اعتماد يحتوي على حمولة JSON تتضمّن ثلاثة عناصر:

  1. رمز مصادقة مرتبط بجلسة بروتوكول أمان طبقة النقل (TLS) لربط الرمز المميّز لإثبات الهوية بجلسة بروتوكول أمان طبقة النقل (TLS)، ستكون قيمة المفتاح العشوائي هي التجزئة لمواد التشفير التي تم تصديرها في بروتوكول أمان طبقة النقل (TLS). يضمن ربط الرمز المميّز بجلسة TLS عدم حدوث هجمات من خلال جهاز وسيط، لأنّ الطرفَين المعنيّين بجلسة TLS فقط هما اللذان يمكنهما إنشاء قيمة الرمز المؤقت.
  2. سيتم توفير شريحة جمهور "uwear". سيتحقّق UWear من أنّه الجمهور المستهدَف لرمز الإقرار.
  3. نوع الرمز المميّز هو "PKI". إذا كان نوع الرمز المميّز هو "PKI"، يعني ذلك أنّ USleep تريد طلب رمز مميّز متكامل. يمكن التحقّق من أنّ الرمز المميّز المُضمّن موقَّع من Google باستخدام الجذر الذي تم تنزيله من نقطة نهاية PKI المعروفة في "المساحة المُفصَّلة". يختلف ذلك عن نوع الرمز المميّز التلقائي لبروتوكول 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. يتطابق المفتاح العشوائي مع المفتاح العشوائي المتوقّع (مادة التشفير المصدَّرة في بروتوكول أمان طبقة النقل). تم التحقّق من ذلك في سياسة OPA أعلاه.

بعد اكتمال جميع عمليات التحقّق واجتيازها، يمكن لفريق UWear تأكيد أنّه سيتم إرسال البيانات ومعالجتها بأمان. سيردّ UWear بعد ذلك ببيانات PHI الحسّاسة خلال جلسة TLS نفسها، وسيكون بإمكان USleep استخدام هذه البيانات لاحتساب جودة نوم العميل.

2. إعداد موارد السحابة الإلكترونية

قبل البدء

  1. إعداد مشروعَين على Google Cloud، أحدهما لتطبيق USleep والآخر لتطبيق UWear لمزيد من المعلومات عن إنشاء مشروع على Google Cloud، يُرجى الرجوع إلى "إعداد مشروعك الأول على Google والتنقّل فيه" (Set up and navigate your first Google project). يمكنك الرجوع إلى مقالة إنشاء المشاريع وإدارتها للحصول على تفاصيل عن كيفية استرداد رقم تعريف المشروع واختلافه عن اسم المشروع ورقمه.
  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 وواجهات برمجة التطبيقات التالية لكلٍّ من المشروعَين.
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. أضِف أذونات لهذين المشروعَين. يمكن إضافة الأذونات باتّباع التفاصيل الواردة في صفحة الويب الخاصة بمنح دور إدارة الهوية وإمكانية الوصول.
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 في Cloud Shell، يمكنك استنساخ مستودع Github الخاص بـ "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. تأكَّد من أنّ بصمة الإصبع تتطابق مع الملخّص التالي لـ SHA-1:
B9:51:20:74:2C:24:E3:AA:34:04:2E:1C:3B:A3:AA:D2:8B:21:23:21

4. إنشاء حساب خدمة "وحدة العمل"

الآن، عليك إنشاء حسابَي خدمة، أحدهما لتحميل "النوم الذكي" والآخر لتحميل "الارتداء الذكي". شغِّل النص البرمجي 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 بالإضافة إلى وحدة تحكّم Serial Console، وبالتالي تصبح السجلات متاحة بعد إنهاء تشغيل الجهاز الظاهري.

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. طلب رمز مميّز من خدمة إثبات الهوية باستخدام شريحة جمهور وعدد عشوائي ونوع رمز مميّز لبنية التحكّم في المفاتيح العامة
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

تتضمّن "وحدة عمل 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
"

مثال على الرمز البرمجي للحصول على تجزئة مفتاح إدارة المفاتيح:

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" واضغط على "منفذ تسلسلي 1(وحدة التحكّم)" ضمن قسم "السجلّات". بعد تشغيل الخادم، من المفترض أن تظهر في أسفل السجلّات رسالة مشابهة لما يلي.

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 واضغط على "منفذ تسلسلي 1(وحدة التحكّم)" ضمن قسم "السجلّات".

يجب أن يظهر ناتج السجلّ بعد بدء تشغيل المثيل بالكامل على النحو التالي:

في مشروع 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" واضغط على "منفذ تسلسلي 1(وحدة التحكّم)" ضمن قسم "السجلّات". اطّلِع على نتائج "عبء العمل" في أسفل السجلّات. من المفترض أن تظهر بالشكل الموضّح أدناه.

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. حذف مثيل جهاز 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. حذف مثيل جهاز UWear الظاهري
gcloud config set project $UWEAR_PROJECT_ID

gcloud compute instances delete uwear --zone $UWEAR_PROJECT_ZONE
  1. إعادة إنشاء مثيل UWear VM باستخدام عنوان 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. تنظيف

يمكن استخدام البرنامج النصي للتنظيف لتنظيف الموارد التي أنشأناها كجزء من هذا الدرس التطبيقي حول الترميز. كجزء من عملية التنظيف هذه، سيتم حذف الموارد التالية:

  • حساب الخدمة UWear ($UWEAR_SERVICE_ACCOUNT).
  • سجلّ UWear للعناصر ($UWEAR_ARTIFACT_REPOSITORY)
  • مثيل UWear Compute
  • حساب الخدمة USleep‏ ($USLEEP_SERVICE_ACCOUNT).
  • سجلّ عناصر USleep ($USLEEP_ARTIFACT_REPOSITORY).
  • مثيل USleep Compute
./cleanup.sh

إذا انتهيت من الاستكشاف، يُرجى حذف مشروعك باتّباع هذه التعليمات.

تهانينا

تهانينا، لقد أكملت دورة codelab بنجاح.

لقد تعرّفت على كيفية مشاركة البيانات بأمان مع الحفاظ على سريتها باستخدام "مساحة سرية".

الخطوة التالية

اطّلِع على بعض هذه الدروس التطبيقية حول الترميز المشابهة...

مراجع إضافية