Skip to content

Pin on certificate SPKI across the full chain, with CustomRootTrust on .NET 5+ - #48

Open
cisco-dmytro-hissa wants to merge 4 commits into
masterfrom
custom-trust-store-pinning
Open

Pin on certificate SPKI across the full chain, with CustomRootTrust on .NET 5+#48
cisco-dmytro-hissa wants to merge 4 commits into
masterfrom
custom-trust-store-pinning

Conversation

@cisco-dmytro-hissa

@cisco-dmytro-hissa cisco-dmytro-hissa commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Description

Reworks CA pinning to match the certificate chains that Windows and Linux actually build (ZTCAPIN-558).

  • Pin on SPKI, not certificate DER. Pinning now compares the SHA-256 of each certificate's SubjectPublicKeyInfo, so the self-signed and cross-signed forms of the same CA both match. Replaces the old root-only Contains() DER comparison, which rejected valid cross-signed chains.
  • Walk the whole chain instead of only the root, since the two platforms place the pinned CA at different positions.
  • New SpkiPinning.cs — hand-rolled DER walk to extract the SPKI, because PublicKey.ExportSubjectPublicKeyInfo() is .NET 6+ only and we still ship net48.
  • .NET 5+ additionally re-validates against a CustomTrustStore (TrustMode = CustomRootTrust), so a pinned certificate must be able to anchor the chain, not merely appear in it. Consequence: pinning an intermediate via GetCustomRootCertificatesPinner is accepted on net48 and rejected on net8.0. Documented on that method; the bundled ca_certs.pem is roots-only, so it behaves identically on both.
  • PEM loading is now explicit (-----BEGIN/END CERTIFICATE----- + base64) rather than relying on framework-specific behavior of the X509Certificate(byte[]) constructor.
  • Fixed a NullReferenceException in HasProxyServer on .NET Core/5+. It reflected over .NET Framework's private webProxy field and dereferenced the null result; now falls back to IWebProxy.IsBypassed/GetProxy. This was 17 test failures on net8.0.
  • Build conversion: all three projects are SDK-style multi-targeting net48;net8.0, packages.config removed, CI switched to dotnet restore/build/test.

How Has This Been Tested?

  • TestComputeSpkiSha256MatchesOpenSsl — SPKI hashes checked against openssl-computed values for the Duo root, intermediate, and a Microsoft root.
  • TestAllBundledCertsHashable — every cert in ca_certs.pem parses through the DER walk; TestEmptyDataThrows covers malformed input.
  • TestPinnedRootAcceptedOnAllFrameworks / TestPinnedIntermediateIsAcceptedOnlyWhereItCanAnchor — pin the anchor contract per TFM.
  • TestUnrelatedPinnedKeyRejected, TestChainErrorRejected, TestSpkiHashIsDeterministic.
  • Fixed TestServer binding to a hardcoded port 8080. dotnet test runs the net48 and net8.0 hosts concurrently, so the second one died with HttpListenerException (183). It now takes an OS-assigned ephemeral port, and listener/write errors no longer take down the test host.

Types of Changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

@cisco-dmytro-hissa
cisco-dmytro-hissa marked this pull request as ready for review August 11, 2026 16:21
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.

1 participant