diff --git a/Cargo.lock b/Cargo.lock index 4907642..96eb5ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,16 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -11,6 +21,39 @@ dependencies = [ "libc", ] +[[package]] +name = "async-stream" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.15", +] + +[[package]] +name = "async-trait" +version = "0.1.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.15", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -50,6 +93,12 @@ version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + [[package]] name = "cc" version = "1.0.79" @@ -62,6 +111,30 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + [[package]] name = "chrono" version = "0.4.24" @@ -77,6 +150,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + [[package]] name = "codespan-reporting" version = "0.11.1" @@ -115,6 +199,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] @@ -227,6 +312,12 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + [[package]] name = "generic-array" version = "0.14.7" @@ -278,6 +369,15 @@ dependencies = [ "cxx-build", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "js-sys" version = "0.3.61" @@ -297,6 +397,8 @@ checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317" name = "libepf" version = "0.1.0" dependencies = [ + "async-trait", + "chacha20poly1305", "chrono", "ed25519-dalek", "hex", @@ -306,6 +408,8 @@ dependencies = [ "serde", "serde_arrays", "sha2", + "tokio", + "tokio-test", "x25519-dalek", ] @@ -358,6 +462,12 @@ version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + [[package]] name = "packed_simd_2" version = "0.3.8" @@ -384,6 +494,12 @@ dependencies = [ "serde", ] +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + [[package]] name = "pkcs8" version = "0.10.2" @@ -400,6 +516,17 @@ version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3d7ddaed09e0eb771a79ab0fd64609ba0afb0a8366421957936ad14cbd13630" +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -540,9 +667,9 @@ dependencies = [ [[package]] name = "subtle" -version = "2.5.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" @@ -586,6 +713,42 @@ dependencies = [ "winapi", ] +[[package]] +name = "tokio" +version = "1.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c786bf8134e5a3a166db9b29ab8f48134739014a3eca7bc6bfa95d673b136f" +dependencies = [ + "autocfg", + "bytes", + "pin-project-lite", + "windows-sys", +] + +[[package]] +name = "tokio-stream" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-test" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53474327ae5e166530d17f2d956afcb4f8a004de581b3cae10f12006bc8163e3" +dependencies = [ + "async-stream", + "bytes", + "futures-core", + "tokio", + "tokio-stream", +] + [[package]] name = "typenum" version = "1.16.0" @@ -604,6 +767,16 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +[[package]] +name = "universal-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d3160b73c9a19f7e2939a2fdad446c57c1bbbbf4d919d3213ff1267a580d8b5" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "version_check" version = "0.9.4" @@ -716,6 +889,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-targets" version = "0.48.0" diff --git a/libepf/Cargo.toml b/libepf/Cargo.toml index d8d0b79..4828feb 100644 --- a/libepf/Cargo.toml +++ b/libepf/Cargo.toml @@ -15,4 +15,10 @@ sha2 = "0.10" ed25519-dalek = { version = "2.0.0-rc.2", features = ["rand_core"] } rand = "0.8" x25519-dalek = "2.0.0-rc.2" -chrono = "0.4" \ No newline at end of file +chrono = "0.4" +tokio = { version = "1.28", features = ["io-util"] } +async-trait = "0.1" +chacha20poly1305 = "0.10" + +[dev-dependencies] +tokio-test = "0.4" \ No newline at end of file diff --git a/libepf/src/ca_pool.rs b/libepf/src/ca_pool.rs index d18799e..bd81034 100644 --- a/libepf/src/ca_pool.rs +++ b/libepf/src/ca_pool.rs @@ -1,5 +1,9 @@ -use crate::pki::{EPFCertificate, EpfPublicKey}; +use crate::pki::{EPFCertificate, EpfPkiSerializable, EpfPublicKey}; use std::collections::HashMap; +use std::error::Error; +use std::ffi::{OsStr}; +use std::fmt::{Display, Formatter}; +use std::fs; pub struct EpfCaPool { pub ca_lookup_table: HashMap, @@ -27,3 +31,36 @@ impl EpfCaPoolOps for EpfCaPool { .insert(cert.details.public_key, cert.clone()); } } + +#[derive(Debug)] +pub enum EpfCaPoolLoaderError { + CertDirDoesNotExist(String) +} +impl Display for EpfCaPoolLoaderError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + EpfCaPoolLoaderError::CertDirDoesNotExist(d) => write!(f, "Certificate dir does not exist: {}", d) + } + } +} +impl Error for EpfCaPoolLoaderError {} + +#[cfg(unix)] +pub fn load_ca_pool() -> Result> { + let mut cert_strings = vec![]; + + for entry in fs::read_dir("/etc/e3pf/certs")? { + let entry = entry?; + if entry.path().extension() == Some(OsStr::new(".pem")) { + cert_strings.push(fs::read_to_string(entry.path())?); + } + } + + let mut ca_pool = EpfCaPool::new(); + + for cert in cert_strings { + ca_pool.insert(&EPFCertificate::from_pem(cert.as_bytes())?); + } + + Ok(ca_pool) +} \ No newline at end of file diff --git a/libepf/src/error.rs b/libepf/src/error.rs new file mode 100644 index 0000000..92b2849 --- /dev/null +++ b/libepf/src/error.rs @@ -0,0 +1,26 @@ +use std::error::Error; +use std::fmt::{Display, Formatter}; +use crate::pki::EpfPkiCertificateValidationError; + +#[derive(Debug)] +pub enum EpfHandshakeError { + AlreadyTunnelled, + UnsupportedProtocolVersion(usize), + InvalidCertificate(EpfPkiCertificateValidationError), + UntrustedCertificate, + EncryptionError, + MissingKeyProof +} +impl Display for EpfHandshakeError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + EpfHandshakeError::AlreadyTunnelled => write!(f, "Already tunneled"), + EpfHandshakeError::UnsupportedProtocolVersion(v) => write!(f, "Unsupported protocol version {}", v), + EpfHandshakeError::InvalidCertificate(e) => write!(f, "Invalid certificate: {}", e), + EpfHandshakeError::UntrustedCertificate => write!(f, "Certificate valid but not trusted"), + EpfHandshakeError::EncryptionError => write!(f, "Encryption error"), + EpfHandshakeError::MissingKeyProof => write!(f, "Missing key proof") + } + } +} +impl Error for EpfHandshakeError {} \ No newline at end of file diff --git a/libepf/src/handshake_stream.rs b/libepf/src/handshake_stream.rs new file mode 100644 index 0000000..d22bca5 --- /dev/null +++ b/libepf/src/handshake_stream.rs @@ -0,0 +1,551 @@ +use std::error::Error; +use std::io; +use async_trait::async_trait; +use chacha20poly1305::{AeadCore, Key, KeyInit, XChaCha20Poly1305, XNonce}; +use chacha20poly1305::aead::{Aead, Payload}; +use ed25519_dalek::{SigningKey}; +use rand::Rng; +use rand::rngs::OsRng; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use x25519_dalek::x25519; +use crate::ca_pool::{load_ca_pool}; +use crate::error::EpfHandshakeError; +use crate::pki::{EPFCertificate, EpfPkiCertificateOps, EpfPrivateKey, EpfPublicKey}; +use crate::protocol::{encode_packet, EpfApplicationData, EpfClientHello, EpfClientState, EpfFinished, EpfMessage, EpfServerHello, EpfServerState, PACKET_APPLICATION_DATA, PACKET_CLIENT_HELLO, PACKET_FINISHED, PACKET_SERVER_HELLO, PROTOCOL_VERSION, recv_packet}; + +///// CLIENT ///// + +#[derive(Clone)] +pub struct EpfClientUpgraded { + inner: T, + state: EpfClientState, + client_random: [u8; 24], + server_random: [u8; 16], + client_cert: Option, + packet_queue: Vec, + server_cert: Option, + cipher: Option, + private_key: EpfPrivateKey, + public_key: EpfPublicKey +} + +pub enum ClientAuthentication { + Cert(Box, EpfPrivateKey), + Ephemeral +} + +#[async_trait] +pub trait EpfClientUpgradable { + async fn upgrade(self, auth: ClientAuthentication) -> EpfClientUpgraded where Self: Sized + AsyncWriteExt + AsyncReadExt + Send; +} + +#[async_trait] +impl EpfClientUpgradable for T where T: AsyncWriteExt + AsyncReadExt + Send { + async fn upgrade(self, auth: ClientAuthentication) -> EpfClientUpgraded where Self: Sized + AsyncWriteExt + AsyncReadExt + Send { + let private_key; + let public_key: [u8; 32]; + let cert; + + match auth { + ClientAuthentication::Cert(cert_d, key) => { + cert = Some(cert_d); + private_key = key; + public_key = key[32..].try_into().unwrap(); + }, + ClientAuthentication::Ephemeral => { + cert = None; + let private_key_l: [u8; 32] = OsRng.gen(); + let private_key_real = SigningKey::from(private_key_l); + public_key = *private_key_real.verifying_key().as_bytes(); + private_key = private_key_real.to_keypair_bytes(); + } + } + + EpfClientUpgraded { + inner: self, + state: EpfClientState::NotStarted, + client_random: OsRng.gen(), + server_random: [0u8; 16], + client_cert: cert.map(|u| *u), + server_cert: None, + packet_queue: vec![], + cipher: None, + private_key, + public_key, + } + } +} + +#[async_trait] +pub trait EpfClientHandshaker { + async fn handshake(&mut self) -> Result<(), Box>; + async fn upgrade(self) -> EpfClientStream where Self: Sized; +} + +#[async_trait] +impl EpfClientHandshaker for EpfClientUpgraded { + async fn handshake(&mut self) -> Result<(), Box> { + match self.state { + EpfClientState::NotStarted => (), + _ => return Err(EpfHandshakeError::AlreadyTunnelled.into()) + } + + // Step 0: Load Trusted Cert Store + let cert_pool = load_ca_pool()?; + + // Step 1: Send Client Hello + self.inner.write_all(&encode_packet(PACKET_CLIENT_HELLO, &EpfClientHello { + protocol_version: PROTOCOL_VERSION, + client_random: self.client_random, + client_certificate: self.client_cert.clone(), + client_public_key: self.public_key, + })?).await?; + + self.state = EpfClientState::WaitingForServerHello; + + // Step 2: Wait for Server Hello + loop { + let packet = recv_packet(&mut self.inner).await?; + + if packet.packet_id != PACKET_SERVER_HELLO { + self.packet_queue.push(packet); + continue; + } + + let server_hello: EpfServerHello = rmp_serde::from_slice(&packet.packet_data)?; + + self.server_random = server_hello.server_random; + + if server_hello.protocol_version != PROTOCOL_VERSION { + return Err(EpfHandshakeError::UnsupportedProtocolVersion(server_hello.protocol_version as usize).into()); + } + + self.server_cert = Some(server_hello.server_certificate); + + break; + } + + // Step 3: Validate Server Certificate + let cert_valid = self.server_cert.as_ref().unwrap().verify(&cert_pool); + if let Err(e) = cert_valid { + return Err(EpfHandshakeError::InvalidCertificate(e).into()) + } + if let Ok(false) = cert_valid { + return Err(EpfHandshakeError::UntrustedCertificate.into()) + } + // Server Cert OK + + // Step 4: Build the cipher + let shared_key = x25519(self.private_key[..32].try_into().unwrap(), self.server_cert.as_ref().unwrap().details.public_key); + + let cc20p1305_key = Key::from(shared_key); + let cc20p1305 = XChaCha20Poly1305::new(&cc20p1305_key); + self.cipher = Some(cc20p1305); + + let payload = Payload { + msg: &[0x42], + aad: &self.server_random, + }; + + let nonce = XNonce::from_slice(&self.client_random); + + let encrypted_0x42 = match self.cipher.as_ref().unwrap().encrypt(nonce, payload) { + Ok(d) => d, + Err(_) => { + return Err(EpfHandshakeError::EncryptionError.into()) + } + }; + + self.inner.write_all(&encode_packet(PACKET_FINISHED, &EpfFinished { + protocol_version: PROTOCOL_VERSION, + encrypted_0x42 + })?).await?; + + self.state = EpfClientState::WaitingForFinished; + + loop { + let packet = recv_packet(&mut self.inner).await?; + + if packet.packet_id != PACKET_FINISHED { + self.packet_queue.push(packet); + continue; + } + + let packet_finished: EpfFinished = rmp_serde::from_slice(&packet.packet_data)?; + + let payload = Payload { + msg: &packet_finished.encrypted_0x42, + aad: &self.server_random, + }; + + let hopefully_0x42 = match self.cipher.as_ref().unwrap().decrypt(nonce, payload) { + Ok(d) => d, + Err(_) => { + return Err(EpfHandshakeError::EncryptionError.into()); + } + }; + + if hopefully_0x42 != vec![0x42] { + return Err(EpfHandshakeError::MissingKeyProof.into()) + } + + break; + } + + self.state = EpfClientState::Transport; + + Ok(()) + } + + async fn upgrade(self) -> EpfClientStream where Self: Sized { + EpfClientStream { + inner: self.clone(), + aad: self.server_random, + client_cert: self.client_cert, + packet_queue: self.packet_queue, + server_cert: self.server_cert.unwrap(), + cipher: self.cipher.unwrap(), + private_key: self.private_key, + public_key: self.public_key, + raw_stream: self.inner + } + } +} + +pub struct EpfClientStream, S: AsyncReadExt + AsyncWriteExt + Unpin> { + inner: T, + raw_stream: S, + aad: [u8; 16], + client_cert: Option, + packet_queue: Vec, + server_cert: EPFCertificate, + cipher: XChaCha20Poly1305, + private_key: EpfPrivateKey, + public_key: EpfPublicKey +} + +#[async_trait] +pub trait EpfStreamOps { + async fn write(&mut self, data: &[u8]) -> Result<(), Box>; + async fn read(&mut self) -> Result, Box>; +} + +#[async_trait] +impl + Send, S: AsyncReadExt + AsyncWriteExt + Unpin + Send> EpfStreamOps for EpfClientStream { + async fn write(&mut self, data: &[u8]) -> Result<(), Box> { + let nonce = XChaCha20Poly1305::generate_nonce(OsRng); + + let payload = Payload { + msg: data, + aad: &self.aad, + }; + + let ciphertext = match self.cipher.encrypt(&nonce, payload) { + Ok(c) => c, + Err(_) => { + return Err(io::Error::new(io::ErrorKind::Other, "Encryption error").into()) + } + }; + let application_data = EpfApplicationData { + protocol_version: PROTOCOL_VERSION, + encrypted_application_data: ciphertext, + nonce: nonce.try_into().unwrap(), + }; + + let packet = encode_packet(PACKET_APPLICATION_DATA, &application_data)?; + + self.raw_stream.write_all(&packet).await?; + + Ok(()) + } + + async fn read(&mut self) -> Result, Box> { + loop { + let packet = recv_packet(&mut self.raw_stream).await?; + + if packet.packet_id != PACKET_APPLICATION_DATA { + self.packet_queue.push(packet); + continue; + } + + let app_data: EpfApplicationData = rmp_serde::from_slice(&packet.packet_data)?; + + let nonce = XNonce::from_slice(&app_data.nonce); + + let payload = Payload { + msg: &app_data.encrypted_application_data, + aad: &self.aad, + }; + + let plaintext = match self.cipher.decrypt(nonce, payload) { + Ok(p) => p, + Err(_) => { + return Err(io::Error::new(io::ErrorKind::Other, "Decryption error").into()) + } + }; + + return Ok(plaintext); + } + } +} + +///// SERVER ///// + +#[derive(Clone)] +pub struct EpfServerUpgraded { + inner: T, + state: EpfServerState, + client_random: [u8; 24], + server_random: [u8; 16], + client_cert: Option, + packet_queue: Vec, + cipher: Option, + cert: EPFCertificate, + private_key: EpfPrivateKey, + public_key: EpfPublicKey +} + +#[async_trait] +pub trait EpfServerUpgradable { + async fn upgrade(self, cert: EPFCertificate, private_key: EpfPrivateKey) -> EpfServerUpgraded where Self: Sized + AsyncWriteExt + AsyncReadExt + Send; +} + +#[async_trait] +impl EpfServerUpgradable for T where T: AsyncWriteExt + AsyncReadExt + Send { + async fn upgrade(self, cert: EPFCertificate, private_key: EpfPrivateKey) -> EpfServerUpgraded where Self: Sized + AsyncWriteExt + AsyncReadExt + Send { + EpfServerUpgraded { + inner: self, + state: EpfServerState::WaitingForClientHello, + server_random: OsRng.gen(), + client_random: [0u8; 24], + cert, + client_cert: None, + packet_queue: vec![], + cipher: None, + private_key, + public_key: SigningKey::from_keypair_bytes(&private_key).unwrap().verifying_key().to_bytes(), + } + } +} + +#[async_trait] +pub trait EpfServerHandshaker { + async fn handshake(&mut self) -> Result<(), Box>; + async fn upgrade(self) -> EpfServerStream where Self: Sized; +} + +#[async_trait] +impl EpfServerHandshaker for EpfServerUpgraded { + async fn handshake(&mut self) -> Result<(), Box> { + match self.state { + EpfServerState::WaitingForClientHello => (), + _ => return Err(EpfHandshakeError::AlreadyTunnelled.into()) + } + + // Step 0: Load Trusted Cert Store + let cert_pool = load_ca_pool()?; + + let client_public_key; + + // Step 1: Wait for Client Hello + loop { + let packet = recv_packet(&mut self.inner).await?; + + if packet.packet_id != PACKET_CLIENT_HELLO { + self.packet_queue.push(packet); + continue; + } + + let client_hello: EpfClientHello = rmp_serde::from_slice(&packet.packet_data)?; + + self.client_random = client_hello.client_random; + + if client_hello.protocol_version != PROTOCOL_VERSION { + return Err(EpfHandshakeError::UnsupportedProtocolVersion(client_hello.protocol_version as usize).into()); + } + + self.client_cert = client_hello.client_certificate; + + client_public_key = client_hello.client_public_key; + + break; + } + + // Step 2: Validate Client Certificate (if present) + if let Some(client_cert) = &self.client_cert { + let cert_valid = client_cert.verify(&cert_pool); + if let Err(e) = cert_valid { + return Err(EpfHandshakeError::InvalidCertificate(e).into()) + } + if let Ok(false) = cert_valid { + return Err(EpfHandshakeError::UntrustedCertificate.into()) + } + } + // Client Cert OK (if present) + + // Step 3: Send Server Hello + self.inner.write_all(&encode_packet(PACKET_SERVER_HELLO, &EpfServerHello { + protocol_version: PROTOCOL_VERSION, + server_certificate: self.cert.clone(), + server_random: self.server_random, + })?).await?; + + self.state = EpfServerState::WaitingForFinished; + + // Step 4: Build the cipher + let shared_key = x25519(self.private_key[..32].try_into().unwrap(), client_public_key); + + let cc20p1305_key = Key::from(shared_key); + let cc20p1305 = XChaCha20Poly1305::new(&cc20p1305_key); + self.cipher = Some(cc20p1305); + + let payload = Payload { + msg: &[0x42], + aad: &self.server_random, + }; + + let nonce = XNonce::from_slice(&self.client_random); + + loop { + let packet = recv_packet(&mut self.inner).await?; + + if packet.packet_id != PACKET_FINISHED { + self.packet_queue.push(packet); + continue; + } + + let packet_finished: EpfFinished = rmp_serde::from_slice(&packet.packet_data)?; + + let payload = Payload { + msg: &packet_finished.encrypted_0x42, + aad: &self.server_random, + }; + + let hopefully_0x42 = match self.cipher.as_ref().unwrap().decrypt(nonce, payload) { + Ok(d) => d, + Err(_) => { + return Err(EpfHandshakeError::EncryptionError.into()); + } + }; + + if hopefully_0x42 != vec![0x42] { + return Err(EpfHandshakeError::MissingKeyProof.into()) + } + + break; + } + + let encrypted_0x42 = match self.cipher.as_ref().unwrap().encrypt(nonce, payload) { + Ok(d) => d, + Err(_) => { + return Err(EpfHandshakeError::EncryptionError.into()) + } + }; + + self.inner.write_all(&encode_packet(PACKET_FINISHED, &EpfFinished { + protocol_version: PROTOCOL_VERSION, + encrypted_0x42 + })?).await?; + + self.state = EpfServerState::WaitingForFinished; + + self.state = EpfServerState::Transport; + + Ok(()) + } + + async fn upgrade(self) -> EpfServerStream where Self: Sized { + EpfServerStream { + inner: self.clone(), + aad: self.server_random, + server_cert: self.cert, + packet_queue: self.packet_queue, + client_cert: self.client_cert, + cipher: self.cipher.unwrap(), + private_key: self.private_key, + public_key: self.public_key, + raw_stream: self.inner + } + } +} + +pub struct EpfServerStream, S: AsyncReadExt + AsyncWriteExt + Unpin> { + inner: T, + raw_stream: S, + aad: [u8; 16], + client_cert: Option, + packet_queue: Vec, + server_cert: EPFCertificate, + cipher: XChaCha20Poly1305, + private_key: EpfPrivateKey, + public_key: EpfPublicKey +} + +#[async_trait] +impl + Send, S: AsyncReadExt + AsyncWriteExt + Unpin + Send> EpfStreamOps for EpfServerStream { + async fn write(&mut self, data: &[u8]) -> Result<(), Box> { + let nonce = XChaCha20Poly1305::generate_nonce(OsRng); + + let payload = Payload { + msg: data, + aad: &self.aad, + }; + + let ciphertext = match self.cipher.encrypt(&nonce, payload) { + Ok(c) => c, + Err(_) => { + return Err(io::Error::new(io::ErrorKind::Other, "Encryption error").into()) + } + }; + let application_data = EpfApplicationData { + protocol_version: PROTOCOL_VERSION, + encrypted_application_data: ciphertext, + nonce: nonce.try_into().unwrap(), + }; + + let packet = encode_packet(PACKET_APPLICATION_DATA, &application_data)?; + + self.raw_stream.write_all(&packet).await?; + + Ok(()) + } + + async fn read(&mut self) -> Result, Box> { + loop { + let packet = recv_packet(&mut self.raw_stream).await?; + + if packet.packet_id != PACKET_APPLICATION_DATA { + self.packet_queue.push(packet); + continue; + } + + let app_data: EpfApplicationData = rmp_serde::from_slice(&packet.packet_data)?; + + let nonce = XNonce::from_slice(&app_data.nonce); + + let payload = Payload { + msg: &app_data.encrypted_application_data, + aad: &self.aad, + }; + + let plaintext = match self.cipher.decrypt(nonce, payload) { + Ok(p) => p, + Err(_) => { + return Err(io::Error::new(io::ErrorKind::Other, "Decryption error").into()) + } + }; + + return Ok(plaintext); + } + } +} + +#[cfg(test)] +mod tests { + use std::io::Cursor; + + #[test] + pub fn stream_test() { + + } +} \ No newline at end of file diff --git a/libepf/src/lib.rs b/libepf/src/lib.rs index b6dccfa..0cdc4c5 100644 --- a/libepf/src/lib.rs +++ b/libepf/src/lib.rs @@ -1,4 +1,6 @@ pub mod ca_pool; pub mod pki; pub mod util; -pub mod protocol; \ No newline at end of file +pub mod protocol; +pub mod handshake_stream; +pub mod error; \ No newline at end of file diff --git a/libepf/src/protocol.rs b/libepf/src/protocol.rs index 08f750a..a8a4a87 100644 --- a/libepf/src/protocol.rs +++ b/libepf/src/protocol.rs @@ -1,23 +1,27 @@ +use std::error::Error; use serde::{Deserialize, Serialize}; -use crate::pki::EPFCertificate; +use tokio::io::{AsyncReadExt}; +use crate::pki::{EPFCertificate, EPFPKI_PUBLIC_KEY_LENGTH}; pub const PROTOCOL_VERSION: u32 = 1; -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Clone)] pub struct EpfMessage { pub packet_id: u32, pub packet_data: Vec } -pub const CLIENT_HELLO: u32 = 1; +pub const PACKET_CLIENT_HELLO: u32 = 1; #[derive(Serialize, Deserialize)] pub struct EpfClientHello { pub protocol_version: u32, - pub client_random: [u8; 16] + pub client_random: [u8; 24], + pub client_certificate: Option, + pub client_public_key: [u8; EPFPKI_PUBLIC_KEY_LENGTH] } -pub const SERVER_HELLO: u32 = 2; +pub const PACKET_SERVER_HELLO: u32 = 2; #[derive(Serialize, Deserialize)] pub struct EpfServerHello { @@ -26,15 +30,7 @@ pub struct EpfServerHello { pub server_random: [u8; 16] } -pub const CLIENT_KEY_EXCHANGE: u32 = 3; - -#[derive(Serialize, Deserialize)] -pub struct EpfClientKeyExchange { - pub protocol_version: u32, - pub encrypted_shared_secret: Vec -} - -pub const FINISHED: u32 = 4; +pub const PACKET_FINISHED: u32 = 3; #[derive(Serialize, Deserialize)] pub struct EpfFinished { @@ -42,14 +38,16 @@ pub struct EpfFinished { pub encrypted_0x42: Vec } -pub const APPLICATION_DATA: u32 = 5; +pub const PACKET_APPLICATION_DATA: u32 = 4; #[derive(Serialize, Deserialize)] pub struct EpfApplicationData { pub protocol_version: u32, - pub application_data: Vec + pub encrypted_application_data: Vec, + pub nonce: [u8; 24] } +#[derive(Clone)] pub enum EpfClientState { NotStarted, WaitingForServerHello, @@ -58,9 +56,9 @@ pub enum EpfClientState { Closed } +#[derive(Clone)] pub enum EpfServerState { WaitingForClientHello, - WaitingForClientKeyExchange, WaitingForFinished, Transport, Closed @@ -72,5 +70,17 @@ pub fn encode_packet(id: u32, packet: &T) -> Result, rmp_s packet_id: id, packet_data: message_data, }; - rmp_serde::to_vec(&message_wrapper) + let mut packet_data = rmp_serde::to_vec(&message_wrapper)?; + let mut packet = (packet_data.len() as u64).to_le_bytes().to_vec(); + // Packet: 8-byte little-endian length prefix, packet data + packet.append(&mut packet_data); + Ok(packet) +} + +pub async fn recv_packet(stream: &mut C) -> Result> { + let packet_length = stream.read_u64_le().await?; + let mut packet_data_buf = vec![0u8; packet_length as usize]; + stream.read_exact(&mut packet_data_buf).await?; + let message: EpfMessage = rmp_serde::from_slice(&packet_data_buf)?; + Ok(message) } \ No newline at end of file