consul/command/tls/cert/create/tls_cert_create_test.go

307 lines
7.1 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package create
import (
"crypto"
"crypto/x509"
"io/fs"
"net"
"os"
"strings"
"testing"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
"github.com/hashicorp/consul/agent/connect"
"github.com/hashicorp/consul/sdk/testutil"
caCreate "github.com/hashicorp/consul/command/tls/ca/create"
)
func TestValidateCommand_noTabs(t *testing.T) {
t.Parallel()
if strings.ContainsRune(New(nil).Help(), '\t') {
t.Fatal("help has tabs")
}
}
func TestTlsCertCreateCommand_InvalidArgs(t *testing.T) {
t.Parallel()
type testcase struct {
args []string
expectErr string
}
cases := map[string]testcase{
"no args (ca/key inferred)": {[]string{},
"Please provide either -server, -client, or -cli"},
"no ca": {[]string{"-ca", "", "-key", ""},
"Please provide the ca"},
"no key": {[]string{"-ca", "foo.pem", "-key", ""},
"Please provide the key"},
"server+client+cli": {[]string{"-server", "-client", "-cli"},
"Please provide either -server, -client, or -cli"},
"server+client": {[]string{"-server", "-client"},
"Please provide either -server, -client, or -cli"},
"server+cli": {[]string{"-server", "-cli"},
"Please provide either -server, -client, or -cli"},
"client+cli": {[]string{"-client", "-cli"},
"Please provide either -server, -client, or -cli"},
"client+node": {[]string{"-client", "-node", "foo"},
"-node requires -server"},
"cli+node": {[]string{"-cli", "-node", "foo"},
"-node requires -server"},
}
for name, tc := range cases {
tc := tc
t.Run(name, func(t *testing.T) {
t.Parallel()
ui := cli.NewMockUi()
cmd := New(ui)
require.NotEqual(t, 0, cmd.Run(tc.args))
got := ui.ErrorWriter.String()
if tc.expectErr == "" {
require.NotEmpty(t, got) // don't care
} else {
require.Contains(t, got, tc.expectErr)
}
})
}
}
func TestTlsCertCreateCommand_fileCreate(t *testing.T) {
testDir := testutil.TempDir(t, "tls")
defer switchToTempDir(t, testDir)()
// Setup CA keys
createCA(t, "consul")
createCA(t, "nomad")
type testcase struct {
name string
typ string
args []string
certPath string
keyPath string
expectCN string
expectDNS []string
expectIP []net.IP
}
// The following subtests must run serially.
cases := []testcase{
{"server0",
"server",
[]string{"-server"},
"dc1-server-consul-0.pem",
"dc1-server-consul-0-key.pem",
"server.dc1.consul",
[]string{
"server.dc1.consul",
"localhost",
},
[]net.IP{{127, 0, 0, 1}},
},
{"server1-with-node",
"server",
[]string{"-server", "-node", "mysrv"},
"dc1-server-consul-1.pem",
"dc1-server-consul-1-key.pem",
"server.dc1.consul",
[]string{
"mysrv.server.dc1.consul",
"server.dc1.consul",
"localhost",
},
[]net.IP{{127, 0, 0, 1}},
},
{"server0-dc2-altdomain",
"server",
[]string{"-server", "-dc", "dc2", "-domain", "nomad"},
"dc2-server-nomad-0.pem",
"dc2-server-nomad-0-key.pem",
"server.dc2.nomad",
[]string{
"server.dc2.nomad",
"localhost",
},
[]net.IP{{127, 0, 0, 1}},
},
{"client0",
"client",
[]string{"-client"},
"dc1-client-consul-0.pem",
"dc1-client-consul-0-key.pem",
"client.dc1.consul",
[]string{
"client.dc1.consul",
"localhost",
},
[]net.IP{{127, 0, 0, 1}},
},
{"client1",
"client",
[]string{"-client"},
"dc1-client-consul-1.pem",
"dc1-client-consul-1-key.pem",
"client.dc1.consul",
[]string{
"client.dc1.consul",
"localhost",
},
[]net.IP{{127, 0, 0, 1}},
},
{"client0-dc2-altdomain",
"client",
[]string{"-client", "-dc", "dc2", "-domain", "nomad"},
"dc2-client-nomad-0.pem",
"dc2-client-nomad-0-key.pem",
"client.dc2.nomad",
[]string{
"client.dc2.nomad",
"localhost",
},
[]net.IP{{127, 0, 0, 1}},
},
{"cli0",
"cli",
[]string{"-cli"},
"dc1-cli-consul-0.pem",
"dc1-cli-consul-0-key.pem",
"cli.dc1.consul",
[]string{
"cli.dc1.consul",
"localhost",
},
nil,
},
{"cli1",
"cli",
[]string{"-cli"},
"dc1-cli-consul-1.pem",
"dc1-cli-consul-1-key.pem",
"cli.dc1.consul",
[]string{
"cli.dc1.consul",
"localhost",
},
nil,
},
{"cli0-dc2-altdomain",
"cli",
[]string{"-cli", "-dc", "dc2", "-domain", "nomad"},
"dc2-cli-nomad-0.pem",
"dc2-cli-nomad-0-key.pem",
"cli.dc2.nomad",
[]string{
"cli.dc2.nomad",
"localhost",
},
nil,
},
}
for _, tc := range cases {
tc := tc
require.True(t, t.Run(tc.name, func(t *testing.T) {
ui := cli.NewMockUi()
cmd := New(ui)
require.Equal(t, 0, cmd.Run(tc.args))
require.Equal(t, "", ui.ErrorWriter.String())
cert, _ := expectFiles(t, tc.certPath, tc.keyPath)
require.Equal(t, tc.expectCN, cert.Subject.CommonName)
require.True(t, cert.BasicConstraintsValid)
require.Equal(t, x509.KeyUsageDigitalSignature|x509.KeyUsageKeyEncipherment, cert.KeyUsage)
switch tc.typ {
case "server":
require.Equal(t,
[]x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
cert.ExtKeyUsage)
case "client":
require.Equal(t,
[]x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
cert.ExtKeyUsage)
case "cli":
require.Len(t, cert.ExtKeyUsage, 0)
}
require.False(t, cert.IsCA)
require.Equal(t, tc.expectDNS, cert.DNSNames)
require.Equal(t, tc.expectIP, cert.IPAddresses)
}))
}
}
func expectFiles(t *testing.T, certPath, keyPath string) (*x509.Certificate, crypto.Signer) {
t.Helper()
require.FileExists(t, certPath)
require.FileExists(t, keyPath)
fi, err := os.Stat(keyPath)
if err != nil {
t.Fatal("should not happen", err)
}
if want, have := fs.FileMode(0600), fi.Mode().Perm(); want != have {
t.Fatalf("private key file %s: permissions: want: %o; have: %o", keyPath, want, have)
}
certData, err := os.ReadFile(certPath)
require.NoError(t, err)
keyData, err := os.ReadFile(keyPath)
require.NoError(t, err)
cert, err := connect.ParseCert(string(certData))
require.NoError(t, err)
require.NotNil(t, cert)
signer, err := connect.ParseSigner(string(keyData))
require.NoError(t, err)
require.NotNil(t, signer)
return cert, signer
}
func createCA(t *testing.T, domain string) {
t.Helper()
ui := cli.NewMockUi()
caCmd := caCreate.New(ui)
args := []string{
"-domain=" + domain,
}
require.Equal(t, 0, caCmd.Run(args))
require.Equal(t, "", ui.ErrorWriter.String())
require.FileExists(t, "consul-agent-ca.pem")
}
// switchToTempDir is meant to be used in a defer statement like:
//
// defer switchToTempDir(t, testDir)()
//
// This exploits the fact that the body of a defer is evaluated
// EXCEPT for the final function call invocation inline with the code
// where it is found. Only the final evaluation happens in the defer
// at a later time. In this case it means we switch to the temp
// directory immediately and defer switching back in one line of test
// code.
func switchToTempDir(t *testing.T, testDir string) func() {
previousDirectory, err := os.Getwd()
require.NoError(t, err)
require.NoError(t, os.Chdir(testDir))
return func() {
os.Chdir(previousDirectory)
}
}