1

I'm looking for help with performing self-signed certificate validation with Xamarin.iOS. My custom stream event handler isn't being called.

I've been working on implementing self-signed certificate validation code in C# using Xamarin.iOS and CFStream. I've been following along with the process laid out in the Apple technical note "Overriding TLS Chain Validation Correctly". When I debug the code I can connect to my server with the self-signed certificate and send and receive messages. The issue is that my custom stream event handler is not being called so I'm not able to verify the certificate. I don't know if the handler not running is due to a misconfiguration or something else?

My connection setup code is as follows.

public void Connect(string host, ushort port)
{
    // Create socket
    CFReadStream cfRead;
    CFWriteStream cfWrite;
    CFStream.CreatePairWithSocketToHost(host, port, out cfRead, out cfWrite);

    // Bind streams to NSInputStream/NSOutputStream
    NSInputStream inStream = Runtime.GetNSObject<NSInputStream>(cfRead.Handle);
    NSOutputStream outStream = Runtime.GetNSObject<NSOutputStream>(cfWrite.Handle);

    // Set SSL protocol
    inStream.SocketSecurityLevel = NSStreamSocketSecurityLevel.NegotiatedSsl;
    outStream.SocketSecurityLevel = NSStreamSocketSecurityLevel.NegotiatedSsl;

    // Set stream to not validate the certificate, we will do it in a callback
    // If callback doesn't fire, then any certificate will be accepted!!
    NSString validateCertChainKey =
        new NSString("kCFStreamSSLValidatesCertificateChain");
    NSNumber falseValue = NSNumber.FromBoolean(false);
    NSDictionary sslSettings =
        NSDictionary.FromObjectAndKey(falseValue, validateCertChainKey);

    NSString streamSslKey = new NSString("kCFStreamPropertySSLSettings");
    if (!CFReadStreamSetProperty(cfRead, streamSslKey, sslSettings)) {
        throw new InvalidOperationException("Set input properties failure");
    }    
    if (!CFWriteStreamSetProperty(cfWrite, streamSslKey, sslSettings)) {
        throw new InvalidOperationException("Set output properties failure");
    }

    // Set callback for events, including for certificate validation
    // These don't appear to be called when events occur
    // Also tried NSStream.Event += ... to no avail
    inStream.Delegate = new CustomStreamDelegate();
    outStream.Delegate = new CustomStreamDelegate();

    // Set run loop (thread) for stream, just use current and default mode
    // Using NSRunLoop.Main doesn't appear to make a difference
    inStream.Schedule(NSRunLoop.Current, NSRunLoopMode.Default);
    outStream.Schedule(NSRunLoop.Current, NSRunLoopMode.Default);

    // Open the streams
    inStream.Open();
    outStream.Open();
}

The ability to set CFStream properties such as "kCFStreamSSLValidatesCertificateChain" to override the certificate chain validation doesn't appear to be exposed in Xamarin. This is brought up in Xamarin bug 31167 with a suggested workaround to set the properties. I'm fairly sure this is working as intended as the connection accepts any SSL certificate as expected of disabling the chain validation.

[DllImport(Constants.CoreFoundationLibrary, EntryPoint = "CFReadStreamSetProperty")]
[return: MarshalAs(UnmanagedType.I1)]
private static extern bool CFReadStreamSetPropertyExtern(IntPtr stream,
    IntPtr propertyName, IntPtr propertyValue);

private static bool CFReadStreamSetProperty(CFReadStream stream, NSString name,
    INativeObject value)
{
    IntPtr valuePtr = value == null ? IntPtr.Zero : value.Handle;
    return CFReadStreamSetPropertyExtern(stream.Handle, name.Handle, valuePtr);
}

[DllImport(Constants.CoreFoundationLibrary, EntryPoint = "CFWriteStreamSetProperty")]
[return: MarshalAs(UnmanagedType.I1)]
private static extern bool CFWriteStreamSetPropertyExtern(IntPtr stream,
    IntPtr propertyName, IntPtr propertyValue);

private static bool CFWriteStreamSetProperty(CFWriteStream stream, NSString name,
    INativeObject value)
{
    IntPtr valuePtr = value == null ? IntPtr.Zero : value.Handle;
    return CFWriteStreamSetPropertyExtern(stream.Handle, name.Handle, valuePtr);
}

Finally the callback delegate in the custom NSStreamDelegate is as follows. I'm certain it's not called as breakpoints are not hit, any logging in the function gives no results, and all certificates are trusted so the custom validation doesn't occur.

// Delegate callback that is not being called    
public override void HandleEvent(NSStream theStream, NSStreamEvent streamEvent)
{
    // Only validate certificate when known to be connected
    if (streamEvent != NSStreamEvent.HasBytesAvailable &&
        streamEvent != NSStreamEvent.HasSpaceAvailable) {
        return;
    }

    // Get trust object from stream
    NSString peerTrustKey = new NSString("kCFStreamPropertySSLPeerTrust");
    SecTrust trust =
        Runtime.GetINativeObject<SecTrust>(theStream[peerTrustKey].Handle, false);

    // Only add the certificate if it hasn't already been added
    NSString anchorAddedKey = new NSString("kAnchorAlreadyAdded");
    NSNumber alreadyAdded = (NSNumber) theStream[anchorAddedKey];
    if (alreadyAdded == null || !alreadyAdded.BoolValue) {
        // Add the custom certificate
        X509CertificateCollection collection =
            new X509CertificateCollection(new[] {v_Certificate});
        trust.SetAnchorCertificates(collection);

        // Allow (false) or disallow (true) all other already trusted certificates
        trust.SetAnchorCertificatesOnly(true);

        // Set that the certificate has been added
        theStream[anchorAddedKey] = NSNumber.FromBoolean(true);
    }

    // Evaluate the trust policy
    // A result of Proceed or Unspecified indicates a trusted certificate
    SecTrustResult res = trust.Evaluate();
    if (res != SecTrustResult.Proceed && res != SecTrustResult.Unspecified) {
        // Not trusted, close the connection
        Disconnect();
    }
}

