Skip to main content

open_agent_id/
signer.rs

1//! Client for the `oaid-signer` daemon.
2//!
3//! The signer daemon holds Ed25519 private keys and exposes a Unix socket
4//! interface for signing operations. The wire protocol uses a 4-byte big-endian
5//! length prefix followed by a JSON message body.
6//!
7//! # Example
8//!
9//! ```rust,no_run
10//! # async fn example() -> Result<(), open_agent_id::Error> {
11//! use open_agent_id::signer::SignerClient;
12//!
13//! let client = SignerClient::connect("/var/run/oaid-signer.sock").await?;
14//! let signature = client.sign("my-key-id", "http", b"payload-bytes").await?;
15//! # Ok(())
16//! # }
17//! ```
18
19use serde::{Deserialize, Serialize};
20use tokio::io::{AsyncReadExt, AsyncWriteExt};
21use tokio::net::UnixStream;
22
23use crate::error::Error;
24
25/// A client that communicates with the `oaid-signer` daemon over a Unix socket.
26pub struct SignerClient {
27    path: String,
28}
29
30/// A request to the signer daemon.
31#[derive(Debug, Serialize)]
32enum SignerRequest {
33    Sign {
34        key_id: String,
35        operation: String,
36        data: String,
37    },
38}
39
40/// A response from the signer daemon.
41#[derive(Debug, Deserialize)]
42#[serde(untagged)]
43enum SignerResponse {
44    Ok { signature: String },
45    Err { error: String },
46}
47
48impl SignerClient {
49    /// Connect to the signer daemon at the given Unix socket path.
50    ///
51    /// This validates that the socket is reachable but does not keep a persistent
52    /// connection; each signing request opens a new connection.
53    pub async fn connect(path: &str) -> Result<Self, Error> {
54        // Verify the socket exists by attempting a connection
55        let stream = UnixStream::connect(path)
56            .await
57            .map_err(|e| Error::Signer(format!("cannot connect to {path}: {e}")))?;
58        drop(stream);
59
60        Ok(Self {
61            path: path.to_string(),
62        })
63    }
64
65    /// Create a client without verifying connectivity.
66    ///
67    /// Useful when the daemon may not be running yet.
68    pub fn new(path: &str) -> Self {
69        Self {
70            path: path.to_string(),
71        }
72    }
73
74    /// Request the signer daemon to sign data.
75    ///
76    /// # Arguments
77    ///
78    /// - `key_id`: The key identifier known to the signer daemon.
79    /// - `operation`: The signing domain, e.g. `"http"` or `"msg"`.
80    /// - `data`: The raw bytes to sign (will be base64url-encoded for the wire).
81    ///
82    /// # Returns
83    ///
84    /// The base64url-encoded Ed25519 signature.
85    pub async fn sign(&self, key_id: &str, operation: &str, data: &[u8]) -> Result<String, Error> {
86        let request = SignerRequest::Sign {
87            key_id: key_id.to_string(),
88            operation: operation.to_string(),
89            data: crate::crypto::base64url_encode(data),
90        };
91
92        let request_json =
93            serde_json::to_vec(&request).map_err(|e| Error::Signer(format!("serialize: {e}")))?;
94
95        let mut stream = UnixStream::connect(&self.path)
96            .await
97            .map_err(|e| Error::Signer(format!("connect: {e}")))?;
98
99        // Write: 4-byte big-endian length + JSON body
100        let len = request_json.len() as u32;
101        stream
102            .write_all(&len.to_be_bytes())
103            .await
104            .map_err(|e| Error::Signer(format!("write length: {e}")))?;
105        stream
106            .write_all(&request_json)
107            .await
108            .map_err(|e| Error::Signer(format!("write body: {e}")))?;
109        stream
110            .flush()
111            .await
112            .map_err(|e| Error::Signer(format!("flush: {e}")))?;
113
114        // Read: 4-byte big-endian length + JSON body
115        let mut len_buf = [0u8; 4];
116        stream
117            .read_exact(&mut len_buf)
118            .await
119            .map_err(|e| Error::Signer(format!("read length: {e}")))?;
120        let resp_len = u32::from_be_bytes(len_buf) as usize;
121
122        if resp_len > 1024 * 1024 {
123            return Err(Error::Signer(format!(
124                "response too large: {resp_len} bytes"
125            )));
126        }
127
128        let mut resp_buf = vec![0u8; resp_len];
129        stream
130            .read_exact(&mut resp_buf)
131            .await
132            .map_err(|e| Error::Signer(format!("read body: {e}")))?;
133
134        let response: SignerResponse = serde_json::from_slice(&resp_buf)
135            .map_err(|e| Error::Signer(format!("deserialize response: {e}")))?;
136
137        match response {
138            SignerResponse::Ok { signature } => Ok(signature),
139            SignerResponse::Err { error } => Err(Error::Signer(format!("signer error: {error}"))),
140        }
141    }
142}