[security] rpc: Do not transfer access/secret key. (#4857)

This is an improvement upon existing implementation
by avoiding transfer of access and secret keys over
the network. This change only exchanges JWT tokens
generated by an rpc client. Even if the JWT can be
traced over the network on a non-TLS connection, this
change makes sure that we never really expose the
secret key over the network.
This commit is contained in:
Harshavardhana 2017-09-19 12:37:56 -07:00 committed by Dee Koder
parent f680b8482f
commit f8024cadbb
14 changed files with 184 additions and 141 deletions

View file

@ -33,16 +33,19 @@ func testAdminCmd(cmd cmdType, t *testing.T) {
}
defer os.RemoveAll(rootPath)
adminServer := adminCmd{}
creds := serverConfig.GetCredential()
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
if err != nil {
t.Fatal(err)
}
adminServer := adminCmd{}
args := LoginRPCArgs{
Username: creds.AccessKey,
Password: creds.SecretKey,
AuthToken: token,
Version: Version,
RequestTime: UTCNow(),
}
reply := LoginRPCReply{}
err = adminServer.Login(&args, &reply)
err = adminServer.Login(&args, &LoginRPCReply{})
if err != nil {
t.Fatalf("Failed to login to admin server - %v", err)
}
@ -52,7 +55,7 @@ func testAdminCmd(cmd cmdType, t *testing.T) {
<-globalServiceSignalCh
}()
ga := AuthRPCArgs{AuthToken: reply.AuthToken}
ga := AuthRPCArgs{AuthToken: token}
genReply := AuthRPCReply{}
switch cmd {
case restartCmd:
@ -91,21 +94,25 @@ func TestReInitDisks(t *testing.T) {
// Setup admin rpc server for an XL backend.
globalIsXL = true
adminServer := adminCmd{}
creds := serverConfig.GetCredential()
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
if err != nil {
t.Fatal(err)
}
args := LoginRPCArgs{
Username: creds.AccessKey,
Password: creds.SecretKey,
AuthToken: token,
Version: Version,
RequestTime: UTCNow(),
}
reply := LoginRPCReply{}
err = adminServer.Login(&args, &reply)
err = adminServer.Login(&args, &LoginRPCReply{})
if err != nil {
t.Fatalf("Failed to login to admin server - %v", err)
}
authArgs := AuthRPCArgs{
AuthToken: reply.AuthToken,
AuthToken: token,
}
authReply := AuthRPCReply{}
@ -114,12 +121,15 @@ func TestReInitDisks(t *testing.T) {
t.Errorf("Expected to pass, but failed with %v", err)
}
token, err = authenticateNode(creds.AccessKey, creds.SecretKey)
if err != nil {
t.Fatal(err)
}
// Negative test case with admin rpc server setup for FS.
globalIsXL = false
fsAdminServer := adminCmd{}
fsArgs := LoginRPCArgs{
Username: creds.AccessKey,
Password: creds.SecretKey,
AuthToken: token,
Version: Version,
RequestTime: UTCNow(),
}
@ -130,7 +140,7 @@ func TestReInitDisks(t *testing.T) {
}
authArgs = AuthRPCArgs{
AuthToken: fsReply.AuthToken,
AuthToken: token,
}
authReply = AuthRPCReply{}
// Attempt ReInitDisks service on a FS backend.
@ -154,9 +164,14 @@ func TestGetConfig(t *testing.T) {
adminServer := adminCmd{}
creds := serverConfig.GetCredential()
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
if err != nil {
t.Fatal(err)
}
args := LoginRPCArgs{
Username: creds.AccessKey,
Password: creds.SecretKey,
AuthToken: token,
Version: Version,
RequestTime: UTCNow(),
}
@ -167,7 +182,7 @@ func TestGetConfig(t *testing.T) {
}
authArgs := AuthRPCArgs{
AuthToken: reply.AuthToken,
AuthToken: token,
}
configReply := ConfigReply{}
@ -198,9 +213,12 @@ func TestWriteAndCommitConfig(t *testing.T) {
adminServer := adminCmd{}
creds := serverConfig.GetCredential()
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
if err != nil {
t.Fatal(err)
}
args := LoginRPCArgs{
Username: creds.AccessKey,
Password: creds.SecretKey,
AuthToken: token,
Version: Version,
RequestTime: UTCNow(),
}
@ -215,7 +233,7 @@ func TestWriteAndCommitConfig(t *testing.T) {
tmpFileName := mustGetUUID()
wArgs := WriteConfigArgs{
AuthRPCArgs: AuthRPCArgs{
AuthToken: reply.AuthToken,
AuthToken: token,
},
TmpFileName: tmpFileName,
Buf: buf,
@ -232,7 +250,7 @@ func TestWriteAndCommitConfig(t *testing.T) {
cArgs := CommitConfigArgs{
AuthRPCArgs: AuthRPCArgs{
AuthToken: reply.AuthToken,
AuthToken: token,
},
FileName: tmpFileName,
}

View file

@ -99,21 +99,22 @@ func (authClient *AuthRPCClient) Login() (err error) {
// Attempt to login if not logged in already.
if authClient.authToken == "" {
// Login to authenticate and acquire a new auth token.
authClient.authToken, err = authenticateNode(authClient.config.accessKey, authClient.config.secretKey)
if err != nil {
return err
}
// Login to authenticate your token.
var (
loginMethod = authClient.config.serviceName + loginMethodName
loginArgs = LoginRPCArgs{
Username: authClient.config.accessKey,
Password: authClient.config.secretKey,
AuthToken: authClient.authToken,
Version: Version,
RequestTime: UTCNow(),
}
loginReply = LoginRPCReply{}
)
if err = authClient.rpcClient.Call(loginMethod, &loginArgs, &loginReply); err != nil {
if err = authClient.rpcClient.Call(loginMethod, &loginArgs, &LoginRPCReply{}); err != nil {
return err
}
authClient.authToken = loginReply.AuthToken
}
return nil
}

View file

@ -20,8 +20,7 @@ package cmd
const loginMethodName = ".Login"
// AuthRPCServer RPC server authenticates using JWT.
type AuthRPCServer struct {
}
type AuthRPCServer struct{}
// Login - Handles JWT based RPC login.
func (b AuthRPCServer) Login(args *LoginRPCArgs, reply *LoginRPCReply) error {
@ -30,14 +29,10 @@ func (b AuthRPCServer) Login(args *LoginRPCArgs, reply *LoginRPCReply) error {
return err
}
// Authenticate using JWT.
token, err := authenticateNode(args.Username, args.Password)
if err != nil {
return err
// Return an error if token is not valid.
if !isAuthTokenValid(args.AuthToken) {
return errAuthentication
}
// Return the token.
reply.AuthToken = token
return nil
}

View file

@ -29,6 +29,10 @@ func TestLogin(t *testing.T) {
}
defer os.RemoveAll(rootPath)
creds := serverConfig.GetCredential()
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
if err != nil {
t.Fatal(err)
}
ls := AuthRPCServer{}
testCases := []struct {
args LoginRPCArgs
@ -38,9 +42,8 @@ func TestLogin(t *testing.T) {
// Valid case.
{
args: LoginRPCArgs{
Username: creds.AccessKey,
Password: creds.SecretKey,
Version: Version,
AuthToken: token,
Version: Version,
},
skewTime: 0,
expectedErr: nil,
@ -48,9 +51,8 @@ func TestLogin(t *testing.T) {
// Valid username, password and request time, not version.
{
args: LoginRPCArgs{
Username: creds.AccessKey,
Password: creds.SecretKey,
Version: "INVALID-" + Version,
AuthToken: token,
Version: "INVALID-" + Version,
},
skewTime: 0,
expectedErr: errServerVersionMismatch,
@ -58,49 +60,17 @@ func TestLogin(t *testing.T) {
// Valid username, password and version, not request time
{
args: LoginRPCArgs{
Username: creds.AccessKey,
Password: creds.SecretKey,
Version: Version,
AuthToken: token,
Version: Version,
},
skewTime: 20 * time.Minute,
expectedErr: errServerTimeMismatch,
},
// Invalid username length
// Invalid token, fails with authentication error
{
args: LoginRPCArgs{
Username: "aaa",
Password: "minio123",
Version: Version,
},
skewTime: 0,
expectedErr: errInvalidAccessKeyLength,
},
// Invalid password length
{
args: LoginRPCArgs{
Username: "minio",
Password: "aaa",
Version: Version,
},
skewTime: 0,
expectedErr: errInvalidSecretKeyLength,
},
// Invalid username
{
args: LoginRPCArgs{
Username: "aaaaa",
Password: creds.SecretKey,
Version: Version,
},
skewTime: 0,
expectedErr: errInvalidAccessKeyID,
},
// Invalid password
{
args: LoginRPCArgs{
Username: creds.AccessKey,
Password: "aaaaaaaa",
Version: Version,
AuthToken: "",
Version: Version,
},
skewTime: 0,
expectedErr: errAuthentication,
@ -108,7 +78,7 @@ func TestLogin(t *testing.T) {
}
for i, test := range testCases {
reply := LoginRPCReply{}
test.args.RequestTime = time.Now().Add(test.skewTime).UTC()
test.args.RequestTime = UTCNow().Add(test.skewTime)
err := ls.Login(&test.args, &reply)
if err != test.expectedErr {
t.Errorf("Test %d: Expected error %v but received %v",

View file

@ -23,26 +23,6 @@ import (
"time"
)
// Login handler implements JWT login token generator, which upon login request
// along with username and password is generated.
func (br *browserPeerAPIHandlers) Login(args *LoginRPCArgs, reply *LoginRPCReply) error {
// Validate LoginRPCArgs
if err := args.IsValid(); err != nil {
return err
}
// Authenticate using JWT.
token, err := authenticateWeb(args.Username, args.Password)
if err != nil {
return err
}
// Return the token.
reply.AuthToken = token
return nil
}
// SetAuthPeerArgs - Arguments collection for SetAuth RPC call
type SetAuthPeerArgs struct {
// For Auth

View file

@ -91,9 +91,12 @@ func (s *TestRPCBrowserPeerSuite) testBrowserPeerRPC(t *testing.T) {
// Validate for failure in login handler with previous credentials.
rclient = newRPCClient(s.testAuthConf.serverAddr, s.testAuthConf.serviceEndpoint, false)
defer rclient.Close()
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
if err != nil {
t.Fatal(err)
}
rargs := &LoginRPCArgs{
Username: creds.AccessKey,
Password: creds.SecretKey,
AuthToken: token,
Version: Version,
RequestTime: UTCNow(),
}
@ -105,20 +108,18 @@ func (s *TestRPCBrowserPeerSuite) testBrowserPeerRPC(t *testing.T) {
}
}
token, err = authenticateNode(creds.AccessKey, creds.SecretKey)
if err != nil {
t.Fatal(err)
}
// Validate for success in loing handled with valid credetnails.
rargs = &LoginRPCArgs{
Username: creds.AccessKey,
Password: creds.SecretKey,
AuthToken: token,
Version: Version,
RequestTime: UTCNow(),
}
rreply = &LoginRPCReply{}
err = rclient.Call("BrowserPeer"+loginMethodName, rargs, rreply)
if err != nil {
if err = rclient.Call("BrowserPeer"+loginMethodName, rargs, rreply); err != nil {
t.Fatal(err)
}
// Validate all the replied fields after successful login.
if rreply.AuthToken == "" {
t.Fatalf("Generated token cannot be empty %s", errInvalidToken)
}
}

View file

@ -35,7 +35,7 @@ type browserPeerAPIHandlers struct {
// Register RPC router
func registerBrowserPeerRPCRouter(mux *router.Router) error {
bpHandlers := &browserPeerAPIHandlers{}
bpHandlers := &browserPeerAPIHandlers{AuthRPCServer{}}
bpRPCServer := newRPCServer()
err := bpRPCServer.RegisterName("BrowserPeer", bpHandlers)

View file

@ -63,10 +63,10 @@ func authenticateJWT(accessKey, secretKey string, expiry time.Duration) (string,
}
utcNow := UTCNow()
token := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, jwtgo.MapClaims{
"exp": utcNow.Add(expiry).Unix(),
"iat": utcNow.Unix(),
"sub": accessKey,
token := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, jwtgo.StandardClaims{
ExpiresAt: utcNow.Add(expiry).Unix(),
IssuedAt: utcNow.Unix(),
Subject: accessKey,
})
return token.SignedString([]byte(serverCred.SecretKey))
@ -93,13 +93,17 @@ func keyFuncCallback(jwtToken *jwtgo.Token) (interface{}, error) {
}
func isAuthTokenValid(tokenString string) bool {
jwtToken, err := jwtgo.Parse(tokenString, keyFuncCallback)
var claims jwtgo.StandardClaims
jwtToken, err := jwtgo.ParseWithClaims(tokenString, &claims, keyFuncCallback)
if err != nil {
errorIf(err, "Unable to parse JWT token string")
return false
}
return jwtToken.Valid
if err = claims.Valid(); err != nil {
errorIf(err, "Invalid claims in JWT token string")
return false
}
return jwtToken.Valid && claims.Subject == serverConfig.GetCredential().AccessKey
}
func isHTTPRequestValid(req *http.Request) bool {
@ -110,14 +114,20 @@ func isHTTPRequestValid(req *http.Request) bool {
// Returns nil if the request is authenticated. errNoAuthToken if token missing.
// Returns errAuthentication for all other errors.
func webRequestAuthenticate(req *http.Request) error {
jwtToken, err := jwtreq.ParseFromRequest(req, jwtreq.AuthorizationHeaderExtractor, keyFuncCallback)
var claims jwtgo.StandardClaims
jwtToken, err := jwtreq.ParseFromRequestWithClaims(req, jwtreq.AuthorizationHeaderExtractor, &claims, keyFuncCallback)
if err != nil {
if err == jwtreq.ErrNoTokenInRequest {
return errNoAuthToken
}
return errAuthentication
}
if err = claims.Valid(); err != nil {
return err
}
if claims.Subject != serverConfig.GetCredential().AccessKey {
return errInvalidAccessKeyID
}
if !jwtToken.Valid {
return errAuthentication
}

View file

@ -17,6 +17,7 @@
package cmd
import (
"net/http"
"os"
"testing"
)
@ -88,6 +89,58 @@ func TestAuthenticateURL(t *testing.T) {
testAuthenticate("url", t)
}
// Tests web request authenticator.
func TestWebRequestAuthenticate(t *testing.T) {
testPath, err := newTestConfig(globalMinioDefaultRegion)
if err != nil {
t.Fatalf("unable initialize config file, %s", err)
}
defer os.RemoveAll(testPath)
creds := serverConfig.GetCredential()
token, err := getTokenString(creds.AccessKey, creds.SecretKey)
if err != nil {
t.Fatalf("unable get token %s", err)
}
testCases := []struct {
req *http.Request
expectedErr error
}{
// Set valid authorization header.
{
req: &http.Request{
Header: http.Header{
"Authorization": []string{token},
},
},
expectedErr: nil,
},
// No authorization header.
{
req: &http.Request{
Header: http.Header{},
},
expectedErr: errNoAuthToken,
},
// Invalid authorization token.
{
req: &http.Request{
Header: http.Header{
"Authorization": []string{"invalid-token"},
},
},
expectedErr: errAuthentication,
},
}
for i, testCase := range testCases {
gotErr := webRequestAuthenticate(testCase.req)
if testCase.expectedErr != gotErr {
t.Errorf("Test %d, expected err %s, got %s", i+1, testCase.expectedErr, gotErr)
}
}
}
func BenchmarkAuthenticateNode(b *testing.B) {
testPath, err := newTestConfig(globalMinioDefaultRegion)
if err != nil {

View file

@ -58,9 +58,12 @@ func createLockTestServer(t *testing.T) (string, *lockServer, string) {
},
}
creds := serverConfig.GetCredential()
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
if err != nil {
t.Fatal(err)
}
loginArgs := LoginRPCArgs{
Username: creds.AccessKey,
Password: creds.SecretKey,
AuthToken: token,
Version: Version,
RequestTime: UTCNow(),
}
@ -69,8 +72,6 @@ func createLockTestServer(t *testing.T) (string, *lockServer, string) {
if err != nil {
t.Fatalf("Failed to login to lock server - %v", err)
}
token := loginReply.AuthToken
return testPath, locker, token
}

View file

@ -235,10 +235,7 @@ func (n *notifier) Validate() error {
if err := n.MySQL.Validate(); err != nil {
return err
}
if err := n.MQTT.Validate(); err != nil {
return err
}
return nil
return n.MQTT.Validate()
}
func (n *notifier) SetAMQPByID(accountID string, amqpn amqpNotify) {

View file

@ -60,8 +60,7 @@ type AuthRPCReply struct{}
// LoginRPCArgs - login username and password for RPC.
type LoginRPCArgs struct {
Username string
Password string
AuthToken string
Version string
RequestTime time.Time
}
@ -80,11 +79,8 @@ func (args LoginRPCArgs) IsValid() error {
return nil
}
// LoginRPCReply - login reply provides generated token to be used
// with subsequent requests.
type LoginRPCReply struct {
AuthToken string
}
// LoginRPCReply - login reply is a dummy struct perhaps for future use.
type LoginRPCReply struct{}
// LockArgs represents arguments for any authenticated lock RPC call.
type LockArgs struct {

View file

@ -34,6 +34,7 @@ import (
"strings"
"testing"
jwtgo "github.com/dgrijalva/jwt-go"
humanize "github.com/dustin/go-humanize"
"github.com/minio/minio-go/pkg/policy"
"github.com/minio/minio-go/pkg/set"
@ -667,6 +668,16 @@ func TestWebCreateURLToken(t *testing.T) {
ExecObjectLayerTest(t, testCreateURLToken)
}
func getTokenString(accessKey, secretKey string) (string, error) {
utcNow := UTCNow()
token := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, jwtgo.StandardClaims{
ExpiresAt: utcNow.Add(defaultJWTExpiry).Unix(),
IssuedAt: utcNow.Unix(),
Subject: accessKey,
})
return token.SignedString([]byte(secretKey))
}
func testCreateURLToken(obj ObjectLayer, instanceType string, t TestErrHandler) {
apiRouter := initTestWebRPCEndPoint(obj)
credentials := serverConfig.GetCredential()
@ -700,6 +711,21 @@ func testCreateURLToken(obj ObjectLayer, instanceType string, t TestErrHandler)
if !isAuthTokenValid(tokenReply.Token) {
t.Fatalf("token is not valid")
}
// Token is invalid.
if isAuthTokenValid("") {
t.Fatalf("token shouldn't be valid, but it is")
}
token, err := getTokenString("invalid-access", credentials.SecretKey)
if err != nil {
t.Fatal(err)
}
// Token has invalid access key.
if isAuthTokenValid(token) {
t.Fatalf("token shouldn't be valid, but it is")
}
}
// Wrapper for calling Upload Handler

View file

@ -92,12 +92,7 @@ func (d config) Save(filename string) error {
func (d config) Load(filename string) error {
d.lock.Lock()
defer d.lock.Unlock()
if err := loadFileConfig(filename, d.data); err != nil {
return err
}
return nil
return loadFileConfig(filename, d.data)
}
// Data - grab internal data map for reading