The original article start my blog: blog.cocosdever.com/2019/08/01/…

What’s New

  • This page was last updated on 05 August 2019
  • First updated August 1, 2019

preface

There are many similar articles on the Internet, but I found that there are some fatal errors and misunderstandings, this article is my test, after looking at the authority of the source code, try to write what the program is doing to understand.

The main character of this article is the following method, which belongs to the NSURLSessionDelegate protocol, not to mention the older version of the HTTPs-related interface (NSURLSessionTaskDelegate has a similar task-level version, which is the same).

- (void)URLSession:(NSURLSession *)session 
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge 
completionHandler:(void(^) (NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
Copy the code

By implementing this approach, we can achieve the following technical requirements:

  1. Verify that the server certificate is in the system trust list
  2. Implement the server bidirectional authentication request
  3. Verify the homemade HTTPS certificate

To understand the implementation of these three requirements, first understand the difference between HTTPS and HTTP, namely the TLS protocol. The HTTPS protocol can be simply understood as HTTP+TLS. The full name of THE TLS protocol is Transport Layer Security. It is divided into TWO parts: TLS Record and TLS Handshake. There are many articles on the Internet. Here is a detailed explanation of SSL/TLS principles.

TLS Handshake

TLS Handshake is a series of key exchanges that allow both client and server to use the same private key to encrypt symmetrically, ensuring secure data transmission. Understanding the whole handshake process will help us understand the use of HTTPS in iOS. Now I will briefly talk about the process of shaking hands, the detailed process can be seen in the article mentioned above.

TLS Handshake:

  1. The Client generates a random number Client random, specifies the supported encryption mode, and sends it to the server. (ClientHello)
  2. The Server acknowledges the encryption mode, generates a random number Server random, gives the Server certificate, and sends it to the client. (SeverHello, SeverHello Done)
  3. If the server requires bidirectional authentication, the Client must provide the Client Key Exchange certificate to the server. Then the client verifies whether the server Certificate is valid, generates a random pre-master number, encrypts the number using the public key in the server Certificate, and sends it to the server.
  4. The server uses its own certificate private key to decrypt the encryption confidence sent by the client to obtain the per-Master
  5. The client and server have the same three random numbers, generate the dialogue private key according to the same algorithm, and use the dialogue private key to encrypt the Finish information to confirm the correctness of the private key and complete the handshake.

Understand the didReceiveChallenge method

Understand the TLS handshake process, Can know the technical requirements of the above mentioned three development timing. DidReceiveChallenge method provides a parameter (void (^) (NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * Credential))completionHandler is a Block that allows developers to provide authorization information to URLSession. There are three types:

typedef NS_ENUM(NSInteger.NSURLSessionAuthChallengeDisposition) {
    // Use the specified certificate
    NSURLSessionAuthChallengeUseCredential = 0.// The default way the system handles challenges is when the proxy method is not implemented
    NSURLSessionAuthChallengePerformDefaultHandling = 1.// The TLS handshake will be cancelled
    NSURLSessionAuthChallengeCancelAuthenticationChallenge = 2./ / reject the authentication challenge to protect the space, the next will protect space certification (actual test found effect and NSURLSessionAuthChallengePerformDefaultHandling similar),
    / / to cancel please use NSURLSessionAuthChallengeCancelAuthenticationChallenge directly
    NSURLSessionAuthChallengeRejectProtectionSpace = 3,}NS_ENUM_AVAILABLE(NSURLSESSION_AVAILABLE.7_0);
Copy the code

Then look at another parameter NSURLAuthenticationChallenge * challenge, it contains the authentication challenge of basic information, which we are concerned about the service side protection (protectionSpace) space, there is a server domain name, port, TLS authenticationMethod information. With this information, developers can see what authentication methods are required for current TLS Handshake. Below I will give a certification covers 99% scenario examples (NSURLAuthenticationMethodServerTrust), authentication server certificate, to help you understand.

Handle certificate issued by authority

For a certificate issued by an authority, the certificate specifies the CA (or a sub-ca) that issues the certificate. The CORRESPONDING CA has its own CA certificate, which is installed in the system before the phone is delivered. Therefore, for the server certificate issued by an authority, The server certificate is valid as long as the CA certificate corresponding to the server certificate is found in the system, the public key of the CA certificate is used to decrypt the signature of the server certificate, and the Hash is consistent with the Hash calculated by the data carried by the server. If you do not implement the didReceiveChallenge protocol method, the system will handle it automatically. Of course, you can also try it yourself, here is the sample code:

- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void(^) (NSURLSessionAuthChallengeDisposition.NSURLCredential * _Nullable))completionHandler {
    
    // Check the authentication mode provided by the server
    NSLog(@"protectionSpace.authenticationMethod = %@", challenge.protectionSpace.authenticationMethod);
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {

	    // Check whether the server certificate is valid.
	    SecTrustResultType result;
	    SecTrustEvaluate(challenge.protectionSpace.serverTrust, &result);
	    
	    if(result == kSecTrustResultUnspecified || result == kSecTrustResultProceed) {
	        NSLog(@ "legal");
	        completionHandler(NSURLSessionAuthChallengePerformDefaultHandling.nil);
	    } else {
	        NSLog(@" Illegal");
	        completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge.nil); }}// Only one-way authentication is handled, other cases are not considered
    completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge.nil);    
}
Copy the code

