pub extern crate openssl;
#[macro_use]
extern crate log;
#[macro_use]
extern crate error_chain;
#[macro_use]
extern crate hyper;
extern crate reqwest;
extern crate serde;
extern crate serde_json;
extern crate base64;
use std::path::Path;
use std::fs::File;
use std::io::{Read, Write};
use std::collections::HashMap;
use openssl::sign::Signer;
use openssl::hash::{hash, MessageDigest};
use openssl::pkey::PKey;
use openssl::x509::{X509, X509Req};
use reqwest::{Client, StatusCode};
use helper::{gen_key, b64, read_pkey, gen_csr};
use error::{Result, ErrorKind};
use serde_json::{Value, from_str, to_string, to_value};
use serde::Serialize;
pub const LETSENCRYPT_DIRECTORY_URL: &'static str = "https://acme-v01.api.letsencrypt.org\
/directory";
pub const LETSENCRYPT_AGREEMENT_URL: &'static str = "https://letsencrypt.org/documents/LE-SA-v1.2-\
November-15-2017.pdf";
pub const LETSENCRYPT_INTERMEDIATE_CERT_URL: &'static str = "https://letsencrypt.org/certs/\
lets-encrypt-x3-cross-signed.pem";
const BIT_LENGTH: u32 = 2048;
pub struct Directory {
url: String,
directory: Value,
}
pub struct Account {
directory: Directory,
pkey: PKey<openssl::pkey::Private>,
}
pub struct AccountRegistration {
directory: Directory,
pkey: Option<PKey<openssl::pkey::Private>>,
email: Option<String>,
contact: Option<Vec<String>>,
agreement: Option<String>,
}
pub struct CertificateSigner<'a> {
account: &'a Account,
domains: &'a [&'a str],
pkey: Option<PKey<openssl::pkey::Private>>,
csr: Option<X509Req>,
}
pub struct SignedCertificate {
cert: X509,
csr: X509Req,
pkey: PKey<openssl::pkey::Private>,
}
pub struct Authorization<'a>(Vec<Challenge<'a>>);
pub struct Challenge<'a> {
account: &'a Account,
ctype: String,
url: String,
token: String,
key_authorization: String,
}
impl Directory {
pub fn lets_encrypt() -> Result<Directory> {
Directory::from_url(LETSENCRYPT_DIRECTORY_URL)
}
pub fn from_url(url: &str) -> Result<Directory> {
let client = Client::new()?;
let mut res = client.get(url).send()?;
let mut content = String::new();
res.read_to_string(&mut content)?;
Ok(Directory {
url: url.to_owned(),
directory: from_str(&content)?,
})
}
fn url_for(&self, resource: &str) -> Option<&str> {
self.directory
.as_object()
.and_then(|o| o.get(resource))
.and_then(|k| k.as_str())
}
pub fn account_registration(self) -> AccountRegistration {
AccountRegistration {
directory: self,
pkey: None,
email: None,
contact: None,
agreement: None,
}
}
fn get_nonce(&self) -> Result<String> {
let url = self.url_for("new-nonce").unwrap_or(&self.url);
let client = Client::new()?;
let res = client.get(url).send()?;
res.headers()
.get::<hyperx::ReplayNonce>()
.ok_or("Replay-Nonce header not found".into())
.and_then(|nonce| Ok(nonce.as_str().to_string()))
}
fn request<T: Serialize>(&self,
pkey: &PKey<openssl::pkey::Private>,
resource: &str,
payload: T)
-> Result<(StatusCode, Value)> {
let mut json = to_value(&payload)?;
let resource_json: Value = to_value(resource)?;
json.as_object_mut()
.and_then(|obj| obj.insert("resource".to_owned(), resource_json));
let jws = self.jws(pkey, json)?;
let client = Client::new()?;
let mut res = client
.post(self.url_for(resource)
.ok_or(format!("URL for resource: {} not found", resource))?)
.body(&jws[..])
.send()?;
let res_json = {
let mut res_content = String::new();
res.read_to_string(&mut res_content)?;
if !res_content.is_empty() {
from_str(&res_content)?
} else {
to_value(true)?
}
};
Ok((*res.status(), res_json))
}
fn jws<T: Serialize>(&self, pkey: &PKey<openssl::pkey::Private>, payload: T) -> Result<String> {
let nonce = self.get_nonce()?;
let mut data: HashMap<String, Value> = HashMap::new();
let mut header: HashMap<String, Value> = HashMap::new();
header.insert("alg".to_owned(), to_value("RS256")?);
header.insert("jwk".to_owned(), self.jwk(pkey)?);
data.insert("header".to_owned(), to_value(&header)?);
let payload = to_string(&payload)?;
let payload64 = b64(&payload.into_bytes());
data.insert("payload".to_owned(), to_value(&payload64)?);
header.insert("nonce".to_owned(), to_value(nonce)?);
let protected64 = b64(&to_string(&header)?.into_bytes());
data.insert("protected".to_owned(), to_value(&protected64)?);
data.insert("signature".to_owned(), {
let mut signer = Signer::new(MessageDigest::sha256(), &pkey)?;
signer
.update(&format!("{}.{}", protected64, payload64).into_bytes())?;
to_value(b64(&signer.sign_to_vec()?))?
});
let json_str = to_string(&data)?;
Ok(json_str)
}
fn jwk(&self, pkey: &PKey<openssl::pkey::Private>) -> Result<Value> {
let rsa = pkey.rsa()?;
let mut jwk: HashMap<String, String> = HashMap::new();
jwk.insert("e".to_owned(),
b64(&rsa.e().to_vec()));
jwk.insert("kty".to_owned(), "RSA".to_owned());
jwk.insert("n".to_owned(),
b64(&rsa.n().to_vec()));
Ok(to_value(jwk)?)
}
}
impl Account {
pub fn authorization<'a>(&'a self, domain: &str) -> Result<Authorization<'a>> {
info!("Sending identifier authorization request for {}", domain);
let mut map = HashMap::new();
map.insert("identifier".to_owned(), {
let mut map = HashMap::new();
map.insert("type".to_owned(), "dns".to_owned());
map.insert("value".to_owned(), domain.to_owned());
map
});
let (status, resp) = self.directory().request(self.pkey(), "new-authz", map)?;
if status != StatusCode::Created {
return Err(ErrorKind::AcmeServerError(resp).into());
}
let mut challenges = Vec::new();
for challenge in resp.as_object()
.and_then(|obj| obj.get("challenges"))
.and_then(|c| c.as_array())
.ok_or("No challenge found")? {
let obj = challenge
.as_object()
.ok_or("Challenge object not found")?;
let ctype = obj.get("type")
.and_then(|t| t.as_str())
.ok_or("Challenge type not found")?
.to_owned();
let uri = obj.get("uri")
.and_then(|t| t.as_str())
.ok_or("URI not found")?
.to_owned();
let token = obj.get("token")
.and_then(|t| t.as_str())
.ok_or("Token not found")?
.to_owned();
let key_authorization = format!("{}.{}",
token,
b64(&hash(MessageDigest::sha256(),
&to_string(&self.directory()
.jwk(self.pkey())?)?
.into_bytes())?));
let challenge = Challenge {
account: self,
ctype: ctype,
url: uri,
token: token,
key_authorization: key_authorization,
};
challenges.push(challenge);
}
Ok(Authorization(challenges))
}
pub fn certificate_signer<'a>(&'a self, domains: &'a [&'a str]) -> CertificateSigner<'a> {
CertificateSigner {
account: self,
domains: domains,
pkey: None,
csr: None,
}
}
pub fn revoke_certificate_from_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let content = {
let mut file = File::open(path)?;
let mut content = Vec::new();
file.read_to_end(&mut content)?;
content
};
let cert = X509::from_pem(&content)?;
self.revoke_certificate(&cert)
}
pub fn revoke_certificate(&self, cert: &X509) -> Result<()> {
let (status, resp) = {
let mut map = HashMap::new();
map.insert("certificate".to_owned(), b64(&cert.to_der()?));
self.directory()
.request(self.pkey(), "revoke-cert", map)?
};
match status {
StatusCode::Ok => info!("Certificate successfully revoked"),
StatusCode::Conflict => warn!("Certificate already revoked"),
_ => return Err(ErrorKind::AcmeServerError(resp).into()),
}
Ok(())
}
pub fn write_private_key<W: Write>(&self, writer: &mut W) -> Result<()> {
Ok(writer.write_all(&self.pkey().private_key_to_pem_pkcs8()?)?)
}
pub fn save_private_key<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let mut file = File::create(path)?;
self.write_private_key(&mut file)
}
pub fn pkey(&self) -> &PKey<openssl::pkey::Private> {
&self.pkey
}
pub fn directory(&self) -> &Directory {
&self.directory
}
}
impl AccountRegistration {
pub fn email(mut self, email: &str) -> AccountRegistration {
self.email = Some(email.to_owned());
self
}
pub fn contact(mut self, contact: &[&str]) -> AccountRegistration {
self.contact = Some(contact.iter().map(|c| c.to_string()).collect());
self
}
pub fn agreement(mut self, url: &str) -> AccountRegistration {
self.agreement = Some(url.to_owned());
self
}
pub fn pkey(mut self, pkey: PKey<openssl::pkey::Private>) -> AccountRegistration {
self.pkey = Some(pkey);
self
}
pub fn pkey_from_file<P: AsRef<Path>>(mut self, path: P) -> Result<AccountRegistration> {
self.pkey = Some(read_pkey(path)?);
Ok(self)
}
pub fn register(self) -> Result<Account> {
info!("Registering account");
let mut map = HashMap::new();
map.insert("agreement".to_owned(),
to_value(self.agreement
.unwrap_or(LETSENCRYPT_AGREEMENT_URL.to_owned()))?);
if let Some(mut contact) = self.contact {
if let Some(email) = self.email {
contact.push(format!("mailto:{}", email));
}
map.insert("contract".to_owned(), to_value(contact)?);
} else if let Some(email) = self.email {
map.insert("contract".to_owned(),
to_value(vec![format!("mailto:{}", email)])?);
}
let pkey = self.pkey.unwrap_or(gen_key()?);
let (status, resp) = self.directory.request(&pkey, "new-reg", map)?;
match status {
StatusCode::Created => debug!("User successfully registered"),
StatusCode::Conflict => debug!("User already registered"),
_ => return Err(ErrorKind::AcmeServerError(resp).into()),
};
Ok(Account {
directory: self.directory,
pkey: pkey,
})
}
}
impl<'a> CertificateSigner<'a> {
pub fn pkey(mut self, pkey: PKey<openssl::pkey::Private>) -> CertificateSigner<'a> {
self.pkey = Some(pkey);
self
}
pub fn pkey_from_file<P: AsRef<Path>>(mut self, path: P) -> Result<CertificateSigner<'a>> {
self.pkey = Some(read_pkey(path)?);
Ok(self)
}
pub fn csr(mut self, csr: X509Req) -> CertificateSigner<'a> {
self.csr = Some(csr);
self
}
pub fn csr_from_file<P: AsRef<Path>>(mut self,
pkey_path: P,
csr_path: P)
-> Result<CertificateSigner<'a>> {
self.pkey = Some(read_pkey(pkey_path)?);
let content = {
let mut file = File::open(csr_path)?;
let mut content = Vec::new();
file.read_to_end(&mut content)?;
content
};
self.csr = Some(X509Req::from_pem(&content)?);
Ok(self)
}
pub fn sign_certificate(self) -> Result<SignedCertificate> {
info!("Signing certificate");
let pkey = self.pkey.unwrap_or(gen_key()?);
let csr = self.csr.unwrap_or(gen_csr(&pkey, self.domains)?);
let mut map = HashMap::new();
map.insert("resource".to_owned(), "new-cert".to_owned());
map.insert("csr".to_owned(), b64(&csr.to_der()?));
let client = Client::new()?;
let jws = self.account.directory().jws(self.account.pkey(), map)?;
let mut res = client
.post(self.account
.directory()
.url_for("new-cert")
.ok_or("new-cert url not found")?)
.body(&jws[..])
.send()?;
if res.status() != &StatusCode::Created {
let res_json = {
let mut res_content = String::new();
res.read_to_string(&mut res_content)?;
from_str(&res_content)?
};
return Err(ErrorKind::AcmeServerError(res_json).into());
}
let mut crt_der = Vec::new();
res.read_to_end(&mut crt_der)?;
let cert = X509::from_der(&crt_der)?;
debug!("Certificate successfully signed");
Ok(SignedCertificate {
cert: cert,
csr: csr,
pkey: pkey,
})
}
}
impl SignedCertificate {
pub fn save_signed_certificate<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let mut file = File::create(path)?;
self.write_signed_certificate(&mut file)
}
pub fn save_intermediate_certificate<P: AsRef<Path>>(&self,
url: Option<&str>,
path: P)
-> Result<()> {
let mut file = File::create(path)?;
self.write_intermediate_certificate(url, &mut file)
}
pub fn save_signed_certificate_and_chain<P: AsRef<Path>>(&self,
url: Option<&str>,
path: P)
-> Result<()> {
let mut file = File::create(path)?;
self.write_signed_certificate(&mut file)?;
self.write_intermediate_certificate(url, &mut file)?;
Ok(())
}
pub fn save_private_key<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let mut file = File::create(path)?;
self.write_private_key(&mut file)
}
pub fn save_csr<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let mut file = File::create(path)?;
self.write_csr(&mut file)
}
pub fn write_signed_certificate<W: Write>(&self, writer: &mut W) -> Result<()> {
writer.write_all(&self.cert.to_pem()?)?;
Ok(())
}
pub fn write_intermediate_certificate<W: Write>(&self,
url: Option<&str>,
writer: &mut W)
-> Result<()> {
let cert = self.get_intermediate_certificate(url)?;
writer.write_all(&cert.to_pem()?)?;
Ok(())
}
fn get_intermediate_certificate(&self, url: Option<&str>) -> Result<X509> {
let client = Client::new()?;
let mut res = client
.get(url.unwrap_or(LETSENCRYPT_INTERMEDIATE_CERT_URL))
.send()?;
let mut content = Vec::new();
res.read_to_end(&mut content)?;
Ok(X509::from_pem(&content)?)
}
pub fn write_private_key<W: Write>(&self, writer: &mut W) -> Result<()> {
Ok(writer.write_all(&self.pkey().private_key_to_pem_pkcs8()?)?)
}
pub fn write_csr<W: Write>(&self, writer: &mut W) -> Result<()> {
Ok(writer.write_all(&self.csr().to_pem()?)?)
}
pub fn cert(&self) -> &X509 {
&self.cert
}
pub fn csr(&self) -> &X509Req {
&self.csr
}
pub fn pkey(&self) -> &PKey<openssl::pkey::Private> {
&self.pkey
}
}
impl<'a> Authorization<'a> {
pub fn get_challenge(&self, pattern: &str) -> Option<&Challenge> {
for challenge in &self.0 {
if challenge.ctype().starts_with(pattern) {
return Some(challenge);
}
}
None
}
pub fn get_http_challenge(&self) -> Option<&Challenge> {
self.get_challenge("http")
}
pub fn get_dns_challenge(&self) -> Option<&Challenge> {
self.get_challenge("dns")
}
pub fn get_tls_sni_challenge(&self) -> Option<&Challenge> {
self.get_challenge("tls-sni")
}
}
impl<'a> Challenge<'a> {
pub fn save_key_authorization<P: AsRef<Path>>(&self, path: P) -> Result<()> {
use std::fs::create_dir_all;
let path = path.as_ref().join(".well-known").join("acme-challenge");
debug!("Saving validation token into: {:?}", &path);
create_dir_all(&path)?;
let mut file = File::create(path.join(&self.token))?;
writeln!(&mut file, "{}", self.key_authorization)?;
Ok(())
}
pub fn signature(&self) -> Result<String> {
Ok(b64(&hash(MessageDigest::sha256(),
&self.key_authorization.clone().into_bytes())?))
}
pub fn ctype(&self) -> &str {
&self.ctype
}
pub fn token(&self) -> &str {
&self.token
}
pub fn key_authorization(&self) -> &str {
&self.key_authorization
}
pub fn validate(&self) -> Result<()> {
info!("Triggering {} validation", self.ctype);
let payload = {
let map = {
let mut map: HashMap<String, Value> = HashMap::new();
map.insert("type".to_owned(), to_value(&self.ctype)?);
map.insert("token".to_owned(), to_value(&self.token)?);
map.insert("resource".to_owned(), to_value("challenge")?);
map.insert("keyAuthorization".to_owned(),
to_value(&self.key_authorization)?);
map
};
self.account.directory().jws(self.account.pkey(), map)?
};
let client = Client::new()?;
let mut resp = client.post(&self.url).body(&payload[..]).send()?;
let mut res_json: Value = {
let mut res_content = String::new();
resp.read_to_string(&mut res_content)?;
from_str(&res_content)?
};
if resp.status() != &StatusCode::Accepted {
return Err(ErrorKind::AcmeServerError(res_json).into());
}
loop {
let status = res_json
.as_object()
.and_then(|o| o.get("status"))
.and_then(|s| s.as_str())
.ok_or("Status not found")?
.to_owned();
if status == "pending" {
debug!("Status is pending, trying again...");
let mut resp = client.get(&self.url).send()?;
res_json = {
let mut res_content = String::new();
resp.read_to_string(&mut res_content)?;
from_str(&res_content)?
};
} else if status == "valid" {
return Ok(());
} else if status == "invalid" {
return Err(ErrorKind::AcmeServerError(res_json).into());
}
use std::thread::sleep;
use std::time::Duration;
sleep(Duration::from_secs(2));
}
}
}
mod hyperx {
header! { (ReplayNonce, "Replay-Nonce") => [String] }
}
pub mod error {
use std::io;
use openssl;
use hyper;
use reqwest;
use serde_json;
error_chain! {
types {
Error, ErrorKind, ChainErr, Result;
}
links {
}
foreign_links {
OpenSslErrorStack(openssl::error::ErrorStack);
IoError(io::Error);
HyperError(hyper::Error);
ReqwestError(reqwest::Error);
ValueParserError(serde_json::Error);
}
errors {
AcmeServerError(resp: serde_json::Value) {
description("Acme server error")
display("Acme server error: {}", acme_server_error_description(resp))
}
}
}
fn acme_server_error_description(resp: &serde_json::Value) -> String {
if let Some(obj) = resp.as_object() {
let t = obj.get("type").and_then(|t| t.as_str()).unwrap_or("");
let detail = obj.get("detail").and_then(|d| d.as_str()).unwrap_or("");
format!("{} {}", t, detail)
} else {
String::new()
}
}
}
pub mod helper {
use std::path::Path;
use std::fs::File;
use std::io::Read;
use openssl;
use openssl::pkey::PKey;
use openssl::rsa::Rsa;
use openssl::x509::{X509Req, X509Name};
use openssl::x509::extension::SubjectAlternativeName;
use openssl::stack::Stack;
use openssl::hash::MessageDigest;
use error::Result;
pub fn gen_key() -> Result<PKey<openssl::pkey::Private>> {
let rsa = Rsa::generate(super::BIT_LENGTH)?;
let key = PKey::from_rsa(rsa)?;
Ok(key)
}
pub fn b64(data: &[u8]) -> String {
::base64::encode_config(data, ::base64::URL_SAFE_NO_PAD)
}
pub fn read_pkey<P: AsRef<Path>>(path: P) -> Result<PKey<openssl::pkey::Private>> {
let mut file = File::open(path)?;
let mut content = Vec::new();
file.read_to_end(&mut content)?;
let key = PKey::private_key_from_pem(&content)?;
Ok(key)
}
pub fn gen_csr(pkey: &PKey<openssl::pkey::Private>, domains: &[&str]) -> Result<X509Req> {
if domains.is_empty() {
return Err("You need to supply at least one or more domain names".into());
}
let mut builder = X509Req::builder()?;
let name = {
let mut name = X509Name::builder()?;
name.append_entry_by_text("CN", domains[0])?;
name.build()
};
builder.set_subject_name(&name)?;
if domains.len() > 1 {
let san_extension = {
let mut san = SubjectAlternativeName::new();
for domain in domains.iter() {
san.dns(domain);
}
san.build(&builder.x509v3_context(None))?
};
let mut stack = Stack::new()?;
stack.push(san_extension)?;
builder.add_extensions(&stack)?;
}
builder.set_pubkey(&pkey)?;
builder.sign(pkey, MessageDigest::sha256())?;
Ok(builder.build())
}
}
#[cfg(test)]
mod tests {
extern crate env_logger;
use super::*;
const LETSENCRYPT_STAGING_DIRECTORY_URL: &'static str = "https://acme-staging.api.letsencrypt.\
org/directory";
fn test_acc() -> Result<Account> {
Directory::from_url(LETSENCRYPT_STAGING_DIRECTORY_URL)?
.account_registration()
.pkey_from_file("tests/user.key")?
.register()
}
#[test]
fn test_gen_key() {
assert!(gen_key().is_ok())
}
#[test]
fn test_b64() {
assert_eq!(b64(&"foobar".to_string().into_bytes()), "Zm9vYmFy");
}
#[test]
fn test_read_pkey() {
assert!(read_pkey("tests/user.key").is_ok());
}
#[test]
fn test_gen_csr() {
let pkey = gen_key().unwrap();
assert!(gen_csr(&pkey, &["example.com"]).is_ok());
assert!(gen_csr(&pkey, &["example.com", "sub.example.com"]).is_ok());
}
#[test]
fn test_directory() {
assert!(Directory::lets_encrypt().is_ok());
let dir = Directory::from_url(LETSENCRYPT_STAGING_DIRECTORY_URL).unwrap();
assert!(dir.url_for("new-reg").is_some());
assert!(dir.url_for("new-authz").is_some());
assert!(dir.url_for("new-cert").is_some());
assert!(!dir.get_nonce().unwrap().is_empty());
let pkey = gen_key().unwrap();
assert!(dir.jwk(&pkey).is_ok());
assert!(dir.jws(&pkey, true).is_ok());
}
#[test]
fn test_account_registration() {
let _ = env_logger::init();
let dir = Directory::from_url(LETSENCRYPT_STAGING_DIRECTORY_URL).unwrap();
assert!(dir.account_registration()
.pkey_from_file("tests/user.key")
.unwrap()
.register()
.is_ok());
}
#[test]
fn test_authorization() {
let _ = env_logger::init();
let account = test_acc().unwrap();
let auth = account.authorization("example.com").unwrap();
assert!(!auth.0.is_empty());
assert!(auth.get_challenge("http").is_some());
assert!(auth.get_http_challenge().is_some());
assert!(auth.get_dns_challenge().is_some());
for challenge in auth.0 {
assert!(!challenge.ctype.is_empty());
assert!(!challenge.url.is_empty());
assert!(!challenge.token.is_empty());
assert!(!challenge.key_authorization.is_empty());
}
}
#[test]
#[ignore]
fn test_sign_certificate() {
use std::env;
let _ = env_logger::init();
let account = test_acc().unwrap();
let auth = account
.authorization(&env::var("TEST_DOMAIN").unwrap())
.unwrap();
let http_challenge = auth.get_http_challenge().unwrap();
assert!(http_challenge
.save_key_authorization(&env::var("TEST_PUBLIC_DIR").unwrap())
.is_ok());
assert!(http_challenge.validate().is_ok());
let cert = account
.certificate_signer(&[&env::var("TEST_DOMAIN").unwrap()])
.sign_certificate()
.unwrap();
account.revoke_certificate(cert.cert()).unwrap();
}
}