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

1. 概览

在开始之前,掌握以下功能和概念的实际运用知识对于本 Codelab 会很有帮助。

4670cd5427aa39a6

学习内容

本实验提供了一个参考实现,介绍如何使用 Confidential Space 执行符合 MPC 要求的区块链签名。为了说明这些概念,我们将演示以下场景:Company Primus 希望将数字资产转移到 Company Secundus。在这种情况下,Company Primus 使用符合 MPC 标准的模型,这意味着它们使用分布式密钥共享,而不是使用单独的私钥。这些主要份额由多方持有,在本示例中为小红和小刚。这种方法为 Company Primus 提供了多项优势,包括简化用户体验、提高运营效率和控制私钥。

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

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

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

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

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

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

  • 如何配置运行机密空间所需的 Cloud 资源
  • 如何根据以下特性授予对受保护资源的访问权限:
  • 定义:工作负载容器
  • 位置:机密空间环境(机密虚拟机上的机密空间映像)
  • 参与者:运行工作负载的账号
  • 如何在运行 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>
  • 为您的项目启用结算功能
  • 为这两个项目启用机密计算 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

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

$PRIMUS_KEYRING

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

$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" ]

注意:LABEL "tee.launch_policy.allow_cmd_override"="true"是映像作者设置的启动政策它允许操作员在执行工作负载时替换 CMD。默认情况下,allow_cmd_override 设置为 false。LABEL &quot;tee.launch_policy.allow_env_override&quot;用于告知 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 config set project $PRIMUS_PROJECT_ID
gcloud compute instances create-with-container mpc-lab-ethereum-node \
  --zone=${PRIMUS_PROJECT_ZONE}\
  --tags=http-server \
  --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 config set project $PRIMUS_PROJECT_ID
gcloud compute instances create $WORKLOAD_VM \
 --confidential-compute-type=SEV \
 --shielded-secure-boot \
 --maintenance-policy=TERMINATE \
 --scopes=cloud-platform \
 --zone=${PRIMUS_PROJECT_ZONE} \
 --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 mpc-lab-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. 点击“下载”可下载并查看交易响应。

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

查看 Ganache 区块链交易

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

  1. 前往 Cloud Compute Engine 页面。
  2. 点击 mpc-lab-ethereum-node VM
  3. 点击 SSH 以打开浏览器窗口中的 SSH 窗口。
  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_KEYRING)。
  • 用于访问受保护资源的服务账号 ($PRIMUS_SERVICEACCOUNT)。
  • 工作负载身份池 ($PRIMUS_WORKLOAD_IDENTITY_POOL)。
  • 工作负载服务账号 ($WORKLOAD_SERVICEACCOUNT)。
  • 工作负载计算实例。
  • 用于存储交易结果的结果存储分区。($PRIMUS_RESULT_STORAGE_BUCKET)。
  • 用于存储工作负载映像 ($PRIMUS_ARTIFACT_REPOSITORY) 的 Artifact Registry。
./cleanup.sh

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

  • 转到 Cloud Platform Console
  • 选择要关停的项目,然后点击“删除”顶部。此操作会安排删除项目。

后续操作

查看一些类似的 Codelab...

深入阅读