En esta guía, se explican los recorridos del usuario de la API de computación multipartita para administrar claves, usuarios, equipos y políticas distribuidos. Está destinado a los clientes de la versión preliminar que están aprendiendo los recorridos del usuario clave.
Descripción general
La API de MPC administra el ciclo de vida de las instancias, los equipos, los usuarios, los llaveros de claves, las claves distribuidas y las ceremonias de firma de umbral, con una gobernanza criptográfica integrada que se aplica en tres capas de políticas:
- Políticas de administración (quórums de roles): Los administradores incorporan usuarios y equipos, y se requiere un quórum de administradores (
EnableUserRoleyApproveRoleChange) para otorgar o revocar los roles de administrador y autor de políticas. Otorgar el rol de Firmante tiene efecto inmediato desde un solo administrador porque el rol de Firmante por sí solo no otorga acceso a las claves existentes; un Firmante solo recibe fragmentos de claves cuando se incluye en una KeyPolicy de un KeyRing durante la generación de claves. - Políticas de claves (
KeyRing.keyPolicy): Los administradores definen la KeyPolicy de un KeyRing cuando lo crean, especificando qué firmantes (Signer, agrupados en equipos) tienen partes de claves y las aprobaciones de firmantes K de N (CreateTransactionyRecordSignerDecision) necesarias para autorizar una firma. - Políticas de transacciones (
TransactionPolicyyKeyList): Un quórum de autores de políticas (POLICY_AUTHOR) propone, aprueba o inhabilita las reglas depermityforbidde Cedar (TransactionPolicy) y las listas de entidades permitidas de direcciones deKeyList. Cada solicitud deCreateTransactionse evalúa en función de las políticas de transacciones activas (PERMIToDENY) antes de firmar.
Requisitos previos
Antes de comenzar, asegúrate de tener lo siguiente:
- Un proyecto de Google Cloud . Consulta Crea proyectos para crear uno.
- Si no usas Cloud Shell, la CLI de
gclouddebe estar instalada y autenticada. Las APIs requeridas deben estar habilitadas, si aún no lo están. Necesitas dos: la API de MPC que se usa en esta guía y Cloud Key Management Service si lo usas para generar y firmar claves de autorización.
gcloud services enable mpc.googleapis.com cloudkms.googleapis.comPara ver las APIs habilitadas, sigue estos pasos:
gcloud services list --enabledPython 3.9 o una versión posterior y las bibliotecas auxiliares del instructivo instaladas:
pip install embit eth-account eth-utils requests(Opcional) Si tu organización se autentica con un proveedor de identidad externo (como Okta o Microsoft Entra ID) sin Cuentas de Google, consulta Configura la federación de identidades de personal (BYOID) para configurar las credenciales predeterminadas de la aplicación (ADC) con tu identidad federada.
Configuración
Actualiza las siguientes variables para propagar los comandos en esta guía.
Configuración obligatoria
Debes personalizar estos parámetros. La guía no funcionará con los valores de marcador de posición.
| Parámetro | Valor | Descripción |
|---|---|---|
| ID del proyecto | my-project |
Es el ID del proyecto de consumidor de tu instancia de MPC. |
| ID de instancia | my-mpc-instance |
El ID de tu instancia de MPC ([a-z]([a-z0-9-]{0,61}[a-z0-9])?). |
| Ubicación | us-central1 |
Ubicación de la Deployment (por ejemplo, us-central1). |
| Principal de administrador 1 | admin-1@example.com |
Es el correo electrónico deGoogle Cloud o la principal de BYOID del primer usuario de ADMIN en la instancia. |
| Director de la escuela 2 | admin-2@example.com |
Es el correo electrónico deGoogle Cloud o la principal de BYOID del segundo usuario de ADMIN en la instancia. |
| Principal del firmante 1 | alice@example.com |
Correo electrónico deGoogle Cloud o principal de BYOID del firmante 1 (Alice). |
| Principal del firmante 2 | bob@example.com |
Google Cloud Correo electrónico o principal de BYOID del firmante 2 (Bob). |
| Principal del firmante 3 | charlie@example.com |
Correo electrónico deGoogle Cloud o principal de BYOID del firmante 3 (Carlos). |
| Principal de Author 1 de la política | dave@example.com |
Correo electrónico deGoogle Cloud o principal de BYOID del autor de la política 1 (Dave). |
| Principal de Policy Author 2 | eve@example.com |
Correo electrónico deGoogle Cloud o principal de BYOID de la autora de políticas 2 (Eva). |
Autentica cada cuenta una vez en tu sesión de terminal para que gcloud auth print-access-token "<principal>" pueda recuperar tokens para cualquier persona sin cambiar de cuenta a mitad del instructivo (ADMIN_1_PRINCIPAL se autentica al final para que siga siendo la cuenta de la CLI de gcloud activa para los comandos de IAM y 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
Etiquetas de recursos del instructivo
Estos son identificadores locales para las claves de autorización creadas con Cloud KMS durante este instructivo. Los valores predeterminados son válidos y se pueden mantener para minimizar las dificultades de configuración. Ten en cuenta que los IDs de recurso para los recursos Team, User, TransactionPolicy, KeyRing, Key, KeyList y Transaction se generan en el servidor y se capturan en variables de shell (TEAM_NAME, SIGNER_1_NAME, POLICY_NAME, KEY_RING_NAME, KEY_NAME, KEY_LIST_NAME, TRANSACTION_NAME) cuando se crea cada recurso.
| Parámetro | Valor | Descripción |
|---|---|---|
| ID del llavero de claves de Cloud KMS | mpc-assent-keys |
Es el llavero de claves de Cloud Key Management Service para las claves de autorización externas almacenadas en Cloud KMS. |
| ID del administrador 1 de Cloud KMS | admin-1 |
Etiqueta de la clave de autorización del administrador 1 de MPC. |
| ID del administrador 2 de Cloud KMS | admin-2 |
Etiqueta de la clave de autorización del administrador 2 de MPC. |
| ID del firmante 1 de Cloud KMS | signer-alice |
Etiqueta de la clave de autorización del firmante 1 de MPC. |
| ID del firmante 2 de Cloud KMS | signer-bob |
Etiqueta de la clave de autorización del firmante 2 de MPC. |
| ID del firmante 3 de Cloud KMS | signer-charlie |
Etiqueta de la clave de autorización del firmante 3 de MPC. |
| ID del autor 1 de la política de Cloud KMS | author-dave |
Etiqueta de la clave de autorización del autor de la política 1. |
| ID del autor de la política 2 de Cloud KMS | author-eve |
Etiqueta de la clave de autorización del autor de la política 2. |
Paso 1: Crea una instancia de MPC
Un Instance de MPC representa tu entorno de MPC dedicado y aislado. Para crear y administrar una instancia, se requiere el rol de IAM de administrador de procesamiento de varias partes (roles/mpc.admin) en tu proyecto. Creas y, luego, inspeccionas el Instance como administrador con MPCInstanceService en el extremo del plano de control (https://mpc.googleapis.com) y envías todas las solicitudes de recursos posteriores a la URL del plano de datos dedicado de la instancia (INSTANCE_URL).
A. Crea la instancia
Primero, asegúrate de que tus principales de administrador (admin-1@example.com y admin-2@example.com) tengan roles/mpc.admin en el proyecto y llama a instances.create (CreateInstance) como administrador 1 para aprovisionar tu instancia 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
Esto devuelve una operación de larga duración (LRO). Debes esperar a que la instancia state se convierta en ACTIVE antes de llamar a su URL del plano de datos.
🕵️♂️ Verifica el estado de la instancia (sondeo)
Sondea el estado de la instancia hasta que state se convierta en ACTIVE y guarda su campo url en 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"
Paso 2: Genera claves de autorización
Para usar la API de MPC, debes generar pares de claves para los siguientes roles distintos. Ten en cuenta que estas son claves de autorización criptográfica externas (User.authorizationKey), lo que significa que las generas y custodias fuera de la instancia de MPC y, por lo tanto, son distintas de los fragmentos de clave de MPC generados durante la creación de la clave:
- Administradores: Autorizan cambios en los roles de usuario de
ADMINyPOLICY_AUTHOR, y creanKey. - Autores de políticas: Autorizan las políticas de administración (
TransactionPolicy). - Firmantes: Proporcionan autorización (
AUTHORIZEoDENY) en las transacciones.
Genera las claves de autorización criptográfica secp256k1 para tus firmantes, autores de políticas y administradores. Estos comandos son idempotentes.
Cloud KMS
Te recomendamos que administres las claves de autorización en Cloud Key Management Service. Asegúrate de que tu identidad tenga permisos para crear llaveros de claves, crear claves, firmar y ver verificadores de claves públicas (como roles/cloudkms.admin y roles/cloudkms.signerVerifier). Asegúrate de tener un KeyRing creado y, luego, genera una clave secp256k1 para cada firmante, autor de políticas y 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
Puedes usar OpenSSL estándar para generar claves secp256k1 para cada firmante, autor de políticas y 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
Paso 3: Registra equipos y usuarios
Antes de que cualquier entidad pueda participar en la firma de transacciones (como SIGNER) o en la creación de políticas (como POLICY_AUTHOR), se le deben otorgar permisos de IAM en el proyecto y registrarla como User a nivel de la aplicación en la instancia (consulta Seguridad y autorización para saber cómo funcionan juntos los roles de IAM y los roles de User).
El registro requiere una coordinación fuera de banda entre el administrador y el usuario:
- La entidad (firmante o autor de la política) genera su par de claves de forma local y transfiere su clave pública al administrador de forma segura.
- El administrador otorga a la principal del usuario el rol de IAM adecuado en el proyecto (
roles/mpc.editorpara los firmantes y los autores de políticas;roles/mpc.adminpara los administradores) y llama a la API de MPC para registrar elprincipaly elauthorizationKeydel usuario.
A. Crea un equipo de firmantes
Cada User con el rol SIGNER debe pertenecer a un Team, y todos los demás User con otros roles no pueden pertenecer a un Team. Como administrador 1 (admin-1@example.com), crea un Team (CreateTeam) de "Firmantes de tesorería" para que contenga a tus tres firmantes y captura su recurso name generado por el 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. Otorga roles de IAM y registra firmantes y autores de políticas
Cada usuario requiere un rol de IAM a nivel de proyecto (roles/mpc.editor para los firmantes y los autores de políticas, y roles/mpc.admin para los administradores) para llamar a la API de MPC y un recurso User a nivel de aplicación dentro de la instancia. Cuando se llama a users.create (CreateUser), el campo roles solo puede estar vacío o ser ["SIGNER"]:
- Firmantes: Se les otorga
roles/mpc.editoren IAM y se crean conroles: ["SIGNER"]yteam: "$TEAM_NAME". El rol deSIGNERse convierte enACTIVEde inmediato sin requerir la aprobación del quórum de administradores, ya que tener solo el rol deSIGNERno otorga acceso a ninguna clave existente. Un firmante solo recibe fragmentos de claves cuando se incluye en elKeyPolicyde unKeyRingdurante la generación de claves. Autores de políticas: Se les otorgó
roles/mpc.editoren IAM y se crearon sin roles iniciales niteam. Luego, se les otorgó el rolPOLICY_AUTHORen el paso 3.C con la aprobación del quórum de administradores (ya quePOLICY_AUTHORyADMINtienen autoridad de administración en toda la instancia).
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. Otorga el rol de autor de políticas (aprobación de quórum de administrador)
Para habilitar o inhabilitar el rol de POLICY_AUTHOR o ADMIN en un User, se requiere un quórum de administradores:
- El administrador 1 llama a
users.enableRole(EnableUserRole), lo que mueve el rol aPENDING_ACTIVEy registra la firma del administrador 1 como la primera aprobación. - El administrador 2 llama a
users.approveRoleChange(ApproveRoleChange) con su firma, lo que cambia el rol aACTIVE.
Cloud KMS
1. Solicita POLICY_AUTHOR como administrador 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. Aprueba a POLICY_AUTHOR como administrador 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. Solicita POLICY_AUTHOR como administrador 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. Aprueba a POLICY_AUTHOR como administrador 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"
Paso 4: Configura las políticas de transacciones
Para configurar las políticas de transacciones, crea y aprueba un TransactionPolicy (y, de manera opcional, un KeyList).
A. Crea una política (PENDING_ACTIVE)
Crea una regla de política de Cedar que describa qué transacciones se permiten. Si bien puedes configurar muchas políticas para controlar diferentes situaciones, en esta guía, se muestra cómo configurar una política para transacciones de Ethereum o Bitcoin. De forma predeterminada, la evaluación de la política de transacciones es Fail-Fast (Block by Default). Si ninguna política de permit activa coincide con una transacción, la decisión de evaluación de la política es DENY. Para obtener todos los detalles sobre los atributos de Cedar, consulta Escribe reglas de política de transacciones.
Ethereum, KMS
Esta regla permite transferencias de Ethereum más pequeñas (<= 2 ETH, representadas como 2,000,000,000 en unidades de 10-9 ETH / Gwei) a una dirección de destinatario específica (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
Esta regla permite transferencias de Ethereum más pequeñas (<= 2 ETH, representadas como 2,000,000,000 en unidades de 10-9 ETH / Gwei) a una dirección de destinatario específica (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
Esta regla permite las transferencias de Bitcoin con el esquema BITCOIN_TX_V2 (BIP 174 PSBT v0) en las que el importe neto de la transferencia saliente no supera los 250,000 satoshis (context.amount_token_scaled <= 2500000, o 0.0025 BTC normalizados a 10-9 unidades decimales fijas) y la comisión del minero no supera los 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
Esta regla permite las transferencias de Bitcoin con el esquema BITCOIN_TX_V2 (BIP 174 PSBT v0) en las que el importe neto de la transferencia saliente no supera los 250,000 satoshis (context.amount_token_scaled <= 2500000, o 0.0025 BTC normalizados a 10-9 unidades decimales fijas) y la comisión del minero no supera los 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. Aprobar la política (transición a ACTIVE)
Los cambios en la política requieren 2 de 2 firmas y, por lo tanto, un segundo autor de la política debe llamar a transactionPolicies.approve (ApproveTransactionPolicy) con un TransactionPolicySigningPayload firmado.
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. Crea y administra una KeyList (opcional)
Una KeyList (KeyListService) es una lista con nombre de direcciones de cadena de bloques (manualEntry, MPC key o keyRing) a las que las políticas pueden hacer referencia como destinos de confianza. Los administradores (ADMIN) pueden crear un KeyList con keyLists.create y actualizar sus entradas en cualquier momento con keyLists.addEntries y 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')
Paso 5: Crea una clave
Como administrador 1 (admin-1@example.com), crea un KeyRing con un KeyPolicy intercalado y genera una clave de MPC distribuida.
A. Crea un KeyRing con una KeyPolicy intercalada
Un KeyRing es una agrupación lógica para tus claves de MPC y define una KeyPolicy (keyRing.keyPolicy) intercalada inmutable que sigue cada clave creada en el KeyRing. Un KeyPolicy es un árbol de reglas de umbral (PolicyNode) cuyo subKeyShares secundario puede ser teamNode (threshold de totalMembers en un Team), userNode (un SIGNER User específico) o un policyNode anidado.
Aquí configuramos una regla de umbral de 2 de 3 para los tres firmantes en ${TEAM_NAME} (threshold: 2, totalMembers: 3, almacenados en 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. Genera una clave distribuida (DKG)
Llama a keys.create (CreateKey) como administrador 1 para ejecutar la generación de claves distribuidas (DKG) en los participantes seleccionados por el KeyPolicy de tu KeyRing.
CreateKey es síncrono y devuelve el recurso ACTIVE Key, incluidos publicKeyInfo.publicKey y publicKeyInfo.blockchainAddresses derivados del servidor (EC_SIGN_SECP256K1 para Ethereum y Bitcoin, o EC_SIGN_ED25519 para Solana una vez que sea compatible):
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. Deriva la dirección de la cadena de bloques y ubica las UTXO
Cuando se completa CreateKey, publicKeyInfo.blockchainAddresses en la respuesta incluye automáticamente direcciones derivadas (secp256k1Addresses.ethereumAddress y secp256k1Addresses.bitcoinMainnetAddresses para EC_SIGN_SECP256K1, o ed25519Addresses.solanaAddress para EC_SIGN_ED25519). También puedes verificar o derivar de forma independiente direcciones de Testnet y secuencias de comandos de bloqueo a partir de la clave pública sin comprimir de 65 bytes (0x04 || X || Y) con las siguientes secuencias de comandos, y ubicar los fondos no gastados necesarios para las transacciones.
Ethereum
Las direcciones de Ethereum se devuelven en .publicKeyInfo.blockchainAddresses.secp256k1Addresses.ethereumAddress y también se pueden derivar del hash Keccak-256 del punto de clave pública sin comprimir de 64 bytes (pub_bytes[1:]), tomando los últimos 20 bytes.
Secuencia de comandos de derivación de direcciones (derive_address.py)
Ejecuta el siguiente bloque para crear y ejecutar 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
Financia la dirección de Ethereum
Antes de transmitir en el paso 7, asegúrate de que tu dirección de Ethereum derivada tenga suficiente ETH de la red de prueba Sepolia para cubrir el valor de la transacción y los costos de gas. Puedes solicitar ETH de testnet gratis ingresando tu dirección recién derivada en el Faucet para Ethereum Sepolia.
Bitcoin
Las direcciones de la red principal de Bitcoin (p2wpkhAddress, p2shP2wpkhAddress y p2pkhAddress) se devuelven en .publicKeyInfo.blockchainAddresses.secp256k1Addresses.bitcoinMainnetAddresses. A partir de la clave pública de EC secp256k1 de 65 bytes sin comprimir (0x04 || X || Y), también puedes derivar la clave pública comprimida de 33 bytes, los scripts de bloqueo y las direcciones de Testnet4 en los tres formatos:
- SegWit nativo (P2WPKH / BIP 173): Dirección Bech32 (
bc1q...en Mainnet,tb1q...en Testnet4) con un testigo de 22 bytesscriptPubKey(0014<hash160>). Este es el formato moderno recomendado con las comisiones por transacción más bajas. - Nested SegWit (P2SH-P2WPKH / BIP 49): Dirección de SegWit encapsulada (
3...en Mainnet,2...en Testnet4) con unscriptPubKey(a914<hash160>87) de 23 bytes y un redeemScript (0014<hash160>) de 22 bytes, compatible con sistemas heredados. - Heredada (P2PKH / BIP 44): Dirección Base58 original de Pay-to-PubKey-Hash (
1...en Mainnet,m/n...en Testnet4) con unscriptPubKeyde 25 bytes (76a914<hash160>88ac).
Secuencia de comandos de derivación de direcciones (derive_btc_address.py)
Ejecuta el siguiente bloque para crear y ejecutar 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
Localización de salidas no gastadas (UTXO)
A diferencia de las cadenas de bloques basadas en cuentas, las transacciones de Bitcoin gastan salidas de transacciones no gastadas (UTXO) específicas. Antes de construir una transacción en el paso 6, haz lo siguiente:
- Financia tu dirección de SegWit nativo derivada (por ejemplo, con un faucet de testnet).
- Consulta los UTXO no gastados de tu dirección con tu nodo de Bitcoin (
bitcoin-cli listunspent 1 9999999 "[\"tb1q...\"]") o una API de explorador de bloques. - Ten en cuenta los siguientes 4 parámetros para el paso 6:
txid: Es el hash de transacción de 32 bytes de la salida no gastada.vout: Es el índice de salida con índice cero (por ejemplo,0o1).value: Es el importe no gastado en satoshis (por ejemplo,200000satoshis = 0.002 BTC).scriptPubKey: Es el hexadecimal de la secuencia de comandos de bloqueo (por ejemplo,0014...).
Paso 6: Ejecuta una operación de MPC
La evaluación de la política y el inicio de la firma ocurren juntos cuando un SIGNER llama a transactions.create (CreateTransaction). La instancia decodifica payload según schema, evalúa todas las políticas de transacciones activas, registra el resultado en policyEvaluationRecord y registra la decisión de AUTHORIZE del firmante que inicia.
A. Crea una transacción (evalúa la política y, luego, inicia la firma)
Envía la transacción como Signer 1 (alice@example.com) para la evaluación y firma de la política:
Ethereum, KMS
🛑 Caso A: Rechazo (valor demasiado alto)
Intenta crear una transacción de 3 ETH (supera nuestro límite de política de 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
Resultado esperado de la evaluación de la política: decision: DENY (denyReason: NO_PERMIT_RULE_MATCHED).
✅ Caso B: Aprobación (valor permitido)
Crea una transacción de 1 Gwei (dentro de nuestro límite 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')
Respuesta esperada: state: PENDING_AUTHORIZATION y policyEvaluationRecord.decision: PERMIT.
Ethereum, OpenSSL
🛑 Caso A: Rechazo (valor demasiado alto)
Intenta crear una transacción de 3 ETH (supera nuestro límite de política de 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
Resultado esperado de la evaluación de la política: decision: DENY (denyReason: NO_PERMIT_RULE_MATCHED).
✅ Caso B: Aprobación (valor permitido)
Crea una transacción de 1 Gwei (dentro de nuestro límite 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')
Respuesta esperada: state: PENDING_AUTHORIZATION y policyEvaluationRecord.decision: PERMIT.
Bitcoin, KMS
En el modelo UTXO de Bitcoin, las transacciones gastan salidas específicas no gastadas. En SegWit (BIP 143), calcular el hash de la firma requiere conocer la cantidad de satoshis y el script de bloqueo de la entrada que se está gastando.
La evaluación de la política de Bitcoin requiere el formato BIP 174 PSBT v0 (BITCOIN_TX_V2) con PSBT_IN_WITNESS_UTXO propagado para cada entrada.
Secuencia de comandos de construcción de PSBT (build_psbt.py)
Ejecuta el siguiente bloque para ensamblar una carga útil de PSBT v0 de BIP 174 válida:
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: Rechazo (se superó el límite de tarifas)
Intentar crear una transacción en la que la comisión del minero supere el límite de la política de 10,000 satoshis En este ejemplo, configurar MINER_FEE_SATS = 15000 (y CHANGE_SATS = 135000) en build_psbt.py escala a max_fee_atomic = 150000 (lo que supera el límite de la política de 100000), lo que produce la siguiente carga útil que activa una decisión de evaluación de la 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 de la evaluación de la política: decision: DENY (denyReason: NO_PERMIT_RULE_MATCHED).
✅ Caso B: Aprobación (PSBT de Bitcoin permitida)
Crea una transacción con la PSBT válida construida anteriormente (RECIPIENT_SATS = 50000 se asigna a amount_token_scaled = 500000 <= 2500000 y MINER_FEE_SATS = 250 se asigna a 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')
Respuesta esperada: state: PENDING_AUTHORIZATION y policyEvaluationRecord.decision: PERMIT.
Bitcoin, OpenSSL
En el modelo UTXO de Bitcoin, las transacciones gastan salidas específicas no gastadas. En SegWit (BIP 143), calcular el hash de la firma requiere conocer la cantidad de satoshis y el script de bloqueo de la entrada que se está gastando.
La evaluación de la política de Bitcoin requiere el formato BIP 174 PSBT v0 (BITCOIN_TX_V2) con PSBT_IN_WITNESS_UTXO propagado para cada entrada.
Secuencia de comandos de construcción de PSBT (build_psbt.py)
Ejecuta el siguiente bloque para ensamblar una carga útil de PSBT v0 de BIP 174 válida:
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: Rechazo (se superó el límite de tarifas)
Intentar crear una transacción en la que la comisión del minero supere el límite de la política de 10,000 satoshis En este ejemplo, configurar MINER_FEE_SATS = 15000 (y CHANGE_SATS = 135000) en build_psbt.py escala a max_fee_atomic = 150000 (lo que supera el límite de la política de 100000), lo que produce la siguiente carga útil que activa una decisión de evaluación de la 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 de la evaluación de la política: decision: DENY (denyReason: NO_PERMIT_RULE_MATCHED).
✅ Caso B: Aprobación (PSBT de Bitcoin permitida)
Crea una transacción con la PSBT válida construida anteriormente (RECIPIENT_SATS = 50000 se asigna a amount_token_scaled = 500000 <= 2500000 y MINER_FEE_SATS = 250 se asigna a 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')
Respuesta esperada: state: PENDING_AUTHORIZATION y policyEvaluationRecord.decision: PERMIT.
🕵️♂️ Verifica el estado de la transacción (sondeo)
Ejecuta el siguiente comando de forma periódica para consultar el estado de la transacción:
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. Registra la decisión del firmante (autorizar)
Como CreateTransaction en el paso 6.A ya registró la decisión AUTHORIZE del firmante 1 (1 de las 2 requeridas por el umbral KeyPolicy de 2 de 3), solo un cofirmante adicional que tenga un fragmento de clave debe autorizar la transacción con transactions.recordSignerDecision (RecordSignerDecision).
Firma el TransactionSigningPayload que nombra a transaction (${TRANSACTION_NAME}) y llama a recordSignerDecision como Signer 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. Recuperar firma final
Una vez que se alcance el umbral de autorizaciones, verifica el estado de la transacción y recupera finalSignature (y recoveryId para las claves 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
Cuando state sea SIGNING_SUCCEEDED, copia el campo finalSignature del JSON que se muestra para el paso 7.
Paso 7: Transmite la transacción a la cadena de bloques
Una vez que tu transacción alcance el estado SIGNING_SUCCEEDED y hayas recuperado el finalSignature, el último paso es construir la transacción de transferencia firmada y transmitirla a la red.
Ethereum
Derivación y transmisión del identificador de recuperación
La API de MPC devuelve una firma ECDSA sin procesar de 64 bytes en finalSignature (que comprende los valores r y s de 32 bytes en formato canónico de S baja según EIP-2), junto con el bit de paridad de recuperación (0 o 1) en recoveryId.
La siguiente secuencia de comandos de Python construye la transacción firmada con tu finalSignature, verifica la dirección del remitente recuperada con la dirección de Ethereum de tu clave de MPC y la transmite a la red.
Guion de la transmisión:
Ejecuta el siguiente bloque que se puede copiar y pegar en tu terminal para escribir y ejecutar 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
Solución de problemas comunes
invalid sender: recovery failedoinvalid transaction v, r, s values: Esto ocurre si los parámetros de transacción de tu secuencia de comandos no coinciden exactamente con la carga útil que se firmó. Asegúrate de que los camposvalue,nonceytocoincidan con lo que se procesó en el paso 6.insufficient funds for gas * price + value: Tu transacción es criptográficamente válida y el nodo la aceptó, pero la dirección de Ethereum de tu clave de MPC no tiene suficientes ETH de testnet para cubrir la operación. Deberás depositar fondos en tu dirección antes de transmitir.
Bitcoin
Ensamblaje y transmisión de transacciones de SegWit
Para las transacciones de SegWit nativo de Bitcoin (BIP 141), la firma de MPC de (r, s) de 64 bytes sin procesar debe tener el formato ASN.1 DER canónico de S bajo, se debe agregar con 0x01 (SIGHASH_ALL) y se debe ensamblar con la clave pública comprimida de 33 bytes en la pila de testigos de SegWit de 2 elementos: [DER signature || 0x01, compressed public key].
Guion de la transmisión:
Ejecuta el siguiente bloque para crear y ejecutar 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
Solución de problemas comunes
mandatory-script-verify-flag-failed (Signature must be zero for failed CHECK(MULTI)SIG operation)/non-mandatory-script-verify-flag (Non-canonical DER signature): Asegúrate de que el componentesde la firma esté en formato canónico low-s (s <= N/2) y codificado correctamente en DER.bad-txns-in-belowout/bad-txns-fee-negative: El total de salidas supera el valor de entrada. Asegúrate de que el cálculo del cambio (InputSatoshis - PaymentSatoshis - MinerFee) sea estrictamente positivo.
📖 Apéndice: Guía operativa (consultas e inspecciones)
Usa estos comandos para inspeccionar y administrar el estado de tus recursos en cualquier momento.
🏢 1. Instancias
Inspecciona y administra las instancias de MPC en tu proyecto y ubicación.
A. Enumera todas las instancias
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. Obtén detalles de una instancia 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. Equipos y usuarios
Inspecciona los equipos y usuarios de tu instancia.
A. Enumera todos los equipos
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. Obtén detalles de un equipo 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/${TEAM_NAME}" | jq
C. Enumera todos los usuarios 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. Obtén detalles de un usuario 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 transacciones y KeyLists
Inspecciona las políticas de transacciones y las listas de claves.
A. Enumera todas las políticas de transacciones
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. Obtén detalles de una política de transacciones 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. Enumera y obtén 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. Claves y llaveros de claves
Inspeccionar llaveros de claves y claves distribuidas
A. Enumera todos los llaveros de claves
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. Enumera todas las claves de un llavero de claves
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. Obtén detalles de una clave distribuida 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. Transacciones (operaciones de firma)
Inspecciona las transacciones y el estado de la firma.
A. Enumera todas las transacciones de una clave (o de todas las claves)
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. Obtener detalles o el estado de una transacción 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