1use std::collections::HashMap;
13use std::sync::{Arc, Mutex};
14
15use serde::{Deserialize, Serialize};
16use zeroize::Zeroizing;
17
18pub const HANDLE_ENV: &str = "M4A_SESSION_HANDLE";
19pub const SOCKET_ENV: &str = "M4A_SIGNER_SOCK";
20
21pub trait CoreSigner: Send + Sync {
23 fn sign(&self, token: &str, op: &str, payload: &[u8]) -> Result<SignedReply, String>;
24 fn provision(&self, _provider: &str, _session: &str) -> Result<(String, String), String> {
26 Err("provision not supported".into())
27 }
28 fn send(&self, _token: &str, _letter: &serde_json::Value) -> Result<serde_json::Value, String> {
30 Err("send not supported".into())
31 }
32 fn revoke(&self, _session: &str) -> Result<(), String> {
33 Ok(())
34 }
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
38pub struct SignedReply {
39 pub nick: String,
40 pub public_key_hex: String,
41 pub signature_hex: String,
42}
43
44pub trait ProcTree: Send + Sync {
46 fn parent(&self, pid: u32) -> Option<u32>;
47}
48
49pub struct LinuxProc;
50
51impl ProcTree for LinuxProc {
52 fn parent(&self, pid: u32) -> Option<u32> {
53 let stat = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
54 let rest = &stat[stat.rfind(')')? + 1..];
56 rest.split_whitespace().nth(1)?.parse().ok()
57 }
58}
59
60struct Binding {
61 session: String,
62 provider: String,
63 nick: String,
64 root_pid: u32,
66 uid: u32,
67 token: Zeroizing<String>,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub enum Refusal {
73 UnknownHandle,
74 WrongUser,
75 NotInSession,
76 Upstream(String),
77}
78
79pub struct SignerProxy {
80 bindings: Mutex<HashMap<String, Binding>>,
81 proc_tree: Box<dyn ProcTree>,
82 core: Arc<dyn CoreSigner>,
83}
84
85impl SignerProxy {
86 pub fn new(core: Arc<dyn CoreSigner>) -> Self {
87 Self::with_proc(core, Box::new(LinuxProc))
88 }
89
90 pub fn with_proc(core: Arc<dyn CoreSigner>, proc_tree: Box<dyn ProcTree>) -> Self {
91 Self { bindings: Mutex::new(HashMap::new()), proc_tree, core }
92 }
93
94 pub fn bind(&self, session: &str, nick: &str, root_pid: u32, uid: u32, token: String) -> String {
97 let mut raw = [0u8; 24];
98 fill_random(&mut raw);
99 let handle = hex::encode(raw);
100 let mut b = self.bindings.lock().unwrap_or_else(|p| p.into_inner());
101 b.retain(|_, v| v.session != session);
102 b.insert(
103 handle.clone(),
104 Binding { session: session.to_owned(), provider: String::new(), nick: nick.to_owned(), root_pid, uid, token: Zeroizing::new(token) },
105 );
106 handle
107 }
108
109 pub fn reserve(&self, session: &str, provider: &str, uid: u32) -> String {
111 let h = self.bind(session, "", 0, uid, String::new());
112 if let Some(b) = self.bindings.lock().unwrap_or_else(|p| p.into_inner()).get_mut(&h) {
113 b.provider = provider.to_owned();
114 }
115 h
116 }
117
118 pub fn attach_pid(&self, session: &str, pid: u32) {
120 for b in self.bindings.lock().unwrap_or_else(|p| p.into_inner()).values_mut() {
121 if b.session == session && b.root_pid == 0 {
122 b.root_pid = pid;
123 }
124 }
125 }
126
127 pub fn has_session(&self, session: &str) -> bool {
128 self.bindings.lock().unwrap_or_else(|p| p.into_inner()).values().any(|b| b.session == session)
129 }
130
131 pub fn release(&self, session: &str) {
133 if self.has_session(session) {
134 self.unbind(session);
135 if let Err(e) = self.core.revoke(session) {
136 tracing::warn!(error = %e, "m4a signer revoke failed");
137 }
138 }
139 }
140
141 fn authorize(&self, handle: &str, peer_pid: u32, peer_uid: u32) -> Result<(Zeroizing<String>, String), Refusal> {
143 let (session, provider, token, nick) = {
144 let b = self.bindings.lock().unwrap_or_else(|p| p.into_inner());
145 let v = b.iter().find(|(h, _)| ct_eq(h.as_bytes(), handle.as_bytes())).map(|(_, v)| v).ok_or(Refusal::UnknownHandle)?;
146 if v.uid != peer_uid {
147 return Err(Refusal::WrongUser);
148 }
149 if v.root_pid == 0 || !self.in_tree(v.root_pid, peer_pid) {
150 return Err(Refusal::NotInSession);
151 }
152 (v.session.clone(), v.provider.clone(), v.token.clone(), v.nick.clone())
153 };
154 if !token.is_empty() {
155 return Ok((token, nick));
156 }
157 let (nick, token) = self.core.provision(&provider, &session).map_err(Refusal::Upstream)?;
158 let mut b = self.bindings.lock().unwrap_or_else(|p| p.into_inner());
159 let v = b.get_mut(handle).ok_or(Refusal::UnknownHandle)?;
160 v.token = Zeroizing::new(token.clone());
161 v.nick = nick.clone();
162 Ok((Zeroizing::new(token), nick))
163 }
164
165 pub fn send_for_peer(&self, handle: &str, peer_pid: u32, peer_uid: u32, letter: &serde_json::Value) -> Result<serde_json::Value, Refusal> {
167 let (token, nick) = self.authorize(handle, peer_pid, peer_uid)?;
168 let out = self.core.send(&token, letter).map_err(Refusal::Upstream)?;
169 if out.get("from").and_then(|v| v.as_str()) != Some(nick.as_str()) {
170 return Err(Refusal::Upstream("core answered for another nick".into()));
171 }
172 Ok(out)
173 }
174
175 pub fn unbind(&self, session: &str) {
177 self.bindings.lock().unwrap_or_else(|p| p.into_inner()).retain(|_, v| v.session != session);
178 }
179
180 fn in_tree(&self, root: u32, mut pid: u32) -> bool {
181 for _ in 0..64 {
182 if pid == root {
183 return true;
184 }
185 match self.proc_tree.parent(pid) {
186 Some(p) if p != 0 && p != pid => pid = p,
187 _ => return false,
188 }
189 }
190 false
191 }
192
193 pub fn sign_for_peer(&self, handle: &str, peer_pid: u32, peer_uid: u32, op: &str, payload: &[u8]) -> Result<SignedReply, Refusal> {
195 let (token, nick) = self.authorize(handle, peer_pid, peer_uid)?;
196 let reply = self.core.sign(&token, op, payload).map_err(Refusal::Upstream)?;
197 if reply.nick != nick {
198 return Err(Refusal::Upstream("core answered for another nick".into()));
199 }
200 Ok(reply)
201 }
202}
203
204fn ct_eq(a: &[u8], b: &[u8]) -> bool {
205 a.len() == b.len() && a.iter().zip(b).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0
206}
207
208fn fill_random(buf: &mut [u8]) {
209 use std::io::Read;
210 std::fs::File::open("/dev/urandom").and_then(|mut f| f.read_exact(buf)).expect("/dev/urandom");
211}
212
213#[derive(Debug, Serialize, Deserialize)]
214pub struct ProxyRequest {
215 pub handle: String,
216 pub op: String,
217 pub payload_hex: String,
218}
219
220#[derive(Debug, Serialize, Deserialize)]
221#[serde(tag = "status", rename_all = "snake_case")]
222pub enum ProxyResponse {
223 Signed(SignedReply),
224 Sent { result: serde_json::Value },
225 Refused { reason: String },
226}
227
228#[cfg(unix)]
231pub async fn serve(proxy: Arc<SignerProxy>, listener: tokio::net::UnixListener) {
232 use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
233 loop {
234 let Ok((stream, _)) = listener.accept().await else { continue };
235 let proxy = proxy.clone();
236 tokio::spawn(async move {
237 let cred = match stream.peer_cred() {
238 Ok(c) => c,
239 Err(_) => return,
240 };
241 let (r, mut w) = stream.into_split();
242 let mut line = String::new();
243 let mut reader = BufReader::new(r.take(64 * 1024));
244 if reader.read_line(&mut line).await.is_err() {
245 return;
246 }
247 let resp = match serde_json::from_str::<ProxyRequest>(&line) {
248 Err(_) => ProxyResponse::Refused { reason: "bad request".into() },
249 Ok(req) => match (hex::decode(&req.payload_hex), cred.pid()) {
250 (Ok(payload), Some(pid)) if pid > 0 => {
251 let proxy = proxy.clone();
252 let uid = cred.uid();
253 let res = tokio::task::spawn_blocking(move || {
254 if req.op == "send" {
255 let letter: serde_json::Value = serde_json::from_slice(&payload).map_err(|_| Refusal::Upstream("bad letter".into()))?;
256 proxy.send_for_peer(&req.handle, pid as u32, uid, &letter).map(|result| ProxyResponse::Sent { result })
257 } else {
258 proxy.sign_for_peer(&req.handle, pid as u32, uid, &req.op, &payload).map(ProxyResponse::Signed)
259 }
260 })
261 .await;
262 match res {
263 Ok(Ok(r)) => r,
264 Ok(Err(e)) => {
265 tracing::warn!(peer_pid = pid, reason = ?e, "m4a signer proxy refused a request");
266 ProxyResponse::Refused { reason: format!("{e:?}") }
267 }
268 Err(_) => ProxyResponse::Refused { reason: "internal".into() },
269 }
270 }
271 _ => ProxyResponse::Refused { reason: "bad request".into() },
272 },
273 };
274 let mut out = serde_json::to_vec(&resp).unwrap_or_default();
275 out.push(b'\n');
276 let _ = w.write_all(&out).await;
277 });
278 }
279}
280
281#[cfg(unix)]
283pub fn request(sock: &std::path::Path, handle: &str, op: &str, payload: &[u8]) -> std::io::Result<ProxyResponse> {
284 use std::io::{BufRead, Write};
285 let mut s = std::os::unix::net::UnixStream::connect(sock)?;
286 let mut req = serde_json::to_vec(&ProxyRequest { handle: handle.into(), op: op.into(), payload_hex: hex::encode(payload) })?;
287 req.push(b'\n');
288 s.write_all(&req)?;
289 let mut line = String::new();
290 std::io::BufReader::new(s).read_line(&mut line)?;
291 serde_json::from_str(&line).map_err(std::io::Error::other)
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297
298 struct FakeTree(HashMap<u32, u32>);
299 impl ProcTree for FakeTree {
300 fn parent(&self, pid: u32) -> Option<u32> {
301 self.0.get(&pid).copied()
302 }
303 }
304 struct Echo;
305 impl CoreSigner for Echo {
306 fn sign(&self, token: &str, _op: &str, _p: &[u8]) -> Result<SignedReply, String> {
307 Ok(SignedReply { nick: token.trim_start_matches("tok-").into(), public_key_hex: String::new(), signature_hex: String::new() })
308 }
309 }
310
311 #[test]
312 fn handle_works_only_inside_its_session_tree_and_for_its_uid() {
313 let tree = FakeTree([(101, 100), (100, 1), (201, 200), (200, 1)].into_iter().collect());
315 let p = SignerProxy::with_proc(Arc::new(Echo), Box::new(tree));
316 let ha = p.bind("sa", "a", 100, 1000, "tok-a".into());
317 let hb = p.bind("sb", "b", 200, 1000, "tok-b".into());
318 assert_eq!(p.sign_for_peer(&ha, 100, 1000, "send_as", b"x").unwrap().nick, "a");
319 assert_eq!(p.sign_for_peer(&ha, 101, 1000, "send_as", b"x").unwrap().nick, "a");
320 assert_eq!(p.sign_for_peer(&ha, 201, 1000, "send_as", b"x"), Err(Refusal::NotInSession));
321 assert_eq!(p.sign_for_peer(&ha, 100, 1001, "send_as", b"x"), Err(Refusal::WrongUser));
322 assert_eq!(p.sign_for_peer(&hb, 201, 1000, "send_as", b"x").unwrap().nick, "b");
323 assert_eq!(p.sign_for_peer("guess", 100, 1000, "send_as", b"x"), Err(Refusal::UnknownHandle));
324 p.unbind("sa");
325 assert_eq!(p.sign_for_peer(&ha, 100, 1000, "send_as", b"x"), Err(Refusal::UnknownHandle));
326 let ha2 = p.bind("sb", "b", 200, 1000, "tok-b".into());
327 assert_eq!(p.sign_for_peer(&hb, 200, 1000, "send_as", b"x"), Err(Refusal::UnknownHandle), "rebind kills the old handle");
328 assert!(p.sign_for_peer(&ha2, 200, 1000, "send_as", b"x").is_ok());
329 }
330}
331
332pub struct UplinkCoreSigner {
335 calls: crate::uplink_calls::NodeCalls,
336 rt: tokio::runtime::Handle,
337}
338
339impl UplinkCoreSigner {
340 pub fn new(calls: crate::uplink_calls::NodeCalls, rt: tokio::runtime::Handle) -> Self {
341 Self { calls, rt }
342 }
343 fn call(&self, op: &str, payload: serde_json::Value) -> Result<serde_json::Value, String> {
344 self.rt.block_on(self.calls.call(op, &payload)).map_err(|e| format!("{e:?}"))
345 }
346}
347
348impl CoreSigner for UplinkCoreSigner {
349 fn sign(&self, token: &str, op: &str, payload: &[u8]) -> Result<SignedReply, String> {
350 let v = self.call("signer.sign", serde_json::json!({ "token": token, "op": op, "payload_hex": hex::encode(payload) }))?;
351 serde_json::from_value(v).map_err(|e| e.to_string())
352 }
353 fn provision(&self, provider: &str, session: &str) -> Result<(String, String), String> {
354 let v = self.call("signer.provision", serde_json::json!({ "provider": provider, "session": session }))?;
355 Ok((v["nick"].as_str().ok_or("no nick")?.to_owned(), v["token"].as_str().ok_or("no token")?.to_owned()))
356 }
357 fn send(&self, token: &str, letter: &serde_json::Value) -> Result<serde_json::Value, String> {
358 let mut p = letter.clone();
359 p["token"] = serde_json::Value::String(token.to_owned());
360 self.call("signer.send", p)
361 }
362 fn revoke(&self, session: &str) -> Result<(), String> {
363 self.call("signer.revoke", serde_json::json!({ "session": session })).map(|_| ())
364 }
365}