// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package consul

import (
	"crypto/x509"
	"encoding/pem"
	"fmt"
	"os"
	"sync"
	"testing"
	"time"

	msgpackrpc "github.com/hashicorp/consul-net-rpc/net-rpc-msgpackrpc"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/hashicorp/consul/acl"
	"github.com/hashicorp/consul/agent/connect"
	ca "github.com/hashicorp/consul/agent/connect/ca"
	"github.com/hashicorp/consul/agent/structs"
	"github.com/hashicorp/consul/sdk/testutil"
	"github.com/hashicorp/consul/sdk/testutil/retry"
	"github.com/hashicorp/consul/testrpc"
)

func testParseCert(t *testing.T, pemValue string) *x509.Certificate {
	cert, err := connect.ParseCert(pemValue)
	if err != nil {
		t.Fatal(err)
	}
	return cert
}

// Test listing root CAs.
func TestConnectCARoots(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	dir1, s1 := testServer(t)
	defer os.RemoveAll(dir1)
	defer s1.Shutdown()
	codec := rpcClient(t, s1)
	defer codec.Close()

	testrpc.WaitForTestAgent(t, s1.RPC, "dc1")

	// Insert some CAs
	state := s1.fsm.State()
	ca1 := connect.TestCA(t, nil)
	ca2 := connect.TestCA(t, nil)
	ca2.Active = false
	idx, _, err := state.CARoots(nil)
	require.NoError(t, err)
	ok, err := state.CARootSetCAS(idx, idx, []*structs.CARoot{ca1, ca2})
	assert.True(t, ok)
	require.NoError(t, err)
	_, caCfg, err := state.CAConfig(nil)
	require.NoError(t, err)

	// Request
	args := &structs.DCSpecificRequest{
		Datacenter: "dc1",
	}
	var reply structs.IndexedCARoots
	require.NoError(t, msgpackrpc.CallWithCodec(codec, "ConnectCA.Roots", args, &reply))

	// Verify
	assert.Equal(t, ca1.ID, reply.ActiveRootID)
	assert.Len(t, reply.Roots, 2)
	for _, r := range reply.Roots {
		// These must never be set, for security
		assert.Equal(t, "", r.SigningCert)
		assert.Equal(t, "", r.SigningKey)
	}
	assert.Equal(t, fmt.Sprintf("%s.consul", caCfg.ClusterID), reply.TrustDomain)
}

func TestConnectCAConfig_GetSet(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	dir1, s1 := testServer(t)
	defer os.RemoveAll(dir1)
	defer s1.Shutdown()
	codec := rpcClient(t, s1)
	defer codec.Close()

	testrpc.WaitForTestAgent(t, s1.RPC, "dc1")

	// Get the starting config
	{
		args := &structs.DCSpecificRequest{
			Datacenter: "dc1",
		}
		var reply structs.CAConfiguration
		assert.NoError(t, msgpackrpc.CallWithCodec(codec, "ConnectCA.ConfigurationGet", args, &reply))

		actual, err := ca.ParseConsulCAConfig(reply.Config)
		assert.NoError(t, err)
		expected, err := ca.ParseConsulCAConfig(s1.config.CAConfig.Config)
		assert.NoError(t, err)
		assert.Equal(t, reply.Provider, s1.config.CAConfig.Provider)
		assert.Equal(t, actual, expected)
	}

	testState := map[string]string{"foo": "bar"}

	// Update a config value
	newConfig := &structs.CAConfiguration{
		Provider: "consul",
		Config: map[string]interface{}{
			"PrivateKey": "",
			"RootCert":   "",
			// This verifies the state persistence for providers although Consul
			// provider doesn't actually use that mechanism outside of tests.
			"test_state": testState,
		},
	}
	{
		args := &structs.CARequest{
			Datacenter: "dc1",
			Config:     newConfig,
		}
		var reply interface{}
		retry.Run(t, func(r *retry.R) {
			r.Check(msgpackrpc.CallWithCodec(codec, "ConnectCA.ConfigurationSet", args, &reply))
		})
	}

	// Verify the new config was set
	{
		args := &structs.DCSpecificRequest{
			Datacenter: "dc1",
		}
		var reply structs.CAConfiguration
		assert.NoError(t, msgpackrpc.CallWithCodec(codec, "ConnectCA.ConfigurationGet", args, &reply))

		actual, err := ca.ParseConsulCAConfig(reply.Config)
		assert.NoError(t, err)
		expected, err := ca.ParseConsulCAConfig(newConfig.Config)
		assert.NoError(t, err)
		assert.Equal(t, reply.Provider, newConfig.Provider)
		assert.Equal(t, actual, expected)
		assert.Equal(t, testState, reply.State)
	}
}

