Add support for PGP encrypting the initial root token. (#1883)

This commit is contained in:
Jeff Mitchell 2016-09-13 18:42:24 -04:00 committed by GitHub
parent 7898a66624
commit 941b066780
12 changed files with 239 additions and 31 deletions

View file

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

View file

@ -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)
@ -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", "")
@ -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 {
@ -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
}
@ -289,8 +299,9 @@ 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
@ -298,11 +309,18 @@ Init Options:
"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.

View file

@ -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"
)
@ -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) {
@ -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
@ -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()
@ -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")
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,7 @@
package vault
import (
"encoding/base64"
"encoding/hex"
"fmt"
@ -8,6 +9,14 @@ import (
"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 {
@ -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")
@ -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)

View file

@ -69,7 +69,10 @@ func testCore_Init_Common(t *testing.T, c *Core, conf *CoreConfig, barrierConf,
}
}
res, err := c.Initialize(barrierConf, recoveryConf)
res, err := c.Initialize(&InitParams{
BarrierConfig: barrierConf,
RecoveryConfig: recoveryConf,
})
if err != nil {
t.Fatalf("err: %v", err)
}
@ -87,7 +90,10 @@ func testCore_Init_Common(t *testing.T, c *Core, conf *CoreConfig, barrierConf,
t.Fatalf("Bad: %#v", res)
}
_, err = c.Initialize(barrierConf, recoveryConf)
_, err = c.Initialize(&InitParams{
BarrierConfig: barrierConf,
RecoveryConfig: recoveryConf,
})
if err != ErrAlreadyInit {
t.Fatalf("err: %v", err)
}
@ -125,7 +131,10 @@ func testCore_Init_Common(t *testing.T, c *Core, conf *CoreConfig, barrierConf,
t.Fatalf("err: %v", err)
}
_, err = c2.Initialize(barrierConf, recoveryConf)
_, err = c2.Initialize(&InitParams{
BarrierConfig: barrierConf,
RecoveryConfig: recoveryConf,
})
if err != ErrAlreadyInit {
t.Fatalf("err: %v", err)
}

View file

@ -95,7 +95,10 @@ func (d *TestSeal) SetRecoveryKey(key []byte) error {
func TestCoreUnsealedWithConfigs(t *testing.T, barrierConf, recoveryConf *SealConfig) (*Core, [][]byte, [][]byte, string) {
seal := &TestSeal{}
core := TestCoreWithSeal(t, seal)
result, err := core.Initialize(barrierConf, recoveryConf)
result, err := core.Initialize(&InitParams{
BarrierConfig: barrierConf,
RecoveryConfig: recoveryConf,
})
if err != nil {
t.Fatalf("err: %s", err)
}

View file

@ -143,10 +143,13 @@ func TestCoreInit(t *testing.T, core *Core) ([]byte, string) {
func TestCoreInitClusterWrapperSetup(t *testing.T, core *Core, clusterAddrs []*net.TCPAddr, handlerSetupFunc func() (http.Handler, http.Handler)) ([]byte, string) {
core.SetClusterListenerAddrs(clusterAddrs)
core.SetClusterSetupFuncs(handlerSetupFunc)
result, err := core.Initialize(&SealConfig{
SecretShares: 1,
SecretThreshold: 1,
}, nil)
result, err := core.Initialize(&InitParams{
BarrierConfig: &SealConfig{
SecretShares: 1,
SecretThreshold: 1,
},
RecoveryConfig: nil,
})
if err != nil {
t.Fatalf("err: %s", err)
}

View file

@ -39,8 +39,9 @@ description: |-
<dl>
<dt>Description</dt>
<dd>
Initializes a new Vault. The Vault must've not been previously
initialized.
Initializes a new Vault. The Vault must not have been previously
initialized. The recovery options, as well as the stored shares option, are
only available when using Vault HSM.
</dd>
<dt>Method</dt>
@ -49,6 +50,12 @@ description: |-
<dt>Parameters</dt>
<dd>
<ul>
<li>
<span class="param">root_token_pgp_key</span>
<span class="param-flags">optional</span>
A PGP public key used to encrypt the initial root token. The key
must be base64-encoded from its original binary representation.
</li>
<li>
<span class="param">secret_shares</span>
<span class="param-flags">required</span>
@ -57,8 +64,10 @@ description: |-
<li>
<span class="param">secret_threshold</span>
<span class="param-flags">required</span>
The number of shares required to reconstruct the master key.
This must be less than or equal to <code>secret_shares</code>.
The number of shares required to reconstruct the master key. This must
be less than or equal to <code>secret_shares</code>. If using Vault HSM
with auto-unsealing, this value must be the same as
<code>secret_shares</code>.
</li>
<li>
<span class="param">pgp_keys</span>
@ -68,6 +77,33 @@ description: |-
original binary representation. The size of this array must be the
same as <code>secret_shares</code>.
</li>
<li>
<span class="param">stored_shares</span>
<span class="param-flags">required</span>
The number of shares that should be encrypted by the HSM and stored for
auto-unsealing (Vault HSM only). Currently must be the same as
<code>secret_shares</code>.
</li>
<li>
<span class="param">recovery_shares</span>
<span class="param-flags">required</span>
The number of shares to split the recovery key into (Vault HSM only).
</li>
<li>
<span class="param">recovery_threshold</span>
<span class="param-flags">required</span>
The number of shares required to reconstruct the recovery key (Vault
HSM only). This must be less than or equal to
<code>recovery_shares</code>.
</li>
<li>
<span class="param">recovery_pgp_keys</span>
<span class="param-flags">optional</span>
An array of PGP public keys used to encrypt the output recovery keys
(Vault HSM only). Ordering is preserved. The keys must be
base64-encoded from their original binary representation. The size of
this array must be the same as <code>recovery_shares</code>.
</li>
</ul>
</dd>