Oregami
Repositories/oxedyne/fe2o3

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
22use crate::collab::{
23 replica_of,
24 DocId,
25 DECODE_LIMITS,
26};
27
28use oxedyne_fe2o3_ore::{
29 envelope::Envelope,
30 op::Record,
31};
32
33use oxedyne_fe2o3_core::prelude::*;
34use oxedyne_fe2o3_jdat::prelude::*;
35use 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)]
49pub struct Opened {
50 record: Record,
51 signer: Vec<u8>, // the 65-byte SEC1 public key that signed the record
52}
53
54impl 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`].
67pub 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.
79fn 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.
102pub 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.
117pub 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.
129pub 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}