使用機密空間搭配未經保護的資源#39 (不會儲存在雲端服務供應商)

1. 總覽

Confidential Space 提供安全的多方資料分享和協作功能,同時讓機構保有資料的機密性。也就是說,機構可以彼此協同合作,同時維持資料的控管權,並防止資料遭到未經授權的存取。

在 Confidential Space 中,您可以匯總及分析機密資料 (通常受監管),同時保有資料的完全控制權,進而獲得雙贏的局面。有了 Confidential Space,機構就能匯總及分析機密資料來獲得價值,讓雙方都受益,同時保有資料的完全控制權,例如個人識別資訊 (PII)、受保護的健康資訊 (PHI)、智慧財產和加密密鑰。

軟硬體需求

課程內容

  • 如何設定執行機密空間所需的雲端資源
  • 如何在執行機密空間映像檔的機密 VM 中執行工作負載
  • 如何根據工作負載程式碼的屬性 (what)、機密空間環境 (where) 和執行工作負載的帳戶 (who),授權存取受保護的資源。

本程式碼研究室著重於如何使用機密空間,搭配託管於 Google Cloud 以外位置的受保護資源。您將瞭解如何透過提供 Nonce、目標對象和 PKI 權杖類型,向 Google Attestation Service 要求自訂的獨立權杖。

在本程式碼研究室中,您將在虛構產品 USleep (容器化應用程式) 和虛構產品 UWear (連線的可穿戴式裝置) 之間設定機密空間,以計算睡眠品質。UWear 會在安全、可信任且隔離的環境 (稱為「受信任的執行環境」或 TEE) 中,與 USleep 共用受保護的健康資訊 (PHI),讓資料擁有者保有完全的機密性。

UWear 同時是工作負載稽核人員資料擁有者工作負載稽核工具會審查正在執行的工作負載中的程式碼,並記下圖片摘要。作為資料擁有者,UWear 會編寫驗證邏輯,以便檢查權杖及其簽名的有效性。它會使用經稽核的工作負載映像檔摘要,寫入驗證政策,只允許特定映像檔摘要在特定環境中存取機密資料。

本程式碼研究室中的 USleep 會部署容器化應用程式。USleep 無法存取機密資料,但會執行經過核准的工作負載,而該工作負載可存取機密資料。

程式碼研究室包含下列步驟:

  • 步驟 1:為程式碼研究室設定必要的雲端資源。設定專案、帳單和權限。下載程式碼研究室原始碼並設定環境變數。
  • 步驟 2:下載根憑證,並與 UWear 原始碼一併儲存。
  • 步驟 3:建立工作負載 VM 將用於 USleep 和 UWear 的個別工作負載服務帳戶。
  • 步驟 4:建立提供認證權杖的 USleep 工作負載。
  • 步驟 5:建立 UWear 工作負載,驗證認證權杖,並在權杖獲得核准時傳送機密資料。
  • 步驟 6:執行 USleep 和 UWear 工作負載。UWear 會提供機密資料,而 USleep 會對資料執行睡眠演算法,並輸出結果。
  • 步驟 7:(選用) 執行未經授權的 USleep 工作負載,並確認未從 UWear 接收機密資料。
  • 步驟 8:清除所有資源。

瞭解工作流程

USleep 會在機密空間中執行工作負載。為了執行工作負載,該工作負載需要存取 UWear 的 PHI。為了取得存取權,USleep 工作負載會先建立安全的 TLS 工作階段。接著,USleep 也會向 Google Attestation Service 要求認證權杖,並附上酬載

USleep 會使用 JSON 酬載要求認證權杖,其中包含三項內容:

  1. 與 TLS 工作階段繫結的認證權杖。為了將認證權杖繫結至 TLS 工作階段,Nonce 值將是 TLS 匯出的鍵控素材的雜湊值。將權杖繫結至 TLS 工作階段,可確保不會發生中間人攻擊,因為只有參與 TLS 工作階段的兩方才能產生 Nonce 值。
  2. 系統會提供「uwear」目標對象。UWear 會驗證這是認證權杖的預期目標對象。
  3. 符記類型為「PKI」權杖類型為「PKI」表示 USleep 想要求自包含權杖。您可以使用從 Confidential Space 的已知 PKI 端點下載的根目錄,驗證自行包含的權杖是否由 Google 簽署。這與預設 OIDC 權杖類型形成對比,後者會使用定期輪替的公開金鑰驗證簽名。

bb013916a3222ce7.png

USleep 工作負載會收到認證權杖。接著,UWear 會與 USleep 建立 TLS 連線,並擷取 USleep 的認證權杖。UWear 會根據根憑證檢查 x5c 聲明,驗證權杖。

在下列情況下,UWear 會核准 USleep 工作負載:

  1. 權杖通過 PKI 驗證邏輯
  2. UWear 會驗證權杖,方法是檢查 x5c 權利要求與根憑證是否相符、檢查權杖是否由葉憑證簽署,最後檢查下載的根憑證是否與 x5c 權利要求中的根憑證相同。
  3. 權杖中的負載量評估要求符合 OPA 政策中指定的屬性條件。OPA 是開放原始碼的通用政策引擎,可在整個堆疊中統一政策執行作業。OPA 會使用與 JSON 類似的語法,設定政策驗證基準值的文件。如需政策檢查的值範例,請參閱 OPA 基準值
  4. 此值與預期的 nonce 相符 (TLS 匯出的密鑰材料)。這項資訊已在上述 OPA 政策中驗證。

