无法从 iOS 访问 https 网络服务

Unable to access https web service from iOS

我正在尝试访问 https 协议上可用的 Web 服务。最初我收到以下错误:

NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9802) errorAn SSL error has occurred and a secure connection to the server cannot be made.

我通过在 info.plist 中添加以下内容来修复它:

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
    <key>NSExceptionDomains</key>
    <dict>
        <key>xx.xx.xxx.xxx</key>
        <dict>
            <key>NSExceptionAllowsInsecureHTTPLoads</key>
            <true/>
            <key>NSIncludesSubdomains</key>
            <true/>
            <key>NSThirdPartyExceptionAllowsInsecureHTTPLoads</key>
            <true/>
        </dict>
    </dict>
</dict>

但现在我在 connectionDidFinishLoading 委托方法中得到以下响应 html:

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>400 Bad Request</title>
</head><body>
<h1>Bad Request</h1>
<p>Your browser sent a request that this server could not understand.<br />
</p>
</body>
</html>

我正在使用以下设置与服务器的信任:

func connection(connection: NSURLConnection, canAuthenticateAgainstProtectionSpace protectionSpace: NSURLProtectionSpace) -> Bool{
    return true
}
func connection(connection: NSURLConnection, willSendRequestForAuthenticationChallenge challenge: NSURLAuthenticationChallenge){
    print("willSendRequestForAuthenticationChallenge")

    let protectionSpace:NSURLProtectionSpace = challenge.protectionSpace
    let sender: NSURLAuthenticationChallengeSender? = challenge.sender

    if(protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust){
        let trust:SecTrustRef = challenge.protectionSpace.serverTrust!
        let credential:NSURLCredential = NSURLCredential.init(forTrust: trust)
        sender?.useCredential(credential, forAuthenticationChallenge: challenge)
    }
    else{
        sender?.performDefaultHandlingForAuthenticationChallenge!(challenge)
    }
}

更新 1

服务器日志显示以下错误:

Hostname xx.xx.xxx.xx provided via SNI and hostname my_secured_host_name provided via HTTP are different

如何在 SNI 中添加主机名?

更新 2

我已经从 info.plist 删除了绕过 http 的密钥,因为该服务已经是 https

更新 3

当我尝试使用 openssl 作为

openssl s_client -showcerts -connect xx.xx.xxx.xxx:443

但我收到以下错误:

CONNECTED(00000003) 8012:error:140790E5:SSL routines:SSL23_WRITE:ssl handshake failure:/SourceCache/OpenSSL098/OpenSSL098-52.40.1/src/ssl/s23_lib.c:185

更新4: 将 Info.plist 更改为以下内容:

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSExceptionDomains</key>
    <dict>
        <key>xx.xx.xxx.xxx</key>
        <dict>
            <key>NSExceptionRequiresForwardSecrecy</key>
            <false/>
            <key>NSIncludesSubdomains</key>
            <true/>
        </dict>
    </dict>
</dict>

仍然出现以下错误:

NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9802) errorAn SSL error has occurred and a secure connection to the server cannot be made.

如果您不告诉我们真实情况URL,我们将很难为您提供帮助

这些是 Apple 对安全连接的要求:

These are the App Transport Security requirements: The server must support at least Transport Layer Security (TLS) protocol version 1.2. Connection ciphers are limited to those that provide forward secrecy (see the list of ciphers below.)

Certificates must be signed using a SHA256 or greater signature hash algorithm, with either a 2048-bit or greater RSA key or a 256-bit or greater Elliptic-Curve (ECC) key. Invalid certificates result in a hard failure and no connection. These are the accepted ciphers:

  • TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
  • TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
  • TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384
  • TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA
  • TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256
  • TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA
  • TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
  • TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
  • TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384
  • TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256
  • TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA

所以,至少有一个失败了

如果您不确定是哪一个并且您有 El Capitan,只需 运行 nscurl 实用程序 /usr/bin/nscurl --ats-diagnostics xx.xx.xxx.xxx 它将测试所有可能的键和值并告诉您哪些有效。

我遇到了同样的问题,适用于我的密钥在哪里(我的证书是用 SHA1 签名的,需要 SHA256 或更高版本):

<key>NSAppTransportSecurity</key>
    <dict>
        <key>NSExceptionDomains</key>
        <dict>
            <key>xx.xx.xxx.xxx</key>
            <dict>
                <key>NSExceptionRequiresForwardSecrecy</key>
                <false/>
                <key>NSIncludesSubdomains</key>
                <true/>
            </dict>
        </dict>
    </dict>

除了已在其他答案和评论中解决的 ATS 问题之外,似乎您正在尝试通过其 IP 地址连接到 SSL 服务器。 以下参考资料可能会有用(我逐字引用了 Apple 的 iOS 开发人员库):

