Skip to content

Repository files navigation

SignBox

A local signing daemon that lets software agents use a blockchain key they can never see.

SignBox sits between an AI agent (or any automated tool) and a blockchain private key. The agent submits transactions it would like to sign; SignBox inspects them, checks them against a deterministic policy, and either signs or refuses. The agent never touches the key — it only ever holds a limited capability: asking for a signature.

Status: spec draft v0.4 · Phase 1 XPR MVP complete, Phase 2 security hardening largely landed — on top of the engine, keystore, daemon, XPR signing, quotas, on-chain policy contract + anti-rollback cache, companion-link onboarding, audit log and MCP server, the daemon now enforces global actor/permission/signing-key binding, verified-before-use RPC (chain-id pin + head-staleness), strict onboarding-payload validation, truly separate sign/broadcast capabilities, a tamper-evident (write-before-sign, HMAC-sealed) audit log, and CI supply-chain gates. The core sits on a chain-agnostic foundation (chain-module registry, policy dialects, sign-oriented keystore backends), and the web companion's policy editor is lossless-by-construction. 347 tests. Approaching beta; not yet production-ready (external audit, HSM/remote keystore backends, native peer-credentials, and audit anchoring pending). First target chain: XPR Network.


The problem

LLM-based agents are probabilistic. Prompt injection, poisoned tools or plain misbehavior can make an agent do things it was never supposed to do. If that agent holds a private key, one successful attack means the key — and everything it controls — is gone.

Giving an agent a key is easy. Giving an agent bounded, revocable, auditable signing power is not. That is what SignBox does.

How it works: the black box

To the agent, SignBox is a black box:

        agent / LLM
            │
            │  plain JSON actions (never bytes, never a hash)
            ▼
┌───────────────────────────┐
│         SignBox           │
│                           │
│  1. validate the JSON     │
│  2. apply the policy      │      policy source of truth:
│  3. sign or refuse        │◄──── on-chain contract,
│                           │      controlled by a superior
└───────────┬───────────────┘      authority — never by the agent
            │
            ▼
   signed transaction | refusal
  • The agent submits raw, unserialized JSON — a readable list of actions. Packed transactions, hex blobs and bare digests are rejected categorically.
  • It receives a single final answer: a signed transaction, or a refusal with a safe reason. Nothing else ever leaves the box.
  • The decision is made by a deterministic policy engine — never by an LLM, never by a heuristic. Same input, same policy, same decision. A policy lives on-chain and can only be changed by the superior authority (a human wallet), never by the agent.

SignBox behaves like a headless programmatic wallet: it receives a readable transaction proposal — exactly as a human wallet receives a signing request — validates it, serializes it itself, and signs it with the key it protects. The only difference from a human wallet is that the approval tap is replaced by a policy.

The security consequence: compromising the agent no longer compromises the key. A fully hijacked agent is reduced to proposing transactions; its maximum blast radius is whatever the policy already allows. The cost of an attack moves from "convince an LLM" (easy) to "take over the host machine" (hard).

What SignBox guarantees

  • Deny by default — an empty or missing policy authorizes nothing.
  • The key never leaves the daemon — no API, log, error or CLI command returns it. Keys are stored encrypted (Argon2id + XChaCha20-Poly1305) and live only in locked memory buffers while in use.
  • Full decoding or refusal — SignBox never signs anything it cannot completely read: no opaque blobs, no unknown fields, no surprise second action buried in a transaction.
  • Fail closed — any error, timeout, ambiguity or unknown value results in a refusal. There is no "probably fine" path.
  • No LLM in the decision loop — the agent can propose and ask; only the policy engine decides.
  • Exact money math — amounts are integers of minimal units (bigint), never floating point. Comparisons require an exact symbol and precision match; lookalike symbols and decimal tricks are refused, not coerced.
  • Identity is bound, not asserted — the daemon signs only for the account, permission and public key it was configured with, verified against the chain's on-chain authority. A permissive or malformed policy can never make it sign for a different actor (confused-deputy). See docs/endpoint-trust.md for the chain-state verification behind this.
  • Signing and broadcasting are independent capabilities — a sign-only agent can never trigger a network submission, and broadcasting is off by default (opt-in per agent). See docs/sign-broadcast-separation.md.
  • Every signing is accounted for — the decision is recorded to a hash-chained audit log before the signature is produced (fail closed if it can't be), and the log is tamper-evident: edits, deletions and truncation are detectable. See docs/audit-integrity.md.

What SignBox does not guarantee

Honesty matters in a security tool:

  • The boundary between agent and key is OS-level isolation — a restricted Unix socket plus a rotating per-agent token (constant-time compared); a native SO_PEERCRED/getpeereid peer-credential check is a planned additional layer. A root attacker — or anything running as the daemon's own user — is outside the software guarantee. Hardware-grade non-exportability requires an HSM/TPM (planned, Phase 3), for which the sign-oriented keystore boundary is already in place. See docs/deployment-hardening.md for how to make that isolation a hard wall (separate users / container, filesystem permissions, systemd, ptrace).
  • Rate limits and daily caps are enforced locally and are best-effort; anything that must be absolutely guaranteed belongs on-chain.
  • SignBox protects the key, not the agent's other data. What an agent legitimately knows, it can still leak.

Two signing paths, kept strictly apart

Runtime signing Onboarding / administration
Who signs SignBox, with the agent's key A human authority, with their own wallet
Used for day-to-day agent transactions creating agents, changing policies, rotating keys
Mechanism policy check, then sign in-process companion web link (printed + QR) opened in the browser; the authority's own wallet signs
Key exposure key stays inside the daemon SignBox never holds the authority's key

The agent's key signs only what the policy allows. Everything administrative — including changing the policy itself — requires the external authority's wallet.

Onboarding an agent

signbox agent create walks the onboarding (interactively, or via flags for scripts):

$ signbox agent create
Chain:        1) XPR Network (default)
Network:      1) mainnet   2) testnet (default)
Authority account (your account name): superdev
Agent account name: superagent
Mode:         1) create a new account (default)   2) onboard an existing one
Key export policy: 1) non-exportable (recommended)   2) encrypted-backup-only
keystore passphrase: ****

