From 4f98a9a2443783d76e57acfb37679372fb836568 Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Fri, 19 May 2023 21:21:34 +0200 Subject: [PATCH] store certs and private keys as generic DER+PEM strings, not using openssl crate --- Cargo.lock | 172 ++++++++++++++++++++++++++++++++- Cargo.toml | 3 + src/domain/acme.rs | 6 +- src/domain/acme/certificate.rs | 45 +++++++++ src/domain/acme/manager.rs | 39 +++++--- src/domain/acme/private_key.rs | 55 +++++++++++ src/domain/acme/store.rs | 50 +++------- 7 files changed, 316 insertions(+), 54 deletions(-) create mode 100644 src/domain/acme/certificate.rs create mode 100644 src/domain/acme/private_key.rs diff --git a/Cargo.lock b/Cargo.lock index 0cdca0c..1f9b7e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,6 +47,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.3.2" @@ -209,6 +218,19 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" +dependencies = [ + "iana-time-zone", + "num-integer", + "num-traits", + "serde", + "winapi", +] + [[package]] name = "clap" version = "4.2.7" @@ -263,6 +285,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + [[package]] name = "cpufeatures" version = "0.2.7" @@ -358,6 +386,41 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0558d22a7b463ed0241e993f76f09f30b126687447751a8638587b864e4b3944" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8bfa2e259f8ee1ce5e97824a3c55ec4404a0d772ca7fa96bf19f0752a046eb" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.15", +] + +[[package]] +name = "darling_macro" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.15", +] + [[package]] name = "data-encoding" version = "2.3.3" @@ -395,10 +458,13 @@ dependencies = [ "mime_guess", "mockall", "openssl", + "pem", "rust-embed", + "rustls 0.21.1", "serde", "serde_json", "serde_urlencoded", + "serde_with", "sha2", "signal-hook", "signal-hook-tokio", @@ -1432,11 +1498,40 @@ checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" dependencies = [ "http", "hyper", - "rustls", + "rustls 0.20.8", "tokio", "tokio-rustls", ] +[[package]] +name = "iana-time-zone" +version = "0.1.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.2.3" @@ -1476,6 +1571,7 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", + "serde", ] [[package]] @@ -1775,6 +1871,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.15" @@ -1870,6 +1976,16 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "pem" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b13fe415cdf3c8e44518e18a7c95a13431d9bdf6d15367d82b23c377fdd441a" +dependencies = [ + "base64 0.21.0", + "serde", +] + [[package]] name = "percent-encoding" version = "2.2.0" @@ -2200,7 +2316,7 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls", + "rustls 0.20.8", "rustls-pemfile", "serde", "serde_json", @@ -2302,6 +2418,18 @@ dependencies = [ "webpki", ] +[[package]] +name = "rustls" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c911ba11bc8433e811ce56fde130ccf32f5127cab0e0194e9c68c5a5b671791e" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + [[package]] name = "rustls-pemfile" version = "1.0.2" @@ -2311,6 +2439,16 @@ dependencies = [ "base64 0.21.0", ] +[[package]] +name = "rustls-webpki" +version = "0.100.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "ryu" version = "1.0.13" @@ -2385,6 +2523,34 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f02d8aa6e3c385bf084924f660ce2a3a6bd333ba55b35e8590b321f35d88513" +dependencies = [ + "base64 0.21.0", + "chrono", + "hex", + "indexmap", + "serde", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc7d5d3932fb12ce722ee5e64dd38c504efba37567f0c402f6ca728c3b8b070" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.15", +] + [[package]] name = "sha1_smol" version = "1.0.0" @@ -2627,7 +2793,7 @@ version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" dependencies = [ - "rustls", + "rustls 0.20.8", "tokio", "webpki", ] diff --git a/Cargo.toml b/Cargo.toml index 832b58b..6fa46f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,3 +34,6 @@ serde_urlencoded = "0.7.1" tokio-util = "0.7.8" acme2 = "0.5.1" openssl = "0.10.52" +rustls = "0.21.1" +pem = "2.0.1" +serde_with = "3.0.0" diff --git a/src/domain/acme.rs b/src/domain/acme.rs index 3dd4acb..8b2e126 100644 --- a/src/domain/acme.rs +++ b/src/domain/acme.rs @@ -1,6 +1,8 @@ pub mod manager; pub mod store; -pub type PrivateKey = openssl::pkey::PKey; +mod private_key; +pub use self::private_key::PrivateKey; -pub type Certificate = openssl::x509::X509; +mod certificate; +pub use self::certificate::Certificate; diff --git a/src/domain/acme/certificate.rs b/src/domain/acme/certificate.rs new file mode 100644 index 0000000..1b92b47 --- /dev/null +++ b/src/domain/acme/certificate.rs @@ -0,0 +1,45 @@ +use std::convert::{From, TryFrom}; +use std::fmt; +use std::str::FromStr; + +use serde_with::{DeserializeFromStr, SerializeDisplay}; + +#[derive(Debug, Clone, PartialEq, DeserializeFromStr, SerializeDisplay)] +/// DER-encoded X.509, like rustls::Certificate. +pub struct Certificate(Vec); + +impl FromStr for Certificate { + type Err = pem::PemError; + + fn from_str(s: &str) -> Result { + Ok(Certificate(pem::parse(s)?.into_contents())) + } +} + +impl fmt::Display for Certificate { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + pem::Pem::new("CERTIFICATE", self.0.clone()).fmt(f) + } +} + +impl TryFrom<&openssl::x509::X509Ref> for Certificate { + type Error = openssl::error::ErrorStack; + + fn try_from(c: &openssl::x509::X509Ref) -> Result { + Ok(Certificate(c.to_der()?)) + } +} + +impl TryFrom<&Certificate> for openssl::x509::X509 { + type Error = openssl::error::ErrorStack; + + fn try_from(c: &Certificate) -> Result { + Ok(openssl::x509::X509::from_der(&c.0)?) + } +} + +impl From for rustls::Certificate { + fn from(c: Certificate) -> Self { + rustls::Certificate(c.0) + } +} diff --git a/src/domain/acme/manager.rs b/src/domain/acme/manager.rs index a877181..dfbc30d 100644 --- a/src/domain/acme/manager.rs +++ b/src/domain/acme/manager.rs @@ -55,17 +55,17 @@ where match store.get_account_key() { Ok(account_key) => { - builder.private_key(account_key); + builder.private_key((&account_key).try_into().map_unexpected()?); } Err(acme::store::GetAccountKeyError::NotFound) => (), Err(acme::store::GetAccountKeyError::Unexpected(err)) => return Err(err.to_unexpected()), } let account = builder.build().await.map_unexpected()?; + let account_key: acme::PrivateKey = + account.private_key().as_ref().try_into().map_unexpected()?; - store - .set_account_key(&account.private_key()) - .map_unexpected()?; + store.set_account_key(&account_key).map_unexpected()?; Ok(sync::Arc::new(ManagerImpl { store, account })) } @@ -89,6 +89,10 @@ where .expect("parsed thirty days from now as Asn1Time"); let cert_with_soonest_not_after = cert + .into_iter() + .map(|cert| openssl::x509::X509::try_from(&cert)) + .try_collect::>() + .map_unexpected()? .into_iter() .reduce(|a, b| if a.not_after() < b.not_after() { a } else { b }) .ok_or(error::Unexpected::from( @@ -206,12 +210,14 @@ where } // Generate an RSA private key for the certificate. - let pkey = acme2::gen_rsa_private_key(4096).map_unexpected()?; + let pkey = acme::PrivateKey::new(); + + let acme2_pkey = (&pkey).try_into().map_unexpected()?; // Create a certificate signing request for the order, and request // the certificate. let order = order - .finalize(acme2::Csr::Automatic(pkey.clone())) + .finalize(acme2::Csr::Automatic(acme2_pkey)) .await .map_unexpected()?; @@ -239,14 +245,17 @@ where // Download the certificate, and panic if it doesn't exist. println!("fetching certificate for domain {}", domain.as_str()); - let cert = - order - .certificate() - .await - .map_unexpected()? - .ok_or(error::Unexpected::from( - "expected the order to return a certificate", - ))?; + let cert = order + .certificate() + .await + .map_unexpected()? + .ok_or(error::Unexpected::from( + "expected the order to return a certificate", + ))? + .into_iter() + .map(|cert| acme::Certificate::try_from(cert.as_ref())) + .try_collect::>() + .map_unexpected()?; if cert.len() <= 1 { return Err(error::Unexpected::from( @@ -260,7 +269,7 @@ where println!("certificate for {} successfully retrieved", domain.as_str()); self.store - .set_certificate(domain.as_str(), &pkey, cert) + .set_certificate(domain.as_str(), pkey, cert) .map_unexpected()?; Ok(()) diff --git a/src/domain/acme/private_key.rs b/src/domain/acme/private_key.rs new file mode 100644 index 0000000..7365ec9 --- /dev/null +++ b/src/domain/acme/private_key.rs @@ -0,0 +1,55 @@ +use std::convert::{From, TryFrom}; +use std::fmt; +use std::str::FromStr; + +use serde_with::{DeserializeFromStr, SerializeDisplay}; + +#[derive(Debug, Clone, PartialEq, DeserializeFromStr, SerializeDisplay)] +/// DER-encoded ASN.1 in either PKCS#8, PKCS#1, or Sec1 format, like rustls::PrivateKey. +pub struct PrivateKey(Vec); + +impl PrivateKey { + pub fn new() -> PrivateKey { + acme2::gen_rsa_private_key(4096) + .expect("RSA private key generated") + .as_ref() + .try_into() + .expect("RSA private key converted to internal representation") + } +} + +impl FromStr for PrivateKey { + type Err = pem::PemError; + + fn from_str(s: &str) -> Result { + Ok(PrivateKey(pem::parse(s)?.into_contents())) + } +} + +impl fmt::Display for PrivateKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + pem::Pem::new("PRIVATE KEY", self.0.clone()).fmt(f) + } +} + +impl TryFrom<&openssl::pkey::PKeyRef> for PrivateKey { + type Error = openssl::error::ErrorStack; + + fn try_from(k: &openssl::pkey::PKeyRef) -> Result { + Ok(PrivateKey(k.private_key_to_der()?)) + } +} + +impl TryFrom<&PrivateKey> for openssl::pkey::PKey { + type Error = openssl::error::ErrorStack; + + fn try_from(k: &PrivateKey) -> Result { + Ok(openssl::pkey::PKey::private_key_from_der(&k.0)?) + } +} + +impl From for rustls::PrivateKey { + fn from(k: PrivateKey) -> Self { + rustls::PrivateKey(k.0) + } +} diff --git a/src/domain/acme/store.rs b/src/domain/acme/store.rs index 1ae46f3..e58d197 100644 --- a/src/domain/acme/store.rs +++ b/src/domain/acme/store.rs @@ -1,4 +1,5 @@ use std::io::{Read, Write}; +use std::str::FromStr; use std::{fs, io, path, sync}; use crate::domain::acme::{Certificate, PrivateKey}; @@ -48,7 +49,7 @@ pub trait Store { fn set_certificate( &self, domain: &str, - key: &PrivateKey, + key: PrivateKey, cert: Vec, ) -> Result<(), error::Unexpected>; @@ -63,8 +64,8 @@ pub trait BoxedStore: Store + Send + Sync + Clone + 'static {} #[derive(Debug, Serialize, Deserialize)] struct StoredPKeyCert { - private_key_pem: String, - cert_pems: Vec, + private_key: PrivateKey, + cert: Vec, } struct FSStore { @@ -109,8 +110,7 @@ impl BoxedStore for sync::Arc {} impl Store for sync::Arc { fn set_account_key(&self, k: &PrivateKey) -> Result<(), error::Unexpected> { let mut file = fs::File::create(self.account_key_path()).map_unexpected()?; - let pem = k.private_key_to_pem_pkcs8().map_unexpected()?; - file.write_all(&pem).map_unexpected()?; + file.write_all(k.to_string().as_bytes()).map_unexpected()?; Ok(()) } @@ -120,11 +120,11 @@ impl Store for sync::Arc { _ => e.to_unexpected().into(), })?; - let mut pem = Vec::::new(); - file.read_to_end(&mut pem).map_unexpected()?; + let mut key = String::new(); + file.read_to_string(&mut key).map_unexpected()?; - let k = PrivateKey::private_key_from_pem(&pem).map_unexpected()?; - Ok(k) + let key = PrivateKey::from_str(&key).map_unexpected()?; + Ok(key) } fn set_http01_challenge_key(&self, token: &str, key: &str) -> Result<(), error::Unexpected> { @@ -154,20 +154,12 @@ impl Store for sync::Arc { fn set_certificate( &self, domain: &str, - key: &PrivateKey, + key: PrivateKey, cert: Vec, ) -> Result<(), error::Unexpected> { let to_store = StoredPKeyCert { - private_key_pem: String::from_utf8(key.private_key_to_pem_pkcs8().map_unexpected()?) - .map_unexpected()?, - cert_pems: cert - .into_iter() - .map(|cert| { - let cert_pem = cert.to_pem().map_unexpected()?; - let cert_pem = String::from_utf8(cert_pem).map_unexpected()?; - Ok::(cert_pem) - }) - .try_collect()?, + private_key: key, + cert: cert, }; let cert_file = fs::File::create(self.certificate_path(domain)).map_unexpected()?; @@ -188,22 +180,14 @@ impl Store for sync::Arc { let stored: StoredPKeyCert = serde_json::from_reader(file).map_unexpected()?; - let key = - PrivateKey::private_key_from_pem(stored.private_key_pem.as_bytes()).map_unexpected()?; - - let cert: Vec = stored - .cert_pems - .into_iter() - .map(|cert| openssl::x509::X509::from_pem(cert.as_bytes()).map_unexpected()) - .try_collect()?; - - Ok((key, cert)) + Ok((stored.private_key, stored.cert)) } } #[cfg(test)] mod tests { use super::*; + use crate::domain::acme; use tempdir::TempDir; #[test] @@ -216,17 +200,15 @@ mod tests { Err::(GetAccountKeyError::NotFound) )); - let k = acme2::gen_rsa_private_key(4096).expect("private key generated"); + let k = acme::PrivateKey::new(); store.set_account_key(&k).expect("account private key set"); assert_eq!( - k.private_key_to_pem_pkcs8().unwrap(), + k, store .get_account_key() .expect("account private key retrieved") - .private_key_to_pem_pkcs8() - .unwrap() ); }