SetProtocols(sslContext, credential.Protocols);
}
- if (credential.Certificate != null)
+ if (credential.CertificateContext != null)
{
- SetCertificate(sslContext, credential.Certificate);
+ SetCertificate(sslContext, credential.CertificateContext);
}
Interop.AppleCrypto.SslBreakOnServerAuth(sslContext, true);
Interop.AppleCrypto.SslSetMaxProtocolVersion(sslContext, maxProtocolId);
}
- private static void SetCertificate(SafeSslHandle sslContext, X509Certificate2 certificate)
+ private static void SetCertificate(SafeSslHandle sslContext, SslStreamCertificateContext context)
{
Debug.Assert(sslContext != null, "sslContext != null");
- Debug.Assert(certificate != null, "certificate != null");
- Debug.Assert(certificate.HasPrivateKey, "certificate.HasPrivateKey");
- X509Chain chain = TLSCertificateExtensions.BuildNewChain(
- certificate,
- includeClientApplicationPolicy: false)!;
- using (chain)
- {
- X509ChainElementCollection elements = chain.ChainElements;
-
- // We need to leave off the EE (first) and root (last) certificate from the intermediates.
- X509Certificate2[] intermediateCerts = elements.Count < 3
- ? Array.Empty<X509Certificate2>()
- : new X509Certificate2[elements.Count - 2];
-
- // Build an array which is [
- // SecIdentityRef for EE cert,
- // SecCertificateRef for intermed0,
- // SecCertificateREf for intermed1,
- // ...
- // ]
- IntPtr[] ptrs = new IntPtr[intermediateCerts.Length + 1];
-
- for (int i = 0; i < intermediateCerts.Length; i++)
- {
- X509Certificate2 intermediateCert = elements[i + 1].Certificate!;
+ IntPtr[] ptrs = new IntPtr[context!.IntermediateCertificates!.Length + 1];
- if (intermediateCert.HasPrivateKey)
- {
- // In the unlikely event that we get a certificate with a private key from
- // a chain, clear it to the certificate.
- //
- // The current value of intermediateCert is still in elements, which will
- // get Disposed at the end of this method. The new value will be
- // in the intermediate certs array, which also gets serially Disposed.
- intermediateCert = new X509Certificate2(intermediateCert.RawData);
- }
+ for (int i = 0; i < context.IntermediateCertificates.Length; i++)
+ {
+ X509Certificate2 intermediateCert = context.IntermediateCertificates[i];
- intermediateCerts[i] = intermediateCert;
- ptrs[i + 1] = intermediateCert.Handle;
+ if (intermediateCert.HasPrivateKey)
+ {
+ // In the unlikely event that we get a certificate with a private key from
+ // a chain, clear it to the certificate.
+ //
+ // The current value of intermediateCert is still in elements, which will
+ // get Disposed at the end of this method. The new value will be
+ // in the intermediate certs array, which also gets serially Disposed.
+ intermediateCert = new X509Certificate2(intermediateCert.RawData);
}
- ptrs[0] = certificate.Handle;
-
- Interop.AppleCrypto.SslSetCertificate(sslContext, ptrs);
+ ptrs[i + 1] = intermediateCert.Handle;
+ }
- // The X509Chain created all new certs for us, so Dispose them.
- // And since the intermediateCerts could have been new instances, Dispose them, too
- for (int i = 0; i < elements.Count; i++)
- {
- elements[i].Certificate!.Dispose();
+ ptrs[0] = context!.Certificate!.Handle;
- if (i < intermediateCerts.Length)
- {
- intermediateCerts[i].Dispose();
- }
- }
- }
+ Interop.AppleCrypto.SslSetCertificate(sslContext, ptrs);
}
}
}
{
internal sealed class SafeFreeSslCredentials : SafeFreeCredentials
{
- public SafeFreeSslCredentials(X509Certificate certificate, SslProtocols protocols, EncryptionPolicy policy)
+ public SafeFreeSslCredentials(SslStreamCertificateContext? certificateContext, SslProtocols protocols, EncryptionPolicy policy)
: base(IntPtr.Zero, true)
{
- Debug.Assert(
- certificate == null || certificate is X509Certificate2,
- "Only X509Certificate2 certificates are supported at this time");
-
- X509Certificate2? cert = (X509Certificate2?)certificate;
-
- if (cert != null)
+ if (certificateContext != null)
{
- Debug.Assert(cert.HasPrivateKey, "cert.HasPrivateKey");
-
// Make a defensive copy of the certificate. In some async cases the
// certificate can have been disposed before being provided to the handshake.
//
// This meshes with the Unix (OpenSSL) PAL, because it extracts the private key
// and cert handle (which get up-reffed) to match the API expectations.
- cert = new X509Certificate2(cert);
+ certificateContext = certificateContext.Duplicate();
- Debug.Assert(cert.HasPrivateKey, "cert clone.HasPrivateKey");
+ Debug.Assert(certificateContext.Certificate.HasPrivateKey, "cert clone.HasPrivateKey");
}
- Certificate = cert;
+ CertificateContext = certificateContext;
Protocols = protocols;
Policy = policy;
}
public SslProtocols Protocols { get; }
- public X509Certificate2? Certificate { get; }
+ public SslStreamCertificateContext? CertificateContext { get; }
public override bool IsInvalid => false;
protected override bool ReleaseHandle()
{
- Certificate?.Dispose();
+ CertificateContext?.Certificate.Dispose();
return true;
}
}
{
if (NetEventSource.Log.IsEnabled())
NetEventSource.Log.UsingCachedCredential(this);
-
_credentialsHandle = cachedCredentialHandle;
_selectedClientCertificate = clientCertificate;
cachedCred = true;
}
else
{
- _credentialsHandle = SslStreamPal.AcquireCredentialsHandle(selectedCert!, _sslAuthenticationOptions.EnabledSslProtocols, _sslAuthenticationOptions.EncryptionPolicy, _sslAuthenticationOptions.IsServer);
+ if (selectedCert != null)
+ {
+ _sslAuthenticationOptions.CertificateContext = SslStreamCertificateContext.Create(selectedCert!);
+ }
+
+ _credentialsHandle = SslStreamPal.AcquireCredentialsHandle(_sslAuthenticationOptions.CertificateContext,
+ _sslAuthenticationOptions.EnabledSslProtocols, _sslAuthenticationOptions.EncryptionPolicy, _sslAuthenticationOptions.IsServer);
+
thumbPrint = guessedThumbPrint; // Delay until here in case something above threw.
_selectedClientCertificate = clientCertificate;
}
}
finally
{
- if (selectedCert != null)
+ if (selectedCert != null && _sslAuthenticationOptions.CertificateContext != null)
{
_sslAuthenticationOptions.CertificateContext = SslStreamCertificateContext.Create(selectedCert);
}
}
else
{
- _credentialsHandle = SslStreamPal.AcquireCredentialsHandle(selectedCert, _sslAuthenticationOptions.EnabledSslProtocols, _sslAuthenticationOptions.EncryptionPolicy, _sslAuthenticationOptions.IsServer);
+ _credentialsHandle = SslStreamPal.AcquireCredentialsHandle(_sslAuthenticationOptions.CertificateContext, _sslAuthenticationOptions.EnabledSslProtocols,
+ _sslAuthenticationOptions.EncryptionPolicy, _sslAuthenticationOptions.IsServer);
thumbPrint = guessedThumbPrint;
}
{
public partial class SslStreamCertificateContext
{
+ private const bool TrimRootCertificate = true;
+
+ private SslStreamCertificateContext(X509Certificate2 target, X509Certificate2[] intermediates)
+ {
+ Certificate = target;
+ IntermediateCertificates = intermediates;
+ }
+
internal static SslStreamCertificateContext Create(X509Certificate2 target) => Create(target, null);
}
}
{
public partial class SslStreamCertificateContext
{
+ // No leaf, no root.
+ private const bool TrimRootCertificate = true;
+
+ private SslStreamCertificateContext(X509Certificate2 target, X509Certificate2[] intermediates)
+ {
+ Certificate = target;
+ IntermediateCertificates = intermediates;
+ }
+
internal static SslStreamCertificateContext Create(X509Certificate2 target)
{
// On OSX we do not need to build chain unless we are asked for it.
{
public partial class SslStreamCertificateContext
{
+ // No leaf, include root.
+ private const bool TrimRootCertificate = false;
+
internal static SslStreamCertificateContext Create(X509Certificate2 target)
{
// On Windows we do not need to build chain unless we are asked for it.
return new SslStreamCertificateContext(target, Array.Empty<X509Certificate2>());
}
+
+ private SslStreamCertificateContext(X509Certificate2 target, X509Certificate2[] intermediates)
+ {
+ if (intermediates.Length > 0)
+ {
+ using (X509Chain chain = new X509Chain())
+ {
+ chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllFlags;
+ chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
+ chain.ChainPolicy.DisableCertificateDownloads = true;
+ bool osCanBuildChain = chain.Build(target);
+
+ int count = 0;
+ foreach (X509ChainStatus status in chain.ChainStatus)
+ {
+ if (status.Status.HasFlag(X509ChainStatusFlags.PartialChain) || status.Status.HasFlag(X509ChainStatusFlags.NotSignatureValid))
+ {
+ osCanBuildChain = false;
+ break;
+ }
+
+ count++;
+ }
+
+ // OS failed to build the chain but we have at least some intermediates.
+ // We will try to add them to "Intermediate Certification Authorities" store.
+ if (!osCanBuildChain)
+ {
+ X509Store? store = new X509Store(StoreName.CertificateAuthority, StoreLocation.LocalMachine);
+
+ try
+ {
+ store.Open(OpenFlags.ReadWrite);
+ }
+ catch
+ {
+ // If using system store fails, try to fall-back to user store.
+ store.Dispose();
+ store = new X509Store(StoreName.CertificateAuthority, StoreLocation.CurrentUser);
+ try
+ {
+ store.Open(OpenFlags.ReadWrite);
+ }
+ catch
+ {
+ store.Dispose();
+ store = null;
+ if (NetEventSource.IsEnabled)
+ {
+ NetEventSource.Error(this, $"Failed to open certificate store for intermediates.");
+ }
+ }
+ }
+
+ if (store != null)
+ {
+ using (store)
+ {
+ // Add everything except the root
+ for (int index = count; index < intermediates.Length - 1; index++)
+ {
+ store.Add(intermediates[index]);
+ }
+
+ osCanBuildChain = chain.Build(target);
+ foreach (X509ChainStatus status in chain.ChainStatus)
+ {
+ if (status.Status.HasFlag(X509ChainStatusFlags.PartialChain) || status.Status.HasFlag(X509ChainStatusFlags.NotSignatureValid))
+ {
+ osCanBuildChain = false;
+ break;
+ }
+ }
+
+ if (!osCanBuildChain)
+ {
+ // Add also root to Intermediate CA store so OS can complete building chain.
+ // (This does not make it trusted.
+ store.Add(intermediates[intermediates.Length - 1]);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Certificate = target;
+ IntermediateCertificates = intermediates;
+ }
}
}
}
X509Certificate2[] intermediates = Array.Empty<X509Certificate2>();
-
using (X509Chain chain = new X509Chain())
{
if (additionalCertificates != null)
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllFlags;
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
chain.ChainPolicy.DisableCertificateDownloads = offline;
- chain.Build(target);
+ bool chainStatus = chain.Build(target);
- // No leaf, no root.
- int count = chain.ChainElements.Count - 2;
+ if (!chainStatus && NetEventSource.IsEnabled)
+ {
+ NetEventSource.Error(null, $"Failed to build chain for {target.Subject}");
+ }
+ int count = chain.ChainElements.Count - (TrimRootCertificate ? 1 : 2);
foreach (X509ChainStatus status in chain.ChainStatus)
{
if (status.Status.HasFlag(X509ChainStatusFlags.PartialChain))
}
// Count can be zero for a self-signed certificate, or a cert issued directly from a root.
- if (count > 0)
+ if (count > 0 && chain.ChainElements.Count > 1)
{
intermediates = new X509Certificate2[count];
for (int i = 0; i < count; i++)
return new SslStreamCertificateContext(target, intermediates);
}
- private SslStreamCertificateContext(X509Certificate2 target, X509Certificate2[] intermediates)
+ internal SslStreamCertificateContext Duplicate()
{
- Certificate = target;
- IntermediateCertificates = intermediates;
+ return new SslStreamCertificateContext(new X509Certificate2(Certificate), IntermediateCertificates);
+
}
}
}
}
public static SafeFreeCredentials AcquireCredentialsHandle(
- X509Certificate certificate,
+ SslStreamCertificateContext? certificateContext,
SslProtocols protocols,
EncryptionPolicy policy,
bool isServer)
{
- return new SafeFreeSslCredentials(certificate, protocols, policy);
+ return new SafeFreeSslCredentials(certificateContext, protocols, policy);
}
internal static byte[]? GetNegotiatedApplicationProtocol(SafeDeleteContext? context)
return HandshakeInternal(credential!, ref context, inputBuffer, ref outputBuffer, sslAuthenticationOptions);
}
- public static SafeFreeCredentials AcquireCredentialsHandle(X509Certificate? certificate,
+ public static SafeFreeCredentials AcquireCredentialsHandle(SslStreamCertificateContext? certificateContext,
SslProtocols protocols, EncryptionPolicy policy, bool isServer)
{
- return new SafeFreeSslCredentials(certificate, protocols, policy);
+ return new SafeFreeSslCredentials(certificateContext?.Certificate, protocols, policy);
}
public static SecurityStatusPal EncryptMessage(SafeDeleteContext securityContext, ReadOnlyMemory<byte> input, int headerSize, int trailerSize, ref byte[] output, out int resultSize)
return SecurityStatusAdapterPal.GetSecurityStatusPalFromNativeInt(errorCode);
}
- public static SafeFreeCredentials AcquireCredentialsHandle(X509Certificate? certificate, SslProtocols protocols, EncryptionPolicy policy, bool isServer)
+ public static SafeFreeCredentials AcquireCredentialsHandle(SslStreamCertificateContext? certificateContext, SslProtocols protocols, EncryptionPolicy policy, bool isServer)
{
// New crypto API supports TLS1.3 but it does not allow to force NULL encryption.
return !UseNewCryptoApi || policy == EncryptionPolicy.NoEncryption ?
- AcquireCredentialsHandleSchannelCred(certificate, protocols, policy, isServer) :
- AcquireCredentialsHandleSchCredentials(certificate, protocols, policy, isServer);
+ AcquireCredentialsHandleSchannelCred(certificateContext?.Certificate, protocols, policy, isServer) :
+ AcquireCredentialsHandleSchCredentials(certificateContext?.Certificate, protocols, policy, isServer);
}
// This is legacy crypto API used on .NET Framework and older Windows versions.
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Text;
+using System.Threading;
using System.Threading.Tasks;
using Microsoft.DotNet.XUnitExtensions;
using Xunit;
{
using Configuration = System.Net.Test.Common.Configuration;
- public class SslStreamNetworkStreamTest
+ public class SslStreamNetworkStreamTest : IDisposable
{
private readonly X509Certificate2 _serverCert;
- private readonly X509CertificateCollection _serverChain;
+ private readonly X509Certificate2Collection _serverChain;
public SslStreamNetworkStreamTest()
{
- (_serverCert, _serverChain) = TestHelper.GenerateCertificates("localhost");
+ (_serverCert, _serverChain) = TestHelper.GenerateCertificates("localhost", this.GetType().Name);
+ }
+
+ public void Dispose()
+ {
+ TestHelper.CleanupCertificates(this.GetType().Name);
}
[Fact]
}
[Fact]
- [PlatformSpecific(TestPlatforms.AnyUnix)]
public async Task SslStream_UntrustedCaWithCustomCallback_OK()
{
- var options = new SslClientAuthenticationOptions() { TargetHost = "localhost" };
- options.RemoteCertificateValidationCallback =
+ var clientOptions = new SslClientAuthenticationOptions() { TargetHost = "localhost" };
+ clientOptions.RemoteCertificateValidationCallback =
(sender, certificate, chain, sslPolicyErrors) =>
{
- chain.ChainPolicy.ExtraStore.AddRange(_serverChain);
chain.ChainPolicy.CustomTrustStore.Add(_serverChain[_serverChain.Count -1]);
chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
return result;
};
+ var serverOptions = new SslServerAuthenticationOptions();
+ serverOptions.ServerCertificateContext = SslStreamCertificateContext.Create(_serverCert, _serverChain);
+
(Stream clientStream, Stream serverStream) = TestHelper.GetConnectedStreams();
using (clientStream)
using (serverStream)
using (SslStream client = new SslStream(clientStream))
using (SslStream server = new SslStream(serverStream))
{
- Task t1 = client.AuthenticateAsClientAsync(options, default);
- Task t2 = server.AuthenticateAsServerAsync(_serverCert);
+ Task t1 = client.AuthenticateAsClientAsync(clientOptions, CancellationToken.None);
+ Task t2 = server.AuthenticateAsServerAsync(serverOptions, CancellationToken.None);
await TestConfiguration.WhenAllOrAnyFailedWithTimeout(t1, t2);
}
public async Task SslStream_UntrustedCaWithCustomCallback_Throws(bool customCallback)
{
string errorMessage;
- var options = new SslClientAuthenticationOptions() { TargetHost = "localhost" };
+ var clientOptions = new SslClientAuthenticationOptions() { TargetHost = "localhost" };
if (customCallback)
{
- options.RemoteCertificateValidationCallback =
+ clientOptions.RemoteCertificateValidationCallback =
(sender, certificate, chain, sslPolicyErrors) =>
{
- chain.ChainPolicy.ExtraStore.AddRange(_serverChain);
chain.ChainPolicy.CustomTrustStore.Add(_serverChain[_serverChain.Count -1]);
chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
// This should work and we should be able to trust the chain.
}
else
{
- errorMessage = "PartialChain";
+ errorMessage = "UntrustedRoot";
}
+ var serverOptions = new SslServerAuthenticationOptions();
+ serverOptions.ServerCertificateContext = SslStreamCertificateContext.Create(_serverCert, _serverChain);
+
(Stream clientStream, Stream serverStream) = TestHelper.GetConnectedStreams();
using (clientStream)
using (serverStream)
using (SslStream client = new SslStream(clientStream))
using (SslStream server = new SslStream(serverStream))
{
- Task t1 = client.AuthenticateAsClientAsync(options, default);
- Task t2 = server.AuthenticateAsServerAsync(_serverCert);
+ Task t1 = client.AuthenticateAsClientAsync(clientOptions, CancellationToken.None);
+ Task t2 = server.AuthenticateAsServerAsync(serverOptions, CancellationToken.None);
var e = await Assert.ThrowsAsync<AuthenticationException>(() => t1);
Assert.Contains(errorMessage, e.Message);
return (new VirtualNetworkStream(vn, isServer: false), new VirtualNetworkStream(vn, isServer: true));
}
- internal static (X509Certificate2 certificate, X509Certificate2Collection) GenerateCertificates(string name, string? testName = null)
+ internal static void CleanupCertificates(string testName)
{
- X509Certificate2Collection chain = new X509Certificate2Collection();
+ string caName = $"O={testName}";
+ try
+ {
+ using (X509Store store = new X509Store(StoreName.CertificateAuthority, StoreLocation.LocalMachine))
+ {
+ store.Open(OpenFlags.ReadWrite);
+ foreach (X509Certificate2 cert in store.Certificates)
+ {
+ if (cert.Subject.Contains(caName))
+ {
+ store.Remove(cert);
+ }
+ }
+ }
+ }
+ catch { };
+
+ try
+ {
+ using (X509Store store = new X509Store(StoreName.CertificateAuthority, StoreLocation.CurrentUser))
+ {
+ store.Open(OpenFlags.ReadWrite);
+ foreach (X509Certificate2 cert in store.Certificates)
+ {
+ if (cert.Subject.Contains(caName))
+ {
+ store.Remove(cert);
+ }
+ }
+ }
+ }
+ catch { };
+ }
+ internal static (X509Certificate2 certificate, X509Certificate2Collection) GenerateCertificates(string targetName, string? testName = null)
+ {
+ if (PlatformDetection.IsWindows && testName != null)
+ {
+ CleanupCertificates(testName);
+ }
+ X509Certificate2Collection chain = new X509Certificate2Collection();
X509ExtensionCollection extensions = new X509ExtensionCollection();
SubjectAlternativeNameBuilder builder = new SubjectAlternativeNameBuilder();
- builder.AddDnsName(name);
+ builder.AddDnsName(targetName);
extensions.Add(builder.Build());
extensions.Add(s_eeConstraints);
extensions.Add(s_eeKeyUsage);
out CertificateAuthority root,
out CertificateAuthority intermediate,
out X509Certificate2 endEntity,
- subjectName: name,
+ subjectName: targetName,
testName: testName,
keySize: 2048,
extensions: extensions);
root.Dispose();
intermediate.Dispose();
+ if (PlatformDetection.IsWindows)
+ {
+ endEntity = new X509Certificate2(endEntity.Export(X509ContentType.Pfx));
+ }
+
return (endEntity, chain);
}
}