Add the ability to generate root tokens via unseal keys.

This commit is contained in:
Jeff Mitchell 2016-01-08 21:21:02 -05:00
parent 1692471445
commit 3b994dbc7f
20 changed files with 2480 additions and 358 deletions

View File

@ -0,0 +1,73 @@
package api
func (c *Sys) RootGenerationStatus() (*RootGenerationStatusResponse, error) {
r := c.c.NewRequest("GET", "/v1/sys/root-generation/attempt")
resp, err := c.c.RawRequest(r)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var result RootGenerationStatusResponse
err = resp.DecodeJSON(&result)
return &result, err
}
func (c *Sys) RootGenerationInit(otp, pgpKey string) error {
body := map[string]interface{}{
"otp": otp,
"pgp_key": pgpKey,
}
r := c.c.NewRequest("PUT", "/v1/sys/root-generation/attempt")
if err := r.SetJSONBody(body); err != nil {
return err
}
resp, err := c.c.RawRequest(r)
if err == nil {
defer resp.Body.Close()
}
return err
}
func (c *Sys) RootGenerationCancel() error {
r := c.c.NewRequest("DELETE", "/v1/sys/root-generation/attempt")
resp, err := c.c.RawRequest(r)
if err == nil {
defer resp.Body.Close()
}
return err
}
func (c *Sys) RootGenerationUpdate(shard, nonce string) (*RootGenerationStatusResponse, error) {
body := map[string]interface{}{
"key": shard,
"nonce": nonce,
}
r := c.c.NewRequest("PUT", "/v1/sys/root-generation/update")
if err := r.SetJSONBody(body); err != nil {
return nil, err
}
resp, err := c.c.RawRequest(r)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var result RootGenerationStatusResponse
err = resp.DecodeJSON(&result)
return &result, err
}
type RootGenerationStatusResponse struct {
Nonce string
Started bool
Progress int
Required int
Complete bool
EncodedRootToken string `json:"encoded_root_token"`
PGPFingerprint string `json:"pgp_fingerprint"`
}

View File

@ -182,6 +182,12 @@ func Commands(metaPtr *command.Meta) map[string]cli.CommandFactory {
}, nil
},
"generate-root": func() (cli.Command, error) {
return &command.GenerateRootCommand{
Meta: meta,
}, nil
},
"renew": func() (cli.Command, error) {
return &command.RenewCommand{
Meta: meta,

348
command/generate-root.go Normal file
View File

@ -0,0 +1,348 @@
package command
import (
"crypto/rand"
"encoding/base64"
"fmt"
"os"
"strings"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/helper/password"
"github.com/hashicorp/vault/helper/pgpkeys"
"github.com/hashicorp/vault/helper/xor"
)
// GenerateRootCommand is a Command that generates a new root token.
type GenerateRootCommand struct {
Meta
// 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 *GenerateRootCommand) Run(args []string) int {
var init, cancel, status, genotp bool
var nonce, decode, otp, pgpKey string
var pgpKeyArr pgpkeys.PubKeyFilesFlag
flags := c.Meta.FlagSet("generate-root", FlagSetDefault)
flags.BoolVar(&init, "init", false, "")
flags.BoolVar(&cancel, "cancel", false, "")
flags.BoolVar(&status, "status", false, "")
flags.BoolVar(&genotp, "genotp", false, "")
flags.StringVar(&decode, "decode", "", "")
flags.StringVar(&otp, "otp", "", "")
flags.StringVar(&nonce, "nonce", "", "")
flags.Var(&pgpKeyArr, "pgp-key", "")
flags.Usage = func() { c.Ui.Error(c.Help()) }
if err := flags.Parse(args); err != nil {
return 1
}
if genotp {
buf := make([]byte, 16)
readLen, err := rand.Read(buf)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error reading random bytes: %s", err))
return 1
}
if readLen != 16 {
c.Ui.Error(fmt.Sprintf("Read %d bytes when we should have read 16", readLen))
return 1
}
c.Ui.Output(fmt.Sprintf("OTP: %s", base64.StdEncoding.EncodeToString(buf)))
return 0
}
if len(decode) > 0 {
if len(otp) == 0 {
c.Ui.Error("Both the value to decode and the OTP must be passed in")
return 1
}
return c.decode(decode, otp)
}
client, err := c.Client()
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error initializing client: %s", err))
return 2
}
// Check if the root generation is started
rootGenerationStatus, err := client.Sys().RootGenerationStatus()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error reading root generation status: %s", err))
return 1
}
// If we are initing, or if we are not started but are not running a
// special function, check otp and pgpkey
if init ||
(!init && !cancel && !status && !genotp && len(decode) == 0 && !rootGenerationStatus.Started) {
switch {
case len(otp) == 0 && (pgpKeyArr == nil || len(pgpKeyArr) == 0):
c.Ui.Error("-otp or -pgp-key must be specified")
return 1
case len(otp) != 0 && pgpKeyArr != nil && len(pgpKeyArr) != 0:
c.Ui.Error("Only one of -otp or -pgp-key must be specified")
return 1
case len(otp) != 0:
err := c.verifyOTP(otp)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error verifying the provided OTP: %s", err))
return 1
}
case pgpKeyArr != nil:
if len(pgpKeyArr) != 1 {
c.Ui.Error("Could not parse PGP key")
return 1
}
if len(pgpKeyArr[0]) == 0 {
c.Ui.Error("Got an empty PGP key")
return 1
}
pgpKey = pgpKeyArr[0]
default:
panic("unreachable case")
}
}
if nonce != "" {
c.Nonce = nonce
}
// Check if we are running doing any restricted variants
switch {
case init:
return c.initRootGeneration(client, otp, pgpKey)
case cancel:
return c.cancelRootGeneration(client)
case status:
return c.rootGenerationStatus(client)
}
// Start the root generation process if not started
if !rootGenerationStatus.Started {
err = client.Sys().RootGenerationInit(otp, pgpKey)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error initializing root generation: %s", err))
return 1
}
rootGenerationStatus, err = client.Sys().RootGenerationStatus()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error reading root generation status: %s", err))
return 1
}
c.Nonce = rootGenerationStatus.Nonce
}
serverNonce := rootGenerationStatus.Nonce
// Get the unseal key
args = flags.Args()
key := c.Key
if len(args) > 0 {
key = args[0]
}
if key == "" {
c.Nonce = serverNonce
fmt.Printf("Root generation operation nonce: %s\n", serverNonce)
fmt.Printf("Key (will be hidden): ")
key, err = password.Read(os.Stdin)
fmt.Printf("\n")
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error attempting to ask for password. The raw error message\n"+
"is shown below, but the most common reason for this error is\n"+
"that you attempted to pipe a value into unseal or you're\n"+
"executing `vault generate-root` from outside of a terminal.\n\n"+
"You should use `vault generate-root` from a terminal for maximum\n"+
"security. If this isn't an option, the unseal key can be passed\n"+
"in using the first parameter.\n\n"+
"Raw error: %s", err))
return 1
}
}
// Provide the key, this may potentially complete the update
statusResp, err := client.Sys().RootGenerationUpdate(strings.TrimSpace(key), c.Nonce)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error attempting generate-root update: %s", err))
return 1
}
c.dumpStatus(statusResp)
return 0
}
func (c *GenerateRootCommand) verifyOTP(otp string) error {
if len(otp) == 0 {
return fmt.Errorf("No OTP passed in")
}
otpBytes, err := base64.StdEncoding.DecodeString(otp)
if err != nil {
return fmt.Errorf("Error decoding base64 OTP value: %s", err)
}
if otpBytes == nil || len(otpBytes) != 16 {
return fmt.Errorf("Decoded OTP value is invalid or wrong length")
}
return nil
}
func (c *GenerateRootCommand) decode(encodedVal, otp string) int {
tokenBytes, err := xor.XORBase64(encodedVal, otp)
if err != nil {
c.Ui.Error(err.Error())
return 1
}
token, err := uuid.FormatUUID(tokenBytes)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error formatting base64 token value: %v", err))
return 1
}
c.Ui.Output(fmt.Sprintf("Root token: %s", token))
return 0
}
// initRootGeneration is used to start the generation process
func (c *GenerateRootCommand) initRootGeneration(client *api.Client, otp string, pgpKey string) int {
// Start the rekey
err := client.Sys().RootGenerationInit(otp, pgpKey)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error initializing root generation: %s", err))
return 1
}
// Provide the current status
return c.rootGenerationStatus(client)
}
// cancelRootGeneration is used to abort the generation process
func (c *GenerateRootCommand) cancelRootGeneration(client *api.Client) int {
err := client.Sys().RootGenerationCancel()
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to cancel root generation: %s", err))
return 1
}
c.Ui.Output("Root generation canceled.")
return 0
}
// rootGenerationStatus is used just to fetch and dump the status
func (c *GenerateRootCommand) rootGenerationStatus(client *api.Client) int {
// Check the status
status, err := client.Sys().RootGenerationStatus()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error reading root generation status: %s", err))
return 1
}
c.dumpStatus(status)
return 0
}
// dumpStatus dumps the status to output
func (c *GenerateRootCommand) dumpStatus(status *api.RootGenerationStatusResponse) {
// Dump the status
statString := fmt.Sprintf(
"Nonce: %s\n"+
"Started: %v\n"+
"Rekey Progress: %d\n"+
"Required Keys: %d\n"+
"Complete: %t",
status.Nonce,
status.Started,
status.Progress,
status.Required,
status.Complete,
)
if len(status.PGPFingerprint) > 0 {
statString = fmt.Sprintf("%s\nPGP Fingerprint: %s", statString, status.PGPFingerprint)
}
if len(status.EncodedRootToken) > 0 {
statString = fmt.Sprintf("%s\n\nEncoded root token: %s", statString, status.EncodedRootToken)
}
c.Ui.Output(statString)
}
func (c *GenerateRootCommand) Synopsis() string {
return "Promotes a token to a root token"
}
func (c *GenerateRootCommand) Help() string {
helpText := `
Usage: vault generate-root [options] [key]
'generate-root' is used to create a new root token.
Root generation can only be done when the Vault is already unsealed. The
operation is done online, but requires that a threshold of the current unseal
keys be provided.
One (and only one) of the following must be provided at attempt
initialization time:
1) A 16-byte, base64-encoded One Time Password (OTP) provided in the '-otp'
flag; the token is XOR'd with this value before it is returned once the final
unseal key has been provided. The '-decode' operation can be used with this
value and the OTP to output the final token value. The '-genotp' flag can be
used to generate a suitable value.
or
2) A file containing a PGP key (binary or base64-encoded) or a Keybase.io
username in the format of "keybase:<username>" in the '-pgp-key' flag. The
final token value will be encrypted with this public key and base64-encoded.
General Options:
` + generalOptionsUsage() + `
Rekey Options:
-init Initialize the root generation attempt. This can only
be done if no generation is already initiated.
-cancel Reset the root generation process by throwing away
prior unseal keys and the configuration.
-status Prints the status of the current attempt. This can be
used to see the status without attempting to provide
an unseal key.
-decode=abcd Decodes and outputs the generated root token. The OTP
used at '-init' time must be provided in the '-otp'
parameter.
-genotp Returns a high-quality OTP suitable for passing into
the '-init' method.
-otp=abcd The base64-encoded 16-byte OTP for use with the
'-init' or '-decode' methods.
-pgp-key A file on disk containing a binary- or base64-format
public PGP key, or a Keybase username specified as
"keybase:<username>". The output root token will be
encrypted and base64-encoded, in order, with the given
public key.
-nonce=abcd The nonce provided at initialization time. This same
nonce value must be provided with each unseal key. If
the unseal key is not being passed in via the command
line the nonce parameter is not required, and will
instead be displayed with the key prompt.
`
return strings.TrimSpace(helpText)
}

View File

