Este guia mostra as jornadas do usuário da API Multi-party Compute para gerenciar chaves distribuídas, usuários, equipes e políticas. Ele é destinado a clientes em pré-lançamento que estão aprendendo as principais jornadas do usuário.
Visão geral
A API MPC gerencia o ciclo de vida de instâncias, equipes, usuários, conjuntos de chaves, chaves distribuídas e cerimônias de assinatura de limite, com governança criptográfica integrada aplicada em três camadas de política:
- Políticas de administração (quóruns de função): os administradores integram usuários e equipes, e um quórum de administradores (
EnableUserRoleeApproveRoleChange) é necessário para conceder ou revogar as funções de administrador e autor de políticas. A concessão do papel de Signatário entra em vigor imediatamente para um único administrador, porque o Signatário por si só não concede acesso às chaves atuais. Um Signatário só recebe compartilhamentos de chaves quando incluído em uma KeyPolicy de um KeyRing durante a geração de chaves. - Políticas de chaves (
KeyRing.keyPolicy): os administradores definem uma KeyPolicy de um KeyRing ao criar o KeyRing, especificando quais Signers (Signer, agrupados em Teams) têm compartilhamentos de chaves e as aprovações de signatários K-of-N (CreateTransactioneRecordSignerDecision) necessárias para autorizar uma assinatura. - Políticas de transação (
TransactionPolicyeKeyList): um quórum de autores de políticas (POLICY_AUTHOR) propõe, aprova ou desativa as regraspermiteforbid(TransactionPolicy) do Cedar e as listas de permissões de endereçosKeyList. Cada solicitação deCreateTransactioné avaliada em relação às políticas de transação ativas (PERMITouDENY) antes da assinatura.
Pré-requisitos
Antes de começar, verifique se você tem:
- Um projeto do Google Cloud . Consulte Criar projetos para saber como fazer isso.
- Se não estiver usando o Cloud Shell, a CLI
gcloudprecisa estar instalada e autenticada. As APIs necessárias precisam estar ativadas, se ainda não estiverem. Você precisa de duas: a API MPC usada neste guia e o Cloud Key Management Service, se estiver usando-o para geração e assinatura de chaves de autorização.
gcloud services enable mpc.googleapis.com cloudkms.googleapis.comPara conferir as APIs ativadas:
gcloud services list --enabledPython 3.9+ e as bibliotecas auxiliares do tutorial instaladas:
pip install embit eth-account eth-utils requests(Opcional) Se a organização faz a autenticação usando um provedor de identidade externo (como Okta ou Microsoft Entra ID) sem Contas do Google, consulte Configurar a federação de identidade de colaboradores (BYOID) para configurar o Application Default Credentials (ADC) com sua identidade federada.
Configuração
Atualize as variáveis a seguir para preencher os comandos neste guia.
Configuração necessária
Você precisa personalizar esses parâmetros. O guia não vai funcionar com os valores de marcador.
| Parâmetro | Valor | Descrição |
|---|---|---|
| ID do projeto | my-project |
O ID do projeto do consumidor da instância do MPC. |
| ID da instância | my-mpc-instance |
O ID da instância de MPC ([a-z]([a-z0-9-]{0,61}[a-z0-9])?). |
| Local | us-central1 |
Local de implantação (por exemplo, us-central1). |
| Admin 1 Principal | admin-1@example.com |
E-mail doGoogle Cloud ou principal do BYOID do primeiro usuário do ADMIN na instância. |
| Admin 2 Principal | admin-2@example.com |
E-mail doGoogle Cloud ou principal do BYOID do segundo usuário do ADMIN na instância. |
| Principal do signatário 1 | alice@example.com |
Google Cloud E-mail ou principal BYOID do Signatário 1 (Alice). |
| Principal do Signatário 2 | bob@example.com |
Google Cloud E-mail ou principal BYOID do Signatário 2 (Bob). |
| Signer 3 Principal | charlie@example.com |
Google Cloud E-mail ou principal BYOID do Signatário 3 (Charlie). |
| Policy Author 1 Principal | dave@example.com |
E-mailGoogle Cloud ou principal BYOID do Autor de Políticas 1 (Dave). |
| Policy Author 2 Principal | eve@example.com |
Google Cloud E-mail ou principal BYOID do autor da política 2 (Eve). |
Autentique cada conta uma vez na sessão do terminal para que gcloud auth print-access-token "<principal>" possa recuperar tokens para qualquer persona sem trocar de conta no meio do tutorial (ADMIN_1_PRINCIPAL é autenticado por último para que permaneça como a conta ativa da CLI do gcloud para comandos do IAM e do Cloud KMS):
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
Rótulos de recursos do tutorial
Esses são identificadores locais para as chaves de autorização criadas usando o Cloud KMS durante este tutorial. Os valores padrão são válidos e podem ser mantidos para minimizar o atrito de configuração. Os IDs de recursos para Team, User, TransactionPolicy, KeyRing, Key, KeyList e Transaction são gerados pelo servidor e capturados em variáveis de shell (TEAM_NAME, SIGNER_1_NAME, POLICY_NAME, KEY_RING_NAME, KEY_NAME, KEY_LIST_NAME, TRANSACTION_NAME) quando cada recurso é criado.
| Parâmetro | Valor | Descrição |
|---|---|---|
| ID do keyring do Cloud KMS | mpc-assent-keys |
KeyRing do Cloud Key Management Service para chaves de autorização externas armazenadas no Cloud KMS. |
| ID do administrador 1 do Cloud KMS | admin-1 |
Rótulo da chave de autorização do administrador 1 do MPC. |
| ID do administrador 2 do Cloud KMS | admin-2 |
Rótulo da chave de autorização do administrador 2 do MPC. |
| ID do signatário 1 do Cloud KMS | signer-alice |
Rótulo da chave de autorização do signatário 1 do MPC. |
| ID do signatário 2 do Cloud KMS | signer-bob |
Rótulo da chave de autorização do signatário 2 do MPC. |
| ID do signatário 3 do Cloud KMS | signer-charlie |
Rótulo da chave de autorização do MPC Signer 3. |
| ID do autor 1 da política do Cloud KMS | author-dave |
Rótulo da chave de autorização do autor da política 1. |
| ID do autor 2 da política do Cloud KMS | author-eve |
Rótulo da chave de autorização do autor da política 2. |
Etapa 1: criar uma instância de MPC
Um Instance de MPC representa seu ambiente de MPC dedicado e isolado. Para criar e administrar uma instância, é necessário ter o papel do IAM de Administrador de computação multipartidária (roles/mpc.admin) no projeto. Você cria e inspeciona o Instance como administrador usando MPCInstanceService no endpoint do plano de controle (https://mpc.googleapis.com) e envia todas as solicitações de recursos subsequentes para o URL do plano de dados dedicado da instância (INSTANCE_URL).
A. Criar a instância
Primeiro, verifique se os principais de administrador (admin-1@example.com e admin-2@example.com) têm roles/mpc.admin no projeto e chame instances.create (CreateInstance) como Admin 1 para provisionar sua instância de 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
Isso retorna uma operação de longa duração (LRO, na sigla em inglês). Aguarde até que a instância state se torne ACTIVE antes de chamar o URL do plano de dados.
🕵️♂️ Verificar o estado da instância (pesquisa)
Consulte o status da instância até que state se torne ACTIVE e salve o campo url em 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"
Etapa 2: gerar chaves de autorização
Para usar a API MPC, é preciso gerar pares de chaves para os seguintes papéis distintos. Essas são chaves de autorização criptográfica externa (User.authorizationKey), o que significa que você as gera e as mantém fora da instância de MPC. Portanto, elas são diferentes das partes de chave de MPC geradas durante a criação da chave:
- Administradores: autorizam mudanças nas funções de usuário do
ADMINe doPOLICY_AUTHORe criamKeys. - Autores de políticas: autorizam políticas de governança (
TransactionPolicy). - Signatários: fornecem autorização (
AUTHORIZEouDENY) em transações.
Gere as chaves de autorização criptográfica secp256k1 para seus signatários, autores de políticas e administradores. Esses comandos são idempotentes.
Cloud KMS
Recomendamos gerenciar as chaves de autorização no Cloud Key Management Service. Verifique se sua identidade tem permissões para criar keyrings, criar chaves, assinar e visualizar verificadores de chaves públicas (como roles/cloudkms.admin e roles/cloudkms.signerVerifier). Verifique se você tem um KeyRing criado e gere uma chave secp256k1 para cada signatário, autor de política e administrador.
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
Use o OpenSSL padrão para gerar chaves secp256k1 para cada Signatário, Autor de Políticas e Administrador.
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
Etapa 3: registrar equipes e usuários
Antes que qualquer entidade possa participar da assinatura de transações (como SIGNER) ou da criação de políticas (como POLICY_AUTHOR), ela precisa receber permissões do IAM no projeto e ser registrada como um User no nível do aplicativo na instância. Consulte Segurança e autorização para saber como os papéis do IAM e os papéis de User funcionam juntos.
O registro requer coordenação fora de banda entre o administrador e o usuário:
- A entidade (signatário ou autor da política) gera o par de chaves localmente e transfere a chave pública para o administrador de forma segura.
- O administrador concede ao principal do usuário o papel do IAM apropriado no projeto (
roles/mpc.editorpara signatários e autores de políticas;roles/mpc.adminpara administradores) e chama a API MPC para registrar oprincipale oauthorizationKeydo usuário.
A. Criar uma equipe de signatários
Cada User com o papel SIGNER precisa pertencer a um Team, e todos os outros Users com outros papéis não podem pertencer a um Team. Como Admin 1 (admin-1@example.com), crie um Team (CreateTeam) "Treasury Signers" para armazenar os três signatários e capturar o recurso name gerado pelo servidor:
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. Conceder papéis do IAM e registrar signatários e autores de políticas
Cada usuário precisa de um papel do IAM para envolvidos no projeto (roles/mpc.editor para signatários e autores de políticas e roles/mpc.admin para administradores) para chamar a API MPC e um recurso User no nível do aplicativo dentro da instância. Ao chamar users.create (CreateUser), o campo roles só pode estar vazio ou ser ["SIGNER"]:
- Signatários: recebem
roles/mpc.editorno IAM e são criados comroles: ["SIGNER"]eteam: "$TEAM_NAME". O papelSIGNERse tornaACTIVEimediatamente, sem exigir a aprovação do quórum de administradores, porque ter apenas o papelSIGNERnão concede acesso a nenhuma chave. Um Signatário só recebe compartilhamentos de chaves quando incluído noKeyPolicyde umKeyRingdurante a geração de chaves. Autores de políticas: receberam
roles/mpc.editorno IAM e foram criados sem papéis iniciais e semteam. Depois, receberam o papelPOLICY_AUTHORna etapa 3.C usando a aprovação de quórum de administrador (já quePOLICY_AUTHOReADMINtêm autoridade de governança em toda a instância).
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. Conceder o papel de autor de políticas (aprovação de quórum de administrador)
Para ativar ou desativar o papel POLICY_AUTHOR ou ADMIN em um User, é necessário um quórum de administradores:
- O Administrador 1 chama
users.enableRole(EnableUserRole), que move o papel paraPENDING_ACTIVEe registra a assinatura do Administrador 1 como a primeira aprovação. - O Admin 2 chama
users.approveRoleChange(ApproveRoleChange) com a assinatura dele, transferindo o papel paraACTIVE.
Cloud KMS
1. Solicitar POLICY_AUTHOR como Admin 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. Aprovar POLICY_AUTHOR como Admin 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. Solicitar POLICY_AUTHOR como Admin 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. Aprovar POLICY_AUTHOR como Admin 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"
Etapa 4: configurar políticas de transação
Configure as políticas de transação criando e aprovando um TransactionPolicy (e, opcionalmente, um KeyList).
A. Criar uma política (PENDING_ACTIVE)
Crie uma regra de política do Cedar que descreva quais transações são permitidas. Embora seja possível configurar muitas políticas para controlar diferentes cenários, este guia demonstra como configurar uma política para transações de Ethereum ou Bitcoin. Por padrão, a avaliação da política de transação é Fail-Fast (bloquear por padrão). Se nenhuma política permit ativa corresponder a uma transação, a decisão de avaliação da política será DENY. Para mais detalhes sobre os atributos do Cedar, consulte Escrever regras de política de transação.
Ethereum, KMS
Essa regra permite transferências menores de Ethereum (<= 2 ETH, representado como 2,000,000,000 em unidades de 10-9 ETH / Gwei) para um endereço de destinatário específico (0x1111111111111111111111111111111111111111).
permit (principal, action, resource)
when {
context.schema == "ETHEREUM_TX_V2" &&
context.secondary_key == "0x1111111111111111111111111111111111111111" &&
context has amount_token_scaled &&
context.amount_token_scaled <= 2000000000
};
sign_policy() {
local author_id=$1
local resource_type=$2
local resource_name=$3
local action=$4
local cedar_doc=$5
python3 -c 'import sys; rt, rn, act, doc = sys.argv[1:]; v = lambda n: bytes([n & 127 | 128]) + v(n >> 7) if n > 127 else bytes([n]); f = lambda k, b: bytes([k << 3 | 2]) + v(len(b)) + b; open("policy_payload.bin", "wb").write(f(1 if rt == "instance" else 2, rn.encode()) + bytes([24, 1 if act == "ACTIVATE" else 2]) + f(4, f(1, doc.encode())))' "${resource_type}" "${resource_name}" "${action}" "${cedar_doc}"
gcloud kms asymmetric-sign \
--version=1 \
--key="${author_id}-key" \
--keyring="mpc-assent-keys" \
--location="us-central1" \
--digest-algorithm=sha256 \
--input-file=policy_payload.bin \
--signature-file=policy_sig.bin > /dev/null
openssl base64 -A -in policy_sig.bin
}
export CEDAR_RULE='permit (principal, action, resource) when { context.schema == "ETHEREUM_TX_V2" && context.secondary_key == "0x1111111111111111111111111111111111111111" && context has amount_token_scaled && context.amount_token_scaled <= 2000000000 };'
# Sign as Policy Author 1
QUORUM_SIGNATURE=$(sign_policy "author-dave" "instance" "${INSTANCE_NAME}" "ACTIVATE" "${CEDAR_RULE}")
POLICY_PAYLOAD_B64=$(openssl base64 -A -in policy_payload.bin)
POLICY_RESPONSE=$(jq -n \
--arg rule "$CEDAR_RULE" \
--arg sig "$QUORUM_SIGNATURE" \
--arg payload "$POLICY_PAYLOAD_B64" \
'{
transactionPolicy: {
displayName: "Ethereum Transfer Permit Policy",
rule: {cedarDocument: $rule}
},
authorizationInfo: {
signature: $sig,
signedPayload: $payload
},
requestId: "11111111-1111-4111-8111-111111111201"
}' \
| curl -s -X POST \
-H "Authorization: Bearer $(gcloud auth print-access-token "dave@example.com")" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d @- \
"${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/transactionPolicies")
echo "$POLICY_RESPONSE" | jq
export POLICY_NAME=$(echo "$POLICY_RESPONSE" | jq -r '.name')
Ethereum, OpenSSL
Essa regra permite transferências menores de Ethereum (<= 2 ETH, representado como 2,000,000,000 em unidades de 10-9 ETH / Gwei) para um endereço de destinatário específico (0x1111111111111111111111111111111111111111).
permit (principal, action, resource)
when {
context.schema == "ETHEREUM_TX_V2" &&
context.secondary_key == "0x1111111111111111111111111111111111111111" &&
context has amount_token_scaled &&
context.amount_token_scaled <= 2000000000
};
sign_policy() {
local author_id=$1
local resource_type=$2
local resource_name=$3
local action=$4
local cedar_doc=$5
python3 -c 'import sys; rt, rn, act, doc = sys.argv[1:]; v = lambda n: bytes([n & 127 | 128]) + v(n >> 7) if n > 127 else bytes([n]); f = lambda k, b: bytes([k << 3 | 2]) + v(len(b)) + b; open("policy_payload.bin", "wb").write(f(1 if rt == "instance" else 2, rn.encode()) + bytes([24, 1 if act == "ACTIVATE" else 2]) + f(4, f(1, doc.encode())))' "${resource_type}" "${resource_name}" "${action}" "${cedar_doc}"
openssl dgst -sha256 -sign "${author_id}_priv.pem" -out policy_sig.bin policy_payload.bin
openssl base64 -A -in policy_sig.bin
}
export CEDAR_RULE='permit (principal, action, resource) when { context.schema == "ETHEREUM_TX_V2" && context.secondary_key == "0x1111111111111111111111111111111111111111" && context has amount_token_scaled && context.amount_token_scaled <= 2000000000 };'
# Sign as Policy Author 1
QUORUM_SIGNATURE=$(sign_policy "author-dave" "instance" "${INSTANCE_NAME}" "ACTIVATE" "${CEDAR_RULE}")
POLICY_PAYLOAD_B64=$(openssl base64 -A -in policy_payload.bin)
POLICY_RESPONSE=$(jq -n \
--arg rule "$CEDAR_RULE" \
--arg sig "$QUORUM_SIGNATURE" \
--arg payload "$POLICY_PAYLOAD_B64" \
'{
transactionPolicy: {
displayName: "Ethereum Transfer Permit Policy",
rule: {cedarDocument: $rule}
},
authorizationInfo: {
signature: $sig,
signedPayload: $payload
},
requestId: "11111111-1111-4111-8111-111111111201"
}' \
| curl -s -X POST \
-H "Authorization: Bearer $(gcloud auth print-access-token "dave@example.com")" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d @- \
"${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/transactionPolicies")
echo "$POLICY_RESPONSE" | jq
export POLICY_NAME=$(echo "$POLICY_RESPONSE" | jq -r '.name')
Bitcoin, KMS
Essa regra permite transferências de Bitcoin usando o esquema BITCOIN_TX_V2 (BIP 174 PSBT v0), em que o valor líquido da transferência de saída não excede 250.000 satoshis (context.amount_token_scaled <= 2500000 ou 0,0025 BTC normalizado para 10-9 unidades decimais fixas) e a taxa do minerador não excede 10.000 satoshis (context.max_fee_atomic <= 100000).
permit (principal, action, resource)
when {
context.schema == "BITCOIN_TX_V2" &&
context has amount_token_scaled &&
context.amount_token_scaled <= 2500000 &&
context.max_fee_atomic <= 100000
};
sign_policy() {
local author_id=$1
local resource_type=$2
local resource_name=$3
local action=$4
local cedar_doc=$5
python3 -c 'import sys; rt, rn, act, doc = sys.argv[1:]; v = lambda n: bytes([n & 127 | 128]) + v(n >> 7) if n > 127 else bytes([n]); f = lambda k, b: bytes([k << 3 | 2]) + v(len(b)) + b; open("policy_payload.bin", "wb").write(f(1 if rt == "instance" else 2, rn.encode()) + bytes([24, 1 if act == "ACTIVATE" else 2]) + f(4, f(1, doc.encode())))' "${resource_type}" "${resource_name}" "${action}" "${cedar_doc}"
gcloud kms asymmetric-sign \
--version=1 \
--key="${author_id}-key" \
--keyring="mpc-assent-keys" \
--location="us-central1" \
--digest-algorithm=sha256 \
--input-file=policy_payload.bin \
--signature-file=policy_sig.bin > /dev/null
openssl base64 -A -in policy_sig.bin
}
export CEDAR_RULE='permit (principal, action, resource) when { context.schema == "BITCOIN_TX_V2" && context has amount_token_scaled && context.amount_token_scaled <= 2500000 && context.max_fee_atomic <= 100000 };'
# Sign as Policy Author 1
QUORUM_SIGNATURE=$(sign_policy "author-dave" "instance" "${INSTANCE_NAME}" "ACTIVATE" "${CEDAR_RULE}")
POLICY_PAYLOAD_B64=$(openssl base64 -A -in policy_payload.bin)
POLICY_RESPONSE=$(jq -n \
--arg rule "$CEDAR_RULE" \
--arg sig "$QUORUM_SIGNATURE" \
--arg payload "$POLICY_PAYLOAD_B64" \
'{
transactionPolicy: {
displayName: "Bitcoin PSBT Permit Policy",
rule: {cedarDocument: $rule}
},
authorizationInfo: {
signature: $sig,
signedPayload: $payload
},
requestId: "11111111-1111-4111-8111-111111111202"
}' \
| curl -s -X POST \
-H "Authorization: Bearer $(gcloud auth print-access-token "dave@example.com")" \
-H "X-Goog-User-Project: my-project" \
-H "Content-Type: application/json" \
-d @- \
"${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/transactionPolicies")
echo "$POLICY_RESPONSE" | jq
export POLICY_NAME=$(echo "$POLICY_RESPONSE" | jq -r '.name')
Bitcoin, OpenSSL
Essa regra permite transferências de Bitcoin usando o esquema BITCOIN_TX_V2 (BIP 174 PSBT v0), em que o valor líquido da transferência de saída não excede 250.000 satoshis (context.amount_token_scaled <= 2500000 ou 0,0025 BTC normalizado para 10-9 unidades decimais fixas) e a taxa do minerador não excede 10.000 satoshis (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. Política de aprovação (transição para ACTIVE)
As mudanças na política exigem duas assinaturas. Portanto, um segundo autor de política precisa chamar transactionPolicies.approve (ApproveTransactionPolicy) com um TransactionPolicySigningPayload assinado.
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
Bitcoin
# 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. Criar e gerenciar uma KeyList (opcional)
Uma KeyList (KeyListService) é uma lista nomeada de endereços de blockchain (manualEntry, key de MPC ou keyRing) que as políticas podem referenciar como destinos confiáveis. Os administradores (ADMIN) podem criar um KeyList com keyLists.create e atualizar as entradas a qualquer momento com keyLists.addEntries e 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')
Etapa 5: criar uma chave
Como Admin 1 (admin-1@example.com), crie um KeyRing com um KeyPolicy inline e gere uma chave MPC distribuída.
A. Criar um KeyRing com uma KeyPolicy inline
Um KeyRing é um grupo lógico para suas chaves de MPC e define um KeyPolicy (keyRing.keyPolicy) inline imutável que cada chave criada no KeyRing segue. Um KeyPolicy é uma árvore de regras de limite (PolicyNode) em que o subKeyShares filho pode ser teamNode (threshold de totalMembers em um Team), userNode (um SIGNER User específico) ou um policyNode aninhado.
Aqui, configuramos uma regra de limite de 2 de 3 para os três signatários em ${TEAM_NAME} (threshold: 2, totalMembers: 3, armazenados em GCP_TEE):
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. Gerar chave distribuída (DKG)
Chame keys.create (CreateKey) como Admin 1 para executar a geração de chaves distribuídas (DKG, na sigla em inglês) entre os participantes selecionados pelo KeyPolicy do seu KeyRing.
CreateKey é síncrono e retorna o recurso ACTIVE Key, incluindo publicKeyInfo.publicKey e publicKeyInfo.blockchainAddresses derivado do servidor (EC_SIGN_SECP256K1 para Ethereum e Bitcoin ou EC_SIGN_ED25519 para Solana quando houver suporte):
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. Derivar endereço de blockchain e localizar UTXOs
Quando CreateKey é concluído, publicKeyInfo.blockchainAddresses na resposta inclui automaticamente endereços derivados (secp256k1Addresses.ethereumAddress e secp256k1Addresses.bitcoinMainnetAddresses para EC_SIGN_SECP256K1 ou ed25519Addresses.solanaAddress para EC_SIGN_ED25519). Também é possível verificar ou derivar de forma independente endereços da Testnet e scripts de bloqueio da chave pública não compactada de 65 bytes (0x04 || X || Y) usando os scripts a seguir e localizar os fundos não gastos necessários para as transações.
Ethereum
Os endereços do Ethereum são retornados em .publicKeyInfo.blockchainAddresses.secp256k1Addresses.ethereumAddress e também podem ser derivados do hash Keccak-256 do ponto de chave pública descompactado de 64 bytes (pub_bytes[1:]), usando os últimos 20 bytes.
Script de derivação de endereço (derive_address.py)
Execute o bloco a seguir para criar e executar 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
Financiar o endereço do Ethereum
Antes de transmitir na Etapa 7, verifique se o endereço Ethereum derivado tem ETH suficiente na rede de teste Sepolia para cobrir o valor da transação e os custos de gás. Para solicitar ETH de testnet sem custo financeiro, insira o endereço recém-derivado no Faucet para Ethereum Sepolia.
Bitcoin
Os endereços da rede principal do Bitcoin (p2wpkhAddress, p2shP2wpkhAddress e p2pkhAddress) são retornados em .publicKeyInfo.blockchainAddresses.secp256k1Addresses.bitcoinMainnetAddresses. Com a chave pública EC secp256k1 de 65 bytes descompactada (0x04 || X || Y), também é possível derivar a chave pública compactada de 33 bytes, os scripts de bloqueio e os endereços da Testnet4 nos três formatos:
- Native SegWit (P2WPKH / BIP 173): endereço Bech32 (
bc1q...na Mainnet,tb1q...na Testnet4) com um witnessscriptPubKeyde 22 bytes (0014<hash160>). Esse é o formato moderno recomendado com as menores taxas de transação. - SegWit aninhado (P2SH-P2WPKH / BIP 49): endereço SegWit encapsulado (
3...na Mainnet,2...na Testnet4) com umscriptPubKey(a914<hash160>87) de 23 bytes e um redeemScript (0014<hash160>) de 22 bytes, compatível com sistemas legados. - Legado (P2PKH / BIP 44): endereço Base58 original de Pay-to-PubKey-Hash (
1...na Mainnet,m/n...na Testnet4) com umscriptPubKey(76a914<hash160>88ac) de 25 bytes.
Script de derivação de endereço (derive_btc_address.py)
Execute o bloco a seguir para criar e executar 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
Como localizar saídas não gastas (UTXOs)
Ao contrário dos blockchains baseados em contas, as transações de Bitcoin gastam saídas de transação não gastas (UTXOs, na sigla em inglês) específicas. Antes de criar uma transação na Etapa 6:
- Adicione fundos ao seu endereço Native SegWit derivado (por exemplo, usando um faucet de testnet).
- Consulte os UTXOs não gastos do seu endereço usando o nó do Bitcoin (
bitcoin-cli listunspent 1 9999999 "[\"tb1q...\"]") ou uma API de explorador de blocos. - Observe os quatro parâmetros a seguir para a Etapa 6:
txid: o hash de transação de 32 bytes da saída não gasta.vout: o índice de saída com indexação zero (por exemplo,0ou1).value: o valor não gasto em satoshis (por exemplo,200000satoshis = 0,002 BTC).scriptPubKey: o hexadecimal do script de bloqueio (por exemplo,0014...).
Etapa 6: executar uma operação de MPC
A avaliação da política e o início da assinatura acontecem juntos quando um SIGNER chama transactions.create (CreateTransaction). A instância decodifica payload de acordo com schema, avalia todas as políticas de transação ativas, registra o resultado em policyEvaluationRecord e registra a decisão de AUTHORIZE do signatário inicial.
A. Criar transação (avaliar política e iniciar assinatura)
Envie a transação como Signer 1 (alice@example.com) para avaliação e assinatura da política:
Ethereum, KMS
🛑 Caso A: rejeição (valor muito alto)
Tentativa de criar uma transação de 3 ETH (excede o limite de 2 ETH da nossa política):
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
Resultado esperado da avaliação da política: decision: DENY (denyReason: NO_PERMIT_RULE_MATCHED).
✅ Caso B: aprovação (valor permitido)
Crie uma transação de 1 Gwei (dentro do limite de 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')
Resposta esperada: state: PENDING_AUTHORIZATION e policyEvaluationRecord.decision: PERMIT.
Ethereum, OpenSSL
🛑 Caso A: rejeição (valor muito alto)
Tentativa de criar uma transação de 3 ETH (excede o limite de 2 ETH da nossa política):
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
Resultado esperado da avaliação da política: decision: DENY (denyReason: NO_PERMIT_RULE_MATCHED).
✅ Caso B: aprovação (valor permitido)
Crie uma transação de 1 Gwei (dentro do limite de 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')
Resposta esperada: state: PENDING_AUTHORIZATION e policyEvaluationRecord.decision: PERMIT.
Bitcoin, KMS
No modelo UTXO do Bitcoin, as transações gastam saídas específicas não gastas. No SegWit (BIP 143), para calcular o hash de assinatura, é preciso saber o valor em satoshis e o script de bloqueio da entrada que está sendo gasta.
A avaliação da política do Bitcoin requer o formato BIP 174 PSBT v0 (BITCOIN_TX_V2) com PSBT_IN_WITNESS_UTXO preenchido para cada entrada.
Script de construção de PSBT (build_psbt.py)
Execute o bloco a seguir para montar um payload válido do 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"
🛑 Caso A: rejeição (limite de taxa excedido)
Tentativa de criar uma transação em que a taxa de mineração excede o limite de 10.000 satoshis da política. Neste exemplo, definir MINER_FEE_SATS = 15000 (e CHANGE_SATS = 135000) em build_psbt.py é escalonado para max_fee_atomic = 150000 (excedendo o limite da política de 100000), produzindo o seguinte payload que aciona uma decisão de avaliação da política 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
Resultado esperado da avaliação da política: decision: DENY (denyReason: NO_PERMIT_RULE_MATCHED).
✅ Caso B: aprovação (PSBT de Bitcoin permitida)
Crie uma transação com o PSBT válido construído anteriormente (RECIPIENT_SATS = 50000 é mapeado para amount_token_scaled = 500000 <= 2500000 e MINER_FEE_SATS = 250 é mapeado para 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')
Resposta esperada: state: PENDING_AUTHORIZATION e policyEvaluationRecord.decision: PERMIT.
Bitcoin, OpenSSL
No modelo UTXO do Bitcoin, as transações gastam saídas específicas não gastas. No SegWit (BIP 143), para calcular o hash de assinatura, é preciso saber o valor em satoshis e o script de bloqueio da entrada que está sendo gasta.
A avaliação da política do Bitcoin requer o formato BIP 174 PSBT v0 (BITCOIN_TX_V2) com PSBT_IN_WITNESS_UTXO preenchido para cada entrada.
Script de construção de PSBT (build_psbt.py)
Execute o bloco a seguir para montar um payload válido do 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"
🛑 Caso A: rejeição (limite de taxa excedido)
Tentativa de criar uma transação em que a taxa de mineração excede o limite de 10.000 satoshis da política. Neste exemplo, definir MINER_FEE_SATS = 15000 (e CHANGE_SATS = 135000) em build_psbt.py é escalonado para max_fee_atomic = 150000 (excedendo o limite da política de 100000), produzindo o seguinte payload que aciona uma decisão de avaliação da política 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
Resultado esperado da avaliação da política: decision: DENY (denyReason: NO_PERMIT_RULE_MATCHED).
✅ Caso B: aprovação (PSBT de Bitcoin permitida)
Crie uma transação com o PSBT válido construído anteriormente (RECIPIENT_SATS = 50000 é mapeado para amount_token_scaled = 500000 <= 2500000 e MINER_FEE_SATS = 250 é mapeado para 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')
Resposta esperada: state: PENDING_AUTHORIZATION e policyEvaluationRecord.decision: PERMIT.
🕵️♂️ Verificar o estado da transação (sondagem)
Verifique o status da transação executando o seguinte comando periodicamente:
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. Registrar decisão do signatário (autorizar)
Como o CreateTransaction na Etapa 6.A já registrou a decisão AUTHORIZE do Signatário 1 (1 de 2 exigidas pelo limite de 2 de 3 de KeyPolicy), apenas mais um co-signatário com uma chave compartilhada precisa autorizar a transação usando transactions.recordSignerDecision (RecordSignerDecision).
Assine o TransactionSigningPayload nomeando transaction (${TRANSACTION_NAME}) e chame recordSignerDecision como Signatário 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. Buscar assinatura final
Quando o limite de autorizações for atingido, verifique o estado da transação e busque finalSignature (e recoveryId para chaves EC_SIGN_SECP256K1):
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
Quando state for SIGNING_SUCCEEDED, copie o campo finalSignature do JSON retornado para a Etapa 7.
Etapa 7: transmitir a transação para o blockchain
Depois que a transação atingir o estado SIGNING_SUCCEEDED e você tiver buscado o finalSignature, a última etapa é criar a transação eletrônica assinada e transmiti-la para a rede.
Ethereum
Derivação e transmissão do identificador de recuperação
A API MPC retorna uma assinatura ECDSA bruta de 64 bytes em finalSignature (composta pelos valores de 32 bytes r e s na forma canônica de baixo S, de acordo com o EIP-2), junto com o bit de paridade de recuperação (0 ou 1) em recoveryId.
O script Python a seguir cria a transação assinada usando seu finalSignature, verifica o endereço do remetente recuperado em relação ao endereço Ethereum da sua chave MPC e o transmite para a rede.
Script de transmissão:
Execute o seguinte bloco de copiar e colar no terminal para gravar e executar 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
Solução de problemas comuns
invalid sender: recovery failedouinvalid transaction v, r, s values:isso ocorre se os parâmetros de transação no seu script não corresponderem exatamente ao payload que foi assinado. Verifique se os camposvalue,nonceetocorrespondem ao que foi processado na Etapa 6.insufficient funds for gas * price + value: sua transação é criptograficamente válida e foi aceita pelo nó, mas o endereço Ethereum da sua chave MPC não tem ETH de testnet suficiente para cobrir a operação. Você precisa financiar seu endereço antes de transmitir.
Bitcoin
Montagem e transmissão de transações eletrônicas do SegWit
Para transações SegWit nativas do Bitcoin (BIP 141), a assinatura MPC (r, s) bruta de 64 bytes precisa ser formatada em ASN.1 DER canônico de baixo S, anexada com 0x01 (SIGHASH_ALL) e montada com a chave pública compactada de 33 bytes na pilha de testemunhas SegWit de 2 elementos: [DER signature || 0x01, compressed public key].
Script de transmissão:
Execute o bloco a seguir para criar e executar 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
Solução de problemas comuns
mandatory-script-verify-flag-failed (Signature must be zero for failed CHECK(MULTI)SIG operation)/non-mandatory-script-verify-flag (Non-canonical DER signature):verifique se o componentesda assinatura está no formato canônico low-s (s <= N/2) e codificado corretamente em DER.bad-txns-in-belowout/bad-txns-fee-negative:o total de saídas excede o valor de entrada. Verifique se o cálculo da mudança (InputSatoshis - PaymentSatoshis - MinerFee) é estritamente positivo.
📖 Apêndice: manual operacional (consultas e inspeções)
Use esses comandos para inspecionar e gerenciar o estado dos recursos a qualquer momento.
🏢 1. Instâncias
Inspecione e gerencie instâncias de MPC no seu projeto e local.
A. Listar todas as instâncias
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. Ver detalhes de uma instância específica
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. Equipes e usuários
Inspecione equipes e usuários na sua instância.
A. Listar todas as equipes
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. Ver detalhes de uma equipe específica
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. Listar todos os usuários registrados
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. Ver detalhes de um usuário específico
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. Políticas de transação e KeyLists
Inspecione as políticas de transação e as listas de chaves.
A. Listar todas as políticas de transação
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. Ver detalhes de uma política de transação específica
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. Listar e receber KeyLists
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. Chaves e KeyRings
Inspecionar keyrings e chaves distribuídas.
A. Listar todos os KeyRings
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. Listar todas as chaves em um 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. Ver detalhes de uma chave distribuída específica
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. Transações (operações de assinatura)
Inspecione as transações e o status de assinatura.
A. Listar todas as transações de uma chave (ou de todas as chaves)
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. Ver detalhes ou status de uma transação específica
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