이 가이드에서는 다자간 계산 API의 사용자 여정을 통해 분산 키, 사용자, 팀, 정책을 관리하는 방법을 설명합니다. 주요 사용자 여정을 학습하는 프리뷰 고객을 대상으로 합니다.
개요
MPC API는 인스턴스, 팀, 사용자, 키링, 분산 키, 임곗값 서명 절차의 수명 주기를 관리하며, 세 가지 정책 레이어에 걸쳐 적용되는 기본 제공 암호화 거버넌스를 갖추고 있습니다.
- 관리 정책 (역할 정족수): 관리자는 사용자 및 팀을 온보딩하며, 관리자 및 정책 작성자 역할을 부여하거나 취소하려면 관리자 정족수 (
EnableUserRole및ApproveRoleChange)가 필요합니다. 서명자 역할 부여는 단일 관리자로부터 즉시 적용됩니다. 서명자만으로는 기존 키에 대한 액세스 권한이 부여되지 않기 때문입니다. 서명자는 키 생성 중에 KeyRing의 KeyPolicy에 포함된 경우에만 키 공유를 받습니다. - 키 정책 (
KeyRing.keyPolicy): 관리자는 KeyRing을 만들 때 KeyRing의 KeyPolicy를 정의하여 키 공유를 보유하는 서명자 (서명자, 팀으로 그룹화됨)와 서명을 승인하는 데 필요한 K-of-N 서명자 승인 (CreateTransaction및RecordSignerDecision)을 지정합니다. - 트랜잭션 정책 (
TransactionPolicy및KeyList): 정책 작성자 (POLICY_AUTHOR)의 정족수가 Cedarpermit및forbid규칙 (TransactionPolicy)과KeyList주소 허용 목록을 제안, 승인 또는 사용 중지합니다. 모든CreateTransaction요청은 서명 전에 활성 트랜잭션 정책 (PERMIT또는DENY)에 따라 평가됩니다.
기본 요건
시작하기 전에 다음 사항을 확인하세요.
- Google Cloud 프로젝트 프로젝트를 만들려면 프로젝트 만들기를 참고하세요.
- Cloud Shell을 사용하지 않는 경우
gcloudCLI가 설치 및 인증되어 있어야 합니다. 필수 API가 사용 설정되어 있어야 합니다(아직 사용 설정하지 않은 경우). 이 가이드에서 사용되는 MPC API와 승인 키 생성 및 서명에 사용하는 경우 Cloud Key Management Service가 필요합니다.
gcloud services enable mpc.googleapis.com cloudkms.googleapis.com사용 설정된 API를 보려면 다음 안내를 따르세요.
gcloud services list --enabledPython 3.9 이상 및 튜토리얼 도우미 라이브러리가 설치되어 있어야 합니다.
pip install embit eth-account eth-utils requests(선택사항) 조직에서 Google 계정 없이 외부 ID 공급업체 (예: Okta 또는 Microsoft Entra ID)를 사용하여 인증하는 경우 직원 ID 제휴 (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 |
Google Cloud 인스턴스에 있는 두 번째 ADMIN 사용자의 이메일 또는 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 (찰리)의 이메일 또는 BYOID 주 구성원입니다. |
| 정책 작성자 1 주 구성원 | dave@example.com |
Google Cloud 정책 작성자 1 (Dave)의 이메일 또는 BYOID 주 구성원입니다. |
| 정책 작성자 2 주 구성원 | eve@example.com |
정책 작성자 2 (이브)의Google Cloud 이메일 또는 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를 사용하여 만든 승인 키의 로컬 식별자입니다. 기본값은 유효하며 설정 과정의 불편을 최소화하기 위해 그대로 유지할 수 있습니다. 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 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)이 반환됩니다. 데이터 영역 URL을 호출하기 전에 인스턴스 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에서 승인 키를 관리하는 것이 좋습니다. 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 역할이 함께 작동하는 방식은 보안 및 승인 참조).
등록하려면 관리자와 사용자 간의 대역 외 조정이 필요합니다.
- 개체 (서명자 또는 정책 작성자)는 로컬에서 키 쌍을 생성하고 공개 키를 관리자에게 안전하게 전송합니다.
- 관리자는 사용자의 주 구성원에게 프로젝트에 대한 적절한 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)를 만들어 서명자 3명을 보유하고 서버에서 생성된 리소스 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이
users.enableRole(EnableUserRole)을 호출하여 역할을PENDING_ACTIVE로 이동하고 관리자 1의 서명을 첫 번째 승인으로 기록합니다. - 관리자 2가 서명과 함께
users.approveRoleChange(ApproveRoleChange)를 호출하여 역할을ACTIVE로 전환합니다.
Cloud KMS
1. POLICY_AUTHOR을 관리자 1 (EnableUserRole)로 요청
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. POLICY_AUTHOR을 관리자 1 (EnableUserRole)로 요청
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
이 규칙은 특정 수신자 주소 (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
이 규칙은 순 송금액이 250,000사토시 (context.amount_token_scaled <= 2500000 또는 10-9 고정 소수점 단위로 정규화된 0.0025BTC)를 초과하지 않고 채굴자 수수료가 10,000사토시 (context.max_fee_atomic <= 100000)를 초과하지 않는 BITCOIN_TX_V2 스키마 (BIP 174 PSBT v0)를 사용하는 비트코인 전송을 허용합니다.
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
이 규칙은 순 송금액이 250,000사토시 (context.amount_token_scaled <= 2500000 또는 10-9 고정 소수점 단위로 정규화된 0.0025BTC)를 초과하지 않고 채굴자 수수료가 10,000사토시 (context.max_fee_atomic <= 100000)를 초과하지 않는 BITCOIN_TX_V2 스키마 (BIP 174 PSBT v0)를 사용하는 비트코인 전송을 허용합니다.
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개가 필요하므로 두 번째 정책 작성자가 서명된 TransactionPolicySigningPayload를 사용하여 transactionPolicies.approve (ApproveTransactionPolicy)를 호출해야 합니다.
이더리움
# 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는 하위 subKeyShares가 teamNode (Team의 totalMembers threshold), userNode (특정 SIGNER User) 또는 중첩된 policyNode일 수 있는 기준 규칙 (PolicyNode)의 트리입니다.
여기서는 ${TEAM_NAME}의 서명자 3명 (threshold: 2, totalMembers: 3, GCP_TEE에 저장됨)에 대해 3명 중 2명 기준점 규칙을 구성합니다.
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)
keys.create (CreateKey)를 관리자 1로 호출하여 KeyRing의 KeyPolicy에서 선택한 참여자 간에 분산 키 생성 (DKG)을 실행합니다.
CreateKey는 동기식이며 publicKeyInfo.publicKey 및 서버에서 파생된 publicKeyInfo.blockchainAddresses (이더리움 및 비트코인의 경우 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)에서 테스트넷 주소와 잠금 스크립트를 독립적으로 확인하거나 파생하고 거래에 필요한 미사용 자금을 찾을 수도 있습니다.
이더리움
이더리움 주소는 .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가 있는지 확인하세요. Ethereum Sepolia용 수도꼭지에 새로 파생된 주소를 입력하여 무료 테스트넷 ETH를 요청할 수 있습니다.
비트코인
비트코인 메인넷 주소 (p2wpkhAddress, p2shP2wpkhAddress, p2pkhAddress)는 .publicKeyInfo.blockchainAddresses.secp256k1Addresses.bitcoinMainnetAddresses로 반환됩니다. 압축되지 않은 65바이트 secp256k1 EC 공개 키 (0x04 || X || Y)에서 33바이트 압축 공개 키, 잠금 스크립트, Testnet4 주소를 세 가지 형식 모두에서 파생할 수 있습니다.
- 네이티브 SegWit (P2WPKH / BIP 173): 22바이트 위트니스
scriptPubKey(0014<hash160>)가 있는 Bech32 주소 (메인넷의 경우bc1q..., Testnet4의 경우tb1q...). 거래 수수료가 가장 낮은 최신 형식으로 권장됩니다. - 중첩 SegWit (P2SH-P2WPKH / BIP 49): 23바이트
scriptPubKey(a914<hash160>87) 및 22바이트 redeemScript (0014<hash160>)가 있는 래핑된 SegWit 주소 (메인넷의3..., Testnet4의2...)로, 기존 시스템과 호환됩니다. - 기존 (P2PKH/BIP 44): 25바이트
scriptPubKey(76a914<hash160>88ac)가 있는 원래 Pay-to-PubKey-Hash Base58 주소(메인넷의1..., Testnet4의m/n...)입니다.
주소 파생 스크립트 (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단계에서 트랜잭션을 구성하기 전에 다음을 수행합니다.
- 파생된 Native SegWit 주소에 자금을 입금합니다 (예: 테스트넷 수도꼭지 사용).
- 비트코인 노드 (
bitcoin-cli listunspent 1 9999999 "[\"tb1q...\"]") 또는 블록 탐색기 API를 사용하여 주소의 미사용 UTXO를 쿼리합니다. - 6단계에서 다음 4가지 매개변수를 확인합니다.
txid: 사용되지 않은 출력의 32바이트 트랜잭션 해시입니다.vout: 0부터 시작하는 출력 색인입니다 (예:0또는1).value: 사토시 단위의 미사용 금액입니다 (예:200000사토시 = 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: 승인 (허용된 값)
1Gwei의 트랜잭션을 만듭니다 (2ETH 한도 내).
# 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: 승인 (허용된 값)
1Gwei의 트랜잭션을 만듭니다 (2ETH 한도 내).
# 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)에서 서명 해시를 계산하려면 지출되는 입력의 사토시 금액과 잠금 스크립트를 알아야 합니다.
비트코인 정책 평가에는 각 입력에 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)에서 서명 해시를 계산하려면 지출되는 입력의 사토시 금액과 잠금 스크립트를 알아야 합니다.
비트코인 정책 평가에는 각 입력에 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에 서명하고 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인 경우 7단계에서 반환된 JSON에서 finalSignature 필드를 복사합니다.
7단계: 블록체인에 트랜잭션 브로드캐스트
트랜잭션이 SIGNING_SUCCEEDED 상태에 도달하고 finalSignature를 가져오면 마지막 단계는 서명된 유선 트랜잭션을 구성하고 네트워크에 브로드캐스트하는 것입니다.
이더리움
복구 식별자 파생 및 브로드캐스트
MPC API는 finalSignature에서 원시 64바이트 ECDSA 서명 (EIP-2에 따라 표준 low-S 형식의 32바이트 r 및 s 값으로 구성됨)과 recoveryId에서 복구 패리티 비트 (0 또는 1)를 반환합니다.
다음 Python 스크립트는 finalSignature를 사용하여 서명된 트랜잭션을 구성하고, 복구된 발신자 주소를 MPC 키의 이더리움 주소와 비교하여 확인하고, 네트워크에 브로드캐스트합니다.
방송 스크립트:
터미널에서 다음 복사하여 붙여넣을 수 있는 블록을 실행하여 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 키의 이더리움 주소에 작업을 처리하기에 충분한 테스트넷 ETH가 없습니다. 브로드캐스트하기 전에 주소에 자금을 입금해야 합니다.
비트코인
SegWit 송금 거래 조립 및 브로드캐스팅
비트코인 네이티브 SegWit 트랜잭션(BIP 141)의 경우 원시 64바이트 (r, s) MPC 서명을 표준 low-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. 키링의 모든 키 나열
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