swiftmacoscertificatevpnnevpnmanager

Connecting to VPN programmatically keeps asking for system keychain credentials


My code connects to VPN using NEVPNManager and certificate (on MacOS), the code works good but whenever I try to connect (targetManager.connection.startVPNTunnel()) the system prompt for system keychain credentials.

macos prompt for system keychain credentials

Is there to make this alert go away after the first approval?

Code:

func initVPNTunnelProviderManager(vpnConfig: Vpn, _ connect: Bool = false) {
    let url = URL(string: vpnConfig.certUrl!)
    do {
        let certData = try Data(contentsOf: url!)

        let targetManager: NEVPNManager = NEVPNManager.shared()
        targetManager.loadFromPreferences(completionHandler: { (error:Error?) in
            if let error = error {
                print(error)
            }

            switch targetManager.connection.status {
            case NEVPNStatus.connected:
                targetManager.connection.stopVPNTunnel()
                break
            case NEVPNStatus.disconnected:
                let ip = vpnConfig.serverUrl

                let providerProtocol = NEVPNProtocolIKEv2()
                providerProtocol.authenticationMethod = .certificate
                providerProtocol.serverAddress = ip
                providerProtocol.remoteIdentifier = ip
                providerProtocol.localIdentifier = "myIdentifier"

                providerProtocol.useExtendedAuthentication = false
                providerProtocol.ikeSecurityAssociationParameters.encryptionAlgorithm = .algorithmAES128GCM
                providerProtocol.ikeSecurityAssociationParameters.diffieHellmanGroup = .group19
                providerProtocol.ikeSecurityAssociationParameters.integrityAlgorithm = .SHA512
                providerProtocol.ikeSecurityAssociationParameters.lifetimeMinutes = 20

                providerProtocol.childSecurityAssociationParameters.encryptionAlgorithm = .algorithmAES128GCM
                providerProtocol.childSecurityAssociationParameters.diffieHellmanGroup = .group19
                providerProtocol.childSecurityAssociationParameters.integrityAlgorithm = .SHA512
                providerProtocol.childSecurityAssociationParameters.lifetimeMinutes = 20

                providerProtocol.deadPeerDetectionRate = .medium
                providerProtocol.disableRedirect = true
                providerProtocol.disableMOBIKE = false
                providerProtocol.enableRevocationCheck = false
                providerProtocol.enablePFS = true
                providerProtocol.useConfigurationAttributeInternalIPSubnet = false

                providerProtocol.serverCertificateCommonName = ip
                providerProtocol.serverCertificateIssuerCommonName = ip
                providerProtocol.disconnectOnSleep = true
                providerProtocol.identityDataPassword = vpnConfig.certPassword
                providerProtocol.certificateType = .ECDSA256
                providerProtocol.identityData = certData

                targetManager.protocolConfiguration = providerProtocol
                targetManager.localizedDescription = vpnConfig.name
                targetManager.isEnabled = true
                targetManager.isOnDemandEnabled = false


                targetManager.saveToPreferences(completionHandler: { (error:Error?) in
                    if let error = error {
                        print(error)
                    } else {
                        print("Save successfully")
                        if connect {
                            do {
                                try targetManager.connection.startVPNTunnel()
                            } catch {
                                print("Failed to connect")
                            }
                        }
                    }
                })
                break
            default:
                print("connection status not handled: \(targetManager.connection.status.rawValue)")
            }
        })
    } catch {
        print(error.localizedDescription)
    }
}

}


Solution

  • The workaround is to not use identityData and identityDataPassword but instead import the identity into the user’s keychain yourself (using SecItemImport) and then pass a persistent reference to the identity to NEVPNManager via the identityReference property.
    Here's a working sample:

    private func identityReference(for pkcs12Data: Data, password: String) -> Data {
    
        var importResult: CFArray? = nil
        let err = SecPKCS12Import(pkcs12Data as NSData, [
            kSecImportExportPassphrase: password
        ] as NSDictionary, &importResult)
        guard err == errSecSuccess else { fatalError() }
        let importArray = importResult! as! [[String:Any]]
        let identity = importArray[0][kSecImportItemIdentity as String]! as! SecIdentity
    
        var copyResult: CFTypeRef? = nil
        let err2 = SecItemCopyMatching([
            kSecValueRef: identity,
            kSecReturnPersistentRef: true
        ] as NSDictionary, &copyResult)
        guard err2 == errSecSuccess else { fatalError() }
        return copyResult! as! Data
    }
    
    func initVPNTunnelProviderManager(vpnConfig: Vpn, _ connect: Bool = false) {
    let url = URL(string: vpnConfig.certUrl!)
    do {
        let certData = try Data(contentsOf: url!)
    
        let targetManager: NEVPNManager = NEVPNManager.shared()
        targetManager.loadFromPreferences(completionHandler: { (error:Error?) in
            if let error = error {
                print(error)
            }
    
            switch targetManager.connection.status {
            case NEVPNStatus.connected:
                targetManager.connection.stopVPNTunnel()
                break
            case NEVPNStatus.disconnected:
                let ip = vpnConfig.serverUrl
    
                let providerProtocol = NEVPNProtocolIKEv2()
                providerProtocol.authenticationMethod = .certificate
                providerProtocol.serverAddress = ip
                providerProtocol.remoteIdentifier = ip
                providerProtocol.localIdentifier = "myIdentifier"
    
                providerProtocol.useExtendedAuthentication = false
                providerProtocol.ikeSecurityAssociationParameters.encryptionAlgorithm = .algorithmAES128GCM
                providerProtocol.ikeSecurityAssociationParameters.diffieHellmanGroup = .group19
                providerProtocol.ikeSecurityAssociationParameters.integrityAlgorithm = .SHA512
                providerProtocol.ikeSecurityAssociationParameters.lifetimeMinutes = 20
    
                providerProtocol.childSecurityAssociationParameters.encryptionAlgorithm = .algorithmAES128GCM
                providerProtocol.childSecurityAssociationParameters.diffieHellmanGroup = .group19
                providerProtocol.childSecurityAssociationParameters.integrityAlgorithm = .SHA512
                providerProtocol.childSecurityAssociationParameters.lifetimeMinutes = 20
    
                providerProtocol.deadPeerDetectionRate = .medium
                providerProtocol.disableRedirect = true
                providerProtocol.disableMOBIKE = false
                providerProtocol.enableRevocationCheck = false
                providerProtocol.enablePFS = true
                providerProtocol.useConfigurationAttributeInternalIPSubnet = false
    
                providerProtocol.serverCertificateCommonName = ip
                providerProtocol.serverCertificateIssuerCommonName = ip
                providerProtocol.disconnectOnSleep = true
                providerProtocol.identityReference = self.identityReference(for: certData, password: vpnConfig.certPassword!)
                providerProtocol.certificateType = .ECDSA256
    
                targetManager.protocolConfiguration = providerProtocol
                targetManager.localizedDescription = vpnConfig.name
                targetManager.isEnabled = true
                targetManager.isOnDemandEnabled = false
    
    
                targetManager.saveToPreferences(completionHandler: { (error:Error?) in
                    if let error = error {
                        print(error)
                    } else {
                        print("Save successfully")
                        if connect {
                            do {
                                try targetManager.connection.startVPNTunnel()
                            } catch {
                                print("Failed to connect")
                            }
                        }
                    }
                })
                break
            default:
                print("connection status not handled: \(targetManager.connection.status.rawValue)")
            }
        })
    } catch {
        print(error.localizedDescription)
    }
    }