Above, there is a place to need to pay special attention to when the certificate is legal, if return NSURLSessionAuthChallengePerformDefaultHandling, said by the system to deal with, Here to perform completionHandler (NSURLSessionAuthChallengeUseCredential, nil) may be used, Like NSURLSessionAuthChallengePerformDefaultHandling effect.

Process the server homemade certificate

There are two cases, one is to ignore the server certificate, the other is to require that the server certificate and our in-app certificate are the same before recognition. When the server certificate is ignored, that is, no validation is required, in which case the didReceiveChallenge method is implemented. Because of the system default is not to accept the authority of the certificate, so can’t return completionHandler (NSURLSessionAuthChallengePerformDefaultHandling, nil); . Easy to confuse is that the document was not specified one-way certification and accreditation completionHandler parameters when the server certificate into what, actual test found homemade certificate first parameter need to NSURLSessionAuthChallengeUseCredential, The second parameter is passed to the server serverTrust(which AFNetworking implements). This code looks like this:

- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void(^) (NSURLSessionAuthChallengeDisposition.NSURLCredential * _Nullable))completionHandler {
    
    // Check the authentication mode provided by the server
    NSLog(@"protectionSpace.authenticationMethod = %@", challenge.protectionSpace.authenticationMethod);
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        completionHandler(NSURLSessionAuthChallengeUseCredential, challenge.protectionSpace.serverTrust);
    }
    // Only one-way authentication is handled, other cases are not considered
    completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge.nil);    
}
Copy the code

To bind certificates, the CA certificate of the server certificate must be the same as the in-app CA certificate (or the server certificate must be the same as the in-app certificate). The principle is the same. Obtain the server certificate, then obtain the local certificate, and check whether they are the same. Homemade certificates are easy, but where can I get a homemade certificate server to test? Here’s a little trick. You can use the Charles tool, go to https://www.baidu.com, and configure the domain name to Charles,

Then the phone connects to the Charles proxy server and exports Charles’s CA certificate.

In this way, when the APP runs and visits https://www.baidu.com, Charles will replace the Baidu certificate with Charles homemade certificate. The corresponding CA certificate of homemade certificate is the one we exported. If you want to export it directly from Charles in pem format, you need to convert it to DER format:

openssl x509 -in certificate.pem -outform der -out certificate.der
Copy the code

This part of the code looks like this:

- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void(^) (NSURLSessionAuthChallengeDisposition.NSURLCredential * _Nullable))completionHandler {
    
    // Check the authentication mode provided by the server
    NSLog(@"protectionSpace.authenticationMethod = %@", challenge.protectionSpace.authenticationMethod);
    
    // Certificate of the server
    NSURLCredential *serverCredential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
    
    // Local certificate
    NSData *certificateData = [NSData dataWithContentsOfFile:[NSBundle.mainBundle pathForResource:@"certificate" ofType:@"der"]].// The system API supports matching multiple certificates
    NSMutableArray *pinnedCertificates = [NSMutableArray array];
    
    / / here is __bridge_transfer ownership to the NSMutableArray management, so there's no need to manually Release SecCertificateCreateWithData
    [pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
    
    // The C interface only binds the set of trusted certificates to the serverTrust object passed in
    SecTrustSetAnchorCertificates(challenge.protectionSpace.serverTrust, (__bridge CFArrayRef)pinnedCertificates);

    SecTrustResultType result;
    SecTrustEvaluate(challenge.protectionSpace.serverTrust, &result);
    
    if(result == kSecTrustResultUnspecified || result == kSecTrustResultProceed) {
        // The CA certificate of Charles is used, so it is valid
        NSLog(@ "legal");
        
        // In addition, you can directly check whether the local APP built-in certificate matches one of the certificate chains included in the server certificate
        CFIndex certificateCount = SecTrustGetCertificateCount(challenge.protectionSpace.serverTrust);
        NSMutableArray *trustChain = [NSMutableArray arrayWithCapacity:(NSUInteger)certificateCount];
        
        for (CFIndex i = 0; i < certificateCount; i++) {
            SecCertificateRef certificate = SecTrustGetCertificateAtIndex(challenge.protectionSpace.serverTrust, i);
            [trustChain addObject:(__bridge_transfer NSData *)SecCertificateCopyData(certificate)];
        }
        
        NSArray *serverCertificates =  [NSArray arrayWithArray:trustChain];
        
        // Check whether the server certificate contains the local certificate
        if ([serverCertificates containsObject:certificateData]) {
            NSLog(@" Server certificate is the same as local certificate");
            completionHandler(NSURLSessionAuthChallengeUseCredential, serverCredential);
        }else {
            completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge.nil); }}else {
        NSLog(@" Illegal");
        completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge.nil); }}Copy the code

The results

One important point to note is that many articles have mixed the server return status code 401 with TLS. The header of the status code 401 is already encrypted after the TLS handshake to verify that both parties are valid. So 401 is not the same thing as HTTPS permission authentication. However, they can all be certified on iOS with the Task-level didReceiveChallenge.

Handle TLS Handshake two-way authentication

Take a look at the didReceiveChallenge documentation first, which makes it very clear

This method is called in two situations:

  • When a remote server asks for client certificates or Windows NT LAN Manager (NTLM) authentication, to allow your app to provide appropriate credentials
  • When a session first establishes a connection to a remote server that uses SSL or TLS, to allow your app to verify the server’s certificate chain

Note

This method handles only the NSURLAuthenticationMethodNTLM, NSURLAuthenticationMethodNegotiate, NSURLAuthenticationMethodClientCertificate, and NSURLAuthenticationMethodServerTrust authentication types. For all other authentication schemes, the session calls only the URLSession:task:didReceiveChallenge:completionHandler: method.

The didReceiveChallenge method of session-level will only be triggered in the following situations

  1. Remote server requires client to provide certificate (bidirectional authentication)
  2. NTLM Certification (provided by Microsoft, specifically Google)
  3. The SSL or TLS handshake phase allows you to verify that the server certificate chain is valid (described above). Other authentication states will call task-level proxy methods.

The second parameter of completionHandler is passed in the certificate recognized by the server.

conclusion

This article first describes the HTTPS protocol encryption principle, then describes the implementation of HTTPS authentication related scenarios that can be encountered in iOS development, gives the common authentication code, and explains why to do it. Some of the code for the C interface is based on the AFNetworking framework, which encapsulates authoritative authentication, one-way custom authentication, and seems to lack two-way authentication. However, AFSecurityPolicy does provide a block for developer custom authentication logic. Can directly implement authentication logic block and assigned to sessionDidReceiveAuthenticationChallenge can. Other interested can consult the code, the source code is relatively simple.