diff --git a/Cargo.toml b/Cargo.toml index d0934e9..977e234 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,4 +12,4 @@ futures = "0.3" getopts = "0.2" log = "0.4" tokio = { version = "0.2", features = ["full"] } -tokio-libtls = "1.1.0-alpha.2" +tokio-libtls = "1.1.0-alpha.3" diff --git a/src/cert.rs b/src/cert.rs new file mode 100644 index 0000000..1d8efc8 --- /dev/null +++ b/src/cert.rs @@ -0,0 +1,154 @@ +// Copyright (c) 2019 Reyk Floeter +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +use derive_more::{Deref, From, Into}; +use dirs::home_dir; +use std::{ + fs::{create_dir, File}, + io::{Error, ErrorKind, Result, Write}, + path::PathBuf, + process::{Command, Output}, +}; + +pub const COUNTRY: &str = "XX"; +pub const CONFIG_DIR: &str = ".yodle"; +pub const SERVER_CERT: &str = "yodle.crt"; +pub const SERVER_KEY: &str = "yodle.key"; +const SERVER_CSR: &str = "yodle.csr"; + +/// A generic keypair representing a client or the server +#[derive(Debug, Clone)] +pub struct Cert { + pub name: String, + pub cert: PathBuf, + pub key: Option, +} + +/// The local server keypair +#[derive(Clone, Debug, Deref, From, Into)] +pub struct KeyPair(Cert); + +impl KeyPair { + /// Get the server keys or generate them if they don't exist + /// + /// # Panics + /// + /// This function may fail if the key cannot be generated. + pub fn new() -> Self { + let home = home_dir().unwrap(); + let mut config_dir = PathBuf::from(home); + config_dir.push(CONFIG_DIR); + + debug!("config_dir {}", config_dir.display()); + + let mut key = PathBuf::from(&config_dir); + key.push(SERVER_KEY); + let key_path = key.to_string_lossy(); + + let mut cert = PathBuf::from(&config_dir); + cert.push(SERVER_CERT); + let cert_path = cert.to_string_lossy(); + + let mut csr = PathBuf::from(&config_dir); + csr.push(SERVER_CSR); + let csr_path = csr.to_string_lossy(); + + if !config_dir.exists() { + create_dir(&config_dir).expect("failed to create config dir"); + } + + if !key.exists() { + gen_key(&key_path).expect("failed to generate key"); + } + + if !csr.exists() { + gen_csr(&key_path, &csr_path).expect("failed to generate signing request"); + } + + if !cert.exists() { + gen_cert(&key_path, &csr_path, &cert_path).expect("failed to generate cert"); + } + + Cert { + name: "localhost".into(), + cert, + key: Some(key), + } + .into() + } +} + +fn command_ok(output: Output) -> Result<()> { + if output.status.success() { + Ok(()) + } else { + Err(Error::new( + ErrorKind::Other, + String::from_utf8(output.stderr).unwrap_or("error".to_string()), + )) + } +} + +fn gen_key(key_path: &str) -> Result<()> { + Command::new("openssl") + .args(&["ecparam", "-name", "secp384r1", "-genkey", "-out", key_path]) + .output()?; + Command::new("chmod").args(&["0640", key_path]).output()?; + Ok(()) +} + +fn gen_csr(key_path: &str, csr_path: &str) -> Result<()> { + debug!("key {} csr {}", key_path, csr_path); + + // XXX use gethostname() libc function or a crate around it + let hostname = String::from_utf8(Command::new("hostname").output()?.stdout) + .map_err(|err| Error::new(ErrorKind::Other, err))?; + let subject = format!("/C={cn}/CN={hn}/O=yodle/", cn = COUNTRY, hn = hostname); + + command_ok( + Command::new("openssl") + .args(&[ + "req", "-new", "-batch", "-subj", &subject, "-key", key_path, "-out", csr_path, + ]) + .output()?, + )?; + Command::new("chmod").args(&["0640", csr_path]).output()?; + + let mut f = File::create(csr_path.to_owned() + ".ext")?; + f.write_all(format!("subjectAltName=DNS:localhost,DNS:{}", hostname).as_bytes())?; + Ok(()) +} + +fn gen_cert(key_path: &str, csr_path: &str, cert_path: &str) -> Result<()> { + command_ok( + Command::new("openssl") + .args(&[ + "x509", + "-sha384", + "-req", + "-days", + "365", + "-fingerprint", + "-in", + csr_path, + "-signkey", + key_path, + "-out", + cert_path, + "-extfile", + &(csr_path.to_owned() + ".ext"), + ]) + .output()?, + ) +} diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..5b37e67 --- /dev/null +++ b/src/client.rs @@ -0,0 +1,37 @@ +// Copyright (c) 2019 Reyk Floeter +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +use crate::Config; +use std::io::Result; +use tokio::prelude::*; +use tokio_libtls::prelude::*; + +pub(crate) async fn run(config: Config) -> Result<()> { + let (cert, key, ca) = config.load_keys()?; + let mut options = config.load_client_options(); + + let tls_config = TlsConfigBuilder::new() + .ca_file(ca) + .keypair_file(cert, key, None) + .build() + .unwrap(); + let addr = config.address.unwrap(); + let mut tls = AsyncTls::connect(&addr.to_string(), &tls_config, options.build()) + .await + .unwrap(); + let _ = tls.write_all(b"OK\r\n").await; + let mut buf = vec![0u8; 1024]; + let _ = tls.read(&mut buf).await; + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 2cc10b0..84c6541 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,45 +16,80 @@ extern crate log; mod cert; +mod client; +mod server; -use cert::ServerKeyPair; +use cert::KeyPair; use log::LevelFilter; -use std::io; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use std::{ + io::{Error, ErrorKind, Result}, + net::SocketAddr, + path::{Path, PathBuf}, + time::Duration, +}; use tokio_libtls::prelude::*; -async fn async_https_connect(servername: String) -> io::Result<()> { - let request = format!( - "GET / HTTP/1.1\r\n\ - Host: {}\r\n\ - Connection: close\r\n\r\n", - servername - ); +#[derive(Clone, Debug, Default)] +pub(crate) struct Config { + keypair: Option, + ca: Option, + timeout: Option, + servername: Option, + address: Option, +} - let config = TlsConfigBuilder::new().build()?; - let mut tls = AsyncTls::connect(&(servername + ":443"), &config, None).await?; - tls.write_all(request.as_bytes()).await?; +impl Config { + pub fn new() -> Self { + Self { + address: "[::1]:8023".parse().ok(), + ..Default::default() + } + } - let mut buf = vec![0u8; 1024]; - tls.read_exact(&mut buf).await?; + pub fn load_keys(&self) -> Result<(&Path, &Path, &Path)> { + let keypair = self + .keypair + .as_ref() + .ok_or(Error::new(ErrorKind::Other, "keypair"))?; + let key = keypair + .key + .as_ref() + .ok_or(Error::new(ErrorKind::Other, "key"))?; + let ca = self.ca.as_ref().ok_or(Error::new(ErrorKind::Other, "CA"))?; + Ok((&keypair.cert, key, ca)) + } - let ok = b"HTTP/1.1 200 OK\r\n"; - assert_eq!(&buf[..ok.len()], ok); + pub fn load_server_options(&self) -> AsyncTlsOptions { + let mut options = AsyncTlsOptions::new(); + if let Some(timeout) = self.timeout { + options.timeout(timeout); + } + if let Some(ref servername) = self.servername { + options.servername(servername); + } else { + options.servername("localhost"); + } + options + } - Ok(()) + pub fn load_client_options(&self) -> AsyncTlsOptions { + self.load_server_options() + } } #[tokio::main] async fn main() { + let mut config = Config::new(); + env_logger::builder() .filter_level(LevelFilter::Debug) .init(); - async_https_connect("www.example.com".to_owned()) - .await - .unwrap(); + let keypair = KeyPair::new(); + config.ca = Some(keypair.cert.clone()); + config.keypair = Some(keypair); - let server_key = ServerKeyPair::new(); + info!("{:?}", config); - info!("{:?}", server_key); + server::run(config).await.expect("server"); } diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000..348fc24 --- /dev/null +++ b/src/server.rs @@ -0,0 +1,47 @@ +// Copyright (c) 2019 Reyk Floeter +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +use crate::Config; +use std::{io::Result}; +use tokio::{net::TcpListener, prelude::*}; +use tokio_libtls::prelude::*; + +pub(crate) async fn run(config: Config) -> Result<()> { + let (cert, key, ca) = config.load_keys()?; + let mut options = config.load_server_options(); + + let tls_config = TlsConfigBuilder::new() + .ca_file(ca) + .keypair_file(cert, key, None) + .build() + .unwrap(); + + let addr = config.address.unwrap(); + let mut listener = TcpListener::bind(&addr).await?; + + loop { + let (tcp, _) = listener.accept().await?; + let options = options.build(); + let mut tls = AsyncTls::accept_stream(tcp, &tls_config, options).await?; + + tokio::spawn(async move { + loop { + let mut buf = vec![0u8; 1024]; + if !tls.read(&mut buf).await.is_ok() || !tls.write_all(&buf).await.is_ok() { + break; + } + } + }); + } +}