ref: Persist discharges.
The discharge cache is moved into the blessingStore so that discharges
may be used across processes.
On my machine, the first call of
'namespace glob identity' with a blessing that has a revocation caveat
takes .7s, but subsequent calls take .25 seconds.
MultiPart: 2/2
Change-Id: Ifdc23e618120baef1638b2ebda1f900b772ce727
diff --git a/lib/security/blessingstore.go b/lib/security/blessingstore.go
index 5ff62b7..0b09ee8 100644
--- a/lib/security/blessingstore.go
+++ b/lib/security/blessingstore.go
@@ -9,10 +9,13 @@
import (
"bytes"
+ "crypto/sha256"
"fmt"
"reflect"
"sort"
+ "strings"
"sync"
+ "time"
"v.io/v23/security"
"v.io/v23/verror"
@@ -27,6 +30,8 @@
errDataOrSignerUnspecified = verror.Register(pkgPath+".errDataOrSignerUnspecified", verror.NoRetry, "{1:}{2:} persisted data or signer is not specified{:_}")
)
+const cacheKeyFormat = uint32(1)
+
// blessingStore implements security.BlessingStore.
type blessingStore struct {
publicKey security.PublicKey
@@ -116,6 +121,105 @@
return m
}
+func (bs *blessingStore) CacheDischarge(discharge security.Discharge, caveat security.Caveat, impetus security.DischargeImpetus) {
+ id := discharge.ID()
+ tp := caveat.ThirdPartyDetails()
+ // Only add to the cache if the caveat did not require arguments.
+ if id == "" || tp == nil || tp.Requirements().ReportArguments {
+ return
+ }
+ key := dcacheKey(tp, impetus)
+ bs.mu.Lock()
+ defer bs.mu.Unlock()
+ old, hadold := bs.state.DischargeCache[key]
+ bs.state.DischargeCache[key] = discharge
+ if err := bs.save(); err != nil {
+ if hadold {
+ bs.state.DischargeCache[key] = old
+ } else {
+ delete(bs.state.DischargeCache, key)
+ }
+ }
+ return
+}
+
+func (bs *blessingStore) ClearDischarges(discharges ...security.Discharge) {
+ bs.mu.Lock()
+ bs.clearDischargesLocked(discharges...)
+ bs.mu.Unlock()
+ return
+}
+
+func (bs *blessingStore) clearDischargesLocked(discharges ...security.Discharge) {
+ for _, d := range discharges {
+ for k, cached := range bs.state.DischargeCache {
+ if cached.Equivalent(d) {
+ delete(bs.state.DischargeCache, k)
+ }
+ }
+ }
+}
+
+func (bs *blessingStore) Discharge(caveat security.Caveat, impetus security.DischargeImpetus) (out security.Discharge) {
+ defer bs.mu.Unlock()
+ bs.mu.Lock()
+ tp := caveat.ThirdPartyDetails()
+ if tp == nil || tp.Requirements().ReportArguments {
+ return
+ }
+ key := dcacheKey(tp, impetus)
+ if cached, exists := bs.state.DischargeCache[key]; exists {
+ out = cached
+ // If the discharge has expired, purge it from the cache.
+ if hasDischargeExpired(out) {
+ out = security.Discharge{}
+ bs.clearDischargesLocked(cached)
+ }
+ }
+ return
+}
+
+func hasDischargeExpired(dis security.Discharge) bool {
+ expiry := dis.Expiry()
+ if expiry.IsZero() {
+ return false
+ }
+ return expiry.Before(time.Now())
+}
+
+func dcacheKey(tp security.ThirdPartyCaveat, impetus security.DischargeImpetus) dischargeCacheKey {
+ // If the algorithm for computing dcacheKey changes, cacheKeyFormat must be changed as well.
+ id := tp.ID()
+ r := tp.Requirements()
+ var method, servers string
+ // We currently do not cache on impetus.Arguments because there it seems there is no
+ // general way to generate a key from them.
+ if r.ReportMethod {
+ method = impetus.Method
+ }
+ if r.ReportServer && len(impetus.Server) > 0 {
+ // Sort the server blessing patterns to increase cache usage.
+ var bps []string
+ for _, bp := range impetus.Server {
+ bps = append(bps, string(bp))
+ }
+ sort.Strings(bps)
+ servers = strings.Join(bps, ",")
+ }
+ h := sha256.New()
+ h.Write(hashString(id))
+ h.Write(hashString(method))
+ h.Write(hashString(servers))
+ var key [sha256.Size]byte
+ copy(key[:], h.Sum(nil))
+ return key
+}
+
+func hashString(d string) []byte {
+ h := sha256.Sum256([]byte(d))
+ return h[:]
+}
+
// DebugString return a human-readable string encoding of the store
// in the following format
// Default Blessings <blessings>
@@ -158,7 +262,10 @@
func newInMemoryBlessingStore(publicKey security.PublicKey) security.BlessingStore {
return &blessingStore{
publicKey: publicKey,
- state: blessingStoreState{PeerBlessings: make(map[security.BlessingPattern]security.Blessings)},
+ state: blessingStoreState{
+ PeerBlessings: make(map[security.BlessingPattern]security.Blessings),
+ DischargeCache: make(map[dischargeCacheKey]security.Discharge),
+ },
}
}
@@ -185,6 +292,10 @@
if err := decodeFromStorage(&bs.state, data, signature, bs.signer.PublicKey()); err != nil {
return err
}
+ if bs.state.CacheKeyFormat != cacheKeyFormat {
+ bs.state.CacheKeyFormat = cacheKeyFormat
+ bs.state.DischargeCache = make(map[dischargeCacheKey]security.Discharge)
+ }
return bs.verifyState()
}
@@ -197,12 +308,17 @@
}
bs := &blessingStore{
publicKey: signer.PublicKey(),
- state: blessingStoreState{PeerBlessings: make(map[security.BlessingPattern]security.Blessings)},
serializer: serializer,
signer: signer,
}
if err := bs.deserialize(); err != nil {
return nil, err
}
+ if bs.state.PeerBlessings == nil {
+ bs.state.PeerBlessings = make(map[security.BlessingPattern]security.Blessings)
+ }
+ if bs.state.DischargeCache == nil {
+ bs.state.DischargeCache = make(map[dischargeCacheKey]security.Discharge)
+ }
return bs, nil
}
diff --git a/lib/security/blessingstore_test.go b/lib/security/blessingstore_test.go
index 339cd59..01cc3df 100644
--- a/lib/security/blessingstore_test.go
+++ b/lib/security/blessingstore_test.go
@@ -140,6 +140,7 @@
if err := tester.testSetDefault(s); err != nil {
t.Error(err)
}
+ testDischargeCache(t, s)
}
func TestBlessingStorePersistence(t *testing.T) {
@@ -164,6 +165,7 @@
if err := tester.testSetDefault(s); err != nil {
t.Error(err)
}
+ testDischargeCache(t, s)
// Recreate the BlessingStore from the directory.
p2, err := LoadPersistentPrincipal(dir, nil)
diff --git a/lib/security/discharge_cache_test.go b/lib/security/discharge_cache_test.go
new file mode 100644
index 0000000..f974d1a
--- /dev/null
+++ b/lib/security/discharge_cache_test.go
@@ -0,0 +1,105 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package security
+
+import (
+ "testing"
+ "time"
+
+ "v.io/v23/security"
+ "v.io/v23/vdl"
+)
+
+func testDischargeCache(t *testing.T, s security.BlessingStore) {
+ var (
+ discharger = mkPrincipal()
+ expiredCav = mkCaveat(security.NewPublicKeyCaveat(discharger.PublicKey(), "moline", security.ThirdPartyRequirements{}, security.UnconstrainedUse()))
+ argsCav = mkCaveat(security.NewPublicKeyCaveat(discharger.PublicKey(), "peoria", security.ThirdPartyRequirements{ReportArguments: true}, security.UnconstrainedUse()))
+ methodCav = mkCaveat(security.NewPublicKeyCaveat(discharger.PublicKey(), "moline", security.ThirdPartyRequirements{ReportMethod: true}, security.UnconstrainedUse()))
+ serverCav = mkCaveat(security.NewPublicKeyCaveat(discharger.PublicKey(), "peoria", security.ThirdPartyRequirements{ReportServer: true}, security.UnconstrainedUse()))
+
+ dEmpty = security.Discharge{}
+ dExpired = mkDischarge(discharger.MintDischarge(expiredCav, mkCaveat(security.NewExpiryCaveat(time.Now().Add(-1*time.Minute)))))
+ dArgs = mkDischarge(discharger.MintDischarge(argsCav, security.UnconstrainedUse()))
+ dMethod = mkDischarge(discharger.MintDischarge(methodCav, security.UnconstrainedUse()))
+ dServer = mkDischarge(discharger.MintDischarge(serverCav, security.UnconstrainedUse()))
+
+ emptyImp = security.DischargeImpetus{}
+ argsImp = security.DischargeImpetus{Arguments: []*vdl.Value{&vdl.Value{}}}
+ methodImp = security.DischargeImpetus{Method: "foo"}
+ otherMethodImp = security.DischargeImpetus{Method: "bar"}
+ serverImp = security.DischargeImpetus{Server: []security.BlessingPattern{security.BlessingPattern("fooserver")}}
+ otherServerImp = security.DischargeImpetus{Server: []security.BlessingPattern{security.BlessingPattern("barserver")}}
+ )
+
+ // Discharges for different cavs should not be cached.
+ d := mkDischarge(discharger.MintDischarge(argsCav, security.UnconstrainedUse()))
+ s.CacheDischarge(d, argsCav, emptyImp)
+ if d := s.Discharge(methodCav, emptyImp); d.ID() != "" {
+ t.Errorf("Discharge for different caveat should not have been in cache")
+ }
+ s.ClearDischarges(d)
+
+ // Add some discharges into the cache.
+ s.CacheDischarge(dArgs, argsCav, argsImp)
+ s.CacheDischarge(dMethod, methodCav, methodImp)
+ s.CacheDischarge(dServer, serverCav, serverImp)
+ s.CacheDischarge(dExpired, expiredCav, emptyImp)
+
+ testCases := []struct {
+ caveat security.Caveat // caveat that we are fetching discharges for.
+ queryImpetus security.DischargeImpetus // Impetus used to query the cache.
+ cachedDischarge security.Discharge // Discharge that we expect to be returned from the cache, nil if the discharge should not be cached.
+ }{
+ // Expired discharges should not be returned by the cache.
+ {expiredCav, emptyImp, dEmpty},
+
+ // Discharges with Impetuses that have Arguments should not be cached.
+ {argsCav, argsImp, dEmpty},
+
+ {methodCav, methodImp, dMethod},
+ {methodCav, otherMethodImp, dEmpty},
+ {methodCav, emptyImp, dEmpty},
+
+ {serverCav, serverImp, dServer},
+ {serverCav, otherServerImp, dEmpty},
+ {serverCav, emptyImp, dEmpty},
+ }
+
+ for i, test := range testCases {
+ out := s.Discharge(test.caveat, test.queryImpetus)
+ if got := out.ID(); got != test.cachedDischarge.ID() {
+ t.Errorf("#%d: got discharge %v, want %v, queried with %v", i, got, test.cachedDischarge.ID(), test.queryImpetus)
+ }
+ }
+ if t.Failed() {
+ t.Logf("dArgs.ID(): %v", dArgs.ID())
+ t.Logf("dMethod.ID(): %v", dMethod.ID())
+ t.Logf("dServer.ID(): %v", dServer.ID())
+ t.Logf("dExpired.ID(): %v", dExpired.ID())
+ }
+}
+
+func mkPrincipal() security.Principal {
+ p, err := NewPrincipal()
+ if err != nil {
+ panic(err)
+ }
+ return p
+}
+
+func mkDischarge(d security.Discharge, err error) security.Discharge {
+ if err != nil {
+ panic(err)
+ }
+ return d
+}
+
+func mkCaveat(c security.Caveat, err error) security.Caveat {
+ if err != nil {
+ panic(err)
+ }
+ return c
+}
diff --git a/lib/security/type.vdl b/lib/security/type.vdl
index 9d65571..9539700 100644
--- a/lib/security/type.vdl
+++ b/lib/security/type.vdl
@@ -18,4 +18,11 @@
// DefaultBlessings is the default Blessings to be shared with peers for which
// no other information is available to select blessings.
DefaultBlessings security.WireBlessings
+ // DischargeCache is the cache of discharges.
+ DischargeCache map[dischargeCacheKey]security.WireDischarge
+ // CacheKeyFormat is the dischargeCacheKey format version. It should incremented
+ // any time the format of the dischargeCacheKey is changed.
+ CacheKeyFormat uint32
}
+
+type dischargeCacheKey [32]byte
diff --git a/lib/security/type.vdl.go b/lib/security/type.vdl.go
index 3fabed7..5681451 100644
--- a/lib/security/type.vdl.go
+++ b/lib/security/type.vdl.go
@@ -32,6 +32,11 @@
// DefaultBlessings is the default Blessings to be shared with peers for which
// no other information is available to select blessings.
DefaultBlessings security.Blessings
+ // DischargeCache is the cache of discharges.
+ DischargeCache map[dischargeCacheKey]security.Discharge
+ // CacheKeyFormat is the dischargeCacheKey format version. It should incremented
+ // any time the format of the dischargeCacheKey is changed.
+ CacheKeyFormat uint32
}
func (blessingStoreState) __VDLReflect(struct {
@@ -39,7 +44,15 @@
}) {
}
+type dischargeCacheKey [32]byte
+
+func (dischargeCacheKey) __VDLReflect(struct {
+ Name string `vdl:"v.io/x/ref/lib/security.dischargeCacheKey"`
+}) {
+}
+
func init() {
vdl.Register((*blessingRootsState)(nil))
vdl.Register((*blessingStoreState)(nil))
+ vdl.Register((*dischargeCacheKey)(nil))
}