pax_global_header00006660000000000000000000000064151737374660014535gustar00rootroot0000000000000052 comment=fb03211d7166b59879aa1e5fb23748b86f8214eb less-awful-ssl-1.0.8/000077500000000000000000000000001517373746600144245ustar00rootroot00000000000000less-awful-ssl-1.0.8/.gitignore000066400000000000000000000002021517373746600164060ustar00rootroot00000000000000/target /lib /classes /checkouts pom.xml pom.xml.asc *.jar *.class .lein-deps-sum .lein-failures .lein-plugins .lein-repl-history less-awful-ssl-1.0.8/README.md000066400000000000000000000104411517373746600157030ustar00rootroot00000000000000## Less Awful SSL Working with Java's crypto libraries requires deep knowledge of a complex API, language-specific key+certificate storage, and knowing how to avoid long-standing bugs in the Java trust algorithms. This library tries to make it less complicated to build simple SSL-enabled applications: given a CA certificate and a signed key and cert, it can give you an SSLContext suitable for creating TCPSockets directly, or handing off to Netty. ## Installation https://clojars.org/less-awful-ssl ## Example In this example we'll be using OpenSSL's stock CA configuration and the OpenSSL perl script to create a CA's directory structure. I'm assuming you want your CA signing key encrypted, but the client and server keys unencrypted (since they'll be deployed to processes which run without human interaction). ```bash # Create the CA directory hierarchy and keypair # http://kremvax.net/howto/ssl-openssl-ca.html cp /usr/lib/ssl/misc/CA.pl ca ./ca -newca # Generate a server key openssl genrsa -out server.key 4096 # Convert key to pcks8 because Java can't read OpenSSL's format openssl pkcs8 -topk8 -nocrypt -in server.key -out server.pkcs8 # Generate a cert request openssl req -new -key server.key -out newreq.pem # Sign request with CA ./ca -sign # Rename signed cert and clean up unused files mv newcert.pem server.crt rm newreq.pem server.key # And generate a client key+cert as well openssl genrsa -out client.key 4096 openssl pkcs8 -topk8 -nocrypt -in client.key -out client.pkcs8 openssl req -new -key client.key -out newreq.pem ./ca -sign mv newcert.pem client.crt rm newreq.pem client.key ``` Now fire up a repl and test that your client and server keys can work together. `(test-ssl)` takes a client key and cert, a server key and cert, and a trusted CA certificate, and verifies that a client can talk to the server over a TLS socket: ```clj (use 'less.awful.ssl) (def d (partial str "/path/to/keys/")) (apply test-ssl (map d ["client.pkcs8" "client.crt" "server.pkcs8" "server.crt" "demoCA/cacert.pem"])) ``` ```clj :accepting :connecting :accepted :connected :client-sent :server-got "hi" :server-sent "hi" :client-got "hi" :client-done :waiting-for-server :server-done ``` Note that this *doesn't* work if you substitute some other certificate for the trust chain, rather than the CA's: ```clj (apply test-ssl (map d ["client.pkcs8" "client.crt" "server.pkcs8" "server.crt" "server.crt"])) ``` ```clj :accepting :connecting :connected:accepted javax.net.ssl.SSLHandshakeException: null cert chain ... SSLHandshakeException Received fatal alert: bad_certificate sun.security.ssl.Alerts.getSSLException (Alerts.java:192) ``` In your app, you'll want to distribute a particular PKCS secret key, the corresponding signed certificate, and the CA certificate (to verify the peer's identity). Then you can build an SSLContext: ```clj (ssl-context "client.pkcs8" "client.crt" "ca.crt") ``` And given an SSL context, you can use it to construct a server or a client TCP socket. See `core.clj/test-ssl` for an example: ```clj (with-open [listener (-> (ssl-context "server.pkcs8" "server.crt" "ca.crt") (server-socket "localhost" 1234)) conn (.accept sock)] ...) ``` ```clj (with-open [sock (-> (ssl-context "client.pkcs8" "server.crt" "ca.crt") (socket "localhost" 1234)] ...) ``` ## Example with a PKCS12 client certificate and org.httpkit Assume you've got a client key/certificate pair for `example.com` as a PKCS12 file `client.p12`, secured with _password_. Also, you've got the Certificate Autority that was used to sign the client certificate as `ca-cert.crt`. Then you could do (your project needs http-kit, of course): ```clj (use 'less.awful.ssl) (require '[org.httpkit.client :as http]) (def password (char-array "secret")) (def req (http/request {:sslengine (ssl-context->engine (ssl-p12-context "client.p12" password "ca-cert.crt")) :url "https://example.com/needs-client-cert" :as :stream})) ``` ## Thanks I am indebted to Ben Linsay and Palomino Labs (http://blog.palominolabs.com/2011/10/18/java-2-way-tlsssl-client-certificates-and-pkcs12-vs-jks-keystores/) for their help in getting this all put together. ## License Copyright © 2013 Kyle Kingsbury (aphyr@aphyr.com) Distributed under the Eclipse Public License, the same as Clojure. less-awful-ssl-1.0.8/doc/000077500000000000000000000000001517373746600151715ustar00rootroot00000000000000less-awful-ssl-1.0.8/doc/intro.md000066400000000000000000000002041517373746600166420ustar00rootroot00000000000000# Introduction to less-awful-ssl TODO: write [great documentation](http://jacobian.org/writing/great-documentation/what-to-write/) less-awful-ssl-1.0.8/project.clj000066400000000000000000000005121517373746600165620ustar00rootroot00000000000000(defproject less-awful-ssl "1.0.8" :description "Get an SSLContext without wanting to rip your hair out." :url "http://github.com/aphyr/less-awful-ssl" :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html"} :profiles {:dev {:dependencies [[org.clojure/clojure "1.12.0"]]}}) less-awful-ssl-1.0.8/src/000077500000000000000000000000001517373746600152135ustar00rootroot00000000000000less-awful-ssl-1.0.8/src/less/000077500000000000000000000000001517373746600161615ustar00rootroot00000000000000less-awful-ssl-1.0.8/src/less/awful/000077500000000000000000000000001517373746600172775ustar00rootroot00000000000000less-awful-ssl-1.0.8/src/less/awful/ssl.clj000066400000000000000000000245131517373746600205770ustar00rootroot00000000000000(ns less.awful.ssl "Interacting with the Java crypto APIs is one of the worst things you can do as a developer. I'm so sorry about all of this." (:use [clojure.java.io :only [input-stream reader file]] [clojure.string :only [join]]) (:require clojure.stacktrace) (:import (java.io FileInputStream BufferedReader InputStreamReader PrintWriter) (java.security Key KeyPair KeyStore KeyFactory PublicKey PrivateKey) (java.security.cert Certificate CertificateFactory) (java.security.spec PKCS8EncodedKeySpec) (java.net InetSocketAddress) (javax.net.ssl SSLContext SSLSocket SSLServerSocket SSLServerSocketFactory SSLSocketFactory KeyManager KeyManagerFactory TrustManager TrustManagerFactory X509KeyManager X509TrustManager))) (defmacro base64->binary [string] (if (try (import 'java.util.Base64) (catch ClassNotFoundException _)) `(let [^String s# ~string] (.decode (java.util.Base64/getMimeDecoder) s#)) (do (import 'javax.xml.bind.DatatypeConverter) `(javax.xml.bind.DatatypeConverter/parseBase64Binary ~string)))) (def ^CertificateFactory x509-cert-factory "The X.509 certificate factory" (CertificateFactory/getInstance "X.509")) (def ^KeyFactory rsa-key-factory "An RSA key factory" (KeyFactory/getInstance "RSA")) (def key-store-password "You know, a mandatory password stored in memory so we can... encrypt... data stored in memory." (char-array "GheesBetDyPhuvwotNolofamLydMues9")) (defn ^Certificate load-certificate "Loads an X.509 certificate from a file." [file] (with-open [stream (input-stream file)] (.generateCertificate x509-cert-factory stream))) (defn ^"[Ljava.security.cert.Certificate;" load-certificate-chain "Loads an X.509 certificate chain from a file." [file] (with-open [stream (input-stream file)] (let [^"[Ljava.security.cert.Certificate;" ar (make-array Certificate 0)] (.toArray (.generateCertificates x509-cert-factory stream) ar)))) (defn public-key "Loads a public key from a .crt file." [file] (.getPublicKey (load-certificate file))) (defn private-key "Loads a private key from a PKCS8 file." [file] (->> file slurp ; LOL Java (re-find #"(?ms)^-----BEGIN ?.*? PRIVATE KEY-----$(.+)^-----END ?.*? PRIVATE KEY-----$") last base64->binary PKCS8EncodedKeySpec. (.generatePrivate rsa-key-factory))) (defn key-pair "Creates a KeyPair from a public and private key" [public-key private-key] (KeyPair. public-key private-key)) (defn key-store "Makes a keystore from a PKCS8 private key file, a public cert file, and the signing CA certificate." [key-file cert-file] (let [key (private-key key-file) certs (load-certificate-chain cert-file)] (doto (KeyStore/getInstance (KeyStore/getDefaultType)) (.load nil nil) ; alias, private key, password, certificate chain (.setKeyEntry "cert" key key-store-password certs)))) (defn trust-store "Makes a trust store, suitable for backing a TrustManager, out of a CA cert file." [ca-cert-file] (doto (KeyStore/getInstance "JKS") (.load nil nil) (.setCertificateEntry "cacert" (load-certificate ca-cert-file)))) (defn trust-manager "An X.509 trust manager for a KeyStore." [^KeyStore key-store] (let [factory (TrustManagerFactory/getInstance "PKIX" "SunJSSE")] ; I'm concerned that getInstance might return the *same* factory each time, ; so we'll defensively lock before mutating here: (locking factory (->> (doto factory (.init key-store)) .getTrustManagers (filter (partial instance? X509TrustManager)) first)))) (defn key-manager "An X.509 key manager for a KeyStore." ([key-store password] (let [factory (KeyManagerFactory/getInstance "SunX509" "SunJSSE")] (locking factory (->> (doto factory (.init key-store, password)) .getKeyManagers (filter (partial instance? X509KeyManager)) first)))) ([key-store] (key-manager key-store key-store-password))) (defn ssl-context-generator "Returns a function that yields SSL contexts. Takes a PKCS8 key file, a certificate file, and optionally, a trusted CA certificate used to verify peers. The arity-1 body accepts a trusted CA certificate only." ([key-file cert-file ca-cert-file] (let [key-manager (key-manager (key-store key-file cert-file)) trust-manager (trust-manager (trust-store ca-cert-file))] (fn build-context [] (doto (SSLContext/getInstance "TLSv1.3") (.init (into-array KeyManager [key-manager]) (into-array TrustManager [trust-manager]) nil))))) ([key-file cert-file] (let [key-manager (key-manager (key-store key-file cert-file))] (fn build-context [] (doto (SSLContext/getInstance "TLSv1.3") (.init (into-array KeyManager [key-manager]) nil nil))))) ([ca-cert-file] (let [trust-manager (trust-manager (trust-store ca-cert-file))] (fn build-context [] (doto (SSLContext/getInstance "TLSv1.3") (.init nil (into-array TrustManager [trust-manager]) nil)))))) (defn ssl-context "Given a PKCS8 key file, a certificate file, and optionally, a trusted CA certificate used to verify peers, returns an SSLContext. The arity-1 body accepts a single trusted CA certificate which is commonly used to reach databases." (^SSLContext [key-file cert-file ca-cert-file] ((ssl-context-generator key-file cert-file ca-cert-file))) (^SSLContext [key-file cert-file] ((ssl-context-generator key-file cert-file))) (^SSLContext [ca-cert-file] ((ssl-context-generator ca-cert-file)))) (defn ssl-p12-context-generator "Returns a function that yields an SSL contexts. Takes a PKCS12 key/cert file, the password for the PKCS12 file, and a CA certificate that was used to sign the PKCS12." [p12 password ca-cert-file] (let [fin (input-stream p12) ks (KeyStore/getInstance "PKCS12")] (fn build-context [] (.load ks fin password) (let [km (key-manager ks password) tm (trust-manager (trust-store ca-cert-file))] (doto (SSLContext/getInstance "TLSv1.2") (.init (into-array KeyManager [km]) (into-array TrustManager [tm]) nil)))))) (defn ssl-p12-context "Given a PKCS12 key/cert file, the password, and a CA certificate that was used to sign the PKCS12, return an SSL Context" [p12 password ca-cert-file] ((ssl-p12-context-generator p12 password ca-cert-file))) (defn ssl-context->engine [ctx] (.createSSLEngine ^SSLContext ctx)) (def enabled-protocols "An array of protocols we support." (into-array String ["TLSv1.3" "TLSv1.2" "TLSv1.1" "TLSv1"])) (defn ^SSLServerSocket server-socket "Given an SSL context, makes a server SSLSocket." [^SSLContext context ^String host port] (let [^SSLServerSocket sock (.. context getServerSocketFactory createServerSocket)] (doto sock (.bind (InetSocketAddress. host ^int port)) (.setNeedClientAuth true) (.setEnabledProtocols enabled-protocols)))) (defn ^SSLSocket socket "Given an SSL context, makes a client SSLSocket." [^SSLContext context ^String host port] (let [^SSLSocket sock (-> context .getSocketFactory (.createSocket host ^int port))] (.setEnabledProtocols sock enabled-protocols) sock)) (defn test-ssl "Given keys and certificates for a client and server, and the signing CA for both, verify that we can use those files to make an SSL connection." [client-key-file client-cert-file server-key-file server-cert-file ca-cert-file] (let [port (+ 1024 (int (rand 60000))) started (promise) ; A dumb echo server server (future (try (with-open [server (server-socket (ssl-context server-key-file server-cert-file ca-cert-file) "localhost" port)] (prn :accepting) (deliver started :ready) (with-open [sock (.accept server) in (-> sock .getInputStream InputStreamReader. BufferedReader.) out (PrintWriter. (.getOutputStream sock))] (prn :accepted) (loop [] (when-let [s (.readLine in)] (prn :server-got s) (.println out s) (.flush out) (prn :server-sent s) (recur))) (prn :server-done))) (catch Throwable t (clojure.stacktrace/print-stack-trace t))))] @started ; Connect to the local server, send some text, and verify it came back (prn :connecting) (with-open [sock (socket (ssl-context client-key-file client-cert-file ca-cert-file) "localhost" port) in (-> sock .getInputStream InputStreamReader. BufferedReader.) out (PrintWriter. (.getOutputStream sock))] (prn :connected) (.println out "hi") (.flush out) (prn :client-sent) (let [response (.readLine in)] (prn :client-got response) (if (= "hi\n" response) :ok [:wrong response])) (prn :client-done)) (prn :waiting-for-server) @server)) less-awful-ssl-1.0.8/test/000077500000000000000000000000001517373746600154035ustar00rootroot00000000000000less-awful-ssl-1.0.8/test/less/000077500000000000000000000000001517373746600163515ustar00rootroot00000000000000less-awful-ssl-1.0.8/test/less/awful/000077500000000000000000000000001517373746600174675ustar00rootroot00000000000000less-awful-ssl-1.0.8/test/less/awful/ssl_test.clj000066400000000000000000000012701517373746600220210ustar00rootroot00000000000000(ns less.awful.ssl-test (:require [clojure.test :refer :all] [less.awful.ssl :as ssl]) (:import (java.nio.charset StandardCharsets))) (deftest base64 (let [test-str (apply str (repeat 4 "less-awful-ssl"))] (testing "decodes with line break" (is (= test-str (-> (ssl/base64->binary "bGVzcy1hd2Z1bC1zc2xsZXNzLWF3ZnVsLXNzbGxlc3MtYXdmdWwtc3NsbGVzcy1h\nd2Z1bC1zc2w=") (String. StandardCharsets/UTF_8))))) (testing "decodes without line break" (is (= test-str (-> (ssl/base64->binary "bGVzcy1hd2Z1bC1zc2xsZXNzLWF3ZnVsLXNzbGxlc3MtYXdmdWwtc3NsbGVzcy1hd2Z1bC1zc2w=") (String. StandardCharsets/UTF_8)))))))