veyron/services/identity: Revocation caveats are issued for every identity and can be revoked from the UI.
* Discharger services run alongside the identity server.
* Revocation manager persists revocation caveat information and ability to revoke them.
* Every Identity gets issued a veryon/services/identity/revocation.RevocationCaveat with their identity.
* Adding CSRF to the revoke page will come in later CL.
* Future UI improvements (showing what has been revoked) will come in a later CL.
Change-Id: I8fbb65c5df85e5cb48ca94293569a8a0ae41f448
diff --git a/services/identity/revocation/bless.go b/services/identity/revocation/bless.go
new file mode 100644
index 0000000..b90a6b0
--- /dev/null
+++ b/services/identity/revocation/bless.go
@@ -0,0 +1,48 @@
+package revocation
+
+import (
+ "fmt"
+ "time"
+
+ "veyron/security/audit"
+ "veyron/security/caveat"
+
+ "veyron2/security"
+)
+
+// Bless creates a blessing on behalf of the identity server.
+func Bless(server security.PrivateID, blessee security.PublicID, email string, duration time.Duration, revocationCaveat security.ThirdPartyCaveat) (security.PublicID, error) {
+ if revocationCaveat != nil {
+ // TODO(suharshs): Extend the duration for blessings with provided revocaionCaveats
+ return server.Bless(blessee, email, duration, []security.ServiceCaveat{caveat.UniversalCaveat(revocationCaveat)})
+ }
+ // return a blessing with a more limited duration, since there is no revocation caveat
+ return server.Bless(blessee, email, duration, nil)
+}
+
+type BlessingAuditEntry struct {
+ Blessee, Blessed security.PublicID
+ Start, End time.Time
+ RevocationCaveat security.ThirdPartyCaveat
+}
+
+// ReadBlessAuditEntry is for use in the googleauth.handler to parse the arguments to the Bless call in util.Bless.
+func ReadBlessAuditEntry(entry audit.Entry) (BlessingAuditEntry, error) {
+ var blessEntry BlessingAuditEntry
+
+ if len(entry.Arguments) < 4 || len(entry.Results) < 1 {
+ return blessEntry, fmt.Errorf("entry is invalid format")
+ }
+
+ blessEntry.Blessee, _ = entry.Arguments[0].(security.PublicID)
+ blessEntry.Start = entry.Timestamp
+ if duration, ok := entry.Arguments[2].(int64); ok {
+ blessEntry.End = blessEntry.Start.Add(time.Duration(duration))
+ }
+ blessEntry.Blessed, _ = entry.Results[0].(security.PublicID)
+ caveats, _ := entry.Arguments[3].([]security.ServiceCaveat)
+ if len(caveats) > 0 {
+ blessEntry.RevocationCaveat, _ = caveats[0].Caveat.(security.ThirdPartyCaveat)
+ }
+ return blessEntry, nil
+}
diff --git a/services/identity/revocation/bless_test.go b/services/identity/revocation/bless_test.go
new file mode 100644
index 0000000..63dc2bc
--- /dev/null
+++ b/services/identity/revocation/bless_test.go
@@ -0,0 +1,97 @@
+package revocation
+
+import (
+ "os"
+ "path/filepath"
+ "reflect"
+ "testing"
+ "time"
+
+ "veyron/security/audit"
+ "veyron/services/security/discharger"
+
+ "veyron2/rt"
+ "veyron2/security"
+)
+
+type auditor struct {
+ LastEntry audit.Entry
+}
+
+func (a *auditor) Audit(entry audit.Entry) error {
+ a.LastEntry = entry
+ return nil
+}
+
+func newAuditedPrivateID(a *auditor) (security.PrivateID, error) {
+ r, err := rt.New()
+ if err != nil {
+ return nil, err
+ }
+ defer r.Cleanup()
+ id := r.Identity()
+ if err != nil {
+ return nil, err
+ }
+ return audit.NewPrivateID(id, a), err
+}
+
+func TestReadBlessAudit(t *testing.T) {
+ var a auditor
+ var revocationDir = filepath.Join(os.TempDir(), "util_bless_test_dir")
+ os.MkdirAll(revocationDir, 0700)
+ defer os.RemoveAll(revocationDir)
+
+ self, err := newAuditedPrivateID(&a)
+ if err != nil {
+ t.Fatalf("failed to create new audited private id: %v", err)
+ }
+
+ // Test caveat
+ correct_blessee := self.PublicID()
+
+ _, cav, err := discharger.NewRevocationCaveat(self.PublicID(), "")
+ if err != nil {
+ t.Fatalf("discharger.NewRevocationCaveat failed: %v", err)
+ }
+
+ correct_blessed, err := Bless(self, self.PublicID(), "test", time.Second, cav)
+ if err != nil {
+ t.Fatalf("Bless: failed with caveats: %v", err)
+ }
+
+ var blessEntry BlessingAuditEntry
+ blessEntry, err = ReadBlessAuditEntry(a.LastEntry)
+ if err != nil {
+ t.Fatal("ReadBlessAuditEntryFailed %v:", err)
+ }
+ if !reflect.DeepEqual(blessEntry.Blessee, correct_blessee) {
+ t.Errorf("blessee incorrect: expected %v got %v", correct_blessee, blessEntry.Blessee)
+ }
+ if !reflect.DeepEqual(blessEntry.Blessed, correct_blessed) {
+ t.Errorf("blessed incorrect: expected %v got %v", correct_blessed, blessEntry.Blessed)
+ }
+ if blessEntry.RevocationCaveat.ID() != cav.ID() {
+ t.Errorf("caveat ID incorrect: expected %s got %s", cav.ID(), blessEntry.RevocationCaveat.ID())
+ }
+
+ // Test no caveat
+ correct_blessed, err = Bless(self, self.PublicID(), "test", time.Second, nil)
+ if err != nil {
+ t.Fatalf("Bless: failed with no caveats: %v", err)
+ }
+
+ blessEntry, err = ReadBlessAuditEntry(a.LastEntry)
+ if err != nil {
+ t.Fatal("ReadBlessAuditEntryFailed %v:", err)
+ }
+ if !reflect.DeepEqual(blessEntry.Blessee, correct_blessee) {
+ t.Errorf("blessee incorrect: expected %v got %v", correct_blessee, blessEntry.Blessee)
+ }
+ if !reflect.DeepEqual(blessEntry.Blessed, correct_blessed) {
+ t.Errorf("blessed incorrect: expected %v got %v", correct_blessed, blessEntry.Blessed)
+ }
+ if blessEntry.RevocationCaveat != nil {
+ t.Errorf("caveat ID incorrect: expected %s got %s", cav.ID(), blessEntry.RevocationCaveat.ID())
+ }
+}
diff --git a/services/identity/revocation/revocation_manager.go b/services/identity/revocation/revocation_manager.go
new file mode 100644
index 0000000..e37dc90
--- /dev/null
+++ b/services/identity/revocation/revocation_manager.go
@@ -0,0 +1,108 @@
+// Package revocation provides tools to create and manage revocation caveats.
+package revocation
+
+import (
+ "crypto/rand"
+ "encoding/hex"
+ "fmt"
+ "path/filepath"
+ "sync"
+ "time"
+
+ "veyron/security/caveat"
+ "veyron/services/identity/util"
+ "veyron2/security"
+ "veyron2/vom"
+)
+
+// RevocationManager persists information for revocation caveats to provided discharges and allow for future revocations.
+type RevocationManager struct {
+ caveatMap *util.DirectoryStore // Map of blessed identity's caveats. ThirdPartyCaveatID -> revocationCaveatID
+}
+
+var revocationMap *util.DirectoryStore
+var revocationLock sync.RWMutex
+
+// NewCaveat returns a security.ThirdPartyCaveat for which discharges will be
+// issued iff Revoke has not been called for the returned caveat.
+func (r *RevocationManager) NewCaveat(dischargerID security.PublicID, dischargerLocation string) (security.ThirdPartyCaveat, error) {
+ var revocation [16]byte
+ if _, err := rand.Read(revocation[:]); err != nil {
+ return nil, err
+ }
+ restriction := revocationCaveat(revocation)
+ cav, err := caveat.NewPublicKeyCaveat(restriction, dischargerID, dischargerLocation, security.ThirdPartyRequirements{})
+ if err != nil {
+ return nil, err
+ }
+ if err = r.caveatMap.Put(hex.EncodeToString([]byte(cav.ID())), hex.EncodeToString(revocation[:])); err != nil {
+ return nil, err
+ }
+ return cav, nil
+}
+
+// Revoke disables discharges from being issued for the provided third-party caveat.
+func (r *RevocationManager) Revoke(caveatID security.ThirdPartyCaveatID) error {
+ token, err := r.caveatMap.Get(hex.EncodeToString([]byte(caveatID)))
+ if err != nil {
+ return err
+ }
+ return revocationMap.Put(token, string(time.Now().Unix()))
+}
+
+// Returns true if the provided caveat has been revoked.
+func (r *RevocationManager) IsRevoked(caveatID security.ThirdPartyCaveatID) bool {
+ token, err := r.caveatMap.Get(hex.EncodeToString([]byte(caveatID)))
+ if err == nil {
+ exists, _ := revocationMap.Exists(token)
+ return exists
+ }
+ return false
+}
+
+type revocationCaveat [16]byte
+
+func (cav revocationCaveat) Validate(security.Context) error {
+ revocationLock.RLock()
+ if revocationMap == nil {
+ revocationLock.RUnlock()
+ return fmt.Errorf("missing call to NewRevocationManager")
+ }
+ revocationLock.RUnlock()
+ exists, err := revocationMap.Exists(hex.EncodeToString(cav[:]))
+ if err != nil {
+ return err
+ }
+ if exists {
+ return fmt.Errorf("revoked")
+ }
+ return nil
+}
+
+// NewRevocationManager returns a RevocationManager that persists information about
+// revocationCaveats and allows for revocation and caveat creation.
+// This function can only be called once because of the use of global variables.
+func NewRevocationManager(dir string) (*RevocationManager, error) {
+ revocationLock.Lock()
+ defer revocationLock.Unlock()
+ if revocationMap != nil {
+ return nil, fmt.Errorf("NewRevocationManager can only be called once")
+ }
+ // If empty string return nil revocationManager
+ if len(dir) == 0 {
+ return nil, nil
+ }
+ caveatMap, err := util.NewDirectoryStore(filepath.Join(dir, "caveat_dir"))
+ if err != nil {
+ return nil, err
+ }
+ revocationMap, err = util.NewDirectoryStore(filepath.Join(dir, "revocation_dir"))
+ if err != nil {
+ return nil, err
+ }
+ return &RevocationManager{caveatMap}, nil
+}
+
+func init() {
+ vom.Register(revocationCaveat{})
+}
diff --git a/services/identity/revocation/revoker_test.go b/services/identity/revocation/revoker_test.go
new file mode 100644
index 0000000..036bd37
--- /dev/null
+++ b/services/identity/revocation/revoker_test.go
@@ -0,0 +1,77 @@
+package revocation
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+ services "veyron/services/security"
+ "veyron/services/security/discharger"
+ "veyron2"
+ "veyron2/ipc"
+ "veyron2/naming"
+ "veyron2/rt"
+ "veyron2/security"
+)
+
+func revokerSetup(t *testing.T) (dischargerID security.PublicID, dischargerEndpoint string, revoker *RevocationManager, closeFunc func(), runtime veyron2.Runtime) {
+ var dir = filepath.Join(os.TempDir(), "revoker_test_dir")
+ r := rt.Init()
+ revokerService, err := NewRevocationManager(dir)
+ if err != nil {
+ t.Fatalf("NewRevocationManager failed: %v", err)
+ }
+
+ dischargerServer, err := r.NewServer()
+ if err != nil {
+ t.Fatalf("rt.R().NewServer: %s", err)
+ }
+ dischargerEP, err := dischargerServer.Listen("tcp", "127.0.0.1:0")
+ if err != nil {
+ t.Fatalf("dischargerServer.Listen failed: %v", err)
+ }
+ dischargerServiceStub := services.NewServerDischarger(discharger.NewDischarger(r.Identity()))
+ if err := dischargerServer.Serve("", ipc.LeafDispatcher(dischargerServiceStub, nil)); err != nil {
+ t.Fatalf("dischargerServer.Serve revoker: %s", err)
+ }
+ return r.Identity().PublicID(),
+ naming.JoinAddressName(dischargerEP.String(), ""),
+ revokerService,
+ func() {
+ defer os.RemoveAll(dir)
+ dischargerServer.Stop()
+ },
+ r
+}
+
+func TestDischargeRevokeDischargeRevokeDischarge(t *testing.T) {
+ dcID, dc, revoker, closeFunc, r := revokerSetup(t)
+ defer closeFunc()
+
+ discharger, err := services.BindDischarger(dc)
+ if err != nil {
+ t.Fatalf("error binding to server: ", err)
+ }
+
+ cav, err := revoker.NewCaveat(dcID, dc)
+ if err != nil {
+ t.Fatalf("failed to create public key caveat: %s", err)
+ }
+
+ var impetus security.DischargeImpetus
+
+ if _, err = discharger.Discharge(r.NewContext(), cav, impetus); err != nil {
+ t.Fatalf("failed to get discharge: %s", err)
+ }
+ if err = revoker.Revoke(cav.ID()); err != nil {
+ t.Fatalf("failed to revoke: %s", err)
+ }
+ if discharge, err := discharger.Discharge(r.NewContext(), cav, impetus); err == nil || discharge != nil {
+ t.Fatalf("got a discharge for a revoked caveat: %s", err)
+ }
+ if err = revoker.Revoke(cav.ID()); err != nil {
+ t.Fatalf("failed to revoke again: %s", err)
+ }
+ if discharge, err := discharger.Discharge(r.NewContext(), cav, impetus); err == nil || discharge != nil {
+ t.Fatalf("got a discharge for a doubly revoked caveat: %s", err)
+ }
+}