Skip to content

[Android] Respect platform trust manager in SslStream#124173

Open
simonrozsival wants to merge 14 commits intodotnet:mainfrom
simonrozsival:feature/android-platform-trust-manager-107695
Open

[Android] Respect platform trust manager in SslStream#124173
simonrozsival wants to merge 14 commits intodotnet:mainfrom
simonrozsival:feature/android-platform-trust-manager-107695

Conversation

@simonrozsival
Copy link
Member

@simonrozsival simonrozsival commented Feb 9, 2026

Problem

On Android, SslStream bypassed the platform's trust infrastructure entirely. The DotnetProxyTrustManager did not consult Android's X509TrustManager, so network_security_config.xml — used for certificate pinning and custom trust anchors — was ignored. RemoteCertificateValidationCallback always received SslPolicyErrors.None even when the platform would have rejected the certificate chain (e.g. due to a pin mismatch).

This is the exact scenario reported in #107695: an app configures SSL pinning via network_security_config.xml with an intentionally wrong pin, but SslPolicyErrors.None is still reported.

Fix

This PR wraps the platform's X509TrustManager (obtained via TrustManagerFactory) inside DotnetProxyTrustManager. During TLS handshakes, the platform's trust infrastructure — including network_security_config.xml — is now consulted. The platform's verdict is combined with managed (.NET) validation to be more strict, never less:

  • Platform rejectssslPolicyErrors is pre-seeded with RemoteCertificateChainErrors. Managed validation cannot clear this flag.
  • Platform accepts → managed validation (X509Chain.Build) still runs independently and can add its own errors.
  • RemoteCertificateValidationCallback always receives the union of both assessments.

This is consistent with iOS behavior, where the platform trust manager is always consulted.

What's tested

The AndroidPlatformTrustTests project is bundled into an APK with a network_security_config.xml that configures per-domain trust anchors and certificate pinning. Tests verified on a physical device (Samsung SM-A165F, API 36):

Test What it proves
NetworkSecurityConfig_CleartextTrafficBlocked_ForConfiguredDomain The XML config is loaded and per-domain cleartext policy works
NetworkSecurityConfig_TrustAnchors_RootCATrustedForConfiguredDomain Per-domain trust anchors are enforced by the platform TrustManager
NetworkSecurityConfig_CertificatePinning_BlocksConnectionWithWrongPin The issue scenario — a bogus pin for dot.net in the XML causes RemoteCertificateChainErrors, even though dot.net's cert is signed by a valid system CA
SslStream_CertificateSignedByTrustedCA_NoChainErrors Server cert signed by root CA trusted in XML config → platform accepts, no chain errors
SslStream_CertificateNotSignedByTrustedCA_ReportsChainErrors Self-signed cert not trusted in XML config → platform rejects, chain errors reported
SslStream_DomainNotInConfig_CallbackReceivesChainErrors Valid cert but connecting to a domain not in XML config → platform rejects (base-config applies)
SslStream_CallbackCanOverridePlatformTrustFailure Callback returning true can still accept despite platform rejection
SslStream_CallbackRejectingUntrustedCertificate_ThrowsAuthenticationException Callback returning falseAuthenticationException
SslStream_UntrustedCertificateWithoutCallback_ThrowsAuthenticationException No callback + platform rejects → AuthenticationException
SslStream_CustomRootTrustWithoutExtraStore_PlatformRejects Dynamic PKI not in XML config → platform rejects, chain errors reported
SslStream_CustomRootTrustWithExtraStore_ManagedOverridesPlatform ExtraStore provided → managed validation is authoritative, platform verdict bypassed

Changes

Java (DotnetProxyTrustManager)

  • Wraps the platform X509TrustManager obtained via TrustManagerFactory
  • Uses X509TrustManagerExtensions.checkServerTrusted(chain, authType, hostname) for hostname-aware validation (required for network_security_config.xml per-domain dispatch)
  • Passes chainTrustedByPlatform boolean to native callback
  • getAcceptedIssuers() returns empty array to avoid restricting client certificate selection

Native C (pal_sslstream.c, pal_trust_manager.c)

  • Refactored SSLStreamCreate: replaced 3 variants with a single function accepting targetHost, trustCerts, and keyManagers
  • Added key manager helpers: SSLStreamCreateKeyManagers and SSLStreamCreateKeyManagersFromKeyStoreEntry
  • GetX509TrustManager() obtains the platform trust manager, optionally initialized with a custom KeyStore
  • targetHost flows through SSLStreamCreateGetTrustManagersDotnetProxyTrustManager constructor
  • Fixed potential SIGSEGV in FreeSSLStream when SSLStreamInitialize was never called

