Signing Groups

Require multiple parties to approve before an action is signed. Multi-party signing splits cryptographic authority so no single entity can act alone. Available on the Enterprise plan.

How It Works

Multi-party signing uses a t-of-n model: you define n entities (agents, humans, or policies), and any t of them must approve before a signature is produced.

The resulting signature is a standard ML-DSA-65 signature - verifiers cannot tell it was produced by multiple parties. This is not multi-sig. It is a single cryptographic signature from distributed key shares.

  1. Configure - Create a signing group for an agent with a minimum approval count
  2. Add entities - Register the parties who can approve (agents, humans, or policies)
  3. Generate keypair - Secret key is split into Shamir shares, one per entity
  4. Sign - Provide t entity IDs and a message. Shares are combined to produce the signature

Entity Classes

Five types of entities can participate in multi-party signing:

Class Type Description Role
A Agent Signer Automated AI agent Autonomous signing participant
B Human Operator Human approver (requires user_id) Manual approval gate
C Policy Engine Automated policy engine Rule-based automated gatekeeper
D Compliance Verifier Automated regulatory compliance check Validates action meets regulatory requirements
E Organizational Authority Senior organizational override Executive-level approval for high-risk operations
Policy Enforcement

If any Class C (policy) entity exists in the config, at least one policy entity must be included in every signing request.

Setup

1. Create a Signing Group

Attach a signing group to an existing agent:

http
POST /api/v1/signing-groups/configs

{
  "agent_id": "agent_abc123",
  "min_approvals": 2
}

Response:
{
  "config_id": "cfg_xyz789",
  "agent_id": "agent_abc123",
  "min_approvals": 2,
  "total_entities": 0,
  "is_active": true
}

2. Add Entities

Register the parties who can approve actions:

http
# Add an AI agent entity
POST /api/v1/signing-groups/configs/{config_id}/entities

{
  "name": "payment-agent",
  "entity_class": "A"
}

# Add a human approver
POST /api/v1/signing-groups/configs/{config_id}/entities

{
  "name": "finance-lead",
  "entity_class": "B",
  "user_id": "usr_jane"
}

# Add a policy engine
POST /api/v1/signing-groups/configs/{config_id}/entities

{
  "name": "spend-limit-policy",
  "entity_class": "C"
}

Each entity gets its own ML-DSA keypair. The algorithm defaults to ml-dsa-65 and can be set to ml-dsa-44 or ml-dsa-87.

3. Generate Signing Group Keypair

Generate a shared keypair. The secret key is split into Shamir shares - one per entity, encrypted at rest. The raw shares are never exposed.

http
POST /api/v1/signing-groups/keypairs

{
  "config_id": "cfg_xyz789"
}

Response:
{
  "keypair_id": "kp_abc456",
  "public_key_b64": "base64...",
  "algorithm": "ML-DSA-65",
  "min_approvals": 2,
  "total_shares": 3,
  "share_assignments": [
    {"entity_id": "ent_...", "entity_name": "payment-agent", "participant_index": 1},
    {"entity_id": "ent_...", "entity_name": "finance-lead", "participant_index": 2},
    {"entity_id": "ent_...", "entity_name": "spend-limit-policy", "participant_index": 3}
  ]
}

Signing

Produce a multi-party signature by specifying which entities participate:

http
POST /api/v1/signing-groups/keypairs/{keypair_id}/sign

{
  "message_b64": "base64-encoded-message",
  "participant_entity_ids": ["ent_abc", "ent_def"]
}

Response:
{
  "signature_b64": "base64...",
  "public_key_b64": "base64...",
  "algorithm": "ML-DSA-65",
  "participants": [...]
}

The output is a standard 3,309-byte ML-DSA-65 signature. Any ML-DSA-65 verifier can validate it using the public key - no special signing-group-aware verification needed.

Approval Sessions

For approvals-queue workflows, use approval sessions. An agent requests an action, and entities approve individually until the required approval count is met.

Request an Action

http
POST /api/v1/signing-groups/sessions

{
  "agent_id": "agent_abc123",
  "action_type": "payment:transfer",
  "params": {
    "amount": 50000,
    "currency": "USD"
  }
}

Response:
{
  "session_id": "thr_abc789",
  "status": "pending",
  "approvals_required": 2,
  "signatures_collected": 0,
  "expires_at": "2025-01-16T10:00:00Z"
}

Approve

http
POST /api/v1/signing-groups/sessions/{session_id}/approve

{
  "entity_id": "ent_finance_lead"
}

Response:
{
  "session_id": "thr_abc789",
  "signatures_collected": 2,
  "approvals_required": 2,
  "status": "approved",
  "approved": true
}

Sessions expire after 24 hours if the required approval count is not met. Each entity can only approve once per session.

Session States

Status Description
pending Waiting for approvals
approved Required approvals met, action approved
expired 24h elapsed without reaching required approvals

Managing Configs

Update Min Approvals

http
PUT /api/v1/signing-groups/configs/{config_id}

{
  "min_approvals": 3
}

Remove an Entity

http
DELETE /api/v1/signing-groups/entities/{entity_id}

List Entities

http
GET /api/v1/signing-groups/configs/{config_id}/entities

