Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
133ea1b
sha3: fix partial-byte squeeze, validate partial-bit inputs, docs and…
dghgit Aug 26, 2026
5afe03d
sha3: share finalize() between do_final_out and do_final_partial_bits…
dghgit Aug 26, 2026
989e339
sha3: rustfmt
dghgit Aug 26, 2026
b8beef8
sha3: address PR #87 review
dghgit Aug 27, 2026
77b5d86
sha3: read NIST FIPS 202 example vectors from bc-test-data
dghgit Aug 27, 2026
b57febb
sha3: add NIST CAVP SHA3VS harness; fix double SHAKE suffix on 4-bit …
dghgit Aug 27, 2026
6153bbf
core: document the partial-byte bit-ordering convention on Hash
dghgit Aug 27, 2026
8496d7e
Add 0.1.3 release notes for the SHA-3 changes (PR #87)
dghgit Aug 27, 2026
c940a5d
Add bench_sha3_mem_usage: struct sizes and massif entry points for SH…
dghgit Aug 27, 2026
19371ae
sha3: fix SHAKE partial-byte squeeze, 4-bit tail double suffix, and p…
ounsworth Aug 27, 2026
ec56486
shortened the release notes
ounsworth Sep 1, 2026
f5b8c3d
some work on the sha3 partial byte bug pr
ounsworth Sep 2, 2026
0666366
tightened up absorb-after-squeeze behaviour for shake
ounsworth Sep 2, 2026
2a517e6
tightened up absorb-after-squeeze behaviour for shake
ounsworth Sep 2, 2026
bad45f9
SHAKE::squeeze_partial_byte now works on num_bits=0
ounsworth Sep 2, 2026
90690de
SHAKE::squeeze_partial_byte now works on num_bits=0
ounsworth Sep 2, 2026
da51bf0
sha3: fix SHAKE partial-byte squeeze, 4-bit tail double suffix, and p…
ounsworth Aug 27, 2026
9c4b16a
shortened the release notes
ounsworth Sep 1, 2026
7903d42
some work on the sha3 partial byte bug pr
ounsworth Sep 2, 2026
225e9d0
tightened up absorb-after-squeeze behaviour for shake
ounsworth Sep 2, 2026
a265fd6
tightened up absorb-after-squeeze behaviour for shake
ounsworth Sep 2, 2026
f7a82ad
SHAKE::squeeze_partial_byte now works on num_bits=0
ounsworth Sep 2, 2026
dbcb020
SHAKE::squeeze_partial_byte now works on num_bits=0
ounsworth Sep 2, 2026
04d1e45
Merge branch 'fix/sha3-shake-partial-bits' of github.com:ounsworth/bc…
ounsworth Sep 2, 2026
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
4 changes: 4 additions & 0 deletions alpha_0.1.3_release_notes.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,3 +3,7 @@
## Major features

## Minor features / bug fixes

* bug fixes to the way SHA3/SHAKE handled absorbing and squeezing a partial final byte.
* Design discussions about whether core::traits::XOF (in the abstract) should allow interleaving absorb -> squeeze ->
absorb (ie "absorb-after-squeeze). Outcome: absorb-after-squeeze forbidden. Could be changed in the future.
2 changes: 1 addition & 1 deletion cli/src/main.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -119,7 +119,7 @@ enum Subcommands {
/// Length of the output in bytes.
length: usize,

#[arg(long)]
#[arg(short)]
/// Output the hashes in hex format.
x: bool,
},
Expand Down
127 changes: 109 additions & 18 deletions crypto/core-test-framework/src/hash.rs
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
//! Generic behaviour tests for anything that implements [`Hash`].

use bouncycastle_core::errors::HashError;
use bouncycastle_core::traits::{Hash, HashAlgParams};

/// Instance of the test framework.
pub struct TestFrameworkHash {
// Put any config options here
/// Can be disabled for hash functions that don't implement [`Hash::do_final_partial_bits`].
pub enable_partial_final_input_tests: bool,
pub enable_partial_byte_tests: bool,
}

impl TestFrameworkHash {
///
pub fn new() -> Self {
Self { enable_partial_final_input_tests: true }
Self { enable_partial_byte_tests: true }
}

/// Test all the members of trait Hash against the given input-output pair.
Expand DownExpand Up@@ -92,22 +93,112 @@ impl TestFrameworkHash {
);
}

if self.enable_partial_final_input_tests {
/*** fn do_final_partial_bits(self, partial_byte: u8, num_partial_bits: usize)-> Result<Vec<u8>, HashError>; ***/
/*** fn do_final_partial_bits_out(self, partial_byte: u8, num_partial_bits: usize, output: &mut [u8]) -> Result<usize, HashError>; ***/
// There's not a lot we can test here because this will require a different expected output from the rest of this test, but we can do something.

//output slice too small -- should just truncate
let mut first_output = vec![0u8; H::default().output_len()];
H::default()
.do_final_partial_bits_out(0xFF, 7, &mut *first_output)
.expect("Failed to finalize partial input");
let len_to_truncate_to = H::default().output_len() - 1;
let mut output = vec![0u8; len_to_truncate_to];
let bytes_written =
H::default().do_final_partial_bits_out(0xFF, 7, &mut *output).unwrap();
assert_eq!(bytes_written, len_to_truncate_to);
assert_eq!(first_output[..len_to_truncate_to], output);
if self.enable_partial_byte_tests {
/*** Testing: ***/
/*** fn do_final_partial_bits(self, partial_byte: u8, num_bits: usize)-> Result<Vec<u8>, HashError>; ***/
/*** fn do_final_partial_bits_out(self, partial_byte: u8, num_bits: usize, output: &mut [u8]) -> Result<usize, HashError>; ***/
// A known-answer test for these needs a different expected output from the rest of this

// Helper: the digest of `input` finished with the low `num_bits` bits of `partial_byte`.
let partial_digest = |partial_byte: u8, num_bits: usize| -> Vec<u8> {
let mut message_digest = H::default();
message_digest.do_update(input);
message_digest
.do_final_partial_bits(partial_byte, num_bits)
.expect("do_final_partial_bits() must succeed for num_bits in 0..=7")
};

// "0 is a valid value and means the message ends on a byte boundary (equivalent to
// Hash::do_final())".
// So, test against the `expected_output` result from above
for partial_byte in [0x00u8, 0x01, 0x80, 0xA5, 0xFF] {
assert_eq!(
partial_digest(partial_byte, 0),
expected_output,
"num_bits = 0 must be equivalent to do_final() / partial_byte: {partial_byte:#04X}"
);
}

// "The num_bits message bits are taken from the least significant bits of
// partial_byte": the unused high bits are not part of the message, and so must not
// change the output.
for num_bits in 0..=7 {
// no overflow: 1u8 << 7 == 0x80
let mask = (1u8 << num_bits) - 1;
for partial_byte in [0x00u8, 0x5A, 0xA5, 0xFF] {
assert_eq!(
partial_digest(partial_byte, num_bits),
partial_digest(partial_byte & mask, num_bits),
"bits above num_bits = {num_bits} must be ignored / partial_byte: {partial_byte:#04X}"
);
}
}

// "num_bits must be in 0..=7; larger values return HashError::InvalidLength."
// The range has to be validated before any shift by num_bits, so check well past
// the width of the shifted type as well as the 8 / 9 boundary.
for num_bits in [8usize, 9, 15, 16, 64, usize::MAX] {
let mut message_digest = H::default();
message_digest.do_update(input);
assert!(
matches!(
message_digest.do_final_partial_bits(0xFF, num_bits),
Err(HashError::InvalidLength(_))
),
"num_bits = {num_bits} must be rejected with InvalidLength"
);

let mut output = vec![0u8; H::OUTPUT_LEN];
let mut message_digest = H::default();
message_digest.do_update(input);
assert!(
matches!(
message_digest.do_final_partial_bits_out(0xFF, num_bits, &mut *output),
Err(HashError::InvalidLength(_))
),
"num_bits = {num_bits} must be rejected with InvalidLength (_out variant)"
);
}

// "The same as Hash::do_final_partial_bits, but takes the output buffer as an
// argument": the two variants must agree, and the Vec variant must return
// output_len() bytes.
for num_bits in 0..=7 {
for partial_byte in [0x00u8, 0x5A, 0xA5, 0xFF] {
let expected_partial_output = partial_digest(partial_byte, num_bits);
assert_eq!(expected_partial_output.len(), H::OUTPUT_LEN);

let mut output = vec![0u8; H::OUTPUT_LEN];
let mut message_digest = H::default();
message_digest.do_update(input);
let bytes_written = message_digest
.do_final_partial_bits_out(partial_byte, num_bits, &mut *output)
.expect("Failed to finalize partial input");
assert_eq!(bytes_written, H::OUTPUT_LEN);
assert_eq!(
output, expected_partial_output,
"do_final_partial_bits_out() must agree with do_final_partial_bits() / num_bits: {num_bits}, partial_byte: {partial_byte:#04X}"
);
}
}

// Each (num_bits, partial_byte) pair is a distinct message, and so must produce a
// distinct digest. This is what catches an implementation that silently drops the
// partial bits, or absorbs the wrong number of them.
let mut partial_outputs: Vec<Vec<u8>> = Vec::new();
for num_bits in 0..=7 {
for partial_byte in 0..(1u16 << num_bits) {
partial_outputs.push(partial_digest(partial_byte as u8, num_bits));
}
}
let num_partial_outputs = partial_outputs.len();
partial_outputs.sort_unstable();
partial_outputs.dedup();
assert_eq!(
partial_outputs.len(),
num_partial_outputs,
"each (num_bits, partial_byte) pair is a distinct message and must hash to a distinct output"
);
}

// check that if you feed it an output slice that's bigger than it needs, that it doesn't touch the extra bytes.
Expand Down
1 change: 1 addition & 0 deletions crypto/core-test-framework/src/lib.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@ pub mod mac;
pub mod signature;
pub mod suspendable_state;
pub mod symmetric_ciphers;
pub mod xof;

mod fixed_seed_rng;
pub use fixed_seed_rng::FixedSeedRNG;
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
133ea1b
sha3: fix partial-byte squeeze, validate partial-bit inputs, docs and…
dghgit Aug 26, 2026
5afe03d
sha3: share finalize() between do_final_out and do_final_partial_bits…
dghgit Aug 26, 2026
989e339
sha3: rustfmt
dghgit Aug 26, 2026
b8beef8
sha3: address PR #87 review
dghgit Aug 27, 2026
77b5d86
sha3: read NIST FIPS 202 example vectors from bc-test-data
dghgit Aug 27, 2026
b57febb
sha3: add NIST CAVP SHA3VS harness; fix double SHAKE suffix on 4-bit …
dghgit Aug 27, 2026
6153bbf
core: document the partial-byte bit-ordering convention on Hash
dghgit Aug 27, 2026
8496d7e
Add 0.1.3 release notes for the SHA-3 changes (PR #87)
dghgit Aug 27, 2026
c940a5d
Add bench_sha3_mem_usage: struct sizes and massif entry points for SH…
dghgit Aug 27, 2026
19371ae
sha3: fix SHAKE partial-byte squeeze, 4-bit tail double suffix, and p…
ounsworth Aug 27, 2026
ec56486
shortened the release notes
ounsworth Sep 1, 2026
f5b8c3d
some work on the sha3 partial byte bug pr
ounsworth Sep 2, 2026
0666366
tightened up absorb-after-squeeze behaviour for shake
ounsworth Sep 2, 2026
2a517e6
tightened up absorb-after-squeeze behaviour for shake
ounsworth Sep 2, 2026
bad45f9
SHAKE::squeeze_partial_byte now works on num_bits=0
ounsworth Sep 2, 2026
90690de
SHAKE::squeeze_partial_byte now works on num_bits=0
ounsworth Sep 2, 2026
da51bf0
sha3: fix SHAKE partial-byte squeeze, 4-bit tail double suffix, and p…
ounsworth Aug 27, 2026
9c4b16a
shortened the release notes
ounsworth Sep 1, 2026
7903d42
some work on the sha3 partial byte bug pr
ounsworth Sep 2, 2026
225e9d0
tightened up absorb-after-squeeze behaviour for shake
ounsworth Sep 2, 2026
a265fd6
tightened up absorb-after-squeeze behaviour for shake
ounsworth Sep 2, 2026
f7a82ad
SHAKE::squeeze_partial_byte now works on num_bits=0
ounsworth Sep 2, 2026
dbcb020
SHAKE::squeeze_partial_byte now works on num_bits=0
ounsworth Sep 2, 2026
04d1e45
Merge branch 'fix/sha3-shake-partial-bits' of github.com:ounsworth/bc…
ounsworth Sep 2, 2026
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
4 changes: 4 additions & 0 deletions alpha_0.1.3_release_notes.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,3 +3,7 @@
## Major features

## Minor features / bug fixes

* bug fixes to the way SHA3/SHAKE handled absorbing and squeezing a partial final byte.
* Design discussions about whether core::traits::XOF (in the abstract) should allow interleaving absorb -> squeeze ->
absorb (ie "absorb-after-squeeze). Outcome: absorb-after-squeeze forbidden. Could be changed in the future.
2 changes: 1 addition & 1 deletion cli/src/main.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -119,7 +119,7 @@ enum Subcommands {
/// Length of the output in bytes.
length: usize,

#[arg(long)]
#[arg(short)]
/// Output the hashes in hex format.
x: bool,
},
Expand Down
127 changes: 109 additions & 18 deletions crypto/core-test-framework/src/hash.rs
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
//! Generic behaviour tests for anything that implements [`Hash`].

use bouncycastle_core::errors::HashError;
use bouncycastle_core::traits::{Hash, HashAlgParams};

/// Instance of the test framework.
pub struct TestFrameworkHash {
// Put any config options here
/// Can be disabled for hash functions that don't implement [`Hash::do_final_partial_bits`].
pub enable_partial_final_input_tests: bool,
pub enable_partial_byte_tests: bool,
}

impl TestFrameworkHash {
///
pub fn new() -> Self {
Self { enable_partial_final_input_tests: true }
Self { enable_partial_byte_tests: true }
}

/// Test all the members of trait Hash against the given input-output pair.
Expand DownExpand Up@@ -92,22 +93,112 @@ impl TestFrameworkHash {
);
}

if self.enable_partial_final_input_tests {
/*** fn do_final_partial_bits(self, partial_byte: u8, num_partial_bits: usize)-> Result<Vec<u8>, HashError>; ***/
/*** fn do_final_partial_bits_out(self, partial_byte: u8, num_partial_bits: usize, output: &mut [u8]) -> Result<usize, HashError>; ***/
// There's not a lot we can test here because this will require a different expected output from the rest of this test, but we can do something.

//output slice too small -- should just truncate
let mut first_output = vec![0u8; H::default().output_len()];
H::default()
.do_final_partial_bits_out(0xFF, 7, &mut *first_output)
.expect("Failed to finalize partial input");
let len_to_truncate_to = H::default().output_len() - 1;
let mut output = vec![0u8; len_to_truncate_to];
let bytes_written =
H::default().do_final_partial_bits_out(0xFF, 7, &mut *output).unwrap();
assert_eq!(bytes_written, len_to_truncate_to);
assert_eq!(first_output[..len_to_truncate_to], output);
if self.enable_partial_byte_tests {
/*** Testing: ***/
/*** fn do_final_partial_bits(self, partial_byte: u8, num_bits: usize)-> Result<Vec<u8>, HashError>; ***/
/*** fn do_final_partial_bits_out(self, partial_byte: u8, num_bits: usize, output: &mut [u8]) -> Result<usize, HashError>; ***/
// A known-answer test for these needs a different expected output from the rest of this

// Helper: the digest of `input` finished with the low `num_bits` bits of `partial_byte`.
let partial_digest = |partial_byte: u8, num_bits: usize| -> Vec<u8> {
let mut message_digest = H::default();
message_digest.do_update(input);
message_digest
.do_final_partial_bits(partial_byte, num_bits)
.expect("do_final_partial_bits() must succeed for num_bits in 0..=7")
};

// "0 is a valid value and means the message ends on a byte boundary (equivalent to
// Hash::do_final())".
// So, test against the `expected_output` result from above
for partial_byte in [0x00u8, 0x01, 0x80, 0xA5, 0xFF] {
assert_eq!(
partial_digest(partial_byte, 0),
expected_output,
"num_bits = 0 must be equivalent to do_final() / partial_byte: {partial_byte:#04X}"
);
}

// "The num_bits message bits are taken from the least significant bits of
// partial_byte": the unused high bits are not part of the message, and so must not
// change the output.
for num_bits in 0..=7 {
// no overflow: 1u8 << 7 == 0x80
let mask = (1u8 << num_bits) - 1;
for partial_byte in [0x00u8, 0x5A, 0xA5, 0xFF] {
assert_eq!(
partial_digest(partial_byte, num_bits),
partial_digest(partial_byte & mask, num_bits),
"bits above num_bits = {num_bits} must be ignored / partial_byte: {partial_byte:#04X}"
);
}
}

// "num_bits must be in 0..=7; larger values return HashError::InvalidLength."
// The range has to be validated before any shift by num_bits, so check well past
// the width of the shifted type as well as the 8 / 9 boundary.
for num_bits in [8usize, 9, 15, 16, 64, usize::MAX] {
let mut message_digest = H::default();
message_digest.do_update(input);
assert!(
matches!(
message_digest.do_final_partial_bits(0xFF, num_bits),
Err(HashError::InvalidLength(_))
),
"num_bits = {num_bits} must be rejected with InvalidLength"
);

let mut output = vec![0u8; H::OUTPUT_LEN];
let mut message_digest = H::default();
message_digest.do_update(input);
assert!(
matches!(
message_digest.do_final_partial_bits_out(0xFF, num_bits, &mut *output),
Err(HashError::InvalidLength(_))
),
"num_bits = {num_bits} must be rejected with InvalidLength (_out variant)"
);
}

// "The same as Hash::do_final_partial_bits, but takes the output buffer as an
// argument": the two variants must agree, and the Vec variant must return
// output_len() bytes.
for num_bits in 0..=7 {
for partial_byte in [0x00u8, 0x5A, 0xA5, 0xFF] {
let expected_partial_output = partial_digest(partial_byte, num_bits);
assert_eq!(expected_partial_output.len(), H::OUTPUT_LEN);

let mut output = vec![0u8; H::OUTPUT_LEN];
let mut message_digest = H::default();
message_digest.do_update(input);
let bytes_written = message_digest
.do_final_partial_bits_out(partial_byte, num_bits, &mut *output)
.expect("Failed to finalize partial input");
assert_eq!(bytes_written, H::OUTPUT_LEN);
assert_eq!(
output, expected_partial_output,
"do_final_partial_bits_out() must agree with do_final_partial_bits() / num_bits: {num_bits}, partial_byte: {partial_byte:#04X}"
);
}
}

// Each (num_bits, partial_byte) pair is a distinct message, and so must produce a
// distinct digest. This is what catches an implementation that silently drops the
// partial bits, or absorbs the wrong number of them.
let mut partial_outputs: Vec<Vec<u8>> = Vec::new();
for num_bits in 0..=7 {
for partial_byte in 0..(1u16 << num_bits) {
partial_outputs.push(partial_digest(partial_byte as u8, num_bits));
}
}
let num_partial_outputs = partial_outputs.len();
partial_outputs.sort_unstable();
partial_outputs.dedup();
assert_eq!(
partial_outputs.len(),
num_partial_outputs,
"each (num_bits, partial_byte) pair is a distinct message and must hash to a distinct output"
);
}

// check that if you feed it an output slice that's bigger than it needs, that it doesn't touch the extra bytes.
Expand Down
1 change: 1 addition & 0 deletions crypto/core-test-framework/src/lib.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@ pub mod mac;
pub mod signature;
pub mod suspendable_state;
pub mod symmetric_ciphers;
pub mod xof;

mod fixed_seed_rng;
pub use fixed_seed_rng::FixedSeedRNG;
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
133ea1b
sha3: fix partial-byte squeeze, validate partial-bit inputs, docs and…
dghgit Aug 26, 2026
5afe03d
sha3: share finalize() between do_final_out and do_final_partial_bits…
dghgit Aug 26, 2026
989e339
sha3: rustfmt
dghgit Aug 26, 2026
b8beef8
sha3: address PR #87 review
dghgit Aug 27, 2026
77b5d86
sha3: read NIST FIPS 202 example vectors from bc-test-data
dghgit Aug 27, 2026
b57febb
sha3: add NIST CAVP SHA3VS harness; fix double SHAKE suffix on 4-bit …
dghgit Aug 27, 2026
6153bbf
core: document the partial-byte bit-ordering convention on Hash
dghgit Aug 27, 2026
8496d7e
Add 0.1.3 release notes for the SHA-3 changes (PR #87)
dghgit Aug 27, 2026
c940a5d
Add bench_sha3_mem_usage: struct sizes and massif entry points for SH…
dghgit Aug 27, 2026
19371ae
sha3: fix SHAKE partial-byte squeeze, 4-bit tail double suffix, and p…
ounsworth Aug 27, 2026
ec56486
shortened the release notes
ounsworth Sep 1, 2026
f5b8c3d
some work on the sha3 partial byte bug pr
ounsworth Sep 2, 2026
0666366
tightened up absorb-after-squeeze behaviour for shake
ounsworth Sep 2, 2026
2a517e6
tightened up absorb-after-squeeze behaviour for shake
ounsworth Sep 2, 2026
bad45f9
SHAKE::squeeze_partial_byte now works on num_bits=0
ounsworth Sep 2, 2026
90690de
SHAKE::squeeze_partial_byte now works on num_bits=0
ounsworth Sep 2, 2026
da51bf0
sha3: fix SHAKE partial-byte squeeze, 4-bit tail double suffix, and p…
ounsworth Aug 27, 2026
9c4b16a
shortened the release notes
ounsworth Sep 1, 2026
7903d42
some work on the sha3 partial byte bug pr
ounsworth Sep 2, 2026
225e9d0
tightened up absorb-after-squeeze behaviour for shake
ounsworth Sep 2, 2026
a265fd6
tightened up absorb-after-squeeze behaviour for shake
ounsworth Sep 2, 2026
f7a82ad
SHAKE::squeeze_partial_byte now works on num_bits=0
ounsworth Sep 2, 2026
dbcb020
SHAKE::squeeze_partial_byte now works on num_bits=0
ounsworth Sep 2, 2026
04d1e45
Merge branch 'fix/sha3-shake-partial-bits' of github.com:ounsworth/bc…
ounsworth Sep 2, 2026
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
4 changes: 4 additions & 0 deletions alpha_0.1.3_release_notes.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,3 +3,7 @@
## Major features

## Minor features / bug fixes

* bug fixes to the way SHA3/SHAKE handled absorbing and squeezing a partial final byte.
* Design discussions about whether core::traits::XOF (in the abstract) should allow interleaving absorb -> squeeze ->
absorb (ie "absorb-after-squeeze). Outcome: absorb-after-squeeze forbidden. Could be changed in the future.
2 changes: 1 addition & 1 deletion cli/src/main.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -119,7 +119,7 @@ enum Subcommands {
/// Length of the output in bytes.
length: usize,

#[arg(long)]
#[arg(short)]
/// Output the hashes in hex format.
x: bool,
},
Expand Down
127 changes: 109 additions & 18 deletions crypto/core-test-framework/src/hash.rs
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
//! Generic behaviour tests for anything that implements [`Hash`].

use bouncycastle_core::errors::HashError;
use bouncycastle_core::traits::{Hash, HashAlgParams};

/// Instance of the test framework.
pub struct TestFrameworkHash {
// Put any config options here
/// Can be disabled for hash functions that don't implement [`Hash::do_final_partial_bits`].
pub enable_partial_final_input_tests: bool,
pub enable_partial_byte_tests: bool,
}

impl TestFrameworkHash {
///
pub fn new() -> Self {
Self { enable_partial_final_input_tests: true }
Self { enable_partial_byte_tests: true }
}

/// Test all the members of trait Hash against the given input-output pair.
Expand DownExpand Up@@ -92,22 +93,112 @@ impl TestFrameworkHash {
);
}

if self.enable_partial_final_input_tests {
/*** fn do_final_partial_bits(self, partial_byte: u8, num_partial_bits: usize)-> Result<Vec<u8>, HashError>; ***/
/*** fn do_final_partial_bits_out(self, partial_byte: u8, num_partial_bits: usize, output: &mut [u8]) -> Result<usize, HashError>; ***/
// There's not a lot we can test here because this will require a different expected output from the rest of this test, but we can do something.

//output slice too small -- should just truncate
let mut first_output = vec![0u8; H::default().output_len()];
H::default()
.do_final_partial_bits_out(0xFF, 7, &mut *first_output)
.expect("Failed to finalize partial input");
let len_to_truncate_to = H::default().output_len() - 1;
let mut output = vec![0u8; len_to_truncate_to];
let bytes_written =
H::default().do_final_partial_bits_out(0xFF, 7, &mut *output).unwrap();
assert_eq!(bytes_written, len_to_truncate_to);
assert_eq!(first_output[..len_to_truncate_to], output);
if self.enable_partial_byte_tests {
/*** Testing: ***/
/*** fn do_final_partial_bits(self, partial_byte: u8, num_bits: usize)-> Result<Vec<u8>, HashError>; ***/
/*** fn do_final_partial_bits_out(self, partial_byte: u8, num_bits: usize, output: &mut [u8]) -> Result<usize, HashError>; ***/
// A known-answer test for these needs a different expected output from the rest of this

// Helper: the digest of `input` finished with the low `num_bits` bits of `partial_byte`.
let partial_digest = |partial_byte: u8, num_bits: usize| -> Vec<u8> {
let mut message_digest = H::default();
message_digest.do_update(input);
message_digest
.do_final_partial_bits(partial_byte, num_bits)
.expect("do_final_partial_bits() must succeed for num_bits in 0..=7")
};

// "0 is a valid value and means the message ends on a byte boundary (equivalent to
// Hash::do_final())".
// So, test against the `expected_output` result from above
for partial_byte in [0x00u8, 0x01, 0x80, 0xA5, 0xFF] {
assert_eq!(
partial_digest(partial_byte, 0),
expected_output,
"num_bits = 0 must be equivalent to do_final() / partial_byte: {partial_byte:#04X}"
);
}

// "The num_bits message bits are taken from the least significant bits of
// partial_byte": the unused high bits are not part of the message, and so must not
// change the output.
for num_bits in 0..=7 {
// no overflow: 1u8 << 7 == 0x80
let mask = (1u8 << num_bits) - 1;
for partial_byte in [0x00u8, 0x5A, 0xA5, 0xFF] {
assert_eq!(
partial_digest(partial_byte, num_bits),
partial_digest(partial_byte & mask, num_bits),
"bits above num_bits = {num_bits} must be ignored / partial_byte: {partial_byte:#04X}"
);
}
}

// "num_bits must be in 0..=7; larger values return HashError::InvalidLength."
// The range has to be validated before any shift by num_bits, so check well past
// the width of the shifted type as well as the 8 / 9 boundary.
for num_bits in [8usize, 9, 15, 16, 64, usize::MAX] {
let mut message_digest = H::default();
message_digest.do_update(input);
assert!(
matches!(
message_digest.do_final_partial_bits(0xFF, num_bits),
Err(HashError::InvalidLength(_))
),
"num_bits = {num_bits} must be rejected with InvalidLength"
);

let mut output = vec![0u8; H::OUTPUT_LEN];
let mut message_digest = H::default();
message_digest.do_update(input);
assert!(
matches!(
message_digest.do_final_partial_bits_out(0xFF, num_bits, &mut *output),
Err(HashError::InvalidLength(_))
),
"num_bits = {num_bits} must be rejected with InvalidLength (_out variant)"
);
}

// "The same as Hash::do_final_partial_bits, but takes the output buffer as an
// argument": the two variants must agree, and the Vec variant must return
// output_len() bytes.
for num_bits in 0..=7 {
for partial_byte in [0x00u8, 0x5A, 0xA5, 0xFF] {
let expected_partial_output = partial_digest(partial_byte, num_bits);
assert_eq!(expected_partial_output.len(), H::OUTPUT_LEN);

let mut output = vec![0u8; H::OUTPUT_LEN];
let mut message_digest = H::default();
message_digest.do_update(input);
let bytes_written = message_digest
.do_final_partial_bits_out(partial_byte, num_bits, &mut *output)
.expect("Failed to finalize partial input");
assert_eq!(bytes_written, H::OUTPUT_LEN);
assert_eq!(
output, expected_partial_output,
"do_final_partial_bits_out() must agree with do_final_partial_bits() / num_bits: {num_bits}, partial_byte: {partial_byte:#04X}"
);
}
}

// Each (num_bits, partial_byte) pair is a distinct message, and so must produce a
// distinct digest. This is what catches an implementation that silently drops the
// partial bits, or absorbs the wrong number of them.
let mut partial_outputs: Vec<Vec<u8>> = Vec::new();
for num_bits in 0..=7 {
for partial_byte in 0..(1u16 << num_bits) {
partial_outputs.push(partial_digest(partial_byte as u8, num_bits));
}
}
let num_partial_outputs = partial_outputs.len();
partial_outputs.sort_unstable();
partial_outputs.dedup();
assert_eq!(
partial_outputs.len(),
num_partial_outputs,
"each (num_bits, partial_byte) pair is a distinct message and must hash to a distinct output"
);
}

// check that if you feed it an output slice that's bigger than it needs, that it doesn't touch the extra bytes.
Expand Down
1 change: 1 addition & 0 deletions crypto/core-test-framework/src/lib.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@ pub mod mac;
pub mod signature;
pub mod suspendable_state;
pub mod symmetric_ciphers;
pub mod xof;

mod fixed_seed_rng;
pub use fixed_seed_rng::FixedSeedRNG;
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
133ea1b
sha3: fix partial-byte squeeze, validate partial-bit inputs, docs and…
dghgit Aug 26, 2026
5afe03d
sha3: share finalize() between do_final_out and do_final_partial_bits…
dghgit Aug 26, 2026
989e339
sha3: rustfmt
dghgit Aug 26, 2026
b8beef8
sha3: address PR #87 review
dghgit Aug 27, 2026
77b5d86
sha3: read NIST FIPS 202 example vectors from bc-test-data
dghgit Aug 27, 2026
b57febb
sha3: add NIST CAVP SHA3VS harness; fix double SHAKE suffix on 4-bit …
dghgit Aug 27, 2026
6153bbf
core: document the partial-byte bit-ordering convention on Hash
dghgit Aug 27, 2026
8496d7e
Add 0.1.3 release notes for the SHA-3 changes (PR #87)
dghgit Aug 27, 2026
c940a5d
Add bench_sha3_mem_usage: struct sizes and massif entry points for SH…
dghgit Aug 27, 2026
19371ae
sha3: fix SHAKE partial-byte squeeze, 4-bit tail double suffix, and p…
ounsworth Aug 27, 2026
ec56486
shortened the release notes
ounsworth Sep 1, 2026
f5b8c3d
some work on the sha3 partial byte bug pr
ounsworth Sep 2, 2026
0666366
tightened up absorb-after-squeeze behaviour for shake
ounsworth Sep 2, 2026
2a517e6
tightened up absorb-after-squeeze behaviour for shake
ounsworth Sep 2, 2026
bad45f9
SHAKE::squeeze_partial_byte now works on num_bits=0
ounsworth Sep 2, 2026
90690de
SHAKE::squeeze_partial_byte now works on num_bits=0
ounsworth Sep 2, 2026
da51bf0
sha3: fix SHAKE partial-byte squeeze, 4-bit tail double suffix, and p…
ounsworth Aug 27, 2026
9c4b16a
shortened the release notes
ounsworth Sep 1, 2026
7903d42
some work on the sha3 partial byte bug pr
ounsworth Sep 2, 2026
225e9d0
tightened up absorb-after-squeeze behaviour for shake
ounsworth Sep 2, 2026
a265fd6
tightened up absorb-after-squeeze behaviour for shake
ounsworth Sep 2, 2026
f7a82ad
SHAKE::squeeze_partial_byte now works on num_bits=0
ounsworth Sep 2, 2026
dbcb020
SHAKE::squeeze_partial_byte now works on num_bits=0
ounsworth Sep 2, 2026
04d1e45
Merge branch 'fix/sha3-shake-partial-bits' of github.com:ounsworth/bc…
ounsworth Sep 2, 2026
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
4 changes: 4 additions & 0 deletions alpha_0.1.3_release_notes.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,3 +3,7 @@
## Major features

## Minor features / bug fixes

* bug fixes to the way SHA3/SHAKE handled absorbing and squeezing a partial final byte.
* Design discussions about whether core::traits::XOF (in the abstract) should allow interleaving absorb -> squeeze ->
absorb (ie "absorb-after-squeeze). Outcome: absorb-after-squeeze forbidden. Could be changed in the future.
2 changes: 1 addition & 1 deletion cli/src/main.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -119,7 +119,7 @@ enum Subcommands {
/// Length of the output in bytes.
length: usize,

#[arg(long)]
#[arg(short)]
/// Output the hashes in hex format.
x: bool,
},
Expand Down
127 changes: 109 additions & 18 deletions crypto/core-test-framework/src/hash.rs
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
//! Generic behaviour tests for anything that implements [`Hash`].

use bouncycastle_core::errors::HashError;
use bouncycastle_core::traits::{Hash, HashAlgParams};

/// Instance of the test framework.
pub struct TestFrameworkHash {
// Put any config options here
/// Can be disabled for hash functions that don't implement [`Hash::do_final_partial_bits`].
pub enable_partial_final_input_tests: bool,
pub enable_partial_byte_tests: bool,
}

impl TestFrameworkHash {
///
pub fn new() -> Self {
Self { enable_partial_final_input_tests: true }
Self { enable_partial_byte_tests: true }
}

/// Test all the members of trait Hash against the given input-output pair.
Expand DownExpand Up@@ -92,22 +93,112 @@ impl TestFrameworkHash {
);
}

if self.enable_partial_final_input_tests {
/*** fn do_final_partial_bits(self, partial_byte: u8, num_partial_bits: usize)-> Result<Vec<u8>, HashError>; ***/
/*** fn do_final_partial_bits_out(self, partial_byte: u8, num_partial_bits: usize, output: &mut [u8]) -> Result<usize, HashError>; ***/
// There's not a lot we can test here because this will require a different expected output from the rest of this test, but we can do something.

//output slice too small -- should just truncate
let mut first_output = vec![0u8; H::default().output_len()];
H::default()
.do_final_partial_bits_out(0xFF, 7, &mut *first_output)
.expect("Failed to finalize partial input");
let len_to_truncate_to = H::default().output_len() - 1;
let mut output = vec![0u8; len_to_truncate_to];
let bytes_written =
H::default().do_final_partial_bits_out(0xFF, 7, &mut *output).unwrap();
assert_eq!(bytes_written, len_to_truncate_to);
assert_eq!(first_output[..len_to_truncate_to], output);
if self.enable_partial_byte_tests {
/*** Testing: ***/
/*** fn do_final_partial_bits(self, partial_byte: u8, num_bits: usize)-> Result<Vec<u8>, HashError>; ***/
/*** fn do_final_partial_bits_out(self, partial_byte: u8, num_bits: usize, output: &mut [u8]) -> Result<usize, HashError>; ***/
// A known-answer test for these needs a different expected output from the rest of this

// Helper: the digest of `input` finished with the low `num_bits` bits of `partial_byte`.
let partial_digest = |partial_byte: u8, num_bits: usize| -> Vec<u8> {
let mut message_digest = H::default();
message_digest.do_update(input);
message_digest
.do_final_partial_bits(partial_byte, num_bits)
.expect("do_final_partial_bits() must succeed for num_bits in 0..=7")
};

// "0 is a valid value and means the message ends on a byte boundary (equivalent to
// Hash::do_final())".
// So, test against the `expected_output` result from above
for partial_byte in [0x00u8, 0x01, 0x80, 0xA5, 0xFF] {
assert_eq!(
partial_digest(partial_byte, 0),
expected_output,
"num_bits = 0 must be equivalent to do_final() / partial_byte: {partial_byte:#04X}"
);
}

// "The num_bits message bits are taken from the least significant bits of
// partial_byte": the unused high bits are not part of the message, and so must not
// change the output.
for num_bits in 0..=7 {
// no overflow: 1u8 << 7 == 0x80
let mask = (1u8 << num_bits) - 1;
for partial_byte in [0x00u8, 0x5A, 0xA5, 0xFF] {
assert_eq!(
partial_digest(partial_byte, num_bits),
partial_digest(partial_byte & mask, num_bits),
"bits above num_bits = {num_bits} must be ignored / partial_byte: {partial_byte:#04X}"
);
}
}

// "num_bits must be in 0..=7; larger values return HashError::InvalidLength."
// The range has to be validated before any shift by num_bits, so check well past
// the width of the shifted type as well as the 8 / 9 boundary.
for num_bits in [8usize, 9, 15, 16, 64, usize::MAX] {
let mut message_digest = H::default();
message_digest.do_update(input);
assert!(
matches!(
message_digest.do_final_partial_bits(0xFF, num_bits),
Err(HashError::InvalidLength(_))
),
"num_bits = {num_bits} must be rejected with InvalidLength"
);

let mut output = vec![0u8; H::OUTPUT_LEN];
let mut message_digest = H::default();
message_digest.do_update(input);
assert!(
matches!(
message_digest.do_final_partial_bits_out(0xFF, num_bits, &mut *output),
Err(HashError::InvalidLength(_))
),
"num_bits = {num_bits} must be rejected with InvalidLength (_out variant)"
);
}

// "The same as Hash::do_final_partial_bits, but takes the output buffer as an
// argument": the two variants must agree, and the Vec variant must return
// output_len() bytes.
for num_bits in 0..=7 {
for partial_byte in [0x00u8, 0x5A, 0xA5, 0xFF] {
let expected_partial_output = partial_digest(partial_byte, num_bits);
assert_eq!(expected_partial_output.len(), H::OUTPUT_LEN);

let mut output = vec![0u8; H::OUTPUT_LEN];
let mut message_digest = H::default();
message_digest.do_update(input);
let bytes_written = message_digest
.do_final_partial_bits_out(partial_byte, num_bits, &mut *output)
.expect("Failed to finalize partial input");
assert_eq!(bytes_written, H::OUTPUT_LEN);
assert_eq!(
output, expected_partial_output,
"do_final_partial_bits_out() must agree with do_final_partial_bits() / num_bits: {num_bits}, partial_byte: {partial_byte:#04X}"
);
}
}

// Each (num_bits, partial_byte) pair is a distinct message, and so must produce a
// distinct digest. This is what catches an implementation that silently drops the
// partial bits, or absorbs the wrong number of them.
let mut partial_outputs: Vec<Vec<u8>> = Vec::new();
for num_bits in 0..=7 {
for partial_byte in 0..(1u16 << num_bits) {
partial_outputs.push(partial_digest(partial_byte as u8, num_bits));
}
}
let num_partial_outputs = partial_outputs.len();
partial_outputs.sort_unstable();
partial_outputs.dedup();
assert_eq!(
partial_outputs.len(),
num_partial_outputs,
"each (num_bits, partial_byte) pair is a distinct message and must hash to a distinct output"
);
}

// check that if you feed it an output slice that's bigger than it needs, that it doesn't touch the extra bytes.
Expand Down
1 change: 1 addition & 0 deletions crypto/core-test-framework/src/lib.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@ pub mod mac;
pub mod signature;
pub mod suspendable_state;
pub mod symmetric_ciphers;
pub mod xof;

mod fixed_seed_rng;
pub use fixed_seed_rng::FixedSeedRNG;
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
133ea1b
sha3: fix partial-byte squeeze, validate partial-bit inputs, docs and…
dghgit Aug 26, 2026
5afe03d
sha3: share finalize() between do_final_out and do_final_partial_bits…
dghgit Aug 26, 2026
989e339
sha3: rustfmt
dghgit Aug 26, 2026
b8beef8
sha3: address PR #87 review
dghgit Aug 27, 2026
77b5d86
sha3: read NIST FIPS 202 example vectors from bc-test-data
dghgit Aug 27, 2026
b57febb
sha3: add NIST CAVP SHA3VS harness; fix double SHAKE suffix on 4-bit …
dghgit Aug 27, 2026
6153bbf
core: document the partial-byte bit-ordering convention on Hash
dghgit Aug 27, 2026
8496d7e
Add 0.1.3 release notes for the SHA-3 changes (PR #87)
dghgit Aug 27, 2026
c940a5d
Add bench_sha3_mem_usage: struct sizes and massif entry points for SH…
dghgit Aug 27, 2026
19371ae
sha3: fix SHAKE partial-byte squeeze, 4-bit tail double suffix, and p…
ounsworth Aug 27, 2026
ec56486
shortened the release notes
ounsworth Sep 1, 2026
f5b8c3d
some work on the sha3 partial byte bug pr
ounsworth Sep 2, 2026
0666366
tightened up absorb-after-squeeze behaviour for shake
ounsworth Sep 2, 2026
2a517e6
tightened up absorb-after-squeeze behaviour for shake
ounsworth Sep 2, 2026
bad45f9
SHAKE::squeeze_partial_byte now works on num_bits=0
ounsworth Sep 2, 2026
90690de
SHAKE::squeeze_partial_byte now works on num_bits=0
ounsworth Sep 2, 2026
da51bf0
sha3: fix SHAKE partial-byte squeeze, 4-bit tail double suffix, and p…
ounsworth Aug 27, 2026
9c4b16a
shortened the release notes
ounsworth Sep 1, 2026
7903d42
some work on the sha3 partial byte bug pr
ounsworth Sep 2, 2026
225e9d0
tightened up absorb-after-squeeze behaviour for shake
ounsworth Sep 2, 2026
a265fd6
tightened up absorb-after-squeeze behaviour for shake
ounsworth Sep 2, 2026
f7a82ad
SHAKE::squeeze_partial_byte now works on num_bits=0
ounsworth Sep 2, 2026
dbcb020
SHAKE::squeeze_partial_byte now works on num_bits=0
ounsworth Sep 2, 2026
04d1e45
Merge branch 'fix/sha3-shake-partial-bits' of github.com:ounsworth/bc…
ounsworth Sep 2, 2026
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
4 changes: 4 additions & 0 deletions alpha_0.1.3_release_notes.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,3 +3,7 @@
## Major features

## Minor features / bug fixes

* bug fixes to the way SHA3/SHAKE handled absorbing and squeezing a partial final byte.
* Design discussions about whether core::traits::XOF (in the abstract) should allow interleaving absorb -> squeeze ->
absorb (ie "absorb-after-squeeze). Outcome: absorb-after-squeeze forbidden. Could be changed in the future.
2 changes: 1 addition & 1 deletion cli/src/main.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -119,7 +119,7 @@ enum Subcommands {
/// Length of the output in bytes.
length: usize,

#[arg(long)]
#[arg(short)]
/// Output the hashes in hex format.
x: bool,
},
Expand Down
127 changes: 109 additions & 18 deletions crypto/core-test-framework/src/hash.rs
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
//! Generic behaviour tests for anything that implements [`Hash`].

use bouncycastle_core::errors::HashError;
use bouncycastle_core::traits::{Hash, HashAlgParams};

/// Instance of the test framework.
pub struct TestFrameworkHash {
// Put any config options here
/// Can be disabled for hash functions that don't implement [`Hash::do_final_partial_bits`].
pub enable_partial_final_input_tests: bool,
pub enable_partial_byte_tests: bool,
}

impl TestFrameworkHash {
///
pub fn new() -> Self {
Self { enable_partial_final_input_tests: true }
Self { enable_partial_byte_tests: true }
}

/// Test all the members of trait Hash against the given input-output pair.
Expand DownExpand Up@@ -92,22 +93,112 @@ impl TestFrameworkHash {
);
}

if self.enable_partial_final_input_tests {
/*** fn do_final_partial_bits(self, partial_byte: u8, num_partial_bits: usize)-> Result<Vec<u8>, HashError>; ***/
/*** fn do_final_partial_bits_out(self, partial_byte: u8, num_partial_bits: usize, output: &mut [u8]) -> Result<usize, HashError>; ***/
// There's not a lot we can test here because this will require a different expected output from the rest of this test, but we can do something.

//output slice too small -- should just truncate
let mut first_output = vec![0u8; H::default().output_len()];
H::default()
.do_final_partial_bits_out(0xFF, 7, &mut *first_output)
.expect("Failed to finalize partial input");
let len_to_truncate_to = H::default().output_len() - 1;
let mut output = vec![0u8; len_to_truncate_to];
let bytes_written =
H::default().do_final_partial_bits_out(0xFF, 7, &mut *output).unwrap();
assert_eq!(bytes_written, len_to_truncate_to);
assert_eq!(first_output[..len_to_truncate_to], output);
if self.enable_partial_byte_tests {
/*** Testing: ***/
/*** fn do_final_partial_bits(self, partial_byte: u8, num_bits: usize)-> Result<Vec<u8>, HashError>; ***/
/*** fn do_final_partial_bits_out(self, partial_byte: u8, num_bits: usize, output: &mut [u8]) -> Result<usize, HashError>; ***/
// A known-answer test for these needs a different expected output from the rest of this

// Helper: the digest of `input` finished with the low `num_bits` bits of `partial_byte`.
let partial_digest = |partial_byte: u8, num_bits: usize| -> Vec<u8> {
let mut message_digest = H::default();
message_digest.do_update(input);
message_digest
.do_final_partial_bits(partial_byte, num_bits)
.expect("do_final_partial_bits() must succeed for num_bits in 0..=7")
};

// "0 is a valid value and means the message ends on a byte boundary (equivalent to
// Hash::do_final())".
// So, test against the `expected_output` result from above
for partial_byte in [0x00u8, 0x01, 0x80, 0xA5, 0xFF] {
assert_eq!(
partial_digest(partial_byte, 0),
expected_output,
"num_bits = 0 must be equivalent to do_final() / partial_byte: {partial_byte:#04X}"
);
}

// "The num_bits message bits are taken from the least significant bits of
// partial_byte": the unused high bits are not part of the message, and so must not
// change the output.
for num_bits in 0..=7 {
// no overflow: 1u8 << 7 == 0x80
let mask = (1u8 << num_bits) - 1;
for partial_byte in [0x00u8, 0x5A, 0xA5, 0xFF] {
assert_eq!(
partial_digest(partial_byte, num_bits),
partial_digest(partial_byte & mask, num_bits),
"bits above num_bits = {num_bits} must be ignored / partial_byte: {partial_byte:#04X}"
);
}
}

// "num_bits must be in 0..=7; larger values return HashError::InvalidLength."
// The range has to be validated before any shift by num_bits, so check well past
// the width of the shifted type as well as the 8 / 9 boundary.
for num_bits in [8usize, 9, 15, 16, 64, usize::MAX] {
let mut message_digest = H::default();
message_digest.do_update(input);
assert!(
matches!(
message_digest.do_final_partial_bits(0xFF, num_bits),
Err(HashError::InvalidLength(_))
),
"num_bits = {num_bits} must be rejected with InvalidLength"
);

let mut output = vec![0u8; H::OUTPUT_LEN];
let mut message_digest = H::default();
message_digest.do_update(input);
assert!(
matches!(
message_digest.do_final_partial_bits_out(0xFF, num_bits, &mut *output),
Err(HashError::InvalidLength(_))
),
"num_bits = {num_bits} must be rejected with InvalidLength (_out variant)"
);
}

// "The same as Hash::do_final_partial_bits, but takes the output buffer as an
// argument": the two variants must agree, and the Vec variant must return
// output_len() bytes.
for num_bits in 0..=7 {
for partial_byte in [0x00u8, 0x5A, 0xA5, 0xFF] {
let expected_partial_output = partial_digest(partial_byte, num_bits);
assert_eq!(expected_partial_output.len(), H::OUTPUT_LEN);

let mut output = vec![0u8; H::OUTPUT_LEN];
let mut message_digest = H::default();
message_digest.do_update(input);
let bytes_written = message_digest
.do_final_partial_bits_out(partial_byte, num_bits, &mut *output)
.expect("Failed to finalize partial input");
assert_eq!(bytes_written, H::OUTPUT_LEN);
assert_eq!(
output, expected_partial_output,
"do_final_partial_bits_out() must agree with do_final_partial_bits() / num_bits: {num_bits}, partial_byte: {partial_byte:#04X}"
);
}
}

// Each (num_bits, partial_byte) pair is a distinct message, and so must produce a
// distinct digest. This is what catches an implementation that silently drops the
// partial bits, or absorbs the wrong number of them.
let mut partial_outputs: Vec<Vec<u8>> = Vec::new();
for num_bits in 0..=7 {
for partial_byte in 0..(1u16 << num_bits) {
partial_outputs.push(partial_digest(partial_byte as u8, num_bits));
}
}
let num_partial_outputs = partial_outputs.len();
partial_outputs.sort_unstable();
partial_outputs.dedup();
assert_eq!(
partial_outputs.len(),
num_partial_outputs,
"each (num_bits, partial_byte) pair is a distinct message and must hash to a distinct output"
);
}

// check that if you feed it an output slice that's bigger than it needs, that it doesn't touch the extra bytes.
Expand Down
1 change: 1 addition & 0 deletions crypto/core-test-framework/src/lib.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@ pub mod mac;
pub mod signature;
pub mod suspendable_state;
pub mod symmetric_ciphers;
pub mod xof;

mod fixed_seed_rng;
pub use fixed_seed_rng::FixedSeedRNG;
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
133ea1b
sha3: fix partial-byte squeeze, validate partial-bit inputs, docs and…
dghgit Aug 26, 2026
5afe03d
sha3: share finalize() between do_final_out and do_final_partial_bits…
dghgit Aug 26, 2026
989e339
sha3: rustfmt
dghgit Aug 26, 2026
b8beef8
sha3: address PR #87 review
dghgit Aug 27, 2026
77b5d86
sha3: read NIST FIPS 202 example vectors from bc-test-data
dghgit Aug 27, 2026
b57febb
sha3: add NIST CAVP SHA3VS harness; fix double SHAKE suffix on 4-bit …
dghgit Aug 27, 2026
6153bbf
core: document the partial-byte bit-ordering convention on Hash
dghgit Aug 27, 2026
8496d7e
Add 0.1.3 release notes for the SHA-3 changes (PR #87)
dghgit Aug 27, 2026
c940a5d
Add bench_sha3_mem_usage: struct sizes and massif entry points for SH…
dghgit Aug 27, 2026
19371ae
sha3: fix SHAKE partial-byte squeeze, 4-bit tail double suffix, and p…
ounsworth Aug 27, 2026
ec56486
shortened the release notes
ounsworth Sep 1, 2026
f5b8c3d
some work on the sha3 partial byte bug pr
ounsworth Sep 2, 2026
0666366
tightened up absorb-after-squeeze behaviour for shake
ounsworth Sep 2, 2026
2a517e6
tightened up absorb-after-squeeze behaviour for shake
ounsworth Sep 2, 2026
bad45f9
SHAKE::squeeze_partial_byte now works on num_bits=0
ounsworth Sep 2, 2026
90690de
SHAKE::squeeze_partial_byte now works on num_bits=0
ounsworth Sep 2, 2026
da51bf0
sha3: fix SHAKE partial-byte squeeze, 4-bit tail double suffix, and p…
ounsworth Aug 27, 2026
9c4b16a
shortened the release notes
ounsworth Sep 1, 2026
7903d42
some work on the sha3 partial byte bug pr
ounsworth Sep 2, 2026
225e9d0
tightened up absorb-after-squeeze behaviour for shake
ounsworth Sep 2, 2026
a265fd6
tightened up absorb-after-squeeze behaviour for shake
ounsworth Sep 2, 2026
f7a82ad
SHAKE::squeeze_partial_byte now works on num_bits=0
ounsworth Sep 2, 2026
dbcb020
SHAKE::squeeze_partial_byte now works on num_bits=0
ounsworth Sep 2, 2026
04d1e45
Merge branch 'fix/sha3-shake-partial-bits' of github.com:ounsworth/bc…
ounsworth Sep 2, 2026
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
4 changes: 4 additions & 0 deletions alpha_0.1.3_release_notes.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,3 +3,7 @@
## Major features

## Minor features / bug fixes

* bug fixes to the way SHA3/SHAKE handled absorbing and squeezing a partial final byte.
* Design discussions about whether core::traits::XOF (in the abstract) should allow interleaving absorb -> squeeze ->
absorb (ie "absorb-after-squeeze). Outcome: absorb-after-squeeze forbidden. Could be changed in the future.
2 changes: 1 addition & 1 deletion cli/src/main.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -119,7 +119,7 @@ enum Subcommands {
/// Length of the output in bytes.
length: usize,

#[arg(long)]
#[arg(short)]
/// Output the hashes in hex format.
x: bool,
},
Expand Down
127 changes: 109 additions & 18 deletions crypto/core-test-framework/src/hash.rs
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
//! Generic behaviour tests for anything that implements [`Hash`].

use bouncycastle_core::errors::HashError;
use bouncycastle_core::traits::{Hash, HashAlgParams};

/// Instance of the test framework.
pub struct TestFrameworkHash {
// Put any config options here
/// Can be disabled for hash functions that don't implement [`Hash::do_final_partial_bits`].
pub enable_partial_final_input_tests: bool,
pub enable_partial_byte_tests: bool,
}

impl TestFrameworkHash {
///
pub fn new() -> Self {
Self { enable_partial_final_input_tests: true }
Self { enable_partial_byte_tests: true }
}

/// Test all the members of trait Hash against the given input-output pair.
Expand DownExpand Up@@ -92,22 +93,112 @@ impl TestFrameworkHash {
);
}

if self.enable_partial_final_input_tests {
/*** fn do_final_partial_bits(self, partial_byte: u8, num_partial_bits: usize)-> Result<Vec<u8>, HashError>; ***/
/*** fn do_final_partial_bits_out(self, partial_byte: u8, num_partial_bits: usize, output: &mut [u8]) -> Result<usize, HashError>; ***/
// There's not a lot we can test here because this will require a different expected output from the rest of this test, but we can do something.

//output slice too small -- should just truncate
let mut first_output = vec![0u8; H::default().output_len()];
H::default()
.do_final_partial_bits_out(0xFF, 7, &mut *first_output)
.expect("Failed to finalize partial input");
let len_to_truncate_to = H::default().output_len() - 1;
let mut output = vec![0u8; len_to_truncate_to];
let bytes_written =
H::default().do_final_partial_bits_out(0xFF, 7, &mut *output).unwrap();
assert_eq!(bytes_written, len_to_truncate_to);
assert_eq!(first_output[..len_to_truncate_to], output);
if self.enable_partial_byte_tests {
/*** Testing: ***/
/*** fn do_final_partial_bits(self, partial_byte: u8, num_bits: usize)-> Result<Vec<u8>, HashError>; ***/
/*** fn do_final_partial_bits_out(self, partial_byte: u8, num_bits: usize, output: &mut [u8]) -> Result<usize, HashError>; ***/
// A known-answer test for these needs a different expected output from the rest of this

// Helper: the digest of `input` finished with the low `num_bits` bits of `partial_byte`.
let partial_digest = |partial_byte: u8, num_bits: usize| -> Vec<u8> {
let mut message_digest = H::default();
message_digest.do_update(input);
message_digest
.do_final_partial_bits(partial_byte, num_bits)
.expect("do_final_partial_bits() must succeed for num_bits in 0..=7")
};

// "0 is a valid value and means the message ends on a byte boundary (equivalent to
// Hash::do_final())".
// So, test against the `expected_output` result from above
for partial_byte in [0x00u8, 0x01, 0x80, 0xA5, 0xFF] {
assert_eq!(
partial_digest(partial_byte, 0),
expected_output,
"num_bits = 0 must be equivalent to do_final() / partial_byte: {partial_byte:#04X}"
);
}

// "The num_bits message bits are taken from the least significant bits of
// partial_byte": the unused high bits are not part of the message, and so must not
// change the output.
for num_bits in 0..=7 {
// no overflow: 1u8 << 7 == 0x80
let mask = (1u8 << num_bits) - 1;
for partial_byte in [0x00u8, 0x5A, 0xA5, 0xFF] {
assert_eq!(
partial_digest(partial_byte, num_bits),
partial_digest(partial_byte & mask, num_bits),
"bits above num_bits = {num_bits} must be ignored / partial_byte: {partial_byte:#04X}"
);
}
}

// "num_bits must be in 0..=7; larger values return HashError::InvalidLength."
// The range has to be validated before any shift by num_bits, so check well past
// the width of the shifted type as well as the 8 / 9 boundary.
for num_bits in [8usize, 9, 15, 16, 64, usize::MAX] {
let mut message_digest = H::default();
message_digest.do_update(input);
assert!(
matches!(
message_digest.do_final_partial_bits(0xFF, num_bits),
Err(HashError::InvalidLength(_))
),
"num_bits = {num_bits} must be rejected with InvalidLength"
);

let mut output = vec![0u8; H::OUTPUT_LEN];
let mut message_digest = H::default();
message_digest.do_update(input);
assert!(
matches!(
message_digest.do_final_partial_bits_out(0xFF, num_bits, &mut *output),
Err(HashError::InvalidLength(_))
),
"num_bits = {num_bits} must be rejected with InvalidLength (_out variant)"
);
}

// "The same as Hash::do_final_partial_bits, but takes the output buffer as an
// argument": the two variants must agree, and the Vec variant must return
// output_len() bytes.
for num_bits in 0..=7 {
for partial_byte in [0x00u8, 0x5A, 0xA5, 0xFF] {
let expected_partial_output = partial_digest(partial_byte, num_bits);
assert_eq!(expected_partial_output.len(), H::OUTPUT_LEN);

let mut output = vec![0u8; H::OUTPUT_LEN];
let mut message_digest = H::default();
message_digest.do_update(input);
let bytes_written = message_digest
.do_final_partial_bits_out(partial_byte, num_bits, &mut *output)
.expect("Failed to finalize partial input");
assert_eq!(bytes_written, H::OUTPUT_LEN);
assert_eq!(
output, expected_partial_output,
"do_final_partial_bits_out() must agree with do_final_partial_bits() / num_bits: {num_bits}, partial_byte: {partial_byte:#04X}"
);
}
}

// Each (num_bits, partial_byte) pair is a distinct message, and so must produce a
// distinct digest. This is what catches an implementation that silently drops the
// partial bits, or absorbs the wrong number of them.
let mut partial_outputs: Vec<Vec<u8>> = Vec::new();
for num_bits in 0..=7 {
for partial_byte in 0..(1u16 << num_bits) {
partial_outputs.push(partial_digest(partial_byte as u8, num_bits));
}
}
let num_partial_outputs = partial_outputs.len();
partial_outputs.sort_unstable();
partial_outputs.dedup();
assert_eq!(
partial_outputs.len(),
num_partial_outputs,
"each (num_bits, partial_byte) pair is a distinct message and must hash to a distinct output"
);
}

// check that if you feed it an output slice that's bigger than it needs, that it doesn't touch the extra bytes.
Expand Down
1 change: 1 addition & 0 deletions crypto/core-test-framework/src/lib.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@ pub mod mac;
pub mod signature;
pub mod suspendable_state;
pub mod symmetric_ciphers;
pub mod xof;

mod fixed_seed_rng;
pub use fixed_seed_rng::FixedSeedRNG;
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
133ea1b
sha3: fix partial-byte squeeze, validate partial-bit inputs, docs and…
dghgit Aug 26, 2026
5afe03d
sha3: share finalize() between do_final_out and do_final_partial_bits…
dghgit Aug 26, 2026
989e339
sha3: rustfmt
dghgit Aug 26, 2026
b8beef8
sha3: address PR #87 review
dghgit Aug 27, 2026
77b5d86
sha3: read NIST FIPS 202 example vectors from bc-test-data
dghgit Aug 27, 2026
b57febb
sha3: add NIST CAVP SHA3VS harness; fix double SHAKE suffix on 4-bit …
dghgit Aug 27, 2026
6153bbf
core: document the partial-byte bit-ordering convention on Hash
dghgit Aug 27, 2026
8496d7e
Add 0.1.3 release notes for the SHA-3 changes (PR #87)
dghgit Aug 27, 2026
c940a5d
Add bench_sha3_mem_usage: struct sizes and massif entry points for SH…
dghgit Aug 27, 2026
19371ae
sha3: fix SHAKE partial-byte squeeze, 4-bit tail double suffix, and p…
ounsworth Aug 27, 2026
ec56486
shortened the release notes
ounsworth Sep 1, 2026
f5b8c3d
some work on the sha3 partial byte bug pr
ounsworth Sep 2, 2026
0666366
tightened up absorb-after-squeeze behaviour for shake
ounsworth Sep 2, 2026
2a517e6
tightened up absorb-after-squeeze behaviour for shake
ounsworth Sep 2, 2026
bad45f9
SHAKE::squeeze_partial_byte now works on num_bits=0
ounsworth Sep 2, 2026
90690de
SHAKE::squeeze_partial_byte now works on num_bits=0
ounsworth Sep 2, 2026
da51bf0
sha3: fix SHAKE partial-byte squeeze, 4-bit tail double suffix, and p…
ounsworth Aug 27, 2026
9c4b16a
shortened the release notes
ounsworth Sep 1, 2026
7903d42
some work on the sha3 partial byte bug pr
ounsworth Sep 2, 2026
225e9d0
tightened up absorb-after-squeeze behaviour for shake
ounsworth Sep 2, 2026
a265fd6
tightened up absorb-after-squeeze behaviour for shake
ounsworth Sep 2, 2026
f7a82ad
SHAKE::squeeze_partial_byte now works on num_bits=0
ounsworth Sep 2, 2026
dbcb020
SHAKE::squeeze_partial_byte now works on num_bits=0
ounsworth Sep 2, 2026
04d1e45
Merge branch 'fix/sha3-shake-partial-bits' of github.com:ounsworth/bc…
ounsworth Sep 2, 2026
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
4 changes: 4 additions & 0 deletions alpha_0.1.3_release_notes.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,3 +3,7 @@
## Major features

## Minor features / bug fixes

* bug fixes to the way SHA3/SHAKE handled absorbing and squeezing a partial final byte.
* Design discussions about whether core::traits::XOF (in the abstract) should allow interleaving absorb -> squeeze ->
absorb (ie "absorb-after-squeeze). Outcome: absorb-after-squeeze forbidden. Could be changed in the future.
2 changes: 1 addition & 1 deletion cli/src/main.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -119,7 +119,7 @@ enum Subcommands {
/// Length of the output in bytes.
length: usize,

#[arg(long)]
#[arg(short)]
/// Output the hashes in hex format.
x: bool,
},
Expand Down
127 changes: 109 additions & 18 deletions crypto/core-test-framework/src/hash.rs
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
//! Generic behaviour tests for anything that implements [`Hash`].

use bouncycastle_core::errors::HashError;
use bouncycastle_core::traits::{Hash, HashAlgParams};

/// Instance of the test framework.
pub struct TestFrameworkHash {
// Put any config options here
/// Can be disabled for hash functions that don't implement [`Hash::do_final_partial_bits`].
pub enable_partial_final_input_tests: bool,
pub enable_partial_byte_tests: bool,
}

impl TestFrameworkHash {
///
pub fn new() -> Self {
Self { enable_partial_final_input_tests: true }
Self { enable_partial_byte_tests: true }
}

/// Test all the members of trait Hash against the given input-output pair.
Expand DownExpand Up@@ -92,22 +93,112 @@ impl TestFrameworkHash {
);
}

if self.enable_partial_final_input_tests {
/*** fn do_final_partial_bits(self, partial_byte: u8, num_partial_bits: usize)-> Result<Vec<u8>, HashError>; ***/
/*** fn do_final_partial_bits_out(self, partial_byte: u8, num_partial_bits: usize, output: &mut [u8]) -> Result<usize, HashError>; ***/
// There's not a lot we can test here because this will require a different expected output from the rest of this test, but we can do something.

//output slice too small -- should just truncate
let mut first_output = vec![0u8; H::default().output_len()];
H::default()
.do_final_partial_bits_out(0xFF, 7, &mut *first_output)
.expect("Failed to finalize partial input");
let len_to_truncate_to = H::default().output_len() - 1;
let mut output = vec![0u8; len_to_truncate_to];
let bytes_written =
H::default().do_final_partial_bits_out(0xFF, 7, &mut *output).unwrap();
assert_eq!(bytes_written, len_to_truncate_to);
assert_eq!(first_output[..len_to_truncate_to], output);
if self.enable_partial_byte_tests {
/*** Testing: ***/
/*** fn do_final_partial_bits(self, partial_byte: u8, num_bits: usize)-> Result<Vec<u8>, HashError>; ***/
/*** fn do_final_partial_bits_out(self, partial_byte: u8, num_bits: usize, output: &mut [u8]) -> Result<usize, HashError>; ***/
// A known-answer test for these needs a different expected output from the rest of this

// Helper: the digest of `input` finished with the low `num_bits` bits of `partial_byte`.
let partial_digest = |partial_byte: u8, num_bits: usize| -> Vec<u8> {
let mut message_digest = H::default();
message_digest.do_update(input);
message_digest
.do_final_partial_bits(partial_byte, num_bits)
.expect("do_final_partial_bits() must succeed for num_bits in 0..=7")
};

// "0 is a valid value and means the message ends on a byte boundary (equivalent to
// Hash::do_final())".
// So, test against the `expected_output` result from above
for partial_byte in [0x00u8, 0x01, 0x80, 0xA5, 0xFF] {
assert_eq!(
partial_digest(partial_byte, 0),
expected_output,
"num_bits = 0 must be equivalent to do_final() / partial_byte: {partial_byte:#04X}"
);
}

// "The num_bits message bits are taken from the least significant bits of
// partial_byte": the unused high bits are not part of the message, and so must not
// change the output.
for num_bits in 0..=7 {
// no overflow: 1u8 << 7 == 0x80
let mask = (1u8 << num_bits) - 1;
for partial_byte in [0x00u8, 0x5A, 0xA5, 0xFF] {
assert_eq!(
partial_digest(partial_byte, num_bits),
partial_digest(partial_byte & mask, num_bits),
"bits above num_bits = {num_bits} must be ignored / partial_byte: {partial_byte:#04X}"
);
}
}

// "num_bits must be in 0..=7; larger values return HashError::InvalidLength."
// The range has to be validated before any shift by num_bits, so check well past
// the width of the shifted type as well as the 8 / 9 boundary.
for num_bits in [8usize, 9, 15, 16, 64, usize::MAX] {
let mut message_digest = H::default();
message_digest.do_update(input);
assert!(
matches!(
message_digest.do_final_partial_bits(0xFF, num_bits),
Err(HashError::InvalidLength(_))
),
"num_bits = {num_bits} must be rejected with InvalidLength"
);

let mut output = vec![0u8; H::OUTPUT_LEN];
let mut message_digest = H::default();
message_digest.do_update(input);
assert!(
matches!(
message_digest.do_final_partial_bits_out(0xFF, num_bits, &mut *output),
Err(HashError::InvalidLength(_))
),
"num_bits = {num_bits} must be rejected with InvalidLength (_out variant)"
);
}

// "The same as Hash::do_final_partial_bits, but takes the output buffer as an
// argument": the two variants must agree, and the Vec variant must return
// output_len() bytes.
for num_bits in 0..=7 {
for partial_byte in [0x00u8, 0x5A, 0xA5, 0xFF] {
let expected_partial_output = partial_digest(partial_byte, num_bits);
assert_eq!(expected_partial_output.len(), H::OUTPUT_LEN);

let mut output = vec![0u8; H::OUTPUT_LEN];
let mut message_digest = H::default();
message_digest.do_update(input);
let bytes_written = message_digest
.do_final_partial_bits_out(partial_byte, num_bits, &mut *output)
.expect("Failed to finalize partial input");
assert_eq!(bytes_written, H::OUTPUT_LEN);
assert_eq!(
output, expected_partial_output,
"do_final_partial_bits_out() must agree with do_final_partial_bits() / num_bits: {num_bits}, partial_byte: {partial_byte:#04X}"
);
}
}

// Each (num_bits, partial_byte) pair is a distinct message, and so must produce a
// distinct digest. This is what catches an implementation that silently drops the
// partial bits, or absorbs the wrong number of them.
let mut partial_outputs: Vec<Vec<u8>> = Vec::new();
for num_bits in 0..=7 {
for partial_byte in 0..(1u16 << num_bits) {
partial_outputs.push(partial_digest(partial_byte as u8, num_bits));
}
}
let num_partial_outputs = partial_outputs.len();
partial_outputs.sort_unstable();
partial_outputs.dedup();
assert_eq!(
partial_outputs.len(),
num_partial_outputs,
"each (num_bits, partial_byte) pair is a distinct message and must hash to a distinct output"
);
}

// check that if you feed it an output slice that's bigger than it needs, that it doesn't touch the extra bytes.
Expand Down
1 change: 1 addition & 0 deletions crypto/core-test-framework/src/lib.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@ pub mod mac;
pub mod signature;
pub mod suspendable_state;
pub mod symmetric_ciphers;
pub mod xof;

mod fixed_seed_rng;
pub use fixed_seed_rng::FixedSeedRNG;
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
133ea1b
sha3: fix partial-byte squeeze, validate partial-bit inputs, docs and…
dghgit Aug 26, 2026
5afe03d
sha3: share finalize() between do_final_out and do_final_partial_bits…
dghgit Aug 26, 2026
989e339
sha3: rustfmt
dghgit Aug 26, 2026
b8beef8
sha3: address PR #87 review
dghgit Aug 27, 2026
77b5d86
sha3: read NIST FIPS 202 example vectors from bc-test-data
dghgit Aug 27, 2026
b57febb
sha3: add NIST CAVP SHA3VS harness; fix double SHAKE suffix on 4-bit …
dghgit Aug 27, 2026
6153bbf
core: document the partial-byte bit-ordering convention on Hash
dghgit Aug 27, 2026
8496d7e
Add 0.1.3 release notes for the SHA-3 changes (PR #87)
dghgit Aug 27, 2026
c940a5d
Add bench_sha3_mem_usage: struct sizes and massif entry points for SH…
dghgit Aug 27, 2026
19371ae
sha3: fix SHAKE partial-byte squeeze, 4-bit tail double suffix, and p…
ounsworth Aug 27, 2026
ec56486
shortened the release notes
ounsworth Sep 1, 2026
f5b8c3d
some work on the sha3 partial byte bug pr
ounsworth Sep 2, 2026
0666366
tightened up absorb-after-squeeze behaviour for shake
ounsworth Sep 2, 2026
2a517e6
tightened up absorb-after-squeeze behaviour for shake
ounsworth Sep 2, 2026
bad45f9
SHAKE::squeeze_partial_byte now works on num_bits=0
ounsworth Sep 2, 2026
90690de
SHAKE::squeeze_partial_byte now works on num_bits=0
ounsworth Sep 2, 2026
da51bf0
sha3: fix SHAKE partial-byte squeeze, 4-bit tail double suffix, and p…
ounsworth Aug 27, 2026
9c4b16a
shortened the release notes
ounsworth Sep 1, 2026
7903d42
some work on the sha3 partial byte bug pr
ounsworth Sep 2, 2026
225e9d0
tightened up absorb-after-squeeze behaviour for shake
ounsworth Sep 2, 2026
a265fd6
tightened up absorb-after-squeeze behaviour for shake
ounsworth Sep 2, 2026
f7a82ad
SHAKE::squeeze_partial_byte now works on num_bits=0
ounsworth Sep 2, 2026
dbcb020
SHAKE::squeeze_partial_byte now works on num_bits=0
ounsworth Sep 2, 2026
04d1e45
Merge branch 'fix/sha3-shake-partial-bits' of github.com:ounsworth/bc…
ounsworth Sep 2, 2026
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
4 changes: 4 additions & 0 deletions alpha_0.1.3_release_notes.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,3 +3,7 @@
## Major features

## Minor features / bug fixes

* bug fixes to the way SHA3/SHAKE handled absorbing and squeezing a partial final byte.
* Design discussions about whether core::traits::XOF (in the abstract) should allow interleaving absorb -> squeeze ->
absorb (ie "absorb-after-squeeze). Outcome: absorb-after-squeeze forbidden. Could be changed in the future.
2 changes: 1 addition & 1 deletion cli/src/main.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -119,7 +119,7 @@ enum Subcommands {
/// Length of the output in bytes.
length: usize,

#[arg(long)]
#[arg(short)]
/// Output the hashes in hex format.
x: bool,
},
Expand Down
127 changes: 109 additions & 18 deletions crypto/core-test-framework/src/hash.rs
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
//! Generic behaviour tests for anything that implements [`Hash`].

use bouncycastle_core::errors::HashError;
use bouncycastle_core::traits::{Hash, HashAlgParams};

/// Instance of the test framework.
pub struct TestFrameworkHash {
// Put any config options here
/// Can be disabled for hash functions that don't implement [`Hash::do_final_partial_bits`].
pub enable_partial_final_input_tests: bool,
pub enable_partial_byte_tests: bool,
}

impl TestFrameworkHash {
///
pub fn new() -> Self {
Self { enable_partial_final_input_tests: true }
Self { enable_partial_byte_tests: true }
}

/// Test all the members of trait Hash against the given input-output pair.
Expand DownExpand Up@@ -92,22 +93,112 @@ impl TestFrameworkHash {
);
}

if self.enable_partial_final_input_tests {
/*** fn do_final_partial_bits(self, partial_byte: u8, num_partial_bits: usize)-> Result<Vec<u8>, HashError>; ***/
/*** fn do_final_partial_bits_out(self, partial_byte: u8, num_partial_bits: usize, output: &mut [u8]) -> Result<usize, HashError>; ***/
// There's not a lot we can test here because this will require a different expected output from the rest of this test, but we can do something.

//output slice too small -- should just truncate
let mut first_output = vec![0u8; H::default().output_len()];
H::default()
.do_final_partial_bits_out(0xFF, 7, &mut *first_output)
.expect("Failed to finalize partial input");
let len_to_truncate_to = H::default().output_len() - 1;
let mut output = vec![0u8; len_to_truncate_to];
let bytes_written =
H::default().do_final_partial_bits_out(0xFF, 7, &mut *output).unwrap();
assert_eq!(bytes_written, len_to_truncate_to);
assert_eq!(first_output[..len_to_truncate_to], output);
if self.enable_partial_byte_tests {
/*** Testing: ***/
/*** fn do_final_partial_bits(self, partial_byte: u8, num_bits: usize)-> Result<Vec<u8>, HashError>; ***/
/*** fn do_final_partial_bits_out(self, partial_byte: u8, num_bits: usize, output: &mut [u8]) -> Result<usize, HashError>; ***/
// A known-answer test for these needs a different expected output from the rest of this

// Helper: the digest of `input` finished with the low `num_bits` bits of `partial_byte`.
let partial_digest = |partial_byte: u8, num_bits: usize| -> Vec<u8> {
let mut message_digest = H::default();
message_digest.do_update(input);
message_digest
.do_final_partial_bits(partial_byte, num_bits)
.expect("do_final_partial_bits() must succeed for num_bits in 0..=7")
};

// "0 is a valid value and means the message ends on a byte boundary (equivalent to
// Hash::do_final())".
// So, test against the `expected_output` result from above
for partial_byte in [0x00u8, 0x01, 0x80, 0xA5, 0xFF] {
assert_eq!(
partial_digest(partial_byte, 0),
expected_output,
"num_bits = 0 must be equivalent to do_final() / partial_byte: {partial_byte:#04X}"
);
}

// "The num_bits message bits are taken from the least significant bits of
// partial_byte": the unused high bits are not part of the message, and so must not
// change the output.
for num_bits in 0..=7 {
// no overflow: 1u8 << 7 == 0x80
let mask = (1u8 << num_bits) - 1;
for partial_byte in [0x00u8, 0x5A, 0xA5, 0xFF] {
assert_eq!(
partial_digest(partial_byte, num_bits),
partial_digest(partial_byte & mask, num_bits),
"bits above num_bits = {num_bits} must be ignored / partial_byte: {partial_byte:#04X}"
);
}
}

// "num_bits must be in 0..=7; larger values return HashError::InvalidLength."
// The range has to be validated before any shift by num_bits, so check well past
// the width of the shifted type as well as the 8 / 9 boundary.
for num_bits in [8usize, 9, 15, 16, 64, usize::MAX] {
let mut message_digest = H::default();
message_digest.do_update(input);
assert!(
matches!(
message_digest.do_final_partial_bits(0xFF, num_bits),
Err(HashError::InvalidLength(_))
),
"num_bits = {num_bits} must be rejected with InvalidLength"
);

let mut output = vec![0u8; H::OUTPUT_LEN];
let mut message_digest = H::default();
message_digest.do_update(input);
assert!(
matches!(
message_digest.do_final_partial_bits_out(0xFF, num_bits, &mut *output),
Err(HashError::InvalidLength(_))
),
"num_bits = {num_bits} must be rejected with InvalidLength (_out variant)"
);
}

// "The same as Hash::do_final_partial_bits, but takes the output buffer as an
// argument": the two variants must agree, and the Vec variant must return
// output_len() bytes.
for num_bits in 0..=7 {
for partial_byte in [0x00u8, 0x5A, 0xA5, 0xFF] {
let expected_partial_output = partial_digest(partial_byte, num_bits);
assert_eq!(expected_partial_output.len(), H::OUTPUT_LEN);

let mut output = vec![0u8; H::OUTPUT_LEN];
let mut message_digest = H::default();
message_digest.do_update(input);
let bytes_written = message_digest
.do_final_partial_bits_out(partial_byte, num_bits, &mut *output)
.expect("Failed to finalize partial input");
assert_eq!(bytes_written, H::OUTPUT_LEN);
assert_eq!(
output, expected_partial_output,
"do_final_partial_bits_out() must agree with do_final_partial_bits() / num_bits: {num_bits}, partial_byte: {partial_byte:#04X}"
);
}
}

// Each (num_bits, partial_byte) pair is a distinct message, and so must produce a
// distinct digest. This is what catches an implementation that silently drops the
// partial bits, or absorbs the wrong number of them.
let mut partial_outputs: Vec<Vec<u8>> = Vec::new();
for num_bits in 0..=7 {
for partial_byte in 0..(1u16 << num_bits) {
partial_outputs.push(partial_digest(partial_byte as u8, num_bits));
}
}
let num_partial_outputs = partial_outputs.len();
partial_outputs.sort_unstable();
partial_outputs.dedup();
assert_eq!(
partial_outputs.len(),
num_partial_outputs,
"each (num_bits, partial_byte) pair is a distinct message and must hash to a distinct output"
);
}

// check that if you feed it an output slice that's bigger than it needs, that it doesn't touch the extra bytes.
Expand Down
1 change: 1 addition & 0 deletions crypto/core-test-framework/src/lib.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@ pub mod mac;
pub mod signature;
pub mod suspendable_state;
pub mod symmetric_ciphers;
pub mod xof;

mod fixed_seed_rng;
pub use fixed_seed_rng::FixedSeedRNG;
Expand Down
Loading