如何利用多方计算和 Confidential Space 交易数字资产

1. 概览

在开始之前,虽然不是完全必要,但如果您对以下功能和概念有一定了解,将有助于您学习本 Codelab。

4670cd5427aa39a6.png

学习内容

本实验提供了一个参考实现,用于使用保密空间执行符合 MPC 标准的区块链签名。为了说明这些概念,我们将介绍一个场景,其中 Primus 公司希望将数字资产转移到 Secundus 公司。在此方案中,Primus 公司使用符合 MPC 标准的模型,这意味着他们使用分布式密钥份额,而不是使用单独的私钥。这些密钥份额由多方(在本例中为 Alice 和 Bob)持有。此方法为 Primus 公司带来了多项好处,包括简化用户体验、提高运营效率以及控制私钥。

为了说明此流程的基本方面,我们将详细介绍技术设置,并引导您完成审批和签署流程,以启动从 Primus 公司到 Secundus 公司的数字资产转移。请注意,Bob 和 Alice 都是 Primus 公司的员工,他们必须批准此交易。

虽然此参考实现演示了签名操作,但并未涵盖 MPC 密钥管理的所有方面。例如,我们不会讨论密钥生成。此外,还有一些替代方法和补充方法,例如使用非 Google Cloud 服务生成共同签名,或者让共同签名者在自己的环境中构建区块链签名,这是一种更加去中心化的架构。我们希望本实验能激发您在 Google Cloud 上采用不同的 MPC 方法。

您将使用一个简单的工作负载,该工作负载使用共同签名人密钥材料在 Confidential Space 中对以太坊交易进行签名。以太坊交易签名是指用户在以太坊区块链上授权交易的过程。如需发送以太坊交易,您需要使用私钥对其进行签名。这可证明您是相应账号的所有者,并授权了相应交易。签名流程如下:

  1. 发件人创建一个交易对象,其中指定了收件人地址、要发送的 ETH 金额以及任何其他相关数据。
  2. 发件人的私钥用于对交易数据进行哈希处理。
  3. 然后,使用私钥对哈希进行签名。
  4. 签名附加到交易对象。
  5. 交易会广播到以太坊网络。

当网络上的节点收到交易时,会验证签名,以确保交易是由相应账号的所有者签名的。如果签名有效,节点会将交易添加到区块链。

首先,您需要配置必要的 Cloud 资源。然后,您将在 Confidential Space 中运行工作负载。本 Codelab 将引导您完成以下简要步骤:

  • 如何配置运行 Confidential Space 所需的 Cloud 资源
  • 如何根据以下属性授权对受保护资源的访问权限:
  • 什么:工作负载容器
  • 位置:Confidential Space 环境(机密虚拟机上的 Confidential Space 映像)
  • Who:运行工作负载的账号
  • 如何在运行 Confidential Space 虚拟机映像的机密虚拟机中运行工作负载

必需的 API

您必须在指定项目中启用以下 API,才能完成本指南。

API 名称

API 标题

cloudkms.googleapis.com

Cloud KMS

compute.googleapis.com

Compute Engine

confidentialcomputing.googleapis.com

机密计算

iamcredentials.googleapis.com

IAM

artifactregistry.googleapis.com

Artifact Registry

2. 设置 Cloud 资源

准备工作

  • 使用以下命令克隆 此代码库,以获取此 Codelab 中使用的必需脚本。
git clone https://github.com/GoogleCloudPlatform/confidential-space.git
  • 更改此 Codelab 的目录。
cd confidential-space/codelabs/digital_asset_transaction_codelab/scripts
  • 确保您已设置必需的项目环境变量,如下所示。如需详细了解如何设置 GCP 项目,请参阅 此 Codelab。您可以参阅这篇文章,详细了解如何检索项目 ID,以及项目 ID 与项目名称和项目编号有何不同。。
export PRIMUS_PROJECT_ID=<GCP project id>
  • 为您的项目启用结算功能
  • 为这两个项目启用 Confidential Computing API 和以下 API。
gcloud services enable \
   cloudapis.googleapis.com \
    cloudkms.googleapis.com \
    cloudresourcemanager.googleapis.com \
    cloudshell.googleapis.com \
    container.googleapis.com \
    containerregistry.googleapis.com \
    iam.googleapis.com \
    confidentialcomputing.googleapis.com
  • 如需设置资源名称的变量,您可以使用以下命令。请注意,此操作会替换公司 A 的 GCP 项目专属资源名称,例如 export PRIMUS_INPUT_STORAGE_BUCKET='primus-input-bucket'
  • 以下变量可针对公司 A 中的 GCP 项目进行设置:

$PRIMUS_INPUT_STORAGE_BUCKET

用于存储加密密钥的存储分区。

$PRIMUS_RESULT_STORAGE_BUCKET

用于存储 MPC 交易结果的存储分区。

$PRIMUS_KEY

用于加密存储在 $PRIMUS_INPUT_STORAGE_BUCKET 中的 Primus Bank 数据的 KMS 密钥。

$PRIMUS_KEYRING

将用于为 Primus Bank 创建加密密钥 $PRIMUS_KEY 的 KMS 密钥环。

$PRIMUS_WIP_PROVIDER

工作负载身份池提供方,其中包含用于 MPC 工作负载服务签名的令牌的属性条件。

$PRIMUS_SERVICEACCOUNT

$PRIMUS_WORKLOAD_IDENTITY_POOL 用于访问受保护资源的服务账号。此服务账号将有权查看存储在 $PRIMUS_INPUT_STORAGE_BUCKET 存储分区中的加密密钥。

$PRIMUS_ARTIFACT_REPOSITORY

用于存储工作负载容器映像的制品库。

$WORKLOAD_SERVICEACCOUNT

有权访问运行工作负载的保密虚拟机的服务账号。

$WORKLOAD_CONTAINER

运行工作负载的 Docker 容器。

$WORKLOAD_IMAGE_NAME

工作负载容器映像的名称。

$WORKLOAD_IMAGE_TAG

工作负载容器映像的标记。

  • 运行以下脚本,根据您的项目 ID 为资源名称设置剩余的变量名称值。
source config_env.sh

设置 Cloud 资源

在此步骤中,您将设置多方计算所需的云资源。在本实验中,您将使用以下私钥:0000000000000000000000000000000000000000000000000000000000000001

在生产环境中,您将生成自己的私钥。不过,在本实验中,我们将此私钥拆分为两个份额,并对每个份额进行加密。在生产场景中,绝不应将密钥存储在纯文本文件中。相反,私钥可以在 Google Cloud 之外生成(或完全跳过,并替换为自定义 MPC 密钥分片创建),然后进行加密,这样一来,任何人都无法访问私钥或密钥分片。在本实验中,我们将使用 Gcloud CLI。

运行以下脚本以设置所需的云资源。在执行这些步骤的过程中,系统将创建以下资源:

  • 一个 Cloud Storage 存储分区 ($PRIMUS_INPUT_STORAGE_BUCKET),用于存储加密的私钥份额。
  • 一个 Cloud Storage 存储分区 ($PRIMUS_RESULT_STORAGE_BUCKET),用于存储数字资产交易的结果。
  • KMS 中的加密密钥 ($PRIMUS_KEY) 和密钥环 ($PRIMUS_KEYRING),用于加密私钥份额。
  • 工作负载身份池 ($PRIMUS_WORKLOAD_IDENTITY_POOL),用于根据其提供方下配置的属性条件验证声明。
  • 附加到上述工作负载身份池 ($PRIMUS_WORKLOAD_IDENTITY_POOL) 的服务账号 ($PRIMUS_SERVICEACCOUNT),具有以下 IAM 访问权限:
  • roles/cloudkms.cryptoKeyDecrypter 使用 KMS 密钥解密数据。
  • objectViewer 以从 Cloud Storage 存储分区读取数据。
  • roles/iam.workloadIdentityUser,用于将此服务账号连接到工作负载身份池。
./setup_resources.sh

3. 创建工作负载

创建工作负载服务账号

现在,您将为工作负载创建一个具有所需角色和权限的服务账号。为此,请运行以下脚本,该脚本将为公司 A 创建工作负载服务账号。此服务账号将由运行工作负载的虚拟机使用。

