treeship_core/attestation/
envelope.rs1use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct Envelope {
12 pub payload: String,
14
15 #[serde(rename = "payloadType")]
18 pub payload_type: String,
19
20 pub signatures: Vec<Signature>,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct Signature {
28 pub keyid: String,
30
31 pub sig: String,
33}
34
35#[derive(Debug)]
37pub enum EnvelopeError {
38 Base64Decode(String),
39 JsonParse(String),
40 EmptyPayload,
41 EmptyPayloadType,
42 NoSignatures,
43}
44
45impl std::fmt::Display for EnvelopeError {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 match self {
48 Self::Base64Decode(e) => write!(f, "base64 decode: {}", e),
49 Self::JsonParse(e) => write!(f, "json parse: {}", e),
50 Self::EmptyPayload => write!(f, "payload is empty"),
51 Self::EmptyPayloadType => write!(f, "payloadType is empty"),
52 Self::NoSignatures => write!(f, "no signatures in envelope"),
53 }
54 }
55}
56
57impl std::error::Error for EnvelopeError {}
58
59impl Envelope {
60 pub fn payload_bytes(&self) -> Result<Vec<u8>, EnvelopeError> {
62 URL_SAFE_NO_PAD
63 .decode(&self.payload)
64 .map_err(|e| EnvelopeError::Base64Decode(e.to_string()))
65 }
66
67 pub fn unmarshal_statement<T: serde::de::DeserializeOwned>(&self) -> Result<T, EnvelopeError> {
69 let bytes = self.payload_bytes()?;
70 serde_json::from_slice(&bytes).map_err(|e| EnvelopeError::JsonParse(e.to_string()))
71 }
72
73 pub fn sig_bytes(sig: &Signature) -> Result<Vec<u8>, EnvelopeError> {
75 URL_SAFE_NO_PAD
76 .decode(&sig.sig)
77 .map_err(|e| EnvelopeError::Base64Decode(format!("sig for key {}: {}", sig.keyid, e)))
78 }
79
80 pub fn to_json(&self) -> Result<Vec<u8>, EnvelopeError> {
82 serde_json::to_vec(self).map_err(|e| EnvelopeError::JsonParse(e.to_string()))
83 }
84
85 pub fn from_json(bytes: &[u8]) -> Result<Self, EnvelopeError> {
87 let e: Envelope =
88 serde_json::from_slice(bytes).map_err(|e| EnvelopeError::JsonParse(e.to_string()))?;
89 if e.payload.is_empty() {
90 return Err(EnvelopeError::EmptyPayload);
91 }
92 if e.payload_type.is_empty() {
93 return Err(EnvelopeError::EmptyPayloadType);
94 }
95 if e.signatures.is_empty() {
96 return Err(EnvelopeError::NoSignatures);
97 }
98 Ok(e)
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use serde::{Deserialize, Serialize};
106
107 #[derive(Debug, PartialEq, Serialize, Deserialize)]
108 struct TestStmt {
109 actor: String,
110 }
111
112 fn make_envelope(payload: &str) -> Envelope {
113 Envelope {
114 payload: URL_SAFE_NO_PAD.encode(payload),
115 payload_type: "application/vnd.treeship.action.v1+json".into(),
116 signatures: vec![Signature {
117 keyid: "key_test".into(),
118 sig: "c2ln".into(),
119 }],
120 }
121 }
122
123 #[test]
124 fn payload_bytes_roundtrip() {
125 let original = b"{\"actor\":\"agent://test\"}";
126 let env = Envelope {
127 payload: URL_SAFE_NO_PAD.encode(original),
128 payload_type: "application/vnd.treeship.action.v1+json".into(),
129 signatures: vec![],
130 };
131 assert_eq!(env.payload_bytes().unwrap(), original);
132 }
133
134 #[test]
135 fn unmarshal_statement() {
136 let stmt = TestStmt {
137 actor: "agent://test".into(),
138 };
139 let json = serde_json::to_vec(&stmt).unwrap();
140 let env = Envelope {
141 payload: URL_SAFE_NO_PAD.encode(&json),
142 payload_type: "application/vnd.treeship.action.v1+json".into(),
143 signatures: vec![],
144 };
145 let decoded: TestStmt = env.unmarshal_statement().unwrap();
146 assert_eq!(decoded, stmt);
147 }
148
149 #[test]
150 fn json_roundtrip() {
151 let env = make_envelope("{\"actor\":\"agent://test\"}");
152 let json = env.to_json().unwrap();
153 let restored = Envelope::from_json(&json).unwrap();
154 assert_eq!(restored.payload, env.payload);
155 assert_eq!(restored.payload_type, env.payload_type);
156 }
157
158 #[test]
159 fn from_json_rejects_empty_payload() {
160 let json =
161 br#"{"payload":"","payloadType":"text/plain","signatures":[{"keyid":"k","sig":"s"}]}"#;
162 assert!(Envelope::from_json(json).is_err());
163 }
164
165 #[test]
166 fn from_json_rejects_no_signatures() {
167 let json = br#"{"payload":"YQ","payloadType":"text/plain","signatures":[]}"#;
168 assert!(Envelope::from_json(json).is_err());
169 }
170
171 #[test]
172 fn from_json_rejects_empty_payload_type() {
173 let json = br#"{"payload":"YQ","payloadType":"","signatures":[{"keyid":"k","sig":"s"}]}"#;
174 assert!(Envelope::from_json(json).is_err());
175 }
176}