SignBox then:

  1. generates the agent's key locally and seals it in a temporary encrypted container (nothing active yet);
  2. builds the onboarding transaction — create the account with owner delegated to the authority account (multisig-preserving, no key copied) and the agent's key on active, register an empty deny-all policy — and prints a companion web link (plus a QR of that link) and a short verification code derived from the generated key;
  3. the authority opens the link, connects their own wallet in the web companion, types the verification code, and signs (SignBox never holds the authority's key). The typed code is the out-of-band anchor: a tampered link carrying a different agent key yields a different code, so signing stays disabled;
  4. after confirming on-chain that what landed matches the request exactly, SignBox promotes the key to active. A failure before the request is shown destroys the temp container; any later failure quarantines it for manual recovery — the key is never orphaned and never lost.

The superior authority pays the account's RAM.

Known limitation (INV-006): the WebAuth wallet does not accept eosio::updateauth/eosio::linkauth in wallet requests, so the dedicated agent permission — the chain-enforced coarse bound meant to complement the fine-grained policy — cannot be created during onboarding today. The agent key sits on the account's active permission and the daemon's policy is the effective barrier. This is possible via Anchor (full ESR signing), which SignBox does not support end-to-end yet — a future implementation is under consideration (#86, relates to #73). Deployment consequences: docs/deployment-hardening.md §8.

What a policy looks like

{
  "schemaVersion": 1,
  "default": "deny",
  "maxActionsPerTransaction": 1,
  "chain": { "name": "XPR", "chainId": "71ee83bc…" },
  "rules": [
    {
      "id": "allow-small-xpr-tips",
      "effect": "allow",
      "match": {
        "contract": "eosio.token",
        "action": "transfer",
        "data.from": "$agent",
        "data.quantity.symbol": "XPR",
        "data.quantity.amount": { "lte": "1000.0000" },
        "data.to": { "notIn": ["blocked.gm"] }
      },
      "limits": {
        "maxPerTransaction": "1000.0000 XPR",
        "maxPerDay": "5000.0000 XPR",
        "maxCountPerRecipientPerHour": 3
      }
    }
  ]
}

Declarative, versioned, JSON-Schema-validated, no executable code. An explicit deny always beats an allow; anything not explicitly allowed is refused. Value and count limits aggregate across a transaction's actions, and maxActionsPerTransaction defaults to 1 — so a multi-action transaction can neither multiply a limit nor slip in an extra action unless the policy explicitly allows it.

Where policies live, and configuration

A policy is not a local file. It lives on-chain, in the central signbox contract — one row per agent account, holding the superior authority, the agent's dedicated permission, a monotonic version, and the policy's canonical JSON + hash. Only the agent's authority can create or change it, by signing an on-chain transaction from an external wallet (never a key SignBox holds).

That is what makes the policy tamper-proof: a compromised agent — or anything with write access to the daemon's host — cannot alter it, because the only gate that matters is the contract's on-chain requireAuth(authority), not a filesystem permission.

The daemon reads the on-chain policy through a local cache that:

  • verifies the policy's hash and canonical form before trusting it;
  • never accepts a lower version than it has already seen (anti-rollback), so a lying RPC or a restored database can't silently downgrade to a more permissive policy;
  • refreshes every 30 s, and re-confirms a value-moving policy within 10 s before signing;
  • fails closed if the policy can't be confirmed.

Configuration is zero-config by default. The daemon uses conventional paths under ~/.signbox/ (keystores, local state, sockets), and serves an agent simply by holding its encrypted keystore. An optional config file exists only for advanced deployment settings — RPC endpoints, the contract account, socket paths — never for agents or policies, which come from the keystores and the chain.

Web companion

A small web app (web/) complements the daemon for the two things a browser wallet does best — both driven by the authority's own wallet, never a key SignBox holds:

  • Onboarding — opens the CLI's signing request, verifies it against compiled, trusted config (the URL fragment is treated as untrusted input), shows a summary derived from the validated actions, and lets the authority sign it in their wallet.
  • Policy editor — a visual node graph that compiles to the same bounded policy document the daemon enforces (shared @sbx-core schema + canonicalizer, so the editor can never emit a policy the daemon would reject). It is lossless by construction: a policy is editable only if it survives a decompile → recompile round trip unchanged; anything the graph can't fully represent opens read-only with the exact on-chain document preserved, and a compile that would silently drop or widen a rule blocks the push. The pushed document is validated once more before signing. See docs/editor-lossless.md.

Project status & roadmap

Phase 1 (XPR MVP) — component status:

Component Status
Deterministic policy engine (deny-by-default, deny>allow, exact-integer limits) ✅ done
Encrypted-file keystore (Argon2id + XChaCha20-Poly1305, metadata-bound) ✅ done
Unserialized-JSON transaction decoding & normalization (INV-014) ✅ done
Unix-socket daemon: authenticated decision pipeline, kill-switch ✅ done
XPR runtime signing via @proton/js (WYSIWYS round-trip, chain-id pinning) ✅ done
Stateful quota journal (SQLite, atomic reserve/commit) ✅ done
CLI (inspect/explain/sign/push, doctor, daemon, key, agent) ✅ done
On-chain policy contract (AssemblyScript) + anti-rollback policy cache ✅ done
Companion-link onboarding (agent create → web companion + wallet) ✅ done
Hash-chained audit log (audit tail/query/verify) ✅ done
MCP server + llms.txt / llms-full.txt for agents ✅ done

Phase 1 (XPR MVP) is functionally complete, and much of Phase 2 security hardening has since landed:

Phase 2 — security hardening Status
Global actor/permission/signing-key binding in the decision pipeline (confused-deputy defense) ✅ done
Verified-before-use RPC — chain-id pin + head-staleness before every data call ✅ done
Strict onboarding-payload validation (anchored on compiled trusted config) ✅ done
Sign and broadcast as truly separate, independently-granted capabilities ✅ done
Tamper-evident audit — write-before-sign intent + HMAC-sealed truncation-evident head ✅ done
CI supply-chain gates — dependency audit, secret scan, release-artifact check ✅ done
Lossless web policy editor (no silent widening on save) ✅ done
Native SO_PEERCRED/getpeereid peer-credential check (#6) planned
External audit anchoring — resist a host-root attacker (#80) planned
RPC quorum, signed releases, fuzzing, external security audit planned

The core also sits on a chain-agnostic foundation built interface-first — a ChainModule registry, per-chain PolicyDialect, and a sign-oriented KeystoreBackend (signDigest, key never crosses the boundary). XPR is refactored onto all three; new chains and remote backends are follow-ups.

Later phases:

Phase Scope Status
3 — High assurance HSM/TPM/PKCS#11 keystore backends (boundary in place), attestation, administrative multisig planned
4 — More chains SUI + MetalBlockchain modules + their policy-registry contracts (#77, #78); remote keystore backends — Vault / AWS-KMS / OS-keychain (#79) planned

Install the CLI

Requirements: Node.js ≥ 22.

Build and install from source (puts signbox and signbox-mcp on your PATH):

npm ci
npm run build
npm link            # or: npm install -g .
signbox --version

Or install a released tarball (see Releases):

npm install -g signbox-0.1.0.tgz

Every release/x.x.x git tag triggers the single release pipeline, which builds and tests the CLI, attaches signbox-x.x.x.tgz to the GitHub Release, and deploys the matching web companion — the two ship together on purpose: the onboarding link protocol binds the CLI and the companion versions, so a release of only one of them would break onboarding. A semver pre-release tag — release/x.x.x-beta.N, -rc.N, … — runs the same pipeline but flags the GitHub Release as Pre-release (never shown as "Latest"), so a beta cannot be mistaken for a stable version.

Development

npm install
npm test          # unit + adversarial test suite (347 tests)
npm run typecheck # strict TypeScript, no emit
npm run build     # compile to ./dist

The on-chain contract is a separate sub-project under contract/ (AssemblyScript / proton-tsc); see contract/BUILD.md for its pinned toolchain and npm test (vert).

The test suite includes the specification's adversarial set (§17.4): second-action injection, wrong token contract, homograph symbols, incorrect decimals, chain-id substitution, keystore tampering, concurrent quota-cap bypass, and policy-cache rollback.

The full architecture and threat model live in docs/signbox-spec-v0.4-complete.md. Start with §1.1 — it is the reading key for everything else.

License

Not yet licensed for public use — this repository is under active early development.

About

SignBox is a local daemon local of controlled signature that aims LLM agent and automated tools

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages