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(), ""),