├── doc └── intro.md ├── .gitignore ├── project.clj ├── test └── less │ └── awful │ └── ssl_test.clj ├── README.md └── src └── less └── awful └── ssl.clj /doc/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction to less-awful-ssl 2 | 3 | TODO: write [great documentation](http://jacobian.org/writing/great-documentation/what-to-write/) 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /lib 3 | /classes 4 | /checkouts 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | .lein-deps-sum 10 | .lein-failures 11 | .lein-plugins 12 | .lein-repl-history 13 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject less-awful-ssl "1.0.8-SNAPSHOT" 2 | :description "Get an SSLContext without wanting to rip your hair out." 3 | :url "http://github.com/aphyr/less-awful-ssl" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :profiles {:dev {:dependencies [[org.clojure/clojure "1.12.0"]]}}) 7 | -------------------------------------------------------------------------------- /test/less/awful/ssl_test.clj: -------------------------------------------------------------------------------- 1 | (ns less.awful.ssl-test 2 | (:require [clojure.test :refer :all] 3 | [less.awful.ssl :as ssl]) 4 | (:import (java.nio.charset StandardCharsets))) 5 | 6 | (deftest base64 7 | (let [test-str (apply str (repeat 4 "less-awful-ssl"))] 8 | (testing "decodes with line break" 9 | (is (= test-str 10 | (-> (ssl/base64->binary "bGVzcy1hd2Z1bC1zc2xsZXNzLWF3ZnVsLXNzbGxlc3MtYXdmdWwtc3NsbGVzcy1h\nd2Z1bC1zc2w=") 11 | (String. StandardCharsets/UTF_8))))) 12 | (testing "decodes without line break" 13 | (is (= test-str 14 | (-> (ssl/base64->binary "bGVzcy1hd2Z1bC1zc2xsZXNzLWF3ZnVsLXNzbGxlc3MtYXdmdWwtc3NsbGVzcy1hd2Z1bC1zc2w=") 15 | (String. StandardCharsets/UTF_8))))))) 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Less Awful SSL 2 | 3 | Working with Java's crypto libraries requires deep knowledge of a complex API, 4 | language-specific key+certificate storage, and knowing how to avoid 5 | long-standing bugs in the Java trust algorithms. This library tries to make it 6 | less complicated to build simple SSL-enabled applications: given a CA 7 | certificate and a signed key and cert, it can give you an SSLContext suitable 8 | for creating TCPSockets directly, or handing off to Netty. 9 | 10 | ## Installation 11 | 12 | https://clojars.org/less-awful-ssl 13 | 14 | ## Example 15 | 16 | In this example we'll be using OpenSSL's stock CA configuration and the OpenSSL 17 | perl script to create a CA's directory structure. I'm assuming you want your CA 18 | signing key encrypted, but the client and server keys unencrypted (since 19 | they'll be deployed to processes which run without human interaction). 20 | 21 | ```bash 22 | # Create the CA directory hierarchy and keypair 23 | # http://kremvax.net/howto/ssl-openssl-ca.html 24 | cp /usr/lib/ssl/misc/CA.pl ca 25 | ./ca -newca 26 | 27 | # Generate a server key 28 | openssl genrsa -out server.key 4096 29 | 30 | # Convert key to pcks8 because Java can't read OpenSSL's format 31 | openssl pkcs8 -topk8 -nocrypt -in server.key -out server.pkcs8 32 | 33 | # Generate a cert request 34 | openssl req -new -key server.key -out newreq.pem 35 | 36 | # Sign request with CA 37 | ./ca -sign 38 | 39 | # Rename signed cert and clean up unused files 40 | mv newcert.pem server.crt 41 | rm newreq.pem server.key 42 | 43 | # And generate a client key+cert as well 44 | openssl genrsa -out client.key 4096 45 | openssl pkcs8 -topk8 -nocrypt -in client.key -out client.pkcs8 46 | openssl req -new -key client.key -out newreq.pem 47 | ./ca -sign 48 | mv newcert.pem client.crt 49 | rm newreq.pem client.key 50 | ``` 51 | 52 | Now fire up a repl and test that your client and server keys can work together. 53 | `(test-ssl)` takes a client key and cert, a server key and cert, and a trusted 54 | CA certificate, and verifies that a client can talk to the server over a TLS 55 | socket: 56 | 57 | ```clj 58 | (use 'less.awful.ssl) 59 | (def d (partial str "/path/to/keys/")) 60 | (apply test-ssl (map d ["client.pkcs8" "client.crt" "server.pkcs8" "server.crt" "demoCA/cacert.pem"])) 61 | ``` 62 | 63 | ```clj 64 | :accepting 65 | :connecting 66 | :accepted 67 | :connected 68 | :client-sent 69 | :server-got "hi" 70 | :server-sent "hi" 71 | :client-got "hi" 72 | :client-done 73 | :waiting-for-server 74 | :server-done 75 | ``` 76 | 77 | Note that this *doesn't* work if you substitute some other certificate for the 78 | trust chain, rather than the CA's: 79 | 80 | ```clj 81 | (apply test-ssl (map d ["client.pkcs8" "client.crt" "server.pkcs8" "server.crt" "server.crt"])) 82 | ``` 83 | 84 | ```clj 85 | :accepting 86 | :connecting 87 | :connected:accepted 88 | 89 | javax.net.ssl.SSLHandshakeException: null cert chain 90 | ... 91 | SSLHandshakeException Received fatal alert: bad_certificate 92 | sun.security.ssl.Alerts.getSSLException (Alerts.java:192) 93 | ``` 94 | 95 | In your app, you'll want to distribute a particular PKCS secret key, the 96 | corresponding signed certificate, and the CA certificate (to verify the 97 | peer's identity). Then you can build an SSLContext: 98 | 99 | ```clj 100 | (ssl-context "client.pkcs8" "client.crt" "ca.crt") 101 | ``` 102 | 103 | And given an SSL context, you can use it to construct a server or a client TCP 104 | socket. See `core.clj/test-ssl` for an example: 105 | 106 | ```clj 107 | (with-open [listener (-> (ssl-context "server.pkcs8" "server.crt" "ca.crt") 108 | (server-socket "localhost" 1234)) 109 | conn (.accept sock)] 110 | ...) 111 | ``` 112 | 113 | ```clj 114 | (with-open [sock (-> (ssl-context "client.pkcs8" "server.crt" "ca.crt") 115 | (socket "localhost" 1234)] 116 | ...) 117 | ``` 118 | 119 | ## Example with a PKCS12 client certificate and org.httpkit 120 | 121 | Assume you've got a client key/certificate pair for `example.com` as a PKCS12 file `client.p12`, 122 | secured with _password_. Also, you've got the Certificate Autority that was used to 123 | sign the client certificate as `ca-cert.crt`. 124 | 125 | Then you could do (your project needs http-kit, of course): 126 | 127 | ```clj 128 | (use 'less.awful.ssl) 129 | (require '[org.httpkit.client :as http]) 130 | 131 | (def password (char-array "secret")) 132 | 133 | (def req (http/request {:sslengine (ssl-context->engine (ssl-p12-context "client.p12" password "ca-cert.crt")) 134 | :url "https://example.com/needs-client-cert" :as :stream})) 135 | ``` 136 | 137 | ## Thanks 138 | 139 | I am indebted to Ben Linsay and Palomino Labs 140 | (http://blog.palominolabs.com/2011/10/18/java-2-way-tlsssl-client-certificates-and-pkcs12-vs-jks-keystores/) 141 | for their help in getting this all put together. 142 | 143 | ## License 144 | 145 | Copyright © 2013 Kyle Kingsbury (aphyr@aphyr.com) 146 | 147 | Distributed under the Eclipse Public License, the same as Clojure. 148 | -------------------------------------------------------------------------------- /src/less/awful/ssl.clj: -------------------------------------------------------------------------------- 1 | (ns less.awful.ssl 2 | "Interacting with the Java crypto APIs is one of the worst things you can do 3 | as a developer. I'm so sorry about all of this." 4 | (:use [clojure.java.io :only [input-stream reader file]] 5 | [clojure.string :only [join]]) 6 | (:require clojure.stacktrace) 7 | (:import (java.io FileInputStream 8 | BufferedReader 9 | InputStreamReader 10 | PrintWriter) 11 | (java.security Key 12 | KeyPair 13 | KeyStore 14 | KeyFactory 15 | PublicKey 16 | PrivateKey) 17 | (java.security.cert Certificate 18 | CertificateFactory) 19 | (java.security.spec PKCS8EncodedKeySpec) 20 | (java.net InetSocketAddress) 21 | (javax.net.ssl SSLContext 22 | SSLSocket 23 | SSLServerSocket 24 | SSLServerSocketFactory 25 | SSLSocketFactory 26 | KeyManager 27 | KeyManagerFactory 28 | TrustManager 29 | TrustManagerFactory 30 | X509KeyManager 31 | X509TrustManager))) 32 | 33 | (defmacro base64->binary [string] 34 | (if (try (import 'java.util.Base64) 35 | (catch ClassNotFoundException _)) 36 | `(let [^String s# ~string] 37 | (.decode (java.util.Base64/getMimeDecoder) s#)) 38 | (do 39 | (import 'javax.xml.bind.DatatypeConverter) 40 | `(javax.xml.bind.DatatypeConverter/parseBase64Binary ~string)))) 41 | 42 | (def ^CertificateFactory x509-cert-factory 43 | "The X.509 certificate factory" 44 | (CertificateFactory/getInstance "X.509")) 45 | 46 | (def ^KeyFactory rsa-key-factory 47 | "An RSA key factory" 48 | (KeyFactory/getInstance "RSA")) 49 | 50 | (def key-store-password 51 | "You know, a mandatory password stored in memory so we can... encrypt... data 52 | stored in memory." 53 | (char-array "GheesBetDyPhuvwotNolofamLydMues9")) 54 | 55 | (defn ^Certificate load-certificate 56 | "Loads an X.509 certificate from a file." 57 | [file] 58 | (with-open [stream (input-stream file)] 59 | (.generateCertificate x509-cert-factory stream))) 60 | 61 | (defn ^"[Ljava.security.cert.Certificate;" load-certificate-chain 62 | "Loads an X.509 certificate chain from a file." 63 | [file] 64 | (with-open [stream (input-stream file)] 65 | (let [^"[Ljava.security.cert.Certificate;" ar (make-array Certificate 0)] 66 | (.toArray (.generateCertificates x509-cert-factory stream) ar)))) 67 | 68 | (defn public-key 69 | "Loads a public key from a .crt file." 70 | [file] 71 | (.getPublicKey (load-certificate file))) 72 | 73 | (defn private-key 74 | "Loads a private key from a PKCS8 file." 75 | [file] 76 | (->> file 77 | slurp 78 | ; LOL Java 79 | (re-find #"(?ms)^-----BEGIN ?.*? PRIVATE KEY-----$(.+)^-----END ?.*? PRIVATE KEY-----$") 80 | last 81 | base64->binary 82 | PKCS8EncodedKeySpec. 83 | (.generatePrivate rsa-key-factory))) 84 | 85 | (defn key-pair 86 | "Creates a KeyPair from a public and private key" 87 | [public-key private-key] 88 | (KeyPair. public-key private-key)) 89 | 90 | (defn key-store 91 | "Makes a keystore from a PKCS8 private key file, a public cert file, and the 92 | signing CA certificate." 93 | [key-file cert-file] 94 | (let [key (private-key key-file) 95 | certs (load-certificate-chain cert-file)] 96 | (doto (KeyStore/getInstance (KeyStore/getDefaultType)) 97 | (.load nil nil) 98 | ; alias, private key, password, certificate chain 99 | (.setKeyEntry "cert" key key-store-password certs)))) 100 | 101 | (defn trust-store 102 | "Makes a trust store, suitable for backing a TrustManager, out of a CA cert 103 | file." 104 | [ca-cert-file] 105 | (doto (KeyStore/getInstance "JKS") 106 | (.load nil nil) 107 | (.setCertificateEntry "cacert" (load-certificate ca-cert-file)))) 108 | 109 | (defn trust-manager 110 | "An X.509 trust manager for a KeyStore." 111 | [^KeyStore key-store] 112 | (let [factory (TrustManagerFactory/getInstance "PKIX" "SunJSSE")] 113 | ; I'm concerned that getInstance might return the *same* factory each time, 114 | ; so we'll defensively lock before mutating here: 115 | (locking factory 116 | (->> (doto factory (.init key-store)) 117 | .getTrustManagers 118 | (filter (partial instance? X509TrustManager)) 119 | first)))) 120 | 121 | (defn key-manager 122 | "An X.509 key manager for a KeyStore." 123 | ([key-store password] 124 | (let [factory (KeyManagerFactory/getInstance "SunX509" "SunJSSE")] 125 | (locking factory 126 | (->> (doto factory (.init key-store, password)) 127 | .getKeyManagers 128 | (filter (partial instance? X509KeyManager)) 129 | first)))) 130 | ([key-store] 131 | (key-manager key-store key-store-password))) 132 | 133 | 134 | (defn ssl-context-generator 135 | "Returns a function that yields SSL contexts. Takes a PKCS8 key file, a 136 | certificate file, and optionally, a trusted CA certificate used to verify peers. 137 | The arity-1 body accepts a trusted CA certificate only." 138 | ([key-file cert-file ca-cert-file] 139 | (let [key-manager (key-manager (key-store key-file cert-file)) 140 | trust-manager (trust-manager (trust-store ca-cert-file))] 141 | (fn build-context [] 142 | (doto (SSLContext/getInstance "TLSv1.2") 143 | (.init (into-array KeyManager [key-manager]) 144 | (into-array TrustManager [trust-manager]) 145 | nil))))) 146 | ([key-file cert-file] 147 | (let [key-manager (key-manager (key-store key-file cert-file))] 148 | (fn build-context [] 149 | (doto (SSLContext/getInstance "TLSv1.2") 150 | (.init (into-array KeyManager [key-manager]) 151 | nil 152 | nil))))) 153 | ([ca-cert-file] 154 | (let [trust-manager (trust-manager (trust-store ca-cert-file))] 155 | (fn build-context [] 156 | (doto (SSLContext/getInstance "TLSv1.2") 157 | (.init nil 158 | (into-array TrustManager [trust-manager]) 159 | nil)))))) 160 | 161 | (defn ssl-context 162 | "Given a PKCS8 key file, a certificate file, and optionally, a trusted CA certificate 163 | used to verify peers, returns an SSLContext. The arity-1 body accepts a single trusted 164 | CA certificate which is commonly used to reach databases." 165 | (^SSLContext [key-file cert-file ca-cert-file] 166 | ((ssl-context-generator key-file cert-file ca-cert-file))) 167 | (^SSLContext [key-file cert-file] 168 | ((ssl-context-generator key-file cert-file))) 169 | (^SSLContext [ca-cert-file] 170 | ((ssl-context-generator ca-cert-file)))) 171 | 172 | (defn ssl-p12-context-generator 173 | "Returns a function that yields an SSL contexts. Takes a PKCS12 key/cert file, the 174 | password for the PKCS12 file, and a CA certificate that was used to sign the PKCS12." 175 | [p12 password ca-cert-file] 176 | (let [fin (input-stream p12) 177 | ks (KeyStore/getInstance "PKCS12")] 178 | (fn build-context [] 179 | (.load ks fin password) 180 | (let [km (key-manager ks password) 181 | tm (trust-manager (trust-store ca-cert-file))] 182 | (doto (SSLContext/getInstance "TLSv1.2") 183 | (.init (into-array KeyManager [km]) 184 | (into-array TrustManager [tm]) 185 | nil)))))) 186 | 187 | (defn ssl-p12-context 188 | "Given a PKCS12 key/cert file, the password, and a CA certificate that was used 189 | to sign the PKCS12, return an SSL Context" 190 | [p12 password ca-cert-file] 191 | ((ssl-p12-context-generator p12 password ca-cert-file))) 192 | 193 | (defn ssl-context->engine 194 | [ctx] 195 | (.createSSLEngine ^SSLContext ctx)) 196 | 197 | (def enabled-protocols 198 | "An array of protocols we support." 199 | (into-array String ["TLSv1.2" "TLSv1.1" "TLSv1"])) 200 | 201 | (defn ^SSLServerSocket server-socket 202 | "Given an SSL context, makes a server SSLSocket." 203 | [^SSLContext context ^String host port] 204 | (let [^SSLServerSocket sock (.. context 205 | getServerSocketFactory 206 | createServerSocket)] 207 | (doto sock 208 | (.bind (InetSocketAddress. host ^int port)) 209 | (.setNeedClientAuth true) 210 | (.setEnabledProtocols enabled-protocols)))) 211 | 212 | (defn ^SSLSocket socket 213 | "Given an SSL context, makes a client SSLSocket." 214 | [^SSLContext context ^String host port] 215 | (let [^SSLSocket sock (-> context 216 | .getSocketFactory 217 | (.createSocket host ^int port))] 218 | (.setEnabledProtocols sock enabled-protocols) 219 | sock)) 220 | 221 | (defn test-ssl 222 | "Given keys and certificates for a client and server, and the signing CA for 223 | both, verify that we can use those files to make an SSL connection." 224 | [client-key-file client-cert-file 225 | server-key-file server-cert-file 226 | ca-cert-file] 227 | 228 | (let [port (+ 1024 (int (rand 60000))) 229 | started (promise) 230 | 231 | ; A dumb echo server 232 | server 233 | (future 234 | (try 235 | (with-open [server (server-socket (ssl-context server-key-file 236 | server-cert-file 237 | ca-cert-file) 238 | "localhost" 239 | port)] 240 | (prn :accepting) 241 | (deliver started :ready) 242 | (with-open [sock (.accept server) 243 | in (-> sock 244 | .getInputStream 245 | InputStreamReader. 246 | BufferedReader.) 247 | out (PrintWriter. (.getOutputStream sock))] 248 | (prn :accepted) 249 | (loop [] 250 | (when-let [s (.readLine in)] 251 | (prn :server-got s) 252 | (.println out s) 253 | (.flush out) 254 | (prn :server-sent s) 255 | (recur))) 256 | (prn :server-done))) 257 | (catch Throwable t 258 | (clojure.stacktrace/print-stack-trace t))))] 259 | 260 | @started 261 | 262 | ; Connect to the local server, send some text, and verify it came back 263 | (prn :connecting) 264 | (with-open [sock (socket (ssl-context client-key-file 265 | client-cert-file 266 | ca-cert-file) 267 | "localhost" 268 | port) 269 | in (-> sock 270 | .getInputStream 271 | InputStreamReader. 272 | BufferedReader.) 273 | out (PrintWriter. (.getOutputStream sock))] 274 | (prn :connected) 275 | (.println out "hi") 276 | (.flush out) 277 | (prn :client-sent) 278 | (let [response (.readLine in)] 279 | (prn :client-got response) 280 | (if (= "hi\n" response) 281 | :ok 282 | [:wrong response])) 283 | (prn :client-done)) 284 | 285 | (prn :waiting-for-server) 286 | @server)) 287 | --------------------------------------------------------------------------------