Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_pearlite/tests/collab_security.rs

13.3 KiB, 1 run

created by r1870400018:59042, 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//! The malicious-peer regression suite: for each shape the security review named, a test that proves
2//! the attack fails. A signed operation is attacker-controlled input from an untrusted peer, and every
3//! test below hands the fold or the seal exactly what a hostile peer would and asserts it is refused,
4//! attributed to the real signer, or skipped -- never trusted, and never able to wedge the whole fold.
5//!
6//! These are pure-crypto unit tests: no store is stood up, because none of the shapes needs one. The
7//! hub's own verify-and-bound ingest is exercised by `collab_roundtrip.rs`; here the concern is the
8//! signing core and the fold.
9
10use oxedyne_fe2o3_austenite::emit::pearl::{
11 Annotation,
12 AnnotationKind,
13};
14use oxedyne_fe2o3_core::prelude::*;
15use oxedyne_fe2o3_jdat::prelude::*;
16use oxedyne_fe2o3_net::ecdsa::P256KeyPair;
17use oxedyne_fe2o3_ore::{
18 id::OpId,
19 op::{
20 Op,
21 Record,
22 Settled,
23 },
24};
25use oxedyne_fe2o3_pearlite::collab::{
26 fold,
27 op,
28 replica_of,
29 sign::{
30 self,
31 Opened,
32 },
33 DocId,
34 Keyring,
35};
36
37
38/// An annotation on `anchor` claiming `author` in its body, for the given document.
39fn annotation(anchor: &str, author: &str) -> Annotation {
40 Annotation::new(anchor, AnnotationKind::Note, "the note text", author, "2026-09-20T12:00:00Z")
41}
42
43/// Seals `op` as a root record under the replica the signer's own key derives, the way an honest peer
44/// would, and returns the envelope.
45fn seal_honest(doc: &DocId, key: &P256KeyPair, counter: u64, op: Op)
46 -> Outcome<oxedyne_fe2o3_ore::envelope::Envelope>
47{
48 let rec = Record::root(OpId::new(replica_of(&key.public_key(), doc), counter), op);
49 let sig = res!(key.sign(&res!(sign::signing_bytes(&rec))));
50 sign::seal(&rec, key.public_key(), sig, doc)
51}
52
53/// Opens a freshly sealed operation, for `doc`, into the verified, signer-bound form the fold consumes.
54fn opened(doc: &DocId, key: &P256KeyPair, counter: u64, op: Op) -> Outcome<Opened> {
55 sign::open(&res!(seal_honest(doc, key, counter, op)), doc)
56}
57
58/// A proposal opening an annotation stamped for `doc`.
59fn proposal(doc: &DocId, key: &P256KeyPair, counter: u64, anchor: &str, author: &str, time: u64)
60 -> Outcome<Opened>
61{
62 opened(doc, key, counter, res!(op::create(doc, &annotation(anchor, author), time)))
63}
64
65
66/// A forged author string in the body loses to the signer: the folded author is the keyring's name for
67/// the signing key, never the string the body claimed.
68#[test]
69fn a_forged_author_string_is_ignored_and_the_signer_is_shown() -> Outcome<()> {
70 let doc = DocId::new("doc-forge");
71 let attacker = res!(P256KeyPair::generate());
72
73 // The body claims to be from "alice"; the attacker holds their own key, not alice's.
74 let op = res!(proposal(&doc, &attacker, 1, "block-1", "alice", 100));
75
76 // An empty keyring shows the fingerprint; either way it is derived from the signing key.
77 let report = fold::fold(&[op], &doc, &Keyring::new());
78 req!(1, report.annotations.len());
79 let shown = &report.annotations[0].author;
80 assert_ne!(shown, "alice", "the forged body author string must not be shown");
81 assert_eq!(shown, &oxedyne_fe2o3_pearlite::collab::fingerprint(&attacker.public_key()),
82 "an unknown signer is shown by its own fingerprint");
83 Ok(())
84}
85
86/// A record claiming a replica identity its signer's key does not derive is refused at the seal, and a
87/// hand-built envelope carrying one is refused at the open -- so id-squatting cannot occur.
88#[test]
89fn a_squatted_replica_id_is_refused_at_seal_and_open() -> Outcome<()> {
90 let attacker = res!(P256KeyPair::generate());
91 let victim = res!(P256KeyPair::generate());
92 let doc = DocId::new("doc-squat");
93 let victim_rep = replica_of(&victim.public_key(), &doc);
94
95 // The attacker builds a record under the victim's replica and signs it with their own key.
96 let op = res!(op::create(&doc, &annotation("block-1", "victim"), 100));
97 let rec = Record::root(OpId::new(victim_rep, 1), op);
98 let sig = res!(attacker.sign(&res!(sign::signing_bytes(&rec))));
99
100 // Sealing with the attacker's key is refused: the header's replica is not the one that key derives.
101 assert!(sign::seal(&rec, attacker.public_key(), sig.clone(), &doc).is_err(),
102 "a record whose replica is not the signer's must not seal");
103
104 // And a hand-built envelope carrying the same claim is refused at the open, though its signature is
105 // perfectly valid over its bytes.
106 let payload = res!(sign::signing_bytes(&rec));
107 let forged = oxedyne_fe2o3_ore::envelope::Envelope::new(payload, attacker.public_key(), sig);
108 assert!(res!(sign::verify(&forged)), "the forged envelope's signature does hold over its bytes");
109 assert!(sign::open(&forged, &doc).is_err(),
110 "opening an envelope whose replica is not its signer's must be refused");
111 Ok(())
112}
113
114/// A counter-poison operation -- a forged op at a huge counter under the victim's replica, which would
115/// wedge every reader's log -- never reaches the fold, because it cannot be opened: its replica is not
116/// the forger's.
117#[test]
118fn a_counter_poison_op_cannot_be_opened() -> Outcome<()> {
119 let attacker = res!(P256KeyPair::generate());
120 let victim = res!(P256KeyPair::generate());
121
122 let doc = DocId::new("doc-poison");
123 let op = res!(op::create(&doc, &annotation("block-1", "victim"), 100));
124 // The poison: the victim's replica, a counter far past anything they will mint.
125 let rec = Record::root(OpId::new(replica_of(&victim.public_key(), &doc), 1_000_000_000), op);
126 let sig = res!(attacker.sign(&res!(sign::signing_bytes(&rec))));
127 let env = oxedyne_fe2o3_ore::envelope::Envelope::new(
128 res!(sign::signing_bytes(&rec)), attacker.public_key(), sig);
129
130 assert!(sign::open(&env, &doc).is_err(),
131 "a counter-poison op forging the victim's replica is refused at the open and never folds");
132 Ok(())
133}
134
135/// A deeply nested annotation body is refused by the bounded decoder rather than recursed into: the
136/// decode terminates, and a validly-signed proposal carrying one is skipped, not fatal to the fold.
137#[test]
138fn a_deeply_nested_body_is_refused_and_skipped_not_fatal() -> Outcome<()> {
139 let doc = DocId::new("doc-deep");
140 let good_key = res!(P256KeyPair::generate());
141 let bad_key = res!(P256KeyPair::generate());
142
143 // A body nested a thousand deep, far past the decode limit. The bytes are built from the inside
144 // out rather than encoded, because the encoder recurses just as the decoder does and would itself
145 // overflow building the bomb -- which a hostile peer is under no obligation to use.
146 let mut deep = vec![Dat::EMPTY_CODE];
147 for _ in 0..1_000 {
148 let payload_len = deep.len();
149 let mut outer = vec![Dat::LIST_CODE];
150 outer = res!(Dat::C64(payload_len as u64).to_bytes(outer));
151 outer.append(&mut deep);
152 deep = outer;
153 }
154
155 // Decoding it directly terminates and errors rather than exhausting the stack.
156 assert!(op::annotation_from_body(&deep).is_err(),
157 "a deeply nested body is refused by the bounded decoder");
158
159 // A validly-signed proposal carrying it is skipped; a good proposal beside it still folds.
160 let bad = res!(opened(&doc, &bad_key, 1, Op::Proposal {
161 title: "block-x".to_string(),
162 body: deep,
163 voice: op::VOICE.to_string(),
164 time: 50,
165 }));
166 let good = res!(proposal(&doc, &good_key, 1, "block-1", "author", 100));
167
168 let report = fold::fold(&[bad, good], &doc, &Keyring::new());
169 req!(1, report.annotations.len(), "the good annotation folds despite the poisoned one");
170 req!(1, report.skipped.len(), "the deeply nested body is reported as skipped");
171 Ok(())
172}
173
174/// A validly-signed proposal whose body is not an annotation at all is skipped and reported, and does
175/// not make the fold fail for the good annotations.
176#[test]
177fn a_malformed_signed_body_is_skipped_not_fatal() -> Outcome<()> {
178 let doc = DocId::new("doc-malformed");
179 let good_key = res!(P256KeyPair::generate());
180 let bad_key = res!(P256KeyPair::generate());
181
182 // A well-formed daticle that is simply not an Annotation map.
183 let not_ann = res!(dat!("just a string, not an annotation").to_bytes(Vec::new()));
184 let bad = res!(opened(&doc, &bad_key, 1, Op::Proposal {
185 title: "block-x".to_string(),
186 body: not_ann,
187 voice: op::VOICE.to_string(),
188 time: 50,
189 }));
190 let good = res!(proposal(&doc, &good_key, 1, "block-1", "author", 100));
191
192 let report = fold::fold(&[bad, good], &doc, &Keyring::new());
193 req!(1, report.annotations.len(), "the good annotation survives a malformed neighbour");
194 req!(1, report.skipped.len(), "the malformed body is reported as skipped");
195 Ok(())
196}
197
198/// An annotation stamped for one document and replayed onto another -- where the same block address
199/// happens to exist -- is refused by the fold, which folds only bodies stamped for the document in hand.
200#[test]
201fn a_cross_document_replay_is_rejected() -> Outcome<()> {
202 let doc_a = DocId::new("doc-A");
203 let doc_b = DocId::new("doc-B");
204 let key = res!(P256KeyPair::generate());
205
206 // Signed for document A, then folded under document B.
207 let op = res!(proposal(&doc_a, &key, 1, "shared-block", "author", 100));
208 let report = fold::fold(&[op], &doc_b, &Keyring::new());
209 req!(0, report.annotations.len(), "an annotation stamped for another document does not fold");
210 req!(1, report.skipped.len(), "and it is reported as skipped");
211 Ok(())
212}
213
214/// An amendment from anyone but the proposal's own signer is refused, so a stranger cannot edit another
215/// author's annotation. The original stands.
216#[test]
217fn a_foreign_amendment_is_rejected() -> Outcome<()> {
218 let doc = DocId::new("doc-amend");
219 let owner = res!(P256KeyPair::generate());
220 let stranger = res!(P256KeyPair::generate());
221
222 let create = res!(proposal(&doc, &owner, 1, "block-1", "owner", 100));
223 let on = create.record().id();
224
225 // The stranger, with a perfectly valid key and signature of their own, amends the owner's proposal.
226 let amend = res!(opened(&doc, &stranger, 1, res!(op::edit(
227 &doc, on, &annotation("block-1", "stranger's rewrite"), 200))));
228
229 let report = fold::fold(&[create, amend], &doc, &Keyring::new());
230 req!(1, report.annotations.len());
231 assert_eq!(report.annotations[0].payload, "the note text",
232 "the stranger's amendment must not replace the owner's annotation body");
233 req!(1, report.skipped.len(), "the foreign amendment is reported as skipped");
234 Ok(())
235}
236
237/// A proposal its own author withdraws stays withdrawn: a later amendment, even from that same author,
238/// cannot resurrect it. Tombstone sticks.
239#[test]
240fn a_withdrawn_proposal_cannot_be_resurrected_by_an_amendment() -> Outcome<()> {
241 let doc = DocId::new("doc-tomb");
242 let owner = res!(P256KeyPair::generate());
243
244 let create = res!(proposal(&doc, &owner, 1, "block-1", "owner", 100));
245 let on = create.record().id();
246
247 // The author withdraws it, then -- later -- tries to amend it back into existence.
248 let withdraw = res!(opened(&doc, &owner, 2, Op::Settled {
249 on,
250 state: Settled::Declined,
251 mark: None,
252 time: 200,
253 }));
254 let amend = res!(opened(&doc, &owner, 3, res!(op::edit(
255 &doc, on, &annotation("block-1", "back from the dead"), 300))));
256 let amend_id = amend.record().id();
257
258 let report = fold::fold(&[create, withdraw, amend], &doc, &Keyring::new());
259 req!(0, report.annotations.len(), "a withdrawn proposal stays withdrawn despite a later amendment");
260 assert!(report.skipped.iter().any(|s| s.id == amend_id),
261 "the resurrection attempt is reported as skipped");
262 Ok(())
263}
264
265/// A validly-signed Settled (and a Said), authored by a key in one document, cannot be replayed into
266/// another to withdraw or disturb that key's annotation there -- even though the signature holds and
267/// the key is the same. The per-document replica binding refuses it at the open, and the fold skips it
268/// belt-and-braces. This is the residual the earlier body-`doc_id` stamp missed: a Settled carries no
269/// body to stamp, and its `on` identifier is not inert across documents, Ore minting counters per log.
270#[test]
271fn a_settled_or_said_from_another_document_cannot_be_replayed() -> Outcome<()> {
272 let doc_a = DocId::new("doc-A");
273 let doc_b = DocId::new("doc-B");
274 let owner = res!(P256KeyPair::generate());
275
276 // The owner's genuine annotation lives in document B.
277 let create_b = res!(proposal(&doc_b, &owner, 1, "block-1", "owner", 100));
278 let on_b = create_b.record().id();
279
280 // A settlement the owner validly signed in document A, naming that identifier. A relay peer of A
281 // lifts it and tries to apply it to B, which -- before the per-document binding -- would have
282 // declined and tombstoned the owner's B annotation, unrecoverable.
283 let settled_a_env = res!(seal_honest(&doc_a, &owner, 2, Op::Settled {
284 on: on_b,
285 state: Settled::Declined,
286 mark: None,
287 time: 200,
288 }));
289 // Replayed into B, it is refused at the open: its replica is the owner's under A, not under B.
290 assert!(sign::open(&settled_a_env, &doc_b).is_err(),
291 "a settlement authored in another document must not open here");
292
293 // Belt and braces: opened legitimately for its own document A, then handed to a fold for document
294 // B, it is skipped -- not applied -- so the victim's annotation stands.
295 let settled_a = res!(sign::open(&settled_a_env, &doc_a));
296 let settled_id = settled_a.record().id();
297 let report = fold::fold(&[create_b, settled_a], &doc_b, &Keyring::new());
298 req!(1, report.annotations.len(), "the victim's annotation survives a cross-document settlement");
299 assert!(report.skipped.iter().any(|s| s.id == settled_id),
300 "the replayed settlement is reported as skipped, not applied");
301
302 // The same holds for a reply, which also carries no body to stamp.
303 let said_a_env = res!(seal_honest(&doc_a, &owner, 3, op::reply(on_b, "a reply from doc A", 210)));
304 assert!(sign::open(&said_a_env, &doc_b).is_err(),
305 "a reply authored in another document must not open here either");
306 Ok(())
307}