31.5 KiB, 30 runs
created by r2848102244:155, 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 | //! Key material, and what a signature is worth once it checks out. |
| 2 | //! |
| 3 | //! The engine holds the cryptography: [`Envelope`] seals a whole record -- |
| 4 | //! the operation, the identifier that names it and the parents that place it -- |
| 5 | //! so an operation lifted out, relabelled or re-parented does not verify. What |
| 6 | //! the engine does not do is choose an algorithm, keep a key or decide who is |
| 7 | //! trusted. Those three are this module's business. |
| 8 | //! |
| 9 | //! # The scheme |
| 10 | //! |
| 11 | //! Ed25519, from `oxedyne_fe2o3_crypto`. It is pure Rust, so the tool builds |
| 12 | //! wherever Rust does and not only where a C toolchain does; its keys are 32 |
| 13 | //! bytes and its signatures 64, which is small enough that sealing every |
| 14 | //! operation costs little beside the operation itself; and it is the scheme the |
| 15 | //! rest of Hematite reaches for first. The name is written into `.ore/key`, so |
| 16 | //! a repository says which scheme its key belongs to rather than leaving a |
| 17 | //! reader to infer it from a length. |
| 18 | //! |
| 19 | //! # The key file is not encrypted |
| 20 | //! |
| 21 | //! `.ore/key` holds the secret key in the clear. Anyone who can read the file |
| 22 | //! can author operations that verify as this replica, so the file is written |
| 23 | //! with mode 0600 and says as much in its own comment field. Passphrase |
| 24 | //! protection is future work; until it exists, `.ore` must not be |
| 25 | //! world-readable and a repository must not be copied anywhere its owner would |
| 26 | //! not put a private key. |
| 27 | //! |
| 28 | //! # What trust means here, stated plainly |
| 29 | //! |
| 30 | //! There is no certificate authority, no web of trust and no revocation. A |
| 31 | //! public key becomes known one of two ways: this repository minted it, or a |
| 32 | //! sync with another repository handed it over and it was written down. That is |
| 33 | //! trust on first use and nothing more. It gives one property, which is worth |
| 34 | //! having: once a key is known, everything signed by it is attributable and |
| 35 | //! nothing signed by anyone else can be passed off as its work. It gives no |
| 36 | //! protection against a first meeting with an impostor. |
| 37 | //! |
| 38 | //! A binding records which replica published a key, not which replica each |
| 39 | //! operation it signed was labelled with. The two differ where a repository |
| 40 | //! authors operations on somebody else's behalf, which is exactly what `ore |
| 41 | //! import` does: a git history's operations carry each author's replica and are |
| 42 | //! sealed by the key of whoever ran the import, because that person is who |
| 43 | //! actually vouches for the bytes. |
| 44 | //! |
| 45 | //! # A binding certifies itself |
| 46 | //! |
| 47 | //! A binding carries a signature over its own statement, made by the very key it |
| 48 | //! binds. That matters the moment a key travels by a route neither end wrote: |
| 49 | //! through a relay, a puller receives operations sealed by keys it has never |
| 50 | //! seen, and the natural source of the binding would be the relay -- which would |
| 51 | //! quietly make the relay a trusted introducer. A self-certified binding cannot |
| 52 | //! be minted by a carrier, because a fabricated one fails its own signature, so |
| 53 | //! the worst a carrier can do is withhold one and leave the operations it would |
| 54 | //! have explained marked [`Prov::Unknown`]. |
| 55 | //! |
| 56 | //! What self-certification does not fix is the trust-on-first-use risk above: a |
| 57 | //! key that signs its own claim to be replica 7 proves control of the key and |
| 58 | //! nothing about the person holding it. |
| 59 | |
| 60 | use crate::store::Keep; |
| 61 | |
| 62 | use oxedyne_fe2o3_core::prelude::*; |
| 63 | use oxedyne_fe2o3_crypto::sign::SignatureScheme; |
| 64 | use oxedyne_fe2o3_iop_crypto::keys::KeyManager; |
| 65 | use oxedyne_fe2o3_iop_crypto::sign::Signer; |
| 66 | use oxedyne_fe2o3_jdat::prelude::*; |
| 67 | use oxedyne_fe2o3_ore::envelope::Envelope; |
| 68 | use oxedyne_fe2o3_ore::id::{ |
| 69 | OpId, |
| 70 | ReplicaId, |
| 71 | }; |
| 72 | use oxedyne_fe2o3_ore::op::Record; |
| 73 | use oxedyne_fe2o3_ore::segment::Entry; |
| 74 | use oxedyne_fe2o3_text::base2x::HEMATITE64; |
| 75 | |
| 76 | use std::collections::BTreeMap; |
| 77 | use std::fs; |
| 78 | use std::path::{ |
| 79 | Path, |
| 80 | PathBuf, |
| 81 | }; |
| 82 | |
| 83 | |
| 84 | /// Name of the key file, within the `.ore` directory. |
| 85 | pub const KEY_FILE: &str = "key"; |
| 86 | |
| 87 | /// Version of the key file this tool writes. |
| 88 | pub const KEY_VERSION: u64 = 1; |
| 89 | |
| 90 | /// The name the key file records the signature scheme under. |
| 91 | pub const SCHEME: &str = "Ed25519"; |
| 92 | |
| 93 | /// What the key file says about itself, so that a person who opens it is told |
| 94 | /// what they are holding. |
| 95 | pub const KEY_COMMENT: &str = "This secret key is NOT encrypted. Anyone who can read this file \ |
| 96 | can author operations that verify as this replica, so the file is written readable only by \ |
| 97 | its owner (mode 0600) and .ore must not be made world readable. Passphrase protection is \ |
| 98 | future work."; |
| 99 | |
| 100 | /// The legend that goes with the provenance marks, written once per listing. |
| 101 | pub const LEGEND: &str = "provenance + signed and verified, ? signed by a key this repository \ |
| 102 | does not know, - unsigned"; |
| 103 | |
| 104 | |
| 105 | /// Writes bytes as text, in the encoding the rest of Hematite writes bytes in. |
| 106 | pub fn text_of(bytes: &[u8]) -> String { |
| 107 | HEMATITE64.to_string(bytes) |
| 108 | } |
| 109 | |
| 110 | /// Reads bytes back from [`text_of`]. |
| 111 | pub fn bytes_of(text: &str) |
| 112 | -> Outcome<Vec<u8>> |
| 113 | { |
| 114 | HEMATITE64.from_str(text) |
| 115 | } |
| 116 | |
| 117 | |
| 118 | /// What is known about the provenance of one operation. |
| 119 | /// |
| 120 | /// The three are exhaustive and none of them is "refused": an entry whose |
| 121 | /// signature does not check out never reaches a value of this type, because |
| 122 | /// [`check`] fails instead. |
| 123 | #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] |
| 124 | pub enum Prov { |
| 125 | /// Sealed, the signature checks out, and the key is one this repository |
| 126 | /// knows. |
| 127 | Verified, |
| 128 | /// Sealed and the signature checks out, but nobody here has seen the key |
| 129 | /// before. The operation is attributable to whoever holds that key, and this |
| 130 | /// repository cannot say who that is. |
| 131 | Unknown, |
| 132 | /// Written down bare, with no provenance to check. |
| 133 | Bare, |
| 134 | } |
| 135 | |
| 136 | impl Prov { |
| 137 | /// Returns the one character that stands for the state in a listing. |
| 138 | pub fn mark(&self) -> char { |
| 139 | match self { |
| 140 | Self::Verified => '+', |
| 141 | Self::Unknown => '?', |
| 142 | Self::Bare => '-', |
| 143 | } |
| 144 | } |
| 145 | } |
| 146 | |
| 147 | |
| 148 | /// What a key signs to say which replica it belongs to. |
| 149 | /// |
| 150 | /// Part of the format: a change here invalidates every binding already written |
| 151 | /// down, so the tag carries a version of its own. |
| 152 | pub const BIND_TAG: &str = "ORE-BIND-1"; |
| 153 | |
| 154 | /// Returns the bytes a key signs to bind itself to a replica. |
| 155 | /// |
| 156 | /// The replica is written in decimal and the key follows its own line feed, so |
| 157 | /// no run of bytes can be read as two different statements. |
| 158 | pub fn bind_statement(replica: u64, public: &[u8]) -> Vec<u8> { |
| 159 | let mut out = fmt!("{}\n{}\n", BIND_TAG, replica).into_bytes(); |
| 160 | out.extend_from_slice(public); |
| 161 | out |
| 162 | } |
| 163 | |
| 164 | |
| 165 | /// One public key, the replica that published it, and that key's own word for |
| 166 | /// it. |
| 167 | #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] |
| 168 | pub struct Binding { |
| 169 | /// The replica that minted the key. |
| 170 | pub replica: u64, |
| 171 | /// The public key, as the scheme encodes it. |
| 172 | pub public: Vec<u8>, |
| 173 | /// The signature over [`bind_statement`], made by the secret half of |
| 174 | /// `public`. Absent in a binding written before bindings certified |
| 175 | /// themselves, and in one a carrier stripped. |
| 176 | pub sig: Option<Vec<u8>>, |
| 177 | } |
| 178 | |
| 179 | impl Binding { |
| 180 | |
| 181 | /// Reports whether the binding is signed by the key it binds. |
| 182 | /// |
| 183 | /// A binding with no signature is not a failure -- it is what a repository |
| 184 | /// made before this existed holds, and it still names a key this repository |
| 185 | /// will accept a signature from. It is simply not something to pass on to a |
| 186 | /// stranger, since nothing in it proves the replica did not misspeak. |
| 187 | pub fn is_certified(&self) -> bool { |
| 188 | match &self.sig { |
| 189 | None => false, |
| 190 | Some(sig) => { |
| 191 | let scheme = match algorithm().clone_with_keys(Some(&self.public), None) { |
| 192 | Ok(s) => s, |
| 193 | Err(_) => return false, |
| 194 | }; |
| 195 | match scheme.verify(&bind_statement(self.replica, &self.public), sig) { |
| 196 | Ok(ok) => ok, |
| 197 | Err(_) => false, |
| 198 | } |
| 199 | }, |
| 200 | } |
| 201 | } |
| 202 | |
| 203 | /// Serialises the binding to a [`Dat`]. The shape is `[replica, key]`, with |
| 204 | /// the self-signature appended where there is one. |
| 205 | pub fn to_dat(&self) -> Dat { |
| 206 | let mut v = vec![ |
| 207 | Dat::U64(self.replica), |
| 208 | Dat::Str(text_of(&self.public)), |
| 209 | ]; |
| 210 | if let Some(sig) = &self.sig { |
| 211 | v.push(Dat::Str(text_of(sig))); |
| 212 | } |
| 213 | Dat::List(v) |
| 214 | } |
| 215 | |
| 216 | /// Reconstructs a binding from a [`Dat`]. |
| 217 | pub fn from_dat(dat: &Dat) |
| 218 | -> Outcome<Self> |
| 219 | { |
| 220 | let v = match dat { |
| 221 | Dat::List(v) if v.len() == 2 || v.len() == 3 => v, |
| 222 | other => return Err(err!( |
| 223 | "A key binding expects a replica and a key, and may carry the key's \ |
| 224 | signature over both; got {:?}.", other; |
| 225 | Decode, Input, Mismatch)), |
| 226 | }; |
| 227 | let replica = match &v[0] { |
| 228 | Dat::U64(n) => *n, |
| 229 | Dat::U32(n) => *n as u64, |
| 230 | other => return Err(err!( |
| 231 | "A key binding's replica expects a number, got {:?}.", other; |
| 232 | Decode, Input, Mismatch)), |
| 233 | }; |
| 234 | let public = match &v[1] { |
| 235 | Dat::Str(s) => res!(bytes_of(s)), |
| 236 | other => return Err(err!( |
| 237 | "A key binding's public key expects a string, got {:?}.", other; |
| 238 | Decode, Input, Mismatch)), |
| 239 | }; |
| 240 | let sig = match v.get(2) { |
| 241 | None => None, |
| 242 | Some(Dat::Str(s)) => Some(res!(bytes_of(s))), |
| 243 | Some(other) => return Err(err!( |
| 244 | "A key binding's signature expects a string, got {:?}.", other; |
| 245 | Decode, Input, Mismatch)), |
| 246 | }; |
| 247 | Ok(Self { replica, public, sig }) |
| 248 | } |
| 249 | } |
| 250 | |
| 251 | |
| 252 | /// Every public key this repository will accept a signature from. |
| 253 | /// |
| 254 | /// It is a set of keys and not a mapping from replica to key, because that is |
| 255 | /// what verification actually asks: an envelope carries the key it was signed |
| 256 | /// with, and the question is whether this repository has seen that key before. |
| 257 | /// Which replica published it is kept for the account a person reads, not for |
| 258 | /// the check. |
| 259 | #[derive(Clone, Debug, Default)] |
| 260 | pub struct Trust { |
| 261 | /// Public key to the replica that published it. |
| 262 | known: BTreeMap<Vec<u8>, u64>, |
| 263 | } |
| 264 | |
| 265 | impl Trust { |
| 266 | |
| 267 | /// Builds the trust set from the bindings a configuration holds. |
| 268 | pub fn of(bindings: &[Binding]) -> Self { |
| 269 | let mut known = BTreeMap::new(); |
| 270 | for b in bindings { |
| 271 | known.insert(b.public.clone(), b.replica); |
| 272 | } |
| 273 | Self { known } |
| 274 | } |
| 275 | |
| 276 | /// Reports whether the key is one this repository knows. |
| 277 | pub fn knows(&self, public: &[u8]) -> bool { |
| 278 | self.known.contains_key(public) |
| 279 | } |
| 280 | } |
| 281 | |
| 282 | |
| 283 | /// Reads a public key from the text form, refusing one the scheme will not take. |
| 284 | /// |
| 285 | /// A typed key that lost a character, or a shell that expanded an empty |
| 286 | /// variable, would otherwise be written down as an owner nobody can ever be. |
| 287 | pub fn public_of(text: &str) |
| 288 | -> Outcome<Vec<u8>> |
| 289 | { |
| 290 | let bytes = res!(bytes_of(text.trim())); |
| 291 | match algorithm().clone_with_keys(Some(&bytes), None) { |
| 292 | Ok(_) => Ok(bytes), |
| 293 | Err(e) => Err(err!(e, |
| 294 | "{:?} is {} bytes, which {} does not accept as a public key.", |
| 295 | text, bytes.len(), SCHEME; |
| 296 | Invalid, Input, Key)), |
| 297 | } |
| 298 | } |
| 299 | |
| 300 | |
| 301 | /// The algorithm alone, holding no keys. |
| 302 | /// |
| 303 | /// [`Envelope::verify`] sets a scheme's own keys aside and uses the one the |
| 304 | /// envelope carries, so what is wanted here is the algorithm and nothing else. |
| 305 | pub fn algorithm() -> SignatureScheme { |
| 306 | SignatureScheme::empty_ed25519() |
| 307 | } |
| 308 | |
| 309 | |
| 310 | /// This replica's key pair, as `.ore/key` holds it. |
| 311 | #[derive(Clone, Debug)] |
| 312 | pub struct Signing { |
| 313 | /// The scheme, holding both keys. |
| 314 | scheme: SignatureScheme, |
| 315 | /// The public key, kept beside the scheme because it is asked for often. |
| 316 | public: Vec<u8>, |
| 317 | /// The replica the key was minted for. |
| 318 | pub replica: ReplicaId, |
| 319 | } |
| 320 | |
| 321 | impl Signing { |
| 322 | |
| 323 | /// Returns the path of the key file within the `.ore` directory `dir`. |
| 324 | pub fn path_of(dir: &Path) -> PathBuf { |
| 325 | dir.join(KEY_FILE) |
| 326 | } |
| 327 | |
| 328 | /// Mints a fresh key pair for a replica. |
| 329 | pub fn mint(replica: ReplicaId) |
| 330 | -> Outcome<Self> |
| 331 | { |
| 332 | let scheme = SignatureScheme::new_ed25519(); |
| 333 | let public = match res!(scheme.get_public_key()) { |
| 334 | Some(pk) => pk.to_vec(), |
| 335 | None => return Err(err!( |
| 336 | "A freshly minted {} key pair has no public key, which cannot happen \ |
| 337 | and means the scheme has changed under this tool.", SCHEME; |
| 338 | Bug, Missing, Key)), |
| 339 | }; |
| 340 | Ok(Self { scheme, public, replica }) |
| 341 | } |
| 342 | |
| 343 | /// Returns the public key in the form the configuration writes it and `ore |
| 344 | /// key` prints it. |
| 345 | pub fn public_text(&self) -> String { |
| 346 | text_of(&self.public) |
| 347 | } |
| 348 | |
| 349 | /// Returns the binding this key adds to a configuration, signed by the key |
| 350 | /// itself so that it can be handed to a stranger. |
| 351 | /// |
| 352 | /// A key that cannot sign its own statement still binds -- the signature is |
| 353 | /// what lets the binding travel, not what makes it true here -- so the |
| 354 | /// failure is dropped rather than raised. |
| 355 | pub fn binding(&self) -> Binding { |
| 356 | let statement = bind_statement(self.replica.inner(), &self.public); |
| 357 | Binding { |
| 358 | replica: self.replica.inner(), |
| 359 | public: self.public.clone(), |
| 360 | sig: self.scheme.sign(&statement).ok(), |
| 361 | } |
| 362 | } |
| 363 | |
| 364 | /// Seals a record, so that what is written down carries who wrote it. |
| 365 | pub fn seal(&self, rec: &Record) |
| 366 | -> Outcome<Envelope> |
| 367 | { |
| 368 | Envelope::seal_record(&self.scheme, rec) |
| 369 | } |
| 370 | |
| 371 | /// Signs arbitrary bytes with this replica's key. |
| 372 | /// |
| 373 | /// For statements that are not operations: a request to a relay, and a |
| 374 | /// binding's word for which replica the key belongs to. An operation is |
| 375 | /// sealed by [`Signing::seal`] instead, which covers the identifier and the |
| 376 | /// parents along with the edit. |
| 377 | pub fn sign(&self, msg: &[u8]) |
| 378 | -> Outcome<Vec<u8>> |
| 379 | { |
| 380 | self.scheme.sign(msg) |
| 381 | } |
| 382 | |
| 383 | /// Returns the public key, as the scheme encodes it. |
| 384 | pub fn public(&self) -> &[u8] { |
| 385 | &self.public |
| 386 | } |
| 387 | |
| 388 | /// Reads the key file, which a repository is not obliged to have. |
| 389 | /// |
| 390 | /// A repository with no key file authors bare records and is none the worse |
| 391 | /// for it, so absence is `None` rather than an error; a file that is there |
| 392 | /// and will not read is an error, because the alternative is quietly |
| 393 | /// authoring unsigned operations in a repository that means to sign. |
| 394 | pub fn read(dir: &Path) |
| 395 | -> Outcome<Option<Self>> |
| 396 | { |
| 397 | let path = Self::path_of(dir); |
| 398 | if !path.is_file() { |
| 399 | return Ok(None); |
| 400 | } |
| 401 | let text = match fs::read_to_string(&path) { |
| 402 | Ok(t) => t, |
| 403 | Err(e) => return Err(err!(e, |
| 404 | "The key {:?} could not be read.", path; |
| 405 | IO, File, Read)), |
| 406 | }; |
| 407 | let dat = match Dat::decode_string(text) { |
| 408 | Ok(d) => d, |
| 409 | Err(e) => return Err(err!(e, |
| 410 | "The key {:?} is not readable JDAT.", path; |
| 411 | Decode, Input)), |
| 412 | }; |
| 413 | let map = match &dat { |
| 414 | Dat::Map(m) => m, |
| 415 | other => return Err(err!( |
| 416 | "The key {:?} expects a map, got {:?}.", path, other; |
| 417 | Decode, Input, Mismatch)), |
| 418 | }; |
| 419 | let version = res!(field_u64(map, "format")); |
| 420 | if version != KEY_VERSION { |
| 421 | return Err(err!( |
| 422 | "The key {:?} declares format version {}, and this tool knows only \ |
| 423 | version {}.", path, version, KEY_VERSION; |
| 424 | Decode, Input, Version, Mismatch)); |
| 425 | } |
| 426 | let named = res!(field_str(map, "scheme")); |
| 427 | if named != SCHEME { |
| 428 | return Err(err!( |
| 429 | "The key {:?} is a {} key, and this tool signs with {}.", |
| 430 | path, named, SCHEME; |
| 431 | Invalid, Input, Mismatch)); |
| 432 | } |
| 433 | let replica = ReplicaId::new(res!(field_u64(map, "replica"))); |
| 434 | let public = res!(bytes_of(&res!(field_str(map, "public")))); |
| 435 | let secret = res!(bytes_of(&res!(field_str(map, "secret")))); |
| 436 | let scheme = match algorithm().clone_with_keys(Some(&public), Some(&secret)) { |
| 437 | Ok(s) => s, |
| 438 | Err(e) => return Err(err!(e, |
| 439 | "The key {:?} holds a {} byte public key and a {} byte secret key, \ |
| 440 | which {} does not accept.", path, public.len(), secret.len(), SCHEME; |
| 441 | Invalid, Input, Key)), |
| 442 | }; |
| 443 | Ok(Some(Self { scheme, public, replica })) |
| 444 | } |
| 445 | |
| 446 | /// Writes the key file, readable only by its owner. |
| 447 | /// |
| 448 | /// The mode is set before anything is written, so the secret never sits on |
| 449 | /// disk under a wider one, and the file is replaced rather than appended to. |
| 450 | pub fn write(&self, dir: &Path) |
| 451 | -> Outcome<PathBuf> |
| 452 | { |
| 453 | let secret = match res!(self.scheme.get_secret_key()) { |
| 454 | Some(sk) => sk.to_vec(), |
| 455 | None => return Err(err!( |
| 456 | "This key pair holds no secret key, so there is nothing to write."; |
| 457 | Missing, Key)), |
| 458 | }; |
| 459 | let mut map = DaticleMap::new(); |
| 460 | map.insert(Dat::Str(fmt!("comment")), Dat::Str(fmt!("{}", KEY_COMMENT))); |
| 461 | map.insert(Dat::Str(fmt!("format")), Dat::U64(KEY_VERSION)); |
| 462 | map.insert(Dat::Str(fmt!("scheme")), Dat::Str(fmt!("{}", SCHEME))); |
| 463 | map.insert(Dat::Str(fmt!("replica")), Dat::U64(self.replica.inner())); |
| 464 | map.insert(Dat::Str(fmt!("public")), Dat::Str(text_of(&self.public))); |
| 465 | map.insert(Dat::Str(fmt!("secret")), Dat::Str(text_of(&secret))); |
| 466 | let text = res!(Dat::Map(map).jdat_to_lines(" ")); |
| 467 | let path = Self::path_of(dir); |
| 468 | res!(write_private(&path, fmt!("{}\n", text).as_bytes())); |
| 469 | Ok(path) |
| 470 | } |
| 471 | } |
| 472 | |
| 473 | |
| 474 | /// Creates or replaces a file that only its owner may read. |
| 475 | /// |
| 476 | /// The permissions are given at creation on a Unix, so there is no window in |
| 477 | /// which the bytes are on disk under the process umask's mode. Elsewhere the |
| 478 | /// file is written as any other and the caller is told, because silently |
| 479 | /// writing a secret world readable is worse than saying so. |
| 480 | pub(crate) fn write_private(path: &Path, bytes: &[u8]) |
| 481 | -> Outcome<()> |
| 482 | { |
| 483 | // A previous key is removed first: opening with create_new is what makes the |
| 484 | // mode apply to a file that did not exist a moment ago. |
| 485 | if path.exists() { |
| 486 | match fs::remove_file(path) { |
| 487 | Ok(()) => (), |
| 488 | Err(e) => return Err(err!(e, |
| 489 | "The key {:?} is there and could not be replaced.", path; |
| 490 | IO, File, Write)), |
| 491 | } |
| 492 | } |
| 493 | let mut opts = fs::OpenOptions::new(); |
| 494 | opts.create_new(true).write(true); |
| 495 | #[cfg(unix)] |
| 496 | { |
| 497 | use std::os::unix::fs::OpenOptionsExt; |
| 498 | opts.mode(0o600); |
| 499 | } |
| 500 | #[cfg(not(unix))] |
| 501 | { |
| 502 | println!("note: {:?} holds an unencrypted secret key and this platform has no \ |
| 503 | file mode for the tool to set; restrict it yourself", path); |
| 504 | } |
| 505 | let mut file = match opts.open(path) { |
| 506 | Ok(f) => f, |
| 507 | Err(e) => return Err(err!(e, |
| 508 | "The key {:?} could not be created.", path; |
| 509 | IO, File, Write)), |
| 510 | }; |
| 511 | use std::io::Write; |
| 512 | match file.write_all(bytes) { |
| 513 | Ok(()) => (), |
| 514 | Err(e) => return Err(err!(e, |
| 515 | "The key {:?} could not be written.", path; |
| 516 | IO, File, Write)), |
| 517 | } |
| 518 | match file.flush() { |
| 519 | Ok(()) => Ok(()), |
| 520 | Err(e) => Err(err!(e, |
| 521 | "The key {:?} could not be flushed.", path; |
| 522 | IO, File, Write)), |
| 523 | } |
| 524 | } |
| 525 | |
| 526 | /// Reads a numeric field of a key file map. |
| 527 | fn field_u64(map: &DaticleMap, key: &str) |
| 528 | -> Outcome<u64> |
| 529 | { |
| 530 | match map.get(&Dat::Str(fmt!("{}", key))) { |
| 531 | Some(Dat::U64(n)) => Ok(*n), |
| 532 | Some(Dat::U32(n)) => Ok(*n as u64), |
| 533 | Some(Dat::U16(n)) => Ok(*n as u64), |
| 534 | Some(Dat::U8(n)) => Ok(*n as u64), |
| 535 | Some(other) => Err(err!( |
| 536 | "The key file field {:?} expects a number, got {:?}.", key, other; |
| 537 | Decode, Input, Mismatch)), |
| 538 | None => Err(err!( |
| 539 | "The key file has no field {:?}.", key; |
| 540 | Decode, Input, Missing)), |
| 541 | } |
| 542 | } |
| 543 | |
| 544 | /// Reads a string field of a key file map. |
| 545 | fn field_str(map: &DaticleMap, key: &str) |
| 546 | -> Outcome<String> |
| 547 | { |
| 548 | match map.get(&Dat::Str(fmt!("{}", key))) { |
| 549 | Some(Dat::Str(s)) => Ok(s.clone()), |
| 550 | Some(other) => Err(err!( |
| 551 | "The key file field {:?} expects a string, got {:?}.", key, other; |
| 552 | Decode, Input, Mismatch)), |
| 553 | None => Err(err!( |
| 554 | "The key file has no field {:?}.", key; |
| 555 | Decode, Input, Missing)), |
| 556 | } |
| 557 | } |
| 558 | |
| 559 | |
| 560 | /// What one entry turned out to be. |
| 561 | pub struct Checked { |
| 562 | pub rec: Record, // the operation it carries |
| 563 | pub prov: Prov, // what is known about who wrote it |
| 564 | pub env: Option<Envelope>, // the seal, where one was kept; see `Keep` |
| 565 | } |
| 566 | |
| 567 | /// Verifies an entry, and says what it is worth. |
| 568 | /// |
| 569 | /// A sealed entry whose signature does not check out is refused here and named, |
| 570 | /// and `where` says where it was found -- a segment file, or the replica that |
| 571 | /// sent it. Nothing about it is placed, dropped or ignored: the caller is told |
| 572 | /// which operation is not what it claims and stops. |
| 573 | /// |
| 574 | /// A signature that checks out under a key nobody here has seen is not a |
| 575 | /// failure. It is attributable to whoever holds that key and this repository |
| 576 | /// cannot say who that is, which is what [`Prov::Unknown`] means and what the |
| 577 | /// `?` in a listing says. |
| 578 | /// |
| 579 | /// A veiled entry is refused too, and this is where a working copy meets one: a |
| 580 | /// veil is put on for a carrier and taken off on arrival, so an entry that is |
| 581 | /// still veiled by the time it is being checked is one nothing here can read. |
| 582 | /// Saying so by name is the point -- the alternative is a repository quietly |
| 583 | /// holding operations it will never be able to render. |
| 584 | pub fn check(entry: &Entry, trust: &Trust, whence: &str) |
| 585 | -> Outcome<Checked> |
| 586 | { |
| 587 | match entry { |
| 588 | Entry::Bare(rec) => Ok(Checked { |
| 589 | rec: rec.clone(), |
| 590 | prov: Prov::Bare, |
| 591 | env: None, |
| 592 | }), |
| 593 | Entry::Veiled(v) => Err(err!( |
| 594 | "The operation {} in {} is veiled, and this repository holds no content \ |
| 595 | key to read it with. A veil comes off where it arrives; one still on here \ |
| 596 | means the key never did. Run `ore key --veil <key>` with the key the \ |
| 597 | repository was veiled under, which is handed over by whoever holds it and \ |
| 598 | never by a relay.", v.head.id(), whence; |
| 599 | Invalid, Input, Missing, Key)), |
| 600 | Entry::Sealed(env) => { |
| 601 | // Peeked first so that a failure can name the operation rather than |
| 602 | // leaving a reader to work out which of them it was. |
| 603 | let rec = res!(peeked(env, whence)); |
| 604 | let scheme = algorithm(); |
| 605 | let ok = match env.verify(&scheme) { |
| 606 | Ok(v) => v, |
| 607 | Err(e) => return Err(err!(e, |
| 608 | "The signature on {} in {} could not be checked: its {} byte public \ |
| 609 | key and {} byte signature are not a {} pair. The operation is \ |
| 610 | refused; it is not what it claims to be.", |
| 611 | rec.id(), whence, env.signer().len(), env.signature().len(), SCHEME; |
| 612 | Invalid, Input, Security, Mismatch)), |
| 613 | }; |
| 614 | if !ok { |
| 615 | return Err(err!( |
| 616 | "The signature on {} in {} does not verify against the public key it \ |
| 617 | carries. The operation has been altered since it was signed, or it \ |
| 618 | was never signed by the holder of that key. It is refused rather \ |
| 619 | than being taken on trust.", rec.id(), whence; |
| 620 | Invalid, Input, Security, Mismatch)); |
| 621 | } |
| 622 | let prov = if trust.knows(env.signer()) { |
| 623 | Prov::Verified |
| 624 | } else { |
| 625 | Prov::Unknown |
| 626 | }; |
| 627 | Ok(Checked { |
| 628 | rec, |
| 629 | prov, |
| 630 | env: Some(env.clone()), |
| 631 | }) |
| 632 | }, |
| 633 | } |
| 634 | } |
| 635 | |
| 636 | |
| 637 | /// Opens a sealed entry's record without checking its signature, naming where it |
| 638 | /// was found if the payload is not a record at all. |
| 639 | /// |
| 640 | /// Shared by [`check`] and [`check_all`] so that the two report a payload that |
| 641 | /// is not a record in the same words, whichever of them met it. |
| 642 | fn peeked(env: &Envelope, whence: &str) |
| 643 | -> Outcome<Record> |
| 644 | { |
| 645 | match env.peek_record() { |
| 646 | Ok(r) => Ok(r), |
| 647 | Err(e) => Err(err!(e, |
| 648 | "A sealed entry in {} carries {} bytes that are not a record.", |
| 649 | whence, env.payload().len(); |
| 650 | Invalid, Input, Decode)), |
| 651 | } |
| 652 | } |
| 653 | |
| 654 | /// Verifies a run of entries together, and says what each is worth. |
| 655 | /// |
| 656 | /// What [`check`] does one entry at a time, for a run of them. The result is the |
| 657 | /// same: every entry in the same order, refusing the whole run and naming the |
| 658 | /// operation if any signature does not hold. |
| 659 | /// |
| 660 | /// # Why the signatures are checked together |
| 661 | /// |
| 662 | /// Ed25519 can check a set of signatures for far less than the sum of the parts, |
| 663 | /// and it need decompress a signer's public key only once however many times |
| 664 | /// that signer appears. Replaying a repository means verifying every operation |
| 665 | /// in it, which is the largest single cost of opening one, so the saving is the |
| 666 | /// whole point of this function existing beside [`check`]. |
| 667 | /// |
| 668 | /// # A failure is still named |
| 669 | /// |
| 670 | /// A batch that does not hold says only that something in it is wrong; it never |
| 671 | /// says what, because it never checked the members separately. So the batch is |
| 672 | /// used to answer "is everything here sound?", and the moment the answer is no |
| 673 | /// -- or the moment the batch could not be attempted at all -- every entry is |
| 674 | /// put to [`check`] one at a time, which names the operation and the file it was |
| 675 | /// found in exactly as it always did. Nothing is dropped and nothing is taken on |
| 676 | /// trust: the run is refused either way, and the only question the fallback |
| 677 | /// answers is which operation to name. |
| 678 | /// |
| 679 | /// `keep` decides whether the envelopes come back with the verdicts. [`check`] |
| 680 | /// has no such argument because it answers about one entry, and one envelope is |
| 681 | /// not what this was costing. |
| 682 | pub fn check_all(entries: &[Entry], trust: &Trust, whence: &str, keep: Keep) |
| 683 | -> Outcome<Vec<Checked>> |
| 684 | { |
| 685 | let scheme = algorithm(); |
| 686 | let sealed: Vec<&Envelope> = entries.iter() |
| 687 | .filter_map(|entry| match entry { |
| 688 | Entry::Sealed(env) => Some(env), |
| 689 | // Nothing to check: the signature is inside the ciphertext, and the |
| 690 | // loop below refuses the entry by name for having got this far. |
| 691 | Entry::Bare(_) => None, |
| 692 | Entry::Veiled(_) => None, |
| 693 | }) |
| 694 | .collect(); |
| 695 | // An error is treated as the batch not holding, rather than raised here, |
| 696 | // because an error raised here could not say which envelope caused it. The |
| 697 | // fallback below meets the same bytes one at a time and reports it properly. |
| 698 | let held = match Envelope::verify_all(&scheme, &sealed) { |
| 699 | Ok(held) => held, |
| 700 | Err(_) => false, |
| 701 | }; |
| 702 | if !held { |
| 703 | for entry in entries { |
| 704 | res!(check(entry, trust, whence)); |
| 705 | } |
| 706 | // Every signature checked out on its own, which cannot follow from a |
| 707 | // batch that did not hold. Something is wrong with the check itself, and |
| 708 | // the entries are refused rather than accepted on the strength of the |
| 709 | // weaker of two disagreeing answers. |
| 710 | return Err(err!( |
| 711 | "The signatures on the {} entries in {} did not hold when they were \ |
| 712 | checked together, and checking them one at a time found nothing wrong \ |
| 713 | with any of them. The two checks disagree, which they cannot, so the \ |
| 714 | entries are refused rather than accepted on the strength of one of them.", |
| 715 | entries.len(), whence; |
| 716 | Bug, Invalid, Security, Mismatch)); |
| 717 | } |
| 718 | attribute_all(entries, trust, whence, keep) |
| 719 | } |
| 720 | |
| 721 | |
| 722 | /// Says what each of a run of entries is worth, checking no signature. |
| 723 | /// |
| 724 | /// The tail of [`check_all`], and it is reachable on its own for one caller and |
| 725 | /// one reason: [`crate::verdict`] records that a run of bytes verified here, |
| 726 | /// under a digest of those exact bytes, and a signature that verified over bytes |
| 727 | /// that have not changed verifies again. Checking it a second time is |
| 728 | /// re-deriving a constant. |
| 729 | /// |
| 730 | /// **An entry may be brought here only where its bytes carry such a verdict.** |
| 731 | /// Everything this returns says the operation is attributable, and nothing in |
| 732 | /// here establishes that; the digest does, at the caller. |
| 733 | /// |
| 734 | /// Nothing else about an entry is taken on trust. A veiled entry is still |
| 735 | /// refused by name, and a signer nobody here has seen is still |
| 736 | /// [`Prov::Unknown`]: neither answer was ever the signature's to give. |
| 737 | pub(crate) fn attribute_all(entries: &[Entry], trust: &Trust, whence: &str, keep: Keep) |
| 738 | -> Outcome<Vec<Checked>> |
| 739 | { |
| 740 | let mut out = Vec::with_capacity(entries.len()); |
| 741 | for entry in entries { |
| 742 | match entry { |
| 743 | Entry::Bare(rec) => out.push(Checked { |
| 744 | rec: rec.clone(), |
| 745 | prov: Prov::Bare, |
| 746 | env: None, |
| 747 | }), |
| 748 | // Named one at a time, in the words `check` uses, so a run holding one |
| 749 | // is refused for the reason it is refused and not for the batch. |
| 750 | Entry::Veiled(_) => { res!(check(entry, trust, whence)); }, |
| 751 | Entry::Sealed(env) => { |
| 752 | let rec = res!(peeked(env, whence)); |
| 753 | let prov = if trust.knows(env.signer()) { |
| 754 | Prov::Verified |
| 755 | } else { |
| 756 | Prov::Unknown |
| 757 | }; |
| 758 | // An envelope holds the whole encoded record a second time, so a |
| 759 | // caller that will drop it is not made a copy of it. This is the |
| 760 | // same `Keep` the replay carries, asked one batch earlier: 47.8 MB |
| 761 | // over a 44,629 operation history, and 2,000 envelopes of it live |
| 762 | // at once inside a batch. |
| 763 | let env = match keep { |
| 764 | Keep::Envelopes => Some(env.clone()), |
| 765 | Keep::Nothing => None, |
| 766 | }; |
| 767 | out.push(Checked { |
| 768 | rec, |
| 769 | prov, |
| 770 | env, |
| 771 | }); |
| 772 | }, |
| 773 | } |
| 774 | } |
| 775 | Ok(out) |
| 776 | } |
| 777 | |
| 778 | |
| 779 | /// What a listing needs to put a mark against an operation. |
| 780 | /// |
| 781 | /// An operation the map does not name is one the log holds and nothing was |
| 782 | /// recorded about, which happens where a record reached the log without passing |
| 783 | /// through a segment; it is reported as bare, because bare is what a reader can |
| 784 | /// safely assume about an operation nothing is known of. |
| 785 | pub fn mark_of(prov: &BTreeMap<OpId, Prov>, id: OpId) -> char { |
| 786 | prov.get(&id).copied().unwrap_or(Prov::Bare).mark() |
| 787 | } |
| 788 | |
| 789 | |
| 790 | #[cfg(test)] |
| 791 | mod tests { |
| 792 | use super::*; |
| 793 | |
| 794 | /// A caller that will drop the envelopes is not made copies of them, and one |
| 795 | /// that will hand them on still gets them. |
| 796 | /// |
| 797 | /// The verdict is the same either way and the seal is the only difference, |
| 798 | /// which is the assertion: `prov` is read on both, so a change that lost the |
| 799 | /// checking rather than the copy would fail here too. |
| 800 | #[test] |
| 801 | fn what_will_be_dropped_is_not_copied() -> Outcome<()> { |
| 802 | use oxedyne_fe2o3_ore::op::{ |
| 803 | Header, |
| 804 | Op, |
| 805 | }; |
| 806 | |
| 807 | let key = res!(Signing::mint(ReplicaId::new(1))); |
| 808 | let trust = Trust::of(&[key.binding()]); |
| 809 | let mut entries = Vec::new(); |
| 810 | let mut parents = Vec::new(); |
| 811 | for i in 1..=4u64 { |
| 812 | let rec = Record::new( |
| 813 | res!(Header::new(OpId::new(ReplicaId::new(1), i), parents.clone())), |
| 814 | Op::Mark { name: fmt!("mark {}", i), body: None, time: None }, |
| 815 | ); |
| 816 | parents = vec![OpId::new(ReplicaId::new(1), i)]; |
| 817 | entries.push(Entry::Sealed(res!(key.seal(&rec)))); |
| 818 | } |
| 819 | |
| 820 | let kept = res!(check_all(&entries, &trust, "a test", Keep::Envelopes)); |
| 821 | assert_eq!(kept.len(), 4); |
| 822 | assert!(kept.iter().all(|c| c.env.is_some()), "a caller that hands them on gets them"); |
| 823 | assert!(kept.iter().all(|c| c.prov == Prov::Verified)); |
| 824 | |
| 825 | let dropped = res!(check_all(&entries, &trust, "a test", Keep::Nothing)); |
| 826 | assert_eq!(dropped.len(), 4); |
| 827 | assert!(dropped.iter().all(|c| c.env.is_none()), "and a caller that will not, does not"); |
| 828 | assert!(dropped.iter().all(|c| c.prov == Prov::Verified), |
| 829 | "while the checking is the same either way"); |
| 830 | let said: Vec<OpId> = dropped.iter().map(|c| c.rec.id()).collect(); |
| 831 | assert_eq!(said, kept.iter().map(|c| c.rec.id()).collect::<Vec<OpId>>(), |
| 832 | "and so are the operations"); |
| 833 | Ok(()) |
| 834 | } |
| 835 | |
| 836 | /// A binding signed by the key it binds says so, and every alteration of it |
| 837 | /// stops saying so. |
| 838 | /// |
| 839 | /// This is what stops a carrier introducing a stranger. A relay that carries |
| 840 | /// bindings can withhold one, and cannot write one: the statement is signed |
| 841 | /// by the secret half of the very key it names, which the carrier does not |
| 842 | /// hold. |
| 843 | #[test] |
| 844 | fn a_binding_certifies_itself_and_nothing_else() -> Outcome<()> { |
| 845 | let key = res!(Signing::mint(ReplicaId::new(7))); |
| 846 | let binding = key.binding(); |
| 847 | assert_eq!(binding.replica, 7); |
| 848 | assert!(binding.is_certified(), "a minted binding is signed by its own key"); |
| 849 | |
| 850 | // Claiming another replica breaks it: the statement covers the number. |
| 851 | let mut lying = binding.clone(); |
| 852 | lying.replica = 8; |
| 853 | assert!(!lying.is_certified(), "a relabelled binding does not certify itself"); |
| 854 | |
| 855 | // So does putting the signature beside another key. |
| 856 | let other = res!(Signing::mint(ReplicaId::new(9))); |
| 857 | let mut swapped = other.binding(); |
| 858 | swapped.sig = binding.sig.clone(); |
| 859 | assert!(!swapped.is_certified(), "a signature does not travel to another key"); |
| 860 | |
| 861 | // And a binding with no signature at all is not one to pass on. |
| 862 | let mut bare = binding.clone(); |
| 863 | bare.sig = None; |
| 864 | assert!(!bare.is_certified()); |
| 865 | |
| 866 | // A tampered signature fails rather than raising. |
| 867 | let mut broken = binding.clone(); |
| 868 | if let Some(sig) = &mut broken.sig { |
| 869 | let last = sig.len() - 1; |
| 870 | sig[last] ^= 0x01; |
| 871 | } |
| 872 | assert!(!broken.is_certified()); |
| 873 | |
| 874 | // It survives the round trip through its file form, signature and all. |
| 875 | let back = res!(Binding::from_dat(&binding.to_dat())); |
| 876 | assert_eq!(back, binding); |
| 877 | assert!(back.is_certified()); |
| 878 | // As does one written before bindings certified themselves. |
| 879 | let back = res!(Binding::from_dat(&bare.to_dat())); |
| 880 | assert_eq!(back, bare); |
| 881 | Ok(()) |
| 882 | } |
| 883 | |
| 884 | /// A public key of the wrong length is refused where it is read, rather than |
| 885 | /// written down as an owner nobody can ever be. |
| 886 | #[test] |
| 887 | fn a_public_key_that_is_not_one_is_refused() -> Outcome<()> { |
| 888 | let key = res!(Signing::mint(ReplicaId::new(1))); |
| 889 | let text = key.public_text(); |
| 890 | assert_eq!(res!(public_of(&text)), key.public().to_vec()); |
| 891 | assert_eq!(res!(public_of(&fmt!(" {} ", text))), key.public().to_vec()); |
| 892 | for bad in ["", "AAAA=2"] { |
| 893 | if public_of(bad).is_ok() { |
| 894 | return Err(err!("The key {:?} was accepted.", bad; Test, Invalid)); |
| 895 | } |
| 896 | } |
| 897 | Ok(()) |
| 898 | } |
| 899 | } |