diff --git a/test-certs/src/configuration/certificates.rs b/test-certs/src/configuration/certificates.rs index 09efaf9..4b9309f 100644 --- a/test-certs/src/configuration/certificates.rs +++ b/test-certs/src/configuration/certificates.rs @@ -35,6 +35,10 @@ pub struct ClientConfiguration { #[serde(default = "ClientConfiguration::default_export_key")] pub export_key: bool, + /// Includes all public certificates that are required for this certificate to be validated. + #[serde(default = "ClientConfiguration::default_include_certificate_chain")] + pub include_certificate_chain: bool, + /// Properties that will be set as Subject Alternative Names (SAN)s. #[serde(flatten)] pub subject_alternative_names: SubjectAlternativeNames, @@ -48,6 +52,10 @@ pub struct ServerConfiguration { #[serde(default = "ServerConfiguration::default_export_key")] pub export_key: bool, + /// Includes all public certificates that are required for this certificate to be validated. + #[serde(default = "ServerConfiguration::default_include_certificate_chain")] + pub include_certificate_chain: bool, + /// Properties that will be set as Subject Alternative Names (SAN)s. #[serde(flatten)] pub subject_alternative_names: SubjectAlternativeNames, @@ -117,12 +125,18 @@ impl ServerConfiguration { fn default_export_key() -> bool { true } + fn default_include_certificate_chain() -> bool { + true + } } impl ClientConfiguration { fn default_export_key() -> bool { true } + fn default_include_certificate_chain() -> bool { + true + } } /// Fixtures for testing certificate generation. #[cfg(any(test, feature = "fixtures"))] @@ -181,6 +195,7 @@ pub mod fixtures { ip: vec![IpAddr::V4(Ipv4Addr::LOCALHOST)], dns_name: vec!["my-client.org".to_string()], }, + include_certificate_chain: ClientConfiguration::default_include_certificate_chain(), }) } @@ -192,6 +207,7 @@ pub mod fixtures { ip: vec![IpAddr::V4(Ipv4Addr::LOCALHOST)], dns_name: vec!["my-server.org".to_string()], }, + include_certificate_chain: ServerConfiguration::default_include_certificate_chain(), }) } } @@ -290,11 +306,13 @@ mod tests { ip: vec![IpAddr::V4(Ipv4Addr::new(192, 168, 1, 10))], dns_name: vec!["my-client.org".to_string()], }, + include_certificate_chain: false, }; let json = json!({ "export_key": false, "ip": "192.168.1.10", - "dns_name": "my-client.org" + "dns_name": "my-client.org", + "include_certificate_chain": false }); let deserialized: ClientConfiguration = serde_json::from_value(json).unwrap(); @@ -313,11 +331,13 @@ mod tests { ], dns_name: vec!["my-server.org".to_string(), "my-server.com".to_string()], }, + include_certificate_chain: false, }; let json = json!({ "export_key": false, "ip": ["192.168.1.1", "192.168.1.2"], - "dns_name": ["my-server.org", "my-server.com"] + "dns_name": ["my-server.org", "my-server.com"], + "include_certificate_chain": false }); let deserialized: ServerConfiguration = serde_json::from_value(json).unwrap(); @@ -333,11 +353,35 @@ mod tests { ip: vec![IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))], dns_name: vec!["my-server.org".to_string()], }, + include_certificate_chain: true, + }; + let json = json!({ + "export_key": false, + "ip": "192.168.1.1", + "dns_name": "my-server.org", + "include_certificate_chain": true + }); + + let deserialized: ServerConfiguration = serde_json::from_value(json).unwrap(); + + assert_eq!(deserialized, expected) + } + + #[test] + fn should_deserialize_cert_chain() { + let expected = ServerConfiguration { + export_key: false, + subject_alternative_names: SubjectAlternativeNames { + ip: vec![IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))], + dns_name: vec!["my-server.org".to_string()], + }, + include_certificate_chain: true, }; let json = json!({ "export_key": false, "ip": "192.168.1.1", - "dns_name": "my-server.org" + "dns_name": "my-server.org", + "include_certificate_chain": true }); let deserialized: ServerConfiguration = serde_json::from_value(json).unwrap(); diff --git a/test-certs/src/generation.rs b/test-certs/src/generation.rs index 1b4b155..f88064c 100644 --- a/test-certs/src/generation.rs +++ b/test-certs/src/generation.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use rcgen::{ BasicConstraints, CertificateParams, DistinguishedName, DnType, ExtendedKeyUsagePurpose, IsCa, KeyPair, KeyUsagePurpose, @@ -11,21 +13,24 @@ use crate::{ }, }; +/// Type alias to make code more readable. +type Issuer = Arc; + /// Extension trait to convert [`CertificateType`] to [`Certificate`]. // NOTE: Instead of a trait use actual types? pub trait CertificateGenerator { /// Build a [`Certificate`]. - fn build(&self, name: &str, issuer: Option<&Certificate>) -> Result; + fn build(&self, name: &str, issuer: Option<&Issuer>) -> Result; } /// Internal trait to actually implement the logic to create a certificate from a specific /// certificate configuration. trait ToCertificate { - fn certificate(&self, name: &str, issuer: Option<&Certificate>) -> Result; + fn certificate(&self, name: &str, issuer: Option<&Issuer>) -> Result; } impl CertificateGenerator for CertificateType { - fn build(&self, name: &str, issuer: Option<&Certificate>) -> Result { + fn build(&self, name: &str, issuer: Option<&Issuer>) -> Result { match self { CertificateType::CertificateAuthority(certificate_authority_configuration) => { certificate_authority_configuration.certificate(name, issuer) @@ -41,7 +46,7 @@ impl CertificateGenerator for CertificateType { } impl ToCertificate for CertificateAuthorityConfiguration { - fn certificate(&self, name: &str, issuer: Option<&Certificate>) -> Result { + fn certificate(&self, name: &str, issuer: Option<&Issuer>) -> Result { let key = KeyPair::generate()?; let certificate_params = issuer_params(name); @@ -53,42 +58,53 @@ impl ToCertificate for CertificateAuthorityConfiguration { key, export_key: self.export_key, name: name.to_string(), + issuer: issuer.cloned(), }) } } impl ToCertificate for ClientConfiguration { - fn certificate(&self, name: &str, issuer: Option<&Certificate>) -> Result { + fn certificate(&self, name: &str, issuer: Option<&Issuer>) -> Result { let key = KeyPair::generate()?; let mut certificate_params = certificate_params(name, &self.subject_alternative_names)?; certificate_params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ClientAuth]; let certificate = sign_cert(certificate_params, &key, issuer)?; + let issuer = self + .include_certificate_chain + .then(|| issuer.cloned()) + .flatten(); Ok(Certificate { certificate, key, export_key: self.export_key, name: name.to_string(), + issuer, }) } } impl ToCertificate for ServerConfiguration { - fn certificate(&self, name: &str, issuer: Option<&Certificate>) -> Result { + fn certificate(&self, name: &str, issuer: Option<&Issuer>) -> Result { let key = KeyPair::generate()?; let mut certificate_params = certificate_params(name, &self.subject_alternative_names)?; certificate_params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth]; let certificate = sign_cert(certificate_params, &key, issuer)?; + let issuer = self + .include_certificate_chain + .then(|| issuer.cloned()) + .flatten(); Ok(Certificate { certificate, key, export_key: self.export_key, name: name.to_string(), + issuer, }) } } @@ -97,7 +113,7 @@ impl ToCertificate for ServerConfiguration { fn sign_cert( certificate_params: CertificateParams, key: &KeyPair, - issuer: Option<&Certificate>, + issuer: Option<&Issuer>, ) -> Result { let certificate = if let Some(issuer) = issuer { certificate_params.signed_by(key, &issuer.certificate, &issuer.key) @@ -178,5 +194,16 @@ mod tests { assert!(result.is_ok()) } + #[test] + fn should_include_certificate_chain() { + let ca = ca_certificate_type(); + let ca_cert = Issuer::new(ca.build("my-ca", None).unwrap()); + let client = client_certificate_type(); + let client_cert = client.build("client", Some(&ca_cert)).unwrap(); + let parent = client_cert.issuer.unwrap(); + + assert_eq!(parent, ca_cert); + } + // TODO: write test to check wether client/server certs are really issued by a ca } diff --git a/test-certs/src/lib.rs b/test-certs/src/lib.rs index bb572c8..eca5108 100644 --- a/test-certs/src/lib.rs +++ b/test-certs/src/lib.rs @@ -3,7 +3,7 @@ use std::{ fmt::{Debug, Display}, io::Write, - path::Path, + path::Path, sync::Arc, }; use configuration::certificates::{CertificateRoot, CertificateType}; @@ -39,6 +39,24 @@ pub struct Certificate { key: KeyPair, export_key: bool, name: String, + issuer: Option>, +} + +impl PartialEq for Certificate { + fn eq(&self, other: &Self) -> bool { + let Certificate { + certificate, + key, + export_key, + name, + issuer, + } = self; + certificate.der() == other.certificate.der() + && key.serialized_der() == other.key.serialize_der() + && *export_key == other.export_key + && *name == other.name + && *issuer == other.issuer + } } impl Certificate { @@ -48,6 +66,19 @@ impl Certificate { let mut cert = std::fs::File::create(&cert_file).map_err(Error::FailedToWriteCertificate)?; + + let mut issuer = self.issuer.clone(); + while let Some(ref current_issuer) = issuer { + if current_issuer.issuer.is_none() { + // NOTE: If we have no issuer anymore we are at top level + // and do not include the root ca. + break; + } + cert.write_fmt(format_args!("{}", current_issuer.certificate.pem())) + .map_err(Error::FailedToWriteCertificate)?; + issuer = current_issuer.issuer.clone(); + } + cert.write_fmt(format_args!("{}", self.certificate.pem())) .map_err(Error::FailedToWriteCertificate)?; @@ -63,8 +94,8 @@ impl Certificate { /// Generates all certificates that are present in the configuration file. // TODO: Make builder and return errors and certificates at the same time, maybe with an Iterator? -pub fn generate(certificate_config: &CertificateRoot) -> Result, Error> { - let certs: Vec, Error>> = certificate_config +pub fn generate(certificate_config: &CertificateRoot) -> Result>, Error> { + let certs: Vec>, Error>> = certificate_config .certificates .iter() .map(|(name, config)| generate_certificates(name, config, None)) @@ -90,17 +121,17 @@ pub fn generate(certificate_config: &CertificateRoot) -> Result fn generate_certificates( name: &str, config: &CertificateType, - issuer: Option<&Certificate>, -) -> Result, Error> { + issuer: Option<&Arc>, +) -> Result>, Error> { let mut result = vec![]; let issuer = config.build(name, issuer)?; - + let issuer = Arc::new(issuer); for (name, config) in config.certificates() { let mut certificates = generate_certificates(name, config, Some(&issuer))?; result.append(&mut certificates); } - result.push(issuer); + result.push(issuer.clone()); Ok(result) } @@ -117,6 +148,7 @@ impl Debug for Certificate { key, export_key, name, + issuer, } = self; f.debug_struct("CertKey") @@ -124,6 +156,7 @@ impl Debug for Certificate { .field("key", &key.serialize_pem()) .field("export_key", export_key) .field("name", name) + .field("issuer", issuer) .finish() } } @@ -172,4 +205,6 @@ mod test { .any(|f| f.file_name() == "my-client.pem"); assert!(file_exists) } + + // TODO: Test if cert chain in file does match up }