func TestConnectCAConfig_GetSet_ACLDeny(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	dir1, s1 := testServerWithConfig(t, func(c *Config) {
		c.PrimaryDatacenter = "dc1"
		c.ACLsEnabled = true
		c.ACLInitialManagementToken = TestDefaultInitialManagementToken
		c.ACLResolverSettings.ACLDefaultPolicy = "deny"
	})
	defer os.RemoveAll(dir1)
	defer s1.Shutdown()

	codec := rpcClient(t, s1)
	defer codec.Close()

	testrpc.WaitForLeader(t, s1.RPC, "dc1")

	opReadToken, err := upsertTestTokenWithPolicyRules(
		codec, TestDefaultInitialManagementToken, "dc1", `operator = "read"`)
	require.NoError(t, err)

	opWriteToken, err := upsertTestTokenWithPolicyRules(
		codec, TestDefaultInitialManagementToken, "dc1", `operator = "write"`)
	require.NoError(t, err)

	// Update a config value
	newConfig := &structs.CAConfiguration{
		Provider: "consul",
		Config: map[string]interface{}{
			"PrivateKey": `
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49
AwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav
q5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==
-----END EC PRIVATE KEY-----`,
			"RootCert": `
-----BEGIN CERTIFICATE-----
MIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ
VGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG
A1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR
AB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7
SkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD
AgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6
NDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6
NWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf
ZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6
ZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw
WQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1
NTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG
SM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA
pY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=
-----END CERTIFICATE-----`,
		},
	}

	args := &structs.CARequest{
		Datacenter:   "dc1",
		Config:       newConfig,
		WriteRequest: structs.WriteRequest{Token: TestDefaultInitialManagementToken},
	}
	var reply interface{}
	require.NoError(t, msgpackrpc.CallWithCodec(codec, "ConnectCA.ConfigurationSet", args, &reply))

	t.Run("deny get with operator:read", func(t *testing.T) {
		args := &structs.DCSpecificRequest{
			Datacenter:   "dc1",
			QueryOptions: structs.QueryOptions{Token: opReadToken.SecretID},
		}

		var reply structs.CAConfiguration
		err = msgpackrpc.CallWithCodec(codec, "ConnectCA.ConfigurationGet", args, &reply)
		assert.True(t, acl.IsErrPermissionDenied(err))
	})

	t.Run("allow get with operator:write", func(t *testing.T) {
		args := &structs.DCSpecificRequest{
			Datacenter:   "dc1",
			QueryOptions: structs.QueryOptions{Token: opWriteToken.SecretID},
		}

		var reply structs.CAConfiguration
		err = msgpackrpc.CallWithCodec(codec, "ConnectCA.ConfigurationGet", args, &reply)
		assert.False(t, acl.IsErrPermissionDenied(err))
		assert.Equal(t, newConfig.Config, reply.Config)
	})
}

// This test case tests that the logic around forcing a rotation without cross
// signing works when requested (and is denied when not requested). This occurs
// if the current CA is not able to cross sign external CA certificates.
func TestConnectCAConfig_GetSetForceNoCrossSigning(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	// Setup a server with a built-in CA that as artificially disabled cross
	// signing. This is simpler than running tests with external CA dependencies.
	dir1, s1 := testServerWithConfig(t, func(c *Config) {
		c.CAConfig.Config["DisableCrossSigning"] = true
	})
	defer os.RemoveAll(dir1)
	defer s1.Shutdown()
	codec := rpcClient(t, s1)
	defer codec.Close()

	testrpc.WaitForTestAgent(t, s1.RPC, "dc1")

	// Store the current root
	rootReq := &structs.DCSpecificRequest{
		Datacenter: "dc1",
	}
	var rootList structs.IndexedCARoots
	require.NoError(t, msgpackrpc.CallWithCodec(codec, "ConnectCA.Roots", rootReq, &rootList))
	require.Len(t, rootList.Roots, 1)
	oldRoot := rootList.Roots[0]

	// Get the starting config
	{
		args := &structs.DCSpecificRequest{
			Datacenter: "dc1",
		}
		var reply structs.CAConfiguration
		require.NoError(t, msgpackrpc.CallWithCodec(codec, "ConnectCA.ConfigurationGet", args, &reply))

		actual, err := ca.ParseConsulCAConfig(reply.Config)
		require.NoError(t, err)
		expected, err := ca.ParseConsulCAConfig(s1.config.CAConfig.Config)
		require.NoError(t, err)
		require.Equal(t, reply.Provider, s1.config.CAConfig.Provider)
		require.Equal(t, actual, expected)
	}

	// Update to a new CA with different key. This should fail since the existing
	// CA doesn't support cross signing so can't rotate safely.
	_, newKey, err := connect.GeneratePrivateKey()
	require.NoError(t, err)
	newConfig := &structs.CAConfiguration{
		Provider: "consul",
		Config: map[string]interface{}{
			"PrivateKey": newKey,
		},
	}
	{
		args := &structs.CARequest{
			Datacenter: "dc1",
			Config:     newConfig,
		}
		var reply interface{}
		err := msgpackrpc.CallWithCodec(codec, "ConnectCA.ConfigurationSet", args, &reply)
		require.EqualError(t, err, "The current CA Provider does not support cross-signing. "+
			"You can try again with ForceWithoutCrossSigningSet but this may cause disruption"+
			" - see documentation for more.")
	}

	// Now try again with the force flag set and it should work
	{
		newConfig.ForceWithoutCrossSigning = true
		args := &structs.CARequest{
			Datacenter: "dc1",
			Config:     newConfig,
		}
		var reply interface{}
		err := msgpackrpc.CallWithCodec(codec, "ConnectCA.ConfigurationSet", args, &reply)
		require.NoError(t, err)
	}

	// Make sure the new root has been added but with no cross-signed intermediate
	{
		args := &structs.DCSpecificRequest{
			Datacenter: "dc1",
		}
		var reply structs.IndexedCARoots
		require.NoError(t, msgpackrpc.CallWithCodec(codec, "ConnectCA.Roots", args, &reply))
		require.Len(t, reply.Roots, 2)

		for _, r := range reply.Roots {
			if r.ID == oldRoot.ID {
				// The old root should no longer be marked as the active root,
				// and none of its other fields should have changed.
				require.False(t, r.Active)
				require.Equal(t, r.Name, oldRoot.Name)
				require.Equal(t, r.RootCert, oldRoot.RootCert)
				require.Equal(t, r.SigningCert, oldRoot.SigningCert)
				require.Equal(t, r.IntermediateCerts, oldRoot.IntermediateCerts)
			} else {
				// The new root should NOT have a valid cross-signed cert from the old
				// root as an intermediate.
				require.True(t, r.Active)
				require.Empty(t, r.IntermediateCerts)
			}
		}
	}
}