@ -0,0 +1,258 @@
package command
import (
"encoding/base64"
"encoding/hex"
"os"
"strings"
"testing"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/helper/pgpkeys"
"github.com/hashicorp/vault/helper/xor"
"github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/vault"
"github.com/mitchellh/cli"
)
func TestGenerateRoot_Cancel(t *testing.T) {
core, key, _ := vault.TestCoreUnsealed(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
ui := new(cli.MockUi)
c := &GenerateRootCommand{
Key: hex.EncodeToString(key),
Meta: Meta{
Ui: ui,
},
}
otpBytes, err := xor.GenerateRandBytes(16)
if err != nil {
t.Fatal(err)
}
otp := base64.StdEncoding.EncodeToString(otpBytes)
args := []string{"-address", addr, "-init", "-otp", otp}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
args = []string{"-address", addr, "-cancel"}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
config, err := core.RootGenerationConfiguration()
if err != nil {
t.Fatalf("err: %s", err)
}
if config != nil {
t.Fatal("should not have a config for root generation")
}
}
func TestGenerateRoot_status(t *testing.T) {
core, key, _ := vault.TestCoreUnsealed(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
ui := new(cli.MockUi)
c := &GenerateRootCommand{
Key: hex.EncodeToString(key),
Meta: Meta{
Ui: ui,
},
}
otpBytes, err := xor.GenerateRandBytes(16)
if err != nil {
t.Fatal(err)
}
otp := base64.StdEncoding.EncodeToString(otpBytes)
args := []string{"-address", addr, "-init", "-otp", otp}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
args = []string{"-address", addr, "-status"}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
if !strings.Contains(string(ui.OutputWriter.Bytes()), "Started: true") {
t.Fatalf("bad: %s", ui.OutputWriter.String())
}
}
func TestGenerateRoot_OTP(t *testing.T) {
core, ts, key, _ := vault.TestCoreWithTokenStore(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
ui := new(cli.MockUi)
c := &GenerateRootCommand{
Key: hex.EncodeToString(key),
Meta: Meta{
Ui: ui,
},
}
// Generate an OTP
otpBytes, err := xor.GenerateRandBytes(16)
if err != nil {
t.Fatal(err)
}
otp := base64.StdEncoding.EncodeToString(otpBytes)
// Init the attempt
args := []string{
"-address", addr,
"-init",
"-otp", otp,
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
config, err := core.RootGenerationConfiguration()
if err != nil {
t.Fatalf("err: %v", err)
}
c.Nonce = config.Nonce
// Provide the key
args = []string{
"-address", addr,
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
beforeNAfter := strings.Split(ui.OutputWriter.String(), "Encoded root token: ")
if len(beforeNAfter) != 2 {
t.Fatalf("did not find encoded root token in %s", ui.OutputWriter.String())
}
encodedToken := strings.TrimSpace(beforeNAfter[1])
decodedToken, err := xor.XORBase64(encodedToken, otp)
if err != nil {
t.Fatal(err)
}
token, err := uuid.FormatUUID(decodedToken)
if err != nil {
t.Fatal(err)
}
req := logical.TestRequest(t, logical.ReadOperation, "lookup-self")
req.ClientToken = token
resp, err := ts.HandleRequest(req)
if err != nil {
t.Fatalf("error running token lookup-self: %v", err)
}
if resp == nil {
t.Fatalf("got nil resp with token lookup-self")
}
if resp.Data == nil {
t.Fatalf("got nil resp.Data with token lookup-self")
}
if resp.Data["orphan"].(bool) != true ||
resp.Data["ttl"].(int64) != 0 ||
resp.Data["num_uses"].(int) != 0 ||
resp.Data["meta"].(map[string]string) != map[string]string(nil) ||
len(resp.Data["policies"].([]string)) != 1 ||
resp.Data["policies"].([]string)[0] != "root" {
t.Fatalf("bad: %#v", resp.Data)
}
}
func TestGenerateRoot_PGP(t *testing.T) {
core, ts, key, _ := vault.TestCoreWithTokenStore(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
ui := new(cli.MockUi)
c := &GenerateRootCommand{
Key: hex.EncodeToString(key),
Meta: Meta{
Ui: ui,
},
}
tempDir, pubFiles, err := getPubKeyFiles(t)
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
// Init the attempt
args := []string{
"-address", addr,
"-init",
"-pgp-key", pubFiles[0],
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
config, err := core.RootGenerationConfiguration()
if err != nil {
t.Fatalf("err: %v", err)
}
c.Nonce = config.Nonce
// Provide the key
args = []string{
"-address", addr,
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
beforeNAfter := strings.Split(ui.OutputWriter.String(), "Encoded root token: ")
if len(beforeNAfter) != 2 {
t.Fatalf("did not find encoded root token in %s", ui.OutputWriter.String())
}
encodedToken := strings.TrimSpace(beforeNAfter[1])
ptBuf, err := pgpkeys.DecryptBytes(encodedToken, pgpkeys.TestPrivKey1)
if err != nil {
t.Fatal(err)
}
if ptBuf == nil {
t.Fatal("returned plain text buffer is nil")
}
token := ptBuf.String()
req := logical.TestRequest(t, logical.ReadOperation, "lookup-self")
req.ClientToken = token
resp, err := ts.HandleRequest(req)
if err != nil {
t.Fatalf("error running token lookup-self: %v", err)
}
if resp == nil {
t.Fatalf("got nil resp with token lookup-self")
}
if resp.Data == nil {
t.Fatalf("got nil resp.Data with token lookup-self")
}
if resp.Data["orphan"].(bool) != true ||
resp.Data["ttl"].(int64) != 0 ||
resp.Data["num_uses"].(int) != 0 ||
resp.Data["meta"].(map[string]string) != map[string]string(nil) ||
len(resp.Data["policies"].([]string)) != 1 ||
resp.Data["policies"].([]string)[0] != "root" {
t.Fatalf("bad: %#v", resp.Data)
}
}

View File

@ -10,6 +10,7 @@ import (
"sort"
"testing"
"github.com/hashicorp/vault/helper/pgpkeys"
"github.com/hashicorp/vault/vault"
"golang.org/x/crypto/openpgp"
@ -29,7 +30,7 @@ func getPubKeyFiles(t *testing.T) (string, []string, error) {
tempDir + "/aapubkey1",
}
decoder := base64.StdEncoding
pub1Bytes, err := decoder.DecodeString(pubKey1)
pub1Bytes, err := decoder.DecodeString(pgpkeys.TestPubKey1)
if err != nil {
t.Fatalf("Error decoding bytes for public key 1: %s", err)
}
@ -37,7 +38,7 @@ func getPubKeyFiles(t *testing.T) (string, []string, error) {
if err != nil {
t.Fatalf("Error writing pub key 1 to temp file: %s", err)
}
pub2Bytes, err := decoder.DecodeString(pubKey2)
pub2Bytes, err := decoder.DecodeString(pgpkeys.TestPubKey2)
if err != nil {
t.Fatalf("Error decoding bytes for public key 2: %s", err)
}
@ -45,7 +46,7 @@ func getPubKeyFiles(t *testing.T) (string, []string, error) {
if err != nil {
t.Fatalf("Error writing pub key 2 to temp file: %s", err)
}
pub3Bytes, err := decoder.DecodeString(pubKey3)
pub3Bytes, err := decoder.DecodeString(pgpkeys.TestPubKey3)
if err != nil {
t.Fatalf("Error decoding bytes for public key 3: %s", err)
}
@ -53,7 +54,7 @@ func getPubKeyFiles(t *testing.T) (string, []string, error) {
if err != nil {
t.Fatalf("Error writing pub key 3 to temp file: %s", err)
}
err = ioutil.WriteFile(pubFiles[3], []byte(aaPubKey1), 0755)
err = ioutil.WriteFile(pubFiles[3], []byte(pgpkeys.TestAAPubKey1), 0755)
if err != nil {
t.Fatalf("Error writing aa pub key 1 to temp file: %s", err)
}
@ -67,15 +68,15 @@ func parseDecryptAndTestUnsealKeys(t *testing.T,
backupKeys map[string][]string,
core *vault.Core) {
decoder := base64.StdEncoding
priv1Bytes, err := decoder.DecodeString(privKey1)
priv1Bytes, err := decoder.DecodeString(pgpkeys.TestPrivKey1)
if err != nil {
t.Fatalf("Error decoding bytes for private key 1: %s", err)
}
priv2Bytes, err := decoder.DecodeString(privKey2)
priv2Bytes, err := decoder.DecodeString(pgpkeys.TestPrivKey2)
if err != nil {
t.Fatalf("Error decoding bytes for private key 2: %s", err)
}
priv3Bytes, err := decoder.DecodeString(privKey3)
priv3Bytes, err := decoder.DecodeString(pgpkeys.TestPrivKey3)
if err != nil {
t.Fatalf("Error decoding bytes for private key 3: %s", err)
}
@ -130,12 +131,12 @@ func parseDecryptAndTestUnsealKeys(t *testing.T,
unsealKeys := []string{}
ptBuf := bytes.NewBuffer(nil)
for i, keyHex := range privBytes {
for i, privKeyBytes := range privBytes {
if i > 2 {
break
}
ptBuf.Reset()
entity, err := openpgp.ReadEntity(packet.NewReader(bytes.NewBuffer(keyHex)))
entity, err := openpgp.ReadEntity(packet.NewReader(bytes.NewBuffer(privKeyBytes)))
if err != nil {
t.Fatalf("Error parsing private key %d: %s", i, err)
}
@ -172,265 +173,3 @@ func parseDecryptAndTestUnsealKeys(t *testing.T,
}
}
const privKey1 = `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
7u2CfYyFPu3AlUaGNMBlvy6PEpU=`
const privKey2 = `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
dXlg3mimA+iK7tABQfG0RJ9YzWs=`
const privKey3 = `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
KFIzGEPKVoBF9U7VBXpyxpsz+A==`
const pubKey1 = `mQENBFXbjPUBCADjNjCUQwfxKL+RR2GA6pv/1K+zJZ8UWIF9S0lk7cVIEfJiprzzwiMwBS5cD0da
rGin1FHvIWOZxujA7oW0O2TUuatqI3aAYDTfRYurh6iKLC+VS+F7H+/mhfFvKmgr0Y5kDCF1j0T/
063QZ84IRGucR/X43IY7kAtmxGXH0dYOCzOe5UBX1fTn3mXGe2ImCDWBH7gOViynXmb6XNvXkP0f
sF5St9jhO7mbZU9EFkv9O3t3EaURfHopsCVDOlCkFCw5ArY+DUORHRzoMX0PnkyQb5OzibkChzpg
8hQssKeVGpuskTdz5Q7PtdW71jXd4fFVzoNH8fYwRpziD2xNvi6HABEBAAG0EFZhdWx0IFRlc3Qg
S2V5IDGJATgEEwECACIFAlXbjPUCGy8GCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEOfLr44B
HbeTo+sH/i7bapIgPnZsJ81hmxPj4W12uvunksGJiC7d4hIHsG7kmJRTJfjECi+AuTGeDwBy84TD
cRaOB6e79fj65Fg6HgSahDUtKJbGxj/lWzmaBuTzlN3CEe8cMwIPqPT2kajJVdOyrvkyuFOdPFOE
A7bdCH0MqgIdM2SdF8t40k/ATfuD2K1ZmumJ508I3gF39jgTnPzD4C8quswrMQ3bzfvKC3klXRlB
C0yoArn+0QA3cf2B9T4zJ2qnvgotVbeK/b1OJRNj6Poeo+SsWNc/A5mw7lGScnDgL3yfwCm1gQXa
QKfOt5x+7GqhWDw10q+bJpJlI10FfzAnhMF9etSqSeURBRW5AQ0EVduM9QEIAL53hJ5bZJ7oEDCn
aY+SCzt9QsAfnFTAnZJQrvkvusJzrTQ088eUQmAjvxkfRqnv981fFwGnh2+I1Ktm698UAZS9Jt8y
jak9wWUICKQO5QUt5k8cHwldQXNXVXFa+TpQWQR5yW1a9okjh5o/3d4cBt1yZPUJJyLKY43Wvptb
6EuEsScO2DnRkh5wSMDQ7dTooddJCmaq3LTjOleRFQbu9ij386Do6jzK69mJU56TfdcydkxkWF5N
ZLGnED3lq+hQNbe+8UI5tD2oP/3r5tXKgMy1R/XPvR/zbfwvx4FAKFOP01awLq4P3d/2xOkMu4Lu
9p315E87DOleYwxk+FoTqXEAEQEAAYkCPgQYAQIACQUCVduM9QIbLgEpCRDny6+OAR23k8BdIAQZ
AQIABgUCVduM9QAKCRAID0JGyHtSGmqYB/4m4rJbbWa7dBJ8VqRU7ZKnNRDR9CVhEGipBmpDGRYu
lEimOPzLUX/ZXZmTZzgemeXLBaJJlWnopVUWuAsyjQuZAfdd8nHkGRHG0/DGum0l4sKTta3OPGHN
C1z1dAcQ1RCr9bTD3PxjLBczdGqhzw71trkQRBRdtPiUchltPMIyjUHqVJ0xmg0hPqFic0fICsr0
YwKoz3h9+QEcZHvsjSZjgydKvfLYcm+4DDMCCqcHuJrbXJKUWmJcXR0y/+HQONGrGJ5xWdO+6eJi
oPn2jVMnXCm4EKc7fcLFrz/LKmJ8seXhxjM3EdFtylBGCrx3xdK0f+JDNQaC/rhUb5V2XuX6VwoH
/AtY+XsKVYRfNIupLOUcf/srsm3IXT4SXWVomOc9hjGQiJ3rraIbADsc+6bCAr4XNZS7moViAAcI
PXFv3m3WfUlnG/om78UjQqyVACRZqqAGmuPq+TSkRUCpt9h+A39LQWkojHqyob3cyLgy6z9Q557O
9uK3lQozbw2gH9zC0RqnePl+rsWIUU/ga16fH6pWc1uJiEBt8UZGypQ/E56/343epmYAe0a87sHx
8iDV+dNtDVKfPRENiLOOc19MmS+phmUyrbHqI91c0pmysYcJZCD3a502X1gpjFbPZcRtiTmGnUKd
OIu60YPNE4+h7u2CfYyFPu3AlUaGNMBlvy6PEpU=`
const pubKey2 = `mQENBFXbkJEBCADKb1ZvlT14XrJa2rTOe5924LQr2PTZlRv+651TXy33yEhelZ+V4sMrELN8fKEG
Zy1kNixmbq3MCF/671k3LigHA7VrOaH9iiQgr6IIq2MeIkUYKZ27C992vQkYLjbYUG8+zl5h69S4
0Ixm0yL0M54XOJ0gm+maEK1ZESKTUlDNkIS7l0jLZSYwfUeGXSEt6FWs8OgbyRTaHw4PDHrDEE9e
Q67K6IZ3YMhPOL4fVk4Jwrp5R/RwiklT+lNozWEyFVwPFH4MeQMs9nMbt+fWlTzEA7tI4acI9yDk
Cm1yN2R9rmY0UjODRiJw6z6sLV2T+Pf32n3MNSUOYczOjZa4VBwjABEBAAG0EFZhdWx0IFRlc3Qg
S2V5IDKJATgEEwECACIFAlXbkJECGy8GCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEOuDLGfr
XolXqz4H/28IuoRxGKoJ064YHjPkkpoddW6zdzzNfHipZnNfEUiTEls4qF1IB81M2xqfiXIFRIdO
2kaLkRPFhO0hRxbtI6VuZYLgG3QCaXhxW6GyFa5zKABqhb5ojexdnAYRswaHV201ZCclj9rnJN1P
Ag0Rz6MdX/w1euEWktQxWKo42oZKyx8oT9p6lrv5KRmGkdrg8K8ARmRILjmwuBAgJM0eXBZHNGWX
elk4YmOgnAAcZA6ZAo1G+8Pg6pKKP61ETewuCg3/u7N0vDttB+ZXqF88W9jAYlvdgbTtajNF5IDY
DjTzWfeCaIB18F9gOzXq15SwWeDDI+CU9Nmq358IzXlxk4e5AQ0EVduQkQEIAOjZV5tbpfIh5Qef
pIp2dpGMVfpgPj4RNc15CyFnb8y6dhCrdybkY9GveXJe4F3GNYnSfB42cgxrfhizX3LakmZQ/SAg
+YO5KxfCIN7Q9LPNeTgPsZZT6h8lVuXUxOFKXfRaR3/tGF5xE3e5CoZRsHV/c92h3t1LdJNOnC5m
UKIPO4zDxiw/C2T2q3rP1kmIMaOH724kEH5A+xcp1cBHyt0tdHtIWuQv6joTJzujqViRhlCwQYzQ
SKpSBxwhBsorPvyinZI/ZXA4XXZc5RoMqV9rikedrb1rENO8JOuPu6tMS+znFu67skq2gFFZwCQW
IjdHm+2ukE+PE580WAWudyMAEQEAAYkCPgQYAQIACQUCVduQkQIbLgEpCRDrgyxn616JV8BdIAQZ
AQIABgUCVduQkQAKCRArYtevdF38xtzgB/4zVzozBpVOnagRkA7FDsHo36xX60Lik+ew0m28ueDD
hnV3bXQsCvn/6wiCVWqLOTDeYCPlyTTpEMyk8zwdCICW6MgSkVHWcEDOrRqIrqm86rirjTGjJSgQ
e3l4CqJvkn6jybShYoBk1OZZV6vVv9hPTXXv9E6dLKoEW5YZBrrF+VC0w1iOIvaAQ+QXph20eV4K
BIrp/bhG6PdnigKxuBZ79cdqDnXIzT9UiIa6LYpR0rbeg+7BmuZTTPS8t+41hIiKS+UZFdKa67eY
ENtyOmEMWOFCLLRJGxkleukchiMJ70rknloZXsvJIweXBzSZ6m7mJQBgaig/L/dXyjv6+j2pNB4H
/1trYUtJjXQKHmqlgCmpCkHt3g7JoxWvglnDNmE6q3hIWuVIYQpnzZy1g05+X9Egwc1WVpBB02H7
PkUZTfpaP/L6DLneMmSKPhZE3I+lPIPjwrxqh6xy5uQezcWkJTNKvPWF4FJzrVvx7XTPjfGvOB0U
PEnjvtZTp5yOhTeZK7DgIEtb/Wcrqs+iRArQKboM930ORSZhwvGK3F9V/gMDpIrvge5vDFsTEYQd
w/2epIewH0L/FUb/6jBRcVEpGo9Ayg+Jnhq14GOGcd1y9oMZ48kYVLVBTA9tQ+82WE8Bch7uFPj4
MFOMVRn1dc3qdXlg3mimA+iK7tABQfG0RJ9YzWs=`
const pubKey3 = `mQENBFXbkiMBCACiHW4/VI2JkfvSEINddS7vE6wEu5e1leNQDaLUh6PrATQZS2a4Q6kRE6WlJumj
6wCeN753Cm93UGQl2Bi3USIEeArIZnPTcocrckOVXxtoLBNKXgqKvEsDXgfw8A+doSfXoDm/3Js4
Wy3WsYKNR9LaPuJZHnpjsFAJhvRVyhH4UFD+1RTSSefq1mozPfDdMoZeZNEpfhwt3DuTJs7RqcTH
CgR2CqhEHnOOE5jJUljHKYLCglE2+8dth1bZlQi4xly/VHZzP3Bn7wKeolK/ROP6VZz/e0xq/BKy
resmxvlBWZ1zWwqGIrV9b0uwYvGrh2hOd5C5+5oGaA2MGcjxwaLBABEBAAG0EFZhdWx0IFRlc3Qg
S2V5IDOJATgEEwECACIFAlXbkiMCGy8GCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEPR5S1b8
LcbdWjEH/2mhqC9a0Vk1IzOgxEoVxYVqVdvaxI0nTZOTfmcFYn4HQlQ+SLEoyNWe5jtkhx4k5uHi
pxwKHzOv02YM14NWC6bvKw2CQETLDPG4Cv8YMUmpho5tnMDdttIzp8HjyJRtHazU1uTes2/yuqh6
LHCejVJI0uST3RibquwdG3QjPP8Umxu+YC9+FOW2Kit/AQ8JluFDJdq3/wSX8VfYZrGdgmreE7KY
MolhCkzGSPj7oFygw8LqKoJvt9tCuBKhZMBuMv1sB5CoJIWdPoqOZc4U7L1XdqfKvFZR/RhuXgN1
lkI9MqrnLDpikL3Lk+ctLxWOjUCW8roqKoHZYBF7XPqdAfm5AQ0EVduSIwEIAOPcjd4QgbLlqIk3
s6BPRRyVzglTgUdf+I0rUDybaDJfJobZd8U6e4hkPvRoQ8tJefnz/qnD/63watAbJYcVTme40I3V
KDOmVGcyaDxiKP1disKqcEJd7XQiI72oAiXmEH0y+5UwnOMks/lwaAGDMGVRjHEXI6fiRPFsfTr8
7qvMJ3pW1OiOXVSezuBNTlmyJC7srQ1/nwxL337ev6D1zQZd3JuhcxLkHrUELLNwzhvcZ70vg645
jAmz8EdmvvoqEPPoHqKgP5AeHACOsTm953KHhgx3NYuGPU/RoIvugKt4Iq5nw7TWFTjPHGVF3GTQ
ry5CZ/AzXiL57hVEhDvmuT8AEQEAAYkCPgQYAQIACQUCVduSIwIbLgEpCRD0eUtW/C3G3cBdIAQZ
AQIABgUCVduSIwAKCRAFI/9Nx3K5IPOFCACsZ/Z4s2LcEoA51TW+T5w+YevlIuq+332JtqNIpuGI
WpGxUxyDyPT0YQWr0SObBORYNr7RP8d/I2rbaFKyaDaKvRofYr+TwXy92phBo7pdEUamBpfrm/sr
+2BgAB2x3HWXp+IMdeVVhqQe8t4cnFm3c1fIdxADyiJuV5ge2Ml5gK5yNwqCQPh7U2RqC+lmVlMJ
GvWInIRn2mf6A7phDYNZfOz6dkar4yyh5r9rRgrZw88r/yIlrq/c6KRUIgnPMrFYEauggOceZ827
+jKkbKWFEuHtiCxW7kRAN25UfnGsPaF+NSGM2q1vCG4HiFydx6lMoXM0Shf8+ZwyrV/5BzAqpWwI
AJ37tEwC58Fboynly6OGOzgPS0xKnzkXMOtquTo0qEH/1bEUsBknn795BmZOTf4oBC5blN6qRv7c
GSP00i+sxql1NhTjJcjJtfOPUzgfW+Af/+HR648z4c7c6MCjDFKnk8ZkoGLRU7ISjenkNFzvu2bj
lxJkil0uJDlLPbbX80ojzV1GS9g+ZxVPR+68N1QLl2FU6zsfg34upmLLHG8VG4vExzgyNkOwfTYv
dgyRNTjnuPue6H12fZZ9uCNeG52v7lR3eoQcCxBOniwgipB8UJ52RWXblwxzCtGtDi/EWB3zLTUn
puKcgucA0LotbihSMxhDylaARfVO1QV6csabM/g=`
const aaPubKey1 = `-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v1
mQENBFXbjPUBCADjNjCUQwfxKL+RR2GA6pv/1K+zJZ8UWIF9S0lk7cVIEfJiprzz
wiMwBS5cD0darGin1FHvIWOZxujA7oW0O2TUuatqI3aAYDTfRYurh6iKLC+VS+F7
H+/mhfFvKmgr0Y5kDCF1j0T/063QZ84IRGucR/X43IY7kAtmxGXH0dYOCzOe5UBX
1fTn3mXGe2ImCDWBH7gOViynXmb6XNvXkP0fsF5St9jhO7mbZU9EFkv9O3t3EaUR
fHopsCVDOlCkFCw5ArY+DUORHRzoMX0PnkyQb5OzibkChzpg8hQssKeVGpuskTdz
5Q7PtdW71jXd4fFVzoNH8fYwRpziD2xNvi6HABEBAAG0EFZhdWx0IFRlc3QgS2V5
IDGJATgEEwECACIFAlXbjPUCGy8GCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJ
EOfLr44BHbeTo+sH/i7bapIgPnZsJ81hmxPj4W12uvunksGJiC7d4hIHsG7kmJRT
JfjECi+AuTGeDwBy84TDcRaOB6e79fj65Fg6HgSahDUtKJbGxj/lWzmaBuTzlN3C
Ee8cMwIPqPT2kajJVdOyrvkyuFOdPFOEA7bdCH0MqgIdM2SdF8t40k/ATfuD2K1Z
mumJ508I3gF39jgTnPzD4C8quswrMQ3bzfvKC3klXRlBC0yoArn+0QA3cf2B9T4z
J2qnvgotVbeK/b1OJRNj6Poeo+SsWNc/A5mw7lGScnDgL3yfwCm1gQXaQKfOt5x+
7GqhWDw10q+bJpJlI10FfzAnhMF9etSqSeURBRW5AQ0EVduM9QEIAL53hJ5bZJ7o
EDCnaY+SCzt9QsAfnFTAnZJQrvkvusJzrTQ088eUQmAjvxkfRqnv981fFwGnh2+I
1Ktm698UAZS9Jt8yjak9wWUICKQO5QUt5k8cHwldQXNXVXFa+TpQWQR5yW1a9okj
h5o/3d4cBt1yZPUJJyLKY43Wvptb6EuEsScO2DnRkh5wSMDQ7dTooddJCmaq3LTj
OleRFQbu9ij386Do6jzK69mJU56TfdcydkxkWF5NZLGnED3lq+hQNbe+8UI5tD2o
P/3r5tXKgMy1R/XPvR/zbfwvx4FAKFOP01awLq4P3d/2xOkMu4Lu9p315E87DOle
Ywxk+FoTqXEAEQEAAYkCPgQYAQIACQUCVduM9QIbLgEpCRDny6+OAR23k8BdIAQZ
AQIABgUCVduM9QAKCRAID0JGyHtSGmqYB/4m4rJbbWa7dBJ8VqRU7ZKnNRDR9CVh
EGipBmpDGRYulEimOPzLUX/ZXZmTZzgemeXLBaJJlWnopVUWuAsyjQuZAfdd8nHk
GRHG0/DGum0l4sKTta3OPGHNC1z1dAcQ1RCr9bTD3PxjLBczdGqhzw71trkQRBRd
tPiUchltPMIyjUHqVJ0xmg0hPqFic0fICsr0YwKoz3h9+QEcZHvsjSZjgydKvfLY
cm+4DDMCCqcHuJrbXJKUWmJcXR0y/+HQONGrGJ5xWdO+6eJioPn2jVMnXCm4EKc7
fcLFrz/LKmJ8seXhxjM3EdFtylBGCrx3xdK0f+JDNQaC/rhUb5V2XuX6VwoH/AtY
+XsKVYRfNIupLOUcf/srsm3IXT4SXWVomOc9hjGQiJ3rraIbADsc+6bCAr4XNZS7
moViAAcIPXFv3m3WfUlnG/om78UjQqyVACRZqqAGmuPq+TSkRUCpt9h+A39LQWko
jHqyob3cyLgy6z9Q557O9uK3lQozbw2gH9zC0RqnePl+rsWIUU/ga16fH6pWc1uJ
iEBt8UZGypQ/E56/343epmYAe0a87sHx8iDV+dNtDVKfPRENiLOOc19MmS+phmUy
rbHqI91c0pmysYcJZCD3a502X1gpjFbPZcRtiTmGnUKdOIu60YPNE4+h7u2CfYyF
Pu3AlUaGNMBlvy6PEpU=
=NUTS
-----END PGP PUBLIC KEY BLOCK-----`

View File

@ -3,21 +3,20 @@ package pgpkeys
import (
"bytes"
"encoding/base64"
"encoding/hex"
"fmt"
"golang.org/x/crypto/openpgp"
"golang.org/x/crypto/openpgp/packet"
)
// EncryptShares takes an ordered set of Shamir key share fragments and
// PGP public keys and encrypts each Shamir key fragment with the corresponding
// public key
// EncryptShares takes an ordered set of byte slices to encrypt and the
// corresponding base64-encoded public keys to encrypt them with, encrypts each
// byte slice with the corresponding public key.
//
// 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) ([]string, [][]byte, error) {
if len(secretShares) != len(pgpKeys) {
func EncryptShares(input [][]byte, pgpKeys []string) ([]string, [][]byte, error) {
if len(input) != len(pgpKeys) {
return nil, nil, fmt.Errorf("Mismatch between number of generated shares and number of PGP keys")
}
encryptedShares := make([][]byte, 0, len(pgpKeys))
@ -31,7 +30,7 @@ func EncryptShares(secretShares [][]byte, pgpKeys []string) ([]string, [][]byte,
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])))
_, err = pt.Write(input[i])
if err != nil {
return nil, nil, fmt.Errorf("Error encrypting PGP message: %s", err)
}
@ -47,6 +46,9 @@ func EncryptShares(secretShares [][]byte, pgpKeys []string) ([]string, [][]byte,
return fingerprints, encryptedShares, nil
}
// GetFingerprints takes in a list of openpgp Entities and returns the
// fingerprints. If entities is nil, it will instead parse both entities and
// fingerprints from the pgpKeys string slice.
func GetFingerprints(pgpKeys []string, entities []*openpgp.Entity) ([]string, error) {
if entities == nil {
var err error
@ -63,6 +65,8 @@ func GetFingerprints(pgpKeys []string, entities []*openpgp.Entity) ([]string, er
return ret, nil
}
// GetEntities takes in a string array of base64-encoded PGP keys and returns
// the openpgp Entities
func GetEntities(pgpKeys []string) ([]*openpgp.Entity, error) {
ret := make([]*openpgp.Entity, 0, len(pgpKeys))
for _, keystring := range pgpKeys {
@ -78,3 +82,36 @@ func GetEntities(pgpKeys []string) ([]*openpgp.Entity, error) {
}
return ret, nil
}
// DecryptBytes takes in base64-encoded encrypted bytes and the base64-encoded
// private key and decrypts it. A bytes.Buffer is returned to allow the caller
// to do useful thing with it (get it as a []byte, get it as a string, use it
// as an io.Reader, etc), and also because this function doesn't know if what
// comes out is binary data or a string, so let the caller decide.
func DecryptBytes(encodedCrypt, privKey string) (*bytes.Buffer, error) {
privKeyBytes, err := base64.StdEncoding.DecodeString(privKey)
if err != nil {
return nil, fmt.Errorf("Error decoding base64 private key: %s", err)
}
cryptBytes, err := base64.StdEncoding.DecodeString(encodedCrypt)
if err != nil {
return nil, fmt.Errorf("Error decoding base64 crypted bytes: %s", err)
}
entity, err := openpgp.ReadEntity(packet.NewReader(bytes.NewBuffer(privKeyBytes)))
if err != nil {
return nil, fmt.Errorf("Error parsing private key: %s", err)
}
entityList := &openpgp.EntityList{entity}
md, err := openpgp.ReadMessage(bytes.NewBuffer(cryptBytes), entityList, nil, nil)
if err != nil {
return nil, fmt.Errorf("Error decrypting the messages: %s", err)
}
ptBuf := bytes.NewBuffer(nil)
ptBuf.ReadFrom(md.UnverifiedBody)
return ptBuf, nil
}

271
helper/pgpkeys/test_keys.go Normal file
View File

@ -0,0 +1,271 @@
package pgpkeys
const (
TestPrivKey1 = `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
7u2CfYyFPu3AlUaGNMBlvy6PEpU=`
TestPrivKey2 = `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
dXlg3mimA+iK7tABQfG0RJ9YzWs=`
TestPrivKey3 = `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
KFIzGEPKVoBF9U7VBXpyxpsz+A==`
TestPubKey1 = `mQENBFXbjPUBCADjNjCUQwfxKL+RR2GA6pv/1K+zJZ8UWIF9S0lk7cVIEfJiprzzwiMwBS5cD0da
rGin1FHvIWOZxujA7oW0O2TUuatqI3aAYDTfRYurh6iKLC+VS+F7H+/mhfFvKmgr0Y5kDCF1j0T/
063QZ84IRGucR/X43IY7kAtmxGXH0dYOCzOe5UBX1fTn3mXGe2ImCDWBH7gOViynXmb6XNvXkP0f
sF5St9jhO7mbZU9EFkv9O3t3EaURfHopsCVDOlCkFCw5ArY+DUORHRzoMX0PnkyQb5OzibkChzpg
8hQssKeVGpuskTdz5Q7PtdW71jXd4fFVzoNH8fYwRpziD2xNvi6HABEBAAG0EFZhdWx0IFRlc3Qg
S2V5IDGJATgEEwECACIFAlXbjPUCGy8GCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEOfLr44B
HbeTo+sH/i7bapIgPnZsJ81hmxPj4W12uvunksGJiC7d4hIHsG7kmJRTJfjECi+AuTGeDwBy84TD
cRaOB6e79fj65Fg6HgSahDUtKJbGxj/lWzmaBuTzlN3CEe8cMwIPqPT2kajJVdOyrvkyuFOdPFOE
A7bdCH0MqgIdM2SdF8t40k/ATfuD2K1ZmumJ508I3gF39jgTnPzD4C8quswrMQ3bzfvKC3klXRlB
C0yoArn+0QA3cf2B9T4zJ2qnvgotVbeK/b1OJRNj6Poeo+SsWNc/A5mw7lGScnDgL3yfwCm1gQXa
QKfOt5x+7GqhWDw10q+bJpJlI10FfzAnhMF9etSqSeURBRW5AQ0EVduM9QEIAL53hJ5bZJ7oEDCn
aY+SCzt9QsAfnFTAnZJQrvkvusJzrTQ088eUQmAjvxkfRqnv981fFwGnh2+I1Ktm698UAZS9Jt8y
jak9wWUICKQO5QUt5k8cHwldQXNXVXFa+TpQWQR5yW1a9okjh5o/3d4cBt1yZPUJJyLKY43Wvptb
6EuEsScO2DnRkh5wSMDQ7dTooddJCmaq3LTjOleRFQbu9ij386Do6jzK69mJU56TfdcydkxkWF5N
ZLGnED3lq+hQNbe+8UI5tD2oP/3r5tXKgMy1R/XPvR/zbfwvx4FAKFOP01awLq4P3d/2xOkMu4Lu
9p315E87DOleYwxk+FoTqXEAEQEAAYkCPgQYAQIACQUCVduM9QIbLgEpCRDny6+OAR23k8BdIAQZ
AQIABgUCVduM9QAKCRAID0JGyHtSGmqYB/4m4rJbbWa7dBJ8VqRU7ZKnNRDR9CVhEGipBmpDGRYu
lEimOPzLUX/ZXZmTZzgemeXLBaJJlWnopVUWuAsyjQuZAfdd8nHkGRHG0/DGum0l4sKTta3OPGHN
C1z1dAcQ1RCr9bTD3PxjLBczdGqhzw71trkQRBRdtPiUchltPMIyjUHqVJ0xmg0hPqFic0fICsr0
YwKoz3h9+QEcZHvsjSZjgydKvfLYcm+4DDMCCqcHuJrbXJKUWmJcXR0y/+HQONGrGJ5xWdO+6eJi
oPn2jVMnXCm4EKc7fcLFrz/LKmJ8seXhxjM3EdFtylBGCrx3xdK0f+JDNQaC/rhUb5V2XuX6VwoH
/AtY+XsKVYRfNIupLOUcf/srsm3IXT4SXWVomOc9hjGQiJ3rraIbADsc+6bCAr4XNZS7moViAAcI
PXFv3m3WfUlnG/om78UjQqyVACRZqqAGmuPq+TSkRUCpt9h+A39LQWkojHqyob3cyLgy6z9Q557O
9uK3lQozbw2gH9zC0RqnePl+rsWIUU/ga16fH6pWc1uJiEBt8UZGypQ/E56/343epmYAe0a87sHx
8iDV+dNtDVKfPRENiLOOc19MmS+phmUyrbHqI91c0pmysYcJZCD3a502X1gpjFbPZcRtiTmGnUKd
OIu60YPNE4+h7u2CfYyFPu3AlUaGNMBlvy6PEpU=`
TestPubKey2 = `mQENBFXbkJEBCADKb1ZvlT14XrJa2rTOe5924LQr2PTZlRv+651TXy33yEhelZ+V4sMrELN8fKEG
Zy1kNixmbq3MCF/671k3LigHA7VrOaH9iiQgr6IIq2MeIkUYKZ27C992vQkYLjbYUG8+zl5h69S4
0Ixm0yL0M54XOJ0gm+maEK1ZESKTUlDNkIS7l0jLZSYwfUeGXSEt6FWs8OgbyRTaHw4PDHrDEE9e
Q67K6IZ3YMhPOL4fVk4Jwrp5R/RwiklT+lNozWEyFVwPFH4MeQMs9nMbt+fWlTzEA7tI4acI9yDk
Cm1yN2R9rmY0UjODRiJw6z6sLV2T+Pf32n3MNSUOYczOjZa4VBwjABEBAAG0EFZhdWx0IFRlc3Qg
S2V5IDKJATgEEwECACIFAlXbkJECGy8GCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEOuDLGfr
XolXqz4H/28IuoRxGKoJ064YHjPkkpoddW6zdzzNfHipZnNfEUiTEls4qF1IB81M2xqfiXIFRIdO
2kaLkRPFhO0hRxbtI6VuZYLgG3QCaXhxW6GyFa5zKABqhb5ojexdnAYRswaHV201ZCclj9rnJN1P
Ag0Rz6MdX/w1euEWktQxWKo42oZKyx8oT9p6lrv5KRmGkdrg8K8ARmRILjmwuBAgJM0eXBZHNGWX
elk4YmOgnAAcZA6ZAo1G+8Pg6pKKP61ETewuCg3/u7N0vDttB+ZXqF88W9jAYlvdgbTtajNF5IDY
DjTzWfeCaIB18F9gOzXq15SwWeDDI+CU9Nmq358IzXlxk4e5AQ0EVduQkQEIAOjZV5tbpfIh5Qef
pIp2dpGMVfpgPj4RNc15CyFnb8y6dhCrdybkY9GveXJe4F3GNYnSfB42cgxrfhizX3LakmZQ/SAg
+YO5KxfCIN7Q9LPNeTgPsZZT6h8lVuXUxOFKXfRaR3/tGF5xE3e5CoZRsHV/c92h3t1LdJNOnC5m
UKIPO4zDxiw/C2T2q3rP1kmIMaOH724kEH5A+xcp1cBHyt0tdHtIWuQv6joTJzujqViRhlCwQYzQ
SKpSBxwhBsorPvyinZI/ZXA4XXZc5RoMqV9rikedrb1rENO8JOuPu6tMS+znFu67skq2gFFZwCQW
IjdHm+2ukE+PE580WAWudyMAEQEAAYkCPgQYAQIACQUCVduQkQIbLgEpCRDrgyxn616JV8BdIAQZ
AQIABgUCVduQkQAKCRArYtevdF38xtzgB/4zVzozBpVOnagRkA7FDsHo36xX60Lik+ew0m28ueDD
hnV3bXQsCvn/6wiCVWqLOTDeYCPlyTTpEMyk8zwdCICW6MgSkVHWcEDOrRqIrqm86rirjTGjJSgQ
e3l4CqJvkn6jybShYoBk1OZZV6vVv9hPTXXv9E6dLKoEW5YZBrrF+VC0w1iOIvaAQ+QXph20eV4K
BIrp/bhG6PdnigKxuBZ79cdqDnXIzT9UiIa6LYpR0rbeg+7BmuZTTPS8t+41hIiKS+UZFdKa67eY
ENtyOmEMWOFCLLRJGxkleukchiMJ70rknloZXsvJIweXBzSZ6m7mJQBgaig/L/dXyjv6+j2pNB4H
/1trYUtJjXQKHmqlgCmpCkHt3g7JoxWvglnDNmE6q3hIWuVIYQpnzZy1g05+X9Egwc1WVpBB02H7
PkUZTfpaP/L6DLneMmSKPhZE3I+lPIPjwrxqh6xy5uQezcWkJTNKvPWF4FJzrVvx7XTPjfGvOB0U
PEnjvtZTp5yOhTeZK7DgIEtb/Wcrqs+iRArQKboM930ORSZhwvGK3F9V/gMDpIrvge5vDFsTEYQd
w/2epIewH0L/FUb/6jBRcVEpGo9Ayg+Jnhq14GOGcd1y9oMZ48kYVLVBTA9tQ+82WE8Bch7uFPj4
MFOMVRn1dc3qdXlg3mimA+iK7tABQfG0RJ9YzWs=`
TestPubKey3 = `mQENBFXbkiMBCACiHW4/VI2JkfvSEINddS7vE6wEu5e1leNQDaLUh6PrATQZS2a4Q6kRE6WlJumj
6wCeN753Cm93UGQl2Bi3USIEeArIZnPTcocrckOVXxtoLBNKXgqKvEsDXgfw8A+doSfXoDm/3Js4
Wy3WsYKNR9LaPuJZHnpjsFAJhvRVyhH4UFD+1RTSSefq1mozPfDdMoZeZNEpfhwt3DuTJs7RqcTH
CgR2CqhEHnOOE5jJUljHKYLCglE2+8dth1bZlQi4xly/VHZzP3Bn7wKeolK/ROP6VZz/e0xq/BKy
resmxvlBWZ1zWwqGIrV9b0uwYvGrh2hOd5C5+5oGaA2MGcjxwaLBABEBAAG0EFZhdWx0IFRlc3Qg
S2V5IDOJATgEEwECACIFAlXbkiMCGy8GCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEPR5S1b8
LcbdWjEH/2mhqC9a0Vk1IzOgxEoVxYVqVdvaxI0nTZOTfmcFYn4HQlQ+SLEoyNWe5jtkhx4k5uHi
pxwKHzOv02YM14NWC6bvKw2CQETLDPG4Cv8YMUmpho5tnMDdttIzp8HjyJRtHazU1uTes2/yuqh6
LHCejVJI0uST3RibquwdG3QjPP8Umxu+YC9+FOW2Kit/AQ8JluFDJdq3/wSX8VfYZrGdgmreE7KY
MolhCkzGSPj7oFygw8LqKoJvt9tCuBKhZMBuMv1sB5CoJIWdPoqOZc4U7L1XdqfKvFZR/RhuXgN1
lkI9MqrnLDpikL3Lk+ctLxWOjUCW8roqKoHZYBF7XPqdAfm5AQ0EVduSIwEIAOPcjd4QgbLlqIk3
s6BPRRyVzglTgUdf+I0rUDybaDJfJobZd8U6e4hkPvRoQ8tJefnz/qnD/63watAbJYcVTme40I3V
KDOmVGcyaDxiKP1disKqcEJd7XQiI72oAiXmEH0y+5UwnOMks/lwaAGDMGVRjHEXI6fiRPFsfTr8
7qvMJ3pW1OiOXVSezuBNTlmyJC7srQ1/nwxL337ev6D1zQZd3JuhcxLkHrUELLNwzhvcZ70vg645
jAmz8EdmvvoqEPPoHqKgP5AeHACOsTm953KHhgx3NYuGPU/RoIvugKt4Iq5nw7TWFTjPHGVF3GTQ
ry5CZ/AzXiL57hVEhDvmuT8AEQEAAYkCPgQYAQIACQUCVduSIwIbLgEpCRD0eUtW/C3G3cBdIAQZ
AQIABgUCVduSIwAKCRAFI/9Nx3K5IPOFCACsZ/Z4s2LcEoA51TW+T5w+YevlIuq+332JtqNIpuGI
WpGxUxyDyPT0YQWr0SObBORYNr7RP8d/I2rbaFKyaDaKvRofYr+TwXy92phBo7pdEUamBpfrm/sr
+2BgAB2x3HWXp+IMdeVVhqQe8t4cnFm3c1fIdxADyiJuV5ge2Ml5gK5yNwqCQPh7U2RqC+lmVlMJ
GvWInIRn2mf6A7phDYNZfOz6dkar4yyh5r9rRgrZw88r/yIlrq/c6KRUIgnPMrFYEauggOceZ827
+jKkbKWFEuHtiCxW7kRAN25UfnGsPaF+NSGM2q1vCG4HiFydx6lMoXM0Shf8+ZwyrV/5BzAqpWwI
AJ37tEwC58Fboynly6OGOzgPS0xKnzkXMOtquTo0qEH/1bEUsBknn795BmZOTf4oBC5blN6qRv7c
GSP00i+sxql1NhTjJcjJtfOPUzgfW+Af/+HR648z4c7c6MCjDFKnk8ZkoGLRU7ISjenkNFzvu2bj
lxJkil0uJDlLPbbX80ojzV1GS9g+ZxVPR+68N1QLl2FU6zsfg34upmLLHG8VG4vExzgyNkOwfTYv
dgyRNTjnuPue6H12fZZ9uCNeG52v7lR3eoQcCxBOniwgipB8UJ52RWXblwxzCtGtDi/EWB3zLTUn
puKcgucA0LotbihSMxhDylaARfVO1QV6csabM/g=`
TestAAPubKey1 = `-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v1
mQENBFXbjPUBCADjNjCUQwfxKL+RR2GA6pv/1K+zJZ8UWIF9S0lk7cVIEfJiprzz
wiMwBS5cD0darGin1FHvIWOZxujA7oW0O2TUuatqI3aAYDTfRYurh6iKLC+VS+F7
H+/mhfFvKmgr0Y5kDCF1j0T/063QZ84IRGucR/X43IY7kAtmxGXH0dYOCzOe5UBX
1fTn3mXGe2ImCDWBH7gOViynXmb6XNvXkP0fsF5St9jhO7mbZU9EFkv9O3t3EaUR
fHopsCVDOlCkFCw5ArY+DUORHRzoMX0PnkyQb5OzibkChzpg8hQssKeVGpuskTdz
5Q7PtdW71jXd4fFVzoNH8fYwRpziD2xNvi6HABEBAAG0EFZhdWx0IFRlc3QgS2V5
IDGJATgEEwECACIFAlXbjPUCGy8GCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJ
EOfLr44BHbeTo+sH/i7bapIgPnZsJ81hmxPj4W12uvunksGJiC7d4hIHsG7kmJRT
JfjECi+AuTGeDwBy84TDcRaOB6e79fj65Fg6HgSahDUtKJbGxj/lWzmaBuTzlN3C
Ee8cMwIPqPT2kajJVdOyrvkyuFOdPFOEA7bdCH0MqgIdM2SdF8t40k/ATfuD2K1Z
mumJ508I3gF39jgTnPzD4C8quswrMQ3bzfvKC3klXRlBC0yoArn+0QA3cf2B9T4z
J2qnvgotVbeK/b1OJRNj6Poeo+SsWNc/A5mw7lGScnDgL3yfwCm1gQXaQKfOt5x+
7GqhWDw10q+bJpJlI10FfzAnhMF9etSqSeURBRW5AQ0EVduM9QEIAL53hJ5bZJ7o
EDCnaY+SCzt9QsAfnFTAnZJQrvkvusJzrTQ088eUQmAjvxkfRqnv981fFwGnh2+I
1Ktm698UAZS9Jt8yjak9wWUICKQO5QUt5k8cHwldQXNXVXFa+TpQWQR5yW1a9okj
h5o/3d4cBt1yZPUJJyLKY43Wvptb6EuEsScO2DnRkh5wSMDQ7dTooddJCmaq3LTj
OleRFQbu9ij386Do6jzK69mJU56TfdcydkxkWF5NZLGnED3lq+hQNbe+8UI5tD2o
P/3r5tXKgMy1R/XPvR/zbfwvx4FAKFOP01awLq4P3d/2xOkMu4Lu9p315E87DOle
Ywxk+FoTqXEAEQEAAYkCPgQYAQIACQUCVduM9QIbLgEpCRDny6+OAR23k8BdIAQZ
AQIABgUCVduM9QAKCRAID0JGyHtSGmqYB/4m4rJbbWa7dBJ8VqRU7ZKnNRDR9CVh
EGipBmpDGRYulEimOPzLUX/ZXZmTZzgemeXLBaJJlWnopVUWuAsyjQuZAfdd8nHk
GRHG0/DGum0l4sKTta3OPGHNC1z1dAcQ1RCr9bTD3PxjLBczdGqhzw71trkQRBRd
tPiUchltPMIyjUHqVJ0xmg0hPqFic0fICsr0YwKoz3h9+QEcZHvsjSZjgydKvfLY
cm+4DDMCCqcHuJrbXJKUWmJcXR0y/+HQONGrGJ5xWdO+6eJioPn2jVMnXCm4EKc7
fcLFrz/LKmJ8seXhxjM3EdFtylBGCrx3xdK0f+JDNQaC/rhUb5V2XuX6VwoH/AtY
+XsKVYRfNIupLOUcf/srsm3IXT4SXWVomOc9hjGQiJ3rraIbADsc+6bCAr4XNZS7
moViAAcIPXFv3m3WfUlnG/om78UjQqyVACRZqqAGmuPq+TSkRUCpt9h+A39LQWko
jHqyob3cyLgy6z9Q557O9uK3lQozbw2gH9zC0RqnePl+rsWIUU/ga16fH6pWc1uJ
iEBt8UZGypQ/E56/343epmYAe0a87sHx8iDV+dNtDVKfPRENiLOOc19MmS+phmUy
rbHqI91c0pmysYcJZCD3a502X1gpjFbPZcRtiTmGnUKdOIu60YPNE4+h7u2CfYyF
Pu3AlUaGNMBlvy6PEpU=
=NUTS
-----END PGP PUBLIC KEY BLOCK-----`
)

71
helper/xor/xor.go Normal file
View File

@ -0,0 +1,71 @@
package xor
import (
"crypto/rand"
"encoding/base64"
"fmt"
)
func GenerateRandBytes(length int) ([]byte, error) {
if length < 0 {
return nil, fmt.Errorf("length must be >= 0")
}
buf := make([]byte, length)
if length == 0 {
return buf, nil
}
n, err := rand.Read(buf)
if err != nil {
return nil, err
}
if n != length {
return nil, fmt.Errorf("unable to read %d bytes; only read %d", length, n)
}
return buf, nil
}
func XORBuffers(a, b []byte) ([]byte, error) {
if len(a) != len(b) {
return nil, fmt.Errorf("length of buffers is not equivalent: %d != %d", len(a), len(b))
}
buf := make([]byte, len(a))
for i, _ := range a {
buf[i] = a[i] ^ b[i]
}
return buf, nil
}
func XORBase64(a, b string) ([]byte, error) {
aBytes, err := base64.StdEncoding.DecodeString(a)
if err != nil {
return nil, fmt.Errorf("error decoding first base64 value: %v", err)
}
if aBytes == nil || len(aBytes) == 0 {
return nil, fmt.Errorf("decoded first base64 value is nil or empty")
}
bBytes, err := base64.StdEncoding.DecodeString(b)
if err != nil {
return nil, fmt.Errorf("error decoding second base64 value: %v", err)
}
if bBytes == nil || len(bBytes) == 0 {
return nil, fmt.Errorf("decoded second base64 value is nil or empty")
}
if len(aBytes) != len(bBytes) {
return nil, fmt.Errorf("decoded values are not same length: %d != %d", len(aBytes), len(bBytes))
}
buf := make([]byte, len(aBytes))
for i, _ := range aBytes {
buf[i] = aBytes[i] ^ bBytes[i]
}
return buf, nil
}

View File

@ -41,6 +41,8 @@ func Handler(core *vault.Core) http.Handler {
mux.Handle("/v1/sys/health", handleSysHealth(core))
mux.Handle("/v1/sys/rotate", proxySysRequest(core))
mux.Handle("/v1/sys/key-status", proxySysRequest(core))
mux.Handle("/v1/sys/root-generation/attempt", handleSysRootGenerationInit(core))
mux.Handle("/v1/sys/root-generation/update", handleSysRootGenerationUpdate(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))

164
http/sys_root_generation.go Normal file
View File

@ -0,0 +1,164 @@
package http
import (
"encoding/hex"
"errors"
"fmt"
"net/http"
"github.com/hashicorp/vault/vault"
)
func handleSysRootGenerationInit(core *vault.Core) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
handleSysRootGenerationInitGet(core, w, r)
case "POST", "PUT":
handleSysRootGenerationInitPut(core, w, r)
case "DELETE":
handleSysRootGenerationInitDelete(core, w, r)
default:
respondError(w, http.StatusMethodNotAllowed, nil)
}
})
}
func handleSysRootGenerationInitGet(core *vault.Core, w http.ResponseWriter, r *http.Request) {
// Get the current seal configuration
sealConfig, err := core.SealConfig()
if err != nil {
respondError(w, http.StatusInternalServerError, err)
return
}
if sealConfig == nil {
respondError(w, http.StatusBadRequest, fmt.Errorf(
"server is not yet initialized"))
return
}
// Get the generation configuration
generationConfig, err := core.RootGenerationConfiguration()
if err != nil {
respondError(w, http.StatusInternalServerError, err)
return
}
// Get the progress
progress, err := core.RootGenerationProgress()
if err != nil {
respondError(w, http.StatusInternalServerError, err)
return
}
// Format the status
status := &RootGenerationStatusResponse{
Started: false,
Progress: progress,
Required: sealConfig.SecretThreshold,
Complete: false,
}
if generationConfig != nil {
status.Nonce = generationConfig.Nonce
status.Started = true
status.PGPFingerprint = generationConfig.PGPFingerprint
}
respondOk(w, status)
}
func handleSysRootGenerationInitPut(core *vault.Core, w http.ResponseWriter, r *http.Request) {
// Parse the request
var req RootGenerationInitRequest
if err := parseRequest(r, &req); err != nil {
respondError(w, http.StatusBadRequest, err)
return
}
if len(req.OTP) > 0 && len(req.PGPKey) > 0 {
respondError(w, http.StatusBadRequest, fmt.Errorf("only one of \"otp\" and \"pgp_key\" must be specified"))
return
}
// Initialize the generation
err := core.RootGenerationInit(req.OTP, req.PGPKey)
if err != nil {
respondError(w, http.StatusBadRequest, err)
return
}
respondOk(w, nil)
}
func handleSysRootGenerationInitDelete(core *vault.Core, w http.ResponseWriter, r *http.Request) {
err := core.RootGenerationCancel()
if err != nil {
respondError(w, http.StatusInternalServerError, err)
return
}
respondOk(w, nil)
}
func handleSysRootGenerationUpdate(core *vault.Core) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Parse the request
var req RootGenerationUpdateRequest
if err := parseRequest(r, &req); err != nil {
respondError(w, http.StatusBadRequest, err)
return
}
if req.Key == "" {
respondError(
w, http.StatusBadRequest,
errors.New("'key' must specified in request body as JSON"))
return
}
// Decode the key, which is hex encoded
key, err := hex.DecodeString(req.Key)
if err != nil {
respondError(
w, http.StatusBadRequest,
errors.New("'key' must be a valid hex-string"))
return
}
// Use the key to make progress on root generation
result, err := core.RootGenerationUpdate(key, req.Nonce)
if err != nil {
respondError(w, http.StatusBadRequest, err)
return
}
resp := &RootGenerationStatusResponse{
Complete: result.Progress == result.Required,
Nonce: req.Nonce,
Progress: result.Progress,
Required: result.Required,
Started: true,
EncodedRootToken: result.EncodedRootToken,
PGPFingerprint: result.PGPFingerprint,
}
respondOk(w, resp)
})
}
type RootGenerationInitRequest struct {
OTP string `json:"otp"`
PGPKey string `json:"pgp_key"`
}
type RootGenerationStatusResponse struct {
Nonce string `json:"nonce"`
Started bool `json:"started"`
Progress int `json:"progress"`
Required int `json:"required"`
Complete bool `json:"complete"`
EncodedRootToken string `json:"encoded_root_token"`
PGPFingerprint string `json:"pgp_fingerprint"`
}
type RootGenerationUpdateRequest struct {
Nonce string
Key string
}

View File

@ -0,0 +1,357 @@
package http
import (
"encoding/base64"
"encoding/hex"
"net/http"
"reflect"
"testing"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/helper/pgpkeys"
"github.com/hashicorp/vault/helper/xor"
"github.com/hashicorp/vault/vault"
)
func TestSysRootGenerationInit_Status(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := TestServer(t, core)
defer ln.Close()
TestServerAuth(t, addr, token)
resp, err := http.Get(addr + "/v1/sys/root-generation/attempt")
if err != nil {
t.Fatalf("err: %s", err)
}
var actual map[string]interface{}
expected := map[string]interface{}{
"started": false,
"progress": float64(0),
"required": float64(1),
"complete": false,
"encoded_root_token": "",
"pgp_fingerprint": "",
}
testResponseStatus(t, resp, 200)
testResponseBody(t, resp, &actual)
expected["nonce"] = actual["nonce"]
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("\nexpected: %#v\nactual: %#v", expected, actual)
}
}
func TestSysRootGenerationInit_Setup_OTP(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := TestServer(t, core)
defer ln.Close()
TestServerAuth(t, addr, token)
otpBytes, err := xor.GenerateRandBytes(16)
if err != nil {
t.Fatal(err)
}
otp := base64.StdEncoding.EncodeToString(otpBytes)
resp := testHttpPut(t, token, addr+"/v1/sys/root-generation/attempt", map[string]interface{}{
"otp": otp,
})
testResponseStatus(t, resp, 204)
resp = testHttpGet(t, token, addr+"/v1/sys/root-generation/attempt")
var actual map[string]interface{}
expected := map[string]interface{}{
"started": true,
"progress": float64(0),
"required": float64(1),
"complete": false,
"encoded_root_token": "",
"pgp_fingerprint": "",
}
testResponseStatus(t, resp, 200)
testResponseBody(t, resp, &actual)
expected["nonce"] = actual["nonce"]
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("\nexpected: %#v\nactual: %#v", expected, actual)
}
}
func TestSysRootGenerationInit_Setup_PGP(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := TestServer(t, core)
defer ln.Close()
TestServerAuth(t, addr, token)
resp := testHttpPut(t, token, addr+"/v1/sys/root-generation/attempt", map[string]interface{}{
"pgp_key": pgpkeys.TestPubKey1,
})
testResponseStatus(t, resp, 204)
resp = testHttpGet(t, token, addr+"/v1/sys/root-generation/attempt")
var actual map[string]interface{}
expected := map[string]interface{}{
"started": true,
"progress": float64(0),
"required": float64(1),
"complete": false,
"encoded_root_token": "",
"pgp_fingerprint": "816938b8a29146fbe245dd29e7cbaf8e011db793",
}
testResponseStatus(t, resp, 200)
testResponseBody(t, resp, &actual)
expected["nonce"] = actual["nonce"]
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("\nexpected: %#v\nactual: %#v", expected, actual)
}
}
func TestSysRootGenerationInit_Cancel(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := TestServer(t, core)
defer ln.Close()
TestServerAuth(t, addr, token)
otpBytes, err := xor.GenerateRandBytes(16)
if err != nil {
t.Fatal(err)
}
otp := base64.StdEncoding.EncodeToString(otpBytes)
resp := testHttpPut(t, token, addr+"/v1/sys/root-generation/attempt", map[string]interface{}{
"otp": otp,
})
resp = testHttpDelete(t, token, addr+"/v1/sys/root-generation/attempt")
testResponseStatus(t, resp, 204)
resp, err = http.Get(addr + "/v1/sys/root-generation/attempt")
if err != nil {
t.Fatalf("err: %s", err)
}
var actual map[string]interface{}
expected := map[string]interface{}{
"started": false,
"progress": float64(0),
"required": float64(1),
"complete": false,
"encoded_root_token": "",
"pgp_fingerprint": "",
}
testResponseStatus(t, resp, 200)
testResponseBody(t, resp, &actual)
expected["nonce"] = actual["nonce"]
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("\nexpected: %#v\nactual: %#v", expected, actual)
}
}
func TestSysRootGeneration_badKey(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := TestServer(t, core)
defer ln.Close()
TestServerAuth(t, addr, token)
otpBytes, err := xor.GenerateRandBytes(16)
if err != nil {
t.Fatal(err)
}
otp := base64.StdEncoding.EncodeToString(otpBytes)
resp := testHttpPut(t, token, addr+"/v1/sys/root-generation/update", map[string]interface{}{
"key": "0123",
"otp": otp,
})
testResponseStatus(t, resp, 400)
}
func TestSysRootGeneration_ReInitUpdate(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := TestServer(t, core)
defer ln.Close()
TestServerAuth(t, addr, token)
otpBytes, err := xor.GenerateRandBytes(16)
if err != nil {
t.Fatal(err)
}
otp := base64.StdEncoding.EncodeToString(otpBytes)
resp := testHttpPut(t, token, addr+"/v1/sys/root-generation/attempt", map[string]interface{}{
"otp": otp,
})
testResponseStatus(t, resp, 204)
resp = testHttpDelete(t, token, addr+"/v1/sys/root-generation/attempt")
testResponseStatus(t, resp, 204)
resp = testHttpPut(t, token, addr+"/v1/sys/root-generation/attempt", map[string]interface{}{
"pgp_key": pgpkeys.TestPubKey1,
})
testResponseStatus(t, resp, 204)
}
func TestSysRootGeneration_Update_OTP(t *testing.T) {
core, master, token := vault.TestCoreUnsealed(t)
ln, addr := TestServer(t, core)
defer ln.Close()
TestServerAuth(t, addr, token)
otpBytes, err := xor.GenerateRandBytes(16)
if err != nil {
t.Fatal(err)
}
otp := base64.StdEncoding.EncodeToString(otpBytes)
resp := testHttpPut(t, token, addr+"/v1/sys/root-generation/attempt", map[string]interface{}{
"otp": otp,
})
testResponseStatus(t, resp, 204)
// We need to get the nonce first before we update
resp, err = http.Get(addr + "/v1/sys/root-generation/attempt")
if err != nil {
t.Fatalf("err: %s", err)
}
var rootGenerationStatus map[string]interface{}
testResponseStatus(t, resp, 200)
testResponseBody(t, resp, &rootGenerationStatus)
resp = testHttpPut(t, token, addr+"/v1/sys/root-generation/update", map[string]interface{}{
"nonce": rootGenerationStatus["nonce"].(string),
"key": hex.EncodeToString(master),
})
var actual map[string]interface{}
expected := map[string]interface{}{
"complete": true,
"nonce": rootGenerationStatus["nonce"].(string),
"progress": float64(1),
"required": float64(1),
"started": true,
"pgp_fingerprint": "",
}
testResponseStatus(t, resp, 200)
testResponseBody(t, resp, &actual)
if actual["encoded_root_token"] == nil {
t.Fatalf("no encoded root token found in response")
}
expected["encoded_root_token"] = actual["encoded_root_token"]
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("\nexpected: %#v\nactual: %#v", expected, actual)
}
decodedToken, err := xor.XORBase64(otp, actual["encoded_root_token"].(string))
if err != nil {
t.Fatal(err)
}
newRootToken, err := uuid.FormatUUID(decodedToken)
if err != nil {
t.Fatal(err)
}
actual = map[string]interface{}{}
expected = map[string]interface{}{
"id": newRootToken,
"display_name": "root",
"meta": interface{}(nil),
"num_uses": float64(0),
"policies": []interface{}{"root"},
"orphan": true,
"ttl": float64(0),
"path": "auth/token/root",
}
resp = testHttpGet(t, newRootToken, addr+"/v1/auth/token/lookup-self")
testResponseStatus(t, resp, 200)
testResponseBody(t, resp, &actual)
expected["creation_time"] = actual["data"].(map[string]interface{})["creation_time"]
if !reflect.DeepEqual(actual["data"], expected) {
t.Fatalf("\nexpected: %#v\nactual: %#v", expected, actual["data"])
}
}
func TestSysRootGeneration_Update_PGP(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/root-generation/attempt", map[string]interface{}{
"pgp_key": pgpkeys.TestPubKey1,
})
testResponseStatus(t, resp, 204)
// We need to get the nonce first before we update
resp, err := http.Get(addr + "/v1/sys/root-generation/attempt")
if err != nil {
t.Fatalf("err: %s", err)
}
var rootGenerationStatus map[string]interface{}
testResponseStatus(t, resp, 200)
testResponseBody(t, resp, &rootGenerationStatus)
resp = testHttpPut(t, token, addr+"/v1/sys/root-generation/update", map[string]interface{}{
"nonce": rootGenerationStatus["nonce"].(string),
"key": hex.EncodeToString(master),
})
var actual map[string]interface{}
expected := map[string]interface{}{
"complete": true,
"nonce": rootGenerationStatus["nonce"].(string),
"progress": float64(1),
"required": float64(1),
"started": true,
"pgp_fingerprint": "816938b8a29146fbe245dd29e7cbaf8e011db793",
}
testResponseStatus(t, resp, 200)
testResponseBody(t, resp, &actual)
if actual["encoded_root_token"] == nil {
t.Fatalf("no encoded root token found in response")
}
expected["encoded_root_token"] = actual["encoded_root_token"]
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("\nexpected: %#v\nactual: %#v", expected, actual)
}
decodedTokenBuf, err := pgpkeys.DecryptBytes(actual["encoded_root_token"].(string), pgpkeys.TestPrivKey1)
if err != nil {
t.Fatal(err)
}
if decodedTokenBuf == nil {
t.Fatal("decoded root token buffer is nil")
}
newRootToken := decodedTokenBuf.String()
actual = map[string]interface{}{}
expected = map[string]interface{}{
"id": newRootToken,
"display_name": "root",
"meta": interface{}(nil),
"num_uses": float64(0),
"policies": []interface{}{"root"},
"orphan": true,
"ttl": float64(0),
"path": "auth/token/root",
}
resp = testHttpGet(t, newRootToken, addr+"/v1/auth/token/lookup-self")
testResponseStatus(t, resp, 200)
testResponseBody(t, resp, &actual)
expected["creation_time"] = actual["data"].(map[string]interface{})["creation_time"]
if !reflect.DeepEqual(actual["data"], expected) {
t.Fatalf("\nexpected: %#v\nactual: %#v", expected, actual["data"])
}
}

View File

@ -3,6 +3,7 @@ package vault
import (
"bytes"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
@ -213,6 +214,12 @@ type Core struct {
// the threshold number of parts is available.
unlockParts [][]byte
// rootGenerationProgress holds the shares until we reach enough
// to verify the master key
rootGenerationConfig *RootGenerationConfig
rootGenerationProgress [][]byte
rootGenerationLock sync.Mutex
// rekeyProgress holds the shares we have until we reach enough
// to verify the master key.
rekeyConfig *SealConfig
@ -869,7 +876,11 @@ func (c *Core) Initialize(config *SealConfig) (*InitResult, error) {
}
if len(config.PGPKeys) > 0 {
_, encryptedShares, err := pgpkeys.EncryptShares(results.SecretShares, config.PGPKeys)
hexEncodedShares := make([][]byte, len(results.SecretShares))
for i, _ := range results.SecretShares {
hexEncodedShares[i] = []byte(hex.EncodeToString(results.SecretShares[i]))
}
_, encryptedShares, err := pgpkeys.EncryptShares(hexEncodedShares, config.PGPKeys)
if err != nil {
return nil, err
}
@ -904,7 +915,7 @@ func (c *Core) Initialize(config *SealConfig) (*InitResult, error) {
}
// Generate a new root token
rootToken, err := c.tokenStore.rootToken()
rootToken, err := c.tokenStore.rootToken("")
if err != nil {
c.logger.Printf("[ERR] core: root token generation failed: %v", err)
return nil, err

View File

@ -13,7 +13,7 @@ import (
// mockExpiration returns a mock expiration manager
func mockExpiration(t *testing.T) *ExpirationManager {
_, ts, _ := mockTokenStore(t)
_, ts, _, _ := TestCoreWithTokenStore(t)
return ts.expiration
}
@ -121,7 +121,7 @@ func TestExpiration_Register(t *testing.T) {
func TestExpiration_RegisterAuth(t *testing.T) {
exp := mockExpiration(t)
root, err := exp.tokenStore.rootToken()
root, err := exp.tokenStore.rootToken("")
if err != nil {
t.Fatalf("err: %v", err)
}
@ -141,7 +141,7 @@ func TestExpiration_RegisterAuth(t *testing.T) {
func TestExpiration_RegisterAuth_NoLease(t *testing.T) {
exp := mockExpiration(t)
root, err := exp.tokenStore.rootToken()
root, err := exp.tokenStore.rootToken("")
if err != nil {
t.Fatalf("err: %v", err)
}
@ -400,7 +400,7 @@ func TestExpiration_RevokeByToken(t *testing.T) {
func TestExpiration_RenewToken(t *testing.T) {
exp := mockExpiration(t)
root, err := exp.tokenStore.rootToken()
root, err := exp.tokenStore.rootToken("")
if err != nil {
t.Fatalf("err: %v", err)
}
@ -431,7 +431,7 @@ func TestExpiration_RenewToken(t *testing.T) {
func TestExpiration_RenewToken_NotRenewable(t *testing.T) {
exp := mockExpiration(t)
root, err := exp.tokenStore.rootToken()
root, err := exp.tokenStore.rootToken("")
if err != nil {
t.Fatalf("err: %v", err)
}
@ -688,7 +688,7 @@ func TestExpiration_revokeEntry(t *testing.T) {
func TestExpiration_revokeEntry_token(t *testing.T) {
exp := mockExpiration(t)
root, err := exp.tokenStore.rootToken()
root, err := exp.tokenStore.rootToken("")
if err != nil {
t.Fatalf("err: %v", err)
}

300
vault/root_generation.go Normal file
View File

@ -0,0 +1,300 @@
package vault
import (
"bytes"
"crypto/rand"
"encoding/base64"
"fmt"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/helper/pgpkeys"
"github.com/hashicorp/vault/helper/xor"
"github.com/hashicorp/vault/shamir"
)
// RootGenerationConfig holds the configuration for a root generation
// command.
type RootGenerationConfig struct {
Nonce string
PGPKey string
PGPFingerprint string
OTP string
}
// RootGenerationResult holds the result of a root generation update
// command
type RootGenerationResult struct {
Progress int
Required int
EncodedRootToken string
PGPFingerprint string
}
// RootGeneration is used to return the root generation progress (num shares)
func (c *Core) RootGenerationProgress() (int, error) {
c.stateLock.RLock()
defer c.stateLock.RUnlock()
if c.sealed {
return 0, ErrSealed
}
if c.standby {
return 0, ErrStandby
}
c.rootGenerationLock.Lock()
defer c.rootGenerationLock.Unlock()
return len(c.rootGenerationProgress), nil
}
// RootGenerationConfig is used to read the root generation configuration
// It stubbornly refuses to return the OTP if one is there.
func (c *Core) RootGenerationConfiguration() (*RootGenerationConfig, error) {
c.stateLock.RLock()
defer c.stateLock.RUnlock()
if c.sealed {
return nil, ErrSealed
}
if c.standby {
return nil, ErrStandby
}
c.rootGenerationLock.Lock()
defer c.rootGenerationLock.Unlock()
// Copy the config if any
var conf *RootGenerationConfig
if c.rootGenerationConfig != nil {
conf = new(RootGenerationConfig)
*conf = *c.rootGenerationConfig
conf.OTP = ""
}
return conf, nil
}
// RootGenerationInit is used to initialize the root generation settings
func (c *Core) RootGenerationInit(otp, pgpKey string) error {
var fingerprint string
switch {
case len(otp) > 0:
otpBytes, err := base64.StdEncoding.DecodeString(otp)
if err != nil {
return fmt.Errorf("error decoding base64 OTP value: %s", err)
}
if otpBytes == nil || len(otpBytes) != 16 {
return fmt.Errorf("decoded OTP value is invalid or wrong length")
}
case len(pgpKey) > 0:
fingerprints, err := pgpkeys.GetFingerprints([]string{pgpKey}, nil)
if err != nil {
return fmt.Errorf("error parsing PGP key: %s", err)
}
if len(fingerprints) != 1 || fingerprints[0] == "" {
return fmt.Errorf("could not acquire PGP key entity")
}
fingerprint = fingerprints[0]
default:
return fmt.Errorf("unreachable condition")
}
c.stateLock.RLock()
defer c.stateLock.RUnlock()
if c.sealed {
return ErrSealed
}
if c.standby {
return ErrStandby
}
c.rootGenerationLock.Lock()
defer c.rootGenerationLock.Unlock()
// Prevent multiple concurrent root generations
if c.rootGenerationConfig != nil {
return fmt.Errorf("root generation already in progress")
}
// Copy the configuration
generationNonce, err := uuid.GenerateUUID()
if err != nil {
return err
}
c.rootGenerationConfig = &RootGenerationConfig{
Nonce: generationNonce,
OTP: otp,
PGPKey: pgpKey,
PGPFingerprint: fingerprint,
}
c.logger.Printf("[INFO] core: root generation initialized (nonce: %s)",
c.rootGenerationConfig.Nonce)
return nil
}
// RootGenerationUpdate is used to provide a new key part
func (c *Core) RootGenerationUpdate(key []byte, nonce string) (*RootGenerationResult, error) {
// Verify the key length
min, max := c.barrier.KeyLength()
max += shamir.ShareOverhead
if len(key) < min {
return nil, &ErrInvalidKey{fmt.Sprintf("key is shorter than minimum %d bytes", min)}
}
if len(key) > max {
return nil, &ErrInvalidKey{fmt.Sprintf("key is longer than maximum %d bytes", max)}
}
// Get the seal configuration
config, err := c.SealConfig()
if err != nil {
return nil, err
}
// Ensure the barrier is initialized
if config == nil {
return nil, ErrNotInit
}
// Ensure we are already unsealed
c.stateLock.RLock()
defer c.stateLock.RUnlock()
if c.sealed {
return nil, ErrSealed
}
if c.standby {
return nil, ErrStandby
}
c.rootGenerationLock.Lock()
defer c.rootGenerationLock.Unlock()
// Ensure a rootGeneration is in progress
if c.rootGenerationConfig == nil {
return nil, fmt.Errorf("no root generation in progress")
}
if nonce != c.rootGenerationConfig.Nonce {
return nil, fmt.Errorf("incorrect nonce supplied; nonce for this root generation operation is %s", c.rootGenerationConfig.Nonce)
}
// Check if we already have this piece
for _, existing := range c.rootGenerationProgress {
if bytes.Equal(existing, key) {
return nil, nil
}
}
// Store this key
c.rootGenerationProgress = append(c.rootGenerationProgress, key)
progress := len(c.rootGenerationProgress)
// Check if we don't have enough keys to unlock
if len(c.rootGenerationProgress) < config.SecretThreshold {
c.logger.Printf("[DEBUG] core: cannot generate root, have %d of %d keys",
progress, config.SecretThreshold)
return &RootGenerationResult{
Progress: progress,
Required: config.SecretThreshold,
PGPFingerprint: c.rootGenerationConfig.PGPFingerprint,
}, nil
}
// Recover the master key
var masterKey []byte
if config.SecretThreshold == 1 {
masterKey = c.rootGenerationProgress[0]
c.rootGenerationProgress = nil
} else {
masterKey, err = shamir.Combine(c.rootGenerationProgress)
c.rootGenerationProgress = nil
if err != nil {
return nil, fmt.Errorf("failed to compute master key: %v", err)
}
}
// Verify the master key
if err := c.barrier.VerifyMaster(masterKey); err != nil {
c.logger.Printf("[ERR] core: root generation aborted, master key verification failed: %v", err)
return nil, err
}
// Generate the raw token bytes
buf := make([]byte, 16)
if _, err := rand.Read(buf); err != nil {
c.logger.Printf("failed to read random bytes: %v", err)
return nil, err
}
uuidStr, err := uuid.FormatUUID(buf)
if err != nil {
c.logger.Printf("error formatting token: %v", err)
return nil, err
}
var tokenBytes []byte
// Get the encoded value first so that if there is an error we don't create
// the root token.
switch {
case len(c.rootGenerationConfig.OTP) > 0:
// This function performs decoding checks so rather than decode the OTP,
// just encode the value we're passing in.
tokenBytes, err = xor.XORBase64(c.rootGenerationConfig.OTP, base64.StdEncoding.EncodeToString(buf))
if err != nil {
c.logger.Printf("[ERR] core: xor of root token failed: %v", err)
return nil, err
}
case len(c.rootGenerationConfig.PGPKey) > 0:
_, tokenBytesArr, err := pgpkeys.EncryptShares([][]byte{[]byte(uuidStr)}, []string{c.rootGenerationConfig.PGPKey})
if err != nil {
c.logger.Printf("[ERR] core: error encrypting new root token: %v", err)
return nil, err
}
tokenBytes = tokenBytesArr[0]
default:
return nil, fmt.Errorf("unreachable condition")
}
_, err = c.tokenStore.rootToken(uuidStr)
if err != nil {
c.logger.Printf("[ERR] core: root token generation failed: %v", err)
return nil, err
}
results := &RootGenerationResult{
Progress: progress,
Required: config.SecretThreshold,
EncodedRootToken: base64.StdEncoding.EncodeToString(tokenBytes),
PGPFingerprint: c.rootGenerationConfig.PGPFingerprint,
}
c.logger.Printf("[INFO] core: root generation finished (nonce: %s)",
c.rootGenerationConfig.Nonce)
c.rootGenerationProgress = nil
c.rootGenerationConfig = nil
return results, nil
}
// RootGenerationCancel is used to cancel an in-progress root generation
func (c *Core) RootGenerationCancel() error {
c.stateLock.RLock()
defer c.stateLock.RUnlock()
if c.sealed {
return ErrSealed
}
if c.standby {
return ErrStandby
}
c.rootGenerationLock.Lock()
defer c.rootGenerationLock.Unlock()
// Clear any progress or config
c.rootGenerationConfig = nil
c.rootGenerationProgress = nil
return nil
}

View File

@ -0,0 +1,292 @@
package vault
import (
"encoding/base64"
"testing"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/helper/pgpkeys"
"github.com/hashicorp/vault/helper/xor"
)
func TestCore_RootGeneration_Lifecycle(t *testing.T) {
c, master, _ := TestCoreUnsealed(t)
// Verify update not allowed
if _, err := c.RootGenerationUpdate(master, ""); err == nil {
t.Fatalf("no root generation in progress")
}
// Should be no progress
num, err := c.RootGenerationProgress()
if err != nil {
t.Fatalf("err: %v", err)
}
if num != 0 {
t.Fatalf("bad: %d", num)
}
// Should be no config
conf, err := c.RootGenerationConfiguration()
if err != nil {
t.Fatalf("err: %v", err)
}
if conf != nil {
t.Fatalf("bad: %v", conf)
}
// Cancel should be idempotent
err = c.RootGenerationCancel()
if err != nil {
t.Fatalf("err: %v", err)
}
otpBytes, err := xor.GenerateRandBytes(16)
if err != nil {
t.Fatal(err)
}
// Start a root generation
err = c.RootGenerationInit(base64.StdEncoding.EncodeToString(otpBytes), "")
if err != nil {
t.Fatalf("err: %v", err)
}
// Should get config
conf, err = c.RootGenerationConfiguration()
if err != nil {
t.Fatalf("err: %v", err)
}
// Cancel should be clear
err = c.RootGenerationCancel()
if err != nil {
t.Fatalf("err: %v", err)
}
// Should be no config
conf, err = c.RootGenerationConfiguration()
if err != nil {
t.Fatalf("err: %v", err)
}
if conf != nil {
t.Fatalf("bad: %v", conf)
}
}
func TestCore_RootGeneration_Init(t *testing.T) {
c, _, _ := TestCoreUnsealed(t)
otpBytes, err := xor.GenerateRandBytes(16)
if err != nil {
t.Fatal(err)
}
err = c.RootGenerationInit(base64.StdEncoding.EncodeToString(otpBytes), "")
if err != nil {
t.Fatalf("err: %v", err)
}
// Second should fail
err = c.RootGenerationInit("", pgpkeys.TestPubKey1)
if err == nil {
t.Fatalf("should fail")
}
}
func TestCore_RootGeneration_InvalidMaster(t *testing.T) {
c, master, _ := TestCoreUnsealed(t)
otpBytes, err := xor.GenerateRandBytes(16)
if err != nil {
t.Fatal(err)
}
err = c.RootGenerationInit(base64.StdEncoding.EncodeToString(otpBytes), "")
if err != nil {
t.Fatalf("err: %v", err)
}
// Fetch new config with generated nonce
rgconf, err := c.RootGenerationConfiguration()
if err != nil {
t.Fatalf("err: %v", err)
}
if rgconf == nil {
t.Fatalf("bad: no rekey config received")
}
// Provide the master (invalid)
master[0]++
_, err = c.RootGenerationUpdate(master, rgconf.Nonce)
if err == nil {
t.Fatalf("expected error")
}
}
func TestCore_RootGeneration_InvalidNonce(t *testing.T) {
c, master, _ := TestCoreUnsealed(t)
otpBytes, err := xor.GenerateRandBytes(16)
if err != nil {
t.Fatal(err)
}
err = c.RootGenerationInit(base64.StdEncoding.EncodeToString(otpBytes), "")
if err != nil {
t.Fatalf("err: %v", err)
}
// Provide the nonce (invalid)
_, err = c.RootGenerationUpdate(master, "abcd")
if err == nil {
t.Fatalf("expected error")
}
}
func TestCore_RootGeneration_Update_OTP(t *testing.T) {
c, master, _ := TestCoreUnsealed(t)
otpBytes, err := xor.GenerateRandBytes(16)
if err != nil {
t.Fatal(err)
}
otp := base64.StdEncoding.EncodeToString(otpBytes)
// Start a root generation
err = c.RootGenerationInit(otp, "")
if err != nil {
t.Fatalf("err: %v", err)
}
// Fetch new config with generated nonce
rkconf, err := c.RootGenerationConfiguration()
if err != nil {
t.Fatalf("err: %v", err)
}
if rkconf == nil {
t.Fatalf("bad: no root generation config received")
}
// Provide the master
result, err := c.RootGenerationUpdate(master, rkconf.Nonce)
if err != nil {
t.Fatalf("err: %v", err)
}
if result == nil {
t.Fatalf("Bad, result is nil")
}
encodedRootToken := result.EncodedRootToken
// Should be no progress
num, err := c.RootGenerationProgress()
if err != nil {
t.Fatalf("err: %v", err)
}
if num != 0 {
t.Fatalf("bad: %d", num)
}
// Should be no config
conf, err := c.RootGenerationConfiguration()
if err != nil {
t.Fatalf("err: %v", err)
}
if conf != nil {
t.Fatalf("bad: %v", conf)
}
tokenBytes, err := xor.XORBase64(encodedRootToken, otp)
if err != nil {
t.Fatal(err)
}
token, err := uuid.FormatUUID(tokenBytes)
if err != nil {
t.Fatal(err)
}
// Ensure that the token is a root token
te, err := c.tokenStore.Lookup(token)
if err != nil {
t.Fatalf("err: %v", err)
}
if te == nil {
t.Fatalf("token was nil")
}
if te.ID != token || te.Parent != "" ||
len(te.Policies) != 1 || te.Policies[0] != "root" {
t.Fatalf("bad: %#v", *te)
}
}
func TestCore_RootGeneration_Update_PGP(t *testing.T) {
c, master, _ := TestCoreUnsealed(t)
// Start a root generation
err := c.RootGenerationInit("", pgpkeys.TestPubKey1)
if err != nil {
t.Fatalf("err: %v", err)
}
// Fetch new config with generated nonce
rkconf, err := c.RootGenerationConfiguration()
if err != nil {
t.Fatalf("err: %v", err)
}
if rkconf == nil {
t.Fatalf("bad: no root generation config received")
}
// Provide the master
result, err := c.RootGenerationUpdate(master, rkconf.Nonce)
if err != nil {
t.Fatalf("err: %v", err)
}
if result == nil {
t.Fatalf("Bad, result is nil")
}
encodedRootToken := result.EncodedRootToken
// Should be no progress
num, err := c.RootGenerationProgress()
if err != nil {
t.Fatalf("err: %v", err)
}
if num != 0 {
t.Fatalf("bad: %d", num)
}
// Should be no config
conf, err := c.RootGenerationConfiguration()
if err != nil {
t.Fatalf("err: %v", err)
}
if conf != nil {
t.Fatalf("bad: %v", conf)
}
ptBuf, err := pgpkeys.DecryptBytes(encodedRootToken, pgpkeys.TestPrivKey1)
if err != nil {
t.Fatal(err)
}
if ptBuf == nil {
t.Fatal("Got nil plaintext key")
}
token := ptBuf.String()
// Ensure that the token is a root token
te, err := c.tokenStore.Lookup(token)
if err != nil {
t.Fatalf("err: %v", err)
}
if te == nil {
t.Fatalf("token was nil")
}
if te.ID != token || te.Parent != "" ||
len(te.Policies) != 1 || te.Policies[0] != "root" {
t.Fatalf("bad: %#v", *te)
}
}

View File

@ -4,13 +4,16 @@ import (
"bytes"
"crypto/sha256"
"fmt"
"log"
"net"
"os"
"os/exec"
"testing"
"time"
"golang.org/x/crypto/ssh"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/audit"
"github.com/hashicorp/vault/helper/salt"
"github.com/hashicorp/vault/logical"
@ -144,6 +147,40 @@ func TestCoreUnsealed(t *testing.T) (*Core, []byte, string) {
return core, key, token
}
// TestCoreWithTokenStore returns an in-memory core that has a token store
// mounted, so that logical token functions can be used
func TestCoreWithTokenStore(t *testing.T) (*Core, *TokenStore, []byte, string) {
c, key, root := TestCoreUnsealed(t)
me := &MountEntry{
Path: "token/",
Type: "token",
Description: "token based credentials",
}
meUUID, err := uuid.GenerateUUID()
if err != nil {
t.Fatal(err)
}
me.UUID = meUUID
view := NewBarrierView(c.barrier, credentialBarrierPrefix+me.UUID+"/")
tokenstore, _ := c.newCredentialBackend("token", c.mountEntrySysView(me), view, nil)
ts := tokenstore.(*TokenStore)
router := NewRouter()
router.Mount(ts, "auth/token/", &MountEntry{UUID: ""}, ts.view)
subview := c.systemBarrierView.SubView(expirationSubPath)
logger := log.New(os.Stderr, "", log.LstdFlags)
exp := NewExpirationManager(router, subview, ts, logger)
ts.SetExpirationManager(exp)
return c, ts, key, root
}
// TestKeyCopy is a silly little function to just copy the key so that
// it can be used with Unseal easily.
func TestKeyCopy(key []byte) []byte {

View File

@ -290,8 +290,9 @@ func (ts *TokenStore) SaltID(id string) string {
}
// RootToken is used to generate a new token with root privileges and no parent
func (ts *TokenStore) rootToken() (*TokenEntry, error) {
func (ts *TokenStore) rootToken(id string) (*TokenEntry, error) {
te := &TokenEntry{
ID: id,
Policies: []string{"root"},
Path: "auth/token/root",
DisplayName: "root",

View File

@ -1,13 +1,10 @@
package vault
import (
"log"
"os"
"reflect"
"testing"
"time"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/logical"
)
@ -21,42 +18,10 @@ func getBackendConfig(c *Core) *logical.BackendConfig {
}
}
func mockTokenStore(t *testing.T) (*Core, *TokenStore, string) {
c, _, root := TestCoreUnsealed(t)
me := &MountEntry{
Path: "token/",
Type: "token",
Description: "token based credentials",
}
uuid, err := uuid.GenerateUUID()
if err != nil {
t.Fatal(err)
}
me.UUID = uuid
view := NewBarrierView(c.barrier, credentialBarrierPrefix+me.UUID+"/")
tokenstore, _ := c.newCredentialBackend("token", c.mountEntrySysView(me), view, nil)
ts := tokenstore.(*TokenStore)
router := NewRouter()
router.Mount(ts, "auth/token/", &MountEntry{UUID: ""}, ts.view)
subview := c.systemBarrierView.SubView(expirationSubPath)
logger := log.New(os.Stderr, "", log.LstdFlags)
exp := NewExpirationManager(router, subview, ts, logger)
ts.SetExpirationManager(exp)
return c, ts, root
}
func TestTokenStore_RootToken(t *testing.T) {
_, ts, _ := mockTokenStore(t)
_, ts, _, _ := TestCoreWithTokenStore(t)
te, err := ts.rootToken()
te, err := ts.rootToken("")
if err != nil {
t.Fatalf("err: %v", err)
}
@ -74,7 +39,7 @@ func TestTokenStore_RootToken(t *testing.T) {
}
func TestTokenStore_CreateLookup(t *testing.T) {
c, ts, _ := mockTokenStore(t)
c, ts, _, _ := TestCoreWithTokenStore(t)
ent := &TokenEntry{Path: "test", Policies: []string{"dev", "ops"}}
if err := ts.create(ent); err != nil {
@ -109,7 +74,7 @@ func TestTokenStore_CreateLookup(t *testing.T) {
}
func TestTokenStore_CreateLookup_ProvidedID(t *testing.T) {
c, ts, _ := mockTokenStore(t)
c, ts, _, _ := TestCoreWithTokenStore(t)
ent := &TokenEntry{
ID: "foobarbaz",
@ -148,7 +113,7 @@ func TestTokenStore_CreateLookup_ProvidedID(t *testing.T) {
}
func TestTokenStore_UseToken(t *testing.T) {
_, ts, root := mockTokenStore(t)
_, ts, _, root := TestCoreWithTokenStore(t)
// Lookup the root token
ent, err := ts.Lookup(root)
@ -214,7 +179,7 @@ func TestTokenStore_UseToken(t *testing.T) {
}
func TestTokenStore_Revoke(t *testing.T) {
_, ts, _ := mockTokenStore(t)
_, ts, _, _ := TestCoreWithTokenStore(t)
ent := &TokenEntry{Path: "test", Policies: []string{"dev", "ops"}}
if err := ts.create(ent); err != nil {
@ -240,7 +205,7 @@ func TestTokenStore_Revoke(t *testing.T) {
}
func TestTokenStore_Revoke_Leases(t *testing.T) {
_, ts, _ := mockTokenStore(t)
_, ts, _, _ := TestCoreWithTokenStore(t)
// Mount a noop backend
noop := &NoopBackend{}
@ -290,7 +255,7 @@ func TestTokenStore_Revoke_Leases(t *testing.T) {
}
func TestTokenStore_Revoke_Orphan(t *testing.T) {
_, ts, _ := mockTokenStore(t)
_, ts, _, _ := TestCoreWithTokenStore(t)
ent := &TokenEntry{Path: "test", Policies: []string{"dev", "ops"}}
if err := ts.create(ent); err != nil {
@ -317,7 +282,7 @@ func TestTokenStore_Revoke_Orphan(t *testing.T) {
}
func TestTokenStore_RevokeTree(t *testing.T) {
_, ts, _ := mockTokenStore(t)
_, ts, _, _ := TestCoreWithTokenStore(t)
ent1 := &TokenEntry{}
if err := ts.create(ent1); err != nil {
@ -361,7 +326,7 @@ func TestTokenStore_RevokeTree(t *testing.T) {
}
func TestTokenStore_RevokeSelf(t *testing.T) {
_, ts, _ := mockTokenStore(t)
_, ts, _, _ := TestCoreWithTokenStore(t)
ent1 := &TokenEntry{}
if err := ts.create(ent1); err != nil {
@ -404,7 +369,7 @@ func TestTokenStore_RevokeSelf(t *testing.T) {
}
func TestTokenStore_HandleRequest_CreateToken_DisplayName(t *testing.T) {
_, ts, root := mockTokenStore(t)
_, ts, _, root := TestCoreWithTokenStore(t)
req := logical.TestRequest(t, logical.UpdateOperation, "create")
req.ClientToken = root
@ -434,7 +399,7 @@ func TestTokenStore_HandleRequest_CreateToken_DisplayName(t *testing.T) {
}
func TestTokenStore_HandleRequest_CreateToken_NumUses(t *testing.T) {
_, ts, root := mockTokenStore(t)
_, ts, _, root := TestCoreWithTokenStore(t)
req := logical.TestRequest(t, logical.UpdateOperation, "create")
req.ClientToken = root
@ -465,7 +430,7 @@ func TestTokenStore_HandleRequest_CreateToken_NumUses(t *testing.T) {
}
func TestTokenStore_HandleRequest_CreateToken_NumUses_Invalid(t *testing.T) {
_, ts, root := mockTokenStore(t)
_, ts, _, root := TestCoreWithTokenStore(t)
req := logical.TestRequest(t, logical.UpdateOperation, "create")
req.ClientToken = root
@ -478,7 +443,7 @@ func TestTokenStore_HandleRequest_CreateToken_NumUses_Invalid(t *testing.T) {
}
func TestTokenStore_HandleRequest_CreateToken_NumUses_Restricted(t *testing.T) {
_, ts, root := mockTokenStore(t)
_, ts, _, root := TestCoreWithTokenStore(t)
req := logical.TestRequest(t, logical.UpdateOperation, "create")
req.ClientToken = root
@ -498,7 +463,7 @@ func TestTokenStore_HandleRequest_CreateToken_NumUses_Restricted(t *testing.T) {
}
func TestTokenStore_HandleRequest_CreateToken_NoPolicy(t *testing.T) {
_, ts, root := mockTokenStore(t)
_, ts, _, root := TestCoreWithTokenStore(t)
req := logical.TestRequest(t, logical.UpdateOperation, "create")
req.ClientToken = root
@ -527,7 +492,7 @@ func TestTokenStore_HandleRequest_CreateToken_NoPolicy(t *testing.T) {
}
func TestTokenStore_HandleRequest_CreateToken_BadParent(t *testing.T) {
_, ts, _ := mockTokenStore(t)
_, ts, _, _ := TestCoreWithTokenStore(t)
req := logical.TestRequest(t, logical.UpdateOperation, "create")
req.ClientToken = "random"
@ -542,7 +507,7 @@ func TestTokenStore_HandleRequest_CreateToken_BadParent(t *testing.T) {
}
func TestTokenStore_HandleRequest_CreateToken(t *testing.T) {
_, ts, root := mockTokenStore(t)
_, ts, _, root := TestCoreWithTokenStore(t)
req := logical.TestRequest(t, logical.UpdateOperation, "create")
req.ClientToken = root
@ -558,7 +523,7 @@ func TestTokenStore_HandleRequest_CreateToken(t *testing.T) {
}
func TestTokenStore_HandleRequest_CreateToken_RootID(t *testing.T) {
_, ts, root := mockTokenStore(t)
_, ts, _, root := TestCoreWithTokenStore(t)
req := logical.TestRequest(t, logical.UpdateOperation, "create")
req.ClientToken = root
@ -575,7 +540,7 @@ func TestTokenStore_HandleRequest_CreateToken_RootID(t *testing.T) {
}
func TestTokenStore_HandleRequest_CreateToken_NonRootID(t *testing.T) {
_, ts, root := mockTokenStore(t)
_, ts, _, root := TestCoreWithTokenStore(t)
testMakeToken(t, ts, root, "client", "", []string{"foo"})
req := logical.TestRequest(t, logical.UpdateOperation, "create")
@ -593,7 +558,7 @@ func TestTokenStore_HandleRequest_CreateToken_NonRootID(t *testing.T) {
}
func TestTokenStore_HandleRequest_CreateToken_NonRoot_Subset(t *testing.T) {
_, ts, root := mockTokenStore(t)
_, ts, _, root := TestCoreWithTokenStore(t)
testMakeToken(t, ts, root, "client", "", []string{"foo", "bar"})
req := logical.TestRequest(t, logical.UpdateOperation, "create")
@ -610,7 +575,7 @@ func TestTokenStore_HandleRequest_CreateToken_NonRoot_Subset(t *testing.T) {
}
func TestTokenStore_HandleRequest_CreateToken_NonRoot_InvalidSubset(t *testing.T) {
_, ts, root := mockTokenStore(t)
_, ts, _, root := TestCoreWithTokenStore(t)
testMakeToken(t, ts, root, "client", "", []string{"foo", "bar"})
req := logical.TestRequest(t, logical.UpdateOperation, "create")
@ -627,7 +592,7 @@ func TestTokenStore_HandleRequest_CreateToken_NonRoot_InvalidSubset(t *testing.T
}
func TestTokenStore_HandleRequest_CreateToken_NonRoot_NoParent(t *testing.T) {
_, ts, root := mockTokenStore(t)
_, ts, _, root := TestCoreWithTokenStore(t)
testMakeToken(t, ts, root, "client", "", []string{"foo"})
req := logical.TestRequest(t, logical.UpdateOperation, "create")
@ -645,7 +610,7 @@ func TestTokenStore_HandleRequest_CreateToken_NonRoot_NoParent(t *testing.T) {
}
func TestTokenStore_HandleRequest_CreateToken_Root_NoParent(t *testing.T) {
_, ts, root := mockTokenStore(t)
_, ts, _, root := TestCoreWithTokenStore(t)
req := logical.TestRequest(t, logical.UpdateOperation, "create")
req.ClientToken = root
@ -667,7 +632,7 @@ func TestTokenStore_HandleRequest_CreateToken_Root_NoParent(t *testing.T) {
}
func TestTokenStore_HandleRequest_CreateToken_PathBased_NoParent(t *testing.T) {
_, ts, root := mockTokenStore(t)
_, ts, _, root := TestCoreWithTokenStore(t)
req := logical.TestRequest(t, logical.UpdateOperation, "create-orphan")
req.ClientToken = root
@ -688,7 +653,7 @@ func TestTokenStore_HandleRequest_CreateToken_PathBased_NoParent(t *testing.T) {
}
func TestTokenStore_HandleRequest_CreateToken_Metadata(t *testing.T) {
_, ts, root := mockTokenStore(t)
_, ts, _, root := TestCoreWithTokenStore(t)
req := logical.TestRequest(t, logical.UpdateOperation, "create")
req.ClientToken = root
@ -714,7 +679,7 @@ func TestTokenStore_HandleRequest_CreateToken_Metadata(t *testing.T) {
}
func TestTokenStore_HandleRequest_CreateToken_Lease(t *testing.T) {
_, ts, root := mockTokenStore(t)
_, ts, _, root := TestCoreWithTokenStore(t)
req := logical.TestRequest(t, logical.UpdateOperation, "create")
req.ClientToken = root
@ -737,7 +702,7 @@ func TestTokenStore_HandleRequest_CreateToken_Lease(t *testing.T) {
}
func TestTokenStore_HandleRequest_CreateToken_TTL(t *testing.T) {
_, ts, root := mockTokenStore(t)
_, ts, _, root := TestCoreWithTokenStore(t)
req := logical.TestRequest(t, logical.UpdateOperation, "create")
req.ClientToken = root
@ -760,7 +725,7 @@ func TestTokenStore_HandleRequest_CreateToken_TTL(t *testing.T) {
}
func TestTokenStore_HandleRequest_Revoke(t *testing.T) {
_, ts, root := mockTokenStore(t)
_, ts, _, root := TestCoreWithTokenStore(t)
testMakeToken(t, ts, root, "child", "", []string{"root", "foo"})
testMakeToken(t, ts, "child", "sub-child", "", []string{"foo"})
@ -792,7 +757,7 @@ func TestTokenStore_HandleRequest_Revoke(t *testing.T) {
}
func TestTokenStore_HandleRequest_RevokeOrphan(t *testing.T) {
_, ts, root := mockTokenStore(t)
_, ts, _, root := TestCoreWithTokenStore(t)
testMakeToken(t, ts, root, "child", "", []string{"root", "foo"})
testMakeToken(t, ts, "child", "sub-child", "", []string{"foo"})
@ -825,7 +790,7 @@ func TestTokenStore_HandleRequest_RevokeOrphan(t *testing.T) {
}
func TestTokenStore_HandleRequest_RevokeOrphan_NonRoot(t *testing.T) {
_, ts, root := mockTokenStore(t)
_, ts, _, root := TestCoreWithTokenStore(t)
testMakeToken(t, ts, root, "child", "", []string{"foo"})
out, err := ts.Lookup("child")
@ -854,7 +819,7 @@ func TestTokenStore_HandleRequest_RevokeOrphan_NonRoot(t *testing.T) {
}
func TestTokenStore_HandleRequest_Lookup(t *testing.T) {
c, ts, root := mockTokenStore(t)
c, ts, _, root := TestCoreWithTokenStore(t)
req := logical.TestRequest(t, logical.ReadOperation, "lookup/"+root)
resp, err := ts.HandleRequest(req)
if err != nil {
@ -944,7 +909,7 @@ func TestTokenStore_HandleRequest_RevokePrefix(t *testing.T) {
ts := exp.tokenStore
// Create new token
root, err := ts.rootToken()
root, err := ts.rootToken("")
if err != nil {
t.Fatalf("err: %v", err)
}
@ -980,7 +945,7 @@ func TestTokenStore_HandleRequest_RevokePrefix(t *testing.T) {
}
func TestTokenStore_HandleRequest_LookupSelf(t *testing.T) {
_, ts, root := mockTokenStore(t)
_, ts, _, root := TestCoreWithTokenStore(t)
req := logical.TestRequest(t, logical.ReadOperation, "lookup-self")
req.ClientToken = root
resp, err := ts.HandleRequest(req)
@ -1017,7 +982,7 @@ func TestTokenStore_HandleRequest_Renew(t *testing.T) {
ts := exp.tokenStore
// Create new token
root, err := ts.rootToken()
root, err := ts.rootToken("")
if err != nil {
t.Fatalf("err: %v", err)
}
@ -1061,7 +1026,7 @@ func TestTokenStore_HandleRequest_RenewSelf(t *testing.T) {
ts := exp.tokenStore
// Create new token
root, err := ts.rootToken()
root, err := ts.rootToken("")
if err != nil {
t.Fatalf("err: %v", err)
}

View File

@ -0,0 +1,186 @@
---
layout: "http"
page_title: "HTTP API: /sys/root-generation/"
sidebar_current: "docs-http-sys-root-generation"
description: |-
The `/sys/root-generation/` endpoints are used to create a new root key for Vault.
---
# /sys/root-generation/attempt
## GET
<dl>
<dt>Description</dt>
<dd>
Reads the configuration and progress of the current root generation
attempt.
</dd>
<dt>Method</dt>
<dd>GET</dd>
<dt>URL</dt>
<dd>`/sys/root-generation/attempt`</dd>
<dt>Parameters</dt>
<dd>
None
</dd>
<dt>Returns</dt>
<dd>
If a root generation is started, `progress` is how many unseal keys have
been provided for this generation attempt, where `required` must be reached
to complete. The `nonce` for the current attempt and whether the attempt is
complete is also displayed. If a PGP key is being used to encrypt the final
root token, its fingerprint will be returned. Note that if an OTP is being
used to encode the final root token, it will never be returned.
```javascript
{
"started": true,
"nonce": "2dbd10f1-8528-6246-09e7-82b25b8aba63",
"progress": 1,
"required": 3,
"pgp_fingerprint": "",
"complete": false
}
```
</dd>
</dl>
## PUT
<dl>
<dt>Description</dt>
<dd>
Initializes a new root generation attempt. Only a single root generation
attempt can take place at a time. One (and only one) of `otp` or `pgp_key`
are required.
</dd>
<dt>Method</dt>
<dd>PUT</dd>
<dt>URL</dt>
<dd>`/sys/root-generation/attempt`</dd>
<dt>Parameters</dt>
<dd>
<ul>
<li>
<span class="param">otp</span>
<span class="param-flags">optional</span>
A base64-encoded 16-byte value. The raw bytes of the token will be
XOR'd with this value before being returned to the final unseal key
provider.
</li>
<li>
<span class="param">pgp_key</span>
<span class="param-flags">optional</span>
A base64-encoded PGP public key. The raw bytes of the token will be
encrypted with this value before being returned to the final unseal key
provider.
</li>
</ul>
</dd>
<dt>Returns</dt>
<dd>
The current progress.
```javascript
{
"started": true,
"nonce": "2dbd10f1-8528-6246-09e7-82b25b8aba63",
"progress": 1,
"required": 3,
"pgp_fingerprint": "816938b8a29146fbe245dd29e7cbaf8e011db793",
"complete": false
}
```
</dd>
</dl>
## DELETE
<dl>
<dt>Description</dt>
<dd>
Cancels any in-progress root generation attempt. This clears any progress
made. This must be called to change the OTP or PGP key being used.
</dd>
<dt>Method</dt>
<dd>DELETE</dd>
<dt>URL</dt>
<dd>`/sys/root-generation/attempt`</dd>
<dt>Parameters</dt>
<dd>None
</dd>
<dt>Returns</dt>
<dd>`204` response code.
</dd>
</dl>
# /sys/root-generation/update
## PUT
<dl>
<dt>Description</dt>
<dd>
Enter a single master key share to progress the root generation attempt.
If the threshold number of master key shares is reached, Vault will
complete the root generation and issue the new token. Otherwise, this API
must be called multiple times until that threshold is met. The attempt
nonce must be provided with each call.
</dd>
<dt>Method</dt>
<dd>PUT</dd>
<dt>URL</dt>
<dd>`/sys/root-generation/update`</dd>
<dt>Parameters</dt>
<dd>
<ul>
<li>
<span class="param">key</span>
<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 attempt.
</li>
</ul>
</dd>
<dt>Returns</dt>
<dd>
A JSON-encoded object indicating the attempt nonce, and completion status,
and the encoded root token, if the attempt is complete.
```javascript
{
"started": true,
"nonce": "2dbd10f1-8528-6246-09e7-82b25b8aba63",
"progress": 3,
"required": 3,
"pgp_fingerprint": "",
"complete": true,
"encoded_root_token": "FPzkNBvwNDeFh4SmGA8c+w=="
}
```
</dd>
</dl>

View File

@ -17,11 +17,15 @@
</li>
<li<%= sidebar_current("docs-http-sys-init") %>>
<a href="#">Initialization</a>
<a href="#">Initialization/Recovery</a>
<ul class="nav nav-visible">
<li<%= sidebar_current("docs-http-sys-init") %>>
<a href="/docs/http/sys-init.html">/sys/init</a>
</li>
<li<%= sidebar_current("docs-http-sys-root-generation") %>>
<a href="/docs/http/sys-root-generation.html">/sys/root-generation</a>
</li>
</ul>
</li>