Xamarin iOS覆盖自签名证书的TLS链验证

时间:2018-01-03 20:48:10

标签: c# ios ssl xamarin xamarin.ios

我正在寻找与Xamarin.iOS进行自签名证书验证的帮助。我的自定义流事件处理程序没有被调用。

我一直在使用Xamarin.iOS和CFStream在C#中实现自签名证书验证代码。我一直在关注Apple技术说明" Overriding TLS Chain Validation Correctly"中列出的流程。当我调试代码时,我可以使用自签名证书连接到我的服务器并发送和接收消息。问题是我的自定义流事件处理程序没有被调用,因此我无法验证证书。我不知道处理程序没有运行是由于配置错误还是其他原因?

我的连接设置代码如下。

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();
}

能够设置CFStream属性,例如&#34; kCFStreamSSLValidatesCertificateChain&#34;覆盖证书链验证似乎没有在Xamarin中公开。这在Xamarin bug 31167中显示,并提供了建议的解决方法来设置属性。我非常确定这是按预期工作的,因为连接接受任何SSL证书,如预期的那样禁用链验证。

[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);
}

最后,自定义NSStreamDelegate中的回调委托如下。我确定它没有被调用,因为没有命中断点,函数中的任何日志记录都没有结果,所有证书都是可信的,因此自定义验证不会发生。

// 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();
    }
}

最后另外,我知道不建议使用自签名证书并且存在很多风险,但它是具有自定义消息协议的遗留系统,因此我的双手并列。我也尝试过使用.NET SslStream和TcpClient,但Mono框架中的实现不完整,所以我没有收到完整的证书链。

1 个答案:

答案 0 :(得分:0)

在进行了更多工作之后,我发现委托回调的原因没有运行。问题是NSRunLoop.Current is not being run long enough for the delegate to be called。需要使用Run或RunUntil(NSDate)调用NSRunLoop以使其保持足够长的时间以便调用委托。

我还了解到可以使用属性索引器运算符直接在流上设置“kCFStreamPropertySSLSettings”。以下是更新的连接方法。 HandleEvent保持不变,并且不需要“CFReadStreamSetProperty”和“CFWriteStreamSetProperty”方法。

// 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");
    }
}

作为一些最后的注释,即使流仍在打开或验证,Open调用也会立即返回。因此,在执行任何读取或写入操作之前,务必确保等待身份验证。一种方法是在事件处理程序中设置身份验证完成标志。 RunLoop需要继续运行,直到设置了标志。

您还会发现,在您从连接的另一端接收到字节之前,NSInputStream不会进行身份验证。因此,对于客户端,您需要在执行自定义证书验证逻辑之前从服务器接收字节(具有HasBytesAvailable)。这意味着如果要验证NSInputStream,则必须保持RunLoop继续运行,直到收到字节为止。 NSOutputStream在连接到服务器后应立即运行验证逻辑(具有HasSpaceAvailable)。