mirror of https://github.com/hashicorp/consul
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1804 lines
50 KiB
1804 lines
50 KiB
// Copyright (c) HashiCorp, Inc. |
|
// SPDX-License-Identifier: BUSL-1.1 |
|
//go:build !fips |
|
|
|
package tlsutil |
|
|
|
import ( |
|
"crypto/tls" |
|
"crypto/x509" |
|
"fmt" |
|
"io" |
|
"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.(*tls.CertificateVerificationError) |
|
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 consul server name, no override", |
|
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: "", |
|
}, |
|
}, |
|
{ |
|
name: "agent tls, skip verify, consul node name for server name, no override", |
|
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: "", |
|
}, |
|
}, |
|
{ |
|
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(4852545616), 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 = os.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 = os.WriteFile(certFile, []byte(pub), 0600) |
|
require.NoError(t, err) |
|
keyFile := filepath.Join("cert.key") |
|
err = os.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 = os.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(io.Discard, tlsServer) |
|
tlsServer.Close() |
|
}() |
|
return clientConn, errc, certc |
|
} |
|
|
|
func loadFile(t *testing.T, path string) string { |
|
t.Helper() |
|
|
|
data, err := os.ReadFile(path) |
|
require.NoError(t, err) |
|
return string(data) |
|
} |
|
|
|
func getExpectedCaPoolByFile(t *testing.T) *x509.CertPool { |
|
pool := x509.NewCertPool() |
|
data, err := os.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 := os.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) |
|
} |
|
}
|
|
|