forked from TrueCloudLab/distribution
Added flag for user configurable cipher suites
Configuration of list of cipher suites allows a user to disable use of weak ciphers or continue to support them for legacy usage if they so choose. List of available cipher suites at: https://golang.org/pkg/crypto/tls/#pkg-constants Default cipher suites have been updated to: - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305 - TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305 - 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 MinimumTLS has also been updated to include TLS 1.3 as an option and now defaults to TLS 1.2 since 1.0 and 1.1 have been deprecated. Signed-off-by: David Luu <david@davidluu.info>
This commit is contained in:
parent
22c074842e
commit
1e625d0076
6 changed files with 412 additions and 41 deletions
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"
|
||||||
|
|
||||||
|
@ -33,6 +34,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)
|
||||||
|
|
||||||
|
@ -127,6 +182,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
|
||||||
|
@ -137,35 +221,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 a new issue