From c7cab2ecd75c51311fe07c84f9a3d984f7ca0b10 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Tue, 21 Apr 2026 13:25:22 +0200 Subject: [PATCH 1/5] make tests --- crates/defguard_certs/src/lib.rs | 89 ++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/crates/defguard_certs/src/lib.rs b/crates/defguard_certs/src/lib.rs index 99d64667e..bfc4c0baa 100644 --- a/crates/defguard_certs/src/lib.rs +++ b/crates/defguard_certs/src/lib.rs @@ -592,6 +592,95 @@ mod tests { ); } + /// Verify that the CA can sign a CSR generated externally with a NIST P-256 + /// (secp256r1) key pair + #[test] + fn test_sign_external_p256_csr_via_from_der() { + use rcgen::PKCS_ECDSA_P256_SHA256; + + let device_key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap(); + let csr_built = Csr::new( + &device_key, + &["device.example.com".to_string()], + vec![(rcgen::DnType::CommonName, "device.example.com")], + ) + .unwrap(); + + let csr_der = csr_built.to_der().to_vec(); + let csr = Csr::from_der(&csr_der).unwrap(); + + let ca = CertificateAuthority::new("Defguard CA", "ca@example.com", 365).unwrap(); + let signed = ca.sign_server_cert(&csr).unwrap(); + + // The signed certificate must carry the device's EC (P-256) public key. + // EC public key OID: 1.2.840.10045.2.1 + let (_, parsed) = x509_parser::parse_x509_certificate(signed.der()).unwrap(); + let spki_alg_oid = parsed + .tbs_certificate + .subject_pki + .algorithm + .algorithm + .to_id_string(); + assert_eq!( + spki_alg_oid, "1.2.840.10045.2.1", + "signed certificate must carry an EC (P-256) public key" + ); + + // The subject public key in the signed cert must match the device key. + let cert_pub_key_bytes = parsed.tbs_certificate.subject_pki.subject_public_key.data; + let device_pub_key_bytes = device_key.public_key_raw(); + assert_eq!( + cert_pub_key_bytes, device_pub_key_bytes, + "signed certificate public key must match the device's P-256 public key" + ); + } + + /// Verify that the CA can sign a CSR produced by an external tool (OpenSSL) + /// using a real NIST P-256 key. + /// The CSR was produced with: + /// openssl ecparam -name prime256v1 -genkey -noout -out device.key + /// openssl req -new -key device.key \ + /// -subj "/CN=device.example.com" \ + /// -addext "subjectAltName=DNS:device.example.com" \ + /// -out device.csr + #[test] + fn test_sign_openssl_p256_csr() { + const CSR_PEM: &str = "-----BEGIN CERTIFICATE REQUEST----- +MIIBBzCBrwIBADAdMRswGQYDVQQDDBJkZXZpY2UuZXhhbXBsZS5jb20wWTATBgcq +hkjOPQIBBggqhkjOPQMBBwNCAARd5+5mjOxyatISxK98hF2LmOwsjuFOlCQbe7u7 +vTJ70sC39Z9U8u4BwbSUl2fyRuKMwOCMt29dffKFoJz4EvMRoDAwLgYJKoZIhvcN +AQkOMSEwHzAdBgNVHREEFjAUghJkZXZpY2UuZXhhbXBsZS5jb20wCgYIKoZIzj0E +AwIDRwAwRAIgb38FDcxhdMUoGb+wDM8wHtVjKO2bKjxOMdbEloxhxK0CIHJMIxiu +mHNLSdvm1lY8N5VL6VyZMtaGi1jjF0en7drb +-----END CERTIFICATE REQUEST-----"; + + let b64: String = CSR_PEM + .lines() + .filter(|l| !l.starts_with("-----")) + .collect(); + let csr_der = BASE64_STANDARD.decode(b64).unwrap(); + + let csr = Csr::from_der(&csr_der).unwrap(); + + let ca = CertificateAuthority::new("Defguard CA", "ca@example.com", 365).unwrap(); + let signed = ca.sign_server_cert(&csr).unwrap(); + + // EC public key OID: 1.2.840.10045.2.1 + let (_, parsed) = x509_parser::parse_x509_certificate(signed.der()).unwrap(); + let spki = &parsed.tbs_certificate.subject_pki; + assert_eq!( + spki.algorithm.algorithm.to_id_string(), + "1.2.840.10045.2.1", + "signed certificate must carry an EC (P-256) public key" + ); + // Confirm it is a 256-bit EC point (uncompressed: 65 bytes). + assert_eq!( + spki.subject_public_key.data.len(), + 65, + "EC P-256 public key must be 65 bytes (uncompressed point)" + ); + } + #[test] fn test_csr_verify_hostname_extra_san_rejected() { let key = generate_key_pair().unwrap(); From a85719798dc0942610a0dcc1d6a66f74ea2eee5a Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Tue, 21 Apr 2026 14:56:13 +0200 Subject: [PATCH 2/5] test for client and server --- crates/defguard_certs/src/lib.rs | 141 ++++++++++++++++--------------- 1 file changed, 72 insertions(+), 69 deletions(-) diff --git a/crates/defguard_certs/src/lib.rs b/crates/defguard_certs/src/lib.rs index bfc4c0baa..8d770e8be 100644 --- a/crates/defguard_certs/src/lib.rs +++ b/crates/defguard_certs/src/lib.rs @@ -96,10 +96,6 @@ impl CertificateAuthority<'_> { Self::from_key_cert_params(ca_key_pair, ca_params) } - /// Sign a server-facing component certificate (`ServerAuth` EKU only). - /// - /// Use [`sign_client_cert`] for Core gRPC client certificates, or - /// [`sign_csr_with_validity`] when custom validity is needed. pub fn sign_server_cert(&self, csr: &Csr) -> Result { self.sign_csr_with_validity( csr, @@ -108,7 +104,6 @@ impl CertificateAuthority<'_> { ) } - /// Sign a Core gRPC client certificate (`ClientAuth` EKU only). pub fn sign_client_cert(&self, csr: &Csr) -> Result { self.sign_csr_with_validity( csr, @@ -117,11 +112,6 @@ impl CertificateAuthority<'_> { ) } - /// Sign a CSR with explicit validity in days and extended key usages. - /// - /// `extended_key_usages` controls which EKUs are encoded in the signed - /// certificate. Pass `&[ServerAuth]` for component server certs and - /// `&[ClientAuth]` for Core gRPC client certs. pub fn sign_csr_with_validity( &self, csr: &Csr, @@ -397,6 +387,21 @@ pub type RcGenKeyPair = rcgen::KeyPair; mod tests { use super::*; + // Generated with: + // openssl ecparam -name prime256v1 -genkey -noout -out device.key + // openssl req -new -key device.key \ + // -subj "/CN=device.example.com" \ + // -addext "subjectAltName=DNS:device.example.com" \ + // -out device.csr + const OPENSSL_P256_CSR_PEM: &str = "-----BEGIN CERTIFICATE REQUEST----- +MIIBBzCBrwIBADAdMRswGQYDVQQDDBJkZXZpY2UuZXhhbXBsZS5jb20wWTATBgcq +hkjOPQIBBggqhkjOPQMBBwNCAARd5+5mjOxyatISxK98hF2LmOwsjuFOlCQbe7u7 +vTJ70sC39Z9U8u4BwbSUl2fyRuKMwOCMt29dffKFoJz4EvMRoDAwLgYJKoZIhvcN +AQkOMSEwHzAdBgNVHREEFjAUghJkZXZpY2UuZXhhbXBsZS5jb20wCgYIKoZIzj0E +AwIDRwAwRAIgb38FDcxhdMUoGb+wDM8wHtVjKO2bKjxOMdbEloxhxK0CIHJMIxiu +mHNLSdvm1lY8N5VL6VyZMtaGi1jjF0en7drb +-----END CERTIFICATE REQUEST-----"; + #[test] fn test_to_from_der() { let key_pair = KeyPair::generate().unwrap(); @@ -553,11 +558,8 @@ mod tests { #[test] fn test_parse_pem_certificate() { - // Create a CA and get its PEM representation let ca = CertificateAuthority::new("Defguard CA", "test@example.com", 365).unwrap(); let pem = ca.cert_pem().unwrap(); - - // Parse the PEM back to DER and ensure it matches the original let parsed = parse_pem_certificate(&pem).unwrap(); assert_eq!(parsed, ca.cert_der); } @@ -592,8 +594,6 @@ mod tests { ); } - /// Verify that the CA can sign a CSR generated externally with a NIST P-256 - /// (secp256r1) key pair #[test] fn test_sign_external_p256_csr_via_from_der() { use rcgen::PKCS_ECDSA_P256_SHA256; @@ -606,79 +606,82 @@ mod tests { ) .unwrap(); - let csr_der = csr_built.to_der().to_vec(); - let csr = Csr::from_der(&csr_der).unwrap(); + let csr = Csr::from_der(&csr_built.to_der().to_vec()).unwrap(); let ca = CertificateAuthority::new("Defguard CA", "ca@example.com", 365).unwrap(); let signed = ca.sign_server_cert(&csr).unwrap(); - // The signed certificate must carry the device's EC (P-256) public key. - // EC public key OID: 1.2.840.10045.2.1 let (_, parsed) = x509_parser::parse_x509_certificate(signed.der()).unwrap(); - let spki_alg_oid = parsed - .tbs_certificate - .subject_pki - .algorithm - .algorithm - .to_id_string(); + // ecPublicKey OID: 1.2.840.10045.2.1 assert_eq!( - spki_alg_oid, "1.2.840.10045.2.1", - "signed certificate must carry an EC (P-256) public key" + parsed + .tbs_certificate + .subject_pki + .algorithm + .algorithm + .to_id_string(), + "1.2.840.10045.2.1", ); - - // The subject public key in the signed cert must match the device key. - let cert_pub_key_bytes = parsed.tbs_certificate.subject_pki.subject_public_key.data; - let device_pub_key_bytes = device_key.public_key_raw(); assert_eq!( - cert_pub_key_bytes, device_pub_key_bytes, - "signed certificate public key must match the device's P-256 public key" + parsed.tbs_certificate.subject_pki.subject_public_key.data, + device_key.public_key_raw(), ); } - /// Verify that the CA can sign a CSR produced by an external tool (OpenSSL) - /// using a real NIST P-256 key. - /// The CSR was produced with: - /// openssl ecparam -name prime256v1 -genkey -noout -out device.key - /// openssl req -new -key device.key \ - /// -subj "/CN=device.example.com" \ - /// -addext "subjectAltName=DNS:device.example.com" \ - /// -out device.csr #[test] fn test_sign_openssl_p256_csr() { - const CSR_PEM: &str = "-----BEGIN CERTIFICATE REQUEST----- -MIIBBzCBrwIBADAdMRswGQYDVQQDDBJkZXZpY2UuZXhhbXBsZS5jb20wWTATBgcq -hkjOPQIBBggqhkjOPQMBBwNCAARd5+5mjOxyatISxK98hF2LmOwsjuFOlCQbe7u7 -vTJ70sC39Z9U8u4BwbSUl2fyRuKMwOCMt29dffKFoJz4EvMRoDAwLgYJKoZIhvcN -AQkOMSEwHzAdBgNVHREEFjAUghJkZXZpY2UuZXhhbXBsZS5jb20wCgYIKoZIzj0E -AwIDRwAwRAIgb38FDcxhdMUoGb+wDM8wHtVjKO2bKjxOMdbEloxhxK0CIHJMIxiu -mHNLSdvm1lY8N5VL6VyZMtaGi1jjF0en7drb ------END CERTIFICATE REQUEST-----"; + let csr = csr_from_pem(OPENSSL_P256_CSR_PEM); + let ca = CertificateAuthority::new("Defguard CA", "ca@example.com", 365).unwrap(); + let signed = ca.sign_server_cert(&csr).unwrap(); + let (_, parsed) = x509_parser::parse_x509_certificate(signed.der()).unwrap(); + assert_p256_spki(&parsed); + assert_eku(&parsed, /* client_auth */ false, /* server_auth */ true); + } + + #[test] + fn test_sign_openssl_p256_csr_client_cert() { + let csr = csr_from_pem(OPENSSL_P256_CSR_PEM); + let ca = CertificateAuthority::new("Defguard CA", "ca@example.com", 365).unwrap(); + let signed = ca.sign_client_cert(&csr).unwrap(); + let (_, parsed) = x509_parser::parse_x509_certificate(signed.der()).unwrap(); + assert_p256_spki(&parsed); + assert_eku(&parsed, /* client_auth */ true, /* server_auth */ false); + } - let b64: String = CSR_PEM + fn csr_from_pem(pem: &str) -> Csr<'static> { + let b64: String = pem .lines() .filter(|l| !l.starts_with("-----")) .collect(); - let csr_der = BASE64_STANDARD.decode(b64).unwrap(); - - let csr = Csr::from_der(&csr_der).unwrap(); - - let ca = CertificateAuthority::new("Defguard CA", "ca@example.com", 365).unwrap(); - let signed = ca.sign_server_cert(&csr).unwrap(); + let der = BASE64_STANDARD.decode(b64).unwrap(); + Csr::from_der(&der).unwrap() + } - // EC public key OID: 1.2.840.10045.2.1 - let (_, parsed) = x509_parser::parse_x509_certificate(signed.der()).unwrap(); + fn assert_p256_spki(parsed: &x509_parser::certificate::X509Certificate<'_>) { let spki = &parsed.tbs_certificate.subject_pki; - assert_eq!( - spki.algorithm.algorithm.to_id_string(), - "1.2.840.10045.2.1", - "signed certificate must carry an EC (P-256) public key" - ); - // Confirm it is a 256-bit EC point (uncompressed: 65 bytes). - assert_eq!( - spki.subject_public_key.data.len(), - 65, - "EC P-256 public key must be 65 bytes (uncompressed point)" - ); + // ecPublicKey OID: 1.2.840.10045.2.1 + assert_eq!(spki.algorithm.algorithm.to_id_string(), "1.2.840.10045.2.1"); + // uncompressed P-256 point: 0x04 || X || Y + assert_eq!(spki.subject_public_key.data.len(), 65); + } + + fn assert_eku( + parsed: &x509_parser::certificate::X509Certificate<'_>, + expect_client_auth: bool, + expect_server_auth: bool, + ) { + use x509_parser::extensions::ParsedExtension; + let eku = parsed + .tbs_certificate + .extensions() + .iter() + .find_map(|ext| match ext.parsed_extension() { + ParsedExtension::ExtendedKeyUsage(eku) => Some(eku.clone()), + _ => None, + }) + .expect("ExtendedKeyUsage extension must be present"); + assert_eq!(eku.client_auth, expect_client_auth); + assert_eq!(eku.server_auth, expect_server_auth); } #[test] From 5472c43da61169b00203450e1417bcd1075446b3 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Tue, 21 Apr 2026 15:10:31 +0200 Subject: [PATCH 3/5] fmt --- crates/defguard_certs/src/lib.rs | 16 ++++++++-------- crates/defguard_proxy_manager/src/handler.rs | 4 ++-- .../tests/integration/auto_adoption_wizard.rs | 3 +-- .../integration/auto_wizard_url_settings.rs | 5 ++--- .../tests/integration/initial_setup.rs | 3 +-- .../tests/integration/migration_wizard.rs | 2 +- 6 files changed, 15 insertions(+), 18 deletions(-) diff --git a/crates/defguard_certs/src/lib.rs b/crates/defguard_certs/src/lib.rs index 8d770e8be..40d849189 100644 --- a/crates/defguard_certs/src/lib.rs +++ b/crates/defguard_certs/src/lib.rs @@ -2,6 +2,7 @@ use std::{net::IpAddr, str::FromStr}; use base64::{Engine, prelude::BASE64_STANDARD}; use chrono::NaiveDateTime; +pub use rcgen::ExtendedKeyUsagePurpose; use rcgen::{ BasicConstraints, Certificate, CertificateParams, CertificateSigningRequestParams, IsCa, Issuer, KeyPair, KeyUsagePurpose, SigningKey, string::Ia5String, @@ -14,8 +15,6 @@ use x509_parser::{ parse_x509_certificate, }; -pub use rcgen::ExtendedKeyUsagePurpose; - const CA_NAME: &str = "Defguard CA"; const NOT_BEFORE_OFFSET_SECS: Duration = Duration::minutes(5); const DEFAULT_CERT_VALIDITY_DAYS: i64 = 1825; @@ -635,7 +634,9 @@ mHNLSdvm1lY8N5VL6VyZMtaGi1jjF0en7drb let signed = ca.sign_server_cert(&csr).unwrap(); let (_, parsed) = x509_parser::parse_x509_certificate(signed.der()).unwrap(); assert_p256_spki(&parsed); - assert_eku(&parsed, /* client_auth */ false, /* server_auth */ true); + assert_eku( + &parsed, /* client_auth */ false, /* server_auth */ true, + ); } #[test] @@ -645,14 +646,13 @@ mHNLSdvm1lY8N5VL6VyZMtaGi1jjF0en7drb let signed = ca.sign_client_cert(&csr).unwrap(); let (_, parsed) = x509_parser::parse_x509_certificate(signed.der()).unwrap(); assert_p256_spki(&parsed); - assert_eku(&parsed, /* client_auth */ true, /* server_auth */ false); + assert_eku( + &parsed, /* client_auth */ true, /* server_auth */ false, + ); } fn csr_from_pem(pem: &str) -> Csr<'static> { - let b64: String = pem - .lines() - .filter(|l| !l.starts_with("-----")) - .collect(); + let b64: String = pem.lines().filter(|l| !l.starts_with("-----")).collect(); let der = BASE64_STANDARD.decode(b64).unwrap(); Csr::from_der(&der).unwrap() } diff --git a/crates/defguard_proxy_manager/src/handler.rs b/crates/defguard_proxy_manager/src/handler.rs index 81d804ff5..d7e8b6704 100644 --- a/crates/defguard_proxy_manager/src/handler.rs +++ b/crates/defguard_proxy_manager/src/handler.rs @@ -62,6 +62,8 @@ use tokio::{ time::sleep, }; use tokio_stream::wrappers::UnboundedReceiverStream; +#[cfg(test)] +use tonic::transport::Endpoint; use tonic::{ Code, Request, Streaming, service::interceptor::InterceptedService, transport::Channel, }; @@ -72,8 +74,6 @@ use crate::{ HandlerTxMap, ProxyError, ProxyTxSet, TEN_SECS, servers::{EnrollmentServer, PasswordResetServer}, }; -#[cfg(test)] -use tonic::transport::Endpoint; const VERSION_ZERO: Version = Version::new(0, 0, 0); diff --git a/crates/defguard_setup/tests/integration/auto_adoption_wizard.rs b/crates/defguard_setup/tests/integration/auto_adoption_wizard.rs index deaec3553..cf61eb351 100644 --- a/crates/defguard_setup/tests/integration/auto_adoption_wizard.rs +++ b/crates/defguard_setup/tests/integration/auto_adoption_wizard.rs @@ -28,9 +28,8 @@ use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; use tokio::time::timeout; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; -use crate::common::SESSION_COOKIE_NAME; - use super::common::{SHUTDOWN_TIMEOUT, make_setup_test_client}; +use crate::common::SESSION_COOKIE_NAME; fn init_tracing_once() { static ONCE: Once = Once::new(); diff --git a/crates/defguard_setup/tests/integration/auto_wizard_url_settings.rs b/crates/defguard_setup/tests/integration/auto_wizard_url_settings.rs index 289558ded..59e285419 100644 --- a/crates/defguard_setup/tests/integration/auto_wizard_url_settings.rs +++ b/crates/defguard_setup/tests/integration/auto_wizard_url_settings.rs @@ -22,11 +22,10 @@ use reqwest::{ }; use serde_json::json; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; - -use crate::common::{SESSION_COOKIE_NAME, TestClient}; +use tokio::{sync::oneshot, time::timeout}; use super::common::{SHUTDOWN_TIMEOUT, make_setup_test_client}; -use tokio::{sync::oneshot, time::timeout}; +use crate::common::{SESSION_COOKIE_NAME, TestClient}; async fn bootstrap_wizard_to_url_settings( pool: &sqlx::PgPool, diff --git a/crates/defguard_setup/tests/integration/initial_setup.rs b/crates/defguard_setup/tests/integration/initial_setup.rs index 08816b2b3..527c6d868 100644 --- a/crates/defguard_setup/tests/integration/initial_setup.rs +++ b/crates/defguard_setup/tests/integration/initial_setup.rs @@ -34,9 +34,8 @@ use tokio::{ time::timeout, }; -use crate::common::SESSION_COOKIE_NAME; - use super::common::{SHUTDOWN_TIMEOUT, make_setup_test_client}; +use crate::common::SESSION_COOKIE_NAME; async fn assert_setup_step(pool: &sqlx::PgPool, expected: InitialSetupStep) { let step = InitialSetupState::get(pool) diff --git a/crates/defguard_setup/tests/integration/migration_wizard.rs b/crates/defguard_setup/tests/integration/migration_wizard.rs index d7f9d72c4..356498254 100644 --- a/crates/defguard_setup/tests/integration/migration_wizard.rs +++ b/crates/defguard_setup/tests/integration/migration_wizard.rs @@ -15,11 +15,11 @@ use reqwest::{ }; use serde_json::json; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; +use tokio::time::timeout; use super::common::{ SHUTDOWN_TIMEOUT, init_settings_with_secret_key, make_migration_test_client, seed_admin_user, }; -use tokio::time::timeout; async fn assert_migration_step(pool: &sqlx::PgPool, expected_variant: &str) { let state = MigrationWizardState::get(pool) From da4bdd3c2773a66892482aa16d462a5222d57736 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Wed, 22 Apr 2026 11:24:45 +0200 Subject: [PATCH 4/5] clippy --- crates/defguard_certs/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/defguard_certs/src/lib.rs b/crates/defguard_certs/src/lib.rs index 40d849189..b463fcfe7 100644 --- a/crates/defguard_certs/src/lib.rs +++ b/crates/defguard_certs/src/lib.rs @@ -605,7 +605,7 @@ mHNLSdvm1lY8N5VL6VyZMtaGi1jjF0en7drb ) .unwrap(); - let csr = Csr::from_der(&csr_built.to_der().to_vec()).unwrap(); + let csr = Csr::from_der(&csr_built.to_der()).unwrap(); let ca = CertificateAuthority::new("Defguard CA", "ca@example.com", 365).unwrap(); let signed = ca.sign_server_cert(&csr).unwrap(); From c275ca234bb67f02f3e80c5a97798291a301fce4 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Fri, 24 Apr 2026 09:35:16 +0200 Subject: [PATCH 5/5] clippy 2 --- crates/defguard_certs/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/defguard_certs/src/lib.rs b/crates/defguard_certs/src/lib.rs index b463fcfe7..2516aad34 100644 --- a/crates/defguard_certs/src/lib.rs +++ b/crates/defguard_certs/src/lib.rs @@ -605,7 +605,7 @@ mHNLSdvm1lY8N5VL6VyZMtaGi1jjF0en7drb ) .unwrap(); - let csr = Csr::from_der(&csr_built.to_der()).unwrap(); + let csr = Csr::from_der(csr_built.to_der()).unwrap(); let ca = CertificateAuthority::new("Defguard CA", "ca@example.com", 365).unwrap(); let signed = ca.sign_server_cert(&csr).unwrap();