Skip to content

Commit

Permalink
Add release.Digests.validate method
Browse files Browse the repository at this point in the history
Validates a file path against one or more SHA digests. Uses the `sha1`
and `sha2` crates, which are part of the [Rust Crypto] project. Uses the
`constant_time_eq` crate to compare the values. A new `Digest` error
variant reports digest mismatches, displaying them has hex strings.

Disable automatic line-ending conversion in JSON files to prevent test
failures on Windows.

  [Rust Crypto]: https://github.com/RustCrypto
  • Loading branch information
theory committed Nov 14, 2024
1 parent bf6db96 commit 10c559b
Show file tree
Hide file tree
Showing 7 changed files with 318 additions and 3 deletions.
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Disable line-ending conversion of JSON files, so that the hash digest
# validation tests pass on Windows.
*.json -text
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ All notable changes to this project will be documented in this file. It uses the
pgxn_meta.
* Changed the errors returned by all the APIs from boxed errors [error
module] errors.
* Added `release.Digests.validate` method to validate a file against one or
more digests.

### 📔 Notes

Expand Down
86 changes: 86 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ exclude = [ ".github", ".vscode", ".gitignore", ".ci", ".pre-*.yaml"]
base64 = "0.22"
boon = "0.6"
chrono = { version = "0.4.38", features = ["serde"] }
constant_time_eq = "0.3"
digest = "0.10"
email_address = "0.2.9"
hex = "0.4"
json-patch = "3.0"
lexopt = "0.3.0"
rand = "0.8.5"
Expand All @@ -27,6 +30,8 @@ semver = { version = "1.0", features = ["std", "serde"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1.0"
serde_with = { version = "3.9.0", features = ["hex"] }
sha1 = "0.10"
sha2 = "0.10"
spdx = "0.10.6"
thiserror = "1.0"
wax = "0.6.0"
Expand All @@ -37,5 +42,4 @@ serde_json = "1.0"

[dev-dependencies]
assert-json-diff = "2.0.2"
hex = "0.4.3"
tempfile = "3.12.0"
4 changes: 4 additions & 0 deletions src/error/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ pub enum Error {
/// Missing property value.
#[error("{0} property missing")]
Missing(&'static str),

/// Hash digest mismatch.
#[error("{0} digest {1} does not match {2}")]
Digest(&'static str, String, String),
}

impl<'s, 'v> From<boon::ValidationError<'s, 'v>> for Error {
Expand Down
59 changes: 58 additions & 1 deletion src/release/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ It supports both the [v1] and [v2] specs.

use crate::{dist::*, error::Error, util};
use chrono::{DateTime, Utc};
use hex;
use serde::{de, Deserialize, Deserializer, Serialize};
use serde_json::Value;
use std::{borrow::Borrow, collections::HashMap, fs::File, path::Path};
use std::{borrow::Borrow, collections::HashMap, fs::File, io, path::Path};

mod v1;
mod v2;
Expand Down Expand Up @@ -57,6 +58,62 @@ impl Digests {
pub fn sha512(&self) -> Option<&[u8; 64]> {
self.sha512.as_ref()
}

/// Validates `path` against one or more of the digests. Returns an error
/// on validation failure.
pub fn validate<P: AsRef<Path>>(&self, path: P) -> Result<(), Error> {
self._validate(File::open(path)?)
}

/// Validates `file` against one or more of the digests. Returns an error
/// on validation failure.
fn _validate<P: io::Read + io::Seek>(&self, mut file: P) -> Result<(), Error> {
use sha1::Sha1;
use sha2::{Digest, Sha256, Sha512};
let mut ok = false;

// Prefer SHA-512.
if let Some(digest) = self.sha512() {
compare(&mut file, digest, Sha512::new(), "SHA-512")?;
ok = true;
}

// Allow SHA-256.
if let Some(digest) = self.sha256() {
compare(&mut file, digest, Sha256::new(), "SHA-256")?;
ok = true;
}

// Fall back on SHA-1 for PGXN v1 distributions.
if let Some(digest) = self.sha1() {
compare(&mut file, digest, Sha1::new(), "SHA-1")?;
ok = true;
}

if ok {
return Ok(());
}

// This should not happen, since the validator ensures there's a digest.
Err(Error::Missing("digests"))
}
}

/// Use `hasher` to hash the contents of `file` and compare the result to
/// `digest`. Returns an error on digest failure.
fn compare<P, D>(mut file: P, digest: &[u8], mut hasher: D, alg: &'static str) -> Result<(), Error>
where
P: io::Read + io::Seek,
D: digest::Digest + io::Write,
{
// Rewind the file, as it may be read multiple times.
file.rewind()?;
io::copy(&mut file, &mut hasher)?;
let hash = hasher.finalize();
if constant_time_eq::constant_time_eq(hash.as_slice(), digest) {
return Ok(());
}
Err(Error::Digest(alg, hex::encode(hash), hex::encode(digest)))
}

/// ReleasePayload represents release metadata populated by PGXN.
Expand Down
Loading

0 comments on commit 10c559b

Please sign in to comment.