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

Client certificate #3

Merged
merged 8 commits into from
Aug 28, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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