工作负载服务账号 ($WORKLOAD_SERVICEACCOUNT) 将具有以下角色:

  • confidentialcomputing.workloadUser 以获取证明令牌
  • logging.logWriter 将日志写入 Cloud Logging。
  • objectViewer 用于从 $PRIMUS_INPUT_STORAGE_BUCKET Cloud Storage 存储分区读取数据。
  • objectUser 将工作负载结果写入 $PRIMUS_RESULT_STORAGE_BUCKET Cloud Storage 存储分区。
./create_workload_service_account.sh

创建工作负载

此步骤涉及创建工作负载 Docker 映像。此 Codelab 中的工作负载是一个简单的 Node.js MPC 应用,该应用使用加密的私钥份额为资产转移交易进行数字签名。此处是工作负载项目代码。工作负载项目包含以下文件。

package.json::此文件包含应为工作负载 MPC 应用使用的软件包列表。在本例中,我们使用的是 @google-cloud/kms、@google-cloud/storage、ethers 和 fast-crc32c 库。此处是我们将在此 Codelab 中使用的 package.json 文件。

index.js::这是工作负载应用的入口点,用于指定在工作负载容器启动时应运行哪些命令。我们还提供了一个未签名的交易示例,该交易通常由要求用户提供签名的不受信任的应用提供。此 index.js 文件还会从 mpc.js 导入函数,我们接下来将创建该文件。以下是 index.js 文件的内容,您也可以点击此处查看。

import {signTransaction, submitTransaction, uploadFromMemory} from './mpc.js';

const signAndSubmitTransaction = async () => {
  try {
    // Create the unsigned transaction object
    const unsignedTransaction = {
      nonce: 0,
      gasLimit: 21000,
      gasPrice: '0x09184e72a000',
      to: '0x0000000000000000000000000000000000000000',
      value: '0x00',
      data: '0x',
    };

    // Sign the transaction
    const signedTransaction = await signTransaction(unsignedTransaction);

    // Submit the transaction to Ganache
    const transaction = await submitTransaction(signedTransaction);

    // Write the transaction receipt
    uploadFromMemory(transaction);

    return transaction;
  } catch (e) {
    console.log(e);
    uploadFromMemory(e);
  }
};

await signAndSubmitTransaction();

mpc.js::在此处进行交易签名。它从 kms-decrypt 和 credential-config 导入函数,我们将在下文中介绍这些函数。以下是 mpc.js 文件的内容,您也可以点击此处查看。

import {Storage} from '@google-cloud/storage';
import {ethers} from 'ethers';

import {credentialConfig} from './credential-config.js';
import {decryptSymmetric} from './kms-decrypt.js';

const providers = ethers.providers;
const Wallet = ethers.Wallet;

// The ID of the GCS bucket holding the encrypted keys
const bucketName = process.env.KEY_BUCKET;

// Name of the encrypted key files.
const encryptedKeyFile1 = 'alice_encrypted_key_share';
const encryptedKeyFile2 = 'bob_encrypted_key_share';

// Create a new storage client with the credentials
const storageWithCreds = new Storage({
  credentials: credentialConfig,
});

// Create a new storage client without the credentials
const storage = new Storage();

const downloadIntoMemory = async (keyFile) => {
  // Downloads the file into a buffer in memory.
  const contents =
      await storageWithCreds.bucket(bucketName).file(keyFile).download();

  return contents;
};

const provider =
    new providers.JsonRpcProvider(`http://${process.env.NODE_URL}:80`);

export const signTransaction = async (unsignedTransaction) => {
  /* Check if Alice and Bob have both approved the transaction
  For this example, we're checking if their encrypted keys are available. */
  const encryptedKey1 =
      await downloadIntoMemory(encryptedKeyFile1).catch(console.error);
  const encryptedKey2 =
      await downloadIntoMemory(encryptedKeyFile2).catch(console.error);

  // For each key share, make a call to KMS to decrypt the key
  const privateKeyshare1 = await decryptSymmetric(encryptedKey1[0]);
  const privateKeyshare2 = await decryptSymmetric(encryptedKey2[0]);

  /* Perform the MPC calculations
  In this example, we're combining the private key shares
  Alternatively, you could import your mpc calculations here */
  const wallet = new Wallet(privateKeyshare1 + privateKeyshare2);

  // Sign the transaction
  const signedTransaction = await wallet.signTransaction(unsignedTransaction);

  return signedTransaction;
};