To override the hostname (to allow a certificate for one specific site to work for another specific site, or to allow a certificate to work when you connected to a host by its IP address), you must replace the policy object that the trust policy uses to determine how to interpret the certificate. To do this, first create a new TLS policy object for the desired hostname. Then create an array containing that policy. Finally, tell the trust object to use that array for future evaluation of trust.

SecTrustRef changeHostForTrust(SecTrustRef trust)
{
        CFMutableArrayRef newTrustPolicies = CFArrayCreateMutable(
                kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);

        SecPolicyRef sslPolicy = SecPolicyCreateSSL(true, CFSTR("www.example.com"));

        CFArrayAppendValue(newTrustPolicies, sslPolicy);

#ifdef MAC_BACKWARDS_COMPATIBILITY
        /* This technique works in OS X (v10.5 and later) */

        SecTrustSetPolicies(trust, newTrustPolicies);
        CFRelease(oldTrustPolicies);

        return trust;
#else
        /* This technique works in iOS 2 and later, or
           OS X v10.7 and later */

        CFMutableArrayRef certificates = CFArrayCreateMutable(
                kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);

        /* Copy the certificates from the original trust object */
        CFIndex count = SecTrustGetCertificateCount(trust);
        CFIndex i=0;
        for (i = 0; i < count; i++) {
                SecCertificateRef item = SecTrustGetCertificateAtIndex(trust, i);
                CFArrayAppendValue(certificates, item);
        }

        /* Create a new trust object */
        SecTrustRef newtrust = NULL;
        if (SecTrustCreateWithCertificates(certificates, newTrustPolicies, &newtrust) != errSecSuccess) {
                /* Probably a good spot to log something. */

                return NULL;
        }

        return newtrust;
#endif
}

来源:iOS Developer Library — Overriding TLS Chain Validation Correctly — Manipulating Trust Objects

请注意,在同一页面上,您可以找到另一个处理 self-signed SSL 证书的代码片段,以防您处理此类证书。

要在 Swift 项目 中使用此函数,请将新的 C 文件 添加到您的项目(File..新.. 文件.. iOS/Source/C_File),例如mysectrust.c和对应的headermysectrust.h如果XCode要求你创建桥接header,说是) :

mysectrust.h

#ifndef mysectrust_h
#define mysectrust_h

#include <Security/Security.h>

SecTrustRef changeHostForTrust(SecTrustRef trust);

#endif /* mysectrust_h */

mysectrust.c

#include "mysectrust.h"

SecTrustRef changeHostForTrust(SecTrustRef trust)
{
    CFMutableArrayRef newTrustPolicies = CFArrayCreateMutable(
                                                              kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);

    SecPolicyRef sslPolicy = SecPolicyCreateSSL(true, CFSTR("www.example.com"));

    CFArrayAppendValue(newTrustPolicies, sslPolicy);

#ifdef MAC_BACKWARDS_COMPATIBILITY
    /* This technique works in OS X (v10.5 and later) */

    SecTrustSetPolicies(trust, newTrustPolicies);
    CFRelease(oldTrustPolicies);

    return trust;
#else
    /* This technique works in iOS 2 and later, or
     OS X v10.7 and later */

    CFMutableArrayRef certificates = CFArrayCreateMutable(
                                                          kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);

    /* Copy the certificates from the original trust object */
    CFIndex count = SecTrustGetCertificateCount(trust);
    CFIndex i=0;
    for (i = 0; i < count; i++) {
        SecCertificateRef item = SecTrustGetCertificateAtIndex(trust, i);
        CFArrayAppendValue(certificates, item);
    }

    /* Create a new trust object */
    SecTrustRef newtrust = NULL;
    if (SecTrustCreateWithCertificates(certificates, newTrustPolicies, &newtrust) != errSecSuccess) {
        /* Probably a good spot to log something. */

        return NULL;
    }

    return newtrust;
#endif
}

当然,将上面代码中的www.example.com替换为你的主机名。

然后,在您的 Xcode 项目 projectname-Bridging-Header.h 中找到桥接 header,并附加以下行:

#import "mysectrust.h"

现在您可以从 Swift 调用此函数,例如:

func whatever(trust: SecTrustRef){

    let newTrust = changeHostForTrust(trust) // call to C function
    ...
}   

我和你有同样的问题,我可以解决它 - 主要是根据你已经研究过的,张贴在这里,以及 Steven Peterson.

提供的背景信息

我发现如果我尝试使用以下设置连接,-9802 错误就消失了:

那么只需要看看这些设置中的哪一个提供了解决方案。这只是找出如果删除哪个会再次破坏它的问题。

当然,这可能与您的情况不同。

请注意,我确实按名称而不是编号指定域,正如 magma 之前指出的那样。 另外(至少在我的情况下)解决这个问题没有涉及编码。

解决了之后,我后来了解到可以自动确定要使用的设置:

/usr/bin/nscurl --ats-diagnostics --verbose https://your-domain.com