diff --git a/.github/workflows/ecosystem-tools.yaml b/.github/workflows/ecosystem-tools.yaml index 2b8c5d8c..3479a137 100644 --- a/.github/workflows/ecosystem-tools.yaml +++ b/.github/workflows/ecosystem-tools.yaml @@ -27,7 +27,8 @@ jobs: go install github.com/swaggo/swag/cmd/swag@v1.16.2 go mod download sudo apt-get update - sudo apt-get install libgpgme-dev libassuan-dev libbtrfs-dev libdevmapper-dev pkg-config rpm uidmap haproxy jq valkey-tools + sudo apt-get install -y libgpgme-dev libassuan-dev libbtrfs-dev \ + libdevmapper-dev pkg-config rpm uidmap haproxy jq valkey-tools whois # install skopeo git clone -b v1.12.0 https://github.com/containers/skopeo.git cd skopeo diff --git a/Makefile b/Makefile index 232379a1..a1e538a5 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ ORAS := $(TOOLSDIR)/bin/oras ORAS_VERSION := 1.2.1 HELM_VERSION := v3.9.1 REGCLIENT := $(TOOLSDIR)/bin/regctl -REGCLIENT_VERSION := v0.5.7 +REGCLIENT_VERSION := v0.9.2 CRICTL := $(TOOLSDIR)/bin/crictl CRICTL_VERSION := v1.26.1 ACTION_VALIDATOR := $(TOOLSDIR)/bin/action-validator diff --git a/go.mod b/go.mod index cd08ea1d..8eebbc3f 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.24.4 require ( github.com/99designs/gqlgen v0.17.81 + github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 github.com/Masterminds/semver v1.5.0 github.com/alicebob/miniredis/v2 v2.35.0 github.com/aquasecurity/trivy v0.65.0 @@ -43,6 +44,7 @@ require ( github.com/json-iterator/go v1.1.12 github.com/migueleliasweb/go-github-mock v1.4.0 github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c + github.com/nathanaelle/password v1.0.0 github.com/nats-io/nats-server/v2 v2.12.1 github.com/nats-io/nats.go v1.47.0 github.com/nmcclain/ldap v0.0.0-20210720162743-7f8d1e44eeba diff --git a/go.sum b/go.sum index 6428dfe7..c0712371 100644 --- a/go.sum +++ b/go.sum @@ -691,6 +691,8 @@ github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7Oputl github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ= github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 h1:IEjq88XO4PuBDcvmjQJcQGg+w+UaafSy8G5Kcb5tBhI= +github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5/go.mod h1:exZ0C/1emQJAw5tHOaUDyY1ycttqBAPcxuzf7QbY6ec= github.com/GoogleCloudPlatform/docker-credential-gcr v2.0.5+incompatible h1:juIaKLLVhqzP55d8x4cSVgwyQv76Z55/fRv/UBr2KkQ= github.com/GoogleCloudPlatform/docker-credential-gcr v2.0.5+incompatible/go.mod h1:BB1eHdMLYEFuFdBlRMb0N7YGVdM5s6Pt0njxgvfbGGs= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc= @@ -1733,6 +1735,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/nathanaelle/password v1.0.0 h1:1Etka3uuBvATlCb72f7P5vsgedus+C91Fgff1oMloq0= +github.com/nathanaelle/password v1.0.0/go.mod h1:wt9xV3xwQmc3Qi0ofowmzR7N+kF1L4cguCuWjAfdj1Q= github.com/nats-io/jwt/v2 v2.8.0 h1:K7uzyz50+yGZDO5o772eRE7atlcSEENpL7P+b74JV1g= github.com/nats-io/jwt/v2 v2.8.0/go.mod h1:me11pOkwObtcBNR8AiMrUbtVOUGkqYjMQZ6jnSdVUIA= github.com/nats-io/nats-server/v2 v2.12.1 h1:0tRrc9bzyXEdBLcHr2XEjDzVpUxWx64aZBm7Rl1QDrA= diff --git a/pkg/api/authn_test.go b/pkg/api/authn_test.go index cc25200c..acd5ff27 100644 --- a/pkg/api/authn_test.go +++ b/pkg/api/authn_test.go @@ -99,7 +99,7 @@ func TestAPIKeys(t *testing.T) { username, seedUser := test.GenerateRandomString() password, seedPass := test.GenerateRandomString() - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(username, password)) defer os.Remove(htpasswdPath) @@ -871,7 +871,7 @@ func TestAPIKeysOpenDBError(t *testing.T) { conf := config.New() username, seedUser := test.GenerateRandomString() password, seedPass := test.GenerateRandomString() - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(username, password)) defer os.Remove(htpasswdPath) @@ -1154,7 +1154,7 @@ func TestCookieSecureFlag(t *testing.T) { username, _ := test.GenerateRandomString() password, _ := test.GenerateRandomString() - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(username, password)) defer os.Remove(htpasswdPath) diff --git a/pkg/api/controller_test.go b/pkg/api/controller_test.go index 91e5b7bc..68eeda24 100644 --- a/pkg/api/controller_test.go +++ b/pkg/api/controller_test.go @@ -719,54 +719,62 @@ func TestHtpasswdSingleCred(t *testing.T) { Convey("Single cred", t, func() { port := test.GetFreePort() baseURL := test.GetBaseURL(port) - singleCredtests := []string{} - user, seedUser := test.GenerateRandomString() - password, seedPass := test.GenerateRandomString() - singleCredtests = append(singleCredtests, test.GetCredString(user, password)) - singleCredtests = append(singleCredtests, test.GetCredString(user, password)) + credFuncs := []func(string, string) string{ + test.GetBcryptCredString, + test.GetSHA256CredString, + test.GetSHA512CredString, + } - for _, testString := range singleCredtests { - func() { - conf := config.New() - conf.HTTP.Port = port + for _, credFunc := range credFuncs { + singleCredtests := []string{} + user, seedUser := test.GenerateRandomString() + password, seedPass := test.GenerateRandomString() + singleCredtests = append(singleCredtests, credFunc(user, password)) + singleCredtests = append(singleCredtests, credFunc(user, password)) - htpasswdPath := test.MakeHtpasswdFileFromString(testString) - defer os.Remove(htpasswdPath) - conf.HTTP.Auth = &config.AuthConfig{ - HTPasswd: config.AuthHTPasswd{ - Path: htpasswdPath, - }, - } + for _, testString := range singleCredtests { + func() { + conf := config.New() + conf.HTTP.Port = port - conf.HTTP.AllowOrigin = conf.HTTP.Address + htpasswdPath := test.MakeHtpasswdFileFromString(testString) + defer os.Remove(htpasswdPath) + conf.HTTP.Auth = &config.AuthConfig{ + HTPasswd: config.AuthHTPasswd{ + Path: htpasswdPath, + }, + } - ctlr := makeController(conf, t.TempDir()) - ctlr.Log.Info().Int64("seedUser", seedUser).Int64("seedPass", seedPass).Msg("random seed for username & password") + conf.HTTP.AllowOrigin = conf.HTTP.Address - cm := test.NewControllerManager(ctlr) - cm.StartAndWait(port) + ctlr := makeController(conf, t.TempDir()) + ctlr.Log.Info().Int64("seedUser", seedUser).Int64("seedPass", seedPass).Msg("random seed for username & password") - defer cm.StopServer() + cm := test.NewControllerManager(ctlr) + cm.StartAndWait(port) - // with creds, should get expected status code - resp, _ := resty.R().SetBasicAuth(user, password).Get(baseURL + "/v2/") - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusOK) + defer cm.StopServer() - header := []string{"Authorization,content-type," + constants.SessionClientHeaderName} + // with creds, should get expected status code + resp, _ := resty.R().SetBasicAuth(user, password).Get(baseURL + "/v2/") + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) - resp, _ = resty.R().SetBasicAuth(user, password).Options(baseURL + "/v2/") - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusNoContent) - So(len(resp.Header()), ShouldEqual, 5) - So(resp.Header()["Access-Control-Allow-Headers"], ShouldResemble, header) - So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "GET,OPTIONS") + header := []string{"Authorization,content-type," + constants.SessionClientHeaderName} - // with invalid creds, it should fail - resp, _ = resty.R().SetBasicAuth("chuck", "chuck").Get(baseURL + "/v2/") - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) - }() + resp, _ = resty.R().SetBasicAuth(user, password).Options(baseURL + "/v2/") + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusNoContent) + So(len(resp.Header()), ShouldEqual, 5) + So(resp.Header()["Access-Control-Allow-Headers"], ShouldResemble, header) + So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "GET,OPTIONS") + + // with invalid creds, it should fail + resp, _ = resty.R().SetBasicAuth("chuck", "chuck").Get(baseURL + "/v2/") + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) + }() + } } }) } @@ -783,7 +791,7 @@ func TestAllowMethodsHeader(t *testing.T) { simpleUser := "simpleUser" simpleUserPassword := "simpleUserPass" - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(simpleUser, simpleUserPassword)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(simpleUser, simpleUserPassword)) defer os.Remove(htpasswdPath) @@ -860,14 +868,14 @@ func TestHtpasswdTwoCreds(t *testing.T) { password1 := "aliciapassword" user2 := "bob" password2 := "robert" - twoCredTests = append(twoCredTests, test.GetCredString(user1, password1)+"\n"+ - test.GetCredString(user2, password2)) + twoCredTests = append(twoCredTests, test.GetBcryptCredString(user1, password1)+"\n"+ + test.GetBcryptCredString(user2, password2)) - twoCredTests = append(twoCredTests, test.GetCredString(user1, password1)+"\n"+ - test.GetCredString(user2, password2)+"\n") + twoCredTests = append(twoCredTests, test.GetBcryptCredString(user1, password1)+"\n"+ + test.GetBcryptCredString(user2, password2)+"\n") - twoCredTests = append(twoCredTests, test.GetCredString(user1, password1)+"\n\n"+ - test.GetCredString(user2, password2)+"\n\n") + twoCredTests = append(twoCredTests, test.GetBcryptCredString(user1, password1)+"\n\n"+ + test.GetBcryptCredString(user2, password2)+"\n\n") for _, testString := range twoCredTests { func() { @@ -920,7 +928,7 @@ func TestHtpasswdFiveCreds(t *testing.T) { credString := strings.Builder{} for key, val := range tests { - credString.WriteString(test.GetCredString(key, val) + "\n") + credString.WriteString(test.GetBcryptCredString(key, val) + "\n") } func() { @@ -1074,7 +1082,7 @@ func TestBasicAuth(t *testing.T) { username, seedUser := test.GenerateRandomString() password, seedPass := test.GenerateRandomString() - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(username, password)) defer os.Remove(htpasswdPath) @@ -1363,7 +1371,7 @@ func TestScaleOutRequestProxy(t *testing.T) { username, _ := test.GenerateRandomString() password, _ := test.GenerateRandomString() - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(username, password)) defer os.Remove(htpasswdPath) @@ -1935,7 +1943,7 @@ func TestMultipleInstance(t *testing.T) { username, seedUser := test.GenerateRandomString() password, seedPass := test.GenerateRandomString() - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(username, password)) defer os.Remove(htpasswdPath) @@ -1979,7 +1987,7 @@ func TestMultipleInstance(t *testing.T) { username, seedUser := test.GenerateRandomString() password, seedPass := test.GenerateRandomString() - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(username, password)) defer os.Remove(htpasswdPath) @@ -2029,7 +2037,7 @@ func TestMultipleInstance(t *testing.T) { username, seedUser := test.GenerateRandomString() password, seedPass := test.GenerateRandomString() - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(username, password)) defer os.Remove(htpasswdPath) @@ -2083,7 +2091,7 @@ func TestTLSWithBasicAuth(t *testing.T) { username, seedUser := test.GenerateRandomString() password, seedPass := test.GenerateRandomString() - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(username, password)) defer os.Remove(htpasswdPath) @@ -2154,7 +2162,7 @@ func TestTLSWithBasicAuthAllowReadAccess(t *testing.T) { username, seedUser := test.GenerateRandomString() password, seedPass := test.GenerateRandomString() - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(username, password)) defer os.Remove(htpasswdPath) @@ -2826,7 +2834,7 @@ func TestTLSMutualAndBasicAuth(t *testing.T) { username, seedUser := test.GenerateRandomString() password, seedPass := test.GenerateRandomString() - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(username, password)) defer os.Remove(htpasswdPath) @@ -2913,7 +2921,7 @@ func TestTLSMutualAndBasicAuthAllowReadAccess(t *testing.T) { username, seedUser := test.GenerateRandomString() password, seedPass := test.GenerateRandomString() - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(username, password)) defer os.Remove(htpasswdPath) @@ -4579,7 +4587,7 @@ func TestOpenIDMiddleware(t *testing.T) { // need a username different than ldap one, to test both logic htpasswdUsername, seedUser := test.GenerateRandomString() htpasswdPassword, seedPass := test.GenerateRandomString() - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(htpasswdUsername, htpasswdPassword)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(htpasswdUsername, htpasswdPassword)) defer os.Remove(htpasswdPath) @@ -4995,7 +5003,7 @@ func TestOpenIDMiddlewareWithRedisSessionDriver(t *testing.T) { // need a username different than ldap one, to test both logic htpasswdUsername, seedUser := test.GenerateRandomString() htpasswdPassword, seedPass := test.GenerateRandomString() - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(htpasswdUsername, htpasswdPassword)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(htpasswdUsername, htpasswdPassword)) defer os.Remove(htpasswdPath) @@ -5481,7 +5489,7 @@ func TestAuthnSessionErrors(t *testing.T) { htpasswdUsername, seedUser := test.GenerateRandomString() htpasswdPassword, seedPass := test.GenerateRandomString() - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(htpasswdUsername, htpasswdPassword)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(htpasswdUsername, htpasswdPassword)) defer os.Remove(htpasswdPath) @@ -5886,7 +5894,7 @@ func TestAuthnMetaDBErrors(t *testing.T) { username, seedUser := test.GenerateRandomString() password, seedPass := test.GenerateRandomString() - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(username, password)) defer os.Remove(htpasswdPath) @@ -5999,7 +6007,7 @@ func TestAuthorization(t *testing.T) { conf.HTTP.Port = port username, seedUser := test.GenerateRandomString() password, seedPass := test.GenerateRandomString() - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(username, password)) defer os.Remove(htpasswdPath) @@ -6119,7 +6127,7 @@ func TestGetUsername(t *testing.T) { username, seedUser := test.GenerateRandomString() password, seedPass := test.GenerateRandomString() - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(username, password)) defer os.Remove(htpasswdPath) @@ -6190,7 +6198,7 @@ func TestAuthorizationMountBlob(t *testing.T) { username1 = strings.ToLower(username1) username2 = strings.ToLower(username2) - content := test.GetCredString(username1, password1) + test.GetCredString(username2, password2) + content := test.GetBcryptCredString(username1, password1) + test.GetBcryptCredString(username2, password2) htpasswdPath := test.MakeHtpasswdFileFromString(content) @@ -6548,7 +6556,7 @@ func TestAuthorizationWithAnonymousPolicyBasicAuthAndSessionHeader(t *testing.T) badpassphrase := "bad" htpasswdUsername, seedUser := test.GenerateRandomString() htpasswdPassword, seedPass := test.GenerateRandomString() - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(htpasswdUsername, htpasswdPassword)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(htpasswdUsername, htpasswdPassword)) defer os.Remove(htpasswdPath) @@ -6758,7 +6766,7 @@ func TestAuthorizationWithMultiplePolicies(t *testing.T) { password1, seedPass1 := test.GenerateRandomString() username2, seedUser2 := test.GenerateRandomString() password2, seedPass2 := test.GenerateRandomString() - content := test.GetCredString(username1, password1) + test.GetCredString(username2, password2) + content := test.GetBcryptCredString(username1, password1) + test.GetBcryptCredString(username2, password2) htpasswdPath := test.MakeHtpasswdFileFromString(content) @@ -6917,7 +6925,7 @@ func TestInvalidCases(t *testing.T) { username, seedUser := test.GenerateRandomString() password, seedPass := test.GenerateRandomString() - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(username, password)) defer os.Remove(htpasswdPath) @@ -6977,8 +6985,8 @@ func TestHTTPReadOnly(t *testing.T) { singleCredtests := []string{} user, seedUser := test.GenerateRandomString() password, seedPass := test.GenerateRandomString() - singleCredtests = append(singleCredtests, test.GetCredString(user, password)) - singleCredtests = append(singleCredtests, test.GetCredString(user, password)+"\n") + singleCredtests = append(singleCredtests, test.GetBcryptCredString(user, password)) + singleCredtests = append(singleCredtests, test.GetBcryptCredString(user, password)+"\n") port := test.GetFreePort() baseURL := test.GetBaseURL(port) @@ -7048,7 +7056,7 @@ func TestCrossRepoMount(t *testing.T) { conf.HTTP.Port = port username, seedUser := test.GenerateRandomString() password, seedPass := test.GenerateRandomString() - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(username, password)) defer os.Remove(htpasswdPath) @@ -7263,7 +7271,7 @@ func TestCrossRepoMount(t *testing.T) { conf := config.New() conf.HTTP.Port = port - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(username, password)) defer os.Remove(htpasswdPath) @@ -7407,7 +7415,7 @@ func TestParallelRequests(t *testing.T) { conf.HTTP.Port = port username, seedUser := test.GenerateRandomString() password, seedPass := test.GenerateRandomString() - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(username, password)) t.Cleanup(func() { os.Remove(htpasswdPath) @@ -8971,7 +8979,7 @@ func TestPagedRepositoriesWithAuthorization(t *testing.T) { conf.HTTP.Port = port username, _ := test.GenerateRandomString() password, _ := test.GenerateRandomString() - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(username, password)) defer os.Remove(htpasswdPath) @@ -12380,7 +12388,7 @@ func TestSearchRoutes(t *testing.T) { user1 := "test" password1 := "test" - testString1 := test.GetCredString(user1, password1) + testString1 := test.GetBcryptCredString(user1, password1) htpasswdPath := test.MakeHtpasswdFileFromString(testString1) @@ -12523,7 +12531,7 @@ func TestSearchRoutes(t *testing.T) { user1 := "test1" password1 := "test1" group1 := "testgroup3" - testString1 := test.GetCredString(user1, password1) + testString1 := test.GetBcryptCredString(user1, password1) htpasswdPath := test.MakeHtpasswdFileFromString(testString1) @@ -12611,7 +12619,7 @@ func TestSearchRoutes(t *testing.T) { password1 := "test2" group1 := "testgroup1" group2 := "secondtestgroup" - testString1 := test.GetCredString(user1, password1) + testString1 := test.GetBcryptCredString(user1, password1) htpasswdPath := test.MakeHtpasswdFileFromString(testString1) defer os.Remove(htpasswdPath) @@ -12681,7 +12689,7 @@ func TestSearchRoutes(t *testing.T) { user1 := "test3" password1 := "test3" group1 := "testgroup" - testString1 := test.GetCredString(user1, password1) + testString1 := test.GetBcryptCredString(user1, password1) htpasswdPath := test.MakeHtpasswdFileFromString(testString1) defer os.Remove(htpasswdPath) @@ -12751,7 +12759,7 @@ func TestSearchRoutes(t *testing.T) { user1 := "test4" password1 := "test4" group1 := "testgroup1" - testString1 := test.GetCredString(user1, password1) + testString1 := test.GetBcryptCredString(user1, password1) htpasswdPath := test.MakeHtpasswdFileFromString(testString1) @@ -12823,7 +12831,7 @@ func TestSearchRoutes(t *testing.T) { user1 := "test5" password1 := "test5" group1 := "testgroup2" - testString1 := test.GetCredString(user1, password1) + testString1 := test.GetBcryptCredString(user1, password1) htpasswdPath := test.MakeHtpasswdFileFromString(testString1) defer os.Remove(htpasswdPath) @@ -12883,7 +12891,7 @@ func TestSearchRoutes(t *testing.T) { user1, seedUser1 := test.GenerateRandomString() password1, seedPass1 := test.GenerateRandomString() - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(user1, password1)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(user1, password1)) defer os.Remove(htpasswdPath) conf.HTTP.Auth = &config.AuthConfig{ diff --git a/pkg/api/htpasswd.go b/pkg/api/htpasswd.go index 5bdbcbdb..75d3f815 100644 --- a/pkg/api/htpasswd.go +++ b/pkg/api/htpasswd.go @@ -3,6 +3,7 @@ package api import ( "bufio" "context" + "crypto/fips140" "errors" "os" "os/signal" @@ -11,6 +12,7 @@ import ( "syscall" "github.com/fsnotify/fsnotify" + cpass "github.com/nathanaelle/password" "golang.org/x/crypto/bcrypt" "zotregistry.dev/zot/v2/pkg/log" @@ -87,15 +89,51 @@ func (s *HTPasswd) Authenticate(username, passphrase string) (ok, present bool) return false, false } - err := bcrypt.CompareHashAndPassword([]byte(passphraseHash), []byte(passphrase)) - ok = err == nil + // first try bcrypt (although disabled if fips140 mode is enabled) + if strings.HasPrefix(passphraseHash, "$2a$") || strings.HasPrefix(passphraseHash, "$2b$") || + strings.HasPrefix(passphraseHash, "$2y$") { + if fips140.Enabled() { + s.log.Warn().Str("username", username).Msg("htpasswd bcrypt failed since fips140 is enabled") - if err != nil && !errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) { - // Log that user's hash has unsupported format. Better than silently return 401. - s.log.Warn().Err(err).Str("username", username).Msg("htpasswd bcrypt compare failed") + return false, present + } + + err := bcrypt.CompareHashAndPassword([]byte(passphraseHash), []byte(passphrase)) + if err != nil { + // Log that user's hash has unsupported format. Better than silently return 401. + s.log.Warn().Err(err).Str("username", username).Msg("htpasswd bcrypt compare failed") + + return false, present + } + + return true, present // success: bcrypt } - return + var crypter cpass.Crypter + + if strings.HasPrefix(passphraseHash, "$5$") { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain + crypter, ok = cpass.SHA256.CrypterFound(passphraseHash) + } else if strings.HasPrefix(passphraseHash, "$6$") { + crypter, ok = cpass.SHA512.CrypterFound(passphraseHash) + } else { + s.log.Warn().Str("username", username).Msg("htpasswd entry has unsupported hash type") + + return false, present + } + + if !ok { + s.log.Warn().Str("username", username).Msg("htpasswd entry parsing failed") + + return false, present + } + + if !crypter.Verify([]byte(passphrase)) { + s.log.Warn().Str("username", username).Msg("htpasswd sha compare failed") + + return false, present + } + + return true, present // success: sha } // HTPasswdWatcher helper which triggers htpasswd reload on file change event. diff --git a/pkg/api/htpasswd_test.go b/pkg/api/htpasswd_test.go index fdd7f84d..a235da4b 100644 --- a/pkg/api/htpasswd_test.go +++ b/pkg/api/htpasswd_test.go @@ -19,7 +19,7 @@ func TestHTPasswdWatcherOriginal(t *testing.T) { username, _ := test.GenerateRandomString() password1, _ := test.GenerateRandomString() password2, _ := test.GenerateRandomString() - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password1)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(username, password1)) defer os.Remove(htpasswdPath) @@ -49,7 +49,7 @@ func TestHTPasswdWatcherOriginal(t *testing.T) { So(present, ShouldBeTrue) // 2. Change file - err = os.WriteFile(htpasswdPath, []byte(test.GetCredString(username, password2)), 0o600) + err = os.WriteFile(htpasswdPath, []byte(test.GetBcryptCredString(username, password2)), 0o600) So(err, ShouldBeNil) // 3. Give some time for the background task @@ -98,8 +98,8 @@ func TestHTPasswdWatcher(t *testing.T) { username2, _ := test.GenerateRandomString() password2, _ := test.GenerateRandomString() - htpasswdPath1 := test.MakeHtpasswdFileFromString(test.GetCredString(username1, password1)) - htpasswdPath2 := test.MakeHtpasswdFileFromString(test.GetCredString(username2, password2)) + htpasswdPath1 := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(username1, password1)) + htpasswdPath2 := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(username2, password2)) defer os.Remove(htpasswdPath1) defer os.Remove(htpasswdPath2) @@ -150,7 +150,7 @@ func TestHTPasswdWatcher(t *testing.T) { So(present, ShouldBeTrue) // Change file content and verify automatic reload - err = os.WriteFile(htpasswdPath1, []byte(test.GetCredString(username1, password2)), 0o600) + err = os.WriteFile(htpasswdPath1, []byte(test.GetBcryptCredString(username1, password2)), 0o600) So(err, ShouldBeNil) time.Sleep(100 * time.Millisecond) ok, present = htp.Authenticate(username1, password2) @@ -158,7 +158,8 @@ func TestHTPasswdWatcher(t *testing.T) { So(present, ShouldBeTrue) // Test multiple users - multiUserContent := test.GetCredString(username1, password1) + "\n" + test.GetCredString(username2, password2) + multiUserContent := test.GetBcryptCredString(username1, password1) + + "\n" + test.GetBcryptCredString(username2, password2) err = os.WriteFile(htpasswdPath1, []byte(multiUserContent), 0o600) So(err, ShouldBeNil) time.Sleep(100 * time.Millisecond) @@ -194,8 +195,8 @@ func TestHTPasswdWatcher(t *testing.T) { username2, _ := test.GenerateRandomString() password2, _ := test.GenerateRandomString() - htpasswdPath1 := test.MakeHtpasswdFileFromString(test.GetCredString(username1, password1)) - htpasswdPath2 := test.MakeHtpasswdFileFromString(test.GetCredString(username2, password2)) + htpasswdPath1 := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(username1, password1)) + htpasswdPath2 := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(username2, password2)) defer os.Remove(htpasswdPath1) defer os.Remove(htpasswdPath2) @@ -236,7 +237,7 @@ func TestHTPasswdWatcher(t *testing.T) { So(present, ShouldBeTrue) // Test file rename (should not trigger reload) - htpasswdPath3 := test.MakeHtpasswdFileFromString(test.GetCredString(username1, password1)) + htpasswdPath3 := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(username1, password1)) defer os.Remove(htpasswdPath3) err = htw.ChangeFile(htpasswdPath3) So(err, ShouldBeNil) @@ -302,8 +303,8 @@ func TestHTPasswdWatcher(t *testing.T) { username2, _ := test.GenerateRandomString() password2, _ := test.GenerateRandomString() - htpasswdPath1 := test.MakeHtpasswdFileFromString(test.GetCredString(username1, password1)) - htpasswdPath2 := test.MakeHtpasswdFileFromString(test.GetCredString(username2, password2)) + htpasswdPath1 := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(username1, password1)) + htpasswdPath2 := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(username2, password2)) defer os.Remove(htpasswdPath1) defer os.Remove(htpasswdPath2) @@ -408,7 +409,7 @@ func TestHTPasswdWatcher(t *testing.T) { // Test 2: File watching with fsnotify resources cleanup username, _ := test.GenerateRandomString() password, _ := test.GenerateRandomString() - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(username, password)) defer os.Remove(htpasswdPath) @@ -490,7 +491,7 @@ func TestHTPasswdWatcher(t *testing.T) { So(test.WaitForLogMessages(logBuffer, "htpasswd watcher terminating...", 1, 5*time.Second), ShouldBeTrue) // Test file with empty lines and comments - content := "\n\n" + test.GetCredString(username, password) + "\n# comment\n" + content := "\n\n" + test.GetBcryptCredString(username, password) + "\n# comment\n" commentedPath := test.MakeHtpasswdFileFromString(content) defer os.Remove(commentedPath) @@ -520,7 +521,7 @@ func TestHTPasswdWatcher(t *testing.T) { So(err, ShouldBeNil) // Load some initial data - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(username, password)) defer os.Remove(htpasswdPath) // Load initial file (this will populate the store) diff --git a/pkg/api/routes_test.go b/pkg/api/routes_test.go index 60d5d5f4..5f59479d 100644 --- a/pkg/api/routes_test.go +++ b/pkg/api/routes_test.go @@ -46,7 +46,7 @@ func TestRoutes(t *testing.T) { username, seedUser := test.GenerateRandomString() password, seedPass := test.GenerateRandomString() - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(username, password)) defer os.Remove(htpasswdPath) diff --git a/pkg/cli/client/client_test.go b/pkg/cli/client/client_test.go index 7beefd8c..285d5360 100644 --- a/pkg/cli/client/client_test.go +++ b/pkg/cli/client/client_test.go @@ -55,7 +55,7 @@ func TestTLSWithAuth(t *testing.T) { username, seedUser := test.GenerateRandomString() password, seedPass := test.GenerateRandomString() - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(username, password)) defer os.Remove(htpasswdPath) conf.HTTP.Auth = &config.AuthConfig{ diff --git a/pkg/cli/server/config_reloader_test.go b/pkg/cli/server/config_reloader_test.go index a9831f2c..3c3711b3 100644 --- a/pkg/cli/server/config_reloader_test.go +++ b/pkg/cli/server/config_reloader_test.go @@ -31,7 +31,7 @@ func TestConfigReloader(t *testing.T) { username := "alice" password := "alice" - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(username, password)) defer os.Remove(htpasswdPath) defer os.Remove(logFile.Name()) // clean up diff --git a/pkg/debug/pprof/pprof_test.go b/pkg/debug/pprof/pprof_test.go index 048c2819..a297640c 100644 --- a/pkg/debug/pprof/pprof_test.go +++ b/pkg/debug/pprof/pprof_test.go @@ -28,8 +28,8 @@ func TestProfilingAuthz(t *testing.T) { password, seedPass := test.GenerateRandomString() authorizationAllRepos := test.AuthorizationAllRepos - testCreds := test.GetCredString(adminUsername, adminPassword) + - test.GetCredString(username, password) + testCreds := test.GetBcryptCredString(adminUsername, adminPassword) + + test.GetBcryptCredString(username, password) htpasswdPath := test.MakeHtpasswdFileFromString(testCreds) defer os.Remove(htpasswdPath) diff --git a/pkg/extensions/extension_image_trust_test.go b/pkg/extensions/extension_image_trust_test.go index 3b537837..fb609c9f 100644 --- a/pkg/extensions/extension_image_trust_test.go +++ b/pkg/extensions/extension_image_trust_test.go @@ -743,7 +743,7 @@ func RunSignatureUploadAndVerificationTests(t *testing.T, cacheDriverParams map[ Convey("Verify uploading cosign public keys with auth configured", func() { globalDir := t.TempDir() port := test.GetFreePort() - testCreds := test.GetCredString("admin", "admin") + "\n" + test.GetCredString("test", "test") + testCreds := test.GetBcryptCredString("admin", "admin") + "\n" + test.GetBcryptCredString("test", "test") htpasswdPath := test.MakeHtpasswdFileFromString(testCreds) defer os.Remove(htpasswdPath) diff --git a/pkg/extensions/extensions_test.go b/pkg/extensions/extensions_test.go index e0c7c667..9c798703 100644 --- a/pkg/extensions/extensions_test.go +++ b/pkg/extensions/extensions_test.go @@ -143,7 +143,7 @@ func TestMgmtExtension(t *testing.T) { Convey("Verify mgmt auth info route enabled with htpasswd", t, func() { username, seedUser := test.GenerateRandomString() password, seedPass := test.GenerateRandomString() - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(username, password)) defer func() { conf.HTTP.Auth.HTPasswd.Path = "" @@ -362,7 +362,7 @@ func TestMgmtExtension(t *testing.T) { Convey("Verify mgmt auth info route enabled with htpasswd + ldap", t, func() { username, seedUser := test.GenerateRandomString() password, seedPass := test.GenerateRandomString() - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(username, password)) defer func() { conf.HTTP.Auth.HTPasswd.Path = "" @@ -448,7 +448,7 @@ func TestMgmtExtension(t *testing.T) { Convey("Verify mgmt auth info route enabled with htpasswd + ldap + bearer", t, func() { username, seedUser := test.GenerateRandomString() password, seedPass := test.GenerateRandomString() - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(username, password)) defer func() { conf.HTTP.Auth.HTPasswd.Path = "" @@ -734,7 +734,7 @@ func TestMgmtExtension(t *testing.T) { Convey("Verify mgmt auth info route enabled with empty openID provider list", t, func() { username, seedUser := test.GenerateRandomString() password, seedPass := test.GenerateRandomString() - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(username, password)) defer func() { conf.HTTP.Auth.HTPasswd.Path = "" diff --git a/pkg/extensions/monitoring/monitoring_test.go b/pkg/extensions/monitoring/monitoring_test.go index 20698d4f..3bddfc86 100644 --- a/pkg/extensions/monitoring/monitoring_test.go +++ b/pkg/extensions/monitoring/monitoring_test.go @@ -171,7 +171,7 @@ func TestMetricsAuthentication(t *testing.T) { password := generateRandomString() metricsuser := generateRandomString() metricspass := generateRandomString() - content := test.GetCredString(username, password) + "\n" + test.GetCredString(metricsuser, metricspass) + content := test.GetBcryptCredString(username, password) + "\n" + test.GetBcryptCredString(metricsuser, metricspass) htpasswdPath := test.MakeHtpasswdFileFromString(content) defer os.Remove(htpasswdPath) @@ -236,7 +236,7 @@ func TestMetricsAuthorization(t *testing.T) { password := generateRandomString() metricsuser := generateRandomString() metricspass := generateRandomString() - content := test.GetCredString(username, password) + "\n" + test.GetCredString(metricsuser, metricspass) + content := test.GetBcryptCredString(username, password) + "\n" + test.GetBcryptCredString(metricsuser, metricspass) htpasswdPath := test.MakeHtpasswdFileFromString(content) defer os.Remove(htpasswdPath) diff --git a/pkg/extensions/search/cve/cve_test.go b/pkg/extensions/search/cve/cve_test.go index b95049b5..f3d789e9 100644 --- a/pkg/extensions/search/cve/cve_test.go +++ b/pkg/extensions/search/cve/cve_test.go @@ -424,7 +424,7 @@ func TestCVESearchDisabled(t *testing.T) { username, seedUser := test.GenerateRandomString() password, seedPass := test.GenerateRandomString() - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(username, password)) defer os.Remove(htpasswdPath) conf.HTTP.Auth = &config.AuthConfig{ @@ -493,7 +493,7 @@ func TestCVESearch(t *testing.T) { username, seedUser := test.GenerateRandomString() password, seedPass := test.GenerateRandomString() - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(username, password)) defer os.Remove(htpasswdPath) dbDir, err := testSetup(t) diff --git a/pkg/extensions/search/userprefs_test.go b/pkg/extensions/search/userprefs_test.go index 77807dad..cf215368 100644 --- a/pkg/extensions/search/userprefs_test.go +++ b/pkg/extensions/search/userprefs_test.go @@ -40,8 +40,8 @@ func TestUserData(t *testing.T) { simpleUser := "test" simpleUserPassword := "test123" - content := test.GetCredString(adminUser, adminPassword) + - test.GetCredString(simpleUser, simpleUserPassword) + content := test.GetBcryptCredString(adminUser, adminPassword) + + test.GetBcryptCredString(simpleUser, simpleUserPassword) htpasswdPath := test.MakeHtpasswdFileFromString(content) defer os.Remove(htpasswdPath) @@ -455,7 +455,7 @@ func TestChangingRepoState(t *testing.T) { forbiddenRepo := "forbidden" accesibleRepo := "accesible" - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(simpleUser, simpleUserPassword)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(simpleUser, simpleUserPassword)) defer os.Remove(htpasswdPath) conf := config.New() @@ -606,7 +606,7 @@ func TestGlobalSearchWithUserPrefFiltering(t *testing.T) { simpleUser := "simpleUser" simpleUserPassword := "simpleUserPass" - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(simpleUser, simpleUserPassword)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(simpleUser, simpleUserPassword)) defer os.Remove(htpasswdPath) conf.HTTP.Auth = &config.AuthConfig{ @@ -801,7 +801,7 @@ func TestExpandedRepoInfoWithUserPrefs(t *testing.T) { simpleUser := "simpleUser" simpleUserPassword := "simpleUserPass" - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(simpleUser, simpleUserPassword)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(simpleUser, simpleUserPassword)) defer os.Remove(htpasswdPath) conf.HTTP.Auth = &config.AuthConfig{ diff --git a/pkg/extensions/sync/sync_test.go b/pkg/extensions/sync/sync_test.go index e10edf61..d8bd2cb8 100644 --- a/pkg/extensions/sync/sync_test.go +++ b/pkg/extensions/sync/sync_test.go @@ -137,7 +137,7 @@ func makeUpstreamServer( var htpasswdPath string if basicAuth { - htpasswdPath = test.MakeHtpasswdFileFromString(test.GetCredString(username, password)) + htpasswdPath = test.MakeHtpasswdFileFromString(test.GetBcryptCredString(username, password)) srcConfig.HTTP.Auth = &config.AuthConfig{ HTPasswd: config.AuthHTPasswd{ Path: htpasswdPath, diff --git a/pkg/log/log_test.go b/pkg/log/log_test.go index ebcecb28..e53c8ad1 100644 --- a/pkg/log/log_test.go +++ b/pkg/log/log_test.go @@ -53,7 +53,7 @@ func TestAuditLogMessages(t *testing.T) { username, seedUser := test.GenerateRandomString() password, seedPass := test.GenerateRandomString() - htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password)) + htpasswdPath := test.MakeHtpasswdFileFromString(test.GetBcryptCredString(username, password)) defer os.Remove(htpasswdPath) diff --git a/pkg/test/common/fs.go b/pkg/test/common/fs.go index 2e7039a4..69aaad9c 100644 --- a/pkg/test/common/fs.go +++ b/pkg/test/common/fs.go @@ -2,6 +2,8 @@ package common import ( "context" + "crypto/rand" + "encoding/base64" "errors" "fmt" "io" @@ -12,6 +14,9 @@ import ( "strings" "time" + "github.com/GehirnInc/crypt" + _ "github.com/GehirnInc/crypt/sha256_crypt" + _ "github.com/GehirnInc/crypt/sha512_crypt" "golang.org/x/crypto/bcrypt" ) @@ -213,7 +218,7 @@ func ReadLogFileAndCountStringOccurence(logPath string, stringToMatch string, } } -func GetCredString(username, password string) string { +func GetBcryptCredString(username, password string) string { hash, err := bcrypt.GenerateFromPassword([]byte(password), 10) if err != nil { panic(err) @@ -224,6 +229,85 @@ func GetCredString(username, password string) string { return usernameAndHash } +const ( + PrefixCryptSha256 = "$5$" + PrefixCryptSha512 = "$6$" + Separator = "$" +) + +// generateSecureRandomString should only be used in tests with length = 16. +func generateSecureRandomString(length int) (string, error) { + bytes := make([]byte, length) + + _, err := rand.Read(bytes) + if err != nil { + return "", err + } + + return base64.URLEncoding.EncodeToString(bytes)[:length], nil +} + +func shaCrypt(password string, rounds string, salt string, prefix string) string { + var ret string + + var strb strings.Builder + + strb.WriteString(prefix) + + if len(rounds) > 0 { + strb.WriteString(rounds) + strb.WriteString(Separator) + } + + strb.WriteString(salt) + totalSalt := strb.String() + + var err error + + switch prefix { + case PrefixCryptSha256: + crypter := crypt.SHA256.New() + ret, err = crypter.Generate([]byte(password), []byte(totalSalt)) + case PrefixCryptSha512: + crypter := crypt.SHA512.New() + ret, err = crypter.Generate([]byte(password), []byte(totalSalt)) + default: + panic("unsupported password hash") + } + + if err != nil { + panic(err) + } + + return ret +} + +func GetSHA256CredString(username, password string) string { + saltstr, err := generateSecureRandomString(16) + if err != nil { + panic(err) + } + + hash := shaCrypt(password, "rounds=5000", saltstr, PrefixCryptSha256) + + usernameAndHash := fmt.Sprintf("%s:%s\n", username, hash) + + return usernameAndHash +} + +func GetSHA512CredString(username, password string) string { + saltstr, err := generateSecureRandomString(16) + if err != nil { + panic(err) + } + + hash := shaCrypt(password, "rounds=5000", saltstr, PrefixCryptSha512) + + usernameAndHash := fmt.Sprintf("%s:%s\n", username, hash) + + return usernameAndHash +} + func MakeHtpasswdFileFromString(fileContent string) string { htpasswdFile, err := os.CreateTemp("", "htpasswd-") if err != nil { diff --git a/pkg/test/common/fs_test.go b/pkg/test/common/fs_test.go index c784bb7b..765e4ad2 100644 --- a/pkg/test/common/fs_test.go +++ b/pkg/test/common/fs_test.go @@ -268,8 +268,8 @@ func TestGetProjectRootDir(t *testing.T) { }) } -func TestGetCredString(t *testing.T) { - Convey("GetCredString panics", t, func() { +func TestGetBcryptCredString(t *testing.T) { + Convey("GetBcryptCredString panics", t, func() { passwordSize := 100 pass := make([]byte, passwordSize) @@ -277,7 +277,7 @@ func TestGetCredString(t *testing.T) { pass[i] = 'Y' } - f := func() { tcommon.GetCredString("testUser", string(pass)) } + f := func() { tcommon.GetBcryptCredString("testUser", string(pass)) } So(f, ShouldPanicWith, bcrypt.ErrPasswordTooLong) }) } diff --git a/pkg/test/image-utils/upload_test.go b/pkg/test/image-utils/upload_test.go index 94f737f1..98dd8fb9 100644 --- a/pkg/test/image-utils/upload_test.go +++ b/pkg/test/image-utils/upload_test.go @@ -178,7 +178,7 @@ func TestUploadImage(t *testing.T) { user1 := "test" password1 := "test" - testString1 := tcommon.GetCredString(user1, password1) + testString1 := tcommon.GetBcryptCredString(user1, password1) htpasswdPath := tcommon.MakeHtpasswdFileFromString(testString1) defer os.Remove(htpasswdPath) @@ -495,7 +495,7 @@ func TestInjectUploadImageWithBasicAuth(t *testing.T) { user := "user" password := "password" - testString := tcommon.GetCredString(user, password) + testString := tcommon.GetBcryptCredString(user, password) htpasswdPath := tcommon.MakeHtpasswdFileFromString(testString) defer os.Remove(htpasswdPath) diff --git a/test/blackbox/ci.sh b/test/blackbox/ci.sh index 832c4e1d..e7cd607f 100755 --- a/test/blackbox/ci.sh +++ b/test/blackbox/ci.sh @@ -15,7 +15,7 @@ tests=("pushpull" "pushpull_authn" "delete_images" "referrers" "metadata" "anony "annotations" "detect_manifest_collision" "cve" "sync" "sync_docker" "sync_replica_cluster" "scrub" "garbage_collect" "metrics" "metrics_minimal" "multiarch_index" "docker_compat" "redis_local" "redis_session_store" "events_nats" "events_http" "events_nats_lint_failure" "events_http_lint_failure" "events_sink_failure" "events_config_decoding" - "fips140") + "fips140" "fips140_authn") for test in ${tests[*]}; do ${BATS} ${BATS_FLAGS} ${SCRIPTPATH}/${test}.bats > ${test}.log & pids+=($!) diff --git a/test/blackbox/fips140_authn.bats b/test/blackbox/fips140_authn.bats new file mode 100644 index 00000000..be40b7dc --- /dev/null +++ b/test/blackbox/fips140_authn.bats @@ -0,0 +1,389 @@ +load helpers_zot +load ../port_helper + +function verify_prerequisites { + if [ ! $(command -v curl) ]; then + echo "you need to install curl as a prerequisite to running the tests" >&3 + return 1 + fi + + if [ ! $(command -v jq) ]; then + echo "you need to install jq as a prerequisite to running the tests" >&3 + return 1 + fi + + if [ ! $(command -v htpasswd) ]; then + echo "you need to install htpasswd as a prerequisite to running the tests" >&3 + return 1 + fi + + if [ ! $(command -v mkpasswd) ]; then + echo "you need to install mkpasswd as a prerequisite to running the tests" >&3 + return 1 + fi + + return 0 +} + +function setup_file() { + # Verify prerequisites are available + if ! $(verify_prerequisites); then + exit 1 + fi + + # Download test data to folder common for the entire suite, not just this file + skopeo --insecure-policy copy --format=oci docker://ghcr.io/project-zot/test-images/busybox:1.36 oci:${TEST_DATA_DIR}/busybox:1.36 + + # Setup zot server + local zot_root_dir=${BATS_FILE_TMPDIR}/zot + local zot_config_file=${BATS_FILE_TMPDIR}/zot_config.json + ZOT_LOG_FILE=${zot_root_dir}/zot-log.json + local zot_htpasswd_file=${BATS_FILE_TMPDIR}/zot_htpasswd + zot_port=$(get_free_port_for_service "zot") + echo ${zot_port} > ${BATS_FILE_TMPDIR}/zot.port + htpasswd -Bbn ${AUTH_USER} ${AUTH_PASS} >> ${zot_htpasswd_file} # bcrypt + echo "${AUTH_USER2}:$(echo ${AUTH_PASS2} | mkpasswd -s -R 1 -m sha-256)" >> ${zot_htpasswd_file} # sha256 + echo "${AUTH_USER3}:$(echo ${AUTH_PASS3} | mkpasswd -s -R 1 -m sha-512)" >> ${zot_htpasswd_file} # sha512 + echo "${AUTH_USER4}:$(echo ${AUTH_PASS4} | mkpasswd -s -R 0 -m sha-256)" >> ${zot_htpasswd_file} # sha256 zero rounds + echo "${AUTH_USER5}:$(echo ${AUTH_PASS5} | mkpasswd -s -R 0 -m sha-512)" >> ${zot_htpasswd_file} # sha512 zero rounds + + echo ${zot_root_dir} >&3 + + mkdir -p ${zot_root_dir} + + touch ${ZOT_LOG_FILE} + cat > ${zot_config_file}</dev/null || true + fi +} + +function teardown_file() { + zot_stop_all + unset GODEBUG +} + +# Helper function to verify authentication and image push +# Args: $1=username, $2=password, $3=hash_type, $4=should_succeed (true/false) +function verify_auth_and_push() { + local user="$1" + local pass="$2" + local hash_type="$3" + local should_succeed="$4" + + zot_port=`cat ${BATS_FILE_TMPDIR}/zot.port` + + # anonymous authn is set for zot, so all auth is ignored for the /v2/ ping + run regctl registry login localhost:${zot_port} -u ${user} -p ${pass} + [ "$status" -eq 0 ] + + run regctl image copy ocidir://${TEST_DATA_DIR}/busybox:1.36 localhost:${zot_port}/test-${hash_type} + + if [ "$should_succeed" = "true" ]; then + [ "$status" -eq 0 ] + else + [ "$status" -eq 1 ] + log_output | jq 'contains("htpasswd bcrypt failed since fips140 is enabled")' | grep true + fi +} + +@test "push image with regclient - setup registry" { + zot_port=`cat ${BATS_FILE_TMPDIR}/zot.port` + run regctl registry set localhost:${zot_port} --tls disabled + [ "$status" -eq 0 ] +} + +@test "push image with bcrypt auth (should fail in FIPS mode)" { + verify_auth_and_push "${AUTH_USER}" "${AUTH_PASS}" "bcrypt" "false" +} + +@test "push image with SHA256 auth (should succeed)" { + verify_auth_and_push "${AUTH_USER2}" "${AUTH_PASS2}" "sha256" "true" +} + +@test "push image with SHA512 auth (should succeed)" { + verify_auth_and_push "${AUTH_USER3}" "${AUTH_PASS3}" "sha512" "true" +} + +@test "push image with SHA256 auth with 0 rounds (should succeed)" { + verify_auth_and_push "${AUTH_USER4}" "${AUTH_PASS4}" "sha256-0rounds" "true" +} + +@test "push image with SHA512 auth with 0 rounds (should succeed)" { + verify_auth_and_push "${AUTH_USER5}" "${AUTH_PASS5}" "sha512-0rounds" "true" +} + +@test "pull image with SHA256 auth" { + zot_port=`cat ${BATS_FILE_TMPDIR}/zot.port` + run regctl registry set localhost:${zot_port} --tls disabled + [ "$status" -eq 0 ] + run regctl registry login localhost:${zot_port} -u ${AUTH_USER2} -p ${AUTH_PASS2} + [ "$status" -eq 0 ] + run regctl image copy localhost:${zot_port}/test-sha256 ocidir://${TEST_DATA_DIR}/busybox:sha256-pulled + [ "$status" -eq 0 ] +} + +@test "pull image with SHA512 auth" { + zot_port=`cat ${BATS_FILE_TMPDIR}/zot.port` + run regctl registry set localhost:${zot_port} --tls disabled + [ "$status" -eq 0 ] + run regctl registry login localhost:${zot_port} -u ${AUTH_USER3} -p ${AUTH_PASS3} + [ "$status" -eq 0 ] + run regctl image copy localhost:${zot_port}/test-sha512 ocidir://${TEST_DATA_DIR}/busybox:sha512-pulled + [ "$status" -eq 0 ] +} + +@test "pull image with SHA256 auth with 0 rounds" { + zot_port=`cat ${BATS_FILE_TMPDIR}/zot.port` + run regctl registry set localhost:${zot_port} --tls disabled + [ "$status" -eq 0 ] + run regctl registry login localhost:${zot_port} -u ${AUTH_USER4} -p ${AUTH_PASS4} + [ "$status" -eq 0 ] + run regctl image copy localhost:${zot_port}/test-sha256-0rounds ocidir://${TEST_DATA_DIR}/busybox:sha256-0rounds-pulled + [ "$status" -eq 0 ] +} + +@test "pull image with SHA512 auth with 0 rounds" { + zot_port=`cat ${BATS_FILE_TMPDIR}/zot.port` + run regctl registry set localhost:${zot_port} --tls disabled + [ "$status" -eq 0 ] + run regctl registry login localhost:${zot_port} -u ${AUTH_USER5} -p ${AUTH_PASS5} + [ "$status" -eq 0 ] + run regctl image copy localhost:${zot_port}/test-sha512-0rounds ocidir://${TEST_DATA_DIR}/busybox:sha512-0rounds-pulled + [ "$status" -eq 0 ] +} + +@test "push OCI artifact with SHA256 auth" { + zot_port=`cat ${BATS_FILE_TMPDIR}/zot.port` + run regctl registry set localhost:${zot_port} --tls disabled + [ "$status" -eq 0 ] + run regctl registry login localhost:${zot_port} -u ${AUTH_USER2} -p ${AUTH_PASS2} + [ "$status" -eq 0 ] + run regctl artifact put localhost:${zot_port}/artifact-sha256:demo < ${BATS_FILE_TMPDIR}/mnist.onnx.check + sha256_out=$(sha256sum ${BATS_FILE_TMPDIR}/mnist.onnx.check | awk '{print $1}') + [ "$sha256_in" = "$sha256_out" ] +} diff --git a/test/blackbox/helpers_zot.bash b/test/blackbox/helpers_zot.bash index 626c8d6e..c6684ad8 100644 --- a/test/blackbox/helpers_zot.bash +++ b/test/blackbox/helpers_zot.bash @@ -8,6 +8,15 @@ ZB_PATH=${ROOT_DIR}/bin/zb-${OS}-${ARCH} TEST_DATA_DIR=${BATS_FILE_TMPDIR}/test/data AUTH_USER=poweruser AUTH_PASS=sup*rSecr9T +# additional creds for sha256/sha512 based password hashes +AUTH_USER2=poweruser2 +AUTH_PASS2=sup*rSecr2T +AUTH_USER3=poweruser3 +AUTH_PASS3=sup*rSecr3T +AUTH_USER4=poweruser4 +AUTH_PASS4=sup*rSecr4T +AUTH_USER5=poweruser5 +AUTH_PASS5=sup*rSecr5T mkdir -p ${TEST_DATA_DIR} diff --git a/test/ports.json b/test/ports.json index bfa52769..d6c84c4a 100644 --- a/test/ports.json +++ b/test/ports.json @@ -408,5 +408,11 @@ "begin": 11430, "end": 11439 } + }, + "blackbox/fips140_authn.bats": { + "zot": { + "begin": 11440, + "end": 11449 + } } }