我正在寻求有关使用 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/