func TestConnectCAConfig_TriggerRotation(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	cases := []struct {
		name     string
		configFn func() (*structs.CAConfiguration, error)
	}{
		{
			name: "new private key provided",
			configFn: func() (*structs.CAConfiguration, error) {
				// Update the provider config to use a new private key, which should
				// cause a rotation.
				_, newKey, err := connect.GeneratePrivateKey()
				if err != nil {
					return nil, err
				}

				return &structs.CAConfiguration{
					Provider: "consul",
					Config: map[string]interface{}{
						"PrivateKey": newKey,
						"RootCert":   "",
					},
				}, nil
			},
		},
		{
			name: "update private key bits",
			configFn: func() (*structs.CAConfiguration, error) {
				return &structs.CAConfiguration{
					Provider: "consul",
					Config: map[string]interface{}{
						"PrivateKeyType": "ec",
						"PrivateKeyBits": 384,
					},
				}, nil
			},
		},
		{
			name: "update private key type",
			configFn: func() (*structs.CAConfiguration, error) {
				return &structs.CAConfiguration{
					Provider: "consul",
					Config: map[string]interface{}{
						"PrivateKeyType": "rsa",
						"PrivateKeyBits": "2048",
					},
				}, nil
			},
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			dir1, s1 := testServer(t)
			defer os.RemoveAll(dir1)
			defer s1.Shutdown()
			codec := rpcClient(t, s1)
			defer codec.Close()

			testrpc.WaitForTestAgent(t, s1.RPC, "dc1")

			// Store the current root
			rootReq := &structs.DCSpecificRequest{
				Datacenter: "dc1",
			}
			var rootList structs.IndexedCARoots
			require.Nil(t, msgpackrpc.CallWithCodec(codec, "ConnectCA.Roots", rootReq, &rootList))
			assert.Len(t, rootList.Roots, 1)
			oldRoot := rootList.Roots[0]

			newConfig, err := tc.configFn()
			require.NoError(t, err)

			{
				args := &structs.CARequest{
					Datacenter: "dc1",
					Config:     newConfig,
				}
				var reply interface{}

				require.NoError(t, msgpackrpc.CallWithCodec(codec, "ConnectCA.ConfigurationSet", args, &reply))
			}

			// Make sure the new root has been added along with an intermediate
			// cross-signed by the old root.
			var newRootPEM string
			testutil.RunStep(t, "ensure roots look correct", func(t *testing.T) {
				args := &structs.DCSpecificRequest{
					Datacenter: "dc1",
				}
				var reply structs.IndexedCARoots
				require.Nil(t, msgpackrpc.CallWithCodec(codec, "ConnectCA.Roots", args, &reply))
				assert.Len(t, reply.Roots, 2)

				for _, r := range reply.Roots {
					if r.ID == oldRoot.ID {
						// The old root should no longer be marked as the active root,
						// and none of its other fields should have changed.
						assert.False(t, r.Active)
						assert.Equal(t, r.Name, oldRoot.Name)
						assert.Equal(t, r.RootCert, oldRoot.RootCert)
						assert.Equal(t, r.SigningCert, oldRoot.SigningCert)
						assert.Equal(t, r.IntermediateCerts, oldRoot.IntermediateCerts)
					} else {
						newRootPEM = r.RootCert
						// The new root should have a valid cross-signed cert from the old
						// root as an intermediate.
						assert.True(t, r.Active)
						assert.Len(t, r.IntermediateCerts, 1)

						xc := testParseCert(t, r.IntermediateCerts[0])
						oldRootCert := testParseCert(t, oldRoot.RootCert)
						newRootCert := testParseCert(t, r.RootCert)

						// Should have the authority key ID and signature algo of the
						// (old) signing CA.
						assert.Equal(t, xc.AuthorityKeyId, oldRootCert.AuthorityKeyId)
						assert.NotEqual(t, xc.SubjectKeyId, oldRootCert.SubjectKeyId)
						assert.Equal(t, xc.SignatureAlgorithm, oldRootCert.SignatureAlgorithm)

						// The common name and SAN should not have changed.
						assert.Equal(t, xc.Subject.CommonName, newRootCert.Subject.CommonName)
						assert.Equal(t, xc.URIs, newRootCert.URIs)
					}
				}
			})

			testutil.RunStep(t, "verify the new config was set", func(t *testing.T) {
				args := &structs.DCSpecificRequest{
					Datacenter: "dc1",
				}
				var reply structs.CAConfiguration
				require.NoError(t, msgpackrpc.CallWithCodec(codec, "ConnectCA.ConfigurationGet", args, &reply))

				actual, err := ca.ParseConsulCAConfig(reply.Config)
				require.NoError(t, err)
				expected, err := ca.ParseConsulCAConfig(newConfig.Config)
				require.NoError(t, err)
				assert.Equal(t, reply.Provider, newConfig.Provider)
				assert.Equal(t, actual, expected)
			})

			testutil.RunStep(t, "verify that new leaf certs get the cross-signed intermediate bundled", func(t *testing.T) {
				// Generate a CSR and request signing
				spiffeId := connect.TestSpiffeIDService(t, "web")
				csr, _ := connect.TestCSR(t, spiffeId)
				args := &structs.CASignRequest{
					Datacenter: "dc1",
					CSR:        csr,
				}
				var reply structs.IssuedCert
				require.NoError(t, msgpackrpc.CallWithCodec(codec, "ConnectCA.Sign", args, &reply))

				testutil.RunStep(t, "verify that the cert is signed by the new CA", func(t *testing.T) {
					roots := x509.NewCertPool()
					require.True(t, roots.AppendCertsFromPEM([]byte(newRootPEM)))
					leaf, err := connect.ParseCert(reply.CertPEM)
					require.NoError(t, err)
					_, err = leaf.Verify(x509.VerifyOptions{
						Roots: roots,
					})
					require.NoError(t, err)
				})

				testutil.RunStep(t, "and that it validates via the intermediate", func(t *testing.T) {
					roots := x509.NewCertPool()
					assert.True(t, roots.AppendCertsFromPEM([]byte(oldRoot.RootCert)))
					leaf, err := connect.ParseCert(reply.CertPEM)
					require.NoError(t, err)

					// Make sure the intermediate was returned as well as leaf
					_, rest := pem.Decode([]byte(reply.CertPEM))
					require.NotEmpty(t, rest)

					intermediates := x509.NewCertPool()
					require.True(t, intermediates.AppendCertsFromPEM(rest))

					_, err = leaf.Verify(x509.VerifyOptions{
						Roots:         roots,
						Intermediates: intermediates,
					})
					require.NoError(t, err)
				})

				testutil.RunStep(t, "verify other fields", func(t *testing.T) {
					assert.Equal(t, "web", reply.Service)
					assert.Equal(t, spiffeId.URI().String(), reply.ServiceURI)
				})
			})
		})
	}
}

