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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 29 additions & 15 deletions src/ibe/boyen_waters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,21 +294,23 @@ impl Compress for PublicKey {
bytes, GT_BYTES, G1_BYTES, G1_BYTES, G2_BYTES, G2_BYTES, G1_BYTES, G1_BYTES, G1_BYTES,
G1_BYTES
];
// from_compressed_unchecked doesn't check whether the element has
// a cofactor. To mount an attack using a cofactor an attacker
// must be able to manipulate the public parameters. But then the
// attacker can simply use parameters they generated themselves.
// Thus checking for a cofactor is superfluous.

let omega = Gt::from_compressed_unchecked(omega);
let g0 = G1Affine::from_compressed_unchecked(g0);
let g1 = G1Affine::from_compressed_unchecked(g1);
let h0 = G2Affine::from_compressed_unchecked(h0);
let h1 = G2Affine::from_compressed_unchecked(h1);
let v1 = G1Affine::from_compressed_unchecked(v1);
let v2 = G1Affine::from_compressed_unchecked(v2);
let v3 = G1Affine::from_compressed_unchecked(v3);
let v4 = G1Affine::from_compressed_unchecked(v4);
// Use the checked `from_compressed` variant, which verifies that each
// point is in the correct prime-order subgroup. Public keys may be
// deserialized from an unauthenticated source before their integrity is
// verified out-of-band; skipping the subgroup check would let an
// adversary embed a point outside the prime-order subgroup (BLS12-381
// has a large, non-trivial cofactor) and mount a small-subgroup attack
// (GHSA-25fp-2fjj-g84w).

let omega = Gt::from_compressed(omega);
let g0 = G1Affine::from_compressed(g0);
let g1 = G1Affine::from_compressed(g1);
let h0 = G2Affine::from_compressed(h0);
let h1 = G2Affine::from_compressed(h1);
let v1 = G1Affine::from_compressed(v1);
let v2 = G1Affine::from_compressed(v2);
let v3 = G1Affine::from_compressed(v3);
let v4 = G1Affine::from_compressed(v4);

omega.and_then(|omega| {
g0.and_then(|g0| {
Expand Down Expand Up @@ -491,6 +493,18 @@ impl Compress for CipherText {
mod tests {
test_ibe!(BoyenWaters);

// Regression test for GHSA-25fp-2fjj-g84w. A public key whose first G1
// component (g0) encodes a valid on-curve point that lies outside the
// prime-order subgroup must be rejected by the checked `from_bytes`.
#[test]
fn from_bytes_rejects_non_subgroup_point() {
let pk = perform_default().pk;
let mut bytes = pk.to_bytes();
// Layout: omega (Gt) precedes g0 (first G1 component).
bytes[GT_BYTES..GT_BYTES + G1_BYTES].copy_from_slice(&NON_SUBGROUP_G1_COMPRESSED);
assert!(bool::from(PublicKey::from_bytes(&bytes).is_none()));
}

#[cfg(feature = "zeroize")]
#[test]
fn shared_secret_zeroize() {
Expand Down
41 changes: 26 additions & 15 deletions src/ibe/cgw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,11 +261,13 @@ impl Compress for PublicKey {
}

fn from_bytes(bytes: &[u8; PK_BYTES]) -> CtOption<Self> {
// from_compressed_unchecked doesn't check whether the element has
// a cofactor. To mount an attack using a cofactor an attacker
// must be able to manipulate the public parameters. But then the
// attacker can simply use parameters they generated themselves.
// Thus checking for a cofactor is superfluous.
// Use the checked `from_compressed` variant, which verifies that each
// point is in the correct prime-order subgroup. Public keys may be
// deserialized from an unauthenticated source before their integrity is
// verified out-of-band; skipping the subgroup check would let an
// adversary embed a point outside the prime-order subgroup (BLS12-381
// has a large, non-trivial cofactor) and mount a small-subgroup attack
// (GHSA-25fp-2fjj-g84w).
let mut a_1 = [G1Affine::default(); 2];
let mut w0ta_1 = [G1Affine::default(); 2];
let mut w1ta_1 = [G1Affine::default(); 2];
Expand All @@ -275,19 +277,17 @@ impl Compress for PublicKey {
for i in 0..2 {
let x = i * G1_BYTES;
let y = x + G1_BYTES;
is_some &= G1Affine::from_compressed_unchecked(bytes[x..y].try_into().unwrap())
is_some &= G1Affine::from_compressed(bytes[x..y].try_into().unwrap())
.map(|el| a_1[i] = el)
.is_some();
is_some &=
G1Affine::from_compressed_unchecked(bytes[96 + x..96 + y].try_into().unwrap())
.map(|el| w0ta_1[i] = el)
.is_some();
is_some &=
G1Affine::from_compressed_unchecked(bytes[192 + x..192 + y].try_into().unwrap())
.map(|el| w1ta_1[i] = el)
.is_some();
is_some &= G1Affine::from_compressed(bytes[96 + x..96 + y].try_into().unwrap())
.map(|el| w0ta_1[i] = el)
.is_some();
is_some &= G1Affine::from_compressed(bytes[192 + x..192 + y].try_into().unwrap())
.map(|el| w1ta_1[i] = el)
.is_some();
}
is_some &= Gt::from_compressed_unchecked(bytes[288..].try_into().unwrap())
is_some &= Gt::from_compressed(bytes[288..].try_into().unwrap())
.map(|el| kta_t = el)
.is_some();

Expand Down Expand Up @@ -486,4 +486,15 @@ impl ConditionallySelectable for UserSecretKey {
#[cfg(test)]
mod tests {
test_ibe!(CGW);

// Regression test for GHSA-25fp-2fjj-g84w. A public key whose first G1
// component encodes a valid on-curve point that lies outside the prime-order
// subgroup must be rejected by the checked `from_bytes` deserialization.
#[test]
fn from_bytes_rejects_non_subgroup_point() {
let pk = perform_default().pk;
let mut bytes = pk.to_bytes();
bytes[..G1_BYTES].copy_from_slice(&NON_SUBGROUP_G1_COMPRESSED);
assert!(bool::from(PublicKey::from_bytes(&bytes).is_none()));
}
}
49 changes: 30 additions & 19 deletions src/kem/cgw_kv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -296,11 +296,13 @@ impl Compress for PublicKey {
}

fn from_bytes(bytes: &[u8; PK_BYTES]) -> CtOption<Self> {
// from_compressed_unchecked doesn't check whether the element has
// a cofactor. To mount an attack using a cofactor an attacker
// must be able to manipulate the public parameters. But then the
// attacker can simply use parameters they generated themselves.
// Thus checking for a cofactor is superfluous.
// Use the checked `from_compressed` variant, which verifies that each
// point is in the correct prime-order subgroup. Public keys may be
// deserialized from an unauthenticated source before their integrity is
// verified out-of-band; skipping the subgroup check would let an
// adversary embed a point outside the prime-order subgroup (BLS12-381
// has a large, non-trivial cofactor) and mount a small-subgroup attack
// (GHSA-25fp-2fjj-g84w).
let mut a_1 = [G1Affine::default(); 2];
let mut w0ta_1 = [G1Affine::default(); 2];
let mut w1ta_1 = [G1Affine::default(); 2];
Expand All @@ -311,23 +313,20 @@ impl Compress for PublicKey {
for i in 0..2 {
let x = i * G1_BYTES;
let y = x + G1_BYTES;
is_some &= G1Affine::from_compressed_unchecked(bytes[x..y].try_into().unwrap())
is_some &= G1Affine::from_compressed(bytes[x..y].try_into().unwrap())
.map(|el| a_1[i] = el)
.is_some();
is_some &=
G1Affine::from_compressed_unchecked(bytes[96 + x..96 + y].try_into().unwrap())
.map(|el| w0ta_1[i] = el)
.is_some();
is_some &=
G1Affine::from_compressed_unchecked(bytes[192 + x..192 + y].try_into().unwrap())
.map(|el| w1ta_1[i] = el)
.is_some();
is_some &=
G1Affine::from_compressed_unchecked(bytes[288 + x..288 + y].try_into().unwrap())
.map(|el| wprime_1[i] = el)
.is_some();
is_some &= G1Affine::from_compressed(bytes[96 + x..96 + y].try_into().unwrap())
.map(|el| w0ta_1[i] = el)
.is_some();
is_some &= G1Affine::from_compressed(bytes[192 + x..192 + y].try_into().unwrap())
.map(|el| w1ta_1[i] = el)
.is_some();
is_some &= G1Affine::from_compressed(bytes[288 + x..288 + y].try_into().unwrap())
.map(|el| wprime_1[i] = el)
.is_some();
}
is_some &= Gt::from_compressed_unchecked(bytes[384..672].try_into().unwrap())
is_some &= Gt::from_compressed(bytes[384..672].try_into().unwrap())
.map(|el| kta_t = el)
.is_some();

Expand Down Expand Up @@ -533,6 +532,18 @@ mod tests {
#[cfg(feature = "mkem")]
test_multi_kem!(CGWKV);

// Regression test for GHSA-25fp-2fjj-g84w. A public key whose first G1
// component encodes a valid on-curve point that lies outside the prime-order
// subgroup must be rejected by the checked `from_bytes` deserialization.
#[test]
fn from_bytes_rejects_non_subgroup_point() {
let mut rng = rand::thread_rng();
let (pk, _sk) = CGWKV::setup(&mut rng);
let mut bytes = pk.to_bytes();
bytes[..G1_BYTES].copy_from_slice(&NON_SUBGROUP_G1_COMPRESSED);
assert!(bool::from(PublicKey::from_bytes(&bytes).is_none()));
}

#[cfg(feature = "zeroize")]
#[test]
fn secret_types_zeroize() {
Expand Down
38 changes: 27 additions & 11 deletions src/kem/kiltz_vahlis_one.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,9 @@ impl HashParameters {
let mut res = [G1Affine::default(); N];
let mut is_some = Choice::from(1u8);
for (i, slot) in res.iter_mut().enumerate() {
// See comment in PublicKey::from_bytes on cofactor.
is_some &= G1Affine::from_compressed_unchecked(array_ref![bytes, i * 48, 48])
// See comment in PublicKey::from_bytes: checked variant verifies
// subgroup membership (GHSA-25fp-2fjj-g84w).
is_some &= G1Affine::from_compressed(array_ref![bytes, i * 48, 48])
.map(|s| {
*slot = s;
})
Expand Down Expand Up @@ -263,16 +264,18 @@ impl Compress for PublicKey {
fn from_bytes(bytes: &[u8; PK_BYTES]) -> CtOption<Self> {
let (g, hzero, h, u, z) = array_refs![&bytes, 96, 48, HASH_PARAMETER_SIZE, 48, 288];

// from_compressed_unchecked doesn't check whether the element has
// a cofactor. To mount an attack using a cofactor an attacker
// must be able to manipulate the public parameters. But then the
// attacker can simply use parameters they generated themselves.
// Thus checking for a cofactor is superfluous.
let g = G2Affine::from_compressed_unchecked(g);
let hzero = G1Affine::from_compressed_unchecked(hzero);
// Use the checked `from_compressed` variant, which verifies that each
// point is in the correct prime-order subgroup. Public keys may be
// deserialized from an unauthenticated source before their integrity is
// verified out-of-band; skipping the subgroup check would let an
// adversary embed a point outside the prime-order subgroup (BLS12-381
// has a large, non-trivial cofactor) and mount a small-subgroup attack
// (GHSA-25fp-2fjj-g84w).
let g = G2Affine::from_compressed(g);
let hzero = G1Affine::from_compressed(hzero);
let h = HashParameters::from_bytes(h);
let u = G1Affine::from_compressed_unchecked(u);
let z = Gt::from_compressed_unchecked(z);
let u = G1Affine::from_compressed(u);
let z = Gt::from_compressed(z);

g.and_then(|g| {
hzero.and_then(|hzero| {
Expand Down Expand Up @@ -364,6 +367,19 @@ mod tests {
#[cfg(feature = "mkem")]
test_multi_kem!(KV1);

// Regression test for GHSA-25fp-2fjj-g84w. A public key whose `hzero` G1
// component encodes a valid on-curve point that lies outside the prime-order
// subgroup must be rejected by the checked `from_bytes` deserialization.
#[test]
fn from_bytes_rejects_non_subgroup_point() {
let mut rng = rand::thread_rng();
let (pk, _sk) = KV1::setup(&mut rng);
let mut bytes = pk.to_bytes();
// Layout: g (G2, 96 bytes) precedes hzero (first G1 component).
bytes[G2_BYTES..G2_BYTES + G1_BYTES].copy_from_slice(&NON_SUBGROUP_G1_COMPRESSED);
assert!(bool::from(PublicKey::from_bytes(&bytes).is_none()));
}

// Two distinct strings whose SHA3-512 digests happen to agree on the 8 bits
// that the buggy `bits()` read (one bit each from the last 8 bytes of the
// digest). Under the old code, both produce the same curve point — i.e. a
Expand Down
13 changes: 13 additions & 0 deletions src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,19 @@ pub(crate) const SCALAR_BYTES: usize = 32;
/// Size of the (default) identity buffer.
pub(crate) const ID_BYTES: usize = 64;

/// Compressed encoding of a G1 point that is on the curve but **not** in the
/// prime-order subgroup (it has a non-trivial cofactor component). The encoding
/// is well-formed, so `from_compressed_unchecked` accepts it, but the checked
/// `from_compressed` rejects it. Used by the `from_bytes` regression tests that
/// guard against the small-subgroup issue (GHSA-25fp-2fjj-g84w). Derived from
/// the non-torsion-free test point in the underlying curve crate.
#[cfg(test)]
pub(crate) const NON_SUBGROUP_G1_COMPRESSED: [u8; G1_BYTES] = [
0x92, 0x8d, 0x48, 0x62, 0xa4, 0x04, 0x39, 0xa6, 0x7f, 0xd7, 0x6a, 0x9c, 0x75, 0x60, 0xe2, 0xff,
0x15, 0x9e, 0x77, 0x0d, 0xcf, 0x68, 0x8f, 0xf7, 0xb2, 0xdd, 0x16, 0x57, 0x92, 0x54, 0x1c, 0x88,
0xee, 0x76, 0xc8, 0x2e, 0xb7, 0x7d, 0xd6, 0xe9, 0xe7, 0x2c, 0x89, 0xcb, 0xf1, 0xa5, 0x6a, 0x68,
];

#[inline(always)]
pub fn rand_scalar<R: RngCore + CryptoRng>(rng: &mut R) -> Scalar {
Scalar::random(rng)
Expand Down
Loading