Skip to content

windows: add Verifier::offline() constructor#238

Open
djc wants to merge 9 commits into
mainfrom
windows-offline
Open

windows: add Verifier::offline() constructor#238
djc wants to merge 9 commits into
mainfrom
windows-offline

Conversation

@djc

@djc djc commented Jul 8, 2026

Copy link
Copy Markdown
Member

Does a bunch of cleanup/refactoring first, and tightens the timeout from 10 to 3s.

Fixes #237.

@djc
djc requested review from complexspaces and ctz July 8, 2026 13:01
@djc
djc force-pushed the windows-offline branch from 95e689b to f4715a7 Compare July 8, 2026 15:15
@lpapp

lpapp commented Jul 14, 2026

Copy link
Copy Markdown

Thanks for the quick response on this, @djc!

The Verifier::offline() constructor gets us most of the way there, but I think it is more restrictive than necessary. CERT_CHAIN_CACHE_ONLY_URL_RETRIEVAL disables all network fetches - including AIA (Authority Information Access) chasing for intermediate certificates, not just revocation.

For comparison, Go's crypto/x509 on Windows calls CertGetCertificateChain with no CERT_CHAIN_REVOCATION_CHECK_* flags and no CERT_CHAIN_CACHE_ONLY_URL_RETRIEVAL. This gives you:

  • AIA fetching (intermediates downloaded if the server does not send the full chain)
  • No revocation network calls
  • Platform trust store (no need to supply roots manually)

Would you consider a mode that simply omits CERT_CHAIN_REVOCATION_CHECK_END_CERT from the flags instead of using CERT_CHAIN_CACHE_ONLY_URL_RETRIEVAL? That would be the more surgical fix - it eliminates the revocation timeout without breaking AIA or requiring the caller to supply their own root certificates.

As it stands, offline() requires the caller to pass in roots explicitly, which removes one of the main benefits of using rustls-platform-verifier over webpki-roots in the first place (automatic use of the system trust store).

@djc

djc commented Jul 14, 2026

Copy link
Copy Markdown
Member Author

Do you want to send a PR against my branch that better fits your requirements? We should potentially rethink the offline() name to better match the semantics of what you're trying to achieve, too.

@lpapp

lpapp commented Jul 14, 2026

Copy link
Copy Markdown

I am fine with this PR. We can revisit with more granular control in the future if needed. Appreciate the quick turnaround. Do you have a sense of when this might be merged and released?

@djc

djc commented Jul 14, 2026

Copy link
Copy Markdown
Member Author

I am fine with this PR. We can revisit with more granular control in the future if needed. Appreciate the quick turnaround. Do you have a sense of when this might be merged and released?

Reviews in this repo have been a little slow recently. @complexspaces will you have time to review this in the next, say, two weeks or so?

@complexspaces

Copy link
Copy Markdown
Collaborator

Yeah I will make time this week 👍. Thanks for putting some refactoring work into this as well.

I wanted to take a second look at the flags being used here since I was confused by this comment in the linked issue:

Remove CERT_CHAIN_REVOCATION_CHECK_END_CERT from the flags, since the result is discarded by the policy step anyway. This eliminates the network call with no change in security posture.

@lpapp

lpapp commented Jul 17, 2026

Copy link
Copy Markdown

Hi @complexspaces, just a friendly nudge - since today is the last working day of the week, would you have a chance to take a look at this? A release with this fix would unblock some downstream work. No pressure of course, and thanks again for taking the time to review!

let mut new: Self = unsafe { mem::zeroed() };
new.cbSize = Self::SIZE;
new
/// Creates a Windows TLS certificate verifier that avoids using online revocation checking.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect an offline constructor to avoid any/all network fetches, not just revocation checks. If the goal is just to avoid OCSP/CRL fetching, then a different name should be used. Regardless, I think it is also useful to state more explicitly what this does for AIA fetching.

However, IDK if the Windows certificate verification API is practically usable in a purely offline mode, in general, since Windows usually fetches the trust anchors themselves dynamically, IIRC.

@lpapp lpapp Jul 17, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, IDK if the Windows certificate verification API is practically usable in a purely offline mode, in general, since Windows usually fetches the trust anchors themselves dynamically, IIRC.

For environments where trust anchors are provisioned into the local cert store, offline mode works fine.

@briansmith briansmith Jul 17, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For environments where trust anchors are provisioned into the local cert store, offline mode works fine.

That's a minority of systems, though, isn't it? My understanding is that on most systems, the root store starts almost empty OOB.

@lpapp lpapp Jul 17, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not have data on what percentage of Windows systems rely on dynamic root fetching vs. pre-provisioned stores. This is an argument for making the behavior configurable rather than a single offline() toggle. If no revocation (omit CERT_CHAIN_REVOCATION_CHECK_END_CERT, leave everything else alone) - eliminates revocation network calls while preserving AIA and dynamic root fetching. This matches Go's crypto/x509 behavior on Windows.

That said, if splitting these into separate modes is a significant effort from the author, I think the current PR is still a clear improvement over the status quo - it enables flows where both revocation and AIA are unnecessary, which is a real use case today.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@briansmith: However, IDK if the Windows certificate verification API is practically usable in a purely offline mode, in general, since Windows usually fetches the trust anchors themselves dynamically, IIRC.

It is hard for me to tell exactly how much AIA is needed (especially with MS's own certificate fetching logic in Windows Update) but from what I can see on my own Windows systems I would lean towards scoping this change to just disallow networking for revocation specifically.

To provide an example: On my Windows 10 system, I only have LetsEncrypt's X1 root by default. X2 is not in any machine/user stores. In that state, I tested a few different flag configurations of CertGetCertificateChain (which is the function that actually fetches certificates that aren't present) when no intermediates from rustls get passed to Windows.

I used reqwest to fetch https://letsencrypt.org which uses a certificate from their X2 root. I also added CERT_CHAIN_CACHE_ONLY_URL_RETRIEVAL to every call of CertGetCertificateChain as a baseline then changed the below to see the different behaviors:

CERT_CHAIN_DISABLE_AIA CERT_CHAIN_DISABLE_AUTH_ROOT_AUTO_UPDATE Result
No No Root is added to Intermediate Certification Authorities store
No Yes Root is added to Intermediate Certification Authorities store by AIA
Yes No Root is added to Third-Party Root Certification Authorities store by Windows Update(?)
Yes Yes Fails to verify certificate due to unknown issuer

Similar applies to the YE intermediate root but I didn't spend the time cleaning that one up too between runs, just X2.

Given that there is a complete failure in a very common case, I think that its safe to say that an entirely offline Windows verifier is not a useful tool to expose from this crate. It won't be able to load a vast majority of sites on the internet.

Maybe it would be best to rename this to cached_revocation or offline_revocation?

@lpapp: For environments where trust anchors are provisioned into the local cert store, offline mode works fine.

I would be surprised if your provisioning process is set up in such a way that machines are always given a fresh copy of the Windows CAB containing all known trusted roots on a regular basis.

I've also ran into a case at work where someone wasn't properly configuring their VDI infrastructure with root certificates so everything 1Password tried to do online failed. I air on the side of assuming the machine won't arrive in an offline-usable state for third-party desktop apps.

@lpapp lpapp Jul 20, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would be surprised if your provisioning process is set up in such a way that machines are always given a fresh copy of the Windows CAB containing all known trusted roots on a regular basis.

This setup does happen.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This setup does happen.

I think it is useful to have a pure no-networking configuration. But, I think that the goal is to fix #237, which requires a different solution and different naming.

@lpapp lpapp Jul 21, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To clarify my use case from #237 - I need to disable revocation checks specifically. Whether AIA remains enabled or disabled is not a concern for my scenario, so either the current offline() approach or a revocation-only toggle would resolve the issue for me. The current one week delay is more concerning to me because this is a high-priority blocker for my project, so I am now also evaluating rustls alternatives.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lpapp Being annoying is unlikely to speed up the process.

@complexspaces

Copy link
Copy Markdown
Collaborator

@lpapp Yup, I am reviewing this today. I'm currently doing some local testing as well on Windows.

@@ -466,7 +466,7 @@ impl CertificateStore {
| CERT_CHAIN_CACHE_END_CERT;

// Lowering URL retrieval timeout from default 15s to 10s to account for higher internet speeds

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: We can either update this comment or just delete it outright now.


#[cfg(any(test, feature = "ffi-testing", feature = "dbg"))]
fn new_with_fake_root(root: &[u8]) -> Result<Self, TlsError> {
fn new_with_fake_root(root: pki_types::CertificateDer<'static>) -> Result<Self, TlsError> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason this doesn't take a reference to CertificateDer if it only needs that? It would avoid the .clone() noise.

impl CertificateStore {
#[cfg(any(test, feature = "ffi-testing", feature = "dbg"))]
fn new_with_fake_root(root: &[u8]) -> Result<Self, TlsError> {
fn new_with_fake_root(root: pki_types::CertificateDer<'static>) -> Result<Self, TlsError> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto here.


/// Creates a Windows TLS certificate verifier that avoids using online revocation checking.
#[cfg_attr(docsrs, doc(cfg(all())))]
pub fn offline(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about making this switchable with a flag setter instead of a whole new constructor? I think its makes the API more confusing exposing the roots parameter outside of the new_with_extra_roots constructor, in addition to a whole new constructor option.

The downside of doing it that way would be that the set_offline_mode(&mut self, offline: bool) function would need to call CertCreateCertificateChainEngine again and replace the existing one. But everywhere else we could more lazily obtain the flag value and not need any more Win32 calls to accommodate.

#[cfg(any(test, feature = "ffi-testing", feature = "dbg"))]
test_only_root_ca_override: None,
crypto_provider,
extra_roots: Some(CertEngine::new(roots, CERT_CHAIN_CACHE_ONLY_URL_RETRIEVAL)?),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its probably best to keep the discussion in #238 (comment), but per my findings there setting this isn't actually enough to stop Windows from doing all networking during the verification calls FYI.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Windows: revocation check blocks up to 10s with no effect on verification outcome

4 participants