Managed C# (SslStream.Android.cs)

  • CreateSslContext refactored: key manager creation extracted; keyManagers global ref released in finally
  • GetTrustCertHandles collects custom root certs from CertificateChainPolicy.CustomTrustStore or SslCertificateTrust
  • VerifyRemoteCertificate receives chainTrustedByPlatform flag and pre-seeds sslPolicyErrors

Build infrastructure (AndroidAppBuilder)

  • Supports NetworkSecurityConfig and NetworkSecurityConfigResourcesDir for including network_security_config.xml in APKs

Behavioral change

Apps using managed-only custom trust roots for CAs not in the Android system store will now see RemoteCertificateChainErrors in the callback (previously SslPolicyErrors.None). This is correct — the callback now reflects the platform's trust assessment. Apps can still accept the connection by returning true from RemoteCertificateValidationCallback.

Exception: When CertificateChainPolicy specifies CustomRootTrust with ExtraStore, the platform's verdict is bypassed because it lacks intermediate certs from ExtraStore and would produce false rejections. The managed chain builder is authoritative in this case.

Fixes #107695

Copilot AI review requested due to automatic review settings February 9, 2026 12:54
@simonrozsival simonrozsival force-pushed the feature/android-platform-trust-manager-107695 branch from 8cf3eb9 to 7f1bf1f Compare February 9, 2026 13:03
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Updates Android SslStream certificate validation so it consults the platform trust infrastructure (including network-security-config.xml) before delegating to managed validation, aligning Android behavior more closely with other platforms and correctly surfacing SslPolicyErrors in callbacks.

Changes:

  • Wrap Android’s default X509TrustManager in DotnetProxyTrustManager and pass platform-trust outcome into managed validation.
  • Plumb targetHost through native/managed layers (for hostname-aware checks on newer Android APIs) and move SNI setup into SSLStreamInitialize.
  • Add build/test infrastructure to include network_security_config.xml (and related resources) and introduce Android-focused tests.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/tasks/AndroidAppBuilder/Templates/AndroidManifest.xml Adds a templated placeholder for networkSecurityConfig on the <application> element.
src/tasks/AndroidAppBuilder/ApkBuilder.cs Copies network_security_config.xml + optional resources into res/ and passes -S res to aapt.
src/tasks/AndroidAppBuilder/AndroidAppBuilder.cs Exposes MSBuild task properties for network security config inputs.
src/native/libs/System.Security.Cryptography.Native.Android/pal_trust_manager.h Extends callback/get-trust-managers APIs to include platform-trust flag and targetHost.
src/native/libs/System.Security.Cryptography.Native.Android/pal_trust_manager.c Fetches default platform X509TrustManager via TrustManagerFactory and constructs the proxy trust manager with hostname.
src/native/libs/System.Security.Cryptography.Native.Android/pal_sslstream.h Threads targetHost into SSLStream create APIs; removes SSLStreamSetTargetHost.
src/native/libs/System.Security.Cryptography.Native.Android/pal_sslstream.c Passes targetHost into trust manager creation; moves SNI handling into SSLStreamInitialize.
src/native/libs/System.Security.Cryptography.Native.Android/pal_jni.h Adds cached JNI handles for TrustManagerFactory and X509TrustManager.
src/native/libs/System.Security.Cryptography.Native.Android/pal_jni.c Initializes JNI refs/method IDs for platform trust manager discovery and updated proxy TM ctor sig.
src/native/libs/System.Security.Cryptography.Native.Android/net/dot/android/crypto/DotnetProxyTrustManager.java Wraps platform trust manager and performs platform trust checks (hostname-aware on API 24+).
src/mono/msbuild/android/build/AndroidBuild.targets Plumbs NetworkSecurityConfig* MSBuild properties into AndroidAppBuilder.
src/libraries/System.Net.Security/tests/FunctionalTests/System.Net.Security.Tests.csproj Adds Android TFM and includes Android-only functional test source.
src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamPlatformTrustManagerTests.Android.cs Adds Android functional tests validating platform-trust reporting/override behavior.
src/libraries/System.Net.Security/tests/AndroidPlatformTrustTests/network_security_config.xml Adds a network security config used by Android-only tests to validate custom trust anchors.
src/libraries/System.Net.Security/tests/AndroidPlatformTrustTests/System.Net.Security.AndroidPlatformTrust.Tests.csproj New Android-only test project that prepares/embeds network security config and CA resource.
src/libraries/System.Net.Security/tests/AndroidPlatformTrustTests/AndroidPlatformTrustTests.cs New tests validating that platform trust anchors affect SslPolicyErrors.
src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs Changes VerifyRemoteCertificate to take SslPolicyErrors by ref (caller-initialized).
src/libraries/System.Net.Security/src/System/Net/Security/SslStream.IO.cs Initializes sslPolicyErrors before calling the ref-based VerifyRemoteCertificate.
src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Android.cs Incorporates platform-trust result into managed validation and avoids reintroducing chain errors when platform already trusted.
src/libraries/System.Net.Security/src/System/Net/Security/Pal.Android/SafeDeleteSslContext.cs Passes targetHost into native SSLStream creation (for trust manager hostname-aware validation); removes explicit SSLStreamSetTargetHost call.
src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.Ssl.cs Updates P/Invokes for new targetHost parameters and removes SSLStreamSetTargetHost interop.

@simonrozsival simonrozsival force-pushed the feature/android-platform-trust-manager-107695 branch from 7f1bf1f to 6243d5b Compare February 9, 2026 14:35
@dotnet-policy-service
Copy link
Contributor

Tagging subscribers to 'arch-android': @vitek-karas, @simonrozsival, @steveisok, @akoeplinger
See info in area-owners.md if you want to be subscribed.

On Android, SslStream's DotnetProxyTrustManager previously bypassed the
platform's trust infrastructure entirely. This meant that Android's
network-security-config.xml (certificate pinning, custom trust anchors)
was ignored, and RemoteCertificateValidationCallback always received
SslPolicyErrors.None even when the platform would have rejected the
certificate chain.

This change wraps the platform's default X509TrustManager inside
DotnetProxyTrustManager so that Android's trust infrastructure is
consulted before delegating to the managed SslStream validation code.

Changes:
- DotnetProxyTrustManager.java now wraps the platform X509TrustManager
  and calls checkServerTrusted/checkClientTrusted before invoking the
  managed callback. Uses X509TrustManagerExtensions for hostname-aware
  validation on API 24+.
- The targetHost is passed through SSLStreamCreate* to GetTrustManagers
  so the trust manager can perform hostname-aware validation.
- SSLStreamSetTargetHost is removed; SNI setup is now done in
  SSLStreamInitialize.
- VerifyRemoteCertificate receives a chainTrustedByPlatform flag. When
  the platform rejects the chain, RemoteCertificateChainErrors is
  reported. When the platform trusts it, AllowUnknownCertificateAuthority
  is set to prevent the managed chain builder from re-introducing errors
  for CAs that are trusted by the platform but not in the managed store.
- AndroidAppBuilder supports NetworkSecurityConfig to include
  network_security_config.xml in test APKs.

Behavioral change: Apps using managed-only custom trust roots for CAs
not in the Android system store will now see RemoteCertificateChainErrors
in the callback (previously SslPolicyErrors.None due to platform bypass).
This is correct — the callback accurately reflects the platform's trust
assessment, and apps can still accept by returning true.

Fixes dotnet#107695
Copilot AI review requested due to automatic review settings February 9, 2026 14:56
@simonrozsival simonrozsival force-pushed the feature/android-platform-trust-manager-107695 branch from 6243d5b to 06ea804 Compare February 9, 2026 14:56
X509TrustManagerExtensions.checkServerTrusted(chain, authType, host)
is available since API 17, and our minimum supported API level is 21.
The Build.VERSION.SDK_INT >= 24 guard was unnecessarily falling back
to hostname-unaware validation on API 21-23.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 3 comments.

- Extract SNI server name setup into SetSNIServerName() with proper
  INIT_LOCALS/RELEASE_LOCALS to prevent JNI local ref leaks on error
- Replace abort_unless with graceful LOG_ERROR + goto cleanup when
  platform X509TrustManager is unavailable
- Add ON_EXCEPTION_PRINT_AND_GOTO after make_java_object_array
- Remove unnecessary comment about API level in DotnetProxyTrustManager
Copilot AI review requested due to automatic review settings February 9, 2026 17:17
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 7 comments.

@stephentoub
Copy link
Member

🤖 Copilot Code Review — PR #124173

