Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Sources/NIOSSL/NIOSSLHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public class NIOSSLHandler: ChannelInboundHandler, ChannelOutboundHandler, Remov
let tlsConfiguration = connection.parentContext.configuration
precondition(
additionalPeerCertificateVerificationCallback == nil || tlsConfiguration.certificateVerification != .none,
"TLSConfiguration.certificateVerification must be either set to .noHostnameVerification or .fullVerification if additionalPeerCertificateVerificationCallback is specified"
"TLSConfiguration.certificateVerification must be either set to .optionalVerification, .noHostnameVerification, or .fullVerification if additionalPeerCertificateVerificationCallback is specified"
)
self.connection = connection
// 96 brings the total size of the buffer to just shy of one page
Expand Down
76 changes: 49 additions & 27 deletions Sources/NIOSSL/SSLContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,51 @@ extension NIOSSLContext {

// Configuring certificate verification
extension NIOSSLContext {
fileprivate enum VerificationMode {
case peerCertificateRequired
case peerCertificatesOptional
}

fileprivate static func setupVerification(
_ context: OpaquePointer,
_ sendCANames: Bool,
_ trustRoots: NIOSSLTrustRoots?,
_ additionalTrustRoots: [NIOSSLAdditionalTrustRoots],
_ verificationMode: VerificationMode
) throws {
switch verificationMode {
case .peerCertificateRequired:
CNIOBoringSSL_SSL_CTX_set_verify(context, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, nil)
case .peerCertificatesOptional:
CNIOBoringSSL_SSL_CTX_set_verify(context, SSL_VERIFY_PEER, nil)
}

// Also, set TRUSTED_FIRST to work around dumb clients that don't know what they're doing and send
// untrusted root certs. X509_VERIFY_PARAM will or-in the flags, so we don't need to load them first.
// This is get0 so we can just ignore the pointer, we don't have an owned ref.
let trustParams = CNIOBoringSSL_SSL_CTX_get0_param(context)!
CNIOBoringSSL_X509_VERIFY_PARAM_set_flags(trustParams, CUnsignedLong(X509_V_FLAG_TRUSTED_FIRST))

func configureTrustRoots(trustRoots: NIOSSLTrustRoots) throws {
switch trustRoots {
case .default:
try NIOSSLContext.platformDefaultConfiguration(context: context)
case .file(let path):
try NIOSSLContext.loadVerifyLocations(path, context: context, sendCANames: sendCANames)
case .certificates(let certs):
for cert in certs {
try NIOSSLContext.addRootCertificate(cert, context: context)
// Add the CA name from the trust root
if sendCANames {
try NIOSSLContext.addCACertificateNameToList(context: context, certificate: cert)
}
}
}
}
try configureTrustRoots(trustRoots: trustRoots ?? .default)
for root in additionalTrustRoots { try configureTrustRoots(trustRoots: .init(from: root)) }
}

private static func configureCertificateValidation(
context: OpaquePointer,
verification: CertificateVerification,
Expand All @@ -610,34 +655,11 @@ extension NIOSSLContext {
// If validation is turned on, set the trust roots and turn on cert validation.
switch verification {
case .fullVerification, .noHostnameVerification:
CNIOBoringSSL_SSL_CTX_set_verify(context, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, nil)

// Also, set TRUSTED_FIRST to work around dumb clients that don't know what they're doing and send
// untrusted root certs. X509_VERIFY_PARAM will or-in the flags, so we don't need to load them first.
// This is get0 so we can just ignore the pointer, we don't have an owned ref.
let trustParams = CNIOBoringSSL_SSL_CTX_get0_param(context)!
CNIOBoringSSL_X509_VERIFY_PARAM_set_flags(trustParams, CUnsignedLong(X509_V_FLAG_TRUSTED_FIRST))

func configureTrustRoots(trustRoots: NIOSSLTrustRoots) throws {
switch trustRoots {
case .default:
try NIOSSLContext.platformDefaultConfiguration(context: context)
case .file(let path):
try NIOSSLContext.loadVerifyLocations(path, context: context, sendCANames: sendCANames)
case .certificates(let certs):
for cert in certs {
try NIOSSLContext.addRootCertificate(cert, context: context)
// Add the CA name from the trust root
if sendCANames {
try NIOSSLContext.addCACertificateNameToList(context: context, certificate: cert)
}
}
}
try setupVerification(context, sendCANames, trustRoots, additionalTrustRoots, .peerCertificateRequired)
case .none(let opts):
if opts.validatePresentedCertificates {
try setupVerification(context, sendCANames, trustRoots, additionalTrustRoots, .peerCertificatesOptional)
}
try configureTrustRoots(trustRoots: trustRoots ?? .default)
for root in additionalTrustRoots { try configureTrustRoots(trustRoots: .init(from: root)) }
default:
break
}
}

Expand Down
35 changes: 33 additions & 2 deletions Sources/NIOSSL/TLSConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,19 @@ public enum NIOSSLSerializationFormats: Sendable {

/// Certificate verification modes.
public enum CertificateVerification: Sendable {
/// All certificate verification disabled.
case none
public struct NoneOptions: Sendable, Equatable, Hashable {
/// While the peer does not have to give you certificates,
/// they can optionally be verified if the peer offers them.
public var validatePresentedCertificates: Bool

fileprivate init() {
// Backwards-compatible
self.validatePresentedCertificates = false
}
}

/// Usable through ``none`` and ``optionalVerification``.
case none(NoneOptions)

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

extension CertificateVerification {
/// Certificates will be validated if they are presented by the peer, i.e., if the peer presents
/// certificates they must pass validation. However, if the peer does not present certificates,
/// the connection will be accepted.
public static var optionalVerification: CertificateVerification {
var options = NoneOptions()
options.validatePresentedCertificates = true
return .none(options)
}

/// All certificate verification disabled.
public static var none: CertificateVerification {
.none(NoneOptions())
}
}

extension CertificateVerification: Hashable {
// empty
}

/// Support for TLS renegotiation.
///
/// In general, renegotiation should not be enabled except in circumstances where it is absolutely necessary.
Expand Down
164 changes: 164 additions & 0 deletions Tests/NIOSSLTests/NIOSSLIntegrationTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2600,6 +2600,170 @@ class NIOSSLIntegrationTest: XCTestCase {
XCTAssertNoThrow(try handshakeCompletePromise.futureResult.wait())
}

func testMacOSConnectionFailsIfServerVerificationOptionalAndPeerPresentsUntrustedCert() throws {
// This test checks that when setting verification to `.optionalVerification`, a peer cannot successfully
// connect when they present an untrusted certificate. On macOS, this exercises the SecTrust validation backend,
// as `serverConfig.trustRoots` is set to `.default` (see the behavioral matrix in
// `NIOSSL/Docs/trust-roots-behavior.md`).
var serverConfig = TLSConfiguration.makeServerConfiguration(
certificateChain: [.certificate(NIOSSLIntegrationTest.cert)],
privateKey: .privateKey(NIOSSLIntegrationTest.key)
)
serverConfig.certificateVerification = .optionalVerification
serverConfig.trustRoots = .default

var clientConfig = TLSConfiguration.makeClientConfiguration()
clientConfig.certificateVerification = .noHostnameVerification
clientConfig.trustRoots = .default
clientConfig.additionalTrustRoots = [.certificates([NIOSSLIntegrationTest.cert])]
// The client presents a random cert but the server won't trust it
let clientCertAndPrivateKey = generateSelfSignedCert()
clientConfig.certificateChain = [.certificate(clientCertAndPrivateKey.0)]
clientConfig.privateKey = .privateKey(clientCertAndPrivateKey.1)

let serverContext = try assertNoThrowWithValue(NIOSSLContext(configuration: serverConfig))
let clientContext = try assertNoThrowWithValue(NIOSSLContext(configuration: clientConfig))

let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
defer {
XCTAssertNoThrow(try group.syncShutdownGracefully())
}

let handshakeCompletePromise = group.next().makePromise(of: Void.self)
let serverChannel: Channel = try serverTLSChannel(
context: serverContext,
handlers: [WaitForHandshakeHandler(handshakeResultPromise: handshakeCompletePromise)],
group: group
)
defer {
XCTAssertNoThrow(try serverChannel.close().wait())
}

let clientChannel = try clientTLSChannel(
context: clientContext,
preHandlers: [],
postHandlers: [],
group: group,
connectingTo: serverChannel.localAddress!,
serverHostname: "localhost"
)
defer {
XCTAssertNoThrow(try clientChannel.close().wait())
}

// The handshake should fail: certificate verification is optional and the client hasn't presented any certs.
XCTAssertThrowsError(try handshakeCompletePromise.futureResult.wait())
}

func testMacOSConnectionSuccessfulIfServerVerificationOptionalAndPeerPresentsTrustedCert() throws {
// This test checks that when setting verification to `.optionalVerification`, a peer can successfully
// connect when they present a trusted certificate. On macOS, this exercises the SecTrust validation backend,
// as `serverConfig.trustRoots` is set to `.default` and the client cert is registered under
// `additionalTrustRoots` (see the behavioral matrix in `NIOSSL/Docs.docc/trust-roots-behavior.md`).
var clientConfig = TLSConfiguration.makeClientConfiguration()
clientConfig.certificateVerification = .noHostnameVerification
clientConfig.trustRoots = .default
clientConfig.additionalTrustRoots = [.certificates([NIOSSLIntegrationTest.cert])]
// The client presents a generated cert
let clientCertAndPrivateKey = generateSelfSignedCert()
clientConfig.certificateChain = [.certificate(clientCertAndPrivateKey.0)]
clientConfig.privateKey = .privateKey(clientCertAndPrivateKey.1)

var serverConfig = TLSConfiguration.makeServerConfiguration(
certificateChain: [.certificate(NIOSSLIntegrationTest.cert)],
privateKey: .privateKey(NIOSSLIntegrationTest.key)
)
serverConfig.certificateVerification = .optionalVerification
serverConfig.trustRoots = .default
// The server trusts the client's generated cert
serverConfig.additionalTrustRoots = [.certificates([clientCertAndPrivateKey.0])]

let serverContext = try assertNoThrowWithValue(NIOSSLContext(configuration: serverConfig))
let clientContext = try assertNoThrowWithValue(NIOSSLContext(configuration: clientConfig))

let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
defer {
XCTAssertNoThrow(try group.syncShutdownGracefully())
}

let handshakeCompletePromise = group.next().makePromise(of: Void.self)
let serverChannel: Channel = try serverTLSChannel(
context: serverContext,
handlers: [WaitForHandshakeHandler(handshakeResultPromise: handshakeCompletePromise)],
group: group
)
defer {
XCTAssertNoThrow(try serverChannel.close().wait())
}

let clientChannel = try clientTLSChannel(
context: clientContext,
preHandlers: [],
postHandlers: [],
group: group,
connectingTo: serverChannel.localAddress!,
serverHostname: "localhost"
)
defer {
XCTAssertNoThrow(try clientChannel.close().wait())
}

// The handshake should succeed: verification is optional, and the client presents a cert the server trusts.
XCTAssertNoThrow(try handshakeCompletePromise.futureResult.wait())
}

func testMacOSConnectionSuccessfulIfServerVerificationOptionalAndNoPeerCert() throws {
// This test checks that when setting verification to `.optionalVerification`, a peer can successfully connect
// when they don't present any certificate. On macOS, this exercises the SecTrust validation backend, as
// `serverConfig.trustRoots` is set to `.default` (see the behavioral matrix in
// `NIOSSL/Docs.docc/trust-roots-behavior.md`).
var serverConfig = TLSConfiguration.makeServerConfiguration(
certificateChain: [.certificate(NIOSSLIntegrationTest.cert)],
privateKey: .privateKey(NIOSSLIntegrationTest.key)
)
serverConfig.certificateVerification = .optionalVerification
serverConfig.trustRoots = .default

// The client doesn't present any certs
var clientConfig = TLSConfiguration.makeClientConfiguration()
clientConfig.certificateVerification = .noHostnameVerification
clientConfig.trustRoots = .default
clientConfig.additionalTrustRoots = [.certificates([NIOSSLIntegrationTest.cert])]

let serverContext = try assertNoThrowWithValue(NIOSSLContext(configuration: serverConfig))
let clientContext = try assertNoThrowWithValue(NIOSSLContext(configuration: clientConfig))

let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
defer {
XCTAssertNoThrow(try group.syncShutdownGracefully())
}

let handshakeCompletePromise = group.next().makePromise(of: Void.self)
let serverChannel: Channel = try serverTLSChannel(
context: serverContext,
handlers: [WaitForHandshakeHandler(handshakeResultPromise: handshakeCompletePromise)],
group: group
)
defer {
XCTAssertNoThrow(try serverChannel.close().wait())
}

let clientChannel = try clientTLSChannel(
context: clientContext,
preHandlers: [],
postHandlers: [],
group: group,
connectingTo: serverChannel.localAddress!,
serverHostname: "localhost"
)
defer {
XCTAssertNoThrow(try clientChannel.close().wait())
}

// The handshake should succeed: certificate verification is optional and the client hasn't presented any certs.
XCTAssertNoThrow(try handshakeCompletePromise.futureResult.wait())
}

func testServerHasNewCallbackCalledToo() throws {
var config = TLSConfiguration.makeServerConfiguration(
certificateChain: [.certificate(NIOSSLIntegrationTest.cert)],
Expand Down
63 changes: 63 additions & 0 deletions Tests/NIOSSLTests/TLSConfigurationTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,69 @@ class TLSConfigurationTest: XCTestCase {
)
}

func testMutualValidationWithCertVerificationOptionalSuccess_NoPeerCert() throws {
// The client doesn't present a cert chain
var clientConfig = TLSConfiguration.makeClientConfiguration()
clientConfig.certificateVerification = .noHostnameVerification
clientConfig.trustRoots = .default
clientConfig.additionalTrustRoots = [.certificates([TLSConfigurationTest.cert1])]

var serverConfig = TLSConfiguration.makeServerConfiguration(
certificateChain: [.certificate(TLSConfigurationTest.cert1)],
privateKey: .privateKey(TLSConfigurationTest.key1)
)
// The server sets `certificateVerification` to `optionalVerification`; handshake should succeed when the client
// hasn't presented any certs
serverConfig.certificateVerification = .optionalVerification
serverConfig.trustRoots = .default

try assertHandshakeSucceeded(withClientConfig: clientConfig, andServerConfig: serverConfig)
}

func testMutualValidationWithCertVerificationOptionalError_PeerCertNotTrusted() throws {
var clientConfig = TLSConfiguration.makeClientConfiguration()
clientConfig.certificateChain = [.certificate(TLSConfigurationTest.cert2)]
clientConfig.privateKey = .privateKey(TLSConfigurationTest.key2)
clientConfig.certificateVerification = .noHostnameVerification
clientConfig.trustRoots = .default
clientConfig.additionalTrustRoots = [.certificates([TLSConfigurationTest.cert1])]

var serverConfig = TLSConfiguration.makeServerConfiguration(
certificateChain: [.certificate(TLSConfigurationTest.cert1)],
privateKey: .privateKey(TLSConfigurationTest.key1)
)
serverConfig.certificateVerification = .optionalVerification
serverConfig.trustRoots = .default
// The server doesn't trust any additional roots; the cert presented by the client will not be trusted
serverConfig.additionalTrustRoots = []

try assertPostHandshakeError(
withClientConfig: clientConfig,
andServerConfig: serverConfig,
errorTextContainsAnyOf: ["SSLV3_ALERT_CERTIFICATE_UNKNOWN", "TLSV1_ALERT_UNKNOWN_CA"]
)
}

func testMutualValidationWithCertVerificationOptionalSuccess_PeerCertTrusted() throws {
var clientConfig = TLSConfiguration.makeClientConfiguration()
clientConfig.certificateChain = [.certificate(TLSConfigurationTest.cert2)]
clientConfig.privateKey = .privateKey(TLSConfigurationTest.key2)
clientConfig.certificateVerification = .noHostnameVerification
clientConfig.trustRoots = .default
clientConfig.additionalTrustRoots = [.certificates([TLSConfigurationTest.cert1])]

var serverConfig = TLSConfiguration.makeServerConfiguration(
certificateChain: [.certificate(TLSConfigurationTest.cert1)],
privateKey: .privateKey(TLSConfigurationTest.key1)
)
serverConfig.certificateVerification = .optionalVerification
serverConfig.trustRoots = .default
// The server trusts the cert presented by the client; we expect a successful handshake
serverConfig.additionalTrustRoots = [.certificates([TLSConfigurationTest.cert2])]

try assertHandshakeSucceeded(withClientConfig: clientConfig, andServerConfig: serverConfig)
}

func testMutualValidationRequiresClientCertificatePreTLS13() throws {
var clientConfig = TLSConfiguration.makeClientConfiguration()
clientConfig.maximumTLSVersion = .tlsv12
Expand Down
Loading