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}