Permalink
Find file
Fetching contributors…
Cannot retrieve contributors at this time
470 lines (373 sloc) 18.7 KB

An adventure with clocks, component, and clojure.spec.

I have long parted with my initial, lacking approach to component handling in Clojure. I now rely on Stuart Sierra’s component library for this.

In this short post, I want to showcase how this library helps structure code around clear functional boundaries and allows testing without having to depend on mocking. This might induce building components for seemingly innocuous code. I will also dive into clojure.spec to show how it helps writing automated tests on top of correct generated inputs.

This article is written as a litterate programming org mode file, If you edit the source you can use C-c v t to generate a single file which can be used as an executable **boot** script, which means you will need to have boot installed in order to execute this. If you want to take a look at the generated output, I have also posted it at https://gist.github.com/pyr/b7c2392480359e8bdee765c744f80412.

I used **boot** here because it is easy to build a standalone executable script with it. Be sure to have BOOT_CLOJURE_VERSION set to 1.9.0-alpha14, since clojure.spec is only available from 1.9.0 onward.

To start we will add a shebang line to make sure that boot is invoked to run this script.

#!/usr/bin/env boot

For the purpose of this article, we will only use a few dependencies:

(set-env! :dependencies '[[com.stuartsierra/component "0.3.1"]
                          [org.clojure/test.check     "0.9.0"]])

For the purpose of this article, we will be building request signing functionality. Since this is a standalone **boot** project test namespaces are pulled here as well:

(ns request.signing
   (:require [com.stuartsierra.component :as component]
             [clojure.test :refer :all]
             [clojure.test.check.generators :as tgen]
             [clojure.spec :as s]
             [clojure.spec.gen :as gen]
             [clojure.spec.test :as st])
   (:import javax.crypto.Mac javax.crypto.spec.SecretKeySpec))

Our request signing functionality will work on incoming requests which look like this:

{:timestamp     1483805460         ;; UNIX Epoch of request
 :payload       "some-command"     ;; Request payload
 :authorization {:key       "..."
                 :signature "..."}}

Provided each user is given an API key, and an API secret, we can define the request signing mechanism to be:

signature = hexadecimal_string(hmac_256(secret-key, timestamp + payload))

Factoring the request timestamp in the signing mechanism provides a good protection against replay attacks: by ensuring that requests come-in within a reasonable time-delta (let’s say 500ms). To implement this a first implementation could be based on two components:

  • A **keystore** component which maps API keys to API secrets
  • A **signer** component which signs a payload

We can do away with the **keystore** component here, rely on a map, or something that behaves like a map. (If you want to investigate how to build map-like constructs, there is an article describing how to do that). I won’t describe here how to build an alternate implementation which would look-up keys in a database, but it is rather straightforward.

As far as signing is concerned, interacting with the JVM is required. To avoid pulling-in additional dependencies, we use the javax.crypto available classes:

(defn bytes->hex [bytes]
  (reduce str (map (partial format "%02x") bytes)))

(defn sign-string [secret-key payload]
  (let [key (SecretKeySpec. (.getBytes secret-key) "HmacSHA256")]
    (-> (doto (Mac/getInstance "HmacSHA256")
          (.init key))
        (.doFinal (.getBytes payload))
        (bytes->hex))))

We now have all necessary bits to write a first authorization function. Here is a first version without the addition of components for now:

(defn request-signature [keystore request]
  (when-let [secret (get keystore (get-in request [:authorization :api-key]))]
    (sign-string secret (str timestamp payload))))

(defn authorized-request? [keystore equest]
  (when-let [signature (request-signature keystore request)]
    (= (get-in request [:authorization :signature]) signature)))

This already gives us a lot of safety: a stolen secret key does not allow signing arbitrary requests as would a simple key/token validation approach, commonly found in API implementations.

One thing this authorization scheme is subject to though is replay attacks, a stolen signed payload can be replayed at will.

To limit this risk, we can rely on good wall clocks to ensure that requests are sent within a reasonable timeframe, which we can store as an option:

(def max-delta-ms 500)

We can then write our updated auhtorization function. Note how here we made authorized-request? use a Authorizer as its input. This can be safely done, since started component get their depencies provided.

(defn authorized-timestamp? [timestamp]
  (let [now (System/currentTimeMillis)]
    (<= (- timestamp max-delta-ms) now (+ timestamp max-delta-ms))))

(defn request-signature [keystore request]
  (when-let [secret (get keystore (get-in request [:authorization :api-key]))]
    (sign-string secret (str (:timestamp request) (:payload request)))))

(defrecord Authorizer [keystore])

(defn authorized-request? [{:keys [keystore]} request]
  (when-let [signature (request-signature keystore request)]
    (and (= (get-in request [:authorization :signature]) signature)
         (authorized-timestamp? (:timestamp request)))))

This solution will provide a good layer of security while being secure enough for most practical purposes. Going one step further would involve guaranteeing no replay attack can be performed by handing-out a one-time token for each request. We will not describe this scheme in this article.

