Skip to main content

krabka_remote_storage/worm/verify/
manifest_read.rs

1//! Getting one manifest out of an attacker-controlled object.
2//!
3//! Every manifest the verifier reads comes from the archive it is auditing, so
4//! this module is the boundary that decides whether an object is a manifest at
5//! all. It caps the read, refuses a body it cannot decode, and refuses a format
6//! version it does not implement, and it reports each refusal as archive
7//! content rather than as a failure to look.
8
9use {
10    crate::worm::{
11        error::WormError,
12        manifest::{MANIFEST_FORMAT_VERSION, SegmentManifest},
13    },
14    krabka_object_store::{ObjectStoreError, read_capped},
15    object_store::{ObjectStore, path::Path},
16    std::sync::Arc,
17};
18
19/// Byte cap on one manifest object.
20///
21/// A manifest describes a single segment copy, so a real one is a few
22/// kilobytes. The cap is what stops a hostile archive from making the verifier
23/// buffer an arbitrary object: [`read_capped`] issues a `HEAD` first and
24/// refuses an oversized object before it reads a byte of it.
25pub const MAX_MANIFEST_BYTES: u64 = 1024 * 1024;
26
27/// One manifest object, as far as the verifier could take it.
28pub(super) enum ManifestRead {
29    /// The object decoded into a manifest of a supported format version.
30    Decoded(Box<SegmentManifest>),
31    /// The object is not a manifest this verifier accepts, and why.
32    Rejected(String),
33}
34
35/// Reads and decodes one manifest object.
36pub(super) async fn read_manifest(
37    store: &Arc<dyn ObjectStore>,
38    key: &str,
39) -> Result<ManifestRead, WormError> {
40    let bytes = match read_capped(store, &Path::from(key), MAX_MANIFEST_BYTES).await {
41        Ok(bytes) => bytes,
42        // An oversized object is archive content and not a failure to look, so
43        // it grades as a break rather than as an error.
44        Err(ObjectStoreError::TooLarge {
45            size, max_bytes, ..
46        }) => {
47            return Ok(ManifestRead::Rejected(format!(
48                "manifest object is {size} bytes, above the {max_bytes} byte cap"
49            )));
50        }
51        Err(e) => {
52            return Err(WormError::Archive(format!(
53                "cannot read manifest `{key}`: {e}"
54            )));
55        }
56    };
57    let manifest: SegmentManifest = match serde_json::from_slice(&bytes) {
58        Ok(manifest) => manifest,
59        Err(e) => {
60            return Ok(ManifestRead::Rejected(format!(
61                "manifest does not decode: {e}"
62            )));
63        }
64    };
65    if let Some(reason) = format_version_problem(manifest.body.format_version) {
66        return Ok(ManifestRead::Rejected(reason));
67    }
68    Ok(ManifestRead::Decoded(Box::new(manifest)))
69}
70
71/// Why a manifest of `version` is refused, or `None` for
72/// [`MANIFEST_FORMAT_VERSION`], the only version a 1.x verifier reads.
73fn format_version_problem(version: u32) -> Option<String> {
74    match version {
75        MANIFEST_FORMAT_VERSION => None,
76        1 => Some(format!(
77            "manifest format version 1 predates krabka 1.0, whose verifier reads only version \
78             {MANIFEST_FORMAT_VERSION}"
79        )),
80        other => Some(format!(
81            "unsupported manifest format version {other}: this build reads only version \
82             {MANIFEST_FORMAT_VERSION}"
83        )),
84    }
85}
86
87/// One manifest object and the key it was read from.
88pub(super) struct KeyedManifest {
89    pub key: String,
90    pub manifest: SegmentManifest,
91}
92
93#[cfg(test)]
94mod tests {
95    use {
96        super::*,
97        crate::worm::verify::test_support::{Archive, put_raw},
98        assert2::check,
99        bytes::Bytes,
100    };
101
102    #[yare::parameterized(
103        pre_1_0 = {
104            1,
105            "manifest format version 1 predates krabka 1.0, whose verifier reads only version 2",
106        },
107        older = { 0, "unsupported manifest format version 0: this build reads only version 2" },
108        future = { 3, "unsupported manifest format version 3: this build reads only version 2" },
109    )]
110    #[test_macro(tokio::test)]
111    async fn a_manifest_of_any_version_but_the_current_one_is_a_break(
112        version: u64,
113        expected: &str,
114    ) {
115        let archive = Archive::build(&[1]).await;
116        let segment = &archive.segments[0];
117        let mut value = serde_json::to_value(&segment.manifest).unwrap();
118        value["body"]["format_version"] = serde_json::json!(version);
119        put_raw(
120            &archive.ops,
121            &segment.manifest_key,
122            Bytes::from(serde_json::to_vec(&value).unwrap()),
123        )
124        .await;
125
126        let report = archive.verify().await;
127
128        check!(report.first_break().map(|found| found.reason.as_str()) == Some(expected));
129    }
130
131    #[test]
132    fn only_the_current_manifest_version_is_read() {
133        check!(MANIFEST_FORMAT_VERSION == 2);
134        check!(format_version_problem(MANIFEST_FORMAT_VERSION) == None);
135    }
136
137    #[tokio::test]
138    async fn an_oversized_manifest_object_is_a_break_and_not_an_error() {
139        let archive = Archive::build(&[1]).await;
140        let huge = vec![b'{'; usize::try_from(MAX_MANIFEST_BYTES).unwrap() + 1];
141        put_raw(
142            &archive.ops,
143            &archive.segments[0].manifest_key,
144            Bytes::from(huge),
145        )
146        .await;
147
148        let report = archive.verify().await;
149
150        check!(!report.ok());
151        match report.first_break() {
152            Some(found) => {
153                check!(found.reason.contains("above the"));
154            }
155            None => {
156                check!(false, "an oversized manifest must record a break");
157            }
158        }
159    }
160}