delivery_blob/
format.rs

1// Copyright 2023 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! This module contains types used to serialize/deserialize and verify delivery blobs into their
6//! typed equivalents ([`DeliveryBlobHeader`], [`Type1Blob`], etc...).
7//!
8//! **WARNING**: Use caution when making changes to this file. All format changes for a given
9//! delivery blob type **must** be backwards compatible, or a new type must be used.
10
11use bitflags::bitflags;
12use crc::Hasher32 as _;
13use static_assertions::const_assert_eq;
14use zerocopy::byteorder::{LE, U32, U64};
15use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
16
17use crate::{DeliveryBlobError, DeliveryBlobHeader, DeliveryBlobType, Type1Blob};
18
19/// Delivery blob magic number (0xfc1ab10b or "Fuchsia Blob" in big-endian).
20const DELIVERY_BLOB_MAGIC: [u8; 4] = [0xfc, 0x1a, 0xb1, 0x0b];
21
22// Binary format compatibility checks:
23const_assert_eq!(std::mem::size_of::<SerializedHeader>(), 12);
24const_assert_eq!(
25    std::mem::size_of::<SerializedType1Blob>(),
26    std::mem::size_of::<SerializedHeader>() + 16
27);
28
29bitflags! {
30    /// Type 1 delivery blob flags.
31    #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
32    struct SerializedType1Flags : u32 {
33        const IS_COMPRESSED = 0x00000001;
34        const VALID_FLAGS_MASK = Self::IS_COMPRESSED.bits();
35    }
36}
37
38impl From<&Type1Blob> for SerializedType1Flags {
39    fn from(value: &Type1Blob) -> Self {
40        if value.is_compressed {
41            SerializedType1Flags::IS_COMPRESSED
42        } else {
43            SerializedType1Flags::empty()
44        }
45    }
46}
47
48/// Serialized header of an RFC 0207 compliant delivery blob.
49#[derive(IntoBytes, KnownLayout, FromBytes, Immutable, Unaligned, Clone, Copy, Debug)]
50#[repr(C)]
51pub(crate) struct SerializedHeader {
52    magic: [u8; 4],
53    delivery_type: U32<LE>,
54    header_length: U32<LE>,
55}
56
57impl SerializedHeader {
58    pub fn decode(&self) -> Result<DeliveryBlobHeader, DeliveryBlobError> {
59        if self.magic != DELIVERY_BLOB_MAGIC {
60            return Err(DeliveryBlobError::BadMagic);
61        }
62        Ok(DeliveryBlobHeader {
63            delivery_type: self.delivery_type.get().try_into()?,
64            header_length: self.header_length.get(),
65        })
66    }
67}
68
69impl From<&DeliveryBlobHeader> for SerializedHeader {
70    fn from(value: &DeliveryBlobHeader) -> Self {
71        Self {
72            magic: DELIVERY_BLOB_MAGIC,
73            delivery_type: Into::<u32>::into(value.delivery_type).into(),
74            header_length: value.header_length.into(),
75        }
76    }
77}
78
79/// Serialized header + metadata of a Type 1 delivery blob. Use [`Type1Blob::parse`] to deserialize
80/// and validate a Type 1 delivery blob as opposed to deserializing this struct directly.
81///
82/// **WARNING**: Changes to this format must be done in a backwards compatible manner, or a new
83/// delivery blob type should be created. This format should be considered an implementation detail,
84/// and not relied on outside of storage-owned components.
85#[derive(IntoBytes, KnownLayout, FromBytes, Immutable, Unaligned, Clone, Copy, Debug)]
86#[repr(C)]
87pub(crate) struct SerializedType1Blob {
88    // Header:
89    header: SerializedHeader,
90    // Metadata:
91    payload_length: U64<LE>,
92    checksum: U32<LE>,
93    flags: U32<LE>,
94}
95
96impl From<Type1Blob> for SerializedType1Blob {
97    fn from(value: Type1Blob) -> Self {
98        let serialized = Self {
99            header: (&value.header).into(),
100            payload_length: (value.payload_length as u64).into(),
101            checksum: Default::default(), // Calculated below.
102            flags: SerializedType1Flags::from(&value).bits().into(),
103        };
104
105        Self { checksum: serialized.checksum().into(), ..serialized }
106    }
107}
108
109impl SerializedType1Blob {
110    pub fn checksum(&self) -> u32 {
111        // Create a copy of the serialized blob but with the checksum zeroed.
112        let header = Self { checksum: 0.into(), ..*self };
113        let mut digest = crc::crc32::Digest::new(crc::crc32::IEEE);
114        digest.write(header.as_bytes());
115        digest.sum32()
116    }
117
118    /// Decode and verify this serialized Type 1 delivery blob.
119    pub fn decode(&self) -> Result<Type1Blob, DeliveryBlobError> {
120        // Validate checksum before other integrity checks.
121        if self.checksum.get() != self.checksum() {
122            return Err(DeliveryBlobError::IntegrityError);
123        }
124        // Validate header.
125        let header: DeliveryBlobHeader = self.header.decode()?;
126        if header.delivery_type != DeliveryBlobType::Type1 {
127            return Err(DeliveryBlobError::InvalidType);
128        }
129        if header.header_length != Type1Blob::HEADER.header_length {
130            return Err(DeliveryBlobError::IntegrityError);
131        }
132        // Validate and decode remaining metadata fields.
133        let payload_length = self.payload_length.get() as usize;
134        let flags = SerializedType1Flags::from_bits(self.flags.get())
135            .ok_or(DeliveryBlobError::IntegrityError)?;
136
137        Ok(Type1Blob {
138            header,
139            payload_length,
140            is_compressed: flags.contains(SerializedType1Flags::IS_COMPRESSED),
141        })
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148    use crate::CompressionMode;
149
150    const TEST_DATA: &[u8] = &[1, 2, 3, 4];
151
152    #[test]
153    fn type_1_round_trip_uncompressed() {
154        let delivery_blob = Type1Blob::generate(TEST_DATA, CompressionMode::Never);
155        assert_eq!(
156            delivery_blob.len(),
157            std::mem::size_of::<SerializedType1Blob>() + TEST_DATA.len()
158        );
159        // We should be able to decode and verify the parsed data.
160        let (metadata, payload) = Type1Blob::parse(&delivery_blob).unwrap().unwrap();
161        assert_eq!(metadata.header, Type1Blob::HEADER);
162        assert_eq!(metadata.payload_length, TEST_DATA.len());
163        assert_eq!(metadata.is_compressed, false);
164        // Verify that the payload matches.
165        assert_eq!(payload, TEST_DATA);
166    }
167
168    #[test]
169    fn type_1_round_trip_empty() {
170        let delivery_blob = Type1Blob::generate(&[], CompressionMode::Never);
171        assert_eq!(delivery_blob.len(), std::mem::size_of::<SerializedType1Blob>());
172        // We should be able to decode and verify the parsed data.
173        let (metadata, payload) = Type1Blob::parse(&delivery_blob).unwrap().unwrap();
174        assert_eq!(metadata.header, Type1Blob::HEADER);
175        assert_eq!(metadata.payload_length, 0);
176        assert_eq!(metadata.is_compressed, false);
177        assert!(payload.is_empty());
178    }
179
180    #[test]
181    fn type_1_not_enough_data() {
182        let delivery_blob = Type1Blob::generate(TEST_DATA, CompressionMode::Never);
183        let not_enough_data = &delivery_blob[..std::mem::size_of::<SerializedType1Blob>() - 1];
184        assert!(Type1Blob::parse(not_enough_data).unwrap().is_none());
185    }
186
187    #[test]
188    fn type_1_bad_magic() {
189        // Create a valid serialized Type 1 blob.
190        let valid: SerializedType1Blob =
191            Type1Blob { header: Type1Blob::HEADER, payload_length: 0, is_compressed: false }
192                .try_into()
193                .unwrap();
194        assert!(Type1Blob::parse(valid.as_bytes()).is_ok());
195        // Corrupt magic, recalculate checksum, and ensure we fail with the correct error.
196        let mut has_corrupt_magic = SerializedType1Blob {
197            header: SerializedHeader { magic: [0, 0, 0, 0], ..valid.header },
198            ..valid
199        };
200        has_corrupt_magic.checksum = has_corrupt_magic.checksum().into();
201        assert_eq!(
202            Type1Blob::parse(has_corrupt_magic.as_bytes()).unwrap_err(),
203            DeliveryBlobError::BadMagic
204        );
205    }
206
207    #[test]
208    fn type_1_invalid_type() {
209        // We should fail to parse a Type 1 blob with the wrong type specified in the header.
210        let has_invalid_type: SerializedType1Blob = Type1Blob {
211            header: DeliveryBlobHeader {
212                delivery_type: DeliveryBlobType::Reserved,
213                header_length: 0,
214            },
215            payload_length: 0,
216            is_compressed: false,
217        }
218        .try_into()
219        .unwrap();
220        assert_eq!(
221            Type1Blob::parse(has_invalid_type.as_bytes()).unwrap_err(),
222            DeliveryBlobError::InvalidType
223        );
224    }
225
226    #[test]
227    fn type_1_invalid_header_length() {
228        // We should fail to parse a Type 1 blob with the wrong header length.
229        let has_invalid_header_length: SerializedType1Blob = Type1Blob {
230            header: DeliveryBlobHeader {
231                delivery_type: DeliveryBlobType::Type1,
232                header_length: Type1Blob::HEADER.header_length + 1,
233            },
234            payload_length: 0,
235            is_compressed: false,
236        }
237        .try_into()
238        .unwrap();
239        assert_eq!(
240            Type1Blob::parse(has_invalid_header_length.as_bytes()).unwrap_err(),
241            DeliveryBlobError::IntegrityError
242        );
243    }
244
245    #[test]
246    fn type_1_verify_checksum() {
247        // Verify that we calculate the correct checksum for a serialized Type 1 blob.
248        let serialized: SerializedType1Blob = Type1Blob {
249            header: Type1Blob::HEADER,
250            payload_length: TEST_DATA.len(),
251            is_compressed: false,
252        }
253        .try_into()
254        .unwrap();
255        assert!(serialized.decode().is_ok());
256        assert_eq!(serialized.checksum.get(), serialized.checksum());
257        // We should fail to parse a Type 1 blob with a corrupted checksum.
258        let corrupted_checksum: u32 = !(serialized.checksum.get());
259        let corrupted_blob =
260            SerializedType1Blob { checksum: corrupted_checksum.into(), ..serialized };
261        assert_eq!(
262            Type1Blob::parse(corrupted_blob.as_bytes()).unwrap_err(),
263            DeliveryBlobError::IntegrityError
264        );
265    }
266}