使用Github API设置webhook时,我可以提供一个秘密。当Github向我发送POST请求时,使用此秘密to encode one of the headers:
此标头的值计算为正文的HMAC十六进制摘要,使用密钥作为密钥。
在手册页上,它们会链接到this Ruby example
OpenSSL::HMAC.hexdigest(HMAC_DIGEST, secret, body)
我需要一种方法在Clojure中重现这一行。
在Google上搜索,为此目的我找到了许多示例函数(1,2,3),但没有一个能够正常工作。我显然做错了,因为它们都提供相同的结果,但它与我从Github收到的标题不匹配。
例如,这是我设法做的最简单的实现。
(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"))))
在具有特定body
集的特定hook-secret
上调用它,会向我"sha1=VtNhKZDOHPU4COL2FSke2ArvtQE="
。同时,我从Github获得的标题是sha1=56d3612990ce1cf53808e2f615291ed80aefb501
。
显然,Github是以十六进制打印的,但是我将输出格式化为十六进制的所有尝试都导致字符串比那个更长。我做错了什么?
答案 0 :(得分:4)
试试这个,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))))
答案 1 :(得分:3)
您是Base64编码摘要,而您需要将其转换为十六进制。您可以这样做,因为@RedDeckWins建议using map
,但使用Java库可能更有效。 This answer对类似问题使用org.apache.commons.codec.binary.Hex
进行编码。
答案 2 :(得分:-1)
为了将来参考,这里有一个完整的环中间件,用于根据本文和引用的主题中的答案验证Clojure中的GitHub 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)))))