Skip to content

Commit c4f0dbe

Browse files
Add support for additional TLS trust roots (#284)
Until now `TLSConfiguration` has support for specifying trust roots using _one_ of the following: * Path to file or directory with certificates, or * A list of `NIOSSLCertificate` the caller has in their hand, or * The system default. It's potentially useful to specify a combination of these, e.g. the system default _and_ some additional provided certificates. This patch adds a new field, `TLSConfiguration.additionalTrustRoots` which will be used in combination with the existing `TLSConfiguration.trustRoots`. `TLSConfigurationTest` needed the following changes: * Stop using `connectInMemory` for testing successful handshakes on Darwin because it is not thread-safe when used with a verify callback. * Add Extended Key Usage extenstions to the keys used in tests to satisfy the stricter requirements of the Security.framework verifier.
1 parent 07c160b commit c4f0dbe

File tree

7 files changed

+295
-74
lines changed

7 files changed

+295
-74
lines changed

Sources/NIOSSL/SSLConnection.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ enum AsyncOperationResult<T> {
4646
/// used to create the connection.
4747
internal final class SSLConnection {
4848
private let ssl: OpaquePointer
49-
private let parentContext: NIOSSLContext
49+
internal let parentContext: NIOSSLContext
5050
private var bio: ByteBufferBIO?
5151
internal var expectedHostname: String?
5252
internal var role: ConnectionRole?

Sources/NIOSSL/SSLContext.swift

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -159,10 +159,30 @@ public final class NIOSSLContext {
159159
returnCode = CNIOBoringSSL_SSL_CTX_set_cipher_list(context, configuration.cipherSuites)
160160
precondition(1 == returnCode)
161161

162+
// On non-Linux platforms, when using the platform default trust roots, we make use of a
163+
// custom verify callback. If we have also been presented with additional trust roots of
164+
// type `.file`, we take the opportunity now to load them in memory to avoid doing so
165+
// repeatedly on the request path.
166+
//
167+
// However, to avoid closely coupling this code with other parts (e.g. the platform-specific
168+
// concerns, and the defaulting of `trustRoots` to `.default` when `nil`), we unilaterally
169+
// convert any `additionalTrustRoots` of type `.file` to `.certificates`.
170+
var configuration = configuration
171+
configuration.additionalTrustRoots = try configuration.additionalTrustRoots.map { trustRoots in
172+
switch trustRoots {
173+
case .file(let path):
174+
return .certificates(try NIOSSLCertificate.fromPEMFile(path))
175+
default:
176+
return trustRoots
177+
}
178+
}
179+
162180
// Configure certificate validation
163-
try NIOSSLContext.configureCertificateValidation(context: context,
164-
verification: configuration.certificateVerification,
165-
trustRoots: configuration.trustRoots)
181+
try NIOSSLContext.configureCertificateValidation(
182+
context: context,
183+
verification: configuration.certificateVerification,
184+
trustRoots: configuration.trustRoots,
185+
additionalTrustRoots: configuration.additionalTrustRoots)
166186

167187
// Configure verification algorithms
168188
if let verifySignatureAlgorithms = configuration.verifySignatureAlgorithms {
@@ -189,7 +209,7 @@ public final class NIOSSLContext {
189209
throw BoringSSLError.unknownError(errorStack)
190210
}
191211
}
192-
212+
193213
// If we were given a certificate chain to use, load it and its associated private key. Before
194214
// we do, set up a passphrase callback if we need to.
195215
if let callbackManager = callbackManager {
@@ -383,10 +403,9 @@ extension NIOSSLContext {
383403
}
384404
}
385405

386-
387406
// Configuring certificate verification
388407
extension NIOSSLContext {
389-
private static func configureCertificateValidation(context: OpaquePointer, verification: CertificateVerification, trustRoots: NIOSSLTrustRoots?) throws {
408+
private static func configureCertificateValidation(context: OpaquePointer, verification: CertificateVerification, trustRoots: NIOSSLTrustRoots?, additionalTrustRoots: [NIOSSLAdditionalTrustRoots]) throws {
390409
// If validation is turned on, set the trust roots and turn on cert validation.
391410
switch verification {
392411
case .fullVerification, .noHostnameVerification:
@@ -398,14 +417,18 @@ extension NIOSSLContext {
398417
let trustParams = CNIOBoringSSL_SSL_CTX_get0_param(context)!
399418
CNIOBoringSSL_X509_VERIFY_PARAM_set_flags(trustParams, CUnsignedLong(X509_V_FLAG_TRUSTED_FIRST))
400419

401-
switch trustRoots {
402-
case .some(.default), .none:
403-
try NIOSSLContext.platformDefaultConfiguration(context: context)
404-
case .some(.file(let f)):
405-
try NIOSSLContext.loadVerifyLocations(f, context: context)
406-
case .some(.certificates(let certs)):
407-
try certs.forEach { try NIOSSLContext.addRootCertificate($0, context: context) }
420+
func configureTrustRoots(trustRoots: NIOSSLTrustRoots) throws {
421+
switch trustRoots {
422+
case .default:
423+
try NIOSSLContext.platformDefaultConfiguration(context: context)
424+
case .file(let path):
425+
try NIOSSLContext.loadVerifyLocations(path, context: context)
426+
case .certificates(let certs):
427+
try certs.forEach { try NIOSSLContext.addRootCertificate($0, context: context) }
428+
}
408429
}
430+
try configureTrustRoots(trustRoots: trustRoots ?? .default)
431+
try additionalTrustRoots.forEach { try configureTrustRoots(trustRoots: .init(from: $0)) }
409432
default:
410433
break
411434
}

Sources/NIOSSL/SecurityFrameworkCertificateVerification.swift

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,12 @@ import Security
2828
extension SSLConnection {
2929
func performSecurityFrameworkValidation(promise: EventLoopPromise<NIOSSLVerificationResult>) {
3030
do {
31+
guard case .default = self.parentContext.configuration.trustRoots ?? .default else {
32+
preconditionFailure("This callback should only be used if we are using the system-default trust.")
33+
}
34+
3135
// Ok, time to kick off a validation. Let's get some certificate buffers.
32-
let certificates: [SecCertificate] = try self.withPeerCertificateChainBuffers { buffers in
36+
let peerCertificates: [SecCertificate] = try self.withPeerCertificateChainBuffers { buffers in
3337
guard let buffers = buffers else {
3438
throw NIOSSLError.unableToValidateCertificate
3539
}
@@ -47,11 +51,33 @@ extension SSLConnection {
4751
var trust: SecTrust? = nil
4852
var result: OSStatus
4953
let policy = SecPolicyCreateSSL(self.role! == .client, self.expectedHostname as CFString?)
50-
result = SecTrustCreateWithCertificates(certificates as CFArray, policy, &trust)
54+
result = SecTrustCreateWithCertificates(peerCertificates as CFArray, policy, &trust)
5155
guard result == errSecSuccess, let actualTrust = trust else {
5256
throw NIOSSLError.unableToValidateCertificate
5357
}
5458

59+
// If there are additional trust roots then we need to add them to the SecTrust as anchors.
60+
let additionalAnchorCertificates: [SecCertificate] = try self.parentContext.configuration.additionalTrustRoots.flatMap { trustRoots -> [NIOSSLCertificate] in
61+
guard case .certificates(let certs) = trustRoots else {
62+
preconditionFailure("This callback happens on the request path, file-based additional trust roots should be pre-loaded when creating the SSLContext.")
63+
}
64+
return certs
65+
}.map {
66+
guard let secCert = SecCertificateCreateWithData(nil, Data(try $0.toDERBytes()) as CFData) else {
67+
throw NIOSSLError.failedToLoadCertificate
68+
}
69+
return secCert
70+
}
71+
if !additionalAnchorCertificates.isEmpty {
72+
// To use additional anchors _and_ the built-in ones we must reenable the built-in ones expicitly.
73+
guard SecTrustSetAnchorCertificatesOnly(actualTrust, false) == errSecSuccess else {
74+
throw NIOSSLError.failedToLoadCertificate
75+
}
76+
guard SecTrustSetAnchorCertificates(actualTrust, additionalAnchorCertificates as CFArray) == errSecSuccess else {
77+
throw NIOSSLError.failedToLoadCertificate
78+
}
79+
}
80+
5581
// We create a DispatchQueue here to be called back on, as this validation may perform network activity.
5682
let callbackQueue = DispatchQueue(label: "io.swiftnio.ssl.validationCallbackQueue")
5783

Sources/NIOSSL/TLSConfiguration.swift

Lines changed: 120 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,43 @@ public enum NIOSSLPrivateKeySource {
4242

4343
/// Places NIOSSL can obtain a trust store from.
4444
public enum NIOSSLTrustRoots {
45+
/// Path to either a file of CA certificates in PEM format, or a directory containing CA certificates in PEM format.
46+
///
47+
/// If a path to a file is provided, the file can contain several CA certificates identified by
48+
///
49+
/// -----BEGIN CERTIFICATE-----
50+
/// ... (CA certificate in base64 encoding) ...
51+
/// -----END CERTIFICATE-----
52+
///
53+
/// sequences. Before, between, and after the certificates, text is allowed which can be used e.g.
54+
/// for descriptions of the certificates.
55+
///
56+
/// If a path to a directory is provided, the files each contain one CA certificate in PEM format.
4557
case file(String)
58+
59+
/// A list of certificates.
4660
case certificates([NIOSSLCertificate])
61+
62+
/// The system default root of trust.
4763
case `default`
64+
65+
internal init(from trustRoots: NIOSSLAdditionalTrustRoots) {
66+
switch trustRoots {
67+
case .file(let path):
68+
self = .file(path)
69+
case .certificates(let certs):
70+
self = .certificates(certs)
71+
}
72+
}
73+
}
74+
75+
/// Places NIOSSL can obtain additional trust roots from.
76+
public enum NIOSSLAdditionalTrustRoots {
77+
/// See `NIOSSLTrustRoots.file`
78+
case file(String)
79+
80+
/// See `NIOSSLTrustRoots.certificates`
81+
case certificates([NIOSSLCertificate])
4882
}
4983

5084
/// Formats NIOSSL supports for serializing keys and certificates.
@@ -179,8 +213,14 @@ public struct TLSConfiguration {
179213

180214
/// The trust roots to use to validate certificates. This only needs to be provided if you intend to validate
181215
/// certificates.
216+
///
217+
/// - NOTE: If certificate validation is enabled and `trustRoots` is `nil` then the system default root of
218+
/// trust is used (as if `trustRoots` had been explicitly set to `.default`).
182219
public var trustRoots: NIOSSLTrustRoots?
183220

221+
/// Additional trust roots to use to validate certificates, used in addition to `trustRoots`.
222+
public var additionalTrustRoots: [NIOSSLAdditionalTrustRoots]
223+
184224
/// The certificates to offer during negotiation. If not present, no certificates will be offered.
185225
public var certificateChain: [NIOSSLCertificateSource]
186226

@@ -224,14 +264,16 @@ public struct TLSConfiguration {
224264
applicationProtocols: [String],
225265
shutdownTimeout: TimeAmount,
226266
keyLogCallback: NIOSSLKeyLogCallback?,
227-
renegotiationSupport: NIORenegotiationSupport) {
267+
renegotiationSupport: NIORenegotiationSupport,
268+
additionalTrustRoots: [NIOSSLAdditionalTrustRoots]) {
228269
self.cipherSuites = cipherSuites
229270
self.verifySignatureAlgorithms = verifySignatureAlgorithms
230271
self.signingSignatureAlgorithms = signingSignatureAlgorithms
231272
self.minimumTLSVersion = minimumTLSVersion
232273
self.maximumTLSVersion = maximumTLSVersion
233274
self.certificateVerification = certificateVerification
234275
self.trustRoots = trustRoots
276+
self.additionalTrustRoots = additionalTrustRoots
235277
self.certificateChain = certificateChain
236278
self.privateKey = privateKey
237279
self.encodedApplicationProtocols = []
@@ -267,7 +309,8 @@ public struct TLSConfiguration {
267309
applicationProtocols: applicationProtocols,
268310
shutdownTimeout: shutdownTimeout,
269311
keyLogCallback: keyLogCallback,
270-
renegotiationSupport: .none) // Servers never support renegotiation: there's no point.
312+
renegotiationSupport: .none, // Servers never support renegotiation: there's no point.
313+
additionalTrustRoots: [])
271314
}
272315

273316
/// Create a TLS configuration for use with server-side contexts.
@@ -298,7 +341,41 @@ public struct TLSConfiguration {
298341
applicationProtocols: applicationProtocols,
299342
shutdownTimeout: shutdownTimeout,
300343
keyLogCallback: keyLogCallback,
301-
renegotiationSupport: .none) // Servers never support renegotiation: there's no point.
344+
renegotiationSupport: .none, // Servers never support renegotiation: there's no point.
345+
additionalTrustRoots: [])
346+
}
347+
348+
/// Create a TLS configuration for use with server-side contexts.
349+
///
350+
/// This provides sensible defaults while requiring that you provide any data that is necessary
351+
/// for server-side function. For client use, try `forClient` instead.
352+
public static func forServer(certificateChain: [NIOSSLCertificateSource],
353+
privateKey: NIOSSLPrivateKeySource,
354+
cipherSuites: String = defaultCipherSuites,
355+
verifySignatureAlgorithms: [SignatureAlgorithm]? = nil,
356+
signingSignatureAlgorithms: [SignatureAlgorithm]? = nil,
357+
minimumTLSVersion: TLSVersion = .tlsv1,
358+
maximumTLSVersion: TLSVersion? = nil,
359+
certificateVerification: CertificateVerification = .none,
360+
trustRoots: NIOSSLTrustRoots = .default,
361+
applicationProtocols: [String] = [],
362+
shutdownTimeout: TimeAmount = .seconds(5),
363+
keyLogCallback: NIOSSLKeyLogCallback? = nil,
364+
additionalTrustRoots: [NIOSSLAdditionalTrustRoots]) -> TLSConfiguration {
365+
return TLSConfiguration(cipherSuites: cipherSuites,
366+
verifySignatureAlgorithms: verifySignatureAlgorithms,
367+
signingSignatureAlgorithms: signingSignatureAlgorithms,
368+
minimumTLSVersion: minimumTLSVersion,
369+
maximumTLSVersion: maximumTLSVersion,
370+
certificateVerification: certificateVerification,
371+
trustRoots: trustRoots,
372+
certificateChain: certificateChain,
373+
privateKey: privateKey,
374+
applicationProtocols: applicationProtocols,
375+
shutdownTimeout: shutdownTimeout,
376+
keyLogCallback: keyLogCallback,
377+
renegotiationSupport: .none, // Servers never support renegotiation: there's no point.
378+
additionalTrustRoots: additionalTrustRoots)
302379
}
303380

304381
/// Creates a TLS configuration for use with client-side contexts.
@@ -327,7 +404,8 @@ public struct TLSConfiguration {
327404
applicationProtocols: applicationProtocols,
328405
shutdownTimeout: shutdownTimeout,
329406
keyLogCallback: keyLogCallback,
330-
renegotiationSupport: .none) // Default value is here for backward-compatibility.
407+
renegotiationSupport: .none, // Default value is here for backward-compatibility.
408+
additionalTrustRoots: [])
331409
}
332410

333411

@@ -358,7 +436,8 @@ public struct TLSConfiguration {
358436
applicationProtocols: applicationProtocols,
359437
shutdownTimeout: shutdownTimeout,
360438
keyLogCallback: keyLogCallback,
361-
renegotiationSupport: renegotiationSupport)
439+
renegotiationSupport: renegotiationSupport,
440+
additionalTrustRoots: [])
362441
}
363442

364443
/// Creates a TLS configuration for use with client-side contexts.
@@ -390,6 +469,41 @@ public struct TLSConfiguration {
390469
applicationProtocols: applicationProtocols,
391470
shutdownTimeout: shutdownTimeout,
392471
keyLogCallback: keyLogCallback,
393-
renegotiationSupport: renegotiationSupport)
472+
renegotiationSupport: renegotiationSupport,
473+
additionalTrustRoots: [])
474+
}
475+
476+
/// Creates a TLS configuration for use with client-side contexts.
477+
///
478+
/// This provides sensible defaults, and can be used without customisation. For server-side
479+
/// contexts, you should use `forServer` instead.
480+
public static func forClient(cipherSuites: String = defaultCipherSuites,
481+
verifySignatureAlgorithms: [SignatureAlgorithm]? = nil,
482+
signingSignatureAlgorithms: [SignatureAlgorithm]? = nil,
483+
minimumTLSVersion: TLSVersion = .tlsv1,
484+
maximumTLSVersion: TLSVersion? = nil,
485+
certificateVerification: CertificateVerification = .fullVerification,
486+
trustRoots: NIOSSLTrustRoots = .default,
487+
certificateChain: [NIOSSLCertificateSource] = [],
488+
privateKey: NIOSSLPrivateKeySource? = nil,
489+
applicationProtocols: [String] = [],
490+
shutdownTimeout: TimeAmount = .seconds(5),
491+
keyLogCallback: NIOSSLKeyLogCallback? = nil,
492+
renegotiationSupport: NIORenegotiationSupport = .none,
493+
additionalTrustRoots: [NIOSSLAdditionalTrustRoots]) -> TLSConfiguration {
494+
return TLSConfiguration(cipherSuites: cipherSuites,
495+
verifySignatureAlgorithms: verifySignatureAlgorithms,
496+
signingSignatureAlgorithms: signingSignatureAlgorithms,
497+
minimumTLSVersion: minimumTLSVersion,
498+
maximumTLSVersion: maximumTLSVersion,
499+
certificateVerification: certificateVerification,
500+
trustRoots: trustRoots,
501+
certificateChain: certificateChain,
502+
privateKey: privateKey,
503+
applicationProtocols: applicationProtocols,
504+
shutdownTimeout: shutdownTimeout,
505+
keyLogCallback: keyLogCallback,
506+
renegotiationSupport: renegotiationSupport,
507+
additionalTrustRoots: additionalTrustRoots)
394508
}
395509
}

Tests/NIOSSLTests/NIOSSLTestHelpers.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,7 @@ func generateSelfSignedCert() -> (NIOSSLCertificate, NIOSSLPrivateKey) {
474474
addExtension(x509: x, nid: NID_basic_constraints, value: "critical,CA:FALSE")
475475
addExtension(x509: x, nid: NID_subject_key_identifier, value: "hash")
476476
addExtension(x509: x, nid: NID_subject_alt_name, value: "DNS:localhost")
477+
addExtension(x509: x, nid: NID_ext_key_usage, value: "critical,serverAuth,clientAuth")
477478

478479
CNIOBoringSSL_X509_sign(x, pkey, CNIOBoringSSL_EVP_sha256())
479480

0 commit comments

Comments
 (0)