How to add certificate and privatekey in https request in swift?

Hi I want to add certificate and its privatekey in https request, like this mentioned in this cURL `curl -L -i -X PUT –cert ./[device_cert].pem –key ./[device_cert_private_key].pem -H 'Content-Type: application/json' -H 'Content-Encoding: utf-8' -d '{"registrationId": "[registration_id]"}' https://global.azure-devices-provisioning.net/%5BID_Scope%5D/registrations/%5Bregistration_id%5D/register?api-version=2021-06-01

Answered by DTS Engineer in 798401022

I recommend that you start by reading TLS for App Developers.

As to your specific question, it depends on what HTTP API you’re using. The most popular is URLSession, and that supports mTLS via the authentication challenge mechanism. You can find general information about that in Handling an authentication challenge.

The specific challenge here is NSURLAuthenticationMethodClientCertificate. To resolve that challenge you need a credential with containing a digital identity (SecIdentity). You usually get that from the keychain using the SecItem API. If you start out with a PEM, you typically convert that to a PKCS#12 and then import it using SecPKCS12Import(…).

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

I recommend that you start by reading TLS for App Developers.

As to your specific question, it depends on what HTTP API you’re using. The most popular is URLSession, and that supports mTLS via the authentication challenge mechanism. You can find general information about that in Handling an authentication challenge.

The specific challenge here is NSURLAuthenticationMethodClientCertificate. To resolve that challenge you need a credential with containing a digital identity (SecIdentity). You usually get that from the keychain using the SecItem API. If you start out with a PEM, you typically convert that to a PKCS#12 and then import it using SecPKCS12Import(…).

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Hi I am very new to swift. What I have tried till now is

import UIKit
import Foundation
import Security

// Custom URLSessionDelegate
class SSLPinningDelegate: NSObject, URLSessionDelegate {
    
    let certificate: SecCertificate
    let privateKey: SecKey
    
    init(certificate: SecCertificate, privateKey: SecKey) {
        self.certificate = certificate
        self.privateKey = privateKey
    }
    
    func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate {
            let credential = URLCredential(identity: privateKey as! SecIdentity, certificates: [certificate], persistence: .forSession)
            completionHandler(.useCredential, credential)
        } else {
            completionHandler(.performDefaultHandling, nil)
        }
    }
}

@objc(AzureProvisionWithCertificate)
class AzureProvisionWithCertificate: NSObject {

  @objc(provisionAndUploadFile:withRegistrationId:withKey:withCertificate:withProvisionHost:withFileNameWithFolder:withModelId:withResolver:withRejecter:)
  func provisionAndUploadFile(scopeId:String, registrationId:String, key:String, certificate:String,  provisionHost:String,  fileNameWithFolder:String,  modelId:String, resolve:@escaping RCTPromiseResolveBlock, reject:@escaping RCTPromiseRejectBlock) -> Void {
   
    print("started: provisionAndUploadFile api")

        // Create a session with the SSLPinningDelegate
      let sslPinningDelegate = SSLPinningDelegate(certificate: certificate as! SecCertificate, privateKey: key as! SecKey)
print("sslPinningDelegate: \(sslPinningDelegate)")


    // Create the URL
    let url = URL(string: "https://global.azure-devices-provisioning.net/\(scopeId)/registrations/\(registrationId)/register?api-version=2021-06-01")!

    // Create the request
    var request = URLRequest(url: url)
    request.httpMethod = "PUT"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.setValue("utf-8", forHTTPHeaderField: "Content-Encoding")
    // Create the request body
    let bodyData = ["registrationId": registrationId]
    let jsonData = try? JSONSerialization.data(withJSONObject: bodyData)

    // Set the request body
    request.httpBody = jsonData

    // Create the session configuration
    let sessionConfig = URLSessionConfiguration.default
    let session = URLSession(configuration: sessionConfig)

    // Create the data task
    let task = session.dataTask(with: request) { (data, response, error) in
        if let error = error {
            print("Error: \(error.localizedDescription)")
            reject("Error", error.localizedDescription, error)
        } else if let data = data {
            // Process the response data
            let responseString = String(data: data, encoding: .utf8)
            print("Response data: \(responseString)")
            resolve("Response data: \(responseString)")
        } else {
            print("No data received")
            reject("Error", "No data received", nil)
        }
    }

    // Start the data task
    task.resume()

  }
}

Please help me to correct it.

Thanks for sharing the code. I have one bit of structural feedback that I’ll come back to below but, in general, the structure of this code looks OK.

Focusing of mTLS for the moment, the key problem is this line:

let credential = URLCredential(identity: privateKey as! SecIdentity, certificates: [certificate], …)

That’s not how you create a credential for mTLS. Rather, you typically do this:

let identity: SecIdentity = …
let credential = URLCredential(identity: identity, certificates: nil, …)

That’s because a digital identity represents both a certificate and the private key that matches the public key in that certificate. You can’t just cast a SecKey object to the SecIdentity type.

Looking at the call site, it’s seems that you’re creating your private key and certificate by casting strings. That’s also not going to work. If your credentials start off in the PEM format, you have to import them into the keychain. Once there, you can get an identity object from the keychain.

The easiest way to do this is to use openssl to convert the credentials to a PKCS#12 (.p12) file and then import that into the keychain using SecPKCS12Import(…).


Oh, and regarding that structural issue, your current code seems to create a new session for each request. That’s not a great plan because sessions are relatively heavyweight. I generally recommend that you create a single session and then reuse that for all your requests.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Hi I have updated the code but I am getting error : Cannot find 'SecIdentityCreateWithCertificate' in scope

import Foundation
import Security
import UIKit

class URLSessionPinningDelegate: NSObject, URLSessionDelegate {
    var identity: SecIdentity

    init(identity: SecIdentity) {
        self.identity = identity
    }

    func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        let credential = URLCredential(identity: self.identity, certificates: nil, persistence: .forSession)
        completionHandler(.useCredential, credential)
    }
}

func loadIdentity(certPath: String, keyPath: String) -> SecIdentity? {
    guard let certData = try? Data(contentsOf: URL(fileURLWithPath: certPath)) else {
        print("Unable to load certificate")
        return nil
    }

    guard let cert = SecCertificateCreateWithData(nil, certData as CFData) else {
        print("Unable to create certificate")
        return nil
    }

    guard let keyData = try? Data(contentsOf: URL(fileURLWithPath: keyPath)) else {
        print("Unable to load private key")
        return nil
    }

    let keyDict: [NSString: Any] = [
        kSecAttrKeyType: kSecAttrKeyTypeRSA,
        kSecAttrKeyClass: kSecAttrKeyClassPrivate,
        kSecAttrKeySizeInBits: 2048,
        kSecReturnPersistentRef: true
    ]

    var error: Unmanaged<CFError>?
    guard let privateKey = SecKeyCreateWithData(keyData as CFData, keyDict as CFDictionary, &error) else {
        // print("Unable to create private key: \(error?.takeRetainedValue() ?? "Unknown error" as CFError)")
        print("Unable to create private key")
        return nil
    }

    var identity: SecIdentity?
    let status = SecIdentityCreateWithCertificate(cert, privateKey, &identity) // GETTING ERROR HERE : Cannot find 'SecIdentityCreateWithCertificate' in scope
    guard status == errSecSuccess else {
        print("Unable to create identity")
        return nil
    }

    return identity
}


@objc(AzureProvisionWithCertificate)
class AzureProvisionWithCertificate: NSObject {

  @objc(provisionAndUploadFile:withRegistrationId:withKey:withCertificate:withProvisionHost:withFileNameWithFolder:withModelId:withResolver:withRejecter:)
  func provisionAndUploadFile(scopeId:String, registrationId:String, key:String, certificate:String,  provisionHost:String,  fileNameWithFolder:String,  modelId:String, resolve:@escaping RCTPromiseResolveBlock, reject:@escaping RCTPromiseRejectBlock) -> Void {
 let certPath = "/path/to/your/device-cert.pem"
    let keyPath = "/path/to/your/device-key.pem"

    guard let identity = loadIdentity(certPath: certificate, keyPath: key) else {
        print("Unable to load identity")
        return
    }
    
    let session = URLSession(configuration: .default, delegate: URLSessionPinningDelegate(identity: identity), delegateQueue: nil)
    
    guard let url = URL(string: "https://global.azure-devices-provisioning.net/[scopeId]/registrations/[registrationId]/register?api-version=2021-06-01") else {
        print("Invalid URL")
        return
    }
      var request = URLRequest(url: url)
    request.httpMethod = "PUT"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.setValue("utf-8", forHTTPHeaderField: "Content-Encoding")
    
    let body = ["registrationId": "registrationId"]
    request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: [])

    let task = session.dataTask(with: request) { data, response, error in
        if let error = error {
            print("Request failed: \(error)")
        } else if let data = data, let responseString = String(data: data, encoding: .utf8) {
            print("Response: \(responseString)")
        }
    }
    
    task.resume()

  }
}

I am using xcode 15.3 and macOS Sonoma 14.5

How to add certificate and privatekey in https request in swift?
 
 
Q