Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Added support for client certificates #29

Merged
merged 2 commits into from

2 participants

@diamondap

I added support for SSL client certificates. This is working in our test environment, running against live servers that require client SSL certs.

My company is currently using a modified version of clj-apache-http, to which we added async and x509 support. Unfortunately, that library is based on an alpha version of the Apache http-async library. The underlying Apache API has changed significantly, so keeping clj-apache-http up to date would require a substantial rewrite.

We like your http client because it's simpler, cleaner, and based on a more stable Java library. We just need it to handle client certificates.

Thanks for the work you put into this.

Andrew Diamond

@neotyk neotyk was assigned
@neotyk neotyk merged commit 91d57af into from
@neotyk
Owner

Thank you for your contribution!
With test and all, it's awesome.

Realeasing v0.4.2 with your changes.

@diamondap
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Feb 17, 2012
Commits on Feb 20, 2012
This page is out of date. Refresh to see the latest.
View
9 src/clj/http/async/client.clj
@@ -55,11 +55,12 @@
- :request-timeout :: request timeout in ms
- :user-agent :: User-Agent branding string
- :async-connect :: Execute connect asynchronously
- - :executor-service :: provide your own executor service for callbacks to be executed on"
+ - :executor-service :: provide your own executor service for callbacks to be executed on
+ - :ssl-context :: provide your own SSL Context"
{:tag AsyncHttpClient}
[& {:keys [compression-enabled
connection-timeout
- follow-redirects
+ follow-redirects
idle-in-pool-timeout
keep-alive
max-conns-per-host
@@ -70,7 +71,8 @@
request-timeout
user-agent
async-connect
- executor-service]}]
+ executor-service
+ ssl-context]}]
(AsyncHttpClient.
(.build
(let [b (AsyncHttpClientConfig$Builder.)]
@@ -94,6 +96,7 @@
(set-realm auth b))
(when request-timeout (.setRequestTimeoutInMs b request-timeout))
(.setUserAgent b (if user-agent user-agent *user-agent*))
+ (when-not (nil? ssl-context) (.setSSLContext b ssl-context))
b))))
(defmacro ^{:private true} gen-methods [& methods]
View
157 src/clj/http/async/client/cert.clj
@@ -0,0 +1,157 @@
+; Copyright 2012 Hubert Iwaniuk
+;
+; Licensed under the Apache License, Version 2.0 (the "License");
+; you may not use this file except in compliance with the License.
+; You may obtain a copy of the License at
+;
+; http://www.apache.org/licenses/LICENSE-2.0
+;
+; Unless required by applicable law or agreed to in writing, software
+; distributed under the License is distributed on an "AS IS" BASIS,
+; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+; See the License for the specific language governing permissions and
+; limitations under the License.
+
+(ns http.async.client.cert
+ "Asynchronous HTTP Client - Clojure - Utils"
+ {:author "Hubert Iwaniuk / Andrew Diamond / RoomKey.com"}
+ (:import (java.security
+ KeyStore
+ SecureRandom)
+ (java.security.cert
+ X509Certificate
+ CertificateFactory)
+ (javax.net.ssl
+ KeyManagerFactory
+ SSLContext
+ HttpsURLConnection
+ X509TrustManager)
+ (java.io
+ File
+ InputStream
+ FileInputStream
+ InputStreamReader
+ FileNotFoundException))
+ (:require [clojure.java.io :as io]))
+
+(defrecord BlindTrustManager
+;; "An X509TrustManager that blindly trusts all certificates, chains and issuers.
+;; All methods return nil, indicating that the client/server is trusted."
+ []
+ X509TrustManager
+ (checkClientTrusted [this chain auth-type] nil)
+ (checkServerTrusted [this chain auth-type] nil)
+ (getAcceptedIssuers [this] nil))
+
+(defn #^InputStream load-embedded-resource
+ "Loads a resource embedded in a jar file. Returns an InputStream"
+ [#^String resource]
+ (let [thr (Thread/currentThread)
+ loader (.getContextClassLoader thr)
+ resource (.getResource loader resource)]
+ (FileInputStream. (File. (. resource toURI)))))
+
+(defn #^InputStream resource-stream
+ "Loads the resource at the specified path, and returns it as an
+ InputStream. If there is no file at the specified path, and we
+ are running as a jar, we'll attempt to load the resource embedded
+ within the jar at the specified path."
+ [#^String path]
+ (try
+ (if (. (io/file path) exists)
+ (FileInputStream. path)
+ (load-embedded-resource path))
+ (catch Exception _ (throw (new FileNotFoundException
+ (str "File or resource \""
+ path
+ "\" could not be found."))))))
+
+(defn #^X509Certificate load-x509-cert
+ "Loads an x509 certificate from the specified path, which may be either
+ a file system path or a path to an embedded resource in a jar file.
+ Returns an instace of java.security.cert.X509Certificate."
+ [#^String path]
+ (let [cert-file-instream (resource-stream path)
+ cert-factory (CertificateFactory/getInstance "X.509")
+ cert (. cert-factory generateCertificate cert-file-instream)]
+ (. cert-file-instream close)
+ (cast X509Certificate cert)))
+
+(defn #^KeyStore load-keystore
+ "Loads a KeyStore from the specified file. Param keystore-stream is
+ an InputStream. If password is provided, that will be used to unlock
+ the KeyStore. Password may be nil. If keystore-stream is nil, this
+ returns an empty default KeyStore."
+ [#^FileInputStream keystore-stream #^String password]
+ (let [ks (KeyStore/getInstance (KeyStore/getDefaultType))]
+ (if keystore-stream
+ (if password
+ (. ks load keystore-stream (.toCharArray password))
+ (. ks load keystore-stream nil))
+ (. ks load nil nil))
+ ks))
+
+(defn #^KeyStore add-x509-cert
+ "Adds the x509 certificate to the specified keystore. Param cert-alias
+ is a name for this cert. Returns KeyStore with the certificate loaded."
+ [#^KeyStore keystore #^String cert-alias #^String certificate]
+ (. keystore setCertificateEntry cert-alias certificate)
+ keystore)
+
+(defn #^KeyManagerFactory key-manager-factory
+ "Returns a key manager for X509 certs using the speficied keystore."
+ [#^KeyStore keystore #^String password]
+ (let [kmf (KeyManagerFactory/getInstance "SunX509")]
+ (if password
+ (. kmf init keystore (. password toCharArray))
+ (. kmf init keystore nil))
+ kmf))
+
+(defn #^SSLContext ssl-context
+ "Creates a new SSLContext with x509 certificates. This allows you to use client
+ certificates in your async http requests.
+
+ File params should be relative to resources when your app is running as a jar.
+ For example, if your certificate file is in resources/security/mycert.pem,
+ then the :certificate-file param would be \"security/mycert.pem\".
+
+ :keystore-file - Path to Java keystore containing any private keys and
+ trusted certificate authority certificates required for this connection.
+ If this is empty, will use default keystore.
+
+ :keystore-password - Password to unlock KeyStore.
+
+ :certificate-file The path to the file containing an X509 certificate
+ (or certificate chain) to be used in the https connection
+
+ :certificate-alias - A name by which to access an X509 certificate that will
+ be loaded into the KeyStore.
+
+ :trust-managers - [optional] A seq of javax.net.ssl.X509TrustManager objects.
+ These are used to verify the certificates sent by the remote host. If
+ you don't specify this option, the connection will use an instance of
+ BlindTrustManager, which blindly trusts all certificates. This is handy,
+ but it's not particularly safe."
+ [& {:keys [keystore-file
+ keystore-password
+ certificate-alias
+ certificate-file
+ trust-managers]}]
+ (let [initial-keystore (load-keystore
+ (resource-stream keystore-file)
+ keystore-password)
+ keystore-with-cert (add-x509-cert
+ initial-keystore
+ certificate-alias
+ (load-x509-cert certificate-file))
+ key-mgr-factory (key-manager-factory keystore-with-cert keystore-password)
+ ctx (SSLContext/getInstance "TLS")
+ key-managers (. key-mgr-factory getKeyManagers)
+ trust-managers (into-array javax.net.ssl.X509TrustManager
+ (or trust-managers
+ (list (new BlindTrustManager))))]
+ (. ctx init key-managers trust-managers (new SecureRandom))
+ ctx))
+
+
+
View
15 test-resources/certificate.crt
@@ -0,0 +1,15 @@
+-----BEGIN CERTIFICATE-----
+MIICYTCCAcoCCQDfSeC/2UXwajANBgkqhkiG9w0BAQUFADB1MQswCQYDVQQGEwJV
+UzERMA8GA1UECBMIVmlyZ2luaWExGDAWBgNVBAcTD0NoYXJsb3R0ZXN2aWxsZTEi
+MCAGA1UEChMZQ2xvanVyZSBIVFRQIEFzeW5jIENsaWVudDEVMBMGA1UEAxMMQ2xv
+anVyZSBVc2VyMB4XDTEyMDIxNzE4NTkwMFoXDTIyMDIxNDE4NTkwMFowdTELMAkG
+A1UEBhMCVVMxETAPBgNVBAgTCFZpcmdpbmlhMRgwFgYDVQQHEw9DaGFybG90dGVz
+dmlsbGUxIjAgBgNVBAoTGUNsb2p1cmUgSFRUUCBBc3luYyBDbGllbnQxFTATBgNV
+BAMTDENsb2p1cmUgVXNlcjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAww81
+inFbnomOdSKRfu6fw+35cvEPgD3LGsQMj4LnnNzr3VQAEmLA4n5uG2EUZk67NTjQ
+qWynkZui3ymBa7TrtCNpsEE8elQSwqIdGWV7xts7N2BpU5GSCCD1tCRXQTpuY/iP
+GLQMJSNfRZE8dHLkAF9deL0/exvWAFGR465ojrMCAwEAATANBgkqhkiG9w0BAQUF
+AAOBgQAM0D5KBH/OAkj6L0kFbLe7p8Rui7wiSXV6EVHIWPXMO2JYc2pJ3zpcoiaQ
+gOEKAhraXqT5eyzG60LhLwU7Y+rdWeY5iziDyyACWKm6qjYDuV+bL/BtDZ4x0gWq
+VwCFZZAwdsHc/ScjJQjGH5T0MT9HEnfO86hFG4QI04mRFWXZlA==
+-----END CERTIFICATE-----
View
BIN  test-resources/keystore.jks
Binary file not shown
View
86 test/http/async/client/test/cert.clj
@@ -0,0 +1,86 @@
+; Copyright 2012 Hubert Iwaniuk
+;
+; Licensed under the Apache License, Version 2.0 (the "License");
+; you may not use this file except in compliance with the License.
+; You may obtain a copy of the License at
+;
+; http://www.apache.org/licenses/LICENSE-2.0
+;
+; Unless required by applicable law or agreed to in writing, software
+; distributed under the License is distributed on an "AS IS" BASIS,
+; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+; See the License for the specific language governing permissions and
+; limitations under the License.
+
+(ns http.async.client.test.cert
+ "Testing of http.async client x509 certificates"
+ {:author "Hubert Iwaniuk / Andrew Diamond / RoomKey.com"}
+ (:refer-clojure :exclude [await])
+ (:use clojure.test
+ http.async.client
+ http.async.client.cert
+ [clojure.stacktrace :only [print-stack-trace]])
+ (:import (com.ning.http.client AsyncHttpClient)
+ (java.security KeyStore)
+ (java.security.cert X509Certificate)
+ (javax.net.ssl KeyManagerFactory SSLContext)
+ (http.async.client.cert BlindTrustManager)))
+
+(set! *warn-on-reflection* true)
+
+(def ks-file "test-resources/keystore.jks")
+(def cert-file "test-resources/certificate.crt")
+(def authtype "RSA")
+(def password "secret")
+(def ks-cert-alias "my_test_cert") ;; alias of cert built into key store
+(def other-cert-alias "other_cert") ;; alias for certificate.crt
+
+(defn load-test-certificate [] (load-x509-cert cert-file))
+
+(defn load-test-keystore [] (load-keystore (resource-stream ks-file) password))
+
+(deftest test-load-x509-cert
+ (is (isa? (class (load-test-certificate)) X509Certificate)))
+
+(deftest test-load-keystore
+ (is (= KeyStore (class (load-test-keystore)))))
+
+(deftest test-blind-trust-manager
+ (let [b (BlindTrustManager.)
+ chain (into-array X509Certificate (list (load-test-certificate)))]
+ (is (nil? (.checkClientTrusted b chain authtype)))
+ (is (nil? (.checkServerTrusted b chain authtype)))))
+
+(deftest test-add-509-cert
+ (let [ks (load-test-keystore)
+ cert (load-test-certificate)
+ ks (add-x509-cert ks other-cert-alias cert)]
+ (is (= cert (.getCertificate ks other-cert-alias)))))
+
+(deftest test-key-manager-factory
+ (let [kmf (key-manager-factory (load-test-keystore) password)]
+ (is (= KeyManagerFactory (class kmf)))))
+
+(deftest test-ssl-context
+ (let [ctx1 (ssl-context :keystore-file ks-file
+ :keystore-password password
+ :certificate-file cert-file
+ :certificate-alias other-cert-alias
+ :trust-managers [(BlindTrustManager.)])
+ ;; Make sure it works without :trust-managers param
+ ctx2 (ssl-context :keystore-file ks-file
+ :keystore-password password
+ :certificate-file cert-file
+ :certificate-alias other-cert-alias)]
+ (is (= SSLContext (class ctx1)))
+ (is (= SSLContext (class ctx2)))))
+
+(deftest test-client
+ (let [ctx (ssl-context :keystore-file ks-file
+ :keystore-password password
+ :certificate-file cert-file
+ :certificate-alias other-cert-alias)
+ client (create-client :ssl-context ctx)]
+ (is (= AsyncHttpClient (class client)))
+ ;; Make sure client is using the SSLContext we supplied
+ (is (= ctx (.getSSLContext (.getConfig client))))))
Something went wrong with that request. Please try again.