/
githubmirror
/
aspnetcore
Обзор
Документация
Войти
/
githubmirror
/
aspnetcore
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Servers/HttpSys/test/FunctionalTests/HttpsTests.cs
515 строк
22 KB
Korolev Dmitry
Add TLS channel binding token access to `ITlsConnectionFeature` (#67436)
09 июл 2026, 19:10
Не верифицирован
09 июл 2026, 19:10
1ae00e6
Код
Авторство
О чём код?
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; using System.Net.Http; using System.Runtime.InteropServices; using System.Security.Authentication; using System.Security.Authentication.ExtendedProtection; using System.Security.Cryptography.X509Certificates; using System.Text; using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.HttpSys.Internal; using Microsoft.AspNetCore.InternalTesting; using Windows.Win32.Networking.HttpServer; using Xunit; namespace Microsoft.AspNetCore.Server.HttpSys; public class HttpsTests : LoggedTest { private static readonly X509Certificate2 _x509Certificate2 = TestResources.GetTestCertificate("eku.client.pfx"); [ConditionalFact] public async Task Https_200OK_Success() { using (Utilities.CreateDynamicHttpsServer(out var address, httpContext => { return Task.FromResult(0); }, LoggerFactory)) { string response = await SendRequestAsync(address); Assert.Equal(string.Empty, response); } } [ConditionalFact] public async Task Https_SendHelloWorld_Success() { using (Utilities.CreateDynamicHttpsServer(out var address, httpContext => { byte[] body = Encoding.UTF8.GetBytes("Hello World"); httpContext.Response.ContentLength = body.Length; return httpContext.Response.Body.WriteAsync(body, 0, body.Length); }, LoggerFactory)) { string response = await SendRequestAsync(address); Assert.Equal("Hello World", response); } } [ConditionalFact] public async Task Https_EchoHelloWorld_Success() { using (Utilities.CreateDynamicHttpsServer(out var address, async httpContext => { var input = await new StreamReader(httpContext.Request.Body).ReadToEndAsync(); Assert.Equal("Hello World", input); var body = Encoding.UTF8.GetBytes("Hello World"); httpContext.Response.ContentLength = body.Length; await httpContext.Response.Body.WriteAsync(body, 0, body.Length); }, LoggerFactory)) { string response = await SendRequestAsync(address, "Hello World"); Assert.Equal("Hello World", response); } } [ConditionalTheory] [InlineData(ClientCertificateMethod.NoCertificate)] [InlineData(ClientCertificateMethod.AllowCertificate)] [InlineData(ClientCertificateMethod.AllowRenegotation)] public async Task Https_ClientCertNotSent_ClientCertNotPresent(ClientCertificateMethod clientCertificateMethod) { using (Utilities.CreateDynamicHttpsServer("", out var root, out var address, options => { options.ClientCertificateMethod = clientCertificateMethod; }, async httpContext => { var tls = httpContext.Features.Get<ITlsConnectionFeature>(); Assert.NotNull(tls); Assert.Null(tls.ClientCertificate); var cert = await tls.GetClientCertificateAsync(CancellationToken.None); Assert.Null(cert); Assert.Null(tls.ClientCertificate); })) { var response = await SendRequestAsync(address); Assert.Equal(string.Empty, response); } } [ConditionalTheory] [InlineData(ClientCertificateMethod.NoCertificate)] [InlineData(ClientCertificateMethod.AllowCertificate)] [InlineData(ClientCertificateMethod.AllowRenegotation)] public async Task Https_ClientCertRequested_ClientCertPresent(ClientCertificateMethod clientCertificateMethod) { using (Utilities.CreateDynamicHttpsServer("", out var root, out var address, options => { options.ClientCertificateMethod = clientCertificateMethod; }, async httpContext => { var tls = httpContext.Features.Get<ITlsConnectionFeature>(); Assert.NotNull(tls); Assert.Null(tls.ClientCertificate); var cert = await tls.GetClientCertificateAsync(CancellationToken.None); if (clientCertificateMethod == ClientCertificateMethod.AllowRenegotation) { Assert.NotNull(cert); Assert.NotNull(tls.ClientCertificate); } else { Assert.Null(cert); Assert.Null(tls.ClientCertificate); } })) { Assert.NotNull(_x509Certificate2); var response = await SendRequestAsync(address, _x509Certificate2); Assert.Equal(string.Empty, response); } } [ConditionalFact] [MaximumOSVersion(OperatingSystems.Windows, WindowsVersions.Win7)] public async Task Https_SkipsITlsHandshakeFeatureOnWin7() { using (Utilities.CreateDynamicHttpsServer(out var address, async httpContext => { try { var tlsFeature = httpContext.Features.Get<ITlsHandshakeFeature>(); Assert.Null(tlsFeature); } catch (Exception ex) { await httpContext.Response.WriteAsync(ex.ToString()); } }, LoggerFactory)) { string response = await SendRequestAsync(address); Assert.Equal(string.Empty, response); } } [ConditionalFact] [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win8)] public async Task Https_SetsITlsHandshakeFeature() { using (Utilities.CreateDynamicHttpsServer(out var address, httpContext => { var tlsFeature = httpContext.Features.Get<ITlsHandshakeFeature>(); Assert.NotNull(tlsFeature); return httpContext.Response.WriteAsJsonAsync(tlsFeature); }, LoggerFactory)) { string response = await SendRequestAsync(address); var result = System.Text.Json.JsonDocument.Parse(response).RootElement; var protocol = (SslProtocols)result.GetProperty("protocol").GetInt32(); Assert.True(protocol > SslProtocols.None, "Protocol: " + protocol); Assert.True(Enum.IsDefined(typeof(SslProtocols), protocol), "Defined: " + protocol); // Mapping is required, make sure it's current #pragma warning disable SYSLIB0058 // Type or member is obsolete var cipherAlgorithm = (CipherAlgorithmType)result.GetProperty("cipherAlgorithm").GetInt32(); Assert.True(cipherAlgorithm > CipherAlgorithmType.Null, "Cipher: " + cipherAlgorithm); #pragma warning restore SYSLIB0058 // Type or member is obsolete var cipherStrength = result.GetProperty("cipherStrength").GetInt32(); Assert.True(cipherStrength > 0, "CipherStrength: " + cipherStrength); #pragma warning disable SYSLIB0058 // Type or member is obsolete var hashAlgorithm = (HashAlgorithmType)result.GetProperty("hashAlgorithm").GetInt32(); Assert.True(hashAlgorithm >= HashAlgorithmType.None, "HashAlgorithm: " + hashAlgorithm); #pragma warning restore SYSLIB0058 // Type or member is obsolete var hashStrength = result.GetProperty("hashStrength").GetInt32(); Assert.True(hashStrength >= 0, "HashStrength: " + hashStrength); // May be 0 for some algorithms #pragma warning disable SYSLIB0058 // Type or member is obsolete var keyExchangeAlgorithm = (ExchangeAlgorithmType)result.GetProperty("keyExchangeAlgorithm").GetInt32(); Assert.True(keyExchangeAlgorithm >= ExchangeAlgorithmType.None, "KeyExchangeAlgorithm: " + keyExchangeAlgorithm); #pragma warning restore SYSLIB0058 // Type or member is obsolete var keyExchangeStrength = result.GetProperty("keyExchangeStrength").GetInt32(); Assert.True(keyExchangeStrength >= 0, "KeyExchangeStrength: " + keyExchangeStrength); if (Environment.OSVersion.Version > new Version(10, 0, 19043, 0)) { var hostName = result.GetProperty("hostName").ToString(); Assert.Equal("localhost", hostName); } } } [ConditionalFact] [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win8)] public async Task Https_ITlsHandshakeFeature_MatchesIHttpSysExtensionInfoFeature() { using (Utilities.CreateDynamicHttpsServer(out var address, async httpContext => { try { var tlsFeature = httpContext.Features.Get<ITlsHandshakeFeature>(); var requestInfoFeature = httpContext.Features.Get<IHttpSysRequestInfoFeature>(); Assert.NotNull(tlsFeature); Assert.NotNull(requestInfoFeature); Assert.True(requestInfoFeature.RequestInfo.Count > 0); var tlsInfo = requestInfoFeature.RequestInfo[(int)HTTP_REQUEST_INFO_TYPE.HttpRequestInfoTypeSslProtocol]; HTTP_SSL_PROTOCOL_INFO tlsCopy; unsafe { using var handle = tlsInfo.Pin(); tlsCopy = Marshal.PtrToStructure<HTTP_SSL_PROTOCOL_INFO>((IntPtr)handle.Pointer); } // Assert.Equal(tlsFeature.Protocol, tlsCopy.Protocol); // These don't directly match because the native and managed enums use different values. #pragma warning disable SYSLIB0058 // Type or member is obsolete Assert.Equal((uint)tlsFeature.CipherAlgorithm, tlsCopy.CipherType); Assert.Equal(tlsFeature.CipherStrength, (int)tlsCopy.CipherStrength); Assert.Equal((uint)tlsFeature.HashAlgorithm, tlsCopy.HashType); Assert.Equal(tlsFeature.HashStrength, (int)tlsCopy.HashStrength); Assert.Equal((uint)tlsFeature.KeyExchangeAlgorithm, tlsCopy.KeyExchangeType); Assert.Equal(tlsFeature.KeyExchangeStrength, (int)tlsCopy.KeyExchangeStrength); #pragma warning restore SYSLIB0058 // Type or member is obsolete } catch (Exception ex) { await httpContext.Response.WriteAsync(ex.ToString()); } }, LoggerFactory)) { string response = await SendRequestAsync(address); Assert.Equal(string.Empty, response); } } [ConditionalFact] [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10_20H2)] public async Task Https_SetsIHttpSysRequestPropertyFeature() { using (Utilities.CreateDynamicHttpsServer(out var address, async httpContext => { try { var requestPropertyFeature = httpContext.Features.Get<IHttpSysRequestPropertyFeature>(); Assert.NotNull(requestPropertyFeature); } catch (Exception ex) { await httpContext.Response.WriteAsync(ex.ToString()); } }, LoggerFactory)) { string response = await SendRequestAsync(address); Assert.Equal(string.Empty, response); } } [ConditionalFact] [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10_20H2)] public async Task Https_TryGetRequestProperty_TlsCipherInfo_RoundTrips() { using (Utilities.CreateDynamicHttpsServer(out var address, async httpContext => { try { var feature = httpContext.Features.Get<IHttpSysRequestPropertyFeature>(); Assert.NotNull(feature); // TlsCipherInfo is available on any HTTPS request without per-binding configuration, // unlike TlsClientHello which requires HTTP_SERVICE_CONFIG_SSL_FLAG_ENABLE_CACHE_CLIENT_HELLO. // The buffer is generously sized so the API can write its (fixed-size) struct without // us having to know the exact size up front. var propertyId = (int)HTTP_REQUEST_PROPERTY.HttpRequestPropertyTlsCipherInfo; var buffer = new byte[4096]; Assert.True(feature.TryGetRequestProperty(propertyId, qualifier: default, output: buffer, out var written)); Assert.InRange(written, 1, buffer.Length); // Buffer too small returns false. Some HTTP_REQUEST_PROPERTY values report the required // size in `bytesReturned`, others do not, so we only assert the false return here. var tooSmall = new byte[1]; Assert.False(feature.TryGetRequestProperty(propertyId, qualifier: default, output: tooSmall, out _)); } catch (Exception ex) { await httpContext.Response.WriteAsync(ex.ToString()); } }, LoggerFactory)) { string response = await SendRequestAsync(address); Assert.Equal(string.Empty, response); } } [ConditionalFact] [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10_20H2)] public async Task Https_SetsIHttpSysRequestTimingFeature() { using (Utilities.CreateDynamicHttpsServer(out var address, async httpContext => { try { var requestTimingFeature = httpContext.Features.Get<IHttpSysRequestTimingFeature>(); Assert.NotNull(requestTimingFeature); Assert.True(requestTimingFeature.Timestamps.Length > (int)HttpSysRequestTimingType.Http3HeaderDecodeEnd); Assert.True(requestTimingFeature.TryGetTimestamp(HttpSysRequestTimingType.RequestHeaderParseStart, out var headerStart)); Assert.True(requestTimingFeature.TryGetTimestamp(HttpSysRequestTimingType.RequestHeaderParseEnd, out var headerEnd)); Assert.True(requestTimingFeature.TryGetElapsedTime(HttpSysRequestTimingType.RequestHeaderParseStart, HttpSysRequestTimingType.RequestHeaderParseEnd, out var elapsed)); Assert.Equal(Stopwatch.GetElapsedTime(headerStart, headerEnd), elapsed); Assert.False(requestTimingFeature.TryGetTimestamp(HttpSysRequestTimingType.Http3StreamStart, out var streamStart)); Assert.False(requestTimingFeature.TryGetTimestamp((HttpSysRequestTimingType)int.MaxValue, out var invalid)); Assert.False(requestTimingFeature.TryGetElapsedTime(HttpSysRequestTimingType.Http3StreamStart, HttpSysRequestTimingType.RequestHeaderParseStart, out elapsed)); } catch (Exception ex) { await httpContext.Response.WriteAsync(ex.ToString()); } }, LoggerFactory)) { string response = await SendRequestAsync(address); Assert.Equal(string.Empty, response); } } [ConditionalFact] [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10_20H2)] public async Task Https_TryGetChannelBindingBytes_Endpoint_ReturnsValidToken() { Exception caught = null; using (Utilities.CreateDynamicHttpsServer( "/", out _, out var address, options => { /* default: HttpAuthenticationHardeningLevel.Medium */ }, async httpContext => { try { var feature = httpContext.Features.Get<ITlsConnectionFeature>(); Assert.NotNull(feature); Assert.True(feature.TryGetChannelBindingBytes(ChannelBindingKind.Endpoint, out var bytes)); Assert.False(bytes.IsEmpty); // The buffer is a SEC_CHANNEL_BINDINGS struct (32-byte header) followed by // the application data: ASCII "tls-server-end-point:" + SHA-256 of the server cert. var raw = bytes.Span; Assert.True(raw.Length >= 32, "Buffer should be at least the header size."); var appDataLength = System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(raw.Slice(24, 4)); var appDataOffset = System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(raw.Slice(28, 4)); Assert.True(appDataOffset + appDataLength <= raw.Length); var appData = raw.Slice((int)appDataOffset, (int)appDataLength); var prefix = "tls-server-end-point:"u8; Assert.True(appData.StartsWith(prefix)); Assert.Equal(prefix.Length + 32 /* SHA-256 length */, appData.Length); } catch (Exception ex) { caught = ex; await httpContext.Response.WriteAsync(ex.ToString()); } }, LoggerFactory)) { string response = await SendRequestAsync(address); Assert.Null(caught); Assert.Equal(string.Empty, response); } } [ConditionalFact] [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10_20H2)] public async Task Https_TryGetChannelBindingBytes_LegacyHardening_ReturnsFalse() { Exception caught = null; using (Utilities.CreateDynamicHttpsServer( "/", out _, out var address, options => options.HttpAuthenticationHardeningLevel = HttpAuthenticationHardeningLevel.Legacy, async httpContext => { try { var feature = httpContext.Features.Get<ITlsConnectionFeature>(); Assert.NotNull(feature); // Legacy is the fast-path — the flag isn't set on the URL group so http.sys // does not deliver per-request CBT, and we short-circuit before asking. Assert.False(feature.TryGetChannelBindingBytes(ChannelBindingKind.Endpoint, out var bytes)); Assert.True(bytes.IsEmpty); } catch (Exception ex) { caught = ex; await httpContext.Response.WriteAsync(ex.ToString()); } }, LoggerFactory)) { string response = await SendRequestAsync(address); Assert.Null(caught); Assert.Equal(string.Empty, response); } } [ConditionalFact] [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10_20H2)] public async Task Https_TryGetChannelBindingBytes_UnsupportedKind_ReturnsFalse() { Exception caught = null; using (Utilities.CreateDynamicHttpsServer( "/", out _, out var address, options => { }, async httpContext => { try { var feature = httpContext.Features.Get<ITlsConnectionFeature>(); Assert.NotNull(feature); // http.sys only exposes tls-server-end-point (Endpoint); other kinds are rejected. Assert.False(feature.TryGetChannelBindingBytes(ChannelBindingKind.Unique, out var uniqueBytes)); Assert.True(uniqueBytes.IsEmpty); Assert.False(feature.TryGetChannelBindingBytes(ChannelBindingKind.Unknown, out var unknownBytes)); Assert.True(unknownBytes.IsEmpty); } catch (Exception ex) { caught = ex; await httpContext.Response.WriteAsync(ex.ToString()); } }, LoggerFactory)) { string response = await SendRequestAsync(address); Assert.Null(caught); Assert.Equal(string.Empty, response); } } private async Task<string> SendRequestAsync(string uri, X509Certificate cert = null) { var handler = new HttpClientHandler(); handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; if (cert != null) { handler.ClientCertificates.Add(cert); } using HttpClient client = new HttpClient(handler); return await client.GetStringAsync(uri); } private async Task<string> SendRequestAsync(string uri, string upload) { var handler = new WinHttpHandler(); handler.ServerCertificateValidationCallback = (a, b, c, d) => true; using (HttpClient client = new HttpClient(handler)) { HttpResponseMessage response = await client.PostAsync(uri, new StringContent(upload)); response.EnsureSuccessStatusCode(); return await response.Content.ReadAsStringAsync(); } } private X509Certificate2 FindClientCert() { var store = new X509Store(); store.Open(OpenFlags.ReadOnly); foreach (var cert in store.Certificates) { bool isClientAuth = false; bool isSmartCard = false; foreach (var extension in cert.Extensions) { var eku = extension as X509EnhancedKeyUsageExtension; if (eku != null) { foreach (var oid in eku.EnhancedKeyUsages) { if (oid.FriendlyName == "Client Authentication") { isClientAuth = true; } else if (oid.FriendlyName == "Smart Card Logon") { isSmartCard = true; break; } } } } if (isClientAuth && !isSmartCard) { return cert; } } return null; } }