func TestConnectCAConfig_Vault_TriggerRotation_Fails(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	ca.SkipIfVaultNotPresent(t)

	t.Parallel()

	testVault := ca.NewTestVaultServer(t)

	token1 := ca.CreateVaultTokenWithAttrs(t, testVault.Client(), &ca.VaultTokenAttributes{
		RootPath:         "pki-root",
		IntermediatePath: "pki-primary",
		ConsulManaged:    true,
		WithSudo:         true,
	})

	token2 := ca.CreateVaultTokenWithAttrs(t, testVault.Client(), &ca.VaultTokenAttributes{
		RootPath:         "pki-root",
		IntermediatePath: "pki-intermediate",
		ConsulManaged:    true,
	})

	newConfig := func(token string, keyType string, keyBits int) map[string]any {
		return map[string]any{
			"Address":             testVault.Addr,
			"Token":               token,
			"RootPKIPath":         "pki-root/",
			"IntermediatePKIPath": "pki-intermediate/",
			"PrivateKeyType":      keyType,
			"PrivateKeyBits":      keyBits,
		}
	}

	_, s1 := testServerWithConfig(t, func(c *Config) {
		c.CAConfig = &structs.CAConfiguration{
			Provider: "vault",
			Config:   newConfig(token1, connect.DefaultPrivateKeyType, connect.DefaultPrivateKeyBits),
		}
	})
	testrpc.WaitForTestAgent(t, s1.RPC, "dc1")

	// note: unlike many table tests, the ordering of these cases does matter
	// because any non-errored case will modify the CA config, and any subsequent
	// tests will use the same agent with that new CA config.
	testSteps := []struct {
		name      string
		configFn  func() *structs.CAConfiguration
		expectErr string
	}{
		{
			name: "allow modifying key type and bits from default",
			configFn: func() *structs.CAConfiguration {
				return &structs.CAConfiguration{
					Provider:                 "vault",
					Config:                   newConfig(token2, "rsa", 4096),
					ForceWithoutCrossSigning: true,
				}
			},
		},
		{
			name: "error when trying to modify key bits",
			configFn: func() *structs.CAConfiguration {
				return &structs.CAConfiguration{
					Provider:                 "vault",
					Config:                   newConfig(token2, "rsa", 2048),
					ForceWithoutCrossSigning: true,
				}
			},
			expectErr: `cannot update the PrivateKeyBits field without changing RootPKIPath`,
		},
		{
			name: "error when trying to modify key type",
			configFn: func() *structs.CAConfiguration {
				return &structs.CAConfiguration{
					Provider:                 "vault",
					Config:                   newConfig(token2, "ec", 256),
					ForceWithoutCrossSigning: true,
				}
			},
			expectErr: `cannot update the PrivateKeyType field without changing RootPKIPath`,
		},
		{
			name: "allow update that does not change key type or bits",
			configFn: func() *structs.CAConfiguration {
				return &structs.CAConfiguration{
					Provider:                 "vault",
					Config:                   newConfig(token2, "rsa", 4096),
					ForceWithoutCrossSigning: true,
				}
			},
		},
	}

	for _, tc := range testSteps {
		testutil.RunStep(t, tc.name, func(t *testing.T) {
			args := &structs.CARequest{
				Datacenter: "dc1",
				Config:     tc.configFn(),
			}
			var reply interface{}

			codec := rpcClient(t, s1)
			err := msgpackrpc.CallWithCodec(codec, "ConnectCA.ConfigurationSet", args, &reply)
			if tc.expectErr == "" {
				require.NoError(t, err)
			} else {
				testutil.RequireErrorContains(t, err, tc.expectErr)
			}
		})
	}
}

