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
74impl<T: SignedBody> Signed<T> {
75 pub(crate) fn sign<A: keys::IdentityKeyAuthority + ?Sized>(body: T, signer: &A) -> Self {
77 let mut value = Self {
78 version: STORE_PROTOCOL_VERSION,
79 body,
80 signature: String::new(),
81 digest: OnceLock::new(),
82 };
83 value.resign(signer);
84 value
85 }
86
87 pub(crate) fn require_version(&self) -> Result<(), StoreProtocolError> {
91 super::require_version(self.version)
92 }
93
94 pub fn verify_by(&self, public_key: &str) -> Result<(), StoreProtocolError> {
97 self.require_version()?;
98 if keys::verify_signature_hex(public_key, &self.signature, self.digest().as_bytes()) {
99 Ok(())
100 } else {
101 Err(StoreProtocolError::InvalidSignature)
102 }
103 }
104
105 pub(crate) fn hash(&self) -> ObjectHash {
107 self.digest()
108 }
109
110 fn digest(&self) -> ObjectHash {
111 *self.digest.get_or_init(|| {
112 ObjectHash::digest(&domain_json(
113 T::DOMAIN,
114 &SignedFields {
115 version: self.version,
116 body: &self.body,
117 },
118 ))
119 })
120 }
121
122 pub(crate) fn body(&self) -> &T {
123 &self.body
124 }
125
126 pub fn body_mut(&mut self) -> &mut T {
134 self.digest = OnceLock::new();
135 &mut self.body
136 }
137
138 pub fn resign<A: keys::IdentityKeyAuthority + ?Sized>(&mut self, signer: &A) {
142 self.signature = keys::sign_hex(signer, self.digest().as_bytes()).1;
143 }
144
145 pub(crate) fn sign_by_device(body: T, signer: &dyn keys::DeviceSigningAuthority) -> Self {
148 let mut value = Self {
149 version: STORE_PROTOCOL_VERSION,
150 body,
151 signature: String::new(),
152 digest: OnceLock::new(),
153 };
154 value.signature = hex::encode(signer.sign(value.digest().as_bytes()));
155 value
156 }
157
158 #[cfg(any(test, feature = "test-utils"))]
161 pub fn corrupt_signature_for_test(&mut self) {
162 self.signature.push('0');
163 }
164}
165
166impl<T> Signed<T> {
167 #[cfg(any(test, feature = "test-utils"))]
170 pub fn unsigned_for_test(body: T) -> Self {
171 Self {
172 version: STORE_PROTOCOL_VERSION,
173 body,
174 signature: String::new(),
175 digest: OnceLock::new(),
176 }
177 }
178}
179
180impl<T: Serialize> Signed<T> {
181 pub fn to_bytes(&self) -> Vec<u8> {
182 serde_json::to_vec(self).expect("a signed artifact serializes")
183 }
184}
185
186impl<T> std::ops::Deref for Signed<T> {
190 type Target = T;
191
192 fn deref(&self) -> &T {
193 &self.body
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200 use keys::UserKeypair;
201
202 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
203 struct Note {
204 text: String,
205 }
206
207 impl SignedBody for Note {
208 const DOMAIN: &'static [u8] = b"store-v1/test/note";
209 }
210
211 fn note(text: &str, signer: &UserKeypair) -> Signed<Note> {
212 Signed::sign(
213 Note {
214 text: text.to_string(),
215 },
216 signer,
217 )
218 }
219
220 #[test]
221 fn an_artifact_hashes_to_the_same_value_every_time_it_is_asked() {
222 let signer = UserKeypair::generate();
223 let note = note("first", &signer);
224
225 let hash = note.hash();
226
227 assert_eq!(hash, note.hash());
228 assert_eq!(hash, note.hash());
229 }
230
231 #[test]
232 fn an_edited_body_hashes_as_the_body_it_now_holds() {
233 let signer = UserKeypair::generate();
234 let mut edited = note("first", &signer);
235 let before = edited.hash();
236
237 edited.body_mut().text = "second".to_string();
238
239 assert_ne!(before, edited.hash());
240 assert_eq!(edited.hash(), note("second", &signer).hash());
241 }
242
243 #[test]
244 fn an_edited_body_fails_verification_until_it_is_signed_again() {
245 let signer = UserKeypair::generate();
246 let public_key = keys::public_key_hex(&signer);
247 let mut edited = note("first", &signer);
248 edited.verify_by(&public_key).unwrap();
249
250 edited.body_mut().text = "second".to_string();
251
252 assert!(matches!(
253 edited.verify_by(&public_key),
254 Err(StoreProtocolError::InvalidSignature)
255 ));
256 edited.resign(&signer);
257 edited.verify_by(&public_key).unwrap();
258 }
259
260 #[test]
261 fn signing_again_leaves_the_artifact_identified_by_the_same_hash() {
262 let signer = UserKeypair::generate();
263 let mut resigned = note("first", &signer);
264 let before = resigned.hash();
265
266 resigned.resign(&UserKeypair::generate());
267
268 assert_eq!(before, resigned.hash());
269 }
270
271 #[test]
272 fn a_round_trip_through_json_keeps_the_artifact_equal_verifiable_and_identified() {
273 let signer = UserKeypair::generate();
274 let public_key = keys::public_key_hex(&signer);
275 let original = note("first", &signer);
276 let hash = original.hash();
277
278 let parsed: Signed<Note> = serde_json::from_slice(&original.to_bytes()).unwrap();
279
280 assert_eq!(parsed, original);
281 parsed.verify_by(&public_key).unwrap();
282 assert_eq!(hash, parsed.hash());
283 }
284}