Usar o Espaço confidencial com recursos protegidos que não são armazenados em um provedor de nuvem

1. Visão geral

O Confidential Space oferece compartilhamento e colaboração de dados seguros com várias partes, permitindo que as organizações preservem a confidencialidade dos dados. Isso significa que as organizações podem colaborar entre si, mantendo o controle dos dados e protegendo-os contra acesso não autorizado.

O Confidential Space permite que você agregue e analise dados sensíveis e regulamentados, mantendo o controle total sobre eles. Com o Confidential Space, as organizações podem agregar e analisar dados sensíveis, como informações de identificação pessoal (PII), informações protegidas de saúde (PHI), propriedade intelectual e segredos criptográficos, mantendo o controle total sobre eles.

O que é necessário

O que você vai aprender

  • Como configurar os recursos do Cloud necessários para executar o Espaço confidencial
  • Como executar uma carga de trabalho em uma VM confidencial que executa a imagem do Confidential Space
  • Como autorizar o acesso a recursos protegidos com base nos atributos do código da carga de trabalho (o quê), no ambiente do Espaço confidencial (onde) e na conta que está executando a carga de trabalho (quem).

Este codelab se concentra em como usar o Confidential Space com recursos protegidos hospedados em outro lugar que não seja o Google Cloud. Você vai aprender a solicitar um token autocontido e personalizado do Serviço de Atestado do Google fornecendo um valor de uso único, o público-alvo e o tipo de token PKI.

Neste codelab, você vai configurar um Espaço confidencial entre um produto fictício, o USleep, um aplicativo contêinerizado, e um produto fictício, o UWear, um dispositivo vestível conectado, para calcular a qualidade do sono. O UWear vai compartilhar informações protegidas de saúde (PHI) com o USleep em um ambiente seguro, protegido e isolado (também conhecido como ambiente de execução confiável ou TEE), para que os proprietários dos dados mantenham a confidencialidade completa.

O UWear é o auditor de carga de trabalho e o proprietário de dados. Como auditor de carga de trabalho,ele analisa o código da carga de trabalho que está sendo executada e registra o resumo da imagem. Como proprietário de dados, o UWear grava a lógica de verificação para verificar a validade do token e da assinatura. Ele grava uma política de validação, usando o resumo de imagem das cargas de trabalho auditadas, que permite apenas que o resumo de imagem específico, em um ambiente específico, tenha acesso aos dados sensíveis.

Neste codelab, a USleep está implantando o aplicativo conteinerizado. O USleep não tem acesso aos dados sensíveis, mas executa a carga de trabalho aprovada que tem permissão de acesso a eles.

O codelab envolve as seguintes etapas:

  • Etapa 1: configurar os recursos de nuvem necessários para o codelab. Configurar projetos, faturamento e permissões. Faça o download do código-fonte do codelab e defina as variáveis de ambiente.
  • Etapa 2: faça o download do certificado raiz e armazene-o com o código-fonte do UWear.
  • Etapa 3: crie contas de serviço de carga de trabalho separadas que serão usadas pela VM da carga de trabalho para USleep e UWear.
  • Etapa 4: crie a carga de trabalho USleep, que fornece um token de atestado.
  • Etapa 5: crie a carga de trabalho do UWear, que valida o token de atestado e envia os dados sensíveis se o token for aprovado.
  • Etapa 6: execute as cargas de trabalho do USleep e do UWear. O UWear vai fornecer os dados sensíveis, e o USleep vai executar um algoritmo de sono nos dados e gerar um resultado.
  • Etapa 7: (opcional) execute uma carga de trabalho de USleep não autorizada e confirme se os dados sensíveis não foram recebidos do UWear.
  • Etapa 8: limpe todos os recursos.

Como entender o fluxo de trabalho

O USleep vai executar a carga de trabalho no Confidential Space. Para executar a carga de trabalho, ele precisa acessar o PHI do UWear. Para ter acesso, a carga de trabalho do USleep primeiro cria uma sessão TLS segura. O USleep também vai solicitar um token de atestado do serviço de atestado do Google com um payload.