Holistic Assessment

Motivation: This PR addresses a real security and correctness bug where Android's network-security-config.xml (certificate pinning, custom trust anchors) was completely bypassed by SslStream. The fix correctly ensures Android's platform trust infrastructure is consulted before managed validation. This aligns with the existing iOS behavior and fixes issue #107695.

Approach: Wrapping the platform's default X509TrustManager inside DotnetProxyTrustManager, threading targetHost from managed through native to Java, and conditioning managed validation on chainTrustedByPlatform is the right layering. It preserves callback semantics while making the platform authoritative for chain trust.

Net positive: ✅ This is a significant correctness and security fix for Android apps. The design is clean and the tests are comprehensive.


Detailed Findings

✅ Correctness — Trust Integration & Callback Behavior

The new DotnetProxyTrustManager correctly delegates to the platform trust manager first (including hostname-aware validation via X509TrustManagerExtensions.checkServerTrusted(chain, authType, targetHost)). The managed side correctly distinguishes between "platform rejected" (RemoteCertificateChainErrors set) and "platform trusted" (strip spurious chain errors) so user callbacks see consistent errors.

✅ Correctness — JNI Lifecycle & Exception Handling

The GetDefaultX509TrustManager function has proper ON_EXCEPTION_PRINT_AND_GOTO(cleanup) checks after JNI method invocations. Local refs are correctly managed through INIT_LOCALS / RELEASE_LOCALS, and the JNI signature for DotnetProxyTrustManagerCtor was correctly updated. The make_java_object_array result is also checked with ON_EXCEPTION_PRINT_AND_GOTO.

✅ Correctness — Managed-Side API Consistency

Changing VerifyRemoteCertificate to use ref SslPolicyErrors and explicitly initializing sslPolicyErrors = SslPolicyErrors.None in CompleteHandshake (SslStream.IO.cs) is consistent and avoids stale error propagation. The pattern of initializing with the platform's verdict and passing by ref ensures the shared logic accumulates errors rather than overwriting the platform's trust decision.

✅ Test Coverage — Good

The tests validate key behaviors: untrusted self-signed certs report chain errors, callbacks can override platform failures, callbacks rejecting cause AuthenticationException, and network-security-config-pinned CA scenario does not surface chain errors.

💡 Performance — Consider Caching X509TrustManagerExtensions

File: DotnetProxyTrustManager.java

java X509TrustManagerExtensions extensions = new X509TrustManagerExtensions(platformTrustManager); extensions.checkServerTrusted(chain, authType, targetHost);

A new X509TrustManagerExtensions instance is created on every handshake (in isServerTrustedByPlatformTrustManager). Consider caching this in the constructor to avoid per-handshake allocations. This is a minor optimization but aligns with the repo's performance-conscious patterns.

(Flagged by multiple models)

💡 Performance — TrustManagerFactory Initialization

File: pal_trust_manager.c

GetDefaultX509TrustManager is called on every handshake via GetTrustManagers. Initializing TrustManagerFactory involves init(null), which loads the system KeyStore (potential disk I/O) and parses certificates. If benchmarking shows this is a bottleneck, consider caching the default X509TrustManager lazily during initialization rather than creating a new factory per connection.

(Flagged by multiple models)

💡 Behavioral Change — SNI Setup Exception Type

Files: pal_sslstream.c, SafeDeleteSslContext.cs

Previously, SSLStreamSetTargetHost mapped UNSUPPORTED_API_LEVEL to PlatformNotSupportedException with SR.net_android_ssl_api_level_unsupported. Now SNI setup is performed inside SSLStreamInitialize, and failures will cause a generic SslException.

The retained ApplyLegacyAndroidSNIWorkaround means this path should rarely fail on API 21-23, but if API-specific exception behavior was relied upon, it would differ. Consider documenting this or returning a distinct status code if preserving exception shape is important.


Summary

LGTM. The security fix is essential, the design is sound, and JNI wiring and exception handling are correct. Consider addressing the minor performance suggestions (caching X509TrustManagerExtensions and potentially the platform trust manager) as follow-up optimizations.


Review generated with input from multiple AI models (Gemini 3 Pro, Claude Opus 4.5). Claude Sonnet 4 (primary).

Copy link
Member

@steveisok steveisok left a comment

Choose a reason for hiding this comment

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

LGTM - I would add more security people to review.

…gement

