Skip to content

Commit e72a7d2

Browse files
authored
Merge pull request #355 from wyattjoh/master
Multiple Signature Support
2 parents 8fe6c9a + 3f5fee2 commit e72a7d2

File tree

3 files changed

+96
-36
lines changed

3 files changed

+96
-36
lines changed

docs/Hook-Rules.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,13 @@ For the regex syntax, check out <http://golang.org/pkg/regexp/syntax/>
186186
}
187187
```
188188

189+
Note that if multiple signatures were passed via a comma separated string, each
190+
will be tried unless a match is found. For example:
191+
192+
```
193+
X-Hub-Signature: sha1=the-first-signature,sha1=the-second-signature
194+
```
195+
189196
### 4. Match payload-hash-sha256
190197
```json
191198
{
@@ -202,6 +209,13 @@ For the regex syntax, check out <http://golang.org/pkg/regexp/syntax/>
202209
}
203210
```
204211

212+
Note that if multiple signatures were passed via a comma separated string, each
213+
will be tried unless a match is found. For example:
214+
215+
```
216+
X-Hub-Signature: sha256=the-first-signature,sha256=the-second-signature
217+
```
218+
205219
### 5. Match payload-hash-sha512
206220
```json
207221
{
@@ -218,6 +232,13 @@ For the regex syntax, check out <http://golang.org/pkg/regexp/syntax/>
218232
}
219233
```
220234

235+
Note that if multiple signatures were passed via a comma separated string, each
236+
will be tried unless a match is found. For example:
237+
238+
```
239+
X-Hub-Signature: sha512=the-first-signature,sha512=the-second-signature
240+
```
241+
221242
### 6. Match Whitelisted IP range
222243

223244
The IP can be IPv4- or IPv6-formatted, using [CIDR notation](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing#CIDR_blocks). To match a single IP address only, use `/32`.

hook/hook.go

Lines changed: 69 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"encoding/json"
1313
"errors"
1414
"fmt"
15+
"hash"
1516
"io/ioutil"
1617
"log"
1718
"math"
@@ -48,13 +49,19 @@ const (
4849

4950
// SignatureError describes an invalid payload signature passed to Hook.
5051
type SignatureError struct {
51-
Signature string
52+
Signature string
53+
Signatures []string
5254
}
5355

5456
func (e *SignatureError) Error() string {
5557
if e == nil {
5658
return "<nil>"
5759
}
60+
61+
if e.Signatures != nil {
62+
return fmt.Sprintf("invalid payload signatures %s", e.Signatures)
63+
}
64+
5865
return fmt.Sprintf("invalid payload signature %s", e.Signature)
5966
}
6067

@@ -94,46 +101,80 @@ func (e *ParseError) Error() string {
94101
return e.Err.Error()
95102
}
96103

97-
// CheckPayloadSignature calculates and verifies SHA1 signature of the given payload
98-
func CheckPayloadSignature(payload []byte, secret string, signature string) (string, error) {
99-
if secret == "" {
100-
return "", errors.New("signature validation secret can not be empty")
104+
// ExtractCommaSeparatedValues will extract the values matching the key.
105+
func ExtractCommaSeparatedValues(source, prefix string) []string {
106+
parts := strings.Split(source, ",")
107+
values := make([]string, 0)
108+
for _, part := range parts {
109+
if strings.HasPrefix(part, prefix) {
110+
values = append(values, strings.TrimPrefix(part, prefix))
111+
}
101112
}
102113

103-
signature = strings.TrimPrefix(signature, "sha1=")
114+
return values
115+
}
116+
117+
// ExtractSignatures will extract all the signatures from the source.
118+
func ExtractSignatures(source, prefix string) []string {
119+
// If there are multiple possible matches, let the comma seperated extractor
120+
// do it's work.
121+
if strings.Contains(source, ",") {
122+
return ExtractCommaSeparatedValues(source, prefix)
123+
}
104124

105-
mac := hmac.New(sha1.New, []byte(secret))
125+
// There were no commas, so just trim the prefix (if it even exists) and
126+
// pass it back.
127+
return []string{
128+
strings.TrimPrefix(source, prefix),
129+
}
130+
}
131+
132+
// ValidateMAC will verify that the expected mac for the given hash will match
133+
// the one provided.
134+
func ValidateMAC(payload []byte, mac hash.Hash, signatures []string) (string, error) {
135+
// Write the payload to the provided hash.
106136
_, err := mac.Write(payload)
107137
if err != nil {
108138
return "", err
109139
}
140+
110141
expectedMAC := hex.EncodeToString(mac.Sum(nil))
111142

112-
if !hmac.Equal([]byte(signature), []byte(expectedMAC)) {
113-
return expectedMAC, &SignatureError{signature}
143+
for _, signature := range signatures {
144+
if hmac.Equal([]byte(signature), []byte(expectedMAC)) {
145+
return expectedMAC, err
146+
}
147+
}
148+
149+
return expectedMAC, &SignatureError{
150+
Signatures: signatures,
114151
}
115-
return expectedMAC, err
116152
}
117153

118-
// CheckPayloadSignature256 calculates and verifies SHA256 signature of the given payload
119-
func CheckPayloadSignature256(payload []byte, secret string, signature string) (string, error) {
154+
// CheckPayloadSignature calculates and verifies SHA1 signature of the given payload
155+
func CheckPayloadSignature(payload []byte, secret string, signature string) (string, error) {
120156
if secret == "" {
121157
return "", errors.New("signature validation secret can not be empty")
122158
}
123159

124-
signature = strings.TrimPrefix(signature, "sha256=")
160+
// Extract the signatures.
161+
signatures := ExtractSignatures(signature, "sha1=")
125162

126-
mac := hmac.New(sha256.New, []byte(secret))
127-
_, err := mac.Write(payload)
128-
if err != nil {
129-
return "", err
130-
}
131-
expectedMAC := hex.EncodeToString(mac.Sum(nil))
163+
// Validate the MAC.
164+
return ValidateMAC(payload, hmac.New(sha1.New, []byte(secret)), signatures)
165+
}
132166

133-
if !hmac.Equal([]byte(signature), []byte(expectedMAC)) {
134-
return expectedMAC, &SignatureError{signature}
167+
// CheckPayloadSignature256 calculates and verifies SHA256 signature of the given payload
168+
func CheckPayloadSignature256(payload []byte, secret string, signature string) (string, error) {
169+
if secret == "" {
170+
return "", errors.New("signature validation secret can not be empty")
135171
}
136-
return expectedMAC, err
172+
173+
// Extract the signatures.
174+
signatures := ExtractSignatures(signature, "sha256=")
175+
176+
// Validate the MAC.
177+
return ValidateMAC(payload, hmac.New(sha256.New, []byte(secret)), signatures)
137178
}
138179

139180
// CheckPayloadSignature512 calculates and verifies SHA512 signature of the given payload
@@ -142,19 +183,11 @@ func CheckPayloadSignature512(payload []byte, secret string, signature string) (
142183
return "", errors.New("signature validation secret can not be empty")
143184
}
144185

145-
signature = strings.TrimPrefix(signature, "sha512=")
146-
147-
mac := hmac.New(sha512.New, []byte(secret))
148-
_, err := mac.Write(payload)
149-
if err != nil {
150-
return "", err
151-
}
152-
expectedMAC := hex.EncodeToString(mac.Sum(nil))
186+
// Extract the signatures.
187+
signatures := ExtractSignatures(signature, "sha512=")
153188

154-
if !hmac.Equal([]byte(signature), []byte(expectedMAC)) {
155-
return expectedMAC, &SignatureError{signature}
156-
}
157-
return expectedMAC, err
189+
// Validate the MAC.
190+
return ValidateMAC(payload, hmac.New(sha512.New, []byte(secret)), signatures)
158191
}
159192

160193
func CheckScalrSignature(headers map[string]interface{}, body []byte, signingKey string, checkDate bool) (bool, error) {
@@ -177,7 +210,7 @@ func CheckScalrSignature(headers map[string]interface{}, body []byte, signingKey
177210
expectedSignature := hex.EncodeToString(mac.Sum(nil))
178211

179212
if !hmac.Equal([]byte(providedSignature), []byte(expectedSignature)) {
180-
return false, &SignatureError{providedSignature}
213+
return false, &SignatureError{Signature: providedSignature}
181214
}
182215

183216
if !checkDate {
@@ -192,7 +225,7 @@ func CheckScalrSignature(headers map[string]interface{}, body []byte, signingKey
192225
delta := math.Abs(now.Sub(date).Seconds())
193226

194227
if delta > 300 {
195-
return false, &SignatureError{"outdated"}
228+
return false, &SignatureError{Signature: "outdated"}
196229
}
197230
return true, nil
198231
}

hook/hook_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,11 @@ var checkPayloadSignatureTests = []struct {
4848
}{
4949
{[]byte(`{"a": "z"}`), "secret", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", true},
5050
{[]byte(`{"a": "z"}`), "secret", "sha1=b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", true},
51+
{[]byte(`{"a": "z"}`), "secret", "sha1=XXXe04cbb22afa8ffbff8796fc1894ed27badd9e,sha1=b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", true},
5152
// failures
5253
{[]byte(`{"a": "z"}`), "secret", "XXXe04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", false},
54+
{[]byte(`{"a": "z"}`), "secret", "sha1=XXXe04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", false},
55+
{[]byte(`{"a": "z"}`), "secret", "sha1=XXXe04cbb22afa8ffbff8796fc1894ed27badd9e,sha1=XXXe04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", false},
5356
{[]byte(`{"a": "z"}`), "secreX", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "900225703e9342328db7307692736e2f7cc7b36e", false},
5457
{[]byte(`{"a": "z"}`), "", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "", false},
5558
}
@@ -76,8 +79,11 @@ var checkPayloadSignature256Tests = []struct {
7679
}{
7780
{[]byte(`{"a": "z"}`), "secret", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", true},
7881
{[]byte(`{"a": "z"}`), "secret", "sha256=f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", true},
82+
{[]byte(`{"a": "z"}`), "secret", "sha256=XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89,sha256=f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", true},
7983
// failures
8084
{[]byte(`{"a": "z"}`), "secret", "XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", false},
85+
{[]byte(`{"a": "z"}`), "secret", "sha256=XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", false},
86+
{[]byte(`{"a": "z"}`), "secret", "sha256=XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89,sha256=XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", false},
8187
{[]byte(`{"a": "z"}`), "", "XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "", false},
8288
}
8389

0 commit comments

Comments
 (0)