While complete, the solution is now hard to test, since it relies on a wall clock. There are three approaches to testing we can take:

  • Good old sleep calls which are a safe way of having spurious test errors :-)
  • Mocking wall clock calls
  • Making the clock a component

It does seem overkill to build a specific clock component for the standard behavior of a wall clock which just reaches out to the system.

(defprotocol Clock  (now! [this]))
(defrecord WallClock [] Clock (now! [this] (System/currentTimeMillis)))

With this simple protocol we can now build our complete component system. This will be quite similar to the previous presented implementation, with the exception that the Authorizer component now depends on clock as well and will use both in authorized-request?.

(defn authorized-timestamp? [clock timestamp]
  (<= (- timestamp max-delta-ms) (now! clock) (+ timestamp max-delta-ms)))

(defn request-signature [keystore request]
  (when-let [secret (get keystore (get-in request [:authorization :api-key]))]
    (sign-string secret (str (:timestamp request) (:payload request)))))

(defrecord Authorizer [clock keystore])

(defn authorized-request? [{:keys [keystore clock]} request]
  (when-let [signature (request-signature keystore request)]
    (and (= (get-in request [:authorization :signature]) signature)
         (authorized-timestamp? clock (:timestamp request)))))

Our resulting system will thus be a three-component one:

  • A **clock** component which will give the current time.
  • A **keystore** component to look-up the secret key corresponding to an API key.
  • An **authorizer** component, used to authorize incoming requests, relying on the two above components.

We can then imagine building the system like this:

(defn start-system [secret-keys]
   (-> (component/system-map :keystore   secret-keys
                             :clock      (->WallClock)
                             :authorizer (map->Authorizer {}))
       (component/system-using {:authorizer [:clock :keystore]})
       (component/start-system)))

With this, everything necessary for authorizing requests is available, but there are no tests yet. If we were to rely on this implementation for tests, we would have to play with timing for test purposes:

(deftest simple-signing
  (let [sys (start-system {:foo "ABCDEFGHIJK"})]
    (doseq [cmd ["start-engine" "thrust" "stop-engine"]]
      (let [request {:timestamp (now! (:clock sys))
                     :payload       cmd
                     :authorization {:api-key :foo}}
            signed  (assoc-in request [:authorization :signature]
                              (request-signature (:keystore sys) request))]
        (is (authorized-request? sys signed))
        (Thread/sleep 600)
        (is (not(authorized-request? sys signed)))))))

This is unfortunately brittle and does not lend itself easily to a large number of tests since it relies on sleep.

Thanks to our component-based approach we can now write an alternate clock:

(defrecord RefClock [state] Clock (now! [_] @state))

Once we have our new clock, we can adapt the start system function:

(defn start-system [secret-keys time]
   (-> (component/system-map :keystore   secret-keys
                             :clock      (if time (->RefClock time) (->WallClock))
                             :authorizer (map->Authorizer {}))
       (component/system-using {:authorizer [:clock :keystore]})
       (component/start-system)))

This new clock can then be used for our tests, doing away with brittle sleep calls and paving the way for generative tests.

(deftest simple-signing
  (let [time (atom 0)
        sys  (start-system {:foo "ABCDEFGHIJK"} time)]
    (doseq [cmd ["start-engine" "thrust" "stop-engine"]]
      (let [request {:timestamp (now! (:clock sys))
                     :payload       cmd
                     :authorization {:api-key :foo}}
            signed  (assoc-in request [:authorization :signature]
                              (request-signature (:keystore sys) request))]
        (is (authorized-request? sys signed))
        (swap! time + max-delta-ms 1) 
        (is (not(authorized-request? sys signed)))))))

While this is nice, it only tests a very small subset of input. To go beyond this, we can reach out to clojure.spec to give us compile-time guarantees that we are using correct types for our functions and to allow building generative tests.

In a few instances, we help generators by providing a set of known values. We start off by forcing every generated keystore instance to be:

{:foo "ABCDEFGH"
 :bar "IJKLMNOP"}

Generated api-key instances will also always be either :foo or :bar. Clock instance generation is bound to a RefClock instance as well.

Let’s look at the code in detail. We start by defining a few predicates to make our specs a bit easier to understand:

(def lookup?           #(instance? clojure.lang.ILookup %))
(def clock?            #(satisfies? Clock %))
(def not-empty-string? #(not= "" %))
(def sig-bytes?        #(= 32 (count %))) ;; Number of bytes in a signature
(def valid-sig-width?  #(= 64 (count %)))
(def valid-sig-chars?  #(re-matches #"^[0-9a-f]+$" %))

Next we can define data types for every plain and compound type we have created:

(s/def ::keystore lookup?)
(s/def ::clock clock?)
(s/def ::authorizer (s/keys :req-un [::keystore ::clock]))
(s/def ::signature (s/and string? valid-sig-width? valid-sig-chars?))
(s/def ::api-key keyword?)
(s/def ::authorization (s/keys :req-un [::api-key] :opt-un [::signature]))
(s/def ::timestamp int?)
(s/def ::secret-key (s/and string? not-empty-string?))
(s/def ::payload (s/and string? not-empty-string?))
(s/def ::request (s/keys :req-un [::timestamp ::payload ::authorization]))
(s/def ::bytes (s/and bytes? sig-bytes?))

I like to also provide separate specs for argument lists:

(s/def ::auth-request? (s/cat :authorizer ::authorizer :request ::request))
(s/def ::request-signature (s/cat :keystore ::keystore :request ::request))
(s/def ::auth-timestamp? (s/cat :clock ::clock :timestamp ::timestamp))
(s/def ::sign-string (s/cat :secret-key ::secret-key :payload string?))
(s/def ::bytes->hex (s/cat :bytes ::bytes))
(s/def ::now! (s/cat :block ::clock))

We can now use the above types to specify our functions. Nothing extraordinary here if you have already used spec.

(s/fdef bytes->hex :args ::bytes->hex :ret ::signature)
(s/fdef sign-string :args ::sign-string :ret ::signature)
(s/fdef now! :args ::now! :ret ::timestamp)
(s/fdef authorized-timestamp? :args ::auth-timestamp? :ret boolean?)
(s/fdef request-signature :args ::request-signature :ret ::signature)
(s/fdef authorized-request? :args ::auth-request? :ret boolean?)

We are now fully specified and using instrument will allow verifying functions are called properly.

The complex bit is to go from here to tests which use generators for building sensible data. Relying on the provided generators will not cut it as they would not be able to build clock and keystore instances, nor would they be able to provide sensible timestamp or signature values.

This is most obvious in request which contains co-dependent information, since the :signature field in the :authorization map depends on the payload and timestamp of the request. Likewise, testing authorized-timestamp? relies on having a solid way of generating timestamp, which we built our Clock protocol for.

Fortunately, spec allows overriding generators. We can start by building simple generators for values we want picked from a narrow set, this is for instance the case for our keystore and related api keys:

(def fake-keystore {:foo "ABCDEFGH" :bar "IJKLMNOP"})
(def fake-time     (atom 0))
(def fake-clock    (->RefClock fake-time))

(defn keystore-gen [] (s/gen #{fake-keystore}))
(defn api-key-gen  [] (s/gen (set (keys fake-keystore))))
(defn clock-gen    [] (s/gen #{fake-clock}))

We can test out this generators on the repl:

(gen/sample (s/gen (s/with-gen ::api-key api-key-gen)))
(gen/sample (s/gen (s/with-gen ::clock-gen clock-gen)))

To instrument bytes->hex we will need a way of generating 32 wide byte arrays. Since there is no such generator, we will need to compose the creation of a 32-width vector and its coercion to a byte array:

(defn bytes-gen    [] (gen/fmap byte-array (gen/vector tgen/byte 32)))

In the above we use byte from clojure.test.check.generators since no such generator exists in clojure.spec.gen.

Only the most complex generator remains, request-gen for building request maps. If we look at our base building blocks, here is what we need to build a correct request map:

  • A **keystore** to sign the request
  • A **clock** to get a correct timestamp
  • A random **api-key**
  • A random **payload**

Once we have these elements we can transform them into a correct request. We will use fmap again here, and split out request generation in two functions:

(defn sign-request [[ks req]]
   (assoc-in req [:authorization :signature] (request-signature ks req)))

(defn build-request [{:keys [clock payload keystore api-key]}]
  (vector
    keystore
    {:timestamp     (now! clock)
     :payload       payload
     :authorization {:api-key api-key}}))

(defn request-gen []
  (gen/fmap
    (comp sign-request build-request)
    (s/gen (s/keys :req-un [::clock ::keystore ::api-key ::payload])
           {::clock clock-gen ::keystore keystore-gen ::api-key api-key-gen})))

We now have a solid way of generating requests, we can again test it on the repl:

(gen/sample (s/gen (s/with-gen ::request request-gen)))

Now that we have good generation available, we can write automated testing for all of our functions. We can do this by enumerating all testable symbols in the current namespace and running generative testing on them, supplying our list of generator overrides. This involves checking that the result is true for all test outputs generated by clojure.spec.test/check:

(def gen-overrides {::keystore      keystore-gen
                    ::clock         clock-gen
                    ::api-key       api-key-gen
                    ::bytes         bytes-gen
                    ::request       request-gen})

(deftest generated-tests
  (doseq [test-output (-> (st/enumerate-namespace 'request.signing)
                          (st/check {:gen gen-overrides}))]
    (testing (-> test-output :sym name)
      (is (true? (-> test-output :clojure.spec.test.check/ret :result))))))

To go, one last step further, we can supply a different function spec to our most important function, authorized-request? to make sure that given all provided inputs, our authorizer determined the request to be authorized:

(deftest specialized-tests
   (testing "authorized-request?"
      (is (true? (-> (st/check-fn authorized-request?
                                  (s/fspec :args ::auth-request? :ret boolean?)
                                  {:gen gen-overrides})
                     :clojure.spec.test.check/ret
                     :result)))))

Last, we run all tests:

(run-tests 'request.signing)

I’d like to thank Max Penet and Gary Fredericks for their valuable input while writing this.