- Replace 3 SSLStreamCreate variants with 1 SSLStreamCreate + 2 key manager helpers
- Key manager helpers return global refs; SSLStreamCreate releases after sslContext.init()
- Managed side has try/catch to release keyManagers global ref on P/Invoke failure
- Add CreateTrustKeyStore for custom root trust via SslCertificateTrust
- GetX509TrustManager accepts optional custom KeyStore
- Fix double-delete of ksType local ref in CreateTrustKeyStore
- LOG_ERROR + return NULL when custom KeyStore creation fails (no silent fallback)
- Simplify SslStream.Android.cs trust validation: pre-seed sslPolicyErrors from platform
- Skip platform trust injection when CertificateChainPolicy uses CustomRootTrust
  without SslCertificateTrust (managed chain builder is authoritative)
- Fix pre-existing SIGSEGV in FreeSSLStream when managedContextCleanup is NULL
Copilot AI review requested due to automatic review settings February 13, 2026 10:59
@simonrozsival simonrozsival force-pushed the feature/android-platform-trust-manager-107695 branch from c188118 to 3a0f51c Compare February 13, 2026 10:59
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 3 comments.

Copilot AI review requested due to automatic review settings February 13, 2026 12:04
@simonrozsival simonrozsival force-pushed the feature/android-platform-trust-manager-107695 branch 2 times, most recently from fc167da to cc7ee3a Compare February 13, 2026 12:09
@simonrozsival simonrozsival force-pushed the feature/android-platform-trust-manager-107695 branch from cc7ee3a to 8c44e4d Compare February 13, 2026 12:09
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 1 comment.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings February 13, 2026 12:19
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated no new comments.

Add two new tests that verify network_security_config.xml is loaded
and effective on Android:

- NetworkSecurityConfig_CleartextTrafficBlocked_ForConfiguredDomain:
  Proves the XML config is loaded by checking per-domain cleartext
  traffic policy via NetworkSecurityPolicy API.

- NetworkSecurityConfig_TrustAnchors_RootCATrustedForConfiguredDomain:
  Proves per-domain trust anchors work by checking the platform
  TrustManager directly — the NDX root CA is trusted for the
  configured domain but not for other domains.

Also update SslStream_CertificateSignedByTrustedCA_NoChainErrors to
use CustomRootTrust so managed validation also accepts the chain,
making the test resilient to running in environments where the NDX
root is not a system CA.
Add a test that connects to dot.net over TLS with a bogus SHA-256
pin in network_security_config.xml. The platform trust manager
rejects the connection (pin mismatch) even though dot.net's cert
is signed by a trusted system CA, proving that pin-set enforcement
works end-to-end through our SslStream → SSLEngine →
DotnetProxyTrustManager path.
@simonrozsival simonrozsival marked this pull request as ready for review February 13, 2026 16:30
Copilot AI review requested due to automatic review settings February 13, 2026 16:30
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 3 comments.

Comment on lines +125 to +156
public async Task NetworkSecurityConfig_CertificatePinning_BlocksConnectionWithWrongPin()
{
// The network_security_config.xml sets a bogus SHA-256 pin for "dot.net".
// When we connect to dot.net over TLS, the platform trust manager should
// reject the connection because the server's certificate chain doesn't
// match the configured pin — even though dot.net's certificate is signed
// by a trusted system CA.

SslPolicyErrors? reportedErrors = null;

using var tcp = new System.Net.Sockets.TcpClient();
await tcp.ConnectAsync("dot.net", 443);
using var sslStream = new SslStream(tcp.GetStream(), leaveInnerStreamOpen: false);

var options = new SslClientAuthenticationOptions
{
TargetHost = "dot.net",
RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
{
reportedErrors = sslPolicyErrors;
return true;
}
};

await sslStream.AuthenticateAsClientAsync(options);

Assert.NotNull(reportedErrors);
Assert.True(
(reportedErrors.Value & SslPolicyErrors.RemoteCertificateChainErrors) != 0,
$"Expected RemoteCertificateChainErrors due to pin mismatch but got: {reportedErrors.Value}");
}

Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

This test connects to an external server (dot.net) over the network. This makes the test:

  1. Flaky - depends on external network availability and could fail in CI environments with restricted network access
  2. Slow - network round-trip adds latency to test execution
  3. Non-deterministic - the certificate presented by dot.net could change over time, potentially invalidating the test

Consider using a local test server with a known certificate (similar to other tests in this file) instead of relying on an external host. This would make the test more reliable and faster.

