-
Notifications
You must be signed in to change notification settings - Fork 11
/
ssl.clj
159 lines (142 loc) · 6.37 KB
/
ssl.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
(ns net.ssl
"Clojure glue code to interact with the horrible JVM SSL code"
(:require [net.ty.channel :as chan]
[net.ty.pipeline :refer [*channel*]]
[clojure.java.io :refer [input-stream]])
(:import java.util.Base64
java.security.KeyStore
java.security.KeyFactory
java.security.PrivateKey
java.security.cert.X509Certificate
java.security.cert.CertificateFactory
java.security.spec.PKCS8EncodedKeySpec
java.io.ByteArrayInputStream
io.netty.channel.ChannelHandler
io.netty.channel.Channel
io.netty.handler.ssl.SslContext
io.netty.handler.ssl.SslContextBuilder
io.netty.handler.ssl.ClientAuth
io.netty.handler.ssl.util.InsecureTrustManagerFactory))
(def ^:dynamic *storage*
"Help net decide how to treat input. The default value
of `:guess` will treat string input as paths under 256
chars - a common value for **PATH_MAX** - and inlined
cert data above that.
A value of `:data` will always assume inlined certs,
and a value of `:file` will always assume paths."
:guess)
(defn cert-string
"Convert input to certificate bytes"
[input]
(cond
(and (map? input) (:path input))
(some-> input :path slurp)
(map? input)
(:data input)
(not (string? input))
(throw (ex-info "cannot convert input to cert string" {}))
(= :file *storage*)
(slurp input)
(= :data *storage*)
input
;; Assume `:guess` for storage
(< (count input) 256)
(slurp input)
:else
input))
(defn ^"[B" cert-bytes
"Get certificate bytes out of an input."
[input]
(if (instance? (Class/forName "[B") input)
input
(.getBytes ^String (cert-string input))))
(defn ^X509Certificate s->cert
"Generate an X509 from a given source."
[^CertificateFactory factory input]
(.generateCertificate ^CertificateFactory factory
(ByteArrayInputStream. (cert-bytes input))))
(defn ^PrivateKey s->pkey
"When reading private keys, we unfortunately have to
read PKCS8 encoded keys, short of pulling-in bouncy castle :-(
Since these keys are usually DER encoded, they're unconvienent to
have laying around in strings. We resort to base64 encoded DER here."
[^KeyFactory factory input]
(let [bytes (.encode (Base64/getEncoder) (cert-bytes input))
kspec (PKCS8EncodedKeySpec. bytes)]
(.generatePrivate factory kspec)))
(defn ^"[Ljava.security.cert.X509Certificate;" ->chain
"Get a certificate chain out of several certificate specs"
[^CertificateFactory fact cert-spec]
(if (sequential? cert-spec)
(into-array X509Certificate (map (partial s->cert fact) cert-spec))
(into-array X509Certificate [(s->cert fact cert-spec)])))
(defn client-context
"Build an SSL client context for netty"
[{:keys [bundle password cert pkey authority storage insecure]}]
(let [storage (or storage :guess)
builder (SslContextBuilder/forClient)]
(.sslProvider builder io.netty.handler.ssl.SslProvider/OPENSSL)
(binding [*storage* storage]
(when (and cert pkey authority)
(let [cert-fact (CertificateFactory/getInstance "X.509")
key-fact (KeyFactory/getInstance "RSA")]
(let [cert (s->cert cert-fact cert)
authority (s->cert cert-fact authority)
pkey (s->pkey key-fact pkey)
chain ^"[Ljava.security.cert.X509Certificate;" (into-array X509Certificate [cert authority])]
(.keyManager builder ^PrivateKey pkey chain)
(.trustManager builder chain))))
(when insecure
(.trustManager builder InsecureTrustManagerFactory/INSTANCE))
(when (and bundle password)
(let [keystore (KeyStore/getInstance "pkcs12")]
(with-open [stream (input-stream bundle)]
(.load keystore stream (char-array password)))
(let [alias (first (enumeration-seq (.aliases keystore)))
k (.getKey keystore alias (char-array password))
chain (.getCertificateChain keystore alias)]
(.keyManager builder ^PrivateKey k ^"[Ljava.security.cert.X509Certificate;" (into-array X509Certificate (seq chain)))
(.trustManager builder ^"[Ljava.security.cert.X509Certificate;" (into-array X509Certificate (seq chain))))))
(.build builder))))
(defn server-context
"Build an SSL client context for netty"
[{:keys [pkey password cert auth-mode ca-cert ciphers
cache-size session-timeout storage]}]
(binding [*storage* (or storage :guess)]
(let [fact (CertificateFactory/getInstance "X.509")
certs ^"[Ljava.security.cert.X509Certificate;" (->chain fact cert)
key-fact (KeyFactory/getInstance "RSA")
pk (s->pkey key-fact pkey)
builder (if (string? password)
^SslContextBuilder (SslContextBuilder/forServer pk ^String password certs)
^SslContextBuilder (SslContextBuilder/forServer pk certs))]
(when ciphers
(.ciphers builder ciphers))
(when ca-cert
(.trustManager builder
^"[Ljava.security.cert.X509Certificate;"
(into-array X509Certificate (->chain fact ca-cert))))
(when cache-size
(.sessionCacheSize builder (long cache-size)))
(when session-timeout
(.sessionTimeout builder (long session-timeout)))
(when auth-mode
(.clientAuth builder (case auth-mode
:auth-mode-optional ClientAuth/OPTIONAL
:auth-mode-require ClientAuth/REQUIRE
:auth-mode-none ClientAuth/NONE
(throw (ex-info "invalid client auth mode" {})))))
(.build ^SslContextBuilder builder))))
(defn ^ChannelHandler new-handler
"Create a new SSL handler from an SslContext"
([^SslContext ctx ^Channel channel]
(.newHandler ctx (.alloc channel)))
([^SslContext ctx ^Channel channel ^String host port]
(.newHandler ctx (.alloc channel) host (int port))))
(defn ^clojure.lang.IFn handler-fn
"Build a handler function to be used in netty pipelines out of an SSL context.
Will yield a 1-arity function of a context and a 3-arity function of a
context, a host, and a port which will add a handler to the context."
[^SslContext ctx]
(fn ^ChannelHandler make-handler []
(new-handler ctx *channel*)))