diff --git a/Cargo.lock b/Cargo.lock index 8120e57..5579bb5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,6 +76,27 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "asn1" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca7444bb097e58fd76ae2b3929d8f5bdb3748000eb51b79141ed7c7b6a9dcdbc" +dependencies = [ + "asn1_derive", + "itoa", +] + +[[package]] +name = "asn1_derive" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0dcd11549aa9d043141c7d54839771cbf9a8d38084808c6fa00e62b46b114f0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -127,6 +148,22 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bcder" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f7c42c9913f68cf9390a225e81ad56a5c515347287eb98baa710090ca1de86d" +dependencies = [ + "bytes", + "smallvec", +] + [[package]] name = "bitflags" version = "2.10.0" @@ -238,12 +275,40 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cryptoki" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff765b99fc49f3116c9a908484486a2b92fd73c48da45c3a69716471c6cc56c6" +dependencies = [ + "bitflags", + "cryptoki-sys", + "libloading", + "log", + "secrecy", +] + +[[package]] +name = "cryptoki-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1fd850498411e4057f1cba79e6e2bc7cbe960544c1046ab46d4685c403a1121" +dependencies = [ + "libloading", +] + [[package]] name = "darling" version = "0.21.3" @@ -279,6 +344,16 @@ dependencies = [ "syn", ] +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + [[package]] name = "deranged" version = "0.5.5" @@ -595,6 +670,16 @@ version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -735,6 +820,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -752,14 +847,18 @@ name = "portail" version = "0.1.0" dependencies = [ "anyhow", + "asn1", "bytes", "chrono", "clap", + "cryptoki", "fast-socks5", "hyper", "nix", "peekable", "regex", + "rustls-pemfile", + "rustls-webpki", "serde", "serde_json", "serde_with", @@ -771,6 +870,7 @@ dependencies = [ "tracing-subscriber", "varlink", "winnow", + "x509-certificate", ] [[package]] @@ -803,6 +903,15 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -903,6 +1012,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "rustls-pki-types" version = "1.14.0" @@ -960,6 +1078,15 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "zeroize", +] + [[package]] name = "serde" version = "1.0.228" @@ -1068,6 +1195,15 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core", +] + [[package]] name = "slab" version = "0.4.11" @@ -1100,6 +1236,16 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "strsim" version = "0.11.1" @@ -1757,11 +1903,44 @@ version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +[[package]] +name = "x509-certificate" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca9eb9a0c822c67129d5b8fcc2806c6bc4f50496b420825069a440669bcfbf7f" +dependencies = [ + "bcder", + "bytes", + "chrono", + "der", + "hex", + "pem", + "ring", + "signature", + "spki", + "thiserror 2.0.18", + "zeroize", +] + [[package]] name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "zmij" diff --git a/Cargo.toml b/Cargo.toml index b068bdc..1b5c7bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,14 +5,18 @@ edition = "2024" [dependencies] anyhow = "1.0.100" +asn1 = "0.23.1" bytes = "1.11.0" chrono = "0.4.43" clap = { version = "4.5.57", features = ["derive", "string"] } +cryptoki = "0.12.0" fast-socks5 = "1.0.0" hyper = { version = "1.8.1", features = ["full"] } nix = { version = "0.31.1", features = ["socket", "uio", "net"] } peekable = { version = "0.4.1", features = ["tokio"] } regex = "1.12.2" +rustls-pemfile = "2.2.0" +rustls-webpki = "0.103.9" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" serde_with = { version = "3.16.1", features = ["chrono"] } @@ -24,3 +28,4 @@ tracing = "0.1.44" tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } varlink = { version = "13.0.0", features = ["tokio"] } winnow = "0.7.14" +x509-certificate = "0.25.0" diff --git a/src/config.rs b/src/config.rs index faa7359..a981548 100644 --- a/src/config.rs +++ b/src/config.rs @@ -29,7 +29,9 @@ pub struct EscapeSettings { pub cacert_file: Option, pub tls_privkey: Option, pub tls_certificate: Option, - pub pkcs11_uri: Option, + + /// If set, PKCS#11 is used to resolve client certificates automatically. + pub pkcs11_library: Option, } #[serde_with::serde_as] @@ -59,7 +61,10 @@ pub struct Settings { /// Settings for the workload socket (e.g. this is where clients connect to, e.g. Firefox or /// another instance of this proxy via routing) + /// If this is unset, there's no TLS on incoming connections. + /// Use this when you are binding a local daemon. pub listener: Option, + /// Settings for connecting to other proxy instances or websites via client certificate /// authentication. pub escaper: Option, diff --git a/src/main.rs b/src/main.rs index 6b7ae77..24a9302 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,6 +14,7 @@ mod proxy; mod rpc; mod state; mod systemd; +mod pkcs11; #[derive(Parser)] #[command(name = "Portail")] diff --git a/src/pkcs11/mod.rs b/src/pkcs11/mod.rs new file mode 100644 index 0000000..0156c46 --- /dev/null +++ b/src/pkcs11/mod.rs @@ -0,0 +1,386 @@ +///! Vendored from https://github.com/mdelete/rustls-pkcs11 +/// Adapted to our need. +use cryptoki::{ + context::{CInitializeArgs, CInitializeFlags, Pkcs11}, + mechanism::{ + Mechanism, MechanismType, + eddsa::{EddsaParams, EddsaSignatureScheme}, + rsa::{PkcsMgfType, PkcsPssParams}, + }, + object::{Attribute, AttributeType, CertificateType, KeyType, ObjectClass}, + session::{Session, UserType}, + types::AuthPin, +}; +use tracing::{debug, error, info}; +use tokio_rustls::rustls::{ + self, OtherError, SignatureAlgorithm, SignatureScheme, + client::ResolvesClientCert, + pki_types::CertificateDer, + sign::{CertifiedKey, Signer, SigningKey}, +}; +use x509_certificate::X509Certificate; +use std::{ + ffi::OsStr, + fmt, + sync::{Arc, Mutex}, +}; + +#[derive(Debug)] +enum PKCS11Error { + CertificateNotFoundError, + KeyHandleNotFoundError, + UnsupportedSignatureSchemeError(SignatureScheme), + SignError(cryptoki::error::Error), +} + +impl std::error::Error for PKCS11Error {} + +impl fmt::Display for PKCS11Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + PKCS11Error::SignError(err) => write!(f, "{}: {}", self, err), + PKCS11Error::UnsupportedSignatureSchemeError(scheme) => { + write!(f, "{}: {:?}", self, scheme) + } + _ => write!(f, "{}", self), + } + } +} + +#[derive(Debug)] +struct PKCS11Signer { + session: Arc>, + scheme: SignatureScheme, +} + +/// Maps rustls schemes to cryptoki mechanisms +/// +/// TLS 1.3 supports: +/// ECDSA_NISTP521_SHA512, ECDSA_NISTP384_SHA384, ECDSA_NISTP256_SHA256 +/// RSA_PSS_SHA512, RSA_PSS_SHA384, RSA_PSS_SHA256 +/// ED25519 +/// TLS 1.2 additionally supports: +/// RSA_PKCS1_SHA384, RSA_PKCS1_SHA256 +/// YubiKey (5.1) supports: +/// ECDSA_NISTP384_SHA384, ECDSA_NISTP256_SHA256 +/// RSA_PKCS1_SHA384, RSA_PKCS1_SHA256 +/// Most HSM do not implement RSA_PSS_*, *_SHA512, and ED25519 +/// +/// # Errors +/// +/// - yields UnsupportedSignatureSchemeError for schemes that are not TLSv1.2 or TLSv1.3 +/// +impl<'a> TryInto> for &PKCS11Signer { + type Error = rustls::Error; + + fn try_into(self) -> Result, rustls::Error> { + match self.scheme { + // 1.2.840.10045.4.3.2 + SignatureScheme::ECDSA_NISTP256_SHA256 => Ok(Mechanism::EcdsaSha256), + // 1.2.840.10045.4.3.3 + SignatureScheme::ECDSA_NISTP384_SHA384 => Ok(Mechanism::EcdsaSha384), + // 1.2.840.10045.4.3.4 + SignatureScheme::ECDSA_NISTP521_SHA512 => Ok(Mechanism::EcdsaSha512), + // 1.2.840.113549.1.1.11 + SignatureScheme::RSA_PKCS1_SHA256 => Ok(Mechanism::Sha256RsaPkcs), + // 1.2.840.113549.1.1.12 + SignatureScheme::RSA_PKCS1_SHA384 => Ok(Mechanism::Sha384RsaPkcs), + // 1.2.840.113549.1.1.13 + SignatureScheme::RSA_PKCS1_SHA512 => Ok(Mechanism::Sha512RsaPkcs), + // 1.2.840.113549.1.1.10 ... + SignatureScheme::RSA_PSS_SHA256 => { + let params = PkcsPssParams { + hash_alg: MechanismType::SHA256_RSA_PKCS, // 2.16.840.1.101.3.4.2.1 + mgf: PkcsMgfType::MGF1_SHA256, // 1.2.840.113549.1.1.8 + 2.16.840.1.101.3.4.2.1 + s_len: 32.into(), + }; + Ok(Mechanism::Sha256RsaPkcsPss(params)) + } + // 1.2.840.113549.1.1.10 ... + SignatureScheme::RSA_PSS_SHA384 => { + let params = PkcsPssParams { + hash_alg: MechanismType::SHA384_RSA_PKCS, // 2.16.840.1.101.3.4.2.2 + mgf: PkcsMgfType::MGF1_SHA384, // 1.2.840.113549.1.1.8 + 2.16.840.1.101.3.4.2.2 + s_len: 48.into(), + }; + Ok(Mechanism::Sha384RsaPkcsPss(params)) + } + // 1.2.840.113549.1.1.10 ... + SignatureScheme::RSA_PSS_SHA512 => { + let params = PkcsPssParams { + hash_alg: MechanismType::SHA512_RSA_PKCS, // 2.16.840.1.101.3.4.2.3 + mgf: PkcsMgfType::MGF1_SHA512, // 1.2.840.113549.1.1.8 + 2.16.840.1.101.3.4.2.3 + s_len: 64.into(), + }; + Ok(Mechanism::Sha384RsaPkcsPss(params)) + } + // TODO: which HSM really supports this? + SignatureScheme::ED25519 => Ok(Mechanism::Eddsa(EddsaParams::new( + EddsaSignatureScheme::Ed25519, + ))), + unsupported_scheme => Err(rustls::Error::Other(OtherError(Arc::new( + PKCS11Error::UnsupportedSignatureSchemeError(unsupported_scheme), + )))), + } + } +} + +/// Maps rustls schemes to cryptoki keytypes +impl Into for &PKCS11Signer { + fn into(self) -> KeyType { + match self.scheme { + SignatureScheme::ECDSA_NISTP256_SHA256 => KeyType::EC, + SignatureScheme::ECDSA_NISTP384_SHA384 => KeyType::EC, + _ => KeyType::RSA, + } + } +} + +impl Signer for PKCS11Signer { + fn sign(&self, message: &[u8]) -> Result, rustls::Error> { + let session = self.session.lock().unwrap(); + + let key_template = vec![ + Attribute::Token(true), + Attribute::Private(true), + Attribute::Sign(true), + Attribute::KeyType(self.into()), + ]; + + let key = session + .find_objects(&key_template) + .unwrap() // how and when might this fail? + .into_iter() + .next() + .ok_or(rustls::Error::Other(OtherError(Arc::new( + PKCS11Error::KeyHandleNotFoundError, + ))))?; + + let mechanism = self.try_into()?; + + let mut signed_message = session.sign(&mechanism, key, message).map_err(|err| { + rustls::Error::Other(OtherError(Arc::new(PKCS11Error::SignError(err)))) + })?; + + // The signed message comes out of the YubiKey in raw r,s-form for EC signatures, needs to be an ASN.1 sequence. + // RSA signatures pass unchanged. + // TODO: how do other HSMs output EC signatures? + let signed_message = match self.into() { + KeyType::EC => signed_message.to_asn1_sig().unwrap(), + _ => signed_message, + }; + + debug!("{:?}({:02x?})", mechanism, signed_message); + Ok(signed_message) + } + + fn scheme(&self) -> SignatureScheme { + self.scheme + } +} + +#[derive(Debug)] +struct PKCS11SigningKey { + session: Arc>, + scheme: SignatureScheme, +} + +impl Into for &PKCS11SigningKey { + fn into(self) -> SignatureAlgorithm { + match self.scheme { + SignatureScheme::ECDSA_NISTP256_SHA256 => SignatureAlgorithm::ECDSA, + SignatureScheme::ECDSA_NISTP384_SHA384 => SignatureAlgorithm::ECDSA, + SignatureScheme::RSA_PKCS1_SHA256 => SignatureAlgorithm::RSA, + SignatureScheme::RSA_PKCS1_SHA384 => SignatureAlgorithm::RSA, + SignatureScheme::RSA_PSS_SHA256 => SignatureAlgorithm::RSA, + SignatureScheme::RSA_PSS_SHA384 => SignatureAlgorithm::RSA, + _ => SignatureAlgorithm::Unknown(0), + } + } +} + +impl SigningKey for PKCS11SigningKey { + fn choose_scheme(&self, offered: &[SignatureScheme]) -> Option> { + debug!( + "schemes offered: {:?}, scheme supported: {:?}", + offered, self.scheme + ); + if offered.contains(&self.scheme) { + return Some(Box::new(PKCS11Signer { + session: self.session.clone(), + scheme: self.scheme, + })); + } + None + } + + // mandatory trait fn, but seems to be unused + fn algorithm(&self) -> SignatureAlgorithm { + debug!("algorithm called!"); + self.into() + } +} + +#[derive(Debug)] +pub struct PKCS11ClientCertResolver { + chain: Vec>, + signing_key: Arc, +} + +impl ResolvesClientCert for PKCS11ClientCertResolver { + fn resolve( + &self, + _acceptable_issuers: &[&[u8]], + _sigschemes: &[SignatureScheme], + ) -> Option> { + Some(Arc::new(CertifiedKey { + cert: self.chain.clone(), + key: self.signing_key.clone(), + ocsp: None, + })) + } + + fn has_certs(&self) -> bool { + self.chain.len() > 0 + } +} + +impl PKCS11ClientCertResolver { + /// Create a new PKCS11ClientCertResolver. + /// + /// # Arguments + /// + /// * `pin` - An optional `&str` of the pin that unlocks the device. Set to `None` if device does not need a pin to unlock. + /// * `path` - Absolute filepath to the PKCS11 library of your device. + /// + /// # Examples + /// + /// ``` + /// let pin = Some("123456"); + /// let tls = rustls::ClientConfig::builder() + /// .with_root_certificates(root_certs) + /// .with_client_cert_resolver(Arc::new( + /// PKCS11ClientCertResolver::new(pin, "/usr/lib/opensc-pkcs11.so")? + /// )); + /// ``` + /// + /// # Errors + /// + /// Authenticator devices are complicated things. A lot can go wrong along the way. + /// To alleviate this, most checks happen at instantiation time here, therefore errors + /// yielded here can have a plethora of reasons: + /// + /// - the PKCS11 library could not be loaded under the file path. + /// - the pin is missing or incorrect. + /// - the authenticator device is not present or inaccessible. + /// - the PKCS11 library has problems communicating with the authenticator device. + /// - the authenticator device cannot read an appropriate certificate from the relevant slot. + /// - the authenticator device's certificate signature scheme is not supported. + /// + pub fn new(pin: Option<&str>, path: &OsStr) -> Result> { + let pkcs11client = Pkcs11::new(path)?; + pkcs11client.initialize(CInitializeArgs::new(CInitializeFlags::OS_LOCKING_OK))?; + + let slot = pkcs11client.get_slots_with_token()?.remove(0); // This only ever shows slot 9a of a YubiKey + let session = pkcs11client.open_ro_session(slot)?; + match pin { + Some(pin) => session.login(UserType::User, Some(&AuthPin::new(pin.into())))?, + None => session.login(UserType::User, None)?, + } + + let search_template = vec![ + Attribute::Class(ObjectClass::CERTIFICATE), + Attribute::CertificateType(CertificateType::X_509), + ]; + let handle = session.find_objects(&search_template)?.remove(0); + let value = session + .get_attributes(handle, &[AttributeType::Value])? + .remove(0); + + let (scheme, chain) = parse_certificate(value)?; + + let session = Arc::new(Mutex::new(session)); + let signing_key = Arc::new(PKCS11SigningKey { session, scheme }); + + Ok(Self { chain, signing_key }) + } +} + +/// helper trait to deal with asn.1 +trait ToASN1 { + fn to_asn1_sig(&mut self) -> Result, asn1::WriteError>; +} + +/// transforms a raw r,s-form signature to an asn1 sequence +impl ToASN1 for Vec { + fn to_asn1_sig(&mut self) -> Result, asn1::WriteError> { + let mut mid = self.len() / 2; + + // if r or s are negative (have the msb set) we need to prepend a null byte before converting to bigint + if self[0] >> 7 == 1 { + self.insert(0, 0u8); + mid += 1; // shift the middle + } + if self[mid] >> 7 == 1 { + self.insert(mid, 0u8); + } + + asn1::write(|w| { + w.write_element(&asn1::SequenceWriter::new(&|w: &mut asn1::Writer| { + let r = asn1::BigInt::new(&self[..mid]); + w.write_element(&r)?; + let s = asn1::BigInt::new(&self[mid..]); + w.write_element(&s)?; + Ok(()) + })) + }) + } +} + +/// certificate helper +fn parse_certificate( + value: Attribute, +) -> Result<(SignatureScheme, Vec>), Box> { + match value { + Attribute::Value(cert) => { + let x509 = X509Certificate::from_der(&cert).unwrap(); + info!( + "CN: {}, Issuer: {}", + x509.subject_common_name().unwrap(), + x509.issuer_common_name().unwrap(), + ); + debug!( + "Signature algorithm: {:?}", + x509.signature_algorithm().unwrap(), + ); + debug!("Pubkey({:02x})", x509.public_key_data()); + + let scheme = match x509.signature_algorithm().unwrap() { + // TODO: most HSMs do not support RSA_PSS_* and *_SHA512 + // Yubikey 5.7 seemingly does not support ED25519 certificates + x509_certificate::SignatureAlgorithm::RsaSha256 => { + SignatureScheme::RSA_PKCS1_SHA256 // TODO: support both RSA_PSS_SHA256 and RSA_PKCS1_SHA256 + } + x509_certificate::SignatureAlgorithm::RsaSha384 => { + SignatureScheme::RSA_PKCS1_SHA384 // TODO: support both RSA_PSS_SHA384 and RSA_PKCS1_SHA384 + } + x509_certificate::SignatureAlgorithm::EcdsaSha256 => { + SignatureScheme::ECDSA_NISTP256_SHA256 + } + x509_certificate::SignatureAlgorithm::EcdsaSha384 => { + SignatureScheme::ECDSA_NISTP384_SHA384 + } + unsupported_scheme => { + error!("Unsupported scheme: {}", unsupported_scheme); + return Err(Box::new(PKCS11Error::UnsupportedSignatureSchemeError( + SignatureScheme::Unknown(0), + ))); + } + }; + + let certificate_der = CertificateDer::from_slice(&cert).into_owned(); + Ok((scheme, vec![certificate_der])) + } + _ => Err(Box::new(PKCS11Error::CertificateNotFoundError)), + } +} diff --git a/src/proxy/mod.rs b/src/proxy/mod.rs index 4cefd4e..ecb74fe 100644 --- a/src/proxy/mod.rs +++ b/src/proxy/mod.rs @@ -1,10 +1,11 @@ -use std::sync::Arc; +use std::{io::BufReader, sync::Arc}; use anyhow::bail; use thiserror::Error; use fast_socks5::SocksError; use tokio::sync::RwLock; use tracing::error; -use tokio_rustls::{TlsAcceptor, TlsStream}; +use tokio_rustls::{TlsAcceptor, TlsStream, rustls::{RootCertStore, ServerConfig, pki_types::{CertificateDer, PrivateKeyDer}, server::{VerifierBuilderError, WebPkiClientVerifier}}}; +use webpki::anchor_from_trusted_cert; use crate::{config::Settings, proxy::context::RequestContext, state::State}; @@ -65,13 +66,113 @@ async fn serve_unauthenticated_proxy( Ok(()) } + +#[derive(Debug, Error)] +enum MaterialError { + #[error("Failed to parse PEM section")] + SectionParsingError, + #[error("Expected a certificate")] + ExpectedCertificate, + #[error("Expected a private key")] + ExpectedPrivateKey +} + +#[derive(Debug, Error)] +enum ServerTLSConfigError { + #[error("Either the TLS chain or the private key is missing while the other is set")] + MisconfiguredServerCertificates, + #[error("Setting the single certificates failed: {0}")] + ServerCertificateConfigError(#[from] tokio_rustls::rustls::Error), + #[error("Failed during I/O: {0}")] + IOError(#[from] std::io::Error), + #[error("Missing private key in the TLS private key file")] + MissingPrivateKey, + #[error("Failed to parse PEM files")] + PEMParsingError, + #[error("Unexpected material nature: {0}")] + UnexpectedMaterialNature(#[from] MaterialError), + #[error("Client verifier construction failed: {0}")] + ClientVerifierBuilderError(#[from] VerifierBuilderError), +} + + +fn expect_certificate(item: rustls_pemfile::Item) -> Result, MaterialError> { + match item { + rustls_pemfile::Item::X509Certificate(cert) => Ok(cert), + _ => Err(MaterialError::ExpectedCertificate) + } +} + +fn expect_private_key(item: rustls_pemfile::Item) -> Result, MaterialError> { + match item { + rustls_pemfile::Item::Pkcs1Key(pkey) => Ok(pkey.into()), + rustls_pemfile::Item::Sec1Key(pkey) => Ok(pkey.into()), + rustls_pemfile::Item::Pkcs8Key(pkey) => Ok(pkey.into()), + _ => Err(MaterialError::ExpectedPrivateKey) + } +} + +fn build_tls_acceptor(settings: &Settings) -> Result, ServerTLSConfigError> { + if let Some(ref listener) = settings.listener { + let config = ServerConfig::builder(); + + let config = if let Some(ref client_ca) = listener.cacert_file { + let mut ca_file = BufReader::new(std::fs::File::open(&client_ca)?); + let certs: Vec<_> = rustls_pemfile::read_all(&mut ca_file) + .map(|item| + item.map_err(|_| MaterialError::SectionParsingError).and_then(expect_certificate) + ) + .collect::, _>>()?; + + let mut roots = RootCertStore::empty(); + for cert in certs { + roots.add(cert)?; + } + + config.with_client_cert_verifier( + // TODO: support unauthenticated. + WebPkiClientVerifier::builder(Arc::new(roots)) + .build()? + ) + } else { + config.with_no_client_auth() + }; + + let config = match (&listener.tls_chain, &listener.tls_privkey) { + (Some(tls_chain), Some(tls_privkey)) => { + let mut tls_chain_file = BufReader::new(std::fs::File::open(&tls_chain)?); + let tls_chain_certs: Vec<_> = rustls_pemfile::read_all(&mut tls_chain_file) + .map(|item| item.map_err(|_| MaterialError::SectionParsingError).and_then(expect_certificate)) + .collect::, _>>()?; + let (tls_private_key, _) = rustls_pemfile::read_one_from_slice(&std::fs::read(&tls_privkey)?).map_err(|_| ServerTLSConfigError::PEMParsingError)?.ok_or(ServerTLSConfigError::MissingPrivateKey)?; + config.with_single_cert( + tls_chain_certs, + expect_private_key(tls_private_key)? + )? + }, + + (None, None) => { + // No server TLS. + return Ok(None); + } + + _ => { + return Err(ServerTLSConfigError::MisconfiguredServerCertificates); + } + }; + + Ok(Some(TlsAcceptor::from(Arc::new(config)))) + } else { + Ok(None) + } +} + pub async fn start( settings: Arc, state: Arc>, listener: tokio::net::TcpListener ) -> anyhow::Result<()> { - // TODO: construct a TLS acceptor based on the settings for the server side. - let tls_acceptor: Option = None; + let tls_acceptor: Option = build_tls_acceptor(&settings)?; loop { let (socket, addr) = listener.accept().await?; diff --git a/src/state.rs b/src/state.rs index 94d6259..b7302ce 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,6 +1,6 @@ use std::sync::{Arc, atomic::AtomicUsize}; -use crate::config::Settings; +use crate::{config::Settings, pkcs11::PKCS11ClientCertResolver}; ///! Contains the state of the application and various state transitions APIs. @@ -15,7 +15,21 @@ impl State { pub fn reload_trust_anchors(&self) { } - pub fn reload_client_certs(&self) { + /// This loads (first time) or reloads (reload or state deserialization) the client certificate + /// resolver to initiate outbound connection based on the new settings. + pub fn reload_client_certs(&mut self, settings: &Settings) { + if let Some(ref escaper) = settings.escaper { + self.client_cert_resolver = if let Some(ref pkcs11_library) = escaper.pkcs11_library { + // TODO: add a proper error handling here that doesn't corrupt the state ideally. + Some(Arc::new(PKCS11ClientCertResolver::new(None, + pkcs11_library.as_os_str() + ).unwrap())) + } else { + // Try to see if we can use usual certificate files here. + // Otherwise, bail out. + None + }; + } } pub fn reload_acl_rules(&self) {