How to generate a self signed certificate and validate properly it in URLSession challenge - roznet/remotestash GitHub Wiki

A common issue in testing is to have a local server with a self signed certificate. It is easy to avoid the failed certificate verification by implementing the urlSession(_:didReceive:completionHandler:) function to force the authentication to succeed with code like

func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
    if challenge.protectionSpace.host == "you-test-host.com" {
        completionHandler(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!))
    } else {
        completionHandler(.performDefaultHandling, nil)
    }
}

But this feels wrong, so RemoteStash will generate a certificate of authority key and public key, embedded in the app the key and verify that requests from the RemoteStash service are properly signed with this key.

How to generate the keys with openssl

First, create a certificate authority. It will require a passphrase to be used later when signing other certificate. RemoteStash will embed a copy of the public key, but the private key will not be shared.

# Subject line to use in the certificate
SUBJ="/C=US/O=RemoteStash/OU=server/CN=localhost"
SUBJCA="/C=US/O=RemoteStashCA/OU=CA/CN=authority"

openssl genrsa -des3 -out remotestash-ca.key 2048
openssl req -x509 -new -nodes -key remotestash-ca.key -sha256 -days 3650 -out remotestash-ca.pem -subj $SUBJCA

Second, create a public and private key to be used by the service. This will be shared and used by the python script and the app.

openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 -keyout remotestash-key.pem -out remotestash-cert.pem -subj $SUBJ

Third, create a signing request and use the certificate of authority private key to sign the keys for the service.

openssl req -new -key remotestash-key.pem -out remotestash-cert.csr -subj $SUBJ
openssl x509 -req -in remotestash-cert.csr -CA remotestash-ca.pem -CAkey remotestash-ca.key -CAcreateserial -out remotestash-cert-signed.pem -days 3650 -sha256

The last step is to convert the signed certificate to a der format which will be read to create the SecPolicy object in swift to validate the certificate

openssl x509 -outform der -in remotestash-cert-signed.pem -out remotestash-cert-signed.der
openssl x509 -outform der -in remotestash-ca.pem -out remotestash-ca.der

The full process is summarised and implement to easy re-run in create.sh

How to validate the authentication challenge

First, extract the certificate used by the request:

    func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        let url  = task.currentRequest?.url ?? URL(fileURLWithPath: "")
        
        // Extract the certificate to validate from the challenge and load our known CA certificate
        guard
            let trust = challenge.protectionSpace.serverTrust,
            let remoteCert = SecTrustGetCertificateAtIndex(trust, 0)
        else {
            logger.error("\(url) server trust failed to setup manual authentication")
            completionHandler(.cancelAuthenticationChallenge,nil)
            return
        }

Second, create a load the public key of the certificate of authority you created, in this case remotestash-ca.der

            let caCertPath = Bundle.main.path(forResource: "remotestash-ca", ofType: "der"),
            let caCertData = try? Data(contentsOf: URL(fileURLWithPath: caCertPath)),
            let caCert = SecCertificateCreateWithData(nil, caCertData as CFData),

Third, create a SecPolicy to validate that the remote certificate was signed by the certificate of authority above. Note that we create a new BasicX509 SecPolicy instead of setting the AnchorCertificates of the passed in trust object because that object uses a SSL policy that validates the SSL chain using the host of the request, while here we do not know the host at the time the certificate was signed.

        // Create a trust object to valid the remote certificate against our certificate authority
        let policy = SecPolicyCreateBasicX509()
        var optionalTrust : SecTrust?
        var trustResult : SecTrustResultType = SecTrustResultType.invalid
        guard
            SecTrustCreateWithCertificates([remoteCert] as CFArray, policy, &optionalTrust) == errSecSuccess,
            let caTrust = optionalTrust,
            SecTrustSetAnchorCertificates(caTrust, [caCert] as CFArray) == errSecSuccess,
            SecTrustGetTrustResult(caTrust, &trustResult) == errSecSuccess
        else{
            logger.error("\(url) server trust failed to execute authentication")
            completionHandler(.cancelAuthenticationChallenge,nil)
            return
        }

Finally, we check the result of the trust evaluation to make sure we can proceed. Note that .unspecified and .proceed both mean the certificate was valid, but .unspecified means the user hasn't yet specified if they accepted that certificate in their keychain.

        switch trustResult{
        case .unspecified,.proceed:
            logger.info("\(url) remotestash certificate valid")
            completionHandler(.useCredential,URLCredential(trust: trust))
        default:
            logger.info("\(url) invalid certificate")
            completionHandler(.cancelAuthenticationChallenge,nil)
        }

The full implementation in in RemoteStashService.swift