diff --git a/CHANGELOG.md b/CHANGELOG.md index e5b74ac..edce1ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 1.0.0-rc.1 (July 3, 2026) + +* Reject trailing bytes in all `deserialize` methods +* Reject identity element in `deterministic_blind_unchecked` to prevent blinding bypass +* Added roundtrip, trailing bytes, truncated, and empty input tests for serialization + ## 1.0.0-rc.0 (July 2, 2026) * Added missing license in Cargo manifest diff --git a/Cargo.toml b/Cargo.toml index 839dcfc..9f25c27 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ name = "voprf-vx" readme = "README.md" repository = "https://github.com/vexahub/voprf-vx/" rust-version = "1.87" -version = "1.0.0-rc.0" +version = "1.0.0-rc.1" [features] alloc = [] @@ -22,7 +22,7 @@ serde = ["curve25519-dalek?/serde", "hybrid-array/serde", "dep:serde"] std = ["alloc"] [dependencies] -curve25519-dalek = { version = "5.0.0-rc", default-features = false, features = ["rand_core", "zeroize"], optional = true } +curve25519-dalek = { version = "5.0.0-rc.1", default-features = false, features = ["rand_core", "zeroize"], optional = true } derive-where = { version = "1", features = ["zeroize-on-drop"] } digest = { version = "0.11", features = ["zeroize"] } displaydoc = { version = "0.2", default-features = false } @@ -36,21 +36,21 @@ serde = { version = "1", default-features = false, features = [ "derive", ], optional = true } sha2 = { version = "0.11", default-features = false, features = ["zeroize"], optional = true } -p256 = { version = "0.14.0-rc", default-features = false, features = ["hash2curve", "oprf"], optional = true } +p256 = { version = "0.14.0-rc.15", default-features = false, features = ["hash2curve", "oprf"], optional = true } subtle = { version = "2.6", default-features = false } zeroize = { version = "1.5", default-features = false } [dev-dependencies] hex = "0.4" -p256 = { version = "0.14.0-rc", default-features = false, features = [ +p256 = { version = "0.14.0-rc.15", default-features = false, features = [ "hash2curve", "oprf", ] } -p384 = { version = "0.14.0-rc", default-features = false, features = [ +p384 = { version = "0.14.0-rc.15", default-features = false, features = [ "hash2curve", "oprf", ] } -p521 = { version = "0.14.0-rc", default-features = false, features = [ +p521 = { version = "0.14.0-rc.15", default-features = false, features = [ "hash2curve", "oprf", ] } diff --git a/README.md b/README.md index f15eb6c..b3ddc55 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Installation Add the following line to the dependencies of your `Cargo.toml`: ``` -voprf = { package = "voprf-vx", version = "1.0.0-rc.0" } +voprf = { package = "voprf-vx", version = "1.0.0-rc.1" } ``` ### Minimum Supported Rust Version diff --git a/src/common.rs b/src/common.rs index 7eacd71..d9a932f 100644 --- a/src/common.rs +++ b/src/common.rs @@ -397,6 +397,12 @@ pub(crate) fn deterministic_blind_unchecked( mode: Mode, ) -> Result<::Elem> { let hashed_point = hash_to_group::(input, mode)?; + + // Identity element would nullify blinding, revealing the input. + if CS::Group::is_identity_elem(hashed_point).into() { + return Err(Error::Input); + } + Ok(hashed_point * blind) } diff --git a/src/serialization.rs b/src/serialization.rs index 0262be1..27b2cca 100644 --- a/src/serialization.rs +++ b/src/serialization.rs @@ -34,6 +34,10 @@ impl OprfClient { pub fn deserialize(mut input: &[u8]) -> Result { let blind = deserialize_scalar::(&mut input)?; + if !input.is_empty() { + return Err(Error::Deserialization); + } + Ok(Self { blind }) } } @@ -59,6 +63,10 @@ impl VoprfClient { let blind = deserialize_scalar::(&mut input)?; let blinded_element = deserialize_elem::(&mut input)?; + if !input.is_empty() { + return Err(Error::Deserialization); + } + Ok(Self { blind, blinded_element, @@ -87,6 +95,10 @@ impl PoprfClient { let blind = deserialize_scalar::(&mut input)?; let blinded_element = deserialize_elem::(&mut input)?; + if !input.is_empty() { + return Err(Error::Deserialization); + } + Ok(Self { blind, blinded_element, @@ -110,6 +122,10 @@ impl OprfServer { pub fn deserialize(mut input: &[u8]) -> Result { let sk = deserialize_scalar::(&mut input)?; + if !input.is_empty() { + return Err(Error::Deserialization); + } + Ok(Self { sk }) } } @@ -134,6 +150,10 @@ impl VoprfServer { let sk = deserialize_scalar::(&mut input)?; let pk = deserialize_elem::(&mut input)?; + if !input.is_empty() { + return Err(Error::Deserialization); + } + Ok(Self { sk, pk }) } } @@ -158,6 +178,10 @@ impl PoprfServer { let sk = deserialize_scalar::(&mut input)?; let pk = deserialize_elem::(&mut input)?; + if !input.is_empty() { + return Err(Error::Deserialization); + } + Ok(Self { sk, pk }) } } @@ -183,6 +207,10 @@ impl Proof { let c_scalar = deserialize_scalar::(&mut input)?; let s_scalar = deserialize_scalar::(&mut input)?; + if !input.is_empty() { + return Err(Error::Deserialization); + } + Ok(Proof { c_scalar, s_scalar }) } } @@ -203,6 +231,10 @@ impl BlindedElement { pub fn deserialize(mut input: &[u8]) -> Result { let value = deserialize_elem::(&mut input)?; + if !input.is_empty() { + return Err(Error::Deserialization); + } + Ok(Self(value)) } } @@ -223,6 +255,10 @@ impl EvaluationElement { pub fn deserialize(mut input: &[u8]) -> Result { let value = deserialize_elem::(&mut input)?; + if !input.is_empty() { + return Err(Error::Deserialization); + } + Ok(Self(value)) } } @@ -317,6 +353,7 @@ mod test { VoprfClient, VoprfServer, }; + // Fuzz: no panics on arbitrary input macro_rules! test_deserialize { ($item:ident, $bytes:ident) => { #[cfg(feature = "ristretto255")] @@ -324,12 +361,43 @@ mod test { let _ = $item::::deserialize(&$bytes[..]); } - let _ = $item::::deserialize(&$bytes[..]); - let _ = $item::::deserialize(&$bytes[..]); - let _ = $item::::deserialize(&$bytes[..]); + let _ = $item::<::p256::NistP256>::deserialize(&$bytes[..]); + let _ = $item::<::p384::NistP384>::deserialize(&$bytes[..]); + let _ = $item::<::p521::NistP521>::deserialize(&$bytes[..]); }; } + // Roundtrip: serialize to deserialize == original + macro_rules! test_roundtrip { + ($item:ident, $cs:ty, $constructor:expr) => {{ + let original = $constructor; + let bytes = original.serialize(); + let recovered = $item::<$cs>::deserialize(&bytes).expect("roundtrip deserialize"); + assert_eq!(original.serialize(), recovered.serialize()); + }}; + } + + // Trailing bytes: valid serialization + extra byte must fail + macro_rules! test_trailing { + ($item:ident, $cs:ty, $constructor:expr) => {{ + let original = $constructor; + let bytes = original.serialize(); + let mut extended = bytes.to_vec(); + extended.push(0x00); + assert!($item::<$cs>::deserialize(&extended).is_err()); + }}; + } + + // Truncated: valid serialization minus one byte must fail + macro_rules! test_truncated { + ($item:ident, $cs:ty, $constructor:expr) => {{ + let original = $constructor; + let bytes = original.serialize(); + let truncated = &bytes[..bytes.len() - 1]; + assert!($item::<$cs>::deserialize(truncated).is_err()); + }}; + } + proptest! { #[test] fn test_nocrash_oprf_client(bytes in vec(any::(), 0..200)) { @@ -377,4 +445,103 @@ mod test { test_deserialize!(Proof, bytes); } } + + macro_rules! structured_tests { + ($cs:ty, $mod:ident) => { + mod $mod { + use super::*; + + use rand::rngs::SysRng; + + #[test] + fn roundtrip_oprf_client() { + let client = OprfClient::<$cs>::blind(b"input", &mut SysRng) + .expect("blind") + .state; + test_roundtrip!(OprfClient, $cs, client); + } + + #[test] + fn roundtrip_oprf_server() { + let server = OprfServer::<$cs>::new(&mut SysRng).expect("new"); + test_roundtrip!(OprfServer, $cs, server); + } + + #[test] + fn roundtrip_voprf_client() { + let client = VoprfClient::<$cs>::blind(b"input", &mut SysRng) + .expect("blind") + .state; + test_roundtrip!(VoprfClient, $cs, client); + } + + #[test] + fn roundtrip_voprf_server() { + let server = VoprfServer::<$cs>::new(&mut SysRng).expect("new"); + test_roundtrip!(VoprfServer, $cs, server); + } + + #[test] + fn roundtrip_poprf_client() { + let client = PoprfClient::<$cs>::blind(b"input", &mut SysRng) + .expect("blind") + .state; + test_roundtrip!(PoprfClient, $cs, client); + } + + #[test] + fn roundtrip_poprf_server() { + let server = PoprfServer::<$cs>::new(&mut SysRng).expect("new"); + test_roundtrip!(PoprfServer, $cs, server); + } + + #[test] + fn trailing_oprf_client() { + let client = OprfClient::<$cs>::blind(b"input", &mut SysRng) + .expect("blind") + .state; + test_trailing!(OprfClient, $cs, client); + } + + #[test] + fn trailing_oprf_server() { + let server = OprfServer::<$cs>::new(&mut SysRng).expect("new"); + test_trailing!(OprfServer, $cs, server); + } + + #[test] + fn truncated_oprf_client() { + let client = OprfClient::<$cs>::blind(b"input", &mut SysRng) + .expect("blind") + .state; + test_truncated!(OprfClient, $cs, client); + } + + #[test] + fn truncated_oprf_server() { + let server = OprfServer::<$cs>::new(&mut SysRng).expect("new"); + test_truncated!(OprfServer, $cs, server); + } + + #[test] + fn empty_input_fails() { + assert!(OprfClient::<$cs>::deserialize(&[]).is_err()); + assert!(OprfServer::<$cs>::deserialize(&[]).is_err()); + assert!(VoprfClient::<$cs>::deserialize(&[]).is_err()); + assert!(VoprfServer::<$cs>::deserialize(&[]).is_err()); + assert!(PoprfClient::<$cs>::deserialize(&[]).is_err()); + assert!(PoprfServer::<$cs>::deserialize(&[]).is_err()); + assert!(BlindedElement::<$cs>::deserialize(&[]).is_err()); + assert!(EvaluationElement::<$cs>::deserialize(&[]).is_err()); + assert!(Proof::<$cs>::deserialize(&[]).is_err()); + } + } + }; + } + + #[cfg(feature = "ristretto255")] + structured_tests!(crate::Ristretto255, ristretto255); + structured_tests!(::p256::NistP256, p256); + structured_tests!(::p384::NistP384, p384); + structured_tests!(::p521::NistP521, p521); }