Properly restore delegations.

This change addresses several points:

1. The integration tests didn't verify that delegations could be used
   for decryption following a restore. The integration tests now
   verify this.

2. There was no functionality for clearing persisted delegations if
   needed. The vault admin can now do this via the command line tool.

3. Restoring active delegations wasn't storing the key with the
   delegation. Keys are now serialised properly.

4. [Minor] The MSP package now reports the name of the offending user
   when it can't find a user name in the database.
This commit is contained in:
Kyle Isom
2016-08-24 13:22:13 -07:00
parent a4b17c3d35
commit 9f39413adb
14 changed files with 405 additions and 49 deletions
+17
View File
@@ -404,3 +404,20 @@ func (c *RemoteServer) Restore(req core.DelegateRequest) (*core.ResponseData, er
return unmarshalResponseData(respBytes)
}
// ResetPersisted issues a persisted delegation reset request,
// clearing out any persisted delegations. This must be done by an
// admin user.
func (c *RemoteServer) ResetPersisted(req core.PurgeRequest) (*core.ResponseData, error) {
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
respBytes, err := c.doAction("reset-persisted", reqBytes)
if err != nil {
return nil, err
}
return unmarshalResponseData(respBytes)
}
+25 -11
View File
@@ -35,17 +35,18 @@ type command struct {
var roServer *client.RemoteServer
var commandSet = map[string]command{
"create": command{Run: runCreate, Desc: "create the disk vault and admin account"},
"create-user": command{Run: runCreateUser, Desc: "create a user account"},
"summary": command{Run: runSummary, Desc: "list the user and delegation summary"},
"delegate": command{Run: runDelegate, Desc: "do decryption delegation"},
"encrypt": command{Run: runEncrypt, Desc: "encrypt a file"},
"decrypt": command{Run: runDecrypt, Desc: "decrypt a file"},
"re-encrypt": command{Run: runReEncrypt, Desc: "re-encrypt a file"},
"order": command{Run: runOrder, Desc: "place an order for delegations"},
"owners": command{Run: runOwner, Desc: "show owners list"},
"status": command{Run: runStatus, Desc: "show Red October persistent delegation state"},
"restore": command{Run: runRestore, Desc: "perform a restore delegation"},
"create": command{Run: runCreate, Desc: "create the disk vault and admin account"},
"create-user": command{Run: runCreateUser, Desc: "create a user account"},
"summary": command{Run: runSummary, Desc: "list the user and delegation summary"},
"delegate": command{Run: runDelegate, Desc: "do decryption delegation"},
"encrypt": command{Run: runEncrypt, Desc: "encrypt a file"},
"decrypt": command{Run: runDecrypt, Desc: "decrypt a file"},
"re-encrypt": command{Run: runReEncrypt, Desc: "re-encrypt a file"},
"order": command{Run: runOrder, Desc: "place an order for delegations"},
"owners": command{Run: runOwner, Desc: "show owners list"},
"status": command{Run: runStatus, Desc: "show Red October persistent delegation state"},
"restore": command{Run: runRestore, Desc: "perform a restore delegation"},
"reset-persisted": command{Run: runResetPersisted, Desc: "reset the persisted delegations"},
}
func registerFlags() {
@@ -313,6 +314,19 @@ func runStatus() {
fmt.Println(resp)
}
func runResetPersisted() {
req := core.PurgeRequest{
Name: user,
Password: pswd,
}
resp, err := roServer.ResetPersisted(req)
processError(err)
fmt.Println(resp.Status)
fmt.Println(resp)
}
func main() {
flag.Usage = func() {
fmt.Println("Usage: ro [options] subcommand")
+34
View File
@@ -995,3 +995,37 @@ func Restore(jsonIn []byte) (out []byte, err error) {
return jsonResponse(out)
}
// ResetPersisted clears the persisted user data from the server. This
// request requires an admin.
func ResetPersisted(jsonIn []byte) (out []byte, err error) {
var req PurgeRequest
defer func() {
if err != nil {
log.Printf("core.resetpersisted failed: user=%s %v", req.Name, err)
} else {
log.Printf("core.resetpersisted success: user=%s", req.Name)
}
}()
if err = json.Unmarshal(jsonIn, &req); err != nil {
return jsonStatusError(err)
}
if err := validateUser(req.Name, req.Password, true); err != nil {
return jsonStatusError(err)
}
st, err := crypt.ResetPersisted()
if err != nil {
return jsonStatusError(err)
}
resp := &StatusData{Status: st.State}
if out, err = json.Marshal(resp); err != nil {
return jsonStatusError(err)
}
return jsonResponse(out)
}
+18 -4
View File
@@ -1306,10 +1306,11 @@ func afterRestartRestore(t *testing.T) {
}
func restoreDelegateRestore(t *testing.T) {
delegateJsonBad := []byte("{\"Name\":\"Alice\",\"Password\":\"Hi\",\"Time\":\"5m\",\"Uses\":1}")
delegateJson1 := []byte("{\"Name\":\"Alice\",\"Password\":\"Hello\",\"Time\":\"5m\",\"Uses\":1}")
delegateJson2 := []byte("{\"Name\":\"Carl\",\"Password\":\"Hello\",\"Time\":\"5m\",\"Uses\":1}")
delegateJson3 := []byte("{\"Name\":\"Carl\",\"Password\":\"Hello\",\"Time\":\"5m\",\"Uses\":1}")
adminUser := []byte("{\"Name\":\"Admin\",\"Password\":\"Admin\"}")
delegateJsonBad := []byte("{\"Name\":\"Alice\",\"Password\":\"Hi\",\"Time\":\"5m\",\"Uses\":3}")
delegateJson1 := []byte("{\"Name\":\"Alice\",\"Password\":\"Hello\",\"Time\":\"5m\",\"Uses\":3}")
delegateJson2 := []byte("{\"Name\":\"Carl\",\"Password\":\"Hello\",\"Time\":\"5m\",\"Uses\":3}")
delegateJson3 := []byte("{\"Name\":\"Carl\",\"Password\":\"Hello\",\"Time\":\"5m\",\"Uses\":3}")
var s ResponseData
respJson, err := Restore(delegateJsonBad)
@@ -1382,6 +1383,19 @@ func restoreDelegateRestore(t *testing.T) {
} else if _, ok = summary.Live["Bob"]; !ok {
t.Fatalf("Bob should be present in the active delegations.")
}
respJson, err = ResetPersisted(adminUser)
if err != nil {
t.Fatalf("Resetting persisted delegations failed: %s", err)
}
err = json.Unmarshal(respJson, &s)
if err != nil {
t.Fatalf("Error resetting persisted delegations: %s", err)
} else if s.Status != "ok" {
t.Fatalf("Error resetting persisted delegations: %s", s.Status)
}
restoreState(t, persist.Active)
}
func TestRestore(t *testing.T) {
+14 -1
View File
@@ -777,7 +777,13 @@ func (c *Cryptor) Restore(name, password string, uses int, slot, durationString
return err
}
c.cache = keycache.NewFrom(uk)
rcache := keycache.NewFrom(uk)
err = rcache.Restore()
if err != nil {
return err
}
c.cache = rcache
c.persist.Persist()
c.persist.Cache().Flush()
return nil
@@ -787,3 +793,10 @@ func (c *Cryptor) Restore(name, password string, uses int, slot, durationString
func (c *Cryptor) Status() *persist.Status {
return c.persist.Status()
}
// ResetPersisted clears any persisted delegations and returns the
// vault to an active delegation state if configured.
func (c *Cryptor) ResetPersisted() (*persist.Status, error) {
err := c.persist.Purge()
return c.persist.Status(), err
}
+38
View File
@@ -11,6 +11,7 @@ import (
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/x509"
"errors"
"fmt"
"log"
@@ -44,6 +45,7 @@ type ActiveUser struct {
AltNames map[string]string
Admin bool
Type string
Key []byte
rsaKey rsa.PrivateKey
eccKey *ecdsa.PrivateKey
@@ -225,8 +227,16 @@ func (cache *Cache) AddKeyFromRecord(record passvault.PasswordRecord, name, pass
switch record.Type {
case passvault.RSARecord:
current.rsaKey, err = record.GetKeyRSA(password)
if err != nil {
return
}
current.Key = x509.MarshalPKCS1PrivateKey(&current.rsaKey)
case passvault.ECCRecord:
current.eccKey, err = record.GetKeyECC(password)
if err != nil {
return
}
current.Key, err = x509.MarshalECPrivateKey(current.eccKey)
default:
err = errors.New("Unknown record type")
}
@@ -370,3 +380,31 @@ func (cache *Cache) DelegateStatus(name string, labels, admins []string) (admins
}
return
}
// Restore unmarshals the private key stored in the delegator to the
// appropriate private structure.
func (cache *Cache) Restore() (err error) {
for index, uk := range cache.UserKeys {
if len(uk.Key) == 0 {
return errors.New("keycache: no private key in active user")
}
rsaPriv, err := x509.ParsePKCS1PrivateKey(uk.Key)
if err == nil {
uk.rsaKey = *rsaPriv
cache.UserKeys[index] = uk
continue
}
ecPriv, err := x509.ParseECPrivateKey(uk.Key)
if err == nil {
uk.eccKey = ecPriv
cache.UserKeys[index] = uk
continue
}
return err
}
return nil
}
+2 -1
View File
@@ -4,6 +4,7 @@ import (
"container/heap"
"crypto/rand"
"errors"
"fmt"
"strings"
)
@@ -191,7 +192,7 @@ func (m MSP) DistributeShares(sec []byte, db UserDatabase) (map[string][][]byte,
case Name:
name := cond.string
if !db.ValidUser(name) {
return nil, errors.New("Unknown user in predicate.")
return nil, fmt.Errorf("Unknown user '%s' in predicate.", name)
}
out[name] = append(out[name], share)
+9
View File
@@ -127,3 +127,12 @@ func (f *File) Status() *Status {
Summary: f.cache.GetSummary(),
}
}
func (f *File) Purge() error {
f.state = Active
f.blob = nil
if err := os.Remove(f.config.Location); err != nil {
return err
}
return nil
}
+98
View File
@@ -8,6 +8,11 @@ import (
"github.com/cloudflare/redoctober/config"
)
func fexists(path string) bool {
_, err := os.Stat(path)
return !os.IsNotExist(err)
}
func TestFileConfig(t *testing.T) {
cfg := &config.Delegations{
Persist: false,
@@ -165,3 +170,96 @@ func TestNewFilePersists(t *testing.T) {
t.Fatalf("fresh store should be persisting")
}
}
func TestActivePurge(t *testing.T) {
sf, err := tempName()
if err != nil {
t.Fatal(err)
}
defer os.Remove(sf)
cfg := &config.Delegations{
Persist: true,
Mechanism: FileMechanism,
Policy: "alice & bob",
Users: []string{"alice", "bob"},
Location: sf,
}
f, err := New(cfg)
if err != nil {
t.Fatal(err)
}
_, ok := f.(*File)
if !ok {
t.Fatalf("persist: expected to get a *File but have %T", f)
}
const expected = "test data"
if err = f.Store([]byte(expected)); err != nil {
t.Fatal(err)
}
if !fexists(sf) {
t.Fatalf("persist: file store wasn't written to disk")
}
err = f.Purge()
if err != nil {
t.Fatalf("%s", err)
}
if fexists(sf) {
t.Fatalf("persist: store should have been removed during purge")
}
}
func TestInactivePurge(t *testing.T) {
sf, err := tempName()
if err != nil {
t.Fatal(err)
}
defer os.Remove(sf)
cfg := &config.Delegations{
Persist: true,
Mechanism: FileMechanism,
Policy: "alice & bob",
Users: []string{"alice", "bob"},
Location: sf,
}
const expected = "test data"
err = ioutil.WriteFile(sf, []byte(expected), 0644)
if err != nil {
t.Fatalf("%s", err)
}
f, err := New(cfg)
if err != nil {
t.Fatal(err)
}
file, ok := f.(*File)
if !ok {
t.Fatalf("persist: expected to get a *File but have %T", f)
}
if err = f.Store([]byte(expected)); err != nil {
t.Fatal(err)
}
err = f.Purge()
if err != nil {
t.Fatalf("%s", err)
}
if fexists(sf) {
t.Fatalf("persist: store should have been removed during purge")
}
if file.Status().State != Active {
t.Fatalf("fresh store should be persisting")
}
}
+4
View File
@@ -57,3 +57,7 @@ func (n *Null) Cache() *keycache.Cache {
cache := keycache.NewCache()
return &cache
}
func (n *Null) Purge() error {
return nil
}
+5 -1
View File
@@ -55,6 +55,10 @@ func TestNewNull(t *testing.T) {
}
if cache := store.Cache(); len(cache.UserKeys) != 0 {
t.Fatal("persist: Null Cache should return an empty cache")
t.Fatal("persist: Null Cache() should return an empty cache")
}
if store.Purge() != nil {
t.Fatal("persist: Null Purge() shouldn't return an error")
}
}
+2
View File
@@ -53,6 +53,8 @@ type Store interface {
// This is not the main keycache. This is the keycache for
// users that can decrypt the store.
Cache() *keycache.Cache
// Purge clears the persisted keys.
Purge() error
}
// FileMechanism indicates that the persistence mechanism is a file.
+19 -18
View File
@@ -29,24 +29,25 @@ import (
// List of URLs to register and their related functions
var functions = map[string]func([]byte) ([]byte, error){
"/create": core.Create,
"/create-user": core.CreateUser,
"/summary": core.Summary,
"/purge": core.Purge,
"/delegate": core.Delegate,
"/password": core.Password,
"/encrypt": core.Encrypt,
"/re-encrypt": core.ReEncrypt,
"/decrypt": core.Decrypt,
"/owners": core.Owners,
"/modify": core.Modify,
"/export": core.Export,
"/order": core.Order,
"/orderout": core.OrdersOutstanding,
"/orderinfo": core.OrderInfo,
"/ordercancel": core.OrderCancel,
"/restore": core.Restore,
"/status": core.Status,
"/create": core.Create,
"/create-user": core.CreateUser,
"/summary": core.Summary,
"/purge": core.Purge,
"/delegate": core.Delegate,
"/password": core.Password,
"/encrypt": core.Encrypt,
"/re-encrypt": core.ReEncrypt,
"/decrypt": core.Decrypt,
"/owners": core.Owners,
"/modify": core.Modify,
"/export": core.Export,
"/order": core.Order,
"/orderout": core.OrdersOutstanding,
"/orderinfo": core.OrderInfo,
"/ordercancel": core.OrderCancel,
"/restore": core.Restore,
"/reset-persisted": core.ResetPersisted,
"/status": core.Status,
}
type userRequest struct {
+120 -13
View File
@@ -9,6 +9,7 @@ package main
import (
"bytes"
"crypto/tls"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
@@ -19,6 +20,7 @@ import (
"testing"
"time"
"github.com/cloudflare/redoctober/client"
"github.com/cloudflare/redoctober/config"
"github.com/cloudflare/redoctober/core"
"github.com/cloudflare/redoctober/persist"
@@ -59,7 +61,8 @@ var (
Uses: 1,
}
encryptInput = &core.EncryptRequest{
encryptMessage = "Why is a raven like a writing desk?\n"
encryptInput = &core.EncryptRequest{
Minimum: 2,
Name: createVaultInput.Name,
Password: createVaultInput.Password,
@@ -636,6 +639,22 @@ func TestPurge(t *testing.T) {
// These need to write files to disk in order to test recovering delegations. //
////////////////////////////////////////////////////////////////////////////////
var (
restore *client.RemoteServer
// restoreSecret is the encrypted data that should be decryptable
// both before and after the restart.
restoreSecret []byte
restoreEncryptInput = &core.EncryptRequest{
Minimum: 2,
Name: createVaultInput.Name,
Password: createVaultInput.Password,
Owners: []string{createUserInput3.Name, createUserInput2.Name},
Data: []byte(base64.StdEncoding.EncodeToString([]byte(encryptMessage))),
}
)
func restoreSetup(t *testing.T, configPath, vaultPath string) (cmd *exec.Cmd) {
const maxAttempts = 5
@@ -650,7 +669,6 @@ func restoreSetup(t *testing.T, configPath, vaultPath string) (cmd *exec.Cmd) {
}
cmd = exec.Command(binaryPath, "-vaultpath", vaultPath, "-f", configPath)
if err := cmd.Start(); err != nil {
t.Fatalf("Error running redoctober command, %v", err)
}
@@ -705,6 +723,9 @@ func TestRestore(t *testing.T) {
// The server has restarted --- verify that the persisted
// delegations are available.
afterRestartRestore(t, cfgPath, vaultPath)
// Verify that we can reset the persisted delegations.
afterRestartPurge(t, cfgPath, vaultPath)
}
func prepareSetup(t *testing.T, pstore, cfgPath string) {
@@ -736,6 +757,16 @@ func prepareSetup(t *testing.T, pstore, cfgPath string) {
if err != nil {
t.Fatalf("failed to write config file: %s", err)
}
// We'll have two uses: one to permit the pre-restart decryption,
// one to permit the post-restart decryption, and one to verify
// the purge functionality.
delegateInput1.Uses = 3
delegateInput2.Uses = 3
// Allow someone to decrypt.
delegateInput1.Users = []string{createVaultInput.Name}
delegateInput2.Users = []string{createVaultInput.Name}
}
func restoreCheckStatus(t *testing.T, expected string) {
@@ -816,17 +847,23 @@ func beforeRestartRestore(t *testing.T, cfgPath, vaultPath string) {
cmd := restoreSetup(t, cfgPath, vaultPath)
defer teardown(t, cmd)
srv, err := client.NewRemoteServer("localhost:8080", "testdata/server.crt")
if err != nil {
t.Fatalf("failed to set up client: %s", err)
}
// Create a vault/admin user and 2 normal users so there is data to work with.
if _, _, err := post("create", createVaultInput); err != nil {
if _, _, err = post("create", createVaultInput); err != nil {
t.Fatalf("failed to create the vault: %s", err)
}
if _, _, err := post("create-user", createUserInput1); err != nil {
if _, err = srv.CreateUser(*createUserInput1); err != nil {
t.Fatalf("couldn't create user %s: %s", createUserInput1.Name, err)
}
if _, _, err := post("create-user", createUserInput2); err != nil {
if _, err = srv.CreateUser(*createUserInput2); err != nil {
t.Fatalf("couldn't create user %s: %s", createUserInput2.Name, err)
}
if _, _, err := post("create-user", createUserInput3); err != nil {
if _, err = srv.CreateUser(*createUserInput3); err != nil {
t.Fatalf("couldn't create user %s: %s", createUserInput3.Name, err)
}
@@ -835,29 +872,55 @@ func beforeRestartRestore(t *testing.T, cfgPath, vaultPath string) {
restoreCheckStatus(t, persist.Active)
// Delegate two users.
if _, _, err := post("delegate", delegateInput2); err != nil {
if _, err = srv.Delegate(*delegateInput2); err != nil {
t.Fatalf("failed to delegate for %s: %s", delegateInput2.Name, err)
}
if _, _, err := post("delegate", delegateInput3); err != nil {
if _, err = srv.Delegate(*delegateInput3); err != nil {
t.Fatalf("failed to delegate for %s: %s", delegateInput3.Name, err)
}
restoreCheckLiveCount(t, 2)
restoreCheckLiveUsers(t, []string{delegateInput2.Name, delegateInput3.Name},
[]string{createUserInput1.Name, createVaultInput.Name})
// Encrypt a message, and make sure it can be decrypted.
resp, err := srv.Encrypt(*restoreEncryptInput)
restoreSecret = resp.Response
decryptInput := &core.DecryptRequest{
Name: createVaultInput.Name,
Password: createVaultInput.Password,
Data: restoreSecret[:],
}
decrypted, err := srv.DecryptIntoData(*decryptInput)
if err != nil {
t.Fatalf("failed to decrypt message: %s", err)
}
decryptedMessage, err := base64.StdEncoding.DecodeString(string(decrypted))
if string(decryptedMessage) != encryptMessage {
t.Fatalf("decryption produced the wrong message: want '%s' but have '%s'",
encryptMessage, decryptedMessage)
}
}
func afterRestartRestore(t *testing.T, cfgPath, vaultPath string) {
cmd := restoreSetup(t, cfgPath, vaultPath)
defer teardown(t, cmd)
srv, err := client.NewRemoteServer("localhost:8080", "testdata/server.crt")
if err != nil {
t.Fatalf("failed to set up client: %s", err)
}
// An existing vault with a persisted delegation store should
// be inactive.
restoreCheckStatus(t, persist.Inactive)
// Delegate a user who wasn't in the persisted delegation set.
if _, _, err := post("delegate", delegateInput1); err != nil {
t.Fatalf("Error delegating with user 1, %v", err)
if _, err := srv.Delegate(*delegateInput1); err != nil {
t.Fatalf("error delegating with user 1, %v", err)
}
restoreCheckLiveCount(t, 1)
@@ -865,7 +928,7 @@ func afterRestartRestore(t *testing.T, cfgPath, vaultPath string) {
[]string{createUserInput2.Name, delegateInput3.Name, createVaultInput.Name})
// Begin the restoration by delegating for a single user.
if _, _, err := post("restore", delegateInput1); err != nil {
if _, err := srv.Restore(*delegateInput1); err != nil {
t.Fatalf("restoration by user %s failed: %s", delegateInput1.Name, err)
}
@@ -877,12 +940,56 @@ func afterRestartRestore(t *testing.T, cfgPath, vaultPath string) {
// Delegate the second user, which should lead to restoring
// the delegations.
if _, _, err := post("restore", delegateInput4); err != nil {
t.Fatalf("restoration by user %s failed: %s", delegateInput4.Name, err)
if _, err := srv.Restore(*delegateInput4); err != nil {
t.Fatalf("restoration by user %s failed: %s", delegateInput1.Name, err)
}
restoreCheckStatus(t, persist.Active)
restoreCheckLiveCount(t, 2)
restoreCheckLiveUsers(t, []string{delegateInput2.Name, delegateInput3.Name},
[]string{createUserInput1.Name, createVaultInput.Name})
decryptInput := &core.DecryptRequest{
Name: createVaultInput.Name,
Password: createVaultInput.Password,
Data: restoreSecret[:],
}
decrypted, err := srv.DecryptIntoData(*decryptInput)
if err != nil {
t.Fatalf("failed to decrypt message: %s", err)
}
decryptedMessage, err := base64.StdEncoding.DecodeString(string(decrypted))
if string(decryptedMessage) != encryptMessage {
t.Fatalf("decryption produced the wrong message: want '%s' but have '%s'",
encryptMessage, decryptedMessage)
}
}
func afterRestartPurge(t *testing.T, cfgPath, vaultPath string) {
cmd := restoreSetup(t, cfgPath, vaultPath)
defer teardown(t, cmd)
srv, err := client.NewRemoteServer("localhost:8080", "testdata/server.crt")
if err != nil {
t.Fatalf("failed to set up client: %s", err)
}
// An existing vault with a persisted delegation store should
// be inactive.
restoreCheckStatus(t, persist.Inactive)
resp, err := srv.ResetPersisted(core.PurgeRequest{Name: createVaultInput.Name, Password: createVaultInput.Password})
if err != nil {
t.Fatalf("failed to reset persisted delegations: %s", err)
}
if resp.Status != "ok" {
t.Fatalf("failed to reset persisted delegations: %s", resp.Status)
}
// An existing vault whose persistence store has been reset
// should be active.
restoreCheckStatus(t, persist.Active)
}