Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/uucore/src/lib/features/checksum/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,22 @@ impl AlgoKind {
use AlgoKind::*;
matches!(self, Sysv | Bsd | Crc | Crc32b)
}

/// When checking untagged format lines, non-XOF non-legacy algorithms
/// should report "improperly formatted lines" if the digest length isn't
/// equivalent to this.
pub fn expected_digest_bit_len(self) -> Option<usize> {
match self {
Self::Md5 => Some(Md5::BIT_SIZE),
Self::Sm3 => Some(Sm3::BIT_SIZE),
Self::Sha1 => Some(Sha1::BIT_SIZE),
Self::Sha224 => Some(Sha224::BIT_SIZE),
Self::Sha256 => Some(Sha256::BIT_SIZE),
Self::Sha384 => Some(Sha384::BIT_SIZE),
Self::Sha512 => Some(Sha512::BIT_SIZE),
_ => None,
}
}
}

/// Holds a length for a SHA2 of SHA3 algorithm kind.
Expand Down
34 changes: 21 additions & 13 deletions src/uucore/src/lib/features/checksum/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use std::io::{self, BufReader, Read, Write, stderr, stdin};
use os_display::Quotable;

use crate::checksum::{
AlgoKind, BlakeLength, ChecksumError, ReadingMode, SizedAlgoKind, digest_reader,
AlgoKind, BlakeLength, ChecksumError, ReadingMode, ShaLength, SizedAlgoKind, digest_reader,
parse_blake_length, unescape_filename,
};
use crate::error::{FromIo, UError, UIoError, UResult, USimpleError};
Expand Down Expand Up @@ -490,14 +490,16 @@ impl LineInfo {
}

