Skip to content

SSlStream on OSX High Sierra throws AuthenticationException when certificate revocation checking is enabled and the server’s certificate extensions includes a CRL but not OCSP  #25872

Closed
@vincentkam

Description

@vincentkam

Overview
A .Net Core 2.0 application running on OSX High Sierra cannot connect to a server using SSLStream with certificate revocation checking enabled. The server’s certificate has extensions for CRLs but not OCSP. If one builds this application on Linux or Windows, the application will connect (to the same server) without any problems. Additionally, using the same certificate chain to host a web server via openssl s_server, Safari is able to connect on OSX without any problems.

Exception details
AuthenticationException message: Remote certificate is invalid according to the validation procedure.
The sole X509ChainStatus in the X509Chain is on the leaf certificate: "An incomplete certificate revocation check has occurred."
The corresponding Apple error code is errSecIncompleteCertRevocationCheck (-67635) (https://developer.apple.com/documentation/security/errsecincompletecertrevocationcheck).

Workaround

  1. If one can retarget the application to .NET Core 1.1 (which uses OpenSSL instead of the Apple Security Framework), the application will connect without problems. Note: the root certificate will have to be installed for OpenSSL.
  2. An alternative is to disable certificate revocation checking in SSLStream, but this has security implications.

Interesting observations

  1. The Apple result code is listed under "OCSP result code" (https://developer.apple.com/documentation/security/1542001-security_framework_result_codes), and the certificates I was using did not support OCSP.
  2. However, when connecting to a server "B" whose certificate chain did support OCSP as well as CRLs, I was able to connect without any problems. For completeness's sake, Server B's certificate also had the following X509v3 extensions that the first server "A" did not: X509v3 Subject Alternative Name, X509v3 Key Usage, X509v3 Extended Key Usage, X509v3 Certificate Policies, Authority Information Access, CT Precertificate SCTs.

Additional thoughts
I am admittedly a little uncertain if the problem is on Apple's end: i.e. the Apple Security Framework has additional restrictions (compared to CNG and OpenSSL) on what it considers to be a well-formed chain. My only hint that indicates otherwise is that that Safari is able to connect a webserver utilizing the same set of certificates.

Reproduction steps

Create a chain of certificates that mimics a certificate signed by DigiCert SHA2 Secure Server CA.

The following are the steps I used:

  1. Create a self-signed root CA such that "X509v3 Key Usage: Digital Signature, Certificate Sign, CRL Sign".
  2. Generate a CRL for the root CA, taking care that the CRL does not have a X509v3 Issuing Distribution Point.
  3. Create an intermediate certificate authority with the same key usage as the root CA and with a X509v3 Issuing Distribution Point (IDP) pointing to the CRL generated in step 2.
  4. Generate a CRL for the intermediate CA. This CRL should contain an X509v3 IDP.
  5. Create the server certificate "A" signed by the intermediate CA. This certificate should have an X509v3 IDP that matches the CRL generated in step 4.
  6. Install the root CA certificate. On OSX, I used Keychain Access, adding the certificate to the login keychain. (I also tried adding the certificate to the system keychain).
  7. Host a test server using openssl s_server
    "openssl s_server -port 44330 -key keyA.pem -cert certA.pem -CAfile intermediatecert.pem"
  8. Create a test app that connects to the OpenSSL s_server using SslStream, calling AuthenticateAsClient with checkCertificateRevocation: true.

On Windows and Linux (after installing the root CA certificate), the
test app will connect without any problems, whereas the application will fail to connect on OSX.
However, as previously noted, if you use the same certificate to host a webserver (e.g.
"openssl s_server -port 44330 -key keyA.pem -cert certA.pem -CAfile intermediatecert.pem -www"),
then Safari will be able to connect to that webserver without any problems.

I have reproduced this with the app targeting .NET Core 2.0 as well as 2.1 preview, using .NET Core SDK 2.1.300-preview1-008174. This also occurs when using SDK 2.1.104 (targeting .NET Core 2.0 only).

I've included my test app below for convenience:

// Program.cs
using System;
using Ssl;
namespace crl_test_app {
    class Program {
        static void Main(string[] args) {
            string serverCertificateName = "tyche";
            string machineName = "tyche";
            while (true) {
                Console.WriteLine("Press enter to connect and send a message.");
                Console.ReadLine();
                Console.WriteLine("Attempting to connect...");
                SslTcpClient.RunClient(machineName, serverCertificateName);
            }
        }
    }
}

// Ssl.cs
using System;
using System.IO;
using System.Linq;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Text;

namespace Ssl {
    public class SslTcpClient {
 
        // Purely for debugging purposes, not needed
        public static bool ValidateServerCertificate(
                object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) {
            var errors = chain.ChainStatus.Aggregate("", (str, x) => str + "," + x.Status);
            Console.WriteLine($"SslPolicyError: {sslPolicyErrors} {errors}");
 
            foreach (var item in chain.ChainElements) foreach (var elemStatus in item.ChainElementStatus)
               Console.WriteLine(item.Certificate.Subject + "->" + elemStatus.StatusInformation);
            return true;
        }
 
        public static void RunClient(string machineName, string serverName)  {
            var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
            socket.Connect(serverName, 44330);
            Console.WriteLine("Client connected.");
                using (var stream = new NetworkStream(socket)) {                
                var sslStream = new SslStream(innerStream: stream, leaveInnerStreamOpen: false,
                    userCertificateValidationCallback: ValidateServerCertificate);
        
                try {
                    sslStream.AuthenticateAsClientAsync(serverName, new X509Certificate2Collection(),
                        SslProtocols.Tls12, checkCertificateRevocation: true).GetAwaiter().GetResult();
                } catch (AuthenticationException e) {
                    Console.WriteLine("Exception: {0}", e.Message);
                    if (e.InnerException != null)
                        Console.WriteLine("Inner exception: {0}", e.InnerException.Message);
 
                    Console.WriteLine("Authentication failed - closing the connection.");
                        return;
                }
 
                var w = new StreamWriter(sslStream);
                w.WriteLine("Hello from the client.");
                w.Flush();  
                
                var serverMessage = ReadLine(new StreamReader(sslStream));
                Console.WriteLine("Server says: {0}", serverMessage);
                Console.WriteLine("Press enter to close the connection.");
                Console.ReadLine();                 
            }
 
            socket.Dispose();
            Console.WriteLine("Client closed.");
        }
 
        static string ReadLine(StreamReader reader) => reader.ReadLine();
 
        static void SendMessage(SslStream sslStream, string s="Hello from the client.") =>
            sslStream.Write(Encoding.UTF8.GetBytes(s));
        }
}

[edit: whitespace, typos]
[EDIT] Add C# syntax highlighting by @karelz

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions