Skip to content

Commit

Permalink
feat: ✨ include certificate chain for client and server certs
Browse files Browse the repository at this point in the history
  • Loading branch information
dergecko committed Dec 15, 2024
1 parent 8ac56e2 commit 71add53
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 17 deletions.
50 changes: 47 additions & 3 deletions test-certs/src/configuration/certificates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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"))]
Expand Down Expand Up @@ -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(),
})
}

Expand All @@ -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(),
})
}
}
Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand All @@ -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();
Expand Down
41 changes: 34 additions & 7 deletions test-certs/src/generation.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::sync::Arc;

use rcgen::{
BasicConstraints, CertificateParams, DistinguishedName, DnType, ExtendedKeyUsagePurpose, IsCa,
KeyPair, KeyUsagePurpose,
Expand All @@ -11,21 +13,24 @@ use crate::{
},
};

/// Type alias to make code more readable.
type Issuer = Arc<Certificate>;

/// 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<Certificate, Error>;
fn build(&self, name: &str, issuer: Option<&Issuer>) -> Result<Certificate, Error>;
}

/// 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<Certificate, Error>;
fn certificate(&self, name: &str, issuer: Option<&Issuer>) -> Result<Certificate, Error>;
}

impl CertificateGenerator for CertificateType {
fn build(&self, name: &str, issuer: Option<&Certificate>) -> Result<Certificate, Error> {
fn build(&self, name: &str, issuer: Option<&Issuer>) -> Result<Certificate, Error> {
match self {
CertificateType::CertificateAuthority(certificate_authority_configuration) => {
certificate_authority_configuration.certificate(name, issuer)
Expand All @@ -41,7 +46,7 @@ impl CertificateGenerator for CertificateType {
}

impl ToCertificate for CertificateAuthorityConfiguration {
fn certificate(&self, name: &str, issuer: Option<&Certificate>) -> Result<Certificate, Error> {
fn certificate(&self, name: &str, issuer: Option<&Issuer>) -> Result<Certificate, Error> {
let key = KeyPair::generate()?;

let certificate_params = issuer_params(name);
Expand All @@ -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<Certificate, Error> {
fn certificate(&self, name: &str, issuer: Option<&Issuer>) -> Result<Certificate, Error> {
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<Certificate, Error> {
fn certificate(&self, name: &str, issuer: Option<&Issuer>) -> Result<Certificate, Error> {
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,
})
}
}
Expand All @@ -97,7 +113,7 @@ impl ToCertificate for ServerConfiguration {
fn sign_cert(
certificate_params: CertificateParams,
key: &KeyPair,
issuer: Option<&Certificate>,
issuer: Option<&Issuer>,
) -> Result<rcgen::Certificate, Error> {
let certificate = if let Some(issuer) = issuer {
certificate_params.signed_by(key, &issuer.certificate, &issuer.key)
Expand Down Expand Up @@ -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
}
49 changes: 42 additions & 7 deletions test-certs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
use std::{
fmt::{Debug, Display},
io::Write,
path::Path,
path::Path, sync::Arc,
};

use configuration::certificates::{CertificateRoot, CertificateType};
Expand Down Expand Up @@ -39,6 +39,24 @@ pub struct Certificate {
key: KeyPair,
export_key: bool,
name: String,
issuer: Option<Arc<Certificate>>,
}

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 {
Expand All @@ -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)?;

Expand All @@ -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<Vec<Certificate>, Error> {
let certs: Vec<Result<Vec<Certificate>, Error>> = certificate_config
pub fn generate(certificate_config: &CertificateRoot) -> Result<Vec<Arc<Certificate>>, Error> {
let certs: Vec<Result<Vec<Arc<Certificate>>, Error>> = certificate_config
.certificates
.iter()
.map(|(name, config)| generate_certificates(name, config, None))
Expand All @@ -90,17 +121,17 @@ pub fn generate(certificate_config: &CertificateRoot) -> Result<Vec<Certificate>
fn generate_certificates(
name: &str,
config: &CertificateType,
issuer: Option<&Certificate>,
) -> Result<Vec<Certificate>, Error> {
issuer: Option<&Arc<Certificate>>,
) -> Result<Vec<Arc<Certificate>>, 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)
}

Expand All @@ -117,13 +148,15 @@ impl Debug for Certificate {
key,
export_key,
name,
issuer,
} = self;

f.debug_struct("CertKey")
.field("certificate", &certificate.pem())
.field("key", &key.serialize_pem())
.field("export_key", export_key)
.field("name", name)
.field("issuer", issuer)
.finish()
}
}
Expand Down Expand Up @@ -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
}

0 comments on commit 71add53

Please sign in to comment.