Skip to content

Commit

Permalink
Merge release/0.2.0
Browse files Browse the repository at this point in the history
  • Loading branch information
pbors committed Dec 15, 2014
2 parents bd0ecff + 22fbfe7 commit 5fabaf9
Show file tree
Hide file tree
Showing 8 changed files with 317 additions and 5 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ This supports only the http endpoint, not the UNIX socket endpoint.
Add clj-docker to your `:dependencies`.

```
[com.palletops/clj-docker "0.1.3"]
[com.palletops/clj-docker "0.2.0"]
```

## Usage
Expand Down
5 changes: 5 additions & 0 deletions ReleaseNotes.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 0.2.0

- Add support for libtrust identity auth
Docker machine uses a trust mechanism based on a shared key.

## 0.1.3

- Allow use of keystore and trust-store
Expand Down
11 changes: 7 additions & 4 deletions project.clj
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
(defproject com.palletops/clj-docker "0.1.3"
(defproject com.palletops/clj-docker "0.2.0"
:description "A clojure wrapper for the Docker API"
:url "https://github.com/palletops/clj-docker"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.apache.commons/commons-compress "1.5"]
[com.palletops/api-builder "0.2.0"]
:dependencies [[com.palletops/api-builder "0.2.0"]
[org.apache.commons/commons-compress "1.5"]
[org.clojure/data.codec "0.1.0"]
[cheshire "5.3.1"]
[clj-http "1.0.1"]
[com.taoensso/timbre "3.1.6"]]
[clj-time "0.8.0"]
[com.taoensso/timbre "3.1.6"]
[net.oauth.core/oauth "20100527"]]
:profiles {:provided {:dependencies [[org.clojure/clojure "1.6.0"]]}}
:plugins [[lein-shell "0.4.0"]]
:aliases {"doc" ["shell" "bundle" "exec" "jekyll" "serve" "--watch"]}
Expand Down
1 change: 1 addition & 0 deletions src/com/palletops/docker.clj
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@
:as :auto}
(select-keys endpoint [:insecure? :keystore :trust-store
:keystore-pass :trust-store-pass])
{:insecure (:insecure? endpoint)}
request)))

