1use std::collections::BTreeMap;
14
15use didbot_data::{dag_cbor, Cid, Value};
16use didbot_key::{secp256r1, KeyError, Signature, SigningKey, VerifyingKey};
17
18pub const REPO_VERSION: i64 = 3;
24
25#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
27pub enum CommitError {
28 #[error("the commit's signature is not a usable signature: {0}")]
30 Malformed(String),
31 #[error("the block is not a commit: {0}")]
34 NotACommit(String),
35 #[error("the commit's signature does not verify against this key")]
42 Unverified,
43 #[error("commit declares repository version {found}, not {REPO_VERSION}")]
45 Version {
46 found: i64,
48 },
49 #[error("commit is for `{found}`, not `{expected}`")]
51 WrongRepo {
52 found: String,
54 expected: String,
56 },
57}
58
59#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct Commit {
66 pub did: String,
68 pub version: i64,
70 pub data: Cid,
72 pub rev: String,
79 pub prev: Option<Cid>,
81}
82
83impl Commit {
84 #[must_use]
93 pub fn to_sign(&self) -> Vec<u8> {
94 dag_cbor::encode(&self.to_value(None))
95 }
96
97 #[must_use]
105 pub fn sign(self, key: &SigningKey) -> SignedCommit {
106 let signature = key.sign(&self.to_sign());
107 SignedCommit {
108 commit: self,
109 signature: signature.to_bytes(),
110 }
111 }
112
113 fn to_value(&self, signature: Option<&[u8; 64]>) -> Value {
115 let mut fields = BTreeMap::from([
116 ("did".to_owned(), Value::String(self.did.clone())),
117 ("version".to_owned(), Value::Integer(self.version)),
118 ("data".to_owned(), Value::Link(self.data.clone())),
119 ("rev".to_owned(), Value::String(self.rev.clone())),
120 (
121 "prev".to_owned(),
122 self.prev
123 .as_ref()
124 .map_or(Value::Null, |cid| Value::Link(cid.clone())),
125 ),
126 ]);
127 if let Some(signature) = signature {
128 fields.insert("sig".to_owned(), Value::Bytes(signature.to_vec()));
129 }
130 Value::Map(fields)
131 }
132}
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub enum CommitKey {
139 Secp256k1(VerifyingKey),
141 P256(secp256r1::VerifyingKey),
143}
144
145impl CommitKey {
146 pub fn from_multibase(multibase: &str) -> Result<Self, KeyError> {
148 match VerifyingKey::from_multibase(multibase) {
149 Ok(key) => Ok(Self::Secp256k1(key)),
150 Err(KeyError::WrongCurve { .. }) => {
151 secp256r1::VerifyingKey::from_multibase(multibase).map(Self::P256)
152 }
153 Err(error) => Err(error),
154 }
155 }
156}
157
158#[derive(Debug, Clone, PartialEq, Eq)]
160pub struct SignedCommit {
161 commit: Commit,
162 signature: [u8; 64],
163}
164
165impl SignedCommit {
166 #[must_use]
168 pub fn new(commit: Commit, signature: [u8; 64]) -> Self {
169 Self { commit, signature }
170 }
171
172 pub fn decode(block: &[u8]) -> Result<Self, CommitError> {
179 let refuse = |why: &str| CommitError::NotACommit(why.to_owned());
180 let value = dag_cbor::decode(block).map_err(|error| refuse(&error.to_string()))?;
181 let Value::Map(mut fields) = value else {
182 return Err(refuse("it is not a map"));
183 };
184 let mut take = |name: &str| {
185 fields
186 .remove(name)
187 .ok_or_else(|| refuse(&format!("it has no `{name}`")))
188 };
189 let (Value::String(did), Value::Integer(version), Value::Link(data), Value::String(rev)) =
190 (take("did")?, take("version")?, take("data")?, take("rev")?)
191 else {
192 return Err(refuse(
193 "`did`, `version`, `data` or `rev` has the wrong type",
194 ));
195 };
196 let prev = match take("prev")? {
197 Value::Null => None,
198 Value::Link(cid) => Some(cid),
199 _ => return Err(refuse("`prev` is neither a link nor null")),
200 };
201 let Value::Bytes(signature) = take("sig")? else {
202 return Err(refuse("`sig` is not bytes"));
203 };
204 let signature: [u8; 64] = signature
205 .try_into()
206 .map_err(|_| refuse("`sig` is not 64 bytes"))?;
207 if let Some(extra) = fields.keys().next() {
208 return Err(refuse(&format!("it has a `{extra}` field")));
209 }
210 Ok(Self::new(
211 Commit {
212 did,
213 version,
214 data,
215 rev,
216 prev,
217 },
218 signature,
219 ))
220 }
221
222 #[must_use]
224 pub fn commit(&self) -> &Commit {
225 &self.commit
226 }
227
228 #[must_use]
230 pub fn signature(&self) -> &[u8; 64] {
231 &self.signature
232 }
233
234 #[must_use]
236 pub fn block(&self) -> Vec<u8> {
237 dag_cbor::encode(&self.commit.to_value(Some(&self.signature)))
238 }
239
240 #[must_use]
242 pub fn cid(&self) -> Cid {
243 Cid::of_dag_cbor(&self.block())
244 }
245
246 pub fn verify(&self, key: &VerifyingKey) -> Result<(), CommitError> {
258 self.check_version()?;
259 let signature = Signature::from_bytes(&self.signature)
260 .map_err(|error| CommitError::Malformed(error.to_string()))?;
261 key.verify(&self.commit.to_sign(), &signature)
262 .map_err(|_| CommitError::Unverified)
263 }
264
265 pub fn verify_for(&self, did: &str, key: &VerifyingKey) -> Result<(), CommitError> {
273 self.check_repo(did)?;
274 self.verify(key)
275 }
276
277 pub fn verify_with(&self, did: &str, key: &CommitKey) -> Result<(), CommitError> {
282 let key = match key {
283 CommitKey::Secp256k1(key) => return self.verify_for(did, key),
284 CommitKey::P256(key) => key,
285 };
286 self.check_repo(did)?;
287 self.check_version()?;
288 let signature = secp256r1::Signature::from_bytes(&self.signature)
289 .map_err(|error| CommitError::Malformed(error.to_string()))?;
290 key.verify(&self.commit.to_sign(), &signature)
291 .map_err(|_| CommitError::Unverified)
292 }
293
294 fn check_repo(&self, did: &str) -> Result<(), CommitError> {
296 if self.commit.did != did {
297 return Err(CommitError::WrongRepo {
298 found: self.commit.did.clone(),
299 expected: did.to_owned(),
300 });
301 }
302 Ok(())
303 }
304
305 fn check_version(&self) -> Result<(), CommitError> {
307 if self.commit.version != REPO_VERSION {
308 return Err(CommitError::Version {
309 found: self.commit.version,
310 });
311 }
312 Ok(())
313 }
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319
320 fn commit() -> Commit {
321 Commit {
322 did: "did:web:kestrel.agents.localhost%3A4100".to_owned(),
323 version: REPO_VERSION,
324 data: Cid::of_dag_cbor(b"a tree root"),
325 rev: "3lqmnopqrstuv".to_owned(),
326 prev: None,
327 }
328 }
329
330 #[test]
331 fn a_signed_commit_verifies_against_its_own_public_half() {
332 let key = SigningKey::generate();
333 let signed = commit().sign(&key);
334 assert!(signed.verify(&key.verifying_key()).is_ok());
335 }
336
337 #[test]
341 fn the_signed_bytes_are_not_the_block() {
342 let signed = commit().sign(&SigningKey::generate());
343 assert_ne!(signed.commit().to_sign(), signed.block());
344 assert!(signed.block().len() > signed.commit().to_sign().len());
345 }
346
347 #[test]
348 fn another_key_does_not_verify_it() {
349 let signed = commit().sign(&SigningKey::generate());
350 let stranger = SigningKey::generate().verifying_key();
351 assert_eq!(signed.verify(&stranger), Err(CommitError::Unverified));
352 }
353
354 #[test]
358 fn no_field_can_be_moved_without_breaking_the_signature() {
359 let key = SigningKey::generate();
360 let public = key.verifying_key();
361 let signed = commit().sign(&key);
362
363 let mut elsewhere = commit();
364 elsewhere.did = "did:web:heron.agents.localhost%3A4100".to_owned();
365 assert!(SignedCommit::new(elsewhere, *signed.signature())
366 .verify(&public)
367 .is_err());
368
369 let mut retree = commit();
370 retree.data = Cid::of_dag_cbor(b"a different tree root");
371 assert!(SignedCommit::new(retree, *signed.signature())
372 .verify(&public)
373 .is_err());
374
375 let mut rewound = commit();
376 rewound.rev = "3lqmnopqrstuu".to_owned();
377 assert!(SignedCommit::new(rewound, *signed.signature())
378 .verify(&public)
379 .is_err());
380
381 let mut chained = commit();
382 chained.prev = Some(Cid::of_dag_cbor(b"an earlier commit"));
383 assert!(SignedCommit::new(chained, *signed.signature())
384 .verify(&public)
385 .is_err());
386 }
387
388 #[test]
390 fn a_valid_signature_for_the_wrong_repository_is_refused() {
391 let key = SigningKey::generate();
392 let signed = commit().sign(&key);
393 assert_eq!(
394 signed.verify_for(
395 "did:web:heron.agents.localhost%3A4100",
396 &key.verifying_key()
397 ),
398 Err(CommitError::WrongRepo {
399 found: commit().did,
400 expected: "did:web:heron.agents.localhost%3A4100".to_owned(),
401 })
402 );
403 assert!(signed
404 .verify_for(&commit().did, &key.verifying_key())
405 .is_ok());
406 }
407
408 #[test]
409 fn a_version_this_crate_does_not_write_is_refused_before_the_signature() {
410 let key = SigningKey::generate();
411 let mut future = commit();
412 future.version = 4;
413 let signed = future.sign(&key);
414 assert_eq!(
415 signed.verify(&key.verifying_key()),
416 Err(CommitError::Version { found: 4 })
417 );
418 }
419
420 #[test]
423 fn signing_the_same_state_twice_gives_the_same_commit() {
424 let key = SigningKey::generate();
425 assert_eq!(commit().sign(&key).cid(), commit().sign(&key).cid());
426 }
427}