coven_protocol/store_commit/
signed.rs1use std::sync::OnceLock;
2
3use serde::{Deserialize, Serialize};
4
5use super::{domain_json, ObjectHash, StoreProtocolError, STORE_PROTOCOL_VERSION};
6use coven_keys::keys;
7
8pub trait SignedBody: Serialize {
15 const DOMAIN: &'static [u8];
16}
17
18#[derive(Clone, Serialize, Deserialize)]
25#[serde(deny_unknown_fields)]
26pub struct Signed<T> {
27 version: u32,
28 body: T,
29 signature: String,
30 #[serde(skip)]
37 digest: OnceLock<ObjectHash>,
38}
39
40impl<T: PartialEq> PartialEq for Signed<T> {
44 fn eq(&self, other: &Self) -> bool {
45 self.version == other.version
46 && self.body == other.body
47 && self.signature == other.signature
48 }
49}
50
51impl<T: Eq> Eq for Signed<T> {}
52
53impl<T: std::fmt::Debug> std::fmt::Debug for Signed<T> {
57 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 formatter
59 .debug_struct("Signed")
60 .field("version", &self.version)
61 .field("body", &self.body)
62 .field("signature", &self.signature)
63 .finish()
64 }
65}
66
67#[derive(Serialize)]
69struct SignedFields<'a, T> {
70 version: u32,
71 body: &'a T,
72}
73
74pub(super) fn signed_body_hash<T: SignedBody>(version: u32, body: &T) -> ObjectHash {
75 ObjectHash::digest(&domain_json(T::DOMAIN, &SignedFields { version, body }))
76}
77
78impl<T: SignedBody> Signed<T> {
79 pub(crate) fn sign<A: keys::IdentityKeyAuthority + ?Sized>(body: T, signer: &A) -> Self {
81 let mut value = Self {
82 version: STORE_PROTOCOL_VERSION,
83 body,
84 signature: String::new(),
85 digest: OnceLock::new(),
86 };
87 value.resign(signer);
88 value
89 }
90
91 pub(crate) fn require_version(&self) -> Result<(), StoreProtocolError> {
95 super::require_version(self.version)
96 }
97
98 pub fn verify_by(&self, public_key: &str) -> Result<(), StoreProtocolError> {
101 self.require_version()?;
102 if keys::verify_signature_hex(public_key, &self.signature, self.digest().as_bytes()) {
103 Ok(())
104 } else {
105 Err(StoreProtocolError::InvalidSignature)
106 }
107 }
108
109 pub(crate) fn hash(&self) -> ObjectHash {
111 self.digest()
112 }
113
114 fn digest(&self) -> ObjectHash {
115 *self
116 .digest
117 .get_or_init(|| signed_body_hash(self.version, &self.body))
118 }
119
120 pub(crate) fn body(&self) -> &T {
121 &self.body
122 }
123
124 pub fn body_mut(&mut self) -> &mut T {
132 self.digest = OnceLock::new();
133 &mut self.body
134 }
135
136 pub fn resign<A: keys::IdentityKeyAuthority + ?Sized>(&mut self, signer: &A) {
140 self.signature = keys::sign_hex(signer, self.digest().as_bytes()).1;
141 }
142
143 pub(crate) fn sign_by_device(body: T, signer: &dyn keys::DeviceSigningAuthority) -> Self {
146 let mut value = Self {
147 version: STORE_PROTOCOL_VERSION,
148 body,
149 signature: String::new(),
150 digest: OnceLock::new(),
151 };
152 value.signature = hex::encode(signer.sign(value.digest().as_bytes()));
153 value
154 }
155
156 #[cfg(any(test, feature = "test-utils"))]
159 pub fn corrupt_signature_for_test(&mut self) {
160 self.signature.push('0');
161 }
162}
163
164impl<T> Signed<T> {
165 #[cfg(any(test, feature = "test-utils"))]
168 pub fn unsigned_for_test(body: T) -> Self {
169 Self {
170 version: STORE_PROTOCOL_VERSION,
171 body,
172 signature: String::new(),
173 digest: OnceLock::new(),
174 }
175 }
176}
177
178impl<T: Serialize> Signed<T> {
179 pub fn to_bytes(&self) -> Vec<u8> {
180 serde_json::to_vec(self).expect("a signed artifact serializes")
181 }
182}
183
184impl<T> std::ops::Deref for Signed<T> {
188 type Target = T;
189
190 fn deref(&self) -> &T {
191 &self.body
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198 use keys::UserKeypair;
199
200 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
201 struct Note {
202 text: String,
203 }
204
205 impl SignedBody for Note {
206 const DOMAIN: &'static [u8] = b"store-v1/test/note";
207 }
208
209 fn note(text: &str, signer: &UserKeypair) -> Signed<Note> {
210 Signed::sign(
211 Note {
212 text: text.to_string(),
213 },
214 signer,
215 )
216 }
217
218 #[test]
219 fn an_artifact_hashes_to_the_same_value_every_time_it_is_asked() {
220 let signer = UserKeypair::generate();
221 let note = note("first", &signer);
222
223 let hash = note.hash();
224
225 assert_eq!(hash, note.hash());
226 assert_eq!(hash, note.hash());
227 }
228
229 #[test]
230 fn an_edited_body_hashes_as_the_body_it_now_holds() {
231 let signer = UserKeypair::generate();
232 let mut edited = note("first", &signer);
233 let before = edited.hash();
234
235 edited.body_mut().text = "second".to_string();
236
237 assert_ne!(before, edited.hash());
238 assert_eq!(edited.hash(), note("second", &signer).hash());
239 }
240
241 #[test]
242 fn an_edited_body_fails_verification_until_it_is_signed_again() {
243 let signer = UserKeypair::generate();
244 let public_key = keys::public_key_hex(&signer);
245 let mut edited = note("first", &signer);
246 edited.verify_by(&public_key).unwrap();
247
248 edited.body_mut().text = "second".to_string();
249
250 assert!(matches!(
251 edited.verify_by(&public_key),
252 Err(StoreProtocolError::InvalidSignature)
253 ));
254 edited.resign(&signer);
255 edited.verify_by(&public_key).unwrap();
256 }
257
258 #[test]
259 fn signing_again_leaves_the_artifact_identified_by_the_same_hash() {
260 let signer = UserKeypair::generate();
261 let mut resigned = note("first", &signer);
262 let before = resigned.hash();
263
264 resigned.resign(&UserKeypair::generate());
265
266 assert_eq!(before, resigned.hash());
267 }
268
269 #[test]
270 fn a_round_trip_through_json_keeps_the_artifact_equal_verifiable_and_identified() {
271 let signer = UserKeypair::generate();
272 let public_key = keys::public_key_hex(&signer);
273 let original = note("first", &signer);
274 let hash = original.hash();
275
276 let parsed: Signed<Note> = serde_json::from_slice(&original.to_bytes()).unwrap();
277
278 assert_eq!(parsed, original);
279 parsed.verify_by(&public_key).unwrap();
280 assert_eq!(hash, parsed.hash());
281 }
282}