2

When setting a webhook with the Github API, I can provide a secret. When Github sends me a POST request, this secret is used to encode one of the headers:

The value of this header is computed as the HMAC hex digest of the body, using the secret as the key.

On the manual page, they link to this Ruby example

OpenSSL::HMAC.hexdigest(HMAC_DIGEST, secret, body)

I need a way to reproduce this line in Clojure.

Googling around, I found a number of example functions (1,2,3) for this purpose, but none of them worked. I'm clearly doing something wrong, because they all provide the same result, but it doesn't match the header I receive from Github.

For example, this is the simplest implementation I managed to cook up.

(ns website.test
  (:import javax.crypto.Mac
           javax.crypto.spec.SecretKeySpec
           org.apache.commons.codec.binary.Base64))

;;; Used in core.clj to verify that the payload matches the secret.x
(defn- hmac
  "Generates a Base64 HMAC with the supplied key on a string of data."
  [^String data]
  (let [algo "HmacSHA1"
        signing-key (SecretKeySpec. (.getBytes hook-secret) algo)
        mac (doto (Mac/getInstance algo) (.init signing-key))]
    (str "sha1="
         (String. (Base64/encodeBase64 (.doFinal mac (.getBytes data)))
                  "UTF-8"))))

Calling it on a particular body with a particular hook-secret set, gives me "sha1=VtNhKZDOHPU4COL2FSke2ArvtQE=". Meanwhile, the header I get from Github is sha1=56d3612990ce1cf53808e2f615291ed80aefb501.

Clearly, Github is printing in hex, but all my attempt to format the output as hex led to much longer strings than that one. What am I doing wrong?

Malabarba
  • 4,473
  • 2
  • 30
  • 49

3 Answers3

4

Try this, excerpted from my github repo:

(ns crypto-tutorial.lib.hmac-test
  (:require [clojure.test :refer :all]
            [crypto-tutorial.lib.util :refer :all]
            [crypto-tutorial.lib.hmac :as hmac]))

(defn sha-1-hmac-reference-impl [key bytes]
  (let [java-bytes (->java-bytes bytes)
        java-key (->java-bytes key)]
    (->>
      (doto (javax.crypto.Mac/getInstance "HmacSHA1")
        (.init (javax.crypto.spec.SecretKeySpec. java-key "HmacSHA1")))
      (#(.doFinal % java-bytes))
      (map (partial format "%02x"))
      (apply str))))
RedDeckWins
  • 2,111
  • 14
  • 16
3

You are Base64 encoding the digest, whereas you need to convert it to hex. You can do this as @RedDeckWins recommends using map, but it would probably be more efficient to use a Java library. This answer to a similar question uses org.apache.commons.codec.binary.Hex to do the encoding.

Community
  • 1
  • 1
Nathan Davis
  • 5,636
  • 27
  • 39
  • Both answers work. Accepting this one because it's the one I ended up using. The funciton is `org.apache.commons.codec.binary.Hex/encodeHexString`. – Malabarba Jul 30 '15 at 18:22
-1

For future reference, here is a complete ring middleware for validating GitHub webhook calls in Clojure based on the answers in this and the cited threads:

https://gist.github.com/ska2342/4567b02531ff611db6a1208ebd4316e6#file-gh-validation-clj

EDIT

The most important parts of the linked code are repeated here as (rightfully) requested in the comments.

;; (c) 2016 Stefan Kamphausen
;; Released under the Eclipse Public License 
(def ^:const ^:private signing-algorithm "HmacSHA1")

(defn- get-signing-key* [secret]
  (SecretKeySpec. (.getBytes secret) signing-algorithm))
(def ^:private get-signing-key (memoize get-signing-key*))

(defn- get-mac* [signing-key]
  (doto (Mac/getInstance signing-algorithm)
    (.init signing-key)))
(def ^:private get-mac (memoize get-mac*))

(defn hmac [^String s signature secret]
  (let [mac (get-mac (get-signing-key secret))]
    ;; MUST use .doFinal which resets mac so that it can be
    ;; reused!
    (str "sha1="
         (Hex/encodeHexString (.doFinal mac (.getBytes s))))))

(defn- validate-string [^String s signature secret]
  (let [calculated (hmac s signature secret)]
    (= signature calculated)))

;; Warn: Body-stream can only be slurped once. Possible
;; conflict with other ring middleware
(defn body-as-string [request]
  (let [body (:body request)]
    (if (string? body)
      body
      (slurp body))))

(defn- valid-github? [secrets request]
  (let [body (body-as-string request)
        signature (get-in request [:headers "x-hub-signature"])]
    (log/debug "Found signature" signature)
    (cond
      ;; only care about post
      (not (= :post (:request-method request)))
      "no-validation-not-a-post"

      ;; No secrets defined, no need to validate
      (not (seq secrets))
      "no-validation-no-secrets"

      ;; we have no signature but secrets are defined -> fail
      (and (not signature) (seq secrets))
      false

      ;; must validate this content
      :else
      (some (partial validate-string body signature) secrets))))

(def default-invalid-response
  {:status  400
   :headers {"Content-Type" "text/plain"}
   :body    "Invalid X-Hub-Signature in request."})

(defn wrap-github-validation
  {:arglists '([handler] [handler options])}
  [handler & [{:keys [secret secrets invalid-response]
               :or   {secret           nil
                      secrets          nil
                      invalid-response default-invalid-response}}]]
  (let [secs (if secret [secret] secrets)]
    (fn [request]
      (if-let [v (valid-github? secs request)]
        (do
          (log/debug "Request validation OK:" v)
          (handler (assoc request
                          :validation {:valid true
                                       :validation v}
                          ;; update body which must be an
                          ;; InputStream
                          :body (io/input-stream (.getBytes body)))))

        (do
          (log/warn "Request invalid! Returning" invalid-response)
invalid-response)))))
Stefan Kamphausen
  • 1,615
  • 15
  • 20