验证 Trello Webhook 签名

huangapple go评论75阅读模式
英文:

Verifying Trello Webhook signature

问题

我遇到了一个问题,无法成功验证来自Trello的Webhook请求。以下是我所了解的情况。

Trello的Webhook文档在这里中提到:

每个Webhook触发器都包含HTTP头X-Trello-Webhook。该头部是一个HMAC-SHA1哈希的base64摘要。哈希内容是完整请求体和回调URL的连接,与Webhook创建时提供的完全相同。用于对此文本进行签名的密钥是您的应用程序的密钥。

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

由于节点中的加密工具的某些默认设置,我们签名的有效负载被视为二进制字符串,而不是UTF-8。例如,如果您将en-dash字符(U+2013或8211的十进制表示)在Node中创建为二进制缓冲区,它将显示为一个由[19]组成的缓冲区,这是8211的8个最低有效位。这就是在计算SHA-1时使用的值。

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

我用来适应Trello节点有效负载问题的函数是:

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

    // 对字符串中的每个符文
    for _, c := range s {

        // 创建一个字节切片
        b := []byte(string(c))

        // 取出最低有效字节的符号
        tmp := b[len(b)-1] << 1
        tmp = tmp >> 1

        // 将其附加到字节字符串中
        byteString = append(byteString, tmp)
    }
    return byteString
}

我也很可能在基本验证步骤中做错了什么。在我看来,它看起来还不错,尽管我对此还有些陌生。

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

    // 将callbackURL和body放入字节切片中
    urlBytes := bitShift(requestURL.String())
    bitBody := bitShift(string(body))

    // 对有效负载进行签名、哈希和编码
    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
}

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

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

英文:

<!-- language-all: lang-go -->

I am having trouble successfully verifying a webhook request from Trello. Here's what I know.

Trello's webhook documentation here states:

> 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.

Which is understandable. They go on to say

> 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.

This is less clear to me. My understanding is that each character of the payload (body + callbackURL) has been put into an 8-bit integer, with the overflow ignored. (Because 8211 == 0b10000000010011, and 0b00010011 == 19) This is where I think my problem is.

The function I am using to accommodate Trello's node payload issue is:

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] &lt;&lt; 1
	    tmp = tmp &gt;&gt; 1

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

It is also very possible that I am doing something wrong with the basic verification step. It looks okay to me, though I am somewhat new to this.

// 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
}

Let me know if you need any more information. Thank you!

Update: This is solved, check out the answers.

答案1

得分: 2

为什么要丢弃最高有效位(MSB)?你将每个rune转换为byte,而byte是无符号的(实际上是uint8的别名),所以最高有效位包含了你丢失的信息。

你可以考虑使用下面这样的函数代替:

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

由于runeint32的别名,将其转换为byte只会丢弃掉最高的24位,这正是你想要的。

(注意:这假设是小端序的情况。)

英文:

Why are you throwing away the MSB? You're converting each rune to byte, which is signless (and actually an alias for uint8), so that bit holds information that you're losing.

You might consider using a function like this instead:
<!-- language: lang-go -->

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

Since rune is an alias for int32, the cast to byte just drops the top 24 bits, which is what you want.

(Caveat: this assumes little-endianness.)

答案2

得分: 1

我的翻译如下:

我的代码有两个问题。主要问题是我在callbackURL中使用了requestURL.String()

注释中提到http.Request.URL
> 对于大多数请求,除了Path和RawQuery之外的字段都将为空。

事实证明,requestURL.String()只返回了[Scheme]://[Host][Path]中的[Path]部分。正确的callbackURL应该是:

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

第二个问题是在这个答案中指出的,如果请求的正文包含有第8位的字符,验证将会失败。

英文:

There were two problems with my code. The major issue was my use of requestURL.String() for the callbackURL.

In the comments above http.Request.URL:
> For most requests, fields other than Path and RawQuery will be empty.

It turned out that requestURL.String() was only giving the [Path] portion of [Scheme]://[Host][Path]. The correct callbackURL is

callbackURL := &quot;https://&quot; + request.Host + request.URL.String()

The second problem was pointed out in this answer, where the verification would have failed for any request whose body contained 8th-bit-having characters.

huangapple
  • 本文由 发表于 2015年12月11日 03:10:27
  • 转载请务必保留本文链接:https://go.coder-hub.com/34209823.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定