agent_mesh_protocol/signer.rs
1//! [`MeshSigner`] — the signing seam that decouples *holding a key* from
2//! *being an [`AgentKey`]*.
3//!
4//! Today an agent's ed25519 seed lives in process memory (see
5//! [`AgentKey::signing_key_bytes`](crate::AgentKey::signing_key_bytes)). That
6//! is fine for a server process, but it is a non-starter for a phone: a
7//! platform keystore (Android Keystore, iOS Secure Enclave) will sign on your
8//! behalf but will **never** export the raw seed.
9//!
10//! `MeshSigner` is the abstraction that lets those two worlds share one signing
11//! path. A signer can produce signatures and reveal its public half — nothing
12//! more. The software implementation ([`AgentKey`]) signs with the seed it
13//! holds; a future keystore-backed signer signs through an opaque handle. The
14//! envelope and (eventually) transport layers sign via `&dyn MeshSigner` so
15//! they never need the seed itself.
16//!
17//! The signer carries no provenance — the [`CertChain`](crate::CertChain) does
18//! that. A caller pairs a `MeshSigner` with the matching cert chain (its
19//! `agent_pubkey` must equal the signer's [`MeshSigner::verifying_key`]), and
20//! the verifier checks the signature against the pubkey *in the cert*. A signer
21//! whose key doesn't match the cert simply produces envelopes that fail
22//! [`verify`](crate::SignedEnvelope::verify).
23
24use ed25519_dalek::{Signature, VerifyingKey};
25
26/// Something that can sign mesh messages without necessarily exposing its
27/// private key.
28///
29/// Implemented by [`AgentKey`](crate::AgentKey) for the software (in-memory
30/// seed) case. Platform-keystore implementations (Android/iOS) are explicitly
31/// out of scope here — this trait is the seam they will plug into without
32/// touching the envelope/transport call sites.
33///
34/// `Send + Sync` so a signer can be shared behind an `Arc` across the
35/// tokio-spawn boundaries the transport layer uses.
36pub trait MeshSigner: Send + Sync {
37 /// The ed25519 public half of this signer's key. Must match the
38 /// `agent_pubkey` of the [`CertChain`](crate::CertChain) it is paired with,
39 /// or the resulting signatures will not verify against the cert.
40 fn verifying_key(&self) -> VerifyingKey;
41
42 /// Produce an ed25519 signature over `msg`.
43 fn sign(&self, msg: &[u8]) -> Signature;
44}
45
46#[cfg(test)]
47mod tests {
48 use super::*;
49 use crate::{AgentKey, AgentMetadata, Caveats, UserKey};
50 use ed25519_dalek::{Signer, SigningKey, Verifier};
51
52 fn fixture_agent() -> AgentKey {
53 let user = UserKey::generate();
54 AgentKey::issue(
55 &user,
56 AgentMetadata {
57 role: "worker".into(),
58 host: "test-host".into(),
59 capabilities: vec!["test".into()],
60 issued_at: "2026-05-28T00:00:00Z".into(),
61 expires_at: None,
62 caveats: Caveats::top(),
63 },
64 )
65 }
66
67 /// A test-double signer that holds a bare `SigningKey` but is NOT an
68 /// `AgentKey` — proving the seam works for non-`AgentKey` holders such as a
69 /// platform keystore wrapper.
70 struct RawSigner {
71 signing: SigningKey,
72 }
73
74 impl MeshSigner for RawSigner {
75 fn verifying_key(&self) -> VerifyingKey {
76 self.signing.verifying_key()
77 }
78 fn sign(&self, msg: &[u8]) -> Signature {
79 self.signing.sign(msg)
80 }
81 }
82
83 #[test]
84 fn agent_key_is_a_mesh_signer() {
85 let agent = fixture_agent();
86 let signer: &dyn MeshSigner = &agent;
87 let msg = b"sign-via-trait";
88 let sig = signer.sign(msg);
89 // The trait's verifying_key must match the agent's cert pubkey, and the
90 // signature must verify under it.
91 assert_eq!(signer.verifying_key().as_bytes(), &agent.public_bytes());
92 signer
93 .verifying_key()
94 .verify(msg, &sig)
95 .expect("trait signature verifies under trait verifying_key");
96 }
97
98 #[test]
99 fn agent_key_trait_sign_matches_inherent_sign() {
100 let agent = fixture_agent();
101 let msg = b"identical";
102 let via_trait = MeshSigner::sign(&agent, msg);
103 let via_inherent = agent.sign(msg);
104 assert_eq!(via_trait.to_bytes(), via_inherent.to_bytes());
105 }
106
107 #[test]
108 fn raw_signer_double_signs_and_verifies() {
109 // A non-AgentKey signer (holds a key, not a cert) still satisfies the
110 // seam: it signs and exposes a matching verifying key.
111 let signing = SigningKey::from_bytes(&[7u8; 32]);
112 let expected_vk = signing.verifying_key();
113 let signer = RawSigner { signing };
114 let msg = b"raw-signer";
115 let sig = MeshSigner::sign(&signer, msg);
116 assert_eq!(signer.verifying_key().as_bytes(), expected_vk.as_bytes());
117 signer
118 .verifying_key()
119 .verify(msg, &sig)
120 .expect("raw signer signature verifies");
121 }
122
123 #[test]
124 fn signer_is_object_safe_and_send_sync() {
125 // Compile-time proof: MeshSigner is object-safe and Send+Sync, so it can
126 // live behind Arc<dyn MeshSigner> and cross spawn boundaries.
127 fn assert_send_sync<T: Send + Sync + ?Sized>() {}
128 assert_send_sync::<dyn MeshSigner>();
129 let agent = fixture_agent();
130 let _boxed: std::sync::Arc<dyn MeshSigner> = std::sync::Arc::new(agent);
131 }
132}