Response:
{
  "entities": [
    {
      "entity_id": "ent_...",
      "name": "payment-agent",
      "entity_class": "A",
      "algorithm": "ML-DSA-65",
      "public_key_b64": "base64...",
      "is_active": true
    }
  ],
  "count": 3
}

Key Lifecycle

Key lifecycle operations let you rotate key material or recover lost shares without disrupting the rest of the signing group.

Key Share Refresh

Refresh generates new Shamir shares for an existing keypair without changing the public key. Use this to rotate key material on a schedule or after a security event. At least t contributing entity shares are required. All active delegations are automatically invalidated when a refresh completes.

python
import httpx

response = httpx.post(
    "https://api.asqav.com/api/v1/signing-groups/keypairs/kp_abc/refresh",
    headers={"X-API-Key": "sk_live_..."},
    json={"contributing_entity_ids": ["ent_1", "ent_2"]}
)

Share Recovery

When an entity loses its key share, other entities can reconstruct it without ever exposing the master key. Exactly t contributing entities must participate. The recovered share is re-encrypted and stored for the target entity.

python
response = httpx.post(
    "https://api.asqav.com/api/v1/signing-groups/keypairs/kp_abc/recover",
    headers={"X-API-Key": "sk_live_..."},
    json={
        "target_entity_id": "ent_lost",
        "contributing_entity_ids": ["ent_1", "ent_2"]
    }
)

Anomaly Detection

Multi-party signing sessions are monitored for anomalous activity. Anomaly events are logged and can trigger webhook notifications via the session.anomaly event.

Delegation

Entities can temporarily delegate their signing authority to another entity. Useful for planned absences, handoffs, or on-call rotations. Delegation certificates are ML-DSA signed and time-bounded - they expire automatically and cannot be renewed without creating a new delegation.

python
# Create a 24-hour delegation
response = httpx.post(
    "https://api.asqav.com/api/v1/signing-groups/delegations",
    headers={"X-API-Key": "sk_live_..."},
    json={
        "config_id": "cfg_xyz",
        "delegator_entity_id": "ent_alice",
        "delegate_entity_id": "ent_bob",
        "duration_hours": 24
    }
)

While the delegation is active, ent_bob can contribute in place of ent_alice. The delegator retains their share and can still participate directly. Delegations are revoked automatically when a key refresh occurs.

Risk Profiles

Risk profiles dynamically raise or lower the required approval count based on the action being requested. This lets you enforce stricter controls for sensitive operations without managing separate signing groups.

Action patterns use glob matching (fnmatch), so payment:* matches payment:transfer, payment:refund, and so on. Rules are evaluated in priority order - the lowest priority number wins.

python
# High-risk payments require all 3 signers
httpx.post(
    "https://api.asqav.com/api/v1/risk-rules",
    headers={"X-API-Key": "sk_live_..."},
    json={
        "name": "high-risk-payments",
        "action_pattern": "payment:*",
        "risk_level": "high",
        "approval_override": 3,
        "priority": 10
    }
)
Risk Level Typical Use
low Read-only or reversible actions
medium State changes with limited blast radius
high Financial transactions, data deletion
critical Infrastructure changes, key operations
Priority Order

Rules are evaluated lowest priority number first. Set priority: 1 for rules that must always apply, and higher numbers for more specific overrides.

Weighted Approvals

Risk profiles can assign different weights to entity classes. For example, a compliance verifier (Class D) approval may count as 2 shares toward the approval count, while a standard agent (Class A) counts as 1:

json
{
  "entity_weights": {"A": 1.0, "B": 1.5, "C": 1.0, "D": 2.0, "E": 2.0}
}

Time-Dependent Approvals

Approval requirements can be adjusted based on time of day. Operations outside business hours can require additional approvals:

json
{
  "time_schedule": {
    "start_hour": 9,
    "end_hour": 17,
    "timezone": "UTC",
    "approvals_add": 1
  }
}

This adds 1 to the required approval count for operations outside the 09:00-17:00 window.

Framework Integration

The SDK extras ship signing adapters for LangChain and CrewAI that emit a signed event for every tool, chain, and LLM call. Pair them with a signing group plus the Claude Code hook (or the Approvals API) to require multi-party approval before high-risk tool calls execute.

LangChain

AsqavCallbackHandler (from asqav.extras.langchain) signs every chain, tool, and LLM event. Attach it to your chain as a callback, then route tool calls through the MCP server's gate_action tool when they need signing group approval.

python
from asqav.extras.langchain import AsqavCallbackHandler

handler = AsqavCallbackHandler(agent_name="my-langchain-agent")
chain.invoke(input, config={"callbacks": [handler]})

CrewAI

AsqavGuardrailProvider (from asqav.extras.crewai) runs a preflight check on each tool call and signs the verdict. When the configured policy points at a signing group, approvals are held until the group's required count is met.

python
from asqav.extras.crewai import AsqavGuardrailProvider

provider = AsqavGuardrailProvider(agent_name="my-crew")
crew = Crew(agents=[...], tasks=[...], guardrail_provider=provider)

See the Integrations page for every adapter and its repository, and the Approvals page for how to wire signing group decisions into the gate.

Use Cases

Best Practices