8166a870b6
Preivously the TLS configurator would default to presenting auto TLS certificates as client certificates. Server agents should not have this behavior and should instead present the manually configured certs. The autoTLS certs for servers are exclusively used for peering and should not be used as the default for outbound communication.
1802 lines
50 KiB
Go
1802 lines
50 KiB
Go
package tlsutil
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
"github.com/hashicorp/go-hclog"
|
|
"github.com/hashicorp/yamux"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/hashicorp/consul/sdk/testutil"
|
|
"github.com/hashicorp/consul/types"
|
|
)
|
|
|
|
func TestConfigurator_IncomingConfig_Common(t *testing.T) {
|
|
// if this test is failing because of expired certificates
|
|
// use the procedure in test/CA-GENERATION.md
|
|
testCases := map[string]struct {
|
|
setupFn func(ProtocolConfig) Config
|
|
configFn func(*Configurator) *tls.Config
|
|
}{
|
|
"Internal RPC": {
|
|
func(lc ProtocolConfig) Config { return Config{InternalRPC: lc} },
|
|
func(c *Configurator) *tls.Config { return c.IncomingRPCConfig() },
|
|
},
|
|
"gRPC": {
|
|
func(lc ProtocolConfig) Config { return Config{GRPC: lc} },
|
|
func(c *Configurator) *tls.Config { return c.IncomingGRPCConfig() },
|
|
},
|
|
"HTTPS": {
|
|
func(lc ProtocolConfig) Config { return Config{HTTPS: lc} },
|
|
func(c *Configurator) *tls.Config { return c.IncomingHTTPSConfig() },
|
|
},
|
|
}
|
|
|
|
for desc, tc := range testCases {
|
|
t.Run(desc, func(t *testing.T) {
|
|
t.Run("MinTLSVersion", func(t *testing.T) {
|
|
cfg := ProtocolConfig{
|
|
TLSMinVersion: "TLSv1_3",
|
|
CertFile: "../test/hostname/Alice.crt",
|
|
KeyFile: "../test/hostname/Alice.key",
|
|
}
|
|
c := makeConfigurator(t, tc.setupFn(cfg))
|
|
|
|
client, errc, _ := startTLSServer(tc.configFn(c))
|
|
if client == nil {
|
|
t.Fatalf("startTLSServer err: %v", <-errc)
|
|
}
|
|
|
|
tlsClient := tls.Client(client, &tls.Config{
|
|
InsecureSkipVerify: true,
|
|
MaxVersion: tls.VersionTLS12,
|
|
})
|
|
|
|
err := tlsClient.Handshake()
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "version not supported")
|
|
})
|
|
|
|
t.Run("CipherSuites", func(t *testing.T) {
|
|
cfg := ProtocolConfig{
|
|
CipherSuites: []types.TLSCipherSuite{types.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384},
|
|
CertFile: "../test/hostname/Alice.crt",
|
|
KeyFile: "../test/hostname/Alice.key",
|
|
}
|
|
c := makeConfigurator(t, tc.setupFn(cfg))
|
|
|
|
client, errc, _ := startTLSServer(tc.configFn(c))
|
|
if client == nil {
|
|
t.Fatalf("startTLSServer err: %v", <-errc)
|
|
}
|
|
|
|
tlsClient := tls.Client(client, &tls.Config{
|
|
InsecureSkipVerify: true,
|
|
MaxVersion: tls.VersionTLS12, // TLS 1.3 cipher suites are not configurable.
|
|
})
|
|
require.NoError(t, tlsClient.Handshake())
|
|
|
|
cipherSuite := tlsClient.ConnectionState().CipherSuite
|
|
require.Equal(t, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, cipherSuite)
|
|
})
|
|
|
|
t.Run("manually configured certificate is preferred over AutoTLS", func(t *testing.T) {
|
|
// Manually configure Alice's certifcate.
|
|
cfg := ProtocolConfig{
|
|
CertFile: "../test/hostname/Alice.crt",
|
|
KeyFile: "../test/hostname/Alice.key",
|
|
}
|
|
c := makeConfigurator(t, tc.setupFn(cfg))
|
|
|
|
// Set Bob's certificate via auto TLS.
|
|
bobCert := loadFile(t, "../test/hostname/Bob.crt")
|
|
bobKey := loadFile(t, "../test/hostname/Bob.key")
|
|
require.NoError(t, c.UpdateAutoTLSCert(bobCert, bobKey))
|
|
|
|
client, errc, _ := startTLSServer(tc.configFn(c))
|
|
if client == nil {
|
|
t.Fatalf("startTLSServer err: %v", <-errc)
|
|
}
|
|
|
|
// Perform a handshake and check the server presented Alice's certificate.
|
|
tlsClient := tls.Client(client, &tls.Config{InsecureSkipVerify: true})
|
|
require.NoError(t, tlsClient.Handshake())
|
|
|
|
certificates := tlsClient.ConnectionState().PeerCertificates
|
|
require.NotEmpty(t, certificates)
|
|
require.Equal(t, "Alice", certificates[0].Subject.CommonName)
|
|
|
|
// Check the server side of the handshake succeded.
|
|
require.NoError(t, <-errc)
|
|
})
|
|
|
|
t.Run("AutoTLS certificate is presented if no certificate was configured manually", func(t *testing.T) {
|
|
// No manually configured certificate.
|
|
c := makeConfigurator(t, Config{})
|
|
|
|
// Set Bob's certificate via auto TLS.
|
|
bobCert := loadFile(t, "../test/hostname/Bob.crt")
|
|
bobKey := loadFile(t, "../test/hostname/Bob.key")
|
|
require.NoError(t, c.UpdateAutoTLSCert(bobCert, bobKey))
|
|
|
|
client, errc, _ := startTLSServer(tc.configFn(c))
|
|
if client == nil {
|
|
t.Fatalf("startTLSServer err: %v", <-errc)
|
|
}
|
|
|
|
// Perform a handshake and check the server presented Bobs's certificate.
|
|
tlsClient := tls.Client(client, &tls.Config{InsecureSkipVerify: true})
|
|
require.NoError(t, tlsClient.Handshake())
|
|
|
|
certificates := tlsClient.ConnectionState().PeerCertificates
|
|
require.NotEmpty(t, certificates)
|
|
require.Equal(t, "Bob", certificates[0].Subject.CommonName)
|
|
|
|
// Check the server side of the handshake succeded.
|
|
require.NoError(t, <-errc)
|
|
})
|
|
|
|
t.Run("VerifyIncoming enabled - successful handshake", func(t *testing.T) {
|
|
cfg := ProtocolConfig{
|
|
CAFile: "../test/hostname/CertAuth.crt",
|
|
CertFile: "../test/hostname/Alice.crt",
|
|
KeyFile: "../test/hostname/Alice.key",
|
|
VerifyIncoming: true,
|
|
}
|
|
c := makeConfigurator(t, tc.setupFn(cfg))
|
|
|
|
client, errc, _ := startTLSServer(tc.configFn(c))
|
|
if client == nil {
|
|
t.Fatalf("startTLSServer err: %v", <-errc)
|
|
}
|
|
|
|
tlsClient := tls.Client(client, &tls.Config{
|
|
InsecureSkipVerify: true,
|
|
GetClientCertificate: func(*tls.CertificateRequestInfo) (*tls.Certificate, error) {
|
|
cert, err := tls.LoadX509KeyPair("../test/hostname/Bob.crt", "../test/hostname/Bob.key")
|
|
return &cert, err
|
|
},
|
|
})
|
|
require.NoError(t, tlsClient.Handshake())
|
|
require.NoError(t, <-errc)
|
|
})
|
|
|
|
t.Run("VerifyIncoming enabled - client provides no certificate", func(t *testing.T) {
|
|
cfg := ProtocolConfig{
|
|
CAFile: "../test/hostname/CertAuth.crt",
|
|
CertFile: "../test/hostname/Alice.crt",
|
|
KeyFile: "../test/hostname/Alice.key",
|
|
VerifyIncoming: true,
|
|
}
|
|
c := makeConfigurator(t, tc.setupFn(cfg))
|
|
|
|
client, errc, _ := startTLSServer(tc.configFn(c))
|
|
if client == nil {
|
|
t.Fatalf("startTLSServer err: %v", <-errc)
|
|
}
|
|
|
|
tlsClient := tls.Client(client, &tls.Config{InsecureSkipVerify: true})
|
|
require.NoError(t, tlsClient.Handshake())
|
|
|
|
err := <-errc
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "client didn't provide a certificate")
|
|
})
|
|
|
|
t.Run("VerifyIncoming enabled - client certificate signed by an unknown CA", func(t *testing.T) {
|
|
cfg := ProtocolConfig{
|
|
CAFile: "../test/ca/root.cer",
|
|
CertFile: "../test/hostname/Alice.crt",
|
|
KeyFile: "../test/hostname/Alice.key",
|
|
VerifyIncoming: true,
|
|
}
|
|
c := makeConfigurator(t, tc.setupFn(cfg))
|
|
|
|
client, errc, _ := startTLSServer(tc.configFn(c))
|
|
if client == nil {
|
|
t.Fatalf("startTLSServer err: %v", <-errc)
|
|
}
|
|
|
|
tlsClient := tls.Client(client, &tls.Config{
|
|
InsecureSkipVerify: true,
|
|
GetClientCertificate: func(*tls.CertificateRequestInfo) (*tls.Certificate, error) {
|
|
cert, err := tls.LoadX509KeyPair("../test/hostname/Bob.crt", "../test/hostname/Bob.key")
|
|
return &cert, err
|
|
},
|
|
})
|
|
require.NoError(t, tlsClient.Handshake())
|
|
|
|
err := <-errc
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "signed by unknown authority")
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConfigurator_IncomingGRPCConfig_Peering(t *testing.T) {
|
|
// Manually configure Alice's certificates
|
|
cfg := Config{
|
|
GRPC: ProtocolConfig{
|
|
CertFile: "../test/hostname/Alice.crt",
|
|
KeyFile: "../test/hostname/Alice.key",
|
|
},
|
|
}
|
|
c := makeConfigurator(t, cfg)
|
|
|
|
// Set Bob's certificate via auto TLS.
|
|
bobCert := loadFile(t, "../test/hostname/Bob.crt")
|
|
bobKey := loadFile(t, "../test/hostname/Bob.key")
|
|
require.NoError(t, c.UpdateAutoTLSCert(bobCert, bobKey))
|
|
|
|
peeringServerName := "server.dc1.peering.1234"
|
|
c.UpdateAutoTLSPeeringServerName(peeringServerName)
|
|
|
|
testutil.RunStep(t, "with peering name", func(t *testing.T) {
|
|
client, errc, _ := startTLSServer(c.IncomingGRPCConfig())
|
|
if client == nil {
|
|
t.Fatalf("startTLSServer err: %v", <-errc)
|
|
}
|
|
tlsClient := tls.Client(client, &tls.Config{
|
|
// When the peering server name is provided the server should present
|
|
// the certificates configured via AutoTLS (Bob).
|
|
ServerName: peeringServerName,
|
|
InsecureSkipVerify: true,
|
|
})
|
|
require.NoError(t, tlsClient.Handshake())
|
|
|
|
certificates := tlsClient.ConnectionState().PeerCertificates
|
|
require.NotEmpty(t, certificates)
|
|
require.Equal(t, "Bob", certificates[0].Subject.CommonName)
|
|
|
|
// Check the server side of the handshake succeded.
|
|
require.NoError(t, <-errc)
|
|
})
|
|
|
|
testutil.RunStep(t, "without name", func(t *testing.T) {
|
|
client, errc, _ := startTLSServer(c.IncomingGRPCConfig())
|
|
if client == nil {
|
|
t.Fatalf("startTLSServer err: %v", <-errc)
|
|
}
|
|
|
|
tlsClient := tls.Client(client, &tls.Config{
|
|
// ServerName: peeringServerName,
|
|
InsecureSkipVerify: true,
|
|
})
|
|
require.NoError(t, tlsClient.Handshake())
|
|
|
|
certificates := tlsClient.ConnectionState().PeerCertificates
|
|
require.NotEmpty(t, certificates)
|
|
|
|
// Should default to presenting the manually configured certificates.
|
|
require.Equal(t, "Alice", certificates[0].Subject.CommonName)
|
|
|
|
// Check the server side of the handshake succeded.
|
|
require.NoError(t, <-errc)
|
|
})
|
|
}
|
|
func TestConfigurator_IncomingInsecureRPCConfig(t *testing.T) {
|
|
// if this test is failing because of expired certificates
|
|
// use the procedure in test/CA-GENERATION.md
|
|
cfg := Config{
|
|
InternalRPC: ProtocolConfig{
|
|
CAFile: "../test/hostname/CertAuth.crt",
|
|
CertFile: "../test/hostname/Alice.crt",
|
|
KeyFile: "../test/hostname/Alice.key",
|
|
VerifyIncoming: true,
|
|
},
|
|
}
|
|
|
|
c := makeConfigurator(t, cfg)
|
|
|
|
client, errc, _ := startTLSServer(c.IncomingInsecureRPCConfig())
|
|
if client == nil {
|
|
t.Fatalf("startTLSServer err: %v", <-errc)
|
|
}
|
|
|
|
tlsClient := tls.Client(client, &tls.Config{InsecureSkipVerify: true})
|
|
require.NoError(t, tlsClient.Handshake())
|
|
|
|
// Check the server side of the handshake succeded.
|
|
require.NoError(t, <-errc)
|
|
}
|
|
|
|
func TestConfigurator_ALPNRPCConfig(t *testing.T) {
|
|
// if this test is failing because of expired certificates
|
|
// use the procedure in test/CA-GENERATION.md
|
|
t.Run("successful protocol negotiation", func(t *testing.T) {
|
|
serverCfg := makeConfigurator(t, Config{
|
|
InternalRPC: ProtocolConfig{
|
|
CAFile: "../test/hostname/CertAuth.crt",
|
|
CertFile: "../test/hostname/Bob.crt",
|
|
KeyFile: "../test/hostname/Bob.key",
|
|
},
|
|
})
|
|
|
|
client, errc, _ := startTLSServer(serverCfg.IncomingALPNRPCConfig([]string{"some-protocol"}))
|
|
if client == nil {
|
|
t.Fatalf("startTLSServer err: %v", <-errc)
|
|
}
|
|
|
|
clientCfg := makeConfigurator(t, Config{
|
|
InternalRPC: ProtocolConfig{
|
|
CAFile: "../test/hostname/CertAuth.crt",
|
|
CertFile: "../test/hostname/Alice.crt",
|
|
KeyFile: "../test/hostname/Alice.key",
|
|
},
|
|
Domain: "consul",
|
|
})
|
|
wrap := clientCfg.OutgoingALPNRPCWrapper()
|
|
|
|
tlsClient, err := wrap("dc1", "bob", "some-protocol", client)
|
|
require.NoError(t, err)
|
|
defer tlsClient.Close()
|
|
|
|
tlsConn := tlsClient.(*tls.Conn)
|
|
require.NoError(t, tlsConn.Handshake())
|
|
require.Equal(t, "some-protocol", tlsConn.ConnectionState().NegotiatedProtocol)
|
|
|
|
// Check the server side of the handshake succeded.
|
|
require.NoError(t, <-errc)
|
|
})
|
|
|
|
t.Run("protocol negotiation fails", func(t *testing.T) {
|
|
serverCfg := makeConfigurator(t, Config{
|
|
InternalRPC: ProtocolConfig{
|
|
CAFile: "../test/hostname/CertAuth.crt",
|
|
CertFile: "../test/hostname/Bob.crt",
|
|
KeyFile: "../test/hostname/Bob.key",
|
|
},
|
|
})
|
|
|
|
client, errc, _ := startTLSServer(serverCfg.IncomingALPNRPCConfig([]string{"some-protocol"}))
|
|
if client == nil {
|
|
t.Fatalf("startTLSServer err: %v", <-errc)
|
|
}
|
|
|
|
clientCfg := makeConfigurator(t, Config{
|
|
InternalRPC: ProtocolConfig{
|
|
CAFile: "../test/hostname/CertAuth.crt",
|
|
CertFile: "../test/hostname/Alice.crt",
|
|
KeyFile: "../test/hostname/Alice.key",
|
|
},
|
|
Domain: "consul",
|
|
})
|
|
wrap := clientCfg.OutgoingALPNRPCWrapper()
|
|
|
|
_, err := wrap("dc1", "bob", "other-protocol", client)
|
|
require.Error(t, err)
|
|
require.Error(t, <-errc)
|
|
})
|
|
|
|
t.Run("no node name in SAN", func(t *testing.T) {
|
|
// Note: Alice.crt has server.dc1.consul as its SAN (as apposed to alice.server.dc1.consul).
|
|
serverCfg := makeConfigurator(t, Config{
|
|
InternalRPC: ProtocolConfig{
|
|
CAFile: "../test/hostname/CertAuth.crt",
|
|
CertFile: "../test/hostname/Alice.crt",
|
|
KeyFile: "../test/hostname/Alice.key",
|
|
},
|
|
})
|
|
|
|
client, errc, _ := startTLSServer(serverCfg.IncomingALPNRPCConfig([]string{"some-protocol"}))
|
|
if client == nil {
|
|
t.Fatalf("startTLSServer err: %v", <-errc)
|
|
}
|
|
|
|
clientCfg := makeConfigurator(t, Config{
|
|
InternalRPC: ProtocolConfig{
|
|
CAFile: "../test/hostname/CertAuth.crt",
|
|
CertFile: "../test/hostname/Bob.crt",
|
|
KeyFile: "../test/hostname/Bob.key",
|
|
},
|
|
Domain: "consul",
|
|
})
|
|
wrap := clientCfg.OutgoingALPNRPCWrapper()
|
|
|
|
_, err := wrap("dc1", "alice", "some-protocol", client)
|
|
require.Error(t, err)
|
|
require.Error(t, <-errc)
|
|
})
|
|
|
|
t.Run("client certificate is always required", func(t *testing.T) {
|
|
cfg := Config{
|
|
InternalRPC: ProtocolConfig{
|
|
VerifyIncoming: false, // this setting is ignored
|
|
CAFile: "../test/hostname/CertAuth.crt",
|
|
CertFile: "../test/hostname/Alice.crt",
|
|
KeyFile: "../test/hostname/Alice.key",
|
|
},
|
|
}
|
|
c := makeConfigurator(t, cfg)
|
|
|
|
client, errc, _ := startTLSServer(c.IncomingALPNRPCConfig([]string{"some-protocol"}))
|
|
if client == nil {
|
|
t.Fatalf("startTLSServer err: %v", <-errc)
|
|
}
|
|
|
|
tlsClient := tls.Client(client, &tls.Config{
|
|
InsecureSkipVerify: true,
|
|
NextProtos: []string{"some-protocol"},
|
|
})
|
|
require.NoError(t, tlsClient.Handshake())
|
|
|
|
err := <-errc
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "client didn't provide a certificate")
|
|
})
|
|
|
|
t.Run("bad DC", func(t *testing.T) {
|
|
serverCfg := makeConfigurator(t, Config{
|
|
InternalRPC: ProtocolConfig{
|
|
CAFile: "../test/hostname/CertAuth.crt",
|
|
CertFile: "../test/hostname/Alice.crt",
|
|
KeyFile: "../test/hostname/Alice.key",
|
|
},
|
|
})
|
|
|
|
client, errc, _ := startTLSServer(serverCfg.IncomingALPNRPCConfig([]string{"some-protocol"}))
|
|
if client == nil {
|
|
t.Fatalf("startTLSServer err: %v", <-errc)
|
|
}
|
|
|
|
clientCfg := makeConfigurator(t, Config{
|
|
InternalRPC: ProtocolConfig{
|
|
CAFile: "../test/hostname/CertAuth.crt",
|
|
CertFile: "../test/hostname/Bob.crt",
|
|
KeyFile: "../test/hostname/Bob.key",
|
|
},
|
|
Domain: "consul",
|
|
})
|
|
wrap := clientCfg.OutgoingALPNRPCWrapper()
|
|
|
|
_, err := wrap("dc2", "*", "some-protocol", client)
|
|
require.Error(t, err)
|
|
require.Error(t, <-errc)
|
|
})
|
|
}
|
|
|
|
func TestConfigurator_OutgoingRPC_ServerMode(t *testing.T) {
|
|
type testCase struct {
|
|
clientConfig Config
|
|
expectName string
|
|
}
|
|
|
|
run := func(t *testing.T, tc testCase) {
|
|
serverCfg := makeConfigurator(t, Config{
|
|
InternalRPC: ProtocolConfig{
|
|
CAFile: "../test/hostname/CertAuth.crt",
|
|
CertFile: "../test/hostname/Alice.crt",
|
|
KeyFile: "../test/hostname/Alice.key",
|
|
VerifyIncoming: true,
|
|
},
|
|
ServerMode: true,
|
|
})
|
|
|
|
serverConn, errc, certc := startTLSServer(serverCfg.IncomingRPCConfig())
|
|
if serverConn == nil {
|
|
t.Fatalf("startTLSServer err: %v", <-errc)
|
|
}
|
|
|
|
clientCfg := makeConfigurator(t, tc.clientConfig)
|
|
|
|
bettyCert := loadFile(t, "../test/hostname/Betty.crt")
|
|
bettyKey := loadFile(t, "../test/hostname/Betty.key")
|
|
require.NoError(t, clientCfg.UpdateAutoTLSCert(bettyCert, bettyKey))
|
|
|
|
wrap := clientCfg.OutgoingRPCWrapper()
|
|
require.NotNil(t, wrap)
|
|
|
|
tlsClient, err := wrap("dc1", serverConn)
|
|
require.NoError(t, err)
|
|
defer tlsClient.Close()
|
|
|
|
err = tlsClient.(*tls.Conn).Handshake()
|
|
require.NoError(t, err)
|
|
|
|
err = <-errc
|
|
require.NoError(t, err)
|
|
|
|
clientCerts := <-certc
|
|
require.NotEmpty(t, clientCerts)
|
|
|
|
require.Equal(t, tc.expectName, clientCerts[0].Subject.CommonName)
|
|
|
|
// Check the server side of the handshake succeeded.
|
|
require.NoError(t, <-errc)
|
|
}
|
|
|
|
tt := map[string]testCase{
|
|
"server with manual cert": {
|
|
clientConfig: Config{
|
|
InternalRPC: ProtocolConfig{
|
|
VerifyOutgoing: true,
|
|
CAFile: "../test/hostname/CertAuth.crt",
|
|
CertFile: "../test/hostname/Bob.crt",
|
|
KeyFile: "../test/hostname/Bob.key",
|
|
},
|
|
ServerMode: true,
|
|
},
|
|
// Even though an AutoTLS cert is configured, the server will prefer the manually configured cert.
|
|
expectName: "Bob",
|
|
},
|
|
"client with manual cert": {
|
|
clientConfig: Config{
|
|
InternalRPC: ProtocolConfig{
|
|
VerifyOutgoing: true,
|
|
CAFile: "../test/hostname/CertAuth.crt",
|
|
CertFile: "../test/hostname/Bob.crt",
|
|
KeyFile: "../test/hostname/Bob.key",
|
|
},
|
|
ServerMode: false,
|
|
},
|
|
expectName: "Betty",
|
|
},
|
|
"client with auto-TLS": {
|
|
clientConfig: Config{
|
|
ServerMode: false,
|
|
AutoTLS: true,
|
|
},
|
|
expectName: "Betty",
|
|
},
|
|
}
|
|
|
|
for name, tc := range tt {
|
|
t.Run(name, func(t *testing.T) {
|
|
run(t, tc)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConfigurator_OutgoingInternalRPCWrapper(t *testing.T) {
|
|
// if this test is failing because of expired certificates
|
|
// use the procedure in test/CA-GENERATION.md
|
|
t.Run("AutoTLS", func(t *testing.T) {
|
|
serverCfg := makeConfigurator(t, Config{
|
|
InternalRPC: ProtocolConfig{
|
|
CAFile: "../test/hostname/CertAuth.crt",
|
|
CertFile: "../test/hostname/Alice.crt",
|
|
KeyFile: "../test/hostname/Alice.key",
|
|
VerifyIncoming: true,
|
|
},
|
|
})
|
|
|
|
client, errc, _ := startTLSServer(serverCfg.IncomingRPCConfig())
|
|
if client == nil {
|
|
t.Fatalf("startTLSServer err: %v", <-errc)
|
|
}
|
|
|
|
clientCfg := makeConfigurator(t, Config{
|
|
AutoTLS: true,
|
|
})
|
|
bobCert := loadFile(t, "../test/hostname/Bob.crt")
|
|
bobKey := loadFile(t, "../test/hostname/Bob.key")
|
|
require.NoError(t, clientCfg.UpdateAutoTLSCert(bobCert, bobKey))
|
|
|
|
wrap := clientCfg.OutgoingRPCWrapper()
|
|
require.NotNil(t, wrap)
|
|
|
|
tlsClient, err := wrap("dc1", client)
|
|
require.NoError(t, err)
|
|
defer tlsClient.Close()
|
|
|
|
err = tlsClient.(*tls.Conn).Handshake()
|
|
require.NoError(t, err)
|
|
|
|
err = <-errc
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("VerifyOutgoing and a manually configured certificate", func(t *testing.T) {
|
|
serverCfg := makeConfigurator(t, Config{
|
|
InternalRPC: ProtocolConfig{
|
|
CAFile: "../test/hostname/CertAuth.crt",
|
|
CertFile: "../test/hostname/Alice.crt",
|
|
KeyFile: "../test/hostname/Alice.key",
|
|
VerifyIncoming: true,
|
|
},
|
|
})
|
|
|
|
client, errc, _ := startTLSServer(serverCfg.IncomingRPCConfig())
|
|
if client == nil {
|
|
t.Fatalf("startTLSServer err: %v", <-errc)
|
|
}
|
|
|
|
clientCfg := makeConfigurator(t, Config{
|
|
InternalRPC: ProtocolConfig{
|
|
VerifyOutgoing: true,
|
|
CAFile: "../test/hostname/CertAuth.crt",
|
|
CertFile: "../test/hostname/Bob.crt",
|
|
KeyFile: "../test/hostname/Bob.key",
|
|
},
|
|
})
|
|
|
|
wrap := clientCfg.OutgoingRPCWrapper()
|
|
require.NotNil(t, wrap)
|
|
|
|
tlsClient, err := wrap("dc1", client)
|
|
require.NoError(t, err)
|
|
defer tlsClient.Close()
|
|
|
|
err = tlsClient.(*tls.Conn).Handshake()
|
|
require.NoError(t, err)
|
|
|
|
err = <-errc
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("outgoing TLS not enabled", func(t *testing.T) {
|
|
serverCfg := makeConfigurator(t, Config{
|
|
InternalRPC: ProtocolConfig{
|
|
CAFile: "../test/hostname/CertAuth.crt",
|
|
CertFile: "../test/hostname/Alice.crt",
|
|
KeyFile: "../test/hostname/Alice.key",
|
|
VerifyIncoming: true,
|
|
},
|
|
})
|
|
|
|
client, errc, _ := startTLSServer(serverCfg.IncomingRPCConfig())
|
|
if client == nil {
|
|
t.Fatalf("startTLSServer err: %v", <-errc)
|
|
}
|
|
|
|
clientCfg := makeConfigurator(t, Config{})
|
|
|
|
wrap := clientCfg.OutgoingRPCWrapper()
|
|
require.NotNil(t, wrap)
|
|
|
|
client, err := wrap("dc1", client)
|
|
require.NoError(t, err)
|
|
defer client.Close()
|
|
|
|
_, isTLS := client.(*tls.Conn)
|
|
require.False(t, isTLS)
|
|
})
|
|
|
|
t.Run("VerifyServerHostname = true", func(t *testing.T) {
|
|
serverCfg := makeConfigurator(t, Config{
|
|
InternalRPC: ProtocolConfig{
|
|
CAFile: "../test/client_certs/rootca.crt",
|
|
CertFile: "../test/client_certs/client.crt",
|
|
KeyFile: "../test/client_certs/client.key",
|
|
},
|
|
})
|
|
|
|
client, errc, _ := startTLSServer(serverCfg.IncomingRPCConfig())
|
|
if client == nil {
|
|
t.Fatalf("startTLSServer err: %v", <-errc)
|
|
}
|
|
|
|
clientCfg := makeConfigurator(t, Config{
|
|
InternalRPC: ProtocolConfig{
|
|
VerifyOutgoing: true,
|
|
VerifyServerHostname: true,
|
|
CAFile: "../test/client_certs/rootca.crt",
|
|
CertFile: "../test/client_certs/client.crt",
|
|
KeyFile: "../test/client_certs/client.key",
|
|
},
|
|
Domain: "consul",
|
|
})
|
|
|
|
wrap := clientCfg.OutgoingRPCWrapper()
|
|
require.NotNil(t, wrap)
|
|
|
|
tlsClient, err := wrap("dc1", client)
|
|
require.NoError(t, err)
|
|
defer tlsClient.Close()
|
|
|
|
err = tlsClient.(*tls.Conn).Handshake()
|
|
require.Error(t, err)
|
|
require.Regexp(t, `certificate is valid for ([a-z].+) not server.dc1.consul`, err.Error())
|
|
})
|
|
|
|
t.Run("VerifyServerHostname = true and incorrect DC name", func(t *testing.T) {
|
|
serverCfg := makeConfigurator(t, Config{
|
|
InternalRPC: ProtocolConfig{
|
|
CAFile: "../test/client_certs/rootca.crt",
|
|
CertFile: "../test/client_certs/client.crt",
|
|
KeyFile: "../test/client_certs/client.key",
|
|
},
|
|
})
|
|
|
|
client, errc, _ := startTLSServer(serverCfg.IncomingRPCConfig())
|
|
if client == nil {
|
|
t.Fatalf("startTLSServer err: %v", <-errc)
|
|
}
|
|
|
|
clientCfg := makeConfigurator(t, Config{
|
|
InternalRPC: ProtocolConfig{
|
|
VerifyServerHostname: true,
|
|
VerifyOutgoing: true,
|
|
CAFile: "../test/client_certs/rootca.crt",
|
|
CertFile: "../test/client_certs/client.crt",
|
|
KeyFile: "../test/client_certs/client.key",
|
|
},
|
|
Domain: "consul",
|
|
})
|
|
|
|
wrap := clientCfg.OutgoingRPCWrapper()
|
|
require.NotNil(t, wrap)
|
|
|
|
tlsClient, err := wrap("dc2", client)
|
|
require.NoError(t, err)
|
|
defer tlsClient.Close()
|
|
|
|
err = tlsClient.(*tls.Conn).Handshake()
|
|
require.Error(t, err)
|
|
require.Regexp(t, `certificate is valid for ([a-z].+) not server.dc2.consul`, err.Error())
|
|
})
|
|
|
|
t.Run("VerifyServerHostname = false", func(t *testing.T) {
|
|
serverCfg := makeConfigurator(t, Config{
|
|
InternalRPC: ProtocolConfig{
|
|
CAFile: "../test/client_certs/rootca.crt",
|
|
CertFile: "../test/client_certs/client.crt",
|
|
KeyFile: "../test/client_certs/client.key",
|
|
},
|
|
})
|
|
|
|
client, errc, _ := startTLSServer(serverCfg.IncomingRPCConfig())
|
|
if client == nil {
|
|
t.Fatalf("startTLSServer err: %v", <-errc)
|
|
}
|
|
|
|
clientCfg := makeConfigurator(t, Config{
|
|
InternalRPC: ProtocolConfig{
|
|
VerifyServerHostname: false,
|
|
VerifyOutgoing: true,
|
|
CAFile: "../test/client_certs/rootca.crt",
|
|
CertFile: "../test/client_certs/client.crt",
|
|
KeyFile: "../test/client_certs/client.key",
|
|
},
|
|
Domain: "other",
|
|
})
|
|
|
|
wrap := clientCfg.OutgoingRPCWrapper()
|
|
require.NotNil(t, wrap)
|
|
|
|
tlsClient, err := wrap("dc1", client)
|
|
require.NoError(t, err)
|
|
defer tlsClient.Close()
|
|
|
|
err = tlsClient.(*tls.Conn).Handshake()
|
|
require.NoError(t, err)
|
|
|
|
// Check the server side of the handshake succeded.
|
|
require.NoError(t, <-errc)
|
|
})
|
|
|
|
t.Run("AutoTLS certificate preferred over manually configured certificate", func(t *testing.T) {
|
|
serverCfg := makeConfigurator(t, Config{
|
|
InternalRPC: ProtocolConfig{
|
|
CAFile: "../test/hostname/CertAuth.crt",
|
|
CertFile: "../test/hostname/Alice.crt",
|
|
KeyFile: "../test/hostname/Alice.key",
|
|
VerifyIncoming: true,
|
|
},
|
|
})
|
|
|
|
client, errc, certc := startTLSServer(serverCfg.IncomingRPCConfig())
|
|
if client == nil {
|
|
t.Fatalf("startTLSServer err: %v", <-errc)
|
|
}
|
|
|
|
clientCfg := makeConfigurator(t, Config{
|
|
InternalRPC: ProtocolConfig{
|
|
VerifyServerHostname: true,
|
|
VerifyOutgoing: true,
|
|
CAFile: "../test/hostname/CertAuth.crt",
|
|
CertFile: "../test/hostname/Bob.crt",
|
|
KeyFile: "../test/hostname/Bob.key",
|
|
},
|
|
Domain: "consul",
|
|
})
|
|
|
|
bettyCert := loadFile(t, "../test/hostname/Betty.crt")
|
|
bettyKey := loadFile(t, "../test/hostname/Betty.key")
|
|
require.NoError(t, clientCfg.UpdateAutoTLSCert(bettyCert, bettyKey))
|
|
|
|
wrap := clientCfg.OutgoingRPCWrapper()
|
|
require.NotNil(t, wrap)
|
|
|
|
tlsClient, err := wrap("dc1", client)
|
|
require.NoError(t, err)
|
|
defer tlsClient.Close()
|
|
|
|
err = tlsClient.(*tls.Conn).Handshake()
|
|
require.NoError(t, err)
|
|
|
|
err = <-errc
|
|
require.NoError(t, err)
|
|
|
|
clientCerts := <-certc
|
|
require.NotEmpty(t, clientCerts)
|
|
require.Equal(t, "Betty", clientCerts[0].Subject.CommonName)
|
|
})
|
|
|
|
t.Run("manually configured certificate is presented if there's no AutoTLS certificate", func(t *testing.T) {
|
|
serverCfg := makeConfigurator(t, Config{
|
|
InternalRPC: ProtocolConfig{
|
|
CAFile: "../test/hostname/CertAuth.crt",
|
|
CertFile: "../test/hostname/Alice.crt",
|
|
KeyFile: "../test/hostname/Alice.key",
|
|
VerifyIncoming: true,
|
|
},
|
|
})
|
|
|
|
client, errc, certc := startTLSServer(serverCfg.IncomingRPCConfig())
|
|
if client == nil {
|
|
t.Fatalf("startTLSServer err: %v", <-errc)
|
|
}
|
|
|
|
clientCfg := makeConfigurator(t, Config{
|
|
InternalRPC: ProtocolConfig{
|
|
VerifyServerHostname: true,
|
|
VerifyOutgoing: true,
|
|
CAFile: "../test/hostname/CertAuth.crt",
|
|
CertFile: "../test/hostname/Bob.crt",
|
|
KeyFile: "../test/hostname/Bob.key",
|
|
},
|
|
Domain: "consul",
|
|
})
|
|
|
|
wrap := clientCfg.OutgoingRPCWrapper()
|
|
require.NotNil(t, wrap)
|
|
|
|
tlsClient, err := wrap("dc1", client)
|
|
require.NoError(t, err)
|
|
defer tlsClient.Close()
|
|
|
|
err = tlsClient.(*tls.Conn).Handshake()
|
|
require.NoError(t, err)
|
|
|
|
err = <-errc
|
|
require.NoError(t, err)
|
|
|
|
clientCerts := <-certc
|
|
require.NotEmpty(t, clientCerts)
|
|
require.Equal(t, "Bob", clientCerts[0].Subject.CommonName)
|
|
})
|
|
}
|
|
|
|
func TestConfigurator_outgoingWrapperALPN_serverHasNoNodeNameInSAN(t *testing.T) {
|
|
// if this test is failing because of expired certificates
|
|
// use the procedure in test/CA-GENERATION.md
|
|
srvConfig := Config{
|
|
InternalRPC: ProtocolConfig{
|
|
CAFile: "../test/hostname/CertAuth.crt",
|
|
CertFile: "../test/hostname/Alice.crt",
|
|
KeyFile: "../test/hostname/Alice.key",
|
|
VerifyOutgoing: false, // doesn't matter
|
|
VerifyServerHostname: false, // doesn't matter
|
|
},
|
|
Domain: "consul",
|
|
}
|
|
|
|
client, errc := startALPNRPCTLSServer(t, &srvConfig, []string{"foo", "bar"})
|
|
if client == nil {
|
|
t.Fatalf("startTLSServer err: %v", <-errc)
|
|
}
|
|
|
|
config := Config{
|
|
InternalRPC: ProtocolConfig{
|
|
CAFile: "../test/hostname/CertAuth.crt",
|
|
CertFile: "../test/hostname/Bob.crt",
|
|
KeyFile: "../test/hostname/Bob.key",
|
|
VerifyOutgoing: false, // doesn't matter
|
|
VerifyServerHostname: false, // doesn't matter
|
|
},
|
|
Domain: "consul",
|
|
}
|
|
|
|
c, err := NewConfigurator(config, nil)
|
|
require.NoError(t, err)
|
|
wrap := c.OutgoingALPNRPCWrapper()
|
|
require.NotNil(t, wrap)
|
|
|
|
_, err = wrap("dc1", "bob", "foo", client)
|
|
require.Error(t, err)
|
|
_, ok := err.(x509.HostnameError)
|
|
require.True(t, ok)
|
|
client.Close()
|
|
|
|
<-errc
|
|
}
|
|
|
|
func TestLoadKeyPair(t *testing.T) {
|
|
type variant struct {
|
|
cert, key string
|
|
shoulderr bool
|
|
isnil bool
|
|
}
|
|
variants := []variant{
|
|
{"", "", false, true},
|
|
{"bogus", "", false, true},
|
|
{"", "bogus", false, true},
|
|
{"../test/key/ourdomain.cer", "", false, true},
|
|
{"", "../test/key/ourdomain.key", false, true},
|
|
{"bogus", "bogus", true, true},
|
|
{"../test/key/ourdomain.cer", "../test/key/ourdomain.key",
|
|
false, false},
|
|
}
|
|
for i, v := range variants {
|
|
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
|
|
cert, err := loadKeyPair(v.cert, v.key)
|
|
if v.shoulderr {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
if v.isnil {
|
|
require.Nil(t, cert)
|
|
} else {
|
|
require.NotNil(t, cert)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConfig_SpecifyDC(t *testing.T) {
|
|
require.Nil(t, SpecificDC("", nil))
|
|
dcwrap := func(dc string, conn net.Conn) (net.Conn, error) { return nil, nil }
|
|
wrap := SpecificDC("", dcwrap)
|
|
require.NotNil(t, wrap)
|
|
conn, err := wrap(nil)
|
|
require.NoError(t, err)
|
|
require.Nil(t, conn)
|
|
}
|
|
|
|
func TestConfigurator_Validation(t *testing.T) {
|
|
// if this test is failing because of expired certificates
|
|
// use the procedure in test/CA-GENERATION.md
|
|
const (
|
|
caFile = "../test/ca/root.cer"
|
|
caPath = "../test/ca_path"
|
|
certFile = "../test/key/ourdomain.cer"
|
|
keyFile = "../test/key/ourdomain.key"
|
|
)
|
|
|
|
t.Run("empty config", func(t *testing.T) {
|
|
_, err := NewConfigurator(Config{}, nil)
|
|
require.NoError(t, err)
|
|
require.NoError(t, new(Configurator).Update(Config{}))
|
|
})
|
|
|
|
t.Run("common fields", func(t *testing.T) {
|
|
type testCase struct {
|
|
config ProtocolConfig
|
|
isValid bool
|
|
}
|
|
|
|
testCases := map[string]testCase{
|
|
"invalid CAFile": {
|
|
ProtocolConfig{CAFile: "bogus"},
|
|
false,
|
|
},
|
|
"invalid CAPath": {
|
|
ProtocolConfig{CAPath: "bogus"},
|
|
false,
|
|
},
|
|
"invalid CertFile": {
|
|
ProtocolConfig{
|
|
CertFile: "bogus",
|
|
KeyFile: keyFile,
|
|
},
|
|
false,
|
|
},
|
|
"invalid KeyFile": {
|
|
ProtocolConfig{
|
|
CertFile: certFile,
|
|
KeyFile: "bogus",
|
|
},
|
|
false,
|
|
},
|
|
"VerifyIncoming set but no CA": {
|
|
ProtocolConfig{
|
|
VerifyIncoming: true,
|
|
CAFile: "",
|
|
CAPath: "",
|
|
CertFile: certFile,
|
|
KeyFile: keyFile,
|
|
},
|
|
false,
|
|
},
|
|
"VerifyIncoming set but no CertFile": {
|
|
ProtocolConfig{
|
|
VerifyIncoming: true,
|
|
CAFile: caFile,
|
|
CertFile: "",
|
|
KeyFile: keyFile,
|
|
},
|
|
false,
|
|
},
|
|
"VerifyIncoming set but no KeyFile": {
|
|
ProtocolConfig{
|
|
VerifyIncoming: true,
|
|
CAFile: caFile,
|
|
CertFile: certFile,
|
|
KeyFile: "",
|
|
},
|
|
false,
|
|
},
|
|
"VerifyIncoming + CAFile": {
|
|
ProtocolConfig{
|
|
VerifyIncoming: true,
|
|
CAFile: caFile,
|
|
CertFile: certFile,
|
|
KeyFile: keyFile,
|
|
},
|
|
true,
|
|
},
|
|
"VerifyIncoming + CAPath": {
|
|
ProtocolConfig{
|
|
VerifyIncoming: true,
|
|
CAPath: caPath,
|
|
CertFile: certFile,
|
|
KeyFile: keyFile,
|
|
},
|
|
true,
|
|
},
|
|
"VerifyIncoming + invalid CAFile": {
|
|
ProtocolConfig{
|
|
VerifyIncoming: true,
|
|
CAFile: "bogus",
|
|
CertFile: certFile,
|
|
KeyFile: keyFile,
|
|
},
|
|
false,
|
|
},
|
|
"VerifyIncoming + invalid CAPath": {
|
|
ProtocolConfig{
|
|
VerifyIncoming: true,
|
|
CAPath: "bogus",
|
|
CertFile: certFile,
|
|
KeyFile: keyFile,
|
|
},
|
|
false,
|
|
},
|
|
"VerifyOutgoing + CAFile": {
|
|
ProtocolConfig{VerifyOutgoing: true, CAFile: caFile},
|
|
true,
|
|
},
|
|
"VerifyOutgoing + CAPath": {
|
|
ProtocolConfig{VerifyOutgoing: true, CAPath: caPath},
|
|
true,
|
|
},
|
|
"VerifyOutgoing + CAFile + CAPath": {
|
|
ProtocolConfig{
|
|
VerifyOutgoing: true,
|
|
CAFile: caFile,
|
|
CAPath: caPath,
|
|
},
|
|
true,
|
|
},
|
|
"VerifyOutgoing but no CA": {
|
|
ProtocolConfig{
|
|
VerifyOutgoing: true,
|
|
CAFile: "",
|
|
CAPath: "",
|
|
},
|
|
false,
|
|
},
|
|
}
|
|
|
|
for desc, tc := range testCases {
|
|
for _, p := range []string{"internal", "grpc", "https"} {
|
|
info := fmt.Sprintf("%s => %s", p, desc)
|
|
|
|
var cfg Config
|
|
switch p {
|
|
case "internal":
|
|
cfg.InternalRPC = tc.config
|
|
case "grpc":
|
|
cfg.GRPC = tc.config
|
|
case "https":
|
|
cfg.HTTPS = tc.config
|
|
default:
|
|
t.Fatalf("unknown protocol: %s", p)
|
|
}
|
|
|
|
_, err1 := NewConfigurator(cfg, nil)
|
|
err2 := new(Configurator).Update(cfg)
|
|
|
|
if tc.isValid {
|
|
require.NoError(t, err1, info)
|
|
require.NoError(t, err2, info)
|
|
} else {
|
|
require.Error(t, err1, info)
|
|
require.Error(t, err2, info)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("VerifyIncoming + AutoTLS", func(t *testing.T) {
|
|
cfg := Config{
|
|
InternalRPC: ProtocolConfig{
|
|
VerifyIncoming: true,
|
|
CAFile: caFile,
|
|
},
|
|
GRPC: ProtocolConfig{
|
|
VerifyIncoming: true,
|
|
CAFile: caFile,
|
|
},
|
|
HTTPS: ProtocolConfig{
|
|
VerifyIncoming: true,
|
|
CAFile: caFile,
|
|
},
|
|
AutoTLS: true,
|
|
}
|
|
|
|
_, err := NewConfigurator(cfg, nil)
|
|
require.NoError(t, err)
|
|
require.NoError(t, new(Configurator).Update(cfg))
|
|
})
|
|
}
|
|
|
|
func TestConfigurator_CommonTLSConfigServerNameNodeName(t *testing.T) {
|
|
type variant struct {
|
|
config Config
|
|
result string
|
|
}
|
|
variants := []variant{
|
|
{config: Config{NodeName: "node", ServerName: "server"},
|
|
result: "server"},
|
|
{config: Config{ServerName: "server"},
|
|
result: "server"},
|
|
{config: Config{NodeName: "node"},
|
|
result: "node"},
|
|
}
|
|
for _, v := range variants {
|
|
c, err := NewConfigurator(v.config, nil)
|
|
require.NoError(t, err)
|
|
tlsConf := c.internalRPCTLSConfig(false)
|
|
require.Empty(t, tlsConf.ServerName)
|
|
}
|
|
}
|
|
|
|
func TestConfigurator_LoadCAs(t *testing.T) {
|
|
type variant struct {
|
|
cafile, capath string
|
|
shouldErr bool
|
|
isNil bool
|
|
count int
|
|
expectedCaPool *x509.CertPool
|
|
}
|
|
variants := []variant{
|
|
{"", "", false, true, 0, nil},
|
|
{"bogus", "", true, true, 0, nil},
|
|
{"", "bogus", true, true, 0, nil},
|
|
{"", "../test/bin", true, true, 0, nil},
|
|
{"../test/ca/root.cer", "", false, false, 1, getExpectedCaPoolByFile(t)},
|
|
{"", "../test/ca_path", false, false, 2, getExpectedCaPoolByDir(t)},
|
|
{"../test/ca/root.cer", "../test/ca_path", false, false, 1, getExpectedCaPoolByFile(t)},
|
|
}
|
|
for i, v := range variants {
|
|
pems, err1 := LoadCAs(v.cafile, v.capath)
|
|
pool, err2 := newX509CertPool(pems)
|
|
info := fmt.Sprintf("case %d", i)
|
|
if v.shouldErr {
|
|
if err1 == nil && err2 == nil {
|
|
t.Fatal("An error is expected but got nil.")
|
|
}
|
|
} else {
|
|
require.NoError(t, err1, info)
|
|
require.NoError(t, err2, info)
|
|
}
|
|
if v.isNil {
|
|
require.Nil(t, pool, info)
|
|
} else {
|
|
require.NotEmpty(t, pems, info)
|
|
require.NotNil(t, pool, info)
|
|
assertDeepEqual(t, v.expectedCaPool, pool, cmpCertPool)
|
|
require.Len(t, pems, v.count, info)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestConfigurator_InternalRPCMutualTLSCapable(t *testing.T) {
|
|
// if this test is failing because of expired certificates
|
|
// use the procedure in test/CA-GENERATION.md
|
|
t.Run("no ca", func(t *testing.T) {
|
|
config := Config{
|
|
Domain: "consul",
|
|
}
|
|
c, err := NewConfigurator(config, nil)
|
|
require.NoError(t, err)
|
|
|
|
require.False(t, c.MutualTLSCapable())
|
|
})
|
|
|
|
t.Run("ca and no keys", func(t *testing.T) {
|
|
config := Config{
|
|
InternalRPC: ProtocolConfig{
|
|
CAFile: "../test/hostname/CertAuth.crt",
|
|
},
|
|
Domain: "consul",
|
|
}
|
|
c, err := NewConfigurator(config, nil)
|
|
require.NoError(t, err)
|
|
|
|
require.False(t, c.MutualTLSCapable())
|
|
})
|
|
|
|
t.Run("ca and manual key", func(t *testing.T) {
|
|
config := Config{
|
|
InternalRPC: ProtocolConfig{
|
|
CAFile: "../test/hostname/CertAuth.crt",
|
|
CertFile: "../test/hostname/Bob.crt",
|
|
KeyFile: "../test/hostname/Bob.key",
|
|
},
|
|
Domain: "consul",
|
|
}
|
|
c, err := NewConfigurator(config, nil)
|
|
require.NoError(t, err)
|
|
|
|
require.True(t, c.MutualTLSCapable())
|
|
})
|
|
|
|
t.Run("autoencrypt ca and no autoencrypt keys", func(t *testing.T) {
|
|
config := Config{
|
|
Domain: "consul",
|
|
}
|
|
c, err := NewConfigurator(config, nil)
|
|
require.NoError(t, err)
|
|
|
|
caPEM := loadFile(t, "../test/hostname/CertAuth.crt")
|
|
require.NoError(t, c.UpdateAutoTLSCA([]string{caPEM}))
|
|
|
|
require.False(t, c.MutualTLSCapable())
|
|
})
|
|
|
|
t.Run("autoencrypt ca and autoencrypt key", func(t *testing.T) {
|
|
config := Config{
|
|
Domain: "consul",
|
|
}
|
|
c, err := NewConfigurator(config, nil)
|
|
require.NoError(t, err)
|
|
|
|
caPEM := loadFile(t, "../test/hostname/CertAuth.crt")
|
|
certPEM := loadFile(t, "../test/hostname/Bob.crt")
|
|
keyPEM := loadFile(t, "../test/hostname/Bob.key")
|
|
require.NoError(t, c.UpdateAutoTLSCA([]string{caPEM}))
|
|
require.NoError(t, c.UpdateAutoTLSCert(certPEM, keyPEM))
|
|
|
|
require.True(t, c.MutualTLSCapable())
|
|
})
|
|
}
|
|
|
|
func TestConfigurator_UpdateAutoTLSCA_DoesNotPanic(t *testing.T) {
|
|
config := Config{
|
|
Domain: "consul",
|
|
}
|
|
c, err := NewConfigurator(config, hclog.New(nil))
|
|
require.NoError(t, err)
|
|
|
|
err = c.UpdateAutoTLSCA([]string{"invalid pem"})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestConfigurator_VerifyIncomingRPC(t *testing.T) {
|
|
c := Configurator{base: &Config{}}
|
|
c.base.InternalRPC.VerifyIncoming = true
|
|
require.True(t, c.VerifyIncomingRPC())
|
|
}
|
|
|
|
func TestConfigurator_OutgoingTLSConfigForCheck(t *testing.T) {
|
|
type testCase struct {
|
|
name string
|
|
conf func() (*Configurator, error)
|
|
skipVerify bool
|
|
serverName string
|
|
expected *tls.Config
|
|
}
|
|
|
|
run := func(t *testing.T, tc testCase) {
|
|
configurator, err := tc.conf()
|
|
require.NoError(t, err)
|
|
c := configurator.OutgoingTLSConfigForCheck(tc.skipVerify, tc.serverName)
|
|
|
|
if diff := cmp.Diff(tc.expected, c, cmp.Options{
|
|
cmpopts.IgnoreFields(tls.Config{}, "GetCertificate", "GetClientCertificate"),
|
|
cmpopts.IgnoreUnexported(tls.Config{}),
|
|
}); diff != "" {
|
|
t.Fatalf("assertion failed: values are not equal\n--- expected\n+++ actual\n%v", diff)
|
|
}
|
|
}
|
|
|
|
testCases := []testCase{
|
|
{
|
|
name: "default tls",
|
|
conf: func() (*Configurator, error) {
|
|
return NewConfigurator(Config{}, nil)
|
|
},
|
|
expected: &tls.Config{},
|
|
},
|
|
{
|
|
name: "default tls, skip verify, no server name",
|
|
conf: func() (*Configurator, error) {
|
|
return NewConfigurator(Config{
|
|
InternalRPC: ProtocolConfig{
|
|
TLSMinVersion: types.TLSv1_2,
|
|
},
|
|
EnableAgentTLSForChecks: false,
|
|
}, nil)
|
|
},
|
|
skipVerify: true,
|
|
expected: &tls.Config{InsecureSkipVerify: true},
|
|
},
|
|
{
|
|
name: "default tls, skip verify, default server name",
|
|
conf: func() (*Configurator, error) {
|
|
return NewConfigurator(Config{
|
|
InternalRPC: ProtocolConfig{
|
|
TLSMinVersion: types.TLSv1_2,
|
|
},
|
|
EnableAgentTLSForChecks: false,
|
|
ServerName: "servername",
|
|
NodeName: "nodename",
|
|
}, nil)
|
|
},
|
|
skipVerify: true,
|
|
expected: &tls.Config{InsecureSkipVerify: true},
|
|
},
|
|
{
|
|
name: "default tls, skip verify, check server name",
|
|
conf: func() (*Configurator, error) {
|
|
return NewConfigurator(Config{
|
|
InternalRPC: ProtocolConfig{
|
|
TLSMinVersion: types.TLSv1_2,
|
|
},
|
|
EnableAgentTLSForChecks: false,
|
|
ServerName: "servername",
|
|
}, nil)
|
|
},
|
|
skipVerify: true,
|
|
serverName: "check-server-name",
|
|
expected: &tls.Config{
|
|
InsecureSkipVerify: true,
|
|
ServerName: "check-server-name",
|
|
},
|
|
},
|
|
{
|
|
name: "agent tls, default server name",
|
|
conf: func() (*Configurator, error) {
|
|
return NewConfigurator(Config{
|
|
InternalRPC: ProtocolConfig{
|
|
TLSMinVersion: types.TLSv1_2,
|
|
},
|
|
EnableAgentTLSForChecks: true,
|
|
NodeName: "nodename",
|
|
ServerName: "servername",
|
|
}, nil)
|
|
},
|
|
expected: &tls.Config{
|
|
MinVersion: tls.VersionTLS12,
|
|
ServerName: "servername",
|
|
},
|
|
},
|
|
{
|
|
name: "agent tls, skip verify, node name for server name",
|
|
conf: func() (*Configurator, error) {
|
|
return NewConfigurator(Config{
|
|
InternalRPC: ProtocolConfig{
|
|
TLSMinVersion: types.TLSv1_2,
|
|
},
|
|
EnableAgentTLSForChecks: true,
|
|
NodeName: "nodename",
|
|
}, nil)
|
|
},
|
|
skipVerify: true,
|
|
expected: &tls.Config{
|
|
InsecureSkipVerify: true,
|
|
MinVersion: tls.VersionTLS12,
|
|
ServerName: "nodename",
|
|
},
|
|
},
|
|
{
|
|
name: "agent tls, skip verify, with server name override",
|
|
conf: func() (*Configurator, error) {
|
|
return NewConfigurator(Config{
|
|
InternalRPC: ProtocolConfig{
|
|
TLSMinVersion: types.TLSv1_2,
|
|
},
|
|
EnableAgentTLSForChecks: true,
|
|
ServerName: "servername",
|
|
}, nil)
|
|
},
|
|
skipVerify: true,
|
|
serverName: "override",
|
|
expected: &tls.Config{
|
|
InsecureSkipVerify: true,
|
|
MinVersion: tls.VersionTLS12,
|
|
ServerName: "override",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
run(t, tc)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConfigurator_ServerNameOrNodeName(t *testing.T) {
|
|
c := Configurator{base: &Config{}}
|
|
type variant struct {
|
|
server, node, expected string
|
|
}
|
|
variants := []variant{
|
|
{"", "", ""},
|
|
{"a", "", "a"},
|
|
{"", "b", "b"},
|
|
{"a", "b", "a"},
|
|
}
|
|
for _, v := range variants {
|
|
c.base.ServerName = v.server
|
|
c.base.NodeName = v.node
|
|
require.Equal(t, v.expected, c.serverNameOrNodeName())
|
|
}
|
|
}
|
|
|
|
func TestConfigurator_InternalRPCVerifyServerHostname(t *testing.T) {
|
|
c := Configurator{base: &Config{}}
|
|
require.False(t, c.VerifyServerHostname())
|
|
|
|
c.base.InternalRPC.VerifyServerHostname = true
|
|
c.autoTLS.verifyServerHostname = false
|
|
require.True(t, c.VerifyServerHostname())
|
|
|
|
c.base.InternalRPC.VerifyServerHostname = false
|
|
c.autoTLS.verifyServerHostname = true
|
|
require.True(t, c.VerifyServerHostname())
|
|
|
|
c.base.InternalRPC.VerifyServerHostname = true
|
|
c.autoTLS.verifyServerHostname = true
|
|
require.True(t, c.VerifyServerHostname())
|
|
}
|
|
|
|
func TestConfigurator_AutoEncryptCert(t *testing.T) {
|
|
c := Configurator{base: &Config{}}
|
|
require.Nil(t, c.AutoEncryptCert())
|
|
|
|
cert, err := loadKeyPair("../test/key/something_expired.cer", "../test/key/something_expired.key")
|
|
require.NoError(t, err)
|
|
c.autoTLS.cert = cert
|
|
require.Equal(t, int64(1561561551), c.AutoEncryptCert().NotAfter.Unix())
|
|
|
|
cert, err = loadKeyPair("../test/key/ourdomain.cer", "../test/key/ourdomain.key")
|
|
require.NoError(t, err)
|
|
c.autoTLS.cert = cert
|
|
require.Equal(t, int64(4803632738), c.AutoEncryptCert().NotAfter.Unix())
|
|
}
|
|
|
|
func TestConfigurator_AuthorizeInternalRPCServerConn(t *testing.T) {
|
|
caPEM, caPK, err := GenerateCA(CAOpts{Days: 5, Domain: "consul"})
|
|
require.NoError(t, err)
|
|
|
|
dir := testutil.TempDir(t, "ca")
|
|
caPath := filepath.Join(dir, "ca.pem")
|
|
err = ioutil.WriteFile(caPath, []byte(caPEM), 0600)
|
|
require.NoError(t, err)
|
|
|
|
// Cert and key are not used, but required to get past validation.
|
|
signer, err := ParseSigner(caPK)
|
|
require.NoError(t, err)
|
|
pub, pk, err := GenerateCert(CertOpts{
|
|
Signer: signer,
|
|
CA: caPEM,
|
|
})
|
|
require.NoError(t, err)
|
|
certFile := filepath.Join("cert.pem")
|
|
err = ioutil.WriteFile(certFile, []byte(pub), 0600)
|
|
require.NoError(t, err)
|
|
keyFile := filepath.Join("cert.key")
|
|
err = ioutil.WriteFile(keyFile, []byte(pk), 0600)
|
|
require.NoError(t, err)
|
|
|
|
cfg := Config{
|
|
InternalRPC: ProtocolConfig{
|
|
VerifyServerHostname: true,
|
|
VerifyIncoming: true,
|
|
CAFile: caPath,
|
|
CertFile: certFile,
|
|
KeyFile: keyFile,
|
|
},
|
|
Domain: "consul",
|
|
}
|
|
c := makeConfigurator(t, cfg)
|
|
|
|
t.Run("wrong DNSName", func(t *testing.T) {
|
|
signer, err := ParseSigner(caPK)
|
|
require.NoError(t, err)
|
|
|
|
pem, _, err := GenerateCert(CertOpts{
|
|
Signer: signer,
|
|
CA: caPEM,
|
|
Name: "server.dc1.consul",
|
|
Days: 5,
|
|
DNSNames: []string{"this-name-is-wrong", "localhost"},
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
s := fakeTLSConn{
|
|
state: tls.ConnectionState{
|
|
VerifiedChains: [][]*x509.Certificate{certChain(t, pem, caPEM)},
|
|
PeerCertificates: certChain(t, pem, caPEM),
|
|
},
|
|
}
|
|
err = c.AuthorizeServerConn("dc1", s)
|
|
testutil.RequireErrorContains(t, err, "is valid for this-name-is-wrong, localhost, not server.dc1.consul")
|
|
})
|
|
|
|
t.Run("wrong CA", func(t *testing.T) {
|
|
caPEM, caPK, err := GenerateCA(CAOpts{Days: 5, Domain: "consul"})
|
|
require.NoError(t, err)
|
|
|
|
dir := testutil.TempDir(t, "other")
|
|
caPath := filepath.Join(dir, "ca.pem")
|
|
err = ioutil.WriteFile(caPath, []byte(caPEM), 0600)
|
|
require.NoError(t, err)
|
|
|
|
signer, err := ParseSigner(caPK)
|
|
require.NoError(t, err)
|
|
|
|
pem, _, err := GenerateCert(CertOpts{
|
|
Signer: signer,
|
|
CA: caPEM,
|
|
Name: "server.dc1.consul",
|
|
Days: 5,
|
|
DNSNames: []string{"server.dc1.consul", "localhost"},
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
s := fakeTLSConn{
|
|
state: tls.ConnectionState{
|
|
VerifiedChains: [][]*x509.Certificate{certChain(t, pem, caPEM)},
|
|
PeerCertificates: certChain(t, pem, caPEM),
|
|
},
|
|
}
|
|
err = c.AuthorizeServerConn("dc1", s)
|
|
testutil.RequireErrorContains(t, err, "signed by unknown authority")
|
|
})
|
|
|
|
t.Run("missing ext key usage", func(t *testing.T) {
|
|
signer, err := ParseSigner(caPK)
|
|
require.NoError(t, err)
|
|
|
|
pem, _, err := GenerateCert(CertOpts{
|
|
Signer: signer,
|
|
CA: caPEM,
|
|
Name: "server.dc1.consul",
|
|
Days: 5,
|
|
DNSNames: []string{"server.dc1.consul", "localhost"},
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageEmailProtection},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
s := fakeTLSConn{
|
|
state: tls.ConnectionState{
|
|
VerifiedChains: [][]*x509.Certificate{certChain(t, pem, caPEM)},
|
|
PeerCertificates: certChain(t, pem, caPEM),
|
|
},
|
|
}
|
|
err = c.AuthorizeServerConn("dc1", s)
|
|
testutil.RequireErrorContains(t, err, "certificate specifies an incompatible key usage")
|
|
})
|
|
|
|
t.Run("disabled by verify_incoming_rpc", func(t *testing.T) {
|
|
cfg := Config{
|
|
InternalRPC: ProtocolConfig{
|
|
VerifyServerHostname: true,
|
|
VerifyIncoming: false,
|
|
CAFile: caPath,
|
|
},
|
|
Domain: "consul",
|
|
}
|
|
c, err := NewConfigurator(cfg, hclog.New(nil))
|
|
require.NoError(t, err)
|
|
|
|
s := fakeTLSConn{}
|
|
err = c.AuthorizeServerConn("dc1", s)
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func TestConfigurator_GRPCServerUseTLS(t *testing.T) {
|
|
t.Run("certificate manually configured", func(t *testing.T) {
|
|
c := makeConfigurator(t, Config{
|
|
GRPC: ProtocolConfig{
|
|
CertFile: "../test/hostname/Alice.crt",
|
|
KeyFile: "../test/hostname/Alice.key",
|
|
},
|
|
})
|
|
require.True(t, c.GRPCServerUseTLS())
|
|
})
|
|
|
|
t.Run("no certificate", func(t *testing.T) {
|
|
c := makeConfigurator(t, Config{})
|
|
require.False(t, c.GRPCServerUseTLS())
|
|
})
|
|
|
|
t.Run("AutoTLS (default)", func(t *testing.T) {
|
|
c := makeConfigurator(t, Config{})
|
|
|
|
bobCert := loadFile(t, "../test/hostname/Bob.crt")
|
|
bobKey := loadFile(t, "../test/hostname/Bob.key")
|
|
require.NoError(t, c.UpdateAutoTLSCert(bobCert, bobKey))
|
|
require.False(t, c.GRPCServerUseTLS())
|
|
})
|
|
|
|
t.Run("AutoTLS w/ UseAutoCert Disabled", func(t *testing.T) {
|
|
c := makeConfigurator(t, Config{
|
|
GRPC: ProtocolConfig{
|
|
UseAutoCert: false,
|
|
},
|
|
})
|
|
|
|
bobCert := loadFile(t, "../test/hostname/Bob.crt")
|
|
bobKey := loadFile(t, "../test/hostname/Bob.key")
|
|
require.NoError(t, c.UpdateAutoTLSCert(bobCert, bobKey))
|
|
require.False(t, c.GRPCServerUseTLS())
|
|
})
|
|
|
|
t.Run("AutoTLS w/ UseAutoCert Enabled", func(t *testing.T) {
|
|
c := makeConfigurator(t, Config{
|
|
GRPC: ProtocolConfig{
|
|
UseAutoCert: true,
|
|
},
|
|
})
|
|
|
|
bobCert := loadFile(t, "../test/hostname/Bob.crt")
|
|
bobKey := loadFile(t, "../test/hostname/Bob.key")
|
|
require.NoError(t, c.UpdateAutoTLSCert(bobCert, bobKey))
|
|
require.True(t, c.GRPCServerUseTLS())
|
|
})
|
|
}
|
|
|
|
type fakeTLSConn struct {
|
|
state tls.ConnectionState
|
|
}
|
|
|
|
func (f fakeTLSConn) ConnectionState() tls.ConnectionState {
|
|
return f.state
|
|
}
|
|
|
|
func certChain(t *testing.T, certs ...string) []*x509.Certificate {
|
|
t.Helper()
|
|
|
|
result := make([]*x509.Certificate, 0, len(certs))
|
|
|
|
for i, c := range certs {
|
|
cert, err := parseCert(c)
|
|
require.NoError(t, err, "cert %d", i)
|
|
result = append(result, cert)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func startRPCTLSServer(t *testing.T, c *Configurator) (net.Conn, <-chan error) {
|
|
client, errc, _ := startTLSServer(c.IncomingRPCConfig())
|
|
return client, errc
|
|
}
|
|
|
|
func startALPNRPCTLSServer(t *testing.T, config *Config, alpnProtos []string) (net.Conn, <-chan error) {
|
|
cfg := makeConfigurator(t, *config).IncomingALPNRPCConfig(alpnProtos)
|
|
client, errc, _ := startTLSServer(cfg)
|
|
return client, errc
|
|
}
|
|
|
|
func makeConfigurator(t *testing.T, config Config) *Configurator {
|
|
t.Helper()
|
|
|
|
c, err := NewConfigurator(config, nil)
|
|
require.NoError(t, err)
|
|
|
|
return c
|
|
}
|
|
|
|
func startTLSServer(tlsConfigServer *tls.Config) (net.Conn, <-chan error, <-chan []*x509.Certificate) {
|
|
errc := make(chan error, 1)
|
|
certc := make(chan []*x509.Certificate, 1)
|
|
|
|
client, server := net.Pipe()
|
|
|
|
// Use yamux to buffer the reads, otherwise it's easy to deadlock
|
|
muxConf := yamux.DefaultConfig()
|
|
serverSession, _ := yamux.Server(server, muxConf)
|
|
clientSession, _ := yamux.Client(client, muxConf)
|
|
clientConn, _ := clientSession.Open()
|
|
serverConn, _ := serverSession.Accept()
|
|
|
|
go func() {
|
|
tlsServer := tls.Server(serverConn, tlsConfigServer)
|
|
if err := tlsServer.Handshake(); err != nil {
|
|
errc <- err
|
|
}
|
|
certc <- tlsServer.ConnectionState().PeerCertificates
|
|
close(errc)
|
|
|
|
// Because net.Pipe() is unbuffered, if both sides
|
|
// Close() simultaneously, we will deadlock as they
|
|
// both send an alert and then block. So we make the
|
|
// server read any data from the client until error or
|
|
// EOF, which will allow the client to Close(), and
|
|
// *then* we Close() the server.
|
|
io.Copy(ioutil.Discard, tlsServer)
|
|
tlsServer.Close()
|
|
}()
|
|
return clientConn, errc, certc
|
|
}
|
|
|
|
func loadFile(t *testing.T, path string) string {
|
|
t.Helper()
|
|
|
|
data, err := ioutil.ReadFile(path)
|
|
require.NoError(t, err)
|
|
return string(data)
|
|
}
|
|
|
|
func getExpectedCaPoolByFile(t *testing.T) *x509.CertPool {
|
|
pool := x509.NewCertPool()
|
|
data, err := ioutil.ReadFile("../test/ca/root.cer")
|
|
if err != nil {
|
|
t.Fatal("could not open test file ../test/ca/root.cer for reading")
|
|
}
|
|
if !pool.AppendCertsFromPEM(data) {
|
|
t.Fatal("could not add test ca ../test/ca/root.cer to pool")
|
|
}
|
|
return pool
|
|
}
|
|
|
|
func getExpectedCaPoolByDir(t *testing.T) *x509.CertPool {
|
|
pool := x509.NewCertPool()
|
|
entries, err := os.ReadDir("../test/ca_path")
|
|
if err != nil {
|
|
t.Fatal("could not open test dir ../test/ca_path for reading")
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
filename := path.Join("../test/ca_path", entry.Name())
|
|
|
|
data, err := ioutil.ReadFile(filename)
|
|
if err != nil {
|
|
t.Fatalf("could not open test file %s for reading", filename)
|
|
}
|
|
|
|
if !pool.AppendCertsFromPEM(data) {
|
|
t.Fatalf("could not add test ca %s to pool", filename)
|
|
}
|
|
}
|
|
|
|
return pool
|
|
}
|
|
|
|
// lazyCerts has a func field which can't be compared.
|
|
var cmpCertPool = cmp.Options{
|
|
cmpopts.IgnoreFields(x509.CertPool{}, "lazyCerts"),
|
|
cmp.AllowUnexported(x509.CertPool{}),
|
|
}
|
|
|
|
func assertDeepEqual(t *testing.T, x, y interface{}, opts ...cmp.Option) {
|
|
t.Helper()
|
|
if diff := cmp.Diff(x, y, opts...); diff != "" {
|
|
t.Fatalf("assertion failed: values are not equal\n--- expected\n+++ actual\n%v", diff)
|
|
}
|
|
}
|