Skip to content

Commit d8b2fdf

Browse files
authored
feat(dgw): improve system store certificate selection (#1341)
The selection is now discriminating based on the extended key usage and the not valid before date. - Discriminate based on the extended key usage: certificate is ignored when the "Server Authentication" (1.3.6.1.5.5.7.3.1) key usage is not specified. - Discriminate based on the "not valid before" date: certificates not yet valid are ignored. - Added generous logging to observe the selection process in details. Issue: DGW-262
1 parent 36b85e6 commit d8b2fdf

3 files changed

Lines changed: 90 additions & 18 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 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 & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ sysinfo = "0.30"
6161
dunce = "1.0"
6262

6363
# Security, crypto…
64-
picky = { version = "7.0.0-rc.10", default-features = false, features = ["jose", "x509", "pkcs12"] }
64+
picky = { version = "7.0.0-rc.10", default-features = false, features = ["jose", "x509", "pkcs12", "time_conversion"] }
6565
zeroize = { version = "1.8", features = ["derive"] }
6666
multibase = "0.9"
6767
argon2 = { version = "0.5", features = ["std"] }

devolutions-gateway/src/tls.rs

Lines changed: 87 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,8 @@ pub mod windows {
144144
}
145145

146146
fn resolve(&self, client_hello: ClientHello<'_>) -> anyhow::Result<Arc<CertifiedKey>> {
147+
use core::fmt::Write as _;
148+
147149
trace!(server_name = ?client_hello.server_name(), "Received ClientHello");
148150

149151
let request_server_name = client_hello
@@ -172,7 +174,7 @@ pub mod windows {
172174

173175
// Look up certificate by subject.
174176
// TODO(perf): the resolution result could probably be cached.
175-
let mut contexts = store.find_by_subject_str(&self.subject_name).with_context(|| {
177+
let contexts = store.find_by_subject_str(&self.subject_name).with_context(|| {
176178
format!(
177179
"failed to find server certificate for {} from system store",
178180
self.subject_name
@@ -187,45 +189,113 @@ pub mod windows {
187189

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

190-
// Sort certificates from the furthest to the earliest expiration
192+
let now = picky::x509::date::UtcDate::now();
193+
194+
// Initial processing and filtering of the available candidates.
195+
let mut contexts: Vec<CertHandleCtx> = contexts
196+
.into_iter()
197+
.enumerate()
198+
.filter_map(|(idx, ctx)| {
199+
let not_after = match picky::x509::Cert::from_der(ctx.as_der()) {
200+
Ok(cert) => {
201+
let serial_number = cert.serial_number().0.iter().fold(
202+
String::new(),
203+
|mut acc, byte| {
204+
let _ = write!(acc, "{byte:X?}");
205+
acc
206+
},
207+
);
208+
let subject = cert.subject_name();
209+
let issuer = cert.issuer_name();
210+
let not_before = cert.valid_not_before();
211+
let not_after = cert.valid_not_after();
212+
213+
trace!(%idx, %serial_number, %subject, %issuer, %not_before, %not_after, "Parsed store certificate");
214+
215+
if now < not_before {
216+
debug!(
217+
%idx, %serial_number, %not_before, "Filtered out certificate based on not before validity date"
218+
);
219+
return None;
220+
}
221+
222+
let has_server_auth_key_purpose = cert.extensions().iter().any(|ext| match ext.extn_value() {
223+
picky::x509::extension::ExtensionView::ExtendedKeyUsage(eku) => eku.contains(picky::oids::kp_server_auth()),
224+
_ => false,
225+
});
226+
227+
if !has_server_auth_key_purpose {
228+
debug!(
229+
%idx, %serial_number, "Filtered out certificate because it does not have the server auth extended usage"
230+
);
231+
return None;
232+
}
233+
234+
not_after
235+
}
236+
Err(error) => {
237+
debug!(%error, "Failed to parse store certificate number {idx}");
238+
picky::x509::date::UtcDate::ymd(1900, 1, 1).expect("hardcoded")
239+
}
240+
};
241+
242+
Some(CertHandleCtx {
243+
idx,
244+
handle: ctx,
245+
not_after,
246+
})
247+
})
248+
.collect();
249+
250+
// Sort certificates from the farthest to the earliest expiration
191251
// time. Note that it appears the certificates are already returned
192252
// in this order, but it is not a documented behavior. It really
193253
// depends on the internal order maintained by the store, and there
194254
// is no guarantee about what this order is, thus we implement the
195255
// logic here anyway.
196-
contexts.sort_by_cached_key(|ctx| match picky::x509::Cert::from_der(ctx.as_der()) {
197-
Ok(cert) => cert.valid_not_after(),
198-
Err(error) => {
199-
warn!(%error, "Failed to parse store certificate");
200-
picky::x509::date::UtcDate::ymd(1900, 1, 1).expect("hardcoded")
201-
}
202-
});
256+
contexts.sort_by(|a, b| b.not_after.cmp(&a.not_after));
257+
258+
if enabled!(tracing::Level::TRACE) {
259+
contexts.iter().enumerate().for_each(|(sorted_idx, ctx)| trace!(%sorted_idx, idx = %ctx.idx, not_after = %ctx.not_after, "Sorted certificate"));
260+
}
203261

204262
// Attempt to acquire a private key and construct CngSigningKey.
205263
let (context, key) = contexts
206264
.into_iter()
207-
.rev() // Revert for: closer to farthest -> farthest to closer
208265
.find_map(|ctx| {
209-
let key = ctx.acquire_key().ok()?;
210-
CngSigningKey::new(key).ok().map(|key| (ctx, key))
266+
let key = ctx
267+
.handle
268+
.acquire_key()
269+
.inspect_err(|error| debug!(idx = %ctx.idx, %error, "Failed to acquire key for certificate"))
270+
.ok()?;
271+
CngSigningKey::new(key)
272+
.inspect_err(|error| debug!(idx = %ctx.idx, %error, "CngSigningKey::new failed"))
273+
.ok()
274+
.map(|key| (ctx, key))
211275
})
212-
.context("failed to aquire private key for certificate")?;
276+
.context("no usable certificate found in the system store")?;
213277

214-
trace!(key_algorithm_group = ?key.key().algorithm_group());
215-
trace!(key_algorithm = ?key.key().algorithm());
278+
trace!(idx = context.idx, not_after = %context.not_after, key_algorithm_group = ?key.key().algorithm_group(), key_algorithm = ?key.key().algorithm(), "Selected certificate");
216279

217280
// Attempt to acquire a full certificate chain.
218281
let chain = context
282+
.handle
219283
.as_chain_der()
220284
.context("certification chain is not available for this certificate")?;
221285
let certs = chain.into_iter().map(CertificateDer::from).collect();
222286

223287
// Return CertifiedKey instance.
224-
Ok(Arc::new(CertifiedKey {
288+
return Ok(Arc::new(CertifiedKey {
225289
cert: certs,
226290
key: Arc::new(key),
227291
ocsp: None,
228-
}))
292+
}));
293+
294+
struct CertHandleCtx {
295+
idx: usize,
296+
handle: rustls_cng::cert::CertContext,
297+
not_after: picky::x509::date::UtcDate,
298+
}
229299
}
230300
}
231301

0 commit comments

Comments
 (0)