Skip to content
Merged
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

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/defguard_certs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ serde.workspace = true
sqlx.workspace = true
thiserror.workspace = true
rustls-pki-types.workspace = true
time = "0.3"
x509-parser = "0.18"
75 changes: 69 additions & 6 deletions crates/defguard_certs/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
use base64::{Engine, prelude::BASE64_STANDARD};
use rcgen::{
BasicConstraints, Certificate, CertificateParams, CertificateSigningRequestParams, IsCa,
Issuer, KeyPair, SigningKey,
BasicConstraints, Certificate, CertificateParams, CertificateSigningRequestParams,
ExtendedKeyUsagePurpose, IsCa, Issuer, KeyPair, KeyUsagePurpose, SigningKey,
};
use rustls_pki_types::{CertificateDer, CertificateSigningRequestDer, pem::PemObject};
use thiserror::Error;
use time::{Duration, OffsetDateTime};
use x509_parser::parse_x509_certificate;

const CA_NAME: &str = "Defguard CA";
const CA_ORG: &str = "Defguard";
const NOT_BEFORE_OFFSET_SECS: Duration = Duration::minutes(5);
const DEFAULT_CERT_VALIDITY_DAYS: i64 = 365;

#[derive(Debug, Error)]
pub enum CertificateError {
Expand Down Expand Up @@ -59,7 +63,8 @@ impl CertificateAuthority<'_> {
pub fn new() -> Result<Self, CertificateError> {
let mut ca_params = CertificateParams::new(vec![CA_NAME.to_string()])?;

ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
// path length 0 to avoid issuing further CAs
ca_params.is_ca = IsCa::Ca(BasicConstraints::Constrained(0));
ca_params
.distinguished_name
.push(rcgen::DnType::OrganizationName, CA_ORG);
Expand All @@ -73,8 +78,35 @@ impl CertificateAuthority<'_> {
}

pub fn sign_csr(&self, csr: &Csr) -> Result<Certificate, CertificateError> {
let csr = csr.params()?;
let cert = csr.signed_by(&self.issuer)?;
// TODO: make validity configurable?
self.sign_csr_with_validity(csr, DEFAULT_CERT_VALIDITY_DAYS)
}

/// Sign CSR with explicit validity in days.
pub fn sign_csr_with_validity(
&self,
csr: &Csr,
days_valid: i64,
) -> Result<Certificate, CertificateError> {
let mut csr_params = csr.params()?;

let now = OffsetDateTime::now_utc();
let not_before = now - NOT_BEFORE_OFFSET_SECS;
let not_after = now + Duration::days(days_valid);

csr_params.params.not_before = not_before;
csr_params.params.not_after = not_after;

csr_params.params.key_usages = vec![
KeyUsagePurpose::DigitalSignature,
KeyUsagePurpose::KeyEncipherment,
];
csr_params.params.extended_key_usages = vec![
ExtendedKeyUsagePurpose::ServerAuth,
ExtendedKeyUsagePurpose::ClientAuth,
];

let cert = csr_params.signed_by(&self.issuer)?;
Ok(cert)
}

Expand All @@ -93,6 +125,14 @@ impl CertificateAuthority<'_> {
}
}

/// Extract the expiry date (not_after) from a certificate.
pub fn get_certificate_expiry(cert: &Certificate) -> Result<OffsetDateTime, CertificateError> {
let (_, parsed) = parse_x509_certificate(cert.der())
.map_err(|e| CertificateError::ParsingError(format!("Failed to parse certificate: {e}")))?;

Ok(parsed.tbs_certificate.validity.not_after.to_datetime())
}