/// Extract the expected digest from the checksum string and decode it
fn get_raw_expected_digest(checksum: &str, byte_len_hint: Option<usize>) -> Option<Vec<u8>> {
fn get_raw_expected_digest(checksum: &str, bit_len_hint: Option<usize>) -> Option<Vec<u8>> {
// If the length of the digest is not a multiple of 2, then it must be
// improperly formatted (1 byte is 2 hex digits, and base64 strings should
// always be a multiple of 4).
if !checksum.len().is_multiple_of(2) {
return None;
}

let byte_len_hint = bit_len_hint.map(|n| n.div_ceil(8));

let checks_hint = |len| byte_len_hint.is_none_or(|hint| hint == len);

// If the length of the string matches the one to be expected (in case it's
Expand Down Expand Up @@ -741,23 +743,23 @@ fn process_algo_based_line(
) -> Result<(), LineCheckError> {
let filename_to_check = line_info.filename.as_slice();

let (algo_kind, algo_byte_len) =
identify_algo_name_and_length(line_info, cli_algo_kind, last_algo)?;
let (algo_kind, algo_len) = identify_algo_name_and_length(line_info, cli_algo_kind, last_algo)?;

// If the digest bitlen is known, we can check the format of the expected
// checksum with it.
let digest_char_length_hint = match (algo_kind, algo_byte_len) {
(AlgoKind::Blake2b | AlgoKind::Blake3, Some(byte_len)) => Some(byte_len),
(AlgoKind::Shake128 | AlgoKind::Shake256, Some(bit_len)) => Some(bit_len.div_ceil(8)),
(AlgoKind::Shake128, None) => Some(sum::Shake128::DEFAULT_BIT_SIZE.div_ceil(8)),
(AlgoKind::Shake256, None) => Some(sum::Shake256::DEFAULT_BIT_SIZE.div_ceil(8)),
let digest_bit_length_hint = match (algo_kind, algo_len) {
(AlgoKind::Blake2b | AlgoKind::Blake3, Some(byte_len)) => Some(byte_len * 8),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

digest_char_length_hint now stores a bit length instead of a byte length hint? the name should probably change then.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done 👍

(AlgoKind::Shake128 | AlgoKind::Shake256, Some(bit_len)) => Some(bit_len),
(AlgoKind::Shake128, None) => Some(sum::Shake128::DEFAULT_BIT_SIZE),
(AlgoKind::Shake256, None) => Some(sum::Shake256::DEFAULT_BIT_SIZE),
_ => None,
};

let expected_checksum = get_raw_expected_digest(&line_info.checksum, digest_char_length_hint)
let expected_checksum = get_raw_expected_digest(&line_info.checksum, digest_bit_length_hint)
.ok_or(LineCheckError::ImproperlyFormatted)?;

let algo = SizedAlgoKind::from_unsized(algo_kind, algo_byte_len)?;
let algo = SizedAlgoKind::from_unsized(algo_kind, algo_len)
.map_err(|_| LineCheckError::ImproperlyFormatted)?;

compute_and_check_digest_from_file(filename_to_check, &expected_checksum, algo, opts)
}
Expand All @@ -779,7 +781,9 @@ fn process_non_algo_based_line(
// Remove the leading asterisk if present - only for the first line
filename_to_check = &filename_to_check[1..];
}
let expected_checksum = get_raw_expected_digest(&line_info.checksum, None)

let expected_digest_sum = cli_algo_kind.expected_digest_bit_len();
let expected_checksum = get_raw_expected_digest(&line_info.checksum, expected_digest_sum)
.ok_or(LineCheckError::ImproperlyFormatted)?;

// When a specific algorithm name is input, use it and use the provided
Expand All @@ -789,7 +793,11 @@ fn process_non_algo_based_line(
ak::Blake2b | ak::Blake3 => Some(expected_checksum.len()),
ak::Sha2 | ak::Sha3 => {
// multiplication by 8 to get the number of bits
Some(expected_checksum.len() * 8)
Some(
ShaLength::try_from(expected_checksum.len() * 8)
.map_err(|_| LineCheckError::ImproperlyFormatted)?
.as_usize(),
)
}
_ => cli_algo_length,
};
Expand Down
11 changes: 9 additions & 2 deletions src/uucore/src/lib/features/sum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@ impl Digest for Blake3 {
#[derive(Default)]
pub struct Sm3(sm3::Sm3);

impl Sm3 {
pub const BIT_SIZE: usize = 256;
}

impl Digest for Sm3 {
fn hash_update(&mut self, input: &[u8]) {
<sm3::Sm3 as sm3::Digest>::update(&mut self.0, input);
Expand All @@ -190,7 +194,7 @@ impl Digest for Sm3 {
}

fn output_bits(&self) -> usize {
256
Self::BIT_SIZE
}
}

Expand Down Expand Up @@ -367,6 +371,9 @@ impl Digest for SysV {
// Implements the Digest trait for sha2 / sha3 algorithms with fixed output
macro_rules! impl_digest_common {
($algo_type: ty, $size: literal) => {
impl $algo_type {
pub const BIT_SIZE: usize = $size;
}
impl Default for $algo_type {
fn default() -> Self {
Self(Default::default())
Expand All @@ -386,7 +393,7 @@ macro_rules! impl_digest_common {
}

fn output_bits(&self) -> usize {
$size
Self::BIT_SIZE
}
}
};
Expand Down
110 changes: 110 additions & 0 deletions tests/by-util/test_cksum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,49 @@ fn test_check_untagged_sha_multiple_files(algo: &str, len: u32) {
.stdout_contains("alice_in_wonderland.txt: OK\n");
}

#[rstest]
#[case::sha2("sha2", &[
"d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f",
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b",
"cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e"
])]
#[case::sha3("sha3", &[
"6b4e03423667dbb73b6e15454f0eb1abd4597f9a1b078e3f5b5a6bc7",
"a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a",
"0c63a75b845e4f7d01107d852e4c2485c51a50aaaa94fc61995e71bbee983a2ac3713831264adb47fb6bd1e058d5f004",
"a69f73cca23a9ac5c8b567dc185a756e97c982164fe25859e0d1dcc1475c80a615b2123af1f5f94c11e3e9402c3ac558f500199d95b6d3e301758586281dcd26"
])]
fn test_check_untagged_sha_invalid_length(#[case] algo: &str, #[case] digests: &[&str; 4]) {
// When checking with --algorithm sha2/sha3, Guess the length from the provided
// digest. Raise "improperly formatted" if the digest is not af adequate
// size (224, 256, 384 or 512 bits).

let (at, mut ucmd) = at_and_ucmd!();
at.touch("a");
at.touch("b");
at.touch("c");
at.touch("d");
at.touch("e");

let invalid = "xxxx e";

ucmd.arg("-a")
.arg(algo)
.arg("-c")
.pipe_in(format!(
"{} a\n{} b\n{} c\n{} d\n{invalid}",
digests[0], digests[1], digests[2], digests[3]
))
.succeeds()
.stdout_contains("a: OK")
.stdout_contains("b: OK")
.stdout_contains("c: OK")
.stdout_contains("d: OK")
.stdout_does_not_contain("e: FAILED")
.stderr_contains("improperly formatted");
}

#[test]
fn test_check_sha2_tagged_variant() {
let scene = TestScenario::new(util_name!());
Expand Down Expand Up @@ -562,6 +605,73 @@ fn test_check_sha2_tagged_variant() {
}
}

#[test]
fn test_check_sha2_tagged_missing_hint() {
// SHA2 (<file>) = <digest>
//
// should fail and raise "improperly formatted" because SHA2 expects a
// mandatory length hint.

let (at, mut ucmd) = at_and_ucmd!();
at.touch("a");
at.touch("b");

// valid digest but missing hint
let invalid_sha224 = "SHA2 (a) = d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f";
// invalid digest
let invalid = "SHA2 (b) = xxxx";

ucmd.arg("-c")
.pipe_in(format!("{invalid_sha224}\n{invalid}"))
.fails()
.stderr_contains("no properly formatted checksum lines found");
}

#[rstest]
#[case::md5("md5", "d41d8cd98f00b204e9800998ecf8427e")]
#[case::sha1("sha1", "da39a3ee5e6b4b0d3255bfef95601890afd80709")]
#[case::sha224("sha224", "d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f")]
#[case::sha256(
"sha256",
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
)]
#[case::sha384(
"sha384",
"38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b"
)]
#[case::sha512(
"sha512",
"cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e"
)]
#[case::sm3(
"sm3",
"1ab21d8355cfa17f8e61194831e81a8f22bec8c728fefb747ed035eb5082aa2b"
)]
fn test_check_untagged_with_invalid_length(#[case] algo: &str, #[case] digest: &str) {
// issue #11202

// Ensures that when checking untagged lines, if the provided algorithm is
// one of `sha(224|256|384|512)`, digests whose length mismatches the
// length of the given algorithm will report as "improperly formatted"
// rather than a FAILED check.

let (at, mut ucmd) = at_and_ucmd!();
at.touch("a");
at.touch("b");

let good_checksum = format!("{digest} a");
let invalid_checksum = "e3b0 b";

ucmd.arg("-a")
.arg(algo)
.arg("-c")
.pipe_in(format!("{invalid_checksum}\n{good_checksum}"))
.succeeds()
.stdout_contains("a: OK")
.stdout_does_not_contain("b: FAILED")
.stderr_contains("WARNING: 1 line is improperly formatted");
}

#[test]
fn test_check_algo() {
for algo in ["bsd", "sysv", "crc", "crc32b"] {
Expand Down
Loading