wip: implelented SingleServerCrypto struct
This commit is contained in:
parent
e2ea82a344
commit
d657f6d766
6 changed files with 371 additions and 0 deletions
|
|
@ -13,3 +13,29 @@ publish.workspace = true
|
||||||
[features]
|
[features]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
rustc-hash = { version = "1.1.0" }
|
||||||
|
tracing = { version = "0.1.40" }
|
||||||
|
# anyhow = "1.0.86"
|
||||||
|
derive_builder = { version = "0.20.0" }
|
||||||
|
thiserror = { version = "1.0.61" }
|
||||||
|
# hot_reload = {version = "0.1.5"}
|
||||||
|
async-trait = { version = "0.1.80" }
|
||||||
|
# tokio-rustls = { version = "0.26.0", features = ["early-data"] }
|
||||||
|
rustls = { version = "0.23.8", default-features = false, features = [
|
||||||
|
"aws_lc_rs",
|
||||||
|
] }
|
||||||
|
rustls-pemfile = { version = "2.1.2" }
|
||||||
|
rustls-webpki = { version = "0.102.4", default-features = false, features = [
|
||||||
|
"std",
|
||||||
|
"aws_lc_rs",
|
||||||
|
] }
|
||||||
|
x509-parser = { version = "0.16.0" }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio = { version = "1.37.0", default-features = false, features = [
|
||||||
|
# "net",
|
||||||
|
"rt-multi-thread",
|
||||||
|
# "time",
|
||||||
|
# "sync",
|
||||||
|
"macros",
|
||||||
|
] }
|
||||||
|
|
|
||||||
102
rpxy-certs/src/certs.rs
Normal file
102
rpxy-certs/src/certs.rs
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
use crate::error::*;
|
||||||
|
use rustc_hash::FxHashMap as HashMap;
|
||||||
|
use rustls::{crypto::aws_lc_rs::sign::any_supported_type, pki_types, sign::CertifiedKey};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use x509_parser::prelude::*;
|
||||||
|
|
||||||
|
/* ------------------------------------------------ */
|
||||||
|
/// Raw certificates in rustls format
|
||||||
|
type Certificate = rustls::pki_types::CertificateDer<'static>;
|
||||||
|
/// Raw private key in rustls format
|
||||||
|
type PrivateKey = pki_types::PrivateKeyDer<'static>;
|
||||||
|
/// Client CA trust anchors subject to the subject key identifier
|
||||||
|
type TrustAnchors = HashMap<Vec<u8>, pki_types::TrustAnchor<'static>>;
|
||||||
|
|
||||||
|
/* ------------------------------------------------ */
|
||||||
|
/// Raw certificates and private keys loaded from files for a single server name
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
|
pub struct SingleServerCrypto {
|
||||||
|
certs: Vec<Certificate>,
|
||||||
|
cert_keys: Arc<Vec<PrivateKey>>,
|
||||||
|
client_ca_certs: Option<Vec<Certificate>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SingleServerCrypto {
|
||||||
|
/// Create a new instance of SingleServerCrypto
|
||||||
|
pub fn new(certs: &[Certificate], cert_keys: &Arc<Vec<PrivateKey>>, client_ca_certs: &Option<Vec<Certificate>>) -> Self {
|
||||||
|
Self {
|
||||||
|
certs: certs.to_owned(),
|
||||||
|
cert_keys: cert_keys.clone(),
|
||||||
|
client_ca_certs: client_ca_certs.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Check if mutual tls is enabled
|
||||||
|
pub fn is_mutual_tls(&self) -> bool {
|
||||||
|
self.client_ca_certs.is_some()
|
||||||
|
}
|
||||||
|
/* ------------------------------------------------ */
|
||||||
|
/// Convert the certificates to bytes in der
|
||||||
|
pub fn certs_bytes(&self) -> Vec<Vec<u8>> {
|
||||||
|
self.certs.iter().map(|c| c.to_vec()).collect()
|
||||||
|
}
|
||||||
|
/// Convert the private keys to bytes in der
|
||||||
|
pub fn cert_keys_bytes(&self) -> Vec<Vec<u8>> {
|
||||||
|
self
|
||||||
|
.cert_keys
|
||||||
|
.iter()
|
||||||
|
.map(|k| match k {
|
||||||
|
pki_types::PrivateKeyDer::Pkcs1(pkcs1) => pkcs1.secret_pkcs1_der().to_owned(),
|
||||||
|
pki_types::PrivateKeyDer::Sec1(sec1) => sec1.secret_sec1_der().to_owned(),
|
||||||
|
pki_types::PrivateKeyDer::Pkcs8(pkcs8) => pkcs8.secret_pkcs8_der().to_owned(),
|
||||||
|
_ => unreachable!(),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
/// Convert the client CA certificates to bytes in der
|
||||||
|
pub fn client_ca_certs_bytes(&self) -> Option<Vec<Vec<u8>>> {
|
||||||
|
self.client_ca_certs.as_ref().map(|v| v.iter().map(|c| c.to_vec()).collect())
|
||||||
|
}
|
||||||
|
/* ------------------------------------------------ */
|
||||||
|
/// Parse the certificates and private keys for a single server and return a rustls CertifiedKey
|
||||||
|
pub fn rustls_certified_key(&self) -> Result<CertifiedKey, RpxyCertError> {
|
||||||
|
let signing_key = self
|
||||||
|
.cert_keys
|
||||||
|
.clone()
|
||||||
|
.iter()
|
||||||
|
.find_map(|k| if let Ok(sk) = any_supported_type(k) { Some(sk) } else { None })
|
||||||
|
.ok_or_else(|| RpxyCertError::InvalidCertificateAndKey)?;
|
||||||
|
|
||||||
|
let cert = self.certs.iter().map(|c| Certificate::from(c.to_vec())).collect::<Vec<_>>();
|
||||||
|
Ok(CertifiedKey::new(cert, signing_key))
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------ */
|
||||||
|
/// Parse the client CA certificates and return a hashmap of pairs of a subject key identifier (key) and a trust anchor (value)
|
||||||
|
pub fn rustls_trust_anchors(&self) -> Result<TrustAnchors, RpxyCertError> {
|
||||||
|
//-> Result<(Vec<OwnedTrustAnchor>, HashSet<Vec<u8>>), anyhow::Error> {
|
||||||
|
let Some(certs) = self.client_ca_certs.as_ref() else {
|
||||||
|
return Err(RpxyCertError::NoClientCert);
|
||||||
|
};
|
||||||
|
let certs = certs.iter().map(|c| Certificate::from(c.to_vec())).collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let trust_anchors = certs
|
||||||
|
.iter()
|
||||||
|
.filter_map(|v| {
|
||||||
|
// retrieve trust anchor
|
||||||
|
let trust_anchor = webpki::anchor_from_trusted_cert(v).ok()?;
|
||||||
|
|
||||||
|
// retrieve ca key id (subject key id)
|
||||||
|
let x509_cert = parse_x509_certificate(v).map(|v| v.1).ok()?;
|
||||||
|
let mut subject_key_ids = x509_cert.iter_extensions().filter_map(|ext| match ext.parsed_extension() {
|
||||||
|
ParsedExtension::SubjectKeyIdentifier(skid) => Some(skid),
|
||||||
|
_ => None,
|
||||||
|
});
|
||||||
|
let skid = subject_key_ids.next()?;
|
||||||
|
|
||||||
|
Some((skid.0.to_owned(), trust_anchor.to_owned()))
|
||||||
|
})
|
||||||
|
.collect::<HashMap<_, _>>();
|
||||||
|
|
||||||
|
Ok(trust_anchors)
|
||||||
|
}
|
||||||
|
}
|
||||||
15
rpxy-certs/src/error.rs
Normal file
15
rpxy-certs/src/error.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
/// Describes things that can go wrong in the Rpxy certificate
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum RpxyCertError {
|
||||||
|
/// Error when reading certificates and keys
|
||||||
|
#[error("Failed to read certificates from file: {0}")]
|
||||||
|
IoError(#[from] std::io::Error),
|
||||||
|
/// Error when parsing certificates and keys to generate a rustls CertifiedKey
|
||||||
|
#[error("Unable to find a valid certificate and key")]
|
||||||
|
InvalidCertificateAndKey,
|
||||||
|
/// Error when parsing client CA certificates: No client certificate found
|
||||||
|
#[error("No client certificate found")]
|
||||||
|
NoClientCert,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
mod certs;
|
||||||
|
mod error;
|
||||||
|
mod service;
|
||||||
|
mod source;
|
||||||
|
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
pub(crate) mod log {
|
||||||
|
pub(crate) use tracing::{debug, error, info, warn};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub use crate::{
|
||||||
|
certs::SingleServerCrypto,
|
||||||
|
source::{CryptoFileSource, CryptoFileSourceBuilder, CryptoFileSourceBuilderError, CryptoSource},
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ------------------------------------------------ */
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn read_server_crt_key_files() {
|
||||||
|
let tls_cert_path = "../example-certs/server.crt";
|
||||||
|
let tls_cert_key_path = "../example-certs/server.key";
|
||||||
|
let crypto_file_source = CryptoFileSourceBuilder::default()
|
||||||
|
.tls_cert_key_path(tls_cert_key_path)
|
||||||
|
.tls_cert_path(tls_cert_path)
|
||||||
|
.build();
|
||||||
|
assert!(crypto_file_source.is_ok());
|
||||||
|
|
||||||
|
let crypto_file_source = crypto_file_source.unwrap();
|
||||||
|
let crypto_elem = crypto_file_source.read().await;
|
||||||
|
assert!(crypto_elem.is_ok());
|
||||||
|
|
||||||
|
let crypto_elem = crypto_elem.unwrap();
|
||||||
|
let certificed_key = crypto_elem.rustls_certified_key();
|
||||||
|
assert!(certificed_key.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn read_server_crt_key_files_with_client_ca_crt() {
|
||||||
|
let tls_cert_path = "../example-certs/server.crt";
|
||||||
|
let tls_cert_key_path = "../example-certs/server.key";
|
||||||
|
let client_ca_cert_path = Some("../example-certs/client.ca.crt");
|
||||||
|
let crypto_file_source = CryptoFileSourceBuilder::default()
|
||||||
|
.tls_cert_key_path(tls_cert_key_path)
|
||||||
|
.tls_cert_path(tls_cert_path)
|
||||||
|
.client_ca_cert_path(client_ca_cert_path)
|
||||||
|
.build();
|
||||||
|
assert!(crypto_file_source.is_ok());
|
||||||
|
|
||||||
|
let crypto_file_source = crypto_file_source.unwrap();
|
||||||
|
let crypto_elem = crypto_file_source.read().await;
|
||||||
|
assert!(crypto_elem.is_ok());
|
||||||
|
|
||||||
|
let crypto_elem = crypto_elem.unwrap();
|
||||||
|
assert!(crypto_elem.is_mutual_tls());
|
||||||
|
|
||||||
|
let certificed_key = crypto_elem.rustls_certified_key();
|
||||||
|
assert!(certificed_key.is_ok());
|
||||||
|
|
||||||
|
let trust_anchors = crypto_elem.rustls_trust_anchors();
|
||||||
|
assert!(trust_anchors.is_ok());
|
||||||
|
|
||||||
|
let trust_anchors = trust_anchors.unwrap();
|
||||||
|
assert_eq!(trust_anchors.len(), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
0
rpxy-certs/src/service.rs
Normal file
0
rpxy-certs/src/service.rs
Normal file
160
rpxy-certs/src/source.rs
Normal file
160
rpxy-certs/src/source.rs
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
use crate::{certs::SingleServerCrypto, error::*, log::*};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use derive_builder::Builder;
|
||||||
|
use std::{
|
||||||
|
fs::File,
|
||||||
|
io::{self, BufReader, Cursor, Read},
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ------------------------------------------------ */
|
||||||
|
#[async_trait]
|
||||||
|
// Trait to read certs and keys anywhere from KVS, file, sqlite, etc.
|
||||||
|
pub trait CryptoSource {
|
||||||
|
type Error;
|
||||||
|
|
||||||
|
/// read crypto materials from source
|
||||||
|
async fn read(&self) -> Result<SingleServerCrypto, Self::Error>;
|
||||||
|
|
||||||
|
/// Returns true when mutual tls is enabled
|
||||||
|
fn is_mutual_tls(&self) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------ */
|
||||||
|
#[derive(Builder, Debug, Clone)]
|
||||||
|
/// Crypto-related file reader implementing `CryptoSource`` trait
|
||||||
|
pub struct CryptoFileSource {
|
||||||
|
#[builder(setter(custom))]
|
||||||
|
/// Always exist
|
||||||
|
pub tls_cert_path: PathBuf,
|
||||||
|
|
||||||
|
#[builder(setter(custom))]
|
||||||
|
/// Always exist
|
||||||
|
pub tls_cert_key_path: PathBuf,
|
||||||
|
|
||||||
|
#[builder(setter(custom), default)]
|
||||||
|
/// This may not exist
|
||||||
|
pub client_ca_cert_path: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CryptoFileSourceBuilder {
|
||||||
|
pub fn tls_cert_path<T: AsRef<Path>>(&mut self, v: T) -> &mut Self {
|
||||||
|
self.tls_cert_path = Some(v.as_ref().to_path_buf());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
pub fn tls_cert_key_path<T: AsRef<Path>>(&mut self, v: T) -> &mut Self {
|
||||||
|
self.tls_cert_key_path = Some(v.as_ref().to_path_buf());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
pub fn client_ca_cert_path<T: AsRef<Path>>(&mut self, v: Option<T>) -> &mut Self {
|
||||||
|
self.client_ca_cert_path = Some(v.map(|p| p.as_ref().to_path_buf()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------ */
|
||||||
|
#[async_trait]
|
||||||
|
impl CryptoSource for CryptoFileSource {
|
||||||
|
type Error = RpxyCertError;
|
||||||
|
/// read crypto materials from source
|
||||||
|
async fn read(&self) -> Result<SingleServerCrypto, Self::Error> {
|
||||||
|
read_certs_and_keys(
|
||||||
|
&self.tls_cert_path,
|
||||||
|
&self.tls_cert_key_path,
|
||||||
|
self.client_ca_cert_path.as_ref(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/// Returns true when mutual tls is enabled
|
||||||
|
fn is_mutual_tls(&self) -> bool {
|
||||||
|
self.client_ca_cert_path.is_some()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------ */
|
||||||
|
/// Read certificates and private keys from file
|
||||||
|
fn read_certs_and_keys(
|
||||||
|
cert_path: &PathBuf,
|
||||||
|
cert_key_path: &PathBuf,
|
||||||
|
client_ca_cert_path: Option<&PathBuf>,
|
||||||
|
) -> Result<SingleServerCrypto, RpxyCertError> {
|
||||||
|
debug!("Read TLS server certificates and private key");
|
||||||
|
|
||||||
|
// certificates
|
||||||
|
let raw_certs = {
|
||||||
|
let mut reader = BufReader::new(File::open(cert_path).map_err(|e| {
|
||||||
|
io::Error::new(
|
||||||
|
e.kind(),
|
||||||
|
format!("Unable to load the certificates [{}]: {e}", cert_path.display()),
|
||||||
|
)
|
||||||
|
})?);
|
||||||
|
rustls_pemfile::certs(&mut reader)
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "Unable to parse the certificates"))?
|
||||||
|
};
|
||||||
|
|
||||||
|
// private keys
|
||||||
|
let raw_cert_keys = {
|
||||||
|
let encoded_keys = {
|
||||||
|
let mut encoded_keys = vec![];
|
||||||
|
File::open(cert_key_path)
|
||||||
|
.map_err(|e| {
|
||||||
|
io::Error::new(
|
||||||
|
e.kind(),
|
||||||
|
format!("Unable to load the certificate keys [{}]: {e}", cert_key_path.display()),
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.read_to_end(&mut encoded_keys)?;
|
||||||
|
encoded_keys
|
||||||
|
};
|
||||||
|
let mut reader = Cursor::new(encoded_keys);
|
||||||
|
let pkcs8_keys = rustls_pemfile::pkcs8_private_keys(&mut reader)
|
||||||
|
.map(|v| v.map(rustls::pki_types::PrivateKeyDer::Pkcs8))
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.map_err(|_| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"Unable to parse the certificates private keys (PKCS8)",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
reader.set_position(0);
|
||||||
|
let mut rsa_keys = rustls_pemfile::rsa_private_keys(&mut reader)
|
||||||
|
.map(|v| v.map(rustls::pki_types::PrivateKeyDer::Pkcs1))
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
let mut keys = pkcs8_keys;
|
||||||
|
keys.append(&mut rsa_keys);
|
||||||
|
if keys.is_empty() {
|
||||||
|
return Err(RpxyCertError::IoError(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"No private keys found - Make sure that they are in PKCS#8/PEM format",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
keys
|
||||||
|
};
|
||||||
|
|
||||||
|
// client ca certificates
|
||||||
|
let client_ca_certs = if let Some(path) = client_ca_cert_path {
|
||||||
|
debug!("Read CA certificates for client authentication");
|
||||||
|
// Reads client certificate and returns client
|
||||||
|
let certs = {
|
||||||
|
let mut reader = BufReader::new(File::open(path).map_err(|e| {
|
||||||
|
io::Error::new(
|
||||||
|
e.kind(),
|
||||||
|
format!("Unable to load the client certificates [{}]: {e}", path.display()),
|
||||||
|
)
|
||||||
|
})?);
|
||||||
|
rustls_pemfile::certs(&mut reader)
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "Unable to parse the client certificates"))?
|
||||||
|
};
|
||||||
|
Some(certs)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(SingleServerCrypto::new(
|
||||||
|
&raw_certs,
|
||||||
|
&Arc::new(raw_cert_keys),
|
||||||
|
&client_ca_certs,
|
||||||
|
))
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue