Skip to content
This repository has been archived by the owner on Sep 29, 2024. It is now read-only.

Commit

Permalink
Merge pull request #3 from keeshux/client-certificate
Browse files Browse the repository at this point in the history
Client certificate
  • Loading branch information
keeshux authored Aug 28, 2018
2 parents 8c3c7b6 + 184b998 commit b2d4530
Show file tree
Hide file tree
Showing 11 changed files with 173 additions and 48 deletions.
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,17 @@ Website: [davidederosa.com][me-website]
The client is known to work with [OpenVPN®][openvpn] 2.3+ servers. Key renegotiation and replay protection are also included, but full-fledged configuration files (.ovpn) are not currently supported.

- [x] Handshake and tunneling over UDP or TCP
- [x] Client-initiated renegotiation
- [x] Replay protection (hardcoded window)
- [x] Data encryption
- [x] Ciphers
- AES-CBC (128 and 256 bit)
- AES-GCM (128 and 256 bit)
- [x] HMAC digest
- [x] HMAC digests
- SHA-1
- SHA-256
- [x] TLS CA validation
- [x] TLS handshake
- CA validation
- Client certificate
- [x] Key renegotiation (client-initiated)
- [x] Replay protection (hardcoded window)

The library does not currently support compression, so you must disable it server-side in order to avoid a confusing loss of data packets. The `TunnelKitProvider.Configuration.LZOFraming` option is deprecated and only provided for interoperability with `comp-lzo no`.

Expand Down Expand Up @@ -73,7 +75,7 @@ For the VPN to work properly, the `BasicTunnel` demo requires:

both in the main app and the tunnel extension target.

In order to test connection to your own server, modify the file `Demo/BasicTunnel-[iOS|macOS]/ViewController.swift` and make sure to set `builder.ca` to the PEM encoded certificate of your VPN server's CA (or `nil` if none).
In order to test connection to your own server, modify the file `Demo/BasicTunnel-[iOS|macOS]/ViewController.swift` and make sure to set `builder.ca` to the PEM encoded certificate of your VPN server's CA (or `nil` to skip CA validation, however discouraged).

Example:

Expand Down
12 changes: 6 additions & 6 deletions TunnelKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@
0EC1BBA620D712DE007C4C7B /* DNSResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EC1BBA420D71190007C4C7B /* DNSResolver.swift */; };
0EC1BBA820D7D803007C4C7B /* ConnectionStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EC1BBA720D7D803007C4C7B /* ConnectionStrategy.swift */; };
0EC1BBA920D7D803007C4C7B /* ConnectionStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EC1BBA720D7D803007C4C7B /* ConnectionStrategy.swift */; };
0ECE3528212EB7770040F253 /* Certificate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECE3527212EB7770040F253 /* Certificate.swift */; };
0ECE352A212EB88E0040F253 /* Certificate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECE3527212EB7770040F253 /* Certificate.swift */; };
0ECE3528212EB7770040F253 /* CryptoContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECE3527212EB7770040F253 /* CryptoContainer.swift */; };
0ECE352A212EB88E0040F253 /* CryptoContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECE3527212EB7770040F253 /* CryptoContainer.swift */; };
0EE7A79520F61EDC00B42E6A /* PacketMacros.h in Sources */ = {isa = PBXBuildFile; fileRef = 0EE7A79420F61EDC00B42E6A /* PacketMacros.h */; };
0EE7A79620F61EDC00B42E6A /* PacketMacros.h in Sources */ = {isa = PBXBuildFile; fileRef = 0EE7A79420F61EDC00B42E6A /* PacketMacros.h */; };
0EE7A79820F6296F00B42E6A /* PacketMacros.m in Sources */ = {isa = PBXBuildFile; fileRef = 0EE7A79720F6296F00B42E6A /* PacketMacros.m */; };
Expand Down Expand Up @@ -206,7 +206,7 @@
0EBBF2FF2085196000E36B40 /* NWTCPConnectionState+Description.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NWTCPConnectionState+Description.swift"; sourceTree = "<group>"; };
0EC1BBA420D71190007C4C7B /* DNSResolver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DNSResolver.swift; sourceTree = "<group>"; };
0EC1BBA720D7D803007C4C7B /* ConnectionStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionStrategy.swift; sourceTree = "<group>"; };
0ECE3527212EB7770040F253 /* Certificate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Certificate.swift; sourceTree = "<group>"; };
0ECE3527212EB7770040F253 /* CryptoContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CryptoContainer.swift; sourceTree = "<group>"; };
0EE7A79420F61EDC00B42E6A /* PacketMacros.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PacketMacros.h; sourceTree = "<group>"; };
0EE7A79720F6296F00B42E6A /* PacketMacros.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PacketMacros.m; sourceTree = "<group>"; };
0EE7A79D20F6488400B42E6A /* DataPathEncryption.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DataPathEncryption.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -451,8 +451,8 @@
isa = PBXGroup;
children = (
0EBBF2E32084FDF400E36B40 /* Transport */,
0ECE3527212EB7770040F253 /* Certificate.swift */,
0EC1BBA720D7D803007C4C7B /* ConnectionStrategy.swift */,
0ECE3527212EB7770040F253 /* CryptoContainer.swift */,
0EC1BBA420D71190007C4C7B /* DNSResolver.swift */,
0EBBF2E42084FE6F00E36B40 /* GenericSocket.swift */,
0EFEB4AA200760EC00F81029 /* InterfaceObserver.swift */,
Expand Down Expand Up @@ -848,7 +848,7 @@
0EFEB4AC200760EC00F81029 /* InterfaceObserver.swift in Sources */,
0EFEB46D2006D3C800F81029 /* Data+Manipulation.swift in Sources */,
0EFEB47B2006D3C800F81029 /* TunnelKitProvider.swift in Sources */,
0ECE3528212EB7770040F253 /* Certificate.swift in Sources */,
0ECE3528212EB7770040F253 /* CryptoContainer.swift in Sources */,
0EFEB4742006D3C800F81029 /* CoreConfiguration.swift in Sources */,
0E07595F20EF6D1400F38FD8 /* CryptoCBC.m in Sources */,
0EC1BBA820D7D803007C4C7B /* ConnectionStrategy.swift in Sources */,
Expand Down Expand Up @@ -899,7 +899,7 @@
0EFEB4A22006D7F300F81029 /* CoreConfiguration.swift in Sources */,
0EFEB4952006D7F300F81029 /* SecureRandom.swift in Sources */,
0EFEB49A2006D7F300F81029 /* MSS.m in Sources */,
0ECE352A212EB88E0040F253 /* Certificate.swift in Sources */,
0ECE352A212EB88E0040F253 /* CryptoContainer.swift in Sources */,
0EFEB48D2006D7F300F81029 /* SessionProxy+EncryptionBridge.swift in Sources */,
0EFEB4922006D7F300F81029 /* ZeroingData.m in Sources */,
0E07596020EF6D1400F38FD8 /* CryptoCBC.m in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Certificate.swift
// CryptoContainer.swift
// TunnelKit
//
// Created by Davide De Rosa on 8/22/18.
Expand Down Expand Up @@ -37,10 +37,10 @@

import Foundation

/// Represents a TLS certificate in PEM format.
public struct Certificate: Equatable {
/// Represents a cryptographic container in PEM format.
public struct CryptoContainer: Equatable {

/// The content of the certificates in PEM format (ASCII).
/// The content in PEM format (ASCII).
public let pem: String

/// :nodoc:
Expand All @@ -55,7 +55,7 @@ public struct Certificate: Equatable {
// MARK: Equatable

/// :nodoc:
public static func ==(lhs: Certificate, rhs: Certificate) -> Bool {
public static func ==(lhs: CryptoContainer, rhs: CryptoContainer) -> Bool {
return lhs.pem == rhs.pem
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,13 @@ extension TunnelKitProvider {
public var digest: SessionProxy.Digest

/// The optional CA certificate to validate server against. Set to `nil` to disable CA validation (default).
public var ca: Certificate?
public var ca: CryptoContainer?

/// The optional client certificate to authenticate with. Set to `nil` to disable client authentication (default).
public var clientCertificate: CryptoContainer?

/// The optional key for `clientCertificate`. Set to `nil` if client authentication unused (default).
public var clientKey: CryptoContainer?

/// The MTU of the link.
public var mtu: Int
Expand Down Expand Up @@ -211,13 +217,26 @@ extension TunnelKitProvider {
throw ProviderError.configuration(field: "protocolConfiguration.providerConfiguration[\(S.digestAlgorithm)]")
}

let ca: Certificate?
if let caPEM = providerConfiguration[S.ca] as? String {
ca = Certificate(pem: caPEM)
let ca: CryptoContainer?
let clientCertificate: CryptoContainer?
let clientKey: CryptoContainer?
if let pem = providerConfiguration[S.ca] as? String {
ca = CryptoContainer(pem: pem)
} else {
ca = nil
}
if let pem = providerConfiguration[S.clientCertificate] as? String {
guard let keyPEM = providerConfiguration[S.clientKey] as? String else {
throw ProviderError.configuration(field: "protocolConfiguration.providerConfiguration[\(S.clientKey)]")
}

clientCertificate = CryptoContainer(pem: pem)
clientKey = CryptoContainer(pem: keyPEM)
} else {
clientCertificate = nil
clientKey = nil
}

prefersResolvedAddresses = providerConfiguration[S.prefersResolvedAddresses] as? Bool ?? false
resolvedAddresses = providerConfiguration[S.resolvedAddresses] as? [String]
guard let endpointProtocolsStrings = providerConfiguration[S.endpointProtocols] as? [String], !endpointProtocolsStrings.isEmpty else {
Expand All @@ -243,6 +262,8 @@ extension TunnelKitProvider {
self.cipher = cipher
self.digest = digest
self.ca = ca
self.clientCertificate = clientCertificate
self.clientKey = clientKey
mtu = providerConfiguration[S.mtu] as? Int ?? 1250
LZOFraming = providerConfiguration[S.LZOFraming] as? Bool ?? false
renegotiatesAfterSeconds = providerConfiguration[S.renegotiatesAfter] as? Int
Expand Down Expand Up @@ -277,6 +298,8 @@ extension TunnelKitProvider {
cipher: cipher,
digest: digest,
ca: ca,
clientCertificate: clientCertificate,
clientKey: clientKey,
mtu: mtu,
LZOFraming: LZOFraming,
renegotiatesAfterSeconds: renegotiatesAfterSeconds,
Expand Down Expand Up @@ -304,6 +327,10 @@ extension TunnelKitProvider {

static let ca = "CA"

static let clientCertificate = "ClientCertificate"

static let clientKey = "ClientKey"

static let mtu = "MTU"

static let LZOFraming = "LZOFraming"
Expand Down Expand Up @@ -336,7 +363,13 @@ extension TunnelKitProvider {
public let digest: SessionProxy.Digest

/// - Seealso: `TunnelKitProvider.ConfigurationBuilder.ca`
public let ca: Certificate?
public let ca: CryptoContainer?

/// - Seealso: `TunnelKitProvider.ConfigurationBuilder.clientCertificate`
public let clientCertificate: CryptoContainer?

/// - Seealso: `TunnelKitProvider.ConfigurationBuilder.clientKey`
public let clientKey: CryptoContainer?

/// - Seealso: `TunnelKitProvider.ConfigurationBuilder.mtu`
public let mtu: Int
Expand Down Expand Up @@ -405,6 +438,12 @@ extension TunnelKitProvider {
if let ca = ca {
dict[S.ca] = ca.pem
}
if let clientCertificate = clientCertificate {
dict[S.clientCertificate] = clientCertificate.pem
}
if let clientKey = clientKey {
dict[S.clientKey] = clientKey.pem
}
if let resolvedAddresses = resolvedAddresses {
dict[S.resolvedAddresses] = resolvedAddresses
}
Expand Down Expand Up @@ -464,6 +503,11 @@ extension TunnelKitProvider {
} else {
log.info("CA verification: disabled")
}
if let _ = clientCertificate {
log.info("Client verification: enabled")
} else {
log.info("Client verification: disabled")
}
log.info("MTU: \(mtu)")
log.info("LZO framing: \(LZOFraming ? "enabled" : "disabled")")
if let renegotiatesAfterSeconds = renegotiatesAfterSeconds {
Expand Down Expand Up @@ -491,7 +535,10 @@ extension TunnelKitProvider.Configuration: Equatable {
builder.cipher = cipher
builder.digest = digest
builder.ca = ca
builder.clientCertificate = clientCertificate
builder.clientKey = clientKey
builder.mtu = mtu
builder.LZOFraming = LZOFraming
builder.renegotiatesAfterSeconds = renegotiatesAfterSeconds
builder.shouldDebug = shouldDebug
builder.debugLogKey = debugLogKey
Expand All @@ -505,6 +552,8 @@ extension TunnelKitProvider.Configuration: Equatable {
(lhs.cipher == rhs.cipher) &&
(lhs.digest == rhs.digest) &&
(lhs.ca == rhs.ca) &&
(lhs.clientCertificate == rhs.clientCertificate) &&
(lhs.clientKey == rhs.clientKey) &&
(lhs.mtu == rhs.mtu) &&
(lhs.LZOFraming == rhs.LZOFraming) &&
(lhs.renegotiatesAfterSeconds == rhs.renegotiatesAfterSeconds)
Expand Down
45 changes: 37 additions & 8 deletions TunnelKit/Sources/AppExtension/TunnelKitProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,12 @@ open class TunnelKitProvider: NEPacketTunnelProvider {

private let prngSeedLength = 64

private let caTmpFilename = "CA.pem"

private var cachesURL: URL {
return URL(fileURLWithPath: NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true)[0])
}
private var tmpCaURL: URL {
return cachesURL.appendingPathComponent(caTmpFilename)

private func temporaryURL(forKey key: String) -> URL {
return cachesURL.appendingPathComponent("\(key).pem")
}

// MARK: Tunnel configuration
Expand Down Expand Up @@ -169,17 +167,44 @@ open class TunnelKitProvider: NEPacketTunnelProvider {
}

let caPath: String?
let clientCertificatePath: String?
let clientKeyPath: String?
if let ca = cfg.ca {
do {
try ca.write(to: tmpCaURL)
caPath = tmpCaURL.path
let url = temporaryURL(forKey: Configuration.Keys.ca)
try ca.write(to: url)
caPath = url.path
} catch {
completionHandler(ProviderError.certificateSerialization)
return
}
} else {
caPath = nil
}
if let clientCertificate = cfg.clientCertificate {
do {
let url = temporaryURL(forKey: Configuration.Keys.clientCertificate)
try clientCertificate.write(to: url)
clientCertificatePath = url.path
} catch {
completionHandler(ProviderError.certificateSerialization)
return
}
} else {
clientCertificatePath = nil
}
if let clientKey = cfg.clientKey {
do {
let url = temporaryURL(forKey: Configuration.Keys.clientKey)
try clientKey.write(to: url)
clientKeyPath = url.path
} catch {
completionHandler(ProviderError.certificateSerialization)
return
}
} else {
clientKeyPath = nil
}

cfg.print(appVersion: appVersion)

Expand All @@ -188,6 +213,8 @@ open class TunnelKitProvider: NEPacketTunnelProvider {
sessionConfiguration.cipher = cfg.cipher
sessionConfiguration.digest = cfg.digest
sessionConfiguration.caPath = caPath
sessionConfiguration.clientCertificatePath = clientCertificatePath
sessionConfiguration.clientKeyPath = clientKeyPath
sessionConfiguration.LZOFraming = cfg.LZOFraming
if let renegotiatesAfterSeconds = cfg.renegotiatesAfterSeconds {
sessionConfiguration.renegotiatesAfter = Double(renegotiatesAfterSeconds)
Expand Down Expand Up @@ -336,7 +363,9 @@ open class TunnelKitProvider: NEPacketTunnelProvider {
// stopped externally, unrecoverable
else {
let fm = FileManager.default
try? fm.removeItem(at: tmpCaURL)
for key in [Configuration.Keys.ca, Configuration.Keys.clientCertificate, Configuration.Keys.clientKey] {
try? fm.removeItem(at: temporaryURL(forKey: key))
}
cancelTunnelWithError(error)
}
}
Expand Down
20 changes: 11 additions & 9 deletions TunnelKit/Sources/Core/Errors.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,17 @@
extern NSString *const TunnelKitErrorDomain;

typedef NS_ENUM(NSInteger, TunnelKitErrorCode) {
TunnelKitErrorCodeCryptoBoxRandomGenerator = 101,
TunnelKitErrorCodeCryptoBoxHMAC,
TunnelKitErrorCodeCryptoBoxEncryption,
TunnelKitErrorCodeCryptoBoxAlgorithm,
TunnelKitErrorCodeTLSBoxCA = 201,
TunnelKitErrorCodeTLSBoxHandshake,
TunnelKitErrorCodeTLSBoxGeneric,
TunnelKitErrorCodeDataPathOverflow = 301,
TunnelKitErrorCodeDataPathPeerIdMismatch
TunnelKitErrorCodeCryptoBoxRandomGenerator = 101,
TunnelKitErrorCodeCryptoBoxHMAC = 102,
TunnelKitErrorCodeCryptoBoxEncryption = 103,
TunnelKitErrorCodeCryptoBoxAlgorithm = 104,
TunnelKitErrorCodeTLSBoxCA = 201,
TunnelKitErrorCodeTLSBoxHandshake = 202,
TunnelKitErrorCodeTLSBoxGeneric = 203,
TunnelKitErrorCodeTLSBoxClientCertificate = 204,
TunnelKitErrorCodeTLSBoxClientKey = 205,
TunnelKitErrorCodeDataPathOverflow = 301,
TunnelKitErrorCodeDataPathPeerIdMismatch = 302
};

static inline NSError *TunnelKitErrorWithCode(TunnelKitErrorCode code) {
Expand Down
16 changes: 16 additions & 0 deletions TunnelKit/Sources/Core/SessionProxy+Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ extension SessionProxy {
// @available(*, deprecated)
public var LZOFraming: Bool

/// The path to the optional client certificate for TLS negotiation (PEM format).
public var clientCertificatePath: String?

/// The path to the private key for the certificate at `clientCertificatePath` (PEM format).
public var clientKeyPath: String?

/// Sends periodical keep-alive packets if set.
public var keepAliveInterval: TimeInterval?

Expand All @@ -104,6 +110,8 @@ extension SessionProxy {
cipher = .aes128cbc
digest = .sha1
caPath = nil
clientCertificatePath = nil
clientKeyPath = nil
LZOFraming = false
keepAliveInterval = nil
renegotiatesAfter = nil
Expand All @@ -121,6 +129,8 @@ extension SessionProxy {
cipher: cipher,
digest: digest,
caPath: caPath,
clientCertificatePath: clientCertificatePath,
clientKeyPath: clientKeyPath,
LZOFraming: LZOFraming,
keepAliveInterval: keepAliveInterval,
renegotiatesAfter: renegotiatesAfter
Expand All @@ -146,6 +156,12 @@ extension SessionProxy {
/// - Seealso: `SessionProxy.ConfigurationBuilder.caPath`
public let caPath: String?

/// - Seealso: `SessionProxy.ConfigurationBuilder.clientCertificatePath`
public let clientCertificatePath: String?

/// - Seealso: `SessionProxy.ConfigurationBuilder.clientKeyPath`
public let clientKeyPath: String?

/// - Seealso: `SessionProxy.ConfigurationBuilder.LZOFraming`
public let LZOFraming: Bool

Expand Down
Loading

0 comments on commit b2d4530

Please sign in to comment.