From 7627be5bf5ca2df18368a87ad577a6341268a5ce Mon Sep 17 00:00:00 2001 From: jmwample <8297368+jmwample@users.noreply.github.com> Date: Tue, 11 Jun 2024 17:07:03 -0600 Subject: [PATCH] interim commit --- crates/obfs4/Cargo.toml | 18 +- crates/obfs4/src/common/curve25519.rs | 194 +++++++++++++----- crates/obfs4/src/framing/handshake.rs | 2 +- .../obfs4/src/handshake/handshake_client.rs | 10 +- .../obfs4/src/handshake/handshake_server.rs | 7 +- 5 files changed, 162 insertions(+), 69 deletions(-) diff --git a/crates/obfs4/Cargo.toml b/crates/obfs4/Cargo.toml index 9b2ef33..2ffc4ad 100644 --- a/crates/obfs4/Cargo.toml +++ b/crates/obfs4/Cargo.toml @@ -18,6 +18,10 @@ debug = ["ptrs/debug"] name = "obfs4" crate-type = ["cdylib", "rlib"] +[[bench]] +name = "nacl" +harness = false + [dependencies] ## Local ptrs = { path="../ptrs", version="0.1.0" } @@ -34,9 +38,10 @@ siphasher = "1.0.0" sha2 = "0.10.8" hmac = { version="0.12.1", features=["reset"]} hkdf = "0.12.3" -crypto_secretbox = { version="0.1.1", features=["salsa20"]} +crypto_secretbox = { version="0.1.1", features=["salsa20", "heapless"]} subtle = "2.5.0" -x25519-dalek = { version = "2.0.1", features = ["static_secrets", "getrandom", "reusable_secrets", "elligator2"], git = "https://github.com/jmwample/curve25519-dalek.git", branch = "elligator2-ntor"} +x25519-dalek = { version = "2.0.1", features = ["static_secrets", "getrandom", "reusable_secrets"]} +# x25519-dalek = { version = "2.0.1", features = ["static_secrets", "getrandom", "reusable_secrets", "elligator2"], git = "https://github.com/jmwample/curve25519-dalek.git", branch = "interim-crate"} ## Utils hex = "0.4.3" @@ -64,7 +69,9 @@ thiserror = "1.0.56" ## transitive dependencies that break things when versions are too low ## i.e. any lower than the exact versions here. -curve25519-dalek = { version="4.1.2", optional=true} +curve25519-dalek = { version="4.1", optional=true} +curve25519-elligator2 = { version="4.1.2", features=["elligator2"], path = "../../../../../dalek-crypto/curve25519-dalek/curve25519-elligator2"} # , git = "https://github.com/jmwample/curve25519-dalek.git", branch = "interim-crate" + anyhow = { version="1.0.20", optional=true} async-trait = { version="0.1.9", optional=true} num-bigint = { version="0.4.2", optional=true} @@ -80,7 +87,10 @@ tracing-subscriber = "0.3.18" hex-literal = "0.4.1" tor-basic-utils = "0.18.0" -# o5 pqc test +# benches +criterion = "0.5" + +# # o5 pqc test # pqc_kyber = {version="0.7.1", features=["kyber1024", "std"]} # ml-kem = "0.1.0" diff --git a/crates/obfs4/src/common/curve25519.rs b/crates/obfs4/src/common/curve25519.rs index 5bdd713..1ee28e0 100644 --- a/crates/obfs4/src/common/curve25519.rs +++ b/crates/obfs4/src/common/curve25519.rs @@ -4,18 +4,132 @@ //! key-agreement trait, but for now we are just re-using the APIs from //! [`x25519_dalek`]. -// TODO: We may want eventually want to expose ReusableSecret instead of -// StaticSecret, for use in places where we need to use a single secret -// twice in one handshake, but we do not need that secret to be persistent. -// -// The trouble here is that if we use ReusableSecret in these cases, we -// cannot easily construct it for testing purposes. We could in theory -// kludge something together using a fake Rng, but that might be more -// trouble than we want to go looking for. #[allow(unused)] -pub use x25519_dalek::{ - EphemeralSecret, PublicKey, PublicRepresentative, ReusableSecret, SharedSecret, StaticSecret, -}; +pub use x25519_dalek::{PublicKey, SharedSecret, StaticSecret}; +pub use curve25519_elligator2::elligator2::representative_from_privkey; + +pub(crate) struct EphemeralSecret (x25519_dalek::EphemeralSecret, u8); + +impl EphemeralSecret { + pub(crate) fn random() -> Self {} + + pub(crate) fn random_from_rng(csprng: T) -> Self {} + + pub(crate) fn diffie_hellman() -> Self {} + + pub fn diffie_hellman(self, their_public: &PublicKey) -> SharedSecret {} +} + +impl From for PublicKey { + fn from(value: EphemeralSecret) -> Self { + + } +} + +/// [`PublicKey`] transformation to a format indistinguishable from uniform +/// random. Requires feature `elligator2`. +/// +/// This allows public keys to be sent over an insecure channel without +/// revealing that an x25519 public key is being shared. +/// +/// # Example +#[cfg_attr(feature = "elligator2", doc = "```")] +#[cfg_attr(not(feature = "elligator2"), doc = "```ignore")] +/// use rand_core::OsRng; +/// use rand_core::RngCore; +/// +/// use x25519_dalek::x25519; +/// use x25519_ephemeralSecret; +/// use x25519_dalek::{PublicKey, PublicRepresentative}; +/// +/// // ~50% of points are not encodable as elligator representatives, but we +/// // want to ensure we select a keypair that is. +/// fn get_representable_ephemeral() -> EphemeralSecret { +/// for i in 0_u8..255 { +/// let secret = EphemeralSecret::random_from_rng(&mut OsRng); +/// match Option::::from(&secret) { +/// Some(_) => return secret, +/// None => continue, +/// } +/// } +/// panic!("we should definitely have found a key by now") +/// } +/// +/// // Generate Alice's key pair. +/// let alice_secret = get_representable_ephemeral(); +/// let alice_representative = Option::::from(&alice_secret).unwrap(); +/// +/// // Generate Bob's key pair. +/// let bob_secret = get_representable_ephemeral(); +/// let bob_representative = Option::::from(&bob_secret).unwrap(); +/// +/// // Alice and Bob should now exchange their representatives and reveal the +/// // public key from the other person. +/// let bob_public = PublicKey::from(&bob_representative); +/// +/// let alice_public = PublicKey::from(&alice_representative); +/// +/// // Once they've done so, they may generate a shared secret. +/// let alice_shared = alice_secret.diffie_hellman(&bob_public); +/// let bob_shared = bob_secret.diffie_hellman(&alice_public); +/// +/// assert_eq!(alice_shared.as_bytes(), bob_shared.as_bytes()); +/// ``` +#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)] +pub struct PublicRepresentative([u8; 32]); + +impl PublicRepresentative { + /// View this public representative as a byte array. + #[inline] + pub fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } + + /// Extract this representative's bytes for serialization. + #[inline] + pub fn to_bytes(&self) -> [u8; 32] { + self.0 + } +} + +impl AsRef<[u8]> for PublicRepresentative { + /// View this shared secret key as a byte array. + #[inline] + fn as_ref(&self) -> &[u8] { + self.as_bytes() + } +} + +impl From<[u8; 32]> for PublicRepresentative { + /// Build a Elligator2 Public key Representative from bytes + fn from(r: [u8; 32]) -> PublicRepresentative { + PublicRepresentative(r) + } +} + +impl<'a> From<&'a [u8; 32]> for PublicRepresentative { + /// Build a Elligator2 Public key Representative from bytes by reference + fn from(r: &'a [u8; 32]) -> PublicRepresentative { + PublicRepresentative(*r) + } +} + +impl<'a> From<&'a EphemeralSecret> for Option { + /// Given an x25519 [`EphemeralSecret`] key, compute its corresponding [`PublicRepresentative`]. + fn from(secret: &'a EphemeralSecret) -> Option { + let repres = representative_from_privkey(&secret.0, secret.1); + let res: Option<[u8; 32]> = repres; + Some(PublicRepresentative(res?)) + } +} + +impl<'a> From<&'a PublicRepresentative> for PublicKey { + /// Given an elligator2 [`PublicRepresentative`], compute its corresponding [`PublicKey`]. + fn from(representative: &'a PublicRepresentative) -> PublicKey { + let point = curve25519_elligator2::MontgomeryPoint::map_to_point(&representative.0); + PublicKey::from(*point.as_bytes()) + } +} use rand_core::{CryptoRng, RngCore}; @@ -28,18 +142,22 @@ pub const REPRESENTATIVE_LENGTH: usize = 32; /// The probablility that a key does not have a representable elligator2 encoding /// is ~50%, so we are (statistiscally) guaranteed to find a representable key /// in relatively few iterations. -pub struct Representable; +pub struct Keys; + +trait RetryLimit { + const RETRY_LIMIT: usize = 128; +} -const RETRY_LIMIT: usize = 128; +impl RetryLimit for Keys {} #[allow(unused)] -impl Representable { +impl Keys { /// Generate a new Elligator2 representable ['StaticSecret'] with the supplied RNG. pub fn static_from_rng(mut rng: R) -> StaticSecret { let mut private = StaticSecret::random_from_rng(&mut rng); let mut repres: Option = (&private).into(); - for _ in 0..RETRY_LIMIT { + for _ in 0..Self::RETRY_LIMIT { if repres.is_some() { return private; } @@ -50,28 +168,12 @@ impl Representable { panic!("failed to generate representable secret, bad RNG provided"); } - /// Generate a new Elligator2 representable ['ReusableSecret'] with the supplied RNG. - pub fn reusable_from_rng(mut rng: R) -> ReusableSecret { - let mut private = ReusableSecret::random_from_rng(&mut rng); - let mut repres: Option = (&private).into(); - - for _ in 0..RETRY_LIMIT { - if repres.is_some() { - return private; - } - private = ReusableSecret::random_from_rng(&mut rng); - repres = (&private).into(); - } - - panic!("failed to generate representable secret, bad RNG provided"); - } - /// Generate a new Elligator2 representable ['EphemeralSecret'] with the supplied RNG. pub fn ephemeral_from_rng(mut rng: R) -> EphemeralSecret { let mut private = EphemeralSecret::random_from_rng(&mut rng); let mut repres: Option = (&private).into(); - for _ in 0..RETRY_LIMIT { + for _ in 0..Self::RETRY_LIMIT { if repres.is_some() { return private; } @@ -87,7 +189,7 @@ impl Representable { let mut private = StaticSecret::random(); let mut repres: Option = (&private).into(); - for _ in 0..RETRY_LIMIT { + for _ in 0..Self::RETRY_LIMIT { if repres.is_some() { return private; } @@ -98,28 +200,12 @@ impl Representable { panic!("failed to generate representable secret, getrandom failed"); } - /// Generate a new Elligator2 representable ['ReusableSecret']. - pub fn random_reusable() -> ReusableSecret { - let mut private = ReusableSecret::random(); - let mut repres: Option = (&private).into(); - - for _ in 0..RETRY_LIMIT { - if repres.is_some() { - return private; - } - private = ReusableSecret::random(); - repres = (&private).into(); - } - - panic!("failed to generate representable secret, getrandom failed"); - } - /// Generate a new Elligator2 representable ['EphemeralSecret']. pub fn random_ephemeral() -> EphemeralSecret { let mut private = EphemeralSecret::random(); let mut repres: Option = (&private).into(); - for _ in 0..RETRY_LIMIT { + for _ in 0..Self::RETRY_LIMIT { if repres.is_some() { return private; } @@ -131,6 +217,8 @@ impl Representable { } } + + #[cfg(test)] mod test { use super::*; @@ -138,17 +226,13 @@ mod test { #[test] fn representative_match() { - let mut repres = <[u8; 32]>::from_hex( + let repres = <[u8; 32]>::from_hex( "8781b04fefa49473ca5943ab23a14689dad56f8118d5869ad378c079fd2f4079", ) .unwrap(); let incorrect = "1af2d7ac95b5dd1ab2b5926c9019fa86f211e77dd796f178f3fe66137b0d5d15"; let expected = "a946c3dd16d99b8c38972584ca599da53e32e8b13c1e9a408ff22fdb985c2d79"; - // we are not clearing the high order bits before translating the representative to a - // public key. - repres[31] &= 0x3f; - let r = PublicRepresentative::from(repres); let p = PublicKey::from(&r); assert_ne!(incorrect, hex::encode(p.as_bytes())); diff --git a/crates/obfs4/src/framing/handshake.rs b/crates/obfs4/src/framing/handshake.rs index fdab8fd..025d225 100644 --- a/crates/obfs4/src/framing/handshake.rs +++ b/crates/obfs4/src/framing/handshake.rs @@ -140,7 +140,7 @@ impl ClientHandshakeMessage { pub fn marshall(&mut self, buf: &mut impl BufMut, mut h: HmacSha256) -> Result<()> { trace!("serializing client handshake"); - h.reset(); // disambiguate reset() implementations Mac v digest + h.reset(); h.update(self.repres.as_bytes().as_ref()); let mark: &[u8] = &h.finalize_reset().into_bytes()[..MARK_LENGTH]; diff --git a/crates/obfs4/src/handshake/handshake_client.rs b/crates/obfs4/src/handshake/handshake_client.rs index 76d2324..1a90edc 100644 --- a/crates/obfs4/src/handshake/handshake_client.rs +++ b/crates/obfs4/src/handshake/handshake_client.rs @@ -187,12 +187,12 @@ fn try_parse( let mut h = HmacSha256::new_from_slice(&key[..]).unwrap(); h.reset(); // disambiguate reset() implementations Mac v digest - let mut r_bytes: [u8; 32] = buf[0..REPRESENTATIVE_LENGTH].try_into().unwrap(); + let r_bytes: [u8; 32] = buf[0..REPRESENTATIVE_LENGTH].try_into().unwrap(); h.update(&r_bytes); - // clear the inconsistent elligator2 bits of the representative after - // using the wire format for deriving the mark - r_bytes[31] &= 0x3f; + // The elligator library internally clears the high-order bits of the + // representative to force a LSR value, but we use the wire format for + // deriving the mark (i.e. without cleared bits). let server_repres = PublicRepresentative::from(r_bytes); let server_auth: [u8; AUTHCODE_LENGTH] = buf[REPRESENTATIVE_LENGTH..REPRESENTATIVE_LENGTH + AUTHCODE_LENGTH].try_into()?; @@ -223,8 +223,6 @@ fn try_parse( hex::encode(mac_received) ); if mac_calculated.ct_eq(mac_received).into() { - let mut r_bytes = server_repres.to_bytes(); - r_bytes[31] &= 0x3f; return Ok(( ServerHandshakeMessage::new(server_repres, server_auth, state.epoch_hr.clone()), pos + MARK_LENGTH + MAC_LENGTH, diff --git a/crates/obfs4/src/handshake/handshake_server.rs b/crates/obfs4/src/handshake/handshake_server.rs index d016c49..9303c73 100644 --- a/crates/obfs4/src/handshake/handshake_server.rs +++ b/crates/obfs4/src/handshake/handshake_server.rs @@ -197,13 +197,14 @@ impl Server { Err(Error::HandshakeErr(RelayHandshakeError::EAgain))?; } - let mut r_bytes: [u8; 32] = buf[0..REPRESENTATIVE_LENGTH].try_into().unwrap(); + let r_bytes: [u8; 32] = buf[0..REPRESENTATIVE_LENGTH].try_into().unwrap(); // derive the mark based on the literal bytes on the wire h.update(&r_bytes[..]); - // clear the bits that are unreliable (and randomized) for elligator2 - r_bytes[31] &= 0x3f; + // The elligator library internally clears the high-order bits of the + // representative to force a LSR value, but we use the wire format for + // deriving the mark (i.e. without cleared bits). let repres = PublicRepresentative::from(&r_bytes); let m = h.finalize_reset().into_bytes();