Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_ore/src/envelope.rs

17.8 KiB, 186 runs

created by r1870400018:17503, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! Signed provenance for an operation.
2//!
3//! An envelope binds three things: the bytes of an operation, the public key of
4//! whoever authored it, and a detached signature over those bytes. History made
5//! of envelopes can be checked rather than trusted -- a reader can establish
6//! who wrote each edit without trusting the party that handed the history over.
7//!
8//! The signature scheme is not chosen here. The caller supplies an
9//! implementation of [`Signer`], which is where key material lives and where
10//! the algorithm is decided; this module only marshals bytes and asks that
11//! implementation to sign or verify. That keeps the crate free of key handling
12//! and free of any particular algorithm's baggage.
13//!
14//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
15//! Anthropic Claude
16
17use crate::op::Record;
18
19use oxedyne_fe2o3_core::prelude::*;
20use oxedyne_fe2o3_iop_crypto::sign::{
21 BatchItem,
22 Signer,
23};
24use oxedyne_fe2o3_jdat::{
25 prelude::*,
26 bdat::DecodeLimits,
27};
28
29
30/// An operation's bytes together with the provenance that attests to them.
31///
32/// The payload is opaque here. [`Envelope::seal_record`] fills it with a whole
33/// record -- the operation together with the header that names it and lists its
34/// parents -- so that both are inside what was signed: an operation lifted out,
35/// re-labelled or re-parented will not verify.
36#[derive(Clone, Debug, Eq, PartialEq)]
37pub struct Envelope {
38 payload: Vec<u8>, // signed bytes, exactly as presented to the signer
39 signer: Vec<u8>, // author's key, as the signature scheme encodes it
40 sig: Vec<u8>, // detached, over the payload
41}
42
43impl Envelope {
44 /// Nothing is verified here; call [`Envelope::verify`] for that.
45 pub fn new(payload: Vec<u8>, signer: Vec<u8>, sig: Vec<u8>) -> Self {
46 Self { payload, signer, sig }
47 }
48
49 /// The public key comes from the scheme, which must have one: an envelope
50 /// nobody can attribute is of no use.
51 pub fn seal<S: Signer>(scheme: &S, payload: Vec<u8>)
52 -> Outcome<Self>
53 {
54 let sig = res!(scheme.sign(&payload));
55 let signer = match res!(scheme.get_public_key()) {
56 Some(pk) => pk.to_vec(),
57 None => return Err(err!(
58 "The signing scheme has no public key set, so a sealed envelope could \
59 not be attributed to anyone.";
60 Missing, Key, Configuration)),
61 };
62 Ok(Self { payload, signer, sig })
63 }
64
65 /// The record is encoded in binary daticle form, so the identifier and the
66 /// parents are covered by the signature along with the operation.
67 pub fn seal_record<S: Signer>(scheme: &S, rec: &Record)
68 -> Outcome<Self>
69 {
70 Self::seal(scheme, res!(rec.to_dat().to_bytes(Vec::new())))
71 }
72
73 /// `scheme` supplies the algorithm only; its own keys are set aside and the
74 /// envelope's public key is used, so a caller cannot accidentally check a
75 /// signature against the wrong key. `false` is a signature that does not
76 /// check out, and an error is a check that could not be made.
77 pub fn verify<S: Signer>(&self, scheme: &S)
78 -> Outcome<bool>
79 {
80 let bound = res!(scheme.clone_with_keys(Some(&self.signer), None));
81 bound.verify(&self.payload, &self.sig)
82 }
83
84 /// Where the scheme has a batch verification equation this costs far less
85 /// than checking them one at a time, which is what makes it worth having:
86 /// replaying a history means verifying every operation in it, and that is
87 /// the largest single cost of reading a repository.
88 ///
89 /// A `false` says the set does not hold and cannot say which member failed,
90 /// because it never checked them separately, so a caller that owes its
91 /// reader the name of the operation falls back to [`Envelope::verify`] over
92 /// the same envelopes to find it; an error says as much about a set that
93 /// could not be checked. The set accepted is the set [`Envelope::verify`]
94 /// accepts, envelope for envelope; a scheme that cannot promise that should
95 /// not offer a batch.
96 pub fn verify_all<'a, S: Signer>(scheme: &S, envs: &[&'a Self])
97 -> Outcome<bool>
98 {
99 let items: Vec<BatchItem<'a>> = envs.iter()
100 .map(|env| BatchItem {
101 public: &env.signer,
102 msg: &env.payload,
103 sig: &env.sig,
104 })
105 .collect();
106 scheme.verify_batch(&items)
107 }
108
109 /// Fails rather than returning anything where the signature does not verify,
110 /// so a caller cannot use the contents by mistake.
111 pub fn open_record<S: Signer>(&self, scheme: &S)
112 -> Outcome<Record>
113 {
114 if !res!(self.verify(scheme)) {
115 return Err(err!(
116 "The envelope's signature does not verify against its enclosed public \
117 key, so its contents are not attributable.";
118 Invalid, Input, Security, Mismatch));
119 }
120 decode_record(&self.payload)
121 }
122
123 /// The signature is not checked, so this is for a caller that has already
124 /// verified or that is inspecting something it does not intend to trust.
125 /// Prefer [`Envelope::open_record`].
126 pub fn peek_record(&self)
127 -> Outcome<Record>
128 {
129 decode_record(&self.payload)
130 }
131
132 /// As [`Envelope::open_record`], but decoding the payload under `lims`.
133 ///
134 /// A payload from an untrusted peer is attacker-controlled input, and the
135 /// unlimited decoder [`Envelope::open_record`] uses will recurse until the
136 /// stack is gone on a deeply nested encoding. A caller reading anything it did
137 /// not author passes limits here; see
138 /// [`Dat::from_bytes_limited`](oxedyne_fe2o3_jdat::Dat::from_bytes_limited).
139 pub fn open_record_limited<S: Signer>(&self, scheme: &S, lims: &DecodeLimits)
140 -> Outcome<Record>
141 {
142 if !res!(self.verify(scheme)) {
143 return Err(err!(
144 "The envelope's signature does not verify against its enclosed public \
145 key, so its contents are not attributable.";
146 Invalid, Input, Security, Mismatch));
147 }
148 decode_record_limited(&self.payload, lims)
149 }
150
151 /// As [`Envelope::peek_record`], but decoding the payload under `lims`, for a
152 /// caller inspecting untrusted bytes it has not verified.
153 pub fn peek_record_limited(&self, lims: &DecodeLimits)
154 -> Outcome<Record>
155 {
156 decode_record_limited(&self.payload, lims)
157 }
158
159 pub fn payload(&self) -> &[u8] {
160 &self.payload
161 }
162
163 pub fn signer(&self) -> &[u8] {
164 &self.signer
165 }
166
167 pub fn signature(&self) -> &[u8] {
168 &self.sig
169 }
170
171 /// The shape is `[payload, signer, signature]`, all three [`Dat::BU64`]:
172 /// keys and signatures readily exceed the 255 bytes a [`Dat::BU8`] length
173 /// field can express, and a truncated length there would corrupt silently.
174 pub fn to_dat(&self) -> Dat {
175 Dat::List(vec![
176 Dat::BU64(self.payload.clone()),
177 Dat::BU64(self.signer.clone()),
178 Dat::BU64(self.sig.clone()),
179 ])
180 }
181
182 pub fn from_dat(dat: &Dat)
183 -> Outcome<Self>
184 {
185 let v = match dat {
186 Dat::List(v) if v.len() == 3 => v,
187 _ => return Err(err!(
188 "An Envelope expects a 3-element Dat::List, got {:?}.", dat;
189 Decode, Input, Mismatch)),
190 };
191 Ok(Self {
192 payload: res!(field_bytes(&v[0], "payload")),
193 signer: res!(field_bytes(&v[1], "signer key")),
194 sig: res!(field_bytes(&v[2], "signature")),
195 })
196 }
197
198 pub fn encode_into(&self, buf: &mut Vec<u8>)
199 -> Outcome<()>
200 {
201 let body = res!(self.to_dat().to_bytes(Vec::new()));
202 buf.extend_from_slice(&body);
203 Ok(())
204 }
205
206 pub fn encode(&self)
207 -> Outcome<Vec<u8>>
208 {
209 let mut buf = Vec::new();
210 res!(self.encode_into(&mut buf));
211 Ok(buf)
212 }
213
214 /// Reads from the front of `buf`, so the count is the bytes consumed and the
215 /// rest is the caller's.
216 pub fn decode(buf: &[u8])
217 -> Outcome<(Self, usize)>
218 {
219 let (dat, used) = res!(Dat::from_bytes(buf));
220 Ok((res!(Self::from_dat(&dat)), used))
221 }
222
223 /// As [`Envelope::decode`], but decoding under `lims`, for a caller reading an
224 /// envelope off the wire from an untrusted peer rather than one it wrote.
225 pub fn decode_limited(buf: &[u8], lims: &DecodeLimits)
226 -> Outcome<(Self, usize)>
227 {
228 let (dat, used) = res!(Dat::from_bytes_limited(buf, lims));
229 Ok((res!(Self::from_dat(&dat)), used))
230 }
231}
232
233
234fn field_bytes(dat: &Dat, what: &str)
235 -> Outcome<Vec<u8>>
236{
237 match dat {
238 Dat::BU64(b) => Ok(b.clone()),
239 other => Err(err!(
240 "An Envelope {} expects Dat::BU64, got {:?}.", what, other;
241 Decode, Input, Mismatch)),
242 }
243}
244
245/// The whole payload must decode, so trailing bytes are a fault and not slack.
246fn decode_record(buf: &[u8])
247 -> Outcome<Record>
248{
249 decode_record_limited(buf, &DecodeLimits::UNLIMITED)
250}
251
252/// As [`decode_record`], but bounding the decode with `lims`, so a payload from
253/// an untrusted peer cannot nest deeply enough to exhaust the stack. The whole
254/// payload must still decode, trailing bytes being a fault and not slack.
255fn decode_record_limited(buf: &[u8], lims: &DecodeLimits)
256 -> Outcome<Record>
257{
258 let (dat, used) = res!(Dat::from_bytes_limited(buf, lims));
259 if used != buf.len() {
260 return Err(err!(
261 "An envelope payload of {} bytes decoded from only {} of them.",
262 buf.len(), used;
263 Decode, Input, Mismatch));
264 }
265 Record::from_dat(&dat)
266}
267
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272 use crate::id::{
273 Anchor,
274 ContentRange,
275 OpId,
276 ReplicaId,
277 };
278 use crate::op::{
279 Header,
280 Op,
281 };
282 use crate::test_support::StubSigner;
283
284 use oxedyne_fe2o3_iop_crypto::keys::KeyManager;
285
286 fn oid(replica: u64, counter: u64) -> OpId {
287 OpId::new(ReplicaId::new(replica), counter)
288 }
289
290 fn sample_op() -> Outcome<Op> {
291 Ok(Op::Splice {
292 left: Some(Anchor::origin(oid(1, 1))),
293 right: None,
294 remove: vec![res!(ContentRange::new(oid(1, 1), 12, 15))],
295 insert: vec![0x7e; 900].into(), // beyond what a BU8 length could hold
296 })
297 }
298
299 fn sample_record(id: OpId) -> Outcome<Record> {
300 Ok(Record::new(
301 res!(Header::new(id, vec![oid(1, 1), oid(2, 4)])),
302 res!(sample_op()),
303 ))
304 }
305
306 /// The stand-in's key relation holds, so that a failure below is the
307 /// envelope's doing and not the stub's.
308 #[test]
309 fn stub_key_relation_holds() -> Outcome<()> {
310 let s = StubSigner::with_seed(3);
311 assert_eq!(StubSigner::public_of(&s.sk), s.pk);
312 Ok(())
313 }
314
315 #[test]
316 fn sealed_envelope_verifies() -> Outcome<()> {
317 let s = StubSigner::with_seed(3);
318 let env = res!(Envelope::seal(&s, b"the payload".to_vec()));
319 assert_eq!(env.signer(), &s.pk[..]);
320 assert_eq!(env.payload(), b"the payload");
321 assert!(res!(env.verify(&s)));
322 Ok(())
323 }
324
325 #[test]
326 fn verification_uses_the_enclosed_key() -> Outcome<()> {
327 let author = StubSigner::with_seed(3);
328 let other = StubSigner::with_seed(200);
329 let env = res!(Envelope::seal(&author, b"payload".to_vec()));
330 // A bystander holding entirely different keys still verifies it.
331 assert!(res!(env.verify(&other)));
332 Ok(())
333 }
334
335 #[test]
336 fn a_substituted_key_fails() -> Outcome<()> {
337 let author = StubSigner::with_seed(3);
338 let impostor = StubSigner::with_seed(200);
339 let env = res!(Envelope::seal(&author, b"payload".to_vec()));
340 let forged = Envelope::new(
341 env.payload().to_vec(),
342 impostor.pk.clone(),
343 env.signature().to_vec(),
344 );
345 assert!(!res!(forged.verify(&author)));
346 Ok(())
347 }
348
349 #[test]
350 fn a_tampered_payload_fails() -> Outcome<()> {
351 let s = StubSigner::with_seed(3);
352 let env = res!(Envelope::seal(&s, b"payload".to_vec()));
353 let tampered = Envelope::new(
354 b"paylaod".to_vec(),
355 env.signer().to_vec(),
356 env.signature().to_vec(),
357 );
358 assert!(!res!(tampered.verify(&s)));
359 Ok(())
360 }
361
362 #[test]
363 fn a_tampered_signature_fails() -> Outcome<()> {
364 let s = StubSigner::with_seed(3);
365 let env = res!(Envelope::seal(&s, b"payload".to_vec()));
366 let mut sig = env.signature().to_vec();
367 sig[0] ^= 0xff;
368 let tampered = Envelope::new(env.payload().to_vec(), env.signer().to_vec(), sig);
369 assert!(!res!(tampered.verify(&s)));
370 Ok(())
371 }
372
373 #[test]
374 fn sealing_without_a_public_key_is_refused() -> Outcome<()> {
375 let s = StubSigner::with_seed(3);
376 let keyless = res!(s.clone_with_keys(None, Some(&s.sk)));
377 assert!(Envelope::seal(&keyless, b"payload".to_vec()).is_err());
378 Ok(())
379 }
380
381 #[test]
382 fn a_record_survives_seal_and_open() -> Outcome<()> {
383 let s = StubSigner::with_seed(11);
384 let rec = res!(sample_record(oid(4, 17)));
385 let env = res!(Envelope::seal_record(&s, &rec));
386 assert_eq!(res!(env.open_record(&s)), rec);
387 Ok(())
388 }
389
390 #[test]
391 fn the_identifier_is_covered_by_the_signature() -> Outcome<()> {
392 let s = StubSigner::with_seed(11);
393 let rec = res!(sample_record(oid(4, 17)));
394 let env = res!(Envelope::seal_record(&s, &rec));
395 let relabelled = res!(sample_record(oid(4, 18)));
396 let forged = Envelope::new(
397 res!(relabelled.to_dat().to_bytes(Vec::new())),
398 env.signer().to_vec(),
399 env.signature().to_vec(),
400 );
401 assert!(!res!(forged.verify(&s)));
402 Ok(())
403 }
404
405 /// So a causal claim cannot be forged from a genuine edit.
406 #[test]
407 fn the_parents_are_covered_by_the_signature() -> Outcome<()> {
408 let s = StubSigner::with_seed(11);
409 let rec = res!(sample_record(oid(4, 17)));
410 let env = res!(Envelope::seal_record(&s, &rec));
411 let reparented = Record::new(
412 res!(Header::new(oid(4, 17), vec![oid(1, 1)])),
413 rec.op.clone(),
414 );
415 let forged = Envelope::new(
416 res!(reparented.to_dat().to_bytes(Vec::new())),
417 env.signer().to_vec(),
418 env.signature().to_vec(),
419 );
420 assert!(!res!(forged.verify(&s)));
421 Ok(())
422 }
423
424 #[test]
425 fn open_refuses_an_unverified_envelope() -> Outcome<()> {
426 let s = StubSigner::with_seed(11);
427 let rec = res!(sample_record(oid(5, 1)));
428 let env = res!(Envelope::seal_record(&s, &rec));
429 let mut sig = env.signature().to_vec();
430 sig[3] ^= 0x01;
431 let forged = Envelope::new(env.payload().to_vec(), env.signer().to_vec(), sig);
432 assert!(forged.open_record(&s).is_err());
433 // Peeking still works, for a caller that knows it is not trusting the result.
434 assert_eq!(res!(forged.peek_record()).id(), rec.id());
435 Ok(())
436 }
437
438 #[test]
439 fn a_batch_of_sound_envelopes_holds() -> Outcome<()> {
440 let a = StubSigner::with_seed(3);
441 let b = StubSigner::with_seed(200);
442 let envs = vec![
443 res!(Envelope::seal_record(&a, &res!(sample_record(oid(4, 1))))),
444 res!(Envelope::seal_record(&b, &res!(sample_record(oid(2, 1))))),
445 res!(Envelope::seal_record(&a, &res!(sample_record(oid(4, 2))))),
446 ];
447 let refs: Vec<&Envelope> = envs.iter().collect();
448 assert!(res!(Envelope::verify_all(&a, &refs)));
449 // The scheme supplies the algorithm only, so a bystander's copy agrees.
450 assert!(res!(Envelope::verify_all(&b, &refs)));
451 assert!(res!(Envelope::verify_all(&a, &[])), "an empty batch holds vacuously");
452 Ok(())
453 }
454
455 /// The property the whole batch arrangement rests on: the batch is allowed to
456 /// be silent about which member failed only because the fallback is
457 /// guaranteed to find it. A batch that failed while every member passed
458 /// singly would leave a caller with nothing to report.
459 #[test]
460 fn a_bad_envelope_fails_the_batch_and_is_found_singly() -> Outcome<()> {
461 let s = StubSigner::with_seed(3);
462 for spoiled in 0..3 {
463 let mut envs = Vec::new();
464 for i in 0..3u64 {
465 let env = res!(Envelope::seal_record(&s, &res!(sample_record(oid(3, i + 1)))));
466 envs.push(if i == spoiled {
467 let mut sig = env.signature().to_vec();
468 sig[0] ^= 0xff;
469 Envelope::new(env.payload().to_vec(), env.signer().to_vec(), sig)
470 } else {
471 env
472 });
473 }
474 let refs: Vec<&Envelope> = envs.iter().collect();
475 assert!(!res!(Envelope::verify_all(&s, &refs)),
476 "the batch holding a spoiled envelope at {} was accepted", spoiled);
477 // The fallback finds it, and finds only it.
478 let mut bad = Vec::new();
479 for (i, env) in envs.iter().enumerate() {
480 if !res!(env.verify(&s)) {
481 bad.push(i);
482 }
483 }
484 assert_eq!(bad, vec![spoiled as usize]);
485 }
486 Ok(())
487 }
488
489 #[test]
490 fn envelope_dat_round_trip() -> Outcome<()> {
491 let s = StubSigner::with_seed(11);
492 let env = res!(Envelope::seal_record(&s, &res!(sample_record(oid(2, 5)))));
493 let back = res!(Envelope::from_dat(&env.to_dat()));
494 assert_eq!(env, back);
495 assert!(res!(back.verify(&s)));
496 Ok(())
497 }
498
499 /// Including a payload longer than a single byte length field could express.
500 #[test]
501 fn envelope_byte_round_trip() -> Outcome<()> {
502 let s = StubSigner::with_seed(11);
503 let env = res!(Envelope::seal_record(&s, &res!(sample_record(oid(2, 5)))));
504 assert!(env.payload().len() > 255);
505 let buf = res!(env.encode());
506 let (back, used) = res!(Envelope::decode(&buf));
507 assert_eq!(used, buf.len());
508 assert_eq!(env, back);
509 assert!(res!(back.verify(&s)));
510 Ok(())
511 }
512
513 #[test]
514 fn envelope_from_dat_rejects_rubbish() -> Outcome<()> {
515 assert!(Envelope::from_dat(&Dat::U64(1)).is_err());
516 assert!(Envelope::from_dat(&Dat::List(vec![
517 Dat::BU64(vec![1]),
518 Dat::BU64(vec![2]),
519 ])).is_err());
520 assert!(Envelope::from_dat(&Dat::List(vec![
521 Dat::BU64(vec![1]),
522 Dat::Str(fmt!("key")),
523 Dat::BU64(vec![3]),
524 ])).is_err());
525 Ok(())
526 }
527
528 #[test]
529 fn a_payload_that_is_not_a_record_is_refused() -> Outcome<()> {
530 let s = StubSigner::with_seed(11);
531 let env = res!(Envelope::seal(&s, b"not a daticle at all".to_vec()));
532 assert!(env.peek_record().is_err());
533 assert!(env.open_record(&s).is_err());
534 Ok(())
535 }
536
537 /// A validly-signed payload that nests past the decode limit is refused rather
538 /// than recursed into, so a hostile encoding from a peer cannot exhaust the
539 /// stack. The signature holds -- the point is that the decode terminates.
540 #[test]
541 fn a_deeply_nested_payload_is_refused_not_recursed() -> Outcome<()> {
542 use oxedyne_fe2o3_jdat::bdat::DecodeLimits;
543
544 // A thousand levels of nesting, far beyond the default depth of 64. The bytes are built from
545 // the inside out rather than encoded, because the encoder recurses exactly as the decoder does
546 // and would overflow building the bomb -- which an attacker is under no obligation to use.
547 let mut payload = vec![Dat::EMPTY_CODE];
548 for _ in 0..1_000 {
549 let payload_len = payload.len();
550 let mut outer = vec![Dat::LIST_CODE];
551 outer = res!(Dat::C64(payload_len as u64).to_bytes(outer));
552 outer.append(&mut payload);
553 payload = outer;
554 }
555 let s = StubSigner::with_seed(11);
556 let env = res!(Envelope::seal(&s, payload));
557 // It verifies, so what follows is the decode's doing and not the signature's.
558 assert!(res!(env.verify(&s)));
559 let lims = DecodeLimits::default();
560 assert!(env.peek_record_limited(&lims).is_err(),
561 "a payload nested past the depth limit must be refused");
562 assert!(env.open_record_limited(&s, &lims).is_err(),
563 "opening a payload nested past the depth limit must be refused");
564 Ok(())
565 }
566}