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!
This commit is contained in:
Milos Gajdos 2021-02-26 08:17:32 +00:00 committed by GitHub
commit 5a76dc8df1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 412 additions and 41 deletions

View file

@ -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) $@"

View file

@ -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

View file

@ -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"`

View file

@ -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`

View file

@ -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 != "" {

View file

@ -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)
}