Skip to content

Commit 4b860f5

Browse files
authored
Sign and verify root hash instead of manifest hash (#64)
1 parent e446814 commit 4b860f5

8 files changed

Lines changed: 70 additions & 30 deletions

File tree

src/error.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,6 @@ pub(crate) enum Error {
7070
backtrace: Option<Backtrace>,
7171
path: DisplayPath,
7272
},
73-
#[snafu(display("manifest hash mismatch"))]
74-
ManifestHashMismatch { backtrace: Option<Backtrace> },
7573
#[snafu(display("manifest `{path}` not found"))]
7674
ManifestNotFound {
7775
backtrace: Option<Backtrace>,
@@ -129,6 +127,8 @@ pub(crate) enum Error {
129127
backtrace: Option<Backtrace>,
130128
path: DisplayPath,
131129
},
130+
#[snafu(display("root hash mismatch"))]
131+
RootHashMismatch { backtrace: Option<Backtrace> },
132132
#[snafu(display("signature `{path}` already exists"))]
133133
SignatureAlreadyExists {
134134
backtrace: Option<Backtrace>,

src/hash.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ use super::*;
44
pub(crate) struct Hash(blake3::Hash);
55

66
impl Hash {
7+
const LEN: usize = blake3::OUT_LEN;
8+
9+
pub(crate) fn as_bytes(&self) -> &[u8; Self::LEN] {
10+
self.0.as_bytes()
11+
}
12+
13+
#[cfg(test)]
714
pub(crate) fn bytes(input: &[u8]) -> Self {
815
Self(blake3::hash(input))
916
}
@@ -15,7 +22,7 @@ impl From<blake3::Hash> for Hash {
1522
}
1623
}
1724

18-
impl From<Hash> for [u8; 32] {
25+
impl From<Hash> for [u8; Hash::LEN] {
1926
fn from(hash: Hash) -> Self {
2027
hash.0.into()
2128
}

src/manifest.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,19 @@ pub(crate) struct Manifest {
88

99
impl Manifest {
1010
pub(crate) const FILENAME: &'static str = "filepack.json";
11+
12+
pub(crate) fn root_hash(&self) -> Hash {
13+
let mut hasher = blake3::Hasher::new();
14+
15+
for (path, entry) in &self.files {
16+
hasher.update(&u64::try_from(path.len()).unwrap().to_le_bytes());
17+
hasher.update(path.as_str().as_bytes());
18+
hasher.update(&entry.size.to_le_bytes());
19+
hasher.update(entry.hash.as_bytes());
20+
}
21+
22+
hasher.finalize().into()
23+
}
1124
}
1225

1326
#[cfg(test)]

src/relative_path.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ impl RelativePath {
2020

2121
const JUNK_NAMES: [&'static str; 2] = [".DS_Store", ".localized"];
2222

23+
pub(crate) fn as_str(&self) -> &str {
24+
self.0.as_str()
25+
}
26+
27+
pub(crate) fn len(&self) -> usize {
28+
self.0.len()
29+
}
30+
2331
pub(crate) fn lint(&self) -> Option<Lint> {
2432
for component in Utf8Path::new(&self.0).components() {
2533
let Utf8Component::Normal(component) = component else {

src/subcommand/create.rs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ impl Create {
2525

2626
let root = self.root.unwrap_or_else(|| current_dir.clone());
2727

28-
let manifest = if let Some(path) = self.manifest {
28+
let manifest_path = if let Some(path) = self.manifest {
2929
path
3030
} else {
3131
root.join(Manifest::FILENAME)
@@ -45,7 +45,7 @@ impl Create {
4545
fs::write(&path, json).context(error::Io { path: &path })?;
4646
}
4747

48-
let cleaned_manifest = current_dir.join(&manifest).lexiclean();
48+
let cleaned_manifest = current_dir.join(&manifest_path).lexiclean();
4949

5050
let cleaned_metadata = self.metadata.map(|path| current_dir.join(path).lexiclean());
5151

@@ -155,9 +155,9 @@ impl Create {
155155
}
156156

157157
ensure! {
158-
self.force || !manifest.try_exists().context(error::Io { path: &manifest })?,
158+
self.force || !manifest_path.try_exists().context(error::Io { path: &manifest_path })?,
159159
error::ManifestAlreadyExists {
160-
path: manifest,
160+
path: manifest_path,
161161
},
162162
}
163163

@@ -173,16 +173,20 @@ impl Create {
173173
bar.inc(entry.size);
174174
}
175175

176-
let json = serde_json::to_string(&Manifest { files }).unwrap();
176+
let manifest = Manifest { files };
177177

178-
fs::write(&manifest, &json).context(error::Io { path: manifest })?;
178+
let json = serde_json::to_string(&manifest).unwrap();
179+
180+
fs::write(&manifest_path, &json).context(error::Io {
181+
path: manifest_path,
182+
})?;
179183

180184
if self.sign {
181185
let private_key_path = options.key_dir()?.join(MASTER_PRIVATE_KEY);
182186

183187
let private_key = PrivateKey::load(&private_key_path)?;
184188

185-
let signature = private_key.sign(json.as_bytes());
189+
let signature = private_key.sign(manifest.root_hash().as_bytes());
186190

187191
let public_key = private_key.public_key();
188192

src/subcommand/verify.rs

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use super::*;
22

33
#[derive(Parser)]
44
pub(crate) struct Verify {
5-
#[arg(help = "Verify that BLAKE3 hash of manifest manifest is <HASH>", long)]
5+
#[arg(help = "Verify manifest root hash is <HASH>", long)]
66
hash: Option<Hash>,
77
#[arg(help = "Ignore missing files", long)]
88
ignore_missing: bool,
@@ -46,26 +46,27 @@ impl Verify {
4646
result => result.context(error::Io { path: &source })?,
4747
};
4848

49+
let manifest = serde_json::from_str::<Manifest>(&json).context(error::DeserializeManifest {
50+
path: Manifest::FILENAME,
51+
})?;
52+
53+
let root_hash = manifest.root_hash();
54+
4955
if let Some(expected) = self.hash {
50-
let actual = Hash::bytes(json.as_bytes());
51-
if actual != expected {
56+
if root_hash != expected {
5257
let style = Style::stderr();
5358
eprintln!(
5459
"\
55-
manifest hash mismatch: `{source}`
56-
expected: {}
57-
actual: {}",
60+
root hash mismatch: `{source}`
61+
expected: {}
62+
actual: {}",
5863
expected.style(style.good()),
59-
actual.style(style.bad()),
64+
root_hash.style(style.bad()),
6065
);
61-
return Err(error::ManifestHashMismatch.build());
66+
return Err(error::RootHashMismatch.build());
6267
}
6368
}
6469

65-
let manifest = serde_json::from_str::<Manifest>(&json).context(error::DeserializeManifest {
66-
path: Manifest::FILENAME,
67-
})?;
68-
6970
let bar = progress_bar::new(manifest.files.values().map(|entry| entry.size).sum());
7071

7172
let mut mismatches = BTreeMap::new();
@@ -180,7 +181,7 @@ mismatched file: `{path}`
180181
.context(error::SignatureMalformed { path })?;
181182

182183
pubkey
183-
.verify(json.as_bytes(), &signature)
184+
.verify(root_hash.as_bytes(), &signature)
184185
.context(error::SignatureInvalid { path })?;
185186

186187
signatures.insert(pubkey);

tests/create.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -586,10 +586,17 @@ fn sign_creates_valid_signature() {
586586
ed25519_dalek::VerifyingKey::from_bytes(&hex::decode(public_key).unwrap().try_into().unwrap())
587587
.unwrap();
588588

589-
let manifest = fs::read_to_string(dir.child("foo/filepack.json")).unwrap();
589+
let mut hasher = blake3::Hasher::new();
590+
591+
hasher.update(&3u64.to_le_bytes());
592+
hasher.update("bar".as_bytes());
593+
hasher.update(&0u64.to_le_bytes());
594+
hasher.update(blake3::hash(&[]).as_bytes());
595+
596+
let root_hash = hasher.finalize();
590597

591598
public_key
592-
.verify_strict(manifest.as_bytes(), &signature)
599+
.verify_strict(root_hash.as_bytes(), &signature)
593600
.unwrap();
594601

595602
Command::cargo_bin("filepack")

tests/verify.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -737,7 +737,7 @@ fn verify_hash() {
737737
.args([
738738
"verify",
739739
"--hash",
740-
"74ddbe0dcf48c634aca1d90f37defd60b230fc52857ffa4b6c956583e8a4daaf",
740+
"409a47939fea6278f60d878fd85d57fe753fa7d87c0dba8e4e4a2b61c81077fb",
741741
])
742742
.current_dir(&dir)
743743
.assert()
@@ -754,10 +754,10 @@ fn verify_hash() {
754754
.assert()
755755
.stderr(is_match(
756756
"\
757-
manifest hash mismatch: `.*filepack\\.json`
758-
expected: 0000000000000000000000000000000000000000000000000000000000000000
759-
actual: 74ddbe0dcf48c634aca1d90f37defd60b230fc52857ffa4b6c956583e8a4daaf
760-
error: manifest hash mismatch\n",
757+
root hash mismatch: `.*filepack\\.json`
758+
expected: 0000000000000000000000000000000000000000000000000000000000000000
759+
actual: 409a47939fea6278f60d878fd85d57fe753fa7d87c0dba8e4e4a2b61c81077fb
760+
error: root hash mismatch\n",
761761
))
762762
.failure();
763763
}

0 commit comments

Comments
 (0)