From c50dfb7dae8f1a9be9649bd065fce4f5e37b27c2 Mon Sep 17 00:00:00 2001 From: Dave Trombley Date: Thu, 4 Jun 2015 11:46:34 -0400 Subject: [PATCH] Added support for bcrypt, plaintext; extension points for other htpasswd hash methods. Signed-off-by: Dave Trombley --- registry/auth/basic/access.go | 38 ++++-------- registry/auth/basic/access_test.go | 48 +++++++++------ registry/auth/basic/htpasswd.go | 95 ++++++++++++++++++++++++++---- 3 files changed, 123 insertions(+), 58 deletions(-) diff --git a/registry/auth/basic/access.go b/registry/auth/basic/access.go index dd7923745..81a22b40b 100644 --- a/registry/auth/basic/access.go +++ b/registry/auth/basic/access.go @@ -9,11 +9,9 @@ package basic import ( - "encoding/base64" "errors" "fmt" "net/http" - "strings" ctxu "github.com/docker/distribution/context" "github.com/docker/distribution/registry/auth" @@ -58,8 +56,7 @@ func (ac *accessController) Authorized(ctx context.Context, accessRecords ...aut return nil, err } - authHeader := req.Header.Get("Authorization") - + authHeader := req.Header.Get("Authorization") if authHeader == "" { challenge := challenge{ realm: ac.realm, @@ -67,35 +64,20 @@ func (ac *accessController) Authorized(ctx context.Context, accessRecords ...aut return nil, &challenge } - parts := strings.Split(req.Header.Get("Authorization"), " ") - - challenge := challenge{ - realm: ac.realm, + user, pass, ok := req.BasicAuth() + if !ok { + return nil, errors.New("Invalid Authorization header") } - - if len(parts) != 2 || strings.ToLower(parts[0]) != "basic" { - challenge.err = ErrPasswordRequired - return nil, &challenge - } - - text, err := base64.StdEncoding.DecodeString(parts[1]) - if err != nil { + + if res, _ := ac.htpasswd.AuthenticateUser(user, pass); !res { + challenge := challenge{ + realm: ac.realm, + } challenge.err = ErrInvalidCredential return nil, &challenge } - credential := strings.Split(string(text), ":") - if len(credential) != 2 { - challenge.err = ErrInvalidCredential - return nil, &challenge - } - - if res, _ := ac.htpasswd.AuthenticateUser(credential[0], credential[1]); !res { - challenge.err = ErrInvalidCredential - return nil, &challenge - } - - return auth.WithUser(ctx, auth.UserInfo{Name: credential[0]}), nil + return auth.WithUser(ctx, auth.UserInfo{Name: user}), nil } func (ch *challenge) ServeHTTP(w http.ResponseWriter, r *http.Request) { diff --git a/registry/auth/basic/access_test.go b/registry/auth/basic/access_test.go index d82573b9b..b731675ef 100644 --- a/registry/auth/basic/access_test.go +++ b/registry/auth/basic/access_test.go @@ -14,8 +14,13 @@ import ( func TestBasicAccessController(t *testing.T) { testRealm := "The-Shire" - testUser := "bilbo" - testHtpasswdContent := "bilbo:{SHA}5siv5c0SHx681xU6GiSx9ZQryqs=" + testUsers := []string{"bilbo","frodo","MiShil","DeokMan"} + testPasswords := []string{"baggins","baggins","새주","공주님"} + testHtpasswdContent := `bilbo:{SHA}5siv5c0SHx681xU6GiSx9ZQryqs= + frodo:$2y$05$926C3y10Quzn/LnqQH86VOEVh/18T6RnLaS.khre96jLNL/7e.K5W + MiShil:$2y$05$0oHgwMehvoe8iAWS8I.7l.KoECXrwVaC16RPfaSCU5eVTFrATuMI2 + DeokMan:공주님` + tempFile, err := ioutil.TempFile("", "htpasswd-test") if err != nil { @@ -36,7 +41,9 @@ func TestBasicAccessController(t *testing.T) { } tempFile.Close() - + + var userNumber = 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := context.WithValue(nil, "http.request", r) authCtx, err := accessController.Authorized(ctx) @@ -55,8 +62,8 @@ func TestBasicAccessController(t *testing.T) { t.Fatal("basic accessController did not set auth.user context") } - if userInfo.Name != testUser { - t.Fatalf("expected user name %q, got %q", testUser, userInfo.Name) + if userInfo.Name != testUsers[userNumber] { + t.Fatalf("expected user name %q, got %q", testUsers[userNumber], userInfo.Name) } w.WriteHeader(http.StatusNoContent) @@ -79,22 +86,25 @@ func TestBasicAccessController(t *testing.T) { t.Fatalf("unexpected non-fail response status: %v != %v", resp.StatusCode, http.StatusUnauthorized) } - req, _ = http.NewRequest("GET", server.URL, nil) + for i := 0; i < len(testUsers); i++ { + userNumber = i + req, _ = http.NewRequest("GET", server.URL, nil) + sekrit := testUsers[i]+":"+testPasswords[i] + credential := "Basic " + base64.StdEncoding.EncodeToString([]byte(sekrit)) - sekrit := "bilbo:baggins" - credential := "Basic " + base64.StdEncoding.EncodeToString([]byte(sekrit)) + req.Header.Set("Authorization", credential) + resp, err = client.Do(req) + + if err != nil { + t.Fatalf("unexpected error during GET: %v", err) + } + defer resp.Body.Close() - req.Header.Set("Authorization", credential) - resp, err = client.Do(req) - - if err != nil { - t.Fatalf("unexpected error during GET: %v", err) - } - defer resp.Body.Close() - - // Request should be authorized - if resp.StatusCode != http.StatusNoContent { - t.Fatalf("unexpected non-success response status: %v != %v", resp.StatusCode, http.StatusNoContent) + // Request should be authorized + if resp.StatusCode != http.StatusNoContent { + t.Fatalf("unexpected non-success response status: %v != %v for %s %s %s", resp.StatusCode, http.StatusNoContent, testUsers[i], testPasswords[i], credential) + } } + } diff --git a/registry/auth/basic/htpasswd.go b/registry/auth/basic/htpasswd.go index 36eca3478..69dae9d86 100644 --- a/registry/auth/basic/htpasswd.go +++ b/registry/auth/basic/htpasswd.go @@ -6,11 +6,14 @@ import ( "encoding/csv" "errors" "os" + "regexp" + "strings" + + "golang.org/x/crypto/bcrypt" ) -// ErrSHARequired - returned in error field of challenge when the htpasswd was not made using SHA1 algorithm. -// (SHA1 is considered obsolete but the alternative for htpasswd is MD5, or system crypt...) -var ErrSHARequired = errors.New("htpasswd file must use SHA (htpasswd -s)") +// AuthenticationFailureErr - a generic error message for authentication failure to be presented to agent. +var AuthenticationFailureErr = errors.New("Bad username or password") // HTPasswd - holds a path to a system .htpasswd file and the machinery to parse it. type HTPasswd struct { @@ -18,18 +21,57 @@ type HTPasswd struct { reader *csv.Reader } +// AuthType represents a particular hash function used in the htpasswd file. +type AuthType int +const ( + PlainText AuthType = iota + SHA1 + ApacheMD5 + BCrypt + Crypt +) + +// String returns a text representation of the AuthType +func (at AuthType) String() string { + switch(at) { + case PlainText: return "plaintext" + case SHA1: return "sha1" + case ApacheMD5: return "md5" + case BCrypt: return "bcrypt" + case Crypt: return "system crypt" + } + return "unknown" +} + + // NewHTPasswd - Create a new HTPasswd with the given path to .htpasswd file. func NewHTPasswd(htpath string) *HTPasswd { return &HTPasswd{path: htpath} } +var bcryptPrefixRegexp *regexp.Regexp = regexp.MustCompile(`^\$2[ab]?y\$`) + +// GetAuthCredentialType - Inspect an htpasswd file credential and guess the encryption algorithm used. +func GetAuthCredentialType(cred string) AuthType { + if strings.HasPrefix(cred, "{SHA}") { + return SHA1 + } + if strings.HasPrefix(cred, "$apr1$") { + return ApacheMD5 + } + if bcryptPrefixRegexp.MatchString(cred) { + return BCrypt + } + // There's just not a great way to distinguish between these next two... + if len(cred) == 13 { + return Crypt + } + return PlainText +} + // AuthenticateUser - Check a given user:password credential against the receiving HTPasswd's file. func (htpasswd *HTPasswd) AuthenticateUser(user string, pwd string) (bool, error) { - // Hash the credential. - sha := sha1.New() - sha.Write([]byte(pwd)) - hash := base64.StdEncoding.EncodeToString(sha.Sum(nil)) // Open the file. in, err := os.Open(htpasswd.path) @@ -43,12 +85,43 @@ func (htpasswd *HTPasswd) AuthenticateUser(user string, pwd string) (bool, error reader.Comment = '#' reader.TrimLeadingSpace = true for entry, readerr := reader.Read(); entry != nil || readerr != nil; entry, readerr = reader.Read() { + if readerr != nil { + return false, readerr + } + if len(entry) == 0 { + continue + } if entry[0] == user { - if len(entry[1]) < 6 || entry[1][0:5] != "{SHA}" { - return false, ErrSHARequired + credential := entry[1] + credType := GetAuthCredentialType(credential) + switch(credType) { + case SHA1: { + sha := sha1.New() + sha.Write([]byte(pwd)) + hash := base64.StdEncoding.EncodeToString(sha.Sum(nil)) + return entry[1][5:] == hash, nil + } + case ApacheMD5: { + return false, errors.New(ApacheMD5.String()+" htpasswd hash function not yet supported") + } + case BCrypt: { + err := bcrypt.CompareHashAndPassword([]byte(credential),[]byte(pwd)) + if err != nil { + return false, err + } + return true, nil + } + case Crypt: { + return false, errors.New(Crypt.String()+" htpasswd hash function not yet supported") + } + case PlainText: { + if pwd == credential { + return true, nil + } + return false, AuthenticationFailureErr + } } - return entry[1][5:] == hash, nil } } - return false, nil + return false, AuthenticationFailureErr }