Merge pull request #3169 from d-luu/configurable_ciphersuites
Added flag for user-configurable cipher suites Thanks, @d-luu for putting the effort into this. Much appreciated!master
						commit
						5a76dc8df1
					
				
							
								
								
									
										2
									
								
								Makefile
								
								
								
								
							
							
						
						
									
										2
									
								
								Makefile
								
								
								
								
							|  | @ -50,7 +50,7 @@ version/version.go: | |||
| 
 | ||||
| check: ## run all linters (TODO: enable "unused", "varcheck", "ineffassign", "unconvert", "staticheck", "goimports", "structcheck")
 | ||||
| 	@echo "$(WHALE) $@" | ||||
| 	@GO111MODULE=off golangci-lint run | ||||
| 	@golangci-lint run | ||||
| 
 | ||||
| test: ## run tests, except integration test with test.short
 | ||||
| 	@echo "$(WHALE) $@" | ||||
|  |  | |||
|  | @ -111,6 +111,9 @@ type Configuration struct { | |||
| 			// Specifies the lowest TLS version allowed
 | ||||
| 			MinimumTLS string `yaml:"minimumtls,omitempty"` | ||||
| 
 | ||||
| 			// Specifies a list of cipher suites allowed
 | ||||
| 			CipherSuites []string `yaml:"ciphersuites,omitempty"` | ||||
| 
 | ||||
| 			// LetsEncrypt is used to configuration setting up TLS through
 | ||||
| 			// Let's Encrypt instead of manually specifying certificate and
 | ||||
| 			// key. If a TLS certificate is specified, the Let's Encrypt
 | ||||
|  |  | |||
|  | @ -80,11 +80,12 @@ var configStruct = Configuration{ | |||
| 		RelativeURLs bool          `yaml:"relativeurls,omitempty"` | ||||
| 		DrainTimeout time.Duration `yaml:"draintimeout,omitempty"` | ||||
| 		TLS          struct { | ||||
| 			Certificate string   `yaml:"certificate,omitempty"` | ||||
| 			Key         string   `yaml:"key,omitempty"` | ||||
| 			ClientCAs   []string `yaml:"clientcas,omitempty"` | ||||
| 			MinimumTLS  string   `yaml:"minimumtls,omitempty"` | ||||
| 			LetsEncrypt struct { | ||||
| 			Certificate  string   `yaml:"certificate,omitempty"` | ||||
| 			Key          string   `yaml:"key,omitempty"` | ||||
| 			ClientCAs    []string `yaml:"clientcas,omitempty"` | ||||
| 			MinimumTLS   string   `yaml:"minimumtls,omitempty"` | ||||
| 			CipherSuites []string `yaml:"ciphersuites,omitempty"` | ||||
| 			LetsEncrypt  struct { | ||||
| 				CacheFile string   `yaml:"cachefile,omitempty"` | ||||
| 				Email     string   `yaml:"email,omitempty"` | ||||
| 				Hosts     []string `yaml:"hosts,omitempty"` | ||||
|  | @ -103,11 +104,12 @@ var configStruct = Configuration{ | |||
| 		} `yaml:"http2,omitempty"` | ||||
| 	}{ | ||||
| 		TLS: struct { | ||||
| 			Certificate string   `yaml:"certificate,omitempty"` | ||||
| 			Key         string   `yaml:"key,omitempty"` | ||||
| 			ClientCAs   []string `yaml:"clientcas,omitempty"` | ||||
| 			MinimumTLS  string   `yaml:"minimumtls,omitempty"` | ||||
| 			LetsEncrypt struct { | ||||
| 			Certificate  string   `yaml:"certificate,omitempty"` | ||||
| 			Key          string   `yaml:"key,omitempty"` | ||||
| 			ClientCAs    []string `yaml:"clientcas,omitempty"` | ||||
| 			MinimumTLS   string   `yaml:"minimumtls,omitempty"` | ||||
| 			CipherSuites []string `yaml:"ciphersuites,omitempty"` | ||||
| 			LetsEncrypt  struct { | ||||
| 				CacheFile string   `yaml:"cachefile,omitempty"` | ||||
| 				Email     string   `yaml:"email,omitempty"` | ||||
| 				Hosts     []string `yaml:"hosts,omitempty"` | ||||
|  |  | |||
|  | @ -795,7 +795,10 @@ http: | |||
|     clientcas: | ||||
|       - /path/to/ca.pem | ||||
|       - /path/to/another/ca.pem | ||||
|     minimumtls: tls1.0 | ||||
|     minimumtls: tls1.2 | ||||
|     ciphersuites: | ||||
|       - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 | ||||
|       - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 | ||||
|     letsencrypt: | ||||
|       cachefile: /path/to/cache-file | ||||
|       email: emailused@letsencrypt.com | ||||
|  | @ -831,10 +834,49 @@ and proxy connections to the registry server. | |||
| 
 | ||||
| | Parameter | Required | Description                                           | | ||||
| |-----------|----------|-------------------------------------------------------| | ||||
| | `certificate` | yes  | Absolute path to the x509 certificate file.           | | ||||
| | `key`         | yes  | Absolute path to the x509 private key file.           | | ||||
| | `clientcas`   | no   | An array of absolute paths to x509 CA files.          | | ||||
| | `minimumtls`  | no   | Minimum TLS version allowed (tls1.0, tls1.1, tls1.2). Defaults to tls1.0 | | ||||
| | `certificate`  | yes  | Absolute path to the x509 certificate file.           | | ||||
| | `key`          | yes  | Absolute path to the x509 private key file.           | | ||||
| | `clientcas`    | no   | An array of absolute paths to x509 CA files.          | | ||||
| | `minimumtls`   | no   | Minimum TLS version allowed (tls1.0, tls1.1, tls1.2, tls1.3). Defaults to tls1.2 | | ||||
| | `ciphersuites` | no   | Cipher suites allowed. Please see below for allowed values and default. | | ||||
| 
 | ||||
| Available cipher suites: | ||||
| - TLS_RSA_WITH_RC4_128_SHA | ||||
| - TLS_RSA_WITH_3DES_EDE_CBC_SHA | ||||
| - TLS_RSA_WITH_AES_128_CBC_SHA | ||||
| - TLS_RSA_WITH_AES_256_CBC_SHA | ||||
| - TLS_RSA_WITH_AES_128_CBC_SHA256 | ||||
| - TLS_RSA_WITH_AES_128_GCM_SHA256 | ||||
| - TLS_RSA_WITH_AES_256_GCM_SHA384 | ||||
| - TLS_ECDHE_ECDSA_WITH_RC4_128_SHA | ||||
| - TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA | ||||
| - TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA | ||||
| - TLS_ECDHE_RSA_WITH_RC4_128_SHA | ||||
| - TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA | ||||
| - TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA | ||||
| - TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA | ||||
| - TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 | ||||
| - TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 | ||||
| - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 | ||||
| - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 | ||||
| - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 | ||||
| - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 | ||||
| - TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 | ||||
| - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 | ||||
| - TLS_AES_128_GCM_SHA256 | ||||
| - TLS_AES_256_GCM_SHA384 | ||||
| - TLS_CHACHA20_POLY1305_SHA256 | ||||
| 
 | ||||
| Default cipher suites: | ||||
| - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 | ||||
| - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 | ||||
| - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 | ||||
| - TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 | ||||
| - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 | ||||
| - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 | ||||
| - TLS_AES_128_GCM_SHA256 | ||||
| - TLS_CHACHA20_POLY1305_SHA256 | ||||
| - TLS_AES_256_GCM_SHA384 | ||||
| 
 | ||||
| ### `letsencrypt` | ||||
| 
 | ||||
|  |  | |||
|  | @ -9,6 +9,7 @@ import ( | |||
| 	"net/http" | ||||
| 	"os" | ||||
| 	"os/signal" | ||||
| 	"strings" | ||||
| 	"syscall" | ||||
| 	"time" | ||||
| 
 | ||||
|  | @ -34,6 +35,60 @@ import ( | |||
| 	"github.com/distribution/distribution/v3/version" | ||||
| ) | ||||
| 
 | ||||
| // a map of TLS cipher suite names to constants in https://golang.org/pkg/crypto/tls/#pkg-constants
 | ||||
| var cipherSuites = map[string]uint16{ | ||||
| 	// TLS 1.0 - 1.2 cipher suites
 | ||||
| 	"TLS_RSA_WITH_RC4_128_SHA":                      tls.TLS_RSA_WITH_RC4_128_SHA, | ||||
| 	"TLS_RSA_WITH_3DES_EDE_CBC_SHA":                 tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, | ||||
| 	"TLS_RSA_WITH_AES_128_CBC_SHA":                  tls.TLS_RSA_WITH_AES_128_CBC_SHA, | ||||
| 	"TLS_RSA_WITH_AES_256_CBC_SHA":                  tls.TLS_RSA_WITH_AES_256_CBC_SHA, | ||||
| 	"TLS_RSA_WITH_AES_128_CBC_SHA256":               tls.TLS_RSA_WITH_AES_128_CBC_SHA256, | ||||
| 	"TLS_RSA_WITH_AES_128_GCM_SHA256":               tls.TLS_RSA_WITH_AES_128_GCM_SHA256, | ||||
| 	"TLS_RSA_WITH_AES_256_GCM_SHA384":               tls.TLS_RSA_WITH_AES_256_GCM_SHA384, | ||||
| 	"TLS_ECDHE_ECDSA_WITH_RC4_128_SHA":              tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, | ||||
| 	"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA":          tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, | ||||
| 	"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA":          tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, | ||||
| 	"TLS_ECDHE_RSA_WITH_RC4_128_SHA":                tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, | ||||
| 	"TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA":           tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, | ||||
| 	"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA":            tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, | ||||
| 	"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA":            tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, | ||||
| 	"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256":       tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, | ||||
| 	"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256":         tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, | ||||
| 	"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256":         tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, | ||||
| 	"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256":       tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, | ||||
| 	"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384":         tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, | ||||
| 	"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384":       tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, | ||||
| 	"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256":   tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, | ||||
| 	"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, | ||||
| 	// TLS 1.3 cipher suites
 | ||||
| 	"TLS_AES_128_GCM_SHA256":       tls.TLS_AES_128_GCM_SHA256, | ||||
| 	"TLS_AES_256_GCM_SHA384":       tls.TLS_AES_256_GCM_SHA384, | ||||
| 	"TLS_CHACHA20_POLY1305_SHA256": tls.TLS_CHACHA20_POLY1305_SHA256, | ||||
| } | ||||
| 
 | ||||
| // a list of default ciphersuites to utilize
 | ||||
| var defaultCipherSuites = []uint16{ | ||||
| 	tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, | ||||
| 	tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, | ||||
| 	tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, | ||||
| 	tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, | ||||
| 	tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, | ||||
| 	tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, | ||||
| 	tls.TLS_AES_128_GCM_SHA256, | ||||
| 	tls.TLS_CHACHA20_POLY1305_SHA256, | ||||
| 	tls.TLS_AES_256_GCM_SHA384, | ||||
| } | ||||
| 
 | ||||
| // maps tls version strings to constants
 | ||||
| var defaultTLSVersionStr = "tls1.2" | ||||
| var tlsVersions = map[string]uint16{ | ||||
| 	// user specified values
 | ||||
| 	"tls1.0": tls.VersionTLS10, | ||||
| 	"tls1.1": tls.VersionTLS11, | ||||
| 	"tls1.2": tls.VersionTLS12, | ||||
| 	"tls1.3": tls.VersionTLS13, | ||||
| } | ||||
| 
 | ||||
| // this channel gets notified when process receives signal. It is global to ease unit testing
 | ||||
| var quit = make(chan os.Signal, 1) | ||||
| 
 | ||||
|  | @ -128,6 +183,35 @@ func NewRegistry(ctx context.Context, config *configuration.Configuration) (*Reg | |||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| // takes a list of cipher suites and converts it to a list of respective tls constants
 | ||||
| // if an empty list is provided, then the defaults will be used
 | ||||
| func getCipherSuites(names []string) ([]uint16, error) { | ||||
| 	if len(names) == 0 { | ||||
| 		return defaultCipherSuites, nil | ||||
| 	} | ||||
| 	cipherSuiteConsts := make([]uint16, len(names)) | ||||
| 	for i, name := range names { | ||||
| 		cipherSuiteConst, ok := cipherSuites[name] | ||||
| 		if !ok { | ||||
| 			return nil, fmt.Errorf("unknown TLS cipher suite '%s' specified for http.tls.cipherSuites", name) | ||||
| 		} | ||||
| 		cipherSuiteConsts[i] = cipherSuiteConst | ||||
| 	} | ||||
| 	return cipherSuiteConsts, nil | ||||
| } | ||||
| 
 | ||||
| // takes a list of cipher suite ids and converts it to a list of respective names
 | ||||
| func getCipherSuiteNames(ids []uint16) []string { | ||||
| 	if len(ids) == 0 { | ||||
| 		return nil | ||||
| 	} | ||||
| 	names := make([]string, len(ids)) | ||||
| 	for i, id := range ids { | ||||
| 		names[i] = tls.CipherSuiteName(id) | ||||
| 	} | ||||
| 	return names | ||||
| } | ||||
| 
 | ||||
| // ListenAndServe runs the registry's HTTP server.
 | ||||
| func (registry *Registry) ListenAndServe() error { | ||||
| 	config := registry.config | ||||
|  | @ -138,35 +222,27 @@ func (registry *Registry) ListenAndServe() error { | |||
| 	} | ||||
| 
 | ||||
| 	if config.HTTP.TLS.Certificate != "" || config.HTTP.TLS.LetsEncrypt.CacheFile != "" { | ||||
| 		var tlsMinVersion uint16 | ||||
| 		if config.HTTP.TLS.MinimumTLS == "" { | ||||
| 			tlsMinVersion = tls.VersionTLS10 | ||||
| 		} else { | ||||
| 			switch config.HTTP.TLS.MinimumTLS { | ||||
| 			case "tls1.0": | ||||
| 				tlsMinVersion = tls.VersionTLS10 | ||||
| 			case "tls1.1": | ||||
| 				tlsMinVersion = tls.VersionTLS11 | ||||
| 			case "tls1.2": | ||||
| 				tlsMinVersion = tls.VersionTLS12 | ||||
| 			default: | ||||
| 				return fmt.Errorf("unknown minimum TLS level '%s' specified for http.tls.minimumtls", config.HTTP.TLS.MinimumTLS) | ||||
| 			} | ||||
| 			dcontext.GetLogger(registry.app).Infof("restricting TLS to %s or higher", config.HTTP.TLS.MinimumTLS) | ||||
| 			config.HTTP.TLS.MinimumTLS = defaultTLSVersionStr | ||||
| 		} | ||||
| 		tlsMinVersion, ok := tlsVersions[config.HTTP.TLS.MinimumTLS] | ||||
| 		if !ok { | ||||
| 			return fmt.Errorf("unknown minimum TLS level '%s' specified for http.tls.minimumtls", config.HTTP.TLS.MinimumTLS) | ||||
| 		} | ||||
| 		dcontext.GetLogger(registry.app).Infof("restricting TLS version to %s or higher", config.HTTP.TLS.MinimumTLS) | ||||
| 
 | ||||
| 		tlsCipherSuites, err := getCipherSuites(config.HTTP.TLS.CipherSuites) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		dcontext.GetLogger(registry.app).Infof("restricting TLS cipher suites to: %s", strings.Join(getCipherSuiteNames(tlsCipherSuites), ",")) | ||||
| 
 | ||||
| 		tlsConf := &tls.Config{ | ||||
| 			ClientAuth:               tls.NoClientCert, | ||||
| 			NextProtos:               nextProtos(config), | ||||
| 			MinVersion:               tlsMinVersion, | ||||
| 			PreferServerCipherSuites: true, | ||||
| 			CipherSuites: []uint16{ | ||||
| 				tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, | ||||
| 				tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, | ||||
| 				tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, | ||||
| 				tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, | ||||
| 				tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, | ||||
| 				tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, | ||||
| 			}, | ||||
| 			CipherSuites:             tlsCipherSuites, | ||||
| 		} | ||||
| 
 | ||||
| 		if config.HTTP.TLS.LetsEncrypt.CacheFile != "" { | ||||
|  |  | |||
|  | @ -3,12 +3,24 @@ package registry | |||
| import ( | ||||
| 	"bufio" | ||||
| 	"context" | ||||
| 	"crypto" | ||||
| 	"crypto/ecdsa" | ||||
| 	"crypto/elliptic" | ||||
| 	"crypto/rand" | ||||
| 	"crypto/rsa" | ||||
| 	"crypto/tls" | ||||
| 	"crypto/x509" | ||||
| 	"crypto/x509/pkix" | ||||
| 	"encoding/pem" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"math/big" | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 	"path" | ||||
| 	"reflect" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
| 	"time" | ||||
| 
 | ||||
|  | @ -38,18 +50,30 @@ func TestNextProtos(t *testing.T) { | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| func setupRegistry() (*Registry, error) { | ||||
| type registryTLSConfig struct { | ||||
| 	cipherSuites    []string | ||||
| 	certificatePath string | ||||
| 	privateKeyPath  string | ||||
| 	certificate     *tls.Certificate | ||||
| } | ||||
| 
 | ||||
| func setupRegistry(tlsCfg *registryTLSConfig, addr string) (*Registry, error) { | ||||
| 	config := &configuration.Configuration{} | ||||
| 	// TODO: this needs to change to something ephemeral as the test will fail if there is any server
 | ||||
| 	// already listening on port 5000
 | ||||
| 	config.HTTP.Addr = ":5000" | ||||
| 	config.HTTP.Addr = addr | ||||
| 	config.HTTP.DrainTimeout = time.Duration(10) * time.Second | ||||
| 	if tlsCfg != nil { | ||||
| 		config.HTTP.TLS.CipherSuites = tlsCfg.cipherSuites | ||||
| 		config.HTTP.TLS.Certificate = tlsCfg.certificatePath | ||||
| 		config.HTTP.TLS.Key = tlsCfg.privateKeyPath | ||||
| 	} | ||||
| 	config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}} | ||||
| 	return NewRegistry(context.Background(), config) | ||||
| } | ||||
| 
 | ||||
| func TestGracefulShutdown(t *testing.T) { | ||||
| 	registry, err := setupRegistry() | ||||
| 	registry, err := setupRegistry(nil, ":5000") | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | @ -98,3 +122,227 @@ func TestGracefulShutdown(t *testing.T) { | |||
| 		t.Error("Body is not {}; ", string(body)) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestGetCipherSuite(t *testing.T) { | ||||
| 	resp, err := getCipherSuites([]string{"TLS_RSA_WITH_AES_128_CBC_SHA"}) | ||||
| 	if err != nil || len(resp) != 1 || resp[0] != tls.TLS_RSA_WITH_AES_128_CBC_SHA { | ||||
| 		t.Errorf("expected cipher suite %q, got %q", | ||||
| 			"TLS_RSA_WITH_AES_128_CBC_SHA", | ||||
| 			strings.Join(getCipherSuiteNames(resp), ","), | ||||
| 		) | ||||
| 	} | ||||
| 
 | ||||
| 	resp, err = getCipherSuites([]string{"TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_AES_128_GCM_SHA256"}) | ||||
| 	if err != nil || len(resp) != 2 || | ||||
| 		resp[0] != tls.TLS_RSA_WITH_AES_128_CBC_SHA || resp[1] != tls.TLS_AES_128_GCM_SHA256 { | ||||
| 		t.Errorf("expected cipher suites %q, got %q", | ||||
| 			"TLS_RSA_WITH_AES_128_CBC_SHA,TLS_AES_128_GCM_SHA256", | ||||
| 			strings.Join(getCipherSuiteNames(resp), ","), | ||||
| 		) | ||||
| 	} | ||||
| 
 | ||||
| 	_, err = getCipherSuites([]string{"TLS_RSA_WITH_AES_128_CBC_SHA", "bad_input"}) | ||||
| 	if err == nil { | ||||
| 		t.Error("did not return expected error about unknown cipher suite") | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func buildRegistryTLSConfig(name, keyType string, cipherSuites []string) (*registryTLSConfig, error) { | ||||
| 	var priv interface{} | ||||
| 	var pub crypto.PublicKey | ||||
| 	var err error | ||||
| 	switch keyType { | ||||
| 	case "rsa": | ||||
| 		priv, err = rsa.GenerateKey(rand.Reader, 2048) | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("failed to create rsa private key: %v", err) | ||||
| 		} | ||||
| 		rsaKey := priv.(*rsa.PrivateKey) | ||||
| 		pub = rsaKey.Public() | ||||
| 	case "ecdsa": | ||||
| 		priv, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader) | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("failed to create ecdsa private key: %v", err) | ||||
| 		} | ||||
| 		ecdsaKey := priv.(*ecdsa.PrivateKey) | ||||
| 		pub = ecdsaKey.Public() | ||||
| 	default: | ||||
| 		return nil, fmt.Errorf("unsupported key type: %v", keyType) | ||||
| 	} | ||||
| 
 | ||||
| 	notBefore := time.Now() | ||||
| 	notAfter := notBefore.Add(time.Minute) | ||||
| 	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) | ||||
| 	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to create serial number: %v", err) | ||||
| 	} | ||||
| 	cert := x509.Certificate{ | ||||
| 		SerialNumber: serialNumber, | ||||
| 		Subject: pkix.Name{ | ||||
| 			Organization: []string{"registry_test"}, | ||||
| 		}, | ||||
| 		NotBefore:             notBefore, | ||||
| 		NotAfter:              notAfter, | ||||
| 		KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, | ||||
| 		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, | ||||
| 		BasicConstraintsValid: true, | ||||
| 		IPAddresses:           []net.IP{net.ParseIP("127.0.0.1")}, | ||||
| 		DNSNames:              []string{"localhost"}, | ||||
| 		IsCA:                  true, | ||||
| 	} | ||||
| 	derBytes, err := x509.CreateCertificate(rand.Reader, &cert, &cert, pub, priv) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to create certificate: %v", err) | ||||
| 	} | ||||
| 	if _, err := os.Stat(os.TempDir()); os.IsNotExist(err) { | ||||
| 		os.Mkdir(os.TempDir(), 1777) | ||||
| 	} | ||||
| 
 | ||||
| 	certPath := path.Join(os.TempDir(), name+".pem") | ||||
| 	certOut, err := os.Create(certPath) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to create pem: %v", err) | ||||
| 	} | ||||
| 	if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil { | ||||
| 		return nil, fmt.Errorf("failed to write data to %s: %v", certPath, err) | ||||
| 	} | ||||
| 	if err := certOut.Close(); err != nil { | ||||
| 		return nil, fmt.Errorf("error closing %s: %v", certPath, err) | ||||
| 	} | ||||
| 
 | ||||
| 	keyPath := path.Join(os.TempDir(), name+".key") | ||||
| 	keyOut, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to open %s for writing: %v", keyPath, err) | ||||
| 	} | ||||
| 	privBytes, err := x509.MarshalPKCS8PrivateKey(priv) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("unable to marshal private key: %v", err) | ||||
| 	} | ||||
| 	if err := pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}); err != nil { | ||||
| 		return nil, fmt.Errorf("failed to write data to key.pem: %v", err) | ||||
| 	} | ||||
| 	if err := keyOut.Close(); err != nil { | ||||
| 		return nil, fmt.Errorf("error closing %s: %v", keyPath, err) | ||||
| 	} | ||||
| 
 | ||||
| 	tlsCert := tls.Certificate{ | ||||
| 		Certificate: [][]byte{derBytes}, | ||||
| 		PrivateKey:  priv, | ||||
| 	} | ||||
| 
 | ||||
| 	tlsTestCfg := registryTLSConfig{ | ||||
| 		cipherSuites:    cipherSuites, | ||||
| 		certificatePath: certPath, | ||||
| 		privateKeyPath:  keyPath, | ||||
| 		certificate:     &tlsCert, | ||||
| 	} | ||||
| 
 | ||||
| 	return &tlsTestCfg, nil | ||||
| } | ||||
| 
 | ||||
| func TestRegistrySupportedCipherSuite(t *testing.T) { | ||||
| 	name := "registry_test_server_supported_cipher" | ||||
| 	cipherSuites := []string{"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"} | ||||
| 	serverTLS, err := buildRegistryTLSConfig(name, "rsa", cipherSuites) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	registry, err := setupRegistry(serverTLS, ":5001") | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	// run registry server
 | ||||
| 	var errchan chan error | ||||
| 	go func() { | ||||
| 		errchan <- registry.ListenAndServe() | ||||
| 	}() | ||||
| 	select { | ||||
| 	case err = <-errchan: | ||||
| 		t.Fatalf("Error listening: %v", err) | ||||
| 	default: | ||||
| 	} | ||||
| 
 | ||||
| 	// Wait for some unknown random time for server to start listening
 | ||||
| 	time.Sleep(3 * time.Second) | ||||
| 
 | ||||
| 	// send tls request with server supported cipher suite
 | ||||
| 	clientCipherSuites, err := getCipherSuites(cipherSuites) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	clientTLS := tls.Config{ | ||||
| 		InsecureSkipVerify: true, | ||||
| 		CipherSuites:       clientCipherSuites, | ||||
| 	} | ||||
| 	dialer := net.Dialer{ | ||||
| 		Timeout: time.Second * 5, | ||||
| 	} | ||||
| 	conn, err := tls.DialWithDialer(&dialer, "tcp", "127.0.0.1:5001", &clientTLS) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	fmt.Fprintf(conn, "GET /v2/ HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n") | ||||
| 
 | ||||
| 	resp, err := http.ReadResponse(bufio.NewReader(conn), nil) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	if resp.Status != "200 OK" { | ||||
| 		t.Error("response status is not 200 OK: ", resp.Status) | ||||
| 	} | ||||
| 	if body, err := ioutil.ReadAll(resp.Body); err != nil || string(body) != "{}" { | ||||
| 		t.Error("Body is not {}; ", string(body)) | ||||
| 	} | ||||
| 
 | ||||
| 	// send stop signal
 | ||||
| 	quit <- os.Interrupt | ||||
| 	time.Sleep(100 * time.Millisecond) | ||||
| } | ||||
| 
 | ||||
| func TestRegistryUnsupportedCipherSuite(t *testing.T) { | ||||
| 	name := "registry_test_server_unsupported_cipher" | ||||
| 	serverTLS, err := buildRegistryTLSConfig(name, "rsa", []string{"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA358"}) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	registry, err := setupRegistry(serverTLS, ":5002") | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	// run registry server
 | ||||
| 	var errchan chan error | ||||
| 	go func() { | ||||
| 		errchan <- registry.ListenAndServe() | ||||
| 	}() | ||||
| 	select { | ||||
| 	case err = <-errchan: | ||||
| 		t.Fatalf("Error listening: %v", err) | ||||
| 	default: | ||||
| 	} | ||||
| 
 | ||||
| 	// Wait for some unknown random time for server to start listening
 | ||||
| 	time.Sleep(3 * time.Second) | ||||
| 
 | ||||
| 	// send tls request with server unsupported cipher suite
 | ||||
| 	clientTLS := tls.Config{ | ||||
| 		InsecureSkipVerify: true, | ||||
| 		CipherSuites:       []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256}, | ||||
| 	} | ||||
| 	dialer := net.Dialer{ | ||||
| 		Timeout: time.Second * 5, | ||||
| 	} | ||||
| 	_, err = tls.DialWithDialer(&dialer, "tcp", "127.0.0.1:5002", &clientTLS) | ||||
| 	if err == nil { | ||||
| 		t.Error("expected TLS connection to timeout") | ||||
| 	} | ||||
| 
 | ||||
| 	// send stop signal
 | ||||
| 	quit <- os.Interrupt | ||||
| 	time.Sleep(100 * time.Millisecond) | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue