Skip to main content

libmoshpit/agent/
protocol.rs

1// Copyright (c) 2025 moshpit developers
2//
3// Licensed under the Apache License, Version 2.0
4// <LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0> or the MIT
5// license <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
6// option. All files in the project carrying such notice may not be copied,
7// modified, or distributed except according to those terms.
8
9//! Request/response types for the moshpit agent Unix-socket protocol.
10//!
11//! The wire format is simple length-prefixed bincode-next:
12//!
13//! ```text
14//! [u32 big-endian message length][bincode-next encoded message]
15//! ```
16//!
17//! Private keys never cross the socket — only public keys and signatures are
18//! returned from the agent.
19
20use bincode_next::{Decode, Encode};
21
22/// A loaded identity known to the agent.
23#[derive(Clone, Debug, Decode, Encode)]
24pub struct AgentIdentityInfo {
25    /// Key algorithm string (e.g. `"X25519"`, `"P384"`).
26    pub algorithm: String,
27    /// `SHA256:<base64>` fingerprint of the public key.
28    pub fingerprint: String,
29    /// Optional comment (e.g. `user@host`).
30    pub comment: String,
31}
32
33/// Requests sent by a client to the agent.
34#[derive(Clone, Debug, Decode, Encode)]
35pub enum AgentRequest {
36    /// List all identities currently held in memory.
37    ListIdentities,
38    /// List identities whose algorithm appears in `supported_algorithms`.
39    ///
40    /// Use this instead of [`AgentRequest::ListIdentities`] when the caller may not
41    /// support every algorithm the agent holds (e.g. a client built without the
42    /// `unstable` feature cannot use ML-DSA keys).
43    ListSupportedIdentities {
44        /// Algorithm strings the caller supports (e.g. `["P384", "P256", "X25519"]`).
45        supported_algorithms: Vec<String>,
46    },
47    /// Return the full public key file bytes for the identity with the given fingerprint.
48    ///
49    /// The fingerprint is the `SHA256:<base64>` form without trailing comment.
50    GetPublicKey(String),
51    /// Sign `data` with the private key identified by `fingerprint`.
52    ///
53    /// Only meaningful for ML-DSA keys; ECDH identity keys don't sign.
54    Sign {
55        /// `SHA256:<base64>` fingerprint (without trailing comment).
56        fingerprint: String,
57        /// Raw bytes to sign.
58        data: Vec<u8>,
59    },
60    /// Add an identity from `key_path`, decrypting it with `passphrase` if encrypted.
61    AddIdentity {
62        /// Absolute path to the private key file.
63        key_path: String,
64        /// Passphrase to decrypt the key; `None` for unencrypted keys.
65        passphrase: Option<String>,
66    },
67    /// Remove the identity identified by `fingerprint`.
68    RemoveIdentity(String),
69    /// Remove all identities from memory.
70    RemoveAllIdentities,
71    /// Lock the agent: clear all keys from memory.
72    Lock,
73    /// Unlock the agent with a master credential (currently a passphrase string).
74    Unlock(String),
75    /// Ask the agent to shut down gracefully.
76    ///
77    /// The agent responds with [`AgentResponse::Ok`], removes its socket file,
78    /// then exits.  Clients should source the output of `mpa stop` to unset
79    /// `MOSHPIT_AGENT_SOCK` after calling this.
80    Shutdown,
81    /// Query the agent's current state (locked flag + loaded identities).
82    Status,
83}
84
85/// Responses from the agent.
86#[derive(Clone, Debug, Decode, Encode)]
87pub enum AgentResponse {
88    /// A list of known identities.
89    Identities(Vec<AgentIdentityInfo>),
90    /// The full public key file bytes for the requested identity.
91    PublicKey(Vec<u8>),
92    /// A signature produced by the agent.
93    Signature(Vec<u8>),
94    /// Generic success.
95    Ok,
96    /// An error message.
97    Error(String),
98    /// Returned in response to [`AgentRequest::Status`].
99    AgentStatus {
100        /// Whether the agent is currently locked (keys cleared from memory).
101        locked: bool,
102        /// Identities currently held in memory (empty when locked).
103        identities: Vec<AgentIdentityInfo>,
104    },
105}
106
107#[cfg(test)]
108mod tests {
109    use bincode_next::{config::standard, decode_from_slice, encode_to_vec};
110
111    use super::{AgentIdentityInfo, AgentRequest, AgentResponse};
112
113    #[test]
114    fn roundtrip_request_lock() -> anyhow::Result<()> {
115        let encoded = encode_to_vec(&AgentRequest::Lock, standard())?;
116        let (rt, _): (AgentRequest, _) = decode_from_slice(&encoded, standard())?;
117        assert!(matches!(rt, AgentRequest::Lock));
118        Ok(())
119    }
120
121    #[test]
122    fn roundtrip_request_unlock() -> anyhow::Result<()> {
123        let encoded = encode_to_vec(AgentRequest::Unlock("secret".to_string()), standard())?;
124        let (rt, _): (AgentRequest, _) = decode_from_slice(&encoded, standard())?;
125        assert!(matches!(rt, AgentRequest::Unlock(ref s) if s == "secret"));
126        Ok(())
127    }
128
129    #[test]
130    fn roundtrip_request_remove_all() -> anyhow::Result<()> {
131        let encoded = encode_to_vec(&AgentRequest::RemoveAllIdentities, standard())?;
132        let (rt, _): (AgentRequest, _) = decode_from_slice(&encoded, standard())?;
133        assert!(matches!(rt, AgentRequest::RemoveAllIdentities));
134        Ok(())
135    }
136
137    #[test]
138    fn roundtrip_request_shutdown() -> anyhow::Result<()> {
139        let encoded = encode_to_vec(&AgentRequest::Shutdown, standard())?;
140        let (rt, _): (AgentRequest, _) = decode_from_slice(&encoded, standard())?;
141        assert!(matches!(rt, AgentRequest::Shutdown));
142        Ok(())
143    }
144
145    #[test]
146    fn roundtrip_response_ok() -> anyhow::Result<()> {
147        let encoded = encode_to_vec(&AgentResponse::Ok, standard())?;
148        let (rt, _): (AgentResponse, _) = decode_from_slice(&encoded, standard())?;
149        assert!(matches!(rt, AgentResponse::Ok));
150        Ok(())
151    }
152
153    #[test]
154    fn agent_identity_info_clone_and_debug() {
155        let info = AgentIdentityInfo {
156            algorithm: "P384".to_string(),
157            fingerprint: "SHA256:abcd".to_string(),
158            comment: "user@host".to_string(),
159        };
160        let cloned = info.clone();
161        assert_eq!(cloned.algorithm, "P384");
162        let debug_str = format!("{info:?}");
163        assert!(debug_str.contains("P384"));
164    }
165}