我正在寻找与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框架中的实现不完整,所以我没有收到完整的证书链。
答案 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)。