From e48299107226a62663433ff63491609828a7d29b Mon Sep 17 00:00:00 2001 From: Soule BA Date: Wed, 8 Jun 2022 16:49:29 +0200 Subject: [PATCH] Add a loginWithTLS function to V1 If implemented, a client will accept custom certificate and certificate authority to authenticate with a remote registry. Signed-off-by: Soule BA --- go.mod | 7 +- go.sum | 10 +- pkg/auth/client_opts.go | 12 + pkg/auth/docker/client_test.go | 1 - pkg/auth/docker/login.go | 16 +- pkg/auth/docker/login_tls.go | 220 +++++++++++++++++++ pkg/auth/docker/login_tls_test.go | 185 ++++++++++++++++ pkg/auth/docker/testdata/tls/ca-cert.pem | 22 ++ pkg/auth/docker/testdata/tls/client-cert.pem | 22 ++ pkg/auth/docker/testdata/tls/client-key.pem | 28 +++ pkg/auth/docker/testdata/tls/server-cert.pem | 22 ++ pkg/auth/docker/testdata/tls/server-key.pem | 28 +++ 12 files changed, 558 insertions(+), 15 deletions(-) create mode 100644 pkg/auth/docker/login_tls.go create mode 100644 pkg/auth/docker/login_tls_test.go create mode 100644 pkg/auth/docker/testdata/tls/ca-cert.pem create mode 100644 pkg/auth/docker/testdata/tls/client-cert.pem create mode 100644 pkg/auth/docker/testdata/tls/client-key.pem create mode 100644 pkg/auth/docker/testdata/tls/server-cert.pem create mode 100644 pkg/auth/docker/testdata/tls/server-key.pem diff --git a/go.mod b/go.mod index c9652ac8..5e516a69 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,9 @@ require ( github.com/containerd/containerd v1.6.6 github.com/distribution/distribution/v3 v3.0.0-20220526142353-ffbd94cbe269 github.com/docker/cli v20.10.17+incompatible + github.com/docker/distribution v2.7.1+incompatible github.com/docker/docker v20.10.17+incompatible + github.com/docker/go-connections v0.4.0 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 @@ -28,9 +30,7 @@ require ( github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/docker/distribution v2.7.1+incompatible // indirect github.com/docker/docker-credential-helpers v0.6.4 // indirect - github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect github.com/docker/go-metrics v0.0.1 // indirect github.com/docker/go-units v0.4.0 // indirect @@ -43,12 +43,10 @@ require ( github.com/gorilla/mux v1.8.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/klauspost/compress v1.13.6 // indirect - github.com/kr/text v0.2.0 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect github.com/moby/locker v1.0.1 // indirect github.com/moby/term v0.0.0-20210610120745-9d4ed1856297 // indirect github.com/morikuni/aec v1.0.0 // indirect - github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.12.1 // indirect github.com/prometheus/client_model v0.2.0 // indirect @@ -64,7 +62,6 @@ require ( google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368 // indirect google.golang.org/grpc v1.43.0 // indirect google.golang.org/protobuf v1.27.1 // indirect - gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index dd3d19a4..f1a743ef 100644 --- a/go.sum +++ b/go.sum @@ -80,7 +80,6 @@ github.com/containerd/cgroups v1.0.3 h1:ADZftAkglvCiD44c77s5YmMqaP2pzVCFZvBmAlBd github.com/containerd/containerd v1.6.6 h1:xJNPhbrmz8xAMDNoVjHy9YHtWwEQNS+CDkcIRh7t8Y0= github.com/containerd/containerd v1.6.6/go.mod h1:ZoP1geJldzCVY3Tonoz7b1IXk8rIX0Nltt5QE4OMNk0= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg= @@ -214,11 +213,11 @@ github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47e github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= @@ -237,8 +236,6 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 h1:rc3tiVYb5z54aKaDfakKn0dDjIyPpTtszkjuMzyt7ec= @@ -600,9 +597,8 @@ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/pkg/auth/client_opts.go b/pkg/auth/client_opts.go index 4f71454b..f385b240 100644 --- a/pkg/auth/client_opts.go +++ b/pkg/auth/client_opts.go @@ -30,6 +30,9 @@ type ( Hostname string Username string Secret string + CertFile string + KeyFile string + CAFile string Insecure bool UserAgent string } @@ -70,6 +73,15 @@ func WithLoginInsecure() LoginOption { } } +// WithLoginTLS returns a function that sets the tls settings on login. +func WithLoginTLS(certFile, keyFile, caFile string) LoginOption { + return func(settings *LoginSettings) { + settings.CertFile = certFile + settings.KeyFile = keyFile + settings.CAFile = caFile + } +} + // WithLoginUserAgent returns a function that sets the UserAgent setting on login. func WithLoginUserAgent(userAgent string) LoginOption { return func(settings *LoginSettings) { diff --git a/pkg/auth/docker/client_test.go b/pkg/auth/docker/client_test.go index 6468efa4..1b64e4ad 100644 --- a/pkg/auth/docker/client_test.go +++ b/pkg/auth/docker/client_test.go @@ -32,7 +32,6 @@ import ( "github.com/phayes/freeport" "github.com/stretchr/testify/suite" "golang.org/x/crypto/bcrypt" - iface "oras.land/oras-go/pkg/auth" ) diff --git a/pkg/auth/docker/login.go b/pkg/auth/docker/login.go index 4e1d1ee6..bd155e53 100644 --- a/pkg/auth/docker/login.go +++ b/pkg/auth/docker/login.go @@ -25,6 +25,8 @@ import ( iface "oras.land/oras-go/pkg/auth" ) +const IndexHostname = "index.docker.io" + // Login logs in to a docker registry identified by the hostname. // Deprecated: use LoginWithOpts func (c *Client) Login(ctx context.Context, hostname, username, secret string, insecure bool) error { @@ -78,9 +80,19 @@ func (c *Client) login(settings *iface.LoginSettings) error { if userAgent == "" { userAgent = "oras" } - if _, token, err := remote.Auth(ctx, &cred, userAgent); err != nil { + + var token string + if (settings.CertFile != "" && settings.KeyFile != "") || settings.CAFile != "" { + _, token, err = c.loginWithTLS(ctx, remote, settings.CertFile, settings.KeyFile, settings.CAFile, &cred, userAgent) + } else { + _, token, err = remote.Auth(ctx, &cred, userAgent) + } + + if err != nil { return err - } else if token != "" { + } + + if token != "" { cred.Username = "" cred.Password = "" cred.IdentityToken = token diff --git a/pkg/auth/docker/login_tls.go b/pkg/auth/docker/login_tls.go new file mode 100644 index 00000000..cb92df81 --- /dev/null +++ b/pkg/auth/docker/login_tls.go @@ -0,0 +1,220 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package docker + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "net/http" + "net/url" + "strings" + "time" + + "github.com/docker/distribution/registry/api/errcode" + "github.com/docker/distribution/registry/client/auth" + "github.com/docker/distribution/registry/client/transport" + "github.com/docker/docker/api/types" + "github.com/docker/docker/errdefs" + "github.com/docker/docker/registry" + "github.com/docker/go-connections/tlsconfig" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +// The following functions are adapted from github.com/docker/docker/registry +// We need these to support passing in a transport that has custom TLS configuration +// They are not exposed in the docker/registry package that's why they are copied here + +type loginCredentialStore struct { + authConfig *types.AuthConfig +} + +func (lcs loginCredentialStore) Basic(*url.URL) (string, string) { + return lcs.authConfig.Username, lcs.authConfig.Password +} + +func (lcs loginCredentialStore) RefreshToken(*url.URL, string) string { + return lcs.authConfig.IdentityToken +} + +func (lcs loginCredentialStore) SetRefreshToken(u *url.URL, service, token string) { + lcs.authConfig.IdentityToken = token +} + +// loginWithTLS tries to login to the v2 registry server. +// A custom tls.Config is used to override the default TLS configuration of the different registry endpoints. +// The tls.Config is created using the provided certificate, certificate key and certificate authority. +func (c *Client) loginWithTLS(ctx context.Context, service registry.Service, certFile, keyFile, caFile string, authConfig *types.AuthConfig, userAgent string) (string, string, error) { + tlsConfig, err := tlsconfig.Client(tlsconfig.Options{CAFile: caFile, CertFile: certFile, KeyFile: keyFile}) + if err != nil { + return "", "", err + } + + endpoints, err := c.getEndpoints(authConfig.ServerAddress, service) + if err != nil { + return "", "", err + } + + var status, token string + for _, endpoint := range endpoints { + endpoint.TLSConfig = tlsConfig + status, token, err = loginV2(authConfig, endpoint, userAgent) + + if err != nil { + if isNotAuthorizedError(err) { + return "", "", err + } + + logrus.WithError(err).Infof("Error logging in to endpoint, trying next endpoint") + continue + } + + return status, token, nil + } + + return "", "", err +} + +// getEndpoints returns the endpoints for the given hostname. +func (c *Client) getEndpoints(address string, service registry.Service) ([]registry.APIEndpoint, error) { + var registryHostName = IndexHostname + + if address != "" { + if !strings.HasPrefix(address, "https://") && !strings.HasPrefix(address, "http://") { + address = fmt.Sprintf("https://%s", address) + } + u, err := url.Parse(address) + if err != nil { + return nil, errdefs.InvalidParameter(errors.Wrapf(err, "unable to parse server address")) + } + registryHostName = u.Host + } + + // Lookup endpoints for authentication using "LookupPushEndpoints", which + // excludes mirrors to prevent sending credentials of the upstream registry + // to a mirror. + endpoints, err := service.LookupPushEndpoints(registryHostName) + if err != nil { + return nil, errdefs.InvalidParameter(err) + } + + return endpoints, nil +} + +// loginV2 tries to login to the v2 registry server. The given registry +// endpoint will be pinged to get authorization challenges. These challenges +// will be used to authenticate against the registry to validate credentials. +func loginV2(authConfig *types.AuthConfig, endpoint registry.APIEndpoint, userAgent string) (string, string, error) { + var ( + endpointStr = strings.TrimRight(endpoint.URL.String(), "/") + "/v2/" + modifiers = registry.Headers(userAgent, nil) + authTransport = transport.NewTransport(newTransport(endpoint.TLSConfig), modifiers...) + credentialAuthConfig = *authConfig + creds = loginCredentialStore{authConfig: &credentialAuthConfig} + ) + + logrus.Debugf("attempting v2 login to registry endpoint %s", endpointStr) + + loginClient, err := v2AuthHTTPClient(endpoint.URL, authTransport, modifiers, creds, nil) + if err != nil { + return "", "", err + } + + req, err := http.NewRequest(http.MethodGet, endpointStr, nil) + if err != nil { + return "", "", err + } + + resp, err := loginClient.Do(req) + if err != nil { + err = translateV2AuthError(err) + return "", "", err + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusOK { + return "Login Succeeded", credentialAuthConfig.IdentityToken, nil + } + + // TODO(dmcgowan): Attempt to further interpret result, status code and error code string + return "", "", errors.Errorf("login attempt to %s failed with status: %d %s", endpointStr, resp.StatusCode, http.StatusText(resp.StatusCode)) +} + +// newTransport returns a new HTTP transport. If tlsConfig is nil, it uses the +// default TLS configuration. +func newTransport(tlsConfig *tls.Config) *http.Transport { + if tlsConfig == nil { + tlsConfig = tlsconfig.ServerDefault() + } + + direct := &net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + } + + return &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: direct.DialContext, + TLSHandshakeTimeout: 10 * time.Second, + TLSClientConfig: tlsConfig, + // TODO(dmcgowan): Call close idle connections when complete and use keep alive + DisableKeepAlives: true, + } +} + +func v2AuthHTTPClient(endpoint *url.URL, authTransport http.RoundTripper, modifiers []transport.RequestModifier, creds auth.CredentialStore, scopes []auth.Scope) (*http.Client, error) { + challengeManager, _, err := registry.PingV2Registry(endpoint, authTransport) + if err != nil { + return nil, err + } + + tokenHandlerOptions := auth.TokenHandlerOptions{ + Transport: authTransport, + Credentials: creds, + OfflineAccess: true, + ClientID: registry.AuthClientID, + Scopes: scopes, + } + tokenHandler := auth.NewTokenHandlerWithOptions(tokenHandlerOptions) + basicHandler := auth.NewBasicHandler(creds) + modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler)) + + return &http.Client{ + Transport: transport.NewTransport(authTransport, modifiers...), + Timeout: 15 * time.Second, + }, nil +} + +func translateV2AuthError(err error) error { + switch e := err.(type) { + case *url.Error: + switch e2 := e.Err.(type) { + case errcode.Error: + switch e2.Code { + case errcode.ErrorCodeUnauthorized: + return errdefs.Unauthorized(err) + } + } + } + + return err +} + +func isNotAuthorizedError(err error) bool { + return strings.Contains(err.Error(), "401 Unauthorized") +} diff --git a/pkg/auth/docker/login_tls_test.go b/pkg/auth/docker/login_tls_test.go new file mode 100644 index 00000000..e7ba1333 --- /dev/null +++ b/pkg/auth/docker/login_tls_test.go @@ -0,0 +1,185 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package docker + +import ( + "context" + "fmt" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "testing" + "time" + + "github.com/distribution/distribution/v3/configuration" + "github.com/distribution/distribution/v3/registry" + _ "github.com/distribution/distribution/v3/registry/auth/htpasswd" + _ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory" + "github.com/phayes/freeport" + "github.com/stretchr/testify/suite" + "golang.org/x/crypto/bcrypt" + + iface "oras.land/oras-go/pkg/auth" +) + +const ( + tlsServerKey = "./testdata/tls/server-key.pem" + tlsServerCert = "./testdata/tls/server-cert.pem" + tlsCA = "./testdata/tls/ca-cert.pem" + tlsKey = "./testdata/tls/client-key.pem" + tlsCert = "./testdata/tls/client-cert.pem" +) + +type DockerClientWithCATestSuite struct { + suite.Suite + DockerRegistryHost string + Client *Client + TempTestDir string +} + +func (suite *DockerClientWithCATestSuite) SetupSuite() { + tempDir, err := ioutil.TempDir("", "oras_auth_docker_test_ca") + suite.Nil(err, "no error creating temp directory for test") + suite.TempTestDir = tempDir + + // Create client + client, err := NewClient(filepath.Join(suite.TempTestDir, testConfig)) + suite.Nil(err, "no error creating client") + var ok bool + suite.Client, ok = client.(*Client) + suite.True(ok, "NewClient returns a *docker.Client inside") + + // Create htpasswd file with bcrypt + secret, err := bcrypt.GenerateFromPassword([]byte(testPassword), bcrypt.DefaultCost) + suite.Nil(err, "no error generating bcrypt password for test htpasswd file") + authRecord := fmt.Sprintf("%s:%s\n", testUsername, string(secret)) + htpasswdPath := filepath.Join(suite.TempTestDir, testHtpasswd) + err = ioutil.WriteFile(htpasswdPath, []byte(authRecord), 0644) + suite.Nil(err, "no error creating test htpasswd file") + + // Registry config + config := &configuration.Configuration{} + port, err := freeport.GetFreePort() + suite.Nil(err, "no error finding free port for test registry") + suite.DockerRegistryHost = fmt.Sprintf("localhost:%d", port) + + // HTTP config + config.HTTP.Addr = fmt.Sprintf(":%d", port) + config.HTTP.DrainTimeout = time.Duration(10) * time.Second + + config.Log.Level = "debug" + + // TLS config + // this set tlsConf.ClientAuth = tls.RequireAndVerifyClientCert in the + // server tls config + config.HTTP.TLS.Certificate = tlsServerCert + config.HTTP.TLS.Key = tlsServerKey + config.HTTP.TLS.ClientCAs = []string{tlsCA} + + // Storage config + config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}} + + // Auth config + config.Auth = configuration.Auth{ + "htpasswd": configuration.Parameters{ + "realm": "localhost", + "path": htpasswdPath, + }, + } + dockerRegistry, err := registry.NewRegistry(context.Background(), config) + suite.Nil(err, "no error finding free port for test registry") + + // Start Docker registry + go func() { + err := dockerRegistry.ListenAndServe() + suite.Nil(err, "no error starting docker registry with ca") + }() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + for { + select { + case <-ctx.Done(): + suite.FailNow("docker registry timed out") + default: + } + req, err := http.NewRequestWithContext( + ctx, + http.MethodGet, + fmt.Sprintf("http://%s/v2/", suite.DockerRegistryHost), + nil, + ) + suite.Nil(err, "no error in generate a /v2/ request") + + resp, err := http.DefaultClient.Do(req) + if err == nil { + resp.Body.Close() + break + } + time.Sleep(time.Second) + } +} + +func (suite *DockerClientWithCATestSuite) TearDownSuite() { + os.RemoveAll(suite.TempTestDir) +} + +func (suite *DockerClientWithCATestSuite) Test_1_LoginWithTLSOpts() { + var err error + + opts := []iface.LoginOption{ + iface.WithLoginContext(newContext()), + iface.WithLoginHostname(suite.DockerRegistryHost), + iface.WithLoginUsername("oscar"), + iface.WithLoginSecret("opponent"), + iface.WithLoginTLS( + tlsCert, + tlsKey, + tlsCA, + ), + } + err = suite.Client.LoginWithOpts(opts...) + suite.NotNil(err, "error logging into registry with invalid credentials (LoginWithOpts)") + + opts = []iface.LoginOption{ + iface.WithLoginContext(newContext()), + iface.WithLoginHostname(suite.DockerRegistryHost), + iface.WithLoginUsername(testUsername), + iface.WithLoginSecret(testPassword), + iface.WithLoginTLS( + tlsCert, + tlsKey, + tlsCA, + ), + } + err = suite.Client.LoginWithOpts(opts...) + suite.Nil(err, "no error logging into registry with valid credentials (LoginWithOpts)") +} + +func (suite *DockerClientWithCATestSuite) Test_2_Logout() { + var err error + + err = suite.Client.Logout(newContext(), "non-existing-host:42") + suite.NotNil(err, "error logging out of registry that has no entry") + + err = suite.Client.Logout(newContext(), suite.DockerRegistryHost) + suite.Nil(err, "no error logging out of registry") +} + +func TestDockerClientWithCATestSuite(t *testing.T) { + suite.Run(t, new(DockerClientWithCATestSuite)) +} diff --git a/pkg/auth/docker/testdata/tls/ca-cert.pem b/pkg/auth/docker/testdata/tls/ca-cert.pem new file mode 100644 index 00000000..d10da16d --- /dev/null +++ b/pkg/auth/docker/testdata/tls/ca-cert.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDozCCAougAwIBAgIUH3U7M1T4wAMS1SkMCFXYpvwItiowDQYJKoZIhvcNAQEL +BQAwYDELMAkGA1UEBhMCZnIxDDAKBgNVBAgMA2lkZjEOMAwGA1UEBwwFcGFyaXMx +DTALBgNVBAoMBG9yYXMxEDAOBgNVBAsMB29yYXMtZ28xEjAQBgNVBAMMCWxvY2Fs +aG9zdDAgFw0yMjA2MDMxMzIzMjNaGA8zMDIxMTAwNDEzMjMyM1owYDELMAkGA1UE +BhMCZnIxDDAKBgNVBAgMA2lkZjEOMAwGA1UEBwwFcGFyaXMxDTALBgNVBAoMBG9y +YXMxEDAOBgNVBAsMB29yYXMtZ28xEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAMsLKz1N2BMUOUUZxfXi5FH/7xhr4JHv +KYTJHzT1bTx6PQLaIDLLssfyqpM461j0HcucTWi8ib72zCh85MmouQH4swlyDkZT +6fDG788ckLyry7R6HLsEeG3M7qYAxO1cY0UNvaOzDJZUyWWyjn705aB0vn+V4FEF +b4j7YlVtDwj4VD0ZgiR7HA5zJ/kNH3egQLCPtO6y8WlZcZRV6DXfE/4qHbNrnz9d +wgnw79ME3L7UbiOPKAZhtqmyMhmP0JLUhBZuNriMNMeOV3Q65/tKE3AdkMXfkPzL +AqxOb3gOW+/iYEFGOjsL2xRoW3Qf2LomJZOOtvQyixFPsC5sOCXbKUsCAwEAAaNT +MFEwHQYDVR0OBBYEFP82l8p04apawGrtqtaRdE81VZvLMB8GA1UdIwQYMBaAFP82 +l8p04apawGrtqtaRdE81VZvLMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBADIsL+USl7iu4CxCaenHpnODCRp90MBIRSTflJbzz/4hwb6xSf0s58TH +V/+OA3PNk5qDTIqQnrHBcLo/gJYknSCYrODDw+mMyyYi+QpDncB1IRKRO2mrVsE8 +ZG3anvICJ/gkySvqpt0vMmeye2Wvn1HxqVo+lWUwydwCz3WNgS4J+2688AlPqsRa +UvqaA2ggcCmy3vcvkKHcFM6dmgHBwKLb3aR8o7vVQaj1zDuXvBjCtbC3/bI7P53j +OJdknADj0L+9CDhkFdvERAN7JGCQPvgvtnJdWy8Teef4hXDFGdS4lzFVxegGVyKb +1cXQZANlnAZxdb42LNd3FhKhRfAwFl4= +-----END CERTIFICATE----- diff --git a/pkg/auth/docker/testdata/tls/client-cert.pem b/pkg/auth/docker/testdata/tls/client-cert.pem new file mode 100644 index 00000000..1fc467fc --- /dev/null +++ b/pkg/auth/docker/testdata/tls/client-cert.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDuDCCAqCgAwIBAgIUYE+dLcRyrIQJZC1c24BtYsb/KqYwDQYJKoZIhvcNAQEL +BQAwYDELMAkGA1UEBhMCZnIxDDAKBgNVBAgMA2lkZjEOMAwGA1UEBwwFcGFyaXMx +DTALBgNVBAoMBG9yYXMxEDAOBgNVBAsMB29yYXMtZ28xEjAQBgNVBAMMCWxvY2Fs +aG9zdDAgFw0yMjA2MDMxNTEzMjhaGA8yNzQ3MTIyMDE1MTMyOFowXTELMAkGA1UE +BhMCZGUxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5MRUwEwYDVQQKDAxP +cmdhbml6YXRpb24xGDAWBgNVBAMMD2xvY2FsaG9zdC5sb2NhbDCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBALGR7SjUVv0OBuPUZky7ylYb4ZMbMGFQcilV +Rpt0t5Dhb0g9CeXgmFo5A18/tv1cLMyp+LvU6Lkkx3aHe5z/KqOae9NLw40ZZXfL +W1jmefcbrVguh2zyvfs6c5iuNwyKv+oh07utkyV20fAMnLSmaTP1ioEMlgKl87jO +lkfLAeKTCOI0X7lSuNOfZxVrG/fRvdP9pGdB2NQrCouo9DWHKS/HSMgNRvjP+zb3 +K5UX312euckoh1+ABJaOnLMRyAysQtZWRVxwyKgNHz3QecDwd40UKcp899DaFW7d +EK2Lu+O6Ef3zeYbNWG0kPKFMUird35+BU5ordcJe3CE6dIrXSucCAwEAAaNrMGkw +HwYDVR0jBBgwFoAU/zaXynThqlrAau2q1pF0TzVVm8swCQYDVR0TBAIwADALBgNV +HQ8EBAMCBPAwLgYDVR0RBCcwJYIJbG9jYWxob3N0ghhjbGllbnQucmVnaXN0cnkt +dGVzdC5jb20wDQYJKoZIhvcNAQELBQADggEBAEx8VqYJzgCjPlyt/LOXHYWOXNdK +DVo8Vpihv4T4nF/IkH4SQQDZj8ZT1Snm27O2nmYDCTm51Zh8bDf+LQhp82Gd9Lwd +wgNf64o9zsxSqOlFiPuMIoHQoW4fNPPkRMs7UThF/BEuv5JMXWI4Vzoyb9SVyKvA +CLHYj+OGGABgZb7Kx6Y0Zu2UwjPKEHh1yg+K1u00oKX6j0dHockJ1Z9uIKaxFs2J +wZ7cSV1vQne9e9c39I2n17d0c4/YEhzybSoKVsmax7e00ivxDGHCIbq3EnlDpSOJ +WQ4YmSrBU/lkWVBVvOLPycBGTJtaFxyTbkKraSb8RTkhnVgRbC3yPDWSLZE= +-----END CERTIFICATE----- diff --git a/pkg/auth/docker/testdata/tls/client-key.pem b/pkg/auth/docker/testdata/tls/client-key.pem new file mode 100644 index 00000000..7d26cee2 --- /dev/null +++ b/pkg/auth/docker/testdata/tls/client-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCxke0o1Fb9Dgbj +1GZMu8pWG+GTGzBhUHIpVUabdLeQ4W9IPQnl4JhaOQNfP7b9XCzMqfi71Oi5JMd2 +h3uc/yqjmnvTS8ONGWV3y1tY5nn3G61YLods8r37OnOYrjcMir/qIdO7rZMldtHw +DJy0pmkz9YqBDJYCpfO4zpZHywHikwjiNF+5UrjTn2cVaxv30b3T/aRnQdjUKwqL +qPQ1hykvx0jIDUb4z/s29yuVF99dnrnJKIdfgASWjpyzEcgMrELWVkVccMioDR89 +0HnA8HeNFCnKfPfQ2hVu3RCti7vjuhH983mGzVhtJDyhTFIq3d+fgVOaK3XCXtwh +OnSK10rnAgMBAAECggEBAJDm7Aktfe7yy1yPrwtfEzZBekp5HXnME9z24a9qB4IK +9KyWYsVcCfdWHxJTBBisZ0J8UaW1kFPFV5+53322HC11myZJ5UH3P34KTHi3Wz/z +LfmPvqOBUtb4U+x7/iAV+ZYuFIJKmpEv7RFeuWIRijgEXwoAI0n18nUU4D2lw9wH +lHQjUKWE6r6QP2oYb2RR+YuNPQlaHnMvlB6+BXOElf/XjJ+ntaN4VAXy9YDw48lU +kb0hrBN80fHLtlpneDGUDIdj2YoNEZXbUvwVHdr7IVmmMBUTtpdr6g6c3uq+wAHP +NBzeLCkll/2Og+QWAUstSVmRgB5IkvIIDpmmdifNfgECgYEA1u6e806mOZSZK1ce +x4UmQr2XYv2Pm/gPf6ik+E/WWFHE6arhKQ+X6WwCKyUPS0BqnIp77V+cNBHCYUbn +ktIgJN5GhnOm5Z5BcKxiC29WJUY3vLdB3zUY1gUoX55Hqf9Qq/gJQ88coH0pMPBM +1jeuKnBJMitlImRyX4I94e0pDQcCgYEA03++TKDLvofYte40nkut51H0GKLCZieE +7s4/5SSPkZ8aUphsFPke/SkDf7XkBdazi1V7RbAxwarCUCYiYiSy9PoqeJJw1ZUz +/hvnjDn4ZAXM8l0BrMFswzYNKgjOJbnoi/3wSzRBKR8XG9bcw3DC7d226o61i/Q3 +T8UQyxIUOyECgYEA0owfGanKHE39Xf+SH1HIEUk4q7hInjl6tUu3j6hmCU7Q3zaO +K3MjFX7BARLk0Irh5Uej7vziP//FsxWKdMFyy6sS21MgA3/sCzxTL3B5qzoGD9BV +rxqModubmU+sVFPP6yanrM2O9YimeVJtcsxIyToF+ieYgwBzKiykAlYZCLECfyEL +1LAkb7FViIbksVQKfyGlrH4s8DMF+b9WeqVTERuvwG0nY7vjMPRddC6APSCsa2FZ ++ejpntyj0bi0PKsZEN02OWyddQPqTDVoJsXCSQ1X2q4D2j8j+dqGl4f52DwM0EkP +ZHxbrdK/CN1QtS3UcKC6A9qicbtsbTgJkpYoPQECgYEA0XNEDYB7XrqC7+OOV9n1 +pXL8CHekFMz1GbGar+uFE4+qhFLunigVLVFgfG4hFg9GnqkKhOqEDWCRtbFcGVWk +v2UZjV4jRTHgiqIiSnJZ5LBkyAqpX/p2YMicxPIaQA5po5DND4+eDcLdBRQR+M0/ +l4WrUTxibYD7wgexafn9Xwg= +-----END PRIVATE KEY----- diff --git a/pkg/auth/docker/testdata/tls/server-cert.pem b/pkg/auth/docker/testdata/tls/server-cert.pem new file mode 100644 index 00000000..9b402f24 --- /dev/null +++ b/pkg/auth/docker/testdata/tls/server-cert.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDuDCCAqCgAwIBAgIUYE+dLcRyrIQJZC1c24BtYsb/KqcwDQYJKoZIhvcNAQEL +BQAwYDELMAkGA1UEBhMCZnIxDDAKBgNVBAgMA2lkZjEOMAwGA1UEBwwFcGFyaXMx +DTALBgNVBAoMBG9yYXMxEDAOBgNVBAsMB29yYXMtZ28xEjAQBgNVBAMMCWxvY2Fs +aG9zdDAgFw0yMjA2MDMxNTE1MzlaGA8yNzQ3MTIyMDE1MTUzOVowXTELMAkGA1UE +BhMCZGUxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5MRUwEwYDVQQKDAxP +cmdhbml6YXRpb24xGDAWBgNVBAMMD2xvY2FsaG9zdC5sb2NhbDCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAKnCh1kt1l6T6YSoZPck+pVR2xBibFJrAOW7 +1cGlRHSUfW9pZTGuPhB0S4jUDvJTwh1hoCyAXKV9/U/5CGeUu08BlVvy4XCVhCoM +/uKiXUyWYSweEaiteF6rRvBFxysTVMhwGHOqoPeIiyQJrLo3P2csqIW6gVYdMgs+ +Fl4bXMbSqDhcE+iMPIf3q266Kj8LC2Z7FFZyrWRlHotj4S+kQVV4b15X49k9dsgp +bmAxceGlRxhaD2Lu/tABRWRnRaAPZNVmZ1bNFaPPeqykyn6O3SDUBVEKbWk1pQwr +CTsunVvzg2QlErcgCY8LAZwnQGDX9PdNfnXdLUsM9BRK2+yeM2sCAwEAAaNrMGkw +HwYDVR0jBBgwFoAU/zaXynThqlrAau2q1pF0TzVVm8swCQYDVR0TBAIwADALBgNV +HQ8EBAMCBPAwLgYDVR0RBCcwJYIJbG9jYWxob3N0ghhjbGllbnQucmVnaXN0cnkt +dGVzdC5jb20wDQYJKoZIhvcNAQELBQADggEBAFuBHRkLGNVpZrfPcAvvpbE9kVD/ +fK9kcb+V3unmiN4koG7aEY7RM+bjeHWOp2RB3nCbyaIzP/QIRHBaNRjrXcchupTM +uN8w0Un9k4jGaia5U5sthYmYqOtZ9E5sm8dy8rQnCF7kpF78HpP4pil8H+ZFhTn5 +b8mCmWY1YZ4rbD8iDqAtaR8sSxDOiffQvRq8mRWtLitYiewX/vaonnE5ECOUj6KD +sAh9UygvDMBCOo6bI1D45lVExm1xd8MZjCqr+kXM3JiGjW2KMcD/j7nWsO1ltWTe +YTzyTjbd7x3DDfvjb7pXNB/lyN9arIuIi3PusltHjXnai1Aiiq/9aYRDB4A= +-----END CERTIFICATE----- diff --git a/pkg/auth/docker/testdata/tls/server-key.pem b/pkg/auth/docker/testdata/tls/server-key.pem new file mode 100644 index 00000000..fd725410 --- /dev/null +++ b/pkg/auth/docker/testdata/tls/server-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCpwodZLdZek+mE +qGT3JPqVUdsQYmxSawDlu9XBpUR0lH1vaWUxrj4QdEuI1A7yU8IdYaAsgFylff1P ++QhnlLtPAZVb8uFwlYQqDP7iol1MlmEsHhGorXheq0bwRccrE1TIcBhzqqD3iIsk +Cay6Nz9nLKiFuoFWHTILPhZeG1zG0qg4XBPojDyH96tuuio/CwtmexRWcq1kZR6L +Y+EvpEFVeG9eV+PZPXbIKW5gMXHhpUcYWg9i7v7QAUVkZ0WgD2TVZmdWzRWjz3qs +pMp+jt0g1AVRCm1pNaUMKwk7Lp1b84NkJRK3IAmPCwGcJ0Bg1/T3TX513S1LDPQU +StvsnjNrAgMBAAECggEAPKDbEgCK5TBSYCx8EsBENnwLjuh341+P7HyPNR+/ORun +SNkNaRbf16SRLYb+55D+bNvnBr25WOmMRLmeRNAbrXXVUAeIeFsN3q7RgUOXqriy +b701dVau/LCTH1DsdvfqecN/GCBCaOV9PBuwcrk0jbanYUtg0c6PhvBfsLB5DBCv +V0KvJAr3KIoSZkuTJ902XjOCq8ngcZz02EEAqJgZ8w0sOS/hzK+SsqdB8rLGMdBy +6uhrpZ+q46wth8wBfVGoLYqduos0nWRYQEmdQeqrlFU9BAGk0ezmCXZEubGzfeUY +4//6whjfhuR+a4yY+IXbU+SRkt7OF1X+G/XaRFQpAQKBgQDRKGOouaXusFFIF+n1 +bpYpb5FXy+hUOlLiPVB3ySz7HxRN0vD4ZUBFexcA2PuuN4bqEUNR71UTH6qbEB4V +wICqee7oEc9kBCKCkYOvnW1LgTkpAlZ6V8h6GQPUOtrGCExTEe6gmIvdwrlc5yOJ +ORWN4npYPWz8fejxiGuGLWt4wQKBgQDPx1iTW/BEDERhBDFWDFLda8TpafaELPcb +G5cybveFFvd8tujjfAYfzH4ngfPCK6z07tCrUSJZBqUB6nudtrRyDFZSN3vrldJQ +MEidoTxe81q1iCOGAV+bGP47zgLN3AODbuGFD+emMJesA0Vr5Nt+nw1oVQCkDskr +lXnP0+irKwKBgQCjg4Ld7j0V7EuvI9ro4AqO3ETxMV0xM+OGMU8ORn1e2T5DVSzD +1Ew5xXAHXprr6UcVCGfrz0JgT/pNfX03niY4oFiwnvPWihD8qIwdp+JBDAZG1CgN +P0FgitveeCB+fxBERRmb/YYi2U+WnLDAX5tFMBBbmbHxdvG8md0NqcaCQQKBgBiW +IzIUwAKdXP4J6/idrrSKyxs/sa8ETx1DD4olPV7fT9vPHRHGHEdpAEiWhQyl9Gpr +U//hsunL8nyejZAlDYctVI2YCS7gZKmefQlCCg3GSCaQ/Hsf8Hs+4t6ayxQnA7dq +yH7hWez9dQUiwfU5eIusMH73CANhyIZCws5H6hFPAoGATydSGy6aopnzciCny4it +8oFXt4OaefnFUKbe1Wy4UmKKc0od/KA2iKz3NqffWOjjQtsr8q2te/z9680fKFHN +t1KHFrNnZtkQ63lc1Z66MUileQsFhsxvHZKetJfYzpiwLI3fHov4MGeFEp/8DsSz +rXmVS6phVQa68n9xSvEHszM= +-----END PRIVATE KEY-----