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.
consul/agent/connect/ca/testing.go

391 lines
9.5 KiB

// Copyright (c) HashiCorp, Inc.
[COMPLIANCE] License changes (#18443) * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Updating the license from MPL to Business Source License Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at <Blog URL>, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl. * add missing license headers * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 --------- Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
1 year ago
// SPDX-License-Identifier: BUSL-1.1
package ca
import (
"errors"
"fmt"
"io"
"os"
"os/exec"
"strings"
"sync"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-uuid"
vaultapi "github.com/hashicorp/vault/api"
"github.com/mitchellh/go-testing-interface"
"github.com/stretchr/testify/require"
"github.com/hashicorp/consul/agent/connect"
"github.com/hashicorp/consul/sdk/freeport"
"github.com/hashicorp/consul/sdk/testutil/retry"
)
// KeyTestCases is a list of the important CA key types that we should test
// against when signing. For now leaf keys are always EC P256 but CA can be EC
// (any NIST curve) or RSA (2048, 4096). Providers must be able to complete all
// signing operations with both types that includes:
// - Sign must be able to sign EC P256 leaf with all these types of CA key
// - CrossSignCA must be able to sign all these types of new CA key with all
// these types of old CA key.
// - SignIntermediate muse bt able to sign all the types of secondary
// intermediate CA key with all these types of primary CA key
var KeyTestCases = []KeyTestCase{
{
Desc: "Default Key Type (EC 256)",
KeyType: connect.DefaultPrivateKeyType,
KeyBits: connect.DefaultPrivateKeyBits,
},
{
Desc: "RSA 2048",
KeyType: "rsa",
KeyBits: 2048,
},
}
type KeyTestCase struct {
Desc string
KeyType string
KeyBits int
}
// CASigningKeyTypes is a struct with params for tests that sign one CA CSR with
// another CA key.
type CASigningKeyTypes struct {
Desc string
SigningKeyType string
SigningKeyBits int
CSRKeyType string
CSRKeyBits int
}
type vaultRequirements struct {
Enterprise bool
}
// CASigningKeyTypeCases returns the cross-product of the important supported CA
// key types for generating table tests for CA signing tests (CrossSignCA and
// SignIntermediate).
func CASigningKeyTypeCases() []CASigningKeyTypes {
cases := make([]CASigningKeyTypes, 0, len(KeyTestCases)*len(KeyTestCases))
for _, outer := range KeyTestCases {
for _, inner := range KeyTestCases {
cases = append(cases, CASigningKeyTypes{
Desc: fmt.Sprintf("%s-%d signing %s-%d", outer.KeyType, outer.KeyBits,
inner.KeyType, inner.KeyBits),
SigningKeyType: outer.KeyType,
SigningKeyBits: outer.KeyBits,
CSRKeyType: inner.KeyType,
CSRKeyBits: inner.KeyBits,
})
}
}
return cases
}
// TestConsulProvider creates a new ConsulProvider, taking care to stub out it's
// Logger so that logging calls don't panic. If logging output is important
func TestConsulProvider(t testing.T, d ConsulProviderStateDelegate) *ConsulProvider {
logger := hclog.New(&hclog.LoggerOptions{Output: io.Discard})
provider := &ConsulProvider{Delegate: d, logger: logger}
return provider
}
// SkipIfVaultNotPresent skips the test if the vault binary is not in PATH.
//
// These tests may be skipped in CI. They are run as part of a separate
// integration test suite.
func SkipIfVaultNotPresent(t testing.T, reqs ...vaultRequirements) {
// Try to safeguard against tests that will never run in CI.
// This substring should match the pattern used by the
// test-connect-ca-providers CI job.
if !strings.Contains(t.Name(), "Vault") {
t.Fatalf("test name must contain Vault, otherwise CI will never run it")
}
vaultBinaryName := os.Getenv("VAULT_BINARY_NAME")
if vaultBinaryName == "" {
vaultBinaryName = "vault"
}
path, err := exec.LookPath(vaultBinaryName)
if err != nil || path == "" {
t.Skipf("%q not found on $PATH - download and install to run this test", vaultBinaryName)
}
// Check for any additional Vault requirements.
for _, r := range reqs {
if r.Enterprise {
ver := vaultVersion(t, vaultBinaryName)
if !strings.Contains(ver, "+ent") {
t.Skipf("%q is not a Vault Enterprise version", ver)
}
}
}
}
func NewTestVaultServer(t retry.TestingTB) *TestVaultServer {
vaultBinaryName := os.Getenv("VAULT_BINARY_NAME")
if vaultBinaryName == "" {
vaultBinaryName = "vault"
}
path, err := exec.LookPath(vaultBinaryName)
if err != nil || path == "" {
t.Fatalf("%q not found on $PATH", vaultBinaryName)
}
ports := freeport.GetN(t, 2)
var (
clientAddr = fmt.Sprintf("127.0.0.1:%d", ports[0])
clusterAddr = fmt.Sprintf("127.0.0.1:%d", ports[1])
)
const token = "root"
client, err := vaultapi.NewClient(&vaultapi.Config{
Address: "http://" + clientAddr,
})
require.NoError(t, err)
client.SetToken(token)
args := []string{
"server",
"-dev",
"-dev-root-token-id",
token,
"-dev-listen-address",
clientAddr,
"-address",
clusterAddr,
// We pass '-dev-no-store-token' to avoid having multiple vaults oddly
// interact and fail like this:
//
// Error initializing Dev mode: rename /.vault-token.tmp /.vault-token: no such file or directory
//
"-dev-no-store-token",
}
cmd := exec.Command(vaultBinaryName, args...)
cmd.Stdout = io.Discard
cmd.Stderr = io.Discard
require.NoError(t, cmd.Start())
testVault := &TestVaultServer{
RootToken: token,
Addr: "http://" + clientAddr,
cmd: cmd,
client: client,
}
t.Cleanup(func() {
if err := testVault.Stop(); err != nil {
t.Logf("failed to stop vault server: %v", err)
}
})
testVault.WaitUntilReady(t)
return testVault
}
type TestVaultServer struct {
RootToken string
Addr string
cmd *exec.Cmd
client *vaultapi.Client
}
var printedVaultVersion sync.Once
var vaultTestVersion string
func (v *TestVaultServer) Client() *vaultapi.Client {
return v.client
}
func (v *TestVaultServer) WaitUntilReady(t retry.TestingTB) {
var version string
retry.Run(t, func(r *retry.R) {
resp, err := v.client.Sys().Health()
if err != nil {
r.Fatalf("err: %v", err)
}
if !resp.Initialized {
r.Fatalf("vault server is not initialized")
}
if resp.Sealed {
r.Fatalf("vault server is sealed")
}
version = resp.Version
})
printedVaultVersion.Do(func() {
vaultTestVersion = version
fmt.Fprintf(os.Stderr, "[INFO] agent/connect/ca: testing with vault server version: %s\n", version)
})
}
func (v *TestVaultServer) Stop() error {
// There was no process
if v.cmd == nil {
return nil
}
if v.cmd.Process != nil {
if err := v.cmd.Process.Signal(os.Interrupt); err != nil && !errors.Is(err, os.ErrProcessDone) {
return fmt.Errorf("failed to kill vault server: %v", err)
}
}
// wait for the process to exit to be sure that the data dir can be
// deleted on all platforms.
if err := v.cmd.Wait(); err != nil {
if strings.Contains(err.Error(), "exec: Wait was already called") {
return nil
}
return err
}
return nil
}
Format certificates properly (rfc7468) with a trailing new line (#10411) * trim carriage return from certificates when inserting rootCA in the inMemDB * format rootCA properly when returning the CA on the connect CA endpoint * Fix linter warnings * Fix providers to trim certs before returning it * trim newlines on write when possible * add changelog * make sure all provider return a trailing newline after the root and intermediate certs * Fix endpoint to return trailing new line * Fix failing test with vault provider * make test more robust * make sure all provider return a trailing newline after the leaf certs * Check for suffix before removing newline and use function * Add comment to consul provider * Update change log Co-authored-by: R.B. Boyer <4903+rboyer@users.noreply.github.com> * fix typo * simplify code callflow Co-authored-by: R.B. Boyer <4903+rboyer@users.noreply.github.com> * extract requireNewLine as shared func * remove dependency to testify in testing file * remove extra newline in vault provider * Add cert newline fix to envoy xds * remove new line from mock provider * Remove adding a new line from provider and fix it when the cert is read * Add a comment to explain the fix * Add missing for leaf certs * fix missing new line * fix missing new line in leaf certs * remove extra new line in test * updage changelog Co-authored-by: Daniel Nephin <dnephin@hashicorp.com> * fix in vault provider and when reading cache (RPC call) * fix AWS provider * fix failing test in the provider * remove comments and empty lines * add check for empty cert in test * fix linter warnings * add new line for leaf and private key * use string concat instead of Sprintf * fix new lines for leaf signing * preallocate slice and remove append * Add new line to `SignIntermediate` and `CrossSignCA` Co-authored-by: R.B. Boyer <4903+rboyer@users.noreply.github.com> Co-authored-by: Daniel Nephin <dnephin@hashicorp.com>
3 years ago
func requireTrailingNewline(t testing.T, leafPEM string) {
t.Helper()
if len(leafPEM) == 0 {
t.Fatalf("cert is empty")
}
if rune(leafPEM[len(leafPEM)-1]) != '\n' {
t.Fatalf("cert does not end with a new line")
Format certificates properly (rfc7468) with a trailing new line (#10411) * trim carriage return from certificates when inserting rootCA in the inMemDB * format rootCA properly when returning the CA on the connect CA endpoint * Fix linter warnings * Fix providers to trim certs before returning it * trim newlines on write when possible * add changelog * make sure all provider return a trailing newline after the root and intermediate certs * Fix endpoint to return trailing new line * Fix failing test with vault provider * make test more robust * make sure all provider return a trailing newline after the leaf certs * Check for suffix before removing newline and use function * Add comment to consul provider * Update change log Co-authored-by: R.B. Boyer <4903+rboyer@users.noreply.github.com> * fix typo * simplify code callflow Co-authored-by: R.B. Boyer <4903+rboyer@users.noreply.github.com> * extract requireNewLine as shared func * remove dependency to testify in testing file * remove extra newline in vault provider * Add cert newline fix to envoy xds * remove new line from mock provider * Remove adding a new line from provider and fix it when the cert is read * Add a comment to explain the fix * Add missing for leaf certs * fix missing new line * fix missing new line in leaf certs * remove extra new line in test * updage changelog Co-authored-by: Daniel Nephin <dnephin@hashicorp.com> * fix in vault provider and when reading cache (RPC call) * fix AWS provider * fix failing test in the provider * remove comments and empty lines * add check for empty cert in test * fix linter warnings * add new line for leaf and private key * use string concat instead of Sprintf * fix new lines for leaf signing * preallocate slice and remove append * Add new line to `SignIntermediate` and `CrossSignCA` Co-authored-by: R.B. Boyer <4903+rboyer@users.noreply.github.com> Co-authored-by: Daniel Nephin <dnephin@hashicorp.com>
3 years ago
}
}
// The zero value implies unprivileged.
type VaultTokenAttributes struct {
RootPath, IntermediatePath string
ConsulManaged bool
VaultManaged bool
WithSudo bool
CustomRules string
}
func (a *VaultTokenAttributes) DisplayName() string {
switch {
case a == nil:
return "unprivileged"
case a.CustomRules != "":
return "custom"
case a.ConsulManaged:
return "consul-managed"
case a.VaultManaged:
return "vault-managed"
default:
return "unprivileged"
}
}
func (a *VaultTokenAttributes) Rules(t testing.T) string {
switch {
case a == nil:
return ""
case a.CustomRules != "":
return a.CustomRules
case a.RootPath == "":
t.Fatal("missing required RootPath")
return "" // dead code
case a.IntermediatePath == "":
t.Fatal("missing required IntermediatePath")
return "" // dead code
case a.ConsulManaged:
// Consul Managed PKI Mounts
rules := fmt.Sprintf(`
path "sys/mounts" {
capabilities = [ "read" ]
}
path "sys/mounts/%[1]s" {
capabilities = [ "create", "read", "update", "delete", "list" ]
}
path "sys/mounts/%[2]s" {
capabilities = [ "create", "read", "update", "delete", "list" ]
}
# Needed for Consul 1.11+
path "sys/mounts/%[2]s/tune" {
capabilities = [ "update" ]
}
# vault token renewal
path "auth/token/renew-self" {
capabilities = [ "update" ]
}
path "auth/token/lookup-self" {
capabilities = [ "read" ]
}
path "%[1]s/*" {
capabilities = [ "create", "read", "update", "delete", "list" ]
}
path "%[2]s/*" {
capabilities = [ "create", "read", "update", "delete", "list" ]
}
`, a.RootPath, a.IntermediatePath)
if a.WithSudo {
rules += fmt.Sprintf(`
path "%[1]s/root/sign-self-issued" {
capabilities = [ "sudo", "update" ]
}
`, a.RootPath)
}
return rules
case a.VaultManaged:
// Vault-managed PKI root.
t.Fatal("TODO: implement this and use it in tests")
return ""
default:
// zero value
return ""
}
}
func CreateVaultTokenWithAttrs(t testing.T, client *vaultapi.Client, attr *VaultTokenAttributes) string {
policyName, err := uuid.GenerateUUID()
require.NoError(t, err)
rules := attr.Rules(t)
token := createVaultTokenAndPolicy(t, client, policyName, rules)
// t.Logf("created vault token with scope %q: %s", attr.DisplayName(), token)
return token
}
func createVaultTokenAndPolicy(t testing.T, client *vaultapi.Client, policyName, policyRules string) string {
require.NoError(t, client.Sys().PutPolicy(policyName, policyRules))
renew := true
tok, err := client.Auth().Token().Create(&vaultapi.TokenCreateRequest{
Policies: []string{policyName},
Renewable: &renew,
})
require.NoError(t, err)
return tok.Auth.ClientToken
}
func vaultVersion(t testing.T, vaultBinaryName string) string {
cmd := exec.Command(vaultBinaryName, []string{"version"}...)
output, err := cmd.Output()
require.NoError(t, err)
return string(output[:len(output)-1])
}