Clojure(或Java)相当于Ruby的HMAC.hexdigest

Clojure (or Java) equivalent to Ruby's HMAC.hexdigest

当使用 Github API 设置 webhook 时,我可以提供一个秘密。当 Github 向我发送 POST 请求时,使用此秘密 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.

在手册页上,他们 link 到 this Ruby example

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

我需要一种在 Clojure 中重现这一行的方法。

谷歌搜索,我找到了一些用于此目的的示例函数 (1,2,3),但其中 none 有效。我显然做错了什么,因为它们都提供相同的结果,但它与我从 Github.

收到的 header 不匹配

例如,这是我想出的最简单的实现。

(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"))))

用特定的 hook-secret 集在特定的 body 上调用它,得到 "sha1=VtNhKZDOHPU4COL2FSke2ArvtQE="。同时,我从 Github 得到的 header 是 sha1=56d3612990ce1cf53808e2f615291ed80aefb501.

很明显,Github 正在以十六进制打印,但我所有尝试将输出格式化为十六进制导致的字符串比那个长得多。我做错了什么?

试试这个,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))))

您正在对摘要进行 Base64 编码,而您需要将其转换为十六进制。您可以按照@RedDeckWins 对类似问题的建议 , but it would probably be more efficient to use a Java library. This answer 使用 org.apache.commons.codec.binary.Hex 进行编码来执行此操作。

为了将来参考,这里有一个完整的环形中间件,用于根据本文中的答案和引用的线程验证 GitHub Clojure 中的 webhook 调用:

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

编辑

链接代码中最重要的部分按照评论中的要求(正确地)在此处重复。

;; (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)))))