func TestConnectCAConfig_UpdateSecondary(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	// Initialize primary as the primary DC
	dir1, s1 := testServerWithConfig(t, func(c *Config) {
		c.Datacenter = "primary"
		c.PrimaryDatacenter = "primary"
	})
	defer os.RemoveAll(dir1)
	defer s1.Shutdown()

	testrpc.WaitForLeader(t, s1.RPC, "primary")

	// secondary as a secondary DC
	dir2, s2 := testServerWithConfig(t, func(c *Config) {
		c.Datacenter = "secondary"
		c.PrimaryDatacenter = "primary"
	})
	defer os.RemoveAll(dir2)
	defer s2.Shutdown()
	codec := rpcClient(t, s2)
	defer codec.Close()

	// Create the WAN link
	joinWAN(t, s2, s1)
	testrpc.WaitForLeader(t, s2.RPC, "secondary")

	// Capture the current root
	rootList, activeRoot, err := getTestRoots(s1, "primary")
	require.NoError(t, err)
	require.Len(t, rootList.Roots, 1)
	rootCert := activeRoot

	testrpc.WaitForActiveCARoot(t, s1.RPC, "primary", rootCert)
	testrpc.WaitForActiveCARoot(t, s2.RPC, "secondary", rootCert)

	// Capture the current intermediate
	rootList, activeRoot, err = getTestRoots(s2, "secondary")
	require.NoError(t, err)
	require.Len(t, rootList.Roots, 1)
	require.Len(t, activeRoot.IntermediateCerts, 1)
	oldIntermediatePEM := activeRoot.IntermediateCerts[0]

	// Update the secondary CA config to use a new private key, which should
	// cause a re-signing with a new intermediate.
	_, newKey, err := connect.GeneratePrivateKey()
	assert.NoError(t, err)
	newConfig := &structs.CAConfiguration{
		Provider: "consul",
		Config: map[string]interface{}{
			"PrivateKey": newKey,
			"RootCert":   "",
		},
	}
	{
		args := &structs.CARequest{
			Datacenter: "secondary",
			Config:     newConfig,
		}
		var reply interface{}

		require.NoError(t, msgpackrpc.CallWithCodec(codec, "ConnectCA.ConfigurationSet", args, &reply))
	}

	// Make sure the new intermediate has replaced the old one in the active root,
	// and that the root itself hasn't changed.
	var newIntermediatePEM string
	{
		args := &structs.DCSpecificRequest{
			Datacenter: "secondary",
		}
		var reply structs.IndexedCARoots
		require.Nil(t, msgpackrpc.CallWithCodec(codec, "ConnectCA.Roots", args, &reply))
		require.Len(t, reply.Roots, 1)
		require.Len(t, reply.Roots[0].IntermediateCerts, 1)
		newIntermediatePEM = reply.Roots[0].IntermediateCerts[0]
		require.NotEqual(t, oldIntermediatePEM, newIntermediatePEM)
		require.Equal(t, reply.Roots[0].RootCert, rootCert.RootCert)
	}

	// Verify the new config was set.
	{
		args := &structs.DCSpecificRequest{
			Datacenter: "secondary",
		}
		var reply structs.CAConfiguration
		require.NoError(t, msgpackrpc.CallWithCodec(codec, "ConnectCA.ConfigurationGet", args, &reply))

		actual, err := ca.ParseConsulCAConfig(reply.Config)
		require.NoError(t, err)
		expected, err := ca.ParseConsulCAConfig(newConfig.Config)
		require.NoError(t, err)
		assert.Equal(t, reply.Provider, newConfig.Provider)
		assert.Equal(t, actual, expected)
	}

	// Verify that new leaf certs get the new intermediate bundled
	{
		// Generate a CSR and request signing
		spiffeId := connect.TestSpiffeIDServiceWithHostDC(t, "web", connect.TestClusterID+".consul", "secondary")
		csr, _ := connect.TestCSR(t, spiffeId)
		args := &structs.CASignRequest{
			Datacenter: "secondary",
			CSR:        csr,
		}
		var reply structs.IssuedCert
		require.NoError(t, msgpackrpc.CallWithCodec(codec, "ConnectCA.Sign", args, &reply))

		// Verify the leaf cert has the new intermediate.
		{
			roots := x509.NewCertPool()
			assert.True(t, roots.AppendCertsFromPEM([]byte(rootCert.RootCert)))
			leaf, err := connect.ParseCert(reply.CertPEM)
			require.NoError(t, err)

			intermediates := x509.NewCertPool()
			require.True(t, intermediates.AppendCertsFromPEM([]byte(newIntermediatePEM)))

			_, err = leaf.Verify(x509.VerifyOptions{
				Roots:         roots,
				Intermediates: intermediates,
			})
			require.NoError(t, err)
		}

		// Verify other fields
		assert.Equal(t, "web", reply.Service)
		assert.Equal(t, spiffeId.URI().String(), reply.ServiceURI)
	}

	// Update a minor field in the config that doesn't trigger an intermediate refresh.
	{
		newConfig := &structs.CAConfiguration{
			Provider: "consul",
			Config: map[string]interface{}{
				"PrivateKey": newKey,
				"RootCert":   "",
			},
		}
		{
			args := &structs.CARequest{
				Datacenter: "secondary",
				Config:     newConfig,
			}
			var reply interface{}

			require.NoError(t, msgpackrpc.CallWithCodec(codec, "ConnectCA.ConfigurationSet", args, &reply))
		}
	}
}