O USleep vai solicitar um token de atestado com um payload JSON que vai conter três coisas:

  1. Um token de atestado vinculado à sessão TLS. Para vincular o token de atestado à sessão TLS, o valor de uso único será o hash do material de chave exportado do TLS. A vinculação do token à sessão TLS garante que não haja ataques de homem-no-meio, já que apenas as duas partes envolvidas na sessão TLS poderão gerar o valor de uso único.
  2. Um público-alvo de "uwear" será fornecido. O UWear vai verificar se ele é o público-alvo do token de atestado.
  3. Um tipo de token "PKI". Um tipo de token "PKI" significa que o USleep quer solicitar um token independente. É possível verificar se o token independente foi assinado pelo Google usando a raiz baixada do endpoint de PKI conhecido do Espaço confidencial. Isso contrasta com o tipo de token OIDC padrão, cuja assinatura é verificada usando uma chave pública que muda regularmente.

bb013916a3222ce7.png

A carga de trabalho USleep recebe o token de atestado. O UWear se junta à conexão TLS com o USleep e recupera o token de atestado do USleep. O UWear vai validar o token verificando a declaração x5c em relação ao certificado raiz.

O UWear vai aprovar a carga de trabalho do USleep se:

  1. O token passa pela lógica de validação de PKI.
  2. O UWear vai validar o token verificando a declaração x5c em relação ao certificado raiz, verificando se o token é assinado pelo certificado de folha e, por fim, se o certificado raiz transferido por download é o mesmo que na declaração x5c.
  3. As declarações de medição de carga de trabalho no token correspondem às condições de atributo especificadas na política de OPA. O OPA é um mecanismo de políticas de uso geral de código aberto que unifica a aplicação de políticas em toda a pilha. A OPA usa documentos com sintaxe semelhante à do JSON para definir valores de referência em que a política é validada. Consulte Valores de referência da OPA para conferir um exemplo dos valores verificados pela política.
  4. O valor de uso único corresponde ao valor de uso único esperado (material de chave exportado TLS). Isso é verificado na política OPA acima.

Depois que todas essas verificações forem concluídas, a UWear poderá confirmar que os dados serão enviados e processados com segurança. O UWear vai responder com as PHI sensíveis na mesma sessão TLS, e o USleep poderá usar esses dados para calcular a qualidade do sono do cliente.

2. Configurar recursos do Cloud

Antes de começar

  1. Configure dois projetos do Google Cloud, um para o USleep e outro para o UWear. Para mais informações sobre como criar um projeto do Google Cloud, consulte o codelab "Configurar e navegar no seu primeiro projeto do Google". Consulte Como criar e gerenciar projetos para saber como recuperar o ID do projeto e como ele é diferente do nome e do número do projeto.
  2. Ative o faturamento dos seus projetos.
  3. Em um dos Cloud Shells do projeto do Google, defina as variáveis de ambiente do projeto necessárias, conforme mostrado abaixo.
export UWEAR_PROJECT_ID=<Google Cloud project id of UWear>
export USLEEP_PROJECT_ID=<Google Cloud project id of USleep>
  1. Ative a API Confidential Computing e as APIs a seguir para ambos os projetos.
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. Extrair seu identificador principal usando
gcloud auth list

# Output should contain
# ACCOUNT: <Principal Identifier>

# Set your member variable
export MEMBER='user:<Principal Identifier>'
  1. Adicione permissões para esses dois projetos. Para adicionar permissões, siga os detalhes na página conceder um papel do 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. No Cloud Shell de um dos seus projetos do Google Cloud, clone o repositório do GitHub do codelab do espaço confidencial usando o comando abaixo para receber os scripts necessários que são usados como parte deste codelab.
git clone https://github.com/GoogleCloudPlatform/confidential-space.git
  1. Mude o diretório para o diretório de scripts do codelab de dados de saúde.
cd confidential-space/codelabs/health_data_analysis_codelab/scripts
  1. Atualize estas duas linhas no script config_env.sh, localizado no diretório codelabs/health_data_analysis_codelab/scripts. Atualize os IDs dos projetos com os IDs do USleep e do UWear. Remova o símbolo de comentário "#" no início da linha.
# TODO: Populate UWear and USleep Project IDs
export UWEAR_PROJECT_ID=your-uwear-project-id
export USLEEP_PROJECT_ID=your-usleep-project-id
  1. Opcional: defina variáveis preexistentes. É possível substituir os nomes de recursos usando estas variáveis (por exemplo, export UWEAR_ARTIFACT_REPOSITORY='my-artifact-repository')
  • É possível definir as variáveis a seguir com nomes de recursos da nuvem atuais. Se a variável for definida, o recurso de nuvem correspondente do projeto será usado. Se a variável não estiver definida, o nome do recurso da nuvem será gerado com base nos valores no script config_env.sh.
  1. Execute o script config_env.sh para definir os nomes das variáveis restantes como valores com base no ID do projeto para nomes de recursos.
# 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. Fazer o download do certificado raiz

  1. Para validar o token independente retornado pelo serviço de atestado, o UWear precisa validar a assinatura com o certificado raiz do espaço confidencial. O UWear precisa fazer o download do certificado raiz e armazená-lo localmente. Em um dos consoles do seu projeto do Google Cloud, execute os seguintes comandos:
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. Gerar a impressão digital do certificado raiz que foi transferido
openssl x509 -fingerprint -in confidential_space_root.pem -noout
  1. Verifique se a impressão digital corresponde ao seguinte resumo SHA-1:
B9:51:20:74:2C:24:E3:AA:34:04:2E:1C:3B:A3:AA:D2:8B:21:23:21

4. Criar uma conta de serviço de carga de trabalho

Agora, você vai criar duas contas de serviço: uma para o USleep e outra para as cargas de trabalho do UWear. Execute o script create_service_accounts.sh para criar contas de serviço de carga de trabalho nos projetos USleep e UWear. As VMs que executam as cargas de trabalho usariam essas contas de serviço.

# Navigate to the scripts folder
cd ~/confidential-space/codelabs/health_data_analysis_codelab/scripts

# Run the create_service_accounts script
./create_service_accounts.sh

O script:

  • Concede o papel iam.serviceAccountUser, que anexa a conta de serviço à carga de trabalho.
  • Concede o papel confidentialcomputing.workloadUser à conta de serviço da carga de trabalho . Isso permite que a conta do usuário gere um token de atestado.
  • Concede o papel logging.logWriter à permissão da conta de serviço da carga de trabalho. Isso permite que o ambiente do Confidential Space grave registros no Cloud Logging, além do console serial, para que os registros fiquem disponíveis após a VM ser encerrada.

5. Criar carga de trabalho do USleep

Nesta etapa, você vai criar imagens do Docker para as cargas de trabalho usadas neste codelab. A carga de trabalho USleep é um aplicativo simples em Golang que determina a qualidade do sono de um cliente usando informações de saúde pessoal em um dispositivo portátil.

Sobre a carga de trabalho do USleep

A carga de trabalho USleep é um aplicativo simples em Golang que determina a qualidade do sono de um cliente usando informações de saúde pessoal em um dispositivo portátil. A carga de trabalho do USleep tem três partes principais:

  1. Configurar uma sessão TLS e extrair o material de chave exportado
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. Solicitar um token do serviço de atestado com um público-alvo, um valor de uso único e um tipo de token 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. Receber os dados sensíveis e calcular a qualidade do sono do usuário
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
  ...
}

Etapas para criar a carga de trabalho do USleep

  1. Execute o script create_usleep_workload.sh para criar a carga de trabalho do USleep. Esse script:
  • Cria o Artifact Registry ($USLEEP_ARTIFACT_REPOSITORY) de propriedade do UWear, onde a carga de trabalho seria publicada.
  • Cria o código usleep/workload.go e o empacota em uma imagem do Docker. Consulte a configuração do Dockerfile para o USleep.
  • Publica a imagem do Docker no Artifact Registry ($USLEEP_ARTIFACT_REPOSITORY) pertencente ao UWear.
  • Conceda à conta de serviço $USLEEP_WORKLOAD_SERVICE_ACCOUNT permissão de leitura para o Artifact Registry ($USLEEP_ARTIFACT_REPOSITORY).
./create_usleep_workload.sh
  1. Importante: nos logs de saída, extraia o resumo de imagem para o USleep.
latest: digest: sha256:<USLEEP_IMAGE_DIGEST> size: 945
  1. Navegue até o diretório UWear
cd ~/confidential-space/codelabs/health_data_analysis_codelab/src/uwear
  1. Substitua o valor em "allowed_submods_container_image_digest" no opa_validation_values.json pelo 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. Criar carga de trabalho do UWear

Sobre a carga de trabalho do UWear

A carga de trabalho do UWear tem quatro partes principais:

  1. Participar da mesma sessão TLS criada na carga de trabalho do USleep e recuperar o token de atestado do USleep pela sessão TLS segura.
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. Valide o token independente da seguinte forma:
  • A verificação da declaração x5c contém uma cadeia de certificados que é encadeada corretamente do certificado de folha ao intermediário e, finalmente, ao certificado raiz.
  • Verifique se o token está assinado pelo certificado de folha contido na declaração x5c.
  • Verifique se o certificado raiz salvo / baixado é o mesmo da declaração 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. A carga de trabalho UWear vai verificar se as declarações de medição de carga de trabalho no token correspondem às condições de atributo especificadas na política OPA. O OPA é um mecanismo de políticas de uso geral de código aberto que unifica a aplicação de políticas em toda a pilha. A OPA usa documentos com sintaxe semelhante à do JSON para definir valores de referência em que a política é validada.
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"
}
  • Exemplo de consulta 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
"

Exemplo de código para gerar o hash 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. Depois que todas essas verificações forem concluídas, a UWear poderá confirmar que os dados serão enviados e processados com segurança. O UWear vai responder com as PHI sensíveis na mesma sessão TLS, e o USleep poderá usar esses dados para calcular a qualidade do sono do cliente.
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()
  
  ...
}

Etapas para criar a carga de trabalho do USleep

  1. Navegue até o diretório de scripts
cd ~/confidential-space/codelabs/health_data_analysis_codelab/scripts
  1. Execute o script create_uwear_workload.sh para criar a carga de trabalho do UWear:
  • Cria o Artifact Registry ($UWEAR_ARTIFACT_REPOSITORY) de propriedade do UWear, onde a carga de trabalho seria publicada.
  • Cria o código uwear/workload.go e o empacota em uma imagem do Docker. Consulte a configuração do Dockerfile para o USleep.
  • Publica a imagem do Docker no Artifact Registry ($UWEAR_ARTIFACT_REPOSITORY) pertencente ao UWear.
  • Conceda à conta de serviço $UWEAR_WORKLOAD_SERVICE_ACCOUNT permissão de leitura para o Artifact Registry ($UWEAR_ARTIFACT_REPOSITORY).
./create_uwear_workload.sh

7. Executar as cargas de trabalho do USleep e do UWear

Executar a carga de trabalho 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

A resposta deve retornar um STATUS: RUNNING, e o EXTERNAL_IP também deve ser retornado de forma semelhante a esta:

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

Armazenar o IP externo em uma variável

export USLEEP_EXTERNAL_IP=<add your external IP> 

Verificar se a carga de trabalho do USleep foi executada corretamente

Para verificar se a carga de trabalho do USleep está sendo executada corretamente, acesse a página Instâncias de VM no projeto do USleep. Clique na instância "usleep" e pressione "Porta serial 1(console)" na seção "Registros". Quando o servidor estiver em funcionamento, na parte de baixo dos registros, eles vão mostrar algo semelhante ao seguinte.

2024/09/13 17:00:00 workload task started
#####----- Local IP Address is <YOUR-LOCAL-IP> -----#####
Starting Server..

Executar a carga de trabalho do 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

