yodle/src/cert.rs

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()?,
)
}