diff --git a/Sources/NIOSSL/SSLContext.swift b/Sources/NIOSSL/SSLContext.swift index 43291607..f55ba530 100644 --- a/Sources/NIOSSL/SSLContext.swift +++ b/Sources/NIOSSL/SSLContext.swift @@ -330,7 +330,20 @@ public final class NIOSSLContext { // Cipher suites. We just pass this straight to BoringSSL. returnCode = CNIOBoringSSL_SSL_CTX_set_cipher_list(context, configuration.cipherSuites) precondition(1 == returnCode) - + + // Curves list. + if let curves = configuration.curves { + returnCode = curves + .map { $0.rawValue } + .withUnsafeBufferPointer { algo in + CNIOBoringSSL_SSL_CTX_set1_group_ids(context, algo.baseAddress, algo.count) + } + if returnCode != 1 { + let errorStack = BoringSSLError.buildErrorStack() + throw BoringSSLError.unknownError(errorStack) + } + } + // Set the PSK Client Configuration callback. if let pskClientConfigurationsCallback = configuration._pskClientIdentityProvider { self.pskClientConfigurationCallback = pskClientConfigurationsCallback diff --git a/Sources/NIOSSL/TLSConfiguration.swift b/Sources/NIOSSL/TLSConfiguration.swift index 3eb314cb..a3e0a696 100644 --- a/Sources/NIOSSL/TLSConfiguration.swift +++ b/Sources/NIOSSL/TLSConfiguration.swift @@ -132,6 +132,29 @@ public struct NIOTLSCipher: RawRepresentable, Hashable, Sendable { } } +/// Available curves to use for TLS. +public struct NIOTLSCurve: RawRepresentable, Hashable, Sendable { + /// Construct a ``NIOTLSCurve`` from the RFC code point for that curve. + public init(rawValue: UInt16) { + self.rawValue = rawValue + } + + /// Construct a ``NIOTLSCurve`` from the RFC code point for that curve. + public init(_ rawValue: RawValue) { + self.rawValue = rawValue + } + + /// The RFC code point for the given curve. + public var rawValue: UInt16 + public typealias RawValue = UInt16 + + public static let secp256r1 = NIOTLSCurve(rawValue: 0x17) + public static let secp384r1 = NIOTLSCurve(rawValue: 0x18) + public static let secp521r1 = NIOTLSCurve(rawValue: 0x19) + public static let x25519 = NIOTLSCurve(rawValue: 0x1D) + public static let x448 = NIOTLSCurve(rawValue: 0x1E) +} + /// Formats NIOSSL supports for serializing keys and certificates. public enum NIOSSLSerializationFormats: Sendable { case pem @@ -253,6 +276,9 @@ public struct TLSConfiguration { /// TLS 1.3 cipher suites cannot be configured. public var cipherSuites: String = defaultCipherSuites + /// TLS curves supported by this handler. Passing `nil` means that a built-in set of curves will be used. + public var curves: [NIOTLSCurve]? + /// Public property used to set the internal ``cipherSuites`` from ``NIOTLSCipher``. public var cipherSuiteValues: [NIOTLSCipher] { get { @@ -466,6 +492,7 @@ extension TLSConfiguration { return self.minimumTLSVersion == comparing.minimumTLSVersion && self.maximumTLSVersion == comparing.maximumTLSVersion && self.cipherSuites == comparing.cipherSuites && + self.curves == comparing.curves && self.verifySignatureAlgorithms == comparing.verifySignatureAlgorithms && self.signingSignatureAlgorithms == comparing.signingSignatureAlgorithms && self.certificateVerification == comparing.certificateVerification && @@ -493,6 +520,7 @@ extension TLSConfiguration { hasher.combine(minimumTLSVersion) hasher.combine(maximumTLSVersion) hasher.combine(cipherSuites) + hasher.combine(curves) hasher.combine(verifySignatureAlgorithms) hasher.combine(signingSignatureAlgorithms) hasher.combine(certificateVerification) diff --git a/Tests/NIOSSLTests/TLSConfigurationTest.swift b/Tests/NIOSSLTests/TLSConfigurationTest.swift index 97d01190..bd103b3f 100644 --- a/Tests/NIOSSLTests/TLSConfigurationTest.swift +++ b/Tests/NIOSSLTests/TLSConfigurationTest.swift @@ -829,6 +829,66 @@ class TLSConfigurationTest: XCTestCase { XCTAssertFalse(config.bestEffortEquals(differentConfig)) } + func testCompatibleCurves() throws { + var clientConfig = TLSConfiguration.makeClientConfiguration() + clientConfig.curves = [.x25519] + clientConfig.cipherSuiteValues = [.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384] + clientConfig.maximumTLSVersion = .tlsv12 + clientConfig.certificateVerification = .noHostnameVerification + clientConfig.trustRoots = .certificates([TLSConfigurationTest.cert1]) + clientConfig.renegotiationSupport = .none + + var serverConfig = TLSConfiguration.makeServerConfiguration( + certificateChain: [.certificate(TLSConfigurationTest.cert1)], + privateKey: .privateKey(TLSConfigurationTest.key1) + ) + serverConfig.cipherSuiteValues = [.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384] + serverConfig.curves = [.x25519] + serverConfig.maximumTLSVersion = .tlsv12 + serverConfig.certificateVerification = .none + try assertHandshakeSucceeded(withClientConfig: clientConfig, andServerConfig: serverConfig) + } + + func testMultipleCompatibleCurves() throws { + var clientConfig = TLSConfiguration.makeClientConfiguration() + clientConfig.curves = [.x25519] + clientConfig.cipherSuiteValues = [.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384] + clientConfig.maximumTLSVersion = .tlsv12 + clientConfig.certificateVerification = .noHostnameVerification + clientConfig.trustRoots = .certificates([TLSConfigurationTest.cert1]) + clientConfig.renegotiationSupport = .none + + var serverConfig = TLSConfiguration.makeServerConfiguration( + certificateChain: [.certificate(TLSConfigurationTest.cert1)], + privateKey: .privateKey(TLSConfigurationTest.key1) + ) + serverConfig.curves = [.x25519, .secp256r1] + serverConfig.cipherSuiteValues = [.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384] + serverConfig.maximumTLSVersion = .tlsv12 + serverConfig.certificateVerification = .none + try assertHandshakeSucceeded(withClientConfig: clientConfig, andServerConfig: serverConfig) + } + + func testNonCompatibleCurves() throws { + var clientConfig = TLSConfiguration.makeClientConfiguration() + clientConfig.curves = [.secp521r1] + clientConfig.cipherSuiteValues = [.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384] + clientConfig.maximumTLSVersion = .tlsv12 + clientConfig.certificateVerification = .noHostnameVerification + clientConfig.trustRoots = .certificates([TLSConfigurationTest.cert1]) + clientConfig.renegotiationSupport = .none + + var serverConfig = TLSConfiguration.makeServerConfiguration( + certificateChain: [.certificate(TLSConfigurationTest.cert1)], + privateKey: .privateKey(TLSConfigurationTest.key1) + ) + serverConfig.curves = [.x25519] + serverConfig.cipherSuiteValues = [.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384] + serverConfig.maximumTLSVersion = .tlsv12 + serverConfig.certificateVerification = .none + try assertHandshakeError(withClientConfig: clientConfig, andServerConfig: serverConfig, errorTextContains: "ALERT_HANDSHAKE_FAILURE") + } + func testCompatibleCipherSuite() throws { // ECDHE_RSA is used here because the public key in .cert1 is derived from a RSA private key. // These could also be RSA based, but cannot be ECDHE_ECDSA. @@ -1072,7 +1132,7 @@ class TLSConfigurationTest: XCTestCase { func testBestEffortEquatableHashableDifferences() { // If this assertion fails, DON'T JUST CHANGE THE NUMBER HERE! Make sure you've added any appropriate transforms below // so that we're testing these best effort functions. - XCTAssertEqual(MemoryLayout.size, 226, "TLSConfiguration has changed size: you probably need to update this test!") + XCTAssertEqual(MemoryLayout.size, 234, "TLSConfiguration has changed size: you probably need to update this test!") let first = TLSConfiguration.makeClientConfiguration() @@ -1110,6 +1170,7 @@ class TLSConfigurationTest: XCTestCase { { $0.minimumTLSVersion = .tlsv13 }, { $0.maximumTLSVersion = .tlsv12 }, { $0.cipherSuites = "AES" }, + { $0.curves = [.x25519] }, { $0.cipherSuiteValues = [.TLS_RSA_WITH_AES_256_CBC_SHA] }, { $0.verifySignatureAlgorithms = [.ed25519] }, { $0.signingSignatureAlgorithms = [.ed25519] },