Multi-party Compute 使用者指南

本指南將逐步說明多方運算 API 的使用者歷程,協助您管理分散式金鑰、使用者、團隊和政策。適用於想瞭解主要使用者歷程的預先發布版客戶。

總覽

MPC API 可管理執行個體、團隊、使用者、金鑰環、分散式金鑰和門檻簽署儀式的生命週期,並透過三個政策層強制執行內建的加密編譯管理機制:

  • 管理政策 (角色法定人數):管理員會將使用者和團隊加入系統,且必須有法定人數的管理員 (EnableUserRole和ApproveRoleChange) 才能授予或撤銷管理員和政策作者角色。授予簽署者角色時,單一管理員即可立即生效,因為簽署者本身無法存取現有金鑰;只有在產生金鑰時,簽署者包含在金鑰環的金鑰政策中,才會收到金鑰共用。
  • 金鑰政策 (KeyRing.keyPolicy):管理員在建立 KeyRing 時,會定義 KeyRing 的 KeyPolicy,指定哪些簽署者 (簽署者,分組為團隊) 持有金鑰共用,以及授權簽章所需的 K-of-N 簽署者核准 (CreateTransaction 和 RecordSignerDecision)。
  • 交易政策 (TransactionPolicy 和 KeyList):政策作者 (POLICY_AUTHOR) 的法定人數提議、核准或停用 Cedar permit 和 forbid 規則 (TransactionPolicy) 和 KeyList 位址許可清單。系統會先根據有效的交易政策 (PERMIT 或 DENY) 評估每個 CreateTransaction 要求,再進行簽署。

必要條件

開始前,請確認您具備以下項目:

  1. A Google Cloud 專案。如要建立專案,請參閱「建立專案」。
  2. 如果未使用 Cloud Shell,請安裝 gcloud CLI 並完成驗證。
  3. 啟用必要的 API (如果尚未啟用)。您需要啟用兩個 API:本指南使用的 MPC API,以及用於產生及簽署授權金鑰的 Cloud Key Management Service。

      gcloud services enable mpc.googleapis.com cloudkms.googleapis.com
    

    如要查看已啟用的 API,請按照下列步驟操作:

      gcloud services list --enabled
    
  4. 已安裝 Python 3.9 以上版本和教學課程輔助程式庫:

      pip install embit eth-account eth-utils requests
    
  5. (選用) 如果貴組織使用外部身分識別提供者 (例如 Okta 或 Microsoft Entra ID) 進行驗證,而非 Google 帳戶,請參閱「設定員工身分聯盟 (BYOID)」,瞭解如何使用聯合身分設定應用程式預設憑證 (ADC)。

設定

更新下列變數,填入本指南中的指令。

必要設定

您必須自訂這些參數。如果使用預留位置值,指南將無法運作。

參數 值 說明
專案 ID my-project MPC 執行個體的消費者專案 ID。
執行個體 ID my-mpc-instance MPC 執行個體的 ID ([a-z]([a-z0-9-]{0,61}[a-z0-9])?)。
位置 us-central1 部署位置 (例如 us-central1)。
管理員 1 主體 admin-1@example.com 執行個體中第一個 ADMIN 使用者的Google Cloud 電子郵件或 BYOID 主體。
管理員 2 主體 admin-2@example.com 執行個體中第二位 ADMIN 使用者的Google Cloud 電子郵件或 BYOID 主體。
簽署者 1 主體 alice@example.com 簽署者 1 (Alice) 的Google Cloud 電子郵件或 BYOID 主體。
簽署人 2 主體 bob@example.com Google Cloud 電子郵件地址或BYOID 主體。
簽署者 3 主體 charlie@example.com 簽署者 3 (Charlie) 的Google Cloud 電子郵件或 BYOID 主體。
政策作者 1 主體 dave@example.com Google Cloud 政策作者 1 (Dave) 的電子郵件地址或 BYOID 主體。
政策作者 2 主體 eve@example.com Google Cloud 政策作者 2 (Eve) 的電子郵件地址或 BYOID 主體。

在終端機工作階段中驗證每個帳戶一次,這樣 gcloud auth print-access-token "<principal>" 就能擷取任何角色的權杖,不必在教學課程中切換帳戶 (ADMIN_1_PRINCIPAL 是最後驗證的帳戶,因此仍是 IAM 和 Cloud KMS 指令的作用中 gcloud CLI 帳戶):

for PRINCIPAL in \
  "admin-2@example.com" \
  "alice@example.com" \
  "bob@example.com" \
  "charlie@example.com" \
  "dave@example.com" \
  "eve@example.com" \
  "admin-1@example.com"; do
  gcloud auth login "$PRINCIPAL"
done

教學課程資源標籤

這些是本教學課程中使用 Cloud KMS 建立的授權金鑰本機 ID。預設值有效,可保留以盡量減少設定阻礙。請注意,Team、User、TransactionPolicy、KeyRing、Key、KeyList 和 Transaction 資源的資源 ID 是由伺服器產生,並在建立各項資源時擷取至殼層變數 (TEAM_NAME、SIGNER_1_NAME、POLICY_NAME、KEY_RING_NAME、KEY_NAME、KEY_LIST_NAME、TRANSACTION_NAME)。

參數 值 說明
Cloud KMS 金鑰環 ID mpc-assent-keys Cloud Key Management Service KeyRing,用於儲存在 Cloud KMS 的外部授權金鑰。
Cloud KMS 管理員 1 ID admin-1 MPC 管理員 1 授權金鑰標籤。
Cloud KMS 管理員 2 ID admin-2 MPC 管理員 2 授權金鑰標籤。
Cloud KMS 簽署者 1 ID signer-alice MPC 簽署者 1 授權金鑰標籤。
Cloud KMS 簽署者 2 ID signer-bob MPC 簽署者 2 授權金鑰標籤。
Cloud KMS 簽署者 3 ID signer-charlie MPC 簽署者 3 授權金鑰標籤。
Cloud KMS 政策作者 1 ID author-dave 政策作者 1 授權金鑰標籤。
Cloud KMS 政策作者 2 ID author-eve 政策作者 2 授權金鑰標籤。

步驟 1:建立 MPC 執行個體

MPC Instance 代表專屬的隔離 MPC 環境。您必須具備專案的多方運算管理員 (roles/mpc.admin) IAM 角色,才能建立及管理執行個體。您可以使用控制層端點 (https://mpc.googleapis.com) 上的 MPCInstanceService,以管理員身分建立及檢查 Instance,並將所有後續資源要求傳送至執行個體的專屬資料層網址 (INSTANCE_URL)。

A. 建立執行個體

首先,請確認管理員主體 (admin-1@example.com 和 admin-2@example.com) 具備專案的 roles/mpc.admin 權限,並以管理員 1 的身分呼叫 instances.create (CreateInstance),佈建 MPC 執行個體:

curl

grant_iam_role() {
  local principal=$1
  local iam_role=$2
  local member="user:${principal}"

  # BYOID principals already start with "principal://"
  if [[ "$principal" == principal://* ]]; then
    member="${principal}"
  fi

  echo "Granting ${iam_role} to ${member}..."
  gcloud projects add-iam-policy-binding "my-project" \
    --member="${member}" \
    --role="${iam_role}" \
    --condition=None > /dev/null
}

for ADMIN_PRINCIPAL in "admin-1@example.com" "admin-2@example.com"; do
  grant_iam_role "$ADMIN_PRINCIPAL" "roles/mpc.admin"
done

export INSTANCE_NAME="projects/my-project/locations/us-central1/instances/my-mpc-instance"

# Check existence
if curl -s -f -o /dev/null -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "https://mpc.googleapis.com/v1beta/${INSTANCE_NAME}"; then
  echo "Instance 'my-mpc-instance' already exists. Skipping creation."
else
  echo "Instance 'my-mpc-instance' not found. Creating..."
  curl -X POST \
    -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d '{
      "displayName": "Tutorial MPC Instance"
    }' \
    "https://mpc.googleapis.com/v1beta/projects/my-project/locations/us-central1/instances?instanceId=my-mpc-instance" | jq
fi

這會傳回長時間執行的作業 (LRO)。您必須等待執行個體 state 變成 ACTIVE,才能呼叫其資料層網址。

🕵️‍♂️ 檢查執行個體狀態 (輪詢)

輪詢執行個體狀態,直到 state 變成 ACTIVE,然後將 url 欄位儲存至 INSTANCE_URL:

curl

INSTANCE_INFO=$(curl -s -X GET \
  -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "https://mpc.googleapis.com/v1beta/${INSTANCE_NAME}")

echo "$INSTANCE_INFO" | jq
export INSTANCE_URL=$(echo "$INSTANCE_INFO" | jq -r '.url')
echo "Instance Data Plane URL: $INSTANCE_URL"

步驟 2:產生授權金鑰

如要使用 MPC API,您必須為下列不同角色產生金鑰組。請注意,這些是外部加密授權金鑰 (User.authorizationKey),也就是說,您是在 MPC 執行個體外部產生及保管這些金鑰,因此與建立金鑰時產生的 MPC 金鑰共用不同:

  • 管理員:授權變更ADMIN和POLICY_AUTHOR使用者角色,以及建立Key。
  • 政策作者:授權管理政策 (TransactionPolicy)。
  • 簽署者:提供交易授權 (AUTHORIZE 或 DENY)。

為簽署者、政策作者和管理員產生 secp256k1 加密授權金鑰。這些指令具有冪等性。

Cloud KMS

建議您在 Cloud Key Management Service 中管理授權金鑰。確認您的身分具備建立金鑰環、建立金鑰、簽署及查看公開金鑰驗證器的權限 (例如 roles/cloudkms.admin 和 roles/cloudkms.signerVerifier)。確認您已建立 KeyRing,然後為每位簽署者、政策作者和管理員產生 secp256k1 金鑰。

provision_keyring() {
  if gcloud kms keyrings describe "mpc-assent-keys" --location="us-central1" > /dev/null 2>&1; then
    echo "KeyRing 'mpc-assent-keys' already exists."
  else
    echo "KeyRing 'mpc-assent-keys' not found. Creating it..."
    gcloud kms keyrings create "mpc-assent-keys" --location="us-central1"
  fi
}

provision_kms_key() {
  local id=$1
  if gcloud kms keys describe "${id}-key" --keyring="mpc-assent-keys" --location="us-central1" > /dev/null 2>&1; then
    echo "KMS key '${id}-key' already exists."
  else
    echo "KMS key '${id}-key' not found. Creating it..."
    gcloud kms keys create "${id}-key" \
      --keyring="mpc-assent-keys" \
      --location="us-central1" \
      --purpose=asymmetric-signing \
      --default-algorithm=ec-sign-secp256k1-sha256 \
      --protection-level=hsm
  fi

  gcloud kms keys versions get-public-key 1 \
    --key="${id}-key" \
    --keyring="mpc-assent-keys" \
    --location="us-central1" \
    --output-file="${id}_pub.pem"

  echo "Retrieved public key from KMS and saved to ${id}_pub.pem:"
  cat "${id}_pub.pem"
}

# Run Setup
provision_keyring

# Generate Keys for Signers, Policy Authors, and Admins
for ID in \
  signer-alice \
  signer-bob \
  signer-charlie \
  author-dave \
  author-eve \
  admin-1 \
  admin-2; do
  echo "Generating KMS key for $ID..."
  provision_kms_key "$ID"
done

OpenSSL

您可以使用標準 OpenSSL,為每位簽署者、政策作者和管理員產生 secp256k1 金鑰。

provision_openssl_key() {
  local id=$1
  if [[ ! -f "${id}_priv.pem" ]]; then
    openssl ecparam -name secp256k1 -genkey -noout -out "${id}_priv.pem"
    openssl ec -in "${id}_priv.pem" -pubout -out "${id}_pub.pem"
    echo "Wrote private key to ${id}_priv.pem and public key to ${id}_pub.pem:"
    cat "${id}_pub.pem"
  else
    echo "Private key file '${id}_priv.pem' already exists. Skipping OpenSSL generation."
    echo "Using existing public key from '${id}_pub.pem':"
    cat "${id}_pub.pem"
  fi
}

for ID in \
  signer-alice \
  signer-bob \
  signer-charlie \
  author-dave \
  author-eve \
  admin-1 \
  admin-2; do
  echo "Generating OpenSSL key for $ID..."
  provision_openssl_key "$ID"
done

步驟 3:註冊團隊和使用者

任何實體都必須具備專案的 IAM 權限,並在執行個體中註冊為應用程式層級的 User,才能參與交易簽署 (做為 SIGNER) 或政策編寫 (做為 POLICY_AUTHOR)。如要瞭解 IAM 角色和 User 角色如何搭配運作,請參閱「安全性與授權」。

註冊時,管理員和使用者必須透過頻外方式協調:

  1. 實體 (簽署者或政策作者) 在本機產生金鑰組,並將公開金鑰安全地轉移給管理員。
  2. 管理員在專案中授予使用者主體適當的 IAM 角色 (簽署者和政策作者為 roles/mpc.editor;管理員為 roles/mpc.admin),並呼叫 MPC API 來註冊使用者的 principal 和 authorizationKey。

A. 建立簽署者團隊

每個具有 SIGNER 角色的 User 都必須屬於 Team,而所有其他具有其他角色的 User 則不得屬於 Team。以管理員 1 (admin-1@example.com) 的身分,建立「Treasury Signers」Team (CreateTeam) 來存放三位簽署者,並擷取伺服器產生的資源 name:

curl

TEAM_RESPONSE=$(curl -s -X POST \
  -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  -H "Content-Type: application/json" \
  -d '{
    "displayName": "Treasury Signers",
  }' \
  "${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/teams?requestId=11111111-1111-4111-8111-111111111101")

echo "$TEAM_RESPONSE" | jq
export TEAM_NAME=$(echo "$TEAM_RESPONSE" | jq -r '.name')
echo "Created Team: $TEAM_NAME"

B. 授予 IAM 角色並註冊簽署者和政策作者

每位使用者都需要專案層級 IAM 角色 (簽署者和政策作者為 roles/mpc.editor,管理員為 roles/mpc.admin),才能呼叫 MPC API 和執行個體中的應用程式層級 User 資源。呼叫 users.create (CreateUser) 時,roles 欄位只能留空或設為 ["SIGNER"]:

  • 簽署者:在 IAM 中授予 roles/mpc.editor,並使用 roles: ["SIGNER"] 和 team: "$TEAM_NAME" 建立。SIGNER 角色會立即變成 ACTIVE,不需經過管理員仲裁核准,因為單獨擁有 SIGNER 角色並不會授予任何現有金鑰的存取權;簽署者只有在金鑰產生期間,被納入 KeyRing 的 KeyPolicy 時,才會收到金鑰共用。
  • 政策作者:在 IAM 中授予 roles/mpc.editor,建立時沒有初始角色和 team,然後在步驟 3.C 中使用管理員法定人數核准授予 POLICY_AUTHOR 角色 (因為 POLICY_AUTHOR 和 ADMIN 具有執行個體層級的管理權限)。

curl

provision_user() {
  local id=$1
  local display_name=$2
  local principal=$3
  local request_id=$4
  local team=$5
  local pubkey_b64
  local signer_fields=""

  # 1. Grant project-level IAM permission (roles/mpc.editor) to the User's principal
  grant_iam_role "$principal" "roles/mpc.editor" >&2

  # 2. Read the public key from the file generated in Step 2 (Base64-encoded PEM/DER bytes)
  pubkey_b64=$(openssl base64 -A -in "${id}_pub.pem")

  if [[ -n "$team" ]]; then
    signer_fields=", \"roles\": [\"SIGNER\"], \"team\": \"${team}\""
  fi

  # 3. Register the application-level User in the MPC instance as Admin 1
  curl -s -X POST \
    -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d "{
      \"displayName\": \"${display_name}\",
      \"principal\": \"${principal}\",
      \"authorizationKey\": {
        \"publicKey\": \"${pubkey_b64}\",
        \"algorithm\": \"PUBLIC_KEY_SIGNATURE_ALGORITHM_ECDSA_SECP256K1\"
      }${signer_fields}
    }" \
    "${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/users?requestId=${request_id}"
}

echo "Registering Signers..."
export SIGNER_1_NAME=$(provision_user "signer-alice" "Signer Alice" "alice@example.com" "11111111-1111-4111-8111-111111111001" "${TEAM_NAME}" | tee /dev/stderr | jq -r '.name')
export SIGNER_2_NAME=$(provision_user "signer-bob" "Signer Bob" "bob@example.com" "11111111-1111-4111-8111-111111111002" "${TEAM_NAME}" | tee /dev/stderr | jq -r '.name')
export SIGNER_3_NAME=$(provision_user "signer-charlie" "Signer Charlie" "charlie@example.com" "11111111-1111-4111-8111-111111111003" "${TEAM_NAME}" | tee /dev/stderr | jq -r '.name')

echo "Registering Policy Authors..."
export POLICY_AUTHOR_1_NAME=$(provision_user "author-dave" "Author Dave" "dave@example.com" "11111111-1111-4111-8111-111111111004" | tee /dev/stderr | jq -r '.name')
export POLICY_AUTHOR_2_NAME=$(provision_user "author-eve" "Author Eve" "eve@example.com" "11111111-1111-4111-8111-111111111005" | tee /dev/stderr | jq -r '.name')

C. 授予政策作者角色 (管理員法定人數核准)

如要啟用或停用User的POLICY_AUTHOR或ADMIN角色,必須取得管理員法定人數同意:

  1. 管理員 1 呼叫 users.enableRole (EnableUserRole),將角色移至 PENDING_ACTIVE,並將管理員 1 的簽章記錄為第一項核准。
  2. 管理員 2 使用簽名呼叫 users.approveRoleChange (ApproveRoleChange),將角色轉換為 ACTIVE。

Cloud KMS

1. 以管理員 1 (EnableUserRole) 的身分要求 POLICY_AUTHOR

sign_role_change() {
  local admin_id=$1
  local target_id=$2
  local principal=$3
  python3 -c 'import sys; inst, prin, pub = sys.argv[1], sys.argv[2], open(sys.argv[3], "rb").read(); v = lambda n: bytes([n & 127 | 128]) + v(n >> 7) if n > 127 else bytes([n]); f = lambda k, b: bytes([k << 3 | 2]) + v(len(b)) + b; open("role_payload.bin", "wb").write(f(1, inst.encode()) + f(2, prin.encode()) + f(3, f(1, pub) + b"\x10\x01") + f(4, b"\x08\x02\x10\x01\x18\x01"))' "${INSTANCE_NAME}" "${principal}" "${target_id}_pub.pem"
  gcloud kms asymmetric-sign \
    --version=1 \
    --key="${admin_id}-key" \
    --keyring="mpc-assent-keys" \
    --location="us-central1" \
    --digest-algorithm=sha256 \
    --input-file=role_payload.bin \
    --signature-file=role_sig.bin > /dev/null
  openssl base64 -A -in role_sig.bin
}

enable_policy_author() {
  local target_user=$1
  local target_id=$2
  local target_principal=$3

  echo "Admin 1 requesting POLICY_AUTHOR for ${target_user}..."
  local admin_1_sig=$(sign_role_change "admin-1" "${target_id}" "${target_principal}")
  local signed_payload_b64=$(openssl base64 -A -in role_payload.bin)

  curl -s -X POST \
    -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d "{
      \"role\": \"POLICY_AUTHOR\",
      \"authorizationInfo\": {
        \"signature\": \"${admin_1_sig}\",
        \"signedPayload\": \"${signed_payload_b64}\"
      }
    }" \
    "${INSTANCE_URL}/v1beta/${target_user}:enableRole" | jq
}

enable_policy_author "${POLICY_AUTHOR_1_NAME}" "author-dave" "dave@example.com"
enable_policy_author "${POLICY_AUTHOR_2_NAME}" "author-eve" "eve@example.com"

2. POLICY_AUTHOR以管理員 2 (ApproveRoleChange) 的身分核准

approve_policy_author() {
  local target_user=$1
  local target_id=$2
  local target_principal=$3

  echo "Admin 2 approving POLICY_AUTHOR for ${target_user}..."
  local admin_2_sig=$(sign_role_change "admin-2" "${target_id}" "${target_principal}")
  local signed_payload_b64=$(openssl base64 -A -in role_payload.bin)

  curl -s -X POST \
    -H "Authorization: Bearer $(gcloud auth print-access-token "admin-2@example.com")" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d "{
      \"authorizationInfo\": {
        \"signature\": \"${admin_2_sig}\",
        \"signedPayload\": \"${signed_payload_b64}\"
      }
    }" \
    "${INSTANCE_URL}/v1beta/${target_user}:approveRoleChange" | jq
}

approve_policy_author "${POLICY_AUTHOR_1_NAME}" "author-dave" "dave@example.com"
approve_policy_author "${POLICY_AUTHOR_2_NAME}" "author-eve" "eve@example.com"

OpenSSL

1. 以管理員 1 (EnableUserRole) 的身分要求 POLICY_AUTHOR

sign_role_change() {
  local admin_id=$1
  local target_id=$2
  local principal=$3
  python3 -c 'import sys; inst, prin, pub = sys.argv[1], sys.argv[2], open(sys.argv[3], "rb").read(); v = lambda n: bytes([n & 127 | 128]) + v(n >> 7) if n > 127 else bytes([n]); f = lambda k, b: bytes([k << 3 | 2]) + v(len(b)) + b; open("role_payload.bin", "wb").write(f(1, inst.encode()) + f(2, prin.encode()) + f(3, f(1, pub) + b"\x10\x01") + f(4, b"\x08\x02\x10\x01\x18\x01"))' "${INSTANCE_NAME}" "${principal}" "${target_id}_pub.pem"
  openssl dgst -sha256 -sign "${admin_id}_priv.pem" -out role_sig.bin role_payload.bin
  openssl base64 -A -in role_sig.bin
}

enable_policy_author() {
  local target_user=$1
  local target_id=$2
  local target_principal=$3

  echo "Admin 1 requesting POLICY_AUTHOR for ${target_user}..."
  local admin_1_sig=$(sign_role_change "admin-1" "${target_id}" "${target_principal}")
  local signed_payload_b64=$(openssl base64 -A -in role_payload.bin)

  curl -s -X POST \
    -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d "{
      \"role\": \"POLICY_AUTHOR\",
      \"authorizationInfo\": {
        \"signature\": \"${admin_1_sig}\",
        \"signedPayload\": \"${signed_payload_b64}\"
      }
    }" \
    "${INSTANCE_URL}/v1beta/${target_user}:enableRole" | jq
}

enable_policy_author "${POLICY_AUTHOR_1_NAME}" "author-dave" "dave@example.com"
enable_policy_author "${POLICY_AUTHOR_2_NAME}" "author-eve" "eve@example.com"

2. POLICY_AUTHOR以管理員 2 (ApproveRoleChange) 的身分核准

approve_policy_author() {
  local target_user=$1
  local target_id=$2
  local target_principal=$3

  echo "Admin 2 approving POLICY_AUTHOR for ${target_user}..."
  local admin_2_sig=$(sign_role_change "admin-2" "${target_id}" "${target_principal}")
  local signed_payload_b64=$(openssl base64 -A -in role_payload.bin)

  curl -s -X POST \
    -H "Authorization: Bearer $(gcloud auth print-access-token "admin-2@example.com")" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d "{
      \"authorizationInfo\": {
        \"signature\": \"${admin_2_sig}\",
        \"signedPayload\": \"${signed_payload_b64}\"
      }
    }" \
    "${INSTANCE_URL}/v1beta/${target_user}:approveRoleChange" | jq
}

