oxedyne/fe2o3/fe2o3_sbj/src/doc.rs
56.3 KiB, 66 runs
created by r1870400018:22210, 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 | //! Reading and writing whole documents: the verification order of `SPEC.md` §2. |
| 2 | //! |
| 3 | //! A document is verified before it is parsed, and content that fails is never parsed at all. The |
| 4 | //! header says what the file is, the envelope says what the tree region should hash to and who |
| 5 | //! vouches for that hash, and only a tree region whose bytes hash to what the author signed is |
| 6 | //! handed to a decoder. Steps 1 to 5 touch no content, so a caller may run them and stop, which is |
| 7 | //! what [`verify_only`] is for. |
| 8 | //! |
| 9 | //! [`write`] is the inverse, and refuses to sign what [`read`] would reject: the tree is validated |
| 10 | //! against its schema, encoded canonically (§3), hashed, and the hash signed, before a byte of it |
| 11 | //! reaches a file. |
| 12 | |
| 13 | use crate::{ |
| 14 | canon, |
| 15 | card::Card, |
| 16 | envelope::{ |
| 17 | self, |
| 18 | Envelope, |
| 19 | }, |
| 20 | index, |
| 21 | kinds::Schema, |
| 22 | limit, |
| 23 | post::Post, |
| 24 | share::Share, |
| 25 | validate, |
| 26 | HEADER_LEN, |
| 27 | SCHEMA_CARD, |
| 28 | SCHEMA_POST, |
| 29 | SCHEMA_SHARE, |
| 30 | }; |
| 31 | |
| 32 | use oxedyne_fe2o3_core::prelude::*; |
| 33 | use oxedyne_fe2o3_crypto::sign::SignatureScheme; |
| 34 | use oxedyne_fe2o3_hash::hash::HashScheme; |
| 35 | use oxedyne_fe2o3_iop_crypto::{ |
| 36 | keys::KeyManager, |
| 37 | sign::Signer, |
| 38 | }; |
| 39 | use oxedyne_fe2o3_iop_hash::api::Hasher; |
| 40 | use oxedyne_fe2o3_jdat::prelude::*; |
| 41 | |
| 42 | /// A verified document: holding one *is* holding a document whose header, envelope, hash, signature, |
| 43 | /// canonical encoding and schema all checked out, because [`read`] is the only way to obtain one. |
| 44 | /// |
| 45 | /// The guarantee is the type's, not the caller's. The fields are private and there is no public |
| 46 | /// constructor, so a `Doc` cannot be minted by a cache, a test helper, an application host or a |
| 47 | /// refactor: every one of them must go through [`read`], which verifies. Anything downstream that |
| 48 | /// renders, indexes, stores or attributes a `Doc` may therefore say so without qualification, and |
| 49 | /// nothing here may be weakened without taking that claim away from all of them at once. |
| 50 | /// |
| 51 | /// The fields are read-only, for the same reason. Handing out `&mut` to the tree would let a |
| 52 | /// verified envelope be paired with a tree its author never signed, which is the same hole reached |
| 53 | /// by a different door. |
| 54 | /// |
| 55 | /// A `Doc` is read from a file, and cannot be made any other way: |
| 56 | /// |
| 57 | /// ```no_run |
| 58 | /// use oxedyne_fe2o3_core::prelude::*; |
| 59 | /// |
| 60 | /// fn show(bytes: &[u8]) -> Outcome<()> { |
| 61 | /// let doc = res!(oxedyne_fe2o3_sbj::doc::read(bytes)); // The only route. |
| 62 | /// let _tree = doc.tree(); |
| 63 | /// let _author = &doc.env().author; |
| 64 | /// Ok(()) |
| 65 | /// } |
| 66 | /// ``` |
| 67 | /// |
| 68 | /// An unverified one cannot be assembled out of its parts (E0451, the fields are private): |
| 69 | /// |
| 70 | /// ```compile_fail |
| 71 | /// use oxedyne_fe2o3_jdat::prelude::Dat; |
| 72 | /// use oxedyne_fe2o3_sbj::{doc::Doc, envelope::Envelope}; |
| 73 | /// |
| 74 | /// fn forge(env: Envelope, tree: Dat) -> Doc { |
| 75 | /// Doc { env, tree } |
| 76 | /// } |
| 77 | /// ``` |
| 78 | /// |
| 79 | /// Nor can a verified one have its tree swapped for one nobody signed (E0616, the same): |
| 80 | /// |
| 81 | /// ```compile_fail |
| 82 | /// use oxedyne_fe2o3_jdat::prelude::Dat; |
| 83 | /// use oxedyne_fe2o3_sbj::doc::Doc; |
| 84 | /// |
| 85 | /// fn tamper(doc: &mut Doc, tree: Dat) { |
| 86 | /// doc.tree = tree; |
| 87 | /// } |
| 88 | /// ``` |
| 89 | #[derive(Clone, Debug)] |
| 90 | pub struct Doc { |
| 91 | /// The signed envelope. |
| 92 | env: Envelope, |
| 93 | /// The decoded node tree. |
| 94 | tree: Dat, |
| 95 | } |
| 96 | |
| 97 | impl Doc { |
| 98 | |
| 99 | /// The envelope the author signed: the schema, the author, the schemes, the time, the address. |
| 100 | pub fn env(&self) -> &Envelope { |
| 101 | &self.env |
| 102 | } |
| 103 | |
| 104 | /// The node tree, which hashes to the address the envelope carries. |
| 105 | pub fn tree(&self) -> &Dat { |
| 106 | &self.tree |
| 107 | } |
| 108 | |
| 109 | /// Takes the tree, for a caller that owns the document and wants only what is in it. |
| 110 | pub fn into_tree(self) -> Dat { |
| 111 | self.tree |
| 112 | } |
| 113 | |
| 114 | /// Takes the document apart, for a caller that owns it and wants both halves. |
| 115 | /// |
| 116 | /// The parts carry no guarantee once separated, which is why they are only ever handed out to a |
| 117 | /// caller that already held the whole: an `Envelope` and a `Dat` cannot be made into a `Doc`. |
| 118 | pub fn into_parts(self) -> (Envelope, Dat) { |
| 119 | (self.env, self.tree) |
| 120 | } |
| 121 | } |
| 122 | |
| 123 | /// What the payload region of an artefact holds, and the one place a schema name chooses a |
| 124 | /// validator. |
| 125 | /// |
| 126 | /// The container carries any schema (§1.2), and the schemas it carries are no longer one shape. An |
| 127 | /// oxeweb document is a tree of typed nodes, so it is validated by walking that tree against the |
| 128 | /// vocabulary its schema admits; a post and a card are flat canonical maps whose whole validity is |
| 129 | /// their own field rules, and the node vocabulary has nothing to say about either. Putting them |
| 130 | /// through [`validate::validate`] would mean teaching a tree walker two schemas with no tree in |
| 131 | /// them, and teaching [`Schema`] — whose stated job is to fix a vocabulary of node kinds and a |
| 132 | /// vocabulary of style properties — two members that have neither. |
| 133 | /// |
| 134 | /// An enum instead, so that the dispatch is exhaustive: a sixth schema cannot be added without the |
| 135 | /// compiler naming every place that must learn about it. The variant carries the schema for a tree, |
| 136 | /// where three names share one shape, and fixes it for a post and a card, where the name and the |
| 137 | /// shape are the same fact. |
| 138 | #[derive(Clone, Debug, PartialEq)] |
| 139 | pub enum Payload { |
| 140 | /// An oxeweb node tree, under whichever of the three `oxeweb/*` schemas admits its vocabulary. |
| 141 | Tree { |
| 142 | /// The schema the envelope declares. |
| 143 | schema: Schema, |
| 144 | /// The node tree. |
| 145 | tree: Dat, |
| 146 | }, |
| 147 | /// A `daimond/post/0` message. |
| 148 | Post(Post), |
| 149 | /// A `daimond/card/0` identity card. |
| 150 | Card(Card), |
| 151 | /// A `daimond/share/0` copy of something one person is sending another. |
| 152 | Share(Share), |
| 153 | } |
| 154 | |
| 155 | impl Payload { |
| 156 | |
| 157 | /// The schema name the envelope declares for this payload. |
| 158 | pub fn schema(&self) -> &'static str { |
| 159 | match self { |
| 160 | Self::Tree { schema, .. } => schema.name(), |
| 161 | Self::Post(_) => SCHEMA_POST, |
| 162 | Self::Card(_) => SCHEMA_CARD, |
| 163 | Self::Share(_) => SCHEMA_SHARE, |
| 164 | } |
| 165 | } |
| 166 | |
| 167 | /// Validates this payload against its own rules, then encodes it canonically. |
| 168 | /// |
| 169 | /// Nothing this crate would refuse to read is ever given a signature and an address, which for |
| 170 | /// a tree means the schema walk of §4 and for a record means its own field rules. Each arm |
| 171 | /// validates before it encodes. |
| 172 | pub fn encode(&self) -> Outcome<Vec<u8>> { |
| 173 | let bytes = match self { |
| 174 | Self::Tree { schema, tree } => res!(encode_tree(tree, schema.name())), |
| 175 | Self::Post(p) => res!(p.encode()), |
| 176 | Self::Card(c) => res!(c.encode()), |
| 177 | Self::Share(s) => res!(s.encode()), |
| 178 | }; |
| 179 | // Checked here as well as inside the arms that check it, so that the container's own limit |
| 180 | // is not something a payload kind added later can be written without meeting. |
| 181 | if bytes.len() > limit::TREE_BYTES { |
| 182 | return Err(err!( |
| 183 | "The {} payload encodes to {} bytes, exceeding the limit of {} bytes (SPEC.md §5).", |
| 184 | self.schema(), bytes.len(), limit::TREE_BYTES; |
| 185 | Invalid, Input, TooBig, LimitReached)); |
| 186 | } |
| 187 | Ok(bytes) |
| 188 | } |
| 189 | |
| 190 | /// Decodes and validates a payload region under the schema the envelope declared. |
| 191 | /// |
| 192 | /// The bytes must already have been hashed and the hash found to be the one the author signed, |
| 193 | /// which is why this is not public: the only caller is [`read_artefact`], and reaching it any |
| 194 | /// other way would be parsing content nobody vouched for. |
| 195 | fn decode( |
| 196 | schema: &str, |
| 197 | bytes: &[u8], |
| 198 | ) |
| 199 | -> Outcome<Self> |
| 200 | { |
| 201 | match schema { |
| 202 | SCHEMA_POST => Ok(Self::Post(res!(Post::decode(bytes)))), |
| 203 | SCHEMA_CARD => Ok(Self::Card(res!(Card::decode(bytes)))), |
| 204 | SCHEMA_SHARE => Ok(Self::Share(res!(Share::decode(bytes)))), |
| 205 | other => { |
| 206 | let schema = res!(Schema::from_name(other)); |
| 207 | // `canon::decode` enforces the depth limit as it descends, and checks every rule of |
| 208 | // §3 that survives a decode, so `canon::check` is not repeated here. |
| 209 | let tree = res!(canon::decode(bytes)); |
| 210 | res!(validate::validate(&tree, schema.name())); |
| 211 | Ok(Self::Tree { |
| 212 | schema, |
| 213 | tree, |
| 214 | }) |
| 215 | }, |
| 216 | } |
| 217 | } |
| 218 | } |
| 219 | |
| 220 | /// A verified artefact: holding one *is* holding a file whose header, envelope, hash, signature, |
| 221 | /// canonical encoding and schema all checked out, because [`read_artefact`] is the only way to |
| 222 | /// obtain one. |
| 223 | /// |
| 224 | /// The same guarantee [`Doc`] carries, over the whole set of schemas rather than the three that are |
| 225 | /// node trees, and for the same reason: the fields are private, there is no public constructor, and |
| 226 | /// no `&mut` is handed out, so a verified envelope cannot be paired with a payload its author never |
| 227 | /// signed. |
| 228 | #[derive(Clone, Debug)] |
| 229 | pub struct Artefact { |
| 230 | /// The signed envelope. |
| 231 | env: Envelope, |
| 232 | /// The decoded, validated payload. |
| 233 | payload: Payload, |
| 234 | } |
| 235 | |
| 236 | impl Artefact { |
| 237 | |
| 238 | /// The envelope the author signed: the schema, the author, the schemes, the time, the address. |
| 239 | pub fn env(&self) -> &Envelope { |
| 240 | &self.env |
| 241 | } |
| 242 | |
| 243 | /// The payload, which hashes to the address the envelope carries. |
| 244 | pub fn payload(&self) -> &Payload { |
| 245 | &self.payload |
| 246 | } |
| 247 | |
| 248 | /// Takes the payload, for a caller that owns the artefact and wants only what is in it. |
| 249 | pub fn into_payload(self) -> Payload { |
| 250 | self.payload |
| 251 | } |
| 252 | |
| 253 | /// Takes the artefact apart, for a caller that owns it and wants both halves. |
| 254 | /// |
| 255 | /// The parts carry no guarantee once separated, which is why they are only ever handed out to a |
| 256 | /// caller that already held the whole. |
| 257 | pub fn into_parts(self) -> (Envelope, Payload) { |
| 258 | (self.env, self.payload) |
| 259 | } |
| 260 | } |
| 261 | |
| 262 | /// The regions of a file, located by the header and the envelope but not yet trusted. |
| 263 | /// |
| 264 | /// The tree region is exactly the `tree_len` bytes the envelope declares. Whatever follows it is |
| 265 | /// the optional index of §1.4, which lies outside the hash, is derived from the tree, and is never |
| 266 | /// trusted, so nothing here reads it. |
| 267 | #[derive(Clone, Copy, Debug)] |
| 268 | struct Regions<'a> { |
| 269 | /// The tree region, exactly as long as the envelope declares. |
| 270 | tree: &'a [u8], |
| 271 | /// Whatever trails the tree region: the optional index, or nothing. |
| 272 | rest: &'a [u8], |
| 273 | } |
| 274 | |
| 275 | /// Reads a document: header, envelope, hash, signature, decode, validate, in that order. |
| 276 | /// |
| 277 | /// The tree is decoded only once its bytes have been hashed and the hash found to be the one the |
| 278 | /// author signed. Decoding enforces the depth limit of §5 as it descends, and rejects bytes that |
| 279 | /// are not the canonical encoding of the tree they decode to (§3). The decoded tree is then |
| 280 | /// validated against the schema the envelope declares. |
| 281 | pub fn read(buf: &[u8]) -> Outcome<Doc> { |
| 282 | let (env, tree_bytes) = res!(verify(buf)); |
| 283 | // Steps 6 and 7. `canon::decode` enforces the depth limit during decoding, and checks the tree |
| 284 | // against every rule of §3 that survives a decode, which is what `canon::check` does, so the |
| 285 | // check is not repeated here. |
| 286 | let tree = res!(canon::decode(tree_bytes)); |
| 287 | res!(validate::validate(&tree, &env.schema)); |
| 288 | Ok(Doc { |
| 289 | env, |
| 290 | tree, |
| 291 | }) |
| 292 | } |
| 293 | |
| 294 | /// Verifies a document without decoding its tree: steps 1 to 5 of §2, which touch no content. |
| 295 | pub fn verify_only(buf: &[u8]) -> Outcome<Envelope> { |
| 296 | let (env, _) = res!(verify(buf)); |
| 297 | Ok(env) |
| 298 | } |
| 299 | |
| 300 | /// Verifies a document and returns its envelope and the tree region the envelope vouches for. |
| 301 | /// |
| 302 | /// The bytes returned have been hashed with the scheme the envelope names, found to hash to what |
| 303 | /// the envelope declares, and that declaration found to have been signed by the author. They have |
| 304 | /// not been decoded, and nothing yet knows whether they are a tree at all. |
| 305 | pub fn verify<'a>(buf: &'a [u8]) -> Outcome<(Envelope, &'a [u8])> { |
| 306 | |
| 307 | // Steps 1 to 3. |
| 308 | let (env, regions) = res!(locate(buf)); |
| 309 | |
| 310 | // Step 4: the tree region hashes to what the envelope declares. |
| 311 | let hash = res!(hash_tree(env.hash_scheme, regions.tree)); |
| 312 | if hash != env.hash { |
| 313 | return Err(err!( |
| 314 | "The {} byte tree region hashes to {}, but the envelope declares the hash {}. The \ |
| 315 | hash is the document's address, so a tree that does not hash to it is not this \ |
| 316 | document.", regions.tree.len(), hex(&hash), hex(&env.hash); |
| 317 | Invalid, Input, Mismatch)); |
| 318 | } |
| 319 | |
| 320 | // Step 5: the author signed that hash. The width is checked first, because a signature of the |
| 321 | // wrong width is not a failed verification but a malformed field, and the signing crate answers |
| 322 | // one with an error carrying nothing about this format in it. |
| 323 | let verifier = res!(verifier(env.sig_scheme, &env.author)); |
| 324 | res!(check_sig_len(env.sig_scheme, env.sig.len())); |
| 325 | let input = env.signing_input(); |
| 326 | if !res!(verifier.verify(&input, &env.sig)) { |
| 327 | return Err(err!( |
| 328 | "The signature in the envelope is not a signature by the author {} over the signing \ |
| 329 | input of this document (SPEC.md §1.3): schema '{}', scheme ids {:#010X} and {:#010X}, \ |
| 330 | time {}, hash {}.", |
| 331 | hex(&env.author), env.schema, env.sig_scheme, env.hash_scheme, env.time, |
| 332 | hex(&env.hash); |
| 333 | Invalid, Input, Security)); |
| 334 | } |
| 335 | |
| 336 | Ok((env, regions.tree)) |
| 337 | } |
| 338 | |
| 339 | /// Returns the region trailing the tree, which holds the optional index of §1.4, if any. |
| 340 | /// |
| 341 | /// The bytes are derived data lying outside the hash, and are not trusted by anything here: what |
| 342 | /// they say is checked against the tree by `index::check` before it is believed. |
| 343 | pub fn index_region<'a>(buf: &'a [u8]) -> Outcome<&'a [u8]> { |
| 344 | let (_, regions) = res!(locate(buf)); |
| 345 | Ok(regions.rest) |
| 346 | } |
| 347 | |
| 348 | /// Reads an artefact of any schema this build carries: header, envelope, hash, signature, then the |
| 349 | /// payload's own decoder and validator. |
| 350 | /// |
| 351 | /// [`read`] is this for the three `oxeweb/*` schemas, and returns a [`Doc`] because a caller that |
| 352 | /// asked for a document wants a tree rather than a match. A caller that will take whatever the file |
| 353 | /// turns out to be asks here. |
| 354 | pub fn read_artefact(buf: &[u8]) -> Outcome<Artefact> { |
| 355 | let (env, payload_bytes) = res!(verify(buf)); |
| 356 | // Steps 6 and 7, dispatched on the schema the author signed. Nothing here runs until the bytes |
| 357 | // have hashed to the address in the envelope and that address has been found to be signed. |
| 358 | let payload = res!(Payload::decode(&env.schema, payload_bytes)); |
| 359 | Ok(Artefact { |
| 360 | env, |
| 361 | payload, |
| 362 | }) |
| 363 | } |
| 364 | |
| 365 | /// Writes an artefact of any schema this build carries: validate, canonical encode, hash, sign, |
| 366 | /// assemble. |
| 367 | /// |
| 368 | /// The payload is validated against its own rules first, so that nothing this crate would refuse to |
| 369 | /// read is ever given a signature and an address. |
| 370 | pub fn write_artefact( |
| 371 | payload: &Payload, |
| 372 | signer: &SignatureScheme, |
| 373 | time: u64, |
| 374 | ) |
| 375 | -> Outcome<Vec<u8>> |
| 376 | { |
| 377 | let bytes = res!(payload.encode()); |
| 378 | let env = res!(seal(&bytes, payload.schema(), signer, time)); |
| 379 | assemble(&env, &bytes) |
| 380 | } |
| 381 | |
| 382 | /// Writes a document: validate, canonical encode, hash, sign, assemble. |
| 383 | /// |
| 384 | /// The tree is validated against the schema first, so that nothing this crate would refuse to read |
| 385 | /// is ever given a signature and an address. The schema must be one of the three that are node |
| 386 | /// trees; a post or a card is written by [`write_artefact`], which takes the payload rather than a |
| 387 | /// tree because neither is one. |
| 388 | pub fn write( |
| 389 | tree: &Dat, |
| 390 | schema: &str, |
| 391 | signer: &SignatureScheme, |
| 392 | time: u64, |
| 393 | ) |
| 394 | -> Outcome<Vec<u8>> |
| 395 | { |
| 396 | // Written out rather than routed through `write_artefact`, which would have to be handed an |
| 397 | // owned tree: a `Dat` clone recurses as deep as the tree goes, which is the cost `validate` |
| 398 | // walks by reference to avoid. Each step below is the same function `write_artefact` calls. |
| 399 | let tree_bytes = res!(encode_tree(tree, schema)); |
| 400 | let env = res!(seal(&tree_bytes, schema, signer, time)); |
| 401 | assemble(&env, &tree_bytes) |
| 402 | } |
| 403 | |
| 404 | /// Writes a document, and appends the optional index of §1.4. |
| 405 | /// |
| 406 | /// The index lies outside the hash, so the document has the same address, and is the same document, |
| 407 | /// whether it is written with an index or without one. |
| 408 | pub fn write_with_index( |
| 409 | tree: &Dat, |
| 410 | schema: &str, |
| 411 | signer: &SignatureScheme, |
| 412 | time: u64, |
| 413 | ) |
| 414 | -> Outcome<Vec<u8>> |
| 415 | { |
| 416 | let tree_bytes = res!(encode_tree(tree, schema)); |
| 417 | let env = res!(seal(&tree_bytes, schema, signer, time)); |
| 418 | let mut buf = res!(assemble(&env, &tree_bytes)); |
| 419 | buf.extend_from_slice(&res!(index::build(&tree_bytes))); |
| 420 | Ok(buf) |
| 421 | } |
| 422 | |
| 423 | /// Hashes a tree region with the scheme the envelope names. |
| 424 | pub fn hash_tree( |
| 425 | scheme: u32, |
| 426 | bytes: &[u8], |
| 427 | ) |
| 428 | -> Outcome<Vec<u8>> |
| 429 | { |
| 430 | let hasher = res!(hasher(scheme)); |
| 431 | Ok(hasher.hash(&[bytes], [0u8; 0]).as_vec()) |
| 432 | } |
| 433 | |
| 434 | /// Locates the regions of a file: steps 1 to 3 of §2. |
| 435 | /// |
| 436 | /// The header is checked, the envelope decoded, and the tree region measured against the bytes |
| 437 | /// available. A tree region shorter than the envelope declares is a rejection rather than a |
| 438 | /// truncation, and one longer than the limit of §5 is refused before a byte of it is read. |
| 439 | fn locate<'a>(buf: &'a [u8]) -> Outcome<(Envelope, Regions<'a>)> { |
| 440 | |
| 441 | // Step 1: the header. Magic, major version, and the envelope length, which is checked against |
| 442 | // the limit of §5 before it is believed. |
| 443 | let hdr = res!(envelope::read_header(buf)); |
| 444 | let env_end = HEADER_LEN + hdr.env_len as usize; |
| 445 | if buf.len() < env_end { |
| 446 | return Err(err!( |
| 447 | "The header declares an envelope of {} bytes, which would end at byte {}, but the \ |
| 448 | file is {} bytes.", hdr.env_len, env_end, buf.len(); |
| 449 | Invalid, Input, Decode)); |
| 450 | } |
| 451 | |
| 452 | // Step 2: the envelope, whose every key must be present and correctly typed. |
| 453 | let env = res!(Envelope::decode(&buf[HEADER_LEN..env_end])); |
| 454 | |
| 455 | // Step 3: the tree region, against the limit and against the bytes there are. |
| 456 | let tree_len = try_into!(usize, env.tree_len); |
| 457 | if tree_len > limit::TREE_BYTES { |
| 458 | return Err(err!( |
| 459 | "The envelope declares a tree region of {} bytes, exceeding the limit of {} bytes \ |
| 460 | (SPEC.md §5). The limit is enforced before decoding, so an envelope claiming a tree \ |
| 461 | larger than this is never believed.", tree_len, limit::TREE_BYTES; |
| 462 | Invalid, Input, TooBig, LimitReached)); |
| 463 | } |
| 464 | let avail = buf.len() - env_end; |
| 465 | if avail < tree_len { |
| 466 | return Err(err!( |
| 467 | "The envelope declares a tree region of {} bytes, but only {} bytes follow the \ |
| 468 | envelope. A tree region shorter than declared is a rejection, not a truncation \ |
| 469 | (SPEC.md §2).", tree_len, avail; |
| 470 | Invalid, Input, Decode)); |
| 471 | } |
| 472 | let tree_end = env_end + tree_len; |
| 473 | |
| 474 | Ok((env, Regions { |
| 475 | tree: &buf[env_end..tree_end], |
| 476 | rest: &buf[tree_end..], |
| 477 | })) |
| 478 | } |
| 479 | |
| 480 | /// Validates a tree against its schema, then encodes it canonically, refusing one too large for §5. |
| 481 | fn encode_tree( |
| 482 | tree: &Dat, |
| 483 | schema: &str, |
| 484 | ) |
| 485 | -> Outcome<Vec<u8>> |
| 486 | { |
| 487 | res!(validate::validate(tree, schema)); |
| 488 | let bytes = res!(canon::encode(tree)); |
| 489 | if bytes.len() > limit::TREE_BYTES { |
| 490 | return Err(err!( |
| 491 | "The tree encodes to {} bytes, exceeding the limit of {} bytes (SPEC.md §5).", |
| 492 | bytes.len(), limit::TREE_BYTES; |
| 493 | Invalid, Input, TooBig, LimitReached)); |
| 494 | } |
| 495 | Ok(bytes) |
| 496 | } |
| 497 | |
| 498 | /// Builds the envelope for an encoded payload, up to but not including the signature. |
| 499 | /// |
| 500 | /// The half of sealing that holds no key material, so that a signer living somewhere this code |
| 501 | /// cannot reach — a browser's non-extractable `CryptoKey`, a hardware token — can still produce an |
| 502 | /// artefact. The caller names the author's public key, takes [`Envelope::signing_input`] away, |
| 503 | /// signs it wherever the secret is, puts the signature in `sig`, and hands the envelope to |
| 504 | /// [`assemble`]. The secret never crosses this boundary in either direction. |
| 505 | /// |
| 506 | /// The returned envelope carries an empty `sig` and is not yet a sealed envelope. `assemble` will |
| 507 | /// happily write one with an empty signature, and [`read`] will refuse it, which is the correct |
| 508 | /// order: an unsigned artefact is a rejection at step 5 and not a special case anywhere earlier. |
| 509 | pub fn envelope_for( |
| 510 | payload_bytes: &[u8], |
| 511 | schema: &str, |
| 512 | author: &[u8], |
| 513 | time: u64, |
| 514 | ) |
| 515 | -> Outcome<Envelope> |
| 516 | { |
| 517 | // The author key is checked at its width here rather than at verification time, because a key |
| 518 | // of the wrong width names no signer and the artefact it would produce is unreadable by |
| 519 | // everybody including its writer. |
| 520 | if author.len() != SignatureScheme::ED25519_PK_LEN { |
| 521 | return Err(err!( |
| 522 | "The v0 envelope names the Ed25519 signature scheme, whose public key is {} bytes, \ |
| 523 | but the author key supplied is {} bytes.", |
| 524 | SignatureScheme::ED25519_PK_LEN, author.len(); |
| 525 | Invalid, Input, Mismatch)); |
| 526 | } |
| 527 | let hash_scheme = envelope::HASH_SCHEME_SHA3_256; |
| 528 | Ok(Envelope { |
| 529 | schema: schema.to_string(), |
| 530 | author: author.to_vec(), |
| 531 | sig_scheme: envelope::SIG_SCHEME_ED25519, |
| 532 | hash_scheme, |
| 533 | time, |
| 534 | hash: res!(hash_tree(hash_scheme, payload_bytes)), |
| 535 | sig: Vec::new(), |
| 536 | tree_len: try_into!(u64, payload_bytes.len()), |
| 537 | }) |
| 538 | } |
| 539 | |
| 540 | /// Builds the envelope for an encoded payload: hash the bytes, then sign the hash. |
| 541 | /// |
| 542 | /// Signing the hash rather than the payload is what binds the artefact's permanent address to its |
| 543 | /// author, and the schema and the scheme ids go into the signing input so that neither can be |
| 544 | /// re-labelled afterwards. |
| 545 | pub fn seal( |
| 546 | payload_bytes: &[u8], |
| 547 | schema: &str, |
| 548 | signer: &SignatureScheme, |
| 549 | time: u64, |
| 550 | ) |
| 551 | -> Outcome<Envelope> |
| 552 | { |
| 553 | // Refused before anything is hashed: a signer whose scheme v0 cannot name in an envelope must |
| 554 | // not produce bytes at all. |
| 555 | let sig_scheme = res!(sig_scheme_id(signer)); |
| 556 | let author = match res!(signer.get_public_key()) { |
| 557 | Some(pk) => pk.to_vec(), |
| 558 | None => return Err(err!( |
| 559 | "The signer holds no public key, so there is no author to name in the envelope."; |
| 560 | Missing, Configuration)), |
| 561 | }; |
| 562 | let mut env = res!(envelope_for(payload_bytes, schema, &author, time)); |
| 563 | env.sig_scheme = sig_scheme; |
| 564 | env.sig = res!(signer.sign(&env.signing_input())); |
| 565 | Ok(env) |
| 566 | } |
| 567 | |
| 568 | /// Assembles a file: header, envelope, payload region. |
| 569 | /// |
| 570 | /// The envelope's signature is written as it stands and is not checked here, since this is the |
| 571 | /// half of writing that a caller signing elsewhere reaches after [`envelope_for`]. What makes an |
| 572 | /// artefact sound is that [`read`] accepts it, and nothing else. |
| 573 | pub fn assemble( |
| 574 | env: &Envelope, |
| 575 | tree_bytes: &[u8], |
| 576 | ) |
| 577 | -> Outcome<Vec<u8>> |
| 578 | { |
| 579 | let env_bytes = res!(env.encode()); |
| 580 | let mut buf = res!(envelope::write_header(env_bytes.len())); |
| 581 | buf.reserve(env_bytes.len() + tree_bytes.len()); |
| 582 | buf.extend_from_slice(&env_bytes); |
| 583 | buf.extend_from_slice(tree_bytes); |
| 584 | Ok(buf) |
| 585 | } |
| 586 | |
| 587 | /// The scheme id a signer signs under, refusing one v0 cannot name in an envelope. |
| 588 | fn sig_scheme_id(signer: &SignatureScheme) -> Outcome<u32> { |
| 589 | match signer { |
| 590 | SignatureScheme::Ed25519(..) => Ok(envelope::SIG_SCHEME_ED25519), |
| 591 | other => Err(err!( |
| 592 | "The v0 envelope names the signature scheme Ed25519 only, but the signer is a {:?}.", |
| 593 | other; |
| 594 | Invalid, Input, Unimplemented)), |
| 595 | } |
| 596 | } |
| 597 | |
| 598 | /// The hash scheme a scheme id names, refusing an id this version does not implement. |
| 599 | /// |
| 600 | /// A signature scheme may be replaced freely, since a signature is checked once and discarded, but |
| 601 | /// a hash scheme may not, because the hash is the address. |
| 602 | fn hasher(scheme: u32) -> Outcome<HashScheme> { |
| 603 | match scheme { |
| 604 | envelope::HASH_SCHEME_SHA3_256 => Ok(HashScheme::new_sha3_256()), |
| 605 | _ => Err(err!( |
| 606 | "The envelope names the hash scheme {:#010X}, which this version does not implement. \ |
| 607 | v0 hashes with SHA3-256, whose scheme id is {:#010X}.", |
| 608 | scheme, envelope::HASH_SCHEME_SHA3_256; |
| 609 | Invalid, Input, Unimplemented)), |
| 610 | } |
| 611 | } |
| 612 | |
| 613 | /// Checks a signature's width against the scheme that wrote it. |
| 614 | /// |
| 615 | /// An unsigned envelope is the case that matters: `envelope_for` builds one with an empty `sig` so |
| 616 | /// that a caller signing elsewhere has something to fill in, and an artefact assembled around one |
| 617 | /// before the signature arrives must be refused here, saying so, rather than deep inside a |
| 618 | /// signature library that has never heard of this format. |
| 619 | fn check_sig_len( |
| 620 | scheme: u32, |
| 621 | len: usize, |
| 622 | ) |
| 623 | -> Outcome<()> |
| 624 | { |
| 625 | let want = match scheme { |
| 626 | envelope::SIG_SCHEME_ED25519 => envelope::SIG_LEN_ED25519, |
| 627 | // Any other scheme id was already refused by `verifier`, which runs first. |
| 628 | _ => return Ok(()), |
| 629 | }; |
| 630 | if len != want { |
| 631 | return Err(err!( |
| 632 | "The envelope carries a signature of {} bytes, but the scheme it names writes \ |
| 633 | signatures of {} bytes. {}", |
| 634 | len, want, |
| 635 | if len == 0 { |
| 636 | "The signature is empty, so this artefact was assembled before it was signed." |
| 637 | } else { |
| 638 | "A signature of the wrong width is a malformed field, not a failed check." |
| 639 | }; |
| 640 | Invalid, Input, Mismatch)); |
| 641 | } |
| 642 | Ok(()) |
| 643 | } |
| 644 | |
| 645 | /// The verifier for a scheme id and an author's public key, refusing an id v0 does not implement. |
| 646 | fn verifier( |
| 647 | scheme: u32, |
| 648 | author: &[u8], |
| 649 | ) |
| 650 | -> Outcome<SignatureScheme> |
| 651 | { |
| 652 | match scheme { |
| 653 | envelope::SIG_SCHEME_ED25519 => { |
| 654 | if author.len() != SignatureScheme::ED25519_PK_LEN { |
| 655 | return Err(err!( |
| 656 | "The envelope names the Ed25519 signature scheme, whose public key is {} \ |
| 657 | bytes, but the author key it carries is {} bytes.", |
| 658 | SignatureScheme::ED25519_PK_LEN, author.len(); |
| 659 | Invalid, Input, Mismatch)); |
| 660 | } |
| 661 | Ok(res!(SignatureScheme::empty_ed25519().set_public_key(Some(author)))) |
| 662 | }, |
| 663 | _ => Err(err!( |
| 664 | "The envelope names the signature scheme {:#010X}, which this version does not \ |
| 665 | implement. v0 signs with Ed25519, whose scheme id is {:#010X}.", |
| 666 | scheme, envelope::SIG_SCHEME_ED25519; |
| 667 | Invalid, Input, Unimplemented)), |
| 668 | } |
| 669 | } |
| 670 | |
| 671 | /// Renders bytes as hexadecimal, for an error message that must name what it rejected. |
| 672 | fn hex(byts: &[u8]) -> String { |
| 673 | let mut s = String::new(); |
| 674 | for b in byts { |
| 675 | s.push_str(&fmt!("{:02x}", b)); |
| 676 | } |
| 677 | s |
| 678 | } |
| 679 | |
| 680 | #[cfg(test)] |
| 681 | mod tests { |
| 682 | use super::*; |
| 683 | use crate::{ |
| 684 | kinds::NodeKind, |
| 685 | MAGIC, |
| 686 | SCHEMA_DOC, |
| 687 | }; |
| 688 | |
| 689 | use oxedyne_fe2o3_jdat::usr::UsrKindId; |
| 690 | |
| 691 | /// A fixed authoring time, so that a document written twice is written the same. |
| 692 | const TIME: u64 = 1_752_000_000_000; |
| 693 | |
| 694 | /// Wraps a payload as a node of the given kind. |
| 695 | fn node(kind: NodeKind, payload: Dat) -> Dat { |
| 696 | Dat::Usr( |
| 697 | UsrKindId::new(kind.code(), Some(kind.label()), None), |
| 698 | Some(Box::new(payload)), |
| 699 | ) |
| 700 | } |
| 701 | |
| 702 | /// A text run, whose payload is a bare string. |
| 703 | fn text(s: &str) -> Dat { |
| 704 | node(NodeKind::Text, dat!(s)) |
| 705 | } |
| 706 | |
| 707 | /// A small but complete document: a heading, a paragraph, and emphasis inside it. |
| 708 | fn sample_tree() -> Dat { |
| 709 | node(NodeKind::Doc, mapdat!{ |
| 710 | "title" => dat!("Style without a cascade"), |
| 711 | "lang" => dat!("en"), |
| 712 | "children" => Dat::List(vec![ |
| 713 | node(NodeKind::Heading, mapdat!{ |
| 714 | "level" => dat!(2u8), |
| 715 | "children" => Dat::List(vec![text("Style without a cascade")]), |
| 716 | }), |
| 717 | node(NodeKind::Para, mapdat!{ |
| 718 | "children" => Dat::List(vec![ |
| 719 | text("A paragraph, and some "), |
| 720 | node(NodeKind::Emph, mapdat!{ |
| 721 | "strong" => dat!(true), |
| 722 | "children" => Dat::List(vec![text("emphasis")]), |
| 723 | }), |
| 724 | ]), |
| 725 | }), |
| 726 | ]), |
| 727 | }) |
| 728 | } |
| 729 | |
| 730 | /// A file carrying the sample document, signed by a fresh key. |
| 731 | fn sample_file() -> Outcome<(SignatureScheme, Vec<u8>)> { |
| 732 | let signer = SignatureScheme::new_ed25519(); |
| 733 | let buf = res!(write(&sample_tree(), SCHEMA_DOC, &signer, TIME)); |
| 734 | Ok((signer, buf)) |
| 735 | } |
| 736 | |
| 737 | /// The offset at which the tree region of a file starts. |
| 738 | fn tree_start(buf: &[u8]) -> Outcome<usize> { |
| 739 | let hdr = res!(envelope::read_header(buf)); |
| 740 | Ok(HEADER_LEN + hdr.env_len as usize) |
| 741 | } |
| 742 | |
| 743 | /// Asserts that reading fails, and that the message says why. |
| 744 | fn rejects(buf: &[u8], what: &str, says: &str) -> Outcome<()> { |
| 745 | match read(buf) { |
| 746 | Ok(_) => Err(err!( |
| 747 | "Expected a rejection of {}, but the document was read.", what; |
| 748 | Test, Invalid)), |
| 749 | Err(e) => { |
| 750 | let msg = fmt!("{}", e); |
| 751 | assert!(msg.contains(says), |
| 752 | "The rejection of {} should say '{}', but says: {}", what, says, msg); |
| 753 | Ok(()) |
| 754 | }, |
| 755 | } |
| 756 | } |
| 757 | |
| 758 | #[test] |
| 759 | fn test_signed_round_trip_00() -> Outcome<()> { |
| 760 | let (signer, buf) = res!(sample_file()); |
| 761 | let doc = res!(read(&buf)); |
| 762 | assert_eq!(doc.tree, sample_tree(), "The tree did not survive a signed round trip."); |
| 763 | assert_eq!(doc.env.schema, SCHEMA_DOC); |
| 764 | assert_eq!(doc.env.time, TIME); |
| 765 | assert_eq!(doc.env.sig_scheme, envelope::SIG_SCHEME_ED25519); |
| 766 | assert_eq!(doc.env.hash_scheme, envelope::HASH_SCHEME_SHA3_256); |
| 767 | match res!(signer.get_public_key()) { |
| 768 | Some(pk) => assert_eq!(&doc.env.author[..], pk, "The author is not the signer."), |
| 769 | None => return Err(err!("The signer holds no public key."; Test, Invalid)), |
| 770 | } |
| 771 | // The envelope's hash is the hash of the tree region, and the region is what it declares. |
| 772 | let start = res!(tree_start(&buf)); |
| 773 | let tree_bytes = &buf[start..]; |
| 774 | assert_eq!(doc.env.tree_len as usize, tree_bytes.len()); |
| 775 | assert_eq!(doc.env.hash, res!(hash_tree(doc.env.hash_scheme, tree_bytes))); |
| 776 | // Writing the same tree twice writes the same bytes: one document, one address. |
| 777 | let again = res!(write(&sample_tree(), SCHEMA_DOC, &signer, TIME)); |
| 778 | assert_eq!(again, buf, "A document written twice is not the same document."); |
| 779 | Ok(()) |
| 780 | } |
| 781 | |
| 782 | #[test] |
| 783 | fn test_verify_only_never_decodes_01() -> Outcome<()> { |
| 784 | let (signer, buf) = res!(sample_file()); |
| 785 | let env = res!(verify_only(&buf)); |
| 786 | assert_eq!(env, res!(read(&buf)).env); |
| 787 | |
| 788 | // A tree region that is signed but is not a tree at all passes every step that touches no |
| 789 | // content, and fails the moment one does. This is the whole point of the ordering. |
| 790 | let rubbish = vec![0xFF; 64]; |
| 791 | let env = res!(seal(&rubbish, SCHEMA_DOC, &signer, TIME)); |
| 792 | let buf = res!(assemble(&env, &rubbish)); |
| 793 | res!(verify_only(&buf)); |
| 794 | assert!(read(&buf).is_err(), "Rubbish under a good signature was decoded as a tree."); |
| 795 | Ok(()) |
| 796 | } |
| 797 | |
| 798 | #[test] |
| 799 | fn test_corrupt_tree_byte_02() -> Outcome<()> { |
| 800 | let (_, buf) = res!(sample_file()); |
| 801 | let start = res!(tree_start(&buf)); |
| 802 | // Every byte of the tree region, flipped one at a time, is caught by the hash. |
| 803 | for i in start..buf.len() { |
| 804 | let mut bad = buf.clone(); |
| 805 | bad[i] ^= 0x01; |
| 806 | res!(rejects(&bad, "a corrupted tree byte", "hashes to")); |
| 807 | assert!(verify_only(&bad).is_err(), "A corrupted tree byte survived verification."); |
| 808 | } |
| 809 | Ok(()) |
| 810 | } |
| 811 | |
| 812 | #[test] |
| 813 | fn test_corrupt_signature_03() -> Outcome<()> { |
| 814 | let (signer, buf) = res!(sample_file()); |
| 815 | let start = res!(tree_start(&buf)); |
| 816 | let tree_bytes = buf[start..].to_vec(); |
| 817 | let mut env = res!(seal(&tree_bytes, SCHEMA_DOC, &signer, TIME)); |
| 818 | env.sig[0] ^= 0x01; |
| 819 | let bad = res!(assemble(&env, &tree_bytes)); |
| 820 | res!(rejects(&bad, "a corrupted signature", "not a signature by the author")); |
| 821 | Ok(()) |
| 822 | } |
| 823 | |
| 824 | #[test] |
| 825 | fn test_signature_by_another_key_04() -> Outcome<()> { |
| 826 | let (signer, buf) = res!(sample_file()); |
| 827 | let start = res!(tree_start(&buf)); |
| 828 | let tree_bytes = buf[start..].to_vec(); |
| 829 | // The document is signed correctly, but by a key that is not the author it names. |
| 830 | let other = SignatureScheme::new_ed25519(); |
| 831 | let mut env = res!(seal(&tree_bytes, SCHEMA_DOC, &other, TIME)); |
| 832 | env.author = match res!(signer.get_public_key()) { |
| 833 | Some(pk) => pk.to_vec(), |
| 834 | None => return Err(err!("The signer holds no public key."; Test, Invalid)), |
| 835 | }; |
| 836 | let bad = res!(assemble(&env, &tree_bytes)); |
| 837 | res!(rejects(&bad, "a signature by another key", "not a signature by the author")); |
| 838 | // And the reverse: the right signature, an author who did not make it. |
| 839 | let mut env = res!(seal(&tree_bytes, SCHEMA_DOC, &signer, TIME)); |
| 840 | env.author = match res!(other.get_public_key()) { |
| 841 | Some(pk) => pk.to_vec(), |
| 842 | None => return Err(err!("The signer holds no public key."; Test, Invalid)), |
| 843 | }; |
| 844 | let bad = res!(assemble(&env, &tree_bytes)); |
| 845 | res!(rejects(&bad, "an author who did not sign", "not a signature by the author")); |
| 846 | Ok(()) |
| 847 | } |
| 848 | |
| 849 | #[test] |
| 850 | fn test_truncated_tree_05() -> Outcome<()> { |
| 851 | let (_, buf) = res!(sample_file()); |
| 852 | // One byte short of the tree region the envelope declares. |
| 853 | let short = &buf[..buf.len() - 1]; |
| 854 | res!(rejects(short, "a truncated tree", "shorter than declared")); |
| 855 | // And truncated to nothing at all. |
| 856 | let start = res!(tree_start(&buf)); |
| 857 | res!(rejects(&buf[..start], "an absent tree", "shorter than declared")); |
| 858 | // A file truncated inside its envelope, and inside its header. |
| 859 | res!(rejects(&buf[..start - 1], "a truncated envelope", "envelope")); |
| 860 | assert!(read(&buf[..4]).is_err(), "A file shorter than its header was read."); |
| 861 | Ok(()) |
| 862 | } |
| 863 | |
| 864 | #[test] |
| 865 | fn test_tree_longer_than_tree_len_06() -> Outcome<()> { |
| 866 | let (signer, buf) = res!(sample_file()); |
| 867 | let start = res!(tree_start(&buf)); |
| 868 | let tree_bytes = buf[start..].to_vec(); |
| 869 | let short = tree_bytes.len() - 1; |
| 870 | |
| 871 | // The strongest form: the tree is one byte longer than `tree_len`, and the author has |
| 872 | // hashed and signed the shortened region, so the hash and the signature both pass. The |
| 873 | // declared region holds a tree cut off one byte from its end, and the decoder says so. |
| 874 | let mut env = res!(seal(&tree_bytes[..short], SCHEMA_DOC, &signer, TIME)); |
| 875 | env.tree_len = short as u64; |
| 876 | let bad = res!(assemble(&env, &tree_bytes)); |
| 877 | res!(verify_only(&bad)); // Steps 1 to 5 pass: the bytes are what the author signed. |
| 878 | assert!(read(&bad).is_err(), "A tree longer than tree_len was decoded."); |
| 879 | |
| 880 | // The plain form: `tree_len` understates the tree by one byte, and the hash covers the tree |
| 881 | // the author meant. The hash of the declared region is not the hash the envelope carries. |
| 882 | let mut env = res!(seal(&tree_bytes, SCHEMA_DOC, &signer, TIME)); |
| 883 | env.tree_len = short as u64; |
| 884 | env.sig = res!(signer.sign(&env.signing_input())); |
| 885 | let bad = res!(assemble(&env, &tree_bytes)); |
| 886 | res!(rejects(&bad, "a tree longer than tree_len", "hashes to")); |
| 887 | |
| 888 | // A byte appended to the tree region without the envelope being told is a trailing byte, |
| 889 | // which is where an index would sit. It is outside the hash, so the document reads, and it |
| 890 | // is the same document at the same address. |
| 891 | let mut padded = buf.clone(); |
| 892 | padded.push(0x00); |
| 893 | let doc = res!(read(&padded)); |
| 894 | assert_eq!(doc.env, res!(read(&buf)).env, "A trailing byte changed the document."); |
| 895 | assert_eq!(res!(index_region(&padded)), &[0x00][..]); |
| 896 | Ok(()) |
| 897 | } |
| 898 | |
| 899 | #[test] |
| 900 | fn test_tree_shorter_than_tree_len_07() -> Outcome<()> { |
| 901 | let (signer, buf) = res!(sample_file()); |
| 902 | let start = res!(tree_start(&buf)); |
| 903 | let mut tree_bytes = buf[start..].to_vec(); |
| 904 | // The tree region carries the tree and one byte more, all of it hashed and signed. The |
| 905 | // canonical encoding of a tree is the tree and nothing else (§3). |
| 906 | tree_bytes.push(0x00); |
| 907 | let env = res!(seal(&tree_bytes, SCHEMA_DOC, &signer, TIME)); |
| 908 | let bad = res!(assemble(&env, &tree_bytes)); |
| 909 | res!(verify_only(&bad)); |
| 910 | res!(rejects(&bad, "a byte trailing the tree inside the tree region", "canonical")); |
| 911 | Ok(()) |
| 912 | } |
| 913 | |
| 914 | #[test] |
| 915 | fn test_unknown_magic_08() -> Outcome<()> { |
| 916 | let (_, buf) = res!(sample_file()); |
| 917 | for i in 0..MAGIC.len() { |
| 918 | let mut bad = buf.clone(); |
| 919 | bad[i] ^= 0xFF; |
| 920 | res!(rejects(&bad, "an unknown magic", "Not an SBJ file")); |
| 921 | } |
| 922 | // An empty file, and a file that is valid BDAT but not an SBJ file at all. |
| 923 | assert!(read(&[]).is_err(), "An empty file was read."); |
| 924 | let bdat = res!(dat!("not a document").to_bytes(Vec::new())); |
| 925 | assert!(read(&bdat).is_err(), "A bare daticle was read as a document."); |
| 926 | Ok(()) |
| 927 | } |
| 928 | |
| 929 | #[test] |
| 930 | fn test_unknown_major_version_09() -> Outcome<()> { |
| 931 | let (_, buf) = res!(sample_file()); |
| 932 | let mut bad = buf.clone(); |
| 933 | bad[5] = 1; // Major version 1. |
| 934 | res!(rejects(&bad, "an unknown major version", "not implemented here")); |
| 935 | let mut bad = buf.clone(); |
| 936 | bad[4] = 0xFF; // Major version 65280 and up. |
| 937 | res!(rejects(&bad, "an unknown major version", "not implemented here")); |
| 938 | Ok(()) |
| 939 | } |
| 940 | |
| 941 | #[test] |
| 942 | fn test_unknown_schemes_10() -> Outcome<()> { |
| 943 | let (signer, buf) = res!(sample_file()); |
| 944 | let start = res!(tree_start(&buf)); |
| 945 | let tree_bytes = buf[start..].to_vec(); |
| 946 | |
| 947 | // A hash scheme this version does not implement is refused before the tree is hashed. |
| 948 | let mut env = res!(seal(&tree_bytes, SCHEMA_DOC, &signer, TIME)); |
| 949 | env.hash_scheme = 0xDEAD_BEEF; |
| 950 | env.sig = res!(signer.sign(&env.signing_input())); |
| 951 | let bad = res!(assemble(&env, &tree_bytes)); |
| 952 | res!(rejects(&bad, "an unknown hash scheme", "does not implement")); |
| 953 | |
| 954 | // And a signature scheme, before the signature is checked. |
| 955 | let mut env = res!(seal(&tree_bytes, SCHEMA_DOC, &signer, TIME)); |
| 956 | env.sig_scheme = 0xDEAD_BEEF; |
| 957 | env.sig = res!(signer.sign(&env.signing_input())); |
| 958 | let bad = res!(assemble(&env, &tree_bytes)); |
| 959 | res!(rejects(&bad, "an unknown signature scheme", "does not implement")); |
| 960 | Ok(()) |
| 961 | } |
| 962 | |
| 963 | #[test] |
| 964 | fn test_tree_region_limit_11() -> Outcome<()> { |
| 965 | let (signer, buf) = res!(sample_file()); |
| 966 | let start = res!(tree_start(&buf)); |
| 967 | let tree_bytes = buf[start..].to_vec(); |
| 968 | // A tree region larger than the limit is refused on the envelope's word alone, without the |
| 969 | // bytes ever being supplied, let alone read. |
| 970 | let mut env = res!(seal(&tree_bytes, SCHEMA_DOC, &signer, TIME)); |
| 971 | env.tree_len = (limit::TREE_BYTES + 1) as u64; |
| 972 | env.sig = res!(signer.sign(&env.signing_input())); |
| 973 | let bad = res!(assemble(&env, &tree_bytes)); |
| 974 | res!(rejects(&bad, "a tree region over the limit", "exceeding the limit")); |
| 975 | Ok(()) |
| 976 | } |
| 977 | |
| 978 | #[test] |
| 979 | fn test_foreign_schema_12() -> Outcome<()> { |
| 980 | // The container carries any schema, but this build validates one, so a payload declaring |
| 981 | // another is rejected rather than read as though it were a document. |
| 982 | let (signer, buf) = res!(sample_file()); |
| 983 | let start = res!(tree_start(&buf)); |
| 984 | let tree_bytes = buf[start..].to_vec(); |
| 985 | let env = res!(seal(&tree_bytes, "oxeweb/cmd/0", &signer, TIME)); |
| 986 | let bad = res!(assemble(&env, &tree_bytes)); |
| 987 | res!(verify_only(&bad)); // The envelope is sound; it is the payload that is foreign. |
| 988 | res!(rejects(&bad, "a foreign schema", "oxeweb/cmd/0")); |
| 989 | // And write refuses to sign a tree it cannot validate. |
| 990 | assert!(write(&sample_tree(), "oxeweb/cmd/0", &signer, TIME).is_err(), |
| 991 | "A tree was signed under a schema this build cannot validate."); |
| 992 | Ok(()) |
| 993 | } |
| 994 | |
| 995 | #[test] |
| 996 | fn test_write_refuses_an_invalid_tree_13() -> Outcome<()> { |
| 997 | let signer = SignatureScheme::new_ed25519(); |
| 998 | // A heading level outside 1..=6 never reaches a file, so it never gets an address. |
| 999 | let tree = node(NodeKind::Doc, mapdat!{ |
| 1000 | "title" => dat!("T"), |
| 1001 | "lang" => dat!("en"), |
| 1002 | "children" => Dat::List(vec![ |
| 1003 | node(NodeKind::Heading, mapdat!{ |
| 1004 | "level" => dat!(7u8), |
| 1005 | "children" => Dat::List(vec![text("Too deep")]), |
| 1006 | }), |
| 1007 | ]), |
| 1008 | }); |
| 1009 | match write(&tree, SCHEMA_DOC, &signer, TIME) { |
| 1010 | Ok(_) => return Err(err!("A heading of level 7 was signed."; Test, Invalid)), |
| 1011 | Err(e) => { |
| 1012 | let msg = fmt!("{}", e); |
| 1013 | assert!(msg.contains("1..=6"), "The refusal should name the range: {}", msg); |
| 1014 | }, |
| 1015 | } |
| 1016 | // And a tree that is not canonical: an empty children list has two encodings. |
| 1017 | let tree = node(NodeKind::Doc, mapdat!{ |
| 1018 | "title" => dat!("T"), |
| 1019 | "lang" => dat!("en"), |
| 1020 | "children" => Dat::List(vec![ |
| 1021 | node(NodeKind::Para, mapdat!{ |
| 1022 | "children" => Dat::List(Vec::new()), |
| 1023 | }), |
| 1024 | ]), |
| 1025 | }); |
| 1026 | assert!(write(&tree, SCHEMA_DOC, &signer, TIME).is_err(), |
| 1027 | "A non-canonical tree was signed."); |
| 1028 | Ok(()) |
| 1029 | } |
| 1030 | |
| 1031 | /// A chain of boxes `depth` nodes deep, the doc at its head and the deepest box childless. |
| 1032 | fn chain(depth: usize) -> Dat { |
| 1033 | let mut inner = node(NodeKind::Boxx, mapdat!{}); |
| 1034 | for _ in 0..(depth - 2) { |
| 1035 | inner = node(NodeKind::Boxx, mapdat!{ |
| 1036 | "children" => Dat::List(vec![inner]), |
| 1037 | }); |
| 1038 | } |
| 1039 | node(NodeKind::Doc, mapdat!{ |
| 1040 | "title" => dat!("A deep document"), |
| 1041 | "lang" => dat!("en"), |
| 1042 | "children" => Dat::List(vec![inner]), |
| 1043 | }) |
| 1044 | } |
| 1045 | |
| 1046 | /// A document at the depth limit, and one past it. |
| 1047 | fn nesting_at_the_limit_and_past_it() -> Outcome<()> { |
| 1048 | let signer = SignatureScheme::new_ed25519(); |
| 1049 | |
| 1050 | // At the limit, a document is written and read like any other. |
| 1051 | let buf = res!(write(&chain(limit::DEPTH), SCHEMA_DOC, &signer, TIME)); |
| 1052 | let doc = res!(read(&buf)); |
| 1053 | assert_eq!(doc.tree, chain(limit::DEPTH), "A tree at the depth limit did not survive."); |
| 1054 | |
| 1055 | // One past it, nothing is signed, and a file assembled by hand around such a tree is |
| 1056 | // refused. The bytes are what the author signed, so the refusal comes from the reading of |
| 1057 | // the tree, which is where the depth limit belongs: the tree is never validated, because |
| 1058 | // it is never built. |
| 1059 | let deep = chain(limit::DEPTH + 1); |
| 1060 | assert!(write(&deep, SCHEMA_DOC, &signer, TIME).is_err(), |
| 1061 | "A tree past the depth limit was signed."); |
| 1062 | let tree_bytes = res!(deep.to_bytes(Vec::new())); |
| 1063 | let env = res!(seal(&tree_bytes, SCHEMA_DOC, &signer, TIME)); |
| 1064 | let bad = res!(assemble(&env, &tree_bytes)); |
| 1065 | res!(verify_only(&bad)); |
| 1066 | res!(rejects(&bad, "a tree past the depth limit", "depth")); |
| 1067 | Ok(()) |
| 1068 | } |
| 1069 | |
| 1070 | #[test] |
| 1071 | fn test_nesting_at_the_limit_and_past_it_15() -> Outcome<()> { |
| 1072 | // A node costs three daticle levels, so a document at the node depth limit of 256 nests |
| 1073 | // daticles 770 deep, and a recursive decoder spends a frame on each. A release build |
| 1074 | // spends about half a kilobyte a level, so the deepest legal document costs it some 400 |
| 1075 | // KiB, well inside the two megabytes a thread is given by default. A debug build spends |
| 1076 | // about eight times that, since it gives every arm of a match its own slot in the frame, |
| 1077 | // so the test runs on a thread with a stack that can hold the deepest document the format |
| 1078 | // permits. The limit is the format's, and it does not move to suit a test. |
| 1079 | let thread = match std::thread::Builder::new() |
| 1080 | .name("sbj_depth".to_string()) |
| 1081 | .stack_size(16 * 1024 * 1024) |
| 1082 | .spawn(nesting_at_the_limit_and_past_it) |
| 1083 | { |
| 1084 | Ok(thread) => thread, |
| 1085 | Err(e) => return Err(err!(e, |
| 1086 | "Could not spawn the thread the deepest legal document is read on."; |
| 1087 | Test, Init)), |
| 1088 | }; |
| 1089 | match thread.join() { |
| 1090 | Ok(outcome) => outcome, |
| 1091 | Err(_) => Err(err!( |
| 1092 | "The thread reading the deepest legal document did not return."; |
| 1093 | Test, Panic)), |
| 1094 | } |
| 1095 | } |
| 1096 | |
| 1097 | #[test] |
| 1098 | fn test_a_doc_is_a_verified_document_16() -> Outcome<()> { |
| 1099 | // The claim the type makes is that holding a `Doc` means the document verified. That a `Doc` |
| 1100 | // cannot be constructed any other way is a fact about compilation, not about execution, so it |
| 1101 | // is asserted by the `compile_fail` doctests on `Doc` rather than here: no test that runs can |
| 1102 | // observe code that does not compile. What runs here is the other half of the claim: that |
| 1103 | // what `read` hands back has in fact been verified, checked from the outside, against the |
| 1104 | // document's own bytes rather than against anything the reader remembers. |
| 1105 | let (signer, buf) = res!(sample_file()); |
| 1106 | let doc = res!(read(&buf)); |
| 1107 | |
| 1108 | // The tree is the tree the envelope vouches for: it re-encodes to the region that was hashed, |
| 1109 | // and that region hashes to the address the envelope carries. |
| 1110 | let tree_bytes = res!(canon::encode(doc.tree())); |
| 1111 | assert_eq!(doc.env().tree_len as usize, tree_bytes.len(), |
| 1112 | "The tree of a Doc is not the length the envelope declares."); |
| 1113 | assert_eq!(doc.env().hash, res!(hash_tree(doc.env().hash_scheme, &tree_bytes)), |
| 1114 | "The tree of a Doc does not hash to the address of its envelope."); |
| 1115 | |
| 1116 | // And the author signed that address. |
| 1117 | let verifier = res!(verifier(doc.env().sig_scheme, &doc.env().author)); |
| 1118 | assert!(res!(verifier.verify(&doc.env().signing_input(), &doc.env().sig)), |
| 1119 | "The envelope of a Doc carries a signature the author did not make."); |
| 1120 | match res!(signer.get_public_key()) { |
| 1121 | Some(pk) => assert_eq!(&doc.env().author[..], pk, "The author is not the signer."), |
| 1122 | None => return Err(err!("The signer holds no public key."; Test, Invalid)), |
| 1123 | } |
| 1124 | |
| 1125 | // The accessors hand out no way to undo any of that: `env` and `tree` borrow, and the `into_` |
| 1126 | // pair consumes the document rather than opening it. |
| 1127 | let (env, tree) = res!(read(&buf)).into_parts(); |
| 1128 | assert_eq!(env, *doc.env()); |
| 1129 | assert_eq!(tree, *doc.tree()); |
| 1130 | assert_eq!(res!(read(&buf)).into_tree(), *doc.tree()); |
| 1131 | |
| 1132 | // Every document that fails at any step fails to become a `Doc` at all, so there is no state |
| 1133 | // in which one exists and its verification did not happen. |
| 1134 | let start = res!(tree_start(&buf)); |
| 1135 | let mut bad = buf.clone(); |
| 1136 | bad[start] ^= 0x01; // A tree byte the author did not sign. |
| 1137 | res!(rejects(&bad, "a tampered tree", "hashes to")); |
| 1138 | let tree_bytes = buf[start..].to_vec(); |
| 1139 | let mut env = res!(seal(&tree_bytes, SCHEMA_DOC, &signer, TIME)); |
| 1140 | env.sig[0] ^= 0x01; // A signature the author did not make. |
| 1141 | let bad = res!(assemble(&env, &tree_bytes)); |
| 1142 | res!(rejects(&bad, "a tampered signature", "not a signature by the author")); |
| 1143 | Ok(()) |
| 1144 | } |
| 1145 | |
| 1146 | #[test] |
| 1147 | fn test_index_is_outside_the_document_14() -> Outcome<()> { |
| 1148 | let signer = SignatureScheme::new_ed25519(); |
| 1149 | let plain = res!(write(&sample_tree(), SCHEMA_DOC, &signer, TIME)); |
| 1150 | let indexed = res!(write_with_index(&sample_tree(), SCHEMA_DOC, &signer, TIME)); |
| 1151 | assert!(indexed.len() > plain.len(), "The index added nothing."); |
| 1152 | |
| 1153 | // Two files carrying the same tree are the same document at the same address, whether or |
| 1154 | // not either carries an index. |
| 1155 | let a = res!(read(&plain)); |
| 1156 | let b = res!(read(&indexed)); |
| 1157 | assert_eq!(a.env, b.env, "The index changed the document's envelope."); |
| 1158 | assert_eq!(a.tree, b.tree, "The index changed the document's tree."); |
| 1159 | assert_eq!(a.env.hash, b.env.hash, "The index changed the document's address."); |
| 1160 | |
| 1161 | // The index that was appended describes the tree it was built from. |
| 1162 | assert_eq!(res!(index_region(&plain)).len(), 0); |
| 1163 | let idx = res!(index::parse(res!(index_region(&indexed)))); |
| 1164 | let (_, tree_bytes) = res!(verify(&indexed)); |
| 1165 | res!(index::check(tree_bytes, &idx)); |
| 1166 | // doc, heading, the heading's text, para, its text, the emph, and the emph's text. |
| 1167 | assert_eq!(idx.len(), 7); |
| 1168 | Ok(()) |
| 1169 | } |
| 1170 | |
| 1171 | /// A post, for the artefact tests. |
| 1172 | fn sample_post() -> Post { |
| 1173 | Post { |
| 1174 | body: fmt!("The crop is in, and the second field can wait."), |
| 1175 | to: vec![0xA1; crate::post::limit::KEY_BYTES], |
| 1176 | nonce: vec![0xB2; crate::post::limit::NONCE_BYTES], |
| 1177 | reply_to: None, |
| 1178 | refs: Vec::new(), |
| 1179 | } |
| 1180 | } |
| 1181 | |
| 1182 | /// A whole file carrying a post can be written and read back. |
| 1183 | /// |
| 1184 | /// This is the gap [`Payload`] closes. Before it, [`write`] was the only writer and it routes |
| 1185 | /// every schema through the node-tree validator, which admits the three `oxeweb/*` names alone, |
| 1186 | /// so no path in the crate could produce or read a file carrying a message. |
| 1187 | #[test] |
| 1188 | fn test_a_post_is_a_whole_artefact_17() -> Outcome<()> { |
| 1189 | let signer = SignatureScheme::new_ed25519(); |
| 1190 | let post = sample_post(); |
| 1191 | let buf = res!(write_artefact(&Payload::Post(post.clone()), &signer, TIME)); |
| 1192 | let back = res!(read_artefact(&buf)); |
| 1193 | assert_eq!(back.env().schema, SCHEMA_POST); |
| 1194 | assert_eq!(*back.payload(), Payload::Post(post)); |
| 1195 | |
| 1196 | // The verification order is the container's, not the payload's: a payload byte the author |
| 1197 | // did not sign fails at the hash, before a decoder sees it. |
| 1198 | let start = res!(tree_start(&buf)); |
| 1199 | let mut bad = buf.clone(); |
| 1200 | bad[start] ^= 0x01; |
| 1201 | match read_artefact(&bad) { |
| 1202 | Ok(_) => return Err(err!("A tampered post was read."; Test, Invalid)), |
| 1203 | Err(e) => assert!(fmt!("{}", e).contains("hashes to")), |
| 1204 | } |
| 1205 | Ok(()) |
| 1206 | } |
| 1207 | |
| 1208 | /// A whole file carrying a card can be written and read back. |
| 1209 | #[test] |
| 1210 | fn test_a_card_is_a_whole_artefact_18() -> Outcome<()> { |
| 1211 | let signer = SignatureScheme::new_ed25519(); |
| 1212 | let card = Card { |
| 1213 | label: fmt!("Jason"), |
| 1214 | enc: vec![0xE1; crate::card::limit::KEY_BYTES], |
| 1215 | role: crate::card::Role::Root, |
| 1216 | prev: None, |
| 1217 | }; |
| 1218 | let buf = res!(write_artefact(&Payload::Card(card.clone()), &signer, TIME)); |
| 1219 | let back = res!(read_artefact(&buf)); |
| 1220 | assert_eq!(back.env().schema, SCHEMA_CARD); |
| 1221 | assert_eq!(*back.payload(), Payload::Card(card)); |
| 1222 | Ok(()) |
| 1223 | } |
| 1224 | |
| 1225 | /// A payload cannot be re-labelled into another schema after signing. |
| 1226 | /// |
| 1227 | /// The attack the length prefix of §1.3 closes, run over the two schemas that made it possible |
| 1228 | /// to attempt: a post and a card are both flat maps, so the bytes of one can be handed to the |
| 1229 | /// other's decoder, and only the envelope says which it is. Re-labelling the envelope is what |
| 1230 | /// the signature refuses. |
| 1231 | #[test] |
| 1232 | fn test_a_payload_cannot_be_relabelled_19() -> Outcome<()> { |
| 1233 | let signer = SignatureScheme::new_ed25519(); |
| 1234 | let bytes = res!(sample_post().encode()); |
| 1235 | let env = res!(seal(&bytes, SCHEMA_POST, &signer, TIME)); |
| 1236 | res!(read_artefact(&res!(assemble(&env, &bytes)))); |
| 1237 | |
| 1238 | // The same bytes, the same hash, the same signature, one word changed in the envelope. |
| 1239 | let mut relabelled = env.clone(); |
| 1240 | relabelled.schema = SCHEMA_CARD.to_string(); |
| 1241 | match read_artefact(&res!(assemble(&relabelled, &bytes))) { |
| 1242 | Ok(_) => Err(err!( |
| 1243 | "A post was read as a card, so the schema is not inside the signature."; |
| 1244 | Test, Invalid)), |
| 1245 | Err(e) => { |
| 1246 | assert!(fmt!("{}", e).contains("not a signature by the author")); |
| 1247 | Ok(()) |
| 1248 | }, |
| 1249 | } |
| 1250 | } |
| 1251 | |
| 1252 | /// A caller may build an envelope, sign it elsewhere, and assemble the artefact, without the |
| 1253 | /// secret key ever reaching this crate. |
| 1254 | /// |
| 1255 | /// The seam the browser uses: `envelope_for` holds no key material, `signing_input` says what |
| 1256 | /// to sign, and `assemble` takes the signature back. The signer here stands in for the one that |
| 1257 | /// lives outside. |
| 1258 | #[test] |
| 1259 | fn test_signing_happens_elsewhere_20() -> Outcome<()> { |
| 1260 | let signer = SignatureScheme::new_ed25519(); |
| 1261 | let author = match res!(signer.get_public_key()) { |
| 1262 | Some(pk) => pk.to_vec(), |
| 1263 | None => return Err(err!("A fresh signer holds no public key."; Test, Bug)), |
| 1264 | }; |
| 1265 | let bytes = res!(sample_post().encode()); |
| 1266 | let mut env = res!(envelope_for(&bytes, SCHEMA_POST, &author, TIME)); |
| 1267 | |
| 1268 | // Unsigned, and refused as such rather than by a special case. |
| 1269 | assert!(env.sig.is_empty()); |
| 1270 | match read_artefact(&res!(assemble(&env, &bytes))) { |
| 1271 | Ok(_) => return Err(err!("An unsigned artefact was read."; Test, Invalid)), |
| 1272 | Err(e) => assert!(fmt!("{}", e).contains("assembled before it was signed")), |
| 1273 | } |
| 1274 | |
| 1275 | env.sig = res!(signer.sign(&env.signing_input())); |
| 1276 | let back = res!(read_artefact(&res!(assemble(&env, &bytes)))); |
| 1277 | assert_eq!(back.env().schema, SCHEMA_POST); |
| 1278 | |
| 1279 | // The same artefact the all-in-one path writes, byte for byte. |
| 1280 | assert_eq!( |
| 1281 | res!(assemble(&env, &bytes)), |
| 1282 | res!(write_artefact(&Payload::Post(sample_post()), &signer, TIME)), |
| 1283 | ); |
| 1284 | Ok(()) |
| 1285 | } |
| 1286 | |
| 1287 | /// A schema this build does not implement is refused, and named. |
| 1288 | /// |
| 1289 | /// This was `daimond/share/0` while that schema was reserved and nothing read one. It is now |
| 1290 | /// implemented (see [`crate::share`]), so the claim is carried by a name that really is |
| 1291 | /// unimplemented: an artefact declaring one is REFUSED rather than read as whichever schema is |
| 1292 | /// nearest, and the refusal says which schema it was handed. |
| 1293 | #[test] |
| 1294 | fn test_an_unimplemented_schema_is_refused_22() -> Outcome<()> { |
| 1295 | /// A schema no validator in this build reads, matching the `foreign_schema` fixture. |
| 1296 | const SCHEMA_FOREIGN: &'static str = "oxeweb/cmd/0"; |
| 1297 | |
| 1298 | let signer = SignatureScheme::new_ed25519(); |
| 1299 | let bytes = res!(sample_post().encode()); |
| 1300 | let env = res!(seal(&bytes, SCHEMA_FOREIGN, &signer, TIME)); |
| 1301 | let buf = res!(assemble(&env, &bytes)); |
| 1302 | |
| 1303 | // Steps 1 to 5 touch no content, so the container is sound and the signature is the |
| 1304 | // author's: the refusal can only be the schema's. |
| 1305 | res!(verify_only(&buf)); |
| 1306 | match read_artefact(&buf) { |
| 1307 | Ok(_) => Err(err!( |
| 1308 | "An artefact declaring the unimplemented schema '{}' was read.", SCHEMA_FOREIGN; |
| 1309 | Test, Invalid)), |
| 1310 | Err(e) => { |
| 1311 | assert!(fmt!("{}", e).contains(SCHEMA_FOREIGN), |
| 1312 | "The refusal does not name the schema it refused: {}", e); |
| 1313 | Ok(()) |
| 1314 | }, |
| 1315 | } |
| 1316 | } |
| 1317 | |
| 1318 | /// A whole file carrying a share can be written and read back. |
| 1319 | /// |
| 1320 | /// The third payload schema through the same container, which is the claim worth making: the |
| 1321 | /// envelope, the address, the signature and every rule of §3 are the container's and did not |
| 1322 | /// change to admit it. |
| 1323 | #[test] |
| 1324 | fn test_a_share_is_a_whole_artefact_24() -> Outcome<()> { |
| 1325 | let signer = SignatureScheme::new_ed25519(); |
| 1326 | let share = crate::share::Share::new( |
| 1327 | fmt!("Life log"), |
| 1328 | vec![0xA1; crate::share::limit::KEY_BYTES], |
| 1329 | vec![0xB2; crate::share::limit::NONCE_BYTES], |
| 1330 | None, |
| 1331 | vec![ |
| 1332 | crate::share::File { path: fmt!("crystal.html"), body: b"<p>hi</p>".to_vec() }, |
| 1333 | crate::share::File { path: fmt!("crystal.json"), body: b"{}".to_vec() }, |
| 1334 | ], |
| 1335 | ); |
| 1336 | let buf = res!(write_artefact(&Payload::Share(share.clone()), &signer, TIME)); |
| 1337 | let back = res!(read_artefact(&buf)); |
| 1338 | assert_eq!(back.env().schema, SCHEMA_SHARE); |
| 1339 | assert_eq!(*back.payload(), Payload::Share(share)); |
| 1340 | |
| 1341 | // The consent bit survives the round trip, which is the whole reason it is in the payload |
| 1342 | // rather than in a wrapper: the receiver checks the SENDER's mark. |
| 1343 | match back.payload() { |
| 1344 | Payload::Share(s) => assert!(s.code, |
| 1345 | "A share carrying a page came back saying it carries no code."), |
| 1346 | other => return Err(err!( |
| 1347 | "A share read back as a {}.", other.schema(); Test, Invalid)), |
| 1348 | } |
| 1349 | |
| 1350 | // THE CONSENT BIT CANNOT BE SET BY WHATEVER CARRIES THE SHARE. The payload is what the |
| 1351 | // hash covers and the signature commits to, so a relay that set `code` on a share whose |
| 1352 | // author said there was none would have to forge a signature to go with it. Built by hand, |
| 1353 | // because a payload whose bit disagrees with its files is one the schema refuses to WRITE |
| 1354 | // and a carrier is not held to the schema. |
| 1355 | let plain = crate::share::Share::new( |
| 1356 | fmt!("Sourdough"), |
| 1357 | vec![0xA1; crate::share::limit::KEY_BYTES], |
| 1358 | vec![0xB2; crate::share::limit::NONCE_BYTES], |
| 1359 | None, |
| 1360 | vec![crate::share::File { path: fmt!("crystal.json"), body: b"{}".to_vec() }], |
| 1361 | ); |
| 1362 | assert!(!plain.code, "A share of one data file claims to carry code."); |
| 1363 | let good = res!(plain.encode()); |
| 1364 | let signed = res!(write_artefact(&Payload::Share(plain.clone()), &signer, TIME)); |
| 1365 | let mut m = match res!(plain.to_dat()) { |
| 1366 | Dat::Map(m) => m, |
| 1367 | other => return Err(err!( |
| 1368 | "A share encodes as a map, and this is a {:?}.", other.kind(); Test, Bug)), |
| 1369 | }; |
| 1370 | m.insert(dat!(crate::share::KEY_CODE), Dat::Bool(true)); |
| 1371 | let lied = res!(Dat::Map(m).to_bytes(Vec::new())); |
| 1372 | assert_eq!(lied.len(), good.len(), |
| 1373 | "The tampered payload is a different length, so this would be testing the length \ |
| 1374 | rather than the bit."); |
| 1375 | let mut carried = signed.clone(); |
| 1376 | let start = carried.len() - good.len(); |
| 1377 | carried[start..].copy_from_slice(&lied); |
| 1378 | match read_artefact(&carried) { |
| 1379 | Ok(_) => return Err(err!( |
| 1380 | "A share whose consent bit was set after signing was read, so the bit is \ |
| 1381 | something its carrier can decide."; Test, Invalid)), |
| 1382 | Err(e) => assert!(fmt!("{}", e).contains("hashes to"), |
| 1383 | "The refusal is not the address's: {}", e), |
| 1384 | } |
| 1385 | |
| 1386 | // And a share cannot be re-labelled as a post after signing, for the same reason a post |
| 1387 | // cannot be re-labelled as a card: the schema is inside the signing input. |
| 1388 | let bytes = res!(res!(match back.payload() { |
| 1389 | Payload::Share(s) => Ok(s.clone()), |
| 1390 | _ => Err(err!("Not a share."; Test, Bug)), |
| 1391 | }).encode()); |
| 1392 | let mut env = res!(seal(&bytes, SCHEMA_SHARE, &signer, TIME)); |
| 1393 | env.schema = SCHEMA_POST.to_string(); |
| 1394 | match read_artefact(&res!(assemble(&env, &bytes))) { |
| 1395 | Ok(_) => Err(err!("A share re-labelled as a post was read."; Test, Invalid)), |
| 1396 | Err(_) => Ok(()), |
| 1397 | } |
| 1398 | } |
| 1399 | |
| 1400 | /// The signing input begins with the schema's length, and each name gives a distinct preimage. |
| 1401 | /// |
| 1402 | /// The §18 property, and the reason a third schema name could be added without touching a byte |
| 1403 | /// of anything already signed: the schema reaches the signing input length-prefixed (§1.3), so |
| 1404 | /// a name that did not exist when a post was signed cannot change how that post's preimage is |
| 1405 | /// read. |
| 1406 | /// |
| 1407 | /// **What this test does and does not prove.** It proves the prefix is written, and written |
| 1408 | /// correctly, which is falsifiable: remove the prefix from `signing_input` and this fails. It |
| 1409 | /// does NOT exhibit a collision, and an honest note about that is worth more than a test that |
| 1410 | /// pretends to. In v0 the schema and the hash are not adjacent — the two scheme ids and the |
| 1411 | /// time sit between them — and a schema is a `String`, so for a schema to swallow the bytes |
| 1412 | /// that follow it, those bytes would have to be valid UTF-8. The v0 signature scheme id begins |
| 1413 | /// `0xF5`, which is not. So the ambiguity is unreachable in v0 by accident of the constants, |
| 1414 | /// and the prefix is what makes it unreachable by design: change a scheme id, add a schema |
| 1415 | /// whose name is a prefix of another, or move a field, and the accident evaporates while the |
| 1416 | /// prefix does not. |
| 1417 | #[test] |
| 1418 | fn test_the_schema_is_length_prefixed_23() -> Outcome<()> { |
| 1419 | let env = |schema: &str| Envelope { |
| 1420 | schema: schema.to_string(), |
| 1421 | author: vec![0u8; 32], |
| 1422 | sig_scheme: envelope::SIG_SCHEME_ED25519, |
| 1423 | hash_scheme: envelope::HASH_SCHEME_SHA3_256, |
| 1424 | time: TIME, |
| 1425 | hash: vec![b'A'; 32], |
| 1426 | sig: Vec::new(), |
| 1427 | tree_len: 1, |
| 1428 | }; |
| 1429 | for schema in [SCHEMA_POST, SCHEMA_CARD, crate::SCHEMA_SHARE, crate::SCHEMA_DOC] { |
| 1430 | let input = env(schema).signing_input(); |
| 1431 | assert!(input.len() > 4, "The signing input of '{}' is too short to hold a length.", |
| 1432 | schema); |
| 1433 | let declared = u32::from_be_bytes([input[0], input[1], input[2], input[3]]); |
| 1434 | assert_eq!(declared as usize, schema.len(), |
| 1435 | "The signing input of '{}' does not begin with the schema's length. Without it, \ |
| 1436 | `schema` and `hash` are two variable-length fields with only fixed-width ones \ |
| 1437 | between them, and where a field ends stops being a fact about the bytes.", schema); |
| 1438 | assert_eq!(&input[4..4 + schema.len()], schema.as_bytes(), |
| 1439 | "The schema does not follow its own length."); |
| 1440 | } |
| 1441 | |
| 1442 | // And the reserved name is a distinct preimage from the two that are implemented, so an |
| 1443 | // artefact of a schema that does not exist yet cannot be read as one that does. |
| 1444 | let post = env(SCHEMA_POST).signing_input(); |
| 1445 | for other in [SCHEMA_CARD, crate::SCHEMA_SHARE, crate::SCHEMA_DOC] { |
| 1446 | assert_ne!(post, env(other).signing_input()); |
| 1447 | } |
| 1448 | Ok(()) |
| 1449 | } |
| 1450 | |
| 1451 | /// An author key of the wrong width is refused before an artefact is built around it. |
| 1452 | #[test] |
| 1453 | fn test_an_author_key_has_one_width_21() -> Outcome<()> { |
| 1454 | let bytes = res!(sample_post().encode()); |
| 1455 | match envelope_for(&bytes, SCHEMA_POST, &[0u8; 31], TIME) { |
| 1456 | Ok(_) => Err(err!("A 31 byte author key was accepted."; Test, Invalid)), |
| 1457 | Err(e) => { |
| 1458 | assert!(fmt!("{}", e).contains("31 bytes")); |
| 1459 | Ok(()) |
| 1460 | }, |
| 1461 | } |
| 1462 | } |
| 1463 | } |