// Test CA signing
func TestConnectCASign(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	tests := []struct {
		caKeyType string
		caKeyBits int
	}{
		{
			caKeyType: connect.DefaultPrivateKeyType,
			caKeyBits: connect.DefaultPrivateKeyBits,
		},
		{
			// Ensure that an RSA Keyed CA can sign EC leaves and they validate.
			caKeyType: "rsa",
			caKeyBits: 2048,
		},
	}

	for _, tt := range tests {
		t.Run(fmt.Sprintf("%s-%d", tt.caKeyType, tt.caKeyBits), func(t *testing.T) {
			dir1, s1 := testServerWithConfig(t, func(cfg *Config) {
				cfg.PrimaryDatacenter = "dc1"
				cfg.CAConfig.Config["PrivateKeyType"] = tt.caKeyType
				cfg.CAConfig.Config["PrivateKeyBits"] = tt.caKeyBits
			})
			defer os.RemoveAll(dir1)
			defer s1.Shutdown()
			codec := rpcClient(t, s1)
			defer codec.Close()

			testrpc.WaitForLeader(t, s1.RPC, "dc1")

			// Generate a CSR and request signing
			spiffeId := connect.TestSpiffeIDService(t, "web")

			// TestCSR will always generate a CSR with an EC key currently.
			csr, _ := connect.TestCSR(t, spiffeId)
			args := &structs.CASignRequest{
				Datacenter: "dc1",
				CSR:        csr,
			}
			var reply structs.IssuedCert
			require.NoError(t, msgpackrpc.CallWithCodec(codec, "ConnectCA.Sign", args, &reply))

			// Generate a second CSR and request signing
			spiffeId2 := connect.TestSpiffeIDService(t, "web2")
			csr, _ = connect.TestCSR(t, spiffeId2)
			args = &structs.CASignRequest{
				Datacenter: "dc1",
				CSR:        csr,
			}

			var reply2 structs.IssuedCert
			require.NoError(t, msgpackrpc.CallWithCodec(codec, "ConnectCA.Sign", args, &reply2))
			require.True(t, reply2.ModifyIndex > reply.ModifyIndex)

			// Get the current CA
			state := s1.fsm.State()
			_, ca, err := state.CARootActive(nil)
			require.NoError(t, err)

			// Verify that the cert is signed by the CA
			require.NoError(t, connect.ValidateLeaf(ca.RootCert, reply.CertPEM, nil))

			// Verify other fields
			assert.Equal(t, "web", reply.Service)
			assert.Equal(t, spiffeId.URI().String(), reply.ServiceURI)
		})
	}
}

