Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
06ea804
[Android] Respect platform trust manager in SslStream
simonrozsival Feb 9, 2026
6e0fe22
Remove unnecessary API 24 check in DotnetProxyTrustManager
simonrozsival Feb 9, 2026
0088428
Create X509TrustManagerExtensions inline instead of storing as field
simonrozsival Feb 9, 2026
798430a
Address review comments: extract SNI setup, fix JNI error handling
simonrozsival Feb 9, 2026
fdfe650
Use const char* for targetHost, improve ApkBuilder validation
simonrozsival Feb 11, 2026
aceb23b
Fix SNI ordering, client cert filtering, SIGSEGV crash, and CustomRoo…
simonrozsival Feb 11, 2026
3a0f51c
Refactor SSLStreamCreate, add custom trust KeyStore, fix JNI ref mana…
simonrozsival Feb 12, 2026
162d8f4
Document trust model: platform + managed = more strict, never less
simonrozsival Feb 13, 2026
8c44e4d
Refine managedTrustOnly: bypass platform only when user-provided Extr…
simonrozsival Feb 13, 2026
d39b4d2
Update src/tasks/AndroidAppBuilder/ApkBuilder.cs
simonrozsival Feb 13, 2026
f3ffdc9
Use CopyFilesToOutputDirectory instead of Publish for _PrepareNetwork…
simonrozsival Feb 13, 2026
1330726
Add ExtraStore bypass tests for AndroidPlatformTrust
simonrozsival Feb 13, 2026
2cf562a
Add network_security_config.xml regression tests
simonrozsival Feb 13, 2026
81743df
Add certificate pinning test via network_security_config.xml
simonrozsival Feb 13, 2026
730c0a6
Address Copilot review feedback in Android trust tests
simonrozsival Feb 14, 2026
5b3545c
Register JNI native method for NativeAOT compatibility
simonrozsival Feb 16, 2026
13ffcdd
Fix JNI local reference leak in GetX509TrustManager
simonrozsival Feb 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,47 +29,45 @@ internal enum PAL_SSLStreamStatus
};

[LibraryImport(Interop.Libraries.AndroidCryptoNative, EntryPoint = "AndroidCryptoNative_SSLStreamCreate")]
private static partial SafeSslHandle SSLStreamCreate(IntPtr sslStreamProxyHandle);
internal static SafeSslHandle SSLStreamCreate(SslStream.JavaProxy sslStreamProxy)
=> SSLStreamCreate(sslStreamProxy.Handle);

[LibraryImport(Interop.Libraries.AndroidCryptoNative, EntryPoint = "AndroidCryptoNative_SSLStreamCreateWithCertificates")]
private static partial SafeSslHandle SSLStreamCreateWithCertificates(
private static partial SafeSslHandle SSLStreamCreate(
IntPtr sslStreamProxyHandle,
[MarshalAs(UnmanagedType.LPUTF8Str)] string? targetHost,
IntPtr[]? trustCerts,
int trustCertsLen,
IntPtr keyManagers);
internal static SafeSslHandle SSLStreamCreate(
SslStream.JavaProxy sslStreamProxy,
string? targetHost,
IntPtr[]? trustCerts,
IntPtr keyManagers = 0)
=> SSLStreamCreate(sslStreamProxy.Handle, targetHost, trustCerts, trustCerts?.Length ?? 0, keyManagers);

[LibraryImport(Interop.Libraries.AndroidCryptoNative, EntryPoint = "AndroidCryptoNative_SSLStreamCreateKeyManagers")]
private static partial IntPtr SSLStreamCreateKeyManagersImpl(
ref byte pkcs8PrivateKey,
int pkcs8PrivateKeyLen,
PAL_KeyAlgorithm algorithm,
IntPtr[] certs,
int certsLen);
internal static SafeSslHandle SSLStreamCreateWithCertificates(
SslStream.JavaProxy sslStreamProxy,
internal static IntPtr SSLStreamCreateKeyManagers(
ReadOnlySpan<byte> pkcs8PrivateKey,
PAL_KeyAlgorithm algorithm,
IntPtr[] certificates)
{
return SSLStreamCreateWithCertificates(
sslStreamProxy.Handle,
return SSLStreamCreateKeyManagersImpl(
ref MemoryMarshal.GetReference(pkcs8PrivateKey),
pkcs8PrivateKey.Length,
algorithm,
certificates,
certificates.Length);
}

[LibraryImport(Interop.Libraries.AndroidCryptoNative, EntryPoint = "AndroidCryptoNative_SSLStreamCreateWithKeyStorePrivateKeyEntry")]
private static partial SafeSslHandle SSLStreamCreateWithKeyStorePrivateKeyEntry(
IntPtr sslStreamProxyHandle,
IntPtr keyStorePrivateKeyEntryHandle);
internal static SafeSslHandle SSLStreamCreateWithKeyStorePrivateKeyEntry(
SslStream.JavaProxy sslStreamProxy,
IntPtr keyStorePrivateKeyEntryHandle)
{
return SSLStreamCreateWithKeyStorePrivateKeyEntry(sslStreamProxy.Handle, keyStorePrivateKeyEntryHandle);
}
[LibraryImport(Interop.Libraries.AndroidCryptoNative, EntryPoint = "AndroidCryptoNative_SSLStreamCreateKeyManagersFromKeyStoreEntry")]
internal static partial IntPtr SSLStreamCreateKeyManagersFromKeyStoreEntry(IntPtr privateKeyEntryHandle);

[LibraryImport(Interop.Libraries.AndroidCryptoNative, EntryPoint = "AndroidCryptoNative_RegisterRemoteCertificateValidationCallback")]
internal static unsafe partial void RegisterRemoteCertificateValidationCallback(
delegate* unmanaged<IntPtr, bool> verifyRemoteCertificate);
delegate* unmanaged<IntPtr, int, bool> verifyRemoteCertificate);

[LibraryImport(Interop.Libraries.AndroidCryptoNative, EntryPoint = "AndroidCryptoNative_SSLStreamInitialize")]
private static unsafe partial int SSLStreamInitializeImpl(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,34 +209,111 @@ internal int ReadPendingWrites(byte[] buf, int offset, int count)

private static SafeSslHandle CreateSslContext(SslStream.JavaProxy sslStreamProxy, SslAuthenticationOptions authOptions)
{
if (authOptions.CertificateContext == null)
// targetHost is passed to the platform's DotnetProxyTrustManager for hostname-aware
// certificate validation. IP literals are excluded because SNIHostName doesn't accept them.
string? targetHost = !authOptions.IsServer
&& !string.IsNullOrEmpty(authOptions.TargetHost)
&& !IPAddress.IsValid(authOptions.TargetHost)
? authOptions.TargetHost
: null;

// Custom trust roots are passed to the platform's TrustManagerFactory KeyStore.
// The platform's trust verdict is combined with managed validation to be more strict
// (see VerifyRemoteCertificate in SslStream.Android.cs).
IntPtr[]? trustCerts = GetTrustCertHandles(authOptions);
IntPtr keyManagers = authOptions.CertificateContext is not null
? CreateKeyManagers(authOptions.CertificateContext)
: IntPtr.Zero;

try
{
return Interop.AndroidCrypto.SSLStreamCreate(sslStreamProxy);
return Interop.AndroidCrypto.SSLStreamCreate(sslStreamProxy, targetHost, trustCerts, keyManagers);
}

SslStreamCertificateContext context = authOptions.CertificateContext;
X509Certificate2 cert = context.TargetCertificate;
Debug.Assert(context.TargetCertificate.HasPrivateKey);

if (Interop.AndroidCrypto.IsKeyStorePrivateKeyEntry(cert.Handle))
finally
{
return Interop.AndroidCrypto.SSLStreamCreateWithKeyStorePrivateKeyEntry(sslStreamProxy, cert.Handle);
// keyManagers is a JNI global ref that was created to survive across
// P/Invoke boundaries. Release it now that SSLStreamCreate has consumed it.
if (keyManagers != IntPtr.Zero)
{
Interop.JObjectLifetime.DeleteGlobalReference(keyManagers);
}
}

PAL_KeyAlgorithm algorithm;
byte[] keyBytes;
using (AsymmetricAlgorithm key = GetPrivateKeyAlgorithm(cert, out algorithm))
static IntPtr CreateKeyManagers(SslStreamCertificateContext context)
{
keyBytes = key.ExportPkcs8PrivateKey();
X509Certificate2 cert = context.TargetCertificate;
Debug.Assert(cert.HasPrivateKey);

IntPtr keyManagers;
if (Interop.AndroidCrypto.IsKeyStorePrivateKeyEntry(cert.Handle))
{
keyManagers = Interop.AndroidCrypto.SSLStreamCreateKeyManagersFromKeyStoreEntry(cert.Handle);
}
else
{
PAL_KeyAlgorithm algorithm;
byte[] keyBytes;
using (AsymmetricAlgorithm key = GetPrivateKeyAlgorithm(cert, out algorithm))
{
keyBytes = key.ExportPkcs8PrivateKey();
}
IntPtr[] ptrs = new IntPtr[context.IntermediateCertificates.Count + 1];
ptrs[0] = cert.Handle;
for (int i = 0; i < context.IntermediateCertificates.Count; i++)
{
ptrs[i + 1] = context.IntermediateCertificates[i].Handle;
}

keyManagers = Interop.AndroidCrypto.SSLStreamCreateKeyManagers(keyBytes, algorithm, ptrs);
}

if (keyManagers == IntPtr.Zero)
{
throw new Interop.AndroidCrypto.SslException();
}

return keyManagers;
}
IntPtr[] ptrs = new IntPtr[context.IntermediateCertificates.Count + 1];
ptrs[0] = cert.Handle;
for (int i = 0; i < context.IntermediateCertificates.Count; i++)

static IntPtr[]? GetTrustCertHandles(SslAuthenticationOptions authOptions)
{
ptrs[i + 1] = context.IntermediateCertificates[i].Handle;
}
// Collect custom trust root certificates to pass to the platform's
// TrustManagerFactory. There are two mutually exclusive sources — when
// CertificateChainPolicy is set it takes precedence (see SslStream.Protocol.cs):
//
// 1. CertificateChainPolicy.CustomTrustStore (when TrustMode is CustomRootTrust)
// 2. SslCertificateTrust (via CertificateContext.Trust) — older API
//
// Note: CertificateChainPolicy.ExtraStore intermediates are NOT passed to the
// platform because Java's KeyStore.setCertificateEntry would elevate them to
// trust anchors. When ExtraStore is populated, the managed chain builder is
// authoritative (see VerifyRemoteCertificate in SslStream.Android.cs).
X509Certificate2Collection? certs;
if (authOptions.CertificateChainPolicy is not null)
{
certs = authOptions.CertificateChainPolicy.TrustMode == X509ChainTrustMode.CustomRootTrust
? authOptions.CertificateChainPolicy.CustomTrustStore
: null;
}
else
{
var trust = authOptions.CertificateContext?.Trust;
certs = trust?._store?.Certificates ?? trust?._trustList;
}

if (certs is null || certs.Count == 0)
{
return null;
}

return Interop.AndroidCrypto.SSLStreamCreateWithCertificates(sslStreamProxy, keyBytes, algorithm, ptrs);
IntPtr[] handles = new IntPtr[certs.Count];
for (int i = 0; i < certs.Count; i++)
{
handles[i] = certs[i].Handle;
}

return handles;
}
}

private static AsymmetricAlgorithm GetPrivateKeyAlgorithm(X509Certificate2 cert, out PAL_KeyAlgorithm algorithm)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,49 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Diagnostics;
using System.Net.Security;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
using System.Threading;

namespace System.Net.Security
{
public partial class SslStream
{
private JavaProxy.RemoteCertificateValidationResult VerifyRemoteCertificate()
private JavaProxy.RemoteCertificateValidationResult VerifyRemoteCertificate(bool chainTrustedByPlatform)
{
// The platform's trust verdict is combined with managed validation to be MORE strict,
// never less. If the platform rejects the chain, sslPolicyErrors is pre-seeded with
// RemoteCertificateChainErrors and managed validation cannot clear it. If the platform
// accepts the chain, managed validation (X509Chain.Build) can still independently
// introduce RemoteCertificateChainErrors.
//
// The platform's verdict is ignored when the user provided intermediate certificates
// via CertificateChainPolicy.ExtraStore. The platform does not have access to these
// intermediates (Java's KeyStore.setCertificateEntry would elevate them to trust
// anchors) and may produce false rejections for chains that require them. The managed
// chain builder has full access to ExtraStore and is authoritative in this case.
//
// Note: ExtraStore is also populated later (in SslStream.Protocol.cs) with peer
// certificates received during the TLS handshake. Those are the same certificates
// the platform already has, so they don't affect this decision. At this point,
// ExtraStore.Count reflects only user-provided certificates because
// SslAuthenticationOptions.UpdateOptions clones the user's CertificateChainPolicy
// for each handshake — peer certs from previous handshakes are never carried over.
bool managedTrustOnly = _sslAuthenticationOptions.CertificateChainPolicy?.ExtraStore?.Count > 0;

SslPolicyErrors sslPolicyErrors = SslPolicyErrors.None;
if (!managedTrustOnly && !chainTrustedByPlatform)
{
sslPolicyErrors = SslPolicyErrors.RemoteCertificateChainErrors;
}

ProtocolToken alertToken = default;

var isValid = VerifyRemoteCertificate(
_sslAuthenticationOptions.CertValidationDelegate,
_sslAuthenticationOptions.CertificateContext?.Trust,
ref alertToken,
out SslPolicyErrors sslPolicyErrors,
ref sslPolicyErrors,
out X509ChainStatusFlags chainStatus);

return new()
Expand Down Expand Up @@ -80,15 +104,15 @@ private static unsafe void RegisterRemoteCertificateValidationCallback()
}

[UnmanagedCallersOnly]
private static unsafe bool VerifyRemoteCertificate(IntPtr sslStreamProxyHandle)
private static unsafe bool VerifyRemoteCertificate(IntPtr sslStreamProxyHandle, int chainTrustedByPlatform)
{
var proxy = (JavaProxy?)GCHandle.FromIntPtr(sslStreamProxyHandle).Target;
Debug.Assert(proxy is not null);
Debug.Assert(proxy.ValidationResult is null);

try
{
proxy.ValidationResult = proxy._sslStream.VerifyRemoteCertificate();
proxy.ValidationResult = proxy._sslStream.VerifyRemoteCertificate(chainTrustedByPlatform != 0);
return proxy.ValidationResult.IsValid;
}
catch (Exception exception)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -601,7 +601,8 @@ private bool CompleteHandshake(ref ProtocolToken alertToken, out SslPolicyErrors
}
#endif

if (!VerifyRemoteCertificate(_sslAuthenticationOptions.CertValidationDelegate, _sslAuthenticationOptions.CertificateContext?.Trust, ref alertToken, out sslPolicyErrors, out chainStatus))
sslPolicyErrors = SslPolicyErrors.None;
if (!VerifyRemoteCertificate(_sslAuthenticationOptions.CertValidationDelegate, _sslAuthenticationOptions.CertificateContext?.Trust, ref alertToken, ref sslPolicyErrors, out chainStatus))
{
_handshakeCompleted = false;
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1038,9 +1038,8 @@ internal SecurityStatusPal Decrypt(Span<byte> buffer, out int outputOffset, out
--*/

//This method validates a remote certificate.
internal bool VerifyRemoteCertificate(RemoteCertificateValidationCallback? remoteCertValidationCallback, SslCertificateTrust? trust, ref ProtocolToken alertToken, out SslPolicyErrors sslPolicyErrors, out X509ChainStatusFlags chainStatus)
internal bool VerifyRemoteCertificate(RemoteCertificateValidationCallback? remoteCertValidationCallback, SslCertificateTrust? trust, ref ProtocolToken alertToken, ref SslPolicyErrors sslPolicyErrors, out X509ChainStatusFlags chainStatus)
{
sslPolicyErrors = SslPolicyErrors.None;
chainStatus = X509ChainStatusFlags.NoError;

// We don't catch exceptions in this method, so it's safe for "accepted" be initialized with true.
Expand Down
Loading
Loading