1#[path = "authority_batches.rs"]
2mod batching;
3pub use batching::{AuthorityBatches, batches};
4pub use crypto::thread_authority_admission::SignedAuthorityAdmission;
6use crypto::{Signer, thread_operation::SignedOperation};
7use heddle_object_model::object::thread_authority_admission::FORMAT;
8pub use heddle_object_model::object::{
9 thread_authority_admission::ThreadAuthorityAdmission,
10 thread_replication::integration::TrustedHostedExecutor,
11};
12
13use crate::{contract as wire, transport::Error};
14
15pub fn sign(
17 value: &ThreadAuthorityAdmission,
18 signer: &impl Signer,
19) -> Result<wire::SignedRecord, Error> {
20 encode(
21 &SignedAuthorityAdmission::sign(value, signer)
22 .map_err(|_| Error::Protocol("authority admission signing failed"))?,
23 )
24}
25pub fn verify(
27 receipt: &wire::SignedRecord,
28 original: &SignedOperation,
29 trust: &TrustedHostedExecutor,
30) -> Result<ThreadAuthorityAdmission, Error> {
31 decode(receipt)?.verify(original, trust).map_err(|_| {
32 Error::Protocol("authority receipt differs from original operation or pinned executor")
33 })
34}
35pub fn verify_signature(receipt: &wire::SignedRecord) -> Result<ThreadAuthorityAdmission, Error> {
37 decode(receipt)?
38 .verify_signature()
39 .map_err(|_| Error::Protocol("invalid authority admission signature"))
40}
41pub fn decode(receipt: &wire::SignedRecord) -> Result<SignedAuthorityAdmission, Error> {
42 if receipt.format != FORMAT {
43 return Err(Error::Protocol("invalid authority admission format"));
44 }
45 let [signature] = receipt.signatures.as_slice() else {
46 return Err(Error::Protocol(
47 "authority admission requires one executor signature",
48 ));
49 };
50 let value = ThreadAuthorityAdmission::decode(&receipt.canonical_record)
51 .map_err(|_| Error::Protocol("invalid canonical authority admission"))?;
52 if signature.public_key != value.executor {
53 return Err(Error::Protocol(
54 "authority admission signature key differs from executor",
55 ));
56 }
57 let signed = SignedAuthorityAdmission {
58 boundary_acceptance: None,
59 canonical: receipt.canonical_record.clone(),
60 signature: signature.signature.clone(),
61 };
62 signed
63 .verify_signature()
64 .map_err(|_| Error::Protocol("invalid authority admission signature"))?;
65 Ok(signed)
66}
67pub fn encode(receipt: &SignedAuthorityAdmission) -> Result<wire::SignedRecord, Error> {
68 let value = receipt
69 .verify_signature()
70 .map_err(|_| Error::Protocol("invalid authority admission signature"))?;
71 Ok(wire::SignedRecord {
72 format: FORMAT.into(),
73 canonical_record: receipt.canonical.clone(),
74 signatures: vec![wire::RecordSignature {
75 public_key: value.executor.to_vec(),
76 signature: receipt.signature.clone(),
77 }],
78 })
79}
80
81pub fn match_batch(
85 batch: &wire::ReplicationOperations,
86) -> Result<Vec<crate::replication::store::ReceivedOperation>, Error> {
87 use std::collections::{BTreeMap, BTreeSet};
88
89 use prost::Message;
90 if batch.operations.is_empty()
91 || batch.operations.len() > 128
92 || batch.authority_admissions.len() > 128
93 || batch.encoded_len() > 1024 * 1024
94 {
95 return Err(Error::Protocol("original authority batch exceeds bounds"));
96 }
97 let mut evidence = crate::boundary_acceptance::Evidence::default();
98 evidence.add(&batch.boundary_acceptances)?;
99 let mut receipts = BTreeMap::new();
100 for record in &batch.authority_admissions {
101 let mut signed = decode(record)?;
102 let statement = signed
103 .verify_signature()
104 .map_err(|_| Error::Protocol("invalid authority admission signature"))?;
105 signed.boundary_acceptance = evidence.matched(&statement.basis)?;
106 let operation_id = statement.subject.operation_id().ok_or(Error::Protocol(
107 "operation batch cannot carry ownership claim admission",
108 ))?;
109 if receipts.insert(operation_id, (signed, statement)).is_some() {
110 return Err(Error::Protocol("duplicate authority admission sidecar"));
111 }
112 }
113 let mut ids = BTreeSet::new();
114 let mut output = Vec::new();
115 for record in &batch.operations {
116 let original = crate::replication::decode_record(record.clone())
117 .map_err(|_| Error::Protocol("invalid original operation signature"))?;
118 let operation = original
119 .verify()
120 .map_err(|_| Error::Protocol("invalid original operation signature"))?;
121 let id = operation
122 .id()
123 .map_err(|_| Error::Protocol("invalid original operation identity"))?;
124 if !ids.insert(id) {
125 return Err(Error::Protocol("duplicate original operation"));
126 }
127 let receipt = if let Some((receipt, statement)) = receipts.remove(&id) {
128 receipt
129 .verify(
130 &original,
131 &TrustedHostedExecutor {
132 spool: statement.spool,
133 spool_genesis: statement.spool_genesis,
134 executor: statement.executor,
135 },
136 )
137 .map_err(|_| {
138 Error::Protocol("authority receipt differs from original operation")
139 })?;
140 Some(receipt)
141 } else {
142 None
143 };
144 output.push(crate::replication::store::ReceivedOperation {
145 original,
146 authority_admission: receipt,
147 });
148 }
149 if !receipts.is_empty() {
150 return Err(Error::Protocol("unmatched authority admission sidecar"));
151 }
152 evidence.finish()?;
153 Ok(output)
154}
155
156#[cfg(test)]
157mod tests {
158 use crypto::Ed25519Signer;
159 use heddle_object_model::object::{
160 CollaborationActor, ContentHash,
161 thread_authority_admission::MAX_BYTES,
162 thread_replication::{
163 ThreadOperation, ThreadOperationBody,
164 metadata::{AUTHORITY_FORMAT, Control, ThreadControl},
165 },
166 };
167 use uuid::Uuid;
168
169 use super::*;
170
171 fn fixture() -> (
172 ThreadAuthorityAdmission,
173 SignedOperation,
174 TrustedHostedExecutor,
175 Ed25519Signer,
176 ) {
177 let author = Ed25519Signer::from_seed(&[41; 32]).expect("author");
178 let executor = Ed25519Signer::from_seed(&[42; 32]).expect("executor");
179 let envelope = b"original authority independently checked at first admission".to_vec();
180 let control = ThreadControl {
181 version: 1,
182 spool: Uuid::from_u128(100),
183 actor: CollaborationActor {
184 principal_id: Uuid::from_u128(101),
185 agent_id: Some("original-agent".into()),
186 },
187 authority_digest: ContentHash::compute_typed(AUTHORITY_FORMAT, &envelope),
188 authority_envelope: envelope,
189 client_operation_id: Uuid::from_u128(102),
190 occurred_at_ms: 1,
191 control: Control::Name("original agent work".into()),
192 };
193 let operation = ThreadOperation {
194 version: 1,
195 thread: ContentHash::from_bytes([43; 32]),
196 parents: Default::default(),
197 publisher: author.public_key().try_into().expect("key"),
198 body: ThreadOperationBody::Metadata(control.encode().expect("control")),
199 };
200 let trust = TrustedHostedExecutor {
201 spool: control.spool,
202 spool_genesis: ContentHash::from_bytes([44; 32]),
203 executor: executor.public_key().try_into().expect("key"),
204 };
205 let receipt = ThreadAuthorityAdmission {
206 version: 3,
207 basis: heddle_object_model::object::original_boundary_acceptance::AdmissionBasis::OriginalAuthority,
208 spool: trust.spool,
209 spool_genesis: trust.spool_genesis,
210 thread: operation.thread,
211 subject: heddle_object_model::object::thread_authority_admission::OriginalAuthoritySubject::Operation(operation.id().expect("ID")),
212 actor: control.actor,
213 publisher: operation.publisher,
214 authority_digest: control.authority_digest,
215 executor: trust.executor,
216 admitted_at_ms: 2000,
217 };
218 (
219 receipt,
220 SignedOperation::sign(&operation, &author).expect("original signature"),
221 trust,
222 executor,
223 )
224 }
225 #[test]
226 fn admission_requires_independent_executor_and_exact_original_identity() {
227 let (value, original, trust, executor) = fixture();
228 let signed = sign(&value, &executor).expect("receipt");
229 assert_eq!(
230 verify(&signed, &original, &trust).expect("independent original admission"),
231 value
232 );
233 for wrong in [
234 TrustedHostedExecutor {
235 executor: [45; 32],
236 ..trust.clone()
237 },
238 TrustedHostedExecutor {
239 spool: Uuid::from_u128(999),
240 ..trust.clone()
241 },
242 TrustedHostedExecutor {
243 spool_genesis: ContentHash::from_bytes([46; 32]),
244 ..trust.clone()
245 },
246 ] {
247 assert!(
248 verify(&signed, &original, &wrong).is_err(),
249 "incoming proof never enrolls its own trust"
250 );
251 }
252 let mut mutations = Vec::new();
253 let mut changed = value.clone();
254 changed.subject =
255 heddle_object_model::object::thread_authority_admission::OriginalAuthoritySubject::Operation(
256 ContentHash::from_bytes([47; 32]),
257 );
258 mutations.push(changed);
259 let mut changed = value.clone();
260 changed.thread = ContentHash::from_bytes([48; 32]);
261 mutations.push(changed);
262 let mut changed = value.clone();
263 changed.publisher = [49; 32];
264 mutations.push(changed);
265 let mut changed = value.clone();
266 changed.actor.agent_id = None;
267 mutations.push(changed);
268 let mut changed = value.clone();
269 changed.actor.principal_id = Uuid::from_u128(888);
270 mutations.push(changed);
271 let mut changed = value;
272 changed.authority_digest = ContentHash::from_bytes([50; 32]);
273 mutations.push(changed);
274 for changed in mutations {
275 assert!(
276 verify(
277 &sign(&changed, &executor).expect("valid executor signature"),
278 &original,
279 &trust
280 )
281 .is_err(),
282 "receipt cannot substitute original scope or authorship"
283 );
284 }
285 }
286 #[test]
287 fn admission_verifies_both_signatures_and_canonical_bounds() {
288 let (value, original, trust, executor) = fixture();
289 let signed = sign(&value, &executor).expect("receipt");
290 let mut changed = signed.clone();
291 let mut new_value = value;
292 new_value.admitted_at_ms += 1;
293 changed.canonical_record = new_value.encode().expect("different timestamp");
294 assert!(
295 verify(&changed, &original, &trust).is_err(),
296 "executor signature binds first admission time"
297 );
298 let mut changed = original.clone();
299 changed.signature[0] ^= 1;
300 assert!(
301 verify(&signed, &changed, &trust).is_err(),
302 "receipt never replaces original signature"
303 );
304 let mut changed = signed.clone();
305 changed.signatures.push(changed.signatures[0].clone());
306 assert!(
307 verify(&changed, &original, &trust).is_err(),
308 "exact one executor signature"
309 );
310 let mut changed = signed;
311 changed.canonical_record.resize(MAX_BYTES + 1, 0);
312 assert!(
313 verify(&changed, &original, &trust).is_err(),
314 "bounded before decoding"
315 );
316 assert!(
317 ThreadAuthorityAdmission::decode(&changed.canonical_record)
318 .expect_err("oversized canonical bytes rejected before parsing")
319 .to_string()
320 .contains("byte bound")
321 );
322 }
323}