(defn base64-json-header
Expand Down
63 changes: 63 additions & 0 deletions src/com/palletops/docker/cert.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
(ns com.palletops.docker.cert
(:require
[clj-time.core :as time]
[clj-time.coerce :refer [to-date to-long]]
[com.palletops.docker.jwk :refer [key-id]])
(:import
[java.security PublicKey]
[java.security.cert Certificate]
[sun.security.util ObjectIdentifier]
[sun.security.x509
AlgorithmId CertificateAlgorithmId CertificateExtensions
CertificateIssuerName
CertificateSerialNumber CertificateSubjectName CertificateValidity
CertificateX509Key CertificateVersion
BasicConstraintsExtension ExtendedKeyUsageExtension
X500Name X509CertImpl X509CertInfo]))

(def CRITICAL true)
(def NON_CRITICAL false)
(def server-auth-oid (ObjectIdentifier. "1.3.6.1.5.5.7.3.1"))
(def client-auth-oid (ObjectIdentifier. "1.3.6.1.5.5.7.3.2"))

(defn ^Certificate new-cert
"Return a new X509 Certificate, signed by `key`."
[{:keys [key ^PublicKey public-key
client-auth server-auth
ca max-path
not-before not-after]
:or {not-before (time/minus (time/now) (time/days 7))
not-after (time/plus (time/now) (time/years 10))
max-path 1024}}]
(let [interval (CertificateValidity. (to-date not-before) (to-date not-after))
algo (AlgorithmId. AlgorithmId/sha256WithECDSA_oid)
v (filter identity
[(if server-auth server-auth-oid)
(if client-auth client-auth-oid)])
_ (println "extensions" v)
ext (doto (CertificateExtensions.)
(.set ExtendedKeyUsageExtension/NAME
(ExtendedKeyUsageExtension.
NON_CRITICAL (java.util.Vector. ^java.util.List v))))
_ (when ca
(.set BasicConstraintsExtension/NAME
(BasicConstraintsExtension. CRITICAL ca max-path)))
cert-info (doto (X509CertInfo.)
(.set X509CertInfo/VERSION
(CertificateVersion. CertificateVersion/V3))
(.set X509CertInfo/VALIDITY
interval)
(.set X509CertInfo/SERIAL_NUMBER
(CertificateSerialNumber. 0))
(.set (str X509CertInfo/ISSUER
"." CertificateSubjectName/DN_NAME)
(X500Name. (str "CN=" (key-id public-key))))
(.set (str X509CertInfo/SUBJECT
"." CertificateSubjectName/DN_NAME)
(X500Name. (str "CN=" (key-id public-key))))
(.set X509CertInfo/KEY (CertificateX509Key. public-key))
(.set X509CertInfo/EXTENSIONS ext)
(.set X509CertInfo/ALGORITHM_ID
(CertificateAlgorithmId. algo)))]
(doto (X509CertImpl. cert-info)
(.sign key "SHA256WithECDSA"))))
37 changes: 37 additions & 0 deletions src/com/palletops/docker/identity_auth.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
(ns com.palletops.docker.identity-auth
"Identity auth scheme as used by docker"
(:import
[java.net InetAddress Socket]
[java.security KeyStore SecureRandom]
[java.security.cert X509Certificate]
[javax.net.ssl KeyManagerFactory SSLContext SSLSocket X509TrustManager]))

(defn ^X509TrustManager accept-all-trust-manager
[]
(reify X509TrustManager
(checkServerTrusted [_ chain autj-type])
(getAcceptedIssuers [_])))

(defn ^X509Certificate server-cert
"Obtain a docker server's X509 Certificate using docker's identity
auth scheme. This creates a connection to the docker server. In
order to obtain the server certificate, it does not verify the
server certificate.
You can add the returned certificate to a trust store to verify further
connections to the server."
[^String host ^Integer port ^KeyStore keystore ^chars keystore-pw]
(let [key-mgr-factory (doto (KeyManagerFactory/getInstance
(KeyManagerFactory/getDefaultAlgorithm))
(.init keystore keystore-pw))
context (doto (SSLContext/getInstance "TLS")
(.init (.getKeyManagers key-mgr-factory)
(into-array X509TrustManager
[(accept-all-trust-manager)])
(SecureRandom.)))
socket-factory (.getSocketFactory context)
addr (InetAddress/getByName host)
^SSLSocket socket (.createSocket socket-factory addr port)
server-cert (first (.getPeerCertificates (.getSession socket)))]
(.close socket)
server-cert))
78 changes: 78 additions & 0 deletions src/com/palletops/docker/jwk.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
(ns com.palletops.docker.jwk
"JSON Web Key Functions"
(:require
[clojure.data.codec.base64 :refer [decode]]
[clojure.java.io :as io]
[clojure.string :as string]
[cheshire.core :refer [parse-stream]])
(:import
java.io.File
[java.security AlgorithmParameters Key KeyFactory MessageDigest]
[java.security.spec
ECGenParameterSpec ECParameterSpec ECPoint ECPrivateKeySpec ECPublicKeySpec]
[org.apache.commons.codec.binary Base32]))

(defmulti curve-name
"Return a JSSE curve name for the given JWK curve name."
(fn [id] id))

(defmethod curve-name :default [id]
(throw (ex-info (str "Unimplemented elliptic curve " id) {:id id})))

(defmethod curve-name "P-256" [_] "secp256r1")

;; http://mail.openjdk.java.net/pipermail/security-dev/2013-October/009107.html
(defn named-curve-spec
[curve-name]
(let [parameters (AlgorithmParameters/getInstance "EC", "SunEC")
gen-spec (ECGenParameterSpec. curve-name)]
(.init parameters gen-spec)
(.getParameterSpec parameters ECParameterSpec)))

(defn base64url->base64
[^String s]
(let [s (-> s
(string/replace \- \+)
(string/replace \_ \/))]
(case (int (mod (count s) 4))
0 (str s "==")
2 (str s "==")
3 (str s "=")
(throw
(ex-info "Invalid base64 string" {:string s})))))

(def base64url->BigInteger
(comp
#(BigInteger. ^bytes %)
decode
#(.getBytes ^String %)
base64url->base64))

(defn ^Key jwk->key
"Convert a JWK map specifying an EC key to a Key"
[{:keys [crv d x y] :as jwk}]
(let [spec (named-curve-spec (curve-name crv))
kf (KeyFactory/getInstance "EC")]
(if d
(.generatePrivate kf (ECPrivateKeySpec. d spec))
(.generatePublic kf (ECPublicKeySpec. (ECPoint. x y) spec)))))

(defn ^Key load-jwk [^File key-file]
(let [{:keys [crv d kid kty x y] :as json} (with-open [r (io/reader key-file)]
(parse-stream r keyword))
json (-> json
(cond-> (:d json) (update-in [:d] base64url->BigInteger))
(cond-> (:m json) (update-in [:m] base64url->BigInteger))
(update-in [:x] base64url->BigInteger)
(update-in [:y] base64url->BigInteger))]
(when-not (= kty "EC")
(throw (ex-info "Unsupported key type" json)))
(jwk->key json)))

(defn key-id [^Key key]
(->> (.digest (MessageDigest/getInstance "SHA-256") (.getEncoded key))
(.encodeAsString (Base32.))
(partition 4)
(take 12)
(map #(apply str %))
(string/join ":")))
125 changes: 125 additions & 0 deletions src/com/palletops/docker/keystore.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
(ns com.palletops.docker.keystore
"Create keystore based on docker machine and boot2docker cert files."
(:require
[clojure.data.codec.base64 :refer [decode]]
[clojure.java.io :as io]
[clojure.string :as string]
[com.palletops.docker.cert :refer [new-cert]]
[com.palletops.docker.jwk :refer [load-jwk]])
(:import
java.io.File
[java.security Key KeyFactory KeyStore]
[java.security.cert Certificate CertificateFactory X509Certificate]
[net.oauth.signature.pem PKCS1EncodedKeySpec]))

(defn ^Key load-key
"Load a key from a PKCS#1 encoded pem file."
[^File key-file]
(let [der (with-open [s (java.io.BufferedInputStream.
(io/input-stream key-file))]
(let [chars (slurp s)
b64chars (->> (string/split-lines chars)
(remove #(re-matches #"---.*---" %))
string/join)]
(decode (.getBytes b64chars))))
key-spec (PKCS1EncodedKeySpec. der)
key-factory (KeyFactory/getInstance "RSA")
key (.generatePrivate key-factory (.getKeySpec key-spec))]
key))

(defn ^X509Certificate load-cert
"Load a X509Certificate from a pem file."
[^CertificateFactory cert-factory ^File path]
(with-open [s (java.io.BufferedInputStream. (io/input-stream path))]
(.generateCertificate cert-factory s)))

(defn cert-path-key-and-certs
"Return a key and certificate chain based on the files in cert-path."
[cert-path]
(let [ca-f (io/file cert-path "ca.pem")]
(if (.exists ca-f)
(let [cert-factory (CertificateFactory/getInstance "X.509")
ca-cert (load-cert cert-factory ca-f)
server-cert (load-cert cert-factory (io/file cert-path "cert.pem"))
key (load-key (io/file cert-path "key.pem"))]
[key [server-cert ca-cert]])
(throw (ex-info (str "No such file: " (str ca-f)) {:file (str ca-f)})))))

(defn ^String key-store
"Return a JKS format keystore, containing the creds specified in
files in cert-path."
[cert-path]
(let [ks-path (io/file cert-path "client.ks")
ks (KeyStore/getInstance "JKS")
pw (.toCharArray "")]
(if (and (.exists ks-path) (pos? (.length ks-path)))
[(str ks-path) (doto ks
(.load (io/input-stream ks-path) pw))]
(let [[key [server-cert ca-cert]] (cert-path-key-and-certs cert-path)]
(doto ks
(.load nil pw)
(.setEntry
"ca"
(java.security.KeyStore$TrustedCertificateEntry. ca-cert)
nil)
(.setEntry
"server"
(java.security.KeyStore$TrustedCertificateEntry. server-cert)
nil)
(.setKeyEntry
"client" key pw
(into-array X509Certificate [server-cert ca-cert])))
(with-open [os (io/output-stream ks-path)]
(.store ks os pw))
[(str ks-path) ks]))))

(defn jwk-key-and-cert
"Return the private key specified by jwk-path. The adjacent public
key is also read and returned. A self-signed X509Certificate is
create as a client certificate and returned."
[jwk-path]
(let [f (io/file jwk-path)]
(if (.exists f)
(let [key (load-jwk f)
public-key (load-jwk
(io/file (.getParent f) "public-key.json"))
cert (new-cert {:key key
:public-key public-key
:client-auth true
:server-auth false})]

[key public-key cert])
(throw (ex-info (str "No such file: " (str f)) {:file (str f)})))))


(defn ^String key-store-jwk
"Return a JKS format keystore, containing the orivate key specified
by jwk-path. The adjacent public key is also read and a self-signed
X509Certificate is create as a client certificate."
[jwk-path]
(let [ks-path (io/file (.getParentFile (io/file jwk-path)) "client.ks")
ks (KeyStore/getInstance "JKS")
pw (.toCharArray "")]
(if (and (.exists ks-path) (pos? (.length ks-path)))
[(str ks-path) (doto ks
(.load (io/input-stream ks-path) pw))]
(let [[key public-key cert] (jwk-key-and-cert jwk-path)]
(doto ks
(.load nil pw)
(.setEntry
"client"
(java.security.KeyStore$TrustedCertificateEntry. cert)
nil)
(.setKeyEntry "clientkey" key pw
(into-array X509Certificate [cert])))
(with-open [os (io/output-stream ks-path)]
(.store ks os pw))
[(str ks-path) ks]))))

(defn add-cert
"Add a cert to the specified keystore"
[ks-path ^KeyStore ks ^X509Certificate cert]
(.setEntry
ks "server" (java.security.KeyStore$TrustedCertificateEntry. cert) nil)
(with-open [os (io/output-stream ks-path)]
(.store ks os (.toCharArray ""))))

0 comments on commit 5fabaf9

Please sign in to comment.