oxedyne/fe2o3/fe2o3_pearlite/src/collab/sign.rs
6.6 KiB, 23 runs
created by r1870400018:58604, 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 | //! Sealing an annotation operation into a signed Ore envelope, and checking one, with a P-256 |
| 2 | //! (WebCrypto ECDSA / SHA-256) signature. |
| 3 | //! |
| 4 | //! # Where the key lives |
| 5 | //! |
| 6 | //! Nowhere here. An author's private key stays with the author: a browser's WebCrypto `CryptoKey` |
| 7 | //! minted `extractable: false`, or a native P-256 key on a server. This module never sees it. What it |
| 8 | //! is handed is the public key and a detached signature over the operation's canonical bytes -- the |
| 9 | //! two things an [`Envelope`] carries beside the payload -- so the same code path serves a browser and |
| 10 | //! a server, and the signing algorithm is decided by whoever holds the key rather than here. |
| 11 | //! |
| 12 | //! # What is signed |
| 13 | //! |
| 14 | //! The whole record: the operation together with the header naming it and its parents, in the same |
| 15 | //! canonical binary daticle form [`Envelope::seal_record`] uses. An operation lifted out of its |
| 16 | //! history, relabelled or reparented, does not verify. |
| 17 | //! |
| 18 | //! The verifier is [`oxedyne_fe2o3_crypto::p256`], pure Rust and wasm-clean, so an operation is checked |
| 19 | //! by identical code on a server and inside a downloaded reader -- the reader never has to trust a |
| 20 | //! server to have checked provenance for it. |
| 21 | |
| 22 | use crate::collab::{ |
| 23 | replica_of, |
| 24 | DocId, |
| 25 | DECODE_LIMITS, |
| 26 | }; |
| 27 | |
| 28 | use oxedyne_fe2o3_ore::{ |
| 29 | envelope::Envelope, |
| 30 | op::Record, |
| 31 | }; |
| 32 | |
| 33 | use oxedyne_fe2o3_core::prelude::*; |
| 34 | use oxedyne_fe2o3_jdat::prelude::*; |
| 35 | use oxedyne_fe2o3_crypto::p256::verify_p256_sha256_fixed; |
| 36 | |
| 37 | /// A verified record together with the public key that signed it. |
| 38 | /// |
| 39 | /// The signer travels with the record because it, and not anything in the body, is the operation's |
| 40 | /// identity: who authored an annotation is who holds the key, and the fold reads the author from here |
| 41 | /// rather than from a free-text field a body could claim anything in. |
| 42 | /// |
| 43 | /// The fields are private and reached only through the accessors, because the type carries an |
| 44 | /// invariant a struct literal could quietly break: every [`Opened`] that exists is one [`open`] |
| 45 | /// returned, so its signature verified and its header names the replica [`replica_of`] derives from |
| 46 | /// this key under the document it was opened for. A caller holding one need not re-establish either, |
| 47 | /// and no caller -- trusted or not -- can mint one that skipped the check. |
| 48 | #[derive(Clone, Debug, Eq, PartialEq)] |
| 49 | pub struct Opened { |
| 50 | record: Record, |
| 51 | signer: Vec<u8>, // the 65-byte SEC1 public key that signed the record |
| 52 | } |
| 53 | |
| 54 | impl Opened { |
| 55 | pub fn record(&self) -> &Record { |
| 56 | &self.record |
| 57 | } |
| 58 | |
| 59 | pub fn signer(&self) -> &[u8] { |
| 60 | &self.signer |
| 61 | } |
| 62 | } |
| 63 | |
| 64 | /// The exact bytes an author signs for `rec`: its canonical binary daticle form, identical to what |
| 65 | /// [`Envelope::seal_record`] places in an envelope's payload, so the header is covered along with the |
| 66 | /// operation. A caller with the private key signs these and hands the signature to [`seal`]. |
| 67 | pub fn signing_bytes(rec: &Record) -> Outcome<Vec<u8>> { |
| 68 | Ok(res!(rec.to_dat().to_bytes(Vec::new()))) |
| 69 | } |
| 70 | |
| 71 | /// Refuses a record whose header names a replica that is not the one its signer's key derives under |
| 72 | /// the document in hand. |
| 73 | /// |
| 74 | /// This is the whole of the identity binding: a replica identity is not a peer's to choose, it is |
| 75 | /// [`replica_of`] of the key and the document, so a record claiming another author's replica -- to |
| 76 | /// pre-empt their next identifier or to poison their counter -- or one lifted from another document |
| 77 | /// and replayed here carries a replica the impostor's own key does not derive under this document, and |
| 78 | /// is refused before it can be sealed or folded. |
| 79 | fn check_replica_binding(rec: &Record, pubkey: &[u8], doc: &DocId) -> Outcome<()> { |
| 80 | let named = rec.id().replica; |
| 81 | let derived = replica_of(pubkey, doc); |
| 82 | if named != derived { |
| 83 | return Err(err!( |
| 84 | "The record is identified {} in document {}, whose replica {} is not the replica {} \ |
| 85 | derived from the signer's key under that document; a replica identity is bound to the key \ |
| 86 | and the document it signs in, not chosen, and an operation from another document does not \ |
| 87 | replay here.", |
| 88 | rec.id(), doc, named, derived; |
| 89 | Invalid, Input, Security, Mismatch)); |
| 90 | } |
| 91 | Ok(()) |
| 92 | } |
| 93 | |
| 94 | /// Seals a record into an envelope carrying a P-256 signature, for the document `doc`. |
| 95 | /// |
| 96 | /// `pubkey` is the 65-byte uncompressed SEC1 point and `sig` the 64-byte `r || s` over |
| 97 | /// [`signing_bytes`]`(rec)` -- exactly what a browser's WebCrypto key, or a native P-256 signer, |
| 98 | /// yields. The signature is checked here, and the record's replica identity is checked to be the one |
| 99 | /// the key derives under `doc`, before the envelope is returned, so a mis-signed or misattributed |
| 100 | /// operation never reaches the hub and every stored envelope is one that verifies against, and is |
| 101 | /// named for, its own enclosed key in its own document. |
| 102 | pub fn seal(rec: &Record, pubkey: Vec<u8>, sig: Vec<u8>, doc: &DocId) -> Outcome<Envelope> { |
| 103 | let payload = res!(signing_bytes(rec)); |
| 104 | if !res!(verify_p256_sha256_fixed(&pubkey, &payload, &sig)) { |
| 105 | return Err(err!( |
| 106 | "The P-256 signature does not verify over the record's bytes, so the envelope would not \ |
| 107 | be attributable to its enclosed public key."; |
| 108 | Invalid, Input, Security, Mismatch)); |
| 109 | } |
| 110 | res!(check_replica_binding(rec, &pubkey, doc)); |
| 111 | Ok(Envelope::new(payload, pubkey, sig)) |
| 112 | } |
| 113 | |
| 114 | /// Checks an envelope's P-256 signature against its own enclosed public key. `false` is a signature |
| 115 | /// that does not hold -- a tampered payload, key or signature -- and an error is a check that could not |
| 116 | /// be made. |
| 117 | pub fn verify(env: &Envelope) -> Outcome<bool> { |
| 118 | verify_p256_sha256_fixed(env.signer(), env.payload(), env.signature()) |
| 119 | } |
| 120 | |
| 121 | /// Verifies an envelope and, only if it holds, decodes the record inside it under the collaboration |
| 122 | /// layer's decode bounds and checks the record's replica against its signer under the document `doc`. |
| 123 | /// |
| 124 | /// An envelope whose signature does not verify, whose payload is a hostile encoding, or whose header |
| 125 | /// claims a replica its key does not derive under `doc` -- which is what an operation lifted from |
| 126 | /// another document does -- yields an error rather than a record, so a caller cannot fold in an |
| 127 | /// operation it has not attributed to this document. The signer travels back with the record in the |
| 128 | /// [`Opened`], since that key -- not anything in the body -- is the operation's author. |
| 129 | pub fn open(env: &Envelope, doc: &DocId) -> Outcome<Opened> { |
| 130 | if !res!(verify(env)) { |
| 131 | return Err(err!( |
| 132 | "The envelope's P-256 signature does not verify against its enclosed public key, so its \ |
| 133 | operation is not attributable and will not be folded."; |
| 134 | Invalid, Input, Security, Mismatch)); |
| 135 | } |
| 136 | let record = res!(env.peek_record_limited(&DECODE_LIMITS)); |
| 137 | res!(check_replica_binding(&record, env.signer(), doc)); |
| 138 | Ok(Opened { record, signer: env.signer().to_vec() }) |
| 139 | } |