|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Net.Http;
using System.Net.Security;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using Microsoft.AspNetCore.Connections.Features;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests;
public class HttpsConnectionMiddlewareTests : LoggedTest
{
private static readonly X509Certificate2 _x509Certificate2 = TestResources.GetTestCertificate();
private static readonly X509Certificate2 _x509Certificate2NoExt = TestResources.GetTestCertificate("no_extensions.pfx");
private static KestrelServerOptions CreateServerOptions()
{
var env = new Mock<IHostEnvironment>();
env.SetupGet(e => e.ContentRootPath).Returns(Directory.GetCurrentDirectory());
var serverOptions = new KestrelServerOptions();
serverOptions.ApplicationServices = new ServiceCollection()
.AddLogging()
.AddSingleton<IHttpsConfigurationService, HttpsConfigurationService>()
.AddSingleton<HttpsConfigurationService.IInitializer, HttpsConfigurationService.Initializer>()
.AddSingleton(env.Object)
.AddSingleton(new KestrelMetrics(new TestMeterFactory()))
.BuildServiceProvider();
return serverOptions;
}
[Fact]
public async Task CanReadAndWriteWithHttpsConnectionMiddleware()
{
void ConfigureListenOptions(ListenOptions listenOptions)
{
listenOptions.UseHttps(new HttpsConnectionAdapterOptions { ServerCertificate = _x509Certificate2 });
};
await using (var server = new TestServer(App, new TestServiceContext(LoggerFactory), ConfigureListenOptions))
{
var result = await server.HttpClientSlim.PostAsync($"https://localhost:{server.Port}/",
new FormUrlEncodedContent(new[] {
new KeyValuePair<string, string>("content", "Hello World?")
}),
validateCertificate: false);
Assert.Equal("content=Hello+World%3F", result);
}
}
[Fact]
public async Task CanReadAndWriteWithHttpsConnectionMiddlewareWithPemCertificate()
{
var configuration = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string>
{
["Certificates:Default:Path"] = Path.Combine("shared", "TestCertificates", "https-aspnet.crt"),
["Certificates:Default:KeyPath"] = Path.Combine("shared", "TestCertificates", "https-aspnet.key"),
["Certificates:Default:Password"] = "aspnetcore",
}).Build();
var options = CreateServerOptions();
var loader = new KestrelConfigurationLoader(options, configuration, options.ApplicationServices.GetRequiredService<IHttpsConfigurationService>(), certificatePathWatcher: null, reloadOnChange: false);
options.ConfigurationLoader = loader; // Since we're constructing it explicitly, we have to hook it up explicitly
loader.Load();
void ConfigureListenOptions(ListenOptions listenOptions)
{
listenOptions.KestrelServerOptions = options;
listenOptions.UseHttps();
};
await using (var server = new TestServer(App, new TestServiceContext(LoggerFactory), ConfigureListenOptions))
{
var result = await server.HttpClientSlim.PostAsync($"https://localhost:{server.Port}/",
new FormUrlEncodedContent(new[] {
new KeyValuePair<string, string>("content", "Hello World?")
}),
validateCertificate: false);
Assert.Equal("content=Hello+World%3F", result);
}
}
[Fact]
public async Task SslStreamIsAvailable()
{
void ConfigureListenOptions(ListenOptions listenOptions)
{
listenOptions.UseHttps(new HttpsConnectionAdapterOptions { ServerCertificate = _x509Certificate2 });
};
await using (var server = new TestServer(context =>
{
var feature = context.Features.Get<ISslStreamFeature>();
Assert.NotNull(feature);
Assert.NotNull(feature.SslStream);
return context.Response.WriteAsync("hello world");
}, new TestServiceContext(LoggerFactory), ConfigureListenOptions))
{
var result = await server.HttpClientSlim.GetStringAsync($"https://localhost:{server.Port}/", validateCertificate: false);
Assert.Equal("hello world", result);
}
}
[Fact]
public async Task HandshakeDetailsAreAvailable()
{
string expectedHostname = null;
void ConfigureListenOptions(ListenOptions listenOptions)
{
listenOptions.UseHttps(
new HttpsConnectionAdapterOptions
{
ServerCertificateSelector = (connection, name) =>
{
expectedHostname = name;
return _x509Certificate2;
}
});
};
await using (var server = new TestServer(context =>
{
var tlsFeature = context.Features.Get<ITlsHandshakeFeature>();
Assert.NotNull(tlsFeature);
Assert.Equal(expectedHostname, tlsFeature.HostName);
Assert.True(tlsFeature.Protocol > SslProtocols.None, "Protocol");
Assert.True(tlsFeature.NegotiatedCipherSuite >= TlsCipherSuite.TLS_NULL_WITH_NULL_NULL, "NegotiatedCipherSuite");
#pragma warning disable SYSLIB0058 // Type or member is obsolete
Assert.True(tlsFeature.CipherAlgorithm > CipherAlgorithmType.Null, "Cipher");
Assert.True(tlsFeature.CipherStrength > 0, "CipherStrength");
Assert.True(tlsFeature.HashAlgorithm >= HashAlgorithmType.None, "HashAlgorithm"); // May be None on Linux.
Assert.True(tlsFeature.HashStrength >= 0, "HashStrength"); // May be 0 for some algorithms
Assert.True(tlsFeature.KeyExchangeAlgorithm >= ExchangeAlgorithmType.None, "KeyExchangeAlgorithm"); // Maybe None on Windows 7
Assert.True(tlsFeature.KeyExchangeStrength >= 0, "KeyExchangeStrength"); // May be 0 on mac
#pragma warning restore SYSLIB0058 // Type or member is obsolete
return context.Response.WriteAsync("hello world");
}, new TestServiceContext(LoggerFactory), ConfigureListenOptions))
{
var result = await server.HttpClientSlim.GetStringAsync($"https://localhost:{server.Port}/", validateCertificate: false);
Assert.Equal("hello world", result);
}
}
[Fact]
public async Task HandshakeDetailsAreAvailableAfterAsyncCallback()
{
void ConfigureListenOptions(ListenOptions listenOptions)
{
listenOptions.UseHttps(async (stream, clientHelloInfo, state, cancellationToken) =>
{
await Task.Yield();
return new SslServerAuthenticationOptions
{
ServerCertificate = _x509Certificate2,
};
}, state: null);
}
await using (var server = new TestServer(context =>
{
var tlsFeature = context.Features.Get<ITlsHandshakeFeature>();
Assert.NotNull(tlsFeature);
Assert.True(tlsFeature.Protocol > SslProtocols.None, "Protocol");
#pragma warning disable SYSLIB0058 // Type or member is obsolete
Assert.True(tlsFeature.CipherAlgorithm > CipherAlgorithmType.Null, "Cipher");
Assert.True(tlsFeature.CipherStrength > 0, "CipherStrength");
Assert.True(tlsFeature.HashAlgorithm >= HashAlgorithmType.None, "HashAlgorithm"); // May be None on Linux.
Assert.True(tlsFeature.HashStrength >= 0, "HashStrength"); // May be 0 for some algorithms
Assert.True(tlsFeature.KeyExchangeAlgorithm >= ExchangeAlgorithmType.None, "KeyExchangeAlgorithm"); // Maybe None on Windows 7
Assert.True(tlsFeature.KeyExchangeStrength >= 0, "KeyExchangeStrength"); // May be 0 on mac
#pragma warning restore SYSLIB0058 // Type or member is obsolete
return context.Response.WriteAsync("hello world");
}, new TestServiceContext(LoggerFactory), ConfigureListenOptions))
{
var result = await server.HttpClientSlim.GetStringAsync($"https://localhost:{server.Port}/", validateCertificate: false);
Assert.Equal("hello world", result);
}
}
[Fact]
public async Task RequireCertificateFailsWhenNoCertificate()
{
await using (var server = new TestServer(App, new TestServiceContext(LoggerFactory), listenOptions =>
{
listenOptions.UseHttps(new HttpsConnectionAdapterOptions
{
ServerCertificate = _x509Certificate2,
ClientCertificateMode = ClientCertificateMode.RequireCertificate
});
}))
{
await Assert.ThrowsAnyAsync<Exception>(
() => server.HttpClientSlim.GetStringAsync($"https://localhost:{server.Port}/"));
}
}
[Fact]
public async Task AllowCertificateContinuesWhenNoCertificate()
{
void ConfigureListenOptions(ListenOptions listenOptions)
{
listenOptions.UseHttps(new HttpsConnectionAdapterOptions
{
ServerCertificate = _x509Certificate2,
ClientCertificateMode = ClientCertificateMode.AllowCertificate
});
}
await using (var server = new TestServer(context =>
{
var tlsFeature = context.Features.Get<ITlsConnectionFeature>();
Assert.NotNull(tlsFeature);
Assert.Null(tlsFeature.ClientCertificate);
return context.Response.WriteAsync("hello world");
}, new TestServiceContext(LoggerFactory), ConfigureListenOptions))
{
var result = await server.HttpClientSlim.GetStringAsync($"https://localhost:{server.Port}/", validateCertificate: false);
Assert.Equal("hello world", result);
}
}
[Fact]
public async Task AsyncCallbackSettingClientCertificateRequiredContinuesWhenNoCertificate()
{
void ConfigureListenOptions(ListenOptions listenOptions)
{
listenOptions.UseHttps((stream, clientHelloInfo, state, cancellationToken) =>
new ValueTask<SslServerAuthenticationOptions>(new SslServerAuthenticationOptions
{
ServerCertificate = _x509Certificate2,
ClientCertificateRequired = true,
RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true,
CertificateRevocationCheckMode = X509RevocationMode.NoCheck
}), state: null);
}
await using (var server = new TestServer(context =>
{
var tlsFeature = context.Features.Get<ITlsConnectionFeature>();
Assert.NotNull(tlsFeature);
Assert.Null(tlsFeature.ClientCertificate);
return context.Response.WriteAsync("hello world");
}, new TestServiceContext(LoggerFactory), ConfigureListenOptions))
{
var result = await server.HttpClientSlim.GetStringAsync($"https://localhost:{server.Port}/", validateCertificate: false);
Assert.Equal("hello world", result);
}
}
[Fact]
public void ThrowsWhenNoServerCertificateIsProvided()
{
Assert.Throws<ArgumentException>(() => CreateMiddleware(new HttpsConnectionAdapterOptions(), ListenOptions.DefaultHttpProtocols));
}
[Fact]
public async Task UsesProvidedServerCertificate()
{
void ConfigureListenOptions(ListenOptions listenOptions)
{
listenOptions.UseHttps(new HttpsConnectionAdapterOptions { ServerCertificate = _x509Certificate2 });
};
await using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory), ConfigureListenOptions))
{
using (var connection = server.CreateConnection())
{
var stream = OpenSslStream(connection.Stream);
await stream.AuthenticateAsClientAsync("localhost");
Assert.True(stream.RemoteCertificate.Equals(_x509Certificate2));
}
}
}
[Fact]
public async Task UsesProvidedServerCertificateSelector()
{
var selectorCalled = 0;
void ConfigureListenOptions(ListenOptions listenOptions)
{
listenOptions.UseHttps(new HttpsConnectionAdapterOptions
{
ServerCertificateSelector = (connection, name) =>
{
Assert.NotNull(connection);
Assert.NotNull(connection.Features.Get<SslStream>());
Assert.Equal("localhost", name);
selectorCalled++;
return _x509Certificate2;
}
});
}
await using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory), ConfigureListenOptions))
{
using (var connection = server.CreateConnection())
{
var stream = OpenSslStream(connection.Stream);
await stream.AuthenticateAsClientAsync("localhost");
Assert.True(stream.RemoteCertificate.Equals(_x509Certificate2));
Assert.Equal(1, selectorCalled);
}
}
}
[Fact]
public async Task UsesProvidedAsyncCallback()
{
var selectorCalled = 0;
void ConfigureListenOptions(ListenOptions listenOptions)
{
listenOptions.UseHttps(async (stream, clientHelloInfo, state, cancellationToken) =>
{
await Task.Yield();
Assert.NotNull(stream);
Assert.Equal("localhost", clientHelloInfo.ServerName);
selectorCalled++;
return new SslServerAuthenticationOptions
{
ServerCertificate = _x509Certificate2
};
}, state: null);
}
await using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory), ConfigureListenOptions))
{
using (var connection = server.CreateConnection())
{
var stream = OpenSslStream(connection.Stream);
await stream.AuthenticateAsClientAsync("localhost");
Assert.True(stream.RemoteCertificate.Equals(_x509Certificate2));
Assert.Equal(1, selectorCalled);
}
}
}
[Fact]
public async Task UsesProvidedServerCertificateSelectorEachTime()
{
var selectorCalled = 0;
void ConfigureListenOptions(ListenOptions listenOptions)
{
listenOptions.UseHttps(new HttpsConnectionAdapterOptions
{
ServerCertificateSelector = (connection, name) =>
{
Assert.NotNull(connection);
Assert.NotNull(connection.Features.Get<SslStream>());
Assert.Equal("localhost", name);
selectorCalled++;
if (selectorCalled == 1)
{
return _x509Certificate2;
}
return _x509Certificate2NoExt;
}
});
}
await using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory), ConfigureListenOptions))
{
using (var connection = server.CreateConnection())
{
var stream = OpenSslStream(connection.Stream);
await stream.AuthenticateAsClientAsync("localhost");
Assert.True(stream.RemoteCertificate.Equals(_x509Certificate2));
Assert.Equal(1, selectorCalled);
}
using (var connection = server.CreateConnection())
{
var stream = OpenSslStream(connection.Stream);
await stream.AuthenticateAsClientAsync("localhost");
Assert.True(stream.RemoteCertificate.Equals(_x509Certificate2NoExt));
Assert.Equal(2, selectorCalled);
}
}
}
[Fact]
public async Task UsesProvidedServerCertificateSelectorValidatesEkus()
{
var selectorCalled = 0;
void ConfigureListenOptions(ListenOptions listenOptions)
{
listenOptions.UseHttps(new HttpsConnectionAdapterOptions
{
ServerCertificateSelector = (features, name) =>
{
selectorCalled++;
return TestResources.GetTestCertificate("eku.code_signing.pfx");
}
});
}
await using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory), ConfigureListenOptions))
{
using (var connection = server.CreateConnection())
{
var stream = OpenSslStream(connection.Stream);
#pragma warning disable SYSLIB0039 // TLS 1.0 and 1.1 are obsolete
await Assert.ThrowsAsync<IOException>(() =>
stream.AuthenticateAsClientAsync("localhost", new X509CertificateCollection(), SslProtocols.Tls12 | SslProtocols.Tls11, false));
#pragma warning restore SYSLIB0039
Assert.Equal(1, selectorCalled);
}
}
}
[Fact]
public async Task UsesProvidedServerCertificateSelectorOverridesServerCertificate()
{
var selectorCalled = 0;
void ConfigureListenOptions(ListenOptions listenOptions)
{
listenOptions.UseHttps(new HttpsConnectionAdapterOptions
{
ServerCertificate = _x509Certificate2NoExt,
ServerCertificateSelector = (connection, name) =>
{
Assert.NotNull(connection);
Assert.NotNull(connection.Features.Get<SslStream>());
Assert.Equal("localhost", name);
selectorCalled++;
return _x509Certificate2;
}
});
}
await using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory), ConfigureListenOptions))
{
using (var connection = server.CreateConnection())
{
var stream = OpenSslStream(connection.Stream);
await stream.AuthenticateAsClientAsync("localhost");
Assert.True(stream.RemoteCertificate.Equals(_x509Certificate2));
Assert.Equal(1, selectorCalled);
}
}
}
[Fact]
public async Task UsesProvidedServerCertificateSelectorFailsIfYouReturnNull()
{
var selectorCalled = 0;
void ConfigureListenOptions(ListenOptions listenOptions)
{
listenOptions.UseHttps(new HttpsConnectionAdapterOptions
{
ServerCertificateSelector = (features, name) =>
{
selectorCalled++;
return null;
}
});
}
await using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory), ConfigureListenOptions))
{
using (var connection = server.CreateConnection())
{
var stream = OpenSslStream(connection.Stream);
#pragma warning disable SYSLIB0039 // TLS 1.0 and 1.1 are obsolete
await Assert.ThrowsAsync<IOException>(() =>
stream.AuthenticateAsClientAsync("localhost", new X509CertificateCollection(), SslProtocols.Tls12 | SslProtocols.Tls11, false));
#pragma warning restore SYSLIB0039
Assert.Equal(1, selectorCalled);
}
}
}
[Theory]
[InlineData(HttpProtocols.Http1)]
[InlineData(HttpProtocols.Http1AndHttp2)] // Make sure Http/1.1 doesn't regress with Http/2 enabled.
public async Task CertificatePassedToHttpContext(HttpProtocols httpProtocols)
{
void ConfigureListenOptions(ListenOptions listenOptions)
{
listenOptions.Protocols = httpProtocols;
listenOptions.UseHttps(options =>
{
options.ServerCertificate = _x509Certificate2;
options.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
options.AllowAnyClientCertificate();
});
}
await using (var server = new TestServer(context =>
{
var tlsFeature = context.Features.Get<ITlsConnectionFeature>();
Assert.NotNull(tlsFeature);
Assert.NotNull(tlsFeature.ClientCertificate);
Assert.NotNull(context.Connection.ClientCertificate);
return context.Response.WriteAsync("hello world");
}, new TestServiceContext(LoggerFactory), ConfigureListenOptions))
{
using (var connection = server.CreateConnection())
{
// SslStream is used to ensure the certificate is actually passed to the server
// HttpClient might not send the certificate because it is invalid or it doesn't match any
// of the certificate authorities sent by the server in the SSL handshake.
// Use a random host name to avoid the TLS session resumption cache.
var stream = OpenSslStreamWithCert(connection.Stream);
await stream.AuthenticateAsClientAsync(Guid.NewGuid().ToString());
await AssertConnectionResult(stream, true);
}
}
}
[Fact]
public async Task RenegotiateForClientCertificateOnHttp1DisabledByDefault()
{
void ConfigureListenOptions(ListenOptions listenOptions)
{
listenOptions.Protocols = HttpProtocols.Http1;
listenOptions.UseHttps(options =>
{
options.ServerCertificate = _x509Certificate2;
options.ClientCertificateMode = ClientCertificateMode.NoCertificate;
options.AllowAnyClientCertificate();
});
}
await using var server = new TestServer(async context =>
{
var tlsFeature = context.Features.Get<ITlsConnectionFeature>();
Assert.NotNull(tlsFeature);
Assert.Null(tlsFeature.ClientCertificate);
Assert.Null(context.Connection.ClientCertificate);
var clientCert = await context.Connection.GetClientCertificateAsync();
Assert.Null(clientCert);
Assert.Null(tlsFeature.ClientCertificate);
Assert.Null(context.Connection.ClientCertificate);
await context.Response.WriteAsync("hello world");
}, new TestServiceContext(LoggerFactory), ConfigureListenOptions);
using var connection = server.CreateConnection();
// SslStream is used to ensure the certificate is actually passed to the server
// HttpClient might not send the certificate because it is invalid or it doesn't match any
// of the certificate authorities sent by the server in the SSL handshake.
// Use a random host name to avoid the TLS session resumption cache.
var stream = OpenSslStreamWithCert(connection.Stream);
await stream.AuthenticateAsClientAsync(Guid.NewGuid().ToString());
await AssertConnectionResult(stream, true);
}
[ConditionalTheory]
[InlineData(HttpProtocols.Http1)]
[InlineData(HttpProtocols.Http1AndHttp2)] // Make sure turning on Http/2 doesn't regress HTTP/1
[TlsAlpnSupported]
[OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Missing platform support.")]
[SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/33566#issuecomment-892031659", Queues = HelixConstants.AlmaLinuxAmd64)] // Outdated OpenSSL client
public async Task CanRenegotiateForClientCertificate(HttpProtocols httpProtocols)
{
void ConfigureListenOptions(ListenOptions listenOptions)
{
listenOptions.Protocols = httpProtocols;
listenOptions.UseHttps(options =>
{
options.ServerCertificate = _x509Certificate2;
options.ClientCertificateMode = ClientCertificateMode.DelayCertificate;
options.AllowAnyClientCertificate();
});
}
await using var server = new TestServer(async context =>
{
var tlsFeature = context.Features.Get<ITlsConnectionFeature>();
Assert.NotNull(tlsFeature);
Assert.Null(tlsFeature.ClientCertificate);
Assert.Null(context.Connection.ClientCertificate);
var clientCert = await context.Connection.GetClientCertificateAsync();
Assert.NotNull(clientCert);
Assert.NotNull(tlsFeature.ClientCertificate);
Assert.NotNull(context.Connection.ClientCertificate);
await context.Response.WriteAsync("hello world");
}, new TestServiceContext(LoggerFactory), ConfigureListenOptions);
using var connection = server.CreateConnection();
// SslStream is used to ensure the certificate is actually passed to the server
// HttpClient might not send the certificate because it is invalid or it doesn't match any
// of the certificate authorities sent by the server in the SSL handshake.
// Use a random host name to avoid the TLS session resumption cache.
var stream = OpenSslStreamWithCert(connection.Stream);
await stream.AuthenticateAsClientAsync(Guid.NewGuid().ToString());
await AssertConnectionResult(stream, true);
}
[ConditionalFact]
[TlsAlpnSupported]
[OSSkipCondition(OperatingSystems.Windows | OperatingSystems.Linux, SkipReason = "MacOS only test.")]
public async Task CanRenegotiateForClientCertificate_MacOS_PlatformNotSupportedException()
{
void ConfigureListenOptions(ListenOptions listenOptions)
{
listenOptions.Protocols = HttpProtocols.Http1;
listenOptions.UseHttps(options =>
{
options.ServerCertificate = _x509Certificate2;
options.ClientCertificateMode = ClientCertificateMode.DelayCertificate;
options.AllowAnyClientCertificate();
});
}
await using var server = new TestServer(async context =>
{
var tlsFeature = context.Features.Get<ITlsConnectionFeature>();
Assert.NotNull(tlsFeature);
Assert.Null(tlsFeature.ClientCertificate);
Assert.Null(context.Connection.ClientCertificate);
await Assert.ThrowsAsync<PlatformNotSupportedException>(() => context.Connection.GetClientCertificateAsync());
var lifetimeNotificationFeature = context.Features.Get<IConnectionLifetimeNotificationFeature>();
Assert.False(
lifetimeNotificationFeature.ConnectionClosedRequested.IsCancellationRequested,
"GetClientCertificateAsync shouldn't cause the connection to be closed.");
await context.Response.WriteAsync("hello world");
}, new TestServiceContext(LoggerFactory), ConfigureListenOptions);
using var connection = server.CreateConnection();
// SslStream is used to ensure the certificate is actually passed to the server
// HttpClient might not send the certificate because it is invalid or it doesn't match any
// of the certificate authorities sent by the server in the SSL handshake.
// Use a random host name to avoid the TLS session resumption cache.
var stream = OpenSslStreamWithCert(connection.Stream);
await stream.AuthenticateAsClientAsync(Guid.NewGuid().ToString());
await AssertConnectionResult(stream, true);
}
[Fact]
public async Task Renegotiate_ServerOptionsSelectionCallback_NotSupported()
{
void ConfigureListenOptions(ListenOptions listenOptions)
{
listenOptions.UseHttps((SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken) =>
{
return ValueTask.FromResult(new SslServerAuthenticationOptions()
{
ServerCertificate = _x509Certificate2,
ClientCertificateRequired = false,
RemoteCertificateValidationCallback = (_, _, _, _) => true,
});
}, state: null);
}
await using var server = new TestServer(async context =>
{
var tlsFeature = context.Features.Get<ITlsConnectionFeature>();
Assert.NotNull(tlsFeature);
Assert.Null(tlsFeature.ClientCertificate);
Assert.Null(context.Connection.ClientCertificate);
var clientCert = await context.Connection.GetClientCertificateAsync();
Assert.Null(clientCert);
Assert.Null(tlsFeature.ClientCertificate);
Assert.Null(context.Connection.ClientCertificate);
await context.Response.WriteAsync("hello world");
}, new TestServiceContext(LoggerFactory), ConfigureListenOptions);
using var connection = server.CreateConnection();
// SslStream is used to ensure the certificate is actually passed to the server
// HttpClient might not send the certificate because it is invalid or it doesn't match any
// of the certificate authorities sent by the server in the SSL handshake.
// Use a random host name to avoid the TLS session resumption cache.
var stream = OpenSslStreamWithCert(connection.Stream);
await stream.AuthenticateAsClientAsync(Guid.NewGuid().ToString());
await AssertConnectionResult(stream, true);
}
[ConditionalFact]
[TlsAlpnSupported]
[OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Missing platform support.")]
[SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/33566#issuecomment-892031659", Queues = HelixConstants.AlmaLinuxAmd64)] // Outdated OpenSSL client
public async Task CanRenegotiateForTlsCallbackOptions()
{
void ConfigureListenOptions(ListenOptions listenOptions)
{
listenOptions.UseHttps(new TlsHandshakeCallbackOptions()
{
OnConnection = context =>
{
context.AllowDelayedClientCertificateNegotation = true;
return ValueTask.FromResult(new SslServerAuthenticationOptions()
{
ServerCertificate = _x509Certificate2,
ClientCertificateRequired = false,
RemoteCertificateValidationCallback = (_, _, _, _) => true,
});
}
});
}
await using var server = new TestServer(async context =>
{
var tlsFeature = context.Features.Get<ITlsConnectionFeature>();
Assert.NotNull(tlsFeature);
Assert.Null(tlsFeature.ClientCertificate);
Assert.Null(context.Connection.ClientCertificate);
var clientCert = await context.Connection.GetClientCertificateAsync();
Assert.NotNull(clientCert);
Assert.NotNull(tlsFeature.ClientCertificate);
Assert.NotNull(context.Connection.ClientCertificate);
await context.Response.WriteAsync("hello world");
}, new TestServiceContext(LoggerFactory), ConfigureListenOptions);
using var connection = server.CreateConnection();
// SslStream is used to ensure the certificate is actually passed to the server
// HttpClient might not send the certificate because it is invalid or it doesn't match any
// of the certificate authorities sent by the server in the SSL handshake.
// Use a random host name to avoid the TLS session resumption cache.
var stream = OpenSslStreamWithCert(connection.Stream);
await stream.AuthenticateAsClientAsync(Guid.NewGuid().ToString());
await AssertConnectionResult(stream, true);
}
[ConditionalFact]
[TlsAlpnSupported]
[OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Missing platform support.")]
[SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/33566#issuecomment-892031659", Queues = HelixConstants.AlmaLinuxAmd64)] // Outdated OpenSSL client
public async Task CanRenegotiateForClientCertificateOnHttp1CanReturnNoCert()
{
void ConfigureListenOptions(ListenOptions listenOptions)
{
listenOptions.Protocols = HttpProtocols.Http1;
listenOptions.UseHttps(options =>
{
options.ServerCertificate = _x509Certificate2;
options.ClientCertificateMode = ClientCertificateMode.DelayCertificate;
options.AllowAnyClientCertificate();
});
}
await using var server = new TestServer(async context =>
{
var tlsFeature = context.Features.Get<ITlsConnectionFeature>();
Assert.NotNull(tlsFeature);
Assert.Null(tlsFeature.ClientCertificate);
Assert.Null(context.Connection.ClientCertificate);
var clientCert = await context.Connection.GetClientCertificateAsync();
Assert.Null(clientCert);
Assert.Null(tlsFeature.ClientCertificate);
Assert.Null(context.Connection.ClientCertificate);
await context.Response.WriteAsync("hello world");
}, new TestServiceContext(LoggerFactory), ConfigureListenOptions);
using var connection = server.CreateConnection();
// SslStream is used to ensure the certificate is actually passed to the server
// HttpClient might not send the certificate because it is invalid or it doesn't match any
// of the certificate authorities sent by the server in the SSL handshake.
// Use a random host name to avoid the TLS session resumption cache.
var stream = new SslStream(connection.Stream);
var clientOptions = new SslClientAuthenticationOptions()
{
TargetHost = Guid.NewGuid().ToString(),
#pragma warning disable SYSLIB0039 // TLS 1.0 and 1.1 are obsolete
EnabledSslProtocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12,
#pragma warning restore SYSLIB0039
};
clientOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true;
await stream.AuthenticateAsClientAsync(clientOptions);
await AssertConnectionResult(stream, true);
}
[ConditionalFact]
[OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Fails on OSX.")]
public async Task ServerCertificateChainInExtraStore()
{
var streams = new List<SslStream>();
CertHelper.CleanupCertificates(nameof(ServerCertificateChainInExtraStore));
(var clientCertificate, var clientChain) = CertHelper.GenerateCertificates(nameof(ServerCertificateChainInExtraStore), longChain: true, serverCertificate: false);
using (var store = new X509Store(StoreName.CertificateAuthority, StoreLocation.CurrentUser))
{
// add chain certificate so we can construct chain since there is no way how to pass intermediates directly.
store.Open(OpenFlags.ReadWrite);
store.AddRange(clientChain);
store.Close();
}
using (var chain = new X509Chain())
{
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllFlags;
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
chain.ChainPolicy.DisableCertificateDownloads = true;
var chainStatus = chain.Build(clientCertificate);
}
void ConfigureListenOptions(ListenOptions listenOptions)
{
listenOptions.UseHttps(new HttpsConnectionAdapterOptions
{
ServerCertificate = _x509Certificate2,
ServerCertificateChain = clientChain,
OnAuthenticate = (con, so) =>
{
so.ClientCertificateRequired = true;
so.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
{
Assert.NotEmpty(chain.ChainPolicy.ExtraStore);
Assert.Contains(clientChain[0], chain.ChainPolicy.ExtraStore);
return true;
};
so.CertificateRevocationCheckMode = X509RevocationMode.NoCheck;
}
});
}
await using (var server = new TestServer(
context => context.Response.WriteAsync("hello world"),
new TestServiceContext(LoggerFactory), ConfigureListenOptions))
{
using (var connection = server.CreateConnection())
{
var stream = OpenSslStreamWithCert(connection.Stream, clientCertificate);
await stream.AuthenticateAsClientAsync("localhost");
await AssertConnectionResult(stream, true);
}
}
CertHelper.CleanupCertificates(nameof(ServerCertificateChainInExtraStore));
clientCertificate.Dispose();
var list = (System.Collections.IList)clientChain;
for (var i = 0; i < list.Count; i++)
{
var c = (X509Certificate)list[i];
c.Dispose();
}
foreach (var s in streams)
{
s.Dispose();
}
}
[ConditionalFact]
// TLS 1.2 and lower have to renegotiate the whole connection to get a client cert, and if that hits an error
// then the connection is aborted.
[OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Missing platform support.")]
[TlsAlpnSupported]
[SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/33566#issuecomment-892031659", Queues = HelixConstants.AlmaLinuxAmd64)] // Outdated OpenSSL client
public async Task RenegotiateForClientCertificateOnPostWithoutBufferingThrows()
{
void ConfigureListenOptions(ListenOptions listenOptions)
{
listenOptions.Protocols = HttpProtocols.Http1;
listenOptions.UseHttps(options =>
{
options.ServerCertificate = _x509Certificate2;
options.ClientCertificateMode = ClientCertificateMode.DelayCertificate;
options.AllowAnyClientCertificate();
});
}
// Under 4kb can sometimes work because it fits into Kestrel's header parsing buffer.
var expectedBody = new string('a', 1024 * 4);
await using var server = new TestServer(async context =>
{
var tlsFeature = context.Features.Get<ITlsConnectionFeature>();
Assert.NotNull(tlsFeature);
Assert.Null(tlsFeature.ClientCertificate);
Assert.Null(context.Connection.ClientCertificate);
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => context.Connection.GetClientCertificateAsync());
Assert.Equal("Client stream needs to be drained before renegotiation.", ex.Message);
Assert.Null(tlsFeature.ClientCertificate);
Assert.Null(context.Connection.ClientCertificate);
}, new TestServiceContext(LoggerFactory), ConfigureListenOptions);
using var connection = server.CreateConnection();
// SslStream is used to ensure the certificate is actually passed to the server
// HttpClient might not send the certificate because it is invalid or it doesn't match any
// of the certificate authorities sent by the server in the SSL handshake.
// Use a random host name to avoid the TLS session resumption cache.
var stream = OpenSslStreamWithCert(connection.Stream);
await stream.AuthenticateAsClientAsync(Guid.NewGuid().ToString());
await AssertConnectionResult(stream, true, expectedBody);
}
[ConditionalFact]
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10)] // HTTP/2 requires Win10
[TlsAlpnSupported]
public async Task ServerOptionsSelectionCallback_SetsALPN()
{
static void ConfigureListenOptions(ListenOptions listenOptions)
{
listenOptions.UseHttps((_, _, _, _) =>
ValueTask.FromResult(new SslServerAuthenticationOptions()
{
ServerCertificate = _x509Certificate2,
}), state: null);
}
await using var server = new TestServer(context => Task.CompletedTask,
new TestServiceContext(LoggerFactory), ConfigureListenOptions);
using var connection = server.CreateConnection();
var stream = OpenSslStream(connection.Stream);
await stream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions()
{
// Use a random host name to avoid the TLS session resumption cache.
TargetHost = Guid.NewGuid().ToString(),
ApplicationProtocols = new() { SslApplicationProtocol.Http2, SslApplicationProtocol.Http11, },
});
Assert.Equal(SslApplicationProtocol.Http2, stream.NegotiatedApplicationProtocol);
}
[ConditionalFact]
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10)] // HTTP/2 requires Win10
[TlsAlpnSupported]
public async Task TlsHandshakeCallbackOptionsOverload_SetsALPN()
{
static void ConfigureListenOptions(ListenOptions listenOptions)
{
listenOptions.UseHttps(new TlsHandshakeCallbackOptions()
{
OnConnection = context =>
{
return ValueTask.FromResult(new SslServerAuthenticationOptions()
{
ServerCertificate = _x509Certificate2,
});
}
});
}
await using var server = new TestServer(context => Task.CompletedTask,
new TestServiceContext(LoggerFactory), ConfigureListenOptions);
using var connection = server.CreateConnection();
var stream = OpenSslStream(connection.Stream);
await stream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions()
{
// Use a random host name to avoid the TLS session resumption cache.
TargetHost = Guid.NewGuid().ToString(),
ApplicationProtocols = new() { SslApplicationProtocol.Http2, SslApplicationProtocol.Http11, },
});
Assert.Equal(SslApplicationProtocol.Http2, stream.NegotiatedApplicationProtocol);
}
[ConditionalFact]
[TlsAlpnSupported]
public async Task TlsHandshakeCallbackOptionsOverload_EmptyAlpnList_DisablesAlpn()
{
static void ConfigureListenOptions(ListenOptions listenOptions)
{
listenOptions.UseHttps(new TlsHandshakeCallbackOptions()
{
OnConnection = context =>
{
return ValueTask.FromResult(new SslServerAuthenticationOptions()
{
ServerCertificate = _x509Certificate2,
ApplicationProtocols = new(),
});
}
});
}
await using var server = new TestServer(context => Task.CompletedTask,
new TestServiceContext(LoggerFactory), ConfigureListenOptions);
using var connection = server.CreateConnection();
var stream = OpenSslStream(connection.Stream);
await stream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions()
{
// Use a random host name to avoid the TLS session resumption cache.
TargetHost = Guid.NewGuid().ToString(),
ApplicationProtocols = new() { SslApplicationProtocol.Http2, SslApplicationProtocol.Http11, },
});
Assert.Equal(default, stream.NegotiatedApplicationProtocol);
}
[ConditionalFact]
[TlsAlpnSupported]
[OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Missing platform support.")]
[SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/33566#issuecomment-892031659", Queues = HelixConstants.AlmaLinuxAmd64)] // Outdated OpenSSL client
public async Task CanRenegotiateForClientCertificateOnPostIfDrained()
{
void ConfigureListenOptions(ListenOptions listenOptions)
{
listenOptions.Protocols = HttpProtocols.Http1;
listenOptions.UseHttps(options =>
{
options.ServerCertificate = _x509Certificate2;
options.ClientCertificateMode = ClientCertificateMode.DelayCertificate;
options.AllowAnyClientCertificate();
});
}
var expectedBody = new string('a', 1024 * 4);
await using var server = new TestServer(async context =>
{
var tlsFeature = context.Features.Get<ITlsConnectionFeature>();
Assert.NotNull(tlsFeature);
Assert.Null(tlsFeature.ClientCertificate);
Assert.Null(context.Connection.ClientCertificate);
// Read the body before requesting the client cert
var body = await new StreamReader(context.Request.Body).ReadToEndAsync();
Assert.Equal(expectedBody, body);
var clientCert = await context.Connection.GetClientCertificateAsync();
Assert.NotNull(clientCert);
Assert.NotNull(tlsFeature.ClientCertificate);
Assert.NotNull(context.Connection.ClientCertificate);
await context.Response.WriteAsync("hello world");
}, new TestServiceContext(LoggerFactory), ConfigureListenOptions);
using var connection = server.CreateConnection();
// SslStream is used to ensure the certificate is actually passed to the server
// HttpClient might not send the certificate because it is invalid or it doesn't match any
// of the certificate authorities sent by the server in the SSL handshake.
// Use a random host name to avoid the TLS session resumption cache.
var stream = OpenSslStreamWithCert(connection.Stream);
await stream.AuthenticateAsClientAsync(Guid.NewGuid().ToString());
await AssertConnectionResult(stream, true, expectedBody);
}
[ConditionalFact]
[OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Missing platform support.")]
[TlsAlpnSupported]
[SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/33566#issuecomment-892031659", Queues = HelixConstants.AlmaLinuxAmd64)] // Outdated OpenSSL client
public async Task RenegotationFailureCausesConnectionClose()
{
void ConfigureListenOptions(ListenOptions listenOptions)
{
listenOptions.Protocols = HttpProtocols.Http1;
listenOptions.UseHttps(options =>
{
options.ServerCertificate = _x509Certificate2;
options.ClientCertificateMode = ClientCertificateMode.DelayCertificate;
options.AllowAnyClientCertificate();
});
}
var expectedBody = new string('a', 1024 * 4);
await using var server = new TestServer(async context =>
{
var tlsFeature = context.Features.Get<ITlsConnectionFeature>();
Assert.NotNull(tlsFeature);
Assert.Null(tlsFeature.ClientCertificate);
Assert.Null(context.Connection.ClientCertificate);
// Request the client cert while there's still body data in the buffers
var ioe = await Assert.ThrowsAsync<InvalidOperationException>(() => context.Connection.GetClientCertificateAsync());
Assert.Equal("Client stream needs to be drained before renegotiation.", ioe.Message);
context.Response.ContentLength = 11;
await context.Response.WriteAsync("hello world");
}, new TestServiceContext(LoggerFactory), ConfigureListenOptions);
using var connection = server.CreateConnection();
// SslStream is used to ensure the certificate is actually passed to the server
// HttpClient might not send the certificate because it is invalid or it doesn't match any
// of the certificate authorities sent by the server in the SSL handshake.
// Use a random host name to avoid the TLS session resumption cache.
var stream = OpenSslStreamWithCert(connection.Stream);
await stream.AuthenticateAsClientAsync(Guid.NewGuid().ToString());
var request = Encoding.UTF8.GetBytes($"POST / HTTP/1.1\r\nHost: localhost\r\nContent-Length: {expectedBody.Length}\r\n\r\n{expectedBody}");
await stream.WriteAsync(request, 0, request.Length).DefaultTimeout();
var reader = new StreamReader(stream);
Assert.Equal("HTTP/1.1 200 OK", await reader.ReadLineAsync().DefaultTimeout());
Assert.Equal("Content-Length: 11", await reader.ReadLineAsync().DefaultTimeout());
Assert.Equal("Connection: close", await reader.ReadLineAsync().DefaultTimeout());
Assert.StartsWith("Date: ", await reader.ReadLineAsync().DefaultTimeout());
Assert.Equal("", await reader.ReadLineAsync().DefaultTimeout());
Assert.Equal("hello world", await reader.ReadLineAsync().DefaultTimeout());
Assert.Null(await reader.ReadLineAsync().DefaultTimeout());
}
[Fact]
public async Task HttpsSchemePassedToRequestFeature()
{
void ConfigureListenOptions(ListenOptions listenOptions)
{
listenOptions.UseHttps(new HttpsConnectionAdapterOptions { ServerCertificate = _x509Certificate2 });
}
await using (var server = new TestServer(context => context.Response.WriteAsync(context.Request.Scheme), new TestServiceContext(LoggerFactory), ConfigureListenOptions))
{
var result = await server.HttpClientSlim.GetStringAsync($"https://localhost:{server.Port}/", validateCertificate: false);
Assert.Equal("https", result);
}
}
[Fact]
public async Task Tls10CanBeDisabled()
{
void ConfigureListenOptions(ListenOptions listenOptions)
{
listenOptions.UseHttps(options =>
{
#pragma warning disable SYSLIB0039 // TLS 1.0 and 1.1 are obsolete
options.SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11;
#pragma warning restore SYSLIB0039
options.ServerCertificate = _x509Certificate2;
options.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
options.AllowAnyClientCertificate();
});
}
await using (var server = new TestServer(context => context.Response.WriteAsync("hello world"), new TestServiceContext(LoggerFactory), ConfigureListenOptions))
{
// SslStream is used to ensure the certificate is actually passed to the server
// HttpClient might not send the certificate because it is invalid or it doesn't match any
// of the certificate authorities sent by the server in the SSL handshake.
using (var connection = server.CreateConnection())
{
var stream = OpenSslStreamWithCert(connection.Stream);
#pragma warning disable SYSLIB0039 // TLS 1.0 and 1.1 are obsolete
var ex = await Assert.ThrowsAnyAsync<Exception>(
async () => await stream.AuthenticateAsClientAsync("localhost", new X509CertificateCollection(), SslProtocols.Tls, false));
#pragma warning restore SYSLIB0039
}
}
}
[Theory]
[InlineData(ClientCertificateMode.AllowCertificate)]
[InlineData(ClientCertificateMode.RequireCertificate)]
public async Task ClientCertificateValidationGetsCalledWithNotNullParameters(ClientCertificateMode mode)
{
var clientCertificateValidationCalled = false;
void ConfigureListenOptions(ListenOptions listenOptions)
{
listenOptions.UseHttps(new HttpsConnectionAdapterOptions
{
ServerCertificate = _x509Certificate2,
ClientCertificateMode = mode,
ClientCertificateValidation = (certificate, chain, sslPolicyErrors) =>
{
clientCertificateValidationCalled = true;
Assert.NotNull(certificate);
Assert.NotNull(chain);
return true;
}
});
}
await using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory), ConfigureListenOptions))
{
using (var connection = server.CreateConnection())
{
var stream = OpenSslStreamWithCert(connection.Stream);
await stream.AuthenticateAsClientAsync("localhost");
await AssertConnectionResult(stream, true);
Assert.True(clientCertificateValidationCalled);
}
}
}
[ConditionalTheory]
[InlineData(ClientCertificateMode.AllowCertificate)]
[InlineData(ClientCertificateMode.RequireCertificate)]
public async Task ValidationFailureRejectsConnection(ClientCertificateMode mode)
{
void ConfigureListenOptions(ListenOptions listenOptions)
{
listenOptions.UseHttps(new HttpsConnectionAdapterOptions
{
ServerCertificate = _x509Certificate2,
ClientCertificateMode = mode,
ClientCertificateValidation = (certificate, chain, sslPolicyErrors) => false
});
}
await using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory), ConfigureListenOptions))
{
using (var connection = server.CreateConnection())
{
var stream = OpenSslStreamWithCert(connection.Stream);
await stream.AuthenticateAsClientAsync("localhost");
await AssertConnectionResult(stream, false);
}
}
}
[Theory]
[InlineData(ClientCertificateMode.AllowCertificate)]
[InlineData(ClientCertificateMode.RequireCertificate)]
public async Task RejectsConnectionOnSslPolicyErrorsWhenNoValidation(ClientCertificateMode mode)
{
void ConfigureListenOptions(ListenOptions listenOptions)
{
listenOptions.UseHttps(new HttpsConnectionAdapterOptions
{
ServerCertificate = _x509Certificate2,
ClientCertificateMode = mode
});
}
await using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory), ConfigureListenOptions))
{
using (var connection = server.CreateConnection())
{
var stream = OpenSslStreamWithCert(connection.Stream);
await stream.AuthenticateAsClientAsync("localhost");
await AssertConnectionResult(stream, false);
}
}
}
[Fact]
public async Task AllowAnyCertOverridesCertificateValidation()
{
void ConfigureListenOptions(ListenOptions listenOptions)
{
listenOptions.UseHttps(options =>
{
options.ServerCertificate = _x509Certificate2;
options.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
options.ClientCertificateValidation = (certificate, x509Chain, sslPolicyErrors) => false;
options.AllowAnyClientCertificate();
});
}
await using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory), ConfigureListenOptions))
{
using (var connection = server.CreateConnection())
{
var stream = OpenSslStreamWithCert(connection.Stream);
await stream.AuthenticateAsClientAsync("localhost");
await AssertConnectionResult(stream, true);
}
}
}
[Fact]
public async Task CertificatePassedToHttpContextIsNotDisposed()
{
void ConfigureListenOptions(ListenOptions listenOptions)
{
listenOptions.UseHttps(options =>
{
options.ServerCertificate = _x509Certificate2;
options.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
options.AllowAnyClientCertificate();
});
}
RequestDelegate app = context =>
{
var tlsFeature = context.Features.Get<ITlsConnectionFeature>();
Assert.NotNull(tlsFeature);
Assert.NotNull(tlsFeature.ClientCertificate);
Assert.NotNull(context.Connection.ClientCertificate);
Assert.NotNull(context.Connection.ClientCertificate.PublicKey);
return context.Response.WriteAsync("hello world");
};
await using (var server = new TestServer(app, new TestServiceContext(LoggerFactory), ConfigureListenOptions))
{
using (var connection = server.CreateConnection())
{
var stream = OpenSslStreamWithCert(connection.Stream);
await stream.AuthenticateAsClientAsync("localhost");
await AssertConnectionResult(stream, true);
}
}
}
[Theory]
[InlineData("no_extensions.pfx")]
public void AcceptsCertificateWithoutExtensions(string testCertName)
{
var certPath = TestResources.GetCertPath(testCertName);
TestOutputHelper.WriteLine("Loading " + certPath);
var cert = new X509Certificate2(certPath, "testPassword");
Assert.Empty(cert.Extensions.OfType<X509EnhancedKeyUsageExtension>());
CreateMiddleware(cert);
}
[Theory]
[InlineData("eku.server.pfx")]
[InlineData("eku.multiple_usages.pfx")]
public void ValidatesEnhancedKeyUsageOnCertificate(string testCertName)
{
var certPath = TestResources.GetCertPath(testCertName);
TestOutputHelper.WriteLine("Loading " + certPath);
var cert = new X509Certificate2(certPath, "testPassword");
Assert.NotEmpty(cert.Extensions);
var eku = Assert.Single(cert.Extensions.OfType<X509EnhancedKeyUsageExtension>());
Assert.NotEmpty(eku.EnhancedKeyUsages);
CreateMiddleware(new HttpsConnectionAdapterOptions
{
ServerCertificate = cert,
},
ListenOptions.DefaultHttpProtocols);
}
[Theory]
[InlineData("eku.code_signing.pfx")]
[InlineData("eku.client.pfx")]
public void ThrowsForCertificatesMissingServerEku(string testCertName)
{
var certPath = TestResources.GetCertPath(testCertName);
TestOutputHelper.WriteLine("Loading " + certPath);
var cert = new X509Certificate2(certPath, "testPassword");
Assert.NotEmpty(cert.Extensions);
var eku = Assert.Single(cert.Extensions.OfType<X509EnhancedKeyUsageExtension>());
Assert.NotEmpty(eku.EnhancedKeyUsages);
var ex = Assert.Throws<InvalidOperationException>(() =>
CreateMiddleware(new HttpsConnectionAdapterOptions
{
ServerCertificate = cert,
},
ListenOptions.DefaultHttpProtocols));
Assert.Equal(CoreStrings.FormatInvalidServerCertificateEku(cert.Thumbprint), ex.Message);
}
[Theory]
[InlineData("no_extensions.pfx")]
public void LogsForCertificateMissingSubjectAlternativeName(string testCertName)
{
var certPath = TestResources.GetCertPath(testCertName);
TestOutputHelper.WriteLine("Loading " + certPath);
var cert = new X509Certificate2(certPath, "testPassword");
Assert.False(CertificateLoader.DoesCertificateHaveASubjectAlternativeName(cert));
var testLogger = new TestApplicationErrorLogger();
CreateMiddleware(
new HttpsConnectionAdapterOptions
{
ServerCertificate = cert,
},
ListenOptions.DefaultHttpProtocols,
testLogger);
Assert.Single(testLogger.Messages.Where(log => log.EventId == 9));
}
[ConditionalTheory]
[InlineData(HttpProtocols.Http1)]
[InlineData(HttpProtocols.Http2)]
[InlineData(HttpProtocols.Http1AndHttp2)]
[TlsAlpnSupported]
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10)]
public async Task ListenOptionsProtolsCanBeSetAfterUseHttps(HttpProtocols httpProtocols)
{
void ConfigureListenOptions(ListenOptions listenOptions)
{
listenOptions.UseHttps(_x509Certificate2);
listenOptions.Protocols = httpProtocols;
}
await using var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory), ConfigureListenOptions);
using var connection = server.CreateConnection();
var sslOptions = new SslClientAuthenticationOptions
{
TargetHost = "localhost",
EnabledSslProtocols = SslProtocols.None,
ApplicationProtocols = new List<SslApplicationProtocol> { SslApplicationProtocol.Http11, SslApplicationProtocol.Http2 },
};
using var stream = OpenSslStream(connection.Stream);
await stream.AuthenticateAsClientAsync(sslOptions);
Assert.Equal(
httpProtocols.HasFlag(HttpProtocols.Http2) ?
SslApplicationProtocol.Http2 :
SslApplicationProtocol.Http11,
stream.NegotiatedApplicationProtocol);
}
[ConditionalFact]
[OSSkipCondition(OperatingSystems.MacOSX | OperatingSystems.Linux, SkipReason = "Downgrade logic only applies on Windows")]
[MaximumOSVersion(OperatingSystems.Windows, WindowsVersions.Win81)]
public void Http1AndHttp2DowngradeToHttp1ForHttpsOnIncompatibleWindowsVersions()
{
var httpConnectionAdapterOptions = new HttpsConnectionAdapterOptions
{
ServerCertificate = _x509Certificate2,
};
var middleware = CreateMiddleware(httpConnectionAdapterOptions, HttpProtocols.Http1AndHttp2);
Assert.Equal(HttpProtocols.Http1, middleware._httpProtocols);
}
[ConditionalFact]
[OSSkipCondition(OperatingSystems.MacOSX | OperatingSystems.Linux, SkipReason = "Downgrade logic only applies on Windows")]
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10)]
public void Http1AndHttp2DoesNotDowngradeOnCompatibleWindowsVersions()
{
var httpConnectionAdapterOptions = new HttpsConnectionAdapterOptions
{
ServerCertificate = _x509Certificate2,
};
var middleware = CreateMiddleware(httpConnectionAdapterOptions, HttpProtocols.Http1AndHttp2);
Assert.Equal(HttpProtocols.Http1AndHttp2, middleware._httpProtocols);
}
[ConditionalFact]
[OSSkipCondition(OperatingSystems.MacOSX | OperatingSystems.Linux, SkipReason = "Error logic only applies on Windows")]
[MaximumOSVersion(OperatingSystems.Windows, WindowsVersions.Win81)]
public void Http2ThrowsOnIncompatibleWindowsVersions()
{
var httpConnectionAdapterOptions = new HttpsConnectionAdapterOptions
{
ServerCertificate = _x509Certificate2,
};
Assert.Throws<NotSupportedException>(() => CreateMiddleware(httpConnectionAdapterOptions, HttpProtocols.Http2));
}
[ConditionalFact]
[OSSkipCondition(OperatingSystems.MacOSX | OperatingSystems.Linux, SkipReason = "Error logic only applies on Windows")]
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10)]
public void Http2DoesNotThrowOnCompatibleWindowsVersions()
{
var httpConnectionAdapterOptions = new HttpsConnectionAdapterOptions
{
ServerCertificate = _x509Certificate2,
};
// Does not throw
CreateMiddleware(httpConnectionAdapterOptions, HttpProtocols.Http2);
}
private static HttpsConnectionMiddleware CreateMiddleware(X509Certificate2 serverCertificate)
{
return CreateMiddleware(new HttpsConnectionAdapterOptions
{
ServerCertificate = serverCertificate,
},
ListenOptions.DefaultHttpProtocols);
}
private static HttpsConnectionMiddleware CreateMiddleware(HttpsConnectionAdapterOptions options, HttpProtocols httpProtocols, TestApplicationErrorLogger testLogger = null)
{
var loggerFactory = testLogger is null ? (ILoggerFactory)NullLoggerFactory.Instance : new LoggerFactory(new[] { new KestrelTestLoggerProvider(testLogger) });
return new HttpsConnectionMiddleware(context => Task.CompletedTask, options, httpProtocols, loggerFactory, new KestrelMetrics(new TestMeterFactory()));
}
private static HttpsConnectionMiddleware CreateMiddleware(HttpsConnectionAdapterOptions options, HttpProtocols httpProtocols)
{
return new HttpsConnectionMiddleware(context => Task.CompletedTask, options, httpProtocols, new KestrelMetrics(new TestMeterFactory()));
}
private static async Task App(HttpContext httpContext)
{
var request = httpContext.Request;
var response = httpContext.Response;
while (true)
{
var buffer = new byte[8192];
var count = await request.Body.ReadAsync(buffer, 0, buffer.Length);
if (count == 0)
{
break;
}
await response.Body.WriteAsync(buffer, 0, count);
}
}
private static SslStream OpenSslStream(Stream rawStream)
{
return new SslStream(rawStream, false, (sender, certificate, chain, errors) => true);
}
/// <summary>
/// SslStream is used to ensure the certificate is actually passed to the server
/// HttpClient might not send the certificate because it is invalid or it doesn't match any
/// of the certificate authorities sent by the server in the SSL handshake.
/// </summary>
private static SslStream OpenSslStreamWithCert(Stream rawStream, X509Certificate2 clientCertificate = null)
{
return new SslStream(rawStream, false, (sender, certificate, chain, errors) => true,
(sender, host, certificates, certificate, issuers) => clientCertificate ?? _x509Certificate2);
}
private static async Task AssertConnectionResult(SslStream stream, bool success, string body = null)
{
var request = body == null ? Encoding.UTF8.GetBytes("GET / HTTP/1.0\r\n\r\n")
: Encoding.UTF8.GetBytes($"POST / HTTP/1.0\r\nContent-Length: {body.Length}\r\n\r\n{body}");
await stream.WriteAsync(request, 0, request.Length);
var reader = new StreamReader(stream);
string line = null;
if (success)
{
line = await reader.ReadLineAsync();
Assert.Equal("HTTP/1.1 200 OK", line);
}
else
{
try
{
line = await reader.ReadLineAsync();
}
catch (IOException) { }
Assert.Null(line);
}
}
}
|