Skip to content

Commit 1379914

Browse files
Gustedearl-warren
authored andcommitted
Improve usage of HMAC output for mailer tokens
- If the incoming mail feature is enabled, tokens are being sent with outgoing mails. These tokens contains information about what type of action is allow with such token (such as replying to a certain issue ID), to verify these tokens the code uses the HMAC-SHA256 construction. - The output of the HMAC is truncated to 80 bits, because this is recommended by RFC2104, but RFC2104 actually doesn't recommend this. It recommends, if truncation should need to take place, it should use max(80, hash_len/2) of the leftmost bits. For HMAC-SHA256 this works out to 128 bits instead of the currently used 80 bits. - Update to token version 2 and disallow any usage of token version 1, token version 2 are generated with 128 bits of HMAC output. - Add test to verify the deprecation of token version 1 and a general MAC check test. (cherry picked from commit 9508aa7)
1 parent 254bded commit 1379914

File tree

2 files changed

+59
-3
lines changed

2 files changed

+59
-3
lines changed

services/mailer/token/token.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,16 @@ import (
2222
//
2323
// The payload is verifiable by the generated HMAC using the user secret. It contains:
2424
// | Timestamp | Action/Handler Type | Action/Handler Data |
25+
//
26+
//
27+
// Version changelog
28+
//
29+
// v1 -> v2:
30+
// Use 128 instead of 80 bits of the HMAC-SHA256 output.
2531

2632
const (
2733
tokenVersion1 byte = 1
34+
tokenVersion2 byte = 2
2835
tokenLifetimeInYears int = 1
2936
)
3037

@@ -70,7 +77,7 @@ func CreateToken(ht HandlerType, user *user_model.User, data []byte) (string, er
7077
return "", err
7178
}
7279

73-
return encodingWithoutPadding.EncodeToString(append([]byte{tokenVersion1}, packagedData...)), nil
80+
return encodingWithoutPadding.EncodeToString(append([]byte{tokenVersion2}, packagedData...)), nil
7481
}
7582

7683
// ExtractToken extracts the action/user tuple from the token and verifies the content
@@ -84,7 +91,7 @@ func ExtractToken(ctx context.Context, token string) (HandlerType, *user_model.U
8491
return UnknownHandlerType, nil, nil, &ErrToken{"no data"}
8592
}
8693

87-
if data[0] != tokenVersion1 {
94+
if data[0] != tokenVersion2 {
8895
return UnknownHandlerType, nil, nil, &ErrToken{fmt.Sprintf("unsupported token version: %v", data[0])}
8996
}
9097

@@ -124,5 +131,8 @@ func generateHmac(secret, payload []byte) []byte {
124131
mac.Write(payload)
125132
hmac := mac.Sum(nil)
126133

127-
return hmac[:10] // RFC2104 recommends not using less then 80 bits
134+
// RFC2104 section 5 recommends that if you do HMAC truncation, you should use
135+
// the max(80, hash_len/2) of the leftmost bits.
136+
// For SHA256 this works out to using 128 of the leftmost bits.
137+
return hmac[:16]
128138
}

tests/integration/incoming_email_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package integration
55

66
import (
7+
"encoding/base32"
78
"io"
89
"net"
910
"net/smtp"
@@ -75,6 +76,51 @@ func TestIncomingEmail(t *testing.T) {
7576
assert.Equal(t, payload, p)
7677
})
7778

79+
tokenEncoding := base32.StdEncoding.WithPadding(base32.NoPadding)
80+
t.Run("Deprecated token version", func(t *testing.T) {
81+
defer tests.PrintCurrentTest(t)()
82+
83+
payload := []byte{1, 2, 3, 4, 5}
84+
85+
token, err := token_service.CreateToken(token_service.ReplyHandlerType, user, payload)
86+
require.NoError(t, err)
87+
assert.NotEmpty(t, token)
88+
89+
// Set the token to version 1.
90+
unencodedToken, err := tokenEncoding.DecodeString(token)
91+
require.NoError(t, err)
92+
unencodedToken[0] = 1
93+
token = tokenEncoding.EncodeToString(unencodedToken)
94+
95+
ht, u, p, err := token_service.ExtractToken(db.DefaultContext, token)
96+
require.ErrorContains(t, err, "unsupported token version: 1")
97+
assert.Equal(t, token_service.UnknownHandlerType, ht)
98+
assert.Nil(t, u)
99+
assert.Nil(t, p)
100+
})
101+
102+
t.Run("MAC check", func(t *testing.T) {
103+
defer tests.PrintCurrentTest(t)()
104+
105+
payload := []byte{1, 2, 3, 4, 5}
106+
107+
token, err := token_service.CreateToken(token_service.ReplyHandlerType, user, payload)
108+
require.NoError(t, err)
109+
assert.NotEmpty(t, token)
110+
111+
// Modify the MAC.
112+
unencodedToken, err := tokenEncoding.DecodeString(token)
113+
require.NoError(t, err)
114+
unencodedToken[len(unencodedToken)-1] ^= 0x01
115+
token = tokenEncoding.EncodeToString(unencodedToken)
116+
117+
ht, u, p, err := token_service.ExtractToken(db.DefaultContext, token)
118+
require.ErrorContains(t, err, "verification failed")
119+
assert.Equal(t, token_service.UnknownHandlerType, ht)
120+
assert.Nil(t, u)
121+
assert.Nil(t, p)
122+
})
123+
78124
t.Run("Handler", func(t *testing.T) {
79125
t.Run("Reply", func(t *testing.T) {
80126
checkReply := func(t *testing.T, payload []byte, issue *issues_model.Issue, commentType issues_model.CommentType) {

0 commit comments

Comments
 (0)