1use std::collections::BTreeMap;
4
5use api::{
6 heddle::api::{common as host, v1alpha2 as wire},
7 hybrid_codec::Reject,
8};
9use heddleco_capability_verifier::{
10 self as permission, VerificationLimits, VerifiedCloneKeyring, VerifiedOwnerState,
11};
12
13pub struct NativeAuthority {
15 envelope: wire::ThreadControlAuthority,
16 publishers: Vec<Vec<u8>>,
17}
18
19fn native_authorities(
20 statement: &host::HostedWitnessStatementV1,
21 original_envelope: &[u8],
22 original_publishers: Vec<Vec<u8>>,
23 boundaries: &[wire::ImportBoundaryAcceptanceV1],
24) -> Option<Vec<NativeAuthority>> {
25 let decode = |bytes: &[u8]| {
26 api::mint_root_association::decode_thread_control_authority_for_verification(bytes).ok()
27 };
28 let mut authorities = match statement.basis {
29 1 => vec![NativeAuthority {
30 envelope: decode(original_envelope)?,
31 publishers: original_publishers,
32 }],
33 2 if boundaries
34 .iter()
35 .any(|e| e.binding.as_ref() == statement.boundary_acceptance.as_ref())
36 && statement.boundary_acceptance.is_some() =>
37 {
38 Vec::new()
39 }
40 _ => return None,
41 };
42 for evidence in boundaries {
46 let signed = evidence.signed_acceptance.as_ref()?;
47 if signed.format != objects::object::original_boundary_acceptance::FORMAT
48 || signed.signatures.len() != 1
49 {
50 return None;
51 }
52 let acceptance = crypto::original_boundary_acceptance::SignedBoundaryAcceptance {
53 canonical: signed.canonical_record.clone(),
54 signature: signed.signatures[0].signature.clone(),
55 }
56 .verify_signature()
57 .ok()?;
58 if signed.signatures[0].public_key != acceptance.accepting_publisher {
59 return None;
60 }
61 let objects::object::thread_replication::SourceAuthor::Account { authority, .. } =
62 acceptance.accepting_author
63 else {
64 return None;
65 };
66 authorities.push(NativeAuthority {
67 envelope: decode(&authority)?,
68 publishers: vec![acceptance.accepting_publisher.to_vec()],
69 });
70 }
71 Some(authorities)
72}
73
74pub trait PublicEvidence {
77 fn validate(&self) -> Result<(), Reject>;
78 fn public_history(&self) -> repo::thread_replication::delegated_import::PublicHistory<'_>;
79 fn policies(&self) -> &[wire::SignedSpoolPolicyRecord];
80 fn statements(&self) -> &[host::SignedHostedWitnessStatementV1];
81 fn binding_selectors(&self) -> Vec<(Vec<u8>, u64)> {
82 Vec::new()
83 }
84 fn imported(&self) -> Option<&wire::ImportPublicProofBundleV1> {
85 None
86 }
87 fn native(&self) -> Option<&wire::NativePublicProofBundleV1> {
88 None
89 }
90 fn delegations(&self) -> &[wire::SignedImportJobDelegationV1] {
91 &[]
92 }
93 fn member_permission(&self) -> Option<&wire::SignedImportMemberPermissionV1> {
94 None
95 }
96 fn native_authorities(
97 &self,
98 statement: &host::HostedWitnessStatementV1,
99 ) -> Option<Vec<NativeAuthority>>;
100}
101fn authority_envelopes(
102 statement: &host::HostedWitnessStatementV1,
103 authorities: &[wire::ImportAuthorityWitnessV1],
104 landings: &[wire::HostedLandingWitnessV1],
105) -> Option<Vec<NativeAuthority>> {
106 if statement.purpose == 2 {
107 let p = authorities.iter().find(|p| {
108 api::hybrid_codec::canonical(*p).is_ok_and(|b| b == statement.canonical_payload)
109 })?;
110 native_authorities(
111 statement,
112 &p.authority_envelope,
113 p.original
114 .as_ref()?
115 .signatures
116 .iter()
117 .map(|s| s.public_key.clone())
118 .collect(),
119 &p.boundary_acceptances,
120 )
121 } else if statement.purpose == 4 {
122 let p = landings.iter().find(|p| {
123 api::hybrid_codec::canonical(*p).is_ok_and(|b| b == statement.canonical_payload)
124 })?;
125 native_authorities(
126 statement,
127 &p.authority_envelope,
128 vec![p.request.as_ref()?.signature.as_ref()?.public_key.clone()],
129 &[],
130 )
131 } else {
132 None
133 }
134}
135impl PublicEvidence for wire::ImportPublicProofBundleV1 {
136 fn validate(&self) -> Result<(), Reject> {
137 api::import_authority::validate_public_bundle(self)
138 }
139 fn public_history(&self) -> repo::thread_replication::delegated_import::PublicHistory<'_> {
140 self.into()
141 }
142 fn policies(&self) -> &[wire::SignedSpoolPolicyRecord] {
143 &self.policies
144 }
145 fn statements(&self) -> &[host::SignedHostedWitnessStatementV1] {
146 &self.statements
147 }
148 fn imported(&self) -> Option<&wire::ImportPublicProofBundleV1> {
149 Some(self)
150 }
151 fn delegations(&self) -> &[wire::SignedImportJobDelegationV1] {
152 &self.delegations
153 }
154 fn member_permission(&self) -> Option<&wire::SignedImportMemberPermissionV1> {
155 self.member_permission.as_ref()
156 }
157 fn native_authorities(
158 &self,
159 statement: &host::HostedWitnessStatementV1,
160 ) -> Option<Vec<NativeAuthority>> {
161 if statement.purpose != 1 {
162 return authority_envelopes(
163 statement,
164 &self.authority_witnesses,
165 &self.landing_witnesses,
166 );
167 }
168 let p = self.genesis_witnesses.iter().find(|p| {
169 api::hybrid_codec::canonical(*p).is_ok_and(|b| b == statement.canonical_payload)
170 })?;
171 native_authorities(
172 statement,
173 &p.creator_authority_envelope,
174 p.original_genesis
175 .as_ref()?
176 .signatures
177 .iter()
178 .map(|s| s.public_key.clone())
179 .collect(),
180 p.boundary_acceptance.as_slice(),
181 )
182 }
183}
184impl PublicEvidence for wire::NativePublicProofBundleV1 {
185 fn validate(&self) -> Result<(), Reject> {
186 api::native_witness::validate_public_bundle(self)
187 }
188 fn public_history(&self) -> repo::thread_replication::delegated_import::PublicHistory<'_> {
189 self.into()
190 }
191 fn policies(&self) -> &[wire::SignedSpoolPolicyRecord] {
192 &self.policies
193 }
194 fn statements(&self) -> &[host::SignedHostedWitnessStatementV1] {
195 &self.statements
196 }
197 fn native(&self) -> Option<&wire::NativePublicProofBundleV1> {
198 Some(self)
199 }
200 fn binding_selectors(&self) -> Vec<(Vec<u8>, u64)> {
201 self.genesis_witnesses
202 .iter()
203 .filter_map(|p| p.binding.as_ref()?.body.as_ref()?.identity.as_ref())
204 .map(|id| (id.owner_state_hash.clone(), id.ownership_transfer_sequence))
205 .collect()
206 }
207 fn native_authorities(
208 &self,
209 statement: &host::HostedWitnessStatementV1,
210 ) -> Option<Vec<NativeAuthority>> {
211 if statement.purpose != 1 {
212 return authority_envelopes(
213 statement,
214 &self.authority_witnesses,
215 &self.landing_witnesses,
216 );
217 }
218 let p = self.genesis_witnesses.iter().find(|p| {
219 api::hybrid_codec::canonical(*p).is_ok_and(|b| b == statement.canonical_payload)
220 })?;
221 native_authorities(
222 statement,
223 &p.creator_authority_envelope,
224 p.original_genesis
225 .as_ref()?
226 .signatures
227 .iter()
228 .map(|s| s.public_key.clone())
229 .collect(),
230 p.boundary_acceptance.as_slice(),
231 )
232 }
233}
234
235#[derive(Clone, Debug, PartialEq)]
237pub enum PublicProof {
238 Import(Box<wire::ImportPublicProofBundleV1>),
239 Native(Box<wire::NativePublicProofBundleV1>),
240}
241impl From<wire::ImportPublicProofBundleV1> for PublicProof {
242 fn from(b: wire::ImportPublicProofBundleV1) -> Self {
243 Self::Import(Box::new(b))
244 }
245}
246impl From<wire::NativePublicProofBundleV1> for PublicProof {
247 fn from(b: wire::NativePublicProofBundleV1) -> Self {
248 Self::Native(Box::new(b))
249 }
250}
251impl PublicProof {
252 pub fn witness_set(&self) -> Option<&host::SignedHostedWitnessSetV1> {
253 match self {
254 Self::Import(b) => b.witness_set.as_ref(),
255 Self::Native(b) => b.witness_set.as_ref(),
256 }
257 }
258 pub fn replace_receiver_metadata(&mut self, refreshed: Self) -> Result<(), Reject> {
259 match (self, refreshed) {
260 (Self::Import(b), Self::Import(r)) => super::history::replace_receiver_metadata(b, *r),
261 (Self::Native(b), Self::Native(r)) => {
262 super::history::replace_native_receiver_metadata(b, *r)
263 }
264 _ => Err(Reject::Protocol),
265 }
266 }
267 pub fn encode_to_vec(&self) -> Vec<u8> {
268 use prost::Message;
269 match self {
270 Self::Import(b) => b.encode_to_vec(),
271 Self::Native(b) => b.encode_to_vec(),
272 }
273 }
274}
275impl PublicEvidence for PublicProof {
276 fn validate(&self) -> Result<(), Reject> {
277 match self {
278 Self::Import(b) => b.validate(),
279 Self::Native(b) => b.validate(),
280 }
281 }
282 fn public_history(&self) -> repo::thread_replication::delegated_import::PublicHistory<'_> {
283 match self {
284 Self::Import(b) => b.as_ref().into(),
285 Self::Native(b) => b.as_ref().into(),
286 }
287 }
288 fn policies(&self) -> &[wire::SignedSpoolPolicyRecord] {
289 match self {
290 Self::Import(b) => &b.policies,
291 Self::Native(b) => &b.policies,
292 }
293 }
294 fn statements(&self) -> &[host::SignedHostedWitnessStatementV1] {
295 match self {
296 Self::Import(b) => &b.statements,
297 Self::Native(b) => &b.statements,
298 }
299 }
300 fn binding_selectors(&self) -> Vec<(Vec<u8>, u64)> {
301 match self {
302 Self::Import(b) => b.binding_selectors(),
303 Self::Native(b) => b.binding_selectors(),
304 }
305 }
306 fn imported(&self) -> Option<&wire::ImportPublicProofBundleV1> {
307 match self {
308 Self::Import(b) => Some(b),
309 _ => None,
310 }
311 }
312 fn native(&self) -> Option<&wire::NativePublicProofBundleV1> {
313 match self {
314 Self::Native(b) => Some(b),
315 _ => None,
316 }
317 }
318 fn delegations(&self) -> &[wire::SignedImportJobDelegationV1] {
319 match self {
320 Self::Import(b) => &b.delegations,
321 _ => &[],
322 }
323 }
324 fn member_permission(&self) -> Option<&wire::SignedImportMemberPermissionV1> {
325 match self {
326 Self::Import(b) => b.member_permission.as_ref(),
327 _ => None,
328 }
329 }
330 fn native_authorities(
331 &self,
332 s: &host::HostedWitnessStatementV1,
333 ) -> Option<Vec<NativeAuthority>> {
334 match self {
335 Self::Import(b) => b.native_authorities(s),
336 Self::Native(b) => b.native_authorities(s),
337 }
338 }
339}
340
341pub struct SelectedAuthority<F, B = wire::ImportPublicProofBundleV1> {
344 history: AcceptedHistory,
345 bundle: B,
346 authorize: F,
347}
348impl<F> SelectedAuthority<F> {
349 pub fn new(
350 history: AcceptedHistory,
351 bundle: wire::ImportPublicProofBundleV1,
352 authorize: F,
353 ) -> Self {
354 Self {
355 history,
356 bundle,
357 authorize,
358 }
359 }
360}
361impl<F> SelectedAuthority<F, wire::NativePublicProofBundleV1> {
362 pub fn new_native(
363 history: AcceptedHistory,
364 bundle: wire::NativePublicProofBundleV1,
365 authorize: F,
366 ) -> Self {
367 Self {
368 history,
369 bundle,
370 authorize,
371 }
372 }
373}
374impl<F> SelectedAuthority<F, PublicProof> {
375 pub fn from_proof(history: AcceptedHistory, bundle: PublicProof, authorize: F) -> Self {
376 Self {
377 history,
378 bundle,
379 authorize,
380 }
381 }
382}
383impl<F, B: PublicEvidence> SelectedAuthority<F, B> {
384 fn selection<'a>(
385 &'a self,
386 selected: &'a HistoricalSelection,
387 ) -> permission::import_delegation::Selection<'a> {
388 permission::import_delegation::Selection {
389 owner: &selected.owner,
390 keyring: &selected.keyring,
391 spool_genesis_digest: self.history.genesis(),
392 initial_owner_id: self.history.initial_owner(),
393 limits: self.history.limits(),
394 }
395 }
396 fn policy(
397 &self,
398 statement: &host::HostedWitnessStatementV1,
399 ) -> Option<&wire::SignedSpoolPolicy> {
400 self.bundle.policies().iter().find_map(|signed| {
402 let body = signed.body.as_ref()?;
403 (body.spool_uuid == statement.spool_uuid
404 && body.sequence == statement.policy_sequence
405 && body.policy_state_hash == statement.policy_state_hash)
406 .then_some(body.policy.as_ref())
407 .flatten()
408 })
409 }
410 fn policy_revocations(&self, statement: &host::HostedWitnessStatementV1) -> Option<&[Vec<u8>]> {
411 if statement.policy_sequence == 0 && statement.policy_state_hash == [0; 32] {
412 if self.bundle.policies().iter().any(|record| {
415 record.body.as_ref().is_some_and(|body| {
416 body.spool_uuid == statement.spool_uuid
417 && body.sequence == 0
418 && body.policy_state_hash == [0; 32]
419 })
420 }) {
421 return None;
422 }
423 return Some(&[]);
424 }
425 self.policy(statement)
426 .map(|policy| policy.revoked_key_ids.as_slice())
427 }
428}
429impl<
430 B: PublicEvidence,
431 F: Fn(
432 &B,
433 i64,
434 &repo::thread_replication::hosted_trust::TrustTransaction<'_>,
435 ) -> repo::thread_replication::Result<()>,
436> repo::thread_replication::delegated_import::AcceptedAuthority for SelectedAuthority<F, B>
437{
438 fn authorize_import(
439 &self,
440 bundle: &wire::ImportPublicProofBundleV1,
441 now: i64,
442 context: &repo::thread_replication::hosted_trust::TrustTransaction<'_>,
443 ) -> repo::thread_replication::Result<()> {
444 if self.bundle.imported() != Some(bundle) {
445 return Err(Reject::StaleContext.into());
446 }
447 (self.authorize)(&self.bundle, now, context)
448 }
449 fn authorize_native(
450 &self,
451 bundle: &wire::NativePublicProofBundleV1,
452 now: i64,
453 context: &repo::thread_replication::hosted_trust::TrustTransaction<'_>,
454 ) -> repo::thread_replication::Result<()> {
455 if self.bundle.native() != Some(bundle) {
456 return Err(Reject::StaleContext.into());
457 }
458 (self.authorize)(&self.bundle, now, context)
459 }
460 fn for_witness(
461 &self,
462 statement: &host::HostedWitnessStatementV1,
463 ) -> repo::thread_replication::Result<permission::import_delegation::Selection<'_>> {
464 let selected = self
465 .history
466 .for_witness(statement)
467 .map_err(authority_error)?;
468 Ok(self.selection(selected))
469 }
470 fn for_native_binding(
471 &self,
472 binding: &wire::NativeGenesisAuthorityV1,
473 ) -> repo::thread_replication::Result<permission::import_delegation::Selection<'_>> {
474 let id = binding.identity.as_ref().ok_or(Reject::GenesisBinding)?;
475 let selected = self
476 .history
477 .bindings
478 .get(&(
479 id.owner_state_hash.clone(),
480 id.ownership_transfer_sequence,
481 binding.owner_chain_digest.clone(),
482 ))
483 .ok_or(Reject::Root)?;
484 Ok(self.selection(selected))
485 }
486 fn for_policy(
487 &self,
488 policy: &wire::SignedPolicyBody,
489 ) -> repo::thread_replication::Result<permission::import_delegation::Selection<'_>> {
490 let selected = self.history.for_policy(policy).map_err(authority_error)?;
491 Ok(self.selection(selected))
492 }
493 fn import_revoked(
494 &self,
495 statement: &host::HostedWitnessStatementV1,
496 revocation: permission::import_delegation::Revocation<'_>,
497 ) -> bool {
498 let (Ok(selected), Some(revoked)) = (
499 self.history.for_witness(statement),
500 self.policy_revocations(statement),
501 ) else {
502 return true;
503 };
504 match revocation {
505 permission::import_delegation::Revocation::Key(id) => {
506 let known = selected
507 .owner
508 .authority_public_keys()
509 .chain(selected.keyring.authority_public_keys().cloned())
510 .chain(
511 self.bundle
512 .delegations()
513 .iter()
514 .filter_map(|d| d.body.as_ref())
515 .flat_map(|d| {
516 [d.delegating_public_key.clone(), d.job_public_key.clone()]
517 }),
518 )
519 .any(|key| api::hybrid_codec::key_id(&key).as_slice() == id);
520 !known || revoked.iter().any(|key| key == id)
521 }
522 permission::import_delegation::Revocation::Cancellation(namespace, id) => {
523 namespace != api::import_authority::CANCELLATION_NAMESPACE
527 || !self
528 .bundle
529 .delegations()
530 .iter()
531 .filter_map(|d| d.body.as_ref())
532 .any(|d| d.cancellation_id == id)
533 && !self
534 .bundle
535 .member_permission()
536 .into_iter()
537 .filter_map(|p| p.body.as_ref())
538 .any(|p| p.cancellation_id == id)
539 }
540 }
541 }
542 fn native_revoked(
543 &self,
544 statement: &host::HostedWitnessStatementV1,
545 revocation: permission::thread_control_authority::Revocation<'_>,
546 ) -> bool {
547 let (Ok(_), Some(revoked), Some(authorities)) = (
548 self.history.for_witness(statement),
549 self.policy_revocations(statement),
550 self.bundle.native_authorities(statement),
551 ) else {
552 return true;
553 };
554 match revocation {
555 permission::thread_control_authority::Revocation::MintRoot(key) => {
556 !authorities
557 .iter()
558 .any(|a| key == a.envelope.mint_root_public_key)
559 || revoked.contains(&api::hybrid_codec::key_id(key).to_vec())
560 }
561 permission::thread_control_authority::Revocation::Publisher(key) => {
562 !authorities
563 .iter()
564 .any(|a| a.publishers.iter().any(|p| p == key))
565 || revoked.contains(&api::hybrid_codec::key_id(key).to_vec())
566 }
567 permission::thread_control_authority::Revocation::Credential(id) => {
568 !authorities.iter().any(|authority| {
569 let envelope = &authority.envelope;
570 let keys = biscuit_verifier::parse_ed25519_public_keys_hex(
571 &hex::encode(&envelope.mint_root_public_key),
572 1,
573 );
574 let inspected = keys.ok().and_then(|keys| {
575 let key = *keys.first()?;
576 let token =
577 biscuit_verifier::signature_v1::verify(&envelope.sealed_biscuit, key)
578 .ok()?;
579 biscuit_verifier::inspect_verified_credential(&token, &key).ok()
580 });
581 inspected
585 .is_some_and(|facts| facts.revocation_identities().any(|known| known == id))
586 })
587 }
588 }
589 }
590}
591fn authority_error(error: impl std::fmt::Display) -> repo::thread_replication::Error {
592 repo::thread_replication::Error::Invalid(error.to_string())
593}
594
595#[derive(Debug, thiserror::Error)]
596pub enum Error {
597 #[error("HYBRID accepted authority rejected: {0}")]
598 Rejected(#[from] Reject),
599 #[error(transparent)]
600 Owner(#[from] permission::Error),
601}
602
603pub struct HistoricalSelection {
606 pub owner: VerifiedOwnerState,
607 pub keyring: VerifiedCloneKeyring,
608}
609pub struct AcceptedHistory {
610 genesis: [u8; 32],
611 initial_owner: [u8; 32],
612 limits: VerificationLimits,
613 states: BTreeMap<(Vec<u8>, u64), HistoricalSelection>,
614 bindings: BTreeMap<(Vec<u8>, u64, Vec<u8>), HistoricalSelection>,
615}
616
617impl AcceptedHistory {
618 pub fn from_selected_spool(
619 bundle: &wire::ImportPublicProofBundleV1,
620 selected: &VerifiedCloneKeyring,
621 now_seconds: i64,
622 limits: VerificationLimits,
623 ) -> Result<Self, Error> {
624 Self::from_public(bundle, selected, now_seconds, limits)
625 }
626 pub fn from_native_spool(
627 bundle: &wire::NativePublicProofBundleV1,
628 selected: &VerifiedCloneKeyring,
629 now_seconds: i64,
630 limits: VerificationLimits,
631 ) -> Result<Self, Error> {
632 Self::from_public(bundle, selected, now_seconds, limits)
633 }
634 pub fn from_public(
635 bundle: &impl PublicEvidence,
636 selected: &VerifiedCloneKeyring,
637 now_seconds: i64,
638 limits: VerificationLimits,
639 ) -> Result<Self, Error> {
640 bundle.validate()?;
641 let public = bundle.public_history();
642 let pinned = selected.wire();
643 if public.owner_genesis != Some(selected.owner_genesis().signed())
644 || !pinned
645 .ownership_transfers
646 .starts_with(public.ownership_transfers)
647 {
648 return Err(Reject::Root.into());
649 }
650 let genesis = permission::creation::spool_genesis_digest(
651 selected
652 .owner_genesis()
653 .signed()
654 .genesis
655 .as_ref()
656 .ok_or(Reject::Root)?,
657 )?;
658 let mut this = Self {
659 genesis,
660 initial_owner: selected.owner_state().owner_id(),
661 limits,
662 states: BTreeMap::new(),
663 bindings: BTreeMap::new(),
664 };
665 let selectors = bundle
666 .statements()
667 .iter()
668 .map(|signed| {
669 let body = signed.body.as_ref().ok_or(Reject::Canonical)?;
670 Ok((
671 body.owner_state_hash.clone(),
672 body.ownership_transfer_sequence,
673 ))
674 })
675 .chain(bundle.policies().iter().map(|signed| {
676 let body = signed.body.as_ref().ok_or(Reject::Canonical)?;
677 Ok((
678 body.owner_state_hash.clone(),
679 body.ownership_transfer_sequence,
680 ))
681 }))
682 .chain(bundle.binding_selectors().into_iter().map(Ok))
683 .collect::<Result<std::collections::BTreeSet<_>, Reject>>()?;
684 for (hash, sequence) in selectors {
685 let count = usize::try_from(sequence).map_err(|_| Reject::Bounds)?;
686 let transfers = public
687 .ownership_transfers
688 .get(..count)
689 .ok_or(Reject::Root)?;
690 let history = public
691 .owner_histories
692 .iter()
693 .find(|h| h.state_hash == hash)
694 .ok_or(Reject::Root)?;
695 let root = history.root.as_ref().ok_or(Reject::Root)?;
696 let mut owner = permission::verify_owner_root(root)?;
697 for transition in &history.accepted_transitions {
698 owner =
699 permission::apply_accepted_transition(&owner, transition, now_seconds, limits)?;
700 }
701 if owner.state_hash().as_slice() != hash {
702 return Err(Reject::Root.into());
703 }
704 let expected_root = if let Some(last) = transfers.last() {
707 let handoff = last
708 .transfer
709 .as_ref()
710 .and_then(|t| t.acceptance.as_ref())
711 .and_then(|a| a.signed_handoff.as_ref())
712 .and_then(|s| s.handoff.as_ref())
713 .ok_or(Reject::Root)?;
714 pinned
715 .transfer_owner_histories
716 .iter()
717 .find(|h| h.state_hash == handoff.destination_owner_key_state_hash)
718 .and_then(|h| h.root.as_ref())
719 .ok_or(Reject::Root)?
720 } else {
721 pinned.owner_root.as_ref().ok_or(Reject::Root)?
722 };
723 if root != expected_root {
724 return Err(Reject::Root.into());
725 }
726 let initial_history = if let Some(first) = transfers.first() {
727 let handoff = first
728 .transfer
729 .as_ref()
730 .and_then(|t| t.acceptance.as_ref())
731 .and_then(|a| a.signed_handoff.as_ref())
732 .and_then(|s| s.handoff.as_ref())
733 .ok_or(Reject::Root)?;
734 public
735 .owner_histories
736 .iter()
737 .find(|h| h.state_hash == handoff.source_owner_key_state_hash)
738 .ok_or(Reject::Root)?
739 } else {
740 history
741 };
742 if initial_history.root != pinned.owner_root {
743 return Err(Reject::Root.into());
744 }
745 let mut wire = pinned.clone();
746 wire.accepted_transitions = initial_history.accepted_transitions.clone();
747 wire.accepted_state_hash = initial_history.state_hash.clone();
748 wire.ownership_transfers = transfers.to_vec();
749 let mut party_states = std::collections::BTreeSet::new();
751 for transfer in transfers {
752 let handoff = transfer
753 .transfer
754 .as_ref()
755 .and_then(|t| t.acceptance.as_ref())
756 .and_then(|a| a.signed_handoff.as_ref())
757 .and_then(|s| s.handoff.as_ref())
758 .ok_or(Reject::Root)?;
759 party_states.insert(&handoff.source_owner_key_state_hash);
760 party_states.insert(&handoff.destination_owner_key_state_hash);
761 }
762 wire.transfer_owner_histories = public
763 .owner_histories
764 .iter()
765 .filter(|h| party_states.contains(&h.state_hash))
766 .cloned()
767 .collect();
768 let keyring = permission::verify_clone_keyring(wire, now_seconds, limits, &[])?;
769 keyring.verify_current_owner(&owner, now_seconds, limits)?;
770 this.states
771 .insert((hash, sequence), HistoricalSelection { owner, keyring });
772 }
773 if let Some(native) = bundle.native() {
774 for p in &native.genesis_witnesses {
775 let body = p
776 .binding
777 .as_ref()
778 .and_then(|b| b.body.as_ref())
779 .ok_or(Reject::GenesisBinding)?;
780 let id = body.identity.as_ref().ok_or(Reject::GenesisBinding)?;
781 let witnessed = this
782 .states
783 .get(&(id.owner_state_hash.clone(), id.ownership_transfer_sequence))
784 .ok_or(Reject::Root)?;
785 let mut resolved = None;
790 for history in public
791 .owner_histories
792 .iter()
793 .filter(|h| h.root == witnessed.keyring.wire().owner_root)
794 {
795 let mut wire = witnessed.keyring.wire().clone();
796 wire.accepted_transitions = history.accepted_transitions.clone();
797 wire.accepted_state_hash = history.state_hash.clone();
798 let Ok(keyring) =
799 permission::verify_clone_keyring(wire, now_seconds, limits, &[])
800 else {
801 continue;
802 };
803 let selection = permission::import_delegation::Selection {
804 owner: &witnessed.owner,
805 keyring: &keyring,
806 spool_genesis_digest: &this.genesis,
807 initial_owner_id: &this.initial_owner,
808 limits,
809 };
810 if permission::import_delegation::native_lineage(&selection).is_ok_and(
811 |(identity, chain, _)| {
812 body.identity.as_ref() == Some(&identity)
813 && chain == body.owner_chain_digest
814 },
815 ) {
816 if resolved.is_some() {
817 return Err(Reject::Canonical.into());
818 }
819 resolved = Some(HistoricalSelection {
820 owner: witnessed.owner.clone(),
821 keyring,
822 });
823 }
824 }
825 this.bindings.insert(
826 (
827 id.owner_state_hash.clone(),
828 id.ownership_transfer_sequence,
829 body.owner_chain_digest.clone(),
830 ),
831 resolved.ok_or(Reject::Root)?,
832 );
833 }
834 }
835 Ok(this)
836 }
837 pub fn for_witness(
838 &self,
839 statement: &host::HostedWitnessStatementV1,
840 ) -> Result<&HistoricalSelection, Error> {
841 let selected = self
842 .states
843 .get(&(
844 statement.owner_state_hash.clone(),
845 statement.ownership_transfer_sequence,
846 ))
847 .ok_or(Reject::Root)?;
848 if statement.spool_uuid != selected.keyring.owner_genesis().spool_uuid()
849 || statement.spool_genesis_digest != self.genesis
850 || statement.owner_id != selected.owner.owner_id()
851 {
852 return Err(Reject::Root.into());
853 }
854 Ok(selected)
855 }
856 pub fn for_policy(
857 &self,
858 policy: &wire::SignedPolicyBody,
859 ) -> Result<&HistoricalSelection, Error> {
860 let selected = self
861 .states
862 .get(&(
863 policy.owner_state_hash.clone(),
864 policy.ownership_transfer_sequence,
865 ))
866 .ok_or(Reject::Root)?;
867 if policy.spool_uuid != selected.keyring.owner_genesis().spool_uuid()
868 || policy.owner_id != selected.owner.owner_id()
869 {
870 return Err(Reject::Root.into());
871 }
872 Ok(selected)
873 }
874 pub fn genesis(&self) -> &[u8; 32] {
875 &self.genesis
876 }
877 pub fn initial_owner(&self) -> &[u8; 32] {
878 &self.initial_owner
879 }
880 pub fn limits(&self) -> VerificationLimits {
881 self.limits
882 }
883}
884
885#[cfg(test)]
886#[path = "authority_policy_tests.rs"]
887mod policy_tests;
888
889#[cfg(test)]
890pub(crate) mod tests {
891 use prost::Message;
892
893 use super::*;
894
895 pub(crate) fn bundle() -> wire::ImportPublicProofBundleV1 {
896 let fixture: serde_json::Value =
897 serde_json::from_str(include_str!("../../tests/fixtures/hybrid-alpha33.json"))
898 .expect("fixed vectors");
899 let bytes = hex::decode(
900 fixture["wire_vectors"]["complete_export"]["wire_hex"]
901 .as_str()
902 .expect("wire bytes"),
903 )
904 .expect("hex");
905 wire::ImportPublicProofBundleV1::decode(bytes.as_slice()).expect("public export")
906 }
907 pub(crate) fn selected(
908 bundle: &wire::ImportPublicProofBundleV1,
909 limits: VerificationLimits,
910 ) -> VerifiedCloneKeyring {
911 let history = &bundle.owner_histories[0];
912 let root = history.root.as_ref().expect("owner root");
913 let wire = wire::CloneAuthorizationKeyring {
914 format_version: 1,
915 spool_uuid: bundle
916 .owner_genesis
917 .as_ref()
918 .expect("genesis")
919 .genesis
920 .as_ref()
921 .expect("body")
922 .spool_uuid
923 .clone(),
924 canonical_spool_path_segments: vec!["acme".into(), "imports".into()],
925 pin: Some(wire::CloneOwnerPin {
926 kind: wire::CloneOwnerPinKind::CloneTofu as i32,
927 expected_owner_id: root.root.as_ref().expect("root body").owner_id.clone(),
928 first_seen_unix_seconds: 1100,
929 }),
930 owner_root: Some(root.clone()),
931 accepted_transitions: history.accepted_transitions.clone(),
932 accepted_state_hash: history.state_hash.clone(),
933 owner_genesis: bundle.owner_genesis.clone(),
934 ownership_transfers: bundle.ownership_transfers.clone(),
935 transfer_owner_histories: vec![],
936 };
937 permission::verify_clone_keyring(wire, 1200, limits, &[])
938 .expect("independent Spool observation")
939 }
940 #[test]
941 fn carried_history_cannot_replace_the_independently_selected_spool() {
942 let original = bundle();
943 let limits = VerificationLimits::new(30 * 24 * 60 * 60).expect("limits");
944 let selected = selected(&original, limits);
945 let accepted = AcceptedHistory::from_selected_spool(&original, &selected, 1200, limits)
946 .expect("exact original history");
947 for signed in &original.statements {
948 accepted
949 .for_witness(signed.body.as_ref().expect("body"))
950 .expect("accepted owner selection");
951 }
952 let mut substituted = original.clone();
953 substituted
954 .owner_genesis
955 .as_mut()
956 .expect("genesis")
957 .genesis
958 .as_mut()
959 .expect("body")
960 .spool_uuid[0] ^= 1;
961 assert!(matches!(
962 AcceptedHistory::from_selected_spool(&substituted, &selected, 1200, limits),
963 Err(Error::Rejected(Reject::Root))
964 ));
965 let mut foreign = original.statements[0]
966 .body
967 .as_ref()
968 .expect("witness")
969 .clone();
970 foreign.spool_uuid[0] ^= 1;
971 assert!(accepted.for_witness(&foreign).is_err());
972 AcceptedHistory::from_selected_spool(&original, &selected, 1200, limits)
973 .expect("original evidence remains usable");
974 }
975
976 #[test]
977 fn historical_revocation_selectors_are_exact_and_keep_their_namespaces() {
978 use permission::{
979 import_delegation::Revocation as Import, thread_control_authority::Revocation as Native,
980 };
981 use repo::thread_replication::delegated_import::AcceptedAuthority as _;
982 let fixture: serde_json::Value =
983 serde_json::from_str(include_str!("../../tests/fixtures/hybrid-alpha33.json"))
984 .expect("published vectors");
985 let decode = |name: &str, signed: bool| {
986 hex::decode(
987 fixture[if signed {
988 "signed_vectors"
989 } else {
990 "wire_vectors"
991 }][name]["wire_hex"]
992 .as_str()
993 .expect("wire"),
994 )
995 .expect("hex")
996 };
997 let mut bundle = bundle();
998 let native: wire::ImportAuthorityWitnessV1 = wire::ImportAuthorityWitnessV1::decode(
999 decode("authority_admission_payload", false).as_slice(),
1000 )
1001 .expect("original payload");
1002 let witness: host::SignedHostedWitnessStatementV1 =
1003 host::SignedHostedWitnessStatementV1::decode(
1004 decode("authority_admission", true).as_slice(),
1005 )
1006 .expect("native witness");
1007 bundle.authority_witnesses.push(native.clone());
1008 bundle.statements.push(witness.clone());
1009 let limits = VerificationLimits::new(30 * 24 * 60 * 60).expect("limits");
1010 let pinned = selected(&bundle, limits);
1011 let history = AcceptedHistory::from_selected_spool(&bundle, &pinned, 1350, limits)
1012 .expect("exact selected history");
1013 let import_statement = bundle
1014 .statements
1015 .iter()
1016 .find_map(|s| s.body.as_ref().filter(|s| s.purpose == 3))
1017 .expect("publication witness")
1018 .clone();
1019 let delegation = bundle.delegations[0]
1020 .body
1021 .as_ref()
1022 .expect("delegation")
1023 .clone();
1024 let authority = SelectedAuthority::new(
1025 history,
1026 bundle,
1027 |_: &wire::ImportPublicProofBundleV1,
1028 _: i64,
1029 _: &repo::thread_replication::hosted_trust::TrustTransaction<'_>| Ok(()),
1030 );
1031 assert!(!authority.import_revoked(
1032 &import_statement,
1033 Import::Key(&api::hybrid_codec::key_id(&delegation.job_public_key))
1034 ));
1035 assert!(!authority.import_revoked(
1036 &import_statement,
1037 Import::Cancellation(
1038 api::import_authority::CANCELLATION_NAMESPACE,
1039 &delegation.cancellation_id
1040 )
1041 ));
1042 assert!(authority.import_revoked(
1043 &import_statement,
1044 Import::Cancellation("credential", &delegation.cancellation_id)
1045 ));
1046 assert!(
1047 authority.import_revoked(&import_statement, Import::Key(&delegation.cancellation_id)),
1048 "cancellation bytes cannot impersonate a key ID"
1049 );
1050 assert!(authority.import_revoked(
1051 &import_statement,
1052 Import::Cancellation(api::import_authority::CANCELLATION_NAMESPACE, &[0; 32])
1053 ));
1054 let statement = witness.body.as_ref().expect("body");
1055 let envelope = wire::ThreadControlAuthority::decode(native.authority_envelope.as_slice())
1056 .expect("retained original credential");
1057 assert!(
1058 !authority.native_revoked(statement, Native::MintRoot(&envelope.mint_root_public_key))
1059 );
1060 assert!(!authority.native_revoked(
1061 statement,
1062 Native::Publisher(
1063 &native.original.as_ref().expect("original").signatures[0].public_key
1064 )
1065 ));
1066 assert!(!authority.native_revoked(statement, Native::Credential("hybrid-native-fixture")));
1067 assert!(authority.native_revoked(statement, Native::Credential("neighboring-session")));
1068 assert!(authority.native_revoked(statement, Native::Publisher(&[0; 32])));
1069 assert!(authority.native_revoked(statement, Native::MintRoot(&[0; 32])));
1070 }
1071
1072 struct FixtureAuthority(AcceptedHistory);
1073 impl FixtureAuthority {
1074 fn selection<'a>(
1075 &'a self,
1076 selected: &'a HistoricalSelection,
1077 ) -> permission::import_delegation::Selection<'a> {
1078 permission::import_delegation::Selection {
1079 owner: &selected.owner,
1080 keyring: &selected.keyring,
1081 spool_genesis_digest: self.0.genesis(),
1082 initial_owner_id: self.0.initial_owner(),
1083 limits: self.0.limits(),
1084 }
1085 }
1086 }
1087 impl repo::thread_replication::delegated_import::AcceptedAuthority for FixtureAuthority {
1088 fn authorize_import(
1089 &self,
1090 _: &wire::ImportPublicProofBundleV1,
1091 now: i64,
1092 _: &repo::thread_replication::hosted_trust::TrustTransaction<'_>,
1093 ) -> repo::thread_replication::Result<()> {
1094 assert_eq!(now, 1_350_000, "use receiver time");
1095 Ok(())
1096 }
1097 fn for_witness(
1098 &self,
1099 statement: &host::HostedWitnessStatementV1,
1100 ) -> repo::thread_replication::Result<permission::import_delegation::Selection<'_>>
1101 {
1102 let selected = self
1103 .0
1104 .for_witness(statement)
1105 .map_err(|e| repo::thread_replication::Error::Invalid(e.to_string()))?;
1106 Ok(self.selection(selected))
1107 }
1108 fn for_policy(
1109 &self,
1110 policy: &wire::SignedPolicyBody,
1111 ) -> repo::thread_replication::Result<permission::import_delegation::Selection<'_>>
1112 {
1113 let selected = self
1114 .0
1115 .for_policy(policy)
1116 .map_err(|e| repo::thread_replication::Error::Invalid(e.to_string()))?;
1117 Ok(self.selection(selected))
1118 }
1119 fn import_revoked(
1120 &self,
1121 _: &host::HostedWitnessStatementV1,
1122 _: permission::import_delegation::Revocation<'_>,
1123 ) -> bool {
1124 false
1126 }
1127 fn native_revoked(
1128 &self,
1129 _: &host::HostedWitnessStatementV1,
1130 _: permission::thread_control_authority::Revocation<'_>,
1131 ) -> bool {
1132 true
1134 }
1135 }
1136
1137 #[test]
1138 fn staged_context_rechecks_concurrent_durable_revocation_before_install() {
1139 use repo::thread_replication::{ThreadReplica, hosted_trust::*};
1140
1141 struct ReceiverClock;
1142 impl Clock for ReceiverClock {
1143 fn now_millis(&self) -> repo::thread_replication::Result<i64> {
1144 Ok(1_350_000)
1145 }
1146 fn elapsed_millis(&self) -> repo::thread_replication::Result<u64> {
1147 Ok(0)
1148 }
1149 }
1150 let fixture: serde_json::Value =
1151 serde_json::from_str(include_str!("../../tests/fixtures/hybrid-alpha33.json"))
1152 .expect("published fixture");
1153 fn record<T: Message + Default>(fixture: &serde_json::Value, name: &str) -> T {
1154 let vector = fixture["wire_vectors"]
1155 .get(name)
1156 .or_else(|| fixture["signed_vectors"].get(name))
1157 .expect("vector");
1158 T::decode(
1159 hex::decode(vector["wire_hex"].as_str().expect("wire hex"))
1160 .expect("hex")
1161 .as_slice(),
1162 )
1163 .expect("canonical record")
1164 }
1165 let mut prepared = bundle();
1166 prepared.history_proofs = [
1167 "genesis_proof",
1168 "genesis_dev_proof",
1169 "publication_proof",
1170 "dev_publication_proof",
1171 ]
1172 .map(|name| record(&fixture, name))
1173 .to_vec();
1174 let limits = VerificationLimits::new(30 * 24 * 60 * 60).expect("limits");
1175 let pinned = selected(&prepared, limits);
1176 let authority = FixtureAuthority(
1177 AcceptedHistory::from_selected_spool(&prepared, &pinned, 1350, limits)
1178 .expect("independent accepted history"),
1179 );
1180 let directory = tempfile::tempdir().expect("receiver");
1181 let repository = repo::Repository::init_default(directory.path()).expect("repository");
1182 let root = RootSelection {
1183 authority: "https://weft.example.test".into(),
1184 root_id: "descriptor-root-1".into(),
1185 public_key: hex::decode(
1186 fixture["keys"]["root"]["public_key_hex"]
1187 .as_str()
1188 .expect("root"),
1189 )
1190 .expect("hex")
1191 .try_into()
1192 .expect("root key"),
1193 };
1194 select_root(repository.heddle_dir(), &root).expect("independent root");
1195 select_spool(
1196 repository.heddle_dir(),
1197 pinned.owner_genesis().spool_uuid(),
1198 *authority.0.genesis(),
1199 *authority.0.initial_owner(),
1200 )
1201 .expect("independent Spool");
1202 let trust = HostedTrust::open(repository.heddle_dir(), &root.authority, ReceiverClock)
1203 .expect("trust");
1204 let records: [wire::SignedRecord; 2] = [
1205 record(&fixture, "converted_main"),
1206 record(&fixture, "converted_dev"),
1207 ];
1208 let install = || {
1209 ThreadReplica::install_hybrid_import(
1210 repository.heddle_dir(),
1211 &trust,
1212 &prepared.encode_to_vec(),
1213 &records,
1214 &authority,
1215 repository.store(),
1216 |_| Ok(()),
1217 )
1218 };
1219 let replicas = install().expect("unchanged complete import control");
1220 let generations = replicas
1221 .iter()
1222 .map(|r| r.generation().expect("generation"))
1223 .collect::<Vec<_>>();
1224 let second = HostedTrust::open(repository.heddle_dir(), &root.authority, ReceiverClock)
1225 .expect("independent transfer");
1226 let revoked = record(&fixture, "revoked_set");
1227 std::thread::spawn(move || second.mutate(&revoked, |_| Ok(())))
1228 .join()
1229 .expect("concurrent writer")
1230 .expect("persist N+1 revocation");
1231 assert!(
1232 matches!(
1233 install(),
1234 Err(repo::thread_replication::Error::Hybrid(Reject::HighWater))
1235 ),
1236 "staged N must never authorize install after durable N+1 revocation"
1237 );
1238 for (replica, generation) in replicas.iter().zip(generations) {
1239 assert_eq!(replica.generation().expect("unchanged replica"), generation);
1240 }
1241 drop(trust);
1242 let reopened = HostedTrust::open(repository.heddle_dir(), &root.authority, ReceiverClock)
1243 .expect("restart");
1244 assert!(matches!(
1245 ThreadReplica::install_hybrid_import(
1246 repository.heddle_dir(),
1247 &reopened,
1248 &prepared.encode_to_vec(),
1249 &records,
1250 &authority,
1251 repository.store(),
1252 |_| Ok(()),
1253 ),
1254 Err(repo::thread_replication::Error::Hybrid(Reject::HighWater))
1255 ));
1256 }
1257}