Skip to main content

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}