マルチパーティ コンピューティング ユーザーガイド

このガイドでは、分散鍵、ユーザー、チーム、ポリシーを管理するための Multi-party Compute API のユーザー ジャーニーについて説明します。これは、主要なユーザー ジャーニーを学習するプレビュー版のお客様を対象としています。

概要

MPC API は、インスタンス、チーム、ユーザー、キーリング、分散鍵、しきい値署名セレモニーのライフサイクルを管理します。また、3 つのポリシーレイヤにわたって組み込みの暗号ガバナンスが適用されます。

  • 管理ポリシー(ロール クォーラム): 管理者はユーザーとチームをオンボーディングします。管理者ロールとポリシー作成者ロールを付与または取り消すには、管理者のクォーラム(EnableUserRole と ApproveRoleChange)が必要です。署名者ロールの付与は、1 人の管理者から直ちに有効になります。署名者だけでは既存の鍵へのアクセス権が付与されないためです。署名者は、鍵の生成時に KeyRing の KeyPolicy に含まれている場合にのみ、鍵の共有を受け取ります。
  • 鍵ポリシー(KeyRing.keyPolicy): 管理者は、KeyRing の作成時に KeyRing の KeyPolicy を定義し、どの署名者(署名者、チームにグループ化)が鍵共有を保持するか、署名の承認に必要な K-of-N 署名者の承認(CreateTransaction と RecordSignerDecision)を指定します。
  • トランザクション ポリシー(TransactionPolicy と KeyList): ポリシー作成者(POLICY_AUTHOR)のクォーラムが、Cedar permit ルールと forbid ルール(TransactionPolicy)および KeyList アドレス許可リストを提案、承認、または無効にします。すべての CreateTransaction リクエストは、署名前にアクティブなトランザクション ポリシー(PERMIT または DENY)に照らして評価されます。

前提条件

