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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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