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.
https://clojars.org/less-awful-ssl
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).
# 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:
(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"]))
: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:
(apply test-ssl (map d ["client.pkcs8" "client.crt" "server.pkcs8" "server.crt" "server.crt"]))
: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:
(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:
(with-open [listener (-> (ssl-context "server.pkcs8" "server.crt" "ca.crt")
(server-socket "localhost" 1234))
conn (.accept sock)]
...)
(with-open [sock (-> (ssl-context "client.pkcs8" "server.crt" "ca.crt")
(socket "localhost" 1234)]
...)
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):
(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}))
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.
Copyright © 2013 Kyle Kingsbury (aphyr@aphyr.com)
Distributed under the Eclipse Public License, the same as Clojure.