You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
CertificateAuthentication: CustomTrustStore/CustomRootTrust is not applied to self-signed client certificates #69763
CertificateAuthenticationHandler applies the configured custom trust (CustomTrustStore together with ChainTrustValidationMode = CustomRootTrust) only to certificates it classifies as chained. Certificates classified as self-signed are validated under a separate policy that never consults that configuration. When AllowedCertificateTypes includes SelfSigned, the trust options therefore do not describe a single, uniform trust boundary, and nothing tells the operator.
What is wrong
Broken invariant: when an app explicitly selects custom root trust and supplies a populated store, that store should be the set of roots the handler trusts for every certificate type it accepts. Today it applies to one type only.
Origin: the self-signed branch in BuildChainPolicy predates CustomTrustStore. The store and trust mode were added later (Allow opt-out from client certificate root checking #15029) to support chained certificates issued by a private CA, and the self-signed branch was not revisited. No test covers the combination of a self-signed certificate with CustomRootTrust.
Silent: the combination is accepted without a log message or validation error.
Classifier: the self-signed/chained decision is made by IsSelfSigned(), which compares subject and issuer names. It selects which validation policy runs, so the fix should not depend on its precision.
Why it matters (defense in depth)
Correctness and least surprise: the platform documents X509ChainPolicy.CustomTrustStore as "a collection of certificates replacing the default certificate trust". The handler's XML docs describe CustomTrustStore as "trusted components of the certificate chain" used in chain validation. Neither says self-signed certificates are exempt, so a reasonable reader expects the store to apply.
Hardening rationale: the documentation already requires apps that accept self-signed certificates to pin them (thumbprint or issuer) in their own validation, see https://learn.microsoft.com/aspnet/core/security/authentication/certauth. Making the framework honor an explicitly configured trust store for self-signed certificates adds a second layer that does not rely on that app-side step being present.
Scope: non-default configuration only. The default is CertificateTypes.Chained with system trust.
src/Security/Authentication/Certificate/src/CertificateAuthenticationHandler.cs:228-233 - self-signed branch: sets AllowUnknownCertificateAuthority and IgnoreEndRevocationUnknown, does not apply CustomTrustStore or TrustMode
src/Security/Authentication/Certificate/src/CertificateAuthenticationHandler.cs:234-242 - chained branch: the only place CustomTrustStore and ChainTrustValidationMode are applied
src/Security/Authentication/Certificate/src/CertificateAuthenticationHandler.cs:118-146 - ValidateCertificateAsync: classification and chain.Build
Deliver in two tiers. Tier 1 is non-breaking and can ship first, including to servicing branches. Tier 2 changes behavior and needs an owner decision on compatibility.
Tier 1: documentation, signal, test coverage (non-breaking)
Docs: in the AllowedCertificateTypes and CustomTrustStore sections of the certificate authentication article, state that self-signed certificates are not evaluated against CustomTrustStore/CustomRootTrust and must be pinned by thumbprint or public key in OnCertificateValidated or an authorization policy. Move the pinning guidance out of the sample walkthrough into those sections. Mirror the note in the XML docs on AllowedCertificateTypes, CustomTrustStore, ChainTrustValidationMode, and CertificateTypes.SelfSigned.
Startup signal: log one warning per scheme, at options configuration time, when AllowedCertificateTypes includes SelfSigned and ChainTrustValidationMode is CustomRootTrust with a non-empty CustomTrustStore. Do not throw; the combination is legitimate for apps that accept private-CA chains and pin self-signed certificates themselves. Do not log per request.
Tests: add tests that pin current behavior for a self-signed certificate under CustomRootTrust so any later change is deliberate.
When useCustomRoot is true, apply CustomTrustStore and TrustMode to every certificate, and do not set AllowUnknownCertificateAuthority for self-signed certificates. A self-signed certificate then validates only if it is present in the store.
When useCustomRoot is false, keep today's behavior unchanged.
Keep the existing self-signed handling for revocation (NoCheck, IgnoreEndRevocationUnknown), the ExtraStore additions, the client-auth EKU check, and the validity-period check.
The non-empty guard is required: with an empty store and CustomRootTrust, every self-signed certificate would fail.
Platform validation (Windows, .NET 10):
With CustomRootTrust, a populated store, and AllowUnknownCertificateAuthority not set, a self-signed certificate validates only when it is in the store and otherwise fails with UntrustedRoot.
If AllowUnknownCertificateAuthority stays set, the store has no effect on a self-signed certificate, so the flag must be dropped in this mode.
Linux and macOS chain engines still need to be checked.
Independent of IsSelfSigned: in useCustomRoot mode, trust no longer depends on the classification, so tightening the classifier is optional and separate.
Alternatives considered
Throw at startup for the combination: rejected, it breaks legitimate setups that pin self-signed certificates in app code.
Tighten IsSelfSigned only: rejected, it does not change trust semantics for a valid self-signed certificate.
Always apply trust to self-signed certificates, or drop AllowUnknownCertificateAuthority unconditionally: rejected, it breaks all self-signed users on system trust.
Dedicated new option for self-signed trust: viable, but it is new public API (API review required) and heavier than reusing the existing options.
Docs only: insufficient on its own because the failure mode is silent.
Compatibility and versioning
Tier 2 breaks apps that use CustomRootTrust with a populated store and AllowedCertificateTypes including SelfSigned and rely on self-signed certificates outside the store being accepted today.
Owner decision needed: (a) opt-in first through an AppContext switch or a new option (a new public option requires the API review process and a PublicAPI.Unshipped.txt entry), then make it the default in the next major with a breaking-change article; or (b) make it the default in the next major only.
Servicing branches: Tier 1 only.
Acceptance criteria
Tier 1 docs and XML docs state that self-signed certificates are not evaluated against CustomTrustStore/CustomRootTrust and need app-side pinning.
A single warning per scheme is logged for the self-signed plus populated-custom-root combination, and nothing is logged for other combinations.
Tests cover self-signed certificates under CustomRootTrust (certificate in store, certificate not in store, empty store) and the unchanged behavior under system trust.
If Tier 2 is accepted: with useCustomRoot, a self-signed certificate outside the store is rejected and one inside the store is accepted; behavior without useCustomRoot is unchanged.
Tier 2 behavior is verified on Windows, Linux, and macOS.
The compatibility decision, any API review for a new option, and the breaking-change documentation are recorded before Tier 2 ships.
Area:area-auth (CertificateAuthenticationHandler in src/Security/Authentication/Certificate) Type:Bug (CustomTrustStore/CustomRootTrust not applied to self-signed certificates)
Hey! I'd like to take a look at this issue. Would it be okay for me to work on it? I was thinking of starting by reproducing the current behavior and adding regression tests, then discussing the compatibility implications before making any behavior changes. Let me know if that sounds like a good approach.
Summary
CertificateAuthenticationHandlerapplies the configured custom trust (CustomTrustStoretogether withChainTrustValidationMode = CustomRootTrust) only to certificates it classifies as chained. Certificates classified as self-signed are validated under a separate policy that never consults that configuration. WhenAllowedCertificateTypesincludesSelfSigned, the trust options therefore do not describe a single, uniform trust boundary, and nothing tells the operator.What is wrong
BuildChainPolicypredatesCustomTrustStore. The store and trust mode were added later (Allow opt-out from client certificate root checking #15029) to support chained certificates issued by a private CA, and the self-signed branch was not revisited. No test covers the combination of a self-signed certificate withCustomRootTrust.IsSelfSigned(), which compares subject and issuer names. It selects which validation policy runs, so the fix should not depend on its precision.Why it matters (defense in depth)
X509ChainPolicy.CustomTrustStoreas "a collection of certificates replacing the default certificate trust". The handler's XML docs describeCustomTrustStoreas "trusted components of the certificate chain" used in chain validation. Neither says self-signed certificates are exempt, so a reasonable reader expects the store to apply.CertificateTypes.Chainedwith system trust.Affected code
BuildChainPolicyAllowUnknownCertificateAuthorityandIgnoreEndRevocationUnknown, does not applyCustomTrustStoreorTrustModeCustomTrustStoreandChainTrustValidationModeare appliedValidateCertificateAsync: classification andchain.BuildIsSelfSigned(name comparison)AllowedCertificateTypesdocsCustomTrustStoredocsChainTrustValidationModedocsSelfSignedandAlldocsRecommended fix
Deliver in two tiers. Tier 1 is non-breaking and can ship first, including to servicing branches. Tier 2 changes behavior and needs an owner decision on compatibility.
Tier 1: documentation, signal, test coverage (non-breaking)
AllowedCertificateTypesandCustomTrustStoresections of the certificate authentication article, state that self-signed certificates are not evaluated againstCustomTrustStore/CustomRootTrustand must be pinned by thumbprint or public key inOnCertificateValidatedor an authorization policy. Move the pinning guidance out of the sample walkthrough into those sections. Mirror the note in the XML docs onAllowedCertificateTypes,CustomTrustStore,ChainTrustValidationMode, andCertificateTypes.SelfSigned.AllowedCertificateTypesincludesSelfSignedandChainTrustValidationModeisCustomRootTrustwith a non-emptyCustomTrustStore. Do not throw; the combination is legitimate for apps that accept private-CA chains and pin self-signed certificates themselves. Do not log per request.CustomRootTrustso any later change is deliberate.Tier 2: apply custom root trust to self-signed certificates (behavior change)
useCustomRoot = ChainTrustValidationMode == CustomRootTrust && CustomTrustStore.Count > 0.useCustomRootis true, applyCustomTrustStoreandTrustModeto every certificate, and do not setAllowUnknownCertificateAuthorityfor self-signed certificates. A self-signed certificate then validates only if it is present in the store.useCustomRootis false, keep today's behavior unchanged.NoCheck,IgnoreEndRevocationUnknown), theExtraStoreadditions, the client-auth EKU check, and the validity-period check.CustomRootTrust, every self-signed certificate would fail.CustomRootTrust, a populated store, andAllowUnknownCertificateAuthoritynot set, a self-signed certificate validates only when it is in the store and otherwise fails withUntrustedRoot.AllowUnknownCertificateAuthoritystays set, the store has no effect on a self-signed certificate, so the flag must be dropped in this mode.IsSelfSigned: inuseCustomRootmode, trust no longer depends on the classification, so tightening the classifier is optional and separate.Alternatives considered
IsSelfSignedonly: rejected, it does not change trust semantics for a valid self-signed certificate.AllowUnknownCertificateAuthorityunconditionally: rejected, it breaks all self-signed users on system trust.Compatibility and versioning
CustomRootTrustwith a populated store andAllowedCertificateTypesincludingSelfSignedand rely on self-signed certificates outside the store being accepted today.AppContextswitch or a new option (a new public option requires the API review process and aPublicAPI.Unshipped.txtentry), then make it the default in the next major with a breaking-change article; or (b) make it the default in the next major only.Acceptance criteria
CustomTrustStore/CustomRootTrustand need app-side pinning.CustomRootTrust(certificate in store, certificate not in store, empty store) and the unchanged behavior under system trust.useCustomRoot, a self-signed certificate outside the store is rejected and one inside the store is accepted; behavior withoutuseCustomRootis unchanged.