oxedyne/ore/cli/tests/provenance.rs
22.0 KiB, 3 runs
created by r2848102244:35, 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, driven through the compiled binary. |
| 2 | //! |
| 3 | //! What is being established here is not that Ed25519 works -- that is tested |
| 4 | //! where it is implemented -- but that this tool actually puts the signature in |
| 5 | //! the way of anything that would falsify a history. So the central test is the |
| 6 | //! forgery: a segment rewritten by somebody with write access to the |
| 7 | //! repository, digests repaired and everything consistent except the one thing |
| 8 | //! a forger cannot produce. If that is refused by name, the verification is |
| 9 | //! real. If it is not, everything else in this file is decoration. |
| 10 | |
| 11 | mod support; |
| 12 | |
| 13 | use support::{ |
| 14 | forge_payload, |
| 15 | forget_key, |
| 16 | key_in, |
| 17 | ore, |
| 18 | read_segment, |
| 19 | runs_in, |
| 20 | segment_path, |
| 21 | set_require_signed, |
| 22 | write, |
| 23 | write_segment, |
| 24 | Scratch, |
| 25 | }; |
| 26 | |
| 27 | use oxedyne_fe2o3_core::prelude::*; |
| 28 | use oxedyne_fe2o3_jdat::prelude::*; |
| 29 | use oxedyne_fe2o3_ore::envelope::Envelope; |
| 30 | use oxedyne_fe2o3_ore::op::{ |
| 31 | Header, |
| 32 | Record, |
| 33 | }; |
| 34 | use oxedyne_fe2o3_ore::segment::Entry; |
| 35 | |
| 36 | use std::fs; |
| 37 | |
| 38 | |
| 39 | /// The bytes the tests write first. |
| 40 | const FIRST: &[u8] = b"hello\nworld\n"; |
| 41 | /// What they write second. |
| 42 | const SECOND: &[u8] = b"hello\nbrave\nworld\n"; |
| 43 | |
| 44 | |
| 45 | /// Returns how many operations a `log` listing put in each state. |
| 46 | fn counted(text: &str) |
| 47 | -> Outcome<(usize, usize, usize)> |
| 48 | { |
| 49 | for line in text.lines() { |
| 50 | if !line.contains("signed and verified,") || !line.contains("unsigned") { |
| 51 | continue; |
| 52 | } |
| 53 | if line.contains("provenance") { |
| 54 | // The legend, which names the states rather than counting them. |
| 55 | continue; |
| 56 | } |
| 57 | let fields: Vec<&str> = line.split_whitespace().collect(); |
| 58 | let mut numbers: Vec<usize> = Vec::new(); |
| 59 | for field in &fields { |
| 60 | if let Ok(n) = field.parse::<usize>() { |
| 61 | numbers.push(n); |
| 62 | } |
| 63 | } |
| 64 | if numbers.len() == 3 { |
| 65 | return Ok((numbers[0], numbers[1], numbers[2])); |
| 66 | } |
| 67 | } |
| 68 | Err(err!("No provenance count is in: {}", text; Test, Missing)) |
| 69 | } |
| 70 | |
| 71 | |
| 72 | /// A fresh repository signs by default, and says so at every point a person |
| 73 | /// looks. |
| 74 | #[test] |
| 75 | fn a_fresh_repository_signs_what_it_writes() -> Outcome<()> { |
| 76 | let scratch = res!(Scratch::new("prov_fresh")); |
| 77 | let root = res!(scratch.sub("work")); |
| 78 | |
| 79 | let out = res!(ore(&root, &["init"])); |
| 80 | let text = fmt!("{}", res!(out.good("init"))); |
| 81 | assert!(text.contains("signing key Ed25519"), |
| 82 | "init mints a key and names the scheme: {}", text); |
| 83 | assert!(text.contains("not encrypted"), |
| 84 | "and does not pretend the secret key is protected: {}", text); |
| 85 | let minted = res!(key_in(&text)); |
| 86 | |
| 87 | res!(write(&root, "a.txt", FIRST)); |
| 88 | res!(res!(ore(&root, &["mark", "first"])).good("mark first")); |
| 89 | |
| 90 | let out = res!(ore(&root, &["who", "a.txt"])); |
| 91 | let text = fmt!("{}", res!(out.good("who a.txt"))); |
| 92 | let runs = runs_in(&text); |
| 93 | assert!(!runs.is_empty(), "the file has runs: {}", text); |
| 94 | for run in &runs { |
| 95 | assert_eq!(run.mark, '+', |
| 96 | "every run is signed by a key this repository knows: {}", text); |
| 97 | } |
| 98 | assert!(text.contains("provenance +"), "and the marks carry a legend: {}", text); |
| 99 | |
| 100 | let out = res!(ore(&root, &["log"])); |
| 101 | let text = fmt!("{}", res!(out.good("log"))); |
| 102 | let (signed, unknown, bare) = res!(counted(&text)); |
| 103 | assert!(signed >= 3, "every operation is verified: {}", text); |
| 104 | assert_eq!((unknown, bare), (0, 0), "and none of them is anything else: {}", text); |
| 105 | |
| 106 | // The key the configuration knows is the key the tool announced. |
| 107 | let cfg = res!(fs::read_to_string(root.join(".ore").join("config"))); |
| 108 | assert!(cfg.contains(&minted), "the configuration holds the minted key: {}", cfg); |
| 109 | |
| 110 | // Every entry on disk is sealed, which is what the marks were reporting. |
| 111 | let (_, entries) = res!(read_segment(&segment_path(&root))); |
| 112 | for entry in &entries { |
| 113 | match entry { |
| 114 | Entry::Sealed(_) => (), |
| 115 | Entry::Bare(rec) => return Err(err!( |
| 116 | "The operation {} was written bare by a repository that holds a key.", |
| 117 | rec.head.id(); Test, Invalid)), |
| 118 | Entry::Veiled(v) => return Err(err!( |
| 119 | "The operation {} was written veiled into a working copy, which veils \ |
| 120 | what it hands to a carrier and nothing else.", v.head.id(); Test, Invalid)), |
| 121 | } |
| 122 | } |
| 123 | Ok(()) |
| 124 | } |
| 125 | |
| 126 | /// The key file is readable by its owner and nobody else. |
| 127 | #[test] |
| 128 | #[cfg(unix)] |
| 129 | fn the_key_file_is_private() -> Outcome<()> { |
| 130 | use std::os::unix::fs::PermissionsExt; |
| 131 | let scratch = res!(Scratch::new("prov_mode")); |
| 132 | let root = res!(scratch.sub("work")); |
| 133 | res!(res!(ore(&root, &["init"])).good("init")); |
| 134 | let path = root.join(".ore").join("key"); |
| 135 | let mode = res!(fs::metadata(&path)).permissions().mode() & 0o777; |
| 136 | assert_eq!(mode, 0o600, "the key file is private: {:o}", mode); |
| 137 | // And it says what it is, in its own words, to whoever opens it. |
| 138 | let text = res!(fs::read_to_string(&path)); |
| 139 | assert!(text.contains("NOT encrypted"), |
| 140 | "the key file says it is unencrypted: {}", text); |
| 141 | Ok(()) |
| 142 | } |
| 143 | |
| 144 | /// A repository that began unsigned gains a key, and the mixed log replays. |
| 145 | #[test] |
| 146 | fn a_key_can_be_minted_later_and_the_mixed_log_replays() -> Outcome<()> { |
| 147 | let scratch = res!(Scratch::new("prov_later")); |
| 148 | let root = res!(scratch.sub("work")); |
| 149 | |
| 150 | let out = res!(ore(&root, &["init", "--unsigned"])); |
| 151 | let text = fmt!("{}", res!(out.good("init --unsigned"))); |
| 152 | assert!(text.contains("no signing key"), "it says what it made: {}", text); |
| 153 | |
| 154 | res!(write(&root, "a.txt", FIRST)); |
| 155 | res!(res!(ore(&root, &["mark", "before"])).good("mark before")); |
| 156 | |
| 157 | let out = res!(ore(&root, &["key"])); |
| 158 | let text = fmt!("{}", res!(out.good("key"))); |
| 159 | assert!(text.contains("minted a signing key"), "the key verb mints one: {}", text); |
| 160 | let first_key = res!(key_in(&text)); |
| 161 | |
| 162 | res!(write(&root, "a.txt", SECOND)); |
| 163 | res!(res!(ore(&root, &["mark", "after"])).good("mark after")); |
| 164 | |
| 165 | // One segment, holding both forms, and it replays. |
| 166 | let (_, entries) = res!(read_segment(&segment_path(&root))); |
| 167 | let bare = entries.iter().filter(|e| matches!(e, Entry::Bare(_))).count(); |
| 168 | let sealed = entries.iter().filter(|e| matches!(e, Entry::Sealed(_))).count(); |
| 169 | assert!(bare > 0 && sealed > 0, |
| 170 | "the one segment holds both forms: {} bare, {} sealed", bare, sealed); |
| 171 | |
| 172 | let out = res!(ore(&root, &["log"])); |
| 173 | let text = fmt!("{}", res!(out.good("log"))); |
| 174 | let (signed, unknown, was_bare) = res!(counted(&text)); |
| 175 | assert_eq!(signed, sealed, "the sealed operations are verified: {}", text); |
| 176 | assert_eq!(was_bare, bare, "and the bare ones are reported as bare: {}", text); |
| 177 | assert_eq!(unknown, 0, "with nothing signed by a stranger: {}", text); |
| 178 | |
| 179 | let out = res!(ore(&root, &["who", "a.txt"])); |
| 180 | let text = fmt!("{}", res!(out.good("who a.txt"))); |
| 181 | let marks: Vec<char> = runs_in(&text).iter().map(|r| r.mark).collect(); |
| 182 | assert!(marks.contains(&'-'), "the old bytes are unsigned: {}", text); |
| 183 | assert!(marks.contains(&'+'), "and the new ones are verified: {}", text); |
| 184 | |
| 185 | // Rotating keeps the old key, so what it signed still verifies. |
| 186 | let out = res!(ore(&root, &["key"])); |
| 187 | let text = fmt!("{}", res!(out.good("key rotate"))); |
| 188 | assert!(text.contains("rotated"), "a second key verb rotates: {}", text); |
| 189 | assert!(text.contains(&first_key), "and says what the old key was: {}", text); |
| 190 | let cfg = res!(fs::read_to_string(root.join(".ore").join("config"))); |
| 191 | assert!(cfg.contains(&first_key), |
| 192 | "the old public key stays in the configuration: {}", cfg); |
| 193 | |
| 194 | let out = res!(ore(&root, &["log"])); |
| 195 | let text = fmt!("{}", res!(out.good("log after rotation"))); |
| 196 | let (still, unknown, _) = res!(counted(&text)); |
| 197 | assert_eq!(still, signed, |
| 198 | "what the old key signed still verifies after a rotation: {}", text); |
| 199 | assert_eq!(unknown, 0, "and nothing became a stranger's: {}", text); |
| 200 | Ok(()) |
| 201 | } |
| 202 | |
| 203 | /// An unsigned repository says so once for the command, not once per operation. |
| 204 | #[test] |
| 205 | fn an_unsigned_repository_says_so_once() -> Outcome<()> { |
| 206 | let scratch = res!(Scratch::new("prov_notice")); |
| 207 | let root = res!(scratch.sub("work")); |
| 208 | res!(res!(ore(&root, &["init", "--unsigned"])).good("init --unsigned")); |
| 209 | res!(write(&root, "a.txt", FIRST)); |
| 210 | res!(write(&root, "b.txt", b"another\n")); |
| 211 | res!(write(&root, "c.txt", b"and another\n")); |
| 212 | |
| 213 | let out = res!(ore(&root, &["mark", "first"])); |
| 214 | let text = fmt!("{}", res!(out.good("mark first"))); |
| 215 | let said = text.lines().filter(|l| l.contains("this repository is unsigned")).count(); |
| 216 | assert_eq!(said, 1, |
| 217 | "the notice is once for the command, whatever it wrote: {}", text); |
| 218 | assert!(text.contains("`ore key`"), "and it says what to do about it: {}", text); |
| 219 | Ok(()) |
| 220 | } |
| 221 | |
| 222 | /// A forged operation is refused, by name, and the repository works again once |
| 223 | /// it is put back. |
| 224 | /// |
| 225 | /// This is the test that proves the verification is real. The segment is |
| 226 | /// rewritten as a well formed one -- every digest recomputed, the framing |
| 227 | /// intact -- so nothing but the signature stands between the forgery and the |
| 228 | /// history. |
| 229 | #[test] |
| 230 | fn a_forged_operation_is_refused_by_name() -> Outcome<()> { |
| 231 | let scratch = res!(Scratch::new("prov_forge")); |
| 232 | let root = res!(scratch.sub("work")); |
| 233 | res!(res!(ore(&root, &["init"])).good("init")); |
| 234 | res!(write(&root, "a.txt", FIRST)); |
| 235 | res!(res!(ore(&root, &["mark", "first"])).good("mark first")); |
| 236 | |
| 237 | let path = segment_path(&root); |
| 238 | let backup = res!(fs::read(&path)); |
| 239 | // The last sealed entry is the mark; the one before it holds the bytes. |
| 240 | let victim = res!(forge_payload(&path, 1)); |
| 241 | |
| 242 | let out = res!(ore(&root, &["log"])); |
| 243 | assert!(!out.ok, "a forged history is refused: {}{}", out.out, out.err); |
| 244 | assert!(out.err.contains(&victim), |
| 245 | "and the refusal names the operation {}: {}", victim, out.err); |
| 246 | assert!(out.err.contains("does not verify"), |
| 247 | "and says what is wrong with it: {}", out.err); |
| 248 | |
| 249 | // Every verb, not merely the one that reads the history out. |
| 250 | for verb in [ |
| 251 | vec!["flags"], |
| 252 | vec!["who", "a.txt"], |
| 253 | vec!["mark", "second"], |
| 254 | vec!["undo"], |
| 255 | ] { |
| 256 | let out = res!(ore(&root, &verb)); |
| 257 | assert!(!out.ok, "`ore {}` is refused too: {}{}", verb.join(" "), out.out, out.err); |
| 258 | assert!(out.err.contains(&victim), |
| 259 | "and names the operation: {}", out.err); |
| 260 | } |
| 261 | |
| 262 | // Put it back, and the repository is a repository again. |
| 263 | res!(fs::write(&path, &backup)); |
| 264 | let out = res!(ore(&root, &["log"])); |
| 265 | let text = fmt!("{}", res!(out.good("log after restoring"))); |
| 266 | let (signed, unknown, bare) = res!(counted(&text)); |
| 267 | assert!(signed >= 3 && unknown == 0 && bare == 0, |
| 268 | "the restored history verifies throughout: {}", text); |
| 269 | Ok(()) |
| 270 | } |
| 271 | |
| 272 | /// A byte flipped and left alone is caught by the digest, before the signature |
| 273 | /// is reached. |
| 274 | /// |
| 275 | /// The two checks answer different questions and are both worth having: the |
| 276 | /// digest catches a damaged file, the signature catches a forged one. This test |
| 277 | /// exists so that a reader knows which is which, and so that the forgery test |
| 278 | /// above cannot pass by accident of the digest. |
| 279 | #[test] |
| 280 | fn a_damaged_segment_is_caught_by_the_digest() -> Outcome<()> { |
| 281 | let scratch = res!(Scratch::new("prov_damage")); |
| 282 | let root = res!(scratch.sub("work")); |
| 283 | res!(res!(ore(&root, &["init"])).good("init")); |
| 284 | res!(write(&root, "a.txt", FIRST)); |
| 285 | res!(res!(ore(&root, &["mark", "first"])).good("mark first")); |
| 286 | |
| 287 | let path = segment_path(&root); |
| 288 | let mut bytes = res!(fs::read(&path)); |
| 289 | let at = bytes.len() / 2; |
| 290 | bytes[at] ^= 0x01; |
| 291 | res!(fs::write(&path, &bytes)); |
| 292 | |
| 293 | let out = res!(ore(&root, &["log"])); |
| 294 | assert!(!out.ok, "a damaged segment is refused: {}{}", out.out, out.err); |
| 295 | assert!(out.err.contains("could not be decoded") || out.err.contains("digest"), |
| 296 | "and the reason is the file rather than the signature: {}", out.err); |
| 297 | Ok(()) |
| 298 | } |
| 299 | |
| 300 | /// Two signing repositories teach each other their keys, and each verifies the |
| 301 | /// other's operations afterwards. |
| 302 | #[test] |
| 303 | fn a_sync_exchanges_keys_and_both_ends_verify() -> Outcome<()> { |
| 304 | let scratch = res!(Scratch::new("prov_sync")); |
| 305 | let here = res!(scratch.sub("here")); |
| 306 | let there = res!(scratch.sub("there")); |
| 307 | |
| 308 | let out = res!(ore(&here, &["init"])); |
| 309 | let here_key = res!(key_in(res!(out.good("init here")))); |
| 310 | let out = res!(ore(&there, &["init"])); |
| 311 | let there_key = res!(key_in(res!(out.good("init there")))); |
| 312 | |
| 313 | res!(write(&here, "a.txt", FIRST)); |
| 314 | res!(res!(ore(&here, &["mark", "mine"])).good("mark mine")); |
| 315 | res!(write(&there, "b.txt", b"theirs\n")); |
| 316 | res!(res!(ore(&there, &["mark", "theirs"])).good("mark theirs")); |
| 317 | |
| 318 | let out = res!(ore(&here, &["sync", &fmt!("{}", there.display())])); |
| 319 | let text = fmt!("{}", res!(out.good("sync"))); |
| 320 | assert!(text.contains("2 known here (1 learned now)"), |
| 321 | "each end learned the other's key: {}", text); |
| 322 | assert!(text.contains("2 known there (1 learned now)"), |
| 323 | "in both directions: {}", text); |
| 324 | |
| 325 | let here_cfg = res!(fs::read_to_string(here.join(".ore").join("config"))); |
| 326 | let there_cfg = res!(fs::read_to_string(there.join(".ore").join("config"))); |
| 327 | assert!(here_cfg.contains(&there_key), "and wrote it down here: {}", here_cfg); |
| 328 | assert!(there_cfg.contains(&here_key), "and there: {}", there_cfg); |
| 329 | |
| 330 | // What crossed is sealed at the far end, not merely checked and forgotten. |
| 331 | let (_, entries) = res!(read_segment(&segment_path(&there))); |
| 332 | for entry in &entries { |
| 333 | if let Entry::Bare(rec) = entry { |
| 334 | return Err(err!( |
| 335 | "The operation {} crossed and was written down bare.", |
| 336 | rec.head.id(); Test, Invalid)); |
| 337 | } |
| 338 | } |
| 339 | |
| 340 | // And each end verifies the other's work, having been taught the key. |
| 341 | for (root, file) in [(&here, "b.txt"), (&there, "a.txt")] { |
| 342 | let out = res!(ore(root, &["who", file])); |
| 343 | let text = fmt!("{}", res!(out.good("who after sync"))); |
| 344 | for run in runs_in(&text) { |
| 345 | assert_eq!(run.mark, '+', |
| 346 | "the other end's bytes verify in {}: {}", root.display(), text); |
| 347 | } |
| 348 | let out = res!(ore(root, &["log"])); |
| 349 | let text = fmt!("{}", res!(out.good("log after sync"))); |
| 350 | let (_, unknown, bare) = res!(counted(&text)); |
| 351 | assert_eq!((unknown, bare), (0, 0), |
| 352 | "and the whole merged history is accounted for: {}", text); |
| 353 | } |
| 354 | Ok(()) |
| 355 | } |
| 356 | |
| 357 | /// An operation whose key this repository has never seen is shown as such, and |
| 358 | /// is not refused. |
| 359 | /// |
| 360 | /// A signature that checks out under an unknown key is a real fact about the |
| 361 | /// operation: somebody holding that key wrote it, and this repository cannot |
| 362 | /// say who. Refusing it would be refusing a history because it came the long |
| 363 | /// way round; accepting it silently would be claiming to know something. |
| 364 | #[test] |
| 365 | fn an_unknown_key_is_marked_rather_than_refused() -> Outcome<()> { |
| 366 | let scratch = res!(Scratch::new("prov_unknown")); |
| 367 | let here = res!(scratch.sub("here")); |
| 368 | let there = res!(scratch.sub("there")); |
| 369 | |
| 370 | let out = res!(ore(&here, &["init"])); |
| 371 | let here_key = res!(key_in(res!(out.good("init here")))); |
| 372 | res!(res!(ore(&there, &["init"])).good("init there")); |
| 373 | res!(write(&here, "a.txt", FIRST)); |
| 374 | res!(res!(ore(&here, &["mark", "mine"])).good("mark mine")); |
| 375 | res!(res!(ore(&here, &["sync", &fmt!("{}", there.display())])).good("sync")); |
| 376 | |
| 377 | // The far end forgets where the history came from, which is the state a |
| 378 | // repository is in when operations reach it by some route that carried no |
| 379 | // keys. |
| 380 | res!(forget_key(&there, &here_key)); |
| 381 | |
| 382 | let out = res!(ore(&there, &["who", "a.txt"])); |
| 383 | let text = fmt!("{}", res!(out.good("who with a forgotten key"))); |
| 384 | let marks: Vec<char> = runs_in(&text).iter().map(|r| r.mark).collect(); |
| 385 | assert!(marks.contains(&'?'), |
| 386 | "the bytes are signed by a key this end no longer knows: {}", text); |
| 387 | |
| 388 | let out = res!(ore(&there, &["log"])); |
| 389 | let text = fmt!("{}", res!(out.good("log with a forgotten key"))); |
| 390 | let (_, unknown, bare) = res!(counted(&text)); |
| 391 | assert!(unknown > 0, "and the log counts them: {}", text); |
| 392 | assert_eq!(bare, 0, "without calling them unsigned: {}", text); |
| 393 | Ok(()) |
| 394 | } |
| 395 | |
| 396 | /// A repository that requires signatures refuses a peer's unsigned operations, |
| 397 | /// and says how many and whose. |
| 398 | #[test] |
| 399 | fn require_signed_refuses_a_bare_peer() -> Outcome<()> { |
| 400 | let scratch = res!(Scratch::new("prov_policy")); |
| 401 | let here = res!(scratch.sub("here")); |
| 402 | let there = res!(scratch.sub("there")); |
| 403 | |
| 404 | res!(res!(ore(&here, &["init"])).good("init here")); |
| 405 | res!(res!(ore(&there, &["init", "--unsigned"])).good("init there")); |
| 406 | res!(write(&here, "a.txt", FIRST)); |
| 407 | res!(res!(ore(&here, &["mark", "mine"])).good("mark mine")); |
| 408 | res!(write(&there, "b.txt", b"unsigned work\n")); |
| 409 | res!(res!(ore(&there, &["mark", "theirs"])).good("mark theirs")); |
| 410 | |
| 411 | // Without the policy the two simply merge, unsigned operations and all. |
| 412 | let out = res!(ore(&here, &["sync", &fmt!("{}", there.display())])); |
| 413 | res!(out.good("sync without the policy")); |
| 414 | let out = res!(ore(&here, &["who", "b.txt"])); |
| 415 | let text = fmt!("{}", res!(out.good("who b.txt"))); |
| 416 | assert!(runs_in(&text).iter().any(|r| r.mark == '-'), |
| 417 | "the peer's unsigned bytes arrived and are marked unsigned: {}", text); |
| 418 | |
| 419 | // With it, a peer that has unsigned work to hand over is refused, before |
| 420 | // anything of theirs is absorbed. |
| 421 | let other = res!(scratch.sub("other")); |
| 422 | res!(res!(ore(&other, &["init", "--unsigned"])).good("init other")); |
| 423 | res!(write(&other, "c.txt", b"more unsigned work\n")); |
| 424 | res!(res!(ore(&other, &["mark", "more"])).good("mark more")); |
| 425 | res!(set_require_signed(&here, true)); |
| 426 | |
| 427 | let out = res!(ore(&here, &["sync", &fmt!("{}", other.display())])); |
| 428 | assert!(!out.ok, "the sync is refused: {}{}", out.out, out.err); |
| 429 | assert!(out.err.contains("requires signed operations"), |
| 430 | "and says why: {}", out.err); |
| 431 | assert!(out.err.contains("are unsigned"), |
| 432 | "and says how many of what: {}", out.err); |
| 433 | assert!(out.err.contains("`ore key`"), |
| 434 | "and says what would fix it: {}", out.err); |
| 435 | // c.txt never reached this working copy. |
| 436 | assert!(!here.join("c.txt").exists(), |
| 437 | "nothing of the refused peer's was absorbed"); |
| 438 | Ok(()) |
| 439 | } |
| 440 | |
| 441 | /// A repository that requires signatures and holds no key says so, and says how |
| 442 | /// to fix it, rather than writing operations it has undertaken not to write. |
| 443 | #[test] |
| 444 | fn require_signed_without_a_key_says_how_to_fix_it() -> Outcome<()> { |
| 445 | let scratch = res!(Scratch::new("prov_keyless")); |
| 446 | let root = res!(scratch.sub("work")); |
| 447 | res!(res!(ore(&root, &["init", "--unsigned"])).good("init --unsigned")); |
| 448 | res!(write(&root, "a.txt", FIRST)); |
| 449 | res!(res!(ore(&root, &["mark", "before"])).good("mark before")); |
| 450 | res!(set_require_signed(&root, true)); |
| 451 | |
| 452 | for verb in [vec!["log"], vec!["flags"], vec!["mark", "second"]] { |
| 453 | let out = res!(ore(&root, &verb)); |
| 454 | assert!(!out.ok, "`ore {}` is refused: {}{}", verb.join(" "), out.out, out.err); |
| 455 | assert!(out.err.contains("requires every operation to be signed"), |
| 456 | "and says what the repository asked for: {}", out.err); |
| 457 | assert!(out.err.contains("`ore key`"), |
| 458 | "and how to give it: {}", out.err); |
| 459 | } |
| 460 | |
| 461 | // The fix works, and is the one the message named. |
| 462 | res!(res!(ore(&root, &["key"])).good("key")); |
| 463 | res!(write(&root, "a.txt", SECOND)); |
| 464 | let out = res!(ore(&root, &["mark", "after"])); |
| 465 | res!(out.good("mark after the fix")); |
| 466 | Ok(()) |
| 467 | } |
| 468 | |
| 469 | /// An operation re-parented into another history does not verify there, |
| 470 | /// because the seal covers the identifier and the parents as well as the edit. |
| 471 | /// |
| 472 | /// The transplant is made deliberately plausible: the operation is given |
| 473 | /// parents the receiving history actually holds, so the causal check that |
| 474 | /// refuses an incomplete batch has nothing to object to and the signature is |
| 475 | /// the only thing left standing between the forgery and the log. That is the |
| 476 | /// property the whole feature rests on -- what is signed is the operation in |
| 477 | /// its place in the history, not merely its bytes. |
| 478 | #[test] |
| 479 | fn a_reparented_operation_does_not_verify() -> Outcome<()> { |
| 480 | let scratch = res!(Scratch::new("prov_lift")); |
| 481 | let here = res!(scratch.sub("here")); |
| 482 | let there = res!(scratch.sub("there")); |
| 483 | res!(res!(ore(&here, &["init"])).good("init here")); |
| 484 | res!(res!(ore(&there, &["init"])).good("init there")); |
| 485 | res!(write(&here, "a.txt", FIRST)); |
| 486 | res!(res!(ore(&here, &["mark", "mine"])).good("mark mine")); |
| 487 | res!(write(&there, "b.txt", b"theirs\n")); |
| 488 | res!(res!(ore(&there, &["mark", "theirs"])).good("mark theirs")); |
| 489 | |
| 490 | // One repository's sealed operation, and the identifier of an operation the |
| 491 | // other one holds to hang it from. |
| 492 | let (_, mine) = res!(read_segment(&segment_path(&here))); |
| 493 | let taken = match mine.iter().rev().find(|e| matches!(e, Entry::Sealed(_))) { |
| 494 | Some(Entry::Sealed(env)) => env.clone(), |
| 495 | _ => return Err(err!("The signing repository wrote nothing sealed."; |
| 496 | Test, Missing)), |
| 497 | }; |
| 498 | let (head, mut theirs) = res!(read_segment(&segment_path(&there))); |
| 499 | let anchor = match theirs.last() { |
| 500 | Some(entry) => res!(entry.id()), |
| 501 | None => return Err(err!("The other repository wrote nothing."; Test, Missing)), |
| 502 | }; |
| 503 | |
| 504 | // The same operation, the same identifier, the same signature, and parents |
| 505 | // the receiving history holds. Everything about it is consistent except who |
| 506 | // signed what. |
| 507 | let lifted = res!(taken.peek_record()); |
| 508 | let reparented = Record::new( |
| 509 | res!(Header::new(lifted.head.id(), vec![anchor])), |
| 510 | lifted.op.clone(), |
| 511 | ); |
| 512 | theirs.push(Entry::Sealed(Envelope::new( |
| 513 | res!(reparented.to_dat().to_bytes(Vec::new())), |
| 514 | taken.signer().to_vec(), |
| 515 | taken.signature().to_vec(), |
| 516 | ))); |
| 517 | res!(write_segment(&segment_path(&there), &head, &theirs)); |
| 518 | |
| 519 | let out = res!(ore(&there, &["log"])); |
| 520 | assert!(!out.ok, "the transplant is refused: {}{}", out.out, out.err); |
| 521 | assert!(out.err.contains("does not verify"), |
| 522 | "and it is the signature that refuses it, the parents being ones this \ |
| 523 | history holds: {}", out.err); |
| 524 | assert!(out.err.contains(&fmt!("{}", lifted.head.id())), |
| 525 | "and the refusal names the operation: {}", out.err); |
| 526 | Ok(()) |
| 527 | } |
| 528 | |
| 529 | /// A sync with a repository whose history has been forged is refused, and |
| 530 | /// nothing of it crosses. |
| 531 | /// |
| 532 | /// The forgery is caught when that repository is opened, which is the first |
| 533 | /// moment anybody reads it. What matters here is that the refusal reaches the |
| 534 | /// sync rather than being confined to the working copy a person is standing in: |
| 535 | /// a peer's history is verified before a single operation of it is absorbed. |
| 536 | #[test] |
| 537 | fn a_sync_with_a_forged_peer_is_refused() -> Outcome<()> { |
| 538 | let scratch = res!(Scratch::new("prov_peer")); |
| 539 | let here = res!(scratch.sub("here")); |
| 540 | let there = res!(scratch.sub("there")); |
| 541 | res!(res!(ore(&here, &["init"])).good("init here")); |
| 542 | res!(res!(ore(&there, &["init"])).good("init there")); |
| 543 | res!(write(&here, "a.txt", FIRST)); |
| 544 | res!(res!(ore(&here, &["mark", "mine"])).good("mark mine")); |
| 545 | res!(write(&there, "b.txt", b"theirs\n")); |
| 546 | res!(res!(ore(&there, &["mark", "theirs"])).good("mark theirs")); |
| 547 | |
| 548 | let victim = res!(forge_payload(&segment_path(&there), 1)); |
| 549 | |
| 550 | let out = res!(ore(&here, &["sync", &fmt!("{}", there.display())])); |
| 551 | assert!(!out.ok, "the sync is refused: {}{}", out.out, out.err); |
| 552 | assert!(out.err.contains(&victim), "and names the operation: {}", out.err); |
| 553 | assert!(!here.join("b.txt").exists(), |
| 554 | "and nothing of that history reached this working copy"); |
| 555 | Ok(()) |
| 556 | } |