export const submitTransaction = async (signedTransaction) => {
  // This can now be sent to Ganache
  const hash = await provider.sendTransaction(signedTransaction);
  return hash;
};

export const uploadFromMemory = async (contents) => {
  // Upload the results to the bucket without service account impersonation
  await storage.bucket(process.env.RESULTS_BUCKET)
      .file('transaction_receipt_' + Date.now())
      .save(JSON.stringify(contents));
};

kms-decrypt.js:此文件包含使用 KMS 中管理的密钥进行解密的相应代码。以下是 kms-decrypt.js 文件的内容,您也可以点击此处查看。

import {KeyManagementServiceClient} from '@google-cloud/kms';
import crc32c from 'fast-crc32c';

import {credentialConfig} from './credential-config.js';

const projectId = process.env.PRIMUS_PROJECT_ID;
const locationId = process.env.PRIMUS_LOCATION;
const keyRingId = process.env.PRIMUS_ENC_KEYRING;
const keyId = process.env.PRIMUS_ENC_KEY;

// Instantiates a client
const client = new KeyManagementServiceClient({
  credentials: credentialConfig,
});

// Build the key name
const keyName = client.cryptoKeyPath(projectId, locationId, keyRingId, keyId);

export const decryptSymmetric = async (ciphertext) => {
  const ciphertextCrc32c = crc32c.calculate(ciphertext);
  const [decryptResponse] = await client.decrypt({
    name: keyName,
    ciphertext,
    ciphertextCrc32c: {
      value: ciphertextCrc32c,
    },
  });

  // Optional, but recommended: perform integrity verification on
  // decryptResponse. For more details on ensuring E2E in-transit integrity to
  // and from Cloud KMS visit:
  // https://cloud.google.com/kms/docs/data-integrity-guidelines
  if (crc32c.calculate(decryptResponse.plaintext) !==
      Number(decryptResponse.plaintextCrc32c.value)) {
    throw new Error('Decrypt: response corrupted in-transit');
  }

  const plaintext = decryptResponse.plaintext.toString();

  return plaintext;
};

credential-config.js:此文件存储了工作负载身份池路径以及服务账号模拟的详细信息。点击此处可查看我们将在此 Codelab 中使用的 credential-config.js 文件。

Dockerfile:最后,我们将创建用于构建工作负载 Docker 映像的 Dockerfile。定义 Dockerfile,如此处指定。

FROM node:16.18.0

ENV NODE_ENV=production

WORKDIR /app

COPY ["package.json", "package-lock.json*", "./"]

RUN npm install --production

COPY . .

LABEL "tee.launch_policy.allow_cmd_override"="true"
LABEL "tee.launch_policy.allow_env_override"="NODE_URL,RESULTS_BUCKET,KEY_BUCKET,PRIMUS_PROJECT_NUMBER,PRIMUS_PROJECT_ID,PRIMUS_WORKLOAD_IDENTITY_POOL,PRIMUS_WIP_PROVIDER,PRIMUS_SERVICEACCOUNT,PRIMUS_ENC_KEYRING,PRIMUS_ENC_KEY"

CMD [ "node", "index.js" ]

注意:Dockerfile 中的 LABEL "tee.launch_policy.allow_cmd_override"="true" 是映像作者设置的启动政策。它允许操作员在执行工作负载时替换 CMD。默认情况下,allow_cmd_override 设置为 false。标签“tee.launch_policy.allow_env_override”用于告知 Confidential Space 映像用户可以使用哪些环境变量。

运行以下脚本以创建工作负载,其中执行以下步骤:

  • 创建 Artifact Registry($PRIMUS_ARTIFACT_REPOSITORY) 以存储工作负载 Docker 映像。
  • 使用所需的资源名称更新工作负载代码。此处提供了本 Codelab 中使用的工作负载代码。
  • 创建 Dockerfile 以构建工作负载代码的 Docker 映像。您可以在此处找到 Dockerfile。
  • 构建 Docker 映像并将其发布到上一步中创建的 Artifact Registry ($PRIMUS_ARTIFACT_REPOSITORY)。
  • $PRIMUS_ARTIFACT_REPOSITORY 授予 $WORKLOAD_SERVICEACCOUNT 读取权限。这是必需的,以便工作负载容器从 Artifact Registry 中拉取工作负载 Docker 映像。
