· 9 years ago · Jan 09, 2017, 04:56 PM
1#!/usr/bin/env boot
2
3(set-env! :dependencies '[[com.stuartsierra/component "0.3.1"]
4 [org.clojure/test.check "0.9.0"]])
5
6(ns request.signing
7 (:require [com.stuartsierra.component :as component]
8 [clojure.test :refer :all]
9 [clojure.test.check.generators :as tgen]
10 [clojure.spec :as s]
11 [clojure.spec.gen :as gen]
12 [clojure.spec.test :as st])
13 (:import javax.crypto.Mac javax.crypto.spec.SecretKeySpec))
14
15(defn bytes->hex [bytes]
16 (reduce str (map (partial format "%02x") bytes)))
17
18(defn sign-string [secret-key payload]
19 (let [key (SecretKeySpec. (.getBytes secret-key) "HmacSHA256")]
20 (-> (doto (Mac/getInstance "HmacSHA256")
21 (.init key))
22 (.doFinal (.getBytes payload))
23 (bytes->hex))))
24
25(def max-delta-ms 500)
26
27(defprotocol Clock (now! [this]))
28(defrecord WallClock [] Clock (now! [this] (System/currentTimeMillis)))
29
30(defn authorized-timestamp? [clock timestamp]
31 (<= (- timestamp max-delta-ms) (now! clock) (+ timestamp max-delta-ms)))
32
33(defn request-signature [keystore request]
34 (when-let [secret (get keystore (get-in request [:authorization :api-key]))]
35 (sign-string secret (str (:timestamp request) (:payload request)))))
36
37(defrecord Authorizer [clock keystore])
38
39(defn authorized-request? [{:keys [keystore clock]} request]
40 (when-let [signature (request-signature keystore request)]
41 (and (= (get-in request [:authorization :signature]) signature)
42 (authorized-timestamp? clock (:timestamp request)))))
43
44(defrecord RefClock [state] Clock (now! [_] @state))
45
46(def lookup? #(instance? clojure.lang.ILookup %))
47(def clock? #(satisfies? Clock %))
48(def not-empty-string? #(not= "" %))
49(def sig-bytes? #(= 32 (count %))) ;; Number of bytes in a signature
50(def valid-sig-width? #(= 64 (count %)))
51(def valid-sig-chars? #(re-matches #"^[0-9a-f]+$" %))
52
53(s/def ::keystore lookup?)
54(s/def ::clock clock?)
55(s/def ::authorizer (s/keys :req-un [::keystore ::clock]))
56(s/def ::signature (s/and string? valid-sig-width? valid-sig-chars?))
57(s/def ::api-key keyword?)
58(s/def ::authorization (s/keys :req-un [::api-key] :opt-un [::signature]))
59(s/def ::timestamp int?)
60(s/def ::secret-key (s/and string? not-empty-string?))
61(s/def ::payload (s/and string? not-empty-string?))
62(s/def ::request (s/keys :req-un [::timestamp ::payload ::authorization]))
63(s/def ::bytes (s/and bytes? sig-bytes?))
64
65(s/def ::auth-request? (s/cat :authorizer ::authorizer :request ::request))
66(s/def ::request-signature (s/cat :keystore ::keystore :request ::request))
67(s/def ::auth-timestamp? (s/cat :clock ::clock :timestamp ::timestamp))
68(s/def ::sign-string (s/cat :secret-key ::secret-key :payload string?))
69(s/def ::bytes->hex (s/cat :bytes ::bytes))
70(s/def ::now! (s/cat :block ::clock))
71
72(s/fdef bytes->hex :args ::bytes->hex :ret ::signature)
73(s/fdef sign-string :args ::sign-string :ret ::signature)
74(s/fdef now! :args ::now! :ret ::timestamp)
75(s/fdef authorized-timestamp? :args ::auth-timestamp? :ret boolean?)
76(s/fdef request-signature :args ::request-signature :ret ::signature)
77(s/fdef authorized-request? :args ::auth-request? :ret boolean?)
78
79(def fake-keystore {:foo "ABCDEFGH" :bar "IJKLMNOP"})
80(def fake-time (atom 0))
81(def fake-clock (->RefClock fake-time))
82
83(defn keystore-gen [] (s/gen #{fake-keystore}))
84(defn api-key-gen [] (s/gen (set (keys fake-keystore))))
85(defn clock-gen [] (s/gen #{fake-clock}))
86
87(defn bytes-gen [] (gen/fmap byte-array (gen/vector tgen/byte 32)))
88
89(defn sign-request [[ks req]]
90 (assoc-in req [:authorization :signature] (request-signature ks req)))
91
92(defn build-request [{:keys [clock payload keystore api-key]}]
93 (vector
94 keystore
95 {:timestamp (now! clock)
96 :payload payload
97 :authorization {:api-key api-key}}))
98
99(defn request-gen []
100 (gen/fmap
101 (comp sign-request build-request)
102 (s/gen (s/keys :req-un [::clock ::keystore ::api-key ::payload])
103 {::clock clock-gen ::keystore keystore-gen ::api-key api-key-gen})))
104
105(def gen-overrides {::keystore keystore-gen
106 ::clock clock-gen
107 ::api-key api-key-gen
108 ::bytes bytes-gen
109 ::request request-gen})
110
111(deftest generated-tests
112 (doseq [test-output (-> (st/enumerate-namespace 'request.signing)
113 (st/check {:gen gen-overrides}))]
114 (testing (-> test-output :sym name)
115 (is (true? (-> test-output :clojure.spec.test.check/ret :result))))))
116
117(deftest specialized-tests
118 (testing "authorized-request?"
119 (is (true? (-> (st/check-fn authorized-request?
120 (s/fspec :args ::auth-request? :ret boolean?)
121 {:gen gen-overrides})
122 :clojure.spec.test.check/ret
123 :result)))))
124
125(deftest failing-authorization
126 (testing "failed authorization"
127 (let [authorizer (Authorizer. fake-clock fake-keystore)]
128 (doseq [[req _] (s/exercise ::request 1000 gen-overrides)]
129 (swap! fake-time + max-delta-ms 1)
130 (is (false? (authorized-request? authorizer req)))))))
131
132(run-tests 'request.signing)