diff --git a/api/sys_root_generation.go b/api/sys_root_generation.go new file mode 100644 index 000000000..e50b0becc --- /dev/null +++ b/api/sys_root_generation.go @@ -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"` +} diff --git a/cli/commands.go b/cli/commands.go index ffaeeea01..eae2b6f64 100644 --- a/cli/commands.go +++ b/cli/commands.go @@ -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, diff --git a/command/generate-root.go b/command/generate-root.go new file mode 100644 index 000000000..f9d322242 --- /dev/null +++ b/command/generate-root.go @@ -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:" 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:". 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) +} diff --git a/command/generate-root_test.go b/command/generate-root_test.go new file mode 100644 index 000000000..d319ab3cd --- /dev/null +++ b/command/generate-root_test.go @@ -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) + } +} diff --git a/command/pgp_test.go b/command/pgp_test.go index 4346d33c8..bc61c8272 100644 --- a/command/pgp_test.go +++ b/command/pgp_test.go @@ -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 = `lQOYBFXbkJEBCADKb1ZvlT14XrJa2rTOe5924LQr2PTZlRv+651TXy33yEhelZ+V4sMrELN8fKEG -Zy1kNixmbq3MCF/671k3LigHA7VrOaH9iiQgr6IIq2MeIkUYKZ27C992vQkYLjbYUG8+zl5h69S4 -0Ixm0yL0M54XOJ0gm+maEK1ZESKTUlDNkIS7l0jLZSYwfUeGXSEt6FWs8OgbyRTaHw4PDHrDEE9e -Q67K6IZ3YMhPOL4fVk4Jwrp5R/RwiklT+lNozWEyFVwPFH4MeQMs9nMbt+fWlTzEA7tI4acI9yDk -Cm1yN2R9rmY0UjODRiJw6z6sLV2T+Pf32n3MNSUOYczOjZa4VBwjABEBAAEAB/oCBqTIsxlUgLtz -HRpWW5MJ+93xvmVV0JHhRK/ygKghq+zpC6S+cn7dwrEj1JTPh+17lyemYQK+RMeiBEduoWNKuHUd -WX353w2411rrc/VuGTglzhd8Ir2BdJlPesCzw4JQnrWqcBqN52W+iwhnE7PWVhnvItWnx6APK5Se -q7dzFWy8Z8tNIHm0pBQbeyo6x2rHHSWkr2fs7V02qFQhii1ayFRMcgdOWSNX6CaZJuYhk/DyjApN -9pVhi3P1pNMpFeV0Pt8Gl1f/9o6/HpAYYEt/6vtVRhFUGgtNi95oc0oyzIJxliRvd6+Z236osigQ -QEBwj1ImRK8TKyWPlykiJWc5BADfldgOCA55o3Qz/z/oVE1mm+a3FmPPTQlHBXotNEsrWV2wmJHe -lNQPI6ZwMtLrBSg8PUpG2Rvao6XJ4ZBl/VcDwfcLgCnALPCcL0L0Z3vH3Sc9Ta/bQWJODG7uSaI1 -iVJ7ArKNtVzTqRQWK967mol9CCqh4A0jRrH0aVEFbrqQ/QQA58iEJaFhzFZjufjC9N8Isn3Ky7xu -h+dk001RNCb1GnNZcx4Ld2IB+uXyYjtg7dNaUhGgGuCBo9nax89bMsBzzUukx3SHq1pxopMg6Dm8 -ImBoIAicuQWgEkaP2T0rlwCozUalJZaG1gyrzkPhkeY7CglpJycHLHfY2MIb46c8+58D/iJ83Q5j -Y4x+sqW2QeUYFwqCcOW8Urg64UxEkgXZXiNMwTAJCaxp/Pz7cgeUDwgv+6CXEdnT1910+byzK9ha -V1Q/65+/JYuCeyHxcoAb4Wtpdl7GALGd/1G0UAmq47yrefEr/b00uS35i1qUUhOzo1NmEZch/bvF -kmJ+WtAHunZcOCu0EFZhdWx0IFRlc3QgS2V5IDKJATgEEwECACIFAlXbkJECGy8GCwkIBwMCBhUI -AgkKCwQWAgMBAh4BAheAAAoJEOuDLGfrXolXqz4H/28IuoRxGKoJ064YHjPkkpoddW6zdzzNfHip -ZnNfEUiTEls4qF1IB81M2xqfiXIFRIdO2kaLkRPFhO0hRxbtI6VuZYLgG3QCaXhxW6GyFa5zKABq -hb5ojexdnAYRswaHV201ZCclj9rnJN1PAg0Rz6MdX/w1euEWktQxWKo42oZKyx8oT9p6lrv5KRmG -kdrg8K8ARmRILjmwuBAgJM0eXBZHNGWXelk4YmOgnAAcZA6ZAo1G+8Pg6pKKP61ETewuCg3/u7N0 -vDttB+ZXqF88W9jAYlvdgbTtajNF5IDYDjTzWfeCaIB18F9gOzXq15SwWeDDI+CU9Nmq358IzXlx -k4edA5gEVduQkQEIAOjZV5tbpfIh5QefpIp2dpGMVfpgPj4RNc15CyFnb8y6dhCrdybkY9GveXJe -4F3GNYnSfB42cgxrfhizX3LakmZQ/SAg+YO5KxfCIN7Q9LPNeTgPsZZT6h8lVuXUxOFKXfRaR3/t -GF5xE3e5CoZRsHV/c92h3t1LdJNOnC5mUKIPO4zDxiw/C2T2q3rP1kmIMaOH724kEH5A+xcp1cBH -yt0tdHtIWuQv6joTJzujqViRhlCwQYzQSKpSBxwhBsorPvyinZI/ZXA4XXZc5RoMqV9rikedrb1r -ENO8JOuPu6tMS+znFu67skq2gFFZwCQWIjdHm+2ukE+PE580WAWudyMAEQEAAQAH/i7ndRPI+t0T -AdEu0dTIdyrrg3g7gd471kQtIVZZwTYSy2yhNY/Ciu72s3ab8QNCxY8dNL5bRk8FKjHslAoNSFdO -8iZSLiDgIHOZOcjYe6pqdgQaeTHodm1Otrn2SbB+K/3oX6W/y1xe18aSojGba/nHMj5PeJbIN9Pi -jmh0WMLD/0rmkTTxR7qQ5+kMV4O29xY4qjdYRD5O0adeZX0mNncmlmQ+rX9yxrtSgFROu1jwVtfP -hcNetifTTshJnTwND8hux5ECEadlIVBHypW28Hth9TRBXmddTmv7L7mdtUO6DybgkpWpw4k4LPsk -uZ6aY4wcGRp7EVfWGr9NHbq/n+0EAOlhDXIGdylkQsndjBMyhPsXZa5fFBmOyHjXj733195Jgr1v -ZjaIomrA9cvYrmN75oKrG1jJsMEl6HfC/ZPzEj6E51/p1PRdHP7CdUUA+DG8x4M3jn+e43psVuAR -a1XbN+8/bOa0ubt7ljVPjAEvWRSvU9dRaQz93w3fduAuM07dBAD/ayK3e0d6JMJMrU50lNOXQBgL -rFbg4rWzPO9BJQdhjOhmOZQiUa1Q+EV+s95yIUg1OAfaMP9KRIljr5RCdGNS6WoMNBAQOSrZpelf -jW4NpzphNfWDGVkUoPoskVtJz/nu9d860dGd3Al0kSmtUpMu5QKlo+sSxXUPbWLUn8V9/wP/ScCW -H+0gtL4R7SFazPeTIP+Cu5oR7A/DlFVLJKa3vo+atkhSvwxHGbg04vb/W4mKhGGVtMBtlhRmaWOe -PhUulU5FdaYsdlpN/Yd+hhgU6NHlyImPGVEHWD8c6CG8qoZfpR33j2sqshs4i/MtJZeBvl62vxPn -9bDN7KAjFNll9axAjIkCPgQYAQIACQUCVduQkQIbLgEpCRDrgyxn616JV8BdIAQZAQIABgUCVduQ -kQAKCRArYtevdF38xtzgB/4zVzozBpVOnagRkA7FDsHo36xX60Lik+ew0m28ueDDhnV3bXQsCvn/ -6wiCVWqLOTDeYCPlyTTpEMyk8zwdCICW6MgSkVHWcEDOrRqIrqm86rirjTGjJSgQe3l4CqJvkn6j -ybShYoBk1OZZV6vVv9hPTXXv9E6dLKoEW5YZBrrF+VC0w1iOIvaAQ+QXph20eV4KBIrp/bhG6Pdn -igKxuBZ79cdqDnXIzT9UiIa6LYpR0rbeg+7BmuZTTPS8t+41hIiKS+UZFdKa67eYENtyOmEMWOFC -LLRJGxkleukchiMJ70rknloZXsvJIweXBzSZ6m7mJQBgaig/L/dXyjv6+j2pNB4H/1trYUtJjXQK -HmqlgCmpCkHt3g7JoxWvglnDNmE6q3hIWuVIYQpnzZy1g05+X9Egwc1WVpBB02H7PkUZTfpaP/L6 -DLneMmSKPhZE3I+lPIPjwrxqh6xy5uQezcWkJTNKvPWF4FJzrVvx7XTPjfGvOB0UPEnjvtZTp5yO -hTeZK7DgIEtb/Wcrqs+iRArQKboM930ORSZhwvGK3F9V/gMDpIrvge5vDFsTEYQdw/2epIewH0L/ -FUb/6jBRcVEpGo9Ayg+Jnhq14GOGcd1y9oMZ48kYVLVBTA9tQ+82WE8Bch7uFPj4MFOMVRn1dc3q -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-----` diff --git a/helper/pgpkeys/encryptshares.go b/helper/pgpkeys/encrypt_decrypt.go similarity index 51% rename from helper/pgpkeys/encryptshares.go rename to helper/pgpkeys/encrypt_decrypt.go index 882de1094..85b80906b 100644 --- a/helper/pgpkeys/encryptshares.go +++ b/helper/pgpkeys/encrypt_decrypt.go @@ -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 +} diff --git a/helper/pgpkeys/test_keys.go b/helper/pgpkeys/test_keys.go new file mode 100644 index 000000000..c10a9055e --- /dev/null +++ b/helper/pgpkeys/test_keys.go @@ -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-----` +) diff --git a/helper/xor/xor.go b/helper/xor/xor.go new file mode 100644 index 000000000..c83ea31ac --- /dev/null +++ b/helper/xor/xor.go @@ -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 +} diff --git a/http/handler.go b/http/handler.go index 64dbf5ffe..3f0728268 100644 --- a/http/handler.go +++ b/http/handler.go @@ -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)) diff --git a/http/sys_root_generation.go b/http/sys_root_generation.go new file mode 100644 index 000000000..a5c8c0289 --- /dev/null +++ b/http/sys_root_generation.go @@ -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 +} diff --git a/http/sys_root_generation_test.go b/http/sys_root_generation_test.go new file mode 100644 index 000000000..3bb5b04b2 --- /dev/null +++ b/http/sys_root_generation_test.go @@ -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"]) + } +} diff --git a/vault/core.go b/vault/core.go index bfd5df56f..fc0b5ed8a 100644 --- a/vault/core.go +++ b/vault/core.go @@ -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 diff --git a/vault/expiration_test.go b/vault/expiration_test.go index 700926153..e167405c0 100644 --- a/vault/expiration_test.go +++ b/vault/expiration_test.go @@ -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) } diff --git a/vault/root_generation.go b/vault/root_generation.go new file mode 100644 index 000000000..2f1375d61 --- /dev/null +++ b/vault/root_generation.go @@ -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 +} diff --git a/vault/root_generation_test.go b/vault/root_generation_test.go new file mode 100644 index 000000000..167f379ed --- /dev/null +++ b/vault/root_generation_test.go @@ -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) + } +} diff --git a/vault/testing.go b/vault/testing.go index de2b6c591..62da80ffa 100644 --- a/vault/testing.go +++ b/vault/testing.go @@ -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 { diff --git a/vault/token_store.go b/vault/token_store.go index eab29bc2a..4870ac8ce 100644 --- a/vault/token_store.go +++ b/vault/token_store.go @@ -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", diff --git a/vault/token_store_test.go b/vault/token_store_test.go index 525b3dda9..7e5a32ee1 100644 --- a/vault/token_store_test.go +++ b/vault/token_store_test.go @@ -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) } diff --git a/website/source/docs/http/sys-root-generation.html.md b/website/source/docs/http/sys-root-generation.html.md new file mode 100644 index 000000000..dd7cb4b82 --- /dev/null +++ b/website/source/docs/http/sys-root-generation.html.md @@ -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 + +
+
Description
+
+ Reads the configuration and progress of the current root generation + attempt. +
+ +
Method
+
GET
+ +
URL
+
`/sys/root-generation/attempt`
+ +
Parameters
+
+ None +
+ +
Returns
+
+ 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 + } + ``` + +
+
+ +## PUT + +
+
Description
+
+ 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. +
+ +
Method
+
PUT
+ +
URL
+
`/sys/root-generation/attempt`
+ +
Parameters
+
+
    +
  • + otp + optional + 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. +
  • +
  • + pgp_key + optional + 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. +
  • +
+
+ +
Returns
+
+ The current progress. + + ```javascript + { + "started": true, + "nonce": "2dbd10f1-8528-6246-09e7-82b25b8aba63", + "progress": 1, + "required": 3, + "pgp_fingerprint": "816938b8a29146fbe245dd29e7cbaf8e011db793", + "complete": false + } + ``` + +
+
+ +## DELETE + +
+
Description
+
+ 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. +
+ +
Method
+
DELETE
+ +
URL
+
`/sys/root-generation/attempt`
+ +
Parameters
+
None +
+ +
Returns
+
`204` response code. +
+
+ +# /sys/root-generation/update + +## PUT + +
+
Description
+
+ 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. +
+ +
Method
+
PUT
+ +
URL
+
`/sys/root-generation/update`
+ +
Parameters
+
+
    +
  • + key + required + A single master share key. +
  • +
  • + nonce + required + The nonce of the attempt. +
  • +
+
+ +
Returns
+
+ 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==" + } + ``` + +
+
diff --git a/website/source/layouts/http.erb b/website/source/layouts/http.erb index 7e3e03c3e..b2afca54b 100644 --- a/website/source/layouts/http.erb +++ b/website/source/layouts/http.erb @@ -17,11 +17,15 @@ > - Initialization + Initialization/Recovery