Skip to main content

trustless_protocol/
message.rs

1use secrecy::SecretBox;
2
3pub use crate::base64::Base64Bytes;
4
5/// Protocol error code with message.
6///
7/// Well-known codes from the key-provider protocol:
8/// - `-1`: internal/infrastructure error
9/// - `1`: certificate not found
10/// - `2`: unsupported signature scheme
11/// - `3`: signing failed
12///
13/// Providers may define additional codes via `Other`.
14#[derive(Debug, thiserror::Error)]
15pub enum ErrorCode {
16    /// Internal/infrastructure error (`-1`).
17    #[error("internal error: {0}")]
18    Internal(String),
19    /// Certificate not found (`1`).
20    #[error("certificate not found: {0}")]
21    CertificateNotFound(String),
22    /// Unsupported signature scheme (`2`).
23    #[error("unsupported scheme: {0}")]
24    UnsupportedScheme(String),
25    /// Signing failed (`3`).
26    #[error("signing failed: {0}")]
27    SigningFailed(String),
28    /// A provider-defined code not covered by the well-known variants.
29    #[error("error (code {code}): {message}")]
30    Other { code: i64, message: String },
31}
32
33impl ErrorCode {
34    pub fn as_i64(&self) -> i64 {
35        match self {
36            ErrorCode::Internal(_) => -1,
37            ErrorCode::CertificateNotFound(_) => 1,
38            ErrorCode::UnsupportedScheme(_) => 2,
39            ErrorCode::SigningFailed(_) => 3,
40            ErrorCode::Other { code, .. } => *code,
41        }
42    }
43}
44
45impl From<ErrorCode> for ErrorPayload {
46    fn from(e: ErrorCode) -> Self {
47        ErrorPayload {
48            code: e.as_i64(),
49            message: e.to_string(),
50        }
51    }
52}
53
54impl From<ErrorPayload> for ErrorCode {
55    fn from(p: ErrorPayload) -> Self {
56        match p.code {
57            -1 => ErrorCode::Internal(p.message),
58            1 => ErrorCode::CertificateNotFound(p.message),
59            2 => ErrorCode::UnsupportedScheme(p.message),
60            3 => ErrorCode::SigningFailed(p.message),
61            code => ErrorCode::Other {
62                code,
63                message: p.message,
64            },
65        }
66    }
67}
68
69/// A protocol request message.
70///
71/// Internally tagged by `method`, with `id` repeated in each variant.
72#[derive(serde::Serialize, serde::Deserialize, Debug)]
73#[serde(tag = "method")]
74pub enum Request {
75    #[serde(rename = "initialize")]
76    Initialize { id: u64, params: InitializeParams },
77    #[serde(rename = "sign")]
78    Sign { id: u64, params: SignParams },
79}
80
81impl Request {
82    pub fn id(&self) -> u64 {
83        match self {
84            Request::Initialize { id, .. } => *id,
85            Request::Sign { id, .. } => *id,
86        }
87    }
88}
89
90/// Parameters for the `initialize` method. Currently empty.
91#[derive(serde::Serialize, serde::Deserialize, Debug)]
92pub struct InitializeParams {}
93
94/// Parameters for the `sign` method.
95#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
96pub struct SignParams {
97    /// The certificate ID (from [`InitializeResult`]) identifying which key to use.
98    pub certificate_id: String,
99    /// The signature scheme name (e.g., `"ECDSA_NISTP256_SHA256"`).
100    pub scheme: String,
101    /// The data to sign. Base64-encoded on the wire. Blobs to sign are not considered sensitive,
102    /// but we wrap them with `SecretBox` to avoid accidental logging or exposure in debug builds.
103    pub blob: SecretBox<Base64Bytes>,
104}
105
106/// A successful protocol response, internally tagged by `method`.
107#[derive(serde::Serialize, serde::Deserialize, Debug)]
108#[serde(tag = "method")]
109pub enum SuccessResponse {
110    #[serde(rename = "initialize")]
111    Initialize { id: u64, result: InitializeResult },
112    #[serde(rename = "sign")]
113    Sign { id: u64, result: SignResult },
114}
115
116/// An error response with no method tag.
117#[derive(serde::Serialize, serde::Deserialize, Debug)]
118pub struct ErrorResponse {
119    pub id: u64,
120    pub error: ErrorPayload,
121}
122
123/// A protocol response message — either a tagged success or an error.
124#[derive(serde::Serialize, serde::Deserialize, Debug)]
125#[serde(untagged)]
126pub enum Response {
127    Success(SuccessResponse),
128    Error(ErrorResponse),
129}
130
131impl From<SuccessResponse> for Response {
132    fn from(s: SuccessResponse) -> Self {
133        Response::Success(s)
134    }
135}
136
137impl From<ErrorResponse> for Response {
138    fn from(e: ErrorResponse) -> Self {
139        Response::Error(e)
140    }
141}
142
143impl Response {
144    pub fn id(&self) -> u64 {
145        match self {
146            Response::Success(SuccessResponse::Initialize { id, .. }) => *id,
147            Response::Success(SuccessResponse::Sign { id, .. }) => *id,
148            Response::Error(ErrorResponse { id, .. }) => *id,
149        }
150    }
151
152    pub fn initialize(id: u64, result: Result<InitializeResult, ErrorPayload>) -> Self {
153        match result {
154            Ok(result) => SuccessResponse::Initialize { id, result }.into(),
155            Err(error) => ErrorResponse { id, error }.into(),
156        }
157    }
158
159    pub fn sign(id: u64, result: Result<SignResult, ErrorPayload>) -> Self {
160        match result {
161            Ok(result) => SuccessResponse::Sign { id, result }.into(),
162            Err(error) => ErrorResponse { id, error }.into(),
163        }
164    }
165}
166
167/// An error payload with a numeric code and human-readable message.
168///
169/// This is the wire-format struct. Use [`ErrorCode`] for typed error handling,
170/// and convert via `From`/`Into` at boundaries.
171#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
172pub struct ErrorPayload {
173    /// Numeric error code. See [`ErrorCode`] for well-known values.
174    pub code: i64,
175    /// Human-readable error description.
176    pub message: String,
177}
178
179/// Result of the `initialize` method.
180#[derive(serde::Serialize, serde::Deserialize, Debug)]
181pub struct InitializeResult {
182    /// Certificate ID to use as the default when no SNI matches.
183    pub default: String,
184    /// All certificates available from this provider.
185    pub certificates: Vec<CertificateInfo>,
186}
187
188/// Metadata for a single certificate returned during initialization.
189#[derive(serde::Serialize, serde::Deserialize, Debug)]
190pub struct CertificateInfo {
191    /// Unique identifier for this certificate. Used in `sign` requests.
192    pub id: String,
193    /// DNS Subject Alternative Names the certificate covers (e.g., `["*.example.com"]`).
194    pub domains: Vec<String>,
195    /// Full certificate chain in PEM format (leaf first, then intermediates).
196    pub pem: String,
197    /// Supported signature scheme names (e.g., `["ECDSA_NISTP256_SHA256"]`).
198    /// Strongly recommended — certificates without valid schemes are skipped.
199    #[serde(default)]
200    pub schemes: Vec<String>,
201}
202
203/// Result of the `sign` method.
204#[derive(serde::Serialize, serde::Deserialize, Debug)]
205pub struct SignResult {
206    /// The signature bytes. Base64-encoded on the wire.
207    pub signature: SecretBox<Base64Bytes>,
208}
209
210#[cfg(test)]
211mod tests {
212    use secrecy::ExposeSecret as _;
213
214    #[derive(serde::Deserialize, Debug)]
215    struct WireRequest {
216        id: u64,
217        method: String,
218        #[allow(dead_code)]
219        params: serde_json::Value,
220    }
221
222    #[test]
223    fn serialize_initialize_request() {
224        let req = super::Request::Initialize {
225            id: 1,
226            params: super::InitializeParams {},
227        };
228        let json = serde_json::to_string(&req).unwrap();
229        let wire: WireRequest = serde_json::from_str(&json).unwrap();
230        assert_eq!(wire.id, 1);
231        assert_eq!(wire.method, "initialize");
232    }
233
234    #[test]
235    fn serialize_sign_request() {
236        let req = super::Request::Sign {
237            id: 42,
238            params: super::SignParams {
239                certificate_id: "cert/v1".to_owned(),
240                scheme: "ECDSA_NISTP256_SHA256".to_owned(),
241                blob: super::Base64Bytes::from(vec![0xde, 0xad, 0xbe, 0xef]).into_secret(),
242            },
243        };
244        let json = serde_json::to_string(&req).unwrap();
245        let wire: WireRequest = serde_json::from_str(&json).unwrap();
246        assert_eq!(wire.id, 42);
247        assert_eq!(wire.method, "sign");
248        let params: super::SignParams = serde_json::from_value(wire.params).unwrap();
249        assert_eq!(params.certificate_id, "cert/v1");
250        assert_eq!(params.scheme, "ECDSA_NISTP256_SHA256");
251        assert_eq!(**params.blob.expose_secret(), vec![0xde, 0xad, 0xbe, 0xef]);
252    }
253
254    #[test]
255    fn deserialize_initialize_request() {
256        let json = r#"{"id":5,"method":"initialize","params":{}}"#;
257        let req: super::Request = serde_json::from_str(json).unwrap();
258        assert_eq!(req.id(), 5);
259        assert!(matches!(req, super::Request::Initialize { .. }));
260    }
261
262    #[test]
263    fn deserialize_sign_request() {
264        let json = r#"{"id":7,"method":"sign","params":{"certificate_id":"c1","scheme":"ED25519","blob":"AQID"}}"#;
265        let req: super::Request = serde_json::from_str(json).unwrap();
266        assert_eq!(req.id(), 7);
267        match req {
268            super::Request::Sign { params, .. } => {
269                assert_eq!(params.certificate_id, "c1");
270                assert_eq!(params.scheme, "ED25519");
271                assert_eq!(params.blob.expose_secret().as_slice(), &[1, 2, 3]);
272            }
273            _ => panic!("expected Sign"),
274        }
275    }
276
277    #[test]
278    fn request_round_trip() {
279        let req = super::Request::Sign {
280            id: 10,
281            params: super::SignParams {
282                certificate_id: "cert/v1".to_owned(),
283                scheme: "ECDSA_NISTP256_SHA256".to_owned(),
284                blob: super::Base64Bytes::from(vec![0xde, 0xad]).into_secret(),
285            },
286        };
287        let json = serde_json::to_string(&req).unwrap();
288        let decoded: super::Request = serde_json::from_str(&json).unwrap();
289        assert_eq!(decoded.id(), 10);
290        match decoded {
291            super::Request::Sign { params, .. } => {
292                assert_eq!(params.certificate_id, "cert/v1");
293                assert_eq!(params.blob.expose_secret().as_slice(), &[0xde, 0xad]);
294            }
295            _ => panic!("expected Sign"),
296        }
297    }
298
299    #[test]
300    fn serialize_initialize_result_response() {
301        let resp = super::Response::Success(super::SuccessResponse::Initialize {
302            id: 1,
303            result: super::InitializeResult {
304                default: "cert1".to_owned(),
305                certificates: vec![super::CertificateInfo {
306                    id: "cert1".to_owned(),
307                    domains: vec!["*.example.com".to_owned()],
308                    pem: "PEM DATA".to_owned(),
309                    schemes: vec!["ECDSA_NISTP256_SHA256".to_owned()],
310                }],
311            },
312        });
313        let json = serde_json::to_string(&resp).unwrap();
314        assert!(json.contains("\"method\":\"initialize\""));
315        assert!(json.contains("\"result\""));
316        assert!(!json.contains("\"error\""));
317        let v: serde_json::Value = serde_json::from_str(&json).unwrap();
318        assert_eq!(v["id"], 1);
319        assert_eq!(v["method"], "initialize");
320        assert_eq!(v["result"]["default"], "cert1");
321    }
322
323    #[test]
324    fn serialize_sign_result_response() {
325        let resp = super::Response::Success(super::SuccessResponse::Sign {
326            id: 2,
327            result: super::SignResult {
328                signature: super::Base64Bytes::from(vec![0xff, 0x00, 0xab]).into_secret(),
329            },
330        });
331        let json = serde_json::to_string(&resp).unwrap();
332        assert!(json.contains("\"method\":\"sign\""));
333        let v: serde_json::Value = serde_json::from_str(&json).unwrap();
334        assert_eq!(v["id"], 2);
335        assert_eq!(v["method"], "sign");
336    }
337
338    #[test]
339    fn serialize_error_response() {
340        let resp = super::Response::Error(super::ErrorResponse {
341            id: 3,
342            error: super::ErrorPayload {
343                code: 1,
344                message: "not found".to_owned(),
345            },
346        });
347        let json = serde_json::to_string(&resp).unwrap();
348        let v: serde_json::Value = serde_json::from_str(&json).unwrap();
349        assert_eq!(v["id"], 3);
350        assert_eq!(v["error"]["code"], 1);
351        assert_eq!(v["error"]["message"], "not found");
352        assert!(!json.contains("\"result\""));
353        assert!(!json.contains("\"method\""));
354    }
355
356    #[test]
357    fn deserialize_result_response() {
358        let json = r#"{"id":1,"method":"initialize","result":{"default":"c1","certificates":[{"id":"c1","domains":["*.test"],"pem":"---"}]}}"#;
359        let resp: super::Response = serde_json::from_str(json).unwrap();
360        assert_eq!(resp.id(), 1);
361        match resp {
362            super::Response::Success(super::SuccessResponse::Initialize { result, .. }) => {
363                assert_eq!(result.default, "c1");
364                assert_eq!(result.certificates.len(), 1);
365            }
366            _ => panic!("expected Initialize Result"),
367        }
368    }
369
370    #[test]
371    fn deserialize_error_response() {
372        let json = r#"{"id":2,"error":{"code":99,"message":"boom"}}"#;
373        let resp: super::Response = serde_json::from_str(json).unwrap();
374        assert_eq!(resp.id(), 2);
375        match resp {
376            super::Response::Error(super::ErrorResponse { error, .. }) => {
377                assert_eq!(error.code, 99);
378                assert_eq!(error.message, "boom");
379            }
380            _ => panic!("expected Error"),
381        }
382    }
383
384    #[test]
385    fn sign_params_blob_round_trip() {
386        let params = super::SignParams {
387            certificate_id: "x".to_owned(),
388            scheme: "RSA_PSS_SHA256".to_owned(),
389            blob: super::Base64Bytes::from((0..=255).collect::<Vec<u8>>()).into_secret(),
390        };
391        let json = serde_json::to_string(&params).unwrap();
392        let decoded: super::SignParams = serde_json::from_str(&json).unwrap();
393        assert_eq!(
394            decoded.blob.expose_secret().as_slice(),
395            params.blob.expose_secret().as_slice()
396        );
397    }
398
399    #[test]
400    fn error_code_as_i64() {
401        assert_eq!(super::ErrorCode::Internal("x".to_owned()).as_i64(), -1);
402        assert_eq!(
403            super::ErrorCode::CertificateNotFound("x".to_owned()).as_i64(),
404            1
405        );
406        assert_eq!(
407            super::ErrorCode::UnsupportedScheme("x".to_owned()).as_i64(),
408            2
409        );
410        assert_eq!(super::ErrorCode::SigningFailed("x".to_owned()).as_i64(), 3);
411        assert_eq!(
412            super::ErrorCode::Other {
413                code: 42,
414                message: "x".to_owned()
415            }
416            .as_i64(),
417            42
418        );
419    }
420
421    #[test]
422    fn error_code_from_error_payload() {
423        let payload = super::ErrorPayload {
424            code: -1,
425            message: "boom".to_owned(),
426        };
427        let code: super::ErrorCode = payload.into();
428        assert!(matches!(code, super::ErrorCode::Internal(m) if m == "boom"));
429
430        let payload = super::ErrorPayload {
431            code: 1,
432            message: "gone".to_owned(),
433        };
434        let code: super::ErrorCode = payload.into();
435        assert!(matches!(code, super::ErrorCode::CertificateNotFound(m) if m == "gone"));
436
437        let payload = super::ErrorPayload {
438            code: 99,
439            message: "custom".to_owned(),
440        };
441        let code: super::ErrorCode = payload.into();
442        assert!(
443            matches!(code, super::ErrorCode::Other { code: 99, message } if message == "custom")
444        );
445    }
446
447    #[test]
448    fn error_code_to_error_payload() {
449        let code = super::ErrorCode::CertificateNotFound("not found".to_owned());
450        let payload: super::ErrorPayload = code.into();
451        assert_eq!(payload.code, 1);
452        assert_eq!(payload.message, "certificate not found: not found");
453    }
454
455    #[test]
456    fn error_payload_serde_preserves_wire_format() {
457        let payload = super::ErrorPayload {
458            code: 1,
459            message: "not found".to_owned(),
460        };
461        let json = serde_json::to_string(&payload).unwrap();
462        let v: serde_json::Value = serde_json::from_str(&json).unwrap();
463        assert_eq!(v["code"], 1);
464
465        let decoded: super::ErrorPayload = serde_json::from_str(&json).unwrap();
466        assert_eq!(decoded.code, 1);
467    }
468}