Skip to content

Security: Prismer-AI/signet

Security

docs/SECURITY.md

Security Model

Cryptographic Primitives

Function Algorithm Crate Parameters
Signing Ed25519 ed25519-dalek 2.x 128-bit security, 64-byte signatures
Key derivation Argon2id argon2 0.5 t=3, m=64MB, p=1 (OWASP minimum)
Key encryption XChaCha20-Poly1305 chacha20poly1305 0.10 24-byte nonce, AEAD with AAD
Hashing SHA-256 sha2 0.10 Params hash, receipt ID, hash chain
Canonicalization RFC 8785 (JCS) json-canon 0.1 Deterministic JSON serialization

Key Storage

Keys are stored at ~/.signet/keys/ (override with SIGNET_HOME).

~/.signet/keys/
├── my-agent.key       # Encrypted private key (Argon2id + XChaCha20-Poly1305)
└── my-agent.pub       # Public key (plaintext JSON)
  • File permissions: 0600 (owner read/write only)
  • Encrypted key file format:
{
  "v": 1,
  "algorithm": "ed25519",
  "name": "my-agent",
  "kdf": "argon2id",
  "kdf_params": { "t": 3, "m": 65536, "p": 1 },
  "salt": "hex...",
  "cipher": "xchacha20-poly1305",
  "nonce": "hex...",
  "ciphertext": "hex..."
}
  • AAD (Additional Authenticated Data): Canonical JSON of header metadata (v, algorithm, name, kdf, kdf_params). Tampering with any header field causes decryption to fail.
  • Unencrypted mode: --unencrypted flag stores raw key bytes for CI/automation. Use SIGNET_PASSPHRASE env var for automated encrypted key access.

Signature Scheme

What gets signed

The full receipt body (minus the sig and id fields) is canonicalized via JCS:

canonical({v, action, signer, ts, nonce}) → bytes → Ed25519.sign(bytes)

The id field is derived after signing (rec_ + first 16 hex chars of SHA-256(signature)), so it is not part of the signed payload. Modifying any signed field — tool name, params, timestamp, signer identity, nonce — invalidates the signature.

Receipt ID generation

receipt.id = "rec_" + hex(SHA-256(signature))[0..16]

This provides a short, collision-resistant identifier derived from the signature itself.

Signed vs Unsigned Fields

Not all receipt fields are inside the Ed25519 signature scope. Building on unsigned fields for security-relevant decisions is unsafe.

v1/v4 Receipt

Field Signed? Notes
v ✅ Receipt version
action (tool, params, params_hash, target, transport, session, call_id, response_hash, trace_id, parent_receipt_id) ✅ All action fields are signed
signer (pubkey, name, owner) ✅
authorization ✅ Delegation chain (v4)
policy ✅ Policy attestation
ts ✅ Timestamp
nonce ✅ Replay protection
sig — The signature itself
id — Derived from sig, not independently signed

v3 BilateralReceipt

Field Signed? Notes
agent_receipt ✅ (by server sig) Embedded v1 receipt (also self-signed)
response ✅ (by server sig)
server ✅ (by server sig)
ts_response ✅ (by server sig)
nonce ✅ (by server sig)
extensions ❌ NOT SIGNED Outside signature scope — tamper-free is not guaranteed

Security implications of extensions

The extensions field on BilateralReceipt is deliberately excluded from the server's signature scope. Do not store security-relevant metadata in extensions.

Attack scenario: A developer stores agent identity or trust scores in extensions, then verifies the receipt expecting that data to be tamper-proof. An attacker modifies extensions after signing without invalidating the signature. Any authorization decision based on that data is compromised.

Safe uses for extensions: debugging metadata, display hints, non-security context. Unsafe uses: agent identity, trust scores, authorization claims, policy decisions. These belong in signed fields (action.params, policy, authorization, or a future attestations field).

Audit Log Integrity

Hash Chain

Each audit record contains a hash linking it to the previous record:

record_hash = SHA-256(canonical({prev_hash, receipt}))
  • Genesis: First record uses sha256:0000...0000
  • Cross-day: New daily files link back to the last record of the previous day
  • Verification: signet verify --chain recomputes every hash and checks continuity

Tamper detection

Attack Detection
Modify a receipt Signature verification fails (signet audit --verify)
Delete a record Hash chain breaks at the gap (signet verify --chain)
Reorder records Hash chain breaks (prev_hash mismatch)
Modify and re-sign Requires the private key; public key verification catches if different key used
Truncate log tail Detectable if expected record count is known

Limitations

  • Audit log is local. An attacker with filesystem access can delete the entire log.
  • No remote attestation server (planned for v2).
  • Hash chain proves ordering and integrity, not completeness — a compromised agent could skip logging (--no-log).

Delegation Revocation

Signet does not have an explicit revocation list. Delegation tokens are valid until their expires field (which is inside the signature scope and cannot be forged).

Recommended pattern: short-lived delegations.

# Issue a 1-hour delegation — auto-expires, no revocation needed
signet delegate create --from alice --to bot --to-name bot \
    --tools Bash --ttl 1h
Strategy How Tradeoff
Short TTL (--ttl 1h) Token expires automatically Requires re-issuance for long tasks
Medium TTL (--ttl 24h) Daily rotation Compromise window is at most 24h
No expiration Omit --ttl / --expires Token valid forever — use only for testing

This is the same approach used by short-lived JWTs and X.509 certificates: keep token lifetime short enough that revocation is unnecessary.

Why no revocation list? Signet receipts are offline-verifiable — adding a revocation list would require verifiers to check an external source, breaking the core offline property. If a future use case requires explicit revocation, a signed revocation receipt (verifiable in the same offline model) is the planned approach.

Threat Model

What Signet proves

  • Agent key X signed intent to call tool Y with params Z at time T
  • The audit log has not been tampered with (hash chain intact)
  • Receipts were created by the holder of the signing key

What Signet does NOT prove

Gap Mitigation (planned)
MCP server received/executed the action v2: Server-side counter-signatures
signer.owner actually controls the key v2: Identity registry / attestation
Agent was authorized to perform the action Out of scope — use policy engines
Params were not modified in transit Out of scope — use TLS for transport

Trust boundaries

┌─────────────────────────────────┐
│  Trusted (Signet's scope)       │
│                                 │
│  Key generation                 │
│  Signing (client-side)          │
│  Audit log append               │
│  Offline verification           │
└─────────────────────────────────┘

┌─────────────────────────────────┐
│  Untrusted (outside scope)      │
│                                 │
│  MCP server execution           │
│  Network transport              │
│  Agent authorization            │
│  Identity binding               │
└─────────────────────────────────┘

Code Safety

  • Zero unsafe blocks in signet-core
  • No unwrap() in production code — all errors propagated via ? and SignetError
  • 172 tests across Rust (68), Python (85), TypeScript (11), WASM (8)
  • CI runs cargo clippy -- -D warnings and cargo fmt --check on every PR

Reporting Vulnerabilities

If you discover a security vulnerability, please email security@prismer.ai instead of opening a public issue. We will respond within 48 hours.

There aren't any published security advisories