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 entre 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 sobre os dados e protegendo-os contra acesso não autorizado.

O Confidential Space permite agregar e analisar dados sensíveis, muitas vezes regulamentados, para gerar valor mútuo, 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 secrets 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 Confidential Space
  • Como executar uma carga de trabalho em uma VM confidencial com 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 Confidential Space (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 personalizado e independente do Google Attestation Service fornecendo um nonce, um público-alvo e o tipo de token PKI.

Neste codelab, você vai configurar um espaço confidencial entre um produto fictício (USleep, um aplicativo contêinerizado) e outro produto fictício (UWear, um dispositivo wearable 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 e isolado (também conhecido como ambiente de execução confiável ou TEE) para que os proprietários dos dados mantenham total confidencialidade.

O UWear é o auditor de carga de trabalho e o proprietário dos dados. Como auditor de carga de trabalho,ele analisa o código na carga de trabalho em execução e anota o resumo da imagem. Como proprietário dos dados, o UWear grava a lógica de verificação para conferir a validade do token e da assinatura. Ele grava uma política de validação usando o resumo da imagem das cargas de trabalho auditadas, que permite apenas que o resumo específico da imagem, em um ambiente específico, acesse os dados sensíveis.

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

O codelab envolve as seguintes etapas:

  • Etapa 1: configure os recursos de nuvem necessários para o codelab. Configure 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 seu código-fonte do UWear.
  • Etapa 3: crie contas de serviço de carga de trabalho separadas que serão usadas pela VM de 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 comprovação e envia os dados sensíveis se o token for aprovado.
  • Etapa 6: execute as cargas de trabalho USleep e 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 USleep não autorizada e confirme se 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, ela precisa de acesso às informações de saúde protegidas do UWear. Para ter acesso, a carga de trabalho 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.

USleep vai solicitar um token de comprovação com um payload JSON que vai conter três coisas:

  1. Um token de comprovação vinculado à sessão TLS. Para vincular o token de atestado à sessão TLS, o valor de nonce será o hash do material de chaves exportado do TLS. Ao vincular o token à sessão TLS, garantimos que não há ataques man-in-the-middle, já que apenas as duas partes envolvidas na sessão TLS podem gerar o valor de nonce.
  2. Um público-alvo de "uwear" será fornecido. O UWear vai verificar se ele é o público-alvo pretendido para o token de atestado.
  3. Um tipo de token "PKI". Um tipo de token "PKI" significa que o USleep quer solicitar um token independente. O token independente pode ser verificado para saber se ele foi assinado pelo Google usando a raiz baixada do endpoint PKI conhecido do Confidential Space. 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 comprovação. O UWear entra na conexão TLS com o USleep e recupera o token de comprovação do USleep. O UWear vai validar o token verificando a declaração x5c com o 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 com o certificado raiz, se o token está assinado pelo certificado folha e, por fim, se o certificado raiz baixado é o mesmo da declaração x5c.
  3. As declarações de medição da carga de trabalho no token correspondem às condições de atributo especificadas na política da OPA. O OPA é um mecanismo de políticas de código aberto e uso geral que unifica a aplicação de políticas em toda a pilha. O OPA usa documentos com sintaxe semelhante ao JSON para definir valores de base que são usados para validar a política. Consulte Valores de referência do OPA para um exemplo dos valores que a política verifica.
  4. O nonce corresponde ao nonce esperado (Material de chaves exportadas do TLS). Isso é verificado na política da OPA acima.

Depois que todas essas verificações forem concluídas e aprovadas, o UWear poderá confirmar que os dados serão enviados e tratados com segurança. O UWear vai responder com as informações de saúde 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 primeiro projeto do Google". Consulte Como criar e gerenciar projetos para saber como recuperar o ID do projeto e a diferença entre ele, o nome e o número do projeto.
  2. Ative o faturamento dos projetos.
  3. Em um dos Cloud Shell do projeto do Google, defina as variáveis de ambiente 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 seguintes APIs nos dois 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. Recupere o identificador do 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 da Web de concessão de 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. Em um dos seus projetos do Google Cloud Cloud Shell, clone o repositório do codelab do Confidential Space no GitHub 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 seus IDs do USleep e do UWear. Não se esqueça de remover 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 as 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 seguintes variáveis com nomes de recursos de nuvem atuais. Se a variável estiver definida, o recurso de nuvem correspondente do projeto será usado. Se a variável não estiver definida, o nome do recurso do Cloud será gerado com base nos valores do script config_env.sh.
  1. Execute o script config_env.sh para definir os nomes de 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. Baixar o 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 Confidential Space. O UWear precisa baixar o 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. Gere a impressão digital do certificado raiz baixado.
openssl x509 -fingerprint -in confidential_space_root.pem -noout
  1. Verifique se a impressão digital corresponde ao resumo SHA-1 a seguir:
B9:51:20:74:2C:24:E3:AA:34:04:2E:1C:3B:A3:AA:D2:8B:21:23:21

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

Agora, crie duas contas de serviço: uma para as cargas de trabalho do USleep e outra para as 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 de usuário gere um token de comprovação.
  • Concede à conta de serviço da carga de trabalho a permissão de função logging.logWriter. 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 o encerramento da VM.

5. Criar carga de trabalho USleep

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

Sobre a carga de trabalho USleep

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

  1. Configurar uma sessão TLS e extrair o material de chaves 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 nonce 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 USleep

  1. Execute o script create_usleep_workload.sh para criar a carga de trabalho USleep. Este script:
  • Cria o Artifact Registry ($USLEEP_ARTIFACT_REPOSITORY) de propriedade da UWear em que 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 USleep.
  • Publica a imagem do Docker no Artifact Registry ($USLEEP_ARTIFACT_REPOSITORY) de propriedade da UWear.
  • Concede à 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 registros de saída, extraia o resumo da imagem para 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. Entrar na mesma sessão TLS criada na carga de trabalho do USleep e recuperar o token de atestado do USleep na 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. Validar o token independente:
  • Verificar se a declaração x5c contém uma cadeia de certificados que se encadeia corretamente do certificado folha ao intermediário e, finalmente, ao certificado raiz.
  • Verificar se o token está assinado pelo certificado de folha contido na declaração x5c.
  • Verificar se o certificado raiz baixado / armazenado é 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. Em seguida, a carga de trabalho do 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 da OPA. O OPA é um mecanismo de políticas de código aberto e uso geral que unifica a aplicação de políticas em toda a pilha. O OPA usa documentos com sintaxe semelhante ao JSON para definir valores de base que são usados para validar a política.
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 receber o hash do 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 e aprovadas, o UWear poderá confirmar que os dados serão enviados e tratados com segurança. O UWear vai responder com as informações de saúde 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 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 da UWear em que 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 USleep.
  • Publica a imagem do Docker no Artifact Registry ($UWEAR_ARTIFACT_REPOSITORY) de propriedade da UWear.
  • Concede à 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 USleep e 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 vai retornar um STATUS: RUNNING e o EXTERNAL_IP também será retornado de forma semelhante a este:

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 USleep foi executada corretamente

Para verificar se a carga de trabalho do USleep está sendo executada corretamente, navegue até 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". Depois que o servidor estiver funcionando, na parte de baixo dos registros, eles vão mostrar algo parecido com isto.

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

Executar 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 ver os registros da carga de trabalho do UWear, navegue até 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 deve ser assim:

No projeto UWear, os registros seriais vão 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 for assim, consulte as observações abaixo para instruções.

Ver os resultados do USleep

Para ver 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 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 da OPA para permitir o novo resumo da imagem. Vamos verificar se o UWear não enviará dados sensíveis para a carga de trabalho não autorizada.

A 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 de VM do 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 rejeitar o pedido 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. Criar a nova instância de VM USleep e executar 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. Extraia 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. Recrie a instância de VM do 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 seriais do UWear, a seguinte mensagem vai aparecer, e a VM USleep não vai 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).
  • A instância de computação do UWear
  • A conta de serviço USleep ($USLEEP_SERVICE_ACCOUNT).
  • O registro de artefatos USleep ($USLEEP_ARTIFACT_REPOSITORY).
  • A instância de computação USleep
./cleanup.sh

Se você já terminou de explorar, exclua o projeto seguindo estas instruções.

Parabéns

Parabéns, você concluiu o codelab.

Você aprendeu a compartilhar dados de forma segura e manter a confidencialidade deles usando o Espaço confidencial.

Qual é a próxima etapa?

Confira alguns codelabs semelhantes:

Leia mais