Merge "x/ref/services/syncbase: Update Glob implementation"
diff --git a/x/ref/services/syncbase/signing/hashcache/hashcache.go b/x/ref/services/syncbase/signing/hashcache/hashcache.go
new file mode 100644
index 0000000..26e8c94
--- /dev/null
+++ b/x/ref/services/syncbase/signing/hashcache/hashcache.go
@@ -0,0 +1,77 @@
+// 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 hashcache implements a simple cache intended to be indexed by hash
+// values. The keys are of type []byte. Values are arbitrary interface{}
+// values. Entries may expire if not used for a duration specified by the
+// client.
+package hashcache
+
+import "sync"
+import "time"
+
+// An internalValue is the client's data plus the data's expiry time.
+type internalValue struct {
+ data interface{}
+ expiry time.Time
+}
+
+// A Cache allows the user to store arbitrary values, keyed by the contents of
+// byte vectors. Entries may be added, deleted, and looked up. They may
+// expire if not used.
+type Cache struct {
+ expiry time.Duration
+ mu sync.Mutex // protects fields below.
+ entries map[string]*internalValue
+ insertionsSinceGC int // number of insertions since last GC
+}
+
+// New() returns a pointer to a new, empty Cache.
+// Entries may expire if not used for "expiry".
+func New(expiry time.Duration) *Cache {
+ return &Cache{expiry: expiry, entries: make(map[string]*internalValue)}
+}
+
+// Lookup() returns the data associated with key[] in *c, and whether there is
+// such a value. The client may not modify the returned data; it is shared
+// with *c.
+func (c *Cache) Lookup(key []byte) (data interface{}, isPresent bool) {
+ var value *internalValue
+ c.mu.Lock()
+ value, isPresent = c.entries[string(key)]
+ if isPresent {
+ value.expiry = time.Now().Add(c.expiry)
+ data = value.data
+ }
+ c.mu.Unlock()
+ return data, isPresent
+}
+
+// Add() associates data with key[] in *c. Any data previously associated with
+// key[] are forgotten. The implementation may discard the association at some
+// future time (set by NewCache()) to limit the size of the cache. data may
+// not be modified after this call; it is shared with *c.
+func (c *Cache) Add(key []byte, data interface{}) {
+ c.mu.Lock()
+ now := time.Now()
+ c.entries[string(key)] = &internalValue{data: data, expiry: now.Add(c.expiry)}
+ c.insertionsSinceGC++
+ // Scan to expire entries if 20% were added since last scan.
+ if c.insertionsSinceGC*5 > len(c.entries) {
+ for ik, iv := range c.entries {
+ if iv.expiry.Before(now) {
+ delete(c.entries, ik)
+ }
+ }
+ c.insertionsSinceGC = 0
+ }
+ c.mu.Unlock()
+}
+
+// Delete() removes any association of data with key[] in *c.
+func (c *Cache) Delete(key []byte) {
+ c.mu.Lock()
+ delete(c.entries, string(key))
+ c.mu.Unlock()
+}
diff --git a/x/ref/services/syncbase/signing/hashcache/hashcache_test.go b/x/ref/services/syncbase/signing/hashcache/hashcache_test.go
new file mode 100644
index 0000000..96ba865
--- /dev/null
+++ b/x/ref/services/syncbase/signing/hashcache/hashcache_test.go
@@ -0,0 +1,83 @@
+// 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 hashcache_test tests the hashcache package.
+package hashcache_test
+
+import "runtime"
+import "testing"
+import "time"
+
+import "v.io/syncbase/x/ref/services/syncbase/signing/hashcache"
+
+// checkHashesWithNoData() checks that hash[start:] have no data in the cache.
+// (The start index is passed, rather than expecting the caller to sub-slice,
+// so that error messages refer to the index.)
+func checkHashesWithNoData(t *testing.T, cache *hashcache.Cache, start int, hash [][]byte) {
+ _, _, callerLine, _ := runtime.Caller(1)
+ for i := start; i != len(hash); i++ {
+ value, found := cache.Lookup(hash[i])
+ if value != nil || found {
+ t.Errorf("line %d: unset cache entry hash[%d]=%v has value %v, but is expected not to be set", callerLine, i, hash[i], value)
+ }
+ }
+}
+
+func TestCache(t *testing.T) {
+ hash := [][]byte{
+ []byte{0x00, 0x01, 0x02, 0x3},
+ []byte{0x04, 0x05, 0x06, 0x7},
+ []byte{0x08, 0x09, 0x0a, 0xb}}
+ var value interface{}
+ var found bool
+ var want string
+
+ cache := hashcache.New(5 * time.Second)
+
+ // The cache should initially have none of the keys.
+ checkHashesWithNoData(t, cache, 0, hash)
+
+ // Add the first key, and check that it's there.
+ want = "hash0"
+ cache.Add(hash[0], want)
+ value, found = cache.Lookup(hash[0])
+ if s, ok := value.(string); !found || !ok || s != want {
+ t.Errorf("cache entry hash[%d]=%v got %v, want %v", 0, hash[0], s, want)
+ }
+ checkHashesWithNoData(t, cache, 1, hash)
+
+ // Add the second key, and check that both it and the first key are there.
+ want = "hash1"
+ cache.Add(hash[1], want)
+ value, found = cache.Lookup(hash[1])
+ if s, ok := value.(string); !ok || s != want {
+ t.Errorf("cache entry hash[%d]=%v got %v, want %v", 1, hash[1], s, want)
+ }
+ want = "hash0"
+ value, found = cache.Lookup(hash[0])
+ if s, ok := value.(string); !found || !ok || s != want {
+ t.Errorf("cache entry hash[%d]=%v got %v, want %v", 0, hash[0], s, want)
+ }
+ checkHashesWithNoData(t, cache, 2, hash)
+
+ // Wait for all entries to time out.
+ time.Sleep(6 * time.Second) // sleep past expiry time
+
+ // Add the first key again, and so many that it will trigger garbage
+ // collection.
+ for i := 0; i != 10; i++ {
+ want = "hash0 again"
+ cache.Add(hash[0], want)
+ value, found = cache.Lookup(hash[0])
+ if s, ok := value.(string); !found || !ok || s != want {
+ t.Errorf("cache entry hash[%d]=%v got %v, want %v", 0, hash[0], s, want)
+ }
+ }
+ // The entry for hash1 should have expired, since the expiry time has
+ // passed, and many things have been inserted into the cache.
+ checkHashesWithNoData(t, cache, 1, hash)
+
+ cache.Delete(hash[0])
+ checkHashesWithNoData(t, cache, 0, hash)
+}
diff --git a/x/ref/services/syncbase/signing/krl/krl.go b/x/ref/services/syncbase/signing/krl/krl.go
new file mode 100644
index 0000000..422f53e
--- /dev/null
+++ b/x/ref/services/syncbase/signing/krl/krl.go
@@ -0,0 +1,39 @@
+// 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 krl implements a trivial, in-memory key revocation list.
+// It is a placeholder for a real key revocation mechanism.
+package krl
+
+import "crypto/sha256"
+import "time"
+
+// A KRL is a key revocation list. It maps the hashes of keys that have been revoked
+// to revocation times.
+type KRL struct {
+ table map[[sha256.Size]byte]time.Time
+}
+
+var notYetRevoked = time.Now().Add(100 * 365 * 24 * time.Hour) // far future
+
+// New() returns a pointer to a new, empty key recovation list.
+func New() *KRL {
+ return &KRL{table: make(map[[sha256.Size]byte]time.Time)}
+}
+
+// Revoke() inserts an entry into *krl recording that key[] was revoked at time
+// "when".
+func (krl *KRL) Revoke(key []byte, when time.Time) {
+ krl.table[sha256.Sum256(key)] = when
+}
+
+// RevocationTime() returns the revocation time for key[].
+// If key[] is not in the list, a time in the far future is returned.
+func (krl *KRL) RevocationTime(key []byte) (whenRevoked time.Time) {
+ var found bool
+ if whenRevoked, found = krl.table[sha256.Sum256(key)]; !found {
+ whenRevoked = notYetRevoked
+ }
+ return whenRevoked
+}
diff --git a/x/ref/services/syncbase/signing/krl/krl_test.go b/x/ref/services/syncbase/signing/krl/krl_test.go
new file mode 100644
index 0000000..73f48ad
--- /dev/null
+++ b/x/ref/services/syncbase/signing/krl/krl_test.go
@@ -0,0 +1,54 @@
+// 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 krl tests the key revocation list package.
+package krl_test
+
+import "runtime"
+import "testing"
+import "time"
+
+import "v.io/syncbase/x/ref/services/syncbase/signing/krl"
+
+// checkKeysNotRevoked() checks that key[start:] have not been revoked. (The
+// start index is passed, rather than expecting the called to sub-slice, so
+// that error messages refer to the expected index.)
+func checkKeysNotRevoked(t *testing.T, krl *krl.KRL, start int, key [][]byte, now time.Time) {
+ _, _, callerLine, _ := runtime.Caller(1)
+ year := 365 * 24 * time.Hour
+ for i := start; i != len(key); i++ {
+ revoked := krl.RevocationTime(key[i])
+ if revoked.Before(now.Add(year)) {
+ t.Errorf("line %d: unrevoked key[%d]=%v has revocation time %v, which is not far enough in the future", callerLine, i, key[i], revoked)
+ }
+ }
+}
+
+func TestKRL(t *testing.T) {
+ now := time.Now()
+ key := [][]byte{
+ []byte{0x00, 0x01, 0x02, 0x3},
+ []byte{0x04, 0x05, 0x06, 0x7},
+ []byte{0x08, 0x09, 0x0a, 0xb}}
+ var revoked time.Time
+
+ krl := krl.New()
+
+ checkKeysNotRevoked(t, krl, 0, key, now)
+
+ krl.Revoke(key[0], now)
+ if revoked = krl.RevocationTime(key[0]); !revoked.Equal(now) {
+ t.Errorf("unrevoked key %v has revocation time %v, but expected %v", key[0], revoked, now)
+ }
+ checkKeysNotRevoked(t, krl, 1, key, now)
+
+ krl.Revoke(key[1], now)
+ if revoked = krl.RevocationTime(key[0]); !revoked.Equal(now) {
+ t.Errorf("unrevoked key %v has revocation time %v, but expected %v", key[0], revoked, now)
+ }
+ if revoked = krl.RevocationTime(key[1]); !revoked.Equal(now) {
+ t.Errorf("unrevoked key %v has revocation time %v, but expected %v", key[1], revoked, now)
+ }
+ checkKeysNotRevoked(t, krl, 2, key, now)
+}
diff --git a/x/ref/services/syncbase/signing/signeddata.vdl b/x/ref/services/syncbase/signing/signeddata.vdl
new file mode 100644
index 0000000..4b4ceb8
--- /dev/null
+++ b/x/ref/services/syncbase/signing/signeddata.vdl
@@ -0,0 +1,72 @@
+// 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 signing
+
+import "v.io/v23/security"
+
+// A DataWithSignature represents a signed, and possibily validated, collection
+// of Item structs.
+//
+// If IsValidated==false and the AuthorSigned signature is valid, it means:
+// The signer whose Blessings have hash BlessingsHash asserts Data.
+//
+// If IsValidated==true and both AuthorSigned and ValidatorSigned signatures are is valid,
+// it means both:
+// 1) The signer whose Blessings b have hash BlessingsHash asserts Data.
+// 2) If vd is the ValidatorData with hash ValidatorDataHash, the owner of
+// vd.PublicKey asserts that it checked that at least the names vd.Names[] were
+// valid in b.
+//
+// The sender obtains:
+// - BlessingsHash (and the wire form of the blessings) with ValidationCache.AddBlessings().
+// - ValidatorDataHash (and the wire form of the ValidataData) with ValidationCache.AddValidatorData().
+//
+// The receiver looks up:
+// - BlessingsHash with ValidationCache.LookupBlessingsData()
+// - ValidatorDataHash with ValidationCache.LookupValidatorData()
+//
+// If not yet there, the receiver inserts the valus into its ValidationCache with:
+// - ValidationCache.AddWireBlessings()
+// - ValidationCache.AddValidatorData()
+type DataWithSignature struct {
+ Data []Item
+ // BlessingsHash is a key for the validation cache; the corresponding
+ // cached value is a security.Blessings.
+ BlessingsHash []byte
+ // AuthorSigned is the signature of Data and BlessingsHash using the
+ // private key associated with the blessings hashed in BlessingsHash.
+ AuthorSigned security.Signature
+
+ IsValidated bool // Whether fields below are meaningful.
+
+ // ValidatorDataHash is a key for the validation cache returned by
+ // ValidatorData.Hash(); the corresponding cached value is the
+ // ValidatorData.
+ ValidatorDataHash []byte
+ ValidatorSigned security.Signature
+}
+
+// An Item represents either a marshalled data item or its SHA-256 hash.
+// The Data field is a []byte, rather than an "any" to make signatures
+// determistic. VOM encoding is not deterministic for two reasons:
+// - map elements may be marshalled in any order
+// - different versions of VOM may marshal in different ways.
+// Thus, the initial producer of a data item marshals the data once, and it is
+// this marshalled form that is transmitted from device to device. If the
+// data were unmarshalled and then remarsahalled, the signatures might not
+// match. The Hash field is used instead of the Data field when the recipient
+// of the DataWithSignature is not permitted to see certain Items' Data
+// fields.
+type Item union {
+ Data []byte // Marshalled form of data.
+ Hash []byte // Hash of what would have been in Data, as returned by SumByteVectorWithLength(Data).
+}
+
+// WireValidatorData is the wire form of ValidatorData.
+// It excludes the unmarshalled form of the public key.
+type WireValidatorData struct {
+ Names []string // Names of valid signing blessings in the Blessings referred to by BlessingsHash.
+ MarshalledPublicKey []byte // PublicKey, marshalled with MarshalBinary().
+}
diff --git a/x/ref/services/syncbase/signing/signeddata.vdl.go b/x/ref/services/syncbase/signing/signeddata.vdl.go
new file mode 100644
index 0000000..f96fe50
--- /dev/null
+++ b/x/ref/services/syncbase/signing/signeddata.vdl.go
@@ -0,0 +1,128 @@
+// 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.
+
+// This file was auto-generated by the vanadium vdl tool.
+// Source: signeddata.vdl
+
+package signing
+
+import (
+ // VDL system imports
+ "v.io/v23/vdl"
+
+ // VDL user imports
+ "v.io/v23/security"
+)
+
+// A DataWithSignature represents a signed, and possibily validated, collection
+// of Item structs.
+//
+// If IsValidated==false and the AuthorSigned signature is valid, it means:
+// The signer whose Blessings have hash BlessingsHash asserts Data.
+//
+// If IsValidated==true and both AuthorSigned and ValidatorSigned signatures are is valid,
+// it means both:
+// 1) The signer whose Blessings b have hash BlessingsHash asserts Data.
+// 2) If vd is the ValidatorData with hash ValidatorDataHash, the owner of
+// vd.PublicKey asserts that it checked that at least the names vd.Names[] were
+// valid in b.
+//
+// The sender obtains:
+// - BlessingsHash (and the wire form of the blessings) with ValidationCache.AddBlessings().
+// - ValidatorDataHash (and the wire form of the ValidataData) with ValidationCache.AddValidatorData().
+//
+// The receiver looks up:
+// - BlessingsHash with ValidationCache.LookupBlessingsData()
+// - ValidatorDataHash with ValidationCache.LookupValidatorData()
+//
+// If not yet there, the receiver inserts the valus into its ValidationCache with:
+// - ValidationCache.AddWireBlessings()
+// - ValidationCache.AddValidatorData()
+type DataWithSignature struct {
+ Data []Item
+ // BlessingsHash is a key for the validation cache; the corresponding
+ // cached value is a security.Blessings.
+ BlessingsHash []byte
+ // AuthorSigned is the signature of Data and BlessingsHash using the
+ // private key associated with the blessings hashed in BlessingsHash.
+ AuthorSigned security.Signature
+ IsValidated bool // Whether fields below are meaningful.
+ // ValidatorDataHash is a key for the validation cache returned by
+ // ValidatorData.Hash(); the corresponding cached value is the
+ // ValidatorData.
+ ValidatorDataHash []byte
+ ValidatorSigned security.Signature
+}
+
+func (DataWithSignature) __VDLReflect(struct {
+ Name string `vdl:"v.io/syncbase/x/ref/services/syncbase/signing.DataWithSignature"`
+}) {
+}
+
+type (
+ // Item represents any single field of the Item union type.
+ //
+ // An Item represents either a marshalled data item or its SHA-256 hash.
+ // The Data field is a []byte, rather than an "any" to make signatures
+ // determistic. VOM encoding is not deterministic for two reasons:
+ // - map elements may be marshalled in any order
+ // - different versions of VOM may marshal in different ways.
+ // Thus, the initial producer of a data item marshals the data once, and it is
+ // this marshalled form that is transmitted from device to device. If the
+ // data were unmarshalled and then remarsahalled, the signatures might not
+ // match. The Hash field is used instead of the Data field when the recipient
+ // of the DataWithSignature is not permitted to see certain Items' Data
+ // fields.
+ Item interface {
+ // Index returns the field index.
+ Index() int
+ // Interface returns the field value as an interface.
+ Interface() interface{}
+ // Name returns the field name.
+ Name() string
+ // __VDLReflect describes the Item union type.
+ __VDLReflect(__ItemReflect)
+ }
+ // ItemData represents field Data of the Item union type.
+ ItemData struct{ Value []byte } // Marshalled form of data.
+ // ItemHash represents field Hash of the Item union type.
+ ItemHash struct{ Value []byte } // Hash of what would have been in Data, as returned by SumByteVectorWithLength(Data).
+ // __ItemReflect describes the Item union type.
+ __ItemReflect struct {
+ Name string `vdl:"v.io/syncbase/x/ref/services/syncbase/signing.Item"`
+ Type Item
+ Union struct {
+ Data ItemData
+ Hash ItemHash
+ }
+ }
+)
+
+func (x ItemData) Index() int { return 0 }
+func (x ItemData) Interface() interface{} { return x.Value }
+func (x ItemData) Name() string { return "Data" }
+func (x ItemData) __VDLReflect(__ItemReflect) {}
+
+func (x ItemHash) Index() int { return 1 }
+func (x ItemHash) Interface() interface{} { return x.Value }
+func (x ItemHash) Name() string { return "Hash" }
+func (x ItemHash) __VDLReflect(__ItemReflect) {}
+
+// WireValidatorData is the wire form of ValidatorData.
+// It excludes the unmarshalled form of the public key.
+type WireValidatorData struct {
+ Names []string // Names of valid signing blessings in the Blessings referred to by BlessingsHash.
+ MarshalledPublicKey []byte // PublicKey, marshalled with MarshalBinary().
+}
+
+func (WireValidatorData) __VDLReflect(struct {
+ Name string `vdl:"v.io/syncbase/x/ref/services/syncbase/signing.WireValidatorData"`
+}) {
+}
+
+func init() {
+ vdl.Register((*DataWithSignature)(nil))
+ vdl.Register((*Item)(nil))
+ vdl.Register((*WireValidatorData)(nil))
+}
diff --git a/x/ref/services/syncbase/signing/signing.go b/x/ref/services/syncbase/signing/signing.go
new file mode 100644
index 0000000..0115f37
--- /dev/null
+++ b/x/ref/services/syncbase/signing/signing.go
@@ -0,0 +1,358 @@
+// 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 signing signs syncbase updates using public key signatures, and
+// allows these signatures to be checked on other nodes.
+//
+// The functionality is geared specifically towards syncbase synchronization
+// because it is designed to allow a signature to remain valid during its
+// propagation across the syncgroup once it has been accepted by at least one
+// member of a syncgroup, even if the original key or its blessings are
+// invalidated in the meantime.
+//
+// There are three types of participant:
+// - an "author", which creates an update, and signs it with Sign().
+// - one or more "validators", each of which receives a change directly from
+// the author, and applies Check() to validate it.
+// - zero or more "checkers', each of whom receives a change from a validator
+// or another checker, and applied Check() to check it.
+//
+// A validator checks the signature and blessings provided by the author, and
+// then appends its own signature, vouching for the fact that the author's
+// signature was good at the time the validator saw it.
+//
+// A checker checks the signatures of both the author and validator but uses
+// weaker checks for signature validity than a validator. In particular, it
+// uses a significant grace period for key expiry so that a change admitted to
+// the syncgroup by a validator has an opportunity to propagate to all the
+// nodes in the syncgroup if the keys or blessings are revoked after the change
+// is admitted, but before it is fully propagated. The intent is that the
+// grace period be chosen to be greater than the diameter of the syncgroup
+// (measured in time). One way to ensure that is to insist that members sync
+// with a central server at least every T time units, and make the grace period
+// be 2T. The central server may sign the data anew to allow new members to pick
+// it up.
+//
+// The model is further complicated by performance concerns. An update written
+// to syncbase might be quite small (perhaps tens of bytes) but:
+// a) a public key signature or verification can take on the order of a
+// millisecond. (Currently, ECDSA signing might a little under 1ms and
+// verification just over 2ms on a workstation. A checker performs two such
+// verifications.)
+// b) unmarshalling even a simple Blessings object can take milliseconds. (!)
+// c) marshalling a public key can take 10us.
+// d) a Blessings object is of the order of a kilobyte of more, which may
+// represent substantial space overhead if duplicated.
+//
+// Because of (a), we wish to batch syncbase updates, so that a single
+// signature check applies to several updates. Thus the Data in a
+// DataWithSignature is a vector of Item, rather than a single Item.
+//
+// However, we will not always wish to put all updates in the same batch. For
+// example, an author and a validator might share two different syncgroups with
+// different memberships. In such a case, the author might keep the batches
+// for one syncgoup separate from batches for the other syncgroup, even though
+// the author blessings and validator identities are the same for all the
+// batches. Thus, because of (b,c,d), it's worth decoupling the author's
+// Blessings data and the validator's key data separately from the signed
+// batches itself, so that the blessings and validator data can be processed
+// once, even though several batches of updates are being sent. A
+// ValidationCache is used to hold this data separately, and allow it to be
+// sent just once, rather than once per signature.
+//
+// Lastly, imagine that the author sends a batch of 10 updates to a validator,
+// and the validator then syncs with a checker that is permitted to see only
+// half of the updates; perhaps ACLs prevent if from seeing the others. This
+// requires that the signature on the batch remain valid even if some of the
+// updates in the batch are removed. This is accomplished via the Item type,
+// which is a VDL union type that contains either the bytes of the marshalled
+// form of the update, or (if the update must not be sent) the SHA-256 hash of
+// the data (which can be computed with SumByteVectorWithLength()).
+package signing
+
+import "bytes"
+import "crypto/sha256"
+import "encoding/binary"
+import "hash"
+import "time"
+
+import "v.io/syncbase/x/ref/services/syncbase/signing/krl"
+import "v.io/v23/context"
+import "v.io/v23/security"
+import "v.io/v23/verror"
+
+const pkgPath = "v.io/syncbase/x/ref/services/syncbase/signing"
+
+// These are among the errors may be returned by Check(), and indicate that the
+// operation should be retried when new data has been added to the
+// ValidationCache. The errors are public to make it easier for the client to
+// test for them.
+var (
+ ErrNeedAuthorBlessingsAndValidatorDataForHash = verror.Register(
+ pkgPath+".ErrNeedAuthorBlessingsAndValidatorDataForHash",
+ verror.RetryRefetch,
+ "{1:}{2:} The ValidationCache contains neither the author blessings nor the validator data{:_}")
+ ErrNeedAuthorBlessingsForHash = verror.Register(
+ pkgPath+".ErrNeedAuthorBlessingsForHash",
+ verror.RetryRefetch,
+ "{1:}{2:} The ValidationCache does not contain the author blessings{:_}")
+ ErrNeedValidatorDataForHash = verror.Register(
+ pkgPath+".ErrNeedValidatorDataForHash",
+ verror.RetryRefetch,
+ "{1:}{2:} The ValidationCache does not contain the validator data{:_}")
+)
+
+// These errors are less likely to be tested for, and so are not exported.
+var (
+ errAuthorKeyIsRevoked = verror.Register(
+ pkgPath+".errAuthorKeyIsRevoked",
+ verror.NoRetry,
+ "{1:}{2:} The author key has been revoked{:_}")
+ errBadAuthorSignature = verror.Register(
+ pkgPath+".errBadAuthorSignature",
+ verror.NoRetry,
+ "{1:}{2:} Author signature verification failed{:_}")
+ errBadValidatorSignature = verror.Register(
+ pkgPath+".errBadValidatorSignature",
+ verror.NoRetry,
+ "{1:}{2:} Validator signature verification failed{:_}")
+ errAuthorBlessingsHaveNoValidNames = verror.Register(
+ pkgPath+".errAuthorBlessingsHaveNoValidNames",
+ verror.NoRetry,
+ "{1:}{2:} Author Blessings have no valid names{:_}")
+ errMayNotValidateOwnSignature = verror.Register(
+ pkgPath+".errMayNotValidateOwnSignature",
+ verror.NoRetry,
+ "{1:}{2:} Author may not validate its own signature{:_}")
+ errSenderIsNotAuthor = verror.Register(
+ pkgPath+".errSenderIsNotAuthor",
+ verror.NoRetry,
+ "{1:}{2:} Author is not sender of RPC; will not validate{:_}")
+ errValidatesWrongNames = verror.Register(
+ pkgPath+".errValidatesWrongNames",
+ verror.NoRetry,
+ "{1:}{2:} The validated names are not a subset of the names sent by the checker{:_}")
+ errValidatorIsSigner = verror.Register(
+ pkgPath+".errValidatorIsSigner",
+ verror.NoRetry,
+ "{1:}{2:} The signature was validated by its author; treating as invalid{:_}")
+ errValidatorKeyIsRevoked = verror.Register(
+ pkgPath+".errValidatorKeyIsRevoked",
+ verror.NoRetry,
+ "{1:}{2:} The validator key is revoked{:_}")
+)
+
+// --------------------------------------------
+
+// SignData() uses authorPrincipal to sign data using blessings (which must be
+// associated with the authorPrincipal). A pointer to a newly constructed
+// DataWithSignature with IsValidated==false is returned. Ensures that the
+// blessings are stored in *cache. Typically, "authorPrincipal" is obtained from
+// v23.GetPrincipal(ctx).
+//
+// If a recipient of the result *d complains that it does not understand the
+// hash d.BlessingHash, the signer should present it with
+// blessingsData.MarshalledBlessings, which will allow the recipient to
+// construct the Blessings. The Blessings are transmitted out of line because
+// they are large, and may be reused for multiple signatures.
+func SignData(ctx *context.T, cache *ValidationCache, authorPrincipal security.Principal,
+ blessings security.Blessings, data []Item) (d *DataWithSignature, blessingsData *BlessingsData, err error) {
+
+ d = new(DataWithSignature)
+ d.Data = data
+ d.BlessingsHash, blessingsData, err = cache.AddBlessings(ctx, blessings)
+ if err == nil {
+ d.AuthorSigned, err = authorPrincipal.Sign(d.authorSignatureHash())
+ }
+ return d, blessingsData, err
+}
+
+// hashByteVectorWithLength() calls hasher.Write() on a representation of
+// len(b), followed by the contents of b.
+func hashByteVectorWithLength(hasher hash.Hash, b []byte) {
+ var length [8]byte
+ binary.LittleEndian.PutUint64(length[:], uint64(len(b)))
+ hasher.Write(length[:])
+ hasher.Write(b)
+}
+
+// SumByteVectorWithLength() returns a SHA-256 hash of
+// len(b), followed by the contents of b.
+func SumByteVectorWithLength(b []byte) []byte {
+ hasher := sha256.New()
+ var length [8]byte
+ binary.LittleEndian.PutUint64(length[:], uint64(len(b)))
+ hasher.Write(length[:])
+ hasher.Write(b)
+ return hasher.Sum(nil)[:]
+}
+
+// authorSignatureHash() returns the hash that the author should sign.
+func (d *DataWithSignature) authorSignatureHash() []byte {
+ hasher := sha256.New()
+ var length [8]byte
+ binary.LittleEndian.PutUint64(length[:], uint64(len(d.Data)))
+ hasher.Write(length[:])
+ for i := range d.Data {
+ if data, gotData := d.Data[i].(ItemData); gotData {
+ hasher.Write(SumByteVectorWithLength(data.Value))
+ } else if hash, gotHash := d.Data[i].(ItemHash); gotHash {
+ hasher.Write(hash.Value)
+ } else {
+ // d.Data is neither a Data nor a Hash. This shouldn't
+ // happen unless the mashalled data is somehow
+ // corrupted. The signature will not match unless the
+ // original author of the data was seeing the same.
+ hasher.Write([]byte("no data"))
+ }
+ }
+ hashByteVectorWithLength(hasher, d.BlessingsHash)
+ return hasher.Sum(nil)[:]
+}
+
+// validatorSignatureHash() returns the hash that the validator should sign,
+// given the hash that the author signed.
+func (d *DataWithSignature) validatorSignatureHash(authorSignatureHash []byte) []byte {
+ var buffer [32]byte
+ var buf []byte = buffer[:]
+ if len(d.AuthorSigned.Hash) > len(buf) {
+ buf = make([]byte, len(d.AuthorSigned.Hash))
+ }
+ hasher := sha256.New()
+ hashByteVectorWithLength(hasher, authorSignatureHash)
+ hashByteVectorWithLength(hasher, d.AuthorSigned.Purpose)
+ hashByteVectorWithLength(hasher, buf[:copy(buf, d.AuthorSigned.Hash)])
+ hashByteVectorWithLength(hasher, d.AuthorSigned.R)
+ hashByteVectorWithLength(hasher, d.AuthorSigned.S)
+ hashByteVectorWithLength(hasher, d.ValidatorDataHash)
+ return hasher.Sum(nil)[:]
+}
+
+// Check() verifies the signature(s) on *d:
+//
+// If d.IsValidated==false, checks that:
+// 1. the author's signature is available in *cache.
+// 2. the author's signature over its blessings and the data is
+// cyprotgraphically valid.
+// 3. security.SigningBlessingNames() yields a non-empty list of names when
+// applied to the author's blessings.
+// 4. the author's public key is not known to be revoked.
+// 5. the local's public key (call.LocalPrincipal().PublicKey()) is not known
+// to be revoked.
+// 6. the author's public key is the public key of the RPC caller.
+// 7. the author's public key and the local public key differ.
+// If checks pass and there are no other errors:
+// - records the list of names found in check (3) in the ValidationData
+// - adds a validation signature using the local public key (which is now the
+// validator)
+// - sets d.IsValidated
+// - returns the list of names found in check (3), and a nil error.
+// Otherwise returns a nil list of names and a non-nil error.
+//
+// If d.Validated==true, checks that:
+// 1. the author's signature and the validator data are available in *cache.
+// 2. the author's signature over its blessings and the data is
+// cyprotgraphically valid.
+// 8. the list of names stored in the ValidatorData by the validator is
+// non-empty.
+// 9. the author's public key and the validator's public key differ.
+// 10. the list of names stored in the ValidatorData by the validator is a
+// subset of the list of names that the author's blessings could have
+// represented.
+// 11. the author's public key is not known to be revoked more than
+// gracePeriod ago.
+// 12. the validator's public key is not known to be revoked more than
+// gracePeriod ago.
+// 13. the validator's signature is cryptographically valid.
+// If checks pass and there are no other errors:
+// - returns the list of names in the validator's data, and a nil error.
+// Otherwise returns a nil list of names and a non-nil error.
+func (d *DataWithSignature) Check(ctx *context.T, cache *ValidationCache, call security.Call,
+ krl *krl.KRL, gracePeriod time.Duration) (names []string, err error) {
+
+ // Verify that we have the Blessings and ValidatorData.
+ var authorBlessingsData *BlessingsData = cache.LookupBlessingsData(ctx, d.BlessingsHash)
+ var validatorData *ValidatorData
+ if d.IsValidated {
+ validatorData = cache.LookupValidatorData(ctx, d.ValidatorDataHash)
+ }
+ if authorBlessingsData == nil || (validatorData == nil && d.IsValidated) { // Check (1).
+ if authorBlessingsData == nil && (validatorData == nil && d.IsValidated) {
+ err = verror.New(ErrNeedAuthorBlessingsAndValidatorDataForHash, ctx)
+ } else if authorBlessingsData == nil {
+ err = verror.New(ErrNeedAuthorBlessingsForHash, ctx)
+ } else {
+ err = verror.New(ErrNeedValidatorDataForHash, ctx)
+ }
+ }
+
+ // Check the author signature.
+ var authorSignatureHash []byte
+ if err == nil {
+ authorSignatureHash = d.authorSignatureHash()
+ if !d.AuthorSigned.Verify(authorBlessingsData.UnmarshalledBlessings.PublicKey(), authorSignatureHash) { // Check (2).
+ err = verror.New(errBadAuthorSignature, ctx)
+ }
+ }
+
+ // Check or create the validator signature.
+ now := time.Now()
+ if err != nil {
+ // err already set
+ } else if !d.IsValidated {
+ // Not yet validated, so this run will attempt to validate.
+ var validatedNames []string
+ var localKeyMarshalled []byte
+ var senderKeyMarshalled []byte
+ validatedNames, _ = security.SigningBlessingNames(ctx, call.LocalPrincipal(),
+ authorBlessingsData.UnmarshalledBlessings)
+ if len(validatedNames) == 0 { // Check (3).
+ err = verror.New(errAuthorBlessingsHaveNoValidNames, ctx)
+ } else if localKeyMarshalled, err = call.LocalPrincipal().PublicKey().MarshalBinary(); err != nil {
+ // err already set
+ } else if krl.RevocationTime(authorBlessingsData.MarshalledPublicKey).Before(now) { // Check (4).
+ err = verror.New(errAuthorKeyIsRevoked, ctx)
+ } else if krl.RevocationTime(localKeyMarshalled).Before(now) { // Check (5).
+ err = verror.New(errValidatorKeyIsRevoked, ctx)
+ } else if senderKeyMarshalled, err = call.RemoteBlessings().PublicKey().MarshalBinary(); err != nil {
+ // err already set
+ } else if !bytes.Equal(senderKeyMarshalled, authorBlessingsData.MarshalledPublicKey) { // Check (6).
+ err = verror.New(errSenderIsNotAuthor, ctx)
+ } else if bytes.Equal(localKeyMarshalled, authorBlessingsData.MarshalledPublicKey) { // Check (7).
+ err = verror.New(errMayNotValidateOwnSignature, ctx)
+ } else {
+ // Local principal is different from author, so can validate.
+ validatorData = &ValidatorData{
+ Names: validatedNames,
+ PublicKey: call.LocalPrincipal().PublicKey(),
+ MarshalledPublicKey: localKeyMarshalled,
+ }
+ d.ValidatorDataHash = cache.AddValidatorData(ctx, validatorData)
+ d.ValidatorSigned, err = call.LocalPrincipal().Sign(d.validatorSignatureHash(authorSignatureHash))
+ d.IsValidated = (err == nil)
+ }
+ } else { // Data already validated; check the validator siganture.
+ if len(validatorData.Names) == 0 { // Check (8).
+ err = verror.New(errAuthorBlessingsHaveNoValidNames, ctx)
+ } else if bytes.Equal(validatorData.MarshalledPublicKey, authorBlessingsData.MarshalledPublicKey) { // Check (9).
+ err = verror.New(errValidatorIsSigner, ctx)
+ } else if !authorBlessingsData.UnmarshalledBlessings.CouldHaveNames(validatorData.Names) { // Check (10).
+ err = verror.New(errValidatesWrongNames, ctx)
+ } else if krl.RevocationTime(authorBlessingsData.MarshalledPublicKey).Before(now.Add(-gracePeriod)) { // Check (11).
+ err = verror.New(errAuthorKeyIsRevoked, ctx)
+ } else if krl.RevocationTime(validatorData.MarshalledPublicKey).Before(now.Add(-gracePeriod)) { // Check (12).
+ err = verror.New(errValidatorKeyIsRevoked, ctx)
+ } else if !d.ValidatorSigned.Verify(validatorData.PublicKey, d.validatorSignatureHash(authorSignatureHash)) { // Check (13).
+ err = verror.New(errBadValidatorSignature, ctx)
+ } // else success.
+ }
+
+ // If there were no errors, return the list of names from the validator.
+ if err == nil {
+ names = make([]string, len(validatorData.Names))
+ copy(names, validatorData.Names)
+ }
+
+ return names, err
+}
diff --git a/x/ref/services/syncbase/signing/signing_test.go b/x/ref/services/syncbase/signing/signing_test.go
new file mode 100644
index 0000000..96881f3
--- /dev/null
+++ b/x/ref/services/syncbase/signing/signing_test.go
@@ -0,0 +1,421 @@
+// 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 signing_test implements a test for the package
+// v.io/syncbase/x/ref/services/syncbase/signing
+package signing_test
+
+import "crypto/sha256"
+import "testing"
+import "time"
+
+import "v.io/syncbase/x/ref/services/syncbase/signing"
+import "v.io/syncbase/x/ref/services/syncbase/signing/krl"
+import "v.io/v23/naming"
+import "v.io/v23/security"
+import "v.io/v23/vdl"
+import "v.io/v23/vom"
+import "v.io/v23/verror"
+import "v.io/x/ref/test"
+import lib_security "v.io/x/ref/lib/security"
+
+import _ "v.io/x/ref/runtime/factories/generic"
+
+// --------------------------------------
+// The following implements a fake security.Call.
+type fakeCall struct {
+ localPrincipal security.Principal
+ localBlessings security.Blessings
+ remoteBlessings security.Blessings
+}
+
+func (fc *fakeCall) Timestamp() time.Time { return time.Now() }
+func (fc *fakeCall) Method() string { return "the_method_name" }
+func (fc *fakeCall) MethodTags() []*vdl.Value { return nil }
+func (fc *fakeCall) Suffix() string { return "the_suffix" }
+func (fc *fakeCall) LocalDischarges() map[string]security.Discharge { return nil }
+func (fc *fakeCall) RemoteDischarges() map[string]security.Discharge { return nil }
+func (fc *fakeCall) LocalPrincipal() security.Principal { return fc.localPrincipal }
+func (fc *fakeCall) LocalBlessings() security.Blessings { return fc.localBlessings }
+func (fc *fakeCall) RemoteBlessings() security.Blessings { return fc.remoteBlessings }
+func (fc *fakeCall) LocalEndpoint() naming.Endpoint { return nil }
+func (fc *fakeCall) RemoteEndpoint() naming.Endpoint { return nil }
+
+// --------------------------------------
+
+// A principalDesc holds the local state of a single principal in the tests below.
+type principalDesc struct {
+ name string
+ principal security.Principal
+ blessings security.Blessings
+ krl *krl.KRL
+ authorBlessingsData *signing.BlessingsData
+ names []string
+ marshalledBlessings []byte
+ blessingsHash []byte
+ validatorData *signing.ValidatorData
+ validatorHash []byte
+ cache *signing.ValidationCache
+ data *signing.DataWithSignature
+}
+
+// makePrincipal() returns a pointer to a newly-initialized principalDesc,
+// with a unique key, and a single blessing named with its own name.
+func makePrincipal(t testing.TB, name string) (desc *principalDesc) {
+ var err error
+ desc = new(principalDesc)
+ desc.name = name
+ desc.principal, err = lib_security.NewPrincipal()
+ if err != nil {
+ t.Fatalf("security.CreatePrincipal %q failed: %v", desc.name, err)
+ }
+ desc.blessings, err = desc.principal.BlessSelf(desc.name)
+ if err != nil {
+ t.Fatalf("principal.BlessSelf %q failed: %v", desc.name, err)
+ }
+ desc.krl = krl.New()
+ desc.cache = signing.NewValidationCache(5 * time.Second)
+ return desc
+}
+
+// makePrincipals() creares one principal per name, and adds
+// the blessings of each to the roots of all.
+func makePrincipals(t testing.TB, names ...string) (principals []*principalDesc) {
+ for i := range names {
+ principals = append(principals, makePrincipal(t, names[i]))
+ }
+ for i := range principals {
+ for j := range principals {
+ principals[j].principal.AddToRoots(principals[i].blessings)
+ }
+ }
+ return principals
+}
+
+// BenchmarkHashData() measures the time taken to do a cryptogrqaphic hash of
+// 1kBytes.
+func BenchmarkHashData(b *testing.B) {
+ var block [1024]byte
+ hasher := sha256.New()
+ b.ResetTimer()
+
+ for i := 0; i < b.N; i++ {
+ hasher.Write(block[:])
+ }
+}
+
+// BenchmarkSignData() measures the time taken to sign something with
+// signing.SignData().
+func BenchmarkSignData(b *testing.B) {
+ ctx, shutdown := test.V23Init()
+ defer shutdown()
+ var err error
+ author := makePrincipal(b, "author")
+ dataToSign := []signing.Item{signing.ItemData{Value: []byte("hello")}}
+ b.ResetTimer()
+
+ for i := 0; i < b.N; i++ {
+ author.data, author.authorBlessingsData, err =
+ signing.SignData(ctx, author.cache, author.principal, author.blessings, dataToSign)
+ }
+ if err != nil {
+ panic(err)
+ }
+}
+
+// BenchmarkSign1000Data() measures the time taken to sign 1000 small data
+// items with signing.SignData().
+func BenchmarkSign1000Data(b *testing.B) {
+ ctx, shutdown := test.V23Init()
+ defer shutdown()
+ var err error
+ author := makePrincipal(b, "author")
+ var dataToSign []signing.Item
+ for i := 0; i != 1000; i++ {
+ dataToSign = append(dataToSign, signing.ItemData{Value: []byte("hello")})
+ }
+ b.ResetTimer()
+
+ for i := 0; i < b.N; i++ {
+ author.data, author.authorBlessingsData, err =
+ signing.SignData(ctx, author.cache, author.principal, author.blessings, dataToSign)
+ }
+ if err != nil {
+ panic(err)
+ }
+}
+
+// BenchmarkSignData() measures the time taken to check a validated signature
+// with DataWithSignature.Check().
+func BenchmarkCheckData(b *testing.B) {
+ ctx, shutdown := test.V23Init()
+ defer shutdown()
+ var err error
+
+ principals := makePrincipals(b, "author", "validator", "checker")
+ author := principals[0]
+ validator := principals[1]
+ checker := principals[2]
+
+ dataToSign := []signing.Item{signing.ItemData{Value: []byte("hello")}}
+ author.data, author.authorBlessingsData, err =
+ signing.SignData(ctx, author.cache, author.principal, author.blessings, dataToSign)
+ if err != nil {
+ panic(err)
+ }
+ callToValidator := fakeCall{
+ localPrincipal: validator.principal,
+ localBlessings: validator.blessings,
+ remoteBlessings: author.blessings,
+ }
+ validator.names, err = author.data.Check(ctx, author.cache, &callToValidator, validator.krl, 24*30*time.Hour)
+ if err != nil {
+ panic(err)
+ }
+ callToChecker := fakeCall{
+ localPrincipal: checker.principal,
+ localBlessings: checker.blessings,
+ remoteBlessings: validator.blessings,
+ }
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ checker.names, err = author.data.Check(ctx, author.cache, &callToChecker, checker.krl, 24*30*time.Hour)
+ }
+}
+
+// BenchmarkSign1000Data() measures the time taken to check a validated
+// signature over 1000 small data items with DataWithSignature.Check().
+func BenchmarkCheck1000Data(b *testing.B) {
+ ctx, shutdown := test.V23Init()
+ defer shutdown()
+ var err error
+
+ principals := makePrincipals(b, "author", "validator", "checker")
+ author := principals[0]
+ validator := principals[1]
+ checker := principals[2]
+
+ var dataToSign []signing.Item
+ for i := 0; i != 1000; i++ {
+ dataToSign = append(dataToSign, signing.ItemData{Value: []byte("hello")})
+ }
+ author.data, author.authorBlessingsData, err =
+ signing.SignData(ctx, author.cache, author.principal, author.blessings, dataToSign)
+ if err != nil {
+ panic(err)
+ }
+ callToValidator := fakeCall{
+ localPrincipal: validator.principal,
+ localBlessings: validator.blessings,
+ remoteBlessings: author.blessings,
+ }
+ validator.names, err = author.data.Check(ctx, author.cache, &callToValidator, validator.krl, 24*30*time.Hour)
+ if err != nil {
+ panic(err)
+ }
+ callToChecker := fakeCall{
+ localPrincipal: checker.principal,
+ localBlessings: checker.blessings,
+ remoteBlessings: validator.blessings,
+ }
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ checker.names, err = author.data.Check(ctx, author.cache, &callToChecker, checker.krl, 24*30*time.Hour)
+ }
+}
+
+// BenchmarkMarshallBlessings() measures the time taken to marshal a Blessings.
+func BenchmarkMarshallBlessings(b *testing.B) {
+ var err error
+ author := makePrincipal(b, "author")
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ author.marshalledBlessings, err = vom.Encode(author.blessings)
+ }
+ if err != nil {
+ b.Fatalf("vom.Encode failed: %v", err)
+ }
+}
+
+// BenchmarkUnmarshallBlessings() measures the time taken to unmashal a Blessings.
+func BenchmarkUnmarshallBlessings(b *testing.B) {
+ var err error
+ author := makePrincipal(b, "author")
+ author.marshalledBlessings, err = vom.Encode(author.blessings)
+ if err != nil {
+ b.Fatalf("vom.Encode failed: %v", err)
+ }
+ var blessings security.Blessings
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ err = vom.Decode(author.marshalledBlessings, &blessings)
+ }
+ if err != nil {
+ b.Fatalf("vom.Encode failed: %v", err)
+ }
+}
+
+// BenchmarkMarshallPublicKey() measures the time taken to marshal a PublicKey.
+func BenchmarkMarshallPublicKey(b *testing.B) {
+ var err error
+ author := makePrincipal(b, "author")
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ _, err = author.principal.PublicKey().MarshalBinary()
+ }
+ if err != nil {
+ b.Fatalf("MarshalBinary() failed: %v", err)
+ }
+}
+
+// BenchmarkUnmarshallPublicKey() measures the time taken to unmarshal a PublicKey.
+func BenchmarkUnmarshallPublicKey(b *testing.B) {
+ var err error
+ author := makePrincipal(b, "author")
+ var marshalledKey []byte
+ marshalledKey, err = author.principal.PublicKey().MarshalBinary()
+ if err != nil {
+ b.Fatalf("MarshalBinary() failed: %v", err)
+ }
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ _, err = security.UnmarshalPublicKey(marshalledKey)
+ }
+ if err != nil {
+ b.Fatalf("MarshalBinary() failed: %v", err)
+ }
+}
+
+// TestSignData() tests that a complete flow of signing, validating, and
+// checking works on a DataWithSignature.
+func TestSignData(t *testing.T) {
+ ctx, shutdown := test.V23Init()
+ defer shutdown()
+
+ var err error
+
+ principals := makePrincipals(t, "author", "validator", "checker")
+ author := principals[0]
+ validator := principals[1]
+ checker := principals[2]
+
+ // Add each princpipal's blessings to each principal's roots.
+ pdList := []*principalDesc{author, validator, checker}
+ for i := 0; i != len(pdList); i++ {
+ for j := 0; j != len(pdList); j++ {
+ pdList[j].principal.AddToRoots(pdList[i].blessings)
+ }
+ }
+
+ // --------------------------------------
+ // Author
+ // Sign some data.
+ dataToSign := []signing.Item{
+ signing.ItemData{Value: []byte("hello")},
+ signing.ItemData{Value: []byte("world")},
+ signing.ItemData{Value: []byte("!")},
+ }
+ author.data, author.authorBlessingsData, err =
+ signing.SignData(ctx, author.cache, author.principal, author.blessings, dataToSign)
+ if err != nil {
+ t.Fatalf("signing.SignData failed: %v", err)
+ }
+ if author.data.IsValidated {
+ t.Fatalf("signing.SignData generated data with IsValidated set")
+ }
+
+ // --------------------------------------
+ // Validator
+ callToValidator := fakeCall{
+ localPrincipal: validator.principal,
+ localBlessings: validator.blessings,
+ remoteBlessings: author.blessings,
+ }
+ // The validator receives author.data from the author.
+ validator.data = new(signing.DataWithSignature)
+ *validator.data = *author.data
+ // Initially the validator doesn't have the author BlessingsData.
+ validator.authorBlessingsData = validator.cache.LookupBlessingsData(ctx, validator.data.BlessingsHash)
+ if validator.authorBlessingsData != nil {
+ t.Errorf("found non-nil BlessingsData for validator.data.BlessingsHash in validator's ValidationCache")
+ }
+ validator.names, err = validator.data.Check(ctx, validator.cache, &callToValidator, validator.krl, 24*30*time.Hour)
+ if verror.ErrorID(err) != signing.ErrNeedAuthorBlessingsForHash.ID {
+ t.Fatalf("validator.data.Check got err %v, want %s", err, signing.ErrNeedAuthorBlessingsForHash.ID)
+ }
+
+ // The validator receives the author's marshalled blessings from the author.
+ validator.marshalledBlessings = author.authorBlessingsData.MarshalledBlessings
+ validator.blessingsHash, validator.authorBlessingsData, err = validator.cache.AddWireBlessings(ctx, validator.marshalledBlessings)
+ if err != nil {
+ t.Fatalf("validator can't add author's marshalled belssings to its ValidationCache: %v", err)
+ }
+
+ validator.names, err = validator.data.Check(ctx, validator.cache, &callToValidator, validator.krl, 24*30*time.Hour)
+ if err != nil {
+ t.Fatalf("validator error calling Check() on data: %v", err)
+ }
+ if !validator.data.IsValidated {
+ t.Fatalf("signing.Check didn't set IsValidated")
+ }
+ // Validator's cache should now have the author's BlessingData, and the validator's ValidatorData.
+ validator.authorBlessingsData = validator.cache.LookupBlessingsData(ctx, validator.data.BlessingsHash)
+ if validator.authorBlessingsData == nil {
+ t.Errorf("didn't finf BlessingsData for validator.data.BlessingsHash in validator's ValidationCache")
+ }
+ validator.validatorData = validator.cache.LookupValidatorData(ctx, validator.data.ValidatorDataHash)
+
+ // --------------------------------------
+ // Checker
+ callToChecker := fakeCall{
+ localPrincipal: checker.principal,
+ localBlessings: checker.blessings,
+ remoteBlessings: validator.blessings,
+ }
+ // The checker recieves validator.data from the validator, except that
+ // data item 1 is replaced by its hash, because (for example) the
+ // check is not allowed to see it.
+ checker.data = new(signing.DataWithSignature)
+ *checker.data = *validator.data
+ checker.data.Data[1] = signing.ItemHash{Value: signing.SumByteVectorWithLength(checker.data.Data[1].(signing.ItemData).Value)}
+
+ // Initially the checker doesn't have the author BlessingsData, or the validator ValidatorData.
+ checker.authorBlessingsData = checker.cache.LookupBlessingsData(ctx, checker.data.BlessingsHash)
+ if checker.authorBlessingsData != nil {
+ t.Errorf("found non-nil blessings data for checker.data.BlessingsHash hash in checker's ValidationCache")
+ }
+ checker.names, err = checker.data.Check(ctx, checker.cache, &callToChecker, checker.krl, 24*30*time.Hour)
+ if verror.ErrorID(err) != signing.ErrNeedAuthorBlessingsAndValidatorDataForHash.ID {
+ t.Fatalf("checker.data.Check got err %v, want %s", err, signing.ErrNeedAuthorBlessingsAndValidatorDataForHash.ID)
+ }
+
+ // The checker receives the author's marshalled blessings from the validator.
+ checker.marshalledBlessings = validator.marshalledBlessings
+ checker.blessingsHash, checker.authorBlessingsData, err = checker.cache.AddWireBlessings(ctx, checker.marshalledBlessings)
+ if err != nil {
+ t.Fatalf("checker can't add author's marshalled belssings to its ValidationCache: %v", err)
+ }
+ checker.names, err = checker.data.Check(ctx, checker.cache, &callToChecker, checker.krl, 24*30*time.Hour)
+ if verror.ErrorID(err) != signing.ErrNeedValidatorDataForHash.ID {
+ t.Fatalf("checker.data.Check got err %v, want %s", err, signing.ErrNeedValidatorDataForHash.ID)
+ }
+
+ // The checker receives the validator's data from the validator, passing through the wire format.
+ wvd := signing.ToWireValidatorData(validator.validatorData)
+ var vd signing.ValidatorData
+ vd, err = signing.FromWireValidatorData(&wvd)
+ if err != nil {
+ t.Fatalf("signing.FromWireValidatorData got error: %v", err)
+ }
+ checker.validatorData = &vd
+
+ // The checker adds the ValidatorData to its cache.
+ checker.validatorHash = checker.cache.AddValidatorData(ctx, checker.validatorData)
+
+ // And now the Check() operation should work.
+ checker.names, err = checker.data.Check(ctx, checker.cache, &callToChecker, checker.krl, 24*30*time.Hour)
+ if err != nil {
+ t.Fatalf("checker.data.Check got unexpected err %v", err)
+ }
+}
diff --git a/x/ref/services/syncbase/signing/validationcache.go b/x/ref/services/syncbase/signing/validationcache.go
new file mode 100644
index 0000000..1c95bd0
--- /dev/null
+++ b/x/ref/services/syncbase/signing/validationcache.go
@@ -0,0 +1,190 @@
+// 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.
+
+// This module implements a cache of data associated with the
+// signatures, keyed by hash values of the data. The intent is that
+// communicating devices will refer to the data using hashes, and transmit the
+// data itself only if the device on the other side does not have the data in
+// its cache.
+
+package signing
+
+import "crypto/sha256"
+import "encoding/binary"
+import "time"
+
+import "v.io/syncbase/x/ref/services/syncbase/signing/hashcache"
+import "v.io/v23/context"
+import "v.io/v23/security"
+import "v.io/v23/vom"
+
+// --------------------------------------------
+
+// A BlessingsData contains information about a security.Blessings object. The
+// object itself is referred to by UnmarshalledBlessings. The implementation
+// constructs all instances; the client should not modify fields.
+type BlessingsData struct {
+ UnmarshalledBlessings security.Blessings // The Blessings.
+ MarshalledBlessings []byte // VOM encoded Blessings.
+ MarshalledPublicKey []byte // Value from blessings.PublicKey().MarshalBinary().
+}
+
+// A ValidatorData is the extra data that a validator signs when validating and
+// signing a DataWithSignature. Clients may construct instances to pass to
+// AddValidatorData(), but should not modify the fields of a constructed
+// ValidatorData.
+type ValidatorData struct {
+ Names []string // Names of valid signing blessings in the Blessings referred to by BlessingsHash.
+ PublicKey security.PublicKey // The key used to create ValidatorSigned.
+ MarshalledPublicKey []byte // PublicKey, marshalled with MarshalBinary().
+}
+
+// hash() returns the hash of *vd. This hash should be used in the
+// ValidatorDataHash field of DataWithSignature, and as the cache key of *vd
+// in a ValidationCache.
+func (vd *ValidatorData) hash() []byte {
+ hasher := sha256.New()
+ var buffer [256]byte
+ var buf []byte = buffer[:]
+ binary.LittleEndian.PutUint64(buf[:], uint64(len(vd.Names)))
+ hasher.Write(buf[:8])
+ for i := range vd.Names {
+ if len(vd.Names[i]) > len(buf) {
+ buf = make([]byte, len(vd.Names[i])+256)
+ }
+ hashByteVectorWithLength(hasher, []byte(vd.Names[i]))
+ }
+ hashByteVectorWithLength(hasher, vd.MarshalledPublicKey)
+ return hasher.Sum(nil)[:]
+}
+
+// A ValidationCache records recently-seen instances of BlessingsData and
+// ValidatorData values, keys by hashes of the blessings and validator keys
+// respectively. Values may expire from the cache if unused for a duration
+// specified with NewValidationCache().
+type ValidationCache struct {
+ blessingsCache *hashcache.Cache
+ validatorCache *hashcache.Cache
+}
+
+// NewValidationCache() returns a pointer to a new, empty ValidationCache with
+// the specified expiry duration..
+func NewValidationCache(expiry time.Duration) *ValidationCache {
+ return &ValidationCache{
+ blessingsCache: hashcache.New(expiry),
+ validatorCache: hashcache.New(expiry)}
+}
+
+// LookupBlessingsData() returns a pointer to the BlessingsData associated with
+// blessingsHash in *vc. blessingsHash should have been returned by a previous
+// call to AddBlessings() or AddWireBlessings() (possibly on another machine).
+// nil is returned if the data is not present. The client should not modify
+// *result, since it is shared with *vc.
+func (vc *ValidationCache) LookupBlessingsData(ctx *context.T, blessingsHash []byte) (result *BlessingsData) {
+ value, found := vc.blessingsCache.Lookup(blessingsHash)
+ if found {
+ result = value.(*BlessingsData)
+ }
+ return result
+}
+
+// addBlessings() adds a BlessingsData for blessings to *vc, and returns a hash
+// value, which if passed to LookupBlessingsData() will yield a pointer to the
+// BlessingsData, or a non-nil error. The fields of BlessingsData other than
+// MarshalledBlessings and UnmarshalledBlessings are constructed by this
+// routine. Requires that blessings and marshalledBlessings represent the same
+// data, or that marshalledBlessings be nil.
+func (vc *ValidationCache) addBlessings(ctx *context.T, blessings security.Blessings,
+ marshalledBlessings []byte) (blessingsHash []byte, data *BlessingsData, err error) {
+
+ blessingsHash = blessings.UniqueID()
+ if value, found := vc.blessingsCache.Lookup(blessingsHash); found {
+ data = value.(*BlessingsData)
+ } else { // not found
+ var marshalledKey []byte
+ if marshalledBlessings == nil {
+ marshalledBlessings, err = vom.Encode(blessings)
+ }
+ if err == nil {
+ marshalledKey, err = blessings.PublicKey().MarshalBinary()
+ }
+ if err == nil {
+ data = &BlessingsData{
+ UnmarshalledBlessings: blessings,
+ MarshalledBlessings: marshalledBlessings,
+ MarshalledPublicKey: marshalledKey}
+ vc.blessingsCache.Add(blessingsHash, data)
+ }
+ }
+ return blessingsHash, data, err
+}
+
+// AddBlessings() adds a BlessingsData for blessings to *cv, and
+// returns a hash value, which if passed to LookupBlessingsData() will yield a
+// pointer to the BlessingsData, or a non-nil error. The fields of
+// BlessingsData other than UnmarshalledBlessings are constructed by this
+// routine.
+func (vc *ValidationCache) AddBlessings(ctx *context.T, blessings security.Blessings) (blessingsHash []byte, data *BlessingsData, err error) {
+ return vc.addBlessings(ctx, blessings, nil)
+}
+
+// AddWireBlessings() adds a BlessingsData for blessings to *cv, and
+// returns a hash value, which if passed to LookupBlessingsData() will yield a
+// pointer to the BlessingsData, or a non-nil error. The fields of
+// BlessingsData other than MarshalledBlessings are constructed by this
+// routine.
+func (vc *ValidationCache) AddWireBlessings(ctx *context.T,
+ marshalledBlessings []byte) (blessingsHash []byte, data *BlessingsData, err error) {
+
+ var blessings security.Blessings
+ err = vom.Decode(marshalledBlessings, &blessings)
+ if err == nil {
+ blessingsHash, data, err = vc.addBlessings(ctx, blessings, marshalledBlessings)
+ }
+ return blessingsHash, data, err
+}
+
+// LookupValidatorData() returns a pointer to the ValidatorData associated with
+// hash validatorHash in *vc validatorHash should have been returned by a
+// previous call to AddValidatorData() (possibly on another machine). nil is
+// returned if the data is not present. The client should not modifiy *result,
+// since it it shared with *vc.
+func (vc *ValidationCache) LookupValidatorData(ctx *context.T, validatorHash []byte) (result *ValidatorData) {
+ value, found := vc.validatorCache.Lookup(validatorHash)
+ if found {
+ result = value.(*ValidatorData)
+ }
+ return result
+}
+
+// AddValidatorData() adds a ValidatorData *vd to cache *vc, and returns a hash
+// value, which if passed to LookupValidatorData() will yield a pointer to the
+// ValidatorData. The client should not modify *vd after the call, since it is
+// shared with *vc.
+func (vc *ValidationCache) AddValidatorData(ctx *context.T, vd *ValidatorData) (validatorDataHash []byte) {
+ validatorDataHash = vd.hash()
+ vc.validatorCache.Add(validatorDataHash, vd)
+ return validatorDataHash
+}
+
+// ToWireValidatorData() puts the wire form of ValidatorData *vd in *wvd.
+func ToWireValidatorData(vd *ValidatorData) (wvd WireValidatorData) {
+ wvd.Names = make([]string, len(vd.Names))
+ copy(wvd.Names, vd.Names)
+ wvd.MarshalledPublicKey = make([]byte, len(vd.MarshalledPublicKey))
+ copy(wvd.MarshalledPublicKey, vd.MarshalledPublicKey)
+ return wvd
+}
+
+// FromWireValidatorData() puts the in-memory form of WireValidatorData *wvd in *vd.
+func FromWireValidatorData(wvd *WireValidatorData) (vd ValidatorData, err error) {
+ vd.PublicKey, err = security.UnmarshalPublicKey(wvd.MarshalledPublicKey)
+ if err == nil {
+ vd.Names = make([]string, len(wvd.Names))
+ copy(vd.Names, wvd.Names)
+ vd.MarshalledPublicKey = make([]byte, len(wvd.MarshalledPublicKey))
+ copy(vd.MarshalledPublicKey, wvd.MarshalledPublicKey)
+ }
+ return vd, err
+}
diff --git a/x/ref/services/syncbase/signing/validationcache_test.go b/x/ref/services/syncbase/signing/validationcache_test.go
new file mode 100644
index 0000000..a45835b
--- /dev/null
+++ b/x/ref/services/syncbase/signing/validationcache_test.go
@@ -0,0 +1,189 @@
+// 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.
+
+// This file tests the validationcache.go module.
+
+package signing_test
+
+import "bytes"
+import "testing"
+import "time"
+
+import "v.io/syncbase/x/ref/services/syncbase/signing"
+import "v.io/v23/security"
+import "v.io/x/ref/test"
+import _ "v.io/x/ref/runtime/factories/generic"
+import lib_security "v.io/x/ref/lib/security"
+
+// A principalVDesc holds the local state of a single principal in the tests below.
+type principalVDesc struct {
+ name string
+ principal security.Principal
+ blessings security.Blessings
+ blessingsHash []byte
+ blessingsData *signing.BlessingsData
+ validatorHash []byte
+ validatorData *signing.ValidatorData
+ cache *signing.ValidationCache
+}
+
+// makePrincipalVDesc() returns a pointer to a newly-initialized principalVDesc,
+// with a unique key, and a single blessing named with its own name.
+func makePrincipalVDesc(t *testing.T, name string) (desc *principalVDesc) {
+ var err error
+ desc = new(principalVDesc)
+ desc.name = name
+ desc.principal, err = lib_security.NewPrincipal()
+ if err != nil {
+ t.Fatalf("lib_security.NewPrincipal %q failed: %v", desc.name, err)
+ }
+ desc.blessings, err = desc.principal.BlessSelf(desc.name)
+ if err != nil {
+ t.Fatalf("principal.BlessSelf %q failed: %v", desc.name, err)
+ }
+ desc.cache = signing.NewValidationCache(5 * time.Second)
+ return desc
+}
+
+func TestValidationCache(t *testing.T) {
+ ctx, shutdown := test.V23Init()
+ defer shutdown()
+
+ var err error
+
+ // Make a principalVDesc for each of the author, validator, and checker.
+ // (The author creates a signed change; the validator is a device
+ // author syncs with; the checker is a device a validator syncs with.)
+ author := makePrincipalVDesc(t, "author")
+ validator := makePrincipalVDesc(t, "validator")
+ checker := makePrincipalVDesc(t, "checker")
+
+ // Add each princpipal's blessings to each principal's roots.
+ pdList := []*principalVDesc{author, validator, checker}
+ for i := 0; i != len(pdList); i++ {
+ for j := 0; j != len(pdList); j++ {
+ pdList[j].principal.AddToRoots(pdList[i].blessings)
+ }
+ }
+
+ // --------------------------------------
+ // Author
+ arbitraryBlessingsData := author.cache.LookupBlessingsData(ctx, []byte{0x00})
+ if arbitraryBlessingsData != nil {
+ t.Errorf("found non-nil blessings data for nonsense hash in author's ValidationCache")
+ }
+ author.blessingsHash, author.blessingsData, err = author.cache.AddBlessings(ctx, author.blessings)
+ if err != nil {
+ t.Fatalf("error from author.cache.AddBlessings(): %v", err)
+ }
+ // Check that the author's data is as we expect.
+ if author.cache.LookupBlessingsData(ctx, author.blessingsHash) != author.blessingsData {
+ t.Fatalf("found wrong blessings data for hash in author's ValidationCache: %v vs %v",
+ author.cache.LookupBlessingsData(ctx, author.blessingsHash), author.blessingsData)
+ }
+
+ // --------------------------------------
+ // Validator
+ // The validator receives author.blessingsHash from the author.
+ // Initially the validator doesn't have the author BlessingsData.
+ authorBlessingsData := validator.cache.LookupBlessingsData(ctx, author.blessingsHash)
+ if authorBlessingsData != nil {
+ t.Errorf("found non-nil blessings data for author.blessingsHash hash in validator's ValidationCache")
+ }
+ // The validator receives the author's marshalled blessings from the author.
+ validator.blessingsHash, validator.blessingsData, err =
+ validator.cache.AddWireBlessings(ctx, author.blessingsData.MarshalledBlessings)
+ if err != nil {
+ t.Fatalf("validator can't add author's marshalled blessings to its ValidationCache: %v", err)
+ }
+ if !bytes.Equal(author.blessingsHash, validator.blessingsHash) {
+ t.Errorf("validator's copy of the blessingsHash different from author's")
+ }
+ // Check that we could have got the blessingsData with a lookup if this were the second time.
+ if validator.cache.LookupBlessingsData(ctx, validator.blessingsHash) != validator.blessingsData {
+ t.Fatalf("found wrong blessings data for hash in validator's ValidationCache")
+ }
+ var marshalledPublicKey []byte
+ marshalledPublicKey, err = validator.principal.PublicKey().MarshalBinary()
+ if err != nil {
+ t.Fatalf("validator.principal.PublicKey().MarshalBinary() got error: %v", err)
+ }
+
+ var validatedNames []string
+ validatedNames, _ = security.SigningBlessingNames(ctx, validator.principal,
+ validator.blessingsData.UnmarshalledBlessings)
+ validator.validatorData = &signing.ValidatorData{
+ Names: validatedNames,
+ PublicKey: validator.principal.PublicKey(),
+ MarshalledPublicKey: marshalledPublicKey}
+ validator.validatorHash = validator.cache.AddValidatorData(ctx, validator.validatorData)
+ if validator.cache.LookupValidatorData(ctx, validator.validatorHash) != validator.validatorData {
+ t.Fatalf("LookupValidatorData returned wrong ValidatorData pointer in validator")
+ }
+
+ // --------------------------------------
+ // Checker
+ // The checker receives validator.blessingsHash from the validator.
+ // Initially the checker doesn't have the author BlessingsData.
+ authorBlessingsData = checker.cache.LookupBlessingsData(ctx, validator.blessingsHash)
+ if authorBlessingsData != nil {
+ t.Errorf("found non-nil blessings data for author.blessingsHash hash in checker's ValidationCache")
+ }
+ // The checker receives the author's marshalled blessings from the validator.
+ checker.blessingsHash, checker.blessingsData, err =
+ checker.cache.AddWireBlessings(ctx, validator.blessingsData.MarshalledBlessings)
+ if err != nil {
+ t.Fatalf("checker can't add author's marshalled blessings (from validator) to ValidationCache: %v", err)
+ }
+ if !bytes.Equal(author.blessingsHash, checker.blessingsHash) {
+ t.Errorf("checker's copy of the blessingsHash different from author's")
+ }
+ // Check that we could have got the blessingsData with a lookup if this where the second time.
+ if checker.cache.LookupBlessingsData(ctx, checker.blessingsHash) != checker.blessingsData {
+ t.Fatalf("found wrong blessings data for hash in checker's ValidationCache")
+ }
+ // The checker recieves validator.validatorHash from the validator.
+ // Initially the checker doesn't have the ValidatorData.
+ validatorData := checker.cache.LookupValidatorData(ctx, validator.validatorHash)
+ if validatorData != nil {
+ t.Errorf("found non-nil validator data for validator.validatorHash hash in checker's ValidationCache")
+ }
+ // The checker receives the validator's data from the validator (or another checker).
+ checker.validatorHash = checker.cache.AddValidatorData(ctx, validator.validatorData)
+ if !bytes.Equal(validator.validatorHash, checker.validatorHash) {
+ t.Fatalf("checker's copy of the validatorHash different from validator's")
+ }
+ // Get the validatorData
+ checker.validatorData = checker.cache.LookupValidatorData(ctx, checker.validatorHash)
+ if checker.validatorData == nil {
+ t.Fatalf("found nil valdidatorData for checker.validatorHash hash in checker's ValidationCache")
+ }
+}
+
+func TestWireValidatorData(t *testing.T) {
+ var err error
+
+ pDesc := makePrincipalVDesc(t, "some_principal")
+
+ var vd signing.ValidatorData
+ vd.Names = []string{"wombat", "foo"}
+ vd.PublicKey = pDesc.principal.PublicKey()
+ vd.MarshalledPublicKey, err = vd.PublicKey.MarshalBinary()
+ if err != nil {
+ t.Fatalf("failed to marshel public key: %v\n", err)
+ }
+
+ var wvd signing.WireValidatorData
+ var vd2 signing.ValidatorData
+
+ wvd = signing.ToWireValidatorData(&vd)
+ vd2, err = signing.FromWireValidatorData(&wvd)
+ if err != nil {
+ t.Fatalf("FromWireValidatorData failed: %v\n", err)
+ }
+ if len(vd.Names) != len(vd2.Names) {
+ t.Fatalf("ToWireValidatorData/FromWireValidatorData failed to transfer Names list correctly:\nold\n%v\n\nnew\n%v\n\nwire\n%v\n",
+ vd, vd2, wvd)
+ }
+}