Verificar se a carga de trabalho do UWear foi executada corretamente

Para conferir os registros da carga de trabalho do UWear, acesse a página Instâncias de VM no projeto do UWear. Clique na instância "uwear" e pressione "Porta serial 1(console)" na seção "Registros".

A saída do registro, depois que a instância for totalmente iniciada, vai ficar assim:

No projeto UWear, os registros de série devem mostrar algo semelhante a

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

Se a carga de trabalho do UWear não estiver assim, consulte as notas abaixo para ver as instruções.

Conferir os resultados do USleep

Para conferir os resultados, volte à página Instâncias de VM no projeto USleep. Clique na instância "usleep" e pressione "Porta serial 1(console)" na seção "Registros". Confira os resultados da carga de trabalho na parte de baixo dos registros. Elas devem ser semelhantes ao exemplo abaixo.

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

O resultado deve ser "total sleep time is less than 8 hours".

Parabéns! Você criou um espaço confidencial entre o UWear e o USleep para compartilhar informações sensíveis.

8. (Opcional) Executar carga de trabalho não autorizada

No próximo cenário, o USleep atualiza o código e executa uma carga de trabalho diferente nos dados de sono fornecidos pelo UWear. A UWear não concordou com essa nova carga de trabalho e não atualizou a política de OPA para permitir o novo resumo de imagem. Vamos verificar se o UWear não envia dados sensíveis para a carga de trabalho não autorizada.

O USleep modifica a carga de trabalho

  1. Defina o projeto como $USLEEP_PROJECT_ID.
gcloud config set project $USLEEP_PROJECT_ID
  1. Exclua a instância da VM USleep.
gcloud compute instances delete usleep --zone $USLEEP_PROJECT_ZONE
  1. Navegue até o diretório usleep/workload.go.
cd ~/confidential-space/codelabs/health_data_analysis_codelab/src/usleep
  1. No arquivo usleep/workload.go. Atualize a linha "audience": "uwear".. Neste exemplo, para mudar o resumo da imagem, vamos atualizar o público-alvo para um valor diferente que a UWear não aprovou. Portanto, o UWear precisa rejeitá-lo por dois motivos: resumo de imagem não aprovado e público-alvo incorreto.
"audience": "anotherCompany.com",
  1. Criar uma nova carga de trabalho do USleep
cd ~/confidential-space/codelabs/health_data_analysis_codelab/scripts

./create_usleep_workload.sh
  1. Crie a nova instância de VM do USleep e execute a carga de trabalho.
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. Extrair o novo IP externo do USleep para uso posterior
export USLEEP_EXTERNAL_IP=<add your external IP>

Executar a carga de trabalho novamente

  1. Excluir a instância de VM do UWear
gcloud config set project $UWEAR_PROJECT_ID

gcloud compute instances delete uwear --zone $UWEAR_PROJECT_ZONE
  1. Recriar a instância de VM UWear usando o novo IP externo
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. Nos logs em série do UWear, a mensagem a seguir deve aparecer, e a VM do USleep não deve receber dados sensíveis.
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. Limpeza

O script de limpeza pode ser usado para limpar os recursos que criamos como parte deste codelab. Como parte dessa limpeza, os seguintes recursos serão excluídos:

  • A conta de serviço do UWear ($UWEAR_SERVICE_ACCOUNT).
  • O registro de artefatos do UWear ($UWEAR_ARTIFACT_REPOSITORY).
  • Instância de computação do UWear
  • A conta de serviço do USleep ($USLEEP_SERVICE_ACCOUNT).
  • O registro de artefato USleep ($USLEEP_ARTIFACT_REPOSITORY).
  • Instância de computação do USleep
./cleanup.sh

Se você já tiver explorado tudo, siga estas instruções para excluir seu projeto.

Parabéns

Parabéns, você concluiu o codelab.

Você aprendeu a compartilhar dados com segurança, mantendo a confidencialidade deles usando o Espaço confidencial.

Qual é a próxima etapa?

Confira alguns desses codelabs semelhantes:

Leia mais