Skip to main content

coven_protocol/store_commit/
signed.rs

1use std::sync::OnceLock;
2
3use serde::{Deserialize, Serialize};
4
5use super::{domain_json, ObjectHash, StoreProtocolError, STORE_PROTOCOL_VERSION};
6use coven_keys::keys;
7
8/// A value that travels signed. The body names the domain its signature is
9/// bound to, so a signature over one artifact can never be replayed as another.
10///
11/// Everything the body holds is signed, structurally: [`Signed`] serializes the
12/// whole body to produce the signed bytes. A field added to a body is covered
13/// the moment it exists, with no separate list to keep in step.
14pub trait SignedBody: Serialize {
15    const DOMAIN: &'static [u8];
16}
17
18/// One signed artifact: the protocol version it was written under, the body,
19/// and the signature over both.
20///
21/// The version lives here rather than inside each body because it says the same
22/// thing about every artifact. It is inside the signed bytes, so it cannot be
23/// edited without invalidating the signature.
24#[derive(Clone, Serialize, Deserialize)]
25#[serde(deny_unknown_fields)]
26pub struct Signed<T> {
27    version: u32,
28    body: T,
29    signature: String,
30    /// The digest of the signed bytes, computed the first time one is asked
31    /// for and held so that hashing, verifying, and re-signing the same
32    /// artifact serialize its body once rather than once per call. It is
33    /// derived state: it never crosses the wire, never enters equality, and
34    /// [`Signed::body_mut`] — the only way the bytes it covers can change —
35    /// drops it.
36    #[serde(skip)]
37    digest: OnceLock<ObjectHash>,
38}
39
40/// Two signed artifacts are the same artifact when they were written under the
41/// same version, carry the same body, and bear the same signature. The cached
42/// digest is a function of the first two, so it says nothing equality does not.
43impl<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
53/// Printed like the three fields that make up the artifact, so that two values
54/// that compare equal also read the same regardless of whether either has been
55/// hashed yet.
56impl<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/// What the signature covers: the version and the body, never the signature.
68#[derive(Serialize)]
69struct SignedFields<'a, T> {
70    version: u32,
71    body: &'a T,
72}
73
74impl<T: SignedBody> Signed<T> {
75    /// Sign `body` under this build's protocol version.
76    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    /// Refuse an artifact written under a version this build does not read.
88    /// [`Self::verify_by`] runs this first; a shape check that has no signer to
89    /// verify against calls it on its own.
90    pub(crate) fn require_version(&self) -> Result<(), StoreProtocolError> {
91        super::require_version(self.version)
92    }
93
94    /// Check the signature against `public_key`, refusing a version this build
95    /// does not read before spending a verification on it.
96    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    /// The artifact's identity: the digest of its domain-separated signed bytes.
106    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    /// The body, mutable, leaving the signature over whatever it held before.
127    /// A draft artifact is built against objects whose slots are only allocated
128    /// later, so its body is edited into final form and then [`Self::resign`]ed;
129    /// a test uses this to build the tampered forms a verifier has to reject.
130    /// Every reader of the value between the two calls sees a signature that
131    /// does not check out. The cached digest goes with the old body: the next
132    /// hash, verification, or re-signing is taken over the bytes as edited.
133    pub fn body_mut(&mut self) -> &mut T {
134        self.digest = OnceLock::new();
135        &mut self.body
136    }
137
138    /// Sign the body this value now holds, replacing any earlier signature.
139    /// The signature is not part of what the digest covers, so an artifact
140    /// signed again is still identified by the same hash.
141    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    /// Sign `body` with a device authority — a retained capability that signs
146    /// on a device's behalf without exposing the key [`Self::sign`] takes.
147    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    /// Damage the signature so verification fails, for tests that assert a
159    /// verifier refuses an artifact whose signature does not check out.
160    #[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    /// An envelope carrying no signature, for tests that need an artifact's
168    /// shape somewhere no verifier reads it.
169    #[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
186/// Reading a signed artifact's fields reads its body. The envelope's own parts —
187/// the version and the signature — are reached through its methods, so a body
188/// field can never be shadowed by one of them.
189impl<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}