Skip to content

Commit

Permalink
Don't use IP addresses for SNI (#119)
Browse files Browse the repository at this point in the history
  • Loading branch information
nuyawktuah authored Jul 21, 2022
1 parent d8230ea commit 3af54d0
Show file tree
Hide file tree
Showing 4 changed files with 207 additions and 2 deletions.
19 changes: 19 additions & 0 deletions NOTICES.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Vapor open source project
//
// Copyright (c) 2017-2022 Vapor project authors
// Licensed under MIT
//
// See LICENSE for license information
//
// SPDX-License-Identifier: MIT
//
//===----------------------------------------------------------------------===//

This product contains a derivation of `NIOSSLTestHelpers.swift` from SwiftNIO SSL.

* LICENSE (Apache License 2.0):
* https://www.apache.org/licenses/LICENSE-2.0
* HOMEPAGE:
* https://github.com/apple/swift-nio-ssl
7 changes: 6 additions & 1 deletion Sources/WebSocketKit/WebSocketClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,12 @@ public final class WebSocketClient {
let context = try NIOSSLContext(
configuration: self.configuration.tlsConfiguration ?? .makeClientConfiguration()
)
let tlsHandler = try NIOSSLClientHandler(context: context, serverHostname: host)
let tlsHandler: NIOSSLClientHandler
do {
tlsHandler = try NIOSSLClientHandler(context: context, serverHostname: host)
} catch let error as NIOSSLExtraError where error == .cannotUseIPAddressInSNI {
tlsHandler = try NIOSSLClientHandler(context: context, serverHostname: nil)
}
return channel.pipeline.addHandler(tlsHandler).flatMap {
channel.pipeline.addHTTPClientHandlers(leftOverBytesStrategy: .forwardBytes, withClientUpgrade: config)
}.flatMap {
Expand Down
142 changes: 142 additions & 0 deletions Tests/WebSocketKitTests/SSLTestHelpers.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
//===----------------------------------------------------------------------===//
//
// This source file is based on NIOSSLTestHelpers.swift from
// the SwiftNIO open source project
//
// Copyright (c) 2017-2021 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import Foundation
@_implementationOnly import CNIOBoringSSL
@testable import NIOSSL

// This function generates a random number suitable for use in an X509
// serial field. This needs to be a positive number less than 2^159
// (such that it will fit into 20 ASN.1 bytes).
// This also needs to be portable across operating systems, and the easiest
// way to do that is to use either getentropy() or read from urandom. Sadly
// we need to support old Linuxes which may not possess getentropy as a syscall
// (and definitely don't support it in glibc), so we need to read from urandom.
// In the future we should just use getentropy and be happy.
func randomSerialNumber() -> ASN1_INTEGER {
let bytesToRead = 20
let fd = open("/dev/urandom", O_RDONLY)
precondition(fd != -1)
defer {
close(fd)
}

var readBytes = Array.init(repeating: UInt8(0), count: bytesToRead)
let readCount = readBytes.withUnsafeMutableBytes {
return read(fd, $0.baseAddress, bytesToRead)
}
precondition(readCount == bytesToRead)

// Our 20-byte number needs to be converted into an integer. This is
// too big for Swift's numbers, but BoringSSL can handle it fine.
let bn = CNIOBoringSSL_BN_new()
defer {
CNIOBoringSSL_BN_free(bn)
}

_ = readBytes.withUnsafeBufferPointer {
CNIOBoringSSL_BN_bin2bn($0.baseAddress, $0.count, bn)
}

// We want to bitshift this right by 1 bit to ensure it's smaller than
// 2^159.
CNIOBoringSSL_BN_rshift1(bn, bn)

// Now we can turn this into our ASN1_INTEGER.
var asn1int = ASN1_INTEGER()
CNIOBoringSSL_BN_to_ASN1_INTEGER(bn, &asn1int)

return asn1int
}

func generateRSAPrivateKey() -> UnsafeMutablePointer<EVP_PKEY> {
let exponent = CNIOBoringSSL_BN_new()
defer {
CNIOBoringSSL_BN_free(exponent)
}

CNIOBoringSSL_BN_set_u64(exponent, 0x10001)

let rsa = CNIOBoringSSL_RSA_new()!
let generateRC = CNIOBoringSSL_RSA_generate_key_ex(rsa, CInt(2048), exponent, nil)
precondition(generateRC == 1)

let pkey = CNIOBoringSSL_EVP_PKEY_new()!
let assignRC = CNIOBoringSSL_EVP_PKEY_assign(pkey, EVP_PKEY_RSA, rsa)

precondition(assignRC == 1)
return pkey
}

func addExtension(x509: OpaquePointer, nid: CInt, value: String) {
var extensionContext = X509V3_CTX()

CNIOBoringSSL_X509V3_set_ctx(&extensionContext, x509, x509, nil, nil, 0)
let ext = value.withCString { (pointer) in
return CNIOBoringSSL_X509V3_EXT_nconf_nid(nil, &extensionContext, nid, UnsafeMutablePointer(mutating: pointer))
}!
CNIOBoringSSL_X509_add_ext(x509, ext, -1)
CNIOBoringSSL_X509_EXTENSION_free(ext)
}

func generateSelfSignedCert(keygenFunction: () -> UnsafeMutablePointer<EVP_PKEY> = generateRSAPrivateKey) -> (NIOSSLCertificate, NIOSSLPrivateKey) {
let pkey = keygenFunction()
let x = CNIOBoringSSL_X509_new()!
CNIOBoringSSL_X509_set_version(x, 2)

// NB: X509_set_serialNumber uses an internal copy of the ASN1_INTEGER, so this is
// safe, there will be no use-after-free.
var serial = randomSerialNumber()
CNIOBoringSSL_X509_set_serialNumber(x, &serial)

let notBefore = CNIOBoringSSL_ASN1_TIME_new()!
var now = time(nil)
CNIOBoringSSL_ASN1_TIME_set(notBefore, now)
CNIOBoringSSL_X509_set_notBefore(x, notBefore)
CNIOBoringSSL_ASN1_TIME_free(notBefore)

now += 60 * 60 // Give ourselves an hour
let notAfter = CNIOBoringSSL_ASN1_TIME_new()!
CNIOBoringSSL_ASN1_TIME_set(notAfter, now)
CNIOBoringSSL_X509_set_notAfter(x, notAfter)
CNIOBoringSSL_ASN1_TIME_free(notAfter)

CNIOBoringSSL_X509_set_pubkey(x, pkey)

let commonName = "localhost"
let name = CNIOBoringSSL_X509_get_subject_name(x)
commonName.withCString { (pointer: UnsafePointer<Int8>) -> Void in
pointer.withMemoryRebound(to: UInt8.self, capacity: commonName.lengthOfBytes(using: .utf8)) { (pointer: UnsafePointer<UInt8>) -> Void in
CNIOBoringSSL_X509_NAME_add_entry_by_NID(name,
NID_commonName,
MBSTRING_UTF8,
UnsafeMutablePointer(mutating: pointer),
CInt(commonName.lengthOfBytes(using: .utf8)),
-1,
0)
}
}
CNIOBoringSSL_X509_set_issuer_name(x, name)

addExtension(x509: x, nid: NID_basic_constraints, value: "critical,CA:FALSE")
addExtension(x509: x, nid: NID_subject_key_identifier, value: "hash")
addExtension(x509: x, nid: NID_subject_alt_name, value: "DNS:localhost")
addExtension(x509: x, nid: NID_ext_key_usage, value: "critical,serverAuth,clientAuth")

CNIOBoringSSL_X509_sign(x, pkey, CNIOBoringSSL_EVP_sha256())

return (NIOSSLCertificate.fromUnsafePointer(takingOwnership: x), NIOSSLPrivateKey.fromUnsafePointer(takingOwnership: pkey))
}

41 changes: 40 additions & 1 deletion Tests/WebSocketKitTests/WebSocketKitTests.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import XCTest
import NIO
import NIOHTTP1
import NIOSSL
import NIOWebSocket
@testable import WebSocketKit

Expand Down Expand Up @@ -262,6 +263,33 @@ final class WebSocketKitTests: XCTestCase {
print("Waiting for server close...")
try server.close(mode: .all).wait()
}

func testIPWithTLS() throws {
let server = try ServerBootstrap.webSocket(on: self.elg, tls: true) { req, ws in
_ = ws.close()
}.bind(host: "127.0.0.1", port: 0).wait()

var tlsConfiguration = TLSConfiguration.makeClientConfiguration()
tlsConfiguration.certificateVerification = .none

let client = WebSocketClient(
eventLoopGroupProvider: .shared(self.elg),
configuration: .init(
tlsConfiguration: tlsConfiguration
)
)

guard let port = server.localAddress?.port else {
XCTFail("couldn't get port from \(server.localAddress.debugDescription)")
return
}

try client.connect(scheme: "wss", host: "127.0.0.1", port: port) { ws in
ws.close(promise: nil)
}.wait()

try server.close(mode: .all).wait()
}

var elg: EventLoopGroup!
override func setUp() {
Expand All @@ -276,9 +304,20 @@ final class WebSocketKitTests: XCTestCase {
extension ServerBootstrap {
static func webSocket(
on eventLoopGroup: EventLoopGroup,
tls: Bool = false,
onUpgrade: @escaping (HTTPRequestHead, WebSocket) -> ()
) -> ServerBootstrap {
ServerBootstrap(group: eventLoopGroup).childChannelInitializer { channel in
return ServerBootstrap(group: eventLoopGroup).childChannelInitializer { channel in
if tls {
let (cert, key) = generateSelfSignedCert()
let configuration = TLSConfiguration.makeServerConfiguration(
certificateChain: [.certificate(cert)],
privateKey: .privateKey(key)
)
let sslContext = try! NIOSSLContext(configuration: configuration)
let handler = NIOSSLServerHandler(context: sslContext)
_ = channel.pipeline.addHandler(handler)
}
let webSocket = NIOWebSocketServerUpgrader(
shouldUpgrade: { channel, req in
return channel.eventLoop.makeSucceededFuture([:])
Expand Down

0 comments on commit 3af54d0

Please sign in to comment.