Skip to content

Commit aca08f0

Browse files
authored
fix(dgw)!: fail-fast on improper certificate for TLS (#1391)
Certificates missing the auth extended key usage, or missing a subject alternative name are now rejected: - immediately fail on startup for certificates from filesystem, and - fail on certificate resolution for system certificate store. Issue: DGW-286
1 parent 882e757 commit aca08f0

File tree

3 files changed

+146
-36
lines changed

3 files changed

+146
-36
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

devolutions-gateway/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ typed-builder = "0.19"
6262
backoff = "0.4"
6363
sysinfo = "0.30"
6464
dunce = "1.0"
65+
bitflags = "2.9"
6566

6667
# Security, crypto…
6768
picky = { version = "7.0.0-rc.10", default-features = false, features = ["jose", "x509", "pkcs12", "time_conversion"] }

devolutions-gateway/src/tls.rs

Lines changed: 144 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,28 @@ pub fn build_server_config(cert_source: CertificateSource) -> anyhow::Result<rus
7373
CertificateSource::External {
7474
certificates,
7575
private_key,
76-
} => builder
77-
.with_single_cert(certificates, private_key)
78-
.context("failed to set server config cert"),
76+
} => {
77+
let first_certificate = certificates.first().context("empty certificate list")?;
78+
79+
if let Ok(report) = check_certificate_now(&first_certificate) {
80+
if report.issues.intersects(
81+
CertIssues::MISSING_SERVER_AUTH_EXTENDED_KEY_USAGE | CertIssues::MISSING_SUBJECT_ALT_NAME,
82+
) {
83+
let serial_number = report.serial_number;
84+
let subject = report.subject;
85+
let issuer = report.issuer;
86+
let not_before = report.not_before;
87+
let not_after = report.not_after;
88+
let issues = report.issues;
89+
90+
anyhow::bail!("found significant issues with the certificate: serial_number = {serial_number}, subject = {subject}, issuer = {issuer}, not_before = {not_before}, not_after = {not_after}, issues = {issues}");
91+
}
92+
}
93+
94+
builder
95+
.with_single_cert(certificates, private_key)
96+
.context("failed to set server config cert")
97+
}
7998

