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.