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
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    /// Sign `body` under this build's protocol version.
80    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    /// Refuse an artifact written under a version this build does not read.
92    /// [`Self::verify_by`] runs this first; a shape check that has no signer to
93    /// verify against calls it on its own.
94    pub(crate) fn require_version(&self) -> Result<(), StoreProtocolError> {
95        super::require_version(self.version)
96    }
97
98    /// Check the signature against `public_key`, refusing a version this build
99    /// does not read before spending a verification on it.
100    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    /// The artifact's identity: the digest of its domain-separated signed bytes.
110    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    /// The body, mutable, leaving the signature over whatever it held before.
125    /// A draft artifact is built against objects whose slots are only allocated
126    /// later, so its body is edited into final form and then [`Self::resign`]ed;
127    /// a test uses this to build the tampered forms a verifier has to reject.
128    /// Every reader of the value between the two calls sees a signature that
129    /// does not check out. The cached digest goes with the old body: the next
130    /// hash, verification, or re-signing is taken over the bytes as edited.
131    pub fn body_mut(&mut self) -> &mut T {
132        self.digest = OnceLock::new();
133        &mut self.body
134    }
135
136    /// Sign the body this value now holds, replacing any earlier signature.
137    /// The signature is not part of what the digest covers, so an artifact
138    /// signed again is still identified by the same hash.
139    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    /// Sign `body` with a device authority — a retained capability that signs
144    /// on a device's behalf without exposing the key [`Self::sign`] takes.
145    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    /// Damage the signature so verification fails, for tests that assert a
157    /// verifier refuses an artifact whose signature does not check out.
158    #[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    /// An envelope carrying no signature, for tests that need an artifact's
166    /// shape somewhere no verifier reads it.
167    #[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
184/// Reading a signed artifact's fields reads its body. The envelope's own parts —
185/// the version and the signature — are reached through its methods, so a body
186/// field can never be shadowed by one of them.
187impl<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}