Skip to content

Commit c2606ae

Browse files
kachayevDerGuteMoritz
authored andcommitted
Improved API for SSL context management (server and client)
1 parent 22ae585 commit c2606ae

File tree

5 files changed

+223
-64
lines changed

5 files changed

+223
-64
lines changed

src/aleph/http.clj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
| `port` | the port the server will bind to. If `0`, the server will bind to a random port.
3636
| `socket-address` | a `java.net.SocketAddress` specifying both the port and interface to bind to.
3737
| `bootstrap-transform` | a function that takes an `io.netty.bootstrap.ServerBootstrap` object, which represents the server, and modifies it.
38-
| `ssl-context` | an `io.netty.handler.ssl.SslContext` object if an SSL connection is desired |
38+
| `ssl-context` | an `io.netty.handler.ssl.SslContext` object or a map of SSL context options (see `aleph.netty/ssl-server-context` for more details) if an SSL connection is desired |
3939
| `manual-ssl?` | set to `true` to indicate that SSL is active, but the caller is managing it (this implies `:ssl-context` is nil). For example, this can be used if you want to use configure SNI (perhaps in `:pipeline-transform`) to select the SSL context based on the client's indicated host name. |
4040
| `pipeline-transform` | a function that takes an `io.netty.channel.ChannelPipeline` object, which represents a connection, and modifies it.
4141
| `executor` | a `java.util.concurrent.Executor` which is used to handle individual requests. To avoid this indirection you may specify `:none`, but in this case extreme care must be taken to avoid blocking operations on the handler's thread.
@@ -107,7 +107,7 @@
107107
the `connection-options` are a map describing behavior across all connections:
108108
109109
|:---|:---
110-
| `ssl-context` | an `io.netty.handler.ssl.SslContext` object, only required if a custom context is required
110+
| `ssl-context` | an `io.netty.handler.ssl.SslContext` object or a map of SSL context options (see `aleph.netty/ssl-client-context` for more details), only required if a custom context is required
111111
| `local-address` | an optional `java.net.SocketAddress` describing which local interface should be used
112112
| `bootstrap-transform` | a function that takes an `io.netty.bootstrap.Bootstrap` object and modifies it.
113113
| `pipeline-transform` | a function that takes an `io.netty.channel.ChannelPipeline` object, which represents a connection, and modifies it.

src/aleph/netty.clj

Lines changed: 192 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,15 @@
3434
NioSocketChannel
3535
NioDatagramChannel]
3636
[io.netty.handler.ssl
37+
ClientAuth
3738
SslContext
3839
SslContextBuilder
39-
SslHandler]
40+
SslHandler
41+
SslProvider]
4042
[io.netty.handler.codec DecoderException]
4143
[io.netty.handler.ssl.util
42-
SelfSignedCertificate InsecureTrustManagerFactory]
44+
SelfSignedCertificate
45+
InsecureTrustManagerFactory]
4346
[io.netty.resolver
4447
AddressResolverGroup
4548
NoopAddressResolverGroup
@@ -727,63 +730,192 @@
727730

728731
;;;
729732

733+
(defn coerce-ssl-provider [provider]
734+
(case provider
735+
:jdk SslProvider/JDK
736+
:openssl SslProvider/OPENSSL
737+
:openssl-refcnt SslProvider/OPENSSL_REFCNT))
738+
739+
(set! *warn-on-reflection* false)
740+
741+
(let [cert-array-class (class (into-array X509Certificate []))]
742+
(defn- check-ssl-args! [private-key certificate-chain]
743+
(when-not (or
744+
(and (instance? File private-key)
745+
(instance? File certificate-chain))
746+
(and (instance? InputStream private-key)
747+
(instance? InputStream certificate-chain))
748+
(and (instance? PrivateKey private-key)
749+
(instance? cert-array-class certificate-chain)))
750+
(throw
751+
(IllegalArgumentException.
752+
"ssl context arguments invalid"))))
753+
754+
(defn ssl-client-context
755+
"Creates a new client SSL context.
756+
Keyword arguments are:
757+
|:---|:----
758+
| `private-key` | a `java.io.File`, `java.io.InputStream`, or `java.security.PrivateKey` containing the client-side private key.
759+
| `certificate-chain` | a `java.io.File`, `java.io.InputStream`, sequence of `java.security.cert.X509Certificate`, or array of `java.security.cert.X509Certificate` containing the client's certificate chain.
760+
| `private-key-password` | a string, the private key's password (optional).
761+
| `trust-store` | a `java.io.File`, `java.io.InputStream`, array of `java.security.cert.X509Certificate`, or a `javax.net.ssl.TrustManagerFactory` to initialize the context's trust manager.
762+
| `ssl-provider` | `SslContext` implementation to use, on of `:jdk`, `:openssl` or `:openssl-refcnt`. Note, that when using OpenSSL based implementations, the library should be installed and linked properly.
763+
| `ciphers` | a sequence of strings, the cipher suites to enable, in the order of preference.
764+
| `protocols` | a sequence of strings, the TLS protocol versions to enable.
765+
| `session-cache-size` | the size of the cache used for storing SSL session objects.
766+
| `session-timeout` | the timeout for the cached SSL session objects, in seconds.
767+
Note that if specified, the types of `private-key` and `certificate-chain` must be \"compatible\": either both input streams, both files, or a private key and an array of certificates."
768+
([] (ssl-client-context {}))
769+
([{:keys [private-key
770+
private-key-password
771+
certificate-chain
772+
trust-store
773+
ssl-provider
774+
ciphers
775+
protocols
776+
session-cache-size
777+
session-timeout]}]
778+
(let [^SslContextBuilder builder (SslContextBuilder/forClient)
779+
certificate-chain' (if-not (sequential? certificate-chain)
780+
certificate-chain
781+
(into-array X509Certificate certificate-chain))]
782+
(when (and private-key certificate-chain')
783+
(check-ssl-args! private-key certificate-chain')
784+
(if (instance? cert-array-class certificate-chain')
785+
(.keyManager builder
786+
private-key
787+
private-key-password
788+
certificate-chain')
789+
(.keyManager builder
790+
certificate-chain'
791+
private-key
792+
private-key-password)))
793+
794+
(cond-> builder
795+
(some? trust-store)
796+
(.trustManager (if-not (sequential? trust-store)
797+
trust-store
798+
(into-array X509Certificate trust-store)))
799+
800+
(some? ssl-provider)
801+
(.provider (coerce-ssl-provider ssl-provider))
802+
803+
(some? ciphers)
804+
(.ciphers ciphers)
805+
806+
(some? protocols)
807+
(.protocols (into-array String protocols))
808+
809+
(some? session-cache-size)
810+
(.sessionCacheSize session-cache-size)
811+
812+
(some? session-timeout)
813+
(.sessionTimeout session-timeout))
814+
815+
(.build builder))))
816+
817+
(defn ssl-server-context
818+
"Creates a new server SSL context.
819+
Keyword arguments are:
820+
|:---|:----
821+
| `private-key` | a `java.io.File`, `java.io.InputStream`, or `java.security.PrivateKey` containing the server-side private key.
822+
| `certificate-chain` | a `java.io.File`, `java.io.InputStream`, or array of `java.security.cert.X509Certificate` containing the server's certificate chain.
823+
| `private-key-password` | a string, the private key's password (optional).
824+
| `trust-store` | a `java.io.File`, `java.io.InputStream`, sequence of `java.security.cert.X509Certificate`, array of `java.security.cert.X509Certificate`, or a `javax.net.ssl.TrustManagerFactory` to initialize the context's trust manager.
825+
| `ssl-provider` | `SslContext` implementation to use, on of `:jdk`, `:openssl` or `:openssl-refcnt`. Note, that when using OpenSSL based implementations, the library should be installed and linked properly.
826+
| `ciphers` | a sequence of strings, the cipher suites to enable, in the order of preference.
827+
| `protocols` | a sequence of strings, the TLS protocol versions to enable.
828+
| `session-cache-size` | the size of the cache used for storing SSL session objects.
829+
| `session-timeout` | the timeout for the cached SSL session objects, in seconds.
830+
| `start-tls` | if the first write request shouldn't be encrypted.
831+
| `client-auth` | the client authentication mode, one of `:none`, `:optional` or `:require`.
832+
Note that if specified, the types of `private-key` and `certificate-chain` must be \"compatible\": either both input streams, both files, or a private key and an array of certificates."
833+
([] (ssl-server-context {}))
834+
([{:keys [private-key
835+
private-key-password
836+
certificate-chain
837+
trust-store
838+
ssl-provider
839+
ciphers
840+
protocols
841+
session-cache-size
842+
session-timeout
843+
start-tls
844+
client-auth]}]
845+
(let [certificate-chain' (if-not (sequential? certificate-chain)
846+
certificate-chain
847+
(into-array X509Certificate certificate-chain))]
848+
(check-ssl-args! private-key certificate-chain')
849+
(let [^SslContextBuilder
850+
b (cond-> (if (instance? cert-array-class certificate-chain')
851+
(SslContextBuilder/forServer private-key
852+
private-key-password
853+
certificate-chain')
854+
(SslContextBuilder/forServer certificate-chain'
855+
private-key
856+
private-key-password))
857+
858+
(some? trust-store)
859+
(.trustManager (if-not (sequential? trust-store)
860+
trust-store
861+
(into-array X509Certificate trust-store)))
862+
863+
(some? ssl-provider)
864+
(.provider (coerce-ssl-provider ssl-provider))
865+
866+
(some? ciphers)
867+
(.ciphers ciphers)
868+
869+
(some? protocols)
870+
(.protocols (into-array String protocols))
871+
872+
(some? session-cache-size)
873+
(.sessionCacheSize session-cache-size)
874+
875+
(some? session-timeout)
876+
(.sessionTimeout session-timeout)
877+
878+
(some? start-tls)
879+
(.startTls (boolean start-tls))
880+
881+
(some? client-auth)
882+
(.clientAuth (case client-auth
883+
:none ClientAuth/NONE
884+
:optional ClientAuth/OPTIONAL
885+
:require ClientAuth/REQUIRE)))]
886+
(.build b))))))
887+
888+
(set! *warn-on-reflection* true)
889+
730890
(defn self-signed-ssl-context
731891
"A self-signed SSL context for servers."
732892
[]
733893
(let [cert (SelfSignedCertificate.)]
734-
(.build (SslContextBuilder/forServer (.certificate cert) (.privateKey cert)))))
894+
(ssl-server-context {:private-key (.privateKey cert)
895+
:certificate-chain (.certificate cert)})))
735896

736897
(defn insecure-ssl-client-context []
737-
(-> (SslContextBuilder/forClient)
738-
(.trustManager InsecureTrustManagerFactory/INSTANCE)
739-
.build))
740-
741-
(defn- check-ssl-args
742-
[private-key certificate-chain]
743-
(when-not
744-
(or (and (instance? File private-key) (instance? File certificate-chain))
745-
(and (instance? InputStream private-key) (instance? InputStream certificate-chain))
746-
(and (instance? PrivateKey private-key) (instance? (class (into-array X509Certificate [])) certificate-chain)))
747-
(throw (IllegalArgumentException. "ssl-client-context arguments invalid"))))
898+
(ssl-client-context {:trust-store InsecureTrustManagerFactory/INSTANCE}))
748899

749-
(set! *warn-on-reflection* false)
900+
(defn- coerce-ssl-context [options->context ssl-context]
901+
(cond
902+
(instance? SslContext ssl-context)
903+
ssl-context
750904

751-
(defn ssl-client-context
752-
"Creates a new client SSL context.
753-
754-
Keyword arguments are:
755-
756-
|:---|:----
757-
| `private-key` | A `java.io.File`, `java.io.InputStream`, or `java.security.PrivateKey` containing the client-side private key.
758-
| `certificate-chain` | A `java.io.File`, `java.io.InputStream`, or array of `java.security.cert.X509Certificate` containing the client's certificate chain.
759-
| `private-key-password` | A string, the private key's password (optional).
760-
| `trust-store` | A `java.io.File`, `java.io.InputStream`, array of `java.security.cert.X509Certificate`, or a `javax.net.ssl.TrustManagerFactory` to initialize the context's trust manager.
761-
762-
Note that if specified, the types of `private-key` and `certificate-chain` must be
763-
\"compatible\": either both input streams, both files, or a private key and an array
764-
of certificates."
765-
([] (ssl-client-context {}))
766-
([{:keys [private-key private-key-password certificate-chain trust-store]}]
767-
(-> (SslContextBuilder/forClient)
768-
(#(if (and private-key certificate-chain)
769-
(do
770-
(check-ssl-args private-key certificate-chain)
771-
(if (instance? (class (into-array X509Certificate [])) certificate-chain)
772-
(.keyManager %
773-
private-key
774-
private-key-password
775-
certificate-chain)
776-
(.keyManager %
777-
certificate-chain
778-
private-key
779-
private-key-password)))
780-
%))
781-
(#(if trust-store
782-
(.trustManager % trust-store)
783-
%))
784-
.build)))
905+
;; in future this option might be interesing
906+
;; for turning application config (e.g. ALPN)
907+
;; depending on the server's capabilities
908+
(instance? SslContextBuilder ssl-context)
909+
(.build ^SslContextBuilder ssl-context)
785910

786-
(set! *warn-on-reflection* true)
911+
(map? ssl-context)
912+
(options->context ssl-context)))
913+
914+
(def ^:private coerce-ssl-server-context
915+
(partial coerce-ssl-context ssl-server-context))
916+
917+
(def ^:private coerce-ssl-client-context
918+
(partial coerce-ssl-context ssl-client-context))
787919

788920
(defn channel-ssl-session [^Channel ch]
789921
(some-> ch
@@ -975,7 +1107,7 @@ initialize an DnsAddressResolverGroup instance.
9751107
epoll?
9761108
nil))
9771109
([pipeline-builder
978-
^SslContext ssl-context
1110+
ssl-context
9791111
bootstrap-transform
9801112
^SocketAddress remote-address
9811113
^SocketAddress local-address
@@ -988,6 +1120,10 @@ initialize an DnsAddressResolverGroup instance.
9881120
EpollSocketChannel
9891121
NioSocketChannel)
9901122

1123+
^SslContext
1124+
ssl-context (when (some? ssl-context)
1125+
(coerce-ssl-client-context ssl-context))
1126+
9911127
pipeline-builder (if ssl-context
9921128
(fn [^ChannelPipeline p]
9931129
(.addLast p "ssl-handler"
@@ -1026,7 +1162,7 @@ initialize an DnsAddressResolverGroup instance.
10261162

10271163
(defn start-server
10281164
[pipeline-builder
1029-
^SslContext ssl-context
1165+
ssl-context
10301166
bootstrap-transform
10311167
on-close
10321168
^SocketAddress socket-address
@@ -1053,6 +1189,10 @@ initialize an DnsAddressResolverGroup instance.
10531189
transport
10541190
(if epoll? :epoll :nio)
10551191

1192+
^SslContext
1193+
ssl-context (when (some? ssl-context)
1194+
(coerce-ssl-server-context ssl-context))
1195+
10561196
pipeline-builder
10571197
(if ssl-context
10581198
(fn [^ChannelPipeline p]

src/aleph/tcp.clj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@
8888
|:---|:-----
8989
| `port` | the port the server will bind to. If `0`, the server will bind to a random port.
9090
| `socket-address` | a `java.net.SocketAddress` specifying both the port and interface to bind to.
91-
| `ssl-context` | an `io.netty.handler.ssl.SslContext` object. If given, the server will only accept SSL connections and call the handler once the SSL session has been successfully established. If a self-signed certificate is all that's required, `(aleph.netty/self-signed-ssl-context)` will suffice.
91+
| `ssl-context` | an `io.netty.handler.ssl.SslContext` object or a map of SSL context options (see `aleph.netty/ssl-server-context` for more details). If given, the server will only accept SSL connections and call the handler once the SSL session has been successfully established. If a self-signed certificate is all that's required, `(aleph.netty/self-signed-ssl-context)` will suffice.
9292
| `bootstrap-transform` | a function that takes an `io.netty.bootstrap.ServerBootstrap` object, which represents the server, and modifies it.
9393
| `pipeline-transform` | a function that takes an `io.netty.channel.ChannelPipeline` object, which represents a connection, and modifies it.
9494
| `raw-stream?` | if true, messages from the stream will be `io.netty.buffer.ByteBuf` objects rather than byte-arrays. This will minimize copying, but means that care must be taken with Netty's buffer reference counting. Only recommended for advanced users."
@@ -164,7 +164,7 @@
164164
| `port` | the port of the server.
165165
| `remote-address` | a `java.net.SocketAddress` specifying the server's address.
166166
| `local-address` | a `java.net.SocketAddress` specifying the local network interface to use.
167-
| `ssl-context` | an explicit `io.netty.handler.ssl.SslHandler` to use. Defers to `ssl?` and `insecure?` configuration if omitted.
167+
| `ssl-context` | an explicit `io.netty.handler.ssl.SslHandler` or a map of SSL context options (see `aleph.netty/ssl-server-context` for more details) to use. Defers to `ssl?` and `insecure?` configuration if omitted.
168168
| `ssl?` | if true, the client attempts to establish a secure connection with the server.
169169
| `insecure?` | if true, the client will ignore the server's certificate.
170170
| `bootstrap-transform` | a function that takes an `io.netty.bootstrap.Bootstrap` object, which represents the client, and modifies it.

test/aleph/ssl.clj

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,19 @@
4444
(def ^X509Certificate client-cert (gen-cert (read-string (slurp "test/client_cert.edn"))))
4545
(def client-key (gen-key 65537 (read-string (slurp "test/client_key.edn"))))
4646

47+
(def server-ssl-context-opts
48+
{:private-key server-key
49+
:certificate-chain [server-cert]
50+
:trust-store [ca-cert]
51+
:client-auth :optional})
52+
4753
(def server-ssl-context
48-
(-> (SslContextBuilder/forServer server-key (into-array X509Certificate [server-cert]))
49-
(.trustManager (into-array X509Certificate [ca-cert]))
50-
(.clientAuth ClientAuth/OPTIONAL)
51-
.build))
54+
(netty/ssl-server-context server-ssl-context-opts))
55+
56+
(def client-ssl-context-opts
57+
{:private-key client-key
58+
:certificate-chain [client-cert]
59+
:trust-store [ca-cert]})
5260

5361
(def client-ssl-context
54-
(netty/ssl-client-context
55-
{:private-key client-key
56-
:certificate-chain (into-array X509Certificate [client-cert])
57-
:trust-store (into-array X509Certificate [ca-cert])}))
62+
(netty/ssl-client-context client-ssl-context-opts))

test/aleph/tcp_ssl_test.clj

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,20 @@
3333
(is (= (.getSubjectDN ^X509Certificate ssl/client-cert)
3434
(.getSubjectDN ^X509Certificate (first (.getPeerCertificates @ssl-session)))))))))
3535

36+
(deftest test-ssl-opts-echo
37+
(let [ssl-session (atom nil)]
38+
(with-server (tcp/start-server (ssl-echo-handler ssl-session)
39+
{:port 10001
40+
:ssl-context ssl/server-ssl-context-opts})
41+
(let [c @(tcp/client {:host "localhost"
42+
:port 10001
43+
:ssl-context ssl/client-ssl-context-opts})]
44+
(s/put! c "foo")
45+
(is (= "foo" (bs/to-string @(s/take! c))))
46+
(is (some? @ssl-session) "SSL session should be defined")
47+
(is (= (.getSubjectDN ^X509Certificate ssl/client-cert)
48+
(.getSubjectDN ^X509Certificate (first (.getPeerCertificates @ssl-session)))))))))
49+
3650
(deftest test-failed-ssl-handshake
3751
(let [ssl-session (atom nil)]
3852
(with-server (tcp/start-server (ssl-echo-handler ssl-session)

0 commit comments

Comments
 (0)