./create_workload.sh

创建区块链节点

Ganache 以太坊节点

在授权工作负载之前,我们需要创建以太坊 Ganache 实例。签名后的交易将提交给此 Ganache 实例。请记下此实例的 IP 地址。运行以下命令后,您可能需要输入 y 以启用 API。

gcloud compute instances create-with-container ${ETHEREUM_NODE} \
  --zone=${PRIMUS_PROJECT_ZONE} \
  --tags=http-server \
  --project=${PRIMUS_PROJECT_ID} \
  --shielded-secure-boot \
  --shielded-vtpm \
  --shielded-integrity-monitoring \
  --container-image=docker.io/trufflesuite/ganache:v7.7.3 \
--container-arg=--wallet.accounts=\"0x0000000000000000000000000000000000000000000000000000000000000001,0x21E19E0C9BAB2400000\" \
  --container-arg=--port=80

4. 授权并运行工作负载

授权工作负载

在此步骤中,我们将在工作负载身份池 ($PRIMUS_WORKLOAD_IDENTITY_POOL) 下设置工作负载身份池提供方。系统会为工作负载身份配置属性条件,如下所示。其中一个条件是验证工作负载映像是否正在从预期的制品库中拉取。

gcloud config set project $PRIMUS_PROJECT_ID
gcloud iam workload-identity-pools providers create-oidc ${PRIMUS_WIP_PROVIDER} \
 --location="${PRIMUS_PROJECT_LOCATION}" \
 --workload-identity-pool="$PRIMUS_WORKLOAD_IDENTITY_POOL" \
 --issuer-uri="https://confidentialcomputing.googleapis.com/" \
 --allowed-audiences="https://sts.googleapis.com" \
 --attribute-mapping="google.subject='assertion.sub'" \
 --attribute-condition="assertion.swname == 'CONFIDENTIAL_SPACE' && 'STABLE' in assertion.submods.confidential_space.support_attributes && assertion.submods.container.image_reference == '${PRIMUS_PROJECT_REPOSITORY_REGION}-docker.pkg.dev/$PRIMUS_PROJECT_ID/$PRIMUS_ARTIFACT_REPOSITORY/$WORKLOAD_IMAGE_NAME:$WORKLOAD_IMAGE_TAG' && '$WORKLOAD_SERVICEACCOUNT@$PRIMUS_PROJECT_ID.iam.gserviceaccount.com' in assertion.google_service_accounts"

运行工作负载

本部分介绍如何在机密虚拟机上运行工作负载。为此,我们将使用元数据标志传递所需的 TEE 实参。此外,我们还将使用“tee-env-*”标志为工作负载容器设置环境变量。该图片包含以下变量:

  • NODE_URL:将处理签名交易的以太坊节点的网址。
  • RESULTS_BUCKET:存储 MPC 交易结果的存储分区。
  • KEY_BUCKET:用于存储 MPC 加密密钥的存储分区。
  • PRIMUS_PROJECT_NUMBER:用于凭据配置文件的项目编号。
  • PRIMUS_PROJECT_ID:用于凭据配置文件的项目 ID。工作负载执行结果将发布到 $PRIMUS_RESULT_STORAGE_BUCKET
  • PRIMUS_WORKLOAD_IDENTITY_POOL:用于验证声明的工作负载身份池。
  • PRIMUS_WIP_POROVIDER:工作负载身份池提供方,其中包含用于验证工作负载所提供令牌的属性条件。
  • WORKLOAD_SERVICEACCOUNT:工作负载的服务账号。
gcloud compute instances create $WORKLOAD_VM \
 --confidential-compute-type=SEV \
 --shielded-secure-boot \
 --maintenance-policy=TERMINATE \
 --scopes=cloud-platform \
 --zone=${PRIMUS_PROJECT_ZONE} \
 --project=${PRIMUS_PROJECT_ID} \
 --image-project=confidential-space-images \
 --image-family=confidential-space \
 --service-account=$WORKLOAD_SERVICEACCOUNT@$PRIMUS_PROJECT_ID.iam.gserviceaccount.com \
 --metadata "^~^tee-image-reference=${PRIMUS_PROJECT_REPOSITORY_REGION}-docker.pkg.dev/$PRIMUS_PROJECT_ID/$PRIMUS_ARTIFACT_REPOSITORY/$WORKLOAD_IMAGE_NAME:$WORKLOAD_IMAGE_TAG~tee-restart-policy=Never~tee-env-NODE_URL=$(gcloud compute instances describe ${ETHEREUM_NODE} --format='get(networkInterfaces[0].networkIP)' --zone=${PRIMUS_PROJECT_ZONE})~tee-env-RESULTS_BUCKET=$PRIMUS_RESULT_STORAGE_BUCKET~tee-env-KEY_BUCKET=$PRIMUS_INPUT_STORAGE_BUCKET~tee-env-PRIMUS_PROJECT_ID=$PRIMUS_PROJECT_ID~tee-env-PRIMUS_PROJECT_NUMBER=$(gcloud projects describe $PRIMUS_PROJECT_ID --format="value(projectNumber)")~tee-env-PRIMUS_WORKLOAD_IDENTITY_POOL=$PRIMUS_WORKLOAD_IDENTITY_POOL~tee-env-PRIMUS_PROJECT_LOCATION=${PRIMUS_PROJECT_LOCATION}~tee-env-PRIMUS_WIP_PROVIDER=$PRIMUS_WIP_PROVIDER~tee-env-PRIMUS_SERVICEACCOUNT=$PRIMUS_SERVICEACCOUNT~tee-env-PRIMUS_KEY=${PRIMUS_KEY}~tee-env-PRIMUS_KEYRING=${PRIMUS_KEYRING}"

检查 Cloud Storage 结果

您可以在 Cloud Storage 中查看交易收据。保密空间可能需要几分钟时间才能启动并显示结果。当虚拟机处于停止状态时,表示容器已完成。

  1. 前往 Cloud Storage 浏览器页面。
  2. 点击 $PRIMUS_RESULT_STORAGE_BUCKET
  3. 点击 transaction_receipt 文件。
  4. 点击“下载”即可下载并查看交易响应。

或者,您也可以运行以下命令来查看结果。

gcloud config set project $PRIMUS_PROJECT_ID
gsutil cat gs://$PRIMUS_RESULT_STORAGE_BUCKET/transaction_receipt

注意:如果未显示结果,您可以前往 Compute Engine Cloud 控制台页面中的 $WORKLOAD_VM,然后点击“串行端口 1(控制台)”以查看日志。

检查 Ganache 区块链交易

您还可以在区块链日志中查看相应交易。

  1. 前往 Cloud Compute Engine 页面。
  2. 点击 ${ETHEREUM_NODE} VM
  3. 点击 SSH 以打开 SSH-in-browser 窗口。
  4. 在 SSH 窗口中,输入 sudo docker ps 以查看正在运行的 Ganache 容器。
  5. 查找 trufflesuite/ganache:v7.7.3 的容器 ID
  6. 输入 sudo docker logs CONTAINER_ID,并将 CONTAINER_ID 替换为 trufflesuite/ganache:v7.7.3 的 ID。
  7. 查看 Ganache 的日志,确认日志中列出了一笔交易。

5. 清理

点击此处可查看用于清理我们在本 Codelab 中创建的资源的脚本。在此清理过程中,以下资源将被删除:

  • 用于存储加密密钥份额的输入存储分区 ($PRIMUS_INPUT_STORAGE_BUCKET).
  • 加密密钥 ($PRIMUS_KEY)。
  • 用于访问受保护资源的服务账号 ($PRIMUS_SERVICEACCOUNT)。
  • 工作负载身份池 ($PRIMUS_WORKLOAD_IDENTITY_POOL)。
  • 工作负载服务账号 ($WORKLOAD_SERVICEACCOUNT)。
  • 工作负载计算实例($WORKLOAD_VM$ETHEREUM_NODE)。
  • 用于存储交易结果的结果存储分区。($PRIMUS_RESULT_STORAGE_BUCKET)。
  • 用于存储工作负载映像的制品注册表 ($PRIMUS_ARTIFACT_REPOSITORY)。
./cleanup.sh

如果您已完成探索,请考虑删除项目。

  • 前往 Cloud Platform 控制台
  • 选择要关停的项目,然后点击顶部的“删除”。这会安排删除相应项目。

后续操作

不妨查看以下类似 Codelab…

深入阅读