open-vault/command/operator_init_test.go
ncabatoff 1c98152fa0
Shamir seals now come in two varieties: legacy and new-style. (#7694)
Shamir seals now come in two varieties: legacy and new-style. Legacy
Shamir is automatically converted to new-style when a rekey operation
is performed. All new Vault initializations using Shamir are new-style.

New-style Shamir writes an encrypted master key to storage, just like
AutoUnseal. The stored master key is encrypted using the shared key that
is split via Shamir's algorithm. Thus when unsealing, we take the key
fragments given, combine them into a Key-Encryption-Key, and use that
to decrypt the master key on disk. Then the master key is used to read
the keyring that decrypts the barrier.
2019-10-18 14:46:00 -04:00

372 lines
7.9 KiB
Go

// +build !race
package command
import (
"fmt"
"os"
"regexp"
"strconv"
"strings"
"testing"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/helper/pgpkeys"
"github.com/hashicorp/vault/vault"
"github.com/mitchellh/cli"
)
func testOperatorInitCommand(tb testing.TB) (*cli.MockUi, *OperatorInitCommand) {
tb.Helper()
ui := cli.NewMockUi()
return ui, &OperatorInitCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}
}
func TestOperatorInitCommand_Run(t *testing.T) {
t.Parallel()
cases := []struct {
name string
args []string
out string
code int
}{
{
"too_many_args",
[]string{"foo"},
"Too many arguments",
1,
},
{
"pgp_keys_multi",
[]string{
"-pgp-keys", "keybase:hashicorp",
"-pgp-keys", "keybase:jefferai",
},
"can only be specified once",
1,
},
{
"root_token_pgp_key_multi",
[]string{
"-root-token-pgp-key", "keybase:hashicorp",
"-root-token-pgp-key", "keybase:jefferai",
},
"can only be specified once",
1,
},
{
"root_token_pgp_key_multi_inline",
[]string{
"-root-token-pgp-key", "keybase:hashicorp,keybase:jefferai",
},
"can only specify one pgp key",
1,
},
{
"recovery_pgp_keys_multi",
[]string{
"-recovery-pgp-keys", "keybase:hashicorp",
"-recovery-pgp-keys", "keybase:jefferai",
},
"can only be specified once",
1,
},
{
"key_shares_pgp_less",
[]string{
"-key-shares", "10",
"-pgp-keys", "keybase:jefferai,keybase:sethvargo",
},
"incorrect number",
2,
},
{
"key_shares_pgp_more",
[]string{
"-key-shares", "1",
"-pgp-keys", "keybase:jefferai,keybase:sethvargo",
},
"incorrect number",
2,
},
}
t.Run("validations", func(t *testing.T) {
t.Parallel()
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testOperatorInitCommand(t)
cmd.client = client
code := cmd.Run(tc.args)
if code != tc.code {
t.Errorf("expected %d to be %d", code, tc.code)
}
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, tc.out) {
t.Errorf("expected %q to contain %q", combined, tc.out)
}
})
}
})
t.Run("status", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServerUninit(t)
defer closer()
ui, cmd := testOperatorInitCommand(t)
cmd.client = client
// Verify the non-init response code
code := cmd.Run([]string{
"-status",
})
if exp := 2; code != exp {
t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String())
}
// Now init to verify the init response code
if _, err := client.Sys().Init(&api.InitRequest{
SecretShares: 1,
SecretThreshold: 1,
}); err != nil {
t.Fatal(err)
}
// Verify the init response code
ui, cmd = testOperatorInitCommand(t)
cmd.client = client
code = cmd.Run([]string{
"-status",
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String())
}
})
t.Run("default", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServerUninit(t)
defer closer()
ui, cmd := testOperatorInitCommand(t)
cmd.client = client
code := cmd.Run([]string{})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String())
}
init, err := client.Sys().InitStatus()
if err != nil {
t.Fatal(err)
}
if !init {
t.Error("expected initialized")
}
re := regexp.MustCompile(`Unseal Key \d+: (.+)`)
output := ui.OutputWriter.String()
match := re.FindAllStringSubmatch(output, -1)
if len(match) < 5 || len(match[0]) < 2 {
t.Fatalf("no match: %#v", match)
}
keys := make([]string, len(match))
for i := range match {
keys[i] = match[i][1]
}
// Try unsealing with those keys - only use 3, which is the default
// threshold.
for i, key := range keys[:3] {
resp, err := client.Sys().Unseal(key)
if err != nil {
t.Fatal(err)
}
exp := (i + 1) % 3 // 1, 2, 0
if resp.Progress != exp {
t.Errorf("expected %d to be %d", resp.Progress, exp)
}
}
status, err := client.Sys().SealStatus()
if err != nil {
t.Fatal(err)
}
if status.Sealed {
t.Errorf("expected vault to be unsealed: %#v", status)
}
})
t.Run("custom_shares_threshold", func(t *testing.T) {
t.Parallel()
keyShares, keyThreshold := 20, 15
client, closer := testVaultServerUninit(t)
defer closer()
ui, cmd := testOperatorInitCommand(t)
cmd.client = client
code := cmd.Run([]string{
"-key-shares", strconv.Itoa(keyShares),
"-key-threshold", strconv.Itoa(keyThreshold),
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String())
}
init, err := client.Sys().InitStatus()
if err != nil {
t.Fatal(err)
}
if !init {
t.Error("expected initialized")
}
re := regexp.MustCompile(`Unseal Key \d+: (.+)`)
output := ui.OutputWriter.String()
match := re.FindAllStringSubmatch(output, -1)
if len(match) < keyShares || len(match[0]) < 2 {
t.Fatalf("no match: %#v", match)
}
keys := make([]string, len(match))
for i := range match {
keys[i] = match[i][1]
}
// Try unsealing with those keys - only use 3, which is the default
// threshold.
for i, key := range keys[:keyThreshold] {
resp, err := client.Sys().Unseal(key)
if err != nil {
t.Fatal(err)
}
exp := (i + 1) % keyThreshold
if resp.Progress != exp {
t.Errorf("expected %d to be %d", resp.Progress, exp)
}
}
status, err := client.Sys().SealStatus()
if err != nil {
t.Fatal(err)
}
if status.Sealed {
t.Errorf("expected vault to be unsealed: %#v", status)
}
})
t.Run("pgp", func(t *testing.T) {
t.Parallel()
tempDir, pubFiles, err := getPubKeyFiles(t)
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
client, closer := testVaultServerUninit(t)
defer closer()
ui, cmd := testOperatorInitCommand(t)
cmd.client = client
code := cmd.Run([]string{
"-key-shares", "4",
"-key-threshold", "2",
"-pgp-keys", fmt.Sprintf("%s,@%s, %s, %s ",
pubFiles[0], pubFiles[1], pubFiles[2], pubFiles[3]),
"-root-token-pgp-key", pubFiles[0],
})
if exp := 0; code != exp {
t.Fatalf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String())
}
re := regexp.MustCompile(`Unseal Key \d+: (.+)`)
output := ui.OutputWriter.String()
match := re.FindAllStringSubmatch(output, -1)
if len(match) < 4 || len(match[0]) < 2 {
t.Fatalf("no match: %#v", match)
}
keys := make([]string, len(match))
for i := range match {
keys[i] = match[i][1]
}
// Try unsealing with one key
decryptedKey := testPGPDecrypt(t, pgpkeys.TestPrivKey1, keys[0])
if _, err := client.Sys().Unseal(decryptedKey); err != nil {
t.Fatal(err)
}
// Decrypt the root token
reToken := regexp.MustCompile(`Root Token: (.+)`)
match = reToken.FindAllStringSubmatch(output, -1)
if len(match) < 1 || len(match[0]) < 2 {
t.Fatalf("no match")
}
root := match[0][1]
decryptedRoot := testPGPDecrypt(t, pgpkeys.TestPrivKey1, root)
if l, exp := len(decryptedRoot), vault.TokenLength+2; l != exp {
t.Errorf("expected %d to be %d", l, exp)
}
})
t.Run("communication_failure", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServerBad(t)
defer closer()
ui, cmd := testOperatorInitCommand(t)
cmd.client = client
code := cmd.Run([]string{
"-key-shares=1",
"-key-threshold=1",
})
if exp := 2; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Error initializing: "
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
t.Run("no_tabs", func(t *testing.T) {
t.Parallel()
_, cmd := testOperatorInitCommand(t)
assertNoTabs(t, cmd)
})
}