500 lines of untested protocol code, fun
This commit is contained in:
parent
d2e1a85554
commit
3b6a8a00d7
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
chrono = "0.4"
|
||||
tokio = { version = "1.28", features = ["io-util"] }
|
||||
async-trait = "0.1"
|
||||
chacha20poly1305 = "0.10"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-test = "0.4"
|
|
@ -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<EpfPublicKey, EPFCertificate>,
|
||||
|
@ -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<EpfCaPool, Box<dyn Error>> {
|
||||
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)
|
||||
}
|
|
@ -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 {}
|
|
@ -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<T: AsyncWriteExt + AsyncReadExt> {
|
||||
inner: T,
|
||||
state: EpfClientState,
|
||||
client_random: [u8; 24],
|
||||
server_random: [u8; 16],
|
||||
client_cert: Option<EPFCertificate>,
|
||||
packet_queue: Vec<EpfMessage>,
|
||||
server_cert: Option<EPFCertificate>,
|
||||
cipher: Option<XChaCha20Poly1305>,
|
||||
private_key: EpfPrivateKey,
|
||||
public_key: EpfPublicKey
|
||||
}
|
||||
|
||||
pub enum ClientAuthentication {
|
||||
Cert(Box<EPFCertificate>, EpfPrivateKey),
|
||||
Ephemeral
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait EpfClientUpgradable {
|
||||
async fn upgrade(self, auth: ClientAuthentication) -> EpfClientUpgraded<Self> where Self: Sized + AsyncWriteExt + AsyncReadExt + Send;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T> EpfClientUpgradable for T where T: AsyncWriteExt + AsyncReadExt + Send {
|
||||
async fn upgrade(self, auth: ClientAuthentication) -> EpfClientUpgraded<Self> 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<S: AsyncWriteExt + AsyncReadExt + Unpin> {
|
||||
async fn handshake(&mut self) -> Result<(), Box<dyn Error>>;
|
||||
async fn upgrade(self) -> EpfClientStream<Self, S> where Self: Sized;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T: AsyncWriteExt + AsyncReadExt + Send + Unpin + Clone> EpfClientHandshaker<T> for EpfClientUpgraded<T> {
|
||||
async fn handshake(&mut self) -> Result<(), Box<dyn Error>> {
|
||||
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<Self, T> 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<T: EpfClientHandshaker<S>, S: AsyncReadExt + AsyncWriteExt + Unpin> {
|
||||
inner: T,
|
||||
raw_stream: S,
|
||||
aad: [u8; 16],
|
||||
client_cert: Option<EPFCertificate>,
|
||||
packet_queue: Vec<EpfMessage>,
|
||||
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<dyn Error>>;
|
||||
async fn read(&mut self) -> Result<Vec<u8>, Box<dyn Error>>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T: EpfClientHandshaker<S> + Send, S: AsyncReadExt + AsyncWriteExt + Unpin + Send> EpfStreamOps for EpfClientStream<T, S> {
|
||||
async fn write(&mut self, data: &[u8]) -> Result<(), Box<dyn Error>> {
|
||||
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<Vec<u8>, Box<dyn Error>> {
|
||||
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<T: AsyncWriteExt + AsyncReadExt> {
|
||||
inner: T,
|
||||
state: EpfServerState,
|
||||
client_random: [u8; 24],
|
||||
server_random: [u8; 16],
|
||||
client_cert: Option<EPFCertificate>,
|
||||
packet_queue: Vec<EpfMessage>,
|
||||
cipher: Option<XChaCha20Poly1305>,
|
||||
cert: EPFCertificate,
|
||||
private_key: EpfPrivateKey,
|
||||
public_key: EpfPublicKey
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait EpfServerUpgradable {
|
||||
async fn upgrade(self, cert: EPFCertificate, private_key: EpfPrivateKey) -> EpfServerUpgraded<Self> where Self: Sized + AsyncWriteExt + AsyncReadExt + Send;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T> EpfServerUpgradable for T where T: AsyncWriteExt + AsyncReadExt + Send {
|
||||
async fn upgrade(self, cert: EPFCertificate, private_key: EpfPrivateKey) -> EpfServerUpgraded<Self> 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<S: AsyncWriteExt + AsyncReadExt + Unpin> {
|
||||
async fn handshake(&mut self) -> Result<(), Box<dyn Error>>;
|
||||
async fn upgrade(self) -> EpfServerStream<Self, S> where Self: Sized;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T: AsyncWriteExt + AsyncReadExt + Send + Unpin + Clone> EpfServerHandshaker<T> for EpfServerUpgraded<T> {
|
||||
async fn handshake(&mut self) -> Result<(), Box<dyn Error>> {
|
||||
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<Self, T> 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<T: EpfServerHandshaker<S>, S: AsyncReadExt + AsyncWriteExt + Unpin> {
|
||||
inner: T,
|
||||
raw_stream: S,
|
||||
aad: [u8; 16],
|
||||
client_cert: Option<EPFCertificate>,
|
||||
packet_queue: Vec<EpfMessage>,
|
||||
server_cert: EPFCertificate,
|
||||
cipher: XChaCha20Poly1305,
|
||||
private_key: EpfPrivateKey,
|
||||
public_key: EpfPublicKey
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T: EpfServerHandshaker<S> + Send, S: AsyncReadExt + AsyncWriteExt + Unpin + Send> EpfStreamOps for EpfServerStream<T, S> {
|
||||
async fn write(&mut self, data: &[u8]) -> Result<(), Box<dyn Error>> {
|
||||
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<Vec<u8>, Box<dyn Error>> {
|
||||
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() {
|
||||
|
||||
}
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
pub mod ca_pool;
|
||||
pub mod pki;
|
||||
pub mod util;
|
||||
pub mod protocol;
|
||||
pub mod protocol;
|
||||
pub mod handshake_stream;
|
||||
pub mod error;
|
|
@ -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<u8>
|
||||
}
|
||||
|
||||
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<EPFCertificate>,
|
||||
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<u8>
|
||||
}
|
||||
|
||||
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<u8>
|
||||
}
|
||||
|
||||
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<u8>
|
||||
pub encrypted_application_data: Vec<u8>,
|
||||
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<T: Serialize>(id: u32, packet: &T) -> Result<Vec<u8>, 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<C: AsyncReadExt + Unpin>(stream: &mut C) -> Result<EpfMessage, Box<dyn Error>> {
|
||||
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)
|
||||
}
|
Loading…
Reference in New Issue