diff --git a/CLAUDE.md b/CLAUDE.md index 97aac7e..f177de9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -89,6 +89,7 @@ These are non-obvious house rules โ€” follow them when writing or modifying code - `cargo mutants` is expected to be run on each crate; surviving mutants must be investigated but not all need to die (e.g. XOR/OR equivalences in crypto code are acceptable). Config lives in `.cargo/mutants.toml` (output dir `custom_mutants_output/`). - Behaviour-critical private functions can use in-file `#[cfg(test)] mod tests` blocks when they can't be exercised from outside the crate. - For traits in `core`, the canonical tests live in `core-test-framework` and are invoked from each implementor's integration tests โ€” don't duplicate them per-implementation. +- The per-width `impl Condition` blocks in `crypto/utils/src/ct.rs` (and their test modules) are deliberately duplicated rather than macro-generated: `cargo mutants` cannot see into `macro_rules!` bodies, so a macro would hide the mask identities from mutation testing. Do not fold them back into a macro. Any change to one width in a group (i64/i32, u64/u32) must be applied to every width in that group. ## CI diff --git a/cli/src/mldsa_cmd.rs b/cli/src/mldsa_cmd.rs index beb92c6..070af7e 100644 --- a/cli/src/mldsa_cmd.rs +++ b/cli/src/mldsa_cmd.rs @@ -1,4 +1,4 @@ -//! Work in progress. +//! Work in progress. //! TODO: Use generic macros to eliminate duplicated code. use crate::helpers::{parse_seed, read_from_file, read_from_file_or_stdin, write_bytes_or_hex}; diff --git a/crypto/core/src/key_material.rs b/crypto/core/src/key_material.rs index dc3244d..fb95d71 100644 --- a/crypto/core/src/key_material.rs +++ b/crypto/core/src/key_material.rs @@ -547,7 +547,7 @@ impl KeyMaterialTrait for KeyMaterial { } self.security_strength = strength; - + Ok(()) } diff --git a/crypto/factory/src/rng_factory.rs b/crypto/factory/src/rng_factory.rs index c316475..492efec 100644 --- a/crypto/factory/src/rng_factory.rs +++ b/crypto/factory/src/rng_factory.rs @@ -30,7 +30,7 @@ //! let output: Vec = h.hash(data); //! ``` //! Equivalently, it may be invoked by passing a string instead of using the constant: -//! +//! //! ``` //! use bouncycastle_factory::AlgorithmFactory; //! use bouncycastle_core::traits::Hash; diff --git a/crypto/hex/src/lib.rs b/crypto/hex/src/lib.rs index 3b53b45..923151d 100644 --- a/crypto/hex/src/lib.rs +++ b/crypto/hex/src/lib.rs @@ -3,14 +3,14 @@ //! This one is implemented using constant-time operations in the conversions //! from Strings to byte values, so it is safe to use on cryptographic secret values. //! -//! It should just work as expected: -//! encode takes any bytes-like rust type and returns a String, +//! It should just work as expected: +//! encode takes any bytes-like rust type and returns a String, //! decode takes a String (which can be in any bytes-like container) and returns a `Vec`. //! //! Moreover, the API of this crate is intended to mirror that of the public `hex` crate, //! so you should generally be able to swap `use hex` for `use bouncycastle_hex` and all the function //! calls and behaviours should work as expected. -//! +//! //! ``` //! use bouncycastle_hex as hex; //! @@ -72,7 +72,7 @@ pub fn encode_out>(input: T, out: &mut [u8]) -> Result::is_within_range(c as i64, 0, 9); let in_af = Condition::::is_within_range(c as i64, 10, 15); - // TODO: redo this once we have ct::u8 implemented + // TODO: redo this once we have ct::u8 implemented // The i64 is wasteful let c_09: i64 = '0' as i64 + (c as i64); @@ -111,7 +111,7 @@ pub fn decode_out>(input: T, out: &mut [u8]) -> Result { @@ -157,7 +157,7 @@ pub fn decode_out>(input: T, out: &mut [u8]) -> Result::is_within_range(b as i64, 65, 70); - // TODO: redo this once we have ct::u8 implemented + // TODO: redo this once we have ct::u8 implemented // The i64 is wasteful let c_09: i64 = b as i64 - ('0' as i64); diff --git a/crypto/mldsa-lowmemory/src/aux_functions.rs b/crypto/mldsa-lowmemory/src/aux_functions.rs index 4d91dc5..2726420 100644 --- a/crypto/mldsa-lowmemory/src/aux_functions.rs +++ b/crypto/mldsa-lowmemory/src/aux_functions.rs @@ -471,7 +471,7 @@ pub(crate) fn sample_in_ball( let mut j = [0u8]; for i in (N - TAU as usize)..N { // 7: (ctx, ๐‘—) โ† H.Squeeze(ctx, 1) - // Note: At first, it might seem to be faster to pre-squeeze a buffer outside the loop. + // Note: At first, it might seem to be faster to pre-squeeze a buffer outside the loop. // However, after experimentation and testing, the difference is not noticeable. h.squeeze_out(&mut j); @@ -567,7 +567,7 @@ pub(crate) fn rej_bounded_poly(rho: &[u8; 64], nonce: &[u8; 2] // SHAKE is fairly inefficient if only 3 bytes are squeezed at a time, so the implementation does a block instead. // size is not a limitation as long as it is a multiple of 3. // 312 seems to be the sweet spot after some experimentation - // which is possibly also related with the average rejection rate. + // which is possibly also related with the average rejection rate. // Also, 312 is a multiple of 8 (efficient for SHAKE) let mut z_arr = [0u8; 312]; h.squeeze_out(&mut z_arr); diff --git a/crypto/mldsa-lowmemory/src/polynomial.rs b/crypto/mldsa-lowmemory/src/polynomial.rs index 2e672b5..95e3387 100644 --- a/crypto/mldsa-lowmemory/src/polynomial.rs +++ b/crypto/mldsa-lowmemory/src/polynomial.rs @@ -95,7 +95,7 @@ impl Polynomial { pub(crate) fn check_norm(&self) -> bool { // Fine that this is not constant-time (returns true early) because it is used in a rejection loop. - // IE the early quit here leads to rejection and continuing to the top of the rejection loop, or failing + // IE the early quit here leads to rejection and continuing to the top of the rejection loop, or failing // the signature validation. // So the i32 that was just checked in a non-constant-time manner is about to get thrown away. diff --git a/crypto/mldsa/src/hash_mldsa.rs b/crypto/mldsa/src/hash_mldsa.rs index af38aff..5e1b535 100644 --- a/crypto/mldsa/src/hash_mldsa.rs +++ b/crypto/mldsa/src/hash_mldsa.rs @@ -1097,7 +1097,7 @@ impl< /// Note that the PH expected here *is not the same* as the `mu` computed by [`MuBuilder`]. /// To make use of this function, the user needs to compute a straight hash of the message using - /// the same hash function as the indicated in the HashML-DSA variant; + /// the same hash function as the indicated in the HashML-DSA variant; /// for example: SHA256 for HashMDSA44_with_SHA256; SHA512 for HashMLDSA65_with_SHA512; etc. fn sign_ph_out( sk: &SK, diff --git a/crypto/mldsa/src/lib.rs b/crypto/mldsa/src/lib.rs index 91852f1..15fa740 100644 --- a/crypto/mldsa/src/lib.rs +++ b/crypto/mldsa/src/lib.rs @@ -116,11 +116,10 @@ //! //! `mu`, `ph`, and `ctx` are binding values that the verifier must reproduce. This means that getting //! them wrong does not compromise security, it just yields a signature the intended -//! verifier won't accept (a correctness/interoperability failure). +//! verifier won't accept (a correctness/interoperability failure). //! One caveat: `ctx` can still be security-relevant at the protocol level (domain separation, replay and //! cross-protocol binding), so choosing it incorrectly can weaken those properties. - #![no_std] #![forbid(unsafe_code)] #![forbid(missing_docs)] diff --git a/crypto/mldsa/tests/mldsa_tests.rs b/crypto/mldsa/tests/mldsa_tests.rs index a07d2e9..33aa8f9 100644 --- a/crypto/mldsa/tests/mldsa_tests.rs +++ b/crypto/mldsa/tests/mldsa_tests.rs @@ -1083,9 +1083,9 @@ struct Kat { } // generated by hand against bc-java -// This is almost tgId=1, tcId=1 from +// This is almost tgId=1, tcId=1 from // https://raw.githubusercontent.com/bcgit/bc-test-data/refs/heads/main/pqc/crypto/mldsa/ML-DSA-sigGen.txt -// with the difference being that an empty ctx is added, instead of testing sign_internal directly, +// with the difference being that an empty ctx is added, instead of testing sign_internal directly, // and the signature value is run against bc-java const MLDSA44_KAT1: Kat = Kat { _parameter_set: "ML-DSA-44", @@ -1096,9 +1096,9 @@ const MLDSA44_KAT1: Kat = Kat { signature: "2bddcee4a9ac1b9d19bc1531365c5613e48b95a530339c52f5fc0b671ee01b6587fe08b290c191f82eace640fae216cca90f40fa93bb309e5ff53afc5a042050bffeb69d4e0041c34fd334a7b576c6ebae68042b315fe78f84300dc011cdb144252a06f35c9a2b0a275d00552f890361d1e7b7439572097d1f3d5833b98b751412deb3c0df23ee3a30782919444bcbcc9ac25c645180c8d1a9fe693086cba4779d6e31b1e5fd1a0ccaeb055c471ef065a273676d78c6e7feebfc607c5578107ac27765345363af57f77431e306d9407ce365708d813e44d3107c108f8c5f2848913a4099f6a0ce2ebf80c7414236d4edc07fee79efe1ab6fe70217bec958ea48221fb6b87cef6f5762b41a9c698fbb45f5994969bee21e40f20c95f9a18389e8b49d6ecccdae9f568313448b5162c981bc9ff320753aede977b15f7ddc68bba076a07deb68ac36826e70d80752fac0356d507b3e283b350a17b015f76c71314f7c2086e44601c4d9c707c99a36e55716fd34384a218242a69876c8dea9f6ec28c40189ee2b8608040a96a813f38240dc85a511b3cebab1ea893270e730aef2e29406fcc1f6826101f1361168ceb3f1632e8ee505143e0f031e744928c1e41eab923ebbfce5e9f159fd160db737b013759382274a1d9409554ab06eb72b5ce2a710d8b08f163df991c956abc823d21b0a6d9d7ae484d6cdb2e04de7a282ef317f488e20d40e4e67cbf05d1528c0ce261e3a65163ad518661989484bc964da21122a6c95ef7036b3273f93833d068d9a2c7933e19ad2fde8378c286ad57f7e22c8aa4153718f356b46b34ca4a986e6e9ef3ddb206693acf3b08c0aa5118c8efbb6a99a0fc0674f6228e2f6f53e229129d4b6c0fd6455e28587f0f169270d044398e2c377ab6f985f7f2235d68192d031f39251f9f0270451beda2d29b654511b73bce0f30174a606a1100f4fc984959704ba0e2df2bdf642ba3b0241ca9889009ae575540c71747246835018ba8faeb473b39e4ede5a6fbe0165a4db2e90739cb051f3e5c1c0ce159539758569ecc8b67a759a2e786d5b4e96834db0dd460ab1c5c7ac8f3cf19596979108dfbdc6c8798e7342026b7571ba64fa86d68e1d5179af5768a7e22db76f3a319193d04eeea626d03d4be7dd83ed6c46e2149a1e2428e60f6566833726bcc93d0b342ff4245665b54167b013b02165d29fb64055112c5d800e853bf28bf149da2284e49dc1c5f478f490ce1002f16431564534244cd9c1e0bd12ea9209efe572afb7effc0153e5b2066b1af0dd05a2d9da13308c90a0244ad95a33a64d07a75d7596e46cdcfd154e18f7677ef16b979a1f431eb59279c65a3a31c5429ceebfcc869cf538722a82e6e765bd6a3042dc825684ad4163e054c113b47276b894580db17c92f0859c240b6812716e408a557c1d7ac8cfc3abf9b43f759bb2a13c903bba936206496fed37696171717cb8995b837e869052c9c25348698617c70e98966d30f56bb5bf41369c0131de4178637c5684a64992b50f9ab9a16b51752e14ea521d5bd42bacdfcf100085629796d22bcaa6a7830f9b5859d75b290a0db031bd51b77fa7911eb17d46dcf16d45e3b1ce8fb7dc19ba969a724e43ccce9487c143302c79fb208b2ab0ea1060af3809d5397c11cc0a79ed39ab06c0ef7bceb8e094ec0e3ea52937442ccc176a31b07a52d7de84c0599dbe736cecfacbb41cac206ec3dc20603606a14a1db3ccad8037a540f3213ee9337b529f447ed2c2e287760973b8175f70c78423ace6179757c519c14a0d7b81af00646afde573a35e909ef9660623cd3651b63b79be508698888e1d3bc87344067dcc099416616c273d5ea0e41a8723d11c5ec49e0c6019f6576a2e87d0237c3f64d49afbaf09fc818829d5d0fcb4336e08503d570dc3c96fa71fd7b2d4f51d0d1acc515c2c4dad23896273b2616f7d848816c5cc17f7bb2ed5f952a708c38eb13f0fdd8f14ec71893d3f19ccc8aa15fed848583093f4d8246f36db300c9cdd5d63b507641e4d2ba9ec284d17f4a05696be00da371bd5bab0ba56a13451278dbdeed02b4093e482b96269707b09a10dba9ca1dffb4e4185f9e392ebabcea1f9d26c037d9c0dcd71f2bc13faf559d195ef0d8be7baa0cf8aa105a8069356aa2287ec1003e0759f27b02772d4855bf0b7e0b3e97b7f77bd3119669695cc706090991e6a542eb7a28547978ba8a05d9103a90789108fa15898d14982bd0d5d098019700b37939f9f0d66e78af49c6318b886ba2a9101c072bb1e7bdbdb7319eb38e3320b4fbb1fcb28e8e7fac0bd493faf3c0547214465d55ca212dae99dc53addc3378b7e7b93acbdc1c9787149ed0214cd9846852ed7c18b23ab0ae8b7afbb725d44997a38422ff17b1dbeb22e2387094e0bc59496786114d0bb398f36fdfb06c70ff0ce47e9c3eb8c32c22062ccd5306026b606a9e9c628a377f0efd71087c95b3c1ffbdec8a91f311fbba4793d3d3fbf350e6a4a491d74ab7fb0cb66afa0d66177853df464f0175cdee4f97a4e620366aa18c2d04ebab82ff31ec07722fd53e0b4926973dd41d10422747261d8772b18ab55e0bbdcb89e224a5fc2679c2b729aaa4e1a78f95cf68af3562b98f0586d02134464f87dd15b843451a9160c5f4704c994a32259ca623c937431cfe55ee97d736916ac3e7ef831a1b6978539ac6c3304de2f43b3b208f73d071d17fd5cae631f617929468fc59d529deab1c0080a85ded180f9b7029376059c5ca3ba2eab9ee556a74373eadb5983f5990146b04bd255f2380450864e33c478cfe42072eeaabf8032f2c22fbf111407bf2cc41f6cc55596cca62a69303089c3ec231f42a8358d8bce315debce8cd4cea4aa478fba3476b5252e2d64da6b70d6ea0c1b4a99abebbd194e62e25442e7ecffc788710ac6dd1da815a0a5ef8dcef34ebcc90c6f29372875d664c2a06f3485dad8bd00d94837f417412399f9f085d8666fcaf38d38620897e2d06c7399eb28c5db0ffa42a233fdc6732c3e525bd49771aad03348aad068a5e729565c10d343a10c5cd6e530994c9a400354de3af3b39d20cf2b54fc0c9bde4b6f520688694d9b53c3628f1744b61271383d852219d8afae7a284d2f0b042f2fb70778b53d30876b5a031904a241e5a1741ff2e88bf8faa60472ba66111f0560ef127ee86d24f2c502fcff7575696f7f450109776f0f5e1bbd77a48952697fb2f8657516cc061de2bcc6aff9b861356b97e576f78abab1d3acb70d14a84b7858c22b2a0ee17e1231a2da3738018205091b263c42597ea6d3d7d9dae3fa030a13162243494c4e7f8fa1a2d8e2ff11153a41474a63646770717487919da9bac9d2d3e3070f23455d686a7dbcc9de00000000000000000000000000000000000f1f343f", }; -// This is almost tgId=1, tcId=1 from +// This is almost tgId=1, tcId=1 from // https://raw.githubusercontent.com/bcgit/bc-test-data/refs/heads/main/pqc/crypto/mldsa/ML-DSA-sigGen.txt -// with the difference being that an empty ctx is added, instead of testing sign_internal directly, +// with the difference being that an empty ctx is added, instead of testing sign_internal directly, // and the signature value is run against bc-java const MLDSA65_KAT1: Kat = Kat { _parameter_set: "ML-DSA-65", @@ -1110,9 +1110,9 @@ const MLDSA65_KAT1: Kat = Kat { }; // generated by hand against bc-java -// This is almost tgId=1, tcId=1 from +// This is almost tgId=1, tcId=1 from // https://raw.githubusercontent.com/bcgit/bc-test-data/refs/heads/main/pqc/crypto/mldsa/ML-DSA-sigGen.txt -// with the difference being that an empty ctx is added, instead of testing sign_internal directly, +// with the difference being that an empty ctx is added, instead of testing sign_internal directly, // and the signature value is run against bc-java const MLDSA87_KAT1: Kat = Kat { _parameter_set: "ML-DSA-87", diff --git a/crypto/mlkem-lowmemory/src/aux_functions.rs b/crypto/mlkem-lowmemory/src/aux_functions.rs index 7df893d..b548f44 100644 --- a/crypto/mlkem-lowmemory/src/aux_functions.rs +++ b/crypto/mlkem-lowmemory/src/aux_functions.rs @@ -62,7 +62,7 @@ pub(crate) fn byte_decode(B: &[u8; PACK_L for j in 0..d { // select the next bit, according to bitcount, then shift it up by j // there is supposed to be a `mod m` here, but that shouldn't matter as they are being checked below - F[i] |= (((B[(i * d + j) / 8] >> (i * d + j) % 8) & 1) as i16) << j; + F[i] |= (((B[(i * d + j) / 8] >> (i * d + j) % 8) & 1) as i16) << j; } // assert the mod m // These are relaxed because they are being checked above in MLKEMPublicKey::pk_decode() diff --git a/crypto/mlkem-lowmemory/src/lib.rs b/crypto/mlkem-lowmemory/src/lib.rs index 207da32..ae93345 100644 --- a/crypto/mlkem-lowmemory/src/lib.rs +++ b/crypto/mlkem-lowmemory/src/lib.rs @@ -208,16 +208,16 @@ //! There are, however, a few exceptions worth mentioning. //! //! If using a [`MLKEM::keygen_from_seed`], then it is your responsibility to ensure that the seed is -//! cryptographically random and unpredictable at a security strength that matches the MLKEM parameter set. +//! cryptographically random and unpredictable at a security strength that matches the MLKEM parameter set. //! //! Also, [`MLKEM::encaps_internal`] requires the encapsulation randomness to be provided, so the ciphertext //! will only be as strong as the randomness that you provide. -//! +//! //! A note about cryptographic side-channel attacks: considerable effort has been expended to attempt //! to make this implementation constant-time, which generally means that the core mathematical algorithm //! code that handles secret data uses bitshift-and-xor type constructions instead of if-and-loop //! constructions. That should give this implementation reasonably good resistance to timing and -//! power analysis key extraction attacks, however: +//! power analysis key extraction attacks, however: //! A) this is a "best-effort" and not formally verified, and //! B) the Rust compiler does not guarantee constant-time behaviour no matter how good the design is code, //! so like all Safe Rust code (ie Rust code that does not include inline assembly), @@ -227,7 +227,7 @@ #![no_std] #![forbid(missing_docs)] #![forbid(unsafe_code)] -// These are because variable names need to be matched exactly against FIPS 204, +// These are because variable names need to be matched exactly against FIPS 204, // for example both 'K' and 'k', or 'A' and 'a' are used and have specific meanings. // linter needs to be instructed to ignore these cases #![allow(non_snake_case)] diff --git a/crypto/mlkem/src/aux_functions.rs b/crypto/mlkem/src/aux_functions.rs index 88966d7..0999837 100644 --- a/crypto/mlkem/src/aux_functions.rs +++ b/crypto/mlkem/src/aux_functions.rs @@ -301,9 +301,8 @@ pub(crate) fn barrett_reduce(a: i16) -> i16 { a - (((t as i32) * q as i32) as i16) } - -// Not currently used. It is left here as a reference since it's useful for debugging if it's -// necessary to output values that are normalized to [0,q] to compare against intermediate results +// Not currently used. It is left here as a reference since it's useful for debugging if it's +// necessary to output values that are normalized to [0,q] to compare against intermediate results // from other libraries. // pub(super) fn cond_sub_q(a: i16) -> i16 { // let tmp = a - q; diff --git a/crypto/mlkem/src/polynomial.rs b/crypto/mlkem/src/polynomial.rs index 6617e46..4002528 100644 --- a/crypto/mlkem/src/polynomial.rs +++ b/crypto/mlkem/src/polynomial.rs @@ -9,7 +9,7 @@ use crate::mlkem::{N, q}; /// A polynomial over the ML-KEM ring. /// -/// Dev note: The following structure does not necessarily need to be declared as public. +/// Dev note: The following structure does not necessarily need to be declared as public. /// There is no real scenario where this function needs to be called directly. /// However, in order to test the Debug and Display traits, it is necessary to use STD, so those /// can't be tested from inline tests in this file and the real unit tests are in a different crate. @@ -46,8 +46,8 @@ impl Polynomial { Self { coeffs: [0i16; N] } } - /// Encodes a 32-byte message `m` into a `Polynomial`, implementing the message - /// encoding step of K-PKE.Encrypt `Decompress_1(ByteDecode_1(m))`, + /// Encodes a 32-byte message `m` into a `Polynomial`, implementing the message + /// encoding step of K-PKE.Encrypt `Decompress_1(ByteDecode_1(m))`, /// (FIPS 203, Alg. 14). Each message bit becomes one coefficient: `Decompress_1` /// (ยง4.2.1) maps bit `1` to `โŒˆq/2โŒ‰ = (q + 1) / 2 = 1665` (for `q = 3329`) and bit /// `0` to `0`, placing a set bit at the point farthest from `0` to maximize the @@ -67,25 +67,25 @@ impl Polynomial { w } - /// Decodes a `Polynomial` into its 32-byte message `m`, implementing the message + /// Decodes a `Polynomial` into its 32-byte message `m`, implementing the message /// recovery step of K-PKE.Decrypt `ByteEncode_1(Compress_1(self))`, /// (FIPS 203, Alg. 15). Each coefficient yields one message bit: `Compress_1` /// (ยง4.2.1) sets the bit when the coefficient lies nearer `q/2` than `0`, i.e. in /// the central interval `[833, 2496]` for `q = 3329`. The decision is computed - /// branchlessly and the bits are packed LSB-first. - /// Coefficients are expected to already be canonical in `[0, q]`: the unsigned - /// interval test is not periodic mod `q`, so the caller reduces beforehand (`poly_reduce()` + /// branchlessly and the bits are packed LSB-first. + /// Coefficients are expected to already be canonical in `[0, q]`: the unsigned + /// interval test is not periodic mod `q`, so the caller reduces beforehand (`poly_reduce()` /// in `pke_decrypt`) and no reduction is repeated here. pub(crate) fn to_msg(self) -> [u8; 32] { - const LOWER: i32 = q as i32 >> 2; // โŒŠq/4โŒ‹ = 832 - const UPPER: i32 = q as i32 - LOWER; // q - โŒŠq/2โŒ‹ = 2497 + const LOWER: i32 = q as i32 >> 2; // โŒŠq/4โŒ‹ = 832 + const UPPER: i32 = q as i32 - LOWER; // q - โŒŠq/2โŒ‹ = 2497 let mut msg = [0u8; 32]; // Using full reduce() might be expected here. - // However, this function is only called by pke_decrypt (see mlkem.rs), which performs a + // However, this function is only called by pke_decrypt (see mlkem.rs), which performs a // reduction on every coefficient of the polynomial immediately prior to the call. - // For completeness, testing against the bc-test-data set of KATs shows that everything passes + // For completeness, testing against the bc-test-data set of KATs shows that everything passes // without modular reduction. // self.cond_sub_q(); @@ -101,8 +101,8 @@ impl Polynomial { msg } - // Not currently used. It is left here as a reference since it's useful for debugging if it's - // necessary to output values that are normalized to [0,q] to compare against intermediate results + // Not currently used. It is left here as a reference since it's useful for debugging if it's + // necessary to output values that are normalized to [0,q] to compare against intermediate results // from other libraries. // pub(crate) fn conditional_add_q(&mut self) { // for x in self.0.iter_mut() { @@ -157,7 +157,7 @@ impl Polynomial { // bc-java has a cond_sub_q() here, however, it is not needed // The reason for this is because a modular reduction is performed immediately // prior to calling pack_ciphertext in mlkem.rs - // This can be corroborated by running the corresponding unit tests + // This can be corroborated by running the corresponding unit tests // let mut s = self.clone(); // s.cond_sub_q(); @@ -250,8 +250,8 @@ impl Polynomial { v } - // Not currently used. It is left here as a reference since it's useful for debugging if it's - // necessary to output values that are normalized to [0,q] to compare against intermediate results + // Not currently used. It is left here as a reference since it's useful for debugging if it's + // necessary to output values that are normalized to [0,q] to compare against intermediate results // from other libraries. // pub(crate) fn cond_sub_q(&mut self) { // for i in 0..N { @@ -357,8 +357,8 @@ pub fn base_mult_montgomery(a: &Polynomial, b: &Polynomial) -> Polynomial { r } -// Not currently used. It is left here as a reference since it's useful for debugging if it's -// necessary to output values that are normalized to [0,q] to compare against intermediate results +// Not currently used. It is left here as a reference since it's useful for debugging if it's +// necessary to output values that are normalized to [0,q] to compare against intermediate results // from other libraries. // /// if a is in \[-q..0], then it shifts it up by q to be in \[0..q] // pub(crate) fn conditional_add_q(a: i16) -> i16 { diff --git a/crypto/mlkem/tests/mlkem_key_tests.rs b/crypto/mlkem/tests/mlkem_key_tests.rs index b3e1e4f..24930d2 100644 --- a/crypto/mlkem/tests/mlkem_key_tests.rs +++ b/crypto/mlkem/tests/mlkem_key_tests.rs @@ -61,7 +61,6 @@ mod mlkem_key_tests { // 2) whether it calculates H(ek) properly from a private key // 3) whether it rejects a private key if the H(ek) is wrong - let seed = KeyMaterial512::from_bytes_as_type( &hex::decode( "000102030405060708090a0b0c0d0e0f diff --git a/crypto/rng/src/hash_drbg80090a.rs b/crypto/rng/src/hash_drbg80090a.rs index 5dc1013..be70cb8 100644 --- a/crypto/rng/src/hash_drbg80090a.rs +++ b/crypto/rng/src/hash_drbg80090a.rs @@ -216,7 +216,7 @@ impl Sp80090ADrbg for HashDRBG80090A { "Provided seed exceeds the maximum seed length.", ))?; } - // On purpose not checking the SecurityStrength field of the seed, + // On purpose not checking the SecurityStrength field of the seed, // because we assume it's pure entropy and hasn't been touched by any actual algoritms yet. if security_strength > H::MAX_SECURITY_STRENGTH { return Err(KeyMaterialError::SecurityStrength( @@ -527,7 +527,7 @@ impl RNG for HashDRBG80090A { /// the hash_df function as defined in SP 800-90Ar1 section 10.3.1. /// no_of_bits_to_return is the length of the provided output buffer. -/// Because array concatenation is not available in a no_std / no_alloc build, this takes many input parameters. +/// Because array concatenation is not available in a no_std / no_alloc build, this takes many input parameters. // To leave a parameter unused, simply provide an empty array &[0u8;0] fn hash_df( in1: &[u8], @@ -550,7 +550,7 @@ fn hash_df( let len = u32::div_ceil(out.len() as u32, H::OUTPUT_LEN as u32); let mut counter: u8 = 0x01; - // note: this could probably be performance optimized a tiny bit by pulling no_of_bits_to_return.to_le_bytes() + // note: this could probably be performance optimized a tiny bit by pulling no_of_bits_to_return.to_le_bytes() // out of the loop and by merging i and counter into the same variable. for i in 1..len { let mut h = H::default(); @@ -566,7 +566,7 @@ fn hash_df( } // Handle the last block separately since not all of it will fit in the output buffer. - // TODO: Check whether it is necessary to do a last block, + // TODO: Check whether it is necessary to do a last block, // or was the requested number of bits already a multiple of the output length let bytes_written = (len - 1) as usize * H::OUTPUT_LEN; let remainder = out.len() - bytes_written; @@ -673,7 +673,7 @@ fn hashgen(v: &[u8], out: &mut [u8]) { } // Handle the last block separately since not all of it will fit in the output buffer. - // TODO: Check whether it is necessary to do a last block, + // TODO: Check whether it is necessary to do a last block, // or was the requested number of bits already a multiple of the output length let bytes_written = (m - 1) as usize * H::OUTPUT_LEN; let remainder = out.len() - bytes_written; diff --git a/crypto/rng/src/lib.rs b/crypto/rng/src/lib.rs index 1840f4c..3040358 100644 --- a/crypto/rng/src/lib.rs +++ b/crypto/rng/src/lib.rs @@ -66,13 +66,13 @@ pub type Default128BitRNG = HashDRBG_SHA256; /// The library's default RNG at the 256-bit security level. pub type Default256BitRNG = HashDRBG_SHA512; -/// Implements the five functions specified in SP 800-90A section 7.4 are -/// - instantate, -/// - generate, -/// - reseed, -/// - uninstantiate, and +/// Implements the five functions specified in SP 800-90A section 7.4 are +/// - instantate, +/// - generate, +/// - reseed, +/// - uninstantiate, and /// - health_test. -/// Note: this function implements Rust's Drop on the sensitive working state in place of the explicit +/// Note: this function implements Rust's Drop on the sensitive working state in place of the explicit /// Uninstantiate function listed in SP 800-90Ar1. pub trait Sp80090ADrbg { /// The input KeyMaterial must be of type [`KeyType::Seed`]. @@ -91,7 +91,7 @@ pub trait Sp80090ADrbg { /// required. /// """ /// - /// This function takes ownership of the seed KeyMaterial object, + /// This function takes ownership of the seed KeyMaterial object, /// to reduce the likelihood of its reuse in a second function call. /// /// There is no entropy requirement on the nonce, but it is expected as a KeyMaterial so that it @@ -106,7 +106,7 @@ pub trait Sp80090ADrbg { ) -> Result<(), RNGError>; /// Reseeds the DRBG with the provided seed. - /// TODO: this needs to be redesigned to take some sort of EntropySource object that will work well + /// TODO: this needs to be redesigned to take some sort of EntropySource object that will work well // with DRBGs that require frequent reseeding. fn reseed( &mut self, diff --git a/crypto/rng/tests/hash_drbg80090a_tests.rs b/crypto/rng/tests/hash_drbg80090a_tests.rs index 840dacf..4a8967b 100644 --- a/crypto/rng/tests/hash_drbg80090a_tests.rs +++ b/crypto/rng/tests/hash_drbg80090a_tests.rs @@ -83,11 +83,11 @@ mod tests { _ => panic!("Expected KeyMaterialError error"), } - // Skipping tests for max lengths of seeds and personalization strings - // because they are on the order of a gigabyte in size. + // Skipping tests for max lengths of seeds and personalization strings + // because they are on the order of a gigabyte in size. // Testing would blow up the test suite. - // Error case: security strength requested at init is higher than the underlying + // Error case: security strength requested at init is higher than the underlying // hash function's max security strength let mut rng = HashDRBG_SHA256::new_unititialized(); let seed = KeyMaterial256::from_bytes_as_type(&DUMMY_SEED[..32], KeyType::Seed).unwrap(); @@ -96,7 +96,7 @@ mod tests { _ => panic!("Expected KeyMaterialError error"), } - // Success case: security strength requested at init is lower than the underlying + // Success case: security strength requested at init is lower than the underlying // hash function's max security strength // ... 112 bit let mut rng = HashDRBG_SHA256::new_unititialized(); @@ -156,10 +156,9 @@ mod tests { _ => panic!("Expected KeyMaterialError error"), } - // Skipping tests for max lengths of seeds and personalization strings - // because they are on the order of a gigabyte in size. + // Skipping tests for max lengths of seeds and personalization strings + // because they are on the order of a gigabyte in size. // Testing would blow up the test suite. - } #[test] @@ -194,8 +193,8 @@ mod tests { _ => panic!("Expected Uninitialized error"), } - // Skipping tests for max lengths of seeds and personalization strings - // because they are on the order of a gigabyte in size. + // Skipping tests for max lengths of seeds and personalization strings + // because they are on the order of a gigabyte in size. // Testing would blow up the test suite. // TODO: Tests for ReseedRequired. Investigate how this gets triggered. The limits are in the exobyte range. @@ -240,8 +239,8 @@ mod tests { _ => panic!("Expected Uninitialized error"), } - // Skipping tests for max lengths of seeds and personalization strings - // because they are on the order of a gigabyte in size. + // Skipping tests for max lengths of seeds and personalization strings + // because they are on the order of a gigabyte in size. // Testing would blow up the test suite. // TODO: tests for ReseedRequired. Investigate how this gets triggered. The limits are in the exobyte range. @@ -291,8 +290,8 @@ mod tests { Ok(_) => panic!("Expected Uninitialized error"), } - // Skipping tests for max lengths of seeds and personalization strings - // because they are on the order of a gigabyte in size. + // Skipping tests for max lengths of seeds and personalization strings + // because they are on the order of a gigabyte in size. // Testing would blow up the test suite. // TODO: tests for ReseedRequired. Investigate how this gets triggered. The limits are in the exobyte range. diff --git a/crypto/sha2/src/sha256.rs b/crypto/sha2/src/sha256.rs index 7cee95c..34d0977 100644 --- a/crypto/sha2/src/sha256.rs +++ b/crypto/sha2/src/sha256.rs @@ -150,7 +150,7 @@ pub struct SHA256Internal { byte_count: u64, x_buf: Secret<[u8; 64]>, x_buf_off: usize, - // TODO: Investigate whether maximum message size (according to FIPS 180-4) should be added + // TODO: Investigate whether maximum message size (according to FIPS 180-4) should be added // (2^64 for SHA256 and 2^128 for SHA512) } diff --git a/crypto/sha2/src/sha512.rs b/crypto/sha2/src/sha512.rs index 60207eb..c31e306 100644 --- a/crypto/sha2/src/sha512.rs +++ b/crypto/sha2/src/sha512.rs @@ -161,7 +161,7 @@ pub struct SHA512Internal { _params: std::marker::PhantomData, state: Sha512State, // NOTE The code currently only supports 2^67 bits, not the full 2^128 - byte_count: u64, + byte_count: u64, x_buf: Secret<[u8; 128]>, x_buf_off: usize, } diff --git a/crypto/sha3/src/sha3.rs b/crypto/sha3/src/sha3.rs index 0393507..3da20b0 100644 --- a/crypto/sha3/src/sha3.rs +++ b/crypto/sha3/src/sha3.rs @@ -169,14 +169,14 @@ impl Hash for SHA3Internal { output } - // TODO: investigate why this doesn't take a &mut [u8; HASH_LEN] + // TODO: investigate why this doesn't take a &mut [u8; HASH_LEN] // Being able to do so would improve ergonomics fn do_final_out(mut self, output: &mut [u8]) -> usize { output.fill(0); - // this shouldn't fail because, by construction, the function is only called once, + // this shouldn't fail because, by construction, the function is only called once, // and this is the only way to absorb partial bits. - self.keccak.absorb_bits(0x02, 2).expect("do_final_out: keccak.absorb_bits failed."); + self.keccak.absorb_bits(0x02, 2).expect("do_final_out: keccak.absorb_bits failed."); let bytes_written = if output.len() <= self.output_len() { self.keccak.squeeze(output) @@ -210,7 +210,7 @@ impl Hash for SHA3Internal { ) -> Result { output.fill(0); - // Mutants note: This is just bit-setting into empty space. + // Mutants note: This is just bit-setting into empty space. // It works the same regardless of whether it's OR or XOR. let mut final_input: u16 = ((partial_byte as u16) & ((1 << num_partial_bits) - 1)) | (0x02 << num_partial_bits); diff --git a/crypto/sha3/src/shake.rs b/crypto/sha3/src/shake.rs index 1bd91dc..6ba2a88 100644 --- a/crypto/sha3/src/shake.rs +++ b/crypto/sha3/src/shake.rs @@ -92,7 +92,7 @@ impl SHAKEInternal { mut self, additional_input: &[u8], ) -> Result, KDFError> { - // At the moment, oversized KeyMaterial is returned for most cases. + // At the moment, oversized KeyMaterial is returned for most cases. let mut output_key = KeyMaterial::<64>::new(); self.derive_key_out_final_internal(additional_input, &mut output_key)?; @@ -307,7 +307,7 @@ impl XOF for SHAKEInternal { if !(1..=7).contains(&num_partial_bits) { return Err(HashError::InvalidLength("must be in the range [0,7]")); } - // Mutants note: This is just bit-setting into empty space. + // Mutants note: This is just bit-setting into empty space. // It works the same regardless of whether it's OR or XOR. let mut final_input: u16 = ((partial_byte as u16) & ((1 << num_partial_bits) - 1)) | (0x0F << num_partial_bits); diff --git a/crypto/utils/src/ct.rs b/crypto/utils/src/ct.rs index 464511a..22ae3bc 100644 --- a/crypto/utils/src/ct.rs +++ b/crypto/utils/src/ct.rs @@ -14,16 +14,14 @@ struct MaskType(core::marker::PhantomData); trait SupportedMaskType: sealed::Sealed {} -macro_rules! supported_mask_type { - ($($t:ty),+) => { - $( - impl sealed::Sealed for MaskType<$t> {} - impl SupportedMaskType for MaskType<$t> {} - )+ - }; -} - -supported_mask_type!(i64, u64); +impl sealed::Sealed for MaskType {} +impl SupportedMaskType for MaskType {} +impl sealed::Sealed for MaskType {} +impl SupportedMaskType for MaskType {} +impl sealed::Sealed for MaskType {} +impl SupportedMaskType for MaskType {} +impl sealed::Sealed for MaskType {} +impl SupportedMaskType for MaskType {} /// Helper functions for checking some condition on some data using constant-time operations. #[derive(Clone, Copy)] @@ -35,65 +33,90 @@ where impl Condition where MaskType: SupportedMaskType {} +// Each signed width is written out by hand rather than macro-generated: `cargo mutants` +// cannot see into macro bodies, and these mask identities are the ones most worth +// mutating. The signed widths must be edited together: `Condition` and `Condition` +// are copies of each other modulo the width token. impl Condition { - // TODO: there are a bunch of impls in here that seem to be generic and not related to i64, - // could those be moved to a generic impl for Condition ? - /// TRUE is the bit vector of all 1's pub const TRUE: Self = Self(-1); /// FALSE is the bit vector of all 0's pub const FALSE: Self = Self(0); + + /// Constant-time mask generation from a compile-time boolean. /// + /// Signed types rely on two's complement negation: `-(true as i64)` is `-1` + /// (all 1s) and `-(false as i64)` is `0` (all 0s). pub const fn from_bool() -> Self { Self(-(VALUE as i64)) } - /// + /// Constant-time mask generation from a runtime boolean. pub const fn from_bool_var(value: bool) -> Self { Self(-(value as i64)) } - /// - pub const fn is_bit_set(value: i64, bit: i64) -> Self { - Self(-((value >> bit) & 1)) + /// Mask from the least-significant bit: TRUE iff bit 0 of `value` is set. + /// This is the parity test: `from_lsb(x)` is TRUE iff `x` is odd. It is + /// the `bit = 0` special case of [`Self::is_bit_set`]. + pub const fn from_lsb(value: i64) -> Self { + Self(-(value & 1)) } - /// + /// TRUE iff bit `bit` of `value` is set. The bit index must be public data + /// (the shift amount is timing-visible on some targets). + pub const fn is_bit_set(value: i64, bit: u32) -> Self { + Self::from_lsb(value >> bit) + } + /// TRUE iff `value < 0`, i.e. the sign (top) bit is set. The unsigned + /// counterpart of this mask is `from_msb`, where the top bit carries a + /// borrow/carry instead of a sign. pub const fn is_negative(value: i64) -> Self { - Self(value >> 63) + // Arithmetic shift replicates the sign bit across the whole word. + Self(value >> (i64::BITS - 1)) } + /// TRUE iff `value != 0`. /// + /// For any nonzero `x`, `x | x.wrapping_neg()` is negative (either `x` or its + /// two's complement has the top bit set); for zero both sides are zero. + /// `wrapping_neg` is required: plain negation overflows at `MIN`. pub const fn is_not_zero(value: i64) -> Self { - Self::is_negative(-Self::or_halves(value)) + Self::is_negative(value | value.wrapping_neg()) } - /// + /// TRUE iff `value == 0`. pub const fn is_zero(value: i64) -> Self { - Self::is_negative(Self::or_halves(value) - 1) + // Complementing the inner value maps TRUE <-> FALSE (all 1s <-> all 0s). + Self(!Self::is_not_zero(value).0) } - /// + /// TRUE iff `x == y`. pub const fn is_equal(x: i64, y: i64) -> Self { Self::is_zero(x ^ y) } + /// TRUE iff `x < y`, for the full signed range. /// + /// The naive `is_negative(x - y)` is wrong whenever `x - y` overflows + /// (e.g. `MIN < 1`): in debug it panics, in release it wraps to the opposite + /// answer. This is the standard overflow-free signed-comparison identity: when + /// the signs of `x` and `y` differ the answer is the sign of `x`; when they + /// agree the difference cannot overflow, so the answer is the sign of `x - y`. pub const fn is_lt(x: i64, y: i64) -> Self { - Self::is_negative(x - y) + Self(((x & !y) | (!(x ^ y) & x.wrapping_sub(y))) >> (i64::BITS - 1)) } - /// - // Note: this cannot currently be marked as const, since it either needs a (non-const) not (!) or a boolean OR is_zero. - pub fn is_lte(x: i64, y: i64) -> Self { - !Self::is_gt(x, y) + /// TRUE iff `x <= y`. + pub const fn is_lte(x: i64, y: i64) -> Self { + // Complementing the inner value maps TRUE <-> FALSE (all 1s <-> all 0s). + Self(!Self::is_gt(x, y).0) } - /// + /// TRUE iff `x > y`. pub const fn is_gt(x: i64, y: i64) -> Self { Self::is_lt(y, x) } - /// - // Note: this cannot currently be marked as const, since it either needs a (non-const) not (!) or a boolean OR is_zero. - pub fn is_gte(x: i64, y: i64) -> Self { - !Self::is_lt(x, y) + /// TRUE iff `x >= y`. + pub const fn is_gte(x: i64, y: i64) -> Self { + Self(!Self::is_lt(x, y).0) } - /// - pub fn is_within_range(value: i64, min: i64, max: i64) -> Self { - Self::is_gte(value, min) & Self::is_lte(value, max) + /// TRUE iff `min <= value <= max`. + pub const fn is_within_range(value: i64, min: i64, max: i64) -> Self { + Self(Self::is_gte(value, min).0 & Self::is_lte(value, max).0) } - /// + /// TRUE iff `value` occurs in `list`. The list contents and length are public. pub fn is_in_list(value: i64, list: &[i64]) -> Self { // Research question: is this actually constant-time? // A clever compiler might turn this into a short-circuiting loop. @@ -103,13 +126,14 @@ impl Condition { let mut c = Self::FALSE; for i in 0..list.len() { let diff = value ^ list[i]; - c |= Condition::::is_zero(diff); + c |= Self::is_zero(diff); } c } - /// Conditionally move the source value to the destination if the condition is true, otherwise nothing is moved. + /// Conditionally move the source value to the destination if the condition is + /// true, otherwise nothing is moved. pub fn mov(self, src: i64, dst: &mut i64) { *dst = self.select(src, *dst); } @@ -132,25 +156,165 @@ impl Condition { /// /// As a result, `1`, which is the negation of `-1`, should be returned, but `-3` is output. /// - /// Therefore, if the [`Self::TRUE`] constant value of the i64 [`Condition`] implementation is changed to `-1`, + /// Therefore, if the [`Self::TRUE`] constant value of the [`Condition`] implementation is changed to `-1`, /// the test also runs normally. pub const fn negate(self, value: i64) -> i64 { (value ^ self.0).wrapping_sub(self.0) } - /// - pub const fn or_halves(value: i64) -> i64 { - (value | (value >> 32)) & 0xFFFFFFFF - } - /// Conditional selection: return `true_value` if the condition is true, otherwise return `false_value`. + /// Conditional selection: return `true_value` if the condition is true, otherwise + /// return `false_value`. pub const fn select(self, true_value: i64, false_value: i64) -> i64 { (true_value & self.0) | (false_value & !self.0) } - /// Conditional swap: returns (lhs, rhs) if the condition is true, otherwise returns (rhs, lhs). + /// Conditional swap: returns (lhs, rhs) if the condition is true, otherwise + /// returns (rhs, lhs). pub const fn swap(self, lhs: i64, rhs: i64) -> (i64, i64) { (self.select(rhs, lhs), self.select(lhs, rhs)) } + /// Convert the mask to a runtime boolean. Only use this at genuine public + /// decision points: branching on the result leaks the condition's value. + pub const fn to_bool(self) -> bool { + self.0 != 0 + } +} + +impl Condition { + /// TRUE is the bit vector of all 1's + pub const TRUE: Self = Self(-1); + /// FALSE is the bit vector of all 0's + pub const FALSE: Self = Self(0); + + /// Constant-time mask generation from a compile-time boolean. + /// + /// Signed types rely on two's complement negation: `-(true as i32)` is `-1` + /// (all 1s) and `-(false as i32)` is `0` (all 0s). + pub const fn from_bool() -> Self { + Self(-(VALUE as i32)) + } + /// Constant-time mask generation from a runtime boolean. + pub const fn from_bool_var(value: bool) -> Self { + Self(-(value as i32)) + } + /// Mask from the least-significant bit: TRUE iff bit 0 of `value` is set. + /// This is the parity test: `from_lsb(x)` is TRUE iff `x` is odd. It is + /// the `bit = 0` special case of [`Self::is_bit_set`]. + pub const fn from_lsb(value: i32) -> Self { + Self(-(value & 1)) + } + /// TRUE iff bit `bit` of `value` is set. The bit index must be public data + /// (the shift amount is timing-visible on some targets). + pub const fn is_bit_set(value: i32, bit: u32) -> Self { + Self::from_lsb(value >> bit) + } + /// TRUE iff `value < 0`, i.e. the sign (top) bit is set. The unsigned + /// counterpart of this mask is `from_msb`, where the top bit carries a + /// borrow/carry instead of a sign. + pub const fn is_negative(value: i32) -> Self { + // Arithmetic shift replicates the sign bit across the whole word. + Self(value >> (i32::BITS - 1)) + } + /// TRUE iff `value != 0`. + /// + /// For any nonzero `x`, `x | x.wrapping_neg()` is negative (either `x` or its + /// two's complement has the top bit set); for zero both sides are zero. + /// `wrapping_neg` is required: plain negation overflows at `MIN`. + pub const fn is_not_zero(value: i32) -> Self { + Self::is_negative(value | value.wrapping_neg()) + } + /// TRUE iff `value == 0`. + pub const fn is_zero(value: i32) -> Self { + // Complementing the inner value maps TRUE <-> FALSE (all 1s <-> all 0s). + Self(!Self::is_not_zero(value).0) + } + /// TRUE iff `x == y`. + pub const fn is_equal(x: i32, y: i32) -> Self { + Self::is_zero(x ^ y) + } + /// TRUE iff `x < y`, for the full signed range. + /// + /// The naive `is_negative(x - y)` is wrong whenever `x - y` overflows + /// (e.g. `MIN < 1`): in debug it panics, in release it wraps to the opposite + /// answer. This is the standard overflow-free signed-comparison identity: when + /// the signs of `x` and `y` differ the answer is the sign of `x`; when they + /// agree the difference cannot overflow, so the answer is the sign of `x - y`. + pub const fn is_lt(x: i32, y: i32) -> Self { + Self(((x & !y) | (!(x ^ y) & x.wrapping_sub(y))) >> (i32::BITS - 1)) + } + /// TRUE iff `x <= y`. + pub const fn is_lte(x: i32, y: i32) -> Self { + // Complementing the inner value maps TRUE <-> FALSE (all 1s <-> all 0s). + Self(!Self::is_gt(x, y).0) + } + /// TRUE iff `x > y`. + pub const fn is_gt(x: i32, y: i32) -> Self { + Self::is_lt(y, x) + } + /// TRUE iff `x >= y`. + pub const fn is_gte(x: i32, y: i32) -> Self { + Self(!Self::is_lt(x, y).0) + } + /// TRUE iff `min <= value <= max`. + pub const fn is_within_range(value: i32, min: i32, max: i32) -> Self { + Self(Self::is_gte(value, min).0 & Self::is_lte(value, max).0) + } + /// TRUE iff `value` occurs in `list`. The list contents and length are public. + pub fn is_in_list(value: i32, list: &[i32]) -> Self { + // Research question: is this actually constant-time? + // A clever compiler might turn this into a short-circuiting loop. + // A quick google search shows that rust doesn't have the ability to annotate specific code blocks + // as no-optimize; the only option is to insert direct assembly. + + let mut c = Self::FALSE; + for i in 0..list.len() { + let diff = value ^ list[i]; + c |= Self::is_zero(diff); + } + + c + } + + /// Conditionally move the source value to the destination if the condition is + /// true, otherwise nothing is moved. + pub fn mov(self, src: i32, dst: &mut i32) { + *dst = self.select(src, *dst); + } + + /// Conditionally negate the value. + /// + /// negate(-1) gives -3 /// - pub const fn to_bool_var(self) -> bool { + /// `value` is `-1` (i.e., all bits are `1`, `...1111`) + /// + /// Condition `self.0` is 1 (`...0001`) (assuming `TRUE`) + /// + /// XOR operation was executed as `value ^ self.0` + /// + /// Then `...1111 XOR ...0001 = ...1110` (i.e., `-2`) + /// + /// Subtraction operation is `wrapping_sub(self.0)` + /// + /// Then `-2 - 1 = -3` + /// + /// As a result, `1`, which is the negation of `-1`, should be returned, but `-3` is output. + /// + /// Therefore, if the [`Self::TRUE`] constant value of the [`Condition`] implementation is changed to `-1`, + /// the test also runs normally. + pub const fn negate(self, value: i32) -> i32 { + (value ^ self.0).wrapping_sub(self.0) + } + /// Conditional selection: return `true_value` if the condition is true, otherwise + /// return `false_value`. + pub const fn select(self, true_value: i32, false_value: i32) -> i32 { + (true_value & self.0) | (false_value & !self.0) + } + /// Conditional swap: returns (lhs, rhs) if the condition is true, otherwise + /// returns (rhs, lhs). + pub const fn swap(self, lhs: i32, rhs: i32) -> (i32, i32) { + (self.select(rhs, lhs), self.select(lhs, rhs)) + } + /// Convert the mask to a runtime boolean. Only use this at genuine public + /// decision points: branching on the result leaks the condition's value. + pub const fn to_bool(self) -> bool { self.0 != 0 } } @@ -159,28 +323,165 @@ impl Condition { // then and change Hex and Base64 to use this. // (there's probably no noticeable performance difference u8 and u64 bit ops on a 64-bit machine, // but there would be on a 8, 16, or 32-bit machine.) +// +// Each unsigned width is written out by hand rather than macro-generated: `cargo mutants` +// cannot see into macro bodies, and these mask identities are the ones most worth +// mutating. The unsigned widths must be edited together: `Condition` and `Condition` +// are copies of each other modulo the width token. Ordering comparisons are deliberately +// omitted: multi-word callers derive `lt` from their subtraction borrow chain and convert it +// with `from_msb`. impl Condition { /// TRUE is the bit vector of all 1's pub const TRUE: Self = Self(u64::MAX); /// FALSE is the bit vector of all 0's pub const FALSE: Self = Self(0); - /// this is the core logic for constant-time mask generation for unsigned integers - /// Unlike signed integers where we can rely on Two's Complement via negation `-(v as i64)`, - /// for u64 we must use wrapping subtraction to achieve the all-ones bit pattern (u64::MAX) for true + /// Constant-time mask generation from a compile-time boolean. + /// + /// Unlike signed integers where we can rely on Two's Complement via negation + /// `-(v as i64)`, for unsigned types we must use wrapping subtraction to achieve + /// the all-ones bit pattern for true: + /// true (1) -> `0 - 1` wraps to MAX (all 1s); false (0) -> `0 - 0 = 0` (all 0s). pub const fn from_bool() -> Self { - // If VALUE is true (1) -> 0 - 1 = u64::MAX (All 1s) - // If VALUE is false (0) -> 0 - 0 = 0 (All 0s) Self(0u64.wrapping_sub(VALUE as u64)) } - /// impl the select function manually for u64 - /// although a fully generic `impl` would be the ultimate long-term goal - pub fn select(self, a: u64, b: u64) -> u64 { - let mask = self.0; - (a & mask) | (b & !mask) + /// Constant-time mask generation from a runtime boolean. + pub const fn from_bool_var(value: bool) -> Self { + Self(0u64.wrapping_sub(value as u64)) + } + /// Mask from the least-significant bit: TRUE iff bit 0 of `value` is set. + /// This is the parity test: `from_lsb(x)` is TRUE iff `x` is odd. It is + /// the `bit = 0` special case of [`Self::is_bit_set`]. + pub const fn from_lsb(value: u64) -> Self { + Self(0u64.wrapping_sub(value & 1)) } + /// Mask from the most-significant bit: TRUE iff the top bit of `value` is set. + /// The signed counterpart of this mask is `is_negative`, where the top bit + /// carries a sign instead of a borrow/carry. /// - pub fn is_true(&self) -> bool { + /// This is the borrow/carry adaptor: the borrow word coming out of a wrapping + /// wide subtraction chain carries its meaning entirely in the top bit, so + /// `from_msb(borrow)` is the `lt` mask of that comparison with no further work. + pub const fn from_msb(value: u64) -> Self { + Self(0u64.wrapping_sub(value >> (u64::BITS - 1))) + } + /// TRUE iff bit `bit` of `value` is set. The bit index must be public data + /// (the shift amount is timing-visible on some targets). + pub const fn is_bit_set(value: u64, bit: u32) -> Self { + Self::from_lsb(value >> bit) + } + /// TRUE iff `value != 0`. + /// + /// For any nonzero `x`, `x | x.wrapping_neg()` has the top bit set (either `x` + /// or its two's complement is >= 2^(BITS-1)); for zero both sides are zero. + pub const fn is_not_zero(value: u64) -> Self { + Self::from_msb(value | value.wrapping_neg()) + } + /// TRUE iff `value == 0`. + pub const fn is_zero(value: u64) -> Self { + // Complementing the inner value maps TRUE <-> FALSE (all 1s <-> all 0s). + Self(!Self::is_not_zero(value).0) + } + /// TRUE iff `x == y`. + pub const fn is_equal(x: u64, y: u64) -> Self { + Self::is_zero(x ^ y) + } + /// Conditional selection: return `true_value` if the condition is true, otherwise + /// return `false_value`. + pub const fn select(self, true_value: u64, false_value: u64) -> u64 { + (true_value & self.0) | (false_value & !self.0) + } + /// Conditionally move the source value to the destination if the condition is + /// true, otherwise nothing is moved. + pub fn mov(self, src: u64, dst: &mut u64) { + *dst = self.select(src, *dst); + } + /// Conditional swap: returns (lhs, rhs) if the condition is true, otherwise + /// returns (rhs, lhs). + pub const fn swap(self, lhs: u64, rhs: u64) -> (u64, u64) { + (self.select(rhs, lhs), self.select(lhs, rhs)) + } + /// Convert the mask to a runtime boolean. Only use this at genuine public + /// decision points: branching on the result leaks the condition's value. + pub const fn to_bool(self) -> bool { + self.0 != 0 + } +} + +impl Condition { + /// TRUE is the bit vector of all 1's + pub const TRUE: Self = Self(u32::MAX); + /// FALSE is the bit vector of all 0's + pub const FALSE: Self = Self(0); + + /// Constant-time mask generation from a compile-time boolean. + /// + /// Unlike signed integers where we can rely on Two's Complement via negation + /// `-(v as i64)`, for unsigned types we must use wrapping subtraction to achieve + /// the all-ones bit pattern for true: + /// true (1) -> `0 - 1` wraps to MAX (all 1s); false (0) -> `0 - 0 = 0` (all 0s). + pub const fn from_bool() -> Self { + Self(0u32.wrapping_sub(VALUE as u32)) + } + /// Constant-time mask generation from a runtime boolean. + pub const fn from_bool_var(value: bool) -> Self { + Self(0u32.wrapping_sub(value as u32)) + } + /// Mask from the least-significant bit: TRUE iff bit 0 of `value` is set. + /// This is the parity test: `from_lsb(x)` is TRUE iff `x` is odd. It is + /// the `bit = 0` special case of [`Self::is_bit_set`]. + pub const fn from_lsb(value: u32) -> Self { + Self(0u32.wrapping_sub(value & 1)) + } + /// Mask from the most-significant bit: TRUE iff the top bit of `value` is set. + /// The signed counterpart of this mask is `is_negative`, where the top bit + /// carries a sign instead of a borrow/carry. + /// + /// This is the borrow/carry adaptor: the borrow word coming out of a wrapping + /// wide subtraction chain carries its meaning entirely in the top bit, so + /// `from_msb(borrow)` is the `lt` mask of that comparison with no further work. + pub const fn from_msb(value: u32) -> Self { + Self(0u32.wrapping_sub(value >> (u32::BITS - 1))) + } + /// TRUE iff bit `bit` of `value` is set. The bit index must be public data + /// (the shift amount is timing-visible on some targets). + pub const fn is_bit_set(value: u32, bit: u32) -> Self { + Self::from_lsb(value >> bit) + } + /// TRUE iff `value != 0`. + /// + /// For any nonzero `x`, `x | x.wrapping_neg()` has the top bit set (either `x` + /// or its two's complement is >= 2^(BITS-1)); for zero both sides are zero. + pub const fn is_not_zero(value: u32) -> Self { + Self::from_msb(value | value.wrapping_neg()) + } + /// TRUE iff `value == 0`. + pub const fn is_zero(value: u32) -> Self { + // Complementing the inner value maps TRUE <-> FALSE (all 1s <-> all 0s). + Self(!Self::is_not_zero(value).0) + } + /// TRUE iff `x == y`. + pub const fn is_equal(x: u32, y: u32) -> Self { + Self::is_zero(x ^ y) + } + /// Conditional selection: return `true_value` if the condition is true, otherwise + /// return `false_value`. + pub const fn select(self, true_value: u32, false_value: u32) -> u32 { + (true_value & self.0) | (false_value & !self.0) + } + /// Conditionally move the source value to the destination if the condition is + /// true, otherwise nothing is moved. + pub fn mov(self, src: u32, dst: &mut u32) { + *dst = self.select(src, *dst); + } + /// Conditional swap: returns (lhs, rhs) if the condition is true, otherwise + /// returns (rhs, lhs). + pub const fn swap(self, lhs: u32, rhs: u32) -> (u32, u32) { + (self.select(rhs, lhs), self.select(lhs, rhs)) + } + /// Convert the mask to a runtime boolean. Only use this at genuine public + /// decision points: branching on the result leaks the condition's value. + pub const fn to_bool(self) -> bool { self.0 != 0 } } diff --git a/crypto/utils/tests/ct_tests.rs b/crypto/utils/tests/ct_tests.rs index 8e9be76..b0106b3 100644 --- a/crypto/utils/tests/ct_tests.rs +++ b/crypto/utils/tests/ct_tests.rs @@ -4,368 +4,829 @@ use bouncycastle_utils::ct::Condition; /// (and example usages) #[cfg(test)] -mod i64_tests { +mod generic_impl_tests { use super::*; #[test] - fn const_tests() { - assert_eq!(Condition::::TRUE.to_bool_var(), true); - assert_eq!(Condition::::FALSE.to_bool_var(), false); + fn test_bit_and() { + let ct1 = Condition::::from_bool::(); + let ct2 = Condition::::from_bool::(); + let cf1 = Condition::::from_bool::(); + let cf2 = Condition::::from_bool::(); + assert_eq!((ct1 & ct2).to_bool(), true); + assert_eq!((ct1 & cf1).to_bool(), false); + assert_eq!((cf1 & cf2).to_bool(), false); } #[test] - fn from_bool() { - assert_eq!(Condition::::from_bool::().to_bool_var(), true); - assert_eq!(Condition::::from_bool::().to_bool_var(), false); + fn test_bit_and_assign() { + let mut ct1 = Condition::::from_bool::(); + let ct2 = Condition::::from_bool::(); + let cf = Condition::::from_bool::(); - let btrue: bool = true; - let bfalse: bool = false; - assert_eq!(Condition::::from_bool_var(btrue).to_bool_var(), true); - assert_eq!(Condition::::from_bool_var(bfalse).to_bool_var(), false); + ct1 &= ct2; + assert_eq!(ct1.to_bool(), true); + + ct1 &= cf; + assert_eq!(ct1.to_bool(), false); } #[test] - fn is_bit_set() { - assert_eq!(Condition::::is_bit_set(1, 0).to_bool_var(), true); - assert_eq!(Condition::::is_bit_set(1, 1).to_bool_var(), false); - assert_eq!(Condition::::is_bit_set(8, 3).to_bool_var(), true); + fn test_bit_or() { + let ct1 = Condition::::from_bool::(); + let ct2 = Condition::::from_bool::(); + let cf1 = Condition::::from_bool::(); + let cf2 = Condition::::from_bool::(); + + assert_eq!((ct1 | ct2).to_bool(), true); + assert_eq!((ct1 | cf1).to_bool(), true); + assert_eq!((cf1 | cf2).to_bool(), false); } #[test] - fn is_negative() { - assert_eq!(Condition::::is_negative(-1).to_bool_var(), true); - assert_eq!(Condition::::is_negative(0).to_bool_var(), false); - assert_eq!(Condition::::is_negative(1).to_bool_var(), false); - assert_eq!(Condition::::is_negative(1 << 12).to_bool_var(), false); + fn test_bit_or_assign() { + let mut ct1 = Condition::::from_bool::(); + let ct2 = Condition::::from_bool::(); + let mut cf1 = Condition::::from_bool::(); + let cf2 = Condition::::from_bool::(); + + ct1 |= ct2; + assert_eq!(ct1.to_bool(), true); + + ct1 |= cf1; + assert_eq!(ct1.to_bool(), true); + + cf1 |= cf2; + assert_eq!(cf1.to_bool(), false); } #[test] - fn is_not_zero() { - assert_eq!(Condition::::is_not_zero(1).to_bool_var(), true); - assert_eq!(Condition::::is_not_zero(0).to_bool_var(), false); - assert_eq!(Condition::::is_not_zero(1 << 12).to_bool_var(), true); - assert_eq!(Condition::::is_not_zero(-10).to_bool_var(), true); + fn test_bit_xor() { + let ct1 = Condition::::from_bool::(); + let ct2 = Condition::::from_bool::(); + let cf1 = Condition::::from_bool::(); + let cf2 = Condition::::from_bool::(); + + assert_eq!((ct1 ^ ct2).to_bool(), false); + assert_eq!((ct1 ^ cf1).to_bool(), true); + assert_eq!((cf1 ^ cf2).to_bool(), false); } #[test] - fn is_zero() { - assert_eq!(Condition::::is_zero(1).to_bool_var(), false); - assert_eq!(Condition::::is_zero(0).to_bool_var(), true); - assert_eq!(Condition::::is_zero(1 << 12).to_bool_var(), false); + fn test_bit_xor_assign() { + let mut ct1 = Condition::::from_bool::(); + let mut ct2 = Condition::::from_bool::(); + let mut cf1 = Condition::::from_bool::(); + let cf2 = Condition::::from_bool::(); + + ct1 ^= ct2; + assert_eq!(ct1.to_bool(), false); + + ct2 ^= cf1; + assert_eq!(ct2.to_bool(), true); + + cf1 ^= cf2; + assert_eq!(cf1.to_bool(), false); } #[test] - fn is_equal() { - assert_eq!(Condition::::is_equal(1, 1).to_bool_var(), true); - assert_eq!(Condition::::is_equal(1, 2).to_bool_var(), false); - assert_eq!(Condition::::is_equal(1, -1).to_bool_var(), false); + fn test_not() { + let c = Condition::::from_bool::(); + assert_eq!((!c).to_bool(), false); + } +} + +/// One test module per unsigned width, written out by hand like the impls they exercise; the +/// u64 and u32 modules must be edited together so the widths stay at exact behavioural +/// parity. Boundary set: 0, 1, MAX, MAX-1, 1 << (BITS-1) โ€” the values where +/// signed-representation tricks would produce wrong masks if they had leaked into the +/// unsigned constructions. +#[cfg(test)] +mod unsigned_u64_tests { + use super::*; + + const MSB: u64 = 1 << (u64::BITS - 1); + const BOUNDARY: [u64; 5] = [0, 1, u64::MAX, u64::MAX - 1, MSB]; + + /// A mask must be exactly all-ones or all-zeros: `to_bool` only tests non-zero, + /// so a constructor returning `1` instead of all-ones would pass it and then + /// quietly corrupt every select it feeds. The two select operands differ in + /// every bit, so the assertion holds exactly when the mask is canonical. + fn assert_canonical(cond: Condition, expected: bool) { + const PATTERN: u64 = (0x5555_5555_5555_5555_u64 & (u64::MAX as u64)) as u64; + let selected = cond.select(PATTERN, !PATTERN); + assert_eq!(selected, if expected { PATTERN } else { !PATTERN }); + assert_eq!(cond.to_bool(), expected); } #[test] - fn is_lt() { - assert_eq!(Condition::::is_lt(1, 2).to_bool_var(), true); - assert_eq!(Condition::::is_lt(2, 1).to_bool_var(), false); - assert_eq!(Condition::::is_lt(2, 2).to_bool_var(), false); - assert_eq!(Condition::::is_lt(0, 1).to_bool_var(), true); - assert_eq!(Condition::::is_lt(-100, -99).to_bool_var(), true); - assert_eq!(Condition::::is_lt(-98, 98).to_bool_var(), true); + fn consts() { + assert_canonical(Condition::::TRUE, true); + assert_canonical(Condition::::FALSE, false); + } - let mut i: i64 = 0; - assert_eq!(Condition::::is_lt(i, 1).to_bool_var(), true); - assert_eq!(Condition::::is_lt(i, -1).to_bool_var(), false); - i = 1; - assert_eq!(Condition::::is_lt(i, 1).to_bool_var(), false); + #[test] + fn from_bool() { + assert_canonical(Condition::::from_bool::(), true); + assert_canonical(Condition::::from_bool::(), false); + assert_canonical(Condition::::from_bool_var(true), true); + assert_canonical(Condition::::from_bool_var(false), false); } #[test] - fn is_lte() { - assert_eq!(Condition::::is_lte(1, 2).to_bool_var(), true); - assert_eq!(Condition::::is_lte(2, 1).to_bool_var(), false); - assert_eq!(Condition::::is_lte(2, 2).to_bool_var(), true); - assert_eq!(Condition::::is_lte(0, 1).to_bool_var(), true); - assert_eq!(Condition::::is_lte(-100, -99).to_bool_var(), true); - assert_eq!(Condition::::is_lte(-98, 98).to_bool_var(), true); + fn from_lsb() { + for v in BOUNDARY { + assert_canonical(Condition::::from_lsb(v), v & 1 == 1); + } } #[test] - fn is_gt() { - assert_eq!(Condition::::is_gt(1, 2).to_bool_var(), false); - assert_eq!(Condition::::is_gt(2, 1).to_bool_var(), true); - assert_eq!(Condition::::is_gt(2, 2).to_bool_var(), false); - assert_eq!(Condition::::is_gt(0, 1).to_bool_var(), false); - assert_eq!(Condition::::is_gt(-100, -99).to_bool_var(), false); - assert_eq!(Condition::::is_gt(-98, 98).to_bool_var(), false); + fn from_msb() { + for v in BOUNDARY { + assert_canonical(Condition::::from_msb(v), v >> (u64::BITS - 1) == 1); + } } + /// `from_msb` exists to convert the borrow word of a wrapping subtraction chain + /// into a mask: `(x - y) >> BITS` of the widening subtraction is all-ones in the + /// low word iff x < y. #[test] - fn is_gte() { - assert_eq!(Condition::::is_gte(1, 2).to_bool_var(), false); - assert_eq!(Condition::::is_gte(2, 1).to_bool_var(), true); - assert_eq!(Condition::::is_gte(2, 2).to_bool_var(), true); - assert_eq!(Condition::::is_gte(0, 1).to_bool_var(), false); - assert_eq!(Condition::::is_gte(-100, -99).to_bool_var(), false); - assert_eq!(Condition::::is_gte(-98, 98).to_bool_var(), false); + fn from_msb_as_borrow_adaptor() { + for x in BOUNDARY { + for y in BOUNDARY { + let borrow = ((x as u128).wrapping_sub(y as u128) >> u64::BITS) as u64; + assert_canonical(Condition::::from_msb(borrow), x < y); + } + } } #[test] - fn is_in_range() { - assert_eq!(Condition::::is_within_range(1, 0, 2).to_bool_var(), true); - assert_eq!(Condition::::is_within_range(2, 0, 1).to_bool_var(), false); - assert_eq!(Condition::::is_within_range(1, -5, 2).to_bool_var(), true); - assert_eq!(Condition::::is_within_range(0, -5, 5).to_bool_var(), true); - assert_eq!(Condition::::is_within_range(1, 0, 0).to_bool_var(), false); + fn is_bit_set() { + // bit 0 agrees with from_lsb on every boundary value + for v in BOUNDARY { + assert_eq!( + Condition::::is_bit_set(v, 0).to_bool(), + Condition::::from_lsb(v).to_bool() + ); + } + // each single-bit value reports exactly its own bit + for k in 0..u64::BITS { + let v: u64 = 1 << k; + for bit in 0..u64::BITS { + assert_canonical(Condition::::is_bit_set(v, bit), bit == k); + } + } + // the top bit agrees with from_msb + for v in BOUNDARY { + assert_eq!( + Condition::::is_bit_set(v, u64::BITS - 1).to_bool(), + Condition::::from_msb(v).to_bool() + ); + } } #[test] - fn is_in_list() { - assert_eq!(Condition::::is_in_list(1, &[1, 2, 3]).to_bool_var(), true); - assert_eq!(Condition::::is_in_list(4, &[1, 2, 3]).to_bool_var(), false); - assert_eq!(Condition::::is_in_list(-3, &[1, 2, 3, 4, -5, -1]).to_bool_var(), false); - assert_eq!(Condition::::is_in_list(3, &[1, 2, 3, 3, 3, 3]).to_bool_var(), true); + fn is_zero_is_not_zero() { + for v in BOUNDARY { + assert_canonical(Condition::::is_zero(v), v == 0); + assert_canonical(Condition::::is_not_zero(v), v != 0); + } } #[test] - fn test_mov() { - let src = 1; - let mut dst = 2; - let c1 = Condition::::from_bool::(); - c1.mov(src, &mut dst); - assert_eq!(dst, 1); + fn is_equal() { + for x in BOUNDARY { + for y in BOUNDARY { + assert_canonical(Condition::::is_equal(x, y), x == y); + } + } + } + + #[test] + fn select_preserves_all_bits() { + // Patterned values (not just MAX/0) so a mask with any wrong bit shows up. + let a: u64 = (0xDEADBEEFCAFEBABE_u64 & u64::MAX as u64) as u64; + let b: u64 = (0x0123456789ABCDEF_u64 & u64::MAX as u64) as u64; + assert_eq!(Condition::::TRUE.select(a, b), a); + assert_eq!(Condition::::FALSE.select(a, b), b); + } - let c2 = Condition::::from_bool::(); + #[test] + fn mov() { + let src: u64 = 1; + let mut dst: u64 = 2; + Condition::::from_bool::().mov(src, &mut dst); + assert_eq!(dst, 1); dst = 2; - c2.mov(src, &mut dst); + Condition::::from_bool::().mov(src, &mut dst); assert_eq!(dst, 2); } #[test] - fn test_negate() { - let c1 = Condition::::TRUE; - assert_eq!(c1.negate(1), -1); - assert_eq!(c1.negate(0), 0); - assert_eq!(c1.negate(-1), 1); + fn swap() { + let (lhs, rhs) = Condition::::from_bool::().swap(1, 2); + assert_eq!((lhs, rhs), (2, 1)); + let (lhs, rhs) = Condition::::from_bool::().swap(1, 2); + assert_eq!((lhs, rhs), (1, 2)); + } - let c2 = Condition::::FALSE; - assert_eq!(c2.negate(1), 1); - assert_eq!(c2.negate(0), 0); - assert_eq!(c2.negate(-1), -1); + #[test] + fn boolean_operators() { + let t = Condition::::TRUE; + let f = Condition::::FALSE; + assert_canonical(!t, false); + assert_canonical(!f, true); + assert_canonical(t & f, false); + assert_canonical(t & t, true); + assert_canonical(t | f, true); + assert_canonical(f | f, false); + assert_canonical(t ^ t, false); + assert_canonical(t ^ f, true); + } +} + +#[cfg(test)] +mod unsigned_u32_tests { + use super::*; + + const MSB: u32 = 1 << (u32::BITS - 1); + const BOUNDARY: [u32; 5] = [0, 1, u32::MAX, u32::MAX - 1, MSB]; + + /// A mask must be exactly all-ones or all-zeros: `to_bool` only tests non-zero, + /// so a constructor returning `1` instead of all-ones would pass it and then + /// quietly corrupt every select it feeds. The two select operands differ in + /// every bit, so the assertion holds exactly when the mask is canonical. + fn assert_canonical(cond: Condition, expected: bool) { + const PATTERN: u32 = (0x5555_5555_5555_5555_u64 & (u32::MAX as u64)) as u32; + let selected = cond.select(PATTERN, !PATTERN); + assert_eq!(selected, if expected { PATTERN } else { !PATTERN }); + assert_eq!(cond.to_bool(), expected); } #[test] - fn test_or_halves() { - // 0 input -> 0 output - assert_eq!(Condition::::or_halves(0), 0); + fn consts() { + assert_canonical(Condition::::TRUE, true); + assert_canonical(Condition::::FALSE, false); + } - // Lower 32 bits should be preserved - assert_eq!(Condition::::or_halves(1), 1); - assert_eq!(Condition::::or_halves(0x12345678), 0x12345678); + #[test] + fn from_bool() { + assert_canonical(Condition::::from_bool::(), true); + assert_canonical(Condition::::from_bool::(), false); + assert_canonical(Condition::::from_bool_var(true), true); + assert_canonical(Condition::::from_bool_var(false), false); + } - // Upper 32 bits should be folded into lower 32 bits - // (1 << 32) OR (1 << 32 >> 32) => 0 OR 1 => 1 - assert_eq!(Condition::::or_halves(1 << 32), 1); + #[test] + fn from_lsb() { + for v in BOUNDARY { + assert_canonical(Condition::::from_lsb(v), v & 1 == 1); + } + } - // Mixed case: Upper 0x10000000 | Lower 0x00000001 => 0x10000001 - assert_eq!(Condition::::or_halves(0x10000000_00000001), 0x10000001); + #[test] + fn from_msb() { + for v in BOUNDARY { + assert_canonical(Condition::::from_msb(v), v >> (u32::BITS - 1) == 1); + } + } - // Negative number check (-1) - // -1 is 0xFFFF...FFFF - // (-1 >> 32) is -1 (Arithmetic shift preserves sign) - // (-1 | -1) is -1 - // -1 & 0xFFFFFFFF is 0x00000000FFFFFFFF (i64 value: 4294967295) - assert_eq!(Condition::::or_halves(-1), 0xFFFFFFFF); + /// `from_msb` exists to convert the borrow word of a wrapping subtraction chain + /// into a mask: `(x - y) >> BITS` of the widening subtraction is all-ones in the + /// low word iff x < y. + #[test] + fn from_msb_as_borrow_adaptor() { + for x in BOUNDARY { + for y in BOUNDARY { + let borrow = ((x as u64).wrapping_sub(y as u64) >> u32::BITS) as u32; + assert_canonical(Condition::::from_msb(borrow), x < y); + } + } + } - // i64::MIN check (Only MSB set) - // i64::MIN = 0x80000000_00000000 - // (val >> 32) = 0xFFFFFFFF_80000000 (Sign extension) - // (val | shifted) = 0xFFFFFFFF_80000000 - // (& mask) = 0x00000000_80000000 - assert_eq!(Condition::::or_halves(i64::MIN), 0x80000000); + #[test] + fn is_bit_set() { + // bit 0 agrees with from_lsb on every boundary value + for v in BOUNDARY { + assert_eq!( + Condition::::is_bit_set(v, 0).to_bool(), + Condition::::from_lsb(v).to_bool() + ); + } + // each single-bit value reports exactly its own bit + for k in 0..u32::BITS { + let v: u32 = 1 << k; + for bit in 0..u32::BITS { + assert_canonical(Condition::::is_bit_set(v, bit), bit == k); + } + } + // the top bit agrees with from_msb + for v in BOUNDARY { + assert_eq!( + Condition::::is_bit_set(v, u32::BITS - 1).to_bool(), + Condition::::from_msb(v).to_bool() + ); + } } #[test] - fn test_select() { - let c = Condition::::from_bool::(); - assert_eq!(c.select(1, 2), 1); - assert_eq!((!c).select(1, 2), 2); + fn is_zero_is_not_zero() { + for v in BOUNDARY { + assert_canonical(Condition::::is_zero(v), v == 0); + assert_canonical(Condition::::is_not_zero(v), v != 0); + } + } - // or the inverse behaviour if you start with 'false'. - let cfalse = Condition::::from_bool::(); - assert_eq!(cfalse.select(1, 2), 2); - assert_eq!((!cfalse).select(1, 2), 1); + #[test] + fn is_equal() { + for x in BOUNDARY { + for y in BOUNDARY { + assert_canonical(Condition::::is_equal(x, y), x == y); + } + } } #[test] - fn test_swap() { - let c = Condition::::from_bool::(); - let (lhs, rhs) = c.swap(1, 2); - assert_eq!(lhs, 2); - assert_eq!(rhs, 1); + fn select_preserves_all_bits() { + // Patterned values (not just MAX/0) so a mask with any wrong bit shows up. + let a: u32 = (0xDEADBEEFCAFEBABE_u64 & u32::MAX as u64) as u32; + let b: u32 = (0x0123456789ABCDEF_u64 & u32::MAX as u64) as u32; + assert_eq!(Condition::::TRUE.select(a, b), a); + assert_eq!(Condition::::FALSE.select(a, b), b); + } + + #[test] + fn mov() { + let src: u32 = 1; + let mut dst: u32 = 2; + Condition::::from_bool::().mov(src, &mut dst); + assert_eq!(dst, 1); + dst = 2; + Condition::::from_bool::().mov(src, &mut dst); + assert_eq!(dst, 2); + } + + #[test] + fn swap() { + let (lhs, rhs) = Condition::::from_bool::().swap(1, 2); + assert_eq!((lhs, rhs), (2, 1)); + let (lhs, rhs) = Condition::::from_bool::().swap(1, 2); + assert_eq!((lhs, rhs), (1, 2)); + } - // or the inverse behaviour if you start with 'false'. - let c = Condition::::from_bool::(); - let (lhs, rhs) = c.swap(1, 2); - assert_eq!(lhs, 1); - assert_eq!(rhs, 2); + #[test] + fn boolean_operators() { + let t = Condition::::TRUE; + let f = Condition::::FALSE; + assert_canonical(!t, false); + assert_canonical(!f, true); + assert_canonical(t & f, false); + assert_canonical(t & t, true); + assert_canonical(t | f, true); + assert_canonical(f | f, false); + assert_canonical(t ^ t, false); + assert_canonical(t ^ f, true); } } +/// One test module per signed width, written out by hand like the impls they exercise; the +/// i64 and i32 modules must be edited together so the widths stay at exact behavioural +/// parity. Boundary set: 0, +/-1, MIN, MIN+1, MAX, MAX-1: the values where +/// overflow or sign-extension mistakes in the mask constructions would show up. #[cfg(test)] -mod u64_tests { +mod signed_i64_tests { use super::*; + const BOUNDARY: [i64; 7] = [0, 1, -1, i64::MIN, i64::MIN + 1, i64::MAX, i64::MAX - 1]; + + /// A mask must be exactly all-ones or all-zeros: `to_bool` only tests non-zero, + /// so a constructor returning `1` instead of `-1` would pass it and then + /// quietly corrupt every select it feeds. The two select operands differ in + /// every bit, so the assertion holds exactly when the mask is canonical. + fn assert_canonical(cond: Condition, expected: bool) { + const PATTERN: i64 = (0x5555_5555_5555_5555_u64 & (i64::MAX as u64)) as i64; + let selected = cond.select(PATTERN, !PATTERN); + assert_eq!(selected, if expected { PATTERN } else { !PATTERN }); + assert_eq!(cond.to_bool(), expected); + } + #[test] - fn const_tests() { - // Ensure TRUE/FALSE are correctly interpreted as boolean. - assert_eq!(Condition::::TRUE.is_true(), true); - assert_eq!(Condition::::FALSE.is_true(), false); + fn consts() { + assert_canonical(Condition::::TRUE, true); + assert_canonical(Condition::::FALSE, false); } #[test] fn from_bool() { - // Compile-time const generics check - assert_eq!(Condition::::from_bool::().is_true(), true); - assert_eq!(Condition::::from_bool::().is_true(), false); + assert_canonical(Condition::::from_bool::(), true); + assert_canonical(Condition::::from_bool::(), false); + assert_canonical(Condition::::from_bool_var(true), true); + assert_canonical(Condition::::from_bool_var(false), false); } #[test] - fn select() { - let t = Condition::::TRUE; - let f = Condition::::FALSE; + fn from_lsb() { + for v in BOUNDARY { + assert_canonical(Condition::::from_lsb(v), v & 1 == 1); + } + } - let val1: u64 = 0xDEADBEEFCAFEBABE; - let val2: u64 = 0x0000000000000000; + #[test] + fn is_bit_set() { + // bit 0 agrees with from_lsb on every boundary value + for v in BOUNDARY { + assert_eq!( + Condition::::is_bit_set(v, 0).to_bool(), + Condition::::from_lsb(v).to_bool() + ); + } + // each single-bit value reports exactly its own bit + for k in 0..i64::BITS { + let v: i64 = (1 as i64) << k; + for bit in 0..i64::BITS { + assert_canonical(Condition::::is_bit_set(v, bit), bit == k); + } + } + // the top bit agrees with is_negative + for v in BOUNDARY { + assert_eq!( + Condition::::is_bit_set(v, i64::BITS - 1).to_bool(), + Condition::::is_negative(v).to_bool() + ); + } + } - // This test is CRITICAL. - // If TRUE was defined as '1' (like i64), this would fail because 'select' relies on bitwise mask. - // It requires TRUE to be u64::MAX (all 1s) to preserve the full bits of val1. - assert_eq!(t.select(val1, val2), val1); - assert_eq!(f.select(val1, val2), val2); + #[test] + fn is_negative() { + for v in BOUNDARY { + assert_canonical(Condition::::is_negative(v), v < 0); + } + } - // Cross check with from_bool - let t_gen = Condition::::from_bool::(); - assert_eq!(t_gen.select(val1, val2), val1); + #[test] + fn is_zero_is_not_zero() { + for v in BOUNDARY { + assert_canonical(Condition::::is_zero(v), v == 0); + assert_canonical(Condition::::is_not_zero(v), v != 0); + } } #[test] - fn bit_ops() { - let t = Condition::::TRUE; - let f = Condition::::FALSE; + fn is_equal() { + for x in BOUNDARY { + for y in BOUNDARY { + assert_canonical(Condition::::is_equal(x, y), x == y); + } + } + } - // NOT - assert_eq!((!t).is_true(), false); - assert_eq!((!f).is_true(), true); + /// Differential check against the native operators over every boundary pair, + /// including the far-apart pairs where a subtract-and-check-sign construction + /// would overflow. + #[test] + fn comparisons_match_native_operators() { + for x in BOUNDARY { + for y in BOUNDARY { + assert_canonical(Condition::::is_lt(x, y), x < y); + assert_canonical(Condition::::is_lte(x, y), x <= y); + assert_canonical(Condition::::is_gt(x, y), x > y); + assert_canonical(Condition::::is_gte(x, y), x >= y); + } + } + } - // AND - assert_eq!((t & t).is_true(), true); - assert_eq!((t & f).is_true(), false); - assert_eq!((f & f).is_true(), false); + /// Regression for the former `is_negative(x - y)` construction, which + /// overflowed for operands more than half the range apart (debug panic, + /// opposite answer in release). + #[test] + fn is_lt_far_apart_operands() { + assert_canonical(Condition::::is_lt(i64::MIN, 1), true); + assert_canonical(Condition::::is_lt(1, i64::MIN), false); + assert_canonical(Condition::::is_lt(i64::MIN, i64::MAX), true); + assert_canonical(Condition::::is_lt(i64::MAX, i64::MIN), false); + } - // OR - assert_eq!((t | t).is_true(), true); - assert_eq!((t | f).is_true(), true); - assert_eq!((f | f).is_true(), false); + #[test] + fn is_within_range() { + assert_canonical(Condition::::is_within_range(1, 0, 2), true); + assert_canonical(Condition::::is_within_range(2, 0, 1), false); + assert_canonical(Condition::::is_within_range(1, -5, 2), true); + assert_canonical(Condition::::is_within_range(0, -5, 5), true); + assert_canonical(Condition::::is_within_range(1, 0, 0), false); + for v in BOUNDARY { + for lo in BOUNDARY { + for hi in BOUNDARY { + assert_canonical( + Condition::::is_within_range(v, lo, hi), + lo <= v && v <= hi, + ); + } + } + } + } + + #[test] + fn is_in_list() { + assert_canonical(Condition::::is_in_list(1, &[1, 2, 3]), true); + assert_canonical(Condition::::is_in_list(4, &[1, 2, 3]), false); + assert_canonical(Condition::::is_in_list(-3, &[1, 2, 3, 4, -5, -1]), false); + assert_canonical(Condition::::is_in_list(3, &[1, 2, 3, 3, 3, 3]), true); + } + + #[test] + fn select_preserves_all_bits() { + // Patterned values (not just -1/0) so a mask with any wrong bit shows up. + let a: i64 = (0xDEADBEEFCAFEBABE_u64 & (i64::MAX as u64)) as i64; + let b: i64 = (0x0123456789ABCDEF_u64 & (i64::MAX as u64)) as i64; + assert_eq!(Condition::::TRUE.select(a, b), a); + assert_eq!(Condition::::FALSE.select(a, b), b); + } - // XOR - assert_eq!((t ^ t).is_true(), false); - assert_eq!((t ^ f).is_true(), true); + #[test] + fn mov() { + let src: i64 = 1; + let mut dst: i64 = 2; + Condition::::from_bool::().mov(src, &mut dst); + assert_eq!(dst, 1); + dst = 2; + Condition::::from_bool::().mov(src, &mut dst); + assert_eq!(dst, 2); + } + + #[test] + fn negate() { + let t = Condition::::TRUE; + assert_eq!(t.negate(1), -1); + assert_eq!(t.negate(0), 0); + assert_eq!(t.negate(-1), 1); + let f = Condition::::FALSE; + assert_eq!(f.negate(1), 1); + assert_eq!(f.negate(0), 0); + assert_eq!(f.negate(-1), -1); + } + + #[test] + fn swap() { + let (lhs, rhs) = Condition::::from_bool::().swap(1, 2); + assert_eq!((lhs, rhs), (2, 1)); + let (lhs, rhs) = Condition::::from_bool::().swap(1, 2); + assert_eq!((lhs, rhs), (1, 2)); + } + + #[test] + fn boolean_operators() { + let t = Condition::::TRUE; + let f = Condition::::FALSE; + assert_canonical(!t, false); + assert_canonical(!f, true); + assert_canonical(t & f, false); + assert_canonical(t & t, true); + assert_canonical(t | f, true); + assert_canonical(f | f, false); + assert_canonical(t ^ t, false); + assert_canonical(t ^ f, true); } } #[cfg(test)] -mod generic_impl_tests { +mod signed_i32_tests { use super::*; + const BOUNDARY: [i32; 7] = [0, 1, -1, i32::MIN, i32::MIN + 1, i32::MAX, i32::MAX - 1]; + + /// A mask must be exactly all-ones or all-zeros: `to_bool` only tests non-zero, + /// so a constructor returning `1` instead of `-1` would pass it and then + /// quietly corrupt every select it feeds. The two select operands differ in + /// every bit, so the assertion holds exactly when the mask is canonical. + fn assert_canonical(cond: Condition, expected: bool) { + const PATTERN: i32 = (0x5555_5555_5555_5555_u64 & (i32::MAX as u64)) as i32; + let selected = cond.select(PATTERN, !PATTERN); + assert_eq!(selected, if expected { PATTERN } else { !PATTERN }); + assert_eq!(cond.to_bool(), expected); + } + #[test] - fn test_bit_and() { - let ct1 = Condition::::from_bool::(); - let ct2 = Condition::::from_bool::(); - let cf1 = Condition::::from_bool::(); - let cf2 = Condition::::from_bool::(); - assert_eq!((ct1 & ct2).to_bool_var(), true); - assert_eq!((ct1 & cf1).to_bool_var(), false); - assert_eq!((cf1 & cf2).to_bool_var(), false); + fn consts() { + assert_canonical(Condition::::TRUE, true); + assert_canonical(Condition::::FALSE, false); } #[test] - fn test_bit_and_assign() { - let mut ct1 = Condition::::from_bool::(); - let ct2 = Condition::::from_bool::(); - let cf = Condition::::from_bool::(); + fn from_bool() { + assert_canonical(Condition::::from_bool::(), true); + assert_canonical(Condition::::from_bool::(), false); + assert_canonical(Condition::::from_bool_var(true), true); + assert_canonical(Condition::::from_bool_var(false), false); + } - ct1 &= ct2; - assert_eq!(ct1.to_bool_var(), true); + #[test] + fn from_lsb() { + for v in BOUNDARY { + assert_canonical(Condition::::from_lsb(v), v & 1 == 1); + } + } - ct1 &= cf; - assert_eq!(ct1.to_bool_var(), false); + #[test] + fn is_bit_set() { + // bit 0 agrees with from_lsb on every boundary value + for v in BOUNDARY { + assert_eq!( + Condition::::is_bit_set(v, 0).to_bool(), + Condition::::from_lsb(v).to_bool() + ); + } + // each single-bit value reports exactly its own bit + for k in 0..i32::BITS { + let v: i32 = (1 as i32) << k; + for bit in 0..i32::BITS { + assert_canonical(Condition::::is_bit_set(v, bit), bit == k); + } + } + // the top bit agrees with is_negative + for v in BOUNDARY { + assert_eq!( + Condition::::is_bit_set(v, i32::BITS - 1).to_bool(), + Condition::::is_negative(v).to_bool() + ); + } } #[test] - fn test_bit_or() { - let ct1 = Condition::::from_bool::(); - let ct2 = Condition::::from_bool::(); - let cf1 = Condition::::from_bool::(); - let cf2 = Condition::::from_bool::(); + fn is_negative() { + for v in BOUNDARY { + assert_canonical(Condition::::is_negative(v), v < 0); + } + } - assert_eq!((ct1 | ct2).to_bool_var(), true); - assert_eq!((ct1 | cf1).to_bool_var(), true); - assert_eq!((cf1 | cf2).to_bool_var(), false); + #[test] + fn is_zero_is_not_zero() { + for v in BOUNDARY { + assert_canonical(Condition::::is_zero(v), v == 0); + assert_canonical(Condition::::is_not_zero(v), v != 0); + } } #[test] - fn test_bit_or_assign() { - let mut ct1 = Condition::::from_bool::(); - let ct2 = Condition::::from_bool::(); - let mut cf1 = Condition::::from_bool::(); - let cf2 = Condition::::from_bool::(); + fn is_equal() { + for x in BOUNDARY { + for y in BOUNDARY { + assert_canonical(Condition::::is_equal(x, y), x == y); + } + } + } - ct1 |= ct2; - assert_eq!(ct1.to_bool_var(), true); + /// Differential check against the native operators over every boundary pair, + /// including the far-apart pairs where a subtract-and-check-sign construction + /// would overflow. + #[test] + fn comparisons_match_native_operators() { + for x in BOUNDARY { + for y in BOUNDARY { + assert_canonical(Condition::::is_lt(x, y), x < y); + assert_canonical(Condition::::is_lte(x, y), x <= y); + assert_canonical(Condition::::is_gt(x, y), x > y); + assert_canonical(Condition::::is_gte(x, y), x >= y); + } + } + } - ct1 |= cf1; - assert_eq!(ct1.to_bool_var(), true); + /// Regression for the former `is_negative(x - y)` construction, which + /// overflowed for operands more than half the range apart (debug panic, + /// opposite answer in release). + #[test] + fn is_lt_far_apart_operands() { + assert_canonical(Condition::::is_lt(i32::MIN, 1), true); + assert_canonical(Condition::::is_lt(1, i32::MIN), false); + assert_canonical(Condition::::is_lt(i32::MIN, i32::MAX), true); + assert_canonical(Condition::::is_lt(i32::MAX, i32::MIN), false); + } - cf1 |= cf2; - assert_eq!(cf1.to_bool_var(), false); + #[test] + fn is_within_range() { + assert_canonical(Condition::::is_within_range(1, 0, 2), true); + assert_canonical(Condition::::is_within_range(2, 0, 1), false); + assert_canonical(Condition::::is_within_range(1, -5, 2), true); + assert_canonical(Condition::::is_within_range(0, -5, 5), true); + assert_canonical(Condition::::is_within_range(1, 0, 0), false); + for v in BOUNDARY { + for lo in BOUNDARY { + for hi in BOUNDARY { + assert_canonical( + Condition::::is_within_range(v, lo, hi), + lo <= v && v <= hi, + ); + } + } + } } #[test] - fn test_bit_xor() { - let ct1 = Condition::::from_bool::(); - let ct2 = Condition::::from_bool::(); - let cf1 = Condition::::from_bool::(); - let cf2 = Condition::::from_bool::(); + fn is_in_list() { + assert_canonical(Condition::::is_in_list(1, &[1, 2, 3]), true); + assert_canonical(Condition::::is_in_list(4, &[1, 2, 3]), false); + assert_canonical(Condition::::is_in_list(-3, &[1, 2, 3, 4, -5, -1]), false); + assert_canonical(Condition::::is_in_list(3, &[1, 2, 3, 3, 3, 3]), true); + } - assert_eq!((ct1 ^ ct2).to_bool_var(), false); - assert_eq!((ct1 | cf1).to_bool_var(), true); - assert_eq!((cf1 | cf2).to_bool_var(), false); + #[test] + fn select_preserves_all_bits() { + // Patterned values (not just -1/0) so a mask with any wrong bit shows up. + let a: i32 = (0xDEADBEEFCAFEBABE_u64 & (i32::MAX as u64)) as i32; + let b: i32 = (0x0123456789ABCDEF_u64 & (i32::MAX as u64)) as i32; + assert_eq!(Condition::::TRUE.select(a, b), a); + assert_eq!(Condition::::FALSE.select(a, b), b); } #[test] - fn test_bit_xor_assign() { - let mut ct1 = Condition::::from_bool::(); - let mut ct2 = Condition::::from_bool::(); - let mut cf1 = Condition::::from_bool::(); - let cf2 = Condition::::from_bool::(); + fn mov() { + let src: i32 = 1; + let mut dst: i32 = 2; + Condition::::from_bool::().mov(src, &mut dst); + assert_eq!(dst, 1); + dst = 2; + Condition::::from_bool::().mov(src, &mut dst); + assert_eq!(dst, 2); + } - ct1 ^= ct2; - assert_eq!(ct1.to_bool_var(), false); + #[test] + fn negate() { + let t = Condition::::TRUE; + assert_eq!(t.negate(1), -1); + assert_eq!(t.negate(0), 0); + assert_eq!(t.negate(-1), 1); + let f = Condition::::FALSE; + assert_eq!(f.negate(1), 1); + assert_eq!(f.negate(0), 0); + assert_eq!(f.negate(-1), -1); + } - ct2 ^= cf1; - assert_eq!(ct2.to_bool_var(), true); + #[test] + fn swap() { + let (lhs, rhs) = Condition::::from_bool::().swap(1, 2); + assert_eq!((lhs, rhs), (2, 1)); + let (lhs, rhs) = Condition::::from_bool::().swap(1, 2); + assert_eq!((lhs, rhs), (1, 2)); + } - cf1 ^= cf2; - assert_eq!(cf1.to_bool_var(), false); + #[test] + fn boolean_operators() { + let t = Condition::::TRUE; + let f = Condition::::FALSE; + assert_canonical(!t, false); + assert_canonical(!f, true); + assert_canonical(t & f, false); + assert_canonical(t & t, true); + assert_canonical(t | f, true); + assert_canonical(f | f, false); + assert_canonical(t ^ t, false); + assert_canonical(t ^ f, true); } +} + +#[cfg(test)] +mod signed_comparison_sweep { + use super::*; + /// Dense sweep of the full i32 range: 256 x 256 pairs stepped 1 << 24 apart, checked + /// against the native operators. This covers every combination of sign and magnitude + /// region, not just the BOUNDARY values. #[test] - fn test_not() { - let c = Condition::::from_bool::(); - assert_eq!((!c).to_bool_var(), false); + fn i32_strided_full_range() { + for a in (i32::MIN..=i32::MAX).step_by(1 << 24) { + for b in (i32::MIN..=i32::MAX).step_by(1 << 24) { + assert_eq!(Condition::::is_lt(a, b).to_bool(), a < b, "{a} {b}"); + assert_eq!(Condition::::is_lte(a, b).to_bool(), a <= b, "{a} {b}"); + assert_eq!(Condition::::is_gt(a, b).to_bool(), a > b, "{a} {b}"); + assert_eq!(Condition::::is_gte(a, b).to_bool(), a >= b, "{a} {b}"); + } + } } } #[cfg(test)] mod ct_bytes_tests { + #[test] + fn test_ct_eq_zero_bytes() { + use bouncycastle_utils::ct::ct_eq_zero_bytes; + + // all-zero inputs, including the empty slice + assert!(ct_eq_zero_bytes(&[])); + assert!(ct_eq_zero_bytes(&[0])); + assert!(ct_eq_zero_bytes(&[0; 32])); + + // a nonzero byte anywhere must be detected: first, last, and high-bit-only + assert!(!ct_eq_zero_bytes(&[1])); + let mut buf = [0u8; 32]; + buf[0] = 1; + assert!(!ct_eq_zero_bytes(&buf)); + buf[0] = 0; + buf[31] = 1; + assert!(!ct_eq_zero_bytes(&buf)); + buf[31] = 0; + buf[15] = 0x80; + assert!(!ct_eq_zero_bytes(&buf)); + } + #[test] fn test_conditional_copy_bytes() { use bouncycastle_utils::ct::conditional_copy_bytes;