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/blesser/oauth.go b/services/identity/blesser/oauth.go
index 90d3875..e865b30 100644
--- a/services/identity/blesser/oauth.go
+++ b/services/identity/blesser/oauth.go
@@ -9,18 +9,23 @@
 
 	"veyron/services/identity"
 	"veyron/services/identity/googleoauth"
+	"veyron/services/identity/revocation"
+
 	"veyron2"
 	"veyron2/ipc"
+	"veyron2/security"
 	"veyron2/vdl/vdlutil"
 	"veyron2/vlog"
 )
 
 type googleOAuth struct {
-	rt                veyron2.Runtime
-	authcodeClient    struct{ ID, Secret string }
-	accessTokenClient struct{ ID string }
-	duration          time.Duration
-	domain            string
+	rt                 veyron2.Runtime
+	authcodeClient     struct{ ID, Secret string }
+	accessTokenClient  struct{ ID string }
+	duration           time.Duration
+	domain             string
+	dischargerLocation string
+	revocationManager  *revocation.RevocationManager
 }
 
 // GoogleParams represents all the parameters provided to NewGoogleOAuthBlesserServer
@@ -39,6 +44,10 @@
 	BlessingDuration time.Duration
 	// If non-empty, only email addresses from this domain will be blessed.
 	DomainRestriction string
+	// The object name of the discharger service. If this is empty then revocation caveats will not be granted.
+	DischargerLocation string
+	// The revocation manager that generates caveats and manages revocation.
+	RevocationManager *revocation.RevocationManager
 }
 
 // NewGoogleOAuthBlesserServer provides an identity.OAuthBlesserService that uses authorization
@@ -51,9 +60,11 @@
 // are generated only for email addresses from that domain.
 func NewGoogleOAuthBlesserServer(p GoogleParams) interface{} {
 	b := &googleOAuth{
-		rt:       p.R,
-		duration: p.BlessingDuration,
-		domain:   p.DomainRestriction,
+		rt:                 p.R,
+		duration:           p.BlessingDuration,
+		domain:             p.DomainRestriction,
+		dischargerLocation: p.DischargerLocation,
+		revocationManager:  p.RevocationManager,
 	}
 	b.authcodeClient.ID = p.AuthorizationCodeClient.ID
 	b.authcodeClient.Secret = p.AuthorizationCodeClient.Secret
@@ -119,7 +130,13 @@
 	if self, err = self.Derive(ctx.LocalID()); err != nil {
 		return nil, err
 	}
-	// TODO(ashankar,ataly): Use the same set of caveats as is used by the HTTP handler.
-	// For example, a third-party revocation caveat?
-	return self.Bless(ctx.RemoteID(), name, b.duration, nil)
+	var revocationCaveat security.ThirdPartyCaveat
+	if b.revocationManager != nil {
+		revocationCaveat, err = b.revocationManager.NewCaveat(b.rt.Identity().PublicID(), b.dischargerLocation)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return revocation.Bless(self, ctx.RemoteID(), name, b.duration, revocationCaveat)
 }
diff --git a/services/identity/googleoauth/handler.go b/services/identity/googleoauth/handler.go
index 54b18bd..96b0709 100644
--- a/services/identity/googleoauth/handler.go
+++ b/services/identity/googleoauth/handler.go
@@ -11,6 +11,7 @@
 package googleoauth
 
 import (
+	"encoding/base64"
 	"encoding/json"
 	"fmt"
 	"net/http"
@@ -21,6 +22,7 @@
 	"code.google.com/p/goauth2/oauth"
 
 	"veyron/services/identity/auditor"
+	"veyron/services/identity/revocation"
 	"veyron/services/identity/util"
 	"veyron2/security"
 	"veyron2/vlog"
@@ -113,9 +115,10 @@
 		return
 	}
 	type tmplentry struct {
-		Blessee    security.PublicID
-		Start, End time.Time
-		Blessed    security.PublicID
+		Blessee            security.PublicID
+		Start, End         time.Time
+		Blessed            security.PublicID
+		RevocationCaveatID string
 	}
 	tmplargs := struct {
 		Log   chan tmplentry
@@ -136,14 +139,21 @@
 					continue
 				}
 				var tmplentry tmplentry
-				tmplentry.Blessee, _ = entry.Arguments[0].(security.PublicID)
-				tmplentry.Start = entry.Timestamp
-				if duration, ok := entry.Arguments[2].(int64); ok {
-					tmplentry.End = tmplentry.Start.Add(time.Duration(duration))
+				var blessEntry revocation.BlessingAuditEntry
+				blessEntry, err = revocation.ReadBlessAuditEntry(entry)
+				tmplentry.Blessee = blessEntry.Blessee
+				tmplentry.Blessed = blessEntry.Blessed
+				tmplentry.Start = blessEntry.Start
+				tmplentry.End = blessEntry.End
+				if err != nil {
+					vlog.Errorf("Unable to read bless audit entry: %v", err)
+					continue
 				}
-				if len(entry.Results) > 0 {
-					tmplentry.Blessed, _ = entry.Results[0].(security.PublicID)
+				if blessEntry.RevocationCaveat != nil {
+					tmplentry.RevocationCaveatID = base64.URLEncoding.EncodeToString([]byte(blessEntry.RevocationCaveat.ID()))
 				}
+				// TODO(suharshs): Make the UI depend on where the caveatID exists and if it hasn't been revoked.
+				// Use the revocation manager IsRevoked function.
 				ch <- tmplentry
 			}
 		}(tmplargs.Log)
diff --git a/services/identity/googleoauth/template.go b/services/identity/googleoauth/template.go
index 8b6a817..4c03534 100644
--- a/services/identity/googleoauth/template.go
+++ b/services/identity/googleoauth/template.go
@@ -7,6 +7,7 @@
 	"html/template"
 )
 
