From a8a90158c1f4ee861f1017aab9874a0d63001c28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dorian=20P=C3=A9ron?= Date: Wed, 25 Mar 2026 23:16:42 +0100 Subject: [PATCH 1/2] test(cksum): Add multiple tests on check length digest validation --- tests/by-util/test_cksum.rs | 110 ++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/tests/by-util/test_cksum.rs b/tests/by-util/test_cksum.rs index 16f18706f68..16ed6d8b373 100644 --- a/tests/by-util/test_cksum.rs +++ b/tests/by-util/test_cksum.rs @@ -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!()); @@ -562,6 +605,73 @@ fn test_check_sha2_tagged_variant() { } } +#[test] +fn test_check_sha2_tagged_missing_hint() { + // SHA2 () = + // + // 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"] { From 6723393d527ba858ec3bc4a06d35eacc014af4e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dorian=20P=C3=A9ron?= Date: Thu, 26 Mar 2026 00:18:10 +0100 Subject: [PATCH 2/2] cksum: make sure --check raises "improperly formatted" for length-guessing related issues cksum: Fix --check failing when digest length is wrong on untagged line --- src/uucore/src/lib/features/checksum/mod.rs | 16 +++++++++ .../src/lib/features/checksum/validate.rs | 34 ++++++++++++------- src/uucore/src/lib/features/sum.rs | 11 ++++-- 3 files changed, 46 insertions(+), 15 deletions(-) diff --git a/src/uucore/src/lib/features/checksum/mod.rs b/src/uucore/src/lib/features/checksum/mod.rs index e6145d3748e..d18e2adaad8 100644 --- a/src/uucore/src/lib/features/checksum/mod.rs +++ b/src/uucore/src/lib/features/checksum/mod.rs @@ -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 { + 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. diff --git a/src/uucore/src/lib/features/checksum/validate.rs b/src/uucore/src/lib/features/checksum/validate.rs index fa51539fe1e..bb47ff6c920 100644 --- a/src/uucore/src/lib/features/checksum/validate.rs +++ b/src/uucore/src/lib/features/checksum/validate.rs @@ -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}; @@ -490,7 +490,7 @@ impl LineInfo { } /// Extract the expected digest from the checksum string and decode it -fn get_raw_expected_digest(checksum: &str, byte_len_hint: Option) -> Option> { +fn get_raw_expected_digest(checksum: &str, bit_len_hint: Option) -> Option> { // 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). @@ -498,6 +498,8 @@ fn get_raw_expected_digest(checksum: &str, byte_len_hint: Option) -> Opti 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 @@ -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), + (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) } @@ -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 @@ -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, }; diff --git a/src/uucore/src/lib/features/sum.rs b/src/uucore/src/lib/features/sum.rs index 5c998cd0764..7a9f1a5ce58 100644 --- a/src/uucore/src/lib/features/sum.rs +++ b/src/uucore/src/lib/features/sum.rs @@ -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]) { ::update(&mut self.0, input); @@ -190,7 +194,7 @@ impl Digest for Sm3 { } fn output_bits(&self) -> usize { - 256 + Self::BIT_SIZE } } @@ -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()) @@ -386,7 +393,7 @@ macro_rules! impl_digest_common { } fn output_bits(&self) -> usize { - $size + Self::BIT_SIZE } } };