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
|
||||
}
|
||||
|
||||
func (c *Sys) RekeyUpdate(shard string) (*RekeyUpdateResponse, error) {
|
||||
body := map[string]interface{}{"key": shard}
|
||||
func (c *Sys) RekeyUpdate(shard, nonce string) (*RekeyUpdateResponse, error) {
|
||||
body := map[string]interface{}{
|
||||
"key": shard,
|
||||
"nonce": nonce,
|
||||
}
|
||||
|
||||
r := c.c.NewRequest("PUT", "/v1/sys/rekey/update")
|
||||
if err := r.SetJSONBody(body); err != nil {
|
||||
|
@ -54,21 +57,56 @@ func (c *Sys) RekeyUpdate(shard string) (*RekeyUpdateResponse, error) {
|
|||
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 {
|
||||
SecretShares int `json:"secret_shares"`
|
||||
SecretThreshold int `json:"secret_threshold"`
|
||||
PGPKeys []string `json:"pgp_keys"`
|
||||
Backup bool
|
||||
}
|
||||
|
||||
type RekeyStatusResponse struct {
|
||||
Started bool
|
||||
T int
|
||||
N int
|
||||
Progress int
|
||||
Required int
|
||||
Nonce string
|
||||
Started bool
|
||||
T int
|
||||
N int
|
||||
Progress int
|
||||
Required int
|
||||
PGPFingerprints []string `json:"pgp_fingerprints"`
|
||||
Backup bool
|
||||
}
|
||||
|
||||
type RekeyUpdateResponse struct {
|
||||
Complete bool
|
||||
Keys []string
|
||||
Nonce 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"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/vault/helper/pgpkeys"
|
||||
"github.com/hashicorp/vault/http"
|
||||
"github.com/hashicorp/vault/vault"
|
||||
"github.com/mitchellh/cli"
|
||||
|
@ -171,9 +172,19 @@ func TestInit_PGP(t *testing.T) {
|
|||
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{
|
||||
SecretShares: 3,
|
||||
SecretThreshold: 2,
|
||||
PGPKeys: pgpKeys,
|
||||
}
|
||||
if !reflect.DeepEqual(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]
|
||||
|
||||
parseDecryptAndTestUnsealKeys(t, ui.OutputWriter.String(), rootToken, core)
|
||||
parseDecryptAndTestUnsealKeys(t, ui.OutputWriter.String(), rootToken, false, nil, core)
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"io/ioutil"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
|
@ -54,7 +55,11 @@ func getPubKeyFiles(t *testing.T) (string, []string, error) {
|
|||
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
|
||||
priv1Bytes, err := decoder.DecodeString(privKey1)
|
||||
if err != nil {
|
||||
|
@ -75,7 +80,12 @@ func parseDecryptAndTestUnsealKeys(t *testing.T, input, rootToken string, core *
|
|||
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 {
|
||||
t.Fatalf("Error compiling regex: %s", err)
|
||||
}
|
||||
|
@ -85,11 +95,30 @@ func parseDecryptAndTestUnsealKeys(t *testing.T, input, rootToken string, core *
|
|||
}
|
||||
|
||||
encodedKeys := []string{}
|
||||
for _, pair := range matches {
|
||||
if len(pair) != 2 {
|
||||
t.Fatalf("Key not found: %#v", pair)
|
||||
matchedFingerprints := []string{}
|
||||
for _, tuple := range matches {
|
||||
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{}
|
||||
|
|
139
command/rekey.go
139
command/rekey.go
|
@ -5,6 +5,7 @@ import (
|
|||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/structs"
|
||||
"github.com/hashicorp/vault/api"
|
||||
"github.com/hashicorp/vault/helper/password"
|
||||
"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
|
||||
// be asked with the `password` helper.
|
||||
Key string
|
||||
|
||||
// The nonce for the rekey request to send along
|
||||
Nonce string
|
||||
}
|
||||
|
||||
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 nonce string
|
||||
var pgpKeys pgpkeys.PubKeyFilesFlag
|
||||
flags := c.Meta.FlagSet("rekey", FlagSetDefault)
|
||||
flags.BoolVar(&init, "init", false, "")
|
||||
flags.BoolVar(&cancel, "cancel", 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(&threshold, "key-threshold", 3, "")
|
||||
flags.StringVar(&nonce, "nonce", "", "")
|
||||
flags.Var(&pgpKeys, "pgp-keys", "")
|
||||
flags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
if nonce != "" {
|
||||
c.Nonce = nonce
|
||||
}
|
||||
|
||||
client, err := c.Client()
|
||||
if err != nil {
|
||||
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
|
||||
if init {
|
||||
return c.initRekey(client, shares, threshold, pgpKeys)
|
||||
} else if cancel {
|
||||
switch {
|
||||
case init:
|
||||
return c.initRekey(client, shares, threshold, pgpKeys, backup)
|
||||
case cancel:
|
||||
return c.cancelRekey(client)
|
||||
} else if status {
|
||||
case status:
|
||||
return c.rekeyStatus(client)
|
||||
case retrieve:
|
||||
return c.rekeyRetrieveStored(client)
|
||||
case delete:
|
||||
return c.rekeyDeleteStored(client)
|
||||
}
|
||||
|
||||
// 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))
|
||||
return 1
|
||||
}
|
||||
} else {
|
||||
shares = rekeyStatus.N
|
||||
threshold = rekeyStatus.T
|
||||
c.Ui.Output(fmt.Sprintf(
|
||||
"Rekey already in progress\n"+
|
||||
"Key Shares: %d\n"+
|
||||
"Key Threshold: %d\n",
|
||||
shares,
|
||||
threshold,
|
||||
))
|
||||
rekeyStatus, err = client.Sys().RekeyStatus()
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error reading rekey status: %s", err))
|
||||
return 1
|
||||
}
|
||||
c.Nonce = rekeyStatus.Nonce
|
||||
}
|
||||
|
||||
shares = rekeyStatus.N
|
||||
threshold = rekeyStatus.T
|
||||
serverNonce := rekeyStatus.Nonce
|
||||
|
||||
// Get the unseal key
|
||||
args = flags.Args()
|
||||
value := c.Key
|
||||
key := c.Key
|
||||
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): ")
|
||||
value, err = password.Read(os.Stdin)
|
||||
key, err = password.Read(os.Stdin)
|
||||
fmt.Printf("\n")
|
||||
if err != nil {
|
||||
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
|
||||
result, err := client.Sys().RekeyUpdate(strings.TrimSpace(value))
|
||||
result, err := client.Sys().RekeyUpdate(strings.TrimSpace(key), c.Nonce)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error attempting rekey update: %s", err))
|
||||
return 1
|
||||
|
@ -119,7 +139,22 @@ func (c *RekeyCommand) Run(args []string) int {
|
|||
|
||||
// Provide the 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(
|
||||
|
@ -140,12 +175,16 @@ func (c *RekeyCommand) Run(args []string) int {
|
|||
}
|
||||
|
||||
// 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
|
||||
err := client.Sys().RekeyInit(&api.RekeyInitRequest{
|
||||
SecretShares: shares,
|
||||
SecretThreshold: threshold,
|
||||
PGPKeys: pgpKeys,
|
||||
Backup: backup,
|
||||
})
|
||||
if err != nil {
|
||||
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
|
||||
c.Ui.Output(fmt.Sprintf(
|
||||
"Started: %v\n"+
|
||||
statString := fmt.Sprintf(
|
||||
"Nonce: %s\n"+
|
||||
"Started: %v\n"+
|
||||
"Key Shares: %d\n"+
|
||||
"Key Threshold: %d\n"+
|
||||
"Rekey Progress: %d\n"+
|
||||
"Required Keys: %d",
|
||||
status.Nonce,
|
||||
status.Started,
|
||||
status.N,
|
||||
status.T,
|
||||
status.Progress,
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -212,7 +282,7 @@ General Options:
|
|||
|
||||
` + generalOptionsUsage() + `
|
||||
|
||||
Unseal Options:
|
||||
Rekey Options:
|
||||
|
||||
-init Initialize the rekey operation by setting the desired
|
||||
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
|
||||
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
|
||||
into.
|
||||
|
||||
-key-threshold=3 The number of key shares required to reconstruct
|
||||
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
|
||||
files on disk containing binary- or base64-format
|
||||
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
|
||||
unseal' command, you will need to hex decode and
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -5,8 +5,10 @@ import (
|
|||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/vault/http"
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/vault"
|
||||
"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 {
|
||||
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)
|
||||
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)
|
||||
c := &RekeyCommand{
|
||||
Key: hex.EncodeToString(key),
|
||||
|
@ -179,6 +195,7 @@ func TestRekey_init_pgp(t *testing.T) {
|
|||
"-key-shares", "3",
|
||||
"-pgp-keys", pubFiles[0] + ",@" + pubFiles[1] + "," + pubFiles[2],
|
||||
"-key-threshold", "2",
|
||||
"-backup", "true",
|
||||
}
|
||||
|
||||
if code := c.Run(args); code != 0 {
|
||||
|
@ -196,6 +213,8 @@ func TestRekey_init_pgp(t *testing.T) {
|
|||
t.Fatal("should rekey")
|
||||
}
|
||||
|
||||
c.Nonce = config.Nonce
|
||||
|
||||
args = []string{
|
||||
"-address", addr,
|
||||
}
|
||||
|
@ -203,5 +222,45 @@ func TestRekey_init_pgp(t *testing.T) {
|
|||
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
|
||||
// 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) {
|
||||
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{}
|
||||
for i, keystring := range pgpKeys {
|
||||
encryptedShares := make([][]byte, 0, len(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)
|
||||
if err != nil {
|
||||
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 {
|
||||
return nil, fmt.Errorf("Error parsing given PGP key: %s", err)
|
||||
}
|
||||
ctBuf := bytes.NewBuffer(nil)
|
||||
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())
|
||||
ret = append(ret, entity)
|
||||
}
|
||||
return encryptedShares, nil
|
||||
return ret, nil
|
||||
}
|
||||
|
|
|
@ -40,26 +40,36 @@ func (p *PubKeyFilesFlag) Set(value string) error {
|
|||
*p = append(*p, key)
|
||||
continue
|
||||
}
|
||||
if keyfile[0] == '@' {
|
||||
keyfile = keyfile[1:]
|
||||
}
|
||||
f, err := os.Open(keyfile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
buf := bytes.NewBuffer(nil)
|
||||
_, err = buf.ReadFrom(f)
|
||||
|
||||
pgpStr, err := ReadPGPFile(keyfile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = base64.StdEncoding.DecodeString(buf.String())
|
||||
if err == nil {
|
||||
*p = append(*p, buf.String())
|
||||
} else {
|
||||
*p = append(*p, base64.StdEncoding.EncodeToString(buf.Bytes()))
|
||||
}
|
||||
*p = append(*p, pgpStr)
|
||||
}
|
||||
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/key-status", proxySysRequest(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/", handleLogical(core, false))
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/hashicorp/vault/helper/pgpkeys"
|
||||
"github.com/hashicorp/vault/vault"
|
||||
)
|
||||
|
||||
|
@ -60,9 +61,18 @@ func handleSysRekeyInitGet(core *vault.Core, w http.ResponseWriter, r *http.Requ
|
|||
Required: sealConfig.SecretThreshold,
|
||||
}
|
||||
if rekeyConf != nil {
|
||||
status.Nonce = rekeyConf.Nonce
|
||||
status.Started = true
|
||||
status.T = rekeyConf.SecretThreshold
|
||||
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)
|
||||
}
|
||||
|
@ -75,11 +85,16 @@ func handleSysRekeyInitPut(core *vault.Core, w http.ResponseWriter, r *http.Requ
|
|||
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
|
||||
err := core.RekeyInit(&vault.SealConfig{
|
||||
SecretShares: req.SecretShares,
|
||||
SecretThreshold: req.SecretThreshold,
|
||||
PGPKeys: req.PGPKeys,
|
||||
Backup: req.Backup,
|
||||
})
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, err)
|
||||
|
@ -127,7 +142,7 @@ func handleSysRekeyUpdate(core *vault.Core) http.Handler {
|
|||
}
|
||||
|
||||
// Use the key to make progress on rekey
|
||||
result, err := core.RekeyUpdate(key)
|
||||
result, err := core.RekeyUpdate(key, req.Nonce)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
|
@ -137,6 +152,7 @@ func handleSysRekeyUpdate(core *vault.Core) http.Handler {
|
|||
resp := &RekeyUpdateResponse{}
|
||||
if result != nil {
|
||||
resp.Complete = true
|
||||
resp.Nonce = req.Nonce
|
||||
|
||||
// Encode the keys
|
||||
keys := make([]string, 0, len(result.SecretShares))
|
||||
|
@ -144,6 +160,9 @@ func handleSysRekeyUpdate(core *vault.Core) http.Handler {
|
|||
keys = append(keys, hex.EncodeToString(k))
|
||||
}
|
||||
resp.Keys = keys
|
||||
|
||||
resp.Backup = result.Backup
|
||||
resp.PGPFingerprints = result.PGPFingerprints
|
||||
}
|
||||
respondOk(w, resp)
|
||||
})
|
||||
|
@ -153,21 +172,29 @@ type RekeyRequest struct {
|
|||
SecretShares int `json:"secret_shares"`
|
||||
SecretThreshold int `json:"secret_threshold"`
|
||||
PGPKeys []string `json:"pgp_keys"`
|
||||
Backup bool `json:"backup"`
|
||||
}
|
||||
|
||||
type RekeyStatusResponse struct {
|
||||
Started bool `json:"started"`
|
||||
T int `json:"t"`
|
||||
N int `json:"n"`
|
||||
Progress int `json:"progress"`
|
||||
Required int `json:"required"`
|
||||
Nonce string `json:"nonce"`
|
||||
Started bool `json:"started"`
|
||||
T int `json:"t"`
|
||||
N int `json:"n"`
|
||||
Progress int `json:"progress"`
|
||||
Required int `json:"required"`
|
||||
PGPFingerprints []string `json:"pgp_fingerprints"`
|
||||
Backup bool `json:"backup"`
|
||||
}
|
||||
|
||||
type RekeyUpdateRequest struct {
|
||||
Key string
|
||||
Nonce string
|
||||
Key string
|
||||
}
|
||||
|
||||
type RekeyUpdateResponse struct {
|
||||
Complete bool `json:"complete"`
|
||||
Keys []string `json:"keys"`
|
||||
Nonce string `json:"nonce"`
|
||||
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{}
|
||||
expected := map[string]interface{}{
|
||||
"started": false,
|
||||
"t": float64(0),
|
||||
"n": float64(0),
|
||||
"progress": float64(0),
|
||||
"required": float64(1),
|
||||
"started": false,
|
||||
"t": float64(0),
|
||||
"n": float64(0),
|
||||
"progress": float64(0),
|
||||
"required": float64(1),
|
||||
"pgp_fingerprints": interface{}(nil),
|
||||
"backup": false,
|
||||
}
|
||||
testResponseStatus(t, resp, 200)
|
||||
testResponseBody(t, resp, &actual)
|
||||
expected["nonce"] = actual["nonce"]
|
||||
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{}
|
||||
expected := map[string]interface{}{
|
||||
"started": true,
|
||||
"t": float64(3),
|
||||
"n": float64(5),
|
||||
"progress": float64(0),
|
||||
"required": float64(1),
|
||||
"started": true,
|
||||
"t": float64(3),
|
||||
"n": float64(5),
|
||||
"progress": float64(0),
|
||||
"required": float64(1),
|
||||
"pgp_fingerprints": interface{}(nil),
|
||||
"backup": false,
|
||||
}
|
||||
testResponseStatus(t, resp, 200)
|
||||
testResponseBody(t, resp, &actual)
|
||||
expected["nonce"] = actual["nonce"]
|
||||
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{}
|
||||
expected := map[string]interface{}{
|
||||
"started": false,
|
||||
"t": float64(0),
|
||||
"n": float64(0),
|
||||
"progress": float64(0),
|
||||
"required": float64(1),
|
||||
"started": false,
|
||||
"t": float64(0),
|
||||
"n": float64(0),
|
||||
"progress": float64(0),
|
||||
"required": float64(1),
|
||||
"pgp_fingerprints": interface{}(nil),
|
||||
"backup": false,
|
||||
}
|
||||
testResponseStatus(t, resp, 200)
|
||||
testResponseBody(t, resp, &actual)
|
||||
expected["nonce"] = actual["nonce"]
|
||||
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)
|
||||
|
||||
// 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{}{
|
||||
"key": hex.EncodeToString(master),
|
||||
"nonce": rekeyStatus["nonce"].(string),
|
||||
"key": hex.EncodeToString(master),
|
||||
})
|
||||
|
||||
var actual 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)
|
||||
testResponseBody(t, resp, &actual)
|
||||
|
@ -141,6 +163,34 @@ func TestSysRekey_Update(t *testing.T) {
|
|||
|
||||
delete(actual, "keys")
|
||||
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 (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
@ -43,6 +44,11 @@ const (
|
|||
// the currently elected 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
|
||||
// HA lock if an error is encountered
|
||||
lockRetryInterval = 10 * time.Second
|
||||
|
@ -96,11 +102,21 @@ type SealConfig struct {
|
|||
// if requested, to encrypt the output unseal tokens. If
|
||||
// provided, it sets the value of SecretShares. Ordering
|
||||
// is important.
|
||||
PGPKeys []string `json:"-"`
|
||||
PGPKeys []string `json:"pgp_keys"`
|
||||
|
||||
// SecretThreshold is the number of parts required
|
||||
// to open the vault. This is the T value of Shamir
|
||||
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
|
||||
|
@ -151,7 +167,15 @@ type InitResult struct {
|
|||
// RekeyResult is used to provide the key parts back after
|
||||
// they are generated as part of the rekey.
|
||||
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
|
||||
|
@ -821,7 +845,7 @@ func (c *Core) Initialize(config *SealConfig) (*InitResult, error) {
|
|||
}
|
||||
|
||||
if len(config.PGPKeys) > 0 {
|
||||
encryptedShares, err := pgpkeys.EncryptShares(results.SecretShares, config.PGPKeys)
|
||||
_, encryptedShares, err := pgpkeys.EncryptShares(results.SecretShares, config.PGPKeys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -1208,13 +1232,16 @@ func (c *Core) RekeyInit(config *SealConfig) error {
|
|||
// Copy the configuration
|
||||
c.rekeyConfig = new(SealConfig)
|
||||
*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
|
||||
}
|
||||
|
||||
// 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
|
||||
min, max := c.barrier.KeyLength()
|
||||
max += shamir.ShareOverhead
|
||||
|
@ -1254,6 +1281,10 @@ func (c *Core) RekeyUpdate(key []byte) (*RekeyResult, error) {
|
|||
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
|
||||
for _, existing := range c.rekeyProgress {
|
||||
if bytes.Equal(existing, key) {
|
||||
|
@ -1311,12 +1342,21 @@ func (c *Core) RekeyUpdate(key []byte) (*RekeyResult, error) {
|
|||
results.SecretShares = shares
|
||||
}
|
||||
|
||||
backupInfo := map[string]string{}
|
||||
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 {
|
||||
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.PGPFingerprints = fingerprints
|
||||
results.Backup = c.rekeyConfig.Backup
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
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!
|
||||
c.rekeyProgress = nil
|
||||
c.rekeyConfig = nil
|
||||
|
@ -1366,6 +1434,49 @@ func (c *Core) RekeyCancel() error {
|
|||
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
|
||||
// allowing any user operations. This allows us to setup any state that
|
||||
// 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)
|
||||
|
||||
// Verify update not allowed
|
||||
if _, err := c.RekeyUpdate(master); err == nil {
|
||||
if _, err := c.RekeyUpdate(master, ""); err == nil {
|
||||
t.Fatalf("no rekey in progress")
|
||||
}
|
||||
|
||||
|
@ -1824,6 +1824,7 @@ func TestCore_Rekey_Lifecycle(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
newConf.Nonce = conf.Nonce
|
||||
if !reflect.DeepEqual(conf, newConf) {
|
||||
t.Fatalf("bad: %v", conf)
|
||||
}
|
||||
|
@ -1887,8 +1888,17 @@ func TestCore_Rekey_Update(t *testing.T) {
|
|||
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
|
||||
result, err := c.RekeyUpdate(master)
|
||||
result, err := c.RekeyUpdate(master, rkconf.Nonce)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
@ -1919,8 +1929,10 @@ func TestCore_Rekey_Update(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
newConf.Nonce = rkconf.Nonce
|
||||
if !reflect.DeepEqual(conf, newConf) {
|
||||
t.Fatalf("bad: %#v", conf)
|
||||
t.Fatalf("\nexpected: %#v\nactual: %#v\n", conf, newConf)
|
||||
}
|
||||
|
||||
// Attempt unseal
|
||||
|
@ -1948,10 +1960,19 @@ func TestCore_Rekey_Update(t *testing.T) {
|
|||
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
|
||||
oldResult := result
|
||||
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 {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
@ -1987,6 +2008,7 @@ func TestCore_Rekey_Update(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
newConf.Nonce = rkconf.Nonce
|
||||
if !reflect.DeepEqual(conf, newConf) {
|
||||
t.Fatalf("bad: %#v", conf)
|
||||
}
|
||||
|
@ -2005,9 +2027,38 @@ func TestCore_Rekey_InvalidMaster(t *testing.T) {
|
|||
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)
|
||||
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 {
|
||||
t.Fatalf("expected error")
|
||||
}
|
||||
|
@ -2151,7 +2202,15 @@ func TestCore_Standby_Rekey(t *testing.T) {
|
|||
if err != nil {
|
||||
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 {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
@ -2173,7 +2232,15 @@ func TestCore_Standby_Rekey(t *testing.T) {
|
|||
if err != nil {
|
||||
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 {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
|
|
@ -41,6 +41,20 @@ func NewSystemBackend(core *Core, config *logical.BackendConfig) logical.Backend
|
|||
},
|
||||
|
||||
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{
|
||||
Pattern: "mounts/(?P<path>.+?)/tune$",
|
||||
|
||||
|
@ -383,6 +397,41 @@ type SystemBackend struct {
|
|||
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
|
||||
func (b *SystemBackend) handleMountTable(
|
||||
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
|
|
|
@ -29,18 +29,24 @@ description: |-
|
|||
|
||||
<dt>Returns</dt>
|
||||
<dd>
|
||||
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
|
||||
keys have been provided for this rekey, where "required" must be reached to
|
||||
complete.
|
||||
If a rekey is started, then `n` is the new shares to generate and `t` is
|
||||
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
|
||||
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
|
||||
{
|
||||
"started": true,
|
||||
"nonce": "2dbd10f1-8528-6246-09e7-82b25b8aba63",
|
||||
"t": 3,
|
||||
"n": 5,
|
||||
"progress": 1,
|
||||
"required": 3
|
||||
"required": 3,
|
||||
"pgp_fingerprints": ["abcd1234"],
|
||||
"backup": true
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -53,8 +59,8 @@ description: |-
|
|||
<dt>Description</dt>
|
||||
<dd>
|
||||
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
|
||||
a new rekey.
|
||||
at a time, and changing the parameters of a rekey requires canceling and
|
||||
starting a new rekey, which will also provide a new nonce.
|
||||
</dd>
|
||||
|
||||
<dt>Method</dt>
|
||||
|
@ -85,6 +91,14 @@ description: |-
|
|||
original binary representation. The size of this array must be the
|
||||
same as <code>secret_shares</code>.
|
||||
</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>
|
||||
</dd>
|
||||
|
||||
|
@ -117,6 +131,67 @@ description: |-
|
|||
</dd>
|
||||
</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
|
||||
|
||||
## PUT
|
||||
|
@ -127,7 +202,8 @@ description: |-
|
|||
Enter a single master key share to progress the rekey of the Vault.
|
||||
If the threshold number of master key shares is reached, Vault
|
||||
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>
|
||||
|
||||
<dt>Method</dt>
|
||||
|
@ -144,18 +220,29 @@ description: |-
|
|||
<span class="param-flags">required</span>
|
||||
A single master share key.
|
||||
</li>
|
||||
<li>
|
||||
<span class="param">nonce</span>
|
||||
<span class="param-flags">required</span>
|
||||
The nonce of the rekey operation.
|
||||
</li>
|
||||
</ul>
|
||||
</dd>
|
||||
|
||||
<dt>Returns</dt>
|
||||
<dd>
|
||||
A JSON-encoded object indicating completion and if so with the (possibly
|
||||
encrypted, if <code>pgp_keys</code> was provided) new master keys:
|
||||
A JSON-encoded object indicating the rekey operation nonce and completion
|
||||
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
|
||||
{
|
||||
"complete": true,
|
||||
"keys": ["one", "two", "three"]
|
||||
"keys": ["one", "two", "three"],
|
||||
"nonce": "2dbd10f1-8528-6246-09e7-82b25b8aba63",
|
||||
"pgp_fingerprints": ["abcd1234"],
|
||||
"backup": true
|
||||
}
|
||||
```
|
||||
|
||||
|
|
Loading…
Reference in a new issue