diff --git a/src/error.rs b/src/error.rs index eb6015be..89fa7ab6 100644 --- a/src/error.rs +++ b/src/error.rs @@ -44,7 +44,11 @@ pub enum Error { BadOrderData(String), /// An openssl error occurred during a crypto operation. - Ssl(SslErrorStack), + RawSsl(SslErrorStack), + + /// An openssl error occurred during a crypto operation. + /// With some textual context. + Ssl(&'static str, SslErrorStack), /// An otherwise uncaught serde error happened. Json(serde_json::Error), @@ -61,6 +65,9 @@ pub enum Error { /// If built with the `client` feature, this is where client specific errors which are not from /// errors forwarded from `curl` end up. Client(String), + + /// A non-openssl error occurred while building data for the CSR. + Csr(String), } impl Error { @@ -98,18 +105,22 @@ impl fmt::Display for Error { Error::BadOrderData(err) => { write!(f, "bad response to new-order query or creation: {}", err) } - Error::Ssl(err) => fmt::Display::fmt(err, f), + Error::RawSsl(err) => fmt::Display::fmt(err, f), + Error::Ssl(context, err) => { + write!(f, "{}: {}", context, err) + } Error::Json(err) => fmt::Display::fmt(err, f), Error::Custom(err) => fmt::Display::fmt(err, f), Error::HttpClient(err) => fmt::Display::fmt(err, f), Error::Client(err) => fmt::Display::fmt(err, f), + Error::Csr(err) => fmt::Display::fmt(err, f), } } } impl From for Error { fn from(e: SslErrorStack) -> Self { - Error::Ssl(e) + Error::RawSsl(e) } } diff --git a/src/lib.rs b/src/lib.rs index 77dc383d..27370191 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,7 @@ pub mod authorization; pub mod directory; pub mod error; pub mod order; +pub mod util; pub use account::Account; pub use authorization::{Authorization, Challenge}; diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 00000000..bfef364d --- /dev/null +++ b/src/util.rs @@ -0,0 +1,98 @@ +use std::collections::HashMap; + +use openssl::hash::MessageDigest; +use openssl::nid::Nid; +use openssl::pkey::PKey; +use openssl::rsa::Rsa; +use openssl::x509::{X509Extension, X509Name, X509Req}; + +use crate::Error; + +pub struct Csr { + /// DER encoded certificate request. + pub data: Vec, + + /// PEM formatted PKCS#8 private key. + pub private_key_pem: Vec, +} + +impl Csr { + /// Generate a CSR in DER format with a PEM formatted PKCS8 private key. + /// + /// The `identifiers` should be a list of domains. The `attributes` should have standard names + /// recognized by openssl. + pub fn generate( + identifiers: &[impl AsRef], + attributes: &HashMap, + ) -> Result { + if identifiers.is_empty() { + return Err(Error::Csr(format!("cannot generate empty CSR"))); + } + + let private_key = Rsa::generate(4096) + .and_then(PKey::from_rsa) + .map_err(|err| Error::Ssl("failed to generate RSA key: {}", err))?; + + let private_key_pem = private_key + .private_key_to_pem_pkcs8() + .map_err(|err| Error::Ssl("failed to format private key as PEM pkcs8: {}", err))?; + + let mut name = X509Name::builder()?; + if !attributes.contains_key("CN") { + name.append_entry_by_nid(Nid::COMMONNAME, identifiers[0].as_ref())?; + } + for (key, value) in attributes { + name.append_entry_by_text(key, value)?; + } + let name = name.build(); + + let mut csr = X509Req::builder()?; + csr.set_subject_name(&name)?; + csr.set_pubkey(&private_key)?; + + let context = csr.x509v3_context(None); + let mut ext = openssl::stack::Stack::new()?; + ext.push(X509Extension::new_nid( + None, + None, + Nid::BASIC_CONSTRAINTS, + "CA:FALSE", + )?)?; + ext.push(X509Extension::new_nid( + None, + None, + Nid::KEY_USAGE, + "digitalSignature,keyEncipherment", + )?)?; + ext.push(X509Extension::new_nid( + None, + None, + Nid::EXT_KEY_USAGE, + "serverAuth,clientAuth", + )?)?; + ext.push(X509Extension::new_nid( + None, + Some(&context), + Nid::SUBJECT_ALT_NAME, + &identifiers + .into_iter() + .try_fold(String::new(), |mut acc, dns| { + if !acc.is_empty() { + acc.push(','); + } + use std::fmt::Write; + write!(acc, "DNS:{}", dns.as_ref())?; + Ok::<_, std::fmt::Error>(acc) + }) + .map_err(|err| Error::Csr(err.to_string()))?, + )?)?; + csr.add_extensions(&ext)?; + + csr.sign(&private_key, MessageDigest::sha256())?; + + Ok(Self { + data: csr.build().to_der()?, + private_key_pem, + }) + } +}