KDB-155: fix: cross-platform NTLM + Windows root store for vendored-openssl - #1
Conversation
The winauth crate's NtlmV2Client is pure Rust (its own MD4/HMAC-MD5/RC4, no SSPI) and works on every platform; only its 'windows' SSPI module is Windows-specific. Lift the cfg(windows) gates so explicit-credential Windows authentication (DOMAIN\user + password over NTLMv2) is available from Linux and macOS clients, matching what FreeTDS, jTDS, and pytds have long supported. - Cargo.toml: winauth becomes a cross-platform optional dependency - AuthMethod::Windows / WindowsAuth / AuthMethod::windows(): gate on feature = "winauth" alone - login helpers (Context::spn, LoginMessage::integrated_security, TokenSspi::new, TokenStream/Connection::flush_sspi, NextBytes import): widened to include feature = "winauth" - AuthMethod::Integrated is unchanged: SSPI on Windows, GSSAPI on Unix Based on v0.12.3.
The v0.12.3 tag was cut without the version bump; crates.io has 0.12.3. Matching it lets [patch.crates-io] consumers replace the registry crate.
The vendored OpenSSL discovers roots via openssl-probe, which only checks Unix filesystem paths — on Windows it finds nothing, so TrustConfig::Default validates against an empty trust store and every strict-validation handshake fails with 'unable to get local issuer certificate' (e.g. Azure SQL with AAD token auth). Load the Windows ROOT certificate store (current-user view, a composite that includes the local-machine store) into the connector via schannel, the same crate rustls-native-certs uses for this. Scoped to cfg(windows) + the vendored-openssl feature + the Default trust branch; TrustAll, CaCertificateLocation, other TLS backends, and non-Windows targets are unchanged.
|
Reviewed. No blockers. Merging this into Worth stating what is and isn't new product risk here, because it's less than the diff size suggests. Two of the three commits are already in production
The base change to
|
| Commit | Assessment |
|---|---|
c34fab2e Bump v0.12.3 |
Matches the version this PR sets. No conflict. |
51f0cbb3 retroactive v0.11.7/v0.11.8 |
CHANGELOG only. |
406ad278 vec reallocations (tiberius-rs#370) |
Purely TokenRow::new() to TokenRow::with_capacity(n) in into_row.rs. Vec pre-allocation on the parameter-encoding path, semantically identical. No risk. |
59db5796 libgssapi 0.8.1 (tiberius-rs#372) |
Touches connection.rs, but entirely inside cfg(all(unix, feature = "integrated-auth-gssapi")). KeeperDB does not enable that feature, so none of it compiles for us. Inert. |
a5dffa0d remove SECURITY.md |
No product code. |
a6b4fcda CI security workflow |
No product code. |
So the upstream drift is benign. Its only overlap with this PR is connection.rs, and the two touch disjoint regions, which is consistent with GitHub reporting the PR as mergeable.
cfg-widening correctness
This is the class of bug I was actually looking for, since asymmetric cfg gates break exactly one feature combination and nothing else:
AuthMethod::Windowswidening does not create a duplicate match arm on Windows.AuthMethod::Integrated(line 318, the SSPI path) andAuthMethod::Windows(line 387, explicit-cred NTLM) are distinct variants, socfg(all(windows, feature = "winauth"))andcfg(feature = "winauth")coexist cleanly. This was my main compile-break worry and it's fine.NtlmSspiBuildercorrectly stayscfg(all(windows, feature = "winauth"))whileNextByteswidens tocfg(feature = "winauth"). Both are used on Windows, neither goes unused.flush_sspihas a single positively-gated definition with no pairednot(any(...))to fall out of sync. The nearbynot(any(...))is forpost_login_encryptionand the TLS backends, unrelated.
The root store fix
- Scoped to
cfg(windows)+vendored-openssl+TrustConfig::Defaultonly, soTrustAll(SQL auth, NTLM, RDS) is untouched and macOS/Linux compile to identical code. That is why this supersedes the native-tls approach, and why the 6x macOS RDS trust-cert re-run I set as a gate on keeperdb#305 is no longer required. schannelis MIT, verified againststeffengy/schannel-rsand the registry, so it clears our dependency license rule.- Skipping unparseable certs with a WARN rather than failing the connection matches
rustls-native-certs. Degrading to an empty trust store when the store can't be opened preserves today's behavior. Both are the right defaults.
Three asks, none blocking
- Merge with a merge commit or rebase, not squash. Squashing collapses
40c5e67ainto a single commit under one author and drops Craig's authorship of the NTLM work. open_current_user("ROOT")deserves a comment. It's correct for what we ship on Windows (the desktop MSI), and the current-user view is a composite that picks up machine and GP-pushed roots such as Zscaler. It would be the wrong store for a Windows service build, which would silently get no roots. Cheap to note now while the reasoning is fresh.- I cannot verify the Windows compile from here. Your
x86_64-pc-windows-msvccross-check is the only evidence on that path, and the real gate stays the KeeperDB Windows desktop build reaching Azure SQL over Entra.
After this merges
keeperdb#305 should become a pin bump only: keep the release dependency line as-is (default-features = false, tds73, vendored-openssl, winauth) and point [patch.crates-io] at the merge commit on main. The comment above that patch entry needs updating too, since it currently explains only the cfg(windows) NTLM lift and cites tiberius-rs#408. It should also mention the Windows root store and that the fork branch is now main.
… services
The current-user ROOT store is correct for user-session processes
(desktop app / MSI). A Windows service running in session 0 would
need open_local_machine("ROOT") instead. Add a comment while the
reasoning is fresh.
Summary
cfg(windows), but the underlyingwinauthNTLMv2 client is pure Rust. This lifts thecfg(windows)requirement tocfg(feature = "winauth")soDOMAIN\userlogins work from Linux/macOS servers. The Windows-native SSPI path (NtlmSspiBuilder) stays gated tocfg(all(windows, ...)).openssl-probeonly checks Unix filesystem paths for CA bundles — on Windows the trust anchors live in registry-backed certificate stores, so the probe finds nothing andTrustConfig::Defaultvalidates against an empty trust store. This loads the Windows ROOT certificate store (current-user composite view) into the OpenSSL connector via theschannelcrate, scoped tocfg(windows)+vendored-opensslfeature +TrustConfig::Defaultonly. Unparseable certs are skipped with a warning; a failure to open the store degrades to existing behavior (empty trust store).Motivation
KeeperDB uses tiberius with
vendored-openssl+winauthfor its MSSQL driver. Two problems:Changes
Cargo.tomlwinauthmoved out ofcfg(windows)deps;schanneladded as optionalcfg(windows)dep wired intovendored-openssl; version bumped to 0.12.3src/client/auth.rscfg(all(windows, feature = "winauth"))→cfg(feature = "winauth")onWindowsAuth,Debugimpl,AuthMethod::Windowssrc/client/connection.rsTokenSspiimport +flush_sspi+Windowsmatch arm cfg gates widened;NextBytesimport separated from Windows-onlyNtlmSspiBuildersrc/tds/codec/login.rsintegrated_security()cfg addsfeature = "winauth"src/tds/codec/token/token_sspi.rsTokenSspi::new()cfg addsfeature = "winauth"src/tds/context.rsspn()cfg addsfeature = "winauth"src/tds/stream/token.rsflush_sspi()cfg addsfeature = "winauth"src/client/tls_stream/opentls_tls_stream.rsTrustConfig::DefaultbranchBlast radius
#[cfg(windows)]on root store codevendored-opensslfeaturenative-tlsandrustlsbackends untouchedTrustConfig::Defaultmatch arm onlyTrustAllandCaCertificateLocationpaths untouchedcfg(feature = "winauth")on NTLMTest plan
cargo check --no-default-features --features tds73,winauth,vendored-openssl,chrono,rust_decimalon macOS — compiles; Windows block cfg'd outschannelusage cross-checked againstx86_64-pc-windows-msvctarget — compiles