Установите SecurityProtocol (Ssl3 или TLS) в.net HttpWebRequest для каждого запроса.

Мое приложение (.net 3.5 sp1) использует HttpWebRequest для связи с разными конечными точками, иногда через HTTPS, где у каждого хост-сервера могут быть разные требования к протоколу безопасности, например, TLS или SSL3, или и то и другое.

Обычно серверы играют хорошо и с радостью договариваются / отказываются от того, какой SecurityProtocol использует TLS или SSL3, но некоторые этого не делают, и когда.net настроен как TLS или SSL3 (по умолчанию, я думаю), те серверы, которые поддерживают только SSL3, вызывают.net бросить ошибку отправки.

Из того, что я могу сказать,.net предоставляет объекту ServicePointManager свойство SecurityProtocol, которое может быть установлено в TLS, SSL3 или оба. Следовательно, в идеале, когда задано и то и другое, идея заключается в том, что клиент и сервер должны договариваться о том, что использовать, но, как уже было сказано, это не работает.

Предположительно вы могли бы установить ServicePointManager.SecurityProtocol = Ssl3, но как насчет конечных точек, которые хотят использовать TLS?

Проблема, которую я вижу с ServicePointManager и SecurityProtocol, заключается в том, что он статичен и, следовательно, имеет широкую область применения.

Так что к вопросу..

Как мне использовать HttpWebRequest с другим SecurityProtocol, например

1) URL 1 установлен для использования TLS | Ssl3 (переговоры)

2) URL 2 установлен на Ssl3 (только Ssl3)

12 ответов

Решение

К сожалению, не похоже, что вы можете настроить это для каждой точки обслуживания. Я бы посоветовал вам подать запрос на добавление функций на веб-сайте MS Connect для этой области.

В качестве грязного обходного пути вы можете попробовать запустить сайты, для которых требуется другой протокол безопасности, в новом домене приложения. Статические экземпляры для каждого домена приложения, так что это должно дать вам необходимую изоляцию.

У меня была та же проблема, и я написал прокси-класс, который открывает порт на локальном хосте и перенаправляет весь трафик на указанный хост: порт.

так что соединение идет так

[ваш код] --- HTTP ---> [прокси на локальном хосте: порт] --- HTTPS ---> [веб-сайт]

фактически его можно использовать для переноса любого протокола в SSL/TLS, а не только в HTTP

using System;
using System.IO;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;

namespace System
{
    class sslProxy : IDisposable
    {
        readonly string host;
        readonly int port;
        readonly TcpListener listener;
        readonly SslProtocols sslProtocols;
        bool disposed;
        static readonly X509CertificateCollection sertCol = new X509CertificateCollection();
        public sslProxy(string url, SslProtocols protocols)
        {
            var uri = new Uri(url);
            host = uri.Host;
            port = uri.Port;
            sslProtocols = protocols;
            listener = new TcpListener(IPAddress.Loopback, 0);
            listener.Start();
            listener.BeginAcceptTcpClient(onAcceptTcpClient, null);
            Proxy = new WebProxy("localhost", (listener.LocalEndpoint as IPEndPoint).Port);
        }
        public WebProxy Proxy
        {
            get;
            private set;
        }
        class stBuf
        {
            public TcpClient tcs;
            public TcpClient tcd;
            public Stream sts;
            public Stream std;
            public byte[] buf;
            public stBuf dup;
        }
        void onAcceptTcpClient(IAsyncResult ar)
        {
            if (disposed) return;
            var tcl = listener.EndAcceptTcpClient(ar);
            TcpClient tcr = null;
            try
            {
                listener.BeginAcceptTcpClient(onAcceptTcpClient, null);
                var nsl = tcl.GetStream();

                tcr = new TcpClient(host, port);
                Stream nsr = tcr.GetStream();
                if (sslProtocols != SslProtocols.None)
                {
                    var sss = new SslStream(nsr, true);
                    sss.AuthenticateAsClient(host, sertCol, sslProtocols, false);
                    nsr = sss;
                } // if

                var sts = new stBuf() { tcs = tcl, sts = nsl, tcd = tcr, std = nsr, buf = new byte[tcl.ReceiveBufferSize] };
                var std = new stBuf() { tcs = tcr, sts = nsr, tcd = tcl, std = nsl, buf = new byte[tcr.ReceiveBufferSize] };
                sts.dup = std;
                std.dup = sts;

                nsl.BeginRead(sts.buf, 0, sts.buf.Length, onReceive, sts);
                nsr.BeginRead(std.buf, 0, std.buf.Length, onReceive, std);
            } // try
            catch
            {
                tcl.Close();
                if (tcr != null) tcr.Close();
            } // catch
        }
        void close(stBuf st)
        {
            var dup = st.dup;
            if (dup != null)
            {
                dup.dup = st.dup = null;
                st.sts.Dispose();
                st.std.Dispose();
            } // if
        }
        void onReceive(IAsyncResult ar)
        {
            var st = ar.AsyncState as stBuf;
            try
            {
                if (!(st.dup != null && st.tcs.Connected && st.sts.CanRead && !disposed)) { close(st); return; };
                var n = st.sts.EndRead(ar);
                if (!(n > 0 && st.tcd.Connected && st.std.CanWrite)) { close(st); return; };
                st.std.Write(st.buf, 0, n);
                if (!(st.tcs.Connected && st.tcd.Connected && st.sts.CanRead && st.std.CanWrite)) { close(st); return; };
                st.sts.BeginRead(st.buf, 0, st.buf.Length, onReceive, st);
            } // try
            catch
            {
                close(st);
            } // catch
        }
        public void Dispose()
        {
            if (!disposed)
            {
                disposed = true;
                listener.Stop();
            } // if
        }
    }
}

пример использования

// create proxy once and keep it
// note you have to mention :443 port (https default)
// ssl protocols to use (enum can use | or + to have many)
var p = new sslProxy("http://www.google.com:443", SslProtocols.Tls);
// using our connections
for (int i=0; i<5; i++)
{
    // url here goes without https just http
    var rq = HttpWebRequest.CreateHttp("http://www.google.com/") as HttpWebRequest;
    // specify that we are connecting via proxy
    rq.Proxy = p.Proxy;
    var rs = rq.GetResponse() as HttpWebResponse;
    var r = new StreamReader(rs.GetResponseStream()).ReadToEnd();
    rs.Dispose();
} // for
// just dispose proxy once done
p.Dispose();

После того, как некоторые из наших поставщиков прекратили поддержку ssl3, в то время как другие используют ее исключительно, в нашей системе появилось много проблем, которые могли бы быть решены с помощью функциональности из этого вопроса. Но шесть лет спустя у нас все еще нет встроенного механизма для достижения этой цели. Наш обходной путь заключается в явном определении протокола безопасности, который будет поддерживать все сценарии, например:

    System.Net.ServicePointManager.SecurityProtocol = 
    System.Net.SecurityProtocolType.Ssl3 
    | System.Net.SecurityProtocolType.Tls12 
    | SecurityProtocolType.Tls11 
    | SecurityProtocolType.Tls;

В соответствии с этим ответом параметр SecurityProtocol фактически относится к домену приложений, поэтому вы можете, если вы решили, что он будет работать, создать отдельные домены приложений для отдельных параметров и распределить запросы по всем параметрам.

Не совсем "аккуратное" решение, но может просто сделать то, что вам нужно, без обращения к сторонним библиотекам.

Я столкнулся с той же проблемой и нашел решение, использующее отражение. В исходном коде для HttpWebRequest вы можете найти внутреннее свойство SslProtocols, которое устанавливается в конструкторе во время создания с текущим значением из ServicePointManager.SecurityProtocol . Итак, после создания HttpWebRequest и перед его выполнением установите для этого свойства соответствующий протокол безопасности :

      var request = (HttpWebRequest)WebRequest.Create(endpoint);
typeof(HttpWebRequest)
.GetProperty("SslProtocols", BindingFlags.NonPublic | BindingFlags.Instance)
.SetValue(request, System.Security.Authentication.SslProtocols.Tls12);

Начиная с Net 4.5, для Windows (от Microsoft) есть пакет HgetpClient и WinHttpHandler, позволяющий устанавливать параметры SslProtocols. Вы можете использовать класс HttpClientHandler для сетевого ядра.

using (var hc = new HttpClient(new WinHttpHandler() // should have it as a static member
{
    AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip,
    SslProtocols = SslProtocols.Tls | 
                   SslProtocols.Tls11 | 
                   SslProtocols.Tls12
}))
{
    var r = hc.SendAsync(new HttpRequestMessage(HttpMethod.Get, "https://..."));
    r.Wait();
    Console.WriteLine(r.Result.StatusCode);
} // using

Вы можете достичь этого с помощью этого кода, чтобы закрыть все базовые соединения и вызвать новое рукопожатие.

HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri);
...
...
...
request.ServicePoint.CloseConnectionGroup(request.ConnectionGroupName);

Я знаю, что этот вопрос старый, но проблема остается даже с.Net 4.7.2. В моем случае у меня есть многопоточное приложение, которое взаимодействует с двумя конечными точками. Одна конечная точка работает только с TLS 1.2, а другая конечная точка работает только с TLS 1.0 (команда, ответственная за это, работает над исправлением своей конечной точки, поэтому она также будет поддерживать TLS 1.2).

Чтобы обойти это, я переместил вызовы службы для конечной точки, которая работает только с TLS 1.0, в отдельный класс в той же сборке, а затем загрузил сборку в отдельный домен приложения. Делая это, я могу использовать эту глобальную переменную:

System.Net.ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls; 

только для вызовов к сломанной конечной точке, в то же время продолжая работать и с вызовами к конечной точке TLS 1.2 (для которых не требуется устанавливать ServicePointManager.SecurityProtocol что-либо конкретное). Это также гарантирует, что когда хорошая конечная точка будет обновлена ​​до TLS 1.3, мне не нужно будет повторно выпускать мое приложение. Мое приложение многопоточное и имеет большую емкость, поэтому блокировка или попытка / окончание не являются адекватными решениями.

Вот код, который я использовал для загрузки сборки в отдельный домен. Обратите внимание, что я загружаю сборку из ее текущего местоположения (временные файлы Aspnet), чтобы она не блокировала сборку в каталоге bin и не блокировала будущие развертывания.

Также обратите внимание, что прокси-класс наследует MarshalByRefObject, поэтому он используется в качестве прозрачного прокси, который сохраняет System.Net.ServicePointManager со своим собственным значением в своем собственном домене приложений.

Это кажется глупым ограничением со стороны.Net Framework, я хотел бы, чтобы мы могли просто указать протокол непосредственно в веб-запросе, а не прыгать через обручи, особенно после нескольких лет этого.:(

Этот код работает, надеюсь, он вам поможет!:)

private static AppDomain _proxyDomain = null;
private static Object _syncObject = new Object();

public void MakeACallToTls10Endpoint(string tls10Endpoint, string jsonRequest)
{
   if (_proxyDomain == null)
   {
      lock(_syncObject) // Only allow one thread to spin up the app domain.
      {
         if (_proxyDomain == null)
         {
            _proxyDomain = AppDomain.CreateDomain("CommunicationProxyDomain");
         }
      }
   }

   Type communicationProxyType = typeof(CommunicationProxy);
   string assemblyPath = communicationProxyType.Assembly.Location;

   // Always loading from the current assembly, sometimes this moves around in ASPNet Tempfiles causing type not found errors if you make it static.
   ObjectHandle objectHandle = _proxyDomain.CreateInstanceFrom(assemblyPath, communicationProxyType.FullName.Split(',')[0]);
   CommunicationProxy communicationProxy = (CommunicationProxy)objectHandle.Unwrap();

   return communicationProxy.ExecuteHttpPost(tls10Endpoint, jsonRequest);
}

Затем в отдельный класс:

[Serializable]
public class CommunicationProxy : MarshalByRefObject
{
   public string ExecuteHttpPost(string tls10Endpoint, string jsonRequest)
   {
      ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls;

      // << Bunch of code to do the request >>
   }
}

Вы можете создать HttpWebRequest "служебный класс" с помощью статического служебного метода для выполнения HttpWebRequests. В методе статической утилиты используйте оператор C# lock для настройки ServicePointManager.SecurityProtocol и создания определенного HttpWebRequest. Оператор блокировки не позволяет другим потокам из того же AppDomain выполнять тот же код в одно и то же время, поэтому только что установленный протокол TLS не будет изменен, пока не будет выполнен весь блок блокировки (= критическая секция).

Но помните, что для действительно высокопроизводительных приложений (чрезвычайно высокопроизводительных!) Такой подход может оказать негативное влияние на производительность.

Установите все это. В моем приложении это работа для разных протоколов безопасности.

System.Net.ServicePointManager.SecurityProtocol = 
System.Net.SecurityProtocolType.Ssl3 
| System.Net.SecurityProtocolType.Tls12 
| SecurityProtocolType.Tls11 
| SecurityProtocolType.Tls;

Немного упрощенно. |= важно.

      ServicePointManager.SecurityProtocol |= SecurityProtocolType.Ssl3 | SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls;

Если вы используете WCF System.ServiceModelэто невозможно сделать в .net4.8, но в версии .netCore есть такая возможность.

Вы не можете полностью заменить, так как отсутствуют многие компоненты, но это может работать только для простых вызовов некоторых API.

Вы можете загрузить обе версии в один и тот же проект, но вам нужно использовать extern aliasразрешать конфликты имен.


Установка

Сначала вам нужно установить NuGet в проект.

Изменить свойства в проекте Referencesи установить Aliasesнапример ServiceModelHack.

Обновление веб-сервиса

В Reference.csданного веб-сервиса в проекте Service References

Для этого примера веб-сервис называется TestService, то файл должен быть в

      Service References\TestService\Reference.cs

Добавить в начало файла строку

      extern alias ServiceModelHack;

И запустите замену всего текста в файле:

System.ServiceModel.-> ServiceModelHack::System.ServiceModel.

Вызов веб-сервиса

В качестве System.Private.ServiceModelотказаться от некоторых функций, которые вы не можете использовать App.configнапрямую, вам нужно вручную установить привязки, например:

      var binding = new ServiceModelHack::System.ServiceModel.BasicHttpsBinding
{
    Security = new ServiceModelHack::System.ServiceModel.BasicHttpsSecurity
    {
        Transport = new ServiceModelHack::System.ServiceModel.HttpTransportSecurity
        {

        }
    }
};
var endpoint = new ServiceModelHack::System.ServiceModel.EndpointAddress("https://example.com/ws");
using (var service = new TestService(binding, endpoint))
{
    Helper.setTls(service, SslProtocols.Tls13); //describe in next part
    service.Ping();
}
Изменение версии TLS

на основе: https://github.com/dotnet/wcf/issues/3442#issuecomment-475356182

      
public static void setTls<T>(ServiceModelHack::System.ServiceModel.ClientBase<T> client, SslProtocols ssl) where T : class
{
    client.Endpoint.EndpointBehaviors.Add(
        new SslProtocolEndpointBehavior
        {
            SslProtocols = ssl,
        }
    );
}
public class SslProtocolEndpointBehavior : ServiceModelHack::System.ServiceModel.Description.IEndpointBehavior
{
    public SslProtocols SslProtocols { get; set; } = SslProtocols.Tls12;
    public void AddBindingParameters(ServiceModelHack::System.ServiceModel.Description.ServiceEndpoint endpoint, ServiceModelHack::System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
    {
        bindingParameters.Add(new Func<HttpClientHandler, HttpMessageHandler>(x =>
        {
            x.SslProtocols = this.SslProtocols;
            return x; // You can just return the modified HttpClientHandler
        }));
    }

    public void ApplyClientBehavior(ServiceModelHack::System.ServiceModel.Description.ServiceEndpoint endpoint, ServiceModelHack::System.ServiceModel.Dispatcher.ClientRuntime clientRuntime) { }
    public void ApplyDispatchBehavior(ServiceModelHack::System.ServiceModel.Description.ServiceEndpoint endpoint, ServiceModelHack::System.ServiceModel.Dispatcher.EndpointDispatcher endpointDispatcher) { }
    public void Validate(ServiceModelHack::System.ServiceModel.Description.ServiceEndpoint endpoint) { }
}
Другие вопросы по тегам