store certs and private keys as generic DER+PEM strings, not using openssl crate

This commit is contained in:
Brian Picciano 2023-05-19 21:21:34 +02:00
parent 209daacf1b
commit 4f98a9a244
7 changed files with 316 additions and 54 deletions

172
Cargo.lock generated
View File

@ -47,6 +47,15 @@ dependencies = [
"memchr", "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]] [[package]]
name = "anstream" name = "anstream"
version = "0.3.2" version = "0.3.2"
@ -209,6 +218,19 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 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]] [[package]]
name = "clap" name = "clap"
version = "4.2.7" version = "4.2.7"
@ -263,6 +285,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "core-foundation-sys"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
[[package]] [[package]]
name = "cpufeatures" name = "cpufeatures"
version = "0.2.7" version = "0.2.7"
@ -358,6 +386,41 @@ dependencies = [
"typenum", "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]] [[package]]
name = "data-encoding" name = "data-encoding"
version = "2.3.3" version = "2.3.3"
@ -395,10 +458,13 @@ dependencies = [
"mime_guess", "mime_guess",
"mockall", "mockall",
"openssl", "openssl",
"pem",
"rust-embed", "rust-embed",
"rustls 0.21.1",
"serde", "serde",
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
"serde_with",
"sha2", "sha2",
"signal-hook", "signal-hook",
"signal-hook-tokio", "signal-hook-tokio",
@ -1432,11 +1498,40 @@ checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c"
dependencies = [ dependencies = [
"http", "http",
"hyper", "hyper",
"rustls", "rustls 0.20.8",
"tokio", "tokio",
"tokio-rustls", "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]] [[package]]
name = "idna" name = "idna"
version = "0.2.3" version = "0.2.3"
@ -1476,6 +1571,7 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"hashbrown 0.12.3", "hashbrown 0.12.3",
"serde",
] ]
[[package]] [[package]]
@ -1775,6 +1871,16 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" 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]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.15" version = "0.2.15"
@ -1870,6 +1976,16 @@ dependencies = [
"windows-sys 0.45.0", "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]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.2.0" version = "2.2.0"
@ -2200,7 +2316,7 @@ dependencies = [
"once_cell", "once_cell",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"rustls", "rustls 0.20.8",
"rustls-pemfile", "rustls-pemfile",
"serde", "serde",
"serde_json", "serde_json",
@ -2302,6 +2418,18 @@ dependencies = [
"webpki", "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]] [[package]]
name = "rustls-pemfile" name = "rustls-pemfile"
version = "1.0.2" version = "1.0.2"
@ -2311,6 +2439,16 @@ dependencies = [
"base64 0.21.0", "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]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.13" version = "1.0.13"
@ -2385,6 +2523,34 @@ dependencies = [
"serde", "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]] [[package]]
name = "sha1_smol" name = "sha1_smol"
version = "1.0.0" version = "1.0.0"
@ -2627,7 +2793,7 @@ version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59"
dependencies = [ dependencies = [
"rustls", "rustls 0.20.8",
"tokio", "tokio",
"webpki", "webpki",
] ]

View File

@ -34,3 +34,6 @@ serde_urlencoded = "0.7.1"
tokio-util = "0.7.8" tokio-util = "0.7.8"
acme2 = "0.5.1" acme2 = "0.5.1"
openssl = "0.10.52" openssl = "0.10.52"
rustls = "0.21.1"
pem = "2.0.1"
serde_with = "3.0.0"

View File

@ -1,6 +1,8 @@
pub mod manager; pub mod manager;
pub mod store; pub mod store;
pub type PrivateKey = openssl::pkey::PKey<openssl::pkey::Private>; mod private_key;
pub use self::private_key::PrivateKey;
pub type Certificate = openssl::x509::X509; mod certificate;
pub use self::certificate::Certificate;

View File

@ -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<u8>);
impl FromStr for Certificate {
type Err = pem::PemError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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<Self, Self::Error> {
Ok(Certificate(c.to_der()?))
}
}
impl TryFrom<&Certificate> for openssl::x509::X509 {
type Error = openssl::error::ErrorStack;
fn try_from(c: &Certificate) -> Result<Self, Self::Error> {
Ok(openssl::x509::X509::from_der(&c.0)?)
}
}
impl From<Certificate> for rustls::Certificate {
fn from(c: Certificate) -> Self {
rustls::Certificate(c.0)
}
}

View File

@ -55,17 +55,17 @@ where
match store.get_account_key() { match store.get_account_key() {
Ok(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::NotFound) => (),
Err(acme::store::GetAccountKeyError::Unexpected(err)) => return Err(err.to_unexpected()), Err(acme::store::GetAccountKeyError::Unexpected(err)) => return Err(err.to_unexpected()),
} }
let account = builder.build().await.map_unexpected()?; let account = builder.build().await.map_unexpected()?;
let account_key: acme::PrivateKey =
account.private_key().as_ref().try_into().map_unexpected()?;
store store.set_account_key(&account_key).map_unexpected()?;
.set_account_key(&account.private_key())
.map_unexpected()?;
Ok(sync::Arc::new(ManagerImpl { store, account })) Ok(sync::Arc::new(ManagerImpl { store, account }))
} }
@ -89,6 +89,10 @@ where
.expect("parsed thirty days from now as Asn1Time"); .expect("parsed thirty days from now as Asn1Time");
let cert_with_soonest_not_after = cert let cert_with_soonest_not_after = cert
.into_iter()
.map(|cert| openssl::x509::X509::try_from(&cert))
.try_collect::<Vec<openssl::x509::X509>>()
.map_unexpected()?
.into_iter() .into_iter()
.reduce(|a, b| if a.not_after() < b.not_after() { a } else { b }) .reduce(|a, b| if a.not_after() < b.not_after() { a } else { b })
.ok_or(error::Unexpected::from( .ok_or(error::Unexpected::from(
@ -206,12 +210,14 @@ where
} }
// Generate an RSA private key for the certificate. // 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 // Create a certificate signing request for the order, and request
// the certificate. // the certificate.
let order = order let order = order
.finalize(acme2::Csr::Automatic(pkey.clone())) .finalize(acme2::Csr::Automatic(acme2_pkey))
.await .await
.map_unexpected()?; .map_unexpected()?;
@ -239,14 +245,17 @@ where
// Download the certificate, and panic if it doesn't exist. // Download the certificate, and panic if it doesn't exist.
println!("fetching certificate for domain {}", domain.as_str()); println!("fetching certificate for domain {}", domain.as_str());
let cert = let cert = order
order .certificate()
.certificate() .await
.await .map_unexpected()?
.map_unexpected()? .ok_or(error::Unexpected::from(
.ok_or(error::Unexpected::from( "expected the order to return a certificate",
"expected the order to return a certificate", ))?
))?; .into_iter()
.map(|cert| acme::Certificate::try_from(cert.as_ref()))
.try_collect::<Vec<acme::Certificate>>()
.map_unexpected()?;
if cert.len() <= 1 { if cert.len() <= 1 {
return Err(error::Unexpected::from( return Err(error::Unexpected::from(
@ -260,7 +269,7 @@ where
println!("certificate for {} successfully retrieved", domain.as_str()); println!("certificate for {} successfully retrieved", domain.as_str());
self.store self.store
.set_certificate(domain.as_str(), &pkey, cert) .set_certificate(domain.as_str(), pkey, cert)
.map_unexpected()?; .map_unexpected()?;
Ok(()) Ok(())

View File

@ -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<u8>);
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<Self, Self::Err> {
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<openssl::pkey::Private>> for PrivateKey {
type Error = openssl::error::ErrorStack;
fn try_from(k: &openssl::pkey::PKeyRef<openssl::pkey::Private>) -> Result<Self, Self::Error> {
Ok(PrivateKey(k.private_key_to_der()?))
}
}
impl TryFrom<&PrivateKey> for openssl::pkey::PKey<openssl::pkey::Private> {
type Error = openssl::error::ErrorStack;
fn try_from(k: &PrivateKey) -> Result<Self, Self::Error> {
Ok(openssl::pkey::PKey::private_key_from_der(&k.0)?)
}
}
impl From<PrivateKey> for rustls::PrivateKey {
fn from(k: PrivateKey) -> Self {
rustls::PrivateKey(k.0)
}
}

View File

@ -1,4 +1,5 @@
use std::io::{Read, Write}; use std::io::{Read, Write};
use std::str::FromStr;
use std::{fs, io, path, sync}; use std::{fs, io, path, sync};
use crate::domain::acme::{Certificate, PrivateKey}; use crate::domain::acme::{Certificate, PrivateKey};
@ -48,7 +49,7 @@ pub trait Store {
fn set_certificate( fn set_certificate(
&self, &self,
domain: &str, domain: &str,
key: &PrivateKey, key: PrivateKey,
cert: Vec<Certificate>, cert: Vec<Certificate>,
) -> Result<(), error::Unexpected>; ) -> Result<(), error::Unexpected>;
@ -63,8 +64,8 @@ pub trait BoxedStore: Store + Send + Sync + Clone + 'static {}
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
struct StoredPKeyCert { struct StoredPKeyCert {
private_key_pem: String, private_key: PrivateKey,
cert_pems: Vec<String>, cert: Vec<Certificate>,
} }
struct FSStore { struct FSStore {
@ -109,8 +110,7 @@ impl BoxedStore for sync::Arc<FSStore> {}
impl Store for sync::Arc<FSStore> { impl Store for sync::Arc<FSStore> {
fn set_account_key(&self, k: &PrivateKey) -> Result<(), error::Unexpected> { fn set_account_key(&self, k: &PrivateKey) -> Result<(), error::Unexpected> {
let mut file = fs::File::create(self.account_key_path()).map_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(k.to_string().as_bytes()).map_unexpected()?;
file.write_all(&pem).map_unexpected()?;
Ok(()) Ok(())
} }
@ -120,11 +120,11 @@ impl Store for sync::Arc<FSStore> {
_ => e.to_unexpected().into(), _ => e.to_unexpected().into(),
})?; })?;
let mut pem = Vec::<u8>::new(); let mut key = String::new();
file.read_to_end(&mut pem).map_unexpected()?; file.read_to_string(&mut key).map_unexpected()?;
let k = PrivateKey::private_key_from_pem(&pem).map_unexpected()?; let key = PrivateKey::from_str(&key).map_unexpected()?;
Ok(k) Ok(key)
} }
fn set_http01_challenge_key(&self, token: &str, key: &str) -> Result<(), error::Unexpected> { fn set_http01_challenge_key(&self, token: &str, key: &str) -> Result<(), error::Unexpected> {
@ -154,20 +154,12 @@ impl Store for sync::Arc<FSStore> {
fn set_certificate( fn set_certificate(
&self, &self,
domain: &str, domain: &str,
key: &PrivateKey, key: PrivateKey,
cert: Vec<Certificate>, cert: Vec<Certificate>,
) -> Result<(), error::Unexpected> { ) -> Result<(), error::Unexpected> {
let to_store = StoredPKeyCert { let to_store = StoredPKeyCert {
private_key_pem: String::from_utf8(key.private_key_to_pem_pkcs8().map_unexpected()?) private_key: key,
.map_unexpected()?, cert: cert,
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::<String, error::Unexpected>(cert_pem)
})
.try_collect()?,
}; };
let cert_file = fs::File::create(self.certificate_path(domain)).map_unexpected()?; let cert_file = fs::File::create(self.certificate_path(domain)).map_unexpected()?;
@ -188,22 +180,14 @@ impl Store for sync::Arc<FSStore> {
let stored: StoredPKeyCert = serde_json::from_reader(file).map_unexpected()?; let stored: StoredPKeyCert = serde_json::from_reader(file).map_unexpected()?;
let key = Ok((stored.private_key, stored.cert))
PrivateKey::private_key_from_pem(stored.private_key_pem.as_bytes()).map_unexpected()?;
let cert: Vec<Certificate> = stored
.cert_pems
.into_iter()
.map(|cert| openssl::x509::X509::from_pem(cert.as_bytes()).map_unexpected())
.try_collect()?;
Ok((key, cert))
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::domain::acme;
use tempdir::TempDir; use tempdir::TempDir;
#[test] #[test]
@ -216,17 +200,15 @@ mod tests {
Err::<PrivateKey, GetAccountKeyError>(GetAccountKeyError::NotFound) Err::<PrivateKey, GetAccountKeyError>(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"); store.set_account_key(&k).expect("account private key set");
assert_eq!( assert_eq!(
k.private_key_to_pem_pkcs8().unwrap(), k,
store store
.get_account_key() .get_account_key()
.expect("account private key retrieved") .expect("account private key retrieved")
.private_key_to_pem_pkcs8()
.unwrap()
); );
} }