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 | |
| 17 | use crate::op::Record; |
| 18 | |
| 19 | use oxedyne_fe2o3_core::prelude::*; |
| 20 | use oxedyne_fe2o3_iop_crypto::sign::{ |
| 21 | BatchItem, |
| 22 | Signer, |
| 23 | }; |
| 24 | use 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)] |
| 37 | pub 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 | |
| 43 | impl 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 | |
| 234 | fn 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. |
| 246 | fn 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. |
| 255 | fn 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)] |
| 270 | mod 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 | } |