Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 107 additions & 15 deletions crates/defguard_certs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -96,10 +95,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<Certificate, CertificateError> {
self.sign_csr_with_validity(
csr,
Expand All @@ -108,7 +103,6 @@ impl CertificateAuthority<'_> {
)
}

/// Sign a Core gRPC client certificate (`ClientAuth` EKU only).
pub fn sign_client_cert(&self, csr: &Csr) -> Result<Certificate, CertificateError> {
self.sign_csr_with_validity(
csr,
Expand All @@ -117,11 +111,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,
Expand Down Expand Up @@ -397,6 +386,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();
Expand Down Expand Up @@ -553,11 +557,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);
}
Expand Down Expand Up @@ -592,6 +593,97 @@ mod tests {
);
}

#[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 = 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();

let (_, parsed) = x509_parser::parse_x509_certificate(signed.der()).unwrap();
// ecPublicKey OID: 1.2.840.10045.2.1
assert_eq!(
parsed
.tbs_certificate
.subject_pki
.algorithm
.algorithm
.to_id_string(),
"1.2.840.10045.2.1",
);
assert_eq!(
parsed.tbs_certificate.subject_pki.subject_public_key.data,
device_key.public_key_raw(),
);
}

#[test]
fn test_sign_openssl_p256_csr() {
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,
);
}

fn csr_from_pem(pem: &str) -> Csr<'static> {
let b64: String = pem.lines().filter(|l| !l.starts_with("-----")).collect();
let der = BASE64_STANDARD.decode(b64).unwrap();
Csr::from_der(&der).unwrap()
}

fn assert_p256_spki(parsed: &x509_parser::certificate::X509Certificate<'_>) {
let spki = &parsed.tbs_certificate.subject_pki;
// 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]
fn test_csr_verify_hostname_extra_san_rejected() {
let key = generate_key_pair().unwrap();
Expand Down
4 changes: 2 additions & 2 deletions crates/defguard_proxy_manager/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand All @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 1 addition & 2 deletions crates/defguard_setup/tests/integration/initial_setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading