Skip to main content

clawft_kernel/
mesh_swarm_pkg.rs

1//! Signed packages over the swarm (ADR-099 section 6, card
2//! mesh-placement-25): [`ArtifactExchange::swarm_fetch_package`] is
3//! [`ArtifactExchange::fetch_package`] with every artifact pulled from many
4//! holders at once.
5//!
6//! The manifest is fetched first and its signatures are checked before any
7//! file is requested. A revoked package, signer or manifest is refused. Once
8//! the manifest verifies, this node may seed each listed file it holds
9//! verified; a peer's descriptor alone never makes content servable.
10
11use std::sync::Arc;
12
13use crate::mesh_artifact::ArtifactExchange;
14use crate::mesh_artifact_pkg::{ExchangedPackage, PackageExchangeError, pinned};
15use crate::mesh_artifact_types::ArtifactKey;
16use crate::mesh_swarm_fetch::{Expect, PeerDialer, SwarmFetchOptions};
17use crate::mesh_swarm_picker::PeerCandidate;
18use crate::workload_kind::KindRegistry;
19use crate::workload_pkg::codec::{hex_decode_exact, hex_encode};
20use crate::workload_pkg::manifest::MAX_MANIFEST_BYTES;
21use crate::workload_pkg::verify::{VerifyError, verify_manifest_signatures_in};
22use crate::workload_pkg::{TrustAnchors, VerifyPolicy, verify_stored_in};
23
24impl ArtifactExchange {
25    /// Fetch a signed package by its manifest hash from `candidates`.
26    pub async fn swarm_fetch_package(
27        self: &Arc<Self>,
28        dialer: Arc<dyn PeerDialer>,
29        candidates: &[PeerCandidate],
30        manifest_hash: &str,
31        anchors: &TrustAnchors,
32        opts: &SwarmFetchOptions,
33    ) -> Result<ExchangedPackage, PackageExchangeError> {
34        self.swarm_fetch_package_in(
35            dialer,
36            candidates,
37            manifest_hash,
38            anchors,
39            opts,
40            &KindRegistry::builtin(),
41        )
42        .await
43    }
44
45    /// [`Self::swarm_fetch_package`] against a caller-supplied kind registry.
46    pub async fn swarm_fetch_package_in(
47        self: &Arc<Self>,
48        dialer: Arc<dyn PeerDialer>,
49        candidates: &[PeerCandidate],
50        manifest_hash: &str,
51        anchors: &TrustAnchors,
52        opts: &SwarmFetchOptions,
53        kinds: &KindRegistry,
54    ) -> Result<ExchangedPackage, PackageExchangeError> {
55        let mh = hex_decode_exact::<32>(manifest_hash).ok_or_else(|| {
56            VerifyError::Manifest("manifest hash must be 64 lower-case hex".into())
57        })?;
58        let cache = self.swarm.cache();
59        if let Some(c) = &cache {
60            c.pin_content(mh);
61        }
62        let mopts = SwarmFetchOptions {
63            expect: Expect {
64                max_size: Some(MAX_MANIFEST_BYTES as u64),
65                ..opts.expect.clone()
66            },
67            ..opts.clone()
68        };
69        let m = self
70            .swarm_fetch(dialer.clone(), candidates.to_vec(), ArtifactKey::Content(mh), &mopts)
71            .await?;
72        if m.descriptor.total_size > MAX_MANIFEST_BYTES as u64 {
73            return Err(VerifyError::Manifest("manifest too large".into()).into());
74        }
75        let manifest = self.read_all(&m.id)?;
76        let verified = verify_manifest_signatures_in(&manifest, anchors, kinds)?;
77        self.refuse_if_revoked(&verified, anchors, &mh)?;
78        let grant = self.authorize(verified.clone(), mh, Vec::new(), anchors);
79
80        if let Some(c) = &cache {
81            for file in verified.body.files() {
82                c.pin_content(pinned(file)?.0);
83            }
84        }
85        let mut files = Vec::new();
86        let mut all_small = true;
87        for file in verified.body.files() {
88            let (hash, size) = pinned(file)?;
89            let fopts = SwarmFetchOptions {
90                expect: Expect {
91                    size: Some(size),
92                    ..opts.expect.clone()
93                },
94                ..opts.clone()
95            };
96            let got = self
97                .swarm_fetch(dialer.clone(), candidates.to_vec(), ArtifactKey::Content(hash), &fopts)
98                .await?;
99            if got.descriptor.total_size != size {
100                return Err(VerifyError::HashMismatch {
101                    path: file.path.clone(),
102                    expected: format!("{size} bytes"),
103                    actual: format!("{} bytes", got.descriptor.total_size),
104                }
105                .into());
106            }
107            all_small &= size <= self.config().materialize_limit;
108            files.push((file.path.clone(), got.id));
109        }
110        if all_small {
111            verify_stored_in(
112                self.store(),
113                &hex_encode(&mh),
114                anchors,
115                &VerifyPolicy::default(),
116                kinds,
117            )?;
118        }
119        Ok(ExchangedPackage { files, ..grant })
120    }
121}