Finally as an aside, I know self signed certificates are not recommended and have many risks but it's a legacy system with a custom message protocol so my hands are tied. I've also tried using the .NET SslStream and TcpClient but the implementation is incomplete in the Mono framework so I don't receive the full certificate chain.

salomon90
  • 11
  • 4

1 Answers1

0

After working on it a bit more I found the cause of the delegate callback not running. The issue is that the NSRunLoop.Current is not being run long enough for the delegate to be called. The NSRunLoop needs to be invoked using Run or RunUntil(NSDate) to keep it alive long enough for the delegate to be called.

I also learned that the "kCFStreamPropertySSLSettings" can be set directly on the streams using the property indexer operators. Below is the updated connection method. The HandleEvent stayed the same, and the "CFReadStreamSetProperty" and "CFWriteStreamSetProperty" methods aren't required.

// Global flag that is set by the HandleEvent if NSStream is open and trusted
bool authenticated = false;

public void Connect(string host, ushort port, int timeout)
{
    // Create socket
    CFReadStream cfRead;
    CFWriteStream cfWrite;
    CFStream.CreatePairWithSocketToHost(host, port, out cfRead, out cfWrite);

    // Bind streams to NSInputStream/NSOutputStream
    NSInputStream inStream = Runtime.GetNSObject<NSInputStream>(cfRead.Handle);
    NSOutputStream outStream = Runtime.GetNSObject<NSOutputStream>(cfWrite.Handle);

    // Set SSL protocol
    inStream.SocketSecurityLevel = NSStreamSocketSecurityLevel.NegotiatedSsl;
    outStream.SocketSecurityLevel = NSStreamSocketSecurityLevel.NegotiatedSsl;

    // Create property to set stream to not validate the certificate
    NSString validateCertChainKey =
        new NSString("kCFStreamSSLValidatesCertificateChain");
    NSNumber falseValue = NSNumber.FromBoolean(false);
    NSDictionary sslSettings =
        NSDictionary.FromObjectAndKey(falseValue, validateCertChainKey);

    // Set stream to not validate the certificate, we will do it in a callback
    // Danger is if callback doesn't fire, then any certificate will be accepted!!
    NSString streamSslKey = new NSString("kCFStreamPropertySSLSettings");
    inStream[streamSslKey] = sslSettings;
    outStream[streamSslKey] = sslSettings;

    // Set callback for events, including for certificate validation
    // These don't appear to be called when events occur
    // Can also use stream.Event += ... to avoid having to create a NSStreamDelegate
    inStream.Delegate = new CustomStreamDelegate();
    outStream.Delegate = new CustomStreamDelegate();

    // Set run loop (thread) for stream, just use current and default mode
    inStream.Schedule(NSRunLoop.Current, NSRunLoopMode.Default);
    outStream.Schedule(NSRunLoop.Current, NSRunLoopMode.Default);

    // Open the streams
    inStream.Open();
    outStream.Open();

    // Run the NSRunLoop.Current using either Run (blocking call) or RunUntil(NSDate)
    // Otherwise the delegate won't be called since the RunLoop doesn't run long enough
    // The below example keep the loop going until the authenticated flag is set
    // or the timeout is reached
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();
    bool timedout = false;
    while(!authenticated && !timedout) {
        NSRunLoop.Current.RunUntil(NSDate.FromTimeIntervalSinceNow(0.01));
        timedout = timeout > 0 && stopwatch.ElapsedMilliseconds > timeout;
    }
    stopwatch.Stop();

    if(timedout){
        inStream.Close();
        outStream.Close();
        throw new InvalidOperationException("Timed out");
    }
}

As some final notes, the Open calls will return immediately even if the stream is still being opened or authenticated. Therefore, it's important to make sure to wait for the authentication to occur before performing any read or write operations. One way is to set an authentication complete flag in the event handler. The RunLoop will need to be kept going until the flag has been set.

You will also find that the NSInputStream won't authenticate until you have received bytes from the other side of the connection. So in the case of a client, you need to receive bytes from the server before the custom certificate validation logic is executed (have HasBytesAvailable). This means if you want to validate the NSInputStream you have to keep the RunLoop going until you receive bytes. The NSOutputStream should run the validation logic (have HasSpaceAvailable) as soon as it connects to the server.

salomon90
  • 11
  • 4
  • A tangent to the answer is if both the NSInputStream and NSOutputStream need to be authenticated? If you only need to authenticate one of the streams, then the NSOutputStream authentication in the above design is sufficient. If not, additional work needs to be done to send a request after authentication, keep the NSRunLoop going to authenticate the NSInputStream, and handle any authentication failure and cleanup. – salomon90 Jan 06 '18 at 01:07