Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for NIST P-256 curve #185

Merged
merged 6 commits into from
Feb 17, 2025
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
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ vector-tests = []
hfs = []
pqclean_kyber1024 = ["pqcrypto-kyber", "pqcrypto-traits", "hfs", "default-resolver"]
xchachapoly = ["chacha20poly1305", "default-resolver"]
p256 = ["dep:p256", "default-resolver"]
risky-raw-split = []

[[bench]]
Expand All @@ -45,7 +46,8 @@ aes-gcm = { version = "0.10", optional = true }
chacha20poly1305 = { version = "0.10", optional = true }
blake2 = { version = "0.10", optional = true }
sha2 = { version = "0.10", optional = true }
curve25519-dalek = { version = "4", optional = true }
curve25519-dalek = { version = "4.1.3", optional = true }
p256 = { version = "0.13.2", features = ["ecdh"], optional = true }

pqcrypto-kyber = { version = "0.8", optional = true }
pqcrypto-traits = { version = "0.3", optional = true }
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ crypto implementations when available.
| CSPRNG | ✔ | ✔ |
| 25519 | ✔ | ✔ |
| 448 | | |
| P-256 | ✔ | |
| AESGCM | ✔ | ✔ |
| ChaChaPoly | ✔ | ✔ |
| SHA256 | ✔ | ✔ |
Expand Down
9 changes: 8 additions & 1 deletion src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,16 @@ pub const TAGLEN: usize = 16;

pub const MAXHASHLEN: usize = 64;
pub const MAXBLOCKLEN: usize = 128;
pub const MAXDHLEN: usize = 56;
pub const MAXMSGLEN: usize = 65535;

// P-256 uncompressed SEC-1 encodings are 65 bytes long, larger
// than the `MAXDHLEN` in the official Noise spec.
#[cfg(feature = "p256")]
pub const MAXDHLEN: usize = 65;
// Curve448 keys are the largest in the official Noise spec.
#[cfg(not(feature = "p256"))]
pub const MAXDHLEN: usize = 56;

#[cfg(feature = "hfs")]
pub const MAXKEMPUBLEN: usize = 4096;
#[cfg(feature = "hfs")]
Expand Down
36 changes: 18 additions & 18 deletions src/handshakestate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ impl HandshakeState {
symmetricstate.initialize(&params.name);
symmetricstate.mix_hash(prologue);

let dh_len = s.pub_len();
let pub_len = s.pub_len();
if initiator {
for token in tokens.premsg_pattern_i {
symmetricstate.mix_hash(
Expand All @@ -100,7 +100,7 @@ impl HandshakeState {
_ => unreachable!(),
}
.get()
.ok_or(StateProblem::MissingKeyMaterial)?[..dh_len],
.ok_or(StateProblem::MissingKeyMaterial)?[..pub_len],
);
}
} else {
Expand All @@ -112,7 +112,7 @@ impl HandshakeState {
_ => unreachable!(),
}
.get()
.ok_or(StateProblem::MissingKeyMaterial)?[..dh_len],
.ok_or(StateProblem::MissingKeyMaterial)?[..pub_len],
);
}
for token in tokens.premsg_pattern_r {
Expand Down Expand Up @@ -152,7 +152,7 @@ impl HandshakeState {
}

pub(crate) fn dh_len(&self) -> usize {
self.s.pub_len()
self.s.dh_len()
}

#[cfg(feature = "hfs")]
Expand Down Expand Up @@ -356,39 +356,39 @@ impl HandshakeState {
}
let last = self.pattern_position == (self.message_patterns.len() - 1);

let dh_len = self.dh_len();
let pub_len = self.e.pub_len();
let mut ptr = message;
for token in &self.message_patterns[self.pattern_position] {
match *token {
Token::E => {
if ptr.len() < dh_len {
if ptr.len() < pub_len {
return Err(Error::Input);
}
self.re[..dh_len].copy_from_slice(&ptr[..dh_len]);
ptr = &ptr[dh_len..];
self.symmetricstate.mix_hash(&self.re[..dh_len]);
self.re[..pub_len].copy_from_slice(&ptr[..pub_len]);
ptr = &ptr[pub_len..];
self.symmetricstate.mix_hash(&self.re[..pub_len]);
if self.params.handshake.is_psk() {
self.symmetricstate.mix_key(&self.re[..dh_len]);
self.symmetricstate.mix_key(&self.re[..pub_len]);
}
self.re.enable();
},
Token::S => {
let data = if self.symmetricstate.has_key() {
if ptr.len() < dh_len + TAGLEN {
if ptr.len() < pub_len + TAGLEN {
return Err(Error::Input);
}
let temp = &ptr[..dh_len + TAGLEN];
ptr = &ptr[dh_len + TAGLEN..];
let temp = &ptr[..pub_len + TAGLEN];
ptr = &ptr[pub_len + TAGLEN..];
temp
} else {
if ptr.len() < dh_len {
if ptr.len() < pub_len {
return Err(Error::Input);
}
let temp = &ptr[..dh_len];
ptr = &ptr[dh_len..];
let temp = &ptr[..pub_len];
ptr = &ptr[pub_len..];
temp
};
self.symmetricstate.decrypt_and_mix_hash(data, &mut self.rs[..dh_len])?;
self.symmetricstate.decrypt_and_mix_hash(data, &mut self.rs[..pub_len])?;
self.rs.enable();
},
Token::Psk(n) => match self.psks[n as usize] {
Expand Down Expand Up @@ -472,7 +472,7 @@ impl HandshakeState {
/// pattern, for example).
#[must_use]
pub fn get_remote_static(&self) -> Option<&[u8]> {
self.rs.get().map(|rs| &rs[..self.dh_len()])
self.rs.get().map(|rs| &rs[..self.s.pub_len()])
}

/// Get the handshake hash.
Expand Down
14 changes: 13 additions & 1 deletion src/params/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,13 @@ impl FromStr for BaseChoice {
/// Which Diffie-Hellman primitive to use. One of `25519` or `448`, per the spec.
#[derive(PartialEq, Copy, Clone, Debug)]
pub enum DHChoice {
/// The Curve25519 ellpitic curve.
/// The Curve25519 elliptic curve.
Curve25519,
/// The Curve448 elliptic curve.
Curve448,
#[cfg(feature = "p256")]
/// The P-256 elliptic curve.
P256,
}

impl FromStr for DHChoice {
Expand All @@ -51,6 +54,8 @@ impl FromStr for DHChoice {
match s {
"25519" => Ok(Curve25519),
"448" => Ok(Curve448),
#[cfg(feature = "p256")]
"P256" => Ok(P256),
_ => Err(PatternProblem::UnsupportedDhType.into()),
}
}
Expand Down Expand Up @@ -270,6 +275,13 @@ mod tests {
assert!(p.handshake.modifiers.list.is_empty());
}

#[test]
#[cfg(feature = "p256")]
fn test_p256() {
let p: NoiseParams = "Noise_XX_P256_AESGCM_SHA256".parse().unwrap();
assert_eq!(p.dh, DHChoice::P256);
}

#[test]
fn test_basic_deferred() {
let p: NoiseParams = "Noise_X1X1_25519_AESGCM_SHA256".parse().unwrap();
Expand Down
97 changes: 97 additions & 0 deletions src/resolvers/default.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ use blake2::{Blake2b, Blake2b512, Blake2s, Blake2s256};
use chacha20poly1305::XChaCha20Poly1305;
use chacha20poly1305::{aead::AeadInPlace, ChaCha20Poly1305, KeyInit};
use curve25519_dalek::montgomery::MontgomeryPoint;
#[cfg(feature = "p256")]
use p256::{self, elliptic_curve::sec1::ToEncodedPoint, EncodedPoint};
#[cfg(feature = "pqclean_kyber1024")]
use pqcrypto_kyber::kyber1024;
#[cfg(feature = "pqclean_kyber1024")]
Expand Down Expand Up @@ -38,6 +40,8 @@ impl CryptoResolver for DefaultResolver {
match *choice {
DHChoice::Curve25519 => Some(Box::<Dh25519>::default()),
DHChoice::Curve448 => None,
#[cfg(feature = "p256")]
DHChoice::P256 => Some(Box::<P256>::default()),
}
}

Expand Down Expand Up @@ -74,6 +78,14 @@ struct Dh25519 {
pubkey: [u8; 32],
}

/// Wraps p256
#[cfg(feature = "p256")]
#[derive(Default)]
struct P256 {
privkey: [u8; 32],
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason not to store privkey as a p256::SecretKey here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I gave this a try again, as I couldn't remember why - it seems it's not trivial:

error[E0515]: cannot return reference to temporary value
   --> src/resolvers/default.rs:241:9
    |
241 |         &self.pubkey.to_sec1_bytes()
    |         ^---------------------------
    |         ||
    |         |temporary value created here
    |         returns a reference to data owned by the current function

The issue is that the Dh trait requires exporting slices:

pub trait Dh: Send + Sync {
    fn pubkey(&self) -> &[u8];
    fn privkey(&self) -> &[u8];

However, p256::SecretKey and p256::PublicKey only return owned types (eg. FieldBytes<...>, or GenericArray<u8, ...>).

Pre-serializing the keys to their byte representation allows keeping the trait unchanged.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also took a quick look and agree this isn't a simple drop-in; the key types available only provide copied-array access to the inner bytes because they are stored as a "generic" field representation.

FWIW I have run into similar issues before with the use of slices for everything in the Dh implementations. It was when attempting to add support for ECDH directly in the ring provider because those also don't (at all) provide access to a slice representation of private keys.

Copy link
Owner

@mcginty mcginty Feb 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it -- that makes sense to keep it as-is then!

I can address this more holistically in a large rework of this trait later. It doesn't make sense to keep these slices if they're holding us back in the long run.

pubkey: EncodedPoint,
}

/// Wraps `aes-gcm`'s AES256-GCM implementation.
#[derive(Default)]
struct CipherAesGcm {
Expand Down Expand Up @@ -175,6 +187,67 @@ impl Dh for Dh25519 {
}
}

#[cfg(feature = "p256")]
impl P256 {
fn derive_pubkey(&mut self) {
let secret_key = p256::SecretKey::from_bytes(&self.privkey.into()).unwrap();
let public_key = secret_key.public_key();
let encoded_pub = public_key.to_encoded_point(false);
self.pubkey = encoded_pub;
}
}

#[cfg(feature = "p256")]
impl Dh for P256 {
fn name(&self) -> &'static str {
"P256"
}

fn pub_len(&self) -> usize {
65 // Uncompressed SEC-1 encoding
}

fn priv_len(&self) -> usize {
32 // Scalar
}

fn dh_len(&self) -> usize {
32
}

fn set(&mut self, privkey: &[u8]) {
let mut bytes = [0u8; 32];
copy_slices!(privkey, bytes);
self.privkey = bytes;
self.derive_pubkey();
}

fn generate(&mut self, rng: &mut dyn Random) {
let mut bytes = [0u8; 32];
rng.fill_bytes(&mut bytes);
self.privkey = bytes;
self.derive_pubkey();
}

fn pubkey(&self) -> &[u8] {
self.pubkey.as_bytes()
}

fn privkey(&self) -> &[u8] {
&self.privkey
}

fn dh(&self, pubkey: &[u8], out: &mut [u8]) -> Result<(), Error> {
let secret_key = p256::SecretKey::from_bytes(&self.privkey.into()).or(Err(Error::Dh))?;
let secret_key_scalar = secret_key.to_nonzero_scalar();
let pub_key: p256::elliptic_curve::PublicKey<p256::NistP256> =
p256::PublicKey::from_sec1_bytes(&pubkey).or(Err(Error::Dh))?;
let dh_output = p256::ecdh::diffie_hellman(secret_key_scalar, pub_key.as_affine());
copy_slices!(dh_output.raw_secret_bytes(), out);
Ok(())
}
}

impl Cipher for CipherAesGcm {
fn name(&self) -> &'static str {
"AESGCM"
Expand Down Expand Up @@ -613,6 +686,30 @@ mod tests {
);
}

#[test]
#[cfg(feature = "p256")]
fn test_p256() {
// Test vector from RFC 5903, section 8.1
let mut keypair = P256::default();
let scalar =
Vec::<u8>::from_hex("C88F01F510D9AC3F70A292DAA2316DE544E9AAB8AFE84049C62A9C57862D1433")
.unwrap();
keypair.set(&scalar);
// Public key is prefixed with 0x04, to indicate uncompressed form, as per SEC1 encoding
let public = Vec::<u8>::from_hex(
"04\
D12DFB5289C8D4F81208B70270398C342296970A0BCCB74C736FC7554494BF63\
56FBF3CA366CC23E8157854C13C58D6AAC23F046ADA30F8353E74F33039872AB",
)
.unwrap();
let mut output = [0u8; 32];
keypair.dh(&public, &mut output).unwrap();
assert_eq!(
hex::encode(output),
"D6840F6B42F6EDAFD13116E0E12565202FEF8E9ECE7DCE03812464D04B9442DE".to_lowercase()
);
}

#[test]
fn test_aesgcm() {
// AES256-GCM tests - gcm-spec.pdf
Expand Down
5 changes: 5 additions & 0 deletions src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ pub trait Dh: Send + Sync {
/// # Errors
/// Returns `Error::Dh` in the event that the Diffie-Hellman failed.
fn dh(&self, pubkey: &[u8], out: &mut [u8]) -> Result<(), Error>;

/// The lenght in bytes of of the DH key exchange. Defaults to the public key.
fn dh_len(&self) -> usize {
self.pub_len()
}
}

/// Cipher operations
Expand Down