diff options
author | Alejandro Rodríguez <alejorro70@gmail.com> | 2018-09-06 23:02:27 +0300 |
---|---|---|
committer | Zeger-Jan van de Weg <zegerjan@gitlab.com> | 2018-09-06 23:02:27 +0300 |
commit | c75e0ede557cb79f0e668a6750081bbb4fa1fc15 (patch) | |
tree | 2702a0a94edf547a08ecaf6cabe2c63edcf938e4 /auth | |
parent | d0950eb3f98d342fd17da7ec4dffbcb0fc82027e (diff) |
Allow server to receive an hmac token with the client timestamp for auth
Diffstat (limited to 'auth')
-rw-r--r-- | auth/extract_test.go | 68 | ||||
-rw-r--r-- | auth/rpccredentials.go | 31 | ||||
-rw-r--r-- | auth/token.go | 92 |
3 files changed, 180 insertions, 11 deletions
diff --git a/auth/extract_test.go b/auth/extract_test.go index f2cc3f773..7d6df8fb0 100644 --- a/auth/extract_test.go +++ b/auth/extract_test.go @@ -2,7 +2,9 @@ package gitalyauth import ( "testing" + "time" + "github.com/grpc-ecosystem/go-grpc-middleware/util/metautils" "github.com/stretchr/testify/require" "golang.org/x/net/context" "google.golang.org/grpc/codes" @@ -11,7 +13,7 @@ import ( "google.golang.org/grpc/status" ) -func TestCheckToken(t *testing.T) { +func TestCheckTokenV1(t *testing.T) { secret := "secret 1234" testCases := []struct { @@ -44,13 +46,75 @@ func TestCheckToken(t *testing.T) { for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { ctx := metadata.NewIncomingContext(context.Background(), tc.md) - err := CheckToken(ctx, secret) + err := CheckToken(ctx, secret, time.Now()) require.Equal(t, tc.code, status.Code(err), "expected grpc code in error %v", err) }) } } +func TestCheckTokenV2(t *testing.T) { + targetTime := time.Unix(1535671600, 0) + secret := []byte("foo") + + testCases := []struct { + desc string + token string + result error + }{ + { + desc: "Valid v2 secret, future time within threshold", + token: "v2.3346cb25ecdb928defd368e7390522a86764bbdf1e8b21aaef27c4c23ec9c899.1535671615", + result: nil, + }, + { + desc: "Valid v2 secret, past time within threshold", + token: "v2.b77158328e80be2984eaf08788419d25f3484eae484aec1297af6bdf1a456610.1535671585", + result: nil, + }, + { + desc: "Invalid secret, time within threshold", + token: "v2.52a3b9016f46853c225c72b87617ac27109bba8a3068002069ab90e28253a911.1535671585", + result: errDenied, + }, + { + desc: "Valid secret, time too much in the future", + token: "v2.ab9e7315aeecf6815fc0df585370157814131acab376f41797ad4ebc4d9a823c.1535671631", + result: errDenied, + }, + { + desc: "Valid secret, time too much in the past", + token: "v2.f805bc69ca3aedd99e814b3fb1fc1e6a1094191691480b168a20fad7c2d24557.1535671569", + result: errDenied, + }, + { + desc: "Mismatching signed and clear message", + token: "v2.319b96a3194c1cb2a2e6f1386161aca1c4cda13257fa9df8a328ab6769649bb0.1535671599", + result: errDenied, + }, + { + desc: "Invalid version", + token: "v3.6fec98e8fe494284ce545c4b421799f02b9718b0eadfc3772d027e1ac5d6d569.1535671601", + result: errDenied, + }, + { + desc: "Empty token", + token: "", + result: errDenied, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + md := metautils.NiceMD{} + md.Set("authorization", "Bearer "+tc.token) + result := CheckToken(md.ToIncoming(context.Background()), string(secret), targetTime) + + require.Equal(t, tc.result, result) + }) + } +} + func credsMD(t *testing.T, creds credentials.PerRPCCredentials) metadata.MD { md, err := creds.GetRequestMetadata(context.Background()) require.NoError(t, err) diff --git a/auth/rpccredentials.go b/auth/rpccredentials.go index cbe94c253..c35cd4d61 100644 --- a/auth/rpccredentials.go +++ b/auth/rpccredentials.go @@ -2,6 +2,9 @@ package gitalyauth import ( "encoding/base64" + "fmt" + "strconv" + "time" "golang.org/x/net/context" "google.golang.org/grpc/credentials" @@ -23,3 +26,31 @@ func (*rpcCredentials) RequireTransportSecurity() bool { return false } func (rc *rpcCredentials) GetRequestMetadata(context.Context, ...string) (map[string]string, error) { return map[string]string{"authorization": "Bearer " + rc.token}, nil } + +// RPCCredentialsV2 can be used with grpc.WithPerRPCCredentials to create a +// grpc.DialOption that inserts an HMAC token with the current timestamp +// for authentication with a Gitaly server. +func RPCCredentialsV2(token string) credentials.PerRPCCredentials { + return &rpcCredentialsV2{token: token} +} + +type rpcCredentialsV2 struct { + token string +} + +func (*rpcCredentialsV2) RequireTransportSecurity() bool { return false } + +func (rc *rpcCredentialsV2) GetRequestMetadata(context.Context, ...string) (map[string]string, error) { + return map[string]string{"authorization": "Bearer " + rc.hmacToken()}, nil +} + +func (rc *rpcCredentialsV2) hmacToken() string { + return hmacToken("v2", []byte(rc.token), time.Now()) +} + +func hmacToken(version string, secret []byte, timestamp time.Time) string { + intTime := timestamp.Unix() + signedTimestamp := hmacSign(secret, strconv.FormatInt(intTime, 10)) + + return fmt.Sprintf("%s.%x.%d", version, signedTimestamp, intTime) +} diff --git a/auth/token.go b/auth/token.go index fc13c5bee..e1c196615 100644 --- a/auth/token.go +++ b/auth/token.go @@ -1,8 +1,14 @@ package gitalyauth import ( + "crypto/hmac" + "crypto/sha256" "crypto/subtle" "encoding/base64" + "encoding/hex" + "strconv" + "strings" + "time" "github.com/grpc-ecosystem/go-grpc-middleware/auth" "golang.org/x/net/context" @@ -10,36 +16,104 @@ import ( "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) error { +func CheckToken(ctx context.Context, secret string, targetTime time.Time) error { if len(secret) == 0 { panic("CheckToken: secret may not be empty") } - encodedToken, err := grpc_auth.AuthFromMD(ctx, "bearer") + authInfo, err := ExtractAuthInfo(ctx) if err != nil { return errUnauthenticated } - token, err := base64.StdEncoding.DecodeString(encodedToken) - if err != nil { - return errUnauthenticated - } + switch authInfo.Version { + case "v1": + decodedToken, err := base64.StdEncoding.DecodeString(authInfo.Message) + if err != nil { + return errUnauthenticated + } - if !tokensEqual(token, []byte(secret)) { - return errDenied + if tokensEqual(decodedToken, []byte(secret)) { + return nil + } + case "v2": + if hmacInfoValid(authInfo.Message, authInfo.SignedMessage, []byte(secret), targetTime, timestampThreshold) { + return nil + } } - 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) +} |