Merge pull request #907 from hashicorp/rekey-work
Add rekey nonce/backup.
This commit is contained in:
commit
20a6f37b38
|
@ -35,8 +35,11 @@ func (c *Sys) RekeyCancel() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Sys) RekeyUpdate(shard string) (*RekeyUpdateResponse, error) {
|
func (c *Sys) RekeyUpdate(shard, nonce string) (*RekeyUpdateResponse, error) {
|
||||||
body := map[string]interface{}{"key": shard}
|
body := map[string]interface{}{
|
||||||
|
"key": shard,
|
||||||
|
"nonce": nonce,
|
||||||
|
}
|
||||||
|
|
||||||
r := c.c.NewRequest("PUT", "/v1/sys/rekey/update")
|
r := c.c.NewRequest("PUT", "/v1/sys/rekey/update")
|
||||||
if err := r.SetJSONBody(body); err != nil {
|
if err := r.SetJSONBody(body); err != nil {
|
||||||
|
@ -54,21 +57,56 @@ func (c *Sys) RekeyUpdate(shard string) (*RekeyUpdateResponse, error) {
|
||||||
return &result, err
|
return &result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Sys) RekeyRetrieveStored() (*RekeyRetrieveResponse, error) {
|
||||||
|
r := c.c.NewRequest("GET", "/v1/sys/rekey/stored")
|
||||||
|
resp, err := c.c.RawRequest(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var result RekeyRetrieveResponse
|
||||||
|
err = resp.DecodeJSON(&result)
|
||||||
|
return &result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Sys) RekeyDeleteStored() error {
|
||||||
|
r := c.c.NewRequest("DELETE", "/v1/sys/rekey/stored")
|
||||||
|
resp, err := c.c.RawRequest(r)
|
||||||
|
if err == nil {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
type RekeyInitRequest struct {
|
type RekeyInitRequest struct {
|
||||||
SecretShares int `json:"secret_shares"`
|
SecretShares int `json:"secret_shares"`
|
||||||
SecretThreshold int `json:"secret_threshold"`
|
SecretThreshold int `json:"secret_threshold"`
|
||||||
PGPKeys []string `json:"pgp_keys"`
|
PGPKeys []string `json:"pgp_keys"`
|
||||||
|
Backup bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type RekeyStatusResponse struct {
|
type RekeyStatusResponse struct {
|
||||||
Started bool
|
Nonce string
|
||||||
T int
|
Started bool
|
||||||
N int
|
T int
|
||||||
Progress int
|
N int
|
||||||
Required int
|
Progress int
|
||||||
|
Required int
|
||||||
|
PGPFingerprints []string `json:"pgp_fingerprints"`
|
||||||
|
Backup bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type RekeyUpdateResponse struct {
|
type RekeyUpdateResponse struct {
|
||||||
Complete bool
|
Nonce string
|
||||||
Keys []string
|
Complete bool
|
||||||
|
Keys []string
|
||||||
|
PGPFingerprints []string `json:"pgp_fingerprints"`
|
||||||
|
Backup bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type RekeyRetrieveResponse struct {
|
||||||
|
Nonce string
|
||||||
|
Keys map[string]string
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/helper/pgpkeys"
|
||||||
"github.com/hashicorp/vault/http"
|
"github.com/hashicorp/vault/http"
|
||||||
"github.com/hashicorp/vault/vault"
|
"github.com/hashicorp/vault/vault"
|
||||||
"github.com/mitchellh/cli"
|
"github.com/mitchellh/cli"
|
||||||
|
@ -171,9 +172,19 @@ func TestInit_PGP(t *testing.T) {
|
||||||
t.Fatalf("err: %s", err)
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pgpKeys := []string{}
|
||||||
|
for _, pubFile := range pubFiles {
|
||||||
|
pub, err := pgpkeys.ReadPGPFile(pubFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad: %v", err)
|
||||||
|
}
|
||||||
|
pgpKeys = append(pgpKeys, pub)
|
||||||
|
}
|
||||||
|
|
||||||
expected := &vault.SealConfig{
|
expected := &vault.SealConfig{
|
||||||
SecretShares: 3,
|
SecretShares: 3,
|
||||||
SecretThreshold: 2,
|
SecretThreshold: 2,
|
||||||
|
PGPKeys: pgpKeys,
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(expected, sealConf) {
|
if !reflect.DeepEqual(expected, sealConf) {
|
||||||
t.Fatalf("bad:\nexpected: %#v\ngot: %#v", expected, sealConf)
|
t.Fatalf("bad:\nexpected: %#v\ngot: %#v", expected, sealConf)
|
||||||
|
@ -190,5 +201,5 @@ func TestInit_PGP(t *testing.T) {
|
||||||
|
|
||||||
rootToken := matches[0][1]
|
rootToken := matches[0][1]
|
||||||
|
|
||||||
parseDecryptAndTestUnsealKeys(t, ui.OutputWriter.String(), rootToken, core)
|
parseDecryptAndTestUnsealKeys(t, ui.OutputWriter.String(), rootToken, false, nil, core)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -54,7 +55,11 @@ func getPubKeyFiles(t *testing.T) (string, []string, error) {
|
||||||
return tempDir, pubFiles, nil
|
return tempDir, pubFiles, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseDecryptAndTestUnsealKeys(t *testing.T, input, rootToken string, core *vault.Core) {
|
func parseDecryptAndTestUnsealKeys(t *testing.T,
|
||||||
|
input, rootToken string,
|
||||||
|
fingerprints bool,
|
||||||
|
backupKeys map[string]string,
|
||||||
|
core *vault.Core) {
|
||||||
decoder := base64.StdEncoding
|
decoder := base64.StdEncoding
|
||||||
priv1Bytes, err := decoder.DecodeString(privKey1)
|
priv1Bytes, err := decoder.DecodeString(privKey1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -75,7 +80,12 @@ func parseDecryptAndTestUnsealKeys(t *testing.T, input, rootToken string, core *
|
||||||
priv3Bytes,
|
priv3Bytes,
|
||||||
}
|
}
|
||||||
|
|
||||||
re, err := regexp.Compile("\\s*Key\\s+\\d+:\\s+(.*)")
|
var re *regexp.Regexp
|
||||||
|
if fingerprints {
|
||||||
|
re, err = regexp.Compile("\\s*Key\\s+\\d+\\s+fingerprint:\\s+([0-9a-fA-F]+);\\s+value:\\s+(.*)")
|
||||||
|
} else {
|
||||||
|
re, err = regexp.Compile("\\s*Key\\s+\\d+:\\s+(.*)")
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Error compiling regex: %s", err)
|
t.Fatalf("Error compiling regex: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -85,11 +95,30 @@ func parseDecryptAndTestUnsealKeys(t *testing.T, input, rootToken string, core *
|
||||||
}
|
}
|
||||||
|
|
||||||
encodedKeys := []string{}
|
encodedKeys := []string{}
|
||||||
for _, pair := range matches {
|
matchedFingerprints := []string{}
|
||||||
if len(pair) != 2 {
|
for _, tuple := range matches {
|
||||||
t.Fatalf("Key not found: %#v", pair)
|
if fingerprints {
|
||||||
|
if len(tuple) != 3 {
|
||||||
|
t.Fatalf("Key not found: %#v", tuple)
|
||||||
|
}
|
||||||
|
matchedFingerprints = append(matchedFingerprints, tuple[1])
|
||||||
|
encodedKeys = append(encodedKeys, tuple[2])
|
||||||
|
} else {
|
||||||
|
if len(tuple) != 2 {
|
||||||
|
t.Fatalf("Key not found: %#v", tuple)
|
||||||
|
}
|
||||||
|
encodedKeys = append(encodedKeys, tuple[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if backupKeys != nil && len(matchedFingerprints) != 0 {
|
||||||
|
testMap := map[string]string{}
|
||||||
|
for i, v := range matchedFingerprints {
|
||||||
|
testMap[v] = encodedKeys[i]
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(testMap, backupKeys) {
|
||||||
|
t.Fatalf("test map and backup map do not match, test map is\n%#v\nbackup map is\n%#v", testMap, backupKeys)
|
||||||
}
|
}
|
||||||
encodedKeys = append(encodedKeys, pair[1])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
unsealKeys := []string{}
|
unsealKeys := []string{}
|
||||||
|
|
139
command/rekey.go
139
command/rekey.go
|
@ -5,6 +5,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/fatih/structs"
|
||||||
"github.com/hashicorp/vault/api"
|
"github.com/hashicorp/vault/api"
|
||||||
"github.com/hashicorp/vault/helper/password"
|
"github.com/hashicorp/vault/helper/password"
|
||||||
"github.com/hashicorp/vault/helper/pgpkeys"
|
"github.com/hashicorp/vault/helper/pgpkeys"
|
||||||
|
@ -17,24 +18,36 @@ type RekeyCommand struct {
|
||||||
// Key can be used to pre-seed the key. If it is set, it will not
|
// Key can be used to pre-seed the key. If it is set, it will not
|
||||||
// be asked with the `password` helper.
|
// be asked with the `password` helper.
|
||||||
Key string
|
Key string
|
||||||
|
|
||||||
|
// The nonce for the rekey request to send along
|
||||||
|
Nonce string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *RekeyCommand) Run(args []string) int {
|
func (c *RekeyCommand) Run(args []string) int {
|
||||||
var init, cancel, status bool
|
var init, cancel, status, delete, retrieve, backup bool
|
||||||
var shares, threshold int
|
var shares, threshold int
|
||||||
|
var nonce string
|
||||||
var pgpKeys pgpkeys.PubKeyFilesFlag
|
var pgpKeys pgpkeys.PubKeyFilesFlag
|
||||||
flags := c.Meta.FlagSet("rekey", FlagSetDefault)
|
flags := c.Meta.FlagSet("rekey", FlagSetDefault)
|
||||||
flags.BoolVar(&init, "init", false, "")
|
flags.BoolVar(&init, "init", false, "")
|
||||||
flags.BoolVar(&cancel, "cancel", false, "")
|
flags.BoolVar(&cancel, "cancel", false, "")
|
||||||
flags.BoolVar(&status, "status", false, "")
|
flags.BoolVar(&status, "status", false, "")
|
||||||
|
flags.BoolVar(&delete, "delete", false, "")
|
||||||
|
flags.BoolVar(&retrieve, "retrieve", false, "")
|
||||||
|
flags.BoolVar(&backup, "backup", false, "")
|
||||||
flags.IntVar(&shares, "key-shares", 5, "")
|
flags.IntVar(&shares, "key-shares", 5, "")
|
||||||
flags.IntVar(&threshold, "key-threshold", 3, "")
|
flags.IntVar(&threshold, "key-threshold", 3, "")
|
||||||
|
flags.StringVar(&nonce, "nonce", "", "")
|
||||||
flags.Var(&pgpKeys, "pgp-keys", "")
|
flags.Var(&pgpKeys, "pgp-keys", "")
|
||||||
flags.Usage = func() { c.Ui.Error(c.Help()) }
|
flags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||||
if err := flags.Parse(args); err != nil {
|
if err := flags.Parse(args); err != nil {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if nonce != "" {
|
||||||
|
c.Nonce = nonce
|
||||||
|
}
|
||||||
|
|
||||||
client, err := c.Client()
|
client, err := c.Client()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf(
|
c.Ui.Error(fmt.Sprintf(
|
||||||
|
@ -43,12 +56,17 @@ func (c *RekeyCommand) Run(args []string) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we are running doing any restricted variants
|
// Check if we are running doing any restricted variants
|
||||||
if init {
|
switch {
|
||||||
return c.initRekey(client, shares, threshold, pgpKeys)
|
case init:
|
||||||
} else if cancel {
|
return c.initRekey(client, shares, threshold, pgpKeys, backup)
|
||||||
|
case cancel:
|
||||||
return c.cancelRekey(client)
|
return c.cancelRekey(client)
|
||||||
} else if status {
|
case status:
|
||||||
return c.rekeyStatus(client)
|
return c.rekeyStatus(client)
|
||||||
|
case retrieve:
|
||||||
|
return c.rekeyRetrieveStored(client)
|
||||||
|
case delete:
|
||||||
|
return c.rekeyDeleteStored(client)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the rekey is started
|
// Check if the rekey is started
|
||||||
|
@ -69,27 +87,29 @@ func (c *RekeyCommand) Run(args []string) int {
|
||||||
c.Ui.Error(fmt.Sprintf("Error initializing rekey: %s", err))
|
c.Ui.Error(fmt.Sprintf("Error initializing rekey: %s", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
} else {
|
rekeyStatus, err = client.Sys().RekeyStatus()
|
||||||
shares = rekeyStatus.N
|
if err != nil {
|
||||||
threshold = rekeyStatus.T
|
c.Ui.Error(fmt.Sprintf("Error reading rekey status: %s", err))
|
||||||
c.Ui.Output(fmt.Sprintf(
|
return 1
|
||||||
"Rekey already in progress\n"+
|
}
|
||||||
"Key Shares: %d\n"+
|
c.Nonce = rekeyStatus.Nonce
|
||||||
"Key Threshold: %d\n",
|
|
||||||
shares,
|
|
||||||
threshold,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shares = rekeyStatus.N
|
||||||
|
threshold = rekeyStatus.T
|
||||||
|
serverNonce := rekeyStatus.Nonce
|
||||||
|
|
||||||
// Get the unseal key
|
// Get the unseal key
|
||||||
args = flags.Args()
|
args = flags.Args()
|
||||||
value := c.Key
|
key := c.Key
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
value = args[0]
|
key = args[0]
|
||||||
}
|
}
|
||||||
if value == "" {
|
if key == "" {
|
||||||
|
c.Nonce = serverNonce
|
||||||
|
fmt.Printf("Rekey operation nonce: %s\n", serverNonce)
|
||||||
fmt.Printf("Key (will be hidden): ")
|
fmt.Printf("Key (will be hidden): ")
|
||||||
value, err = password.Read(os.Stdin)
|
key, err = password.Read(os.Stdin)
|
||||||
fmt.Printf("\n")
|
fmt.Printf("\n")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf(
|
c.Ui.Error(fmt.Sprintf(
|
||||||
|
@ -106,7 +126,7 @@ func (c *RekeyCommand) Run(args []string) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provide the key, this may potentially complete the update
|
// Provide the key, this may potentially complete the update
|
||||||
result, err := client.Sys().RekeyUpdate(strings.TrimSpace(value))
|
result, err := client.Sys().RekeyUpdate(strings.TrimSpace(key), c.Nonce)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Error attempting rekey update: %s", err))
|
c.Ui.Error(fmt.Sprintf("Error attempting rekey update: %s", err))
|
||||||
return 1
|
return 1
|
||||||
|
@ -119,7 +139,22 @@ func (c *RekeyCommand) Run(args []string) int {
|
||||||
|
|
||||||
// Provide the keys
|
// Provide the keys
|
||||||
for i, key := range result.Keys {
|
for i, key := range result.Keys {
|
||||||
c.Ui.Output(fmt.Sprintf("Key %d: %s", i+1, key))
|
if len(result.PGPFingerprints) > 0 {
|
||||||
|
c.Ui.Output(fmt.Sprintf("Key %d fingerprint: %s; value: %s", i+1, result.PGPFingerprints[i], key))
|
||||||
|
} else {
|
||||||
|
c.Ui.Output(fmt.Sprintf("Key %d: %s", i+1, key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Ui.Output(fmt.Sprintf("\nOperation nonce: %s", result.Nonce))
|
||||||
|
|
||||||
|
if len(result.PGPFingerprints) > 0 && result.Backup {
|
||||||
|
c.Ui.Output(fmt.Sprintf(
|
||||||
|
"\n" +
|
||||||
|
"The encrypted unseal keys have been backed up to \"core/unseal-keys\n" +
|
||||||
|
"in your physical backend. It is your responsibility to remove these\n" +
|
||||||
|
"if and when desired.",
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Ui.Output(fmt.Sprintf(
|
c.Ui.Output(fmt.Sprintf(
|
||||||
|
@ -140,12 +175,16 @@ func (c *RekeyCommand) Run(args []string) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
// initRekey is used to start the rekey process
|
// initRekey is used to start the rekey process
|
||||||
func (c *RekeyCommand) initRekey(client *api.Client, shares, threshold int, pgpKeys pgpkeys.PubKeyFilesFlag) int {
|
func (c *RekeyCommand) initRekey(client *api.Client,
|
||||||
|
shares, threshold int,
|
||||||
|
pgpKeys pgpkeys.PubKeyFilesFlag,
|
||||||
|
backup bool) int {
|
||||||
// Start the rekey
|
// Start the rekey
|
||||||
err := client.Sys().RekeyInit(&api.RekeyInitRequest{
|
err := client.Sys().RekeyInit(&api.RekeyInitRequest{
|
||||||
SecretShares: shares,
|
SecretShares: shares,
|
||||||
SecretThreshold: threshold,
|
SecretThreshold: threshold,
|
||||||
PGPKeys: pgpKeys,
|
PGPKeys: pgpKeys,
|
||||||
|
Backup: backup,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Error initializing rekey: %s", err))
|
c.Ui.Error(fmt.Sprintf("Error initializing rekey: %s", err))
|
||||||
|
@ -177,18 +216,49 @@ func (c *RekeyCommand) rekeyStatus(client *api.Client) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dump the status
|
// Dump the status
|
||||||
c.Ui.Output(fmt.Sprintf(
|
statString := fmt.Sprintf(
|
||||||
"Started: %v\n"+
|
"Nonce: %s\n"+
|
||||||
|
"Started: %v\n"+
|
||||||
"Key Shares: %d\n"+
|
"Key Shares: %d\n"+
|
||||||
"Key Threshold: %d\n"+
|
"Key Threshold: %d\n"+
|
||||||
"Rekey Progress: %d\n"+
|
"Rekey Progress: %d\n"+
|
||||||
"Required Keys: %d",
|
"Required Keys: %d",
|
||||||
|
status.Nonce,
|
||||||
status.Started,
|
status.Started,
|
||||||
status.N,
|
status.N,
|
||||||
status.T,
|
status.T,
|
||||||
status.Progress,
|
status.Progress,
|
||||||
status.Required,
|
status.Required,
|
||||||
))
|
)
|
||||||
|
if len(status.PGPFingerprints) != 0 {
|
||||||
|
statString = fmt.Sprintf("\nPGP Key Fingerprints: %s", status.PGPFingerprints)
|
||||||
|
statString = fmt.Sprintf("\nBackup Storage: %t", status.Backup)
|
||||||
|
}
|
||||||
|
c.Ui.Output(statString)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RekeyCommand) rekeyRetrieveStored(client *api.Client) int {
|
||||||
|
storedKeys, err := client.Sys().RekeyRetrieveStored()
|
||||||
|
if err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Error retrieving stored keys: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
secret := &api.Secret{
|
||||||
|
Data: structs.New(storedKeys).Map(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return OutputSecret(c.Ui, "table", secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RekeyCommand) rekeyDeleteStored(client *api.Client) int {
|
||||||
|
err := client.Sys().RekeyDeleteStored()
|
||||||
|
if err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Failed to delete stored keys: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
c.Ui.Output("Stored keys deleted.")
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,7 +282,7 @@ General Options:
|
||||||
|
|
||||||
` + generalOptionsUsage() + `
|
` + generalOptionsUsage() + `
|
||||||
|
|
||||||
Unseal Options:
|
Rekey Options:
|
||||||
|
|
||||||
-init Initialize the rekey operation by setting the desired
|
-init Initialize the rekey operation by setting the desired
|
||||||
number of shares and the key threshold. This can only be
|
number of shares and the key threshold. This can only be
|
||||||
|
@ -225,12 +295,23 @@ Unseal Options:
|
||||||
This can be used to see the status without attempting
|
This can be used to see the status without attempting
|
||||||
to provide an unseal key.
|
to provide an unseal key.
|
||||||
|
|
||||||
|
-retrieve Retrieve backed-up keys. Only available if the PGP keys
|
||||||
|
were provided and the backup has not been deleted.
|
||||||
|
|
||||||
|
-delete Delete any backed-up keys.
|
||||||
|
|
||||||
-key-shares=5 The number of key shares to split the master key
|
-key-shares=5 The number of key shares to split the master key
|
||||||
into.
|
into.
|
||||||
|
|
||||||
-key-threshold=3 The number of key shares required to reconstruct
|
-key-threshold=3 The number of key shares required to reconstruct
|
||||||
the master key.
|
the master key.
|
||||||
|
|
||||||
|
-nonce=abcd The nonce provided at rekey initialization time. This
|
||||||
|
same nonce value must be provided with each unseal
|
||||||
|
key. If the unseal key is not being passed in via the
|
||||||
|
the command line the nonce parameter is not required,
|
||||||
|
and will instead be displayed with the key prompt.
|
||||||
|
|
||||||
-pgp-keys If provided, must be a comma-separated list of
|
-pgp-keys If provided, must be a comma-separated list of
|
||||||
files on disk containing binary- or base64-format
|
files on disk containing binary- or base64-format
|
||||||
public PGP keys, or Keybase usernames specified as
|
public PGP keys, or Keybase usernames specified as
|
||||||
|
@ -240,6 +321,12 @@ Unseal Options:
|
||||||
public keys. If you want to use them with the 'vault
|
public keys. If you want to use them with the 'vault
|
||||||
unseal' command, you will need to hex decode and
|
unseal' command, you will need to hex decode and
|
||||||
decrypt; this will be the plaintext unseal key.
|
decrypt; this will be the plaintext unseal key.
|
||||||
|
|
||||||
|
-backup=false If true, and if the key shares are PGP-encrypted, a
|
||||||
|
plaintext backup of the PGP-encrypted keys will be
|
||||||
|
stored at "core/unseal-keys-backup" in your physical
|
||||||
|
storage. You can retrieve or delete them via the
|
||||||
|
'sys/rekey/backup' endpoint.
|
||||||
`
|
`
|
||||||
return strings.TrimSpace(helpText)
|
return strings.TrimSpace(helpText)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,10 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/hashicorp/vault/http"
|
"github.com/hashicorp/vault/http"
|
||||||
|
"github.com/hashicorp/vault/logical"
|
||||||
"github.com/hashicorp/vault/vault"
|
"github.com/hashicorp/vault/vault"
|
||||||
"github.com/mitchellh/cli"
|
"github.com/mitchellh/cli"
|
||||||
)
|
)
|
||||||
|
@ -77,7 +79,12 @@ func TestRekey_init(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
args := []string{"-address", addr, "-init", "-key-threshold=10", "-key-shares=10"}
|
args := []string{
|
||||||
|
"-address", addr,
|
||||||
|
"-init",
|
||||||
|
"-key-threshold", "10",
|
||||||
|
"-key-shares", "10",
|
||||||
|
}
|
||||||
if code := c.Run(args); code != 0 {
|
if code := c.Run(args); code != 0 {
|
||||||
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||||
}
|
}
|
||||||
|
@ -159,6 +166,15 @@ func TestRekey_init_pgp(t *testing.T) {
|
||||||
ln, addr := http.TestServer(t, core)
|
ln, addr := http.TestServer(t, core)
|
||||||
defer ln.Close()
|
defer ln.Close()
|
||||||
|
|
||||||
|
bc := &logical.BackendConfig{
|
||||||
|
Logger: nil,
|
||||||
|
System: logical.StaticSystemView{
|
||||||
|
DefaultLeaseTTLVal: time.Hour * 24,
|
||||||
|
MaxLeaseTTLVal: time.Hour * 24 * 30,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
sysBackend := vault.NewSystemBackend(core, bc)
|
||||||
|
|
||||||
ui := new(cli.MockUi)
|
ui := new(cli.MockUi)
|
||||||
c := &RekeyCommand{
|
c := &RekeyCommand{
|
||||||
Key: hex.EncodeToString(key),
|
Key: hex.EncodeToString(key),
|
||||||
|
@ -179,6 +195,7 @@ func TestRekey_init_pgp(t *testing.T) {
|
||||||
"-key-shares", "3",
|
"-key-shares", "3",
|
||||||
"-pgp-keys", pubFiles[0] + ",@" + pubFiles[1] + "," + pubFiles[2],
|
"-pgp-keys", pubFiles[0] + ",@" + pubFiles[1] + "," + pubFiles[2],
|
||||||
"-key-threshold", "2",
|
"-key-threshold", "2",
|
||||||
|
"-backup", "true",
|
||||||
}
|
}
|
||||||
|
|
||||||
if code := c.Run(args); code != 0 {
|
if code := c.Run(args); code != 0 {
|
||||||
|
@ -196,6 +213,8 @@ func TestRekey_init_pgp(t *testing.T) {
|
||||||
t.Fatal("should rekey")
|
t.Fatal("should rekey")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.Nonce = config.Nonce
|
||||||
|
|
||||||
args = []string{
|
args = []string{
|
||||||
"-address", addr,
|
"-address", addr,
|
||||||
}
|
}
|
||||||
|
@ -203,5 +222,45 @@ func TestRekey_init_pgp(t *testing.T) {
|
||||||
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
parseDecryptAndTestUnsealKeys(t, ui.OutputWriter.String(), token, core)
|
type backupStruct struct {
|
||||||
|
Keys map[string]string
|
||||||
|
}
|
||||||
|
backupVals := &backupStruct{}
|
||||||
|
|
||||||
|
req := logical.TestRequest(t, logical.ReadOperation, "rekey/backup")
|
||||||
|
resp, err := sysBackend.HandleRequest(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error running backed-up unseal key fetch: %v", err)
|
||||||
|
}
|
||||||
|
if resp == nil {
|
||||||
|
t.Fatalf("got nil resp with unseal key fetch")
|
||||||
|
}
|
||||||
|
if resp.Data["keys"] == nil {
|
||||||
|
t.Fatalf("could not retrieve unseal keys from token")
|
||||||
|
}
|
||||||
|
if resp.Data["nonce"] != config.Nonce {
|
||||||
|
t.Fatalf("nonce mismatch between rekey and backed-up keys")
|
||||||
|
}
|
||||||
|
|
||||||
|
backupVals.Keys = resp.Data["keys"].(map[string]string)
|
||||||
|
|
||||||
|
// Now delete and try again; the values should be inaccessible
|
||||||
|
req = logical.TestRequest(t, logical.DeleteOperation, "rekey/backup")
|
||||||
|
resp, err = sysBackend.HandleRequest(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error running backed-up unseal key delete: %v", err)
|
||||||
|
}
|
||||||
|
req = logical.TestRequest(t, logical.ReadOperation, "rekey/backup")
|
||||||
|
resp, err = sysBackend.HandleRequest(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error running backed-up unseal key fetch: %v", err)
|
||||||
|
}
|
||||||
|
if resp == nil {
|
||||||
|
t.Fatalf("got nil resp with unseal key fetch")
|
||||||
|
}
|
||||||
|
if resp.Data["keys"] != nil {
|
||||||
|
t.Fatalf("keys found when they should have been deleted")
|
||||||
|
}
|
||||||
|
|
||||||
|
parseDecryptAndTestUnsealKeys(t, ui.OutputWriter.String(), token, true, backupVals.Keys, core)
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,12 +16,56 @@ import (
|
||||||
//
|
//
|
||||||
// Note: There is no corresponding test function; this functionality is
|
// Note: There is no corresponding test function; this functionality is
|
||||||
// thoroughly tested in the init and rekey command unit tests
|
// thoroughly tested in the init and rekey command unit tests
|
||||||
func EncryptShares(secretShares [][]byte, pgpKeys []string) ([][]byte, error) {
|
func EncryptShares(secretShares [][]byte, pgpKeys []string) ([]string, [][]byte, error) {
|
||||||
if len(secretShares) != len(pgpKeys) {
|
if len(secretShares) != len(pgpKeys) {
|
||||||
return nil, fmt.Errorf("Mismatch between number of generated shares and number of PGP keys")
|
return nil, nil, fmt.Errorf("Mismatch between number of generated shares and number of PGP keys")
|
||||||
}
|
}
|
||||||
encryptedShares := [][]byte{}
|
encryptedShares := make([][]byte, 0, len(pgpKeys))
|
||||||
for i, keystring := range pgpKeys {
|
entities, err := GetEntities(pgpKeys)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
for i, entity := range entities {
|
||||||
|
ctBuf := bytes.NewBuffer(nil)
|
||||||
|
pt, err := openpgp.Encrypt(ctBuf, []*openpgp.Entity{entity}, nil, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("Error setting up encryption for PGP message: %s", err)
|
||||||
|
}
|
||||||
|
_, err = pt.Write([]byte(hex.EncodeToString(secretShares[i])))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("Error encrypting PGP message: %s", err)
|
||||||
|
}
|
||||||
|
pt.Close()
|
||||||
|
encryptedShares = append(encryptedShares, ctBuf.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
fingerprints, err := GetFingerprints(nil, entities)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fingerprints, encryptedShares, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFingerprints(pgpKeys []string, entities []*openpgp.Entity) ([]string, error) {
|
||||||
|
if entities == nil {
|
||||||
|
var err error
|
||||||
|
entities, err = GetEntities(pgpKeys)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ret := make([]string, 0, len(entities))
|
||||||
|
for _, entity := range entities {
|
||||||
|
ret = append(ret, fmt.Sprintf("%x", entity.PrimaryKey.Fingerprint))
|
||||||
|
}
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetEntities(pgpKeys []string) ([]*openpgp.Entity, error) {
|
||||||
|
ret := make([]*openpgp.Entity, 0, len(pgpKeys))
|
||||||
|
for _, keystring := range pgpKeys {
|
||||||
data, err := base64.StdEncoding.DecodeString(keystring)
|
data, err := base64.StdEncoding.DecodeString(keystring)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Error decoding given PGP key: %s", err)
|
return nil, fmt.Errorf("Error decoding given PGP key: %s", err)
|
||||||
|
@ -30,17 +74,7 @@ func EncryptShares(secretShares [][]byte, pgpKeys []string) ([][]byte, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Error parsing given PGP key: %s", err)
|
return nil, fmt.Errorf("Error parsing given PGP key: %s", err)
|
||||||
}
|
}
|
||||||
ctBuf := bytes.NewBuffer(nil)
|
ret = append(ret, entity)
|
||||||
pt, err := openpgp.Encrypt(ctBuf, []*openpgp.Entity{entity}, nil, nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Error setting up encryption for PGP message: %s", err)
|
|
||||||
}
|
|
||||||
_, err = pt.Write([]byte(hex.EncodeToString(secretShares[i])))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Error encrypting PGP message: %s", err)
|
|
||||||
}
|
|
||||||
pt.Close()
|
|
||||||
encryptedShares = append(encryptedShares, ctBuf.Bytes())
|
|
||||||
}
|
}
|
||||||
return encryptedShares, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,26 +40,36 @@ func (p *PubKeyFilesFlag) Set(value string) error {
|
||||||
*p = append(*p, key)
|
*p = append(*p, key)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if keyfile[0] == '@' {
|
|
||||||
keyfile = keyfile[1:]
|
pgpStr, err := ReadPGPFile(keyfile)
|
||||||
}
|
|
||||||
f, err := os.Open(keyfile)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
buf := bytes.NewBuffer(nil)
|
|
||||||
_, err = buf.ReadFrom(f)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = base64.StdEncoding.DecodeString(buf.String())
|
*p = append(*p, pgpStr)
|
||||||
if err == nil {
|
|
||||||
*p = append(*p, buf.String())
|
|
||||||
} else {
|
|
||||||
*p = append(*p, base64.StdEncoding.EncodeToString(buf.Bytes()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ReadPGPFile(path string) (string, error) {
|
||||||
|
if path[0] == '@' {
|
||||||
|
path = path[1:]
|
||||||
|
}
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
_, err = buf.ReadFrom(f)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = base64.StdEncoding.DecodeString(buf.String())
|
||||||
|
if err == nil {
|
||||||
|
return buf.String(), nil
|
||||||
|
}
|
||||||
|
return base64.StdEncoding.EncodeToString(buf.Bytes()), nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -42,6 +42,7 @@ func Handler(core *vault.Core) http.Handler {
|
||||||
mux.Handle("/v1/sys/rotate", proxySysRequest(core))
|
mux.Handle("/v1/sys/rotate", proxySysRequest(core))
|
||||||
mux.Handle("/v1/sys/key-status", proxySysRequest(core))
|
mux.Handle("/v1/sys/key-status", proxySysRequest(core))
|
||||||
mux.Handle("/v1/sys/rekey/init", handleSysRekeyInit(core))
|
mux.Handle("/v1/sys/rekey/init", handleSysRekeyInit(core))
|
||||||
|
mux.Handle("/v1/sys/rekey/backup", proxySysRequest(core))
|
||||||
mux.Handle("/v1/sys/rekey/update", handleSysRekeyUpdate(core))
|
mux.Handle("/v1/sys/rekey/update", handleSysRekeyUpdate(core))
|
||||||
mux.Handle("/v1/", handleLogical(core, false))
|
mux.Handle("/v1/", handleLogical(core, false))
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/helper/pgpkeys"
|
||||||
"github.com/hashicorp/vault/vault"
|
"github.com/hashicorp/vault/vault"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -60,9 +61,18 @@ func handleSysRekeyInitGet(core *vault.Core, w http.ResponseWriter, r *http.Requ
|
||||||
Required: sealConfig.SecretThreshold,
|
Required: sealConfig.SecretThreshold,
|
||||||
}
|
}
|
||||||
if rekeyConf != nil {
|
if rekeyConf != nil {
|
||||||
|
status.Nonce = rekeyConf.Nonce
|
||||||
status.Started = true
|
status.Started = true
|
||||||
status.T = rekeyConf.SecretThreshold
|
status.T = rekeyConf.SecretThreshold
|
||||||
status.N = rekeyConf.SecretShares
|
status.N = rekeyConf.SecretShares
|
||||||
|
if rekeyConf.PGPKeys != nil && len(rekeyConf.PGPKeys) != 0 {
|
||||||
|
pgpFingerprints, err := pgpkeys.GetFingerprints(rekeyConf.PGPKeys, nil)
|
||||||
|
if err != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, err)
|
||||||
|
}
|
||||||
|
status.PGPFingerprints = pgpFingerprints
|
||||||
|
status.Backup = rekeyConf.Backup
|
||||||
|
}
|
||||||
}
|
}
|
||||||
respondOk(w, status)
|
respondOk(w, status)
|
||||||
}
|
}
|
||||||
|
@ -75,11 +85,16 @@ func handleSysRekeyInitPut(core *vault.Core, w http.ResponseWriter, r *http.Requ
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.Backup && len(req.PGPKeys) == 0 {
|
||||||
|
respondError(w, http.StatusBadRequest, fmt.Errorf("cannot request a backup of the new keys without providing PGP keys for encryption"))
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize the rekey
|
// Initialize the rekey
|
||||||
err := core.RekeyInit(&vault.SealConfig{
|
err := core.RekeyInit(&vault.SealConfig{
|
||||||
SecretShares: req.SecretShares,
|
SecretShares: req.SecretShares,
|
||||||
SecretThreshold: req.SecretThreshold,
|
SecretThreshold: req.SecretThreshold,
|
||||||
PGPKeys: req.PGPKeys,
|
PGPKeys: req.PGPKeys,
|
||||||
|
Backup: req.Backup,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondError(w, http.StatusBadRequest, err)
|
respondError(w, http.StatusBadRequest, err)
|
||||||
|
@ -127,7 +142,7 @@ func handleSysRekeyUpdate(core *vault.Core) http.Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the key to make progress on rekey
|
// Use the key to make progress on rekey
|
||||||
result, err := core.RekeyUpdate(key)
|
result, err := core.RekeyUpdate(key, req.Nonce)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondError(w, http.StatusBadRequest, err)
|
respondError(w, http.StatusBadRequest, err)
|
||||||
return
|
return
|
||||||
|
@ -137,6 +152,7 @@ func handleSysRekeyUpdate(core *vault.Core) http.Handler {
|
||||||
resp := &RekeyUpdateResponse{}
|
resp := &RekeyUpdateResponse{}
|
||||||
if result != nil {
|
if result != nil {
|
||||||
resp.Complete = true
|
resp.Complete = true
|
||||||
|
resp.Nonce = req.Nonce
|
||||||
|
|
||||||
// Encode the keys
|
// Encode the keys
|
||||||
keys := make([]string, 0, len(result.SecretShares))
|
keys := make([]string, 0, len(result.SecretShares))
|
||||||
|
@ -144,6 +160,9 @@ func handleSysRekeyUpdate(core *vault.Core) http.Handler {
|
||||||
keys = append(keys, hex.EncodeToString(k))
|
keys = append(keys, hex.EncodeToString(k))
|
||||||
}
|
}
|
||||||
resp.Keys = keys
|
resp.Keys = keys
|
||||||
|
|
||||||
|
resp.Backup = result.Backup
|
||||||
|
resp.PGPFingerprints = result.PGPFingerprints
|
||||||
}
|
}
|
||||||
respondOk(w, resp)
|
respondOk(w, resp)
|
||||||
})
|
})
|
||||||
|
@ -153,21 +172,29 @@ type RekeyRequest struct {
|
||||||
SecretShares int `json:"secret_shares"`
|
SecretShares int `json:"secret_shares"`
|
||||||
SecretThreshold int `json:"secret_threshold"`
|
SecretThreshold int `json:"secret_threshold"`
|
||||||
PGPKeys []string `json:"pgp_keys"`
|
PGPKeys []string `json:"pgp_keys"`
|
||||||
|
Backup bool `json:"backup"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RekeyStatusResponse struct {
|
type RekeyStatusResponse struct {
|
||||||
Started bool `json:"started"`
|
Nonce string `json:"nonce"`
|
||||||
T int `json:"t"`
|
Started bool `json:"started"`
|
||||||
N int `json:"n"`
|
T int `json:"t"`
|
||||||
Progress int `json:"progress"`
|
N int `json:"n"`
|
||||||
Required int `json:"required"`
|
Progress int `json:"progress"`
|
||||||
|
Required int `json:"required"`
|
||||||
|
PGPFingerprints []string `json:"pgp_fingerprints"`
|
||||||
|
Backup bool `json:"backup"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RekeyUpdateRequest struct {
|
type RekeyUpdateRequest struct {
|
||||||
Key string
|
Nonce string
|
||||||
|
Key string
|
||||||
}
|
}
|
||||||
|
|
||||||
type RekeyUpdateResponse struct {
|
type RekeyUpdateResponse struct {
|
||||||
Complete bool `json:"complete"`
|
Nonce string `json:"nonce"`
|
||||||
Keys []string `json:"keys"`
|
Complete bool `json:"complete"`
|
||||||
|
Keys []string `json:"keys"`
|
||||||
|
PGPFingerprints []string `json:"pgp_fingerprints"`
|
||||||
|
Backup bool `json:"backup"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,16 +22,19 @@ func TestSysRekeyInit_Status(t *testing.T) {
|
||||||
|
|
||||||
var actual map[string]interface{}
|
var actual map[string]interface{}
|
||||||
expected := map[string]interface{}{
|
expected := map[string]interface{}{
|
||||||
"started": false,
|
"started": false,
|
||||||
"t": float64(0),
|
"t": float64(0),
|
||||||
"n": float64(0),
|
"n": float64(0),
|
||||||
"progress": float64(0),
|
"progress": float64(0),
|
||||||
"required": float64(1),
|
"required": float64(1),
|
||||||
|
"pgp_fingerprints": interface{}(nil),
|
||||||
|
"backup": false,
|
||||||
}
|
}
|
||||||
testResponseStatus(t, resp, 200)
|
testResponseStatus(t, resp, 200)
|
||||||
testResponseBody(t, resp, &actual)
|
testResponseBody(t, resp, &actual)
|
||||||
|
expected["nonce"] = actual["nonce"]
|
||||||
if !reflect.DeepEqual(actual, expected) {
|
if !reflect.DeepEqual(actual, expected) {
|
||||||
t.Fatalf("bad: %#v", actual)
|
t.Fatalf("\nexpected: %#v\nactual: %#v", expected, actual)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,16 +54,19 @@ func TestSysRekeyInit_Setup(t *testing.T) {
|
||||||
|
|
||||||
var actual map[string]interface{}
|
var actual map[string]interface{}
|
||||||
expected := map[string]interface{}{
|
expected := map[string]interface{}{
|
||||||
"started": true,
|
"started": true,
|
||||||
"t": float64(3),
|
"t": float64(3),
|
||||||
"n": float64(5),
|
"n": float64(5),
|
||||||
"progress": float64(0),
|
"progress": float64(0),
|
||||||
"required": float64(1),
|
"required": float64(1),
|
||||||
|
"pgp_fingerprints": interface{}(nil),
|
||||||
|
"backup": false,
|
||||||
}
|
}
|
||||||
testResponseStatus(t, resp, 200)
|
testResponseStatus(t, resp, 200)
|
||||||
testResponseBody(t, resp, &actual)
|
testResponseBody(t, resp, &actual)
|
||||||
|
expected["nonce"] = actual["nonce"]
|
||||||
if !reflect.DeepEqual(actual, expected) {
|
if !reflect.DeepEqual(actual, expected) {
|
||||||
t.Fatalf("bad: %#v", actual)
|
t.Fatalf("\nexpected: %#v\nactual: %#v", expected, actual)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,16 +92,19 @@ func TestSysRekeyInit_Cancel(t *testing.T) {
|
||||||
|
|
||||||
var actual map[string]interface{}
|
var actual map[string]interface{}
|
||||||
expected := map[string]interface{}{
|
expected := map[string]interface{}{
|
||||||
"started": false,
|
"started": false,
|
||||||
"t": float64(0),
|
"t": float64(0),
|
||||||
"n": float64(0),
|
"n": float64(0),
|
||||||
"progress": float64(0),
|
"progress": float64(0),
|
||||||
"required": float64(1),
|
"required": float64(1),
|
||||||
|
"pgp_fingerprints": interface{}(nil),
|
||||||
|
"backup": false,
|
||||||
}
|
}
|
||||||
testResponseStatus(t, resp, 200)
|
testResponseStatus(t, resp, 200)
|
||||||
testResponseBody(t, resp, &actual)
|
testResponseBody(t, resp, &actual)
|
||||||
|
expected["nonce"] = actual["nonce"]
|
||||||
if !reflect.DeepEqual(actual, expected) {
|
if !reflect.DeepEqual(actual, expected) {
|
||||||
t.Fatalf("bad: %#v", actual)
|
t.Fatalf("\nexpected: %#v\nactual: %#v", expected, actual)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,13 +132,26 @@ func TestSysRekey_Update(t *testing.T) {
|
||||||
})
|
})
|
||||||
testResponseStatus(t, resp, 204)
|
testResponseStatus(t, resp, 204)
|
||||||
|
|
||||||
|
// We need to get the nonce first before we update
|
||||||
|
resp, err := http.Get(addr + "/v1/sys/rekey/init")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
var rekeyStatus map[string]interface{}
|
||||||
|
testResponseStatus(t, resp, 200)
|
||||||
|
testResponseBody(t, resp, &rekeyStatus)
|
||||||
|
|
||||||
resp = testHttpPut(t, token, addr+"/v1/sys/rekey/update", map[string]interface{}{
|
resp = testHttpPut(t, token, addr+"/v1/sys/rekey/update", map[string]interface{}{
|
||||||
"key": hex.EncodeToString(master),
|
"nonce": rekeyStatus["nonce"].(string),
|
||||||
|
"key": hex.EncodeToString(master),
|
||||||
})
|
})
|
||||||
|
|
||||||
var actual map[string]interface{}
|
var actual map[string]interface{}
|
||||||
expected := map[string]interface{}{
|
expected := map[string]interface{}{
|
||||||
"complete": true,
|
"complete": true,
|
||||||
|
"nonce": rekeyStatus["nonce"].(string),
|
||||||
|
"backup": false,
|
||||||
|
"pgp_fingerprints": interface{}(nil),
|
||||||
}
|
}
|
||||||
testResponseStatus(t, resp, 200)
|
testResponseStatus(t, resp, 200)
|
||||||
testResponseBody(t, resp, &actual)
|
testResponseBody(t, resp, &actual)
|
||||||
|
@ -141,6 +163,34 @@ func TestSysRekey_Update(t *testing.T) {
|
||||||
|
|
||||||
delete(actual, "keys")
|
delete(actual, "keys")
|
||||||
if !reflect.DeepEqual(actual, expected) {
|
if !reflect.DeepEqual(actual, expected) {
|
||||||
t.Fatalf("bad: %#v", actual)
|
t.Fatalf("\nexpected: %#v\nactual: %#v", expected, actual)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSysRekey_ReInitUpdate(t *testing.T) {
|
||||||
|
core, master, token := vault.TestCoreUnsealed(t)
|
||||||
|
ln, addr := TestServer(t, core)
|
||||||
|
defer ln.Close()
|
||||||
|
TestServerAuth(t, addr, token)
|
||||||
|
|
||||||
|
resp := testHttpPut(t, token, addr+"/v1/sys/rekey/init", map[string]interface{}{
|
||||||
|
"secret_shares": 5,
|
||||||
|
"secret_threshold": 3,
|
||||||
|
})
|
||||||
|
testResponseStatus(t, resp, 204)
|
||||||
|
|
||||||
|
resp = testHttpDelete(t, token, addr+"/v1/sys/rekey/init")
|
||||||
|
testResponseStatus(t, resp, 204)
|
||||||
|
|
||||||
|
resp = testHttpPut(t, token, addr+"/v1/sys/rekey/init", map[string]interface{}{
|
||||||
|
"secret_shares": 5,
|
||||||
|
"secret_threshold": 3,
|
||||||
|
})
|
||||||
|
testResponseStatus(t, resp, 204)
|
||||||
|
|
||||||
|
resp = testHttpPut(t, token, addr+"/v1/sys/rekey/update", map[string]interface{}{
|
||||||
|
"key": hex.EncodeToString(master),
|
||||||
|
})
|
||||||
|
|
||||||
|
testResponseStatus(t, resp, 400)
|
||||||
|
}
|
||||||
|
|
125
vault/core.go
125
vault/core.go
|
@ -3,6 +3,7 @@ package vault
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -43,6 +44,11 @@ const (
|
||||||
// the currently elected leader.
|
// the currently elected leader.
|
||||||
coreLeaderPrefix = "core/leader/"
|
coreLeaderPrefix = "core/leader/"
|
||||||
|
|
||||||
|
// coreUnsealKeysBackupPath is the path used to back upencrypted unseal
|
||||||
|
// keys if specified during a rekey operation. This is outside of the
|
||||||
|
// barrier.
|
||||||
|
coreUnsealKeysBackupPath = "core/unseal-keys-backup"
|
||||||
|
|
||||||
// lockRetryInterval is the interval we re-attempt to acquire the
|
// lockRetryInterval is the interval we re-attempt to acquire the
|
||||||
// HA lock if an error is encountered
|
// HA lock if an error is encountered
|
||||||
lockRetryInterval = 10 * time.Second
|
lockRetryInterval = 10 * time.Second
|
||||||
|
@ -96,11 +102,21 @@ type SealConfig struct {
|
||||||
// if requested, to encrypt the output unseal tokens. If
|
// if requested, to encrypt the output unseal tokens. If
|
||||||
// provided, it sets the value of SecretShares. Ordering
|
// provided, it sets the value of SecretShares. Ordering
|
||||||
// is important.
|
// is important.
|
||||||
PGPKeys []string `json:"-"`
|
PGPKeys []string `json:"pgp_keys"`
|
||||||
|
|
||||||
// SecretThreshold is the number of parts required
|
// SecretThreshold is the number of parts required
|
||||||
// to open the vault. This is the T value of Shamir
|
// to open the vault. This is the T value of Shamir
|
||||||
SecretThreshold int `json:"secret_threshold"`
|
SecretThreshold int `json:"secret_threshold"`
|
||||||
|
|
||||||
|
// Nonce is a nonce generated by Vault used to ensure that when unseal keys
|
||||||
|
// are submitted for a rekey operation, the rekey operation itself is the
|
||||||
|
// one intended. This prevents hijacking of the rekey operation, since it
|
||||||
|
// is unauthenticated.
|
||||||
|
Nonce string `json:"nonce"`
|
||||||
|
|
||||||
|
// Backup indicates whether or not a backup of PGP-encrypted unseal keys
|
||||||
|
// should be stored at coreUnsealKeysBackupPath after successful rekeying.
|
||||||
|
Backup bool `json:"backup"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate is used to sanity check the seal configuration
|
// Validate is used to sanity check the seal configuration
|
||||||
|
@ -151,7 +167,15 @@ type InitResult struct {
|
||||||
// RekeyResult is used to provide the key parts back after
|
// RekeyResult is used to provide the key parts back after
|
||||||
// they are generated as part of the rekey.
|
// they are generated as part of the rekey.
|
||||||
type RekeyResult struct {
|
type RekeyResult struct {
|
||||||
SecretShares [][]byte
|
SecretShares [][]byte
|
||||||
|
PGPFingerprints []string
|
||||||
|
Backup bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// RekeyBackup stores the backup copy of PGP-encrypted keys
|
||||||
|
type RekeyBackup struct {
|
||||||
|
Nonce string
|
||||||
|
Keys map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrInvalidKey is returned if there is an error with a
|
// ErrInvalidKey is returned if there is an error with a
|
||||||
|
@ -821,7 +845,7 @@ func (c *Core) Initialize(config *SealConfig) (*InitResult, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(config.PGPKeys) > 0 {
|
if len(config.PGPKeys) > 0 {
|
||||||
encryptedShares, err := pgpkeys.EncryptShares(results.SecretShares, config.PGPKeys)
|
_, encryptedShares, err := pgpkeys.EncryptShares(results.SecretShares, config.PGPKeys)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -1208,13 +1232,16 @@ func (c *Core) RekeyInit(config *SealConfig) error {
|
||||||
// Copy the configuration
|
// Copy the configuration
|
||||||
c.rekeyConfig = new(SealConfig)
|
c.rekeyConfig = new(SealConfig)
|
||||||
*c.rekeyConfig = *config
|
*c.rekeyConfig = *config
|
||||||
c.logger.Printf("[INFO] core: rekey initialized (shares: %d, threshold: %d)",
|
|
||||||
c.rekeyConfig.SecretShares, c.rekeyConfig.SecretThreshold)
|
// Initialize the nonce
|
||||||
|
c.rekeyConfig.Nonce = uuid.GenerateUUID()
|
||||||
|
c.logger.Printf("[INFO] core: rekey initialized (nonce: %s, shares: %d, threshold: %d)",
|
||||||
|
c.rekeyConfig.Nonce, c.rekeyConfig.SecretShares, c.rekeyConfig.SecretThreshold)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RekeyUpdate is used to provide a new key part
|
// RekeyUpdate is used to provide a new key part
|
||||||
func (c *Core) RekeyUpdate(key []byte) (*RekeyResult, error) {
|
func (c *Core) RekeyUpdate(key []byte, nonce string) (*RekeyResult, error) {
|
||||||
// Verify the key length
|
// Verify the key length
|
||||||
min, max := c.barrier.KeyLength()
|
min, max := c.barrier.KeyLength()
|
||||||
max += shamir.ShareOverhead
|
max += shamir.ShareOverhead
|
||||||
|
@ -1254,6 +1281,10 @@ func (c *Core) RekeyUpdate(key []byte) (*RekeyResult, error) {
|
||||||
return nil, fmt.Errorf("no rekey in progress")
|
return nil, fmt.Errorf("no rekey in progress")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if nonce != c.rekeyConfig.Nonce {
|
||||||
|
return nil, fmt.Errorf("incorrect nonce supplied; nonce for this rekey operation is %s", c.rekeyConfig.Nonce)
|
||||||
|
}
|
||||||
|
|
||||||
// Check if we already have this piece
|
// Check if we already have this piece
|
||||||
for _, existing := range c.rekeyProgress {
|
for _, existing := range c.rekeyProgress {
|
||||||
if bytes.Equal(existing, key) {
|
if bytes.Equal(existing, key) {
|
||||||
|
@ -1311,12 +1342,21 @@ func (c *Core) RekeyUpdate(key []byte) (*RekeyResult, error) {
|
||||||
results.SecretShares = shares
|
results.SecretShares = shares
|
||||||
}
|
}
|
||||||
|
|
||||||
|
backupInfo := map[string]string{}
|
||||||
if len(c.rekeyConfig.PGPKeys) > 0 {
|
if len(c.rekeyConfig.PGPKeys) > 0 {
|
||||||
encryptedShares, err := pgpkeys.EncryptShares(results.SecretShares, c.rekeyConfig.PGPKeys)
|
fingerprints, encryptedShares, err := pgpkeys.EncryptShares(results.SecretShares, c.rekeyConfig.PGPKeys)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(fingerprints); i++ {
|
||||||
|
encShare := bytes.NewBuffer(encryptedShares[i])
|
||||||
|
backupInfo[fingerprints[i]] = hex.EncodeToString(encShare.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
results.SecretShares = encryptedShares
|
results.SecretShares = encryptedShares
|
||||||
|
results.PGPFingerprints = fingerprints
|
||||||
|
results.Backup = c.rekeyConfig.Backup
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encode the seal configuration
|
// Encode the seal configuration
|
||||||
|
@ -1343,6 +1383,34 @@ func (c *Core) RekeyUpdate(key []byte) (*RekeyResult, error) {
|
||||||
return nil, fmt.Errorf("failed to update seal configuration: %v", err)
|
return nil, fmt.Errorf("failed to update seal configuration: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.rekeyConfig.Backup && len(backupInfo) > 0 {
|
||||||
|
|
||||||
|
backupVals := &RekeyBackup{
|
||||||
|
Nonce: c.rekeyConfig.Nonce,
|
||||||
|
Keys: backupInfo,
|
||||||
|
}
|
||||||
|
buf, err = json.Marshal(backupVals)
|
||||||
|
if err != nil {
|
||||||
|
// Don't return, we need to clear the progress/config below, so just
|
||||||
|
// log the error
|
||||||
|
// FIXME: in /v2/ when sys endpoints return normal Responses,
|
||||||
|
// ensure a warning is given
|
||||||
|
c.logger.Printf("[ERR] core: failed to marshal unseal key backup: %v", err)
|
||||||
|
} else {
|
||||||
|
pe := &physical.Entry{
|
||||||
|
Key: coreUnsealKeysBackupPath,
|
||||||
|
Value: buf,
|
||||||
|
}
|
||||||
|
if err := c.physical.Put(pe); err != nil {
|
||||||
|
// Don't return, we need to clear the progress/config below, so just
|
||||||
|
// log the error
|
||||||
|
// FIXME: in /v2/ when sys endpoints return normal Responses,
|
||||||
|
// ensure a warning is given
|
||||||
|
c.logger.Printf("[ERR] core: failed to save unseal key backup: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Done!
|
// Done!
|
||||||
c.rekeyProgress = nil
|
c.rekeyProgress = nil
|
||||||
c.rekeyConfig = nil
|
c.rekeyConfig = nil
|
||||||
|
@ -1366,6 +1434,49 @@ func (c *Core) RekeyCancel() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RekeyRetrieveBackup is used to retrieve any backed-up PGP-encrypted unseal
|
||||||
|
// keys
|
||||||
|
func (c *Core) RekeyRetrieveBackup() (*RekeyBackup, error) {
|
||||||
|
c.stateLock.RLock()
|
||||||
|
defer c.stateLock.RUnlock()
|
||||||
|
if c.sealed {
|
||||||
|
return nil, ErrSealed
|
||||||
|
}
|
||||||
|
if c.standby {
|
||||||
|
return nil, ErrStandby
|
||||||
|
}
|
||||||
|
|
||||||
|
entry, err := c.physical.Get(coreUnsealKeysBackupPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if entry == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := &RekeyBackup{}
|
||||||
|
err = json.Unmarshal(entry.Value, ret)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RekeyDeleteBackup is used to delete any backed-up PGP-encrypted unseal keys
|
||||||
|
func (c *Core) RekeyDeleteBackup() error {
|
||||||
|
c.stateLock.RLock()
|
||||||
|
defer c.stateLock.RUnlock()
|
||||||
|
if c.sealed {
|
||||||
|
return ErrSealed
|
||||||
|
}
|
||||||
|
if c.standby {
|
||||||
|
return ErrStandby
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.physical.Delete(coreUnsealKeysBackupPath)
|
||||||
|
}
|
||||||
|
|
||||||
// postUnseal is invoked after the barrier is unsealed, but before
|
// postUnseal is invoked after the barrier is unsealed, but before
|
||||||
// allowing any user operations. This allows us to setup any state that
|
// allowing any user operations. This allows us to setup any state that
|
||||||
// requires the Vault to be unsealed such as mount tables, logical backends,
|
// requires the Vault to be unsealed such as mount tables, logical backends,
|
||||||
|
|
|
@ -1781,7 +1781,7 @@ func TestCore_Rekey_Lifecycle(t *testing.T) {
|
||||||
c, master, _ := TestCoreUnsealed(t)
|
c, master, _ := TestCoreUnsealed(t)
|
||||||
|
|
||||||
// Verify update not allowed
|
// Verify update not allowed
|
||||||
if _, err := c.RekeyUpdate(master); err == nil {
|
if _, err := c.RekeyUpdate(master, ""); err == nil {
|
||||||
t.Fatalf("no rekey in progress")
|
t.Fatalf("no rekey in progress")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1824,6 +1824,7 @@ func TestCore_Rekey_Lifecycle(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
|
newConf.Nonce = conf.Nonce
|
||||||
if !reflect.DeepEqual(conf, newConf) {
|
if !reflect.DeepEqual(conf, newConf) {
|
||||||
t.Fatalf("bad: %v", conf)
|
t.Fatalf("bad: %v", conf)
|
||||||
}
|
}
|
||||||
|
@ -1887,8 +1888,17 @@ func TestCore_Rekey_Update(t *testing.T) {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch new config with generated nonce
|
||||||
|
rkconf, err := c.RekeyConfig()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
if rkconf == nil {
|
||||||
|
t.Fatalf("bad: no rekey config received")
|
||||||
|
}
|
||||||
|
|
||||||
// Provide the master
|
// Provide the master
|
||||||
result, err := c.RekeyUpdate(master)
|
result, err := c.RekeyUpdate(master, rkconf.Nonce)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1919,8 +1929,10 @@ func TestCore_Rekey_Update(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newConf.Nonce = rkconf.Nonce
|
||||||
if !reflect.DeepEqual(conf, newConf) {
|
if !reflect.DeepEqual(conf, newConf) {
|
||||||
t.Fatalf("bad: %#v", conf)
|
t.Fatalf("\nexpected: %#v\nactual: %#v\n", conf, newConf)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt unseal
|
// Attempt unseal
|
||||||
|
@ -1948,10 +1960,19 @@ func TestCore_Rekey_Update(t *testing.T) {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch new config with generated nonce
|
||||||
|
rkconf, err = c.RekeyConfig()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
if rkconf == nil {
|
||||||
|
t.Fatalf("bad: no rekey config received")
|
||||||
|
}
|
||||||
|
|
||||||
// Provide the parts master
|
// Provide the parts master
|
||||||
oldResult := result
|
oldResult := result
|
||||||
for i := 0; i < 3; i++ {
|
for i := 0; i < 3; i++ {
|
||||||
result, err = c.RekeyUpdate(oldResult.SecretShares[i])
|
result, err = c.RekeyUpdate(oldResult.SecretShares[i], rkconf.Nonce)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1987,6 +2008,7 @@ func TestCore_Rekey_Update(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
|
newConf.Nonce = rkconf.Nonce
|
||||||
if !reflect.DeepEqual(conf, newConf) {
|
if !reflect.DeepEqual(conf, newConf) {
|
||||||
t.Fatalf("bad: %#v", conf)
|
t.Fatalf("bad: %#v", conf)
|
||||||
}
|
}
|
||||||
|
@ -2005,9 +2027,38 @@ func TestCore_Rekey_InvalidMaster(t *testing.T) {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch new config with generated nonce
|
||||||
|
rkconf, err := c.RekeyConfig()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
if rkconf == nil {
|
||||||
|
t.Fatalf("bad: no rekey config received")
|
||||||
|
}
|
||||||
|
|
||||||
// Provide the master (invalid)
|
// Provide the master (invalid)
|
||||||
master[0]++
|
master[0]++
|
||||||
_, err = c.RekeyUpdate(master)
|
_, err = c.RekeyUpdate(master, rkconf.Nonce)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCore_Rekey_InvalidNonce(t *testing.T) {
|
||||||
|
c, master, _ := TestCoreUnsealed(t)
|
||||||
|
|
||||||
|
// Start a rekey
|
||||||
|
newConf := &SealConfig{
|
||||||
|
SecretThreshold: 3,
|
||||||
|
SecretShares: 5,
|
||||||
|
}
|
||||||
|
err := c.RekeyInit(newConf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide the nonce (invalid)
|
||||||
|
_, err = c.RekeyUpdate(master, "abcd")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("expected error")
|
t.Fatalf("expected error")
|
||||||
}
|
}
|
||||||
|
@ -2151,7 +2202,15 @@ func TestCore_Standby_Rekey(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
result, err := core.RekeyUpdate(key)
|
// Fetch new config with generated nonce
|
||||||
|
rkconf, err := core.RekeyConfig()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
if rkconf == nil {
|
||||||
|
t.Fatalf("bad: no rekey config received")
|
||||||
|
}
|
||||||
|
result, err := core.RekeyUpdate(key, rkconf.Nonce)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -2173,7 +2232,15 @@ func TestCore_Standby_Rekey(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
result, err = core2.RekeyUpdate(result.SecretShares[0])
|
// Fetch new config with generated nonce
|
||||||
|
rkconf, err = core2.RekeyConfig()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
if rkconf == nil {
|
||||||
|
t.Fatalf("bad: no rekey config received")
|
||||||
|
}
|
||||||
|
result, err = core2.RekeyUpdate(result.SecretShares[0], rkconf.Nonce)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,20 @@ func NewSystemBackend(core *Core, config *logical.BackendConfig) logical.Backend
|
||||||
},
|
},
|
||||||
|
|
||||||
Paths: []*framework.Path{
|
Paths: []*framework.Path{
|
||||||
|
&framework.Path{
|
||||||
|
Pattern: "rekey/backup$",
|
||||||
|
|
||||||
|
Fields: map[string]*framework.FieldSchema{},
|
||||||
|
|
||||||
|
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||||
|
logical.ReadOperation: b.handleRekeyRetrieve,
|
||||||
|
logical.DeleteOperation: b.handleRekeyDelete,
|
||||||
|
},
|
||||||
|
|
||||||
|
HelpSynopsis: strings.TrimSpace(sysHelp["mount_tune"][0]),
|
||||||
|
HelpDescription: strings.TrimSpace(sysHelp["mount_tune"][1]),
|
||||||
|
},
|
||||||
|
|
||||||
&framework.Path{
|
&framework.Path{
|
||||||
Pattern: "mounts/(?P<path>.+?)/tune$",
|
Pattern: "mounts/(?P<path>.+?)/tune$",
|
||||||
|
|
||||||
|
@ -383,6 +397,41 @@ type SystemBackend struct {
|
||||||
Backend *framework.Backend
|
Backend *framework.Backend
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleRekeyRetrieve returns backed-up, PGP-encrypted unseal keys from a
|
||||||
|
// rekey operation
|
||||||
|
func (b *SystemBackend) handleRekeyRetrieve(
|
||||||
|
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||||
|
backup, err := b.Core.RekeyRetrieveBackup()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to look up backed-up keys: %v", err)
|
||||||
|
}
|
||||||
|
if backup == nil {
|
||||||
|
return logical.ErrorResponse("no backed-up keys found"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format the status
|
||||||
|
resp := &logical.Response{
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"nonce": backup.Nonce,
|
||||||
|
"keys": backup.Keys,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleRekeyDelete deletes backed-up, PGP-encrypted unseal keys from a rekey
|
||||||
|
// operation
|
||||||
|
func (b *SystemBackend) handleRekeyDelete(
|
||||||
|
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||||
|
err := b.Core.RekeyDeleteBackup()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error during deletion of backed-up keys: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
// handleMountTable handles the "mounts" endpoint to provide the mount table
|
// handleMountTable handles the "mounts" endpoint to provide the mount table
|
||||||
func (b *SystemBackend) handleMountTable(
|
func (b *SystemBackend) handleMountTable(
|
||||||
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||||
|
|
|
@ -29,18 +29,24 @@ description: |-
|
||||||
|
|
||||||
<dt>Returns</dt>
|
<dt>Returns</dt>
|
||||||
<dd>
|
<dd>
|
||||||
If a rekey is started, then "n" is the new shares to generate and "t" is
|
If a rekey is started, then `n` is the new shares to generate and `t` is
|
||||||
the threshold required for the new shares. The "progress" is how many unseal
|
the threshold required for the new shares. `progress` is how many unseal
|
||||||
keys have been provided for this rekey, where "required" must be reached to
|
keys have been provided for this rekey, where `required` must be reached to
|
||||||
complete.
|
complete. The `nonce` for the current rekey operation is also displayed. If
|
||||||
|
PGP keys are being used to encrypt the final shares, the key fingerprints
|
||||||
|
and whether the final keys will be backed up to physical storage will also
|
||||||
|
be displayed.
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
{
|
{
|
||||||
"started": true,
|
"started": true,
|
||||||
|
"nonce": "2dbd10f1-8528-6246-09e7-82b25b8aba63",
|
||||||
"t": 3,
|
"t": 3,
|
||||||
"n": 5,
|
"n": 5,
|
||||||
"progress": 1,
|
"progress": 1,
|
||||||
"required": 3
|
"required": 3,
|
||||||
|
"pgp_fingerprints": ["abcd1234"],
|
||||||
|
"backup": true
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -53,8 +59,8 @@ description: |-
|
||||||
<dt>Description</dt>
|
<dt>Description</dt>
|
||||||
<dd>
|
<dd>
|
||||||
Initializes a new rekey attempt. Only a single rekey attempt can take place
|
Initializes a new rekey attempt. Only a single rekey attempt can take place
|
||||||
at a time, and changing the parameters of a rekey requires canceling and starting
|
at a time, and changing the parameters of a rekey requires canceling and
|
||||||
a new rekey.
|
starting a new rekey, which will also provide a new nonce.
|
||||||
</dd>
|
</dd>
|
||||||
|
|
||||||
<dt>Method</dt>
|
<dt>Method</dt>
|
||||||
|
@ -85,6 +91,14 @@ description: |-
|
||||||
original binary representation. The size of this array must be the
|
original binary representation. The size of this array must be the
|
||||||
same as <code>secret_shares</code>.
|
same as <code>secret_shares</code>.
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<spam class="param">backup</span>
|
||||||
|
<span class="param-flags">optional</spam>
|
||||||
|
If using PGP-encrypted keys, whether Vault should also back them up to
|
||||||
|
a well-known location in physical storage (`core/unseal-keys-backup`).
|
||||||
|
These can then be retrieved and removed via the `sys/rekey/backup`
|
||||||
|
endpoint.
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</dd>
|
</dd>
|
||||||
|
|
||||||
|
@ -117,6 +131,67 @@ description: |-
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
|
# /sys/rekey/backup
|
||||||
|
|
||||||
|
## GET
|
||||||
|
|
||||||
|
<dl>
|
||||||
|
<dt>Description</dt>
|
||||||
|
<dd>
|
||||||
|
Return the backup copy of PGP-encrypted unseal keys. The returned value is
|
||||||
|
the nonce of the rekey operation and a map of PGP key fingerprint to
|
||||||
|
hex-encoded PGP-encrypted key.
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt>Method</dt>
|
||||||
|
<dd>GET</dd>
|
||||||
|
|
||||||
|
<dt>URL</dt>
|
||||||
|
<dd>`/sys/rekey/backup`</dd>
|
||||||
|
|
||||||
|
<dt>Parameters</dt>
|
||||||
|
<dd>
|
||||||
|
None
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt>Returns</dt>
|
||||||
|
<dd>
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"nonce": "2dbd10f1-8528-6246-09e7-82b25b8aba63",
|
||||||
|
"keys": {
|
||||||
|
"abcd1234": "..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
## DELETE
|
||||||
|
|
||||||
|
<dl>
|
||||||
|
<dt>Description</dt>
|
||||||
|
<dd>
|
||||||
|
Delete the backup copy of PGP-encrypted unseal keys.
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt>Method</dt>
|
||||||
|
<dd>DELETE</dd>
|
||||||
|
|
||||||
|
<dt>URL</dt>
|
||||||
|
<dd>`/sys/rekey/backup`</dd>
|
||||||
|
|
||||||
|
<dt>Parameters</dt>
|
||||||
|
<dd>None
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt>Returns</dt>
|
||||||
|
<dd>`204` response code.
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
# /sys/rekey/update
|
# /sys/rekey/update
|
||||||
|
|
||||||
## PUT
|
## PUT
|
||||||
|
@ -127,7 +202,8 @@ description: |-
|
||||||
Enter a single master key share to progress the rekey of the Vault.
|
Enter a single master key share to progress the rekey of the Vault.
|
||||||
If the threshold number of master key shares is reached, Vault
|
If the threshold number of master key shares is reached, Vault
|
||||||
will complete the rekey. Otherwise, this API must be called multiple
|
will complete the rekey. Otherwise, this API must be called multiple
|
||||||
times until that threshold is met.
|
times until that threshold is met. The rekey nonce operation must be
|
||||||
|
provided with each call.
|
||||||
</dd>
|
</dd>
|
||||||
|
|
||||||
<dt>Method</dt>
|
<dt>Method</dt>
|
||||||
|
@ -144,18 +220,29 @@ description: |-
|
||||||
<span class="param-flags">required</span>
|
<span class="param-flags">required</span>
|
||||||
A single master share key.
|
A single master share key.
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="param">nonce</span>
|
||||||
|
<span class="param-flags">required</span>
|
||||||
|
The nonce of the rekey operation.
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</dd>
|
</dd>
|
||||||
|
|
||||||
<dt>Returns</dt>
|
<dt>Returns</dt>
|
||||||
<dd>
|
<dd>
|
||||||
A JSON-encoded object indicating completion and if so with the (possibly
|
A JSON-encoded object indicating the rekey operation nonce and completion
|
||||||
encrypted, if <code>pgp_keys</code> was provided) new master keys:
|
status; if completed, the new master keys are returned. If the keys are
|
||||||
|
PGP-encrypted, an array of key fingerprints will also be provided (with the
|
||||||
|
order in which the keys were used for encryption) along with whether or not
|
||||||
|
the keys were backed up to physical storage:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
{
|
{
|
||||||
"complete": true,
|
"complete": true,
|
||||||
"keys": ["one", "two", "three"]
|
"keys": ["one", "two", "three"],
|
||||||
|
"nonce": "2dbd10f1-8528-6246-09e7-82b25b8aba63",
|
||||||
|
"pgp_fingerprints": ["abcd1234"],
|
||||||
|
"backup": true
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue