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")
 | check: ## run all linters (TODO: enable "unused", "varcheck", "ineffassign", "unconvert", "staticheck", "goimports", "structcheck")
 | ||||||
| 	@echo "$(WHALE) $@" | 	@echo "$(WHALE) $@" | ||||||
| 	@GO111MODULE=off golangci-lint run | 	@golangci-lint run | ||||||
| 
 | 
 | ||||||
| test: ## run tests, except integration test with test.short
 | test: ## run tests, except integration test with test.short
 | ||||||
| 	@echo "$(WHALE) $@" | 	@echo "$(WHALE) $@" | ||||||
|  |  | ||||||
|  | @ -111,6 +111,9 @@ type Configuration struct { | ||||||
| 			// Specifies the lowest TLS version allowed
 | 			// Specifies the lowest TLS version allowed
 | ||||||
| 			MinimumTLS string `yaml:"minimumtls,omitempty"` | 			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
 | 			// LetsEncrypt is used to configuration setting up TLS through
 | ||||||
| 			// Let's Encrypt instead of manually specifying certificate and
 | 			// Let's Encrypt instead of manually specifying certificate and
 | ||||||
| 			// key. If a TLS certificate is specified, the Let's Encrypt
 | 			// key. If a TLS certificate is specified, the Let's Encrypt
 | ||||||
|  |  | ||||||
|  | @ -80,11 +80,12 @@ var configStruct = Configuration{ | ||||||
| 		RelativeURLs bool          `yaml:"relativeurls,omitempty"` | 		RelativeURLs bool          `yaml:"relativeurls,omitempty"` | ||||||
| 		DrainTimeout time.Duration `yaml:"draintimeout,omitempty"` | 		DrainTimeout time.Duration `yaml:"draintimeout,omitempty"` | ||||||
| 		TLS          struct { | 		TLS          struct { | ||||||
| 			Certificate string   `yaml:"certificate,omitempty"` | 			Certificate  string   `yaml:"certificate,omitempty"` | ||||||
| 			Key         string   `yaml:"key,omitempty"` | 			Key          string   `yaml:"key,omitempty"` | ||||||
| 			ClientCAs   []string `yaml:"clientcas,omitempty"` | 			ClientCAs    []string `yaml:"clientcas,omitempty"` | ||||||
| 			MinimumTLS  string   `yaml:"minimumtls,omitempty"` | 			MinimumTLS   string   `yaml:"minimumtls,omitempty"` | ||||||
| 			LetsEncrypt struct { | 			CipherSuites []string `yaml:"ciphersuites,omitempty"` | ||||||
|  | 			LetsEncrypt  struct { | ||||||
| 				CacheFile string   `yaml:"cachefile,omitempty"` | 				CacheFile string   `yaml:"cachefile,omitempty"` | ||||||
| 				Email     string   `yaml:"email,omitempty"` | 				Email     string   `yaml:"email,omitempty"` | ||||||
| 				Hosts     []string `yaml:"hosts,omitempty"` | 				Hosts     []string `yaml:"hosts,omitempty"` | ||||||
|  | @ -103,11 +104,12 @@ var configStruct = Configuration{ | ||||||
| 		} `yaml:"http2,omitempty"` | 		} `yaml:"http2,omitempty"` | ||||||
| 	}{ | 	}{ | ||||||
| 		TLS: struct { | 		TLS: struct { | ||||||
| 			Certificate string   `yaml:"certificate,omitempty"` | 			Certificate  string   `yaml:"certificate,omitempty"` | ||||||
| 			Key         string   `yaml:"key,omitempty"` | 			Key          string   `yaml:"key,omitempty"` | ||||||
| 			ClientCAs   []string `yaml:"clientcas,omitempty"` | 			ClientCAs    []string `yaml:"clientcas,omitempty"` | ||||||
| 			MinimumTLS  string   `yaml:"minimumtls,omitempty"` | 			MinimumTLS   string   `yaml:"minimumtls,omitempty"` | ||||||
| 			LetsEncrypt struct { | 			CipherSuites []string `yaml:"ciphersuites,omitempty"` | ||||||
|  | 			LetsEncrypt  struct { | ||||||
| 				CacheFile string   `yaml:"cachefile,omitempty"` | 				CacheFile string   `yaml:"cachefile,omitempty"` | ||||||
| 				Email     string   `yaml:"email,omitempty"` | 				Email     string   `yaml:"email,omitempty"` | ||||||
| 				Hosts     []string `yaml:"hosts,omitempty"` | 				Hosts     []string `yaml:"hosts,omitempty"` | ||||||
|  |  | ||||||
|  | @ -795,7 +795,10 @@ http: | ||||||
|     clientcas: |     clientcas: | ||||||
|       - /path/to/ca.pem |       - /path/to/ca.pem | ||||||
|       - /path/to/another/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: |     letsencrypt: | ||||||
|       cachefile: /path/to/cache-file |       cachefile: /path/to/cache-file | ||||||
|       email: emailused@letsencrypt.com |       email: emailused@letsencrypt.com | ||||||
|  | @ -831,10 +834,49 @@ and proxy connections to the registry server. | ||||||
| 
 | 
 | ||||||
| | Parameter | Required | Description                                           | | | Parameter | Required | Description                                           | | ||||||
| |-----------|----------|-------------------------------------------------------| | |-----------|----------|-------------------------------------------------------| | ||||||
| | `certificate` | yes  | Absolute path to the x509 certificate file.           | | | `certificate`  | yes  | Absolute path to the x509 certificate file.           | | ||||||
| | `key`         | yes  | Absolute path to the x509 private key file.           | | | `key`          | yes  | Absolute path to the x509 private key file.           | | ||||||
| | `clientcas`   | no   | An array of absolute paths to x509 CA files.          | | | `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 | | | `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` | ### `letsencrypt` | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -9,6 +9,7 @@ import ( | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"os" | 	"os" | ||||||
| 	"os/signal" | 	"os/signal" | ||||||
|  | 	"strings" | ||||||
| 	"syscall" | 	"syscall" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
|  | @ -34,6 +35,60 @@ import ( | ||||||
| 	"github.com/distribution/distribution/v3/version" | 	"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
 | // this channel gets notified when process receives signal. It is global to ease unit testing
 | ||||||
| var quit = make(chan os.Signal, 1) | var quit = make(chan os.Signal, 1) | ||||||
| 
 | 
 | ||||||
|  | @ -128,6 +183,35 @@ func NewRegistry(ctx context.Context, config *configuration.Configuration) (*Reg | ||||||
| 	}, nil | 	}, 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.
 | // ListenAndServe runs the registry's HTTP server.
 | ||||||
| func (registry *Registry) ListenAndServe() error { | func (registry *Registry) ListenAndServe() error { | ||||||
| 	config := registry.config | 	config := registry.config | ||||||
|  | @ -138,35 +222,27 @@ func (registry *Registry) ListenAndServe() error { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if config.HTTP.TLS.Certificate != "" || config.HTTP.TLS.LetsEncrypt.CacheFile != "" { | 	if config.HTTP.TLS.Certificate != "" || config.HTTP.TLS.LetsEncrypt.CacheFile != "" { | ||||||
| 		var tlsMinVersion uint16 |  | ||||||
| 		if config.HTTP.TLS.MinimumTLS == "" { | 		if config.HTTP.TLS.MinimumTLS == "" { | ||||||
| 			tlsMinVersion = tls.VersionTLS10 | 			config.HTTP.TLS.MinimumTLS = defaultTLSVersionStr | ||||||
| 		} 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) |  | ||||||
| 		} | 		} | ||||||
|  | 		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{ | 		tlsConf := &tls.Config{ | ||||||
| 			ClientAuth:               tls.NoClientCert, | 			ClientAuth:               tls.NoClientCert, | ||||||
| 			NextProtos:               nextProtos(config), | 			NextProtos:               nextProtos(config), | ||||||
| 			MinVersion:               tlsMinVersion, | 			MinVersion:               tlsMinVersion, | ||||||
| 			PreferServerCipherSuites: true, | 			PreferServerCipherSuites: true, | ||||||
| 			CipherSuites: []uint16{ | 			CipherSuites:             tlsCipherSuites, | ||||||
| 				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, |  | ||||||
| 			}, |  | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if config.HTTP.TLS.LetsEncrypt.CacheFile != "" { | 		if config.HTTP.TLS.LetsEncrypt.CacheFile != "" { | ||||||
|  |  | ||||||
|  | @ -3,12 +3,24 @@ package registry | ||||||
| import ( | import ( | ||||||
| 	"bufio" | 	"bufio" | ||||||
| 	"context" | 	"context" | ||||||
|  | 	"crypto" | ||||||
|  | 	"crypto/ecdsa" | ||||||
|  | 	"crypto/elliptic" | ||||||
|  | 	"crypto/rand" | ||||||
|  | 	"crypto/rsa" | ||||||
|  | 	"crypto/tls" | ||||||
|  | 	"crypto/x509" | ||||||
|  | 	"crypto/x509/pkix" | ||||||
|  | 	"encoding/pem" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io/ioutil" | 	"io/ioutil" | ||||||
|  | 	"math/big" | ||||||
| 	"net" | 	"net" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"os" | 	"os" | ||||||
|  | 	"path" | ||||||
| 	"reflect" | 	"reflect" | ||||||
|  | 	"strings" | ||||||
| 	"testing" | 	"testing" | ||||||
| 	"time" | 	"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{} | 	config := &configuration.Configuration{} | ||||||
| 	// TODO: this needs to change to something ephemeral as the test will fail if there is any server
 | 	// TODO: this needs to change to something ephemeral as the test will fail if there is any server
 | ||||||
| 	// already listening on port 5000
 | 	// already listening on port 5000
 | ||||||
| 	config.HTTP.Addr = ":5000" | 	config.HTTP.Addr = addr | ||||||
| 	config.HTTP.DrainTimeout = time.Duration(10) * time.Second | 	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{}{}} | 	config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}} | ||||||
| 	return NewRegistry(context.Background(), config) | 	return NewRegistry(context.Background(), config) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestGracefulShutdown(t *testing.T) { | func TestGracefulShutdown(t *testing.T) { | ||||||
| 	registry, err := setupRegistry() | 	registry, err := setupRegistry(nil, ":5000") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
|  | @ -98,3 +122,227 @@ func TestGracefulShutdown(t *testing.T) { | ||||||
| 		t.Error("Body is not {}; ", string(body)) | 		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