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

标签 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 属性(如“kCFStreamSSLValidatesCertificateChain”)以覆盖证书链验证的能力似乎没有在 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)如下。我确定它不会被调用,因为没有命中断点,函数中的任何日志记录都没有结果,并且所有证书都是可信的,因此不会发生自定义验证。

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

最后,顺便说一句,我知道不推荐使用自签名证书并且有很多风险,但它是一个带有自定义消息协议(protocol)的遗留系统,所以我束手无策。我也尝试过使用 .NET SslStream 和 TcpClient,但在 Mono 框架中的实现不完整,所以我没有收到完整的证书链。

最佳答案

在进一步研究之后,我找到了委托(delegate)回调未运行的原因。问题是 NSRunLoop.Current is not being run long enough for the delegate to be called .需要使用 Run 或 RunUntil(NSDate) 调用 NSRunLoop,以使其保持足够长的生命周期,以便调用委托(delegate)。

我还了解到可以使用属性索引器运算符直接在流上设置“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)。

关于c# - Xamarin iOS 覆盖自签名证书的 TLS 链验证,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48084903/

相关文章:

delphi - 在 Delphi 中捕获 MSVCR120 丢失的错误消息

c# - 无法访问关闭的流异常

iphone - 使滚动条在 uiscrollview 中始终可见

ios - iBeacon : didRangeBeacons not being called

xcode - iOS本地化不一致

ruby-on-rails - 需要 SSL 来支持我网站上的 Facebook 身份验证吗?

WCF 路由到启用 SSL 的服务

c# - 伪造派生类但调用真正的构造函数并忽略基类构造函数

c# - 在 C++ 或 Java 应用程序中嵌入 Flash Player?

c# - 在条件范围内声明隐式类型变量并在外部使用它