始める前に、次のものが揃っていることを確認してください。

  1. Google Cloud プロジェクト。プロジェクトを作成するには、プロジェクトを作成するをご覧ください。
  2. Cloud Shell を使用していない場合は、gcloud CLI がインストールされ、認証されていること。
  3. 必要な API が有効になっていること(まだ有効になっていない場合)。このガイドで使用する MPC API と、認証鍵の生成と署名に使用する場合は Cloud Key Management Service の 2 つが必要です。

      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. (省略可)組織が Google アカウントを使用せずに外部 ID プロバイダ(Okta や Microsoft Entra ID など)を使用して認証する場合は、Workforce Identity 連携(BYOID)を構成するを参照して、フェデレーション ID でアプリケーションのデフォルト認証情報(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 インスタンス内の 2 番目の ADMIN ユーザーのGoogle Cloud メールアドレスまたは BYOID プリンシパル。
署名者 1 のプリンシパル alice@example.com Google Cloud メールアドレスまたは署名者 1(Alice)の BYOID プリンシパル。
署名者 2 のプリンシパル bob@example.com Google Cloud メールアドレスまたは署名者 2(Bob)の BYOID プリンシパル。
署名者 3 のプリンシパル charlie@example.com Google Cloud 署名者 3(Charlie)のメールアドレスまたは BYOID プリンシパル。
ポリシー作成者 1 プリンシパル dave@example.com Google Cloud ポリシー作成者 1(Dave)のメールアドレスまたは BYOID プリンシパル。
ポリシー作成者 2 のプリンシパル eve@example.com ポリシー作成者 2(Eve)のGoogle Cloud メールアドレスまたは BYOID プリンシパル。

ターミナル セッションで各アカウントを 1 回認証します。これにより、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 を使用して作成された認証鍵のローカル識別子です。デフォルト値は有効であり、設定の手間を最小限に抑えるためにそのまま使用できます。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 KeyRing ID mpc-assent-keys Cloud KMS に保存されている外部認証鍵用の Cloud Key Management Service KeyRing。
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 を作成して検査し、以降のすべてのリソース リクエストをインスタンスの専用データプレーン URL(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 になるまで待ってから、データプレーン URL を呼び出す必要があります。

🕵️‍♂️ インスタンスの状態を確認する(ポーリング)

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 で認証鍵を管理することをおすすめします。ID に、キーリングの作成、鍵の作成、署名、公開鍵検証ツールの表示(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: チームとユーザーを登録する

エンティティがトランザクション署名(SIGNER として)またはポリシー作成(POLICY_AUTHOR として)に参加するには、プロジェクトに対する IAM 権限が付与され、インスタンスにアプリケーション レベルの User として登録されている必要があります(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)として、3 人の署名者を保持する「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 ロールを付与して署名者とポリシー作成者を登録する

各ユーザーは、MPC API を呼び出すためのプロジェクト レベルの IAM ロール(署名者とポリシー作成者の場合は roles/mpc.editor、管理者の場合は roles/mpc.admin)と、インスタンス内のアプリケーション レベルの 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. 管理者 2(ApproveRoleChange)として POLICY_AUTHOR を承認する

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. 管理者 2(ApproveRoleChange)として POLICY_AUTHOR を承認する

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 ポリシールールを作成します。さまざまなシナリオを管理するために多くのポリシーを構成できますが、このガイドでは、Ethereum または Bitcoin のトランザクションのポリシーを設定する方法について説明します。デフォルトでは、トランザクション ポリシーの評価はフェイルファスト(デフォルトでブロック)です。有効な permit ポリシーがトランザクションと一致しない場合、ポリシー評価の決定は DENY になります。Cedar 属性の詳細については、トランザクション ポリシールールを作成するをご覧ください。

Ethereum、KMS

このルールは、特定の受信者アドレス(0x1111111111111111111111111111111111111111)への小規模なイーサリアム転送(<= 2 ETH、10-9 ETH / Gwei 単位で 2,000,000,000 として表される)を許可します。

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

このルールは、特定の受信者アドレス(0x1111111111111111111111111111111111111111)への小規模なイーサリアム転送(<= 2 ETH、10-9 ETH / Gwei 単位で 2,000,000,000 として表される)を許可します。

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')

ビットコイン、KMS

このルールでは、BITCOIN_TX_V2 スキーマ(BIP 174 PSBT v0)を使用したビットコインの送金が許可されます。ただし、純アウトバウンド送金額が 250,000 satoshi(context.amount_token_scaled <= 2500000、または 10-9 の固定小数点単位に正規化された 0.0025 BTC)を超えず、マイナー手数料が 10,000 satoshi(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')

ビットコイン、OpenSSL

このルールでは、BITCOIN_TX_V2 スキーマ(BIP 174 PSBT v0)を使用したビットコインの送金が許可されます。ただし、純アウトバウンド送金額が 250,000 satoshi(context.amount_token_scaled <= 2500000、または 10-9 の固定小数点単位に正規化された 0.0025 BTC)を超えず、マイナー手数料が 10,000 satoshi(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. ポリシーを承認する(ACTIVE に移行する)

ポリシーの変更には 2 つの署名が必要であるため、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. インライン KeyPolicy を使用して KeyRing を作成する

KeyRing は MPC 鍵の論理グループであり、KeyRing で作成されたすべての鍵が従う不変のインライン KeyPolicy(keyRing.keyPolicy)を定義します。KeyPolicy は、しきい値ルール(PolicyNode)のツリーです。その子 subKeyShares は、teamNode(Team の totalMembers の threshold)、userNode(特定の SIGNER User)、またはネストされた policyNode にすることができます。

ここでは、${TEAM_NAME} の 3 つの署名者(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 は同期的に動作し、publicKeyInfo.publicKey とサーバーから取得された publicKeyInfo.blockchainAddresses(Ethereum と Bitcoin の場合は EC_SIGN_SECP256K1、Solana の場合はサポート開始後に EC_SIGN_ED25519)を含む ACTIVE Key リソースを返します。

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

Ethereum アドレスに資金を供給する

ステップ 7 でブロードキャストする前に、派生した Ethereum アドレスに、トランザクション値とガス費用をカバーするのに十分な Sepolia テストネット ETH があることを確認してください。新しく取得したアドレスを Faucet for Ethereum Sepolia に入力すると、無料のテストネット ETH をリクエストできます。

ビットコイン

ビットコイン メインネットのアドレス(p2wpkhAddress、p2shP2wpkhAddress、p2pkhAddress)が .publicKeyInfo.blockchainAddresses.secp256k1Addresses.bitcoinMainnetAddresses で返されます。非圧縮の 65 バイトの secp256k1 EC 公開鍵(0x04 || X || Y)から、33 バイトの圧縮公開鍵、ロック スクリプト、Testnet4 アドレスを 3 つの形式すべてで導出することもできます。

  1. ネイティブ SegWit(P2WPKH / BIP 173): 22 バイトのウィットネス scriptPubKey(0014<hash160>)を含む Bech32 アドレス(メインネットでは bc1q...、テストネット 4 では tb1q...)。これは、取引手数料が最も低い、推奨される最新の形式です。
  2. ネストされた SegWit(P2SH-P2WPKH / BIP 49): ラップされた SegWit アドレス(メインネットでは 3...、テストネット 4 では 2...)。23 バイトの scriptPubKey(a914<hash160>87)と 22 バイトの redeemScript(0014<hash160>)を使用し、レガシー システムと互換性があります。
  3. レガシー(P2PKH / BIP 44): 元の Pay-to-PubKey-Hash Base58 アドレス(メインネットでは 1...、テストネット 4 では 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. 派生したネイティブ SegWit アドレスに資金を供給します(テストネットのフォーセットを使用するなど)。
  2. ビットコイン ノード(bitcoin-cli listunspent 1 9999999 "[\"tb1q...\"]")またはブロック エクスプローラ API を使用して、アドレスの未使用の UTXO をクエリします。
  3. ステップ 6 の次の 4 つのパラメータに注意してください。
    • txid: 未使用出力の 32 バイトのトランザクション ハッシュ。
    • vout: ゼロから始まる出力インデックス(例: 0、1)。
    • value: 未使用の金額(satoshi 単位)(例: 200000 satoshi = 0.002 BTC)。
    • scriptPubKey: ロック スクリプトの 16 進数(例: 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。

ビットコイン、KMS

ビットコインの UTXO モデルでは、トランザクションは特定の未使用の出力を使用します。SegWit(BIP 143)では、署名ハッシュを計算するために、使用される入力のサトシ額とロック スクリプトを知る必要があります。

Bitcoin ポリシーの評価には、各入力に PSBT_IN_WITNESS_UTXO が入力された BIP 174 PSBT v0 形式(BITCOIN_TX_V2)が必要です。

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。

ビットコイン、OpenSSL

ビットコインの UTXO モデルでは、トランザクションは特定の未使用の出力を使用します。SegWit(BIP 143)では、署名ハッシュを計算するために、使用される入力のサトシ額とロック スクリプトを知る必要があります。

Bitcoin ポリシーの評価には、各入力に PSBT_IN_WITNESS_UTXO が入力された BIP 174 PSBT v0 形式(BITCOIN_TX_V2)が必要です。

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 つのうちの 1 つ)がすでに記録されているため、鍵共有を保持する追加の共同署名者 1 人だけが、transactions.recordSignerDecision(RecordSignerDecision)を使用してトランザクションを承認する必要があります。

transaction(${TRANSACTION_NAME})を指定して TransactionSigningPayload に署名し、署名者 2(bob@example.com)として recordSignerDecision を呼び出します。

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

リカバリ識別子の導出とブロードキャスト

MPC API は、finalSignature で未加工の 64 バイト ECDSA 署名(EIP-2 に準拠した正規の低 S 形式の 32 バイトの r 値と s 値で構成)と、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 ワイヤー トランザクションの組み立てとブロードキャスト

ビットコイン ネイティブ SegWit トランザクション(BIP 141)の場合、未加工の 64 バイトの (r, s) MPC 署名を正規の低 S ASN.1 DER にフォーマットし、0x01(SIGHASH_ALL)を追加して、33 バイトの圧縮公開鍵とともに 2 要素の SegWit ウィットネス スタック([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. 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/${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