Используйте конфиденциальное пространство с защищенными ресурсами, которые не хранятся у поставщика облачных услуг. Используйте конфиденциальное пространство с защищенными ресурсами, которые не хранятся у поставщика облачных услуг.

1. Обзор

Confidential Space предлагает безопасный многосторонний обмен данными и совместную работу, позволяя при этом организациям сохранять конфиденциальность своих данных. Это означает, что организации могут сотрудничать друг с другом, сохраняя при этом контроль над своими данными и защищая их от несанкционированного доступа.

Конфиденциальное пространство открывает сценарии, в которых вы хотите получить взаимную выгоду от агрегирования и анализа конфиденциальных, часто регулируемых данных, сохраняя при этом полный контроль над ними. С помощью Confidential Space организации могут получить взаимную выгоду от агрегирования и анализа конфиденциальных данных, таких как личная информация (PII), защищенная медицинская информация (PHI), интеллектуальная собственность и криптографические секреты, сохраняя при этом полный контроль над ними.

Что вам понадобится

Что вы узнаете

  • Как настроить необходимые облачные ресурсы для работы Конфиденциального пространства
  • Как запустить рабочую нагрузку на конфиденциальной виртуальной машине, на которой работает образ конфиденциального пространства
  • Как авторизовать доступ к защищенным ресурсам на основе атрибутов кода рабочей нагрузки ( what ), среды конфиденциального пространства ( where ) и учетной записи, под которой выполняется рабочая нагрузка ( who ).

В этой лаборатории кода основное внимание уделяется использованию Конфиденциального пространства с защищенными ресурсами, размещенными не в Google Cloud . Вы узнаете, как запросить собственный автономный токен в Службе аттестации Google, указав одноразовый номер, аудиторию и тип токена PKI.

В этой лаборатории вы создадите конфиденциальное пространство между вымышленным продуктом — USleep, контейнерным приложением, и вымышленным продуктом — UWear, подключенным носимым устройством, для расчета качества вашего сна. UWear будет передавать защищенную медицинскую информацию (PHI) компании USleep в безопасной, надежной и изолированной среде (также известной как Trusted Execution Environment или TEE), так что владельцы данных сохраняют полную конфиденциальность.

UWear одновременно является аудитором рабочей нагрузки и владельцем данных . В качестве аудитора рабочей нагрузки он просматривает код выполняемой рабочей нагрузки и принимает во внимание дайджест изображения. Как владелец данных , UWear пишет логику проверки для проверки действительности токена и его подписи. Он пишет политику проверки, используя дайджест образа проверенных рабочих нагрузок, которая позволяет только конкретному дайджесту образа в конкретной среде получить доступ к конфиденциальным данным.

USleep в этой лаборатории кода развертывает контейнерное приложение. USleep не имеет доступа к конфиденциальным данным, но выполняет утвержденную рабочую нагрузку, которой разрешен доступ к конфиденциальным данным.

Лаборатория кода включает в себя следующие шаги:

  • Шаг 1. Настройте необходимые облачные ресурсы для лаборатории кода. Настройте проекты, выставление счетов и разрешения. Загрузите исходный код Codelab и установите переменные среды.
  • Шаг 2. Загрузите корневой сертификат и сохраните его вместе с исходным кодом UWear.
  • Шаг 3. Создайте отдельные учетные записи службы рабочей нагрузки, которые будут использоваться виртуальной машиной рабочей нагрузки для USleep и UWear.
  • Шаг 4. Создайте рабочую нагрузку USleep, которая предоставляет токен аттестации.
  • Шаг 5. Создайте рабочую нагрузку UWear, которая проверяет токен аттестации и отправляет конфиденциальные данные, если токен одобрен.
  • Шаг 6. Запустите рабочие нагрузки USleep и UWear. UWear предоставит конфиденциальные данные, а USleep запустит на этих данных алгоритм сна и выдаст результат.
  • Шаг 7. (Необязательно) Запустите несанкционированную рабочую нагрузку USleep и убедитесь, что конфиденциальные данные не были получены от UWear.
  • Шаг 8: Очистите все ресурсы.

Понимание рабочего процесса

USleep будет выполнять рабочую нагрузку в Конфиденциальном пространстве. Для выполнения рабочей нагрузки ему необходим доступ к PHI UWear. Чтобы получить доступ, рабочая нагрузка USleep сначала создает защищенный сеанс TLS. Затем USleep также запросит токен аттестации от службы аттестации Google с полезной нагрузкой .

USleep запросит токен аттестации с полезной нагрузкой JSON, который будет содержать три вещи:

  1. Токен аттестации, привязанный к сеансу TLS . Чтобы привязать токен аттестации к сеансу TLS, значением nonce будет хэш экспортированного ключевого материала TLS . Привязка токена к сеансу TLS гарантирует отсутствие атак типа «машина посередине», поскольку только две стороны, участвующие в сеансе TLS, смогут генерировать значение nonce.
  2. Аудитория «uwear» будет обеспечена. UWear проверит, что это целевая аудитория для токена аттестации.
  3. Тип токена «PKI». Тип токена «PKI» означает, что USleep хочет запросить автономный токен. Автономный токен можно проверить, подписан ли он Google, используя корень, загруженный из известной конечной точки 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. Настройте облачные ресурсы

Прежде чем начать

  1. Настройте два проекта Google Cloud: один для USleep и один для UWear. Дополнительную информацию о создании проекта Google Cloud см. в кодовой лаборатории «Настройка и навигация по вашему первому проекту 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. Включите 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 Cloud Shell клонируйте репозиторий Github Confidential Space Codelab, используя приведенную ниже команду, чтобы получить необходимые скрипты, которые используются как часть этой лаборатории кода.
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_anaанализ_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 необходимо будет проверить подпись на соответствие корневому сертификату конфиденциального пространства. 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. Виртуальные машины, на которых выполняются рабочие нагрузки, будут использовать эти учетные записи служб.

# 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 в дополнение к последовательной консоли, поэтому журналы доступны после завершения работы виртуальной машины. Создавайте рабочие нагрузки.

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. Этот сценарий:
  • Создает реестр артефактов ( $USLEEP_ARTIFACT_REPOSITORY ), принадлежащий UWear, в котором будет опубликована рабочая нагрузка.
  • Создает код usleep/workload.go и упаковывает его в образ Docker. См. конфигурацию Dockerfile для USleep.
  • Публикует образ Docker в реестре артефактов ( $USLEEP_ARTIFACT_REPOSITORY ), принадлежащем UWear.
  • Предоставляет сервисному аккаунту $USLEEP_WORKLOAD_SERVICE_ACCOUNT разрешение на чтение реестра артефактов ( $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 состоит из четырех основных частей:

  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 := decodeAndParseCertificate(string(rawRootCertificate))
  if err != nil {
    return jwt.Token{}, fmt.Errorf("DecodeAndParseCertificate(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 Hash :

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:
  • Создает реестр артефактов ( $UWEAR_ARTIFACT_REPOSITORY ), принадлежащий UWear, в котором будет опубликована рабочая нагрузка.
  • Создает код uwear/workload.go и упаковывает его в образ Docker. См. конфигурацию Dockerfile для USleep.
  • Публикует образ Docker в реестре артефактов ( $UWEAR_ARTIFACT_REPOSITORY ), принадлежащем UWear.
  • Предоставляет сервисному аккаунту $UWEAR_WORKLOAD_SERVICE_ACCOUNT разрешение на чтение реестра артефактов ( $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 \
 --min-cpu-platform="AMD Milan" \
 --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

Ответ должен вернуть СТАТУС: ВЫПОЛНЕНИЕ, а 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 \
 --min-cpu-platform="AMD Milan" \
 --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 \
 --min-cpu-platform="AMD Milan" \
 --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, используя новый внешний IP-адрес.
gcloud compute instances create \
 --confidential-compute-type=SEV \
 --shielded-secure-boot \
 --maintenance-policy=MIGRATE \
 --min-cpu-platform="AMD Milan" \
 --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
  • Учетная запись службы USleep ( $USLEEP_SERVICE_ACCOUNT ).
  • Реестр артефактов USleep ( $USLEEP_ARTIFACT_REPOSITORY ).
  • Экземпляр USleep Compute
./cleanup.sh

Если вы закончили изучение, рассмотрите возможность удаления проекта, следуя этим инструкциям .

Поздравления

Поздравляем, вы успешно завершили работу над кодом!

Вы узнали, как безопасно обмениваться данными, сохраняя при этом их конфиденциальность, с помощью Confidential Space.

Что дальше?

Посмотрите некоторые из этих похожих лабораторий кода...

Дальнейшее чтение