Expand description
cratefield-signer — the key-reference signing port for wallet
automation (issue #761).
A venture asks to sign an EVM transaction, an ERC-4337
UserOperation, EIP-712 data or a Solana message by key
reference. It never handles raw keys: keys are minted here and
referenced everywhere else, and no API in this crate — port,
reference implementations, or fakes — exports private key material.
That guarantee is the trait’s shape (KeySigner has no method
that can answer the question) as much as any implementation’s
discipline.
Every signature is audit-chained: subject, key reference, payload
hash, decoded intent, policy decision, outcome. Every sign call
passes through the guardrails module before the provider is called,
and GuardedSigner is the composition that makes skipping it
impossible. Audit failure fails closed, as does a guardrail that
cannot evaluate: an unrecorded or undecided signature is one that
never happened.
The default composition is SecretsSigner — a secp256k1 or
ed25519 key held in the Secrets port, unsealed only inside sign —
wrapped in a GuardedSigner. It mints session keys only,
never owner keys: there is no import API, and a key’s whole life is
under a secret a venture can delete.
use cratefield_signer::{
EvmTransaction, FakeSigner, GuardedSigner, MemorySignAudit, Payload, Scheme,
SignerError, StaticGuardrails, Subject,
};
let guardrails = StaticGuardrails::new().allow_evm(
1,
"0x000000000000000000000000000000000000aaaa",
[0xa9, 0x05, 0x9c, 0xbb], // ERC-20 transfer
)?;
let signer = GuardedSigner::new(FakeSigner::new(), guardrails, MemorySignAudit::new());
let subject = Subject::new("acme", None)?;
let key = pollster::block_on(async {
signer.create_key(&subject, Scheme::Secp256k1, "session-1").await
})?;
let tx = EvmTransaction {
chain_id: 1,
nonce: 0,
max_priority_fee_per_gas: 1_000_000_000,
max_fee_per_gas: 2_000_000_000,
gas_limit: 21_000,
to: Some("0x000000000000000000000000000000000000aaaa".into()),
value: 1_000_000_000_000_000_000,
data: vec![0xa9, 0x05, 0x9c, 0xbb], // the allowed selector
};
let signature = pollster::block_on(async {
signer
.sign(&subject, key.key_ref(), &Payload::EvmTransaction(tx))
.await
})?;
assert!(matches!(signature, cratefield_signer::Signature::Secp256k1 { .. }));§Reading order
payload: what can be signed, how each signing hash is computed, and theIntentthe guardrails see.port: theKeySignertrait and its error vocabulary.guardrails: theGuardrailsseam and theStaticGuardrailsreference allowlist.audit: theSignAuditseam and the hash-chainedMemorySignAudit.guarded:GuardedSigner, the order enforcement lives in.secrets_signer: the Secrets-port provider.fakesandconformance: the deterministic fake and the suite every provider runs.
Re-exports§
pub use audit::AuditAnchor;pub use audit::MemorySignAudit;pub use audit::PolicyDecisionRecord;pub use audit::SignAudit;pub use audit::SignAuditRecord;pub use audit::SignOutcome;pub use conformance::guardrails_conformance;pub use conformance::key_signer_conformance;pub use conformance::sign_audit_conformance;pub use fakes::FakeGuardrails;pub use fakes::FakeSigner;pub use fakes::RefusingAudit;pub use guarded::GuardedSigner;pub use guardrails::Guardrails;pub use guardrails::PolicyDecision;pub use guardrails::SignContext;pub use guardrails::StaticGuardrails;pub use keys::KeyInfo;pub use keys::KeyRef;pub use keys::KeyRole;pub use keys::Scheme;pub use keys::Signature;pub use keys::Subject;pub use payload::Chain;pub use payload::Eip712;pub use payload::EvmTransaction;pub use payload::Intent;pub use payload::Payload;pub use payload::SELECTOR_EXECUTE;pub use payload::SolanaMessage;pub use payload::UserOperation;pub use port::KeySigner;pub use port::SignerError;pub use secrets_signer::SecretsSigner;
Modules§
- audit
- The audit seam and its reference implementation. Every sign attempt —
allowed, denied, or unhashable — is one linked record: subject, key
reference, payload hash, decoded intent, the policy decision, and the
outcome.
MemorySignAuditchains the records with sha256 the waycratefield-secrets’ own audit chain does, andverifynames the first entry whose hash does not follow from its predecessor. - conformance
- The conformance kit: the async suite any
KeySignermust pass. Reference implementations run it in this crate’s tests; a new provider runs the same function, and what it proves is the same: - fakes
- The in-memory
KeySignerfor tests: deterministic keys from fixed-seed derivation, no secrets store, no I/O. A fake’s value is reproducibility — the same key reference signs the same way on every run, so conformance failures read as failures and not as flaky entropy. - guarded
- The
GuardedSigner: the composition a venture uses. It wraps anyKeySignerso that no signature is possible except through the guardrails and the audit chain, in that order — and the provider is unreachable from here on any other path. - guardrails
- The guardrails seam: what every sign call passes through before the
provider is called (issue #761). The trait is the seam a real policy
engine plugs into;
StaticGuardrailsis the reference implementation — an allowlist of EVM(chain, to, selector)rules and Solana program ids, a per-transaction value cap, and a kill switch that denies everything while it is tripped. - keys
- Keys and their public face: the reference, the scheme, the identity a
signature must verify against, and the signature itself. Nothing in
this module — or this crate — can hand a caller private key material;
KeyInfois deliberately all-public-fields public data. - payload
- The payloads a venture asks to have signed, and what this crate can
say about each of them: which scheme signs it, what digest the
signature is over, and the decoded
Intentthe guardrails read. - port
- The
KeySignerport: create a key, describe it, sign with it. The trait’s shape is the no-export guarantee — there is no method that can return private key material, so no implementation reachable through this trait can be asked for one. - secrets_
signer - The
SecretsSigner: aKeySignerthat keeps its keys in the Secrets port. A key is 32 random bytes, sealed into the subject’s tenant store under a name that is also itsKeyRef;signis the only place that unseals one, into aZeroizingbuffer that never leaves the call.