Skip to content

Commit 173cc69

Browse files
josephnoiraryan-25
andauthored
Enable opportunistic TLS verification (#565)
### Motivation: In some scenarios a server might want to accept connections from both, authenticated and not authenticated peers and defer decisions after the handshake. This new options allows a host to only do TLS verificaiton if the peer presents certificates and accept connections otherwise. ### Modifications: Add a new associated value to the `none` case of `CertificationVerification` to maintain backwards compatibility. This option enables opportunistic verification. ### Result: A new configuration case for TLS verification. --------- Co-authored-by: aryan-25 <aryan_shah@apple.com>
1 parent f6599fd commit 173cc69

File tree

5 files changed

+310
-30
lines changed

5 files changed

+310
-30
lines changed

Sources/NIOSSL/NIOSSLHandler.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public class NIOSSLHandler: ChannelInboundHandler, ChannelOutboundHandler, Remov
7878
let tlsConfiguration = connection.parentContext.configuration
7979
precondition(
8080
additionalPeerCertificateVerificationCallback == nil || tlsConfiguration.certificateVerification != .none,
81-
"TLSConfiguration.certificateVerification must be either set to .noHostnameVerification or .fullVerification if additionalPeerCertificateVerificationCallback is specified"
81+
"TLSConfiguration.certificateVerification must be either set to .optionalVerification, .noHostnameVerification, or .fullVerification if additionalPeerCertificateVerificationCallback is specified"
8282
)
8383
self.connection = connection
8484
// 96 brings the total size of the buffer to just shy of one page

Sources/NIOSSL/SSLContext.swift

Lines changed: 49 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,51 @@ extension NIOSSLContext {
600600

601601
// Configuring certificate verification
602602
extension NIOSSLContext {
603+
fileprivate enum VerificationMode {
604+
case peerCertificateRequired
605+
case peerCertificatesOptional
606+
}
607+
608+
fileprivate static func setupVerification(
609+
_ context: OpaquePointer,
610+
_ sendCANames: Bool,
611+
_ trustRoots: NIOSSLTrustRoots?,
612+
_ additionalTrustRoots: [NIOSSLAdditionalTrustRoots],
613+
_ verificationMode: VerificationMode
614+
) throws {
615+
switch verificationMode {
616+
case .peerCertificateRequired:
617+
CNIOBoringSSL_SSL_CTX_set_verify(context, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, nil)
618+
case .peerCertificatesOptional:
619+
CNIOBoringSSL_SSL_CTX_set_verify(context, SSL_VERIFY_PEER, nil)
620+
}
621+
622+
// Also, set TRUSTED_FIRST to work around dumb clients that don't know what they're doing and send
623+
// untrusted root certs. X509_VERIFY_PARAM will or-in the flags, so we don't need to load them first.
624+
// This is get0 so we can just ignore the pointer, we don't have an owned ref.
625+
let trustParams = CNIOBoringSSL_SSL_CTX_get0_param(context)!
626+
CNIOBoringSSL_X509_VERIFY_PARAM_set_flags(trustParams, CUnsignedLong(X509_V_FLAG_TRUSTED_FIRST))
627+
628+
func configureTrustRoots(trustRoots: NIOSSLTrustRoots) throws {
629+
switch trustRoots {
630+
case .default:
631+
try NIOSSLContext.platformDefaultConfiguration(context: context)
632+
case .file(let path):
633+
try NIOSSLContext.loadVerifyLocations(path, context: context, sendCANames: sendCANames)
634+
case .certificates(let certs):
635+
for cert in certs {
636+
try NIOSSLContext.addRootCertificate(cert, context: context)
637+
// Add the CA name from the trust root
638+
if sendCANames {
639+
try NIOSSLContext.addCACertificateNameToList(context: context, certificate: cert)
640+
}
641+
}
642+
}
643+
}
644+
try configureTrustRoots(trustRoots: trustRoots ?? .default)
645+
for root in additionalTrustRoots { try configureTrustRoots(trustRoots: .init(from: root)) }
646+
}
647+
603648
private static func configureCertificateValidation(
604649
context: OpaquePointer,
605650
verification: CertificateVerification,
@@ -610,34 +655,11 @@ extension NIOSSLContext {
610655
// If validation is turned on, set the trust roots and turn on cert validation.
611656
switch verification {
612657
case .fullVerification, .noHostnameVerification:
613-
CNIOBoringSSL_SSL_CTX_set_verify(context, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, nil)
614-
615-
// Also, set TRUSTED_FIRST to work around dumb clients that don't know what they're doing and send
616-
// untrusted root certs. X509_VERIFY_PARAM will or-in the flags, so we don't need to load them first.
617-
// This is get0 so we can just ignore the pointer, we don't have an owned ref.
618-
let trustParams = CNIOBoringSSL_SSL_CTX_get0_param(context)!
619-
CNIOBoringSSL_X509_VERIFY_PARAM_set_flags(trustParams, CUnsignedLong(X509_V_FLAG_TRUSTED_FIRST))
620-
621-
func configureTrustRoots(trustRoots: NIOSSLTrustRoots) throws {
622-
switch trustRoots {
623-
case .default:
624-
try NIOSSLContext.platformDefaultConfiguration(context: context)
625-
case .file(let path):
626-
try NIOSSLContext.loadVerifyLocations(path, context: context, sendCANames: sendCANames)
627-
case .certificates(let certs):
628-
for cert in certs {
629-
try NIOSSLContext.addRootCertificate(cert, context: context)
630-
// Add the CA name from the trust root
631-
if sendCANames {
632-
try NIOSSLContext.addCACertificateNameToList(context: context, certificate: cert)
633-
}
634-
}
635-
}
658+
try setupVerification(context, sendCANames, trustRoots, additionalTrustRoots, .peerCertificateRequired)
659+
case .none(let opts):
660+
if opts.validatePresentedCertificates {
661+
try setupVerification(context, sendCANames, trustRoots, additionalTrustRoots, .peerCertificatesOptional)
636662
}
637-
try configureTrustRoots(trustRoots: trustRoots ?? .default)
638-
for root in additionalTrustRoots { try configureTrustRoots(trustRoots: .init(from: root)) }
639-
default:
640-
break
641663
}
642664
}
643665

Sources/NIOSSL/TLSConfiguration.swift

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,19 @@ public enum NIOSSLSerializationFormats: Sendable {
169169

170170
/// Certificate verification modes.
171171
public enum CertificateVerification: Sendable {
172-
/// All certificate verification disabled.
173-
case none
172+
public struct NoneOptions: Sendable, Equatable, Hashable {
173+
/// While the peer does not have to give you certificates,
174+
/// they can optionally be verified if the peer offers them.
175+
public var validatePresentedCertificates: Bool
176+
177+
fileprivate init() {
178+
// Backwards-compatible
179+
self.validatePresentedCertificates = false
180+
}
181+
}
182+
183+
/// Usable through ``none`` and ``optionalVerification``.
184+
case none(NoneOptions)
174185

175186
/// Certificates will be validated against the trust store, but will not
176187
/// be checked to see if they are valid for the given hostname.
@@ -181,6 +192,26 @@ public enum CertificateVerification: Sendable {
181192
case fullVerification
182193
}
183194

195+
extension CertificateVerification {
196+
/// Certificates will be validated if they are presented by the peer, i.e., if the peer presents
197+
/// certificates they must pass validation. However, if the peer does not present certificates,
198+
/// the connection will be accepted.
199+
public static var optionalVerification: CertificateVerification {
200+
var options = NoneOptions()
201+
options.validatePresentedCertificates = true
202+
return .none(options)
203+
}
204+
205+
/// All certificate verification disabled.
206+
public static var none: CertificateVerification {
207+
.none(NoneOptions())
208+
}
209+
}
210+
211+
extension CertificateVerification: Hashable {
212+
// empty
213+
}
214+
184215
/// Support for TLS renegotiation.
185216
///
186217
/// In general, renegotiation should not be enabled except in circumstances where it is absolutely necessary.

Tests/NIOSSLTests/NIOSSLIntegrationTest.swift

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2600,6 +2600,170 @@ class NIOSSLIntegrationTest: XCTestCase {
26002600
XCTAssertNoThrow(try handshakeCompletePromise.futureResult.wait())
26012601
}
26022602

2603+
func testMacOSConnectionFailsIfServerVerificationOptionalAndPeerPresentsUntrustedCert() throws {
2604+
// This test checks that when setting verification to `.optionalVerification`, a peer cannot successfully
2605+
// connect when they present an untrusted certificate. On macOS, this exercises the SecTrust validation backend,
2606+
// as `serverConfig.trustRoots` is set to `.default` (see the behavioral matrix in
2607+
// `NIOSSL/Docs/trust-roots-behavior.md`).
2608+
var serverConfig = TLSConfiguration.makeServerConfiguration(
2609+
certificateChain: [.certificate(NIOSSLIntegrationTest.cert)],
2610+
privateKey: .privateKey(NIOSSLIntegrationTest.key)
2611+
)
2612+
serverConfig.certificateVerification = .optionalVerification
2613+
serverConfig.trustRoots = .default
2614+
2615+
var clientConfig = TLSConfiguration.makeClientConfiguration()
2616+
clientConfig.certificateVerification = .noHostnameVerification
2617+
clientConfig.trustRoots = .default
2618+
clientConfig.additionalTrustRoots = [.certificates([NIOSSLIntegrationTest.cert])]
2619+
// The client presents a random cert but the server won't trust it
2620+
let clientCertAndPrivateKey = generateSelfSignedCert()
2621+
clientConfig.certificateChain = [.certificate(clientCertAndPrivateKey.0)]
2622+
clientConfig.privateKey = .privateKey(clientCertAndPrivateKey.1)
2623+
2624+
let serverContext = try assertNoThrowWithValue(NIOSSLContext(configuration: serverConfig))
2625+
let clientContext = try assertNoThrowWithValue(NIOSSLContext(configuration: clientConfig))
2626+
2627+
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
2628+
defer {
2629+
XCTAssertNoThrow(try group.syncShutdownGracefully())
2630+
}
2631+
2632+
let handshakeCompletePromise = group.next().makePromise(of: Void.self)
2633+
let serverChannel: Channel = try serverTLSChannel(
2634+
context: serverContext,
2635+
handlers: [WaitForHandshakeHandler(handshakeResultPromise: handshakeCompletePromise)],
2636+
group: group
2637+
)
2638+
defer {
2639+
XCTAssertNoThrow(try serverChannel.close().wait())
2640+
}
2641+
2642+
let clientChannel = try clientTLSChannel(
2643+
context: clientContext,
2644+
preHandlers: [],
2645+
postHandlers: [],
2646+
group: group,
2647+
connectingTo: serverChannel.localAddress!,
2648+
serverHostname: "localhost"
2649+
)
2650+
defer {
2651+
XCTAssertNoThrow(try clientChannel.close().wait())
2652+
}
2653+
2654+
// The handshake should fail: certificate verification is optional and the client hasn't presented any certs.
2655+
XCTAssertThrowsError(try handshakeCompletePromise.futureResult.wait())
2656+
}
2657+
2658+
func testMacOSConnectionSuccessfulIfServerVerificationOptionalAndPeerPresentsTrustedCert() throws {
2659+
// This test checks that when setting verification to `.optionalVerification`, a peer can successfully
2660+
// connect when they present a trusted certificate. On macOS, this exercises the SecTrust validation backend,
2661+
// as `serverConfig.trustRoots` is set to `.default` and the client cert is registered under
2662+
// `additionalTrustRoots` (see the behavioral matrix in `NIOSSL/Docs.docc/trust-roots-behavior.md`).
2663+
var clientConfig = TLSConfiguration.makeClientConfiguration()
2664+
clientConfig.certificateVerification = .noHostnameVerification
2665+
clientConfig.trustRoots = .default
2666+
clientConfig.additionalTrustRoots = [.certificates([NIOSSLIntegrationTest.cert])]
2667+
// The client presents a generated cert
2668+
let clientCertAndPrivateKey = generateSelfSignedCert()
2669+
clientConfig.certificateChain = [.certificate(clientCertAndPrivateKey.0)]
2670+
clientConfig.privateKey = .privateKey(clientCertAndPrivateKey.1)
2671+
2672+
var serverConfig = TLSConfiguration.makeServerConfiguration(
2673+
certificateChain: [.certificate(NIOSSLIntegrationTest.cert)],
2674+
privateKey: .privateKey(NIOSSLIntegrationTest.key)
2675+
)
2676+
serverConfig.certificateVerification = .optionalVerification
2677+
serverConfig.trustRoots = .default
2678+
// The server trusts the client's generated cert
2679+
serverConfig.additionalTrustRoots = [.certificates([clientCertAndPrivateKey.0])]
2680+
2681+
let serverContext = try assertNoThrowWithValue(NIOSSLContext(configuration: serverConfig))
2682+
let clientContext = try assertNoThrowWithValue(NIOSSLContext(configuration: clientConfig))
2683+
2684+
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
2685+
defer {
2686+
XCTAssertNoThrow(try group.syncShutdownGracefully())
2687+
}
2688+
2689+
let handshakeCompletePromise = group.next().makePromise(of: Void.self)
2690+
let serverChannel: Channel = try serverTLSChannel(
2691+
context: serverContext,
2692+
handlers: [WaitForHandshakeHandler(handshakeResultPromise: handshakeCompletePromise)],
2693+
group: group
2694+
)
2695+
defer {
2696+
XCTAssertNoThrow(try serverChannel.close().wait())
2697+
}
2698+
2699+
let clientChannel = try clientTLSChannel(
2700+
context: clientContext,
2701+
preHandlers: [],
2702+
postHandlers: [],
2703+
group: group,
2704+
connectingTo: serverChannel.localAddress!,
2705+
serverHostname: "localhost"
2706+
)
2707+
defer {
2708+
XCTAssertNoThrow(try clientChannel.close().wait())
2709+
}
2710+
2711+
// The handshake should succeed: verification is optional, and the client presents a cert the server trusts.
2712+
XCTAssertNoThrow(try handshakeCompletePromise.futureResult.wait())
2713+
}
2714+
2715+
func testMacOSConnectionSuccessfulIfServerVerificationOptionalAndNoPeerCert() throws {
2716+
// This test checks that when setting verification to `.optionalVerification`, a peer can successfully connect
2717+
// when they don't present any certificate. On macOS, this exercises the SecTrust validation backend, as
2718+
// `serverConfig.trustRoots` is set to `.default` (see the behavioral matrix in
2719+
// `NIOSSL/Docs.docc/trust-roots-behavior.md`).
2720+
var serverConfig = TLSConfiguration.makeServerConfiguration(
2721+
certificateChain: [.certificate(NIOSSLIntegrationTest.cert)],
2722+
privateKey: .privateKey(NIOSSLIntegrationTest.key)
2723+
)
2724+
serverConfig.certificateVerification = .optionalVerification
2725+
serverConfig.trustRoots = .default
2726+
2727+
// The client doesn't present any certs
2728+
var clientConfig = TLSConfiguration.makeClientConfiguration()
2729+
clientConfig.certificateVerification = .noHostnameVerification
2730+
clientConfig.trustRoots = .default
2731+
clientConfig.additionalTrustRoots = [.certificates([NIOSSLIntegrationTest.cert])]
2732+
2733+
let serverContext = try assertNoThrowWithValue(NIOSSLContext(configuration: serverConfig))
2734+
let clientContext = try assertNoThrowWithValue(NIOSSLContext(configuration: clientConfig))
2735+
2736+
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
2737+
defer {
2738+
XCTAssertNoThrow(try group.syncShutdownGracefully())
2739+
}
2740+
2741+
let handshakeCompletePromise = group.next().makePromise(of: Void.self)
2742+
let serverChannel: Channel = try serverTLSChannel(
2743+
context: serverContext,
2744+
handlers: [WaitForHandshakeHandler(handshakeResultPromise: handshakeCompletePromise)],
2745+
group: group
2746+
)
2747+
defer {
2748+
XCTAssertNoThrow(try serverChannel.close().wait())
2749+
}
2750+
2751+
let clientChannel = try clientTLSChannel(
2752+
context: clientContext,
2753+
preHandlers: [],
2754+
postHandlers: [],
2755+
group: group,
2756+
connectingTo: serverChannel.localAddress!,
2757+
serverHostname: "localhost"
2758+
)
2759+
defer {
2760+
XCTAssertNoThrow(try clientChannel.close().wait())
2761+
}
2762+
2763+
// The handshake should succeed: certificate verification is optional and the client hasn't presented any certs.
2764+
XCTAssertNoThrow(try handshakeCompletePromise.futureResult.wait())
2765+
}
2766+
26032767
func testServerHasNewCallbackCalledToo() throws {
26042768
var config = TLSConfiguration.makeServerConfiguration(
26052769
certificateChain: [.certificate(NIOSSLIntegrationTest.cert)],

Tests/NIOSSLTests/TLSConfigurationTest.swift

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,69 @@ class TLSConfigurationTest: XCTestCase {
552552
)
553553
}
554554

555+
func testMutualValidationWithCertVerificationOptionalSuccess_NoPeerCert() throws {
556+
// The client doesn't present a cert chain
557+
var clientConfig = TLSConfiguration.makeClientConfiguration()
558+
clientConfig.certificateVerification = .noHostnameVerification
559+
clientConfig.trustRoots = .default
560+
clientConfig.additionalTrustRoots = [.certificates([TLSConfigurationTest.cert1])]
561+
562+
var serverConfig = TLSConfiguration.makeServerConfiguration(
563+
certificateChain: [.certificate(TLSConfigurationTest.cert1)],
564+
privateKey: .privateKey(TLSConfigurationTest.key1)
565+
)
566+
// The server sets `certificateVerification` to `optionalVerification`; handshake should succeed when the client
567+
// hasn't presented any certs
568+
serverConfig.certificateVerification = .optionalVerification
569+
serverConfig.trustRoots = .default
570+
571+
try assertHandshakeSucceeded(withClientConfig: clientConfig, andServerConfig: serverConfig)
572+
}
573+
574+
func testMutualValidationWithCertVerificationOptionalError_PeerCertNotTrusted() throws {
575+
var clientConfig = TLSConfiguration.makeClientConfiguration()
576+
clientConfig.certificateChain = [.certificate(TLSConfigurationTest.cert2)]
577+
clientConfig.privateKey = .privateKey(TLSConfigurationTest.key2)
578+
clientConfig.certificateVerification = .noHostnameVerification
579+
clientConfig.trustRoots = .default
580+
clientConfig.additionalTrustRoots = [.certificates([TLSConfigurationTest.cert1])]
581+
582+
var serverConfig = TLSConfiguration.makeServerConfiguration(
583+
certificateChain: [.certificate(TLSConfigurationTest.cert1)],
584+
privateKey: .privateKey(TLSConfigurationTest.key1)
585+
)
586+
serverConfig.certificateVerification = .optionalVerification
587+
serverConfig.trustRoots = .default
588+
// The server doesn't trust any additional roots; the cert presented by the client will not be trusted
589+
serverConfig.additionalTrustRoots = []
590+
591+
try assertPostHandshakeError(
592+
withClientConfig: clientConfig,
593+
andServerConfig: serverConfig,
594+
errorTextContainsAnyOf: ["SSLV3_ALERT_CERTIFICATE_UNKNOWN", "TLSV1_ALERT_UNKNOWN_CA"]
595+
)
596+
}
597+
598+
func testMutualValidationWithCertVerificationOptionalSuccess_PeerCertTrusted() throws {
599+
var clientConfig = TLSConfiguration.makeClientConfiguration()
600+
clientConfig.certificateChain = [.certificate(TLSConfigurationTest.cert2)]
601+
clientConfig.privateKey = .privateKey(TLSConfigurationTest.key2)
602+
clientConfig.certificateVerification = .noHostnameVerification
603+
clientConfig.trustRoots = .default
604+
clientConfig.additionalTrustRoots = [.certificates([TLSConfigurationTest.cert1])]
605+
606+
var serverConfig = TLSConfiguration.makeServerConfiguration(
607+
certificateChain: [.certificate(TLSConfigurationTest.cert1)],
608+
privateKey: .privateKey(TLSConfigurationTest.key1)
609+
)
610+
serverConfig.certificateVerification = .optionalVerification
611+
serverConfig.trustRoots = .default
612+
// The server trusts the cert presented by the client; we expect a successful handshake
613+
serverConfig.additionalTrustRoots = [.certificates([TLSConfigurationTest.cert2])]
614+
615+
try assertHandshakeSucceeded(withClientConfig: clientConfig, andServerConfig: serverConfig)
616+
}
617+
555618
func testMutualValidationRequiresClientCertificatePreTLS13() throws {
556619
var clientConfig = TLSConfiguration.makeClientConfiguration()
557620
clientConfig.maximumTLSVersion = .tlsv12

0 commit comments

Comments
 (0)