diff --git a/CHANGELOG.md b/CHANGELOG.md index ca340fd..a709cbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project _will soon adhere_ to [Semantic Versioning](https://semver.org/ ### Fixed - Handling of mixed DATA_V1/DATA_V2 packets. [#30](https://github.com/keeshux/tunnelkit/issues/30) +- Support for `--tls-auth` wrapping. [#34](https://github.com/keeshux/tunnelkit/pull/34) ## 1.1.2 (2018-10-18) diff --git a/README.md b/README.md index 0188188..6f7a520 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ The client is known to work with [OpenVPNĀ®][openvpn] 2.3+ servers. Key renegoti - [x] TLS handshake - CA validation - Client certificate +- [x] TLS wrapping + - Authentication (`--tls-auth`) - [x] Compression framing - Disabled - Compress (2.4) diff --git a/TunnelKit/Sources/AppExtension/TunnelKitProvider+Configuration.swift b/TunnelKit/Sources/AppExtension/TunnelKitProvider+Configuration.swift index 889cf0e..a0c0c91 100644 --- a/TunnelKit/Sources/AppExtension/TunnelKitProvider+Configuration.swift +++ b/TunnelKit/Sources/AppExtension/TunnelKitProvider+Configuration.swift @@ -140,9 +140,9 @@ extension TunnelKitProvider { /// Sets compression framing, disabled by default. public var compressionFraming: SessionProxy.CompressionFraming - /// The optional TLS wrapping. + /// The optional TLS wrapping. When `strategy == .auth`, uses `digest` as HMAC algorithm. public var tlsWrap: SessionProxy.TLSWrap? - + /// Sends periodical keep-alive packets (ping) if set. Useful with stateful firewalls. public var keepAliveSeconds: Int? diff --git a/TunnelKit/Sources/Core/ControlChannel.swift b/TunnelKit/Sources/Core/ControlChannel.swift index dd1bf81..2d51b31 100644 --- a/TunnelKit/Sources/Core/ControlChannel.swift +++ b/TunnelKit/Sources/Core/ControlChannel.swift @@ -64,6 +64,10 @@ class ControlChannel { self.init(serializer: PlainSerializer()) } + convenience init(withAuthKey key: StaticKey, digest: SessionProxy.Digest) throws { + self.init(serializer: try AuthSerializer(withKey: key, digest: digest)) + } + private init(serializer: ControlChannelSerializer) { self.serializer = serializer sessionId = nil diff --git a/TunnelKit/Sources/Core/ControlChannelSerializer.swift b/TunnelKit/Sources/Core/ControlChannelSerializer.swift index de73743..0ecf65f 100644 --- a/TunnelKit/Sources/Core/ControlChannelSerializer.swift +++ b/TunnelKit/Sources/Core/ControlChannelSerializer.swift @@ -127,3 +127,79 @@ extension ControlChannel { } } } + +extension ControlChannel { + class AuthSerializer: ControlChannelSerializer { + private let encrypter: Encrypter + + private let decrypter: Decrypter + + private let prefixLength: Int + + private let hmacLength: Int + + private let authLength: Int + + private let preambleLength: Int + + private var currentReplayId: BidirectionalState + + private let plain: PlainSerializer + + init(withKey key: StaticKey, digest: SessionProxy.Digest) throws { + let crypto = CryptoBox(cipherAlgorithm: nil, digestAlgorithm: digest.rawValue) + try crypto.configure( + withCipherEncKey: nil, + cipherDecKey: nil, + hmacEncKey: key.hmacSendKey, + hmacDecKey: key.hmacReceiveKey + ) + encrypter = crypto.encrypter() + decrypter = crypto.decrypter() + + prefixLength = PacketOpcodeLength + PacketSessionIdLength + hmacLength = crypto.digestLength() + authLength = hmacLength + PacketReplayIdLength + PacketReplayTimestampLength + preambleLength = prefixLength + authLength + + currentReplayId = BidirectionalState(withResetValue: 1) + plain = PlainSerializer() + } + + func reset() { + currentReplayId.reset() + } + + func serialize(packet: ControlPacket) throws -> Data { + return try serialize(packet: packet, timestamp: UInt32(Date().timeIntervalSince1970)) + } + + func serialize(packet: ControlPacket, timestamp: UInt32) throws -> Data { + let data = try packet.serialized(withAuthenticator: encrypter, replayId: currentReplayId.outbound, timestamp: timestamp) + currentReplayId.outbound += 1 + return data + } + + // XXX: start/end are ignored, parses whole packet + func deserialize(data packet: Data, start: Int, end: Int?) throws -> ControlPacket { + let end = packet.count + + // data starts with (prefix=(header + sessionId) + auth=(hmac + replayId)) + guard end >= preambleLength else { + throw ControlChannelError("Missing HMAC") + } + + // needs a copy for swapping + var authPacket = packet + let authCount = authPacket.count + try authPacket.withUnsafeMutableBytes { (ptr: UnsafeMutablePointer) in + PacketSwapCopy(ptr, packet, prefixLength, authLength) + try decrypter.verifyBytes(ptr, length: authCount, flags: nil) + } + + // TODO: validate replay packet id + + return try plain.deserialize(data: authPacket, start: authLength, end: nil) + } + } +} diff --git a/TunnelKit/Sources/Core/ControlPacket.h b/TunnelKit/Sources/Core/ControlPacket.h index ee70101..95b701a 100644 --- a/TunnelKit/Sources/Core/ControlPacket.h +++ b/TunnelKit/Sources/Core/ControlPacket.h @@ -29,6 +29,8 @@ NS_ASSUME_NONNULL_BEGIN +@protocol Encrypter; + @interface ControlPacket : NSObject - (instancetype)initWithCode:(PacketCode)code @@ -58,4 +60,12 @@ NS_ASSUME_NONNULL_BEGIN @end +@interface ControlPacket (Authentication) + +//- (NSInteger)capacityWithAuthenticator:(id)auth; +//- (BOOL)serializeTo:(uint8_t *)to authenticatingWith:(id)auth replayId:(uint32_t)replayId timestamp:(uint32_t)timestamp error:(NSError * _Nullable __autoreleasing *)error; +- (nullable NSData *)serializedWithAuthenticator:(id)auth replayId:(uint32_t)replayId timestamp:(uint32_t)timestamp error:(NSError * _Nullable __autoreleasing *)error; + +@end + NS_ASSUME_NONNULL_END diff --git a/TunnelKit/Sources/Core/ControlPacket.m b/TunnelKit/Sources/Core/ControlPacket.m index 626ce6e..2b26a32 100644 --- a/TunnelKit/Sources/Core/ControlPacket.m +++ b/TunnelKit/Sources/Core/ControlPacket.m @@ -137,3 +137,42 @@ } @end + +@implementation ControlPacket (Authentication) + +- (NSInteger)capacityWithAuthenticator:(id)auth +{ + return auth.digestLength + PacketReplayIdLength + PacketReplayTimestampLength + self.capacity; +} + +- (BOOL)serializeTo:(uint8_t *)to authenticatingWith:(id)auth replayId:(uint32_t)replayId timestamp:(uint32_t)timestamp error:(NSError *__autoreleasing _Nullable *)error +{ + uint8_t *ptr = to + auth.digestLength; + const uint8_t *subject = ptr; + *(uint32_t *)ptr = CFSwapInt32HostToBig(replayId); + ptr += PacketReplayIdLength; + *(uint32_t *)ptr = CFSwapInt32HostToBig(timestamp); + ptr += PacketReplayTimestampLength; + ptr += PacketHeaderSet(ptr, self.code, self.key, self.sessionId.bytes); + ptr += [self rawSerializeTo:ptr]; + + const NSInteger subjectLength = ptr - subject; + NSInteger totalLength; + if (![auth encryptBytes:subject length:subjectLength dest:to destLength:&totalLength flags:NULL error:error]) { + return NO; + } + NSCAssert(totalLength == auth.digestLength + subjectLength, @"Encrypted packet size != (Digest + Subject)"); + PacketSwap(to, auth.digestLength + PacketReplayIdLength + PacketReplayTimestampLength, PacketOpcodeLength + PacketSessionIdLength); + return YES; +} + +- (NSData *)serializedWithAuthenticator:(id)auth replayId:(uint32_t)replayId timestamp:(uint32_t)timestamp error:(NSError *__autoreleasing _Nullable *)error +{ + NSMutableData *data = [[NSMutableData alloc] initWithLength:[self capacityWithAuthenticator:auth]]; + if (![self serializeTo:data.mutableBytes authenticatingWith:auth replayId:replayId timestamp:timestamp error:error]) { + return nil; + } + return data; +} + +@end diff --git a/TunnelKit/Sources/Core/PacketMacros.h b/TunnelKit/Sources/Core/PacketMacros.h index f4f540e..7b4d54a 100644 --- a/TunnelKit/Sources/Core/PacketMacros.h +++ b/TunnelKit/Sources/Core/PacketMacros.h @@ -45,6 +45,8 @@ NS_ASSUME_NONNULL_BEGIN #define PacketAckLengthLength ((NSInteger)1) #define PacketPeerIdLength ((NSInteger)3) #define PacketPeerIdDisabled ((uint32_t)0xffffffu) +#define PacketReplayIdLength ((NSInteger)4) +#define PacketReplayTimestampLength ((NSInteger)4) typedef NS_ENUM(uint8_t, PacketCode) { PacketCodeSoftResetV1 = 0x03, @@ -95,4 +97,26 @@ static inline int PacketHeaderGetDataV2PeerId(const uint8_t *from) return ntohl(*(const uint32_t *)from & 0xffffff00); } +#pragma mark - Utils + +static inline void PacketSwap(uint8_t *ptr, NSInteger len1, NSInteger len2) +{ + // two buffers due to overlapping + uint8_t buf1[len1]; + uint8_t buf2[len2]; + memcpy(buf1, ptr, len1); + memcpy(buf2, ptr + len1, len2); + memcpy(ptr, buf2, len2); + memcpy(ptr + len2, buf1, len1); +} + +static inline void PacketSwapCopy(uint8_t *dst, NSData *src, NSInteger len1, NSInteger len2) +{ + NSCAssert(src.length >= len1 + len2, @"src is smaller than expected"); + memcpy(dst, src.bytes + len1, len2); + memcpy(dst + len2, src.bytes, len1); + const NSInteger preambleLength = len1 + len2; + memcpy(dst + preambleLength, src.bytes + preambleLength, src.length - preambleLength); +} + NS_ASSUME_NONNULL_END diff --git a/TunnelKit/Sources/Core/SessionProxy+TLSWrap.swift b/TunnelKit/Sources/Core/SessionProxy+TLSWrap.swift index 8b75f69..05feef6 100644 --- a/TunnelKit/Sources/Core/SessionProxy+TLSWrap.swift +++ b/TunnelKit/Sources/Core/SessionProxy+TLSWrap.swift @@ -32,7 +32,9 @@ extension SessionProxy { /// The wrapping strategy. public enum Strategy: String, Codable { - case none + + /// Authenticates payload (--tls-auth). + case auth } /// The wrapping strategy. diff --git a/TunnelKit/Sources/Core/SessionProxy.swift b/TunnelKit/Sources/Core/SessionProxy.swift index 921d1b9..842e025 100644 --- a/TunnelKit/Sources/Core/SessionProxy.swift +++ b/TunnelKit/Sources/Core/SessionProxy.swift @@ -174,11 +174,9 @@ public class SessionProxy { isStopping = false if let tlsWrap = configuration.tlsWrap { - - // TODO: select strategy switch tlsWrap.strategy { - default: - controlChannel = ControlChannel() + case .auth: + controlChannel = try ControlChannel(withAuthKey: tlsWrap.key, digest: configuration.digest) } } else { controlChannel = ControlChannel() diff --git a/TunnelKitTests/ControlChannelTests.swift b/TunnelKitTests/ControlChannelTests.swift new file mode 100644 index 0000000..6505f44 --- /dev/null +++ b/TunnelKitTests/ControlChannelTests.swift @@ -0,0 +1,133 @@ +// +// ControlChannelTests.swift +// TunnelKitTests +// +// Created by Davide De Rosa on 9/10/18. +// Copyright (c) 2018 Davide De Rosa. All rights reserved. +// +// https://github.com/keeshux +// +// This file is part of TunnelKit. +// +// TunnelKit is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// TunnelKit is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with TunnelKit. If not, see . +// + +import XCTest +@testable import TunnelKit +import __TunnelKitNative + +class ControlChannelTests: XCTestCase { + private let hex = "634a4d2d459d606c8e6abbec168fdcd1871462eaa2eaed84c8f403bdf8c7da737d81b5774cc35fe0a42b38aa053f1335fd4a22d721880433bbb20ae1f2d88315b2d186b3b377685506fa39d85d38da16c2ecc0d631bda64f9d8f5a8d073f18aab97ade23e49ea9e7de86784d1ed5fa356df5f7fa1d163e5537efa8d4ba61239dc301a9aa55de0e06e33a7545f7d0cc153405576464ba92942dafa5fb79c7a60663ff1e7da3122ae09d4561653bef3eeb312ad68b191e2f94cbcf4e21caff0b59f8be86567bd21787070c2dc10a8baf7e87ce2e07d7d7de25ead11bd6d6e6ec030c0a3fd50d2d0ca3c0378022bb642e954868d7b93e18a131ecbb12b0bbedb1ce" +// private let key = Data(hex: "b2d186b3b377685506fa39d85d38da16c2ecc0d6") + + override func setUp() { + super.setUp() + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + +// 38 // HARD_RESET +// 858fe14742fdae40 // session_id +// e67c9137933a412a711c0d0514aca6db6476d17d // hmac +// 000000015b96c947 // replay packet_id (seq + timestamp) +// 00 // ack_size +// 00000000 // message packet_id (HARD_RESET -> UInt32(0)) + func testHMAC() { + let key = StaticKey(biData: Data(hex: hex)) + let server = CryptoBox(cipherAlgorithm: nil, digestAlgorithm: SessionProxy.Digest.sha1.rawValue) + XCTAssertNoThrow(try server.configure(withCipherEncKey: nil, cipherDecKey: nil, hmacEncKey: key.hmacReceiveKey, hmacDecKey: key.hmacSendKey)) + +// let original = Data(hex: "38858fe14742fdae40e67c9137933a412a711c0d0514aca6db6476d17d000000015b96c9470000000000") + let hmac = Data(hex: "e67c9137933a412a711c0d0514aca6db6476d17d") + let subject = Data(hex: "000000015b96c94738858fe14742fdae400000000000") + let data = hmac + subject + print(data.toHex()) + + XCTAssertNoThrow(try server.decrypter().verifyData(data, extra: nil)) + } + +// 38 // HARD_RESET +// bccfd171ce22e085 // session_id +// e01a3454c354f3c3093b00fc8d6228a8b69ef503d56f6a572ebd26a800711b4cd4df2b9daf06cb90f82379e7815e39fb73be4ac5461752db4f35120474af82b2 // hmac +// 000000015b93b65d // replay packet_id +// 00 // ack_size +// 00000000 // message packet_id + func testAuth() { + let client = try! ControlChannel.AuthSerializer(withKey: StaticKey(data: Data(hex: hex), direction: .client), digest: .sha512) + let server = try! ControlChannel.AuthSerializer(withKey: StaticKey(data: Data(hex: hex), direction: .server), digest: .sha512) + +// let original = Data(hex: "38bccfd1") + let original = Data(hex: "38bccfd171ce22e085e01a3454c354f3c3093b00fc8d6228a8b69ef503d56f6a572ebd26a800711b4cd4df2b9daf06cb90f82379e7815e39fb73be4ac5461752db4f35120474af82b2000000015b93b65d0000000000") + let timestamp = UInt32(0x5b93b65d) + + let packet: ControlPacket + do { + packet = try client.deserialize(data: original, start: 0, end: nil) + } catch let e { + XCTAssertNil(e) + return + } + XCTAssertEqual(packet.code, .hardResetClientV2) + XCTAssertEqual(packet.sessionId, Data(hex: "bccfd171ce22e085")) + XCTAssertNil(packet.ackIds) + XCTAssertEqual(packet.packetId, 0) + + let raw: Data + do { + raw = try server.serialize(packet: packet, timestamp: timestamp) + } catch let e { + XCTAssertNil(e) + return + } + print("raw: \(raw.toHex())") + print("org: \(original.toHex())") + XCTAssertEqual(raw, original) + } + + func testCrypt() { + let client = try! ControlChannel.CryptSerializer(withKey: StaticKey(data: Data(hex: hex), direction: .client)) + let server = try! ControlChannel.CryptSerializer(withKey: StaticKey(data: Data(hex: hex), direction: .server)) + + let original = Data(hex: "407bf3d6a260e6476d000000015ba4155887940856ddb70e01693980c5c955cb5506ecf9fd3e0bcee0c802ec269427d43bf1cda1837ffbf30c83cacff852cd0b7f4c") + let timestamp = UInt32(0x5ba41558) + + let packet: ControlPacket + do { + packet = try client.deserialize(data: original, start: 0, end: nil) + } catch let e { + XCTAssertNil(e) + return + } + XCTAssertEqual(packet.code, .hardResetServerV2) + XCTAssertEqual(packet.sessionId, Data(hex: "7bf3d6a260e6476d")) + XCTAssertEqual(packet.ackIds?.count, 1) + XCTAssertEqual(packet.ackRemoteSessionId, Data(hex: "a62ec85cc767f0a6")) + XCTAssertEqual(packet.packetId, 0) + + let raw: Data + do { + raw = try server.serialize(packet: packet, timestamp: timestamp) + } catch let e { + XCTAssertNil(e) + return + } + print("raw: \(raw.toHex())") + print("org: \(original.toHex())") + XCTAssertEqual(raw, original) + } +} diff --git a/TunnelKitTests/EncryptionPerformanceTests.swift b/TunnelKitTests/EncryptionPerformanceTests.swift index 768ebec..75e1c89 100644 --- a/TunnelKitTests/EncryptionPerformanceTests.swift +++ b/TunnelKitTests/EncryptionPerformanceTests.swift @@ -80,8 +80,9 @@ class EncryptionPerformanceTests: XCTestCase { // 0.684s func testGCMEncryption() { let suite = TestUtils.generateDataSuite(1000, 100000) + let iv: [UInt8] = [0x11, 0x22, 0x33, 0x44] let ad: [UInt8] = [0x11, 0x22, 0x33, 0x44] - var flags = CryptoFlags(packetId: 0, ad: ad, adLength: 4) + var flags = CryptoFlags(iv: iv, ivLength: 4, ad: ad, adLength: 4) measure { for data in suite { let _ = try! self.gcmEncrypter.encryptData(data, flags: &flags) diff --git a/TunnelKitTests/EncryptionTests.swift b/TunnelKitTests/EncryptionTests.swift index a87c26c..43d2132 100644 --- a/TunnelKitTests/EncryptionTests.swift +++ b/TunnelKitTests/EncryptionTests.swift @@ -79,9 +79,9 @@ class EncryptionTests: XCTestCase { func testGCM() { let (client, server) = clientServer("aes-256-gcm", nil) -// let packetId: UInt32 = 0x56341200 + let packetId: [UInt8] = [0x56, 0x34, 0x12, 0x00] let ad: [UInt8] = [0x00, 0x12, 0x34, 0x56] - var flags = CryptoFlags(packetId: 0, ad: ad, adLength: 4) + var flags = CryptoFlags(iv: packetId, ivLength: 4, ad: ad, adLength: 4) let plain = Data(hex: "00112233445566778899") let encrypted = try! client.encrypter().encryptData(plain, flags: &flags) let decrypted = try! server.decrypter().decryptData(encrypted, flags: &flags)