approve_policy_author "${POLICY_AUTHOR_1_NAME}" "author-dave" "dave@example.com"
approve_policy_author "${POLICY_AUTHOR_2_NAME}" "author-eve" "eve@example.com"

步驟 4:設定交易政策

建立並核准 TransactionPolicy (以及選用的 KeyList),設定交易政策。

A. 建立政策 (PENDING_ACTIVE)

建立 Cedar 政策規則,說明允許哪些交易。您可以設定多項政策來管理不同情境,但本指南僅示範如何設定以太幣或比特幣交易政策。根據預設,交易政策評估為「快速失敗 (預設封鎖)」;如果沒有任何有效的 permit 政策與交易相符,政策評估決策為 DENY。如要進一步瞭解 Cedar 屬性,請參閱「編寫交易政策規則」。

Ethereum、KMS

這項規則允許將較小金額的以太幣 (<= 2 ETH,以 10-9 ETH / Gwei 為單位,表示為 2,000,000,000) 轉移至特定收款人地址 (0x1111111111111111111111111111111111111111)。

permit (principal, action, resource)
when {
  context.schema == "ETHEREUM_TX_V2" &&
  context.secondary_key == "0x1111111111111111111111111111111111111111" &&
  context has amount_token_scaled &&
  context.amount_token_scaled <= 2000000000
};
sign_policy() {
  local author_id=$1
  local resource_type=$2
  local resource_name=$3
  local action=$4
  local cedar_doc=$5
  python3 -c 'import sys; rt, rn, act, doc = sys.argv[1:]; v = lambda n: bytes([n & 127 | 128]) + v(n >> 7) if n > 127 else bytes([n]); f = lambda k, b: bytes([k << 3 | 2]) + v(len(b)) + b; open("policy_payload.bin", "wb").write(f(1 if rt == "instance" else 2, rn.encode()) + bytes([24, 1 if act == "ACTIVATE" else 2]) + f(4, f(1, doc.encode())))' "${resource_type}" "${resource_name}" "${action}" "${cedar_doc}"
  gcloud kms asymmetric-sign \
    --version=1 \
    --key="${author_id}-key" \
    --keyring="mpc-assent-keys" \
    --location="us-central1" \
    --digest-algorithm=sha256 \
    --input-file=policy_payload.bin \
    --signature-file=policy_sig.bin > /dev/null
  openssl base64 -A -in policy_sig.bin
}

export CEDAR_RULE='permit (principal, action, resource) when { context.schema == "ETHEREUM_TX_V2" && context.secondary_key == "0x1111111111111111111111111111111111111111" && context has amount_token_scaled && context.amount_token_scaled <= 2000000000 };'

# Sign as Policy Author 1
QUORUM_SIGNATURE=$(sign_policy "author-dave" "instance" "${INSTANCE_NAME}" "ACTIVATE" "${CEDAR_RULE}")
POLICY_PAYLOAD_B64=$(openssl base64 -A -in policy_payload.bin)

POLICY_RESPONSE=$(jq -n \
  --arg rule "$CEDAR_RULE" \
  --arg sig "$QUORUM_SIGNATURE" \
  --arg payload "$POLICY_PAYLOAD_B64" \
  '{
    transactionPolicy: {
      displayName: "Ethereum Transfer Permit Policy",
      rule: {cedarDocument: $rule}
    },
    authorizationInfo: {
      signature: $sig,
      signedPayload: $payload
    },
    requestId: "11111111-1111-4111-8111-111111111201"
  }' \
| curl -s -X POST \
    -H "Authorization: Bearer $(gcloud auth print-access-token "dave@example.com")" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d @- \
    "${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/transactionPolicies")

echo "$POLICY_RESPONSE" | jq
export POLICY_NAME=$(echo "$POLICY_RESPONSE" | jq -r '.name')

Ethereum、OpenSSL

這項規則允許將較小金額的以太幣 (<= 2 ETH,以 10-9 ETH / Gwei 為單位,表示為 2,000,000,000) 轉移至特定收款人地址 (0x1111111111111111111111111111111111111111)。

permit (principal, action, resource)
when {
  context.schema == "ETHEREUM_TX_V2" &&
  context.secondary_key == "0x1111111111111111111111111111111111111111" &&
  context has amount_token_scaled &&
  context.amount_token_scaled <= 2000000000
};
sign_policy() {
  local author_id=$1
  local resource_type=$2
  local resource_name=$3
  local action=$4
  local cedar_doc=$5
  python3 -c 'import sys; rt, rn, act, doc = sys.argv[1:]; v = lambda n: bytes([n & 127 | 128]) + v(n >> 7) if n > 127 else bytes([n]); f = lambda k, b: bytes([k << 3 | 2]) + v(len(b)) + b; open("policy_payload.bin", "wb").write(f(1 if rt == "instance" else 2, rn.encode()) + bytes([24, 1 if act == "ACTIVATE" else 2]) + f(4, f(1, doc.encode())))' "${resource_type}" "${resource_name}" "${action}" "${cedar_doc}"
  openssl dgst -sha256 -sign "${author_id}_priv.pem" -out policy_sig.bin policy_payload.bin
  openssl base64 -A -in policy_sig.bin
}

export CEDAR_RULE='permit (principal, action, resource) when { context.schema == "ETHEREUM_TX_V2" && context.secondary_key == "0x1111111111111111111111111111111111111111" && context has amount_token_scaled && context.amount_token_scaled <= 2000000000 };'

# Sign as Policy Author 1
QUORUM_SIGNATURE=$(sign_policy "author-dave" "instance" "${INSTANCE_NAME}" "ACTIVATE" "${CEDAR_RULE}")
POLICY_PAYLOAD_B64=$(openssl base64 -A -in policy_payload.bin)

POLICY_RESPONSE=$(jq -n \
  --arg rule "$CEDAR_RULE" \
  --arg sig "$QUORUM_SIGNATURE" \
  --arg payload "$POLICY_PAYLOAD_B64" \
  '{
    transactionPolicy: {
      displayName: "Ethereum Transfer Permit Policy",
      rule: {cedarDocument: $rule}
    },
    authorizationInfo: {
      signature: $sig,
      signedPayload: $payload
    },
    requestId: "11111111-1111-4111-8111-111111111201"
  }' \
| curl -s -X POST \
    -H "Authorization: Bearer $(gcloud auth print-access-token "dave@example.com")" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d @- \
    "${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/transactionPolicies")

echo "$POLICY_RESPONSE" | jq
export POLICY_NAME=$(echo "$POLICY_RESPONSE" | jq -r '.name')

Bitcoin、KMS

這項規則允許使用 BITCOIN_TX_V2 結構定義 (BIP 174 PSBT v0) 轉移比特幣,前提是淨轉出金額不超過 250,000 聰 (context.amount_token_scaled <= 2500000,或 0.0025 BTC 標準化為 10-9 固定小數位數單位),且礦工手續費不超過 10,000 聰 (context.max_fee_atomic <= 100000)。

permit (principal, action, resource)
when {
  context.schema == "BITCOIN_TX_V2" &&
  context has amount_token_scaled &&
  context.amount_token_scaled <= 2500000 &&
  context.max_fee_atomic <= 100000
};
sign_policy() {
  local author_id=$1
  local resource_type=$2
  local resource_name=$3
  local action=$4
  local cedar_doc=$5
  python3 -c 'import sys; rt, rn, act, doc = sys.argv[1:]; v = lambda n: bytes([n & 127 | 128]) + v(n >> 7) if n > 127 else bytes([n]); f = lambda k, b: bytes([k << 3 | 2]) + v(len(b)) + b; open("policy_payload.bin", "wb").write(f(1 if rt == "instance" else 2, rn.encode()) + bytes([24, 1 if act == "ACTIVATE" else 2]) + f(4, f(1, doc.encode())))' "${resource_type}" "${resource_name}" "${action}" "${cedar_doc}"
  gcloud kms asymmetric-sign \
    --version=1 \
    --key="${author_id}-key" \
    --keyring="mpc-assent-keys" \
    --location="us-central1" \
    --digest-algorithm=sha256 \
    --input-file=policy_payload.bin \
    --signature-file=policy_sig.bin > /dev/null
  openssl base64 -A -in policy_sig.bin
}

export CEDAR_RULE='permit (principal, action, resource) when { context.schema == "BITCOIN_TX_V2" && context has amount_token_scaled && context.amount_token_scaled <= 2500000 && context.max_fee_atomic <= 100000 };'

# Sign as Policy Author 1
QUORUM_SIGNATURE=$(sign_policy "author-dave" "instance" "${INSTANCE_NAME}" "ACTIVATE" "${CEDAR_RULE}")
POLICY_PAYLOAD_B64=$(openssl base64 -A -in policy_payload.bin)

POLICY_RESPONSE=$(jq -n \
  --arg rule "$CEDAR_RULE" \
  --arg sig "$QUORUM_SIGNATURE" \
  --arg payload "$POLICY_PAYLOAD_B64" \
  '{
    transactionPolicy: {
      displayName: "Bitcoin PSBT Permit Policy",
      rule: {cedarDocument: $rule}
    },
    authorizationInfo: {
      signature: $sig,
      signedPayload: $payload
    },
    requestId: "11111111-1111-4111-8111-111111111202"
  }' \
| curl -s -X POST \
    -H "Authorization: Bearer $(gcloud auth print-access-token "dave@example.com")" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d @- \
    "${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/transactionPolicies")

echo "$POLICY_RESPONSE" | jq
export POLICY_NAME=$(echo "$POLICY_RESPONSE" | jq -r '.name')

Bitcoin、OpenSSL

這項規則允許使用 BITCOIN_TX_V2 結構定義 (BIP 174 PSBT v0) 轉移比特幣,前提是淨轉出金額不超過 250,000 聰 (context.amount_token_scaled <= 2500000,或 0.0025 BTC 標準化為 10-9 固定小數位數單位),且礦工手續費不超過 10,000 聰 (context.max_fee_atomic <= 100000)。

permit (principal, action, resource)
when {
  context.schema == "BITCOIN_TX_V2" &&
  context has amount_token_scaled &&
  context.amount_token_scaled <= 2500000 &&
  context.max_fee_atomic <= 100000
};
sign_policy() {
  local author_id=$1
  local resource_type=$2
  local resource_name=$3
  local action=$4
  local cedar_doc=$5
  python3 -c 'import sys; rt, rn, act, doc = sys.argv[1:]; v = lambda n: bytes([n & 127 | 128]) + v(n >> 7) if n > 127 else bytes([n]); f = lambda k, b: bytes([k << 3 | 2]) + v(len(b)) + b; open("policy_payload.bin", "wb").write(f(1 if rt == "instance" else 2, rn.encode()) + bytes([24, 1 if act == "ACTIVATE" else 2]) + f(4, f(1, doc.encode())))' "${resource_type}" "${resource_name}" "${action}" "${cedar_doc}"
  openssl dgst -sha256 -sign "${author_id}_priv.pem" -out policy_sig.bin policy_payload.bin
  openssl base64 -A -in policy_sig.bin
}

export CEDAR_RULE='permit (principal, action, resource) when { context.schema == "BITCOIN_TX_V2" && context has amount_token_scaled && context.amount_token_scaled <= 2500000 && context.max_fee_atomic <= 100000 };'

# Sign as Policy Author 1
QUORUM_SIGNATURE=$(sign_policy "author-dave" "instance" "${INSTANCE_NAME}" "ACTIVATE" "${CEDAR_RULE}")
POLICY_PAYLOAD_B64=$(openssl base64 -A -in policy_payload.bin)

POLICY_RESPONSE=$(jq -n \
  --arg rule "$CEDAR_RULE" \
  --arg sig "$QUORUM_SIGNATURE" \
  --arg payload "$POLICY_PAYLOAD_B64" \
  '{
    transactionPolicy: {
      displayName: "Bitcoin PSBT Permit Policy",
      rule: {cedarDocument: $rule}
    },
    authorizationInfo: {
      signature: $sig,
      signedPayload: $payload
    },
    requestId: "11111111-1111-4111-8111-111111111202"
  }' \
| curl -s -X POST \
    -H "Authorization: Bearer $(gcloud auth print-access-token "dave@example.com")" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d @- \
    "${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/transactionPolicies")

echo "$POLICY_RESPONSE" | jq
export POLICY_NAME=$(echo "$POLICY_RESPONSE" | jq -r '.name')

B. 核准政策 (轉換為「有效」)

政策變更需要 2 個簽名,因此第二位政策作者必須使用已簽署的 TransactionPolicySigningPayload 呼叫 transactionPolicies.approve (ApproveTransactionPolicy)。

Ethereum

# 1. Check the policy state
POLICY_INFO=$(curl -s -H "Authorization: Bearer $(gcloud auth print-access-token "eve@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${POLICY_NAME}")

POLICY_STATE=$(echo "$POLICY_INFO" | jq -r '.state // "UNKNOWN"')

if [ "$POLICY_STATE" == "PENDING_ACTIVE" ]; then
  # 2. Sign as Policy Author 2 naming transaction_policy ($POLICY_NAME)
  QUORUM_SIGNATURE_2=$(sign_policy "author-eve" "transaction_policy" "${POLICY_NAME}" "ACTIVATE" "${CEDAR_RULE}")
  POLICY_PAYLOAD_2_B64=$(openssl base64 -A -in policy_payload.bin)

  # 3. Call ApproveTransactionPolicy as Policy Author 2
  curl -s -X POST \
    -H "Authorization: Bearer $(gcloud auth print-access-token "eve@example.com")" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d "{
      \"authorizationInfo\": {
        \"signature\": \"${QUORUM_SIGNATURE_2}\",
        \"signedPayload\": \"${POLICY_PAYLOAD_2_B64}\"
      }
    }" \
    "${INSTANCE_URL}/v1beta/${POLICY_NAME}:approve" | jq
elif [ "$POLICY_STATE" == "ACTIVE" ]; then
  echo "Policy '${POLICY_NAME}' is already ACTIVE. Skipping approval."
else
  echo "Policy '${POLICY_NAME}' is in an unexpected state: $POLICY_STATE"
fi

比特幣

# 1. Check the policy state
POLICY_INFO=$(curl -s -H "Authorization: Bearer $(gcloud auth print-access-token "eve@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${POLICY_NAME}")

POLICY_STATE=$(echo "$POLICY_INFO" | jq -r '.state // "UNKNOWN"')

if [ "$POLICY_STATE" == "PENDING_ACTIVE" ]; then
  # 2. Sign as Policy Author 2 naming transaction_policy ($POLICY_NAME)
  QUORUM_SIGNATURE_2=$(sign_policy "author-eve" "transaction_policy" "${POLICY_NAME}" "ACTIVATE" "${CEDAR_RULE}")
  POLICY_PAYLOAD_2_B64=$(openssl base64 -A -in policy_payload.bin)

  # 3. Call ApproveTransactionPolicy as Policy Author 2
  curl -s -X POST \
    -H "Authorization: Bearer $(gcloud auth print-access-token "eve@example.com")" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d "{
      \"authorizationInfo\": {
        \"signature\": \"${QUORUM_SIGNATURE_2}\",
        \"signedPayload\": \"${POLICY_PAYLOAD_2_B64}\"
      }
    }" \
    "${INSTANCE_URL}/v1beta/${POLICY_NAME}:approve" | jq
elif [ "$POLICY_STATE" == "ACTIVE" ]; then
  echo "Policy '${POLICY_NAME}' is already ACTIVE. Skipping approval."
else
  echo "Policy '${POLICY_NAME}' is in an unexpected state: $POLICY_STATE"
fi

C. 建立及管理 KeyList (選用)

KeyList (KeyListService) 是區塊鏈位址 (manualEntry、MPC key 或 keyRing) 的具名清單,政策可將其視為可信任的目的地。管理員 (ADMIN) 可以使用 keyLists.create 建立 KeyList,並隨時使用 keyLists.addEntries 和 keyLists.removeEntries 更新項目:

curl

KEY_LIST_RESPONSE=$(curl -s -X POST \
  -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  -H "Content-Type: application/json" \
  -d '{
    "displayName": "Approved Destinations",
    "description": "Trusted recipient addresses",
    "entries": [
      {
        "manualEntry": {
          "address": "0x1111111111111111111111111111111111111111",
          "source": "Treasury onboarding",
          "reason": "Approved recipient address"
        }
      }
    ]
  }' \
  "${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/keyLists?requestId=11111111-1111-4111-8111-111111111301")

echo "$KEY_LIST_RESPONSE" | jq
export KEY_LIST_NAME=$(echo "$KEY_LIST_RESPONSE" | jq -r '.name')

步驟 5:建立金鑰

以管理員 1 (admin-1@example.com) 的身分,建立含有內嵌 KeyPolicy 的 KeyRing,並產生分散式 MPC 金鑰。

A. 建立含有內嵌金鑰政策的金鑰環

KeyRing 是 MPC 金鑰的邏輯群組,並定義不可變更的內嵌 KeyPolicy (keyRing.keyPolicy),在 KeyRing 中建立的所有金鑰都會遵循這個政策。KeyPolicy 是門檻規則 (PolicyNode) 的樹狀結構,其子項 subKeyShares 可以是 teamNode (Team 中 totalMembers 的 threshold)、userNode (特定 SIGNER User) 或巢狀 policyNode。

我們在 ${TEAM_NAME} (threshold: 2、totalMembers: 3,儲存在 GCP_TEE) 中,為三個簽署者設定 2-of-3 門檻規則:

curl

KEY_RING_RESPONSE=$(jq -n \
  --arg team "${TEAM_NAME}" \
  '{
    displayName: "Treasury KeyRing",
    keyPolicy: {
      policy: {
        policyNodeId: "treasury-2-of-3",
        threshold: 1,
        subKeyShares: [
          {
            teamNode: {
              team: $team,
              threshold: 2,
              totalMembers: 3,
              storageInfo: {storageType: "GCP_TEE"}
            }
          }
        ]
      }
    }
  }' \
| curl -s -X POST \
    -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d @- \
    "${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/keyRings?requestId=11111111-1111-4111-8111-111111111401")

echo "$KEY_RING_RESPONSE" | jq
export KEY_RING_NAME=$(echo "$KEY_RING_RESPONSE" | jq -r '.name')
echo "Created KeyRing: $KEY_RING_NAME"

B. 產生分散式金鑰 (DKG)

以管理員 1 的身分呼叫 keys.create (CreateKey),在 KeyRing 的 KeyPolicy 選取的參與者之間執行分散式金鑰產生 (DKG)。

CreateKey 是同步作業,會傳回 ACTIVE Key 資源,包括 publicKeyInfo.publicKey 和伺服器衍生的 publicKeyInfo.blockchainAddresses (以太坊和比特幣為 EC_SIGN_SECP256K1,Solana 則為 EC_SIGN_ED25519,支援後):

curl

KEY_RESPONSE=$(curl -s -X POST \
  -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  -H "Content-Type: application/json" \
  -d '{
    "algorithm": "EC_SIGN_SECP256K1"
  }' \
  "${INSTANCE_URL}/v1beta/${KEY_RING_NAME}/keys?requestId=11111111-1111-4111-8111-111111111501")

echo "$KEY_RESPONSE" | jq
export KEY_NAME=$(echo "$KEY_RESPONSE" | jq -r '.name')
export PUBLIC_KEY_B64=$(echo "$KEY_RESPONSE" | jq -r '.publicKeyInfo.publicKey')

C. 衍生區塊鏈位址並找出 UTXO

CreateKey 完成後,回應中的 publicKeyInfo.blockchainAddresses 會自動包含衍生地址 (EC_SIGN_SECP256K1 的 secp256k1Addresses.ethereumAddress 和 secp256k1Addresses.bitcoinMainnetAddresses,或 EC_SIGN_ED25519 的 ed25519Addresses.solanaAddress)。您也可以使用下列指令碼,從 65 位元組未壓縮的公開金鑰 (0x04 || X || Y) 獨立驗證或衍生 Testnet 地址和鎖定指令碼,並找出交易所需的任何未花費資金。

Ethereum

Ethereum 位址會以 .publicKeyInfo.blockchainAddresses.secp256k1Addresses.ethereumAddress 傳回,也可以從 64 位元組未壓縮公開金鑰點 (pub_bytes[1:]) 的 Keccak-256 雜湊衍生,並取最後 20 個位元組。

位址衍生指令碼 (derive_address.py)

執行下列區塊,建立並執行 derive_address.py:

cat <<'EOF' > derive_address.py
import base64
from eth_utils import keccak, to_checksum_address

# Insert your base64 publicKey from Step 5.B
PUBLIC_KEY_B64 = "your-public-key"

pub_bytes = base64.b64decode(PUBLIC_KEY_B64)
if len(pub_bytes) == 65 and pub_bytes[0] == 4:
    raw_addr = "0x" + keccak(pub_bytes[1:])[-20:].hex()
    checksummed_addr = to_checksum_address(raw_addr)
    print("Ethereum Address:", checksummed_addr)
else:
    print(f"Error: Expected 65-byte uncompressed key, got {len(pub_bytes)} bytes.")
EOF
python3 derive_address.py

為以太坊位址提供資金

在步驟 7 廣播前,請確認衍生出的以太坊地址有足夠的 Sepolia 測試網 ETH,可支付交易金額和燃料費用。只要在 Faucet for Ethereum Sepolia 輸入新產生的位址,即可索取免費的測試網 ETH。

比特幣

比特幣主網位址 (p2wpkhAddress、p2shP2wpkhAddress 和 p2pkhAddress) 會在 .publicKeyInfo.blockchainAddresses.secp256k1Addresses.bitcoinMainnetAddresses 中傳回。您也可以從未壓縮的 65 位元組 secp256k1 EC 公開金鑰 (0x04 || X || Y) 衍生出 33 位元組的壓縮公開金鑰、鎖定指令碼,以及所有三種格式的 Testnet4 位址:

  1. 原生隔離見證 (P2WPKH / BIP 173):Bech32 位址 (主網為 bc1q...,Testnet4 為 tb1q...),具有 22 位元組見證 scriptPubKey (0014<hash160>)。這是建議使用的現代格式,交易手續費最低。
  2. 巢狀 SegWit (P2SH-P2WPKH / BIP 49):包裝 SegWit 位址 (主網為 3...,Testnet4 為 2...),具有 23 位元組 scriptPubKey (a914<hash160>87) 和 22 位元組 redeemScript (0014<hash160>),與舊版系統相容。
  3. 舊版 (P2PKH/BIP 44):原始 Pay-to-PubKey-Hash Base58 位址 (主網為 1...,Testnet4 為 m/n...),具有 25 位元組 scriptPubKey (76a914<hash160>88ac)。

位址衍生指令碼 (derive_btc_address.py)

執行下列區塊,建立並執行 derive_btc_address.py:

cat <<'EOF' > derive_btc_address.py
"""Derives Bitcoin addresses (Native SegWit, Nested SegWit, Legacy) from an MPC public key using embit."""

import base64
from embit import ec, script
from embit.networks import NETWORKS

# Insert the Base64-encoded public key from Step 5.B (publicKeyInfo.publicKey, decodes to 65 raw bytes)
PUBLIC_KEY_B64 = "your-public-key"

raw_bytes = base64.b64decode(PUBLIC_KEY_B64)
if len(raw_bytes) != 65 or raw_bytes[0] != 4:
  raise ValueError(f"Expected 65-byte uncompressed public key (0x04...), got {len(raw_bytes)} bytes")

# 1. Parse and compress public key to 33 bytes
pub = ec.PublicKey.parse(raw_bytes)
pub.compressed = True
compressed_pubkey_hex = pub.sec().hex()

print("================================================================")
print(f"Compressed Public Key (33 bytes): {compressed_pubkey_hex}")
print("================================================================")

# 2. Native SegWit (P2WPKH - BIP 173)
p2wpkh = script.p2wpkh(pub)
print("\n[1. Native SegWit (P2WPKH / Bech32 - BIP 173)]")
print(f"  ScriptPubKey:           {p2wpkh.data.hex()}")
print(f"  Testnet4 Address (tb):  {p2wpkh.address(NETWORKS['test'])}")
print(f"  Mainnet Address (bc):   {p2wpkh.address(NETWORKS['main'])}")

# 3. Nested SegWit (P2SH-P2WPKH - BIP 49)
p2sh_p2wpkh = script.p2sh(p2wpkh)
print("\n[2. Nested SegWit (P2SH-P2WPKH / Base58 - BIP 49)]")
print(f"  RedeemScript:           {p2wpkh.data.hex()}")
print(f"  ScriptPubKey:           {p2sh_p2wpkh.data.hex()}")
print(f"  Testnet4 Address (2):   {p2sh_p2wpkh.address(NETWORKS['test'])}")
print(f"  Mainnet Address (3):    {p2sh_p2wpkh.address(NETWORKS['main'])}")

# 4. Legacy (P2PKH - BIP 44)
p2pkh = script.p2pkh(pub)
print("\n[3. Legacy (P2PKH / Base58 - BIP 44)]")
print(f"  ScriptPubKey:           {p2pkh.data.hex()}")
print(f"  Testnet4 Address (m/n): {p2pkh.address(NETWORKS['test'])}")
print(f"  Mainnet Address (1):    {p2pkh.address(NETWORKS['main'])}")
EOF

python3 derive_btc_address.py

找出未花費輸出 (UTXO)

與以帳戶為基礎的區塊鏈不同,比特幣交易會花費特定的未花費交易輸出 (UTXO)。在步驟 6 建構交易之前:

  1. 為衍生出的原生隔離見證地址提供資金 (例如使用測試網水龍頭)。
  2. 使用比特幣節點 (bitcoin-cli listunspent 1 9999999 "[\"tb1q...\"]") 或區塊瀏覽器 API,查詢地址未花費的 UTXO。
  3. 請記下以下 4 個參數,以便在步驟 6 使用:
    • txid:未花費輸出內容的 32 位元組交易雜湊。
    • vout:以零為索引的輸出索引 (例如 0 或 1)。
    • value:未花費的聰數 (例如 200000 聰 = 0.002 BTC)。
    • scriptPubKey:鎖定指令碼十六進位值 (例如 0014...)。

步驟 6:執行 MPC 作業

當 SIGNER 呼叫 transactions.create (CreateTransaction) 時,系統會同時評估政策並啟動簽署程序。執行個體會根據 schema 解碼 payload、評估所有有效的交易政策、將結果記錄在 policyEvaluationRecord,並記錄啟動簽署者的 AUTHORIZE 決定。

A. 建立交易 (評估政策並啟動簽署程序)

以簽署者 1 (alice@example.com) 的身分提交交易,進行政策評估和簽署:

Ethereum、KMS

🛑 情況 A:拒絕 (價值過高)

嘗試建立 3 ETH 的交易 (超過 2 ETH 的政策限制):

sign_tx_decision() {
  local signer_id=$1
  local resource_type=$2
  local resource_name=$3
  local decision=$4
  local digest_hex=$5
  python3 -c 'import sys; rt, rn, dec, dg = sys.argv[1:]; f = lambda k, b: bytes([k << 3 | 2, len(b)]) + b; open("tx_decision_payload.bin", "wb").write(f(1 if rt == "key" else 2, rn.encode()) + bytes([24, 1 if dec == "AUTHORIZE" else 2]) + f(4, bytes.fromhex(dg)))' "${resource_type}" "${resource_name}" "${decision}" "${digest_hex}"
  gcloud kms asymmetric-sign \
    --version=1 \
    --key="${signer_id}-key" \
    --keyring="mpc-assent-keys" \
    --location="us-central1" \
    --digest-algorithm=sha256 \
    --input-file=tx_decision_payload.bin \
    --signature-file=tx_decision_sig.bin > /dev/null
  openssl base64 -A -in tx_decision_sig.bin
}

# Payload for 3 ETH (amount_token_scaled = 3,000,000,000 > 2,000,000,000)
RAW_TX_BYTES_HEX="ef808504a817c8008252089411111111111111111111111111111111111111118829a2241af62c00008083aa36a78080"
PAYLOAD_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$RAW_TX_BYTES_HEX')).decode('utf-8'))")
DIGEST_HEX=$(python3 -c "from eth_utils import keccak; print(keccak(bytes.fromhex('$RAW_TX_BYTES_HEX')).hex())")
DIGEST_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$DIGEST_HEX')).decode('utf-8'))")

SIGNER_1_SIG_B64=$(sign_tx_decision "signer-alice" "key" "${KEY_NAME}" "AUTHORIZE" "${DIGEST_HEX}")
SIGNED_DECISION_PAYLOAD_B64=$(openssl base64 -A -in tx_decision_payload.bin)

curl -s -X POST \
  -H "Authorization: Bearer $(gcloud auth print-access-token "alice@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  -H "Content-Type: application/json" \
  -d "{
    \"transaction\": {
      \"action\": \"Action::\\\"Transfer\\\"\",
      \"schema\": \"ETHEREUM_TX_V2\",
      \"payload\": \"${PAYLOAD_B64}\",
      \"digest\": \"${DIGEST_B64}\"
    },
    \"authorizationInfo\": {
      \"signature\": \"${SIGNER_1_SIG_B64}\",
      \"signedPayload\": \"${SIGNED_DECISION_PAYLOAD_B64}\"
    },
    \"requestId\": \"11111111-1111-4111-8111-111111111601\"
  }" \
  "${INSTANCE_URL}/v1beta/${KEY_NAME}/transactions" | jq

預期的政策評估結果:decision: DENY (denyReason: NO_PERMIT_RULE_MATCHED)。

✅ 案例 B:核准 (允許值)

建立 1 Gwei 的交易 (在 2 ETH 限制內):

# Payload for 1 Gwei
RAW_TX_BYTES_HEX="eb808504a817c800825208941111111111111111111111111111111111111111843b9aca008083aa36a78080"
PAYLOAD_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$RAW_TX_BYTES_HEX')).decode('utf-8'))")
export DIGEST_HEX=$(python3 -c "from eth_utils import keccak; print(keccak(bytes.fromhex('$RAW_TX_BYTES_HEX')).hex())")
export DIGEST_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$DIGEST_HEX')).decode('utf-8'))")

SIGNER_1_SIG_B64=$(sign_tx_decision "signer-alice" "key" "${KEY_NAME}" "AUTHORIZE" "${DIGEST_HEX}")
SIGNED_DECISION_PAYLOAD_B64=$(openssl base64 -A -in tx_decision_payload.bin)

RESPONSE=$(curl -s -X POST \
  -H "Authorization: Bearer $(gcloud auth print-access-token "alice@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  -H "Content-Type: application/json" \
  -d "{
    \"transaction\": {
      \"action\": \"Action::\\\"Transfer\\\"\",
      \"schema\": \"ETHEREUM_TX_V2\",
      \"payload\": \"${PAYLOAD_B64}\",
      \"digest\": \"${DIGEST_B64}\"
    },
    \"authorizationInfo\": {
      \"signature\": \"${SIGNER_1_SIG_B64}\",
      \"signedPayload\": \"${SIGNED_DECISION_PAYLOAD_B64}\"
    },
    \"requestId\": \"11111111-1111-4111-8111-111111111602\"
  }" \
  "${INSTANCE_URL}/v1beta/${KEY_NAME}/transactions")

echo "$RESPONSE" | jq
export TRANSACTION_NAME=$(echo "$RESPONSE" | jq -r '.name')

預期的回應:state: PENDING_AUTHORIZATION 和 policyEvaluationRecord.decision: PERMIT。

Ethereum、OpenSSL

🛑 情況 A:拒絕 (價值過高)

嘗試建立 3 ETH 的交易 (超過 2 ETH 的政策限制):

sign_tx_decision() {
  local signer_id=$1
  local resource_type=$2
  local resource_name=$3
  local decision=$4
  local digest_hex=$5
  python3 -c 'import sys; rt, rn, dec, dg = sys.argv[1:]; f = lambda k, b: bytes([k << 3 | 2, len(b)]) + b; open("tx_decision_payload.bin", "wb").write(f(1 if rt == "key" else 2, rn.encode()) + bytes([24, 1 if dec == "AUTHORIZE" else 2]) + f(4, bytes.fromhex(dg)))' "${resource_type}" "${resource_name}" "${decision}" "${digest_hex}"
  openssl dgst -sha256 -sign "${signer_id}_priv.pem" -out tx_decision_sig.bin tx_decision_payload.bin
  openssl base64 -A -in tx_decision_sig.bin
}

# Payload for 3 ETH (amount_token_scaled = 3,000,000,000 > 2,000,000,000)
RAW_TX_BYTES_HEX="ef808504a817c8008252089411111111111111111111111111111111111111118829a2241af62c00008083aa36a78080"
PAYLOAD_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$RAW_TX_BYTES_HEX')).decode('utf-8'))")
DIGEST_HEX=$(python3 -c "from eth_utils import keccak; print(keccak(bytes.fromhex('$RAW_TX_BYTES_HEX')).hex())")
DIGEST_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$DIGEST_HEX')).decode('utf-8'))")

SIGNER_1_SIG_B64=$(sign_tx_decision "signer-alice" "key" "${KEY_NAME}" "AUTHORIZE" "${DIGEST_HEX}")
SIGNED_DECISION_PAYLOAD_B64=$(openssl base64 -A -in tx_decision_payload.bin)

curl -s -X POST \
  -H "Authorization: Bearer $(gcloud auth print-access-token "alice@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  -H "Content-Type: application/json" \
  -d "{
    \"transaction\": {
      \"action\": \"Action::\\\"Transfer\\\"\",
      \"schema\": \"ETHEREUM_TX_V2\",
      \"payload\": \"${PAYLOAD_B64}\",
      \"digest\": \"${DIGEST_B64}\"
    },
    \"authorizationInfo\": {
      \"signature\": \"${SIGNER_1_SIG_B64}\",
      \"signedPayload\": \"${SIGNED_DECISION_PAYLOAD_B64}\"
    },
    \"requestId\": \"11111111-1111-4111-8111-111111111601\"
  }" \
  "${INSTANCE_URL}/v1beta/${KEY_NAME}/transactions" | jq

預期的政策評估結果:decision: DENY (denyReason: NO_PERMIT_RULE_MATCHED)。

✅ 案例 B:核准 (允許值)

建立 1 Gwei 的交易 (在 2 ETH 限制內):

# Payload for 1 Gwei
RAW_TX_BYTES_HEX="eb808504a817c800825208941111111111111111111111111111111111111111843b9aca008083aa36a78080"
PAYLOAD_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$RAW_TX_BYTES_HEX')).decode('utf-8'))")
export DIGEST_HEX=$(python3 -c "from eth_utils import keccak; print(keccak(bytes.fromhex('$RAW_TX_BYTES_HEX')).hex())")
export DIGEST_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$DIGEST_HEX')).decode('utf-8'))")

SIGNER_1_SIG_B64=$(sign_tx_decision "signer-alice" "key" "${KEY_NAME}" "AUTHORIZE" "${DIGEST_HEX}")
SIGNED_DECISION_PAYLOAD_B64=$(openssl base64 -A -in tx_decision_payload.bin)

RESPONSE=$(curl -s -X POST \
  -H "Authorization: Bearer $(gcloud auth print-access-token "alice@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  -H "Content-Type: application/json" \
  -d "{
    \"transaction\": {
      \"action\": \"Action::\\\"Transfer\\\"\",
      \"schema\": \"ETHEREUM_TX_V2\",
      \"payload\": \"${PAYLOAD_B64}\",
      \"digest\": \"${DIGEST_B64}\"
    },
    \"authorizationInfo\": {
      \"signature\": \"${SIGNER_1_SIG_B64}\",
      \"signedPayload\": \"${SIGNED_DECISION_PAYLOAD_B64}\"
    },
    \"requestId\": \"11111111-1111-4111-8111-111111111602\"
  }" \
  "${INSTANCE_URL}/v1beta/${KEY_NAME}/transactions")

echo "$RESPONSE" | jq
export TRANSACTION_NAME=$(echo "$RESPONSE" | jq -r '.name')

預期的回應:state: PENDING_AUTHORIZATION 和 policyEvaluationRecord.decision: PERMIT。

Bitcoin、KMS

在比特幣 UTXO 模型中,交易會花費特定的未花費輸出。在 SegWit (BIP 143) 中,計算簽章雜湊時,必須知道 satoshi 數量和所花費輸入內容的鎖定指令碼。

比特幣政策評估作業必須採用 BIP 174 PSBT v0 格式 (BITCOIN_TX_V2),且每個輸入內容都必須填入 PSBT_IN_WITNESS_UTXO。

PSBT 建構指令碼 (build_psbt.py)

執行下列區塊,組合有效的 BIP 174 PSBT v0 酬載:

cat <<'EOF' > build_psbt.py
"""Constructs a BIP 174 PSBT v0 payload using embit for Bitcoin policy evaluation."""

from embit import script, transaction
from embit.psbt import PSBT

# 1. Inputs (From Step 5.C)
PREV_TXID_HEX = "8f594f33156993f1c980c2273dcae4a92cf2517fbc29264421395e63a63916d8"
PREV_VOUT = int("1")
INPUT_VALUE_SATS = int("200000")  # Total UTXO input value in satoshis
SENDER_SCRIPTPUBKEY_HEX = "0014eced05cd51d964102a20c856b09080ad620e9da2"

# 2. Output Allocations
RECIPIENT_SATS = int("50000")  # Recipient payment in satoshis
MINER_FEE_SATS = int("250")  # Miner fee in satoshis
RECIPIENT_SCRIPTPUBKEY_HEX = "0014751e76e8199196d454941c45d1b3a323f1433bd6"

# Change Output calculation: InputSats - RecipientSats - MinerFeeSats
CHANGE_SATS = INPUT_VALUE_SATS - RECIPIENT_SATS - MINER_FEE_SATS
if CHANGE_SATS <= 0:
  raise ValueError(f"Insufficient input satoshis ({INPUT_VALUE_SATS}) for payment and fee")

# 3. Construct Unsigned Transaction (Version 2, 1 input, 2 outputs, locktime 0)
tx = transaction.Transaction(
    version=2,
    vin=[transaction.TransactionInput(
        txid=bytes.fromhex(PREV_TXID_HEX),
        vout=PREV_VOUT,
        sequence=0xFFFFFFFF,
    )],
    vout=[
        transaction.TransactionOutput(
            value=RECIPIENT_SATS,
            script_pubkey=script.Script(bytes.fromhex(RECIPIENT_SCRIPTPUBKEY_HEX)),
        ),
        transaction.TransactionOutput(
            value=CHANGE_SATS,
            script_pubkey=script.Script(bytes.fromhex(SENDER_SCRIPTPUBKEY_HEX)),
        ),
    ],
    locktime=0,
)

# 4. Package into BIP 174 PSBT with Input Witness UTXO
psbt = PSBT(tx)
psbt.inputs[0].witness_utxo = transaction.TransactionOutput(
    value=INPUT_VALUE_SATS,
    script_pubkey=script.Script(bytes.fromhex(SENDER_SCRIPTPUBKEY_HEX)),
)

print(psbt.serialize().hex())
EOF

BTC_PSBT_HEX=$(python3 build_psbt.py)
echo "Generated PSBT Hex (${#BTC_PSBT_HEX} chars):"
echo "$BTC_PSBT_HEX"

🛑 情況 A:拒絕 (超過手續費上限)

嘗試建立交易,但礦工手續費超過 10,000 聰的政策限制。在這個範例中,在 build_psbt.py 中設定 MINER_FEE_SATS = 15000 (和 CHANGE_SATS = 135000) 會調整為 max_fee_atomic = 150000 (超過 100000 的政策上限),產生下列酬載,觸發 DENY 政策評估決策。

sign_tx_decision() {
  local signer_id=$1
  local resource_type=$2
  local resource_name=$3
  local decision=$4
  local digest_hex=$5
  python3 -c 'import sys; rt, rn, dec, dg = sys.argv[1:]; f = lambda k, b: bytes([k << 3 | 2, len(b)]) + b; open("tx_decision_payload.bin", "wb").write(f(1 if rt == "key" else 2, rn.encode()) + bytes([24, 1 if dec == "AUTHORIZE" else 2]) + f(4, bytes.fromhex(dg)))' "${resource_type}" "${resource_name}" "${decision}" "${digest_hex}"
  gcloud kms asymmetric-sign \
    --version=1 \
    --key="${signer_id}-key" \
    --keyring="mpc-assent-keys" \
    --location="us-central1" \
    --digest-algorithm=sha256 \
    --input-file=tx_decision_payload.bin \
    --signature-file=tx_decision_sig.bin > /dev/null
  openssl base64 -A -in tx_decision_sig.bin
}

# Construct a PSBT payload with miner_fee_sats = 15000 -> max_fee_atomic = 150000 (exceeds 100000 limit)
REJECTED_PSBT_HEX="70736274ff0100710200000001d81639a6635e3921442629bc7f51f22ca9e4ca3d27c280c9f1936915334f598f0100000000ffffffff0250c3000000000000160014751e76e8199196d454941c45d1b3a323f1433bd6580f020000000000160014eced05cd51d964102a20c856b09080ad620e9da2000000000001011f400d030000000000160014eced05cd51d964102a20c856b09080ad620e9da2000000"
REJECTED_PAYLOAD_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$REJECTED_PSBT_HEX')).decode('utf-8'))")
REJECTED_DIGEST_HEX=$(python3 -c "from embit import script; from embit.psbt import PSBT; p=PSBT.parse(bytes.fromhex('$REJECTED_PSBT_HEX')); u=p.inputs[0].witness_utxo; sc=script.Script(b'\x76\xa9\x14'+u.script_pubkey.data[2:]+b'\x88\xac'); print(p.tx.sighash_segwit(0, sc, u.value).hex())")
REJECTED_DIGEST_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$REJECTED_DIGEST_HEX')).decode('utf-8'))")

SIGNER_1_SIG_B64=$(sign_tx_decision "signer-alice" "key" "${KEY_NAME}" "AUTHORIZE" "${REJECTED_DIGEST_HEX}")
SIGNED_DECISION_PAYLOAD_B64=$(openssl base64 -A -in tx_decision_payload.bin)

curl -s -X POST \
  -H "Authorization: Bearer $(gcloud auth print-access-token "alice@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  -H "Content-Type: application/json" \
  -d "{
    \"transaction\": {
      \"action\": \"Action::\\\"Transfer\\\"\",
      \"schema\": \"BITCOIN_TX_V2\",
      \"payload\": \"${REJECTED_PAYLOAD_B64}\",
      \"digest\": \"${REJECTED_DIGEST_B64}\"
    },
    \"authorizationInfo\": {
      \"signature\": \"${SIGNER_1_SIG_B64}\",
      \"signedPayload\": \"${SIGNED_DECISION_PAYLOAD_B64}\"
    },
    \"requestId\": \"11111111-1111-4111-8111-111111111603\"
  }" \
  "${INSTANCE_URL}/v1beta/${KEY_NAME}/transactions" | jq

預期的政策評估結果:decision: DENY (denyReason: NO_PERMIT_RULE_MATCHED)。

✅ 案例 B:核准 (允許比特幣 PSBT)

使用先前建構的有效 PSBT 建立交易 (RECIPIENT_SATS = 50000 對應至 amount_token_scaled = 500000 <= 2500000,MINER_FEE_SATS = 250 對應至 max_fee_atomic = 2500 <= 100000):

PAYLOAD_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$BTC_PSBT_HEX')).decode('utf-8'))")
export DIGEST_HEX=$(python3 -c "from embit import script; from embit.psbt import PSBT; p=PSBT.parse(bytes.fromhex('$BTC_PSBT_HEX')); u=p.inputs[0].witness_utxo; sc=script.Script(b'\x76\xa9\x14'+u.script_pubkey.data[2:]+b'\x88\xac'); print(p.tx.sighash_segwit(0, sc, u.value).hex())")
export DIGEST_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$DIGEST_HEX')).decode('utf-8'))")

SIGNER_1_SIG_B64=$(sign_tx_decision "signer-alice" "key" "${KEY_NAME}" "AUTHORIZE" "${DIGEST_HEX}")
SIGNED_DECISION_PAYLOAD_B64=$(openssl base64 -A -in tx_decision_payload.bin)

RESPONSE=$(curl -s -X POST \
  -H "Authorization: Bearer $(gcloud auth print-access-token "alice@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  -H "Content-Type: application/json" \
  -d "{
    \"transaction\": {
      \"action\": \"Action::\\\"Transfer\\\"\",
      \"schema\": \"BITCOIN_TX_V2\",
      \"payload\": \"${PAYLOAD_B64}\",
      \"digest\": \"${DIGEST_B64}\"
    },
    \"authorizationInfo\": {
      \"signature\": \"${SIGNER_1_SIG_B64}\",
      \"signedPayload\": \"${SIGNED_DECISION_PAYLOAD_B64}\"
    },
    \"requestId\": \"11111111-1111-4111-8111-111111111604\"
  }" \
  "${INSTANCE_URL}/v1beta/${KEY_NAME}/transactions")

echo "$RESPONSE" | jq
export TRANSACTION_NAME=$(echo "$RESPONSE" | jq -r '.name')

預期的回應:state: PENDING_AUTHORIZATION 和 policyEvaluationRecord.decision: PERMIT。

Bitcoin、OpenSSL

在比特幣 UTXO 模型中,交易會花費特定的未花費輸出。在 SegWit (BIP 143) 中,計算簽章雜湊時,必須知道 satoshi 數量和所花費輸入內容的鎖定指令碼。

比特幣政策評估作業必須採用 BIP 174 PSBT v0 格式 (BITCOIN_TX_V2),且每個輸入內容都必須填入 PSBT_IN_WITNESS_UTXO。

PSBT 建構指令碼 (build_psbt.py)

執行下列區塊,組合有效的 BIP 174 PSBT v0 酬載:

cat <<'EOF' > build_psbt.py
"""Constructs a BIP 174 PSBT v0 payload using embit for Bitcoin policy evaluation."""

from embit import script, transaction
from embit.psbt import PSBT

# 1. Inputs (From Step 5.C)
PREV_TXID_HEX = "8f594f33156993f1c980c2273dcae4a92cf2517fbc29264421395e63a63916d8"
PREV_VOUT = int("1")
INPUT_VALUE_SATS = int("200000")  # Total UTXO input value in satoshis
SENDER_SCRIPTPUBKEY_HEX = "0014eced05cd51d964102a20c856b09080ad620e9da2"

