diff --git a/client/client.go b/client/client.go index 19564ee..414ea29 100644 --- a/client/client.go +++ b/client/client.go @@ -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) +} diff --git a/cmd/ro/main.go b/cmd/ro/main.go index 57ebbd8..1218382 100644 --- a/cmd/ro/main.go +++ b/cmd/ro/main.go @@ -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") diff --git a/core/core.go b/core/core.go index c0baa8d..ac23c63 100644 --- a/core/core.go +++ b/core/core.go @@ -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) +} diff --git a/core/core_test.go b/core/core_test.go index f861f81..8fa8e33 100644 --- a/core/core_test.go +++ b/core/core_test.go @@ -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) { diff --git a/cryptor/cryptor.go b/cryptor/cryptor.go index 4d9b945..ea8890e 100644 --- a/cryptor/cryptor.go +++ b/cryptor/cryptor.go @@ -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 +} diff --git a/keycache/keycache.go b/keycache/keycache.go index 9268964..00443e8 100644 --- a/keycache/keycache.go +++ b/keycache/keycache.go @@ -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(¤t.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 +} diff --git a/msp/msp.go b/msp/msp.go index 75a7782..3e11621 100644 --- a/msp/msp.go +++ b/msp/msp.go @@ -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) diff --git a/persist/file.go b/persist/file.go index b03a018..3b7144a 100644 --- a/persist/file.go +++ b/persist/file.go @@ -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 +} diff --git a/persist/file_test.go b/persist/file_test.go index 44f6963..05c03fe 100644 --- a/persist/file_test.go +++ b/persist/file_test.go @@ -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") + } +} diff --git a/persist/null.go b/persist/null.go index 8d9d4fd..3f261ec 100644 --- a/persist/null.go +++ b/persist/null.go @@ -57,3 +57,7 @@ func (n *Null) Cache() *keycache.Cache { cache := keycache.NewCache() return &cache } + +func (n *Null) Purge() error { + return nil +} diff --git a/persist/null_test.go b/persist/null_test.go index af972cc..60bf9e3 100644 --- a/persist/null_test.go +++ b/persist/null_test.go @@ -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") } } diff --git a/persist/persist.go b/persist/persist.go index 8eaf5f8..b0a91c1 100644 --- a/persist/persist.go +++ b/persist/persist.go @@ -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. diff --git a/redoctober.go b/redoctober.go index 27a2f7c..1d0f7e3 100644 --- a/redoctober.go +++ b/redoctober.go @@ -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 { diff --git a/redoctober_test.go b/redoctober_test.go index 05e8aa6..46199b0 100644 --- a/redoctober_test.go +++ b/redoctober_test.go @@ -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) }