8099
#[cfg(windows)]
81100
CertificateSource::SystemStore {
@@ -116,6 +135,7 @@ pub mod windows {
116135
use tokio_rustls::rustls::sign::CertifiedKey;
117136

118137
use crate::config::dto;
138+
use crate::tls::{check_certificate, CertIssues};
119139

120140
const CACHE_DURATION: time::Duration = time::Duration::seconds(45);
121141

@@ -157,8 +177,6 @@ pub mod windows {
157177
}
158178

159179
fn resolve(&self, client_hello: ClientHello<'_>) -> anyhow::Result<Arc<CertifiedKey>> {
160-
use core::fmt::Write as _;
161-
162180
trace!(server_name = ?client_hello.server_name(), "Received ClientHello");
163181

164182
let request_server_name = client_hello
@@ -212,52 +230,54 @@ pub mod windows {
212230

213231
trace!(subject_name = %self.subject_name, count = contexts.len(), "Found certificate contexts");
214232

215-
let x509_date_now = picky::x509::date::UtcDate::from(now);
233+
// We will accumulate all the certificate issues we observe next.
234+
let mut cert_issues = CertIssues::empty();
216235

217236
// Initial processing and filtering of the available candidates.
218237
let mut contexts: Vec<CertHandleCtx> = contexts
219238
.into_iter()
220239
.enumerate()
221240
.filter_map(|(idx, ctx)| {
222-
let not_after = match picky::x509::Cert::from_der(ctx.as_der()) {
223-
Ok(cert) => {
224-
let serial_number = cert.serial_number().0.iter().fold(
225-
String::new(),
226-
|mut acc, byte| {
227-
let _ = write!(acc, "{byte:X?}");
228-
acc
229-
},
241+
let not_after = match check_certificate(ctx.as_der(), now) {
242+
Ok(report) => {
243+
trace!(
244+
%idx,
245+
serial_number = %report.serial_number,
246+
subject = %report.subject,
247+
issuer = %report.issuer,
248+
not_before = %report.not_before,
249+
not_after = %report.not_after,
250+
issues = %report.issues,
251+
"Parsed store certificate"
230252
);
231-
let subject = cert.subject_name();
232-
let issuer = cert.issuer_name();
233-
let not_before = cert.valid_not_before();
234-
let not_after = cert.valid_not_after();
235-
236-
trace!(%idx, %serial_number, %subject, %issuer, %not_before, %not_after, "Parsed store certificate");
237253

238-
if x509_date_now < not_before {
239-
debug!(
240-
%idx, %serial_number, %not_before, "Filtered out certificate based on not before validity date"
241-
);
242-
return None;
243-
}
244-
245-
let has_server_auth_key_purpose = cert.extensions().iter().any(|ext| match ext.extn_value() {
246-
picky::x509::extension::ExtensionView::ExtendedKeyUsage(eku) => eku.contains(picky::oids::kp_server_auth()),
247-
_ => false,
248-
});
254+
// Accumulate the issues found.
255+
cert_issues |= report.issues;
256+
257+
// Skip the certificate if any of the following is true:
258+
// - The certificate is not yet valid.
259+
// - The certificate is missing the server auth extended key usage.
260+
// - The certificate is missing a subject alternative name (SAN) extension.
261+
let skip = report.issues.intersects(
262+
CertIssues::NOT_YET_VALID
263+
| CertIssues::MISSING_SERVER_AUTH_EXTENDED_KEY_USAGE
264+
| CertIssues::MISSING_SUBJECT_ALT_NAME,
265+
);
249266

250-
if !has_server_auth_key_purpose {
267+
if skip {
251268
debug!(
252-
%idx, %serial_number, "Filtered out certificate because it does not have the server auth extended usage"
269+
%idx,
270+
serial_number = %report.serial_number,
271+
issues = %report.issues,
272+
"Filtered out certificate because it has significant issues"
253273
);
254274
return None;
255275
}
256276

257-
not_after
277+
report.not_after
258278
}
259279
Err(error) => {
260-
debug!(%error, "Failed to parse store certificate number {idx}");
280+
debug!(%idx, %error, "Failed to check store certificate");
261281
picky::x509::date::UtcDate::ymd(1900, 1, 1).expect("hardcoded")
262282
}
263283
};
@@ -296,7 +316,9 @@ pub mod windows {
296316
.ok()
297317
.map(|key| (ctx, key))
298318
})
299-
.context("no usable certificate found in the system store")?;
319+
.with_context(|| {
320+
format!("no usable certificate found in the system store; observed issues: {cert_issues}")
321+
})?;
300322

301323
trace!(idx = context.idx, not_after = %context.not_after, key_algorithm_group = ?key.key().algorithm_group(), key_algorithm = ?key.key().algorithm(), "Selected certificate");
302324

@@ -343,6 +365,92 @@ pub mod windows {
343365
}
344366
}
345367

368+
pub struct CertReport {
369+
pub serial_number: String,
370+
pub subject: picky::x509::name::DirectoryName,
371+
pub issuer: picky::x509::name::DirectoryName,
372+
pub not_before: picky::x509::date::UtcDate,
373+
pub not_after: picky::x509::date::UtcDate,
374+
pub issues: CertIssues,
375+
}
376+
377+
bitflags::bitflags! {
378+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
379+
pub struct CertIssues: u8 {
380+
const NOT_YET_VALID = 0b00000001;
381+
const EXPIRED = 0b00000010;
382+
const MISSING_SERVER_AUTH_EXTENDED_KEY_USAGE = 0b00000100;
383+
const MISSING_SUBJECT_ALT_NAME = 0b00001000;
384+
}
385+
}
386+
387+
impl core::fmt::Display for CertIssues {
388+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
389+
bitflags::parser::to_writer(self, f)
390+
}
391+
}
392+
393+
pub fn check_certificate_now(cert: &[u8]) -> anyhow::Result<CertReport> {
394+
check_certificate(cert, time::OffsetDateTime::now_utc())
395+
}
396+
397+
pub fn check_certificate(cert: &[u8], at: time::OffsetDateTime) -> anyhow::Result<CertReport> {
398+
use anyhow::Context as _;
399+
use core::fmt::Write as _;
400+
401+
let cert = picky::x509::Cert::from_der(cert).context("failed to parse certificate")?;
402+
let at = picky::x509::date::UtcDate::from(at);
403+
404+
let mut issues = CertIssues::empty();
405+
406+
let serial_number = cert.serial_number().0.iter().fold(String::new(), |mut acc, byte| {
407+
let _ = write!(acc, "{byte:X?}");
408+
acc
409+
});
410+
let subject = cert.subject_name();
411+
let issuer = cert.issuer_name();
412+
let not_before = cert.valid_not_before();
413+
let not_after = cert.valid_not_after();
414+
415+
if at < not_before {
416+
issues.insert(CertIssues::NOT_YET_VALID);
417+
} else if not_after < at {
418+
issues.insert(CertIssues::EXPIRED);
419+
}
420+
421+
let mut has_server_auth_key_purpose = false;
422+
let mut has_san = false;
423+
424+
for ext in cert.extensions() {
425+
match ext.extn_value() {
426+
picky::x509::extension::ExtensionView::ExtendedKeyUsage(eku)
427+
if eku.contains(picky::oids::kp_server_auth()) =>
428+
{
429+
has_server_auth_key_purpose = true;
430+
}
431+
picky::x509::extension::ExtensionView::SubjectAltName(_) => has_san = true,
432+
_ => {}
433+
}
434+
}
435+
436+
if !has_server_auth_key_purpose {
437+
issues.insert(CertIssues::MISSING_SERVER_AUTH_EXTENDED_KEY_USAGE);
438+
}
439+
440+
if !has_san {
441+
issues.insert(CertIssues::MISSING_SUBJECT_ALT_NAME);
442+
}
443+
444+
Ok(CertReport {
445+
serial_number,
446+
subject,
447+
issuer,
448+
not_before,
449+
not_after,
450+
issues,
451+
})
452+
}
453+
346454
pub mod sanity {
347455
use tokio_rustls::rustls;
348456

0 commit comments

Comments
 (0)