验证 Trello Webhook 签名

Verifying Trello Webhook signature

我无法成功验证来自 Trello 的 Webhook 请求。这是我知道的。

Trello 的 webhook 文档 here 指出:

Each webhook trigger contains the HTTP header X-Trello-Webhook. The header is a base64 digest of an HMAC-SHA1 hash. The hashed content is the concatenation of the full request body and the callbackURL exactly as it was provided during webhook creation. The key used to sign this text is your application’s secret.

这是可以理解的。他们接着说

Because of certain defaults in the crypto utilities in node, the payloads that we sign are treated as binary strings, not utf-8. For example, if you take the en-dash character (U+2013 or 8211 in decimal), and create a binary buffer out of it in Node, it will show up as a buffer of [19], which are the 8 least significant bits of 8211. That is the value that is being used in the digest to compute the SHA-1.

我不太清楚。我的理解是有效负载(body + callbackURL)的每个字符都被放入一个 8 位整数,溢出被忽略。 (因为 8211 == 0b10000000010011,和 0b00010011 == 19)这就是我认为我的问题所在。

我用来解决 Trello 节点负载问题的函数是:

func bitShift(s string) []byte {
    var byteString []byte

    // For each rune in the string
    for _, c := range s {

        // Create a byte slice
        b := []byte(string(c))

        // Take the sign off the least significant byte
        tmp := b[len(b)-1] << 1
        tmp = tmp >> 1

        // Append it to the byte string
        byteString = append(byteString, tmp)
    }
    return byteString
}

也很有可能我在基本验证步骤上做错了。我觉得还可以,虽然我对此有点陌生。

// VerifyNotificationHeader ...
func VerifyNotificationHeader(signedHeader, trelloAPISecret string, requestURL *url.URL, body []byte) bool {

    // Put callbackURL and body into byte slice
    urlBytes := bitShift(requestURL.String())
    bitBody := bitShift(string(body))

    // Sign, hash, and encode the payload
    secret := []byte(trelloAPISecret)
    keyHMAC := hmac.New(sha1.New, secret)
    keyHMAC.Write(append(bitBody, urlBytes...))
    signedHMAC := keyHMAC.Sum(nil)
    base64signedHMAC := base64.StdEncoding.EncodeToString(signedHMAC)

    if comp := strings.EqualFold(base64signedHMAC, signedHeader); !comp {
        return false
    }
    return true
}

如果您需要更多信息,请告诉我。谢谢!

更新:已解决,查看答案。

你为什么要扔掉 MSB?您正在将每个 rune 转换为 byte,这是无符号的(实际上是 uint8 的别名),因此该位包含您正在丢失的信息。

您可以考虑改用这样的函数:

func ascii(s string) []byte {
    var ret []byte
    for _, r := range s {
        ret = append(ret, byte(r))
    }
    return ret
}

因为 runeint32 的别名,转换为 byte 只是删除前 24 位,这就是你想要的。

(注意:这假设小端。)

我的代码有两个问题。主要问题是我将 requestURL.String() 用于 callbackURL

comments上面http.Request.URL

For most requests, fields other than Path and RawQuery will be empty.

原来 requestURL.String() 只给出了 [Scheme]://[Host][Path][Path] 部分。正确的 callbackURL 是

callbackURL := "https://" + request.Host + request.URL.String()

第二个问题已在中指出,其中对于正文中包含第 8 位字符的任何请求,验证都会失败。