Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a loginWithTLS function to V1 #171

Merged
merged 1 commit into from Jun 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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")
}