Skip to main content

heddle_thread_api/hybrid/
authority.rs

1//! Select exact historical owner contexts from independently observed Spool
2//! lineage. Public histories supply signatures, never a replacement root.
3use 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
13/// Identities whose revocations the native verifier evaluates at statement time.
14pub 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    // Boundary originals retain their own provenance/signature gates. Only the
43    // separately signed acceptors (including dependency acceptances) supply
44    // current revocation identities; no original is relabeled as an acceptor.
45    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
74/// Typed native and import bundles share verified lineage selection, while
75/// retaining their separate validators and disclosure contracts.
76pub 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/// Explicit transport dispatch; an import failure never becomes native history.
236#[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
341/// Historical permission is bound to exact authenticated witness order. The
342/// caller supplies today's disclosure check, which runs again at commit.
343pub 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        // The native verifier independently authenticates this exact policy.
401        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            // Exactly this authenticated head denotes no policy. A signed
413            // record cannot stand in for the implicit genesis policy.
414            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                // Cancellation status is attested at exact accepted order by
524                // the witness; only identifiers actually bound by its signed
525                // delegation history can use that historical acceptance.
526                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                    // The authenticated witness attests these exact authorization
582                    // credentials at statement time: original for OriginalAuthority,
583                    // acceptor for BoundaryAcceptance, never the delivery credential.
584                    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
603/// A verified state for one accepted owner/transfer selection. It grants no
604/// current device authority and must be used with an authenticated witness.
605pub 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            // Only the initial pinned root or the exact destination of a
705            // verified prefix handoff may supply this historical owner.
706            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            // The transfer verifier resolves exact signed parties itself.
750            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                // The binding retains its original keyring endpoint even when
786                // accepted authority or other genesis chains have advanced.
787                // Every candidate is independently authenticated against the
788                // selected immutable root and complete signed transfer prefix.
789                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            // The published fixture has no cancellations or revoked user keys.
1125            false
1126        }
1127        fn native_revoked(
1128            &self,
1129            _: &host::HostedWitnessStatementV1,
1130            _: permission::thread_control_authority::Revocation<'_>,
1131        ) -> bool {
1132            // This fixture only authorizes import geneses and job conversions.
1133            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}