Skip to content

Commit a8eb315

Browse files
felixschlegeldnadobaFranzBusch
authored
TLSConfiguration: separate client and broker TLS configuration (#111)
* Refactor `KafkaConfiguration.TLSConfiguration` Motivation: Previously, `TLSConfiguration` forced users into providing a key store / key pair for client verification and did not expose a canonical way to disable client verification. Modifications: * `KafkaConfiguration.TLSConfiguration` * separate `Client` and `Broker` `TLS` configuration * provide default `TLSConfiguration`: `.disableClientVerification`, and verify broker identity * `KafkaConfiguration.SecurityProtocol`: * provide `TLSConfiguration()` as default parameter value for `TLSConfiguration:` * update `TLSConfiguration` examples in `README` * Rename Security configuration types * Review David Co-authored-by: David Nadoba <[email protected]> * Review Franz Modifications: * consistent naming: `disabled` instead of `disable` * rename `rootCert` -> `trustRoots` * rename `caCertificates` -> `trustRoots` * rename `crlLocation` -> `certificateRevocationListPath` Co-authored-by: Franz Busch <[email protected]> * Review David Modifications: * add `certificateRevocationListPath` parameter default value `nil` * `KafkaConfiguration.TLSConfiguration.brokerVerification`: more readable default value documentation * README: Update security examples --------- Co-authored-by: David Nadoba <[email protected]> Co-authored-by: Franz Busch <[email protected]>
1 parent 950d1f8 commit a8eb315

File tree

3 files changed

+110
-125
lines changed

3 files changed

+110
-125
lines changed

README.md

+2-33
Original file line numberDiff line numberDiff line change
@@ -204,23 +204,8 @@ configuration.securityProtocol = .plaintext
204204
#### TLS
205205

206206
```swift
207-
let leafCert = KafkaConfiguration.TLSConfiguration.LeafAndIntermediates.pem("YOUR_LEAF_CERTIFICATE")
208-
let rootCert = KafkaConfiguration.TLSConfiguration.Root.pem("YOUR_ROOT_CERTIFICATE")
209-
210-
let privateKey = KafkaConfiguration.TLSConfiguration.PrivateKey(
211-
location: .file(location: "KEY_FILE"),
212-
password: ""
213-
)
214-
215-
let tlsConfig = KafkaConfiguration.TLSConfiguration.keyPair(
216-
privateKey: privateKey,
217-
publicKeyCertificate: leafCert,
218-
caCertificate: rootCert,
219-
crlLocation: nil
220-
)
221-
222207
var configuration = KafkaProducerConfiguration(bootstrapBrokerAddresses: [])
223-
configuration.securityProtocol = .tls(configuration: tlsConfig)
208+
configuration.securityProtocol = .tls()
224209
```
225210

226211
#### SASL
@@ -239,30 +224,14 @@ config.securityProtocol = .saslPlaintext(
239224
#### SASL + TLS
240225

241226
```swift
242-
let leafCert = KafkaConfiguration.TLSConfiguration.LeafAndIntermediates.pem("YOUR_LEAF_CERTIFICATE")
243-
let rootCert = KafkaConfiguration.TLSConfiguration.Root.pem("YOUR_ROOT_CERTIFICATE")
244-
245-
let privateKey = KafkaConfiguration.TLSConfiguration.PrivateKey(
246-
location: .file(location: "KEY_FILE"),
247-
password: ""
248-
)
249-
250-
let tlsConfig = KafkaConfiguration.TLSConfiguration.keyPair(
251-
privateKey: privateKey,
252-
publicKeyCertificate: leafCert,
253-
caCertificate: rootCert,
254-
crlLocation: nil
255-
)
256-
257227
let saslMechanism = KafkaConfiguration.SASLMechanism.scramSHA256(
258228
username: "USERNAME",
259229
password: "PASSWORD"
260230
)
261231

262232
var config = KafkaProducerConfiguration(bootstrapBrokerAddresses: [])
263233
config.securityProtocol = .saslTLS(
264-
saslMechanism: saslMechanism,
265-
tlsConfiguaration: tlsConfig
234+
saslMechanism: saslMechanism
266235
)
267236
```
268237

Sources/Kafka/Configuration/KafkaConfiguration+Security.swift

+105-89
Original file line numberDiff line numberDiff line change
@@ -42,32 +42,28 @@ extension KafkaConfiguration {
4242
}
4343
}
4444

45-
public struct Root: Sendable, Hashable {
46-
internal enum _Root: Sendable, Hashable {
45+
public struct TrustRoots: Sendable, Hashable {
46+
internal enum _TrustRoots: Sendable, Hashable {
4747
case probe
48-
case disableBrokerVerification
4948
case file(location: String)
5049
case pem(String)
5150
}
5251

53-
let _internal: _Root
52+
let _internal: _TrustRoots
5453

5554
/// A list of standard paths will be probed and the first one found will be used as the default root certificate location path.
56-
public static let probe = Root(_internal: .probe)
57-
58-
/// Disable OpenSSL's built-in broker (server) certificate verification.
59-
public static let disableBrokerVerification = Root(_internal: .disableBrokerVerification)
55+
public static let probe = TrustRoots(_internal: .probe)
6056

6157
/// File or directory path to root certificate(s) for verifying the broker's key.
62-
public static func file(location: String) -> Root {
63-
return Root(
58+
public static func file(location: String) -> TrustRoots {
59+
return TrustRoots(
6460
_internal: .file(location: location)
6561
)
6662
}
6763

68-
/// Root certificate String for verifying the broker's key.
69-
public static func pem(_ pem: String) -> Root {
70-
return Root(
64+
/// Trust roots certificate String for verifying the broker's key.
65+
public static func pem(_ pem: String) -> TrustRoots {
66+
return TrustRoots(
7167
_internal: .pem(pem)
7268
)
7369
}
@@ -122,112 +118,134 @@ extension KafkaConfiguration {
122118
}
123119
}
124120

125-
internal enum _TLSConfiguration: Sendable, Hashable {
126-
case keyPair(
121+
/// Configuration for the TLS identity of the client.
122+
public struct ClientIdentity: Sendable, Hashable {
123+
internal enum _ClientIdentity: Sendable, Hashable {
124+
case keyPair(
125+
privateKey: PrivateKey,
126+
certificates: LeafAndIntermediates
127+
)
128+
case keyStore(keyStore: KeyStore)
129+
}
130+
131+
let _internal: _ClientIdentity
132+
133+
/// Use TLS client verification with a given private/public key pair.
134+
///
135+
/// - Parameters:
136+
/// - privateKey: The client's private key (PEM) used for authentication.
137+
/// - certificate: The client's public key (PEM) used for authentication.
138+
public static func keyPair(
127139
privateKey: PrivateKey,
128-
publicKeyCertificate: LeafAndIntermediates,
129-
caCertificate: Root,
130-
crlLocation: String?
131-
)
132-
case keyStore(
133-
keyStore: KeyStore,
134-
caCertificate: Root,
135-
crlLocation: String?
136-
)
140+
certificates: LeafAndIntermediates
141+
) -> ClientIdentity {
142+
return .init(
143+
_internal: .keyPair(
144+
privateKey: privateKey,
145+
certificates: certificates
146+
)
147+
)
148+
}
149+
150+
/// Use TLS client verification with a given key store.
151+
///
152+
/// - Parameters:
153+
/// - keyStore: The client's keystore (PKCS#12) used for authentication.
154+
public static func keyStore(keyStore: KeyStore) -> ClientIdentity {
155+
return .init(_internal: .keyStore(keyStore: keyStore))
156+
}
137157
}
138158

139-
let _internal: _TLSConfiguration
140-
141-
/// Use TLS with a given private/public key pair.
142-
///
143-
/// - Parameters:
144-
///
145-
/// - privateKey: The client's private key (PEM) used for authentication.
146-
/// - publicKeyCertificate: The client's public key (PEM) used for authentication.
147-
/// - caCertificate: File or directory path to CA certificate(s) for verifying the broker's key.
148-
/// - crLocation: Path to CRL for verifying broker's certificate validity.
149-
public static func keyPair(
150-
privateKey: PrivateKey,
151-
publicKeyCertificate: LeafAndIntermediates,
152-
caCertificate: Root = .probe,
153-
crlLocation: String?
154-
) -> TLSConfiguration {
155-
return TLSConfiguration(
156-
_internal: .keyPair(
157-
privateKey: privateKey,
158-
publicKeyCertificate: publicKeyCertificate,
159-
caCertificate: caCertificate,
160-
crlLocation: crlLocation
159+
/// Configuration for the TLS verification of the broker.
160+
public struct BrokerVerification: Sendable, Hashable {
161+
internal enum _BrokerVerification: Sendable, Hashable {
162+
case disabled
163+
case verify(
164+
trustRoots: TrustRoots,
165+
certificateRevocationListPath: String?
161166
)
162-
)
163-
}
167+
}
168+
169+
let _internal: _BrokerVerification
170+
171+
/// Do not verify the identity of the broker.
172+
public static let disabled: BrokerVerification = .init(_internal: .disabled)
164173

165-
///
166-
/// - Parameters:
167-
///
168-
/// - keyStore: The client's keystore (PKCS#12) used for authentication.
169-
/// - caCertificate: File or directory path to CA certificate(s) for verifying the broker's key.
170-
/// - crlLocation: Path to CRL for verifying broker's certificate validity.
171-
public static func keyStore(
172-
keyStore: KeyStore,
173-
caCertificate: Root = .probe,
174-
crlLocation: String?
175-
) -> TLSConfiguration {
176-
return TLSConfiguration(
177-
_internal: .keyStore(
178-
keyStore: keyStore,
179-
caCertificate: caCertificate,
180-
crlLocation: crlLocation
174+
/// Verify the identity of the broker.
175+
///
176+
/// Parameters:
177+
/// - trustRoots: File or directory path to CA certificate(s) for verifying the broker's key.
178+
/// - certificateRevocationListPath: Path to CRL for verifying broker's certificate validity.
179+
public static func verify(
180+
trustRoots: TrustRoots = .probe,
181+
certificateRevocationListPath: String? = nil
182+
) -> BrokerVerification {
183+
return .init(
184+
_internal: .verify(
185+
trustRoots: trustRoots,
186+
certificateRevocationListPath: certificateRevocationListPath
187+
)
181188
)
182-
)
189+
}
183190
}
184191

192+
/// Configuration for the TLS verification of the client.
193+
/// Default: `nil`
194+
public var clientIdentity: ClientIdentity? = nil
195+
196+
/// Configuration for the TLS verification of the broker.
197+
/// Default: `verify(trustRoots: .probe, certificateRevocationListPath: nil)``
198+
public var brokerVerification: BrokerVerification = .verify(
199+
trustRoots: .probe,
200+
certificateRevocationListPath: nil
201+
)
202+
203+
public init() {}
204+
185205
// MARK: TLSConfiguration + Dictionary
186206

187207
internal var dictionary: [String: String] {
188208
var resultDict: [String: String] = [:]
189209

190-
switch self._internal {
191-
case .keyPair(let privateKey, let publicKeyCertificate, let caCertificate, let crlLocation):
210+
// Client TLS Verification
211+
switch self.clientIdentity?._internal {
212+
case .none:
213+
break
214+
case .keyPair(let privateKey, let certificate):
192215
switch privateKey.key._internal {
193216
case .file(location: let location):
194217
resultDict["ssl.key.location"] = location
195218
case .pem(let pem):
196219
resultDict["ssl.key.pem"] = pem
197220
}
198221
resultDict["ssl.key.password"] = privateKey.password
199-
switch publicKeyCertificate._internal {
222+
switch certificate._internal {
200223
case .file(location: let location):
201224
resultDict["ssl.key.location"] = location
202225
resultDict["ssl.certificate.location"] = location
203226
case .pem(let pem):
204227
resultDict["ssl.certificate.pem"] = pem
205228
}
206-
switch caCertificate._internal {
207-
case .disableBrokerVerification:
208-
resultDict["enable.ssl.certificate.verification"] = String(false)
209-
case .probe:
210-
resultDict["ssl.ca.location"] = "probe"
211-
case .file(location: let location):
212-
resultDict["ssl.ca.location"] = location
213-
case .pem(let pem):
214-
resultDict["ssl.ca.pem"] = pem
215-
}
216-
resultDict["ssl.crl.location"] = crlLocation
217-
case .keyStore(let keyStore, let caCertificate, let crlLocation):
229+
case .keyStore(let keyStore):
218230
resultDict["ssl.keystore.location"] = keyStore.location
219231
resultDict["ssl.keystore.password"] = keyStore.password
220-
switch caCertificate._internal {
221-
case .disableBrokerVerification:
222-
resultDict["enable.ssl.certificate.verification"] = String(false)
232+
}
233+
234+
// Broker TLS Verification
235+
switch self.brokerVerification._internal {
236+
case .disabled:
237+
resultDict["enable.ssl.certificate.verification"] = String(false)
238+
case .verify(let trustRoots, let certificateRevocationListPath):
239+
resultDict["enable.ssl.certificate.verification"] = String(true)
240+
switch trustRoots._internal {
223241
case .probe:
224242
resultDict["ssl.ca.location"] = "probe"
225243
case .file(location: let location):
226244
resultDict["ssl.ca.location"] = location
227245
case .pem(let pem):
228246
resultDict["ssl.ca.pem"] = pem
229247
}
230-
resultDict["ssl.crl.location"] = crlLocation
248+
resultDict["ssl.crl.location"] = certificateRevocationListPath
231249
}
232250

233251
return resultDict
@@ -277,7 +295,7 @@ extension KafkaConfiguration {
277295
}
278296

279297
/// Disable automatic key refresh by setting this property.
280-
public static let disable: KeyRefreshAttempts = .init(rawValue: 0)
298+
public static let disabled: KeyRefreshAttempts = .init(rawValue: 0)
281299
}
282300

283301
/// Minimum time in between key refresh attempts.
@@ -311,7 +329,6 @@ extension KafkaConfiguration {
311329
/// Default OAuthBearer method.
312330
///
313331
/// - Parameters:
314-
///
315332
/// - configuration: SASL/OAUTHBEARER configuration.
316333
/// The format is implementation-dependent and must be parsed accordingly.
317334
/// The default unsecured token implementation (see https://tools.ietf.org/html/rfc7515#appendix-A.5) recognizes space-separated name=value pairs with valid names including principalClaimName, principal, scopeClaimName, scope, and lifeSeconds.
@@ -327,7 +344,6 @@ extension KafkaConfiguration {
327344
/// OpenID Connect (OIDC).
328345
///
329346
/// - Parameters:
330-
///
331347
/// - configuration: SASL/OAUTHBEARER configuration.
332348
/// The format is implementation-dependent and must be parsed accordingly.
333349
/// The default unsecured token implementation (see https://tools.ietf.org/html/rfc7515#appendix-A.5) recognizes space-separated name=value pairs with valid names including principalClaimName, principal, scopeClaimName, scope, and lifeSeconds.
@@ -481,7 +497,7 @@ extension KafkaConfiguration {
481497
)
482498

483499
/// Use the Transport Layer Security (TLS) protocol.
484-
public static func tls(configuration: TLSConfiguration) -> SecurityProtocol {
500+
public static func tls(configuration: TLSConfiguration = TLSConfiguration()) -> SecurityProtocol {
485501
return SecurityProtocol(
486502
_internal: .tls(configuration: configuration)
487503
)
@@ -497,7 +513,7 @@ extension KafkaConfiguration {
497513
/// Use the Simple Authentication and Security Layer (SASL) with TLS.
498514
public static func saslTLS(
499515
saslMechanism: SASLMechanism,
500-
tlsConfiguaration: TLSConfiguration
516+
tlsConfiguaration: TLSConfiguration = TLSConfiguration()
501517
) -> SecurityProtocol {
502518
return SecurityProtocol(
503519
_internal: .saslTLS(saslMechanism: saslMechanism, tlsConfiguaration: tlsConfiguaration)

Sources/Kafka/Configuration/KafkaConfiguration.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public enum KafkaConfiguration {
6969
}
7070

7171
/// Disable the intervalled refresh (not recommended).
72-
public static let disable: RefreshInterval = .init(rawValue: -1)
72+
public static let disabled: RefreshInterval = .init(rawValue: -1)
7373
}
7474

7575
/// Period of time at which topic and broker metadata is refreshed to proactively discover any new brokers, topics, partitions or partition leader changes.
@@ -165,7 +165,7 @@ public enum KafkaConfiguration {
165165
}
166166

167167
/// Disable disconnecting from the broker on a number of send failures.
168-
public static let disable: MaximumFailures = .init(rawValue: 0)
168+
public static let disabled: MaximumFailures = .init(rawValue: 0)
169169
}
170170

171171
/// Disconnect from the broker when this number of send failures (e.g., timed-out requests) is reached.
@@ -224,7 +224,7 @@ public enum KafkaConfiguration {
224224
}
225225

226226
/// Disable the backoff and reconnect immediately.
227-
public static let disable: Backoff = .init(rawValue: 0)
227+
public static let disabled: Backoff = .init(rawValue: 0)
228228
}
229229

230230
/// The initial time to wait before reconnecting to a broker after the connection has been closed.

0 commit comments

Comments
 (0)