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>(
69 &self,
70 ) -> Result<T, EnvelopeError> {
71 let bytes = self.payload_bytes()?;
72 serde_json::from_slice(&bytes)
73 .map_err(|e| EnvelopeError::JsonParse(e.to_string()))
74 }
75
76 pub fn sig_bytes(sig: &Signature) -> Result<Vec<u8>, EnvelopeError> {
78 URL_SAFE_NO_PAD
79 .decode(&sig.sig)
80 .map_err(|e| EnvelopeError::Base64Decode(
81 format!("sig for key {}: {}", sig.keyid, e)
82 ))
83 }
84
85 pub fn to_json(&self) -> Result<Vec<u8>, EnvelopeError> {
87 serde_json::to_vec(self)
88 .map_err(|e| EnvelopeError::JsonParse(e.to_string()))
89 }
90
91 pub fn from_json(bytes: &[u8]) -> Result<Self, EnvelopeError> {
93 let e: Envelope = serde_json::from_slice(bytes)
94 .map_err(|e| EnvelopeError::JsonParse(e.to_string()))?;
95 if e.payload.is_empty() { return Err(EnvelopeError::EmptyPayload); }
96 if e.payload_type.is_empty() { return Err(EnvelopeError::EmptyPayloadType); }
97 if e.signatures.is_empty() { return Err(EnvelopeError::NoSignatures); }
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 { keyid: "key_test".into(), sig: "c2ln".into() }],
117 }
118 }
119
120 #[test]
121 fn payload_bytes_roundtrip() {
122 let original = b"{\"actor\":\"agent://test\"}";
123 let env = Envelope {
124 payload: URL_SAFE_NO_PAD.encode(original),
125 payload_type: "application/vnd.treeship.action.v1+json".into(),
126 signatures: vec![],
127 };
128 assert_eq!(env.payload_bytes().unwrap(), original);
129 }
130
131 #[test]
132 fn unmarshal_statement() {
133 let stmt = TestStmt { actor: "agent://test".into() };
134 let json = serde_json::to_vec(&stmt).unwrap();
135 let env = Envelope {
136 payload: URL_SAFE_NO_PAD.encode(&json),
137 payload_type: "application/vnd.treeship.action.v1+json".into(),
138 signatures: vec![],
139 };
140 let decoded: TestStmt = env.unmarshal_statement().unwrap();
141 assert_eq!(decoded, stmt);
142 }
143
144 #[test]
145 fn json_roundtrip() {
146 let env = make_envelope("{\"actor\":\"agent://test\"}");
147 let json = env.to_json().unwrap();
148 let restored = Envelope::from_json(&json).unwrap();
149 assert_eq!(restored.payload, env.payload);
150 assert_eq!(restored.payload_type, env.payload_type);
151 }
152
153 #[test]
154 fn from_json_rejects_empty_payload() {
155 let json = br#"{"payload":"","payloadType":"text/plain","signatures":[{"keyid":"k","sig":"s"}]}"#;
156 assert!(Envelope::from_json(json).is_err());
157 }
158
159 #[test]
160 fn from_json_rejects_no_signatures() {
161 let json = br#"{"payload":"YQ","payloadType":"text/plain","signatures":[]}"#;
162 assert!(Envelope::from_json(json).is_err());
163 }
164
165 #[test]
166 fn from_json_rejects_empty_payload_type() {
167 let json = br#"{"payload":"YQ","payloadType":"","signatures":[{"keyid":"k","sig":"s"}]}"#;
168 assert!(Envelope::from_json(json).is_err());
169 }
170}