From e9f91c66cf242d819455dc1cb49754f6bac9f05b Mon Sep 17 00:00:00 2001 From: Matt Connew Date: Fri, 2 Oct 2020 18:46:45 -0700 Subject: [PATCH] Add limited support for SpnEndpointIdentity with HTTP --- .../src/Resources/Strings.resx | 14 +++++-- ...tWebSocketTransportDuplexSessionChannel.cs | 13 ++---- .../Channels/HttpChannelFactory.cs | 9 ++-- .../Channels/HttpsChannelFactory.cs | 27 ++++++++++++ .../Channels/TransportSecurityHelpers.cs | 40 ++++++++++++++---- .../tests/ServiceModel/HttpIdentityTests.cs | 41 +++++++++++++++++++ 6 files changed, 120 insertions(+), 24 deletions(-) create mode 100644 src/System.ServiceModel.Http/tests/ServiceModel/HttpIdentityTests.cs diff --git a/src/System.Private.ServiceModel/src/Resources/Strings.resx b/src/System.Private.ServiceModel/src/Resources/Strings.resx index b79fde3ba0e..db400c4be97 100644 --- a/src/System.Private.ServiceModel/src/Resources/Strings.resx +++ b/src/System.Private.ServiceModel/src/Resources/Strings.resx @@ -2481,9 +2481,6 @@ A signed supporting token is not expected in the security header in this context. - - Crypto algorithm '{0}' not supported in this context. - Security token manager could not parse token with name '{0}', namespace '{1}', valueType '{2}'. @@ -2820,4 +2817,13 @@ The DeliveryRequirementsAttribute on contract '{0}' specifies a QueuedDeliveryRequirements value of NotAllowed. However, the configured binding for this contract specifies that it does support queued delivery. A queued binding may not be used with this contract. - \ No newline at end of file + + The HTTPS channel factory does not support explicit specification of an identity in the EndpointAddress unless the authentication scheme is NTLM or Negotiate. + + + The endpoint identity specified when creating the HTTPS channel to '{0}' contains multiple server certificates. However, the HTTPS transport only supports the specification of a single server certificate. In order to create an HTTPS channel, please specify no more than one server certificate in the endpoint identity. + + + Only HOST and HTTP service principal names are supported . + + diff --git a/src/System.Private.ServiceModel/src/System/ServiceModel/Channels/ClientWebSocketTransportDuplexSessionChannel.cs b/src/System.Private.ServiceModel/src/System/ServiceModel/Channels/ClientWebSocketTransportDuplexSessionChannel.cs index 3c64ab29427..3302de8d6b4 100644 --- a/src/System.Private.ServiceModel/src/System/ServiceModel/Channels/ClientWebSocketTransportDuplexSessionChannel.cs +++ b/src/System.Private.ServiceModel/src/System/ServiceModel/Channels/ClientWebSocketTransportDuplexSessionChannel.cs @@ -21,7 +21,6 @@ internal class ClientWebSocketTransportDuplexSessionChannel : WebSocketTransport private SecurityTokenProviderContainer _webRequestTokenProvider; private SecurityTokenProviderContainer _webRequestProxyTokenProvider; private volatile bool _cleanupStarted; - private volatile bool _cleanupIdentity; public ClientWebSocketTransportDuplexSessionChannel(HttpChannelFactory channelFactory, ClientWebSocketFactory connectionFactory, EndpointAddress remoteAddress, Uri via) : base(channelFactory, remoteAddress, via) @@ -67,14 +66,6 @@ protected internal override async Task OnOpenAsync(TimeSpan timeout) ChannelParameterCollection channelParameterCollection = new ChannelParameterCollection(); - if (HttpChannelFactory.MapIdentity(RemoteAddress, _channelFactory.AuthenticationScheme)) - { - lock (ThisLock) - { - _cleanupIdentity = HttpTransportSecurityHelpers.AddIdentityMapping(Via, RemoteAddress); - } - } - X509Certificate2 clientCertificate = null; HttpsChannelFactory httpsChannelFactory = _channelFactory as HttpsChannelFactory; if (httpsChannelFactory != null && httpsChannelFactory.RequireClientCertificate) @@ -207,6 +198,10 @@ private async Task CreateWebSocketWithFactoryAsync(X509Certificate2 c { headers[WebSocketTransportSettings.BinaryEncoderTransferModeHeader] = _channelFactory.TransferMode.ToString(); } + if (HttpChannelFactory.MapIdentity(RemoteAddress, _channelFactory.AuthenticationScheme)) + { + headers[HttpRequestHeader.Host] = HttpTransportSecurityHelpers.GeIdentityHostHeader(RemoteAddress); + } var credentials = _channelFactory.GetCredentials(); ws = await _connectionFactory.CreateWebSocketAsync(Via, headers, credentials, WebSocketSettings.Clone(), timeoutHelper); diff --git a/src/System.Private.ServiceModel/src/System/ServiceModel/Channels/HttpChannelFactory.cs b/src/System.Private.ServiceModel/src/System/ServiceModel/Channels/HttpChannelFactory.cs index 898930a238c..8906a3936ba 100644 --- a/src/System.Private.ServiceModel/src/System/ServiceModel/Channels/HttpChannelFactory.cs +++ b/src/System.Private.ServiceModel/src/System/ServiceModel/Channels/HttpChannelFactory.cs @@ -990,6 +990,11 @@ public HttpClientChannelAsyncRequest(HttpClientRequestChannel channel) public async Task SendRequestAsync(Message message, TimeoutHelper timeoutHelper) { _timeoutHelper = timeoutHelper; + if (_channel.Factory.MapIdentity(_to)) + { + HttpTransportSecurityHelpers.AddIdentityMapping(_to, message); + } + _factory.ApplyManualAddressing(ref _to, ref _via, message); _httpClient = await _channel.GetHttpClientAsync(_to, _via, _timeoutHelper); @@ -1202,10 +1207,6 @@ private bool PrepareMessageHeaders(Message message) _httpRequestMessage.Headers.Expect.TryParseAdd(value); } } - else if (string.Compare(name, "host", StringComparison.OrdinalIgnoreCase) == 0) - { - // this should be controlled through Via - } else if (string.Compare(name, "referer", StringComparison.OrdinalIgnoreCase) == 0) { // referrer is proper spelling, but referer is the what is in the protocol. diff --git a/src/System.Private.ServiceModel/src/System/ServiceModel/Channels/HttpsChannelFactory.cs b/src/System.Private.ServiceModel/src/System/ServiceModel/Channels/HttpsChannelFactory.cs index 17d59acb687..6f3f7d43642 100644 --- a/src/System.Private.ServiceModel/src/System/ServiceModel/Channels/HttpsChannelFactory.cs +++ b/src/System.Private.ServiceModel/src/System/ServiceModel/Channels/HttpsChannelFactory.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. +using System.IdentityModel.Claims; using System.IdentityModel.Selectors; using System.IdentityModel.Tokens; using System.Net.Http; @@ -59,6 +60,32 @@ public override T GetProperty() protected override void ValidateCreateChannelParameters(EndpointAddress remoteAddress, Uri via) { + if (remoteAddress.Identity != null) + { + X509CertificateEndpointIdentity certificateIdentity = + remoteAddress.Identity as X509CertificateEndpointIdentity; + if (certificateIdentity != null) + { + if (certificateIdentity.Certificates.Count > 1) + { + throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgument(nameof(remoteAddress), SR.Format( + SR.HttpsIdentityMultipleCerts, remoteAddress.Uri)); + } + } + + EndpointIdentity identity = remoteAddress.Identity; + bool validIdentity = (certificateIdentity != null) + || ClaimTypes.Spn.Equals(identity.IdentityClaim.ClaimType) + || ClaimTypes.Upn.Equals(identity.IdentityClaim.ClaimType) + || ClaimTypes.Dns.Equals(identity.IdentityClaim.ClaimType); + + if (!IsWindowsAuth(AuthenticationScheme) + && !validIdentity) + { + throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgument(nameof(remoteAddress), SR.HttpsExplicitIdentity); + } + } + if (string.Compare(via.Scheme, "wss", StringComparison.OrdinalIgnoreCase) != 0) { ValidateScheme(via); diff --git a/src/System.Private.ServiceModel/src/System/ServiceModel/Channels/TransportSecurityHelpers.cs b/src/System.Private.ServiceModel/src/System/ServiceModel/Channels/TransportSecurityHelpers.cs index e895d209e45..a805df43618 100644 --- a/src/System.Private.ServiceModel/src/System/ServiceModel/Channels/TransportSecurityHelpers.cs +++ b/src/System.Private.ServiceModel/src/System/ServiceModel/Channels/TransportSecurityHelpers.cs @@ -16,6 +16,7 @@ using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Security.Principal; +using System.ServiceModel.Diagnostics; using System.ServiceModel.Security; using System.ServiceModel.Security.Tokens; using System.Text; @@ -328,17 +329,42 @@ internal static class HttpTransportSecurityHelpers { private static Dictionary s_targetNameCounter = new Dictionary(); - public static bool AddIdentityMapping(Uri via, EndpointAddress target) + public static void AddIdentityMapping(EndpointAddress target, Message message) { - // On Desktop, we do mutual auth when the EndpointAddress has an identity. We need - // support from HttpClient before any functionality can be added here. - return false; + var hostHeader = GeIdentityHostHeader(target); + HttpRequestMessageProperty requestProperty; + if (!message.Properties.TryGetValue(HttpRequestMessageProperty.Name, out requestProperty)) + { + requestProperty = new HttpRequestMessageProperty(); + message.Properties.Add(HttpRequestMessageProperty.Name, requestProperty); + } + + requestProperty.Headers[HttpRequestHeader.Host] = hostHeader; } - public static void RemoveIdentityMapping(Uri via, EndpointAddress target, bool validateState) + public static string GeIdentityHostHeader(EndpointAddress target) { - // On Desktop, we do mutual auth when the EndpointAddress has an identity. We need - // support from HttpClient before any functionality can be added here. + EndpointIdentity identity = target.Identity; + string value; + if (identity != null && !(identity is X509CertificateEndpointIdentity)) + { + value = SecurityUtils.GetSpnFromIdentity(identity, target); + } + else + { + value = SecurityUtils.GetSpnFromTarget(target); + } + + // HttpClientHandler supports specifying the SPN via the HOST header. The service name is hard coded to "HTTP/". "HTTP/" + // is an alias for the "HOST/" service name so we accept either but can't accept anything else. + if (!(value.StartsWith("host/", StringComparison.OrdinalIgnoreCase) || value.StartsWith("http/", StringComparison.OrdinalIgnoreCase))) + { + throw Fx.Exception.AsError(new InvalidOperationException(SR.OnlyDefaultSpnServiceSupported)); + } + + // The leading service name has been constrained to be either "HTTP/" or "HOST/" which are both 5 charactes long. + // This needs to be removed to provide just the hostname part for the Host header. + return value.Substring(5); } public static void AddServerCertIdentityValidation(HttpClientHandler httpClientHandler, EndpointAddress to) diff --git a/src/System.ServiceModel.Http/tests/ServiceModel/HttpIdentityTests.cs b/src/System.ServiceModel.Http/tests/ServiceModel/HttpIdentityTests.cs new file mode 100644 index 00000000000..16e10efba33 --- /dev/null +++ b/src/System.ServiceModel.Http/tests/ServiceModel/HttpIdentityTests.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.ServiceModel; +using System.Text; +using System.Threading.Channels; +using Infrastructure.Common; +using Xunit; + +public static class HttpIdentityTests +{ + [WcfFact] + public static void SpnEndpointIdentity_NotSupportedThrows() + { + var identity = EndpointIdentity.CreateSpnIdentity("SERVICE/serverhostname"); + var endpointAddress = new EndpointAddress(new Uri("https://serverhostname/fakeService.svc"), identity); + var binding = new BasicHttpsBinding(BasicHttpsSecurityMode.Transport); + binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Windows; + var factory = new ChannelFactory(binding, endpointAddress); + var channel = factory.CreateChannel(); + Assert.Throws(() => channel.Echo("")); + } + + [WcfFact] + public static void UpnEndpointIdentityThrows() + { + var identity = EndpointIdentity.CreateUpnIdentity("user@contoso.com"); + var endpointAddress = new EndpointAddress(new Uri("https://serverhostname/fakeService.svc"), identity); + var binding = new BasicHttpsBinding(BasicHttpsSecurityMode.Transport); + binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Windows; + var factory = new ChannelFactory(binding, endpointAddress); + var channel = factory.CreateChannel(); + Assert.Throws(() => channel.Echo("")); + } + + [ServiceContract] + public interface IService + { + [OperationContract] + string Echo(string value); + } +}