Merge pull request #34 from keeshux/tls-auth

Implement tls-auth
This commit is contained in:
Davide De Rosa 2018-10-19 16:26:04 +02:00 committed by GitHub
commit 22ab63b4a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 300 additions and 10 deletions

View File

@ -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)

View File

@ -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)

View File

@ -140,7 +140,7 @@ 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.

View File

@ -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

View File

@ -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<UInt32>
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<UInt8>) 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)
}
}
}

View File

@ -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<Encrypter>)auth;
//- (BOOL)serializeTo:(uint8_t *)to authenticatingWith:(id<Encrypter>)auth replayId:(uint32_t)replayId timestamp:(uint32_t)timestamp error:(NSError * _Nullable __autoreleasing *)error;
- (nullable NSData *)serializedWithAuthenticator:(id<Encrypter>)auth replayId:(uint32_t)replayId timestamp:(uint32_t)timestamp error:(NSError * _Nullable __autoreleasing *)error;
@end
NS_ASSUME_NONNULL_END

View File

@ -137,3 +137,42 @@
}
@end
@implementation ControlPacket (Authentication)
- (NSInteger)capacityWithAuthenticator:(id<Encrypter>)auth
{
return auth.digestLength + PacketReplayIdLength + PacketReplayTimestampLength + self.capacity;
}
- (BOOL)serializeTo:(uint8_t *)to authenticatingWith:(id<Encrypter>)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<Encrypter>)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

View File

@ -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

View File

@ -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.

View File

@ -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()

View File

@ -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 <http://www.gnu.org/licenses/>.
//
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)
}
}

View File

@ -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)

View File

@ -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)