在Xamarin中使用NSUrlSessionDelegate进行客户端证书认证

7

我希望在我的Xamarin应用程序中实现客户端证书认证。此外,我使用自定义证书颁发机构(CA)和TLS 1.2。

到目前为止,我已经成功地在Android、UWP和WPF上运行它。唯一缺少的平台是iOS。

这是我的NSUrlSessionDelegate:

public class SSLSessionDelegate : NSUrlSessionDelegate, INSUrlSessionDelegate
{
    private NSUrlCredential Credential { get; set; }
    private SecIdentity identity = null;
    private X509Certificate2 ClientCertificate = null;

    private readonly SecCertificate CACertificate = null;

    public SSLSessionDelegate(byte[] caCert) : base()
    {
        if (caCert != null)
        {
            CACertificate = new SecCertificate(new X509Certificate2(caCert));
        }
    }

    public void SetClientCertificate(byte[] pkcs12, char[] password)
    {
        if (pkcs12 != null)
        {
            ClientCertificate = new X509Certificate2(pkcs12, new string(password));
            identity = SecIdentity.Import(ClientCertificate);

            SecCertificate certificate = new SecCertificate(ClientCertificate);
            SecCertificate[] certificates = { certificate };

            Credential = NSUrlCredential.FromIdentityCertificatesPersistance(identity, certificates, NSUrlCredentialPersistence.ForSession);
        }
        else
        {
            ClientCertificate = null;
            identity = null;
            Credential = null;
        }
    }

    public override void DidReceiveChallenge(NSUrlSession session, NSUrlAuthenticationChallenge challenge, Action<NSUrlSessionAuthChallengeDisposition, NSUrlCredential> completionHandler)
    {
        if (challenge.ProtectionSpace.AuthenticationMethod == NSUrlProtectionSpace.AuthenticationMethodClientCertificate)
        {
            NSUrlCredential c = Credential;
            if (c != null)
            {
                completionHandler.Invoke(NSUrlSessionAuthChallengeDisposition.UseCredential, c);
                return;
            }
        }

        if (challenge.ProtectionSpace.AuthenticationMethod == NSUrlProtectionSpace.AuthenticationMethodServerTrust)
        {
            SecTrust secTrust = challenge.ProtectionSpace.ServerSecTrust;
            secTrust.SetAnchorCertificates(new SecCertificate[] {
                CACertificate
            });
            secTrust.SetAnchorCertificatesOnly(true);

        }
        completionHandler.Invoke(NSUrlSessionAuthChallengeDisposition.PerformDefaultHandling, null);
    }
}

如果没有配置客户端证书,这将起作用。 DidReceiveChallenge 会被调用一次,其认证方法为AuthenticationMethodServerTrust,并且自定义的CA被接受。

但是,一旦配置了客户端证书,DidReceiveChallenge 就会被调用4次(每个AuthenticationMethod调用两次),我会收到一个NSURLErrorDomain (-1200) 错误。

有人知道我做错了什么吗?


更新

SSLSessionDelegate 的使用方式如下:

public class HttpsServer : AbstractRemoteServer, IRemoteServer
{
    private static readonly Logger LOG = LogManager.GetLogger();

    private SSLSessionDelegate sSLSessionDelegate;

    private NSUrlSession session;

    private NSUrl baseAddress;

    public HttpsServer()
    {
        sSLSessionDelegate = new SSLSessionDelegate(SSLSupport.GetTruststoreRaw());
        NSUrlSessionConfiguration configuration = NSUrlSessionConfiguration.DefaultSessionConfiguration;
        configuration.HttpShouldSetCookies = true;
        configuration.TimeoutIntervalForRequest = 30;
        configuration.TLSMinimumSupportedProtocol = SslProtocol.Tls_1_2;
        configuration.TimeoutIntervalForResource = 30;
        NSMutableDictionary requestHeaders;
        if (configuration.HttpAdditionalHeaders != null)
        {
            requestHeaders = (NSMutableDictionary)configuration.HttpAdditionalHeaders.MutableCopy();
        }
        else
        {
            requestHeaders = new NSMutableDictionary();
        }
        AppendHeaders(requestHeaders, SSLSupport.GetDefaultHeaders());
        configuration.HttpAdditionalHeaders = requestHeaders;

        session = NSUrlSession.FromConfiguration(configuration, (INSUrlSessionDelegate)sSLSessionDelegate, NSOperationQueue.MainQueue);
        baseAddress = NSUrl.FromString(SSLSupport.GetBaseAddress());
    }

    public void SetClientCertificate(byte[] pkcs12, char[] password)
    {
        sSLSessionDelegate.SetClientCertificate(pkcs12, password);
    }

    public override async Task<string> GetString(string url, Dictionary<string, string> headers, CancellationToken cancellationToken)
    {
        NSData responseContent = await GetRaw(url, headers, cancellationToken);
        return NSString.FromData(responseContent, NSStringEncoding.UTF8).ToString();
    }

    private async Task<NSData> GetRaw(string url, Dictionary<string, string> headers, CancellationToken cancellationToken)
    {
        NSMutableUrlRequest request = GetRequest(url);
        request.HttpMethod = "GET";
        request.Headers = AppendHeaders(request.Headers, headers);

        Task<NSUrlSessionDataTaskRequest> taskRequest = session.CreateDataTaskAsync(request, out NSUrlSessionDataTask task);
        cancellationToken.Register(() =>
        {
            if (task != null)
            {
                task.Cancel();
            }
        });
        try
        {
            task.Resume();
            NSUrlSessionDataTaskRequest taskResponse = await taskRequest;
            if (taskResponse == null || taskResponse.Response == null)
            {
                throw new Exception(task.Error.Description);
            }
            else
            {
                NSHttpUrlResponse httpResponse = (NSHttpUrlResponse)taskResponse.Response;
                if (httpResponse.StatusCode == 303)
                {
                    if (!httpResponse.AllHeaderFields.TryGetValue(new NSString("Location"), out NSObject locationValue))
                    {
                        throw new Exception("redirect received without Location-header!");
                    }
                    return await GetRaw(locationValue.ToString(), headers, cancellationToken);
                }
                if (httpResponse.StatusCode != 200)
                {
                    throw new Exception("unsupported statuscode: " + httpResponse.Description);
                }
                return taskResponse.Data;
            }
        }
        catch (Exception ex)
        {
            throw new Exception("communication exception: " + ex.Message);
        }
    }
}

这是我的 Info.plist 文件。
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSExceptionDomains</key>
    <dict>
        <key>XXXXXXXXXX</key>
        <dict>
            <key>NSExceptionAllowsInsecureHTTPLoads</key>
            <true/>
            <key>NSIncludesSubdomains</key>
            <true/>
        </dict>
    </dict>
</dict>

更新 2

我没能找到解决方案,也没有人能够给我提示,所以我现在暂时放弃了客户端证书。我改用 OAuth2 进行授权,并为服务器使用自己的证书颁发机构(而不是自签名证书)进行身份验证,这个方法效果很好。

但我仍然对这个问题感兴趣,很高兴听取任何关于如何使其正常工作的想法。

1个回答

1
我建议使用ModernHttpClient。它支持Android和iOS的客户端证书。它是开源的,如果你想完成自己的实现,你可以随时查看他们的GitHub作为参考。

ModernHttpClient


1
事实上,ModernHttpClient本来是我的首选,但由于它相当古老(最新版本发布于2015年5月),所以对我来说不是一个选择。但至少你提出了一个解决方案,所以我会奖励你。 - AlexS
1
看起来这个项目已经在这里被重启了 https://github.com/alexrainman/ModernHttpClient - mrmichaeldev
这个库只有一个私人维护者,最新版本发布于2017年。因此,尽管我欣赏亚历山大·雷耶斯的努力,但我不会考虑将其用于生产。 - AlexS

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接