0

I'm using .NET 4.5 c# to connect to a server that supports only TLS 1.2. (Apple Push Notification Server).

My code fails here:

sslStream.AuthenticateAsClient(server, myAppleCertificates, SslProtocols.Tls12, true);

I did a lot of testing and Googling and found the following:

1) This code works fine if i use a window 10 pc, but doesn't if I run this on my development server or production server. Windows Server 2008 R2.

2) All the pc's and servers are running .net 4.6+

3) I can add the line "ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;" - its still behaves exactly the same.

4) I can replicate the issue on Windows 10, buy simply changing it to: sslStream.AuthenticateAsClient(server, myAppleCertificates, SslProtocols.Tls11, true);

5) I have added the following registry entries and rebooted the server will no effect:

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client]
"Enabled"=dword:00000001

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client]
"DisabledByDefault"=dword:00000000

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server]
"Enabled"=dword:00000001

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server]
"DisabledByDefault"=dword:00000000

5) Server is up to date with windows updates. (actually... im not 100% on this one)

6) IISCrypto, says the protocols are there, and set as defaults correctly.

7) I can use Chrome to connect to APN, no problems, on my PC, and both the servers.

8) If i use Fiddler to test, it behaves exactly the same as my application, after i enable the TLS12 setting in the fiddler settings. I guess this is because Fiddler is build in .net 4.

Its as though even if you force .NET to use TLS1.2, it simply doesn't, and reverts back to 1.1. Does anyone have any more idea's here.

Thanks

Here is my test code:

public class SslTcpClient
    {
        private static Hashtable certificateErrors = new Hashtable();

        public static bool ValidateServerCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
        {
            if (sslPolicyErrors == SslPolicyErrors.None) return true;

            Console.WriteLine("Certificate error: {0}", sslPolicyErrors);
            return false;
        }
        public static X509Certificate SelectLocalCertificate(object sender, string targetHost, X509CertificateCollection localCertificates, X509Certificate remoteCertificate, string[] acceptableIssuers)
        {
            Console.WriteLine("Client is selecting a local certificate.");
            if (acceptableIssuers != null && acceptableIssuers.Length > 0 && localCertificates != null && localCertificates.Count > 0)
            {
                // Use the first certificate that is from an acceptable issuer.
                foreach (X509Certificate certificate in localCertificates)
                {
                    string issuer = certificate.Issuer;
                    if (Array.IndexOf(acceptableIssuers, issuer) != -1) return certificate;
                }
            }
            if (localCertificates != null && localCertificates.Count > 0)  return localCertificates[0];

            return null;
        }

        public static void RunClient(string server)
        {
            ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;

            TcpClient client = new TcpClient(server, 443);
            Console.WriteLine("Client connected.");
            SslStream sslStream = new SslStream(client.GetStream(), false, new RemoteCertificateValidationCallback(ValidateServerCertificate), null);
            Console.WriteLine("SSL stream obtained");

            try
            {
                X509Certificate localCertificate = new X509Certificate("VOIPPush.p12", "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");

                Console.WriteLine("Local Certificte features");
                Console.WriteLine("=========================");

                Console.WriteLine("Issuer: " + localCertificate.Issuer);
                Console.WriteLine("Subject: " + localCertificate.Subject);

                Console.WriteLine("CertHash: " + localCertificate.GetCertHashString());
                Console.WriteLine("EffectiveDate: " + localCertificate.GetEffectiveDateString());
                Console.WriteLine("ExpirationDate: " + localCertificate.GetExpirationDateString());
                Console.WriteLine("Format: " + localCertificate.GetFormat());
                Console.WriteLine("HashCode: " + localCertificate.GetHashCode().ToString());
                Console.WriteLine("KeyAlgorithm: " + localCertificate.GetKeyAlgorithm());
                Console.WriteLine("KeyAlgorithmParameters: " + localCertificate.GetKeyAlgorithmParametersString());
                Console.WriteLine("PublicKey: " + localCertificate.GetPublicKeyString());
                Console.WriteLine("SerialNumber: " + localCertificate.GetSerialNumberString());
                X509CertificateCollection localCertificates = new X509CertificateCollection();
                localCertificates.Add(localCertificate);

                sslStream.AuthenticateAsClient(server, localCertificates, SslProtocols.Tls12, true);
                Console.WriteLine("Authenticated as client");

                X509Certificate remoteCertificate = sslStream.RemoteCertificate;

                Console.WriteLine("Remote Certificte features");
                Console.WriteLine("==========================");

                Console.WriteLine("Issuer: " + remoteCertificate.Issuer);
                Console.WriteLine("Subject: " + remoteCertificate.Subject);

                Console.WriteLine("CertHash: " + remoteCertificate.GetCertHashString());
                Console.WriteLine("EffectiveDate: " + remoteCertificate.GetEffectiveDateString());
                Console.WriteLine("ExpirationDate: " + remoteCertificate.GetExpirationDateString());
                Console.WriteLine("Format: " + remoteCertificate.GetFormat());
                Console.WriteLine("HashCode: " + remoteCertificate.GetHashCode().ToString());
                Console.WriteLine("KeyAlgorithm: " + remoteCertificate.GetKeyAlgorithm());
                Console.WriteLine("KeyAlgorithmParameters: " + remoteCertificate.GetKeyAlgorithmParametersString());
                Console.WriteLine("PublicKey: " + remoteCertificate.GetPublicKeyString());
                Console.WriteLine("SerialNumber: " + remoteCertificate.GetSerialNumberString());

            }
            catch (AuthenticationException e)
            {
                Console.WriteLine("Exception: {0}", e.ToString());
                client.Close();
                return;
            }
            byte[] messsage = Encoding.UTF8.GetBytes(@"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n");

            // no use in writing more, this is just a test...

            Console.WriteLine("Writing to sslStream...");
            sslStream.Write(messsage);
            Console.WriteLine("Writing done");
            sslStream.Flush();

            Console.WriteLine("Reading Response...");
            string serverMessage = ReadMessage(sslStream);
            Console.WriteLine("Server says: {0}", serverMessage);

            client.Close();
            Console.WriteLine("Client closed.");
        }
        static string ReadMessage(SslStream sslStream)
        {
            // Read the  message sent by the server.
            // The end of the message is signaled using the
            // "<EOF>" marker.
            byte[] buffer = new byte[2048];
            StringBuilder messageData = new StringBuilder();
            int bytes = -1;
            do
            {
                bytes = sslStream.Read(buffer, 0, buffer.Length);

                // Use Decoder class to convert from bytes to UTF8
                // in case a character spans two buffers.
                Decoder decoder = Encoding.UTF8.GetDecoder();
                char[] chars = new char[decoder.GetCharCount(buffer, 0, bytes)];
                decoder.GetChars(buffer, 0, bytes, chars, 0);
                messageData.Append(chars);
                // Check for EOF.
                if (messageData.ToString().IndexOf("<EOF>") != -1)
                {
                    break;
                }
            } while (bytes != 0);

            return messageData.ToString();
        }

        public static void Main(string[] args)
        {
            SslTcpClient.RunClient("api.push.apple.com");
            Console.ReadLine();
        }
    }
Conrad de Wet
  • 477
  • 6
  • 15
  • You never stated how your code fails. You just said "it fails". You need to specifically describe what happens, including any relevant error messages in the question. – mason Jun 08 '17 at 12:15
  • Exception: "A call to SSPI failed, see...." "The Message received was unexpected, or badly formatted... " – Conrad de Wet Jun 08 '17 at 12:34
  • Same exception here: https://stackoverflow.com/questions/14558398/apple-push-notification-server-side-in-c-sharp –  Jun 08 '17 at 12:39

3 Answers3

1

On a Windows Server 2008 R2 production server I am using:

_iPhoneSslStream.AuthenticateAsClient(hostname, certificatesCollection, SslProtocols.Tls, false);

Complete source code for authentication:

String hostname = "gateway.push.apple.com";

X509Certificate2 clientCertificate = new X509Certificate2(_iPhoneCertificate, _iPhonePassword);
X509Certificate2Collection certificatesCollection = new X509Certificate2Collection(clientCertificate);

_iPhoneClient = new TcpClient(hostname, 2195);
_iPhoneSslStream = new SslStream(_iPhoneClient.GetStream(), false, new RemoteCertificateValidationCallback(ValidateServerCertificate), null);

_iPhoneSslStream.AuthenticateAsClient(hostname, certificatesCollection, SslProtocols.Tls, false);

and

public static bool ValidateServerCertificate(object sender,
                                         X509Certificate certificate,
                                         X509Chain chain,
                                         SslPolicyErrors sslPolicyErrors)
{
    if (sslPolicyErrors == SslPolicyErrors.None)
        return true;

     // Do not allow this client to communicate with unauthenticated servers.
     return false;
}

PS I am still using the Legacy Binary Provider API: https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/BinaryProviderAPI.html

  • Yip, that's pretty much the same code as me... same result. SslProtocols.Tls or SslProtocols.Tls12 does the same thing: ... "The message received was unexpected or badly formatted" – Conrad de Wet Jun 08 '17 at 12:26
  • So I guess it is not a connection issue, but you are sending wrong data to the server. –  Jun 08 '17 at 12:27
  • I'm pretty sure its something in the way .NET works or something with .NET and the server, because my code works fine on the Windows 10 PCs. – Conrad de Wet Jun 08 '17 at 12:28
  • It is a connection issue, because on the servers it errors out, and on the windows 10 box, it give a json response. – Conrad de Wet Jun 08 '17 at 12:29
  • Post a complete code to reproduce the issue. It is working fine here, we send about ~10.000 notifications each day. –  Jun 08 '17 at 12:29
  • Code posted to the original message – Conrad de Wet Jun 08 '17 at 12:35
  • It seems you are using the new interface and not the old legacy interface. I tried to use it, and after lot of wasted time, I rolled back to legacy interface. –  Jun 08 '17 at 12:36
  • (Hand on face)! – Conrad de Wet Jun 08 '17 at 12:39
  • You are right... I will try to move on the new interface again, in the next future. But, right now, the legacy interface is still working and I have not news about a oncoming EOL. –  Jun 08 '17 at 12:44
  • Let me try the legacy interface. – Conrad de Wet Jun 08 '17 at 12:47
  • Oddly i'm getting the same error now with the legacy interface on my windows 10 box. You using a .p12 with password? – Conrad de Wet Jun 08 '17 at 13:19
  • "To establish a TLS session with APNs, an Entrust Secure CA root certificate must be installed on the provider’s server. If the server is running macOS, this root certificate is already in the keychain. On other systems, the certificate might not be available. You can download this certificate from the Entrust SSL Certificates website." – Conrad de Wet Jun 08 '17 at 13:24
  • Yes, a .P12 certificate. Please check if you are using a debug or release version of the certificate (they use different servers). About you original question, maybe HTTP/2 is supported on Windows 10 only: https://stackoverflow.com/questions/32685151/how-to-make-the-net-httpclient-use-http-2-0 –  Jun 08 '17 at 13:34
  • We had an issue with the VOIP certificate - it seemed that normal push was fine on legacy, but it didn't want to accept the Voip Certificate. In the end it think its better to use the APN key with Http2 based connection. – Conrad de Wet Jun 13 '17 at 07:57
0

"Currently, in the standard .NET framework, HTTP/2 is not supported in HttpClient. However, in the .NET Core framework, it is.. but it requires to be run on Windows 10 (or, I assume Windows Server 2016)"

Ref: Is there any way to use the new http/2 api to send push notifications in C#?

  • No we are not using HttpClient. We are constructing our own TcpConnection, constructing HTTP2 frames, and sending thing like that. But finally we have it working... see below! :) – Conrad de Wet Jun 13 '17 at 07:47
0

Ok, just so that if someone stumbles upon this... here is the deal.

No matter how much windows updates, patching, registry whatever... windows server 2008 simply could not establish a LTS 1.2 (only) connection to Apple. So we tried windows server 2012....

Out the box... same issue. Didn't work! O.M.W. Anyway... two days of windows updates later, service pack 2 (that also includes .Net 4.6.2)... it worked!

We ended up making use of HttpTwo https://github.com/Redth/HttpTwo/ To help with the frame construction, but it all works fine now.

Conrad de Wet
  • 477
  • 6
  • 15