Skip to content

Commit

Permalink
Add a loginWithTLS function to V1
Browse files Browse the repository at this point in the history
If implemented, a client will accept custom certificate and certificate authority
to authenticate with a remote registry.

Signed-off-by: Soule BA <bah.soule@gmail.com>
  • Loading branch information
souleb committed Jun 9, 2022
1 parent d3ec86b commit 0014e15
Show file tree
Hide file tree
Showing 12 changed files with 558 additions and 15 deletions.
7 changes: 2 additions & 5 deletions go.mod
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
)
10 changes: 3 additions & 7 deletions go.sum
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand All @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
12 changes: 12 additions & 0 deletions pkg/auth/client_opts.go
Expand Up @@ -30,6 +30,9 @@ type (
Hostname string
Username string
Secret string
CertFile string
KeyFile string
CAFile string
Insecure bool
UserAgent string
}
Expand Down Expand Up @@ -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) {
Expand Down
1 change: 0 additions & 1 deletion pkg/auth/docker/client_test.go
Expand Up @@ -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"
)

Expand Down
16 changes: 14 additions & 2 deletions pkg/auth/docker/login.go
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
220 changes: 220 additions & 0 deletions 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")
}

0 comments on commit 0014e15

Please sign in to comment.