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),
/// 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 {
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<SslErrorStack> for Error {
fn from(e: SslErrorStack) -> Self {
- Error::Ssl(e)
+ Error::RawSsl(e)
}
}
--- /dev/null
+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<u8>,
+
+ /// PEM formatted PKCS#8 private key.
+ pub private_key_pem: Vec<u8>,
+}
+
+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<str>],
+ attributes: &HashMap<String, &str>,
+ ) -> Result<Self, Error> {
+ 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,
+ })
+ }
+}