+// TODO(suharshs): Add an if statement to only show the revoke buttons for non-revoked ids.
 var tmpl = template.Must(template.New("auditor").Funcs(tmplFuncMap()).Parse(`<!doctype html>
 <html>
 <head>
@@ -37,7 +38,23 @@
     $(this).click(function() { setTimeText($(this)); });
     setTimeText($(this));
   });
+
+  // Setup the revoke buttons click events.
+  $(".revoke").click(function() {
+    // TODO(suharshs): Implement "authorization" so that just making this post request with the URL doesn't work.
+    var revokeButton = $(this);
+    $.ajax({
+      url: "/revoke/",
+      type: "POST",
+      data: $(this).val()
+    }).done(function(data) {
+      // TODO(suharshs): Have a fail message, add a strikethrough on the revoked caveats.
+      console.log(data)
+      revokeButton.remove()
+    });
+  });
 });
+
 </script>
 </head>
 <body>
@@ -51,6 +68,7 @@
   <th>Issued</th>
   <th>Expires</th>
   <th>PublicKey</th>
+  <th>Revoked</th>
   </tr>
 </thead>
 <tbody>
@@ -61,6 +79,7 @@
 <td><div class="unixtime" data-unixtime={{.Start.Unix}}>{{.Start.String}}</div></td>
 <td><div class="unixtime" data-unixtime={{.End.Unix}}>{{.End.String}}</div></td>
 <td>{{publicKeyHash .Blessee.PublicKey}}</td>
+<td><button class="revoke" value="{{.RevocationCaveatID}}" type="button">Revoke</button></td>
 </tr>
 {{else}}
 <tr>
diff --git a/services/identity/handlers/revoke.go b/services/identity/handlers/revoke.go
new file mode 100644
index 0000000..ff3d5a4
--- /dev/null
+++ b/services/identity/handlers/revoke.go
@@ -0,0 +1,50 @@
+package handlers
+
+import (
+	"encoding/base64"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"veyron/services/identity/revocation"
+
+	"veyron2/security"
+)
+
+// Revoke is an http.Handler implementation that revokes a Veyron PrivateID.
+type Revoke struct {
+	RevocationManager *revocation.RevocationManager
+}
+
+// TODO(suharshs): Move this to the googleoauth handler to enable authorization on this.
+func (h Revoke) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Content-Type", "application/json")
+	const (
+		success = `{"success": "true"}`
+		failure = `{"success": "false"}`
+	)
+	// Get the caveat string from the request.
+	// TODO(suharshs): Send a multi part form value from the client to server and parse it here.
+	content, err := ioutil.ReadAll(r.Body)
+	if err != nil {
+		fmt.Printf("Failed to parse request: %s", err)
+		w.Write([]byte(failure))
+		return
+	}
+	decoded_caveatID, err := base64.URLEncoding.DecodeString(string(content))
+	if err != nil {
+		fmt.Printf("base64 decoding failed: %s", err)
+		w.Write([]byte(failure))
+		return
+	}
+
+	caveatID := security.ThirdPartyCaveatID(string(decoded_caveatID))
+
+	if err := h.RevocationManager.Revoke(caveatID); err != nil {
+		fmt.Printf("Revocation failed: %s", err)
+		w.Write([]byte(failure))
+		return
+	}
+
+	w.Write([]byte(success))
+	return
+}
diff --git a/services/identity/identityd/main.go b/services/identity/identityd/main.go
index 255d91f..48ba659 100644
--- a/services/identity/identityd/main.go
+++ b/services/identity/identityd/main.go
@@ -8,6 +8,7 @@
 	"net"
 	"net/http"
 	"os"
+	"path/filepath"
 	"strings"
 	"time"
 
@@ -18,6 +19,9 @@
 	"veyron/services/identity/blesser"
 	"veyron/services/identity/googleoauth"
 	"veyron/services/identity/handlers"
+	"veyron/services/identity/revocation"
+	services "veyron/services/security"
+	"veyron/services/security/discharger"
 
 	"veyron2"
 	"veyron2/ipc"
@@ -44,6 +48,9 @@
 	googleConfigInstalled = flag.String("google_config_installed", "", "Path to the JSON-encoded OAuth client configuration for installed client applications that obtain blessings (via the OAuthBlesser.BlessUsingAuthorizationCode RPC) from this server (like the 'identity' command like tool and its 'seekblessing' command.")
 	googleConfigChrome    = flag.String("google_config_chrome", "", "Path to the JSON-encoded OAuth client configuration for Chrome browser applications that obtain blessings from this server (via the OAuthBlesser.BlessUsingAccessToken RPC) from this server.")
 	googleDomain          = flag.String("google_domain", "", "An optional domain name. When set, only email addresses from this domain are allowed to authenticate via Google OAuth")
+
+	// Revoker/Discharger configuration
+	revocationDir = flag.String("revocation_dir", filepath.Join(os.TempDir(), "revocation_dir"), "Path where the revocation manager will store caveat and revocation information.")
 )
 
 func main() {
@@ -55,15 +62,25 @@
 		dumpAuditLog()
 		return
 	}
+
+	// Calling with empty string returns a empty RevocationManager
+	revocationManager, err := revocation.NewRevocationManager(*revocationDir)
+	if err != nil {
+		vlog.Fatalf("Failed to start RevocationManager: %v", err)
+	}
+
 	// Setup handlers
 	http.Handle("/pubkey/", handlers.Object{r.Identity().PublicID().PublicKey()}) // public key of this identity server
 	if enableRandomHandler() {
 		http.Handle("/random/", handlers.Random{r}) // mint identities with a random name
 	}
 	http.HandleFunc("/bless/", handlers.Bless) // use a provided PrivateID to bless a provided PublicID
+	if revocationManager != nil {
+		http.Handle("/revoke/", handlers.Revoke{revocationManager}) // revoke an identity that was provided by the server.
+	}
 
 	// Google OAuth
-	ipcServer, ipcServerEP, err := setupGoogleBlessingServer(r)
+	ipcServer, ipcServerEP, err := setupGoogleBlessingDischargingServer(r, revocationManager)
 	if err != nil {
 		vlog.Fatalf("Failed to setup veyron services for blessing: %v", err)
 	}
@@ -86,17 +103,18 @@
 		}
 		if len(servers) == 0 {
 			// No addresses published, publish the endpoint instead (which may not be usable everywhere, but oh-well).
-			servers = append(servers, naming.JoinAddressName(ipcServerEP.String(), ""))
+			servers = append(servers, ipcServerEP.String())
 		}
 		args := struct {
-			Self                 string
-			GoogleWeb, RandomWeb bool
-			GoogleServers        []string
+			Self                            string
+			GoogleWeb, RandomWeb            bool
+			GoogleServers, DischargeServers []string
 		}{
-			Self:          rt.R().Identity().PublicID().Names()[0],
-			GoogleWeb:     len(*googleConfigWeb) > 0,
-			RandomWeb:     enableRandomHandler(),
-			GoogleServers: servers,
+			Self:             rt.R().Identity().PublicID().Names()[0],
+			GoogleWeb:        len(*googleConfigWeb) > 0,
+			RandomWeb:        enableRandomHandler(),
+			GoogleServers:    appendSuffixTo(servers, "google"),
+			DischargeServers: appendSuffixTo(servers, "discharger"),
 		}
 		if err := tmpl.Execute(w, args); err != nil {
 			vlog.Info("Failed to render template:", err)
@@ -107,12 +125,49 @@
 	<-signals.ShutdownOnSignals()
 }
 
-func setupGoogleBlessingServer(r veyron2.Runtime) (ipc.Server, naming.Endpoint, error) {
+func appendSuffixTo(objectname []string, suffix string) []string {
+	names := make([]string, len(objectname))
+	for i, o := range objectname {
+		names[i] = naming.JoinAddressName(o, suffix)
+	}
+	return names
+}
+
+// newDispatcher returns a dispatcher for both the blessing and the discharging service.
+// their suffix. ReflectInvoker is used to invoke methods.
+func newDispatcher(params blesser.GoogleParams) ipc.Dispatcher {
+	blessingService := ipc.ReflectInvoker(blesser.NewGoogleOAuthBlesserServer(params))
+	dischargerService := ipc.ReflectInvoker(services.NewServerDischarger(discharger.NewDischarger(params.R.Identity())))
+	allowEveryoneACLAuth := vsecurity.NewACLAuthorizer(vsecurity.NewWhitelistACL(map[security.BlessingPattern]security.LabelSet{
+		security.AllPrincipals: security.AllLabels,
+	}))
+	return &dispatcher{blessingService, dischargerService, allowEveryoneACLAuth}
+}
+
+type dispatcher struct {
+	blessingInvoker, dischargerInvoker ipc.Invoker
+	auth                               security.Authorizer
+}
+
+func (d dispatcher) Lookup(suffix, method string) (ipc.Invoker, security.Authorizer, error) {
+	switch suffix {
+	case "google":
+		return d.blessingInvoker, d.auth, nil
+	case "discharger":
+		return d.dischargerInvoker, d.auth, nil
+	default:
+		return nil, nil, fmt.Errorf("suffix does not exist")
+	}
+}
+
+// Starts the blessing service and the discharging service on the same port.
+func setupGoogleBlessingDischargingServer(r veyron2.Runtime, revocationManager *revocation.RevocationManager) (ipc.Server, naming.Endpoint, error) {
 	var enable bool
 	params := blesser.GoogleParams{
 		R:                 r,
 		BlessingDuration:  time.Duration(*minExpiryDays) * 24 * time.Hour,
 		DomainRestriction: *googleDomain,
+		RevocationManager: revocationManager,
 	}
 	if authcode, clientID, clientSecret := enableGoogleOAuth(*googleConfigInstalled); authcode {
 		enable = true
@@ -134,14 +189,13 @@
 	if err != nil {
 		return nil, nil, fmt.Errorf("server.Listen(%q, %q) failed: %v", "tcp", *address, err)
 	}
-	allowEveryoneACL := vsecurity.NewWhitelistACL(map[security.BlessingPattern]security.LabelSet{
-		security.AllPrincipals: security.AllLabels,
-	})
-	objectname := fmt.Sprintf("identity/%s/google", r.Identity().PublicID().Names()[0])
-	if err := server.Serve(objectname, ipc.LeafDispatcher(blesser.NewGoogleOAuthBlesserServer(params), vsecurity.NewACLAuthorizer(allowEveryoneACL))); err != nil {
-		return nil, nil, fmt.Errorf("failed to start Veyron service: %v", err)
+	params.DischargerLocation = naming.JoinAddressName(ep.String(), "discharger")
+	dispatcher := newDispatcher(params)
+	objectname := fmt.Sprintf("identity/%s", r.Identity().PublicID().Names()[0])
+	if err := server.Serve(objectname, dispatcher); err != nil {
+		return nil, nil, fmt.Errorf("failed to start Veyron services: %v", err)
 	}
-	vlog.Infof("Google blessing service enabled at endpoint %v and name %q", ep, objectname)
+	vlog.Infof("Google blessing and discharger services enabled at endpoint %v and name %q", ep, objectname)
 	return server, ep, nil
 }
 
@@ -281,6 +335,12 @@
 Blessings are provided via Veyron RPCs to: <tt>{{range .GoogleServers}}{{.}}{{end}}</tt>
 </div>
 {{end}}
+{{if .DischargeServers}}
+<div class="well">
+RevocationCaveat Discharges are provided via Veyron RPCs to: <tt>{{range .DischargeServers}}{{.}}{{end}}</tt>
+</div>
+{{end}}
+
 
 {{if .GoogleWeb}}
 <div class="well">
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)
+	}
+}
diff --git a/services/identity/util/directory_store.go b/services/identity/util/directory_store.go
new file mode 100644
index 0000000..ef722d5
--- /dev/null
+++ b/services/identity/util/directory_store.go
@@ -0,0 +1,46 @@
+package util
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+)
+
+// DirectoryStore implements a key-value store on a filesystem where data for each key is stored in its own file.
+// TODO(suharshs): When vstore is ready replace this with the veyron store.
+type DirectoryStore struct {
+	dir string
+}
+
+func (s DirectoryStore) Exists(key string) (bool, error) {
+	_, err := os.Stat(s.pathName(key))
+	return !os.IsNotExist(err), nil
+}
+
+func (s DirectoryStore) Put(key, value string) error {
+	return ioutil.WriteFile(s.pathName(key), []byte(value), 0600)
+}
+
+func (s DirectoryStore) Get(key string) (string, error) {
+	bytes, err := ioutil.ReadFile(s.pathName(key))
+	return string(bytes), err
+}
+
+func (s DirectoryStore) pathName(key string) string {
+	return filepath.Join(string(s.dir), key)
+}
+
+// NewDirectoryStore returns a key-value store that uses one file per key,
+// and places all data in the provided directory.
+func NewDirectoryStore(dir string) (*DirectoryStore, error) {
+	if len(dir) == 0 {
+		return nil, fmt.Errorf("must provide non-empty directory name")
+	}
+	// Make the directory if it doesn't already exist.
+	if err := os.MkdirAll(dir, 0700); err != nil {
+		return nil, err
+	}
+
+	return &DirectoryStore{dir}, nil
+}
diff --git a/services/security/discharger/revoker.go b/services/security/discharger/revoker.go
index 524e688..6e6cba8 100644
--- a/services/security/discharger/revoker.go
+++ b/services/security/discharger/revoker.go
@@ -30,14 +30,14 @@
 
 func (dir revocationDir) put(caveatNonce string, caveatPreimage []byte) error {
 	if len(dir) == 0 {
-		return fmt.Errorf("missing call to NewRecocationCaveat")
+		return fmt.Errorf("missing call to NewRevoker")
 	}
-	return ioutil.WriteFile(filepath.Join(string(dir), caveatNonce), caveatPreimage, 0777)
+	return ioutil.WriteFile(filepath.Join(string(dir), caveatNonce), caveatPreimage, 0600)
 }
 
 func (dir revocationDir) Revoke(ctx ipc.ServerContext, caveatPreimage ssecurity.RevocationToken) error {
 	if len(dir) == 0 {
-		return fmt.Errorf("missing call to NewRecocationCaveat")
+		return fmt.Errorf("missing call to NewRevoker")
 	}
 	caveatNonce := sha256.Sum256(caveatPreimage[:])
 	return revocationService.put(hex.EncodeToString(caveatNonce[:]), caveatPreimage[:])
diff --git a/services/security/discharger/revoker_test.go b/services/security/discharger/revoker_test.go
index 035f56d..44b46de 100644
--- a/services/security/discharger/revoker_test.go
+++ b/services/security/discharger/revoker_test.go
@@ -13,7 +13,7 @@
 )
 
 func revokerSetup(t *testing.T) (dischargerID security.PublicID, dischargerEndpoint, revokerEndpoint string, closeFunc func(), runtime veyron2.Runtime) {
-	var revokerDirPath = filepath.Join(os.TempDir(), "revoker_dir")
+	var revokerDirPath = filepath.Join(os.TempDir(), "revoker_test_dir")
 	r := rt.Init()
 	// Create and start revoker and revocation discharge service
 	revokerServer, err := r.NewServer()
@@ -34,17 +34,17 @@
 		t.Fatalf("revokerServer.Serve discharger: %s", err)
 	}
 
-	dischargerServer, err := rt.R().NewServer()
+	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("revokerServer.Listen failed: %v", err)
+		t.Fatalf("dischargerServer.Listen failed: %v", err)
 	}
 	dischargerServiceStub := services.NewServerDischarger(NewDischarger(r.Identity()))
 	if err := dischargerServer.Serve("", ipc.LeafDispatcher(dischargerServiceStub, nil)); err != nil {
-		t.Fatalf("revokerServer.Serve revoker: %s", err)
+		t.Fatalf("dischargerServer.Serve revoker: %s", err)
 	}
 	return r.Identity().PublicID(),
 		naming.JoinAddressName(dischargerEP.String(), ""),