#![forbid(unsafe_code)]
use crate::backend::{require_supported, Operation};
use crate::error::{Error, Result};
use crypto_bigint::modular::{BoxedMontyForm, BoxedMontyParams};
use crypto_bigint::{BoxedUint, Choice, CtEq, CtGt, CtLt, NonZero, Odd};
use zeroize::Zeroize;
pub fn compute(
other_public: &[u8],
my_private: &[u8],
p: &[u8],
q: Option<&[u8]>,
) -> Result<Vec<u8>> {
require_supported(Operation::DhAgreement)?;
if p.is_empty() {
return Err(Error::Key("DH modulus p is empty".into()));
}
let q_bytes = strip_leading_zeros(q.ok_or_else(|| {
Error::Key("DH subgroup order q is required for subgroup validation".into())
})?);
let other_public = strip_leading_zeros(other_public);
if my_private.len() > p.len() {
return Err(Error::Key(
"DH private exponent longer than modulus byte length".into(),
));
}
let bits = (p.len() as u32) * 8;
let p_uint = BoxedUint::from_be_slice(p, bits)
.map_err(|e| Error::Key(format!("DH modulus parse: {e:?}")))?;
let zero = BoxedUint::zero_with_precision(bits);
let one = BoxedUint::one_with_precision(bits);
let p_odd = Option::<Odd<BoxedUint>>::from(Odd::new(p_uint.clone()))
.ok_or_else(|| Error::Key("DH modulus p must be odd".into()))?;
let params = BoxedMontyParams::new(p_odd);
let q_uint = BoxedUint::from_be_slice(q_bytes, bits)
.map_err(|e| Error::Key(format!("DH subgroup order q parse: {e:?}")))?;
if !bool::from(q_uint.ct_gt(&one) & q_uint.ct_lt(&p_uint)) {
return Err(Error::Key(
"DH subgroup order q out of range (must satisfy 1 < q < p)".into(),
));
}
let q_nonzero = Option::<NonZero<BoxedUint>>::from(NonZero::new(q_uint.clone()))
.ok_or_else(|| Error::Key("DH subgroup order q out of range".into()))?;
if !bool::from(p_uint.wrapping_sub(&one).rem_vartime(&q_nonzero).is_zero()) {
return Err(Error::Key(
"DH subgroup order q does not divide p - 1".into(),
));
}
let y_uint = BoxedUint::from_be_slice(other_public, bits)
.map_err(|e| Error::Key(format!("DH peer public key parse: {e:?}")))?;
let y_gt_one: Choice = y_uint.ct_gt(&one);
let y_lt_p: Choice = y_uint.ct_lt(&p_uint);
if !bool::from(y_gt_one & y_lt_p) {
return Err(Error::Key(
"DH peer public key out of range (must be in 2..p-1)".into(),
));
}
let y_mont = BoxedMontyForm::new(y_uint, ¶ms);
let subgroup_check = y_mont.pow(&q_uint).retrieve();
if !bool::from(subgroup_check.ct_eq(&one)) {
return Err(Error::Key(
"DH peer public key fails subgroup check (y^q mod p != 1)".into(),
));
}
let mut priv_uint = BoxedUint::from_be_slice(my_private, bits)
.map_err(|e| Error::Key(format!("DH private exponent parse: {e:?}")))?;
let x_in_range: Choice = priv_uint.ct_gt(&zero) & priv_uint.ct_lt(&q_uint);
if !bool::from(x_in_range) {
priv_uint.zeroize();
return Err(Error::Key(
"DH private exponent out of range (must be in 1..q-1)".into(),
));
}
let mut shared_mont = y_mont.pow(&priv_uint);
priv_uint.zeroize();
let mut shared_uint = shared_mont.retrieve();
shared_mont.zeroize();
let mut raw = shared_uint.to_be_bytes();
shared_uint.zeroize();
let out = left_pad_to(&raw, p.len());
raw.zeroize();
Ok(out)
}
fn strip_leading_zeros(input: &[u8]) -> &[u8] {
let start = input
.iter()
.position(|&b| b != 0)
.unwrap_or(input.len().saturating_sub(1));
&input[start..]
}
fn left_pad_to(input: &[u8], target_len: usize) -> Vec<u8> {
if input.len() == target_len {
return input.to_vec();
}
if input.len() < target_len {
let mut out = vec![0u8; target_len - input.len()];
out.extend_from_slice(input);
return out;
}
input[input.len() - target_len..].to_vec()
}
#[cfg(test)]
mod tests {
use super::*;
const RFC5114_GROUP2_P: &str = "\
AD107E1E9123A9D0D660FAA79559C51FA20D64E5683B9FD1B54B1597B61D0A75E6FA141D\
F95A56DBAF9A3C407BA1DF15EB3D688A309C180E1DE6B85A1274A0A66D3F8152AD6AC212\
9037C9EDEFDA4DF8D91E8FEF55B7394B7AD5B7D0B6C12207C9F98D11ED34DBF6C6BA0B2C\
8BBC27BE6A00E0A0B9C49708B3BF8A317091883681286130BC8985DB1602E714415D9330\
278273C7DE31EFDC7310F7121FD5A07415987D9ADC0A486DCDF93ACC44328387315D75E1\
98C641A480CD86A1B9E587E8BE60E69CC928B2B9C52172E413042E9B23F10B0E16E79763\
C9B53DCF4BA80A29E3FB73C16B8E75B97EF363E2FFA31F71CF9DE5384E71B81C0AC4DFFE\
0C10E64F";
const RFC5114_GROUP2_G: &str = "\
AC4032EF4F2D9AE39DF30B5C8FFDAC506CDEBE7B89998CAF74866A08CFE4FFE3A6824A4E\
10B9A6F0DD921F01A70C4AFAAB739D7700C29F52C57DB17C620A8652BE5E9001A8D66AD7\
C17669101999024AF4D027275AC1348BB8A762D0521BC98AE247150422EA1ED409939D54\
DA7460CDB5F6C6B250717CBEF180EB34118E98D119529A45D6F834566E3025E316A330EF\
BB77A86F0C1AB15B051AE3D428C8F8ACB70A8137150B8EEB10E183EDD19963DDD9E263E4\
770589EF6AA21E7F5F2FF381B539CCE3409D13CD566AFBB48D6C019181E1BCFE94B30269\
EDFE72FE9B6AA4BD7B5A0F1C71CFFF4C19C418E1F6EC017981BC087F2A7065B384B890D3\
191F2BFA";
const RFC5114_GROUP2_Q: &str = "801C0D34C58D93FE997177101F80535A4738CEBCBF389A99B36371EB";
fn hex(s: &str) -> Vec<u8> {
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
.collect()
}
#[test]
fn small_prime_roundtrip() {
let p = &[23u8];
let q = &[11u8];
let g = 4u8;
let p_uint = 23u32;
let x_a = 5u8;
let x_b = 7u8;
let y_a = modpow_u32(g as u32, x_a as u32, p_uint) as u8;
let y_b = modpow_u32(g as u32, x_b as u32, p_uint) as u8;
let shared_a = compute(&[y_b], &[x_a], p, Some(q)).unwrap();
let shared_b = compute(&[y_a], &[x_b], p, Some(q)).unwrap();
assert_eq!(shared_a, shared_b);
assert_eq!(shared_a.len(), 1);
}
fn modpow_u32(base: u32, exp: u32, modulus: u32) -> u32 {
let mut result = 1u64;
let mut b = base as u64 % modulus as u64;
let m = modulus as u64;
let mut e = exp;
while e > 0 {
if e & 1 == 1 {
result = (result * b) % m;
}
b = (b * b) % m;
e >>= 1;
}
result as u32
}
#[test]
fn rfc5114_group2_roundtrip() {
let p = hex(RFC5114_GROUP2_P);
let g = hex(RFC5114_GROUP2_G);
let q = hex(RFC5114_GROUP2_Q);
let x_a = {
let mut v = vec![0u8; q.len() - 1];
v.push(0x11);
v
};
let x_b = {
let mut v = vec![0u8; q.len() - 1];
v.push(0x23);
v
};
let y_a = compute(&g, &x_a, &p, Some(&q)).unwrap();
let y_b = compute(&g, &x_b, &p, Some(&q)).unwrap();
assert_eq!(y_a.len(), p.len(), "y_a must be padded to len(p)");
assert_eq!(y_b.len(), p.len(), "y_b must be padded to len(p)");
let shared_a = compute(&y_b, &x_a, &p, Some(&q)).unwrap();
let shared_b = compute(&y_a, &x_b, &p, Some(&q)).unwrap();
assert_eq!(shared_a, shared_b);
assert_eq!(shared_a.len(), p.len());
}
#[test]
fn rejects_y_zero() {
let p = &[23u8];
let q = &[11u8];
let err = compute(&[0u8], &[5u8], p, Some(q)).unwrap_err();
assert!(err.to_string().contains("out of range"), "{err}");
}
#[test]
fn rejects_y_one() {
let p = &[23u8];
let q = &[11u8];
let err = compute(&[1u8], &[5u8], p, Some(q)).unwrap_err();
assert!(err.to_string().contains("out of range"), "{err}");
}
#[test]
fn rejects_y_equal_p() {
let p = &[23u8];
let q = &[11u8];
let err = compute(&[23u8], &[5u8], p, Some(q)).unwrap_err();
assert!(err.to_string().contains("out of range"), "{err}");
}
#[test]
fn rejects_missing_q() {
let p = &[23u8];
let err = compute(&[5u8], &[3u8], p, None).unwrap_err();
assert!(
err.to_string().contains("subgroup order q is required"),
"{err}"
);
}
#[test]
fn rejects_zero_q() {
let p = &[23u8];
let q = &[0u8];
let err = compute(&[4u8], &[1u8], p, Some(q)).unwrap_err();
assert!(err.to_string().contains("q out of range"), "{err}");
}
#[test]
fn rejects_invalid_group_parameters() {
let p = &[23u8];
for q in [&[1u8][..], &[23u8][..]] {
let err = compute(&[4u8], &[1u8], p, Some(q)).unwrap_err();
assert!(err.to_string().contains("q out of range"), "{err}");
}
let err = compute(&[4u8], &[1u8], p, Some(&[7u8])).unwrap_err();
assert!(err.to_string().contains("does not divide"), "{err}");
}
#[test]
fn rejects_private_exponent_out_of_range() {
let p = &[23u8];
let q = &[11u8];
for x in [0u8, 11u8, 12u8] {
let err = compute(&[4u8], &[x], p, Some(q)).unwrap_err();
assert!(
err.to_string().contains("private exponent out of range"),
"{err}"
);
}
}
#[test]
fn accepts_peer_public_with_leading_zero_byte() {
let p = &[23u8];
let q = &[11u8];
let padded = compute(&[0u8, 4u8], &[5u8], p, Some(q)).unwrap();
assert_eq!(padded, compute(&[4u8], &[5u8], p, Some(q)).unwrap());
}
#[test]
fn rejects_bad_subgroup_point() {
let p = &[23u8];
let q = &[11u8];
let err = compute(&[22u8], &[5u8], p, Some(q)).unwrap_err();
assert!(err.to_string().contains("subgroup check"), "{err}");
}
#[test]
fn rejects_even_modulus() {
let p = &[22u8]; let q = &[11u8];
let err = compute(&[5u8], &[3u8], p, Some(q)).unwrap_err();
assert!(err.to_string().contains("must be odd"), "{err}");
}
#[test]
fn rejects_private_longer_than_modulus() {
let p = &[23u8];
let q = &[11u8];
let too_long = &[0u8, 3u8];
let err = compute(&[5u8], too_long, p, Some(q)).unwrap_err();
assert!(
err.to_string()
.contains("private exponent longer than modulus"),
"{err}"
);
}
#[test]
fn output_is_left_padded_to_p_length() {
let p = &[23u8];
let q = &[11u8];
let shared = compute(&[4u8], &[1u8], p, Some(q)).unwrap();
assert_eq!(shared, vec![4u8]);
assert_eq!(shared.len(), p.len());
}
}