pub struct Csr<'a> {
csr: CertificateSigningRequestDer<'a>,
}
Expand Down Expand Up @@ -207,7 +247,7 @@ mod tests {
#[test]
fn test_sign_csr() {
let ca = CertificateAuthority::new().unwrap();
let cert_key_pair = KeyPair::generate().unwrap();
let cert_key_pair = generate_key_pair().unwrap();
let csr = Csr::new(
&cert_key_pair,
&["example.com".to_string(), "www.example.com".to_string()],
Expand All @@ -221,6 +261,29 @@ mod tests {
assert!(signed_cert.pem().contains("BEGIN CERTIFICATE"));
}

#[test]
fn test_sign_csr_with_validity() {
use x509_parser::parse_x509_certificate;

let ca = CertificateAuthority::new().unwrap();
let cert_key_pair = generate_key_pair().unwrap();
let csr = Csr::new(
&cert_key_pair,
&["example.com".to_string()],
vec![(rcgen::DnType::CommonName, "example.com")],
)
.unwrap();
let signed_cert: Certificate = ca.sign_csr_with_validity(&csr, 90).unwrap();
let der = signed_cert.der();
let (_rem, parsed) = parse_x509_certificate(der).unwrap();
let validity = parsed.tbs_certificate.validity;
let not_before = validity.not_before.to_datetime();
let not_after = validity.not_after.to_datetime();
let days = (not_after - not_before).whole_days();
assert!((89..=91).contains(&days), "expected 89-91 days, got {days}");
assert!(not_after > not_before);
}

#[test]
fn test_der_to_pem() {
assert_eq!(PemLabel::Certificate.as_str(), "CERTIFICATE");
Expand Down
32 changes: 16 additions & 16 deletions crates/defguard_common/src/db/models/device.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,5 @@
use std::{fmt, net::IpAddr};

use crate::{
KEY_LENGTH,
csv::AsCsv,
db::{
Id, NoId,
models::{
ModelError, WireguardNetwork,
user::User,
wireguard::{
LocationMfaMode, NetworkAddressError, ServiceLocationMode, WIREGUARD_MAX_HANDSHAKE,
},
},
},
};
use base64::{Engine, prelude::BASE64_STANDARD};
use chrono::{NaiveDate, NaiveDateTime, Timelike, Utc};
use ipnetwork::IpNetwork;
Expand All @@ -32,6 +18,21 @@ use thiserror::Error;
use tracing::{debug, error, info};
use utoipa::ToSchema;

use crate::{
KEY_LENGTH,
csv::AsCsv,
db::{
Id, NoId,
models::{
ModelError, WireguardNetwork,
user::User,
wireguard::{
LocationMfaMode, NetworkAddressError, ServiceLocationMode, WIREGUARD_MAX_HANDSHAKE,
},
},
},
};

#[derive(Serialize, ToSchema)]
pub struct DeviceConfig {
pub network_id: Id,
Expand Down Expand Up @@ -1005,9 +1006,8 @@ mod test {
use claims::{assert_err, assert_ok};
use sqlx::postgres::{PgConnectOptions, PgPoolOptions};

use crate::db::setup_pool;

use super::*;
use crate::db::setup_pool;

impl Device<Id> {
/// Create new device and assign IP in a given network
Expand Down
4 changes: 4 additions & 0 deletions crates/defguard_common/src/db/models/gateway.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ pub struct Gateway<I = NoId> {
pub hostname: Option<String>,
pub connected_at: Option<NaiveDateTime>,
pub disconnected_at: Option<NaiveDateTime>,
pub has_certificate: bool,
pub certificate_expiry: Option<NaiveDateTime>,
}

impl<I> Gateway<I> {
Expand All @@ -39,6 +41,8 @@ impl Gateway {
hostname: None,
connected_at: None,
disconnected_at: None,
has_certificate: false,
certificate_expiry: None,
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/defguard_common/src/db/models/group.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,10 +160,10 @@ impl Group<Id> {

#[cfg(test)]
mod test {
use crate::db::setup_pool;
use sqlx::postgres::{PgConnectOptions, PgPoolOptions};

use super::*;
use crate::db::setup_pool;

#[sqlx::test]
async fn test_group(_: PgPoolOptions, options: PgConnectOptions) {
Expand Down
5 changes: 3 additions & 2 deletions crates/defguard_common/src/db/models/mfa_info.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use serde::{Deserialize, Serialize};
use sqlx::{Error as SqlxError, PgPool, query_as};

use crate::db::{
Id,
models::{MFAMethod, user::User},
};
use serde::{Deserialize, Serialize};
use sqlx::{Error as SqlxError, PgPool, query_as};

#[derive(Deserialize, Serialize)]
pub struct MFAInfo {
Expand Down
3 changes: 2 additions & 1 deletion crates/defguard_common/src/db/models/oauth2authorizedapp.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use crate::db::{Id, NoId};
use model_derive::Model;
use sqlx::{Error as SqlxError, PgPool, query_as};

use crate::db::{Id, NoId};

#[derive(Model)]
pub struct OAuth2AuthorizedApp<I = NoId> {
pub id: I,
Expand Down
3 changes: 2 additions & 1 deletion crates/defguard_common/src/db/models/oauth2token.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use crate::{config::server_config, db::Id, random::gen_alphanumeric};
use chrono::{TimeDelta, Utc};
use sqlx::{Error as SqlxError, PgPool, query, query_as};

use crate::{config::server_config, db::Id, random::gen_alphanumeric};

pub struct OAuth2Token {
pub oauth2authorizedapp_id: Id,
pub access_token: String,
Expand Down
Loading
Loading