package connect import ( "crypto/tls" "crypto/x509" "encoding/pem" "testing" "github.com/hashicorp/consul/agent" "github.com/hashicorp/consul/agent/connect" "github.com/hashicorp/consul/api" "github.com/stretchr/testify/require" ) func TestReloadableTLSConfig(t *testing.T) { require := require.New(t) base := defaultTLSConfig(nil) c := newReloadableTLSConfig(base) // The dynamic config should be the one we loaded (with some different hooks) got := c.TLSConfig() expect := base.Clone() // Equal and even cmp.Diff fail on tls.Config due to unexported fields in // each. Compare a few things to prove it's returning the bits we // specifically set. require.Equal(expect.Certificates, got.Certificates) require.Equal(expect.RootCAs, got.RootCAs) require.Equal(expect.ClientCAs, got.ClientCAs) require.Equal(expect.InsecureSkipVerify, got.InsecureSkipVerify) require.Equal(expect.MinVersion, got.MinVersion) require.Equal(expect.CipherSuites, got.CipherSuites) require.NotNil(got.GetClientCertificate) require.NotNil(got.GetConfigForClient) require.Contains(got.NextProtos, "h2") ca := connect.TestCA(t, nil) // Now change the config as if we just loaded certs from Consul new := TestTLSConfig(t, "web", ca) err := c.SetTLSConfig(new) require.Nil(err) // Change the passed config to ensure SetTLSConfig made a copy otherwise this // is racey. expect = new.Clone() new.Certificates = nil // The dynamic config should be the one we loaded (with some different hooks) got = c.TLSConfig() require.Equal(expect.Certificates, got.Certificates) require.Equal(expect.RootCAs, got.RootCAs) require.Equal(expect.ClientCAs, got.ClientCAs) require.Equal(expect.InsecureSkipVerify, got.InsecureSkipVerify) require.Equal(expect.MinVersion, got.MinVersion) require.Equal(expect.CipherSuites, got.CipherSuites) require.NotNil(got.GetClientCertificate) require.NotNil(got.GetConfigForClient) require.Contains(got.NextProtos, "h2") } func Test_verifyServerCertMatchesURI(t *testing.T) { ca1 := connect.TestCA(t, nil) tests := []struct { name string certs []*x509.Certificate expected connect.CertURI wantErr bool }{ { name: "simple match", certs: TestPeerCertificates(t, "web", ca1), expected: connect.TestSpiffeIDService(t, "web"), wantErr: false, }, { name: "mismatch", certs: TestPeerCertificates(t, "web", ca1), expected: connect.TestSpiffeIDService(t, "db"), wantErr: true, }, { name: "no certs", certs: []*x509.Certificate{}, expected: connect.TestSpiffeIDService(t, "db"), wantErr: true, }, { name: "nil certs", certs: nil, expected: connect.TestSpiffeIDService(t, "db"), wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := verifyServerCertMatchesURI(tt.certs, tt.expected) if tt.wantErr { require.NotNil(t, err) } else { require.Nil(t, err) } }) } } func testCertPEMBlock(t *testing.T, pemValue string) []byte { t.Helper() // The _ result below is not an error but the remaining PEM bytes. block, _ := pem.Decode([]byte(pemValue)) require.NotNil(t, block) require.Equal(t, "CERTIFICATE", block.Type) return block.Bytes } func TestClientSideVerifier(t *testing.T) { ca1 := connect.TestCA(t, nil) ca2 := connect.TestCA(t, ca1) webCA1PEM, _ := connect.TestLeaf(t, "web", ca1) webCA2PEM, _ := connect.TestLeaf(t, "web", ca2) webCA1 := testCertPEMBlock(t, webCA1PEM) xcCA2 := testCertPEMBlock(t, ca2.SigningCert) webCA2 := testCertPEMBlock(t, webCA2PEM) tests := []struct { name string tlsCfg *tls.Config rawCerts [][]byte wantErr string }{ { name: "ok service ca1", tlsCfg: TestTLSConfig(t, "web", ca1), rawCerts: [][]byte{webCA1}, wantErr: "", }, { name: "untrusted CA", tlsCfg: TestTLSConfig(t, "web", ca2), // only trust ca2 rawCerts: [][]byte{webCA1}, // present ca1 wantErr: "unknown authority", }, { name: "cross signed intermediate", tlsCfg: TestTLSConfig(t, "web", ca1), // only trust ca1 rawCerts: [][]byte{webCA2, xcCA2}, // present ca2 signed cert, and xc wantErr: "", }, { name: "cross signed without intermediate", tlsCfg: TestTLSConfig(t, "web", ca1), // only trust ca1 rawCerts: [][]byte{webCA2}, // present ca2 signed cert only wantErr: "unknown authority", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { require := require.New(t) err := clientSideVerifier(tt.tlsCfg, tt.rawCerts) if tt.wantErr == "" { require.Nil(err) } else { require.NotNil(err) require.Contains(err.Error(), tt.wantErr) } }) } } func TestServerSideVerifier(t *testing.T) { ca1 := connect.TestCA(t, nil) ca2 := connect.TestCA(t, ca1) webCA1PEM, _ := connect.TestLeaf(t, "web", ca1) webCA2PEM, _ := connect.TestLeaf(t, "web", ca2) apiCA1PEM, _ := connect.TestLeaf(t, "api", ca1) apiCA2PEM, _ := connect.TestLeaf(t, "api", ca2) webCA1 := testCertPEMBlock(t, webCA1PEM) xcCA2 := testCertPEMBlock(t, ca2.SigningCert) webCA2 := testCertPEMBlock(t, webCA2PEM) apiCA1 := testCertPEMBlock(t, apiCA1PEM) apiCA2 := testCertPEMBlock(t, apiCA2PEM) // Setup a local test agent to query agent := agent.NewTestAgent("test-consul", "") defer agent.Shutdown() cfg := api.DefaultConfig() cfg.Address = agent.HTTPAddr() client, err := api.NewClient(cfg) require.Nil(t, err) // Setup intentions to validate against. We actually default to allow so first // setup a blanket deny rule for db, then only allow web. connect := client.Connect() ixn := &api.Intention{ SourceNS: "default", SourceName: "*", DestinationNS: "default", DestinationName: "db", Action: api.IntentionActionDeny, SourceType: api.IntentionSourceConsul, Meta: map[string]string{}, } id, _, err := connect.IntentionCreate(ixn, nil) require.Nil(t, err) require.NotEmpty(t, id) ixn = &api.Intention{ SourceNS: "default", SourceName: "web", DestinationNS: "default", DestinationName: "db", Action: api.IntentionActionAllow, SourceType: api.IntentionSourceConsul, Meta: map[string]string{}, } id, _, err = connect.IntentionCreate(ixn, nil) require.Nil(t, err) require.NotEmpty(t, id) tests := []struct { name string service string tlsCfg *tls.Config rawCerts [][]byte wantErr string }{ { name: "ok service ca1, allow", service: "db", tlsCfg: TestTLSConfig(t, "db", ca1), rawCerts: [][]byte{webCA1}, wantErr: "", }, { name: "untrusted CA", service: "db", tlsCfg: TestTLSConfig(t, "db", ca2), // only trust ca2 rawCerts: [][]byte{webCA1}, // present ca1 wantErr: "unknown authority", }, { name: "cross signed intermediate, allow", service: "db", tlsCfg: TestTLSConfig(t, "db", ca1), // only trust ca1 rawCerts: [][]byte{webCA2, xcCA2}, // present ca2 signed cert, and xc wantErr: "", }, { name: "cross signed without intermediate", service: "db", tlsCfg: TestTLSConfig(t, "db", ca1), // only trust ca1 rawCerts: [][]byte{webCA2}, // present ca2 signed cert only wantErr: "unknown authority", }, { name: "ok service ca1, deny", service: "db", tlsCfg: TestTLSConfig(t, "db", ca1), rawCerts: [][]byte{apiCA1}, wantErr: "denied", }, { name: "cross signed intermediate, deny", service: "db", tlsCfg: TestTLSConfig(t, "db", ca1), // only trust ca1 rawCerts: [][]byte{apiCA2, xcCA2}, // present ca2 signed cert, and xc wantErr: "denied", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { v := newServerSideVerifier(client, tt.service) err := v(tt.tlsCfg, tt.rawCerts) if tt.wantErr == "" { require.Nil(t, err) } else { require.NotNil(t, err) require.Contains(t, err.Error(), tt.wantErr) } }) } }