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)))))
当使用 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 对类似问题的建议 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)))))