Closed Chenyibo closed 2 years ago
I did a bit of research on this today and exposing this functionality does not seem like something that would fit nicely into our APIs for NIOSSLContext
. For example, using NIOSSLHTTP1Client
as a test bed, and badssl.com
as test API, I was able to establish a connection with TLSv1, and then use the NIOSSLContext
object available in NIOSSLHTTP1Client
to extract the TLS version, but I had to expose a public function (getTLSVersion) in NIOSSLContext
to get the final TLS version data:
NIOSSLHTTP1Client
private final class HTTPResponseHandler: ChannelInboundHandler {
...
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
let httpResponsePart = unwrapInboundIn(data)
switch httpResponsePart {
case .head(let httpResponseHeader):
print("\(httpResponseHeader.version) \(httpResponseHeader.status.code) \(httpResponseHeader.status.reasonPhrase)")
for (name, value) in httpResponseHeader.headers {
print("\(name): \(value)")
}
case .body(var byteBuffer):
if let data = byteBuffer.readData(length: byteBuffer.readableBytes) {
FileHandle.standardOutput.write(data)
}
case .end(_):
closeFuture = context.channel.close()
// Extract the version from the cached SSL OpaquePointer for this connection that is saved in NIOSSLContext.
print("Context TLS version \(sslContext.getTLSVersion())")
promise.succeed(())
}
}
...
}
if let u = arg1 {
url = URL(string: u)!
} else {
url = URL(string: "https://tls-v1-0.badssl.com:1010")!
}
var tlsConfiguration = TLSConfiguration.makeClientConfiguration()
tlsConfiguration.maximumTLSVersion = .tlsv11
tlsConfiguration.minimumTLSVersion = .tlsv1
let sslContext = try! NIOSSLContext(configuration: tlsConfiguration)
And from NIOSSLContext.swift
, I had to grab the sslPointer
from the incoming connection and cache this as property of the class to extract TLS version from this property when the connection is finished. This is not great, and probably something that we want to avoid doing. Note that the reason I had to cache the sslPointer
is because this is the object that BoringSSL uses to extract the on-wire version of TLS used for the connection.
public final class NIOSSLContext {
// Note that this pointer holds data based on 1 connection ONLY.
private var sslPointer: OpaquePointer?
internal func createConnection() -> SSLConnection? {
guard let ssl = CNIOBoringSSL_SSL_new(self.sslContext) else {
return nil
}
// This is not great
sslPointer = ssl
...
}
public func getTLSVersion() -> String {
guard self.sslPointer != nil else {
return "(N/A)"
}
let uint16Version = CNIOBoringSSL_SSL_version(sslPointer)
switch uint16Version {
case TLS1_3_VERSION:
return "TLSv1.3"
case TLS1_2_VERSION:
return "TLSv1.2"
case TLS1_1_VERSION:
return "TLSv1.1"
case TLS1_VERSION:
return "TLSv1"
case DTLS1_VERSION:
return "DTLSv1"
case DTLS1_2_VERSION:
return "DTLSv1.2"
default:
return "unknown";
}
}
...
}
Produces the following:
$ swift run NIOSSLHTTP1Client
[3/3] Build complete!
HTTP/1.1 200 OK
Server: nginx/1.10.3 (Ubuntu)
Date: Mon, 10 Jan 2022 17:55:47 GMT
Content-Type: text/html
Content-Length: 496
Last-Modified: Sat, 04 Dec 2021 00:09:04 GMT
Connection: close
ETag: "61aab1a0-1f0"
Cache-Control: no-store
Accept-Ranges: bytes
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="/icons/favicon-red.ico"/>
<link rel="apple-touch-icon" href="/icons/icon-red.png"/>
<title>tls-v1-0.badssl.com</title>
<link rel="stylesheet" href="/style.css">
<style>body { background: red; }</style>
</head>
<body>
<div id="content">
<h1 style="font-size: 12vw;">
tls-v1-0.<br>badssl.com
</h1>
</div>
</body>
</html>
Context TLS version TLSv1
However, @Lukasa I'd like to get your thoughts on this matter too.
Thanks for the investigation @agnosticdev!
I think the best move here is not to expose this API on the NIOSSLContext
, because there is no requirement that a single NIOSSLContext
have a 1:1 relationship with an SSLConnection
. In fact, it probably shouldn't, as the NIOSSLContext
hides a TLS session cache that can speed up subsequent TLS transactions with the same state.
The best API probably extends NIOSSLHandler
to allow querying of this at runtime. We can then provide a helper API on Channel
that abstracts away the need to find the handler and then call this method. As the NIOSSLHandler
does have a 1:1 relationship with the TLS connection, we can safely store this value.
I think the best move here is not to expose this API on the NIOSSLContext, because there is no requirement that a single NIOSSLContext have a 1:1 relationship with an SSLConnection. In fact, it probably shouldn't, as the NIOSSLContext hides a TLS session cache that can speed up subsequent TLS transactions with the same state.
Agreed, and it would require exposing a public API on NIOSSLContext
which is something that does not sit well with me.
Regarding:
The best API probably extends NIOSSLHandler to allow querying of this at runtime. We can then provide a helper API on Channel that abstracts away the need to find the handler and then call this method. As the NIOSSLHandler does have a 1:1 relationship with the TLS connection, we can safely store this value.
Okay great. I'll take a look at this path and post something if I run into any issues. Appreciate the feedback here @Lukasa as always.
@Lukasa question here on the placement of these APIs to extract the TLSVersion
. Right now I have them sitting in NIOSSLContext
as extensions for development but wanted to get your thoughts on if there was a better place for them out in the swift-nio-ssl
project?
extension NIOSSLHandler {
/// Variable that can be queried during the connection lifecycle to grab the `TLSVesion` used on the connection.
public var tlsVersionForHandler: TLSVersion {
return self.connection.getTLSVersionForConnection()
}
}
extension Channel {
/// API to add the `NIOSSLHandler` to a channel to extract the `TLSVersion` from the result type during
/// lifecycle events such as `whenSuccess` or `whenComplete`.
public func nioSSL_tlsVersionOnChannel() -> EventLoopFuture<NIOSSLHandler> {
return self.pipeline.handler(type: NIOSSLHandler.self)
}
}
extension ChannelPipeline.SynchronousOperations {
/// API to query the `TLSVersion` directly from the `Channel`.
public func nioSSL_tlsVersionForChannel() -> TLSVersion {
let handler = try! self.handler(type: NIOSSLHandler.self)
return handler.tlsVersionForHandler
}
}
Thank you!
I think putting them in the file with NIOSSLHandler
is the better place.
Great, that works perfect. Thanks.
@Chenyibo #338 has now been merged. Once this hits a release you can take advantage of this feature with 2 APIs to extract the final TLSVersion
.
Using NIOSSLHTTP1Client
as an example, you could create an EventLoopFuture
to extract the TLSVersion
:
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
let httpResponsePart = unwrapInboundIn(data)
// First Option, creating an EventLoopFuture to extract the TLSVersion
let tlsVersionForChannel = context.channel.nioSSL_tlsVersion()
switch httpResponsePart {
...
case .end(_):
// Extract the finished TLSVersion before the channel is about to be closed.
// This should be the final TLSVersion used on the connection.
tlsVersionForChannel.whenComplete { result in
switch result {
case .success(let tlsVersion):
print("TLSVersion: \(tlsVersion!)")
case .failure(let error):
print("error: \(error.localizedDescription)")
}
}
closeFuture = context.channel.close()
promise.succeed(())
}
}
Also, using the second API option, you could extract the TLSVersion
directly from the Channel
. However, make sure that you do not directly add this to channelRead
as it may create a deadlock. I would create a helper function here to extract the TLSVersion
if you need it in this context:
func getTLSVersion(context: ChannelHandlerContext) throws {
// Second Option, extract the TLSVersion directly from the channel.
guard let syncTLSVersion = try context.channel.pipeline.syncOperations.nioSSL_tlsVersion() else {
fatalError()
}
print("TLSVersion: \(syncTLSVersion)")
}
@Chenyibo #338 has now been merged. Once this hits a release you can take advantage of this feature with 2 APIs to extract the final
TLSVersion
. UsingNIOSSLHTTP1Client
as an example, you could create anEventLoopFuture
to extract theTLSVersion
:func channelRead(context: ChannelHandlerContext, data: NIOAny) { let httpResponsePart = unwrapInboundIn(data) // First Option, creating an EventLoopFuture to extract the TLSVersion let tlsVersionForChannel = context.channel.nioSSL_tlsVersion() switch httpResponsePart { ... case .end(_): // Extract the finished TLSVersion before the channel is about to be closed. // This should be the final TLSVersion used on the connection. tlsVersionForChannel.whenComplete { result in switch result { case .success(let tlsVersion): print("TLSVersion: \(tlsVersion!)") case .failure(let error): print("error: \(error.localizedDescription)") } } closeFuture = context.channel.close() promise.succeed(()) } }
Also, using the second API option, you could extract the
TLSVersion
directly from theChannel
. However, make sure that you do not directly add this tochannelRead
as it may create a deadlock. I would create a helper function here to extract theTLSVersion
if you need it in this context:func getTLSVersion(context: ChannelHandlerContext) throws { // Second Option, extract the TLSVersion directly from the channel. guard let syncTLSVersion = try context.channel.pipeline.syncOperations.nioSSL_tlsVersion() else { fatalError() } print("TLSVersion: \(syncTLSVersion)") }
@agnosticdev Great,Thank you very very much!
When using swift-nio-ssl to carry out TLS handshake with the server, I need to know the final handshake TLS version. But currently swift-nio-ssl does not support showing the result, I hope you can expose this function, thanks!