完成並通過所有檢查後,UWear 就能確認資料會安全地傳送及處理。接著,UWear 會透過相同的 TLS 工作階段回傳敏感的 PHI,USleep 就能使用這些資料計算客戶的睡眠品質。

2. 設定雲端資源

事前準備

  1. 請設定兩個 Google Cloud 專案,一個用於 USleep,另一個用於 UWear。如要進一步瞭解如何建立 Google Cloud 專案,請參閱「設定及瀏覽第一個 Google 專案」程式碼研究室。您可以參閱「建立及管理專案」,進一步瞭解如何擷取專案 ID,以及專案 ID 與專案名稱和專案編號的差異。
  2. 啟用專案的帳單功能
  3. Google 專案的 Cloud Shell 中,設定必要的專案環境變數,如下所示。
export UWEAR_PROJECT_ID=<Google Cloud project id of UWear>
export USLEEP_PROJECT_ID=<Google Cloud project id of USleep>
  1. 兩個專案啟用機密運算 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. 使用以下方法擷取主體 ID
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 專案的 Cloud Shell 中,使用下列指令複製 Confidential Space Codelab GitHub 存放區,取得用於本程式碼研究室的必要指令碼。
git clone https://github.com/GoogleCloudPlatform/confidential-space.git
  1. 將目錄變更為健康資料程式碼研究室的指令碼目錄。
cd confidential-space/codelabs/health_data_analysis_codelab/scripts
  1. 更新 config_env.sh 指令碼中的這兩行程式碼,該指令碼位於 codelabs/health_data_analysis_codelab/scripts 目錄中。請將專案 ID 更新為 USleep 和 UWear 專案 ID。請務必移除行首的註解符號「#」。
# 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 指令碼,將剩餘的變數名稱設為資源名稱的值,並以專案 ID 為依據。
# 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 需要根據機密空間根憑證驗證簽名。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. 建立工作負載服務帳戶

接下來,您將建立兩個服務帳戶,一個用於 USleep,另一個用於 UWear 工作負載。執行 create_service_accounts.sh 指令碼,在 USleep 和 UWear 專案中建立工作負載服務帳戶。執行工作負載的 VM 會使用這些服務帳戶。

# 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 角色授予工作負載服務帳戶權限。這樣一來,機密空間環境就能將記錄寫入 Cloud Logging (除了序列控制台),讓您在 VM 終止後仍能存取記錄。

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. 向認證服務要求權杖,並指定目標對象、Nonce 和 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 工作負載。這個指令碼會:
  • 建立 UWear 擁有的 Artifact Registry ($USLEEP_ARTIFACT_REPOSITORY),用於發布工作負載。
  • 建構 usleep/workload.go 程式碼,並將其封裝在 Docker 映像檔中。請參閱 USleep 的 Dockerfile 設定。
  • 將 Docker 映像檔發布至 UWear 擁有的 Artifact Registry ($USLEEP_ARTIFACT_REPOSITORY)。
  • 授予服務帳戶 $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. opa_validation_values.json 中「allowed_submods_container_image_digest」下的值,替換為 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. 加入在 USleep 工作負載中建立的 TLS 工作階段,並透過安全的 TLS 工作階段從 USleep 擷取認證權杖。
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 工作負載會檢查符記中的 workload 評量宣告是否符合 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 會透過相同的 TLS 工作階段回傳敏感的 PHI,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 工作負載:
  • 建立 UWear 擁有的 Artifact Registry ($UWEAR_ARTIFACT_REPOSITORY),用於發布工作負載。
  • 建構 uwear/workload.go 程式碼,並將其封裝在 Docker 映像檔中。請參閱 USleep 的 Dockerfile 設定。
  • 將 Docker 映像檔發布至 UWear 擁有的 Artifact Registry ($UWEAR_ARTIFACT_REPOSITORY)。
  • 授予服務帳戶 $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 專案中的「VM 執行個體」頁面。按一下「usleep」例項,然後按下「Logs」(記錄) 部分下方的「Serial port 1 (console)」(序列埠 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 專案中的「VM Instances」(VM 執行個體) 頁面。按一下「uwear」例項,然後按一下「Logs」(記錄) 部分下方的「Serial port 1 (console)」(序列埠 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 專案中的「VM Instances」頁面。按一下「usleep」例項,然後按下「Logs」(記錄) 部分下方的「Serial port 1 (console)」(序列埠 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 VM 執行個體。
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 VM 執行個體並執行工作負載
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. 擷取新的 USleep 外部 IP,以供日後使用
export USLEEP_EXTERNAL_IP=<add your external IP>

重新執行工作負載

  1. 刪除 UWear VM 執行個體
gcloud config set project $UWEAR_PROJECT_ID

gcloud compute instances delete uwear --zone $UWEAR_PROJECT_ZONE
  1. 使用新的外部 IP 重新建立 UWear VM 執行個體
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 VM 不應接收任何機密資料
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 運算執行個體
  • USleep 服務帳戶 ($USLEEP_SERVICE_ACCOUNT)。
  • USleep 異常項目登錄 ($USLEEP_ARTIFACT_REPOSITORY)。
  • USleep 運算執行個體
./cleanup.sh

如果您已完成探索,請考慮按照這篇文章的操作說明刪除專案。

恭喜

恭喜,您已順利完成本程式碼研究室!

您已瞭解如何使用機密空間,在確保資料機密性的前提下安全分享資料。

後續步驟

查看一些類似的程式碼研究室…

其他資訊