1. 總覽
在開始之前,雖然不是必要,但建議您先瞭解以下功能和概念,以利於完成本程式碼研究室。
課程內容
本實驗室提供參考實作項目,說明如何使用機密空間執行符合多方計算的區塊鏈簽署作業。為說明這些概念,我們將介紹以下情境:公司 Primus 想將數位資產轉移至公司 Secundus。在這個情況下,Primus 公司使用 MPC 相容的模型,也就是說,他們使用的是分散式金鑰共用項,而非個別的私密金鑰。這些金鑰分片由多方持有,在本例中為小莉和志明。這種做法可為 Primus 公司帶來多項優勢,包括簡化使用者體驗、提高作業效率,以及掌控私密金鑰。
為說明這個程序的基本面向,我們將詳細說明技術設定,並逐步引導您完成啟動數位資產從 Company Primus 轉移至 Company Secundus 的核准與簽署程序。請注意,Bob 和 Alice 都是 Primus 公司的員工,因此必須核准交易。
雖然這個參考實作項目會示範簽署作業,但並未涵蓋 MPC 金鑰管理的所有層面。舉例來說,我們不會討論金鑰產生作業。此外,還有其他替代和互補的方法,例如使用非 Google Cloud 服務產生共同簽署,或是讓共同簽署者在自己的環境中建構區塊鏈簽署,這是更去中心化的架構。我們希望這個實驗室能激發您在 Google Cloud 上使用 MPC 的不同做法。
您將使用簡單的工作負載,透過共同簽署者金鑰素材在機密空間中簽署以太坊交易。以太坊交易簽署為例,這是使用者在以太坊區塊鏈上授權交易的程序。如要傳送以太坊交易,您必須使用私密金鑰簽署交易。這可證明你是帳戶擁有者,並授權進行交易。簽署程序如下:
- 寄件者會建立交易物件,指定收件者地址、要傳送的 ETH 數量,以及任何其他相關資料。
- 使用者會使用發送者的私密金鑰對交易資料進行散列。
- 然後使用私密金鑰簽署散列值。
- 簽名會附加至交易物件。
- 交易會廣播至以太坊網路。
當網路上的節點收到交易時,會驗證簽名,確認簽名者是帳戶擁有者。如果簽章有效,節點就會將交易加入區塊鏈。
首先,您將設定必要的 Cloud 資源。接著,您將在 Confidential Space 中執行工作負載。本程式碼研究室會引導您完成下列高階步驟:
- 如何設定執行機密空間所需的雲端資源
- 如何根據下列屬性授權存取受保護的資源:
- 內容:工作負載容器
- 位置:機密空間環境 (機密 VM 上的機密空間映像檔)
- Who:執行工作負載的帳戶
- 如何在執行機密空間 VM 映像檔的機密 VM 中執行工作負載
必要 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. 設定雲端資源
事前準備
- 使用下列指令複製 這個存放區,取得用於本程式碼研究室的必要指令碼。
git clone https://github.com/GoogleCloudPlatform/confidential-space.git
- 變更本程式碼研究室的目錄。
cd confidential-space/codelabs/digital_asset_transaction_codelab/scripts
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 | 用於為 Primus Bank 加密 $PRIMUS_INPUT_STORAGE_BUCKET 中儲存的資料的 KMS 金鑰。 |
$PRIMUS_KEYRING | 用於為 Primus Bank 建立加密金鑰 $PRIMUS_KEY 的 KMS 鑰圈。 |
$PRIMUS_WIP_PROVIDER | Workload Identity Pool 提供者,其中包含用於由 MPC 工作負載服務簽署的權杖的屬性條件。 |
$PRIMUS_SERVICEACCOUNT | $PRIMUS_WORKLOAD_IDENTITY_POOL 用來存取受保護資源的服務帳戶。這個服務帳戶將具備查看 $PRIMUS_INPUT_STORAGE_BUCKET 值區中儲存的加密金鑰的權限。 |
$PRIMUS_ARTIFACT_REPOSITORY | 用於儲存工作負載容器映像檔的構件存放區。 |
$WORKLOAD_SERVICEACCOUNT | 服務帳戶具備執行工作負載的機密 VM 存取權。 |
$WORKLOAD_CONTAINER | 執行工作負載的 Docker 容器。 |
$WORKLOAD_IMAGE_NAME | 工作負載容器映像檔的名稱。 |
$WORKLOAD_IMAGE_TAG | 工作負載容器映像檔的標記。 |
- 執行下列指令碼,將剩餘的變數名稱設為資源名稱的值,並根據專案 ID 進行設定。
source config_env.sh
設定雲端資源
在這個步驟中,您將設定多方運算所需的雲端資源。本研究室會使用以下私密金鑰: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_SERVICEACCOUNT
) 已連結至上述工作負載身分集區 ($PRIMUS_WORKLOAD_IDENTITY_POOL
),並具備下列身分與存取權管理存取權: roles/cloudkms.cryptoKeyDecrypter
使用 KMS 金鑰解密資料。objectViewer
讀取 Cloud Storage 值區中的資料。roles/iam.workloadIdentityUser
,用於將此服務帳戶連結至工作負載身分池。
./setup_resources.sh
3. 建立工作負載
建立工作負載服務帳戶
接下來,您將為工作負載建立服務帳戶,並設定必要的角色和權限。如要這麼做,請執行以下指令碼,為公司 A 建立工作負載服務帳戶。執行工作負載的 VM 會使用這個服務帳戶。
工作負載服務帳戶 ($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 映像檔。這個程式碼研究室的工作負載是簡單的 Node.js MPC 應用程式,可使用加密的私密金鑰共用項目簽署數位交易,用於轉移資產。以下是工作負載專案程式碼。工作負載專案包含下列檔案。
package.json:此檔案包含應用於工作負載 MPC 應用程式的套件清單。在本例中,我們使用 @google-cloud/kms、@google-cloud/storage、ethers 和 fast-crc32c 程式庫。這裡是本程式碼研究室要使用的 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:此檔案會儲存工作負載身分池路徑和服務帳戶冒用功能的詳細資料。這裡是我們在本程式碼研究室中使用的 credential-config.js 檔案。
Dockerfile:最後,我們會建立 Dockerfile,用於建構工作負載 Docker 映像檔。請參閱這裡的說明,瞭解如何定義 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」會告知機密空間圖像使用者可使用的環境變數。
執行以下指令碼,建立工作負載,並執行下列步驟:
- 建立 Artifact Registry(
$PRIMUS_ARTIFACT_REPOSITORY
) 來儲存工作負載 Docker 映像檔。 - 請更新工作負載程式碼,加入必要的資源名稱。這裡是本程式碼研究室使用的負載程式碼。
- 建立 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. 授權及執行工作負載
授權工作負載
在這個步驟中,我們會在 workload identity pool ($PRIMUS_WORKLOAD_IDENTITY_POOL
) 下設定 workload identity pool 提供者。我們為 workload identity 設定了屬性條件,如下所示。其中一個條件是驗證工作負載映像檔是否從預期的構件存放區提取。
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"
執行工作負載
本節將說明如何在機密 VM 上執行工作負載。為此,我們會使用中繼資料標記傳遞必要的 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 Identity Pool 提供者,其中包含用於驗證工作負載提供的權杖的屬性條件。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 中查看交易收據。系統可能需要幾分鐘的時間才能啟動機密空間,並顯示結果。當 VM 處於停止狀態時,您就會知道容器已完成。
- 前往「Cloud Storage Browser」(Cloud Storage 瀏覽器) 頁面。
- 按一下「
$PRIMUS_RESULT_STORAGE_BUCKET
」。 - 按一下
transaction_receipt
檔案。 - 按一下「Download」下載並查看交易回應。
或者,您也可以執行下列指令來查看結果。
gcloud config set project $PRIMUS_PROJECT_ID
gsutil cat gs://$PRIMUS_RESULT_STORAGE_BUCKET/transaction_receipt
注意:如果沒有顯示結果,您可以前往 Compute Engine 雲端控制台頁面中的 $WORKLOAD_VM,然後按一下「Serial port 1 (console)」(序列埠 1 (主控台)) 查看記錄。
檢查 Ganache 區塊鏈交易
您也可以在區塊鏈記錄中查看交易。
- 前往 Cloud Compute Engine 頁面。
- 按一下
${ETHEREUM_NODE}
VM
。 - 按一下
SSH
開啟瀏覽器中的 SSH 視窗。 - 在 SSH 視窗中輸入
sudo docker ps
,查看執行中的 Ganache 容器。 - 找出
trufflesuite/ganache:v7.7.3
的容器 ID - 輸入
sudo docker logs CONTAINER_ID
,並將 CONTAINER_ID 替換為trufflesuite/ganache:v7.7.3
的 ID。 - 查看 Ganache 的記錄,確認記錄中是否列有交易。
5. 清理
這裡提供的程式碼可用於清除我們在本程式碼研究室中建立的資源。在本次清理作業中,系統會刪除下列資源:
- 輸入用於儲存加密金鑰分片的儲存空間值區 (
$PRIMUS_INPUT_STORAGE_BUCKET)
)。 - 加密金鑰 (
$PRIMUS_KEY
)。 - 用於存取受保護資源的服務帳戶 (
$PRIMUS_SERVICEACCOUNT
)。 - Workload Identity 集區 (
$PRIMUS_WORKLOAD_IDENTITY_POOL
)。 - 工作負載服務帳戶 (
$WORKLOAD_SERVICEACCOUNT
)。 - 工作負載運算執行個體 (
$WORKLOAD_VM
和$ETHEREUM_NODE
)。 - 用於儲存交易結果的結果儲存值區。(
$PRIMUS_RESULT_STORAGE_BUCKET
)。 - 用於儲存工作負載映像檔 (
$PRIMUS_ARTIFACT_REPOSITORY
) 的構件登錄。
./cleanup.sh
如果您已完成探索,請考慮刪除專案。
- 前往 Cloud Platform Console
- 選取要關閉的專案,然後點選頂端的「刪除」。這會排定刪除專案的時間。
後續步驟
查看一些類似的程式碼研究室…