186 lines
5.4 KiB
Rust
186 lines
5.4 KiB
Rust
// Copyright (c) 2019 Reyk Floeter <contact@reykfloeter.com>
|
|
//
|
|
// 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 bubblebabble::stablebabble;
|
|
use derive_more::{Deref, From, Into};
|
|
use dirs::home_dir;
|
|
use std::{
|
|
fs::{create_dir, File},
|
|
io::{Error, ErrorKind, Result, Write},
|
|
net::SocketAddr,
|
|
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<PathBuf>,
|
|
}
|
|
|
|
/// The local server keypair
|
|
#[derive(Clone, Debug, Deref, From, Into)]
|
|
pub(crate) 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(crate) fn new(config: &Config) -> 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, config).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, config: &Config) -> 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 hostname = hostname.trim();
|
|
|
|
let babble = if let Some(SocketAddr::V6(ref address)) = config.address {
|
|
stablebabble(&address.ip().octets())
|
|
} else {
|
|
return Err(Error::new(ErrorKind::Other, "invalid address"));
|
|
};
|
|
|
|
let subject = format!(
|
|
"/C={cn}/CN={hn}/O={sn}/OU={bb}",
|
|
cn = COUNTRY,
|
|
hn = hostname,
|
|
sn = config
|
|
.servername
|
|
.as_ref()
|
|
.map(|s| s.as_str())
|
|
.unwrap_or("yodle"),
|
|
bb = babble,
|
|
);
|
|
|
|
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")?;
|
|
let mut san = format!(
|
|
"subjectAltName=DNS:localhost,DNS:{hn},DNS:{bb}",
|
|
hn = hostname,
|
|
bb = babble
|
|
);
|
|
if let Some(ref servername) = config.servername {
|
|
san.push_str(&format!(",DNS:{}", servername));
|
|
}
|
|
eprintln!("{}", san);
|
|
f.write_all(san.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()?,
|
|
)
|
|
}
|