Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix certificate ctx on windows and macOS #39818

Merged
merged 6 commits into from
Aug 10, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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 @@ -7,6 +7,15 @@ namespace System.Net.Security
{
public partial class SslStreamCertificateContext
{
// No leaf, no root.
private const int _trimChain = 2;
wfurt marked this conversation as resolved.
Show resolved Hide resolved

private SslStreamCertificateContext(X509Certificate2 target, X509Certificate2[] intermediates)
{
Certificate = target;
IntermediateCertificates = intermediates;
}

internal static SslStreamCertificateContext Create(X509Certificate2 target) => Create(target, null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ namespace System.Net.Security
{
public partial class SslStreamCertificateContext
{
// No leaf, no root.
private const int _trimChain = 2;

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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,103 @@ namespace System.Net.Security
{
public partial class SslStreamCertificateContext
{
// No leaf, include root.
private const int _trimChain = 1;

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>());
}


wfurt marked this conversation as resolved.
Show resolved Hide resolved
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.
Copy link
Member

Choose a reason for hiding this comment

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

Do you really want to add all intermediates? Or just the ones that the OS ends up using in a chain?

e.g. Add all of intermediates to chain.ChainPolicy.ExtraStore and build again; then add anything relevant (preferably using a diffing algorithm so that it doesn't potentially reset custom store property overrides).

Using a second chain object (you could copy the policy object over) would make the diff easy, just walk each position and compare that cert.RawData values SequenceEqual.

Ideally, after each add you'd rerun the offline/no-extra build to see if you can avoid invalidating existing higher-scoped entries.

Copy link
Member

Choose a reason for hiding this comment

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

(While a diffing algorithm and a while loop sound potentially expensive, most chains are three items long, so there aren't many iterations)

Copy link
Member Author

Choose a reason for hiding this comment

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

I was considering to add some more complicated logic. But as you said if most chains are 3 long, it does not matter that much. I assumed that adding intermediate once to store is cheap enough so I decided not to care.

Copy link
Member

Choose a reason for hiding this comment

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

There's one thing to check:

  • Add an intermediate to the store
  • Open the intermediates store in MMC (e.g. add snap-in, certificates, local computer, then pick Intermediate Certificate Authorities / Certificates in the nav tree)
  • Pick that same certificate, right click, Properties
  • Pick "Enable only the following purposes"
  • Toggle some checkmarks
  • Use the API as you have it
  • Right click on the Certificates entry in the nav bar and refresh
  • Reopen the properties dialog.

If the property resets, we need to do complex stuff here. If it doesn't, we're good. (I think we do "merge properties", which should (generally) be safe, but it's good to test that before we run the risk of altering machine state)

Copy link
Member Author

Choose a reason for hiding this comment

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

that assumes same CA set between runs, right? (right now we generate unique set for each test run and I had situation where it would build up before adding cleanup to the tests)

Copy link
Member

Choose a reason for hiding this comment

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

It assumes that we ever get something in the intermediates collection that's already in the store. E.g. if what we need is a 4 hop chain:

  • root -> intermed1 -> intermed2 -> end-entity

And intermed1 is already in the store, but intermed2 wasn't; we'll end up calling store.Add(intermed1) which will cause the merge logic to apply. Since it changes system persisted state we need to be careful to ensure it's only additive, not destructive.

Copy link
Member Author

Choose a reason for hiding this comment

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

here is what I did:

  • modified the test helper to save/load certificates from pfx and I disabled the cleanup
  • I run the test and I check both CA.
  • I removed root CA, run tests and verified that it triggers the adding logic
  • I removed root again and I modified properties on intermediate and I also added friendly name
  • run test again. Root CA was added again and intermediate preserved with my modifications

so the conclusion is that adding existing certificate is not destructive.

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]);
bartonjs marked this conversation as resolved.
Show resolved Hide resolved
}

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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ public static SslStreamCertificateContext Create(X509Certificate2 target, X509Ce
}

X509Certificate2[] intermediates = Array.Empty<X509Certificate2>();

using (X509Chain chain = new X509Chain())
{
if (additionalCertificates != null)
Expand All @@ -32,11 +31,14 @@ public static SslStreamCertificateContext Create(X509Certificate2 target, X509Ce
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 - _trimChain;
foreach (X509ChainStatus status in chain.ChainStatus)
{
if (status.Status.HasFlag(X509ChainStatusFlags.PartialChain))
Expand All @@ -48,7 +50,7 @@ public static SslStreamCertificateContext Create(X509Certificate2 target, X509Ce
}

// 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)
wfurt marked this conversation as resolved.
Show resolved Hide resolved
{
intermediates = new X509Certificate2[count];
for (int i = 0; i < count; i++)
Expand All @@ -69,11 +71,5 @@ public static SslStreamCertificateContext Create(X509Certificate2 target, X509Ce

return new SslStreamCertificateContext(target, intermediates);
}

private SslStreamCertificateContext(X509Certificate2 target, X509Certificate2[] intermediates)
{
Certificate = target;
IntermediateCertificates = intermediates;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Xunit;

Expand All @@ -17,11 +18,16 @@ namespace System.Net.Security.Tests
public class SslStreamNetworkStreamTest
{
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);
}

~SslStreamNetworkStreamTest()
wfurt marked this conversation as resolved.
Show resolved Hide resolved
{
TestHelper.ClenupCertificates(this.GetType().Name);
}

[Fact]
Expand Down Expand Up @@ -269,14 +275,12 @@ await TestConfiguration.WhenAllOrAnyFailedWithTimeout(
}

[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;

Expand All @@ -286,28 +290,29 @@ public async Task SslStream_UntrustedCaWithCustomCallback_OK()
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);
}
}

[Fact]
[PlatformSpecific(TestPlatforms.AnyUnix)]
public async Task SslStream_UntrustedCaWithCustomCallback_Throws()
{
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;
// This should work and we should be able to trust the chain.
Expand All @@ -316,14 +321,17 @@ public async Task SslStream_UntrustedCaWithCustomCallback_Throws()
return false;
};

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 Assert.ThrowsAsync<AuthenticationException>(() => t1);
// Server side should finish since we run custom callback after handshake is done.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,53 @@ internal static (VirtualNetworkStream ClientStream, VirtualNetworkStream ServerS
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 ClenupCertificates(string testName)
wfurt marked this conversation as resolved.
Show resolved Hide resolved
{
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)
{
ClenupCertificates(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);
Expand All @@ -91,7 +130,7 @@ internal static (X509Certificate2 certificate, X509Certificate2Collection) Gener
out CertificateAuthority root,
out CertificateAuthority intermediate,
out X509Certificate2 endEntity,
subjectName: name,
subjectName: targetName,
testName: testName,
keySize: 2048,
extensions: extensions);
Expand All @@ -103,6 +142,11 @@ internal static (X509Certificate2 certificate, X509Certificate2Collection) Gener
root.Dispose();
intermediate.Dispose();

if (PlatformDetection.IsWindows)
{
endEntity = new X509Certificate2(endEntity.Export(X509ContentType.Pfx));
}

return (endEntity, chain);
}
}
Expand Down