Merge pull request #907 from hashicorp/rekey-work

Add rekey nonce/backup.
This commit is contained in:
Jeff Mitchell 2016-01-06 09:55:19 -05:00
commit 20a6f37b38
14 changed files with 791 additions and 131 deletions

View File

@ -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
} }

View File

@ -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)
} }

View File

@ -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{}

View File

@ -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)
} }

View File

@ -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)
} }

View File

@ -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
} }

View File

@ -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
}

View File

@ -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))

View File

@ -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"`
} }

View File

@ -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)
}

View File

@ -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,

View File

@ -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)
} }

View File

@ -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) {

View File

@ -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
} }
``` ```