클라우드 제공업체에 저장되지 않은 보호된 리소스로 Confidential Space 사용

1. 개요

Confidential Space는 조직에서 데이터의 기밀성을 유지하면서 여러 당사자 간에 안전하게 데이터를 공유하고 공동작업할 수 있도록 지원합니다. 즉, 조직은 데이터를 계속 관리하고 무단 액세스로부터 보호하면서도 서로 협력할 수 있습니다.

Confidential Space를 사용하면 민감한 정보(종종 규제 대상)를 집계 및 분석하여 상호 가치를 얻는 동시에 데이터를 완전히 제어할 수 있는 시나리오를 실현할 수 있습니다. Confidential Space를 통해 조직은 개인 식별 정보 (PII), 보호 건강 정보 (PHI), 지적 재산, 암호화된 보안 비밀과 같은 민감한 정보를 집계 및 분석하여 상호 가치를 얻는 동시에 이를 완전히 제어할 수 있습니다.

필요한 항목

학습할 내용

  • Confidential Space 실행에 필요한 Cloud 리소스를 구성하는 방법
  • Confidential Space 이미지를 실행하는 Confidential VM에서 워크로드를 실행하는 방법
  • 워크로드 코드 (what), Confidential Space 환경 (where), 워크로드를 실행하는 계정 (who)의 속성을 기반으로 보호된 리소스에 대한 액세스를 승인하는 방법

이 Codelab에서는 Google Cloud가 아닌 다른 위치에 호스팅된 보호된 리소스에서 Confidential Space를 사용하는 방법을 중점적으로 설명합니다. nonce, 대상, PKI 토큰 유형을 제공하여 Google 증명 서비스에서 맞춤형 독립형 토큰을 요청하는 방법을 알아봅니다.

이 Codelab에서는 가상의 제품인 컨테이너화된 애플리케이션 USleep과 가상의 제품인 연결된 웨어러블 기기 UWear 간에 Confidential Space를 설정하여 수면의 질을 계산합니다. UWear는 데이터 소유자가 완전한 기밀을 유지할 수 있도록 안전하고 보안이 유지되며 격리된 환경 (신뢰할 수 있는 실행 환경 또는 TEE)에서 USleep과 보호 건강 정보 (PHI)를 공유합니다.

UWear는 워크로드 감사자이자 데이터 소유자입니다. 워크로드 감사자로서 실행 중인 워크로드의 코드를 검토하고 이미지 다이제스트를 기록합니다. UWear는 데이터 소유자로서 토큰 및 서명의 유효성을 확인하는 확인 로직을 작성합니다. 감사된 워크로드 이미지 다이제스트를 사용하여 특정 환경의 특정 이미지 다이제스트만 민감한 정보에 액세스할 수 있도록 허용하는 유효성 검사 정책을 작성합니다.

이 Codelab의 USleep은 컨테이너화된 애플리케이션을 배포합니다. USleep은 민감한 정보에 액세스할 수 없지만 민감한 정보에 액세스할 수 있는 승인된 워크로드를 실행합니다.

이 Codelab에서는 다음 단계를 진행합니다.

  • 1단계: Codelab에 필요한 Cloud 리소스를 설정합니다. 프로젝트, 결제, 권한을 설정합니다. Codelab 소스 코드를 다운로드하고 환경 변수를 설정합니다.
  • 2단계: 루트 인증서를 다운로드하여 UWear 소스 코드와 함께 저장합니다.
  • 3단계: USleep 및 UWear에 워크로드 VM에서 사용할 별도의 워크로드 서비스 계정을 만듭니다.
  • 4단계: 증명 토큰을 제공하는 USleep 워크로드를 만듭니다.
  • 5단계: 증명 토큰을 확인하고 토큰이 승인되면 민감한 데이터를 전송하는 UWear 워크로드를 만듭니다.
  • 6단계: USleep 및 UWear 워크로드를 실행합니다. UWear는 민감한 데이터를 제공하고 USleep은 데이터에 수면 알고리즘을 실행하여 결과를 출력합니다.
  • 7단계: (선택사항) 승인되지 않은 USleep 워크로드를 실행하고 UWear에서 민감한 정보가 수신되지 않았는지 확인합니다.
  • 8단계: 모든 리소스를 삭제합니다.

워크플로 이해하기

USleep이 Confidential Space에서 워크로드를 실행합니다. 워크로드를 실행하려면 UWear의 PHI에 액세스해야 합니다. 액세스하기 위해 USleep 워크로드는 먼저 보안 TLS 세션을 만듭니다. 그러면 USleep은 페이로드와 함께 Google 증명 서비스에 증명 토큰을 요청합니다.

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가 예상 nonce (TLS 내보낸 키 입력 자료)와 일치합니다. 이는 위의 OPA 정책에서 확인할 수 있습니다.

이러한 모든 검사가 완료되고 통과되면 UWear는 데이터가 안전하게 전송되고 처리될 수 있음을 확인할 수 있습니다. 그러면 UWear는 동일한 TLS 세션을 통해 민감한 PHI로 응답하고 USleep은 이 데이터를 사용하여 고객의 수면 품질을 계산할 수 있습니다.

2. Cloud 리소스 설정

시작하기 전에

  1. USleep용 Google Cloud 프로젝트와 UWear용 Google Cloud 프로젝트를 각각 하나씩 설정합니다. Google Cloud 프로젝트를 만드는 방법에 관한 자세한 내용은 '첫 번째 Google 프로젝트 설정 및 탐색' Codelab을 참고하세요. 프로젝트 만들기 및 관리에서 프로젝트 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. 두 프로젝트 모두에 Confidential Computing API 및 다음 API를 사용 설정합니다.
gcloud config set project $UWEAR_PROJECT_ID
gcloud services enable \
    cloudapis.googleapis.com \
    cloudshell.googleapis.com \
    container.googleapis.com \
    containerregistry.googleapis.com \
    confidentialcomputing.googleapis.com

gcloud config set project $USLEEP_PROJECT_ID
gcloud services enable \
    cloudapis.googleapis.com \
    cloudshell.googleapis.com \
    container.googleapis.com \
    containerregistry.googleapis.com \
    confidentialcomputing.googleapis.com
  1. 다음을 사용하여 주 구성원 식별자 검색
gcloud auth list

# Output should contain
# ACCOUNT: <Principal Identifier>

# Set your member variable
export MEMBER='user:<Principal Identifier>'
  1. 두 프로젝트에 대한 권한을 추가합니다. IAM 역할 부여 웹페이지의 세부정보에 따라 권한을 추가할 수 있습니다.
gcloud config set project $UWEAR_PROJECT_ID

# Add Artifact Registry Administrator role
gcloud projects add-iam-policy-binding $UWEAR_PROJECT_ID --member=$MEMBER --role='roles/iam.serviceAccountAdmin'

# Add Service Account Administrator role
gcloud projects add-iam-policy-binding $UWEAR_PROJECT_ID --member=$MEMBER --role='roles/artifactregistry.admin'
gcloud config set project $USLEEP_PROJECT_ID

# Add Service Account Administrator role
gcloud projects add-iam-policy-binding $USLEEP_PROJECT_ID --member=$MEMBER --role='roles/iam.serviceAccountAdmin'

# Add Artifact Registry Administrator role
gcloud projects add-iam-policy-binding $USLEEP_PROJECT_ID --member=$MEMBER --role='roles/artifactregistry.admin'

# Add Compute Administrator role
gcloud projects add-iam-policy-binding $USLEEP_PROJECT_ID --member=$MEMBER --role='roles/compute.admin'

# Add Storage Administrator role
gcloud projects add-iam-policy-binding $USLEEP_PROJECT_ID --member=$MEMBER --role='roles/compute.storageAdmin'
  1. Google Cloud 프로젝트 Cloud Shell 중 하나에서 아래 명령어를 사용하여 Confidential Space Codelab GitHub 저장소를 클론하여 이 Codelab의 일부로 사용되는 필수 스크립트를 가져옵니다.
git clone https://github.com/GoogleCloudPlatform/confidential-space.git
  1. 디렉터리를 건강 데이터 Codelab의 스크립트 디렉터리로 변경합니다.
cd confidential-space/codelabs/health_data_analysis_codelab/scripts
  1. codelabs/health_data_analysis_codelab/scripts 디렉터리에 있는 config_env.sh 스크립트의 두 줄을 업데이트합니다. 프로젝트 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에서 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. 워크로드 서비스 계정 만들기

이제 USleep 워크로드용 서비스 계정 1개와 UWear 워크로드용 서비스 계정 1개를 만듭니다. 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 역할을 부여합니다. 이렇게 하면 Confidential Space 환경에서 직렬 콘솔 외에도 Cloud Logging에 로그를 쓸 수 있으므로 VM이 종료된 후에도 로그를 사용할 수 있습니다.워크로드 만들기

5. USleep 워크로드 만들기

이 단계에서는 이 Codelab에서 사용되는 워크로드의 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 구성을 참고하세요.
  • UWear가 소유한 Artifact Registry ($USLEEP_ARTIFACT_REPOSITORY)에 Docker 이미지를 게시합니다.
  • 서비스 계정 $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 워크로드가 토큰의 워크로드 측정 클레임이 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 구성을 참고하세요.
  • UWear가 소유한 Artifact Registry ($UWEAR_ARTIFACT_REPOSITORY)에 Docker 이미지를 게시합니다.
  • 서비스 계정 $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' 인스턴스를 클릭하고 로그 섹션에서 '직렬 포트 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 인스턴스 페이지로 이동합니다. '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 프로젝트의 VM 인스턴스 페이지로 돌아갑니다. '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 간에 Confidential Space를 만들었습니다.

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. 삭제

정리 스크립트를 사용하면 이 Codelab의 일부로 만든 리소스를 정리할 수 있습니다. 이번 정리 작업의 일환으로 다음 리소스가 삭제됩니다.

  • UWear 서비스 계정 ($UWEAR_SERVICE_ACCOUNT)
  • UWear 아티팩트 레지스트리 ($UWEAR_ARTIFACT_REPOSITORY)
  • UWear Compute 인스턴스
  • USleep 서비스 계정 ($USLEEP_SERVICE_ACCOUNT)
  • USleep 아티팩트 레지스트리 ($USLEEP_ARTIFACT_REPOSITORY)
  • USleep 컴퓨팅 인스턴스
./cleanup.sh

둘러보기가 끝나면 이 안내에 따라 프로젝트를 삭제해 보세요.

축하합니다

축하합니다. Codelab을 완료했습니다.

Confidential Space를 사용하여 기밀을 유지하면서 데이터를 안전하게 공유하는 방법을 알아봤습니다.

다음 단계

다음과 같은 유사한 Codelab을 확인해 보세요.

추가 자료