]> git.proxmox.com Git - proxmox-acme-rs.git/commitdiff
add util::Csr for CSR generation
authorWolfgang Bumiller <w.bumiller@proxmox.com>
Mon, 12 Apr 2021 12:04:10 +0000 (14:04 +0200)
committerWolfgang Bumiller <w.bumiller@proxmox.com>
Mon, 12 Apr 2021 12:54:21 +0000 (14:54 +0200)
This is essentially taken from pmg-rs and should be used
from there.

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
src/error.rs
src/lib.rs
src/util.rs [new file with mode: 0644]

index eb6015becdb7e2ff8af91a46436e1939410cab3f..89fa7ab60a537dd36bc844da11a5952592e95de2 100644 (file)
@@ -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<SslErrorStack> for Error {
     fn from(e: SslErrorStack) -> Self {
-        Error::Ssl(e)
+        Error::RawSsl(e)
     }
 }
 
index 77dc383de15fae2f53fc5607ce640aa82a5078a9..273701913168725f21cc78e123f6c5c685679c23 100644 (file)
@@ -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 (file)
index 0000000..bfef364
--- /dev/null
@@ -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<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,
+        })
+    }
+}