1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
|
package gitalyauth
import (
"context"
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"encoding/hex"
"strconv"
"strings"
"time"
grpc_auth "github.com/grpc-ecosystem/go-grpc-middleware/auth"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
const (
timestampThreshold = 30 * time.Second
)
var (
errUnauthenticated = status.Errorf(codes.Unauthenticated, "authentication required")
errDenied = status.Errorf(codes.PermissionDenied, "permission denied")
)
// AuthInfo contains the authentication information coming from a request
type AuthInfo struct {
Version string
SignedMessage []byte
Message string
}
// CheckToken checks the 'authentication' header of incoming gRPC
// metadata in ctx. It returns nil if and only if the token matches
// secret.
func CheckToken(ctx context.Context, secret string, targetTime time.Time) error {
if len(secret) == 0 {
panic("CheckToken: secret may not be empty")
}
authInfo, err := ExtractAuthInfo(ctx)
if err != nil {
return errUnauthenticated
}
switch authInfo.Version {
case "v1":
decodedToken, err := base64.StdEncoding.DecodeString(authInfo.Message)
if err != nil {
return errUnauthenticated
}
if tokensEqual(decodedToken, []byte(secret)) {
return nil
}
case "v2":
if hmacInfoValid(authInfo.Message, authInfo.SignedMessage, []byte(secret), targetTime, timestampThreshold) {
return nil
}
}
return errDenied
}
func tokensEqual(tok1, tok2 []byte) bool {
return subtle.ConstantTimeCompare(tok1, tok2) == 1
}
// ExtractAuthInfo returns an `AuthInfo` with the data extracted from `ctx`
func ExtractAuthInfo(ctx context.Context) (*AuthInfo, error) {
token, err := grpc_auth.AuthFromMD(ctx, "bearer")
if err != nil {
return nil, err
}
split := strings.SplitN(string(token), ".", 3)
// v1 is base64-encoded using base64.StdEncoding, which cannot contain a ".".
// A v1 token cannot slip through here.
if len(split) != 3 {
return &AuthInfo{Version: "v1", Message: token}, nil
}
version, sig, msg := split[0], split[1], split[2]
decodedSig, err := hex.DecodeString(sig)
if err != nil {
return nil, err
}
return &AuthInfo{Version: version, SignedMessage: decodedSig, Message: msg}, nil
}
func hmacInfoValid(message string, signedMessage, secret []byte, targetTime time.Time, timestampThreshold time.Duration) bool {
expectedHMAC := hmacSign(secret, message)
if !hmac.Equal(signedMessage, expectedHMAC) {
return false
}
timestamp, err := strconv.ParseInt(message, 10, 64)
if err != nil {
return false
}
issuedAt := time.Unix(timestamp, 0)
lowerBound := targetTime.Add(-timestampThreshold)
upperBound := targetTime.Add(timestampThreshold)
return issuedAt.After(lowerBound) && issuedAt.Before(upperBound)
}
func hmacSign(secret []byte, message string) []byte {
mac := hmac.New(sha256.New, secret)
mac.Write([]byte(message))
return mac.Sum(nil)
}
|