// Bench how long Signing RPC takes. This was used to ballpark reasonable
// default rate limit to protect servers from thundering herds of signing
// requests on root rotation.
func BenchmarkConnectCASign(b *testing.B) {
	t := &testing.T{}

	dir1, s1 := testServer(t)
	defer os.RemoveAll(dir1)
	defer s1.Shutdown()
	codec := rpcClient(t, s1)
	defer codec.Close()

	testrpc.WaitForLeader(t, s1.RPC, "dc1")

	// Generate a CSR and request signing
	spiffeID := connect.TestSpiffeIDService(b, "web")
	csr, _ := connect.TestCSR(b, spiffeID)
	args := &structs.CASignRequest{
		Datacenter: "dc1",
		CSR:        csr,
	}
	var reply structs.IssuedCert

	b.ResetTimer()
	for n := 0; n < b.N; n++ {
		if err := msgpackrpc.CallWithCodec(codec, "ConnectCA.Sign", args, &reply); err != nil {
			b.Fatalf("err: %v", err)
		}
	}
}

func TestConnectCASign_rateLimit(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	dir1, s1 := testServerWithConfig(t, func(c *Config) {
		c.Datacenter = "dc1"
		c.PrimaryDatacenter = "dc1"
		c.Bootstrap = true
		c.CAConfig.Config = map[string]interface{}{
			// It actually doesn't work as expected with some higher values because
			// the token bucket is initialized with max(10%, 1) burst which for small
			// values is 1 and then the test completes so fast it doesn't actually
			// replenish any tokens so you only get the burst allowed through. This is
			// OK, running the test slower is likely to be more brittle anyway since
			// it will become more timing dependent whether the actual rate the
			// requests are made matches the expectation from the sleeps etc.
			"CSRMaxPerSecond": 1,
		}
	})
	defer os.RemoveAll(dir1)
	defer s1.Shutdown()
	codec := rpcClient(t, s1)
	defer codec.Close()

	testrpc.WaitForLeader(t, s1.RPC, "dc1")

	// Generate a CSR and request signing a few times in a loop.
	spiffeID := connect.TestSpiffeIDService(t, "web")
	csr, _ := connect.TestCSR(t, spiffeID)
	args := &structs.CASignRequest{
		Datacenter: "dc1",
		CSR:        csr,
	}
	var reply structs.IssuedCert

	errs := make([]error, 10)
	for i := 0; i < len(errs); i++ {
		errs[i] = msgpackrpc.CallWithCodec(codec, "ConnectCA.Sign", args, &reply)
	}

	limitedCount := 0
	successCount := 0
	for _, err := range errs {
		if err == nil {
			successCount++
		} else if err.Error() == ErrRateLimited.Error() {
			limitedCount++
		} else {
			require.NoError(t, err)
		}
	}
	// I've only ever seen this as 1/9 however if the test runs slowly on an
	// over-subscribed CPU (e.g. in CI) it's possible that later requests could
	// have had their token replenished and succeed so we allow a little slack -
	// the test here isn't really the exact token bucket response more a sanity
	// check that some limiting is being applied. Note that we can't just measure
	// the time it took to send them all and infer how many should have succeeded
	// without some complex modeling of the token bucket algorithm.
	require.Truef(t, successCount >= 1, "at least 1 CSRs should have succeeded, got %d", successCount)
	require.Truef(t, limitedCount >= 7, "at least 7 CSRs should have been rate limited, got %d", limitedCount)
}

