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

1. 概览

在开始之前,虽然不是完全必要,但如果您对以下功能和概念有一定的了解,将会对此 Codelab 有所帮助。

4670cd5427aa39a6.png

学习内容

本实验提供了一个参考实现,用于使用 Confidential Space 执行符合 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 Bank 在 $PRIMUS_INPUT_STORAGE_BUCKET 中存储的数据的 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

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

$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)。
  • $WORKLOAD_SERVICEACCOUNT 授予对 $PRIMUS_ARTIFACT_REPOSITORY 的读取权限。这对于让工作负载容器从 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 中查看交易收据。Confidential Space 可能需要几分钟才能启动并显示结果。当虚拟机处于停止状态时,您就会知道容器已完成。

  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
  • 用于存储工作负载映像的 Artifact Registry ($PRIMUS_ARTIFACT_REPOSITORY)。
./cleanup.sh

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

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

后续操作

查看下列类似 Codelab…

深入阅读