Suggested change
public async Task NetworkSecurityConfig_CertificatePinning_BlocksConnectionWithWrongPin()
{
// The network_security_config.xml sets a bogus SHA-256 pin for "dot.net".
// When we connect to dot.net over TLS, the platform trust manager should
// reject the connection because the server's certificate chain doesn't
// match the configured pin — even though dot.net's certificate is signed
// by a trusted system CA.
SslPolicyErrors? reportedErrors = null;
using var tcp = new System.Net.Sockets.TcpClient();
await tcp.ConnectAsync("dot.net", 443);
using var sslStream = new SslStream(tcp.GetStream(), leaveInnerStreamOpen: false);
var options = new SslClientAuthenticationOptions
{
TargetHost = "dot.net",
RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
{
reportedErrors = sslPolicyErrors;
return true;
}
};
await sslStream.AuthenticateAsClientAsync(options);
Assert.NotNull(reportedErrors);
Assert.True(
(reportedErrors.Value & SslPolicyErrors.RemoteCertificateChainErrors) != 0,
$"Expected RemoteCertificateChainErrors due to pin mismatch but got: {reportedErrors.Value}");
}

Copilot uses AI. Check for mistakes.
Comment on lines +496 to +503
foreach (X509Certificate2 cert in collection)
{
if (cert.Subject == cert.Issuer)
return cert;
cert.Dispose();
}

throw new InvalidOperationException("Root CA not found in contoso.com.p7b");
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

In GetRootCertificate, non-root certificates are disposed in the loop, but if no root certificate is found (the throw case), the last certificate checked is never disposed, causing a memory leak in that error path. Additionally, the returned root certificate should be explicitly exported and reloaded (similar to line 484) to avoid potential handle aliasing issues with the collection's internal references.

Suggest wrapping the entire collection enumeration in a try-finally that disposes all certificates in the collection, or use a pattern that ensures all certs are disposed regardless of the control flow path.

Suggested change
foreach (X509Certificate2 cert in collection)
{
if (cert.Subject == cert.Issuer)
return cert;
cert.Dispose();
}
throw new InvalidOperationException("Root CA not found in contoso.com.p7b");
X509Certificate2? rootCertificate = null;
try
{
foreach (X509Certificate2 cert in collection)
{
if (cert.Subject == cert.Issuer)
{
rootCertificate = new X509Certificate2(cert.Export(X509ContentType.Cert));
break;
}
}
}
finally
{
foreach (X509Certificate2 cert in collection)
{
if (!ReferenceEquals(cert, rootCertificate))
{
cert.Dispose();
}
}
}
if (rootCertificate is null)
{
throw new InvalidOperationException("Root CA not found in contoso.com.p7b");
}
return rootCertificate;

Copilot uses AI. Check for mistakes.
Comment on lines +63 to +75
collection.Import(P7bPath);
X509Certificate2 root = null;
foreach (var cert in collection)
{
if (cert.Subject == cert.Issuer)
{
root = cert;
break;
}
}
if (root == null)
throw new Exception("Root CA not found in " + P7bPath);
System.IO.File.WriteAllBytes(OutputPath, root.RawData);
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

The ExtractRootCA inline MSBuild task has the same certificate disposal issue as GetRootCertificate in the test file. When a non-root certificate is found in the loop, it's never disposed. If no root is found, the last certificate checked is also never disposed. Additionally, the collection itself should be disposed after use.

Wrap the code in proper try-finally blocks to ensure all certificates are disposed, or iterate with explicit disposal of each certificate.

Suggested change
collection.Import(P7bPath);
X509Certificate2 root = null;
foreach (var cert in collection)
{
if (cert.Subject == cert.Issuer)
{
root = cert;
break;
}
}
if (root == null)
throw new Exception("Root CA not found in " + P7bPath);
System.IO.File.WriteAllBytes(OutputPath, root.RawData);
try
{
collection.Import(P7bPath);
X509Certificate2 root = null;
foreach (X509Certificate2 cert in collection)
{
if (cert.Subject == cert.Issuer)
{
root = cert;
break;
}
}
if (root == null)
{
throw new Exception("Root CA not found in " + P7bPath);
}
System.IO.File.WriteAllBytes(OutputPath, root.RawData);
}
finally
{
foreach (X509Certificate2 cert in collection)
{
cert.Dispose();
}
}

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Maui -Android certificate pinning using network-security-config ignore incorrect pin

3 participants