func TestConnectCASign_concurrencyLimit(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	dir1, s1 := testServerWithConfig(t, func(c *Config) {
		c.Datacenter = "dc1"
		c.PrimaryDatacenter = "dc1"
		c.Bootstrap = true
		c.CAConfig.Config = map[string]interface{}{
			// Must disable the rate limit since it takes precedence
			"CSRMaxPerSecond":  0,
			"CSRMaxConcurrent": 1,
		}
	})
	defer os.RemoveAll(dir1)
	defer s1.Shutdown()

	testrpc.WaitForLeader(t, s1.RPC, "dc1")

	// Generate a CSR and request signing a few times in a loop.
	spiffeID := connect.TestSpiffeIDService(t, "web")
	csr, _ := connect.TestCSR(t, spiffeID)
	args := &structs.CASignRequest{
		Datacenter: "dc1",
		CSR:        csr,
	}

	var wg sync.WaitGroup

	errs := make(chan error, 10)
	times := make(chan time.Duration, cap(errs))
	start := time.Now()
	for i := 0; i < cap(errs); i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			codec := rpcClient(t, s1)
			defer codec.Close()
			var reply structs.IssuedCert
			errs <- msgpackrpc.CallWithCodec(codec, "ConnectCA.Sign", args, &reply)
			times <- time.Since(start)
		}()
	}

	wg.Wait()
	close(errs)

	limitedCount := 0
	successCount := 0
	var minTime, maxTime time.Duration
	for err := range errs {
		elapsed := <-times
		if elapsed < minTime || minTime == 0 {
			minTime = elapsed
		}
		if elapsed > maxTime {
			maxTime = elapsed
		}
		if err == nil {
			successCount++
		} else if err.Error() == ErrRateLimited.Error() {
			limitedCount++
		} else {
			require.NoError(t, err)
		}
	}

	// These are very hand wavy - on my mac times look like this:
	//     2.776009ms
	//     3.705813ms
	//     4.527212ms
	//     5.267755ms
	//     6.119809ms
	//     6.958083ms
	//     7.869179ms
	//     8.675058ms
	//     9.512281ms
	//     10.238183ms
	//
	// But it's indistinguishable from noise - even if you disable the concurrency
	// limiter you get pretty much the same pattern/spread.
	//
	// On the other hand it's only timing that stops us from not hitting the 500ms
	// timeout. On highly CPU constrained CI box this could be brittle if we
	// assert that we never get rate limited.
	//
	// So this test is not super strong - but it's a sanity check at least that
	// things don't break when configured this way, and through manual
	// inspection/debug logging etc. we can verify it's actually doing the
	// concurrency limit thing. If you add a 100ms sleep into the sign endpoint
	// after the rate limit code for example it makes it much more obvious:
	//
	//   With 100ms sleep an no concurrency limit:
	//     min=109ms, max=118ms
	//   With concurrency limit of 1:
	//     min=106ms, max=538ms (with ~half hitting the 500ms timeout)
	//
	// Without instrumenting the endpoint to make the RPC take an artificially
	// long time it's hard to know what else we can do to actively detect that the
	// requests were serialized.
	t.Logf("min=%s, max=%s", minTime, maxTime)
	//t.Fail() // Uncomment to see the time spread logged
	require.Truef(t, successCount >= 1, "at least 1 CSRs should have succeeded, got %d", successCount)
}

func TestConnectCASignValidation(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	dir1, s1 := testServerWithConfig(t, func(c *Config) {
		c.PrimaryDatacenter = "dc1"
		c.ACLsEnabled = true
		c.ACLInitialManagementToken = "root"
		c.ACLResolverSettings.ACLDefaultPolicy = "deny"
	})
	defer os.RemoveAll(dir1)
	defer s1.Shutdown()
	codec := rpcClient(t, s1)
	defer codec.Close()

	testrpc.WaitForLeader(t, s1.RPC, "dc1")

	webToken := createToken(t, codec, `service "web" { policy = "write" }`)
	testWebID := connect.TestSpiffeIDService(t, "web")

	tests := []struct {
		name    string
		id      connect.CertURI
		wantErr string
	}{
		{
			name: "different cluster",
			id: &connect.SpiffeIDService{
				Host:       "55555555-4444-3333-2222-111111111111.consul",
				Namespace:  testWebID.Namespace,
				Datacenter: testWebID.Datacenter,
				Service:    testWebID.Service,
			},
			wantErr: "different trust domain",
		},
		{
			name:    "same cluster should validate",
			id:      testWebID,
			wantErr: "",
		},
		{
			name: "same cluster, CSR for a different DC should NOT validate",
			id: &connect.SpiffeIDService{
				Host:       testWebID.Host,
				Namespace:  testWebID.Namespace,
				Datacenter: "dc2",
				Service:    testWebID.Service,
			},
			wantErr: "different datacenter",
		},
		{
			name: "same cluster and DC, different service should not have perms",
			id: &connect.SpiffeIDService{
				Host:       testWebID.Host,
				Namespace:  testWebID.Namespace,
				Datacenter: testWebID.Datacenter,
				Service:    "db",
			},
			wantErr: "Permission denied",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			csr, _ := connect.TestCSR(t, tt.id)
			args := &structs.CASignRequest{
				Datacenter:   "dc1",
				CSR:          csr,
				WriteRequest: structs.WriteRequest{Token: webToken},
			}
			var reply structs.IssuedCert
			err := msgpackrpc.CallWithCodec(codec, "ConnectCA.Sign", args, &reply)
			if tt.wantErr == "" {
				require.NoError(t, err)
				// No other validation that is handled in different tests
			} else {
				require.Error(t, err)
				require.Contains(t, err.Error(), tt.wantErr)
			}
		})
	}
}