# 2. Output Allocations
RECIPIENT_SATS = int("50000")  # Recipient payment in satoshis
MINER_FEE_SATS = int("250")  # Miner fee in satoshis
RECIPIENT_SCRIPTPUBKEY_HEX = "0014751e76e8199196d454941c45d1b3a323f1433bd6"

# Change Output calculation: InputSats - RecipientSats - MinerFeeSats
CHANGE_SATS = INPUT_VALUE_SATS - RECIPIENT_SATS - MINER_FEE_SATS
if CHANGE_SATS <= 0:
  raise ValueError(f"Insufficient input satoshis ({INPUT_VALUE_SATS}) for payment and fee")

# 3. Construct Unsigned Transaction (Version 2, 1 input, 2 outputs, locktime 0)
tx = transaction.Transaction(
    version=2,
    vin=[transaction.TransactionInput(
        txid=bytes.fromhex(PREV_TXID_HEX),
        vout=PREV_VOUT,
        sequence=0xFFFFFFFF,
    )],
    vout=[
        transaction.TransactionOutput(
            value=RECIPIENT_SATS,
            script_pubkey=script.Script(bytes.fromhex(RECIPIENT_SCRIPTPUBKEY_HEX)),
        ),
        transaction.TransactionOutput(
            value=CHANGE_SATS,
            script_pubkey=script.Script(bytes.fromhex(SENDER_SCRIPTPUBKEY_HEX)),
        ),
    ],
    locktime=0,
)

# 4. Package into BIP 174 PSBT with Input Witness UTXO
psbt = PSBT(tx)
psbt.inputs[0].witness_utxo = transaction.TransactionOutput(
    value=INPUT_VALUE_SATS,
    script_pubkey=script.Script(bytes.fromhex(SENDER_SCRIPTPUBKEY_HEX)),
)

print(psbt.serialize().hex())
EOF

BTC_PSBT_HEX=$(python3 build_psbt.py)
echo "Generated PSBT Hex (${#BTC_PSBT_HEX} chars):"
echo "$BTC_PSBT_HEX"

🛑 情況 A:拒絕 (超過手續費上限)

嘗試建立交易,但礦工手續費超過 10,000 聰的政策限制。在這個範例中,在 build_psbt.py 中設定 MINER_FEE_SATS = 15000 (和 CHANGE_SATS = 135000) 會調整為 max_fee_atomic = 150000 (超過 100000 的政策上限),產生下列酬載,觸發 DENY 政策評估決策。

sign_tx_decision() {
  local signer_id=$1
  local resource_type=$2
  local resource_name=$3
  local decision=$4
  local digest_hex=$5
  python3 -c 'import sys; rt, rn, dec, dg = sys.argv[1:]; f = lambda k, b: bytes([k << 3 | 2, len(b)]) + b; open("tx_decision_payload.bin", "wb").write(f(1 if rt == "key" else 2, rn.encode()) + bytes([24, 1 if dec == "AUTHORIZE" else 2]) + f(4, bytes.fromhex(dg)))' "${resource_type}" "${resource_name}" "${decision}" "${digest_hex}"
  openssl dgst -sha256 -sign "${signer_id}_priv.pem" -out tx_decision_sig.bin tx_decision_payload.bin
  openssl base64 -A -in tx_decision_sig.bin
}

# Construct a PSBT payload with miner_fee_sats = 15000 -> max_fee_atomic = 150000 (exceeds 100000 limit)
REJECTED_PSBT_HEX="70736274ff0100710200000001d81639a6635e3921442629bc7f51f22ca9e4ca3d27c280c9f1936915334f598f0100000000ffffffff0250c3000000000000160014751e76e8199196d454941c45d1b3a323f1433bd6580f020000000000160014eced05cd51d964102a20c856b09080ad620e9da2000000000001011f400d030000000000160014eced05cd51d964102a20c856b09080ad620e9da2000000"
REJECTED_PAYLOAD_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$REJECTED_PSBT_HEX')).decode('utf-8'))")
REJECTED_DIGEST_HEX=$(python3 -c "from embit import script; from embit.psbt import PSBT; p=PSBT.parse(bytes.fromhex('$REJECTED_PSBT_HEX')); u=p.inputs[0].witness_utxo; sc=script.Script(b'\x76\xa9\x14'+u.script_pubkey.data[2:]+b'\x88\xac'); print(p.tx.sighash_segwit(0, sc, u.value).hex())")
REJECTED_DIGEST_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$REJECTED_DIGEST_HEX')).decode('utf-8'))")

SIGNER_1_SIG_B64=$(sign_tx_decision "signer-alice" "key" "${KEY_NAME}" "AUTHORIZE" "${REJECTED_DIGEST_HEX}")
SIGNED_DECISION_PAYLOAD_B64=$(openssl base64 -A -in tx_decision_payload.bin)

curl -s -X POST \
  -H "Authorization: Bearer $(gcloud auth print-access-token "alice@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  -H "Content-Type: application/json" \
  -d "{
    \"transaction\": {
      \"action\": \"Action::\\\"Transfer\\\"\",
      \"schema\": \"BITCOIN_TX_V2\",
      \"payload\": \"${REJECTED_PAYLOAD_B64}\",
      \"digest\": \"${REJECTED_DIGEST_B64}\"
    },
    \"authorizationInfo\": {
      \"signature\": \"${SIGNER_1_SIG_B64}\",
      \"signedPayload\": \"${SIGNED_DECISION_PAYLOAD_B64}\"
    },
    \"requestId\": \"11111111-1111-4111-8111-111111111603\"
  }" \
  "${INSTANCE_URL}/v1beta/${KEY_NAME}/transactions" | jq

預期的政策評估結果:decision: DENY (denyReason: NO_PERMIT_RULE_MATCHED)。

✅ 案例 B:核准 (允許比特幣 PSBT)

使用先前建構的有效 PSBT 建立交易 (RECIPIENT_SATS = 50000 對應至 amount_token_scaled = 500000 <= 2500000,MINER_FEE_SATS = 250 對應至 max_fee_atomic = 2500 <= 100000):

PAYLOAD_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$BTC_PSBT_HEX')).decode('utf-8'))")
export DIGEST_HEX=$(python3 -c "from embit import script; from embit.psbt import PSBT; p=PSBT.parse(bytes.fromhex('$BTC_PSBT_HEX')); u=p.inputs[0].witness_utxo; sc=script.Script(b'\x76\xa9\x14'+u.script_pubkey.data[2:]+b'\x88\xac'); print(p.tx.sighash_segwit(0, sc, u.value).hex())")
export DIGEST_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$DIGEST_HEX')).decode('utf-8'))")

SIGNER_1_SIG_B64=$(sign_tx_decision "signer-alice" "key" "${KEY_NAME}" "AUTHORIZE" "${DIGEST_HEX}")
SIGNED_DECISION_PAYLOAD_B64=$(openssl base64 -A -in tx_decision_payload.bin)

RESPONSE=$(curl -s -X POST \
  -H "Authorization: Bearer $(gcloud auth print-access-token "alice@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  -H "Content-Type: application/json" \
  -d "{
    \"transaction\": {
      \"action\": \"Action::\\\"Transfer\\\"\",
      \"schema\": \"BITCOIN_TX_V2\",
      \"payload\": \"${PAYLOAD_B64}\",
      \"digest\": \"${DIGEST_B64}\"
    },
    \"authorizationInfo\": {
      \"signature\": \"${SIGNER_1_SIG_B64}\",
      \"signedPayload\": \"${SIGNED_DECISION_PAYLOAD_B64}\"
    },
    \"requestId\": \"11111111-1111-4111-8111-111111111604\"
  }" \
  "${INSTANCE_URL}/v1beta/${KEY_NAME}/transactions")

echo "$RESPONSE" | jq
export TRANSACTION_NAME=$(echo "$RESPONSE" | jq -r '.name')

預期的回應:state: PENDING_AUTHORIZATION 和 policyEvaluationRecord.decision: PERMIT。

🕵️‍♂️ 檢查交易狀態 (輪詢)

定期執行下列指令,輪詢交易狀態:

curl

curl -s -X GET \
  -H "Authorization: Bearer $(gcloud auth print-access-token "alice@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${TRANSACTION_NAME}" | jq

B. 記錄簽署者決定 (授權)

由於步驟 6.A 中的 CreateTransaction 已記錄簽署者 1 的 AUTHORIZE 決策 (3 分之 2 KeyPolicy 門檻所需的 2 個簽署者之一),因此只需要另一位持有金鑰共用權的共同簽署者,使用 transactions.recordSignerDecision (RecordSignerDecision) 授權交易即可。

簽署 TransactionSigningPayload,將 transaction (${TRANSACTION_NAME}) 命名為 recordSignerDecision,並以簽署人 2 (bob@example.com) 的身分呼叫:

curl

# 1. Check state for Signer 2
TX_INFO=$(curl -s -H "Authorization: Bearer $(gcloud auth print-access-token "bob@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${TRANSACTION_NAME}")

TX_STATE=$(echo "$TX_INFO" | jq -r '.state // "UNKNOWN"')

if [ "$TX_STATE" == "PENDING_AUTHORIZATION" ]; then
  # 2. Sign as Signer 2 naming transaction ($TRANSACTION_NAME)
  SIGNER_2_SIG_B64=$(sign_tx_decision "signer-bob" "transaction" "${TRANSACTION_NAME}" "AUTHORIZE" "${DIGEST_HEX}")
  SIGNER_2_PAYLOAD_B64=$(openssl base64 -A -in tx_decision_payload.bin)

  # 3. Call RecordSignerDecision as Signer 2
  echo "Recording AUTHORIZE decision for Signer 2..."
  curl -s -X POST \
    -H "Authorization: Bearer $(gcloud auth print-access-token "bob@example.com")" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d "{
      \"decision\": \"AUTHORIZE\",
      \"authorizationInfo\": {
        \"signature\": \"${SIGNER_2_SIG_B64}\",
        \"signedPayload\": \"${SIGNER_2_PAYLOAD_B64}\"
      }
    }" \
    "${INSTANCE_URL}/v1beta/${TRANSACTION_NAME}:recordSignerDecision" | jq
elif [ "$TX_STATE" == "SIGNING" ] || [ "$TX_STATE" == "SIGNING_SUCCEEDED" ]; then
  echo "Transaction is in state $TX_STATE. Skipping Signer 2."
else
  echo "Transaction is in an unexpected state: $TX_STATE"
fi

C. 擷取最終簽名

達到授權門檻後,請檢查交易狀態並擷取 finalSignature (如果是 EC_SIGN_SECP256K1 金鑰,則為 recoveryId):

curl

curl -s -X GET \
  -H "Authorization: Bearer $(gcloud auth print-access-token "alice@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${TRANSACTION_NAME}" | jq

如果 state 為 SIGNING_SUCCEEDED,請從傳回的 JSON 複製 finalSignature 欄位,用於步驟 7。


步驟 7:將交易廣播至區塊鏈

交易達到 SIGNING_SUCCEEDED 狀態,且您已擷取 finalSignature 後,最後一個步驟是建構已簽署的線路交易,並廣播至網路。

Ethereum

復原 ID 衍生和廣播

MPC API 會在 finalSignature 中傳回原始 64 位元組 ECDSA 簽章 (包含標準低 S 形式的 32 位元組 r 和 s 值,符合 EIP-2 標準),以及 recoveryId 中的復原同位位元 (0 或 1)。

下列 Python 指令碼會使用 finalSignature 建構已簽署的交易、根據 MPC 金鑰的 Ethereum 位址驗證復原的寄件者位址,並將交易廣播至網路。

廣播腳本:

在終端機中執行下列可複製貼上的區塊,編寫並執行 broadcast_eth.py:

cat <<'EOF' > broadcast_eth.py
"""Broadcasts an Ethereum transaction using a normalized MPC signature."""

import base64
import os
import requests
from eth_account import Account
try:
    from eth_account._utils.transactions import encode_transaction, serializable_unsigned_transaction_from_dict
except ImportError:
    from eth_account._utils.legacy_transactions import encode_transaction, serializable_unsigned_transaction_from_dict

# ==========================================
# 1. INPUTS
# ==========================================
FINAL_SIGNATURE_B64 = "your-final-signature"
RPC_URL = "https://ethereum-sepolia-rpc.publicnode.com"
EXPECTED_SENDER = "0xyour-mpc-key-address"

if not FINAL_SIGNATURE_B64 or FINAL_SIGNATURE_B64 == "your-final-signature":
    raise ValueError("FINAL_SIGNATURE_B64 is not set. Please provide a valid signature.")

# ==========================================
# 2. TRANSACTION PARAMETERS
# ==========================================
CHAIN_ID = 11155111  # Sepolia Testnet
tx_dict = {
    "nonce": int("0"),
    "gasPrice": int("20000000000"),  # 20 Gwei
    "gas": int("21000"),
    "to": "0x1111111111111111111111111111111111111111",
    "value": int("1000000000"),  # 1 Gwei
    "data": b"",
    "chainId": CHAIN_ID,
}

# ==========================================
# 3. DECODE & SPLIT SIGNATURE
# ==========================================
missing_padding = len(FINAL_SIGNATURE_B64) % 4
if missing_padding:
    FINAL_SIGNATURE_B64 += "=" * (4 - missing_padding)

sig_bytes = base64.b64decode(FINAL_SIGNATURE_B64)
r = int.from_bytes(sig_bytes[:32], byteorder="big")
s = int.from_bytes(sig_bytes[32:], byteorder="big")

# ==========================================
# 4. RECOVERY IDENTIFIER (V) DERIVATION
# ==========================================
chain_v_base = (CHAIN_ID * 2) + 35

# ==========================================
# 5. GENERATE UNSIGNED TX & RECOVER SENDER ADDRESS
# ==========================================
unsigned_tx = serializable_unsigned_transaction_from_dict(tx_dict)

derived_sender = None
encoded_tx = None
final_v = chain_v_base

EXPECTED_SENDER_PLACEHOLDER = "0xyour-mpc-key-address"
is_valid_expected = EXPECTED_SENDER and EXPECTED_SENDER != EXPECTED_SENDER_PLACEHOLDER

for parity in [0, 1]:
    try_v = chain_v_base + parity
    try:
        tx_candidate = encode_transaction(unsigned_tx, vrs=(try_v, r, s))
        addr = Account.recover_transaction("0x" + tx_candidate.hex())

        if is_valid_expected and EXPECTED_SENDER.lower() == addr.lower():
            derived_sender = addr
            final_v = try_v
            encoded_tx = tx_candidate
            break

        if not is_valid_expected and derived_sender is None:
            derived_sender = addr
            final_v = try_v
            encoded_tx = tx_candidate
            print(f"Warning: EXPECTED_SENDER not provided or is placeholder. Assuming first recovered address: {addr}")

    except Exception:
        continue

if encoded_tx is None:
    if is_valid_expected:
        raise ValueError(f"Could not recover address matching EXPECTED_SENDER: {EXPECTED_SENDER}")
    else:
        raise ValueError("Could not derive broadcastable transaction. Ensure signature matches payload.")

if derived_sender:
    print(f"Derived Sender Address: {derived_sender}")
else:
    print("Derived Sender Address: Could not recover (ensure signature matches current transaction payload)")

valid_tx_hex = "0x" + encoded_tx.hex()
print(f"Broadcastable Transaction Hex:\n{valid_tx_hex}\n")

# ==========================================
# 6. BROADCAST TO NETWORK
# ==========================================
payload = {
    "jsonrpc": "2.0",
    "method": "eth_sendRawTransaction",
    "params": [valid_tx_hex],
    "id": 1,
}

print(f"Broadcasting to {RPC_URL}...")
response = requests.post(RPC_URL, json=payload)
resp_json = response.json()
print("RPC Response:", resp_json)
if "result" in resp_json:
    tx_hash = resp_json["result"]
    print(f"View transaction on Sepolia Etherscan: https://sepolia.etherscan.io/tx/{tx_hash}")
EOF

python3 broadcast_eth.py

疑難排解常見錯誤

  • invalid sender: recovery failed 或 invalid transaction v, r, s values:如果指令碼中的交易參數與已簽署的酬載不完全相符,就會發生這種情況。確認 value、nonce 和 to 欄位與步驟 6 處理的內容相符。
  • insufficient funds for gas * price + value:交易已通過密碼編譯驗證,並由節點接受,但 MPC 金鑰的 Ethereum 位址沒有足夠的測試網 ETH 來支付作業費用。你必須先為地址提供資金,才能廣播。

比特幣

SegWit 電匯交易組合與廣播

如果是比特幣原生隔離見證交易 (BIP 141),原始 64 位元組 (r, s) MPC 簽章必須採用標準低 S ASN.1 DER 格式,並附加 0x01 (SIGHASH_ALL),然後與 33 位元組壓縮公開金鑰組合成 2 元素隔離見證堆疊:[DER signature || 0x01, compressed public key]。

廣播腳本:

執行下列區塊,建立並執行 broadcast_btc.py:

cat <<'EOF' > broadcast_btc.py
"""Assembles a signed Bitcoin Native SegWit transaction from an MPC signature using embit and broadcasts it."""

import base64
import urllib.request
from embit import ec, script, transaction

# ==========================================
# 1. INPUTS
# ==========================================
FINAL_SIGNATURE_B64 = "your-final-signature"
COMPRESSED_PUBKEY_HEX = "03deb76948c14003446a8c1d79e88fb3607c9bbc1f5143b186aa7b61d6b1facc36"

PREV_TXID_HEX = "8f594f33156993f1c980c2273dcae4a92cf2517fbc29264421395e63a63916d8"
PREV_VOUT = int("1")

RECIPIENT_SATS = int("50000")
RECIPIENT_SCRIPTPUBKEY_HEX = "0014751e76e8199196d454941c45d1b3a323f1433bd6"

CHANGE_SATS = int("149750")
CHANGE_SCRIPTPUBKEY_HEX = "0014eced05cd51d964102a20c856b09080ad620e9da2"

BROADCAST_API_URL = "https://mempool.space/testnet4/api/tx"

# ==========================================
# 2. CONVERT RAW (r, s) SIGNATURE TO CANONICAL LOW-S ASN.1 DER + SIGHASH_ALL
# ==========================================
sig_bytes = base64.b64decode(FINAL_SIGNATURE_B64)
internal_sig = ec.secp256k1.ecdsa_signature_parse_compact(sig_bytes)
ec.secp256k1.ecdsa_signature_normalize(internal_sig, internal_sig)
der_sig = ec.secp256k1.ecdsa_signature_serialize_der(internal_sig) + b"\x01"  # 0x01 = SIGHASH_ALL

# ==========================================
# 3. BUILD TRANSACTION & ATTACH SEGWIT WITNESS STACK
# ==========================================
tx = transaction.Transaction(
    version=2,
    vin=[transaction.TransactionInput(
        txid=bytes.fromhex(PREV_TXID_HEX),
        vout=PREV_VOUT,
        sequence=0xFFFFFFFF,
    )],
    vout=[
        transaction.TransactionOutput(
            value=RECIPIENT_SATS,
            script_pubkey=script.Script(bytes.fromhex(RECIPIENT_SCRIPTPUBKEY_HEX)),
        ),
        transaction.TransactionOutput(
            value=CHANGE_SATS,
            script_pubkey=script.Script(bytes.fromhex(CHANGE_SCRIPTPUBKEY_HEX)),
        ),
    ],
    locktime=0,
)

pubkey_bytes = bytes.fromhex(COMPRESSED_PUBKEY_HEX)
tx.vin[0].witness = transaction.Witness([der_sig, pubkey_bytes])

signed_tx_hex = tx.serialize().hex()
print("--- Signed Bitcoin SegWit Wire Transaction Hex ---")
print(signed_tx_hex)

# ==========================================
# 4. BROADCAST TO NETWORK
# ==========================================
if BROADCAST_API_URL:
  print(f"\nBroadcasting to {BROADCAST_API_URL}...")
  try:
    req = urllib.request.Request(
        BROADCAST_API_URL,
        data=signed_tx_hex.encode("utf-8"),
        headers={"Content-Type": "text/plain"},
        method="POST",
    )
    with urllib.request.urlopen(req) as resp:
      txid_result = resp.read().decode("utf-8").strip()
      print(f"✅ Broadcast Successful! Transaction ID (txid): {txid_result}")
      print(f"Track in Mempool Explorer: https://mempool.space/testnet4/tx/{txid_result}")
  except Exception as e:
    print(f"Broadcast response: {e}")
EOF

python3 broadcast_btc.py

疑難排解常見錯誤

  • mandatory-script-verify-flag-failed (Signature must be zero for failed CHECK(MULTI)SIG operation) / non-mandatory-script-verify-flag (Non-canonical DER signature):確保簽章的 s 元件採用標準 low-s 格式 (s <= N/2),並正確以 DER 編碼。
  • bad-txns-in-belowout / bad-txns-fee-negative:輸出總額超過輸入值。請確認變更計算結果 (InputSatoshis - PaymentSatoshis - MinerFee) 絕對是正值。

📖 附錄:作業指南 (查詢和檢查)

您隨時可以使用這些指令,檢查及管理資源狀態。


🏢 1. 執行個體

檢查及管理專案和位置中的 MPC 執行個體。

A. 列出所有執行個體

curl

curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "https://mpc.googleapis.com/v1beta/projects/my-project/locations/us-central1/instances" | jq

B. 取得特定執行個體的詳細資料

curl

curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "https://mpc.googleapis.com/v1beta/${INSTANCE_NAME}" | jq

👥 2. 團隊與使用者

檢查執行個體中的團隊和使用者。

A. 列出所有團隊

curl

curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/teams" | jq

B. 取得特定團隊的詳細資料

curl

curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${TEAM_NAME}" | jq

C. 列出所有已註冊的使用者

curl

curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/users" | jq

D. 取得特定使用者的詳細資料

curl

curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${SIGNER_1_NAME}" | jq

📜 3. 交易政策和 KeyList

檢查交易政策和金鑰清單。

A. 列出所有交易政策

curl

curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/transactionPolicies" | jq

B. 取得特定交易政策的詳細資料

curl

curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${POLICY_NAME}" | jq

C. 列出及取得 KeyList

curl

# List all KeyLists
curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/keyLists" | jq

# Get a specific KeyList
curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${KEY_LIST_NAME}" | jq

🔑 4. 金鑰和金鑰環

檢查金鑰環和分散式金鑰。

A. 列出所有 KeyRing

curl

curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/keyRings" | jq

B. 列出金鑰環中的所有金鑰

curl

curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${KEY_RING_NAME}/keys" | jq

C. 取得特定分散式金鑰的詳細資料

curl

curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${KEY_NAME}" | jq

📝 5. 交易 (簽署作業)

檢查交易和簽署狀態。

A. 列出特定金鑰 (或所有金鑰) 的所有交易

curl

# List transactions for a specific Key
curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${KEY_NAME}/transactions" | jq

# List transactions across all KeyRings and Keys using the '-' wildcard
curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/keyRings/-/keys/-/transactions" | jq

B. 取得特定交易的詳細資料或狀態

curl

curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${TRANSACTION_NAME}" | jq