Skip to content

Commit

Permalink
Add support for PGP encrypting the initial root token. (hashicorp#1883)
Browse files Browse the repository at this point in the history
  • Loading branch information
jefferai authored Sep 13, 2016
1 parent 7898a66 commit 941b066
Show file tree
Hide file tree
Showing 12 changed files with 239 additions and 31 deletions.
1 change: 1 addition & 0 deletions api/sys_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type InitRequest struct {
RecoveryShares int `json:"recovery_shares"`
RecoveryThreshold int `json:"recovery_threshold"`
RecoveryPGPKeys []string `json:"recovery_pgp_keys"`
RootTokenPGPKey string `json:"root_token_pgp_key"`
}

type InitStatusResponse struct {
Expand Down
28 changes: 23 additions & 5 deletions command/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ type InitCommand struct {

func (c *InitCommand) Run(args []string) int {
var threshold, shares, storedShares, recoveryThreshold, recoveryShares int
var pgpKeys, recoveryPgpKeys pgpkeys.PubKeyFilesFlag
var pgpKeys, recoveryPgpKeys, rootTokenPgpKey pgpkeys.PubKeyFilesFlag
var auto, check bool
var consulServiceName string
flags := c.Meta.FlagSet("init", meta.FlagSetDefault)
Expand All @@ -30,6 +30,7 @@ func (c *InitCommand) Run(args []string) int {
flags.IntVar(&threshold, "key-threshold", 3, "")
flags.IntVar(&storedShares, "stored-shares", 0, "")
flags.Var(&pgpKeys, "pgp-keys", "")
flags.Var(&rootTokenPgpKey, "root-token-pgp-key", "")
flags.IntVar(&recoveryShares, "recovery-shares", 5, "")
flags.IntVar(&recoveryThreshold, "recovery-threshold", 3, "")
flags.Var(&recoveryPgpKeys, "recovery-pgp-keys", "")
Expand All @@ -50,6 +51,15 @@ func (c *InitCommand) Run(args []string) int {
RecoveryPGPKeys: recoveryPgpKeys,
}

switch len(rootTokenPgpKey) {
case 0:
case 1:
initRequest.RootTokenPGPKey = rootTokenPgpKey[0]
default:
c.Ui.Error("Only one PGP key can be specified for encrypting the root token")
return 1
}

// If running in 'auto' mode, run service discovery based on environment
// variables of Consul.
if auto {
Expand All @@ -60,7 +70,7 @@ func (c *InitCommand) Run(args []string) int {
// Create a client to communicate with Consul
consulClient, err := consulapi.NewClient(consulConfig)
if err != nil {
c.Ui.Error(fmt.Sprintf("failed to create Consul client:%v", err))
c.Ui.Error(fmt.Sprintf("Failed to create Consul client:%v", err))
return 1
}

Expand Down Expand Up @@ -289,20 +299,28 @@ Init Options:
-key-threshold=3 The number of key shares required to reconstruct
the master key.
-stored-shares=0 The number of unseal keys to store. This is not
normally available.
-stored-shares=0 The number of unseal keys to store. Only used with
Vault HSM. Must currently be equivalent to the
number of shares.
-pgp-keys If provided, must be a comma-separated list of
files on disk containing binary- or base64-format
public PGP keys, or Keybase usernames specified as
"keybase:<username>". The number of given entries
must match 'key-shares'. The output unseal keys will
be encrypted and base64-encoded, in order, with the
given public keys. If you want to use them with the
given public keys. If you want to use them with the
'vault unseal' command, you will need to base64-
decode and decrypt; this will be the plaintext
unseal key.
-root-token-pgp-key If provided, a file on disk with a binary- or
base64-format public PGP key, or a Keybase username
specified as "keybase:<username>". The output root
token will be encrypted and base64-encoded, in
order, with the given public key. You will need
to base64-decode and decrypt the result.
-recovery-shares=5 The number of key shares to split the recovery key
into. Only used with Vault HSM.
Expand Down
97 changes: 96 additions & 1 deletion command/init_test.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
package command

import (
"bytes"
"encoding/base64"
"os"
"reflect"
"regexp"
"strings"
"testing"

"github.com/hashicorp/vault/helper/pgpkeys"
"github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/meta"
"github.com/hashicorp/vault/vault"
"github.com/keybase/go-crypto/openpgp"
"github.com/keybase/go-crypto/openpgp/packet"
"github.com/mitchellh/cli"
)

Expand Down Expand Up @@ -148,6 +153,57 @@ func TestInit_custom(t *testing.T) {
if !reflect.DeepEqual(expected, sealConf) {
t.Fatalf("expected:\n%#v\ngot:\n%#v\n", expected, sealConf)
}

re, err := regexp.Compile("\\s+Initial Root Token:\\s+(.*)")
if err != nil {
t.Fatalf("Error compiling regex: %s", err)
}
matches := re.FindAllStringSubmatch(ui.OutputWriter.String(), -1)
if len(matches) != 1 {
t.Fatalf("Unexpected number of tokens found, got %d", len(matches))
}

rootToken := matches[0][1]

client, err := c.Client()
if err != nil {
t.Fatalf("Error fetching client: %v", err)
}

client.SetToken(rootToken)

re, err = regexp.Compile("\\s*Unseal Key \\d+: (.*)")
if err != nil {
t.Fatalf("Error compiling regex: %s", err)
}
matches = re.FindAllStringSubmatch(ui.OutputWriter.String(), -1)
if len(matches) != 7 {
t.Fatalf("Unexpected number of keys returned, got %d, matches was \n\n%#v\n\n, input was \n\n%s\n\n", len(matches), matches, ui.OutputWriter.String())
}

var unsealed bool
for i := 0; i < 3; i++ {
decodedKey, err := base64.StdEncoding.DecodeString(strings.TrimSpace(matches[i][1]))
if err != nil {
t.Fatalf("err decoding key %v: %v", matches[i][1], err)
}
unsealed, err = core.Unseal(decodedKey)
if err != nil {
t.Fatalf("err during unseal: %v; key was %v", err, matches[i][1])
}
}
if !unsealed {
t.Fatal("expected to be unsealed")
}

tokenInfo, err := client.Auth().Token().LookupSelf()
if err != nil {
t.Fatalf("Error looking up root token info: %v", err)
}

if tokenInfo.Data["policies"].([]interface{})[0].(string) != "root" {
t.Fatalf("expected root policy")
}
}

func TestInit_PGP(t *testing.T) {
Expand Down Expand Up @@ -181,6 +237,7 @@ func TestInit_PGP(t *testing.T) {
"-key-shares", "2",
"-pgp-keys", pubFiles[0] + ",@" + pubFiles[1] + "," + pubFiles[2],
"-key-threshold", "2",
"-root-token-pgp-key", pubFiles[0],
}

// This should fail, as key-shares does not match pgp-keys size
Expand All @@ -193,6 +250,7 @@ func TestInit_PGP(t *testing.T) {
"-key-shares", "4",
"-pgp-keys", pubFiles[0] + ",@" + pubFiles[1] + "," + pubFiles[2] + "," + pubFiles[3],
"-key-threshold", "2",
"-root-token-pgp-key", pubFiles[0],
}

ui.OutputWriter.Reset()
Expand Down Expand Up @@ -242,7 +300,44 @@ func TestInit_PGP(t *testing.T) {
t.Fatalf("Unexpected number of tokens found, got %d", len(matches))
}

rootToken := matches[0][1]
encRootToken := matches[0][1]
privKeyBytes, err := base64.StdEncoding.DecodeString(pgpkeys.TestPrivKey1)
if err != nil {
t.Fatalf("error decoding private key: %v", err)
}
ptBuf := bytes.NewBuffer(nil)
entity, err := openpgp.ReadEntity(packet.NewReader(bytes.NewBuffer(privKeyBytes)))
if err != nil {
t.Fatalf("Error parsing private key: %s", err)
}
var rootBytes []byte
rootBytes, err = base64.StdEncoding.DecodeString(encRootToken)
if err != nil {
t.Fatalf("Error decoding root token: %s", err)
}
entityList := &openpgp.EntityList{entity}
md, err := openpgp.ReadMessage(bytes.NewBuffer(rootBytes), entityList, nil, nil)
if err != nil {
t.Fatalf("Error decrypting root token: %s", err)
}
ptBuf.ReadFrom(md.UnverifiedBody)
rootToken := ptBuf.String()

parseDecryptAndTestUnsealKeys(t, ui.OutputWriter.String(), rootToken, false, nil, nil, core)

client, err := c.Client()
if err != nil {
t.Fatalf("Error fetching client: %v", err)
}

client.SetToken(rootToken)

tokenInfo, err := client.Auth().Token().LookupSelf()
if err != nil {
t.Fatalf("Error looking up root token info: %v", err)
}

if tokenInfo.Data["policies"].([]interface{})[0].(string) != "root" {
t.Fatalf("expected root policy")
}
}
11 changes: 7 additions & 4 deletions command/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -574,10 +574,13 @@ func (c *ServerCommand) Run(args []string) int {

func (c *ServerCommand) enableDev(core *vault.Core, rootTokenID string) (*vault.InitResult, error) {
// Initialize it with a basic single key
init, err := core.Initialize(&vault.SealConfig{
SecretShares: 1,
SecretThreshold: 1,
}, nil)
init, err := core.Initialize(&vault.InitParams{
BarrierConfig: &vault.SealConfig{
SecretShares: 1,
SecretThreshold: 1,
},
RecoveryConfig: nil,
})
if err != nil {
return nil, err
}
Expand Down
9 changes: 8 additions & 1 deletion http/sys_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,13 @@ func handleSysInitPut(core *vault.Core, w http.ResponseWriter, r *http.Request)
}
}

result, initErr := core.Initialize(barrierConfig, recoveryConfig)
initParams := &vault.InitParams{
BarrierConfig: barrierConfig,
RecoveryConfig: recoveryConfig,
RootTokenPGPKey: req.RootTokenPGPKey,
}

result, initErr := core.Initialize(initParams)
if initErr != nil {
if !errwrap.ContainsType(initErr, new(vault.NonFatalError)) {
respondError(w, http.StatusBadRequest, initErr)
Expand Down Expand Up @@ -128,6 +134,7 @@ type InitRequest struct {
RecoveryShares int `json:"recovery_shares"`
RecoveryThreshold int `json:"recovery_threshold"`
RecoveryPGPKeys []string `json:"recovery_pgp_keys"`
RootTokenPGPKey string `json:"root_token_pgp_key"`
}

type InitResponse struct {
Expand Down
11 changes: 7 additions & 4 deletions logical/testing/testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,13 @@ func Test(tt TestT, c TestCase) {
}

// Initialize the core
init, err := core.Initialize(&vault.SealConfig{
SecretShares: 1,
SecretThreshold: 1,
}, nil)
init, err := core.Initialize(&vault.InitParams{
BarrierConfig: &vault.SealConfig{
SecretShares: 1,
SecretThreshold: 1,
},
RecoveryConfig: nil,
})
if err != nil {
tt.Fatal("error initializing core: ", err)
return
Expand Down
15 changes: 12 additions & 3 deletions vault/core_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ func TestCore_Unseal_MultiShare(t *testing.T) {
SecretShares: 5,
SecretThreshold: 3,
}
res, err := c.Initialize(sealConf, nil)
res, err := c.Initialize(&InitParams{
BarrierConfig: sealConf,
RecoveryConfig: nil,
})
if err != nil {
t.Fatalf("err: %v", err)
}
Expand Down Expand Up @@ -141,7 +144,10 @@ func TestCore_Unseal_Single(t *testing.T) {
SecretShares: 1,
SecretThreshold: 1,
}
res, err := c.Initialize(sealConf, nil)
res, err := c.Initialize(&InitParams{
BarrierConfig: sealConf,
RecoveryConfig: nil,
})
if err != nil {
t.Fatalf("err: %v", err)
}
Expand Down Expand Up @@ -196,7 +202,10 @@ func TestCore_Route_Sealed(t *testing.T) {
t.Fatalf("err: %v", err)
}

res, err := c.Initialize(sealConf, nil)
res, err := c.Initialize(&InitParams{
BarrierConfig: sealConf,
RecoveryConfig: nil,
})
if err != nil {
t.Fatalf("err: %v", err)
}
Expand Down
23 changes: 22 additions & 1 deletion vault/init.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
package vault

import (
"encoding/base64"
"encoding/hex"
"fmt"

"github.com/hashicorp/vault/helper/pgpkeys"
"github.com/hashicorp/vault/shamir"
)

// InitParams keeps the init function from being littered with too many
// params, that's it!
type InitParams struct {
BarrierConfig *SealConfig
RecoveryConfig *SealConfig
RootTokenPGPKey string
}

// InitResult is used to provide the key parts back after
// they are generated as part of the initialization.
type InitResult struct {
Expand Down Expand Up @@ -79,7 +88,10 @@ func (c *Core) generateShares(sc *SealConfig) ([]byte, [][]byte, error) {

// Initialize is used to initialize the Vault with the given
// configurations.
func (c *Core) Initialize(barrierConfig, recoveryConfig *SealConfig) (*InitResult, error) {
func (c *Core) Initialize(initParams *InitParams) (*InitResult, error) {
barrierConfig := initParams.BarrierConfig
recoveryConfig := initParams.RecoveryConfig

if c.seal.RecoveryKeySupported() {
if recoveryConfig == nil {
return nil, fmt.Errorf("recovery configuration must be supplied")
Expand Down Expand Up @@ -219,6 +231,15 @@ func (c *Core) Initialize(barrierConfig, recoveryConfig *SealConfig) (*InitResul
results.RootToken = rootToken.ID
c.logger.Info("core: root token generated")

if initParams.RootTokenPGPKey != "" {
_, encryptedVals, err := pgpkeys.EncryptShares([][]byte{[]byte(results.RootToken)}, []string{initParams.RootTokenPGPKey})
if err != nil {
c.logger.Error("core: root token encryption failed", "error", err)
return nil, err
}
results.RootToken = base64.StdEncoding.EncodeToString(encryptedVals[0])
}

// Prepare to re-seal
if err := c.preSeal(); err != nil {
c.logger.Error("core: pre-seal teardown failed", "error", err)
Expand Down
Loading

0 comments on commit 941b066

Please sign in to comment.