Skip to content

Commit 4753cd3

Browse files
authored
Use CBOR-based fingerprint serialization (#241)
1 parent 2031e94 commit 4753cd3

34 files changed

Lines changed: 1194 additions & 219 deletions

README.md

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -879,41 +879,29 @@ of file hashing and verification.
879879

880880
Filepack allows for the creation of Ed25519 signatures over the contents of a
881881
manifest, which thus commit to the contents of the directory covered by the
882-
manifest. Signatures are made not over serialized manifest, but over a message
883-
containing a "fingerprint" hash, a Merkle tree hash created from the contents
884-
of the manifest. This keeps signatures independent of the manifest format,
885-
avoids issues with canonicalization of the manifest JSON, avoids hash loops due
886-
to the inclusion of signatures in the manifest itself, and allows proving the
887-
inclusion of files covered by a signature using a Merkle receipt.
882+
manifest. Signatures are made over a message containing a "fingerprint" hash of
883+
a canonical CBOR serialization of the manifest. This keeps signatures
884+
independent of the manifest format, avoids issues with canonicalization of the
885+
manifest JSON, avoids hash loops due to the inclusion of signatures in the
886+
manifest itself, and allows proving the inclusion of files covered by a
887+
signature.
888888

889889
### Fingerprints
890890

891-
Although only package fingerprints are exposed externally, several types of
892-
fingerprints are used internally, namely directory, entry, file, and message
893-
fingerprints.
894-
895-
Fingerprints are constructed to be unique, both between and within types,
896-
meaning that it is impossible two different values with different types or
897-
contents but which have the same fingerprint.
898-
899-
Fingerprints are BLAKE3 hashes. To guarantee that fingerprints are unique
900-
between types, the hasher is first initialized with a length-prefixed string
901-
unique to each type.
902-
903-
After the prefix, the value is hashed as a sequence of TLV fields.
904-
905-
Fields are hashed in order, but may be skipped, in the case of optional fields,
906-
or repeated, in the case of fields containing multiple values.
891+
Package fingerprints are the BLAKE3 hash of a canonical CBOR serialization of
892+
the contents of the manifest. Fingerprints are constructed to be unique,
893+
meaning meaning that it is impossible for two different packages with different
894+
contents to have the same fingerprint.
907895

908896
Currently, no fingerprint test vectors exist, and the best documentation is the
909897
code itself.
910898

911899
In particular, see:
912900

913-
- [FingerprintHasher](src/fingerprint_hasher.rs)
914-
- [FingerprintPrefix](src/fingerprint_prefix.rs)
915901
- [Manifest](src/manifest.rs)
916902
- [Directory](src/directory.rs)
917903
- [Entry](src/entry.rs)
918904
- [Files](src/file.rs)
919905
- [Message](src/message.rs)
906+
907+
And the [cbor](src/cbor.rs) module for the encoding.

src/cbor.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
use super::*;
2+
3+
pub(crate) use self::{
4+
directory::Directory, encode::Encode, encoder::Encoder, entry::Entry, entry_type::EntryType,
5+
head::Head, major_type::MajorType, map_encoder::MapEncoder, version::Version,
6+
};
7+
8+
#[cfg(test)]
9+
pub(crate) use self::{
10+
decode::Decode, decode_error::DecodeError, decoder::Decoder, map_decoder::MapDecoder,
11+
};
12+
13+
#[cfg(test)]
14+
mod decode;
15+
#[cfg(test)]
16+
pub(crate) mod decode_error;
17+
#[cfg(test)]
18+
mod decoder;
19+
mod directory;
20+
mod encode;
21+
mod encoder;
22+
mod entry;
23+
mod entry_type;
24+
mod head;
25+
mod major_type;
26+
#[cfg(test)]
27+
mod map_decoder;
28+
mod map_encoder;
29+
mod version;

src/cbor/decode.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
use super::*;
2+
3+
pub(crate) trait Decode: Sized {
4+
fn decode(decoder: &mut Decoder) -> Result<Self, DecodeError>;
5+
}
6+
7+
impl<K, V> Decode for BTreeMap<K, V>
8+
where
9+
K: Clone + Decode + Debug + Ord + PartialOrd,
10+
V: Decode,
11+
{
12+
fn decode(decoder: &mut Decoder) -> Result<Self, DecodeError> {
13+
let mut decoder = decoder.map::<K>()?;
14+
15+
let mut map = BTreeMap::new();
16+
while let Some((key, value)) = decoder.next::<V>()? {
17+
map.insert(key, value);
18+
}
19+
20+
decoder.finish()?;
21+
22+
Ok(map)
23+
}
24+
}
25+
26+
impl Decode for String {
27+
fn decode(decoder: &mut Decoder) -> Result<Self, DecodeError> {
28+
Ok(decoder.text()?.to_owned())
29+
}
30+
}
31+
32+
impl Decode for Vec<u8> {
33+
fn decode(decoder: &mut Decoder) -> Result<Self, DecodeError> {
34+
Ok(decoder.bytes()?.to_vec())
35+
}
36+
}
37+
38+
impl Decode for u8 {
39+
fn decode(decoder: &mut Decoder) -> Result<Self, DecodeError> {
40+
decoder
41+
.integer()?
42+
.try_into()
43+
.context(decode_error::IntegerRange)
44+
}
45+
}
46+
47+
impl Decode for u64 {
48+
fn decode(decoder: &mut Decoder) -> Result<Self, DecodeError> {
49+
decoder.integer()
50+
}
51+
}
52+
53+
impl Decode for usize {
54+
fn decode(decoder: &mut Decoder) -> Result<Self, DecodeError> {
55+
decoder
56+
.integer()?
57+
.try_into()
58+
.context(decode_error::IntegerRange)
59+
}
60+
}

src/cbor/decode_error.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
use super::*;
2+
3+
#[derive(Debug, PartialEq, Snafu)]
4+
#[snafu(context(suffix(false)), visibility(pub(crate)))]
5+
pub(crate) enum DecodeError {
6+
#[snafu(display("failed to parse component"))]
7+
Component { source: ComponentError },
8+
#[snafu(display("integer out of range"))]
9+
IntegerRange { source: TryFromIntError },
10+
#[snafu(display("map keys out of order"))]
11+
KeyOrder,
12+
#[snafu(display("overlong integer"))]
13+
OverlongInteger,
14+
#[snafu(display("reserved additional information value: {value}"))]
15+
ReservedAdditionalInformation { value: u8 },
16+
#[snafu(display("size out of range"))]
17+
SizeRange { source: TryFromIntError },
18+
#[snafu(display("trailing bytes"))]
19+
TrailingBytes,
20+
#[snafu(display("truncated"))]
21+
Truncated,
22+
#[snafu(display("unconsumed map entries"))]
23+
UnconsumedEntries,
24+
#[snafu(display("unexpected key"))]
25+
UnexpectedKey,
26+
#[snafu(display("expected {expected} but found {actual}"))]
27+
UnexpectedType {
28+
expected: MajorType,
29+
actual: MajorType,
30+
},
31+
#[snafu(display("unexpected value"))]
32+
UnexpectedValue,
33+
#[snafu(display("string not valid unicode"))]
34+
Unicode { source: Utf8Error },
35+
#[snafu(display("unsupported additional information value: {value}"))]
36+
UnsupportedAdditionalInformation { value: u8 },
37+
}

src/cbor/decoder.rs

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
use super::*;
2+
3+
pub(crate) struct Decoder {
4+
buffer: Vec<u8>,
5+
i: usize,
6+
}
7+
8+
impl Decoder {
9+
fn array<const N: usize>(&mut self) -> Result<[u8; N], DecodeError> {
10+
Ok(self.slice(N)?.try_into().unwrap())
11+
}
12+
13+
pub(crate) fn bytes(&mut self) -> Result<&[u8], DecodeError> {
14+
let len = self
15+
.expect(MajorType::Bytes)?
16+
.try_into()
17+
.context(decode_error::SizeRange)?;
18+
19+
self.slice(len)
20+
}
21+
22+
fn expect(&mut self, expected: MajorType) -> Result<u64, DecodeError> {
23+
let Head { major_type, value } = self.head()?;
24+
25+
ensure!(
26+
major_type == expected,
27+
decode_error::UnexpectedType {
28+
actual: major_type,
29+
expected,
30+
}
31+
);
32+
33+
Ok(value)
34+
}
35+
36+
pub(crate) fn finish(self) -> Result<(), DecodeError> {
37+
ensure!(self.i == self.buffer.len(), decode_error::TrailingBytes);
38+
Ok(())
39+
}
40+
41+
pub(crate) fn head(&mut self) -> Result<Head, DecodeError> {
42+
let initial_byte = self.array::<1>()?[0];
43+
44+
let major_type = MajorType::from_initial_byte(initial_byte);
45+
46+
let additional_information = initial_byte & 0b11111;
47+
48+
let value = match additional_information {
49+
0..24 => additional_information.into(),
50+
24 => u8::from_be_bytes(self.array()?).into(),
51+
25 => u16::from_be_bytes(self.array()?).into(),
52+
26 => u32::from_be_bytes(self.array()?).into(),
53+
27 => u64::from_be_bytes(self.array()?),
54+
value @ 28..31 => {
55+
return Err(decode_error::ReservedAdditionalInformation { value }.build());
56+
}
57+
value @ 31 => {
58+
return Err(decode_error::UnsupportedAdditionalInformation { value }.build());
59+
}
60+
32..=u8::MAX => unreachable!(),
61+
};
62+
63+
let min = match additional_information {
64+
0..24 => 0,
65+
24 => 24,
66+
25 => 0x100,
67+
26 => 0x1_0000,
68+
27 => 0x1_0000_0000,
69+
_ => unreachable!(),
70+
};
71+
72+
ensure!(value >= min, decode_error::OverlongInteger);
73+
74+
Ok(Head { major_type, value })
75+
}
76+
77+
pub(crate) fn integer(&mut self) -> Result<u64, DecodeError> {
78+
self.expect(MajorType::Integer)
79+
}
80+
81+
pub(crate) fn map<K>(&mut self) -> Result<MapDecoder<K>, DecodeError> {
82+
let len = self.expect(MajorType::Map)?;
83+
Ok(MapDecoder::new(self, len))
84+
}
85+
86+
pub(crate) fn new(buffer: Vec<u8>) -> Self {
87+
Self { buffer, i: 0 }
88+
}
89+
90+
fn slice(&mut self, n: usize) -> Result<&[u8], DecodeError> {
91+
let start = self.i;
92+
let end = start + n;
93+
94+
ensure! {
95+
end <= self.buffer.len(),
96+
decode_error::Truncated,
97+
}
98+
99+
self.i = end;
100+
101+
Ok(&self.buffer[start..end])
102+
}
103+
104+
pub(crate) fn text(&mut self) -> Result<&str, DecodeError> {
105+
let len = self
106+
.expect(MajorType::Text)?
107+
.try_into()
108+
.context(decode_error::SizeRange)?;
109+
110+
str::from_utf8(self.slice(len)?).context(decode_error::Unicode)
111+
}
112+
}
113+
114+
#[cfg(test)]
115+
mod tests {
116+
use super::*;
117+
118+
#[test]
119+
fn integer_range() {
120+
assert!(matches!(
121+
u8::decode(&mut Decoder::new(256u64.encode_to_vec())),
122+
Err(DecodeError::IntegerRange { .. }),
123+
));
124+
}
125+
126+
#[test]
127+
fn overlong_integer() {
128+
#[track_caller]
129+
fn case(bytes: &[u8]) {
130+
assert_eq!(
131+
Decoder::new(bytes.to_vec()).head(),
132+
Err(DecodeError::OverlongInteger),
133+
);
134+
}
135+
136+
case(&[0x18, 0x17]);
137+
case(&[0x19, 0x00, 0xFF]);
138+
case(&[0x1A, 0x00, 0x00, 0xFF, 0xFF]);
139+
case(&[0x1B, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF]);
140+
}
141+
142+
#[test]
143+
fn reserved_additional_information() {
144+
assert_eq!(
145+
Decoder::new(vec![0x1C]).head(),
146+
Err(DecodeError::ReservedAdditionalInformation { value: 28 }),
147+
);
148+
}
149+
150+
#[test]
151+
fn trailing_bytes() {
152+
assert_eq!(
153+
Decoder::new(vec![0x00]).finish(),
154+
Err(DecodeError::TrailingBytes)
155+
);
156+
}
157+
158+
#[test]
159+
fn truncated() {
160+
assert_eq!(Decoder::new(vec![]).head(), Err(DecodeError::Truncated),);
161+
}
162+
163+
#[test]
164+
fn type_mismatch() {
165+
assert_eq!(
166+
Decoder::new(vec![0x60]).integer(),
167+
Err(DecodeError::UnexpectedType {
168+
expected: MajorType::Integer,
169+
actual: MajorType::Text,
170+
}),
171+
);
172+
}
173+
174+
#[test]
175+
#[expect(invalid_from_utf8)]
176+
fn unicode() {
177+
assert_eq!(
178+
Decoder::new(vec![0x62, 0xFF, 0xFE]).text().map(drop),
179+
Err(DecodeError::Unicode {
180+
source: str::from_utf8(&[0xFF, 0xFE]).unwrap_err()
181+
}),
182+
);
183+
}
184+
185+
#[test]
186+
fn unsupported_additional_information() {
187+
assert_eq!(
188+
Decoder::new(vec![0x1F]).head(),
189+
Err(DecodeError::UnsupportedAdditionalInformation { value: 31 }),
190+
);
191+
}
192+
}

0 commit comments

Comments
 (0)