From 56c59d57b12cd4ca25f3cfaa6229f47014e6ed12 Mon Sep 17 00:00:00 2001 From: lash Date: Wed, 17 Jan 2018 06:04:49 +0100 Subject: [PATCH 1/6] swarm/storage: External signing, chunk data verification --- swarm/storage/resource.go | 121 +++++++++++++++++++-------------- swarm/storage/resource_ens.go | 12 ++-- swarm/storage/resource_test.go | 82 +++++++++++++++++----- 3 files changed, 138 insertions(+), 77 deletions(-) diff --git a/swarm/storage/resource.go b/swarm/storage/resource.go index 34444f92c2..c4b4e51597 100644 --- a/swarm/storage/resource.go +++ b/swarm/storage/resource.go @@ -93,7 +93,7 @@ type resource struct { // treated as a resource update chunk. type ResourceValidator interface { - isOwner(string) (bool, error) + isOwner(string, common.Address) (bool, error) nameHash(string) common.Hash } @@ -105,7 +105,6 @@ type ResourceHandler struct { hashLock sync.Mutex resourceLock sync.RWMutex hasher SwarmHash - privKey *ecdsa.PrivateKey maxChunkData int64 } @@ -127,7 +126,6 @@ func NewResourceHandler(privKey *ecdsa.PrivateKey, datadir string, cloudStore Cl rpcClient: rpcClient, resources: make(map[string]*resource), hasher: hasher(), - privKey: privKey, maxChunkData: DefaultBranches * int64(hasher().Size()), } @@ -187,9 +185,13 @@ func NewResource(name string, startBlock uint64, frequency uint64, nameHashFunc // Creates a new root entry for a mutable resource identified by `name` with the specified `frequency`. // // The start block of the resource update will be the actual current block height of the connected network. -func (self *ResourceHandler) NewResource(name string, frequency uint64) (*resource, error) { +func (self *ResourceHandler) NewResource(name string, frequency uint64, signature [signatureLength]byte) (*resource, error) { - ok, err := self.validator.isOwner(name) + addr, err := self.getAddressFromDataSig([]byte(name), signature) + if err != nil { + return nil, fmt.Errorf("Corrupt signature") + } + ok, err := self.validator.isOwner(name, addr) if err != nil { return nil, err } else if !ok { @@ -463,9 +465,13 @@ func parseUpdate(blob []byte) (period uint32, version uint32, ensname []byte, da // It is the caller's responsibility to make sure that this data is not stale. // // A resource update cannot span chunks, and thus has max length 4096 -func (self *ResourceHandler) Update(name string, data []byte) (Key, error) { +func (self *ResourceHandler) Update(name string, data []byte, signature [signatureLength]byte) (Key, error) { - ok, err := self.validator.isOwner(name) + addr, err := self.getAddressFromDataSig(data, signature) + if err != nil { + return nil, fmt.Errorf("Invalid data/signature: %v", err) + } + ok, err := self.validator.isOwner(name, addr) if err != nil { return nil, err } else if !ok { @@ -501,34 +507,36 @@ func (self *ResourceHandler) Update(name string, data []byte) (Key, error) { } version++ + // create the update chunk // prepend version and period to allow reverse lookups // data header length does NOT include the header length prefix bytes themselves headerlength := uint16(len(resource.nameHash) + 4 + 4) - fulldata := make([]byte, int(headerlength)+2+len(data)) - cursor := 0 - binary.LittleEndian.PutUint16(fulldata, headerlength) - cursor += 2 - - binary.LittleEndian.PutUint32(fulldata[cursor:], nextperiod) - cursor += 4 - - binary.LittleEndian.PutUint32(fulldata[cursor:], version) - cursor += 4 - - copy(fulldata[cursor:], resource.nameHash[:]) - cursor += len(resource.nameHash) - - copy(fulldata[cursor:], data) - - // create the update chunk and send it key := self.resourceHash(resource.nameHash, nextperiod, version) chunk := NewChunk(key, nil) - chunk.SData, err = self.signContent(fulldata) - if err != nil { - return nil, err - } - chunk.Size = int64(len(fulldata)) + chunk.SData = make([]byte, signatureLength+int(headerlength)+2+len(data)) + + cursor := 0 + copy(chunk.SData, signature[:]) + cursor += signatureLength + + binary.LittleEndian.PutUint16(chunk.SData[cursor:], headerlength) + cursor += 2 + + binary.LittleEndian.PutUint32(chunk.SData[cursor:], nextperiod) + cursor += 4 + + binary.LittleEndian.PutUint32(chunk.SData[cursor:], version) + cursor += 4 + + copy(chunk.SData[cursor:], resource.nameHash[:]) + cursor += len(resource.nameHash) + + copy(chunk.SData[cursor:], data) + + chunk.Size = int64(len(chunk.SData)) + + // send the chunk self.Put(chunk) log.Trace("resource update", "name", resource.name, "key", key, "currentblock", currentblock, "lastperiod", nextperiod, "version", version, "data", chunk.SData) @@ -594,33 +602,33 @@ func (self *ResourceHandler) resourceHash(namehash common.Hash, period uint32, v return self.hasher.Sum(nil) } -func (self *ResourceHandler) signContent(data []byte) ([]byte, error) { +func (self *ResourceHandler) getContentAccount(chunkdata []byte) (common.Address, error) { + if len(chunkdata) <= signatureLength { + return common.Address{}, fmt.Errorf("zero-length data") + } + var signaturetype [signatureLength]byte + copy(signaturetype[:], chunkdata[:signatureLength]) + return self.getAddressFromDataSig(chunkdata[signatureLength:], signaturetype) +} + +func (self *ResourceHandler) getContentName(chunkdata []byte) (string, error) { + nameoffset := signatureLength + 2 + 4 + 4 + if len(chunkdata) < nameoffset { + return "", fmt.Errorf("invalid chunk data") + } + namelength := binary.LittleEndian.Uint16(chunkdata[signatureLength : signatureLength+2]) + namebytes := make([]byte, namelength) + copy(namebytes, chunkdata[nameoffset:nameoffset+int(namelength)-2-4-4]) + return string(namebytes), nil +} + +func (self *ResourceHandler) getAddressFromDataSig(data []byte, signature [signatureLength]byte) (common.Address, error) { self.hashLock.Lock() self.hasher.Reset() self.hasher.Write(data) datahash := self.hasher.Sum(nil) self.hashLock.Unlock() - - signature, err := crypto.Sign(datahash, self.privKey) - if err != nil { - return nil, err - } - datawithsign := make([]byte, len(data)+signatureLength) - copy(datawithsign[:signatureLength], signature) - copy(datawithsign[signatureLength:], data) - return datawithsign, nil -} - -func (self *ResourceHandler) getContentAccount(chunkdata []byte) (common.Address, error) { - if len(chunkdata) <= signatureLength { - return common.Address{}, fmt.Errorf("zero-length data") - } - self.hashLock.Lock() - self.hasher.Reset() - self.hasher.Write(chunkdata[signatureLength:]) - datahash := self.hasher.Sum(nil) - self.hashLock.Unlock() - pub, err := crypto.SigToPub(datahash, chunkdata[:signatureLength]) + pub, err := crypto.SigToPub(datahash, signature[:]) if err != nil { return common.Address{}, err } @@ -632,7 +640,16 @@ func (self *ResourceHandler) verifyContent(chunkdata []byte) error { if err != nil { return err } - log.Warn("ens owner lookup not implemented, verify will return true in all cases", "address", address) + name, err := self.getContentName(chunkdata) + if err != nil { + return err + } + ok, err := self.validator.isOwner(name, address) + if err != nil { + return err + } else if !ok { + return fmt.Errorf("not owner") + } return nil } diff --git a/swarm/storage/resource_ens.go b/swarm/storage/resource_ens.go index a82311b08f..8a742e2716 100644 --- a/swarm/storage/resource_ens.go +++ b/swarm/storage/resource_ens.go @@ -8,27 +8,25 @@ import ( // ENS validation of mutable resource owners type ENSValidator struct { - owner common.Address - api *ens.ENS + api *ens.ENS } -func NewENSValidator(owneraddress common.Address, contractaddress common.Address, backend bind.ContractBackend, transactOpts *bind.TransactOpts) (*ENSValidator, error) { +func NewENSValidator(contractaddress common.Address, backend bind.ContractBackend, transactOpts *bind.TransactOpts) (*ENSValidator, error) { var err error validator := &ENSValidator{} validator.api, err = ens.NewENS(transactOpts, contractaddress, backend) if err != nil { return nil, err } - validator.owner = owneraddress return validator, nil } -func (self *ENSValidator) isOwner(name string) (bool, error) { +func (self *ENSValidator) isOwner(name string, address common.Address) (bool, error) { owneraddr, err := self.api.Owner(self.nameHash(name)) if err != nil { return false, err } - return owneraddr == self.owner, nil + return owneraddr == address, nil } func (self *ENSValidator) nameHash(name string) common.Hash { @@ -45,7 +43,7 @@ func NewGenericValidator(hashFunc func(string) common.Hash) *GenericValidator { hashFunc: hashFunc, } } -func (self *GenericValidator) isOwner(name string) (bool, error) { +func (self *GenericValidator) isOwner(name string, address common.Address) (bool, error) { return true, nil } diff --git a/swarm/storage/resource_test.go b/swarm/storage/resource_test.go index 951899ebfa..bc1fe37634 100644 --- a/swarm/storage/resource_test.go +++ b/swarm/storage/resource_test.go @@ -141,7 +141,12 @@ func TestResourceReverseLookup(t *testing.T) { } // create a new resource - rsrc, err := rh.NewResource(domainName, resourceFrequency) + signature, err := signContent(privkey, []byte(domainName)) + if err != nil { + teardownTest(t, err) + } + + rsrc, err := rh.NewResource(domainName, resourceFrequency, signature) if err != nil { teardownTest(t, err) } @@ -149,7 +154,12 @@ func TestResourceReverseLookup(t *testing.T) { // update data fwdBlocks(int(resourceFrequency+1), backend) data := []byte("foo") - resourcekey, err := rh.Update(domainName, data) + signature, err = signContent(privkey, data) + if err != nil { + teardownTest(t, err) + } + + resourcekey, err := rh.Update(domainName, data, signature) if err != nil { teardownTest(t, err) } @@ -205,7 +215,8 @@ func TestResourceHandler(t *testing.T) { if err != nil { teardownTest(t, err) } - _, err = rh.NewResource(domainName, resourceFrequency) + signature, err := signContent(privkey, []byte(resourcevalidname)) + _, err = rh.NewResource(domainName, resourceFrequency, signature) if err != nil { teardownTest(t, err) } @@ -230,28 +241,48 @@ func TestResourceHandler(t *testing.T) { // update halfway to first period resourcekey := make(map[string]Key) fwdBlocks(int(resourceFrequency/2), backend) - resourcekey["blinky"], err = rh.Update(domainName, []byte("blinky")) + data := []byte("blinky") + signature, err = signContent(privkey, data) + if err != nil { + teardownTest(t, err) + } + resourcekey["blinky"], err = rh.Update(domainName, data, signature) if err != nil { teardownTest(t, err) } // update on first period fwdBlocks(int(resourceFrequency/2), backend) - resourcekey["pinky"], err = rh.Update(domainName, []byte("pinky")) + data = []byte("pinky") + signature, err = signContent(privkey, data) + if err != nil { + teardownTest(t, err) + } + resourcekey["pinky"], err = rh.Update(domainName, data, signature) if err != nil { teardownTest(t, err) } // update on second period fwdBlocks(int(resourceFrequency), backend) - resourcekey["inky"], err = rh.Update(domainName, []byte("inky")) + data = []byte("inky") + signature, err = signContent(privkey, data) + if err != nil { + teardownTest(t, err) + } + resourcekey["inky"], err = rh.Update(domainName, data, signature) if err != nil { teardownTest(t, err) } // update just after second period fwdBlocks(1, backend) - resourcekey["clyde"], err = rh.Update(domainName, []byte("clyde")) + data = []byte("clyde") + signature, err = signContent(privkey, data) + if err != nil { + teardownTest(t, err) + } + resourcekey["clyde"], err = rh.Update(domainName, data, signature) if err != nil { teardownTest(t, err) } @@ -350,7 +381,7 @@ func TestResourceENSOwner(t *testing.T) { t.Fatal(err) } - validator, err := NewENSValidator(addr, contractAddr, contractbackend, transactOpts) + validator, err := NewENSValidator(contractAddr, contractbackend, transactOpts) if err != nil { t.Fatal(err) } @@ -361,28 +392,29 @@ func TestResourceENSOwner(t *testing.T) { teardownTest(t, err) } + signature, err := signContent(privkey, []byte(domainName)) + if err != nil { + teardownTest(t, err) + } // create new resource when we are owner = ok - _, err = rh.NewResource(domainName, 42) + _, err = rh.NewResource(domainName, 42, signature) if err != nil { teardownTest(t, fmt.Errorf("Create resource fail: %v", err)) } + data := []byte("foo") + signature, err = signContent(privkey, data) + // update resource when we are owner = ok - _, err = rh.Update(domainName, []byte("foo")) + _, err = rh.Update(domainName, data, signature) if err != nil { teardownTest(t, fmt.Errorf("Update resource fail: %v", err)) } // create new resource when we are NOT owner = !ok - addrtwo := crypto.PubkeyToAddress(privkeytwo.PublicKey) - validator.owner = addrtwo - - _, err = rh.NewResource(domainName, 42) - if err == nil { - teardownTest(t, fmt.Errorf("Expected resource create fail due to owner mismatch")) - } + signaturetwo, err := signContent(privkeytwo, data) // update resource when we are owner = ok - _, err = rh.Update(domainName, []byte("foo")) + _, err = rh.Update(domainName, data, signaturetwo) if err == nil { teardownTest(t, fmt.Errorf("Expected resource update fail due to owner mismatch")) } @@ -503,6 +535,20 @@ func setupENS(addr common.Address, transactOpts *bind.TransactOpts, sub string, return contractAddress, contractBackend, nil } +func signContent(privKey *ecdsa.PrivateKey, data []byte) ([signatureLength]byte, error) { + hasher.Reset() + hasher.Write(data) + datahash := hasher.Sum(nil) + + signature, err := crypto.Sign(datahash, privKey) + if err != nil { + return [signatureLength]byte{}, err + } + var signaturetype [signatureLength]byte + copy(signaturetype[:], signature) + return signaturetype, nil +} + type testCloudStore struct { } From f482ecc6de362489ef7ea9bdfb23397cc1d04297 Mon Sep 17 00:00:00 2001 From: lash Date: Wed, 17 Jan 2018 06:22:46 +0100 Subject: [PATCH 2/6] swarm/storage: Add signature type --- swarm/storage/resource.go | 25 +++++++++++++++++++------ swarm/storage/resource_test.go | 9 ++++----- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/swarm/storage/resource.go b/swarm/storage/resource.go index c4b4e51597..1de6254545 100644 --- a/swarm/storage/resource.go +++ b/swarm/storage/resource.go @@ -22,6 +22,17 @@ const ( indexSize = 24 ) +type Signature [signatureLength]byte + +func NewSignature(b []byte) (Signature, error) { + var s Signature + if len(b) != signatureLength { + return [signatureLength]byte{}, fmt.Errorf("Must be %d bytes", signatureLength) + } + copy(s[:], b) + return s, nil +} + // Encapsulates an actual resource update. When synced it contains the most recent // version of the resource update data. type resource struct { @@ -185,7 +196,7 @@ func NewResource(name string, startBlock uint64, frequency uint64, nameHashFunc // Creates a new root entry for a mutable resource identified by `name` with the specified `frequency`. // // The start block of the resource update will be the actual current block height of the connected network. -func (self *ResourceHandler) NewResource(name string, frequency uint64, signature [signatureLength]byte) (*resource, error) { +func (self *ResourceHandler) NewResource(name string, frequency uint64, signature Signature) (*resource, error) { addr, err := self.getAddressFromDataSig([]byte(name), signature) if err != nil { @@ -465,7 +476,7 @@ func parseUpdate(blob []byte) (period uint32, version uint32, ensname []byte, da // It is the caller's responsibility to make sure that this data is not stale. // // A resource update cannot span chunks, and thus has max length 4096 -func (self *ResourceHandler) Update(name string, data []byte, signature [signatureLength]byte) (Key, error) { +func (self *ResourceHandler) Update(name string, data []byte, signature Signature) (Key, error) { addr, err := self.getAddressFromDataSig(data, signature) if err != nil { @@ -606,9 +617,11 @@ func (self *ResourceHandler) getContentAccount(chunkdata []byte) (common.Address if len(chunkdata) <= signatureLength { return common.Address{}, fmt.Errorf("zero-length data") } - var signaturetype [signatureLength]byte - copy(signaturetype[:], chunkdata[:signatureLength]) - return self.getAddressFromDataSig(chunkdata[signatureLength:], signaturetype) + signature, err := NewSignature(chunkdata[:signatureLength]) + if err != nil { + return zeroAddr, err + } + return self.getAddressFromDataSig(chunkdata[signatureLength:], signature) } func (self *ResourceHandler) getContentName(chunkdata []byte) (string, error) { @@ -622,7 +635,7 @@ func (self *ResourceHandler) getContentName(chunkdata []byte) (string, error) { return string(namebytes), nil } -func (self *ResourceHandler) getAddressFromDataSig(data []byte, signature [signatureLength]byte) (common.Address, error) { +func (self *ResourceHandler) getAddressFromDataSig(data []byte, signature Signature) (common.Address, error) { self.hashLock.Lock() self.hasher.Reset() self.hasher.Write(data) diff --git a/swarm/storage/resource_test.go b/swarm/storage/resource_test.go index bc1fe37634..a8b801b2f1 100644 --- a/swarm/storage/resource_test.go +++ b/swarm/storage/resource_test.go @@ -535,18 +535,17 @@ func setupENS(addr common.Address, transactOpts *bind.TransactOpts, sub string, return contractAddress, contractBackend, nil } -func signContent(privKey *ecdsa.PrivateKey, data []byte) ([signatureLength]byte, error) { +func signContent(privKey *ecdsa.PrivateKey, data []byte) (Signature, error) { hasher.Reset() hasher.Write(data) datahash := hasher.Sum(nil) - signature, err := crypto.Sign(datahash, privKey) + signaturebytes, err := crypto.Sign(datahash, privKey) if err != nil { return [signatureLength]byte{}, err } - var signaturetype [signatureLength]byte - copy(signaturetype[:], signature) - return signaturetype, nil + signature, err := NewSignature(signaturebytes) + return signature, err } type testCloudStore struct { From e1e867cdf9875e060aed1a5a5a91a0fb39ba1a4f Mon Sep 17 00:00:00 2001 From: lash Date: Thu, 18 Jan 2018 01:13:13 +0100 Subject: [PATCH 3/6] swarm/storage: Simplify code, correct content hashing --- swarm/storage/resource.go | 417 +++++++++++++++------------------ swarm/storage/resource_ens.go | 48 +++- swarm/storage/resource_test.go | 328 +++++++++++--------------- 3 files changed, 361 insertions(+), 432 deletions(-) diff --git a/swarm/storage/resource.go b/swarm/storage/resource.go index 1de6254545..04e8d06ac4 100644 --- a/swarm/storage/resource.go +++ b/swarm/storage/resource.go @@ -1,7 +1,6 @@ package storage import ( - "crypto/ecdsa" "encoding/binary" "fmt" "path/filepath" @@ -19,12 +18,18 @@ import ( const ( signatureLength = 65 - indexSize = 24 + indexSize = 16 + dbDirName = "resource" + chunkSize = 4096 // temporary until we implement DPA in the resourcehandler ) type Signature [signatureLength]byte -func NewSignature(b []byte) (Signature, error) { +var emptySignature Signature + +type SignFunc func(common.Hash) (Signature, error) + +func bytesToSignature(b []byte) (Signature, error) { var s Signature if len(b) != signatureLength { return [signatureLength]byte{}, fmt.Errorf("Must be %d bytes", signatureLength) @@ -46,6 +51,11 @@ type resource struct { updated time.Time } +// TODO Expire content after a defined period (to force resync) +func (r *resource) isSynced() bool { + return !r.updated.IsZero() +} + // Mutable resource is an entity which allows updates to a resource // without resorting to ENS on each update. // The update scheme is built on swarm chunks with chunk keys following @@ -104,8 +114,9 @@ type resource struct { // treated as a resource update chunk. type ResourceValidator interface { - isOwner(string, common.Address) (bool, error) + checkAccess(string, common.Address) (bool, error) nameHash(string) common.Hash + sign(common.Hash) (Signature, error) // SignFunc } type ResourceHandler struct { @@ -116,13 +127,15 @@ type ResourceHandler struct { hashLock sync.Mutex resourceLock sync.RWMutex hasher SwarmHash - maxChunkData int64 } // Create or open resource update chunk store -func NewResourceHandler(privKey *ecdsa.PrivateKey, datadir string, cloudStore CloudStore, rpcClient *rpc.Client, validator ResourceValidator) (*ResourceHandler, error) { - path := filepath.Join(datadir, "resource") - dbStore, err := NewDbStore(datadir, nil, singletonSwarmDbCapacity, 0) +func NewResourceHandler(datadir string, cloudStore CloudStore, rpcClient *rpc.Client, validator ResourceValidator) (*ResourceHandler, error) { + + hashfunc := MakeHashFunc(SHA3Hash) + + path := filepath.Join(datadir, dbDirName) + dbStore, err := NewDbStore(datadir, hashfunc, singletonSwarmDbCapacity, 0) if err != nil { return nil, err } @@ -130,14 +143,12 @@ func NewResourceHandler(privKey *ecdsa.PrivateKey, datadir string, cloudStore Cl memStore: NewMemStore(dbStore, singletonSwarmDbCapacity), DbStore: dbStore, } - hasher := MakeHashFunc("SHA3") rh := &ResourceHandler{ - ChunkStore: newResourceChunkStore(path, hasher, localStore, cloudStore), - rpcClient: rpcClient, - resources: make(map[string]*resource), - hasher: hasher(), - maxChunkData: DefaultBranches * int64(hasher().Size()), + ChunkStore: newResourceChunkStore(path, hashfunc, localStore, cloudStore), + rpcClient: rpcClient, + resources: make(map[string]*resource), + hasher: hashfunc(), } if validator != nil { @@ -149,72 +160,57 @@ func NewResourceHandler(privKey *ecdsa.PrivateKey, datadir string, cloudStore Cl rh.hasher.Reset() rh.hasher.Write([]byte(name)) return common.BytesToHash(rh.hasher.Sum(nil)) - }) + }, nil) } return rh, nil } -func validateInput(name string, frequency uint64) (string, error) { - // frequency 0 is invalid - if frequency == 0 { - return "", fmt.Errorf("Frequency cannot be 0") - } - - // must have name - if name == "" { - return "", fmt.Errorf("Name cannot be empty") - } - - // make sure our ens identifier is idna safe - validname, err := idna.ToASCII(name) - if err != nil { - return "", err - } - - return validname, nil -} - -// Creates a standalone resource object -// -// Can be passed to SetResource if external root data lookups are used -func NewResource(name string, startBlock uint64, frequency uint64, nameHashFunc func(name string) common.Hash) (*resource, error) { - - validname, err := validateInput(name, frequency) - if err != nil { - return nil, err - } - - return &resource{ - name: validname, - nameHash: nameHashFunc(validname), - startBlock: startBlock, - frequency: frequency, - }, nil +// \TODO should be hashsize * branches from the chosen chunker, implement with dpa +func (self *ResourceHandler) chunkSize() int64 { + return chunkSize } // Creates a new root entry for a mutable resource identified by `name` with the specified `frequency`. // +// The signature data should match the hash of the idna-converted name by the validator's namehash function, NOT the raw name bytes. +// // The start block of the resource update will be the actual current block height of the connected network. -func (self *ResourceHandler) NewResource(name string, frequency uint64, signature Signature) (*resource, error) { +func (self *ResourceHandler) NewResource(name string, frequency uint64, verify bool) (*resource, error) { - addr, err := self.getAddressFromDataSig([]byte(name), signature) - if err != nil { - return nil, fmt.Errorf("Corrupt signature") - } - ok, err := self.validator.isOwner(name, addr) - if err != nil { - return nil, err - } else if !ok { - return nil, fmt.Errorf("Not owner of '%s'", name) + // frequency 0 is invalid + if frequency == 0 { + return nil, fmt.Errorf("Frequency cannot be 0") } - validname, err := validateInput(name, frequency) + // must have name + if name == "" { + return nil, fmt.Errorf("Empty name") + } + + validName, err := toSafeName(name) if err != nil { return nil, err } - nameHash := self.validator.nameHash(validname) + nameHash := self.validator.nameHash(validName) + + if verify { + signature, err := self.validator.sign(nameHash) + if err != nil { + return nil, fmt.Errorf("Sign fail: %v", err) + } + addr, err := getAddressFromDataSig(nameHash, signature) + if err != nil { + return nil, fmt.Errorf("Retrieve address from signature fail: %v", err) + } + ok, err := self.validator.checkAccess(name, addr) + if err != nil { + return nil, err + } else if !ok { + return nil, fmt.Errorf("Not owner of '%s'", name) + } + } // get our blockheight at this time currentblock, err := self.getBlock() @@ -224,22 +220,19 @@ func (self *ResourceHandler) NewResource(name string, frequency uint64, signatur // chunk with key equal to namehash points to data of first blockheight + update frequency // from this we know from what blockheight we should look for updates, and how often - chunk := NewChunk(Key(nameHash[:]), nil) + chunk := NewChunk(Key(nameHash.Bytes()), nil) chunk.SData = make([]byte, indexSize) - // resource update root chunks follow same convention as "normal" chunks - // with 8 bytes prefix specifying size val := make([]byte, 8) - chunk.SData[0] = 16 // size, little-endian binary.LittleEndian.PutUint64(val, currentblock) - copy(chunk.SData[8:16], val) + copy(chunk.SData[:8], val) binary.LittleEndian.PutUint64(val, frequency) - copy(chunk.SData[16:], val) + copy(chunk.SData[8:], val) self.Put(chunk) - log.Debug("new resource", "name", validname, "key", nameHash, "startBlock", currentblock, "frequency", frequency) + log.Debug("new resource", "name", validName, "key", nameHash, "startBlock", currentblock, "frequency", frequency) rsrc := &resource{ - name: validname, + name: validName, nameHash: nameHash, startBlock: currentblock, frequency: frequency, @@ -250,42 +243,6 @@ func (self *ResourceHandler) NewResource(name string, frequency uint64, signatur return self.resources[name], nil } -// Set an externally defined resource object -// -// If the resource update root chunk is located externally (for example as a normal -// chunk looked up by ENS) the data would be manually added with this method). -// -// Method will fail if resource is already registered in this session, unless -// `allowOverwrite` is set -func (self *ResourceHandler) SetExternalResource(rsrc *resource, allowOverwrite bool) error { - - utfname, err := idna.ToUnicode(rsrc.name) - if err != nil { - return fmt.Errorf("Invalid IDNA rsrc name '%s'", rsrc.name) - } - if !allowOverwrite { - self.resourceLock.Lock() - _, ok := self.resources[utfname] - self.resourceLock.Unlock() - if ok { - return fmt.Errorf("Resource exists") - } - } - - // get our blockheight at this time - currentblock, err := self.getBlock() - if err != nil { - return err - } - - if rsrc.startBlock > currentblock { - return fmt.Errorf("Startblock cannot be higher than current block (%d > %d)", rsrc.startBlock, currentblock) - } - - self.resources[utfname] = rsrc - return nil -} - // Searches and retrieves the specific version of the resource update identified by `name` // at the specific block height // @@ -360,7 +317,7 @@ func (self *ResourceHandler) lookup(rsrc *resource, name string, period uint32, } for period > 0 { - key := self.resourceHash(rsrc.nameHash, period, version) + key := self.resourceHash(period, version, rsrc.nameHash) chunk, err := self.Get(key) if err == nil { if specificversion { @@ -370,7 +327,7 @@ func (self *ResourceHandler) lookup(rsrc *resource, name string, period uint32, log.Trace("rsrc update version 1 found, checking for version updates", "period", period, "key", key) for { newversion := version + 1 - key := self.resourceHash(rsrc.nameHash, period, newversion) + key := self.resourceHash(period, newversion, rsrc.nameHash) newchunk, err := self.Get(key) if err != nil { return self.updateResourceIndex(rsrc, chunk, &name) @@ -394,8 +351,8 @@ func (self *ResourceHandler) loadResource(name string, refresh bool) (*resource, rsrc := self.getResource(name) if rsrc == nil || refresh { rsrc = &resource{} - // make sure our ens identifier is idna safe - validname, err := idna.ToASCII(name) + // make sure our name is safe to use + validname, err := toSafeName(name) if err != nil { return nil, err } @@ -409,17 +366,11 @@ func (self *ResourceHandler) loadResource(name string, refresh bool) (*resource, } // sanity check for chunk data - // data is prefixed by 8 bytes of size - if len(chunk.SData) < indexSize { - return nil, fmt.Errorf("Invalid chunk length %d", len(chunk.SData)) - } else { - chunklength := binary.LittleEndian.Uint64(chunk.SData[:8]) - if chunklength != uint64(16) { - return nil, fmt.Errorf("Invalid chunk length header %d", chunklength) - } + if len(chunk.SData) != indexSize { + return nil, fmt.Errorf("Invalid chunk length %d, should be %d", len(chunk.SData), indexSize) } - rsrc.startBlock = binary.LittleEndian.Uint64(chunk.SData[8:16]) - rsrc.frequency = binary.LittleEndian.Uint64(chunk.SData[16:]) + rsrc.startBlock = binary.LittleEndian.Uint64(chunk.SData[:8]) + rsrc.frequency = binary.LittleEndian.Uint64(chunk.SData[8:]) } else { rsrc.name = self.resources[name].name rsrc.nameHash = self.resources[name].nameHash @@ -432,15 +383,21 @@ func (self *ResourceHandler) loadResource(name string, refresh bool) (*resource, // update mutable resource index map with specified content func (self *ResourceHandler) updateResourceIndex(rsrc *resource, chunk *Chunk, indexname *string) (*resource, error) { - // rsrc update data chunks are total hacks - // and have no size prefix :D - err := self.verifyContent(chunk.SData) - if err != nil { - return nil, err - } - // update our rsrcs entry map - period, version, _, data, err := parseUpdate(chunk.SData[signatureLength:]) + signature, period, version, name, data, err := parseUpdate(chunk.SData) + if rsrc.name != name { + return nil, fmt.Errorf("Update belongs to '%s', but have '%s'", name, rsrc.name) + } + self.hashLock.Lock() + self.hasher.Reset() + self.hasher.Write(chunk.Key[:]) + self.hasher.Write(data) + digest := self.hasher.Sum(nil) + self.hashLock.Unlock() + _, err = getAddressFromDataSig(common.BytesToHash(digest), signature) + if err != nil { + return nil, fmt.Errorf("Invalid signature: %v", err) + } rsrc.lastPeriod = period rsrc.version = version rsrc.updated = time.Now() @@ -451,22 +408,23 @@ func (self *ResourceHandler) updateResourceIndex(rsrc *resource, chunk *Chunk, i return rsrc, nil } -func parseUpdate(blob []byte) (period uint32, version uint32, ensname []byte, data []byte, err error) { - headerlength := binary.LittleEndian.Uint16(blob[:2]) - if int(headerlength+2) > len(blob) { - return 0, 0, nil, nil, fmt.Errorf("Reported header length %d longer than actual data length %d", headerlength, len(blob)) +func parseUpdate(chunkdata []byte) (signature Signature, period uint32, version uint32, name string, data []byte, err error) { + copy(signature[:], chunkdata[:signatureLength]) + cursor := signatureLength + headerlength := binary.LittleEndian.Uint16(chunkdata[cursor : cursor+2]) + if int(headerlength+2) > len(chunkdata) { + return emptySignature, 0, 0, "", nil, fmt.Errorf("Reported header length %d longer than actual data length %d", headerlength, len(chunkdata)) } - cursor := 2 - period = binary.LittleEndian.Uint32(blob[cursor : cursor+4]) + cursor += 2 + period = binary.LittleEndian.Uint32(chunkdata[cursor : cursor+4]) cursor += 4 - version = binary.LittleEndian.Uint32(blob[cursor : cursor+4]) + version = binary.LittleEndian.Uint32(chunkdata[cursor : cursor+4]) cursor += 4 - namelength := int(headerlength) - cursor + 2 - ensname = make([]byte, namelength) - copy(ensname, blob[cursor:]) + namelength := int(headerlength) - cursor + signatureLength + 2 + name = string(chunkdata[cursor : cursor+namelength]) cursor += namelength - data = make([]byte, len(blob)-cursor) - copy(data, blob[cursor:]) + data = make([]byte, len(chunkdata)-cursor) + copy(data, chunkdata[cursor:]) return } @@ -476,32 +434,18 @@ func parseUpdate(blob []byte) (period uint32, version uint32, ensname []byte, da // It is the caller's responsibility to make sure that this data is not stale. // // A resource update cannot span chunks, and thus has max length 4096 -func (self *ResourceHandler) Update(name string, data []byte, signature Signature) (Key, error) { - - addr, err := self.getAddressFromDataSig(data, signature) - if err != nil { - return nil, fmt.Errorf("Invalid data/signature: %v", err) - } - ok, err := self.validator.isOwner(name, addr) - if err != nil { - return nil, err - } else if !ok { - return nil, fmt.Errorf("Not owner of '%s'", name) - } - - // can be only one chunk long minus 65 byte signature - if int64(len(data)) > self.maxChunkData { - return nil, fmt.Errorf("Data overflow: %d / %d bytes", len(data), 4096-signatureLength) - } +func (self *ResourceHandler) Update(indexname string, data []byte) (Key, error) { // get the cached information - self.resourceLock.Lock() - defer self.resourceLock.Unlock() - resource, ok := self.resources[name] - if !ok { - return nil, fmt.Errorf("No such resource") - } else if resource.updated.IsZero() { - return nil, fmt.Errorf("Invalid resource") + rsrc := self.getResource(indexname) + if !rsrc.isSynced() { + return nil, fmt.Errorf("Resource object not in sync") + } + + // an update can be only one chunk long + datalimit := self.chunkSize() - int64(signatureLength-len(self.resources[indexname].name)-8) + if int64(len(data)) > datalimit { + return nil, fmt.Errorf("Data overflow: %d / %d bytes", len(data), datalimit) } // get our blockheight at this time and the next block of the update period @@ -509,21 +453,59 @@ func (self *ResourceHandler) Update(name string, data []byte, signature Signatur if err != nil { return nil, err } - nextperiod := getNextPeriod(resource.startBlock, currentblock, resource.frequency) + nextperiod := getNextPeriod(rsrc.startBlock, currentblock, rsrc.frequency) // if we already have an update for this block then increment version + // (resource object MUST be in sync for version to be correct) var version uint32 - if self.hasUpdate(name, nextperiod) { - version = resource.version + if self.hasUpdate(indexname, nextperiod) { + version = rsrc.version } version++ + // calculate the chunk key + key := self.resourceHash(nextperiod, version, rsrc.nameHash) + + // sign the data hash with the key + digest := self.keyDataHash(key, data) + signature, err := self.validator.sign(digest) + if err != nil { + return nil, err + } + + // get the address of the signer (which also checks that it's a valid signature) + addr, err := getAddressFromDataSig(digest, signature) + if err != nil { + return nil, fmt.Errorf("Invalid data/signature: %v", err) + } + + // check if the signer has access to update + ok, err := self.validator.checkAccess(indexname, addr) + if err != nil { + return nil, err + } else if !ok { + return nil, fmt.Errorf("Address %x does not have access to update %s", addr, indexname) + } + + chunk := newUpdateChunk(key, signature, nextperiod, version, self.resources[indexname].name, data) + + // send the chunk + self.Put(chunk) + log.Trace("resource update", "name", rsrc.name, "key", key, "currentblock", currentblock, "lastperiod", nextperiod, "version", version, "data", chunk.SData) + + // update our resources map entry and return the new key + rsrc.lastPeriod = nextperiod + rsrc.version = version + rsrc.data = make([]byte, len(data)) + copy(rsrc.data, data) + return key, nil +} + +func newUpdateChunk(key Key, signature Signature, period uint32, version uint32, name string, data []byte) *Chunk { // create the update chunk // prepend version and period to allow reverse lookups - // data header length does NOT include the header length prefix bytes themselves - headerlength := uint16(len(resource.nameHash) + 4 + 4) + headerlength := uint16(len(name) + 4 + 4) - key := self.resourceHash(resource.nameHash, nextperiod, version) chunk := NewChunk(key, nil) chunk.SData = make([]byte, signatureLength+int(headerlength)+2+len(data)) @@ -531,32 +513,24 @@ func (self *ResourceHandler) Update(name string, data []byte, signature Signatur copy(chunk.SData, signature[:]) cursor += signatureLength + // data header length does NOT include the header length prefix bytes themselves binary.LittleEndian.PutUint16(chunk.SData[cursor:], headerlength) cursor += 2 - binary.LittleEndian.PutUint32(chunk.SData[cursor:], nextperiod) + binary.LittleEndian.PutUint32(chunk.SData[cursor:], period) cursor += 4 binary.LittleEndian.PutUint32(chunk.SData[cursor:], version) cursor += 4 - copy(chunk.SData[cursor:], resource.nameHash[:]) - cursor += len(resource.nameHash) + namebytes := []byte(name) + copy(chunk.SData[cursor:], namebytes) + cursor += len(namebytes) copy(chunk.SData[cursor:], data) chunk.Size = int64(len(chunk.SData)) - - // send the chunk - self.Put(chunk) - log.Trace("resource update", "name", resource.name, "key", key, "currentblock", currentblock, "lastperiod", nextperiod, "version", version, "data", chunk.SData) - - // update our resources map entry and return the new key - resource.lastPeriod = nextperiod - resource.version = version - resource.data = make([]byte, len(data)) - copy(resource.data, data) - return key, nil + return chunk } // Closes the datastore. @@ -599,80 +573,37 @@ func (self *ResourceHandler) setResource(name string, rsrc *resource) { self.resources[name] = rsrc } -func (self *ResourceHandler) resourceHash(namehash common.Hash, period uint32, version uint32) Key { +func (self *ResourceHandler) resourceHash(period uint32, version uint32, namehash common.Hash) Key { // format is: hash(namehash|period|version) self.hashLock.Lock() defer self.hashLock.Unlock() self.hasher.Reset() - self.hasher.Write(namehash[:]) b := make([]byte, 4) binary.LittleEndian.PutUint32(b, period) self.hasher.Write(b) binary.LittleEndian.PutUint32(b, version) self.hasher.Write(b) + self.hasher.Write(namehash[:]) return self.hasher.Sum(nil) } -func (self *ResourceHandler) getContentAccount(chunkdata []byte) (common.Address, error) { - if len(chunkdata) <= signatureLength { - return common.Address{}, fmt.Errorf("zero-length data") - } - signature, err := NewSignature(chunkdata[:signatureLength]) - if err != nil { - return zeroAddr, err - } - return self.getAddressFromDataSig(chunkdata[signatureLength:], signature) -} - -func (self *ResourceHandler) getContentName(chunkdata []byte) (string, error) { - nameoffset := signatureLength + 2 + 4 + 4 - if len(chunkdata) < nameoffset { - return "", fmt.Errorf("invalid chunk data") - } - namelength := binary.LittleEndian.Uint16(chunkdata[signatureLength : signatureLength+2]) - namebytes := make([]byte, namelength) - copy(namebytes, chunkdata[nameoffset:nameoffset+int(namelength)-2-4-4]) - return string(namebytes), nil -} - -func (self *ResourceHandler) getAddressFromDataSig(data []byte, signature Signature) (common.Address, error) { - self.hashLock.Lock() - self.hasher.Reset() - self.hasher.Write(data) - datahash := self.hasher.Sum(nil) - self.hashLock.Unlock() - pub, err := crypto.SigToPub(datahash, signature[:]) +func getAddressFromDataSig(datahash common.Hash, signature Signature) (common.Address, error) { + pub, err := crypto.SigToPub(datahash.Bytes(), signature[:]) if err != nil { return common.Address{}, err } return crypto.PubkeyToAddress(*pub), nil } -func (self *ResourceHandler) verifyContent(chunkdata []byte) error { - address, err := self.getContentAccount(chunkdata) - if err != nil { - return err - } - name, err := self.getContentName(chunkdata) - if err != nil { - return err - } - ok, err := self.validator.isOwner(name, address) - if err != nil { - return err - } else if !ok { - return fmt.Errorf("not owner") - } - return nil -} - func (self *ResourceHandler) hasUpdate(name string, period uint32) bool { return self.resources[name].lastPeriod == period } +// \TODO chunkSize is a workaround until the ChunkStore interface exports a method to get the chunk size directly type resourceChunkStore struct { localStore ChunkStore netStore ChunkStore + chunkSize int64 } func newResourceChunkStore(path string, hasher SwarmHasher, localStore *LocalStore, cloudStore CloudStore) *resourceChunkStore { @@ -717,3 +648,21 @@ func getNextPeriod(start uint64, current uint64, frequency uint64) uint32 { period := blockdiff / frequency return uint32(period + 1) } + +func toSafeName(name string) (string, error) { + // make sure our ens identifier is idna safe + validname, err := idna.ToASCII(name) + if err != nil { + return "", err + } + return validname, nil +} + +func (self *ResourceHandler) keyDataHash(key Key, data []byte) common.Hash { + self.hashLock.Lock() + defer self.hashLock.Unlock() + self.hasher.Reset() + self.hasher.Write(key[:]) + self.hasher.Write(data) + return common.BytesToHash(self.hasher.Sum(nil)) +} diff --git a/swarm/storage/resource_ens.go b/swarm/storage/resource_ens.go index 8a742e2716..ccd4ed5344 100644 --- a/swarm/storage/resource_ens.go +++ b/swarm/storage/resource_ens.go @@ -1,27 +1,47 @@ package storage import ( + "fmt" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/contracts/ens" ) -// ENS validation of mutable resource owners -type ENSValidator struct { - api *ens.ENS +type baseValidator struct { + signFunc SignFunc } -func NewENSValidator(contractaddress common.Address, backend bind.ContractBackend, transactOpts *bind.TransactOpts) (*ENSValidator, error) { +func (b *baseValidator) sign(datahash common.Hash) (Signature, error) { + if b.signFunc == nil { + return emptySignature, fmt.Errorf("No signature function") + } + return b.signFunc(datahash) +} + +// ENS validation of mutable resource owners +type ENSValidator struct { + *baseValidator + api *ens.ENS + hashlength int +} + +func NewENSValidator(contractaddress common.Address, backend bind.ContractBackend, transactOpts *bind.TransactOpts, signFunc SignFunc) (*ENSValidator, error) { var err error - validator := &ENSValidator{} + validator := &ENSValidator{ + baseValidator: &baseValidator{ + signFunc: signFunc, + }, + } validator.api, err = ens.NewENS(transactOpts, contractaddress, backend) if err != nil { return nil, err } + validator.hashlength = len(ens.EnsNode(dbDirName).Bytes()) return validator, nil } -func (self *ENSValidator) isOwner(name string, address common.Address) (bool, error) { +func (self *ENSValidator) checkAccess(name string, address common.Address) (bool, error) { owneraddr, err := self.api.Owner(self.nameHash(name)) if err != nil { return false, err @@ -35,15 +55,23 @@ func (self *ENSValidator) nameHash(name string) common.Hash { // Default fallthrough validation of mutable resource ownership type GenericValidator struct { - hashFunc func(string) common.Hash + *baseValidator + hashFunc func(string) common.Hash + hashlength int } -func NewGenericValidator(hashFunc func(string) common.Hash) *GenericValidator { +func NewGenericValidator(hashFunc func(string) common.Hash, signFunc SignFunc) *GenericValidator { return &GenericValidator{ - hashFunc: hashFunc, + baseValidator: &baseValidator{ + signFunc: signFunc, + }, + hashFunc: hashFunc, + hashlength: len(hashFunc(dbDirName).Bytes()), } + } -func (self *GenericValidator) isOwner(name string, address common.Address) (bool, error) { + +func (self *GenericValidator) checkAccess(name string, address common.Address) (bool, error) { return true, nil } diff --git a/swarm/storage/resource_test.go b/swarm/storage/resource_test.go index a8b801b2f1..2a4f4ca4d5 100644 --- a/swarm/storage/resource_test.go +++ b/swarm/storage/resource_test.go @@ -29,7 +29,7 @@ import ( ) var ( - hasher = MakeHashFunc("SHA3")() + testHasher = MakeHashFunc(SHA3Hash)() zeroAddr = common.Address{} startBlock = uint64(4200) resourceFrequency = uint64(42) @@ -67,14 +67,8 @@ func (r *FakeRPC) BlockNumber() (string, error) { // check that signature address matches update signer address func TestResourceSignature(t *testing.T) { - // privkey for signing updates - privkey, err := crypto.GenerateKey() - if err != nil { - return - } - // set up rpc and create resourcehandler - rh, _, err, teardownTest := setupTest(privkey, nil, nil) + rh, _, signer, teardownTest, err := setupTest(nil, nil) if err != nil { teardownTest(t, err) } @@ -86,8 +80,7 @@ func TestResourceSignature(t *testing.T) { } // generate a hash for block 4200 version 1 - key := rh.resourceHash(ens.EnsNode(validname), 1, 1) - chunk := NewChunk(key, nil) + key := rh.resourceHash(1, 1, rh.validator.nameHash(validname)) // generate some bogus data for the chunk and sign it data := make([]byte, 8) @@ -95,26 +88,26 @@ func TestResourceSignature(t *testing.T) { if err != nil { teardownTest(t, err) } - hasher.Reset() - hasher.Write(data) - datahash := hasher.Sum(nil) - sig, err := crypto.Sign(datahash, privkey) + testHasher.Reset() + testHasher.Write(data) + digest := rh.keyDataHash(key, data) + sig, err := rh.validator.sign(digest) if err != nil { teardownTest(t, err) } - // put sig and data in the chunk - chunk.SData = make([]byte, 8+signatureLength) - copy(chunk.SData[:signatureLength], sig) - copy(chunk.SData[signatureLength:], data) + chunk := newUpdateChunk(key, sig, 1, 1, validname, data) + + log.Warn("key", "chunk", chunk.Key, "real", key) // check that we can recover the owner account from the update chunk's signature - // TODO: change this to verifyContent on ENS integration - recoveredaddress, err := rh.getContentAccount(chunk.SData) + checksig, _, _, _, newdata, err := parseUpdate(chunk.SData) + checkdigest := rh.keyDataHash(chunk.Key, newdata) + recoveredaddress, err := getAddressFromDataSig(checkdigest, checksig) if err != nil { teardownTest(t, err) } - originaladdress := crypto.PubkeyToAddress(privkey.PublicKey) + originaladdress := crypto.PubkeyToAddress(signer.privKey.PublicKey) if recoveredaddress != originaladdress { teardownTest(t, fmt.Errorf("addresses dont match: %x != %x", originaladdress, recoveredaddress)) @@ -122,90 +115,69 @@ func TestResourceSignature(t *testing.T) { teardownTest(t, nil) } -// determine resource update metadata from chunk data -func TestResourceReverseLookup(t *testing.T) { - - // privkey for signing updates - privkey, err := crypto.GenerateKey() - if err != nil { - return - } - - // make fake backend, set up rpc and create resourcehandler - backend := &fakeBackend{ - blocknumber: startBlock, - } - rh, _, err, teardownTest := setupTest(privkey, backend, nil) - if err != nil { - teardownTest(t, err) - } - - // create a new resource - signature, err := signContent(privkey, []byte(domainName)) - if err != nil { - teardownTest(t, err) - } - - rsrc, err := rh.NewResource(domainName, resourceFrequency, signature) - if err != nil { - teardownTest(t, err) - } - - // update data - fwdBlocks(int(resourceFrequency+1), backend) - data := []byte("foo") - signature, err = signContent(privkey, data) - if err != nil { - teardownTest(t, err) - } - - resourcekey, err := rh.Update(domainName, data, signature) - if err != nil { - teardownTest(t, err) - } - chunk, err := rh.ChunkStore.(*resourceChunkStore).localStore.(*LocalStore).memStore.Get(Key(resourcekey)) - if err != nil { - teardownTest(t, err) - } - - // check if data after header length offset is as expected - headerlength := binary.LittleEndian.Uint16(chunk.SData[signatureLength : signatureLength+2]) - if !bytes.Equal(chunk.SData[signatureLength+headerlength+2:], data) { - teardownTest(t, fmt.Errorf("Expected chunk data with header length %d (pos %d) to match %x, but was %x", headerlength, signatureLength+headerlength+2, data, chunk.SData[signatureLength+headerlength+2:])) - } - - // get name, period, version from chunk and check - revperiod, revversion, revname, revdata, err := parseUpdate(chunk.SData[signatureLength:]) - - if !bytes.Equal(revname, rsrc.nameHash.Bytes()) { - teardownTest(t, fmt.Errorf("Expected retrieved name from chunk data to be '%x', was '%x'", rsrc.nameHash.Bytes(), revname)) - } - if !bytes.Equal(revdata, data) { - teardownTest(t, fmt.Errorf("Expected retrieved data from chunk data to be '%x', was '%x'", data, revdata)) - } - - if revperiod != 2 { - teardownTest(t, fmt.Errorf("Expected retrieved period from chunk data to be 1, was %d", revperiod)) - } - if revversion != 1 { - teardownTest(t, fmt.Errorf("Expected retrieved version from chunk data to be 1, was %d", revversion)) - } -} - +// +//// determine resource update metadata from chunk data +//func TestResourceReverseLookup(t *testing.T) { +// +// // make fake backend, set up rpc and create resourcehandler +// backend := &fakeBackend{ +// blocknumber: startBlock, +// } +// rh, _, _, teardownTest, err := setupTest(backend, nil) +// if err != nil { +// teardownTest(t, err) +// } +// +// rsrc, err := rh.NewResource(domainName, resourceFrequency, false) +// if err != nil { +// teardownTest(t, err) +// } +// +// // update data +// fwdBlocks(int(resourceFrequency+1), backend) +// data := []byte("foo") +// resourcekey, err := rh.Update(domainName, data) +// if err != nil { +// teardownTest(t, err) +// } +// chunk, err := rh.ChunkStore.(*resourceChunkStore).localStore.(*LocalStore).memStore.Get(Key(resourcekey)) +// if err != nil { +// teardownTest(t, err) +// } +// +// // check if data after header length offset is as expected +// headerlength := binary.LittleEndian.Uint16(chunk.SData[signatureLength : signatureLength+2]) +// if !bytes.Equal(chunk.SData[signatureLength+headerlength+2:], data) { +// teardownTest(t, fmt.Errorf("Expected chunk data with header length %d (pos %d) to match %x, but was %x", headerlength, signatureLength+headerlength+2, data, chunk.SData[signatureLength+headerlength+2:])) +// } +// +// // get name, period, version from chunk and check +// _, revperiod, revversion, revname, revdata, err := parseUpdate(chunk.SData[signatureLength:]) +// +// //if !bytes.Equal(revname, rsrc.nameHash.Bytes()) { +// if revname == rsrc.name { +// teardownTest(t, fmt.Errorf("Expected retrieved name from chunk data to be '%x', was '%x'", rsrc.nameHash.Bytes(), revname)) +// } +// if !bytes.Equal(revdata, data) { +// teardownTest(t, fmt.Errorf("Expected retrieved data from chunk data to be '%x', was '%x'", data, revdata)) +// } +// +// if revperiod != 2 { +// teardownTest(t, fmt.Errorf("Expected retrieved period from chunk data to be 1, was %d", revperiod)) +// } +// if revversion != 1 { +// teardownTest(t, fmt.Errorf("Expected retrieved version from chunk data to be 1, was %d", revversion)) +// } +//} +// // make updates and retrieve them based on periods and versions func TestResourceHandler(t *testing.T) { - // privkey for signing updates - privkey, err := crypto.GenerateKey() - if err != nil { - return - } - // make fake backend, set up rpc and create resourcehandler backend := &fakeBackend{ blocknumber: startBlock, } - rh, datadir, err, teardownTest := setupTest(privkey, backend, nil) + rh, datadir, _, teardownTest, err := setupTest(backend, nil) if err != nil { teardownTest(t, err) } @@ -215,8 +187,7 @@ func TestResourceHandler(t *testing.T) { if err != nil { teardownTest(t, err) } - signature, err := signContent(privkey, []byte(resourcevalidname)) - _, err = rh.NewResource(domainName, resourceFrequency, signature) + _, err = rh.NewResource(domainName, resourceFrequency, false) if err != nil { teardownTest(t, err) } @@ -229,8 +200,8 @@ func TestResourceHandler(t *testing.T) { } else if len(chunk.SData) < 16 { teardownTest(t, fmt.Errorf("chunk data must be minimum 16 bytes, is %d", len(chunk.SData))) } - startblocknumber := binary.LittleEndian.Uint64(chunk.SData[8:16]) - chunkfrequency := binary.LittleEndian.Uint64(chunk.SData[16:]) + startblocknumber := binary.LittleEndian.Uint64(chunk.SData[:8]) + chunkfrequency := binary.LittleEndian.Uint64(chunk.SData[8:]) if startblocknumber != backend.blocknumber { teardownTest(t, fmt.Errorf("stored block number %d does not match provided block number %d", startblocknumber, backend.blocknumber)) } @@ -242,11 +213,7 @@ func TestResourceHandler(t *testing.T) { resourcekey := make(map[string]Key) fwdBlocks(int(resourceFrequency/2), backend) data := []byte("blinky") - signature, err = signContent(privkey, data) - if err != nil { - teardownTest(t, err) - } - resourcekey["blinky"], err = rh.Update(domainName, data, signature) + resourcekey["blinky"], err = rh.Update(domainName, data) if err != nil { teardownTest(t, err) } @@ -254,11 +221,7 @@ func TestResourceHandler(t *testing.T) { // update on first period fwdBlocks(int(resourceFrequency/2), backend) data = []byte("pinky") - signature, err = signContent(privkey, data) - if err != nil { - teardownTest(t, err) - } - resourcekey["pinky"], err = rh.Update(domainName, data, signature) + resourcekey["pinky"], err = rh.Update(domainName, data) if err != nil { teardownTest(t, err) } @@ -266,11 +229,7 @@ func TestResourceHandler(t *testing.T) { // update on second period fwdBlocks(int(resourceFrequency), backend) data = []byte("inky") - signature, err = signContent(privkey, data) - if err != nil { - teardownTest(t, err) - } - resourcekey["inky"], err = rh.Update(domainName, data, signature) + resourcekey["inky"], err = rh.Update(domainName, data) if err != nil { teardownTest(t, err) } @@ -278,11 +237,7 @@ func TestResourceHandler(t *testing.T) { // update just after second period fwdBlocks(1, backend) data = []byte("clyde") - signature, err = signContent(privkey, data) - if err != nil { - teardownTest(t, err) - } - resourcekey["clyde"], err = rh.Update(domainName, data, signature) + resourcekey["clyde"], err = rh.Update(domainName, data) if err != nil { teardownTest(t, err) } @@ -293,7 +248,7 @@ func TestResourceHandler(t *testing.T) { // it will match on second iteration startblocknumber + (resourceFrequency * 3) fwdBlocks(int(resourceFrequency*2)-1, backend) - rh2, err := NewResourceHandler(privkey, datadir, &testCloudStore{}, rh.rpcClient, nil) + rh2, err := NewResourceHandler(datadir, &testCloudStore{}, rh.rpcClient, nil) _, err = rh2.LookupLatest(domainName, true) if err != nil { teardownTest(t, err) @@ -310,45 +265,25 @@ func TestResourceHandler(t *testing.T) { teardownTest(t, fmt.Errorf("resource period was %d, expected 3", rh2.resources[domainName].lastPeriod)) } - rsrc, err := NewResource(domainName, startblocknumber, resourceFrequency, rh2.validator.nameHash) - if err != nil { - teardownTest(t, err) - } - err = rh2.SetExternalResource(rsrc, true) - if err != nil { - teardownTest(t, err) - } - - // latest block, latest version - resource, err := rh2.LookupLatest(domainName, false) // if key is specified, refresh is implicit - if err != nil { - teardownTest(t, err) - } - - // check data - if !bytes.Equal(resource.data, []byte("clyde")) { - teardownTest(t, fmt.Errorf("resource data (latest) was %v, expected %v", rh2.resources[domainName].data, []byte("clyde"))) - } - // specific block, latest version - resource, err = rh2.LookupHistorical(domainName, 3, true) + rsrc, err := rh2.LookupHistorical(domainName, 3, true) if err != nil { teardownTest(t, err) } // check data - if !bytes.Equal(resource.data, []byte("clyde")) { + if !bytes.Equal(rsrc.data, []byte("clyde")) { teardownTest(t, fmt.Errorf("resource data (historical) was %v, expected %v", rh2.resources[domainName].data, []byte("clyde"))) } // specific block, specific version - resource, err = rh2.LookupVersion(domainName, 3, 1, true) + rsrc, err = rh2.LookupVersion(domainName, 3, 1, true) if err != nil { teardownTest(t, err) } // check data - if !bytes.Equal(resource.data, []byte("inky")) { + if !bytes.Equal(rsrc.data, []byte("inky")) { teardownTest(t, fmt.Errorf("resource data (historical) was %v, expected %v", rh2.resources[domainName].data, []byte("inky"))) } teardownTest(t, nil) @@ -358,21 +293,15 @@ func TestResourceHandler(t *testing.T) { // create ENS enabled resource update, with and without valid owner func TestResourceENSOwner(t *testing.T) { - // privkey for signing updates - privkey, err := crypto.GenerateKey() + // signer containing private key + signer, err := newTestSigner() if err != nil { - return - } - - // privkey for checking wrong owner - privkeytwo, err := crypto.GenerateKey() - if err != nil { - return + t.Fatal(err) } // ens address and transact options - addr := crypto.PubkeyToAddress(privkey.PublicKey) - transactOpts := bind.NewKeyedTransactor(privkey) + addr := crypto.PubkeyToAddress(signer.privKey.PublicKey) + transactOpts := bind.NewKeyedTransactor(signer.privKey) // set up ENS sim domainparts := strings.Split(domainName, ".") @@ -381,40 +310,37 @@ func TestResourceENSOwner(t *testing.T) { t.Fatal(err) } - validator, err := NewENSValidator(contractAddr, contractbackend, transactOpts) + validator, err := NewENSValidator(contractAddr, contractbackend, transactOpts, signer.signContent) if err != nil { t.Fatal(err) } // set up rpc and create resourcehandler with ENS sim backend - rh, _, err, teardownTest := setupTest(privkey, contractbackend, validator) + rh, _, _, teardownTest, err := setupTest(contractbackend, validator) if err != nil { teardownTest(t, err) } - signature, err := signContent(privkey, []byte(domainName)) - if err != nil { - teardownTest(t, err) - } // create new resource when we are owner = ok - _, err = rh.NewResource(domainName, 42, signature) + _, err = rh.NewResource(domainName, resourceFrequency, true) if err != nil { teardownTest(t, fmt.Errorf("Create resource fail: %v", err)) } data := []byte("foo") - signature, err = signContent(privkey, data) - // update resource when we are owner = ok - _, err = rh.Update(domainName, data, signature) + _, err = rh.Update(domainName, data) if err != nil { teardownTest(t, fmt.Errorf("Update resource fail: %v", err)) } - // create new resource when we are NOT owner = !ok - signaturetwo, err := signContent(privkeytwo, data) // update resource when we are owner = ok - _, err = rh.Update(domainName, data, signaturetwo) + signertwo, err := newTestSigner() + if err != nil { + teardownTest(t, err) + } + rh.validator.(*ENSValidator).signFunc = signertwo.signContent + _, err = rh.Update(domainName, data) if err == nil { teardownTest(t, fmt.Errorf("Expected resource update fail due to owner mismatch")) } @@ -430,7 +356,7 @@ func fwdBlocks(count int, backend *fakeBackend) { } // create rpc and resourcehandler -func setupTest(privkey *ecdsa.PrivateKey, contractbackend bind.ContractBackend, validator ResourceValidator) (rh *ResourceHandler, datadir string, err error, teardown func(*testing.T, error)) { +func setupTest(contractbackend bind.ContractBackend, validator ResourceValidator) (rh *ResourceHandler, datadir string, signer *testSigner, teardown func(*testing.T, error), err error) { var fsClean func() var rpcClean func() @@ -443,6 +369,15 @@ func setupTest(privkey *ecdsa.PrivateKey, contractbackend bind.ContractBackend, } } + if validator == nil { + // create a new signer, which creates the private key + signer, err = newTestSigner() + if err != nil { + return + } + validator = NewGenericValidator(testHashFunc, signer.signContent) + } + // temp datadir datadir, err = ioutil.TempDir("", "rh") if err != nil { @@ -480,8 +415,7 @@ func setupTest(privkey *ecdsa.PrivateKey, contractbackend bind.ContractBackend, return } - // choose if with ens or not - rh, err = NewResourceHandler(privkey, datadir, &testCloudStore{}, rpcclient, validator) + rh, err = NewResourceHandler(datadir, &testCloudStore{}, rpcclient, validator) teardown = func(t *testing.T, err error) { cleanF() if err != nil { @@ -499,12 +433,12 @@ func setupENS(addr common.Address, transactOpts *bind.TransactOpts, sub string, var tophash [32]byte var subhash [32]byte - hasher.Reset() - hasher.Write([]byte(top)) - copy(tophash[:], hasher.Sum(nil)) - hasher.Reset() - hasher.Write([]byte(sub)) - copy(subhash[:], hasher.Sum(nil)) + testHasher.Reset() + testHasher.Write([]byte(top)) + copy(tophash[:], testHasher.Sum(nil)) + testHasher.Reset() + testHasher.Write([]byte(sub)) + copy(subhash[:], testHasher.Sum(nil)) // initialize contract backend and deploy contractBackend := &fakeBackend{ @@ -535,17 +469,35 @@ func setupENS(addr common.Address, transactOpts *bind.TransactOpts, sub string, return contractAddress, contractBackend, nil } -func signContent(privKey *ecdsa.PrivateKey, data []byte) (Signature, error) { - hasher.Reset() - hasher.Write(data) - datahash := hasher.Sum(nil) +func testHashFunc(name string) common.Hash { + testHasher.Reset() + testHasher.Write([]byte(name)) + return common.BytesToHash(testHasher.Sum(nil)) +} - signaturebytes, err := crypto.Sign(datahash, privKey) +type testSigner struct { + privKey *ecdsa.PrivateKey + hasher SwarmHash +} + +func newTestSigner() (*testSigner, error) { + privKey, err := crypto.GenerateKey() if err != nil { - return [signatureLength]byte{}, err + return nil, err } - signature, err := NewSignature(signaturebytes) - return signature, err + return &testSigner{ + privKey: privKey, + hasher: testHasher, + }, nil +} + +func (self *testSigner) signContent(data common.Hash) (signature Signature, err error) { + signaturebytes, err := crypto.Sign(data.Bytes(), self.privKey) + if err != nil { + return + } + signature, err = bytesToSignature(signaturebytes) + return } type testCloudStore struct { From c051bec50536390d4ed304644fdae1ad93b4f73a Mon Sep 17 00:00:00 2001 From: lash Date: Thu, 18 Jan 2018 01:35:26 +0100 Subject: [PATCH 4/6] swarm/storage: Add test for reverse metadata retrieval --- swarm/storage/resource.go | 9 ---- swarm/storage/resource_test.go | 88 ++++++++++------------------------ 2 files changed, 25 insertions(+), 72 deletions(-) diff --git a/swarm/storage/resource.go b/swarm/storage/resource.go index 04e8d06ac4..51010d7ca4 100644 --- a/swarm/storage/resource.go +++ b/swarm/storage/resource.go @@ -29,15 +29,6 @@ var emptySignature Signature type SignFunc func(common.Hash) (Signature, error) -func bytesToSignature(b []byte) (Signature, error) { - var s Signature - if len(b) != signatureLength { - return [signatureLength]byte{}, fmt.Errorf("Must be %d bytes", signatureLength) - } - copy(s[:], b) - return s, nil -} - // Encapsulates an actual resource update. When synced it contains the most recent // version of the resource update data. type resource struct { diff --git a/swarm/storage/resource_test.go b/swarm/storage/resource_test.go index 2a4f4ca4d5..f645522054 100644 --- a/swarm/storage/resource_test.go +++ b/swarm/storage/resource_test.go @@ -65,7 +65,10 @@ func (r *FakeRPC) BlockNumber() (string, error) { } // check that signature address matches update signer address -func TestResourceSignature(t *testing.T) { +func TestResourceReverse(t *testing.T) { + + period := uint32(4) + version := uint32(2) // set up rpc and create resourcehandler rh, _, signer, teardownTest, err := setupTest(nil, nil) @@ -80,7 +83,7 @@ func TestResourceSignature(t *testing.T) { } // generate a hash for block 4200 version 1 - key := rh.resourceHash(1, 1, rh.validator.nameHash(validname)) + key := rh.resourceHash(period, version, rh.validator.nameHash(validname)) // generate some bogus data for the chunk and sign it data := make([]byte, 8) @@ -96,13 +99,11 @@ func TestResourceSignature(t *testing.T) { teardownTest(t, err) } - chunk := newUpdateChunk(key, sig, 1, 1, validname, data) - - log.Warn("key", "chunk", chunk.Key, "real", key) + chunk := newUpdateChunk(key, sig, period, version, validname, data) // check that we can recover the owner account from the update chunk's signature - checksig, _, _, _, newdata, err := parseUpdate(chunk.SData) - checkdigest := rh.keyDataHash(chunk.Key, newdata) + checksig, checkperiod, checkversion, checkname, checkdata, err := parseUpdate(chunk.SData) + checkdigest := rh.keyDataHash(chunk.Key, checkdata) recoveredaddress, err := getAddressFromDataSig(checkdigest, checksig) if err != nil { teardownTest(t, err) @@ -112,64 +113,25 @@ func TestResourceSignature(t *testing.T) { if recoveredaddress != originaladdress { teardownTest(t, fmt.Errorf("addresses dont match: %x != %x", originaladdress, recoveredaddress)) } + + if !bytes.Equal(key[:], chunk.Key[:]) { + teardownTest(t, fmt.Errorf("Expected chunk key '%x', was '%x'", key, chunk.Key)) + } + if period != checkperiod { + teardownTest(t, fmt.Errorf("Expected period '%d', was '%d'", period, checkperiod)) + } + if version != checkversion { + teardownTest(t, fmt.Errorf("Expected version '%d', was '%d'", version, checkversion)) + } + if validname != checkname { + teardownTest(t, fmt.Errorf("Expected name '%s', was '%s'", validname, checkname)) + } + if !bytes.Equal(data, checkdata) { + teardownTest(t, fmt.Errorf("Expectedn data '%x', was '%x'", data, checkdata)) + } teardownTest(t, nil) } -// -//// determine resource update metadata from chunk data -//func TestResourceReverseLookup(t *testing.T) { -// -// // make fake backend, set up rpc and create resourcehandler -// backend := &fakeBackend{ -// blocknumber: startBlock, -// } -// rh, _, _, teardownTest, err := setupTest(backend, nil) -// if err != nil { -// teardownTest(t, err) -// } -// -// rsrc, err := rh.NewResource(domainName, resourceFrequency, false) -// if err != nil { -// teardownTest(t, err) -// } -// -// // update data -// fwdBlocks(int(resourceFrequency+1), backend) -// data := []byte("foo") -// resourcekey, err := rh.Update(domainName, data) -// if err != nil { -// teardownTest(t, err) -// } -// chunk, err := rh.ChunkStore.(*resourceChunkStore).localStore.(*LocalStore).memStore.Get(Key(resourcekey)) -// if err != nil { -// teardownTest(t, err) -// } -// -// // check if data after header length offset is as expected -// headerlength := binary.LittleEndian.Uint16(chunk.SData[signatureLength : signatureLength+2]) -// if !bytes.Equal(chunk.SData[signatureLength+headerlength+2:], data) { -// teardownTest(t, fmt.Errorf("Expected chunk data with header length %d (pos %d) to match %x, but was %x", headerlength, signatureLength+headerlength+2, data, chunk.SData[signatureLength+headerlength+2:])) -// } -// -// // get name, period, version from chunk and check -// _, revperiod, revversion, revname, revdata, err := parseUpdate(chunk.SData[signatureLength:]) -// -// //if !bytes.Equal(revname, rsrc.nameHash.Bytes()) { -// if revname == rsrc.name { -// teardownTest(t, fmt.Errorf("Expected retrieved name from chunk data to be '%x', was '%x'", rsrc.nameHash.Bytes(), revname)) -// } -// if !bytes.Equal(revdata, data) { -// teardownTest(t, fmt.Errorf("Expected retrieved data from chunk data to be '%x', was '%x'", data, revdata)) -// } -// -// if revperiod != 2 { -// teardownTest(t, fmt.Errorf("Expected retrieved period from chunk data to be 1, was %d", revperiod)) -// } -// if revversion != 1 { -// teardownTest(t, fmt.Errorf("Expected retrieved version from chunk data to be 1, was %d", revversion)) -// } -//} -// // make updates and retrieve them based on periods and versions func TestResourceHandler(t *testing.T) { @@ -496,7 +458,7 @@ func (self *testSigner) signContent(data common.Hash) (signature Signature, err if err != nil { return } - signature, err = bytesToSignature(signaturebytes) + copy(signature[:], signaturebytes) return } From 1b81fb85c29d2f7c4803622b0bb95f4194e5131c Mon Sep 17 00:00:00 2001 From: lash Date: Thu, 18 Jan 2018 03:19:29 +0100 Subject: [PATCH 5/6] swarm/storage: Remove signatures from non-validated resources --- swarm/storage/resource.go | 319 +++++++++++++++++++-------------- swarm/storage/resource_ens.go | 34 +--- swarm/storage/resource_test.go | 122 +++++++------ 3 files changed, 260 insertions(+), 215 deletions(-) diff --git a/swarm/storage/resource.go b/swarm/storage/resource.go index 51010d7ca4..342727ca5b 100644 --- a/swarm/storage/resource.go +++ b/swarm/storage/resource.go @@ -25,14 +25,14 @@ const ( type Signature [signatureLength]byte -var emptySignature Signature - type SignFunc func(common.Hash) (Signature, error) -// Encapsulates an actual resource update. When synced it contains the most recent +type nameHashFunc func(string) common.Hash + +// Encapsulates an specific resource update. When synced it contains the most recent // version of the resource update data. type resource struct { - name string + name *string nameHash common.Hash startBlock uint64 lastPeriod uint32 @@ -47,6 +47,14 @@ func (r *resource) isSynced() bool { return !r.updated.IsZero() } +// Implement to activate validation of resource updates +// Specifically signing data and verification of signatures +type ResourceValidator interface { + checkAccess(string, common.Address) (bool, error) + nameHash(string) common.Hash // nameHashFunc + sign(common.Hash) (Signature, error) // SignFunc +} + // Mutable resource is an entity which allows updates to a resource // without resorting to ENS on each update. // The update scheme is built on swarm chunks with chunk keys following @@ -56,8 +64,9 @@ func (r *resource) isSynced() bool { // expressed in terms of number of blocks. // // The root entry of a mutable resource is tied to a unique identifier, -// typically - but not necessarily - an ens name. It also contains the -// block number when the resource update was first registered, and +// typically - but not necessarily - an ens name. The identifier must be +// an valid IDNA string. It also contains the block number +// when the resource update was first registered, and // the block frequency with which the resource will be updated, both of // which are stored as little-endian uint64 values in the database (for a // total of 16 bytes). @@ -68,10 +77,6 @@ func (r *resource) isSynced() bool { // starting at block 4200 with frequency 42 will have updates on block 4242, // 4284, 4326 and so on. // -// The identifier is supplied as a string, but will be IDNA converted and -// passed through the ENS namehash function. Pure ascii identifiers without -// periods will thus merely be hashed. -// // Note that the root entry is not required for the resource update scheme to // work. A normal chunk of the blocknumber/frequency data can also be created, // and pointed to by an external resource (ENS or manifest entry) @@ -79,7 +84,7 @@ func (r *resource) isSynced() bool { // Actual data updates are also made in the form of swarm chunks. The keys // of the updates are the hash of a concatenation of properties as follows: // -// sha256(namehash|period|version) +// sha256(period|version|namehash) // // The period is (currentblock - startblock) / frequency // @@ -91,8 +96,12 @@ func (r *resource) isSynced() bool { // // A lookup agent need only know the identifier name in order to get the versions // -// the chunk data is: sign(resourcedata)|resourcedata -// the resourcedata is: headerlength|period|version|name|data +// the resourcedata is: +// headerlength|period|version|identifier|data +// +// if a validator is active, the chunk data is: +// sign(resourcedata)|resourcedata +// otherwise, the chunk data is the same as the resourcedata // // headerlength is a 16 bit value containing the byte length of period|version|name // period and version are both 32 bit values. name can have arbitrary length @@ -103,13 +112,6 @@ func (r *resource) isSynced() bool { // stored using a separate store, and forwarding/syncing protocols carry per-chunk // flags to tell whether the chunk can be validated or not; if not it is to be // treated as a resource update chunk. - -type ResourceValidator interface { - checkAccess(string, common.Address) (bool, error) - nameHash(string) common.Hash - sign(common.Hash) (Signature, error) // SignFunc -} - type ResourceHandler struct { ChunkStore validator ResourceValidator @@ -118,9 +120,12 @@ type ResourceHandler struct { hashLock sync.Mutex resourceLock sync.RWMutex hasher SwarmHash + nameHash nameHashFunc } // Create or open resource update chunk store +// +// If validator is nil, signature and access validation will be deactivated func NewResourceHandler(datadir string, cloudStore CloudStore, rpcClient *rpc.Client, validator ResourceValidator) (*ResourceHandler, error) { hashfunc := MakeHashFunc(SHA3Hash) @@ -140,18 +145,19 @@ func NewResourceHandler(datadir string, cloudStore CloudStore, rpcClient *rpc.Cl rpcClient: rpcClient, resources: make(map[string]*resource), hasher: hashfunc(), + validator: validator, } - if validator != nil { - rh.validator = validator + if rh.validator != nil { + rh.nameHash = rh.validator.nameHash } else { - rh.validator = NewGenericValidator(func(name string) common.Hash { + rh.nameHash = func(name string) common.Hash { rh.hashLock.Lock() defer rh.hashLock.Unlock() rh.hasher.Reset() rh.hasher.Write([]byte(name)) return common.BytesToHash(rh.hasher.Sum(nil)) - }, nil) + } } return rh, nil @@ -167,26 +173,20 @@ func (self *ResourceHandler) chunkSize() int64 { // The signature data should match the hash of the idna-converted name by the validator's namehash function, NOT the raw name bytes. // // The start block of the resource update will be the actual current block height of the connected network. -func (self *ResourceHandler) NewResource(name string, frequency uint64, verify bool) (*resource, error) { +func (self *ResourceHandler) NewResource(name string, frequency uint64) (*resource, error) { // frequency 0 is invalid if frequency == 0 { return nil, fmt.Errorf("Frequency cannot be 0") } - // must have name - if name == "" { - return nil, fmt.Errorf("Empty name") + if !isSafeName(name) { + return nil, fmt.Errorf("Invalid name: '%s'", name) } - validName, err := toSafeName(name) - if err != nil { - return nil, err - } + nameHash := self.nameHash(name) - nameHash := self.validator.nameHash(validName) - - if verify { + if self.validator != nil { signature, err := self.validator.sign(nameHash) if err != nil { return nil, fmt.Errorf("Sign fail: %v", err) @@ -220,10 +220,10 @@ func (self *ResourceHandler) NewResource(name string, frequency uint64, verify b binary.LittleEndian.PutUint64(val, frequency) copy(chunk.SData[8:], val) self.Put(chunk) - log.Debug("new resource", "name", validName, "key", nameHash, "startBlock", currentblock, "frequency", frequency) + log.Debug("new resource", "name", name, "key", nameHash, "startBlock", currentblock, "frequency", frequency) rsrc := &resource{ - name: validName, + name: &name, nameHash: nameHash, startBlock: currentblock, frequency: frequency, @@ -231,7 +231,7 @@ func (self *ResourceHandler) NewResource(name string, frequency uint64, verify b } self.setResource(name, rsrc) - return self.resources[name], nil + return rsrc, nil } // Searches and retrieves the specific version of the resource update identified by `name` @@ -247,7 +247,7 @@ func (self *ResourceHandler) LookupVersion(name string, period uint32, version u if err != nil { return nil, err } - return self.lookup(rsrc, name, period, version, refresh) + return self.lookup(rsrc, period, version, refresh) } // Retrieves the latest version of the resource update identified by `name` @@ -263,7 +263,7 @@ func (self *ResourceHandler) LookupHistorical(name string, period uint32, refres if err != nil { return nil, err } - return self.lookup(rsrc, name, period, 0, refresh) + return self.lookup(rsrc, period, 0, refresh) } // Retrieves the latest version of the resource update identified by `name` @@ -288,11 +288,11 @@ func (self *ResourceHandler) LookupLatest(name string, refresh bool) (*resource, return nil, err } nextperiod := getNextPeriod(rsrc.startBlock, currentblock, rsrc.frequency) - return self.lookup(rsrc, name, nextperiod, 0, refresh) + return self.lookup(rsrc, nextperiod, 0, refresh) } // base code for public lookup methods -func (self *ResourceHandler) lookup(rsrc *resource, name string, period uint32, version uint32, refresh bool) (*resource, error) { +func (self *ResourceHandler) lookup(rsrc *resource, period uint32, version uint32, refresh bool) (*resource, error) { if period == 0 { return nil, fmt.Errorf("period must be >0") @@ -312,7 +312,7 @@ func (self *ResourceHandler) lookup(rsrc *resource, name string, period uint32, chunk, err := self.Get(key) if err == nil { if specificversion { - return self.updateResourceIndex(rsrc, chunk, &name) + return self.updateResourceIndex(rsrc, chunk) } // check if we have versions > 1. If a version fails, the previous version is used and returned. log.Trace("rsrc update version 1 found, checking for version updates", "period", period, "key", key) @@ -321,7 +321,7 @@ func (self *ResourceHandler) lookup(rsrc *resource, name string, period uint32, key := self.resourceHash(period, newversion, rsrc.nameHash) newchunk, err := self.Get(key) if err != nil { - return self.updateResourceIndex(rsrc, chunk, &name) + return self.updateResourceIndex(rsrc, chunk) } log.Trace("version update found, checking next", "version", version, "period", period, "key", key) chunk = newchunk @@ -336,19 +336,18 @@ func (self *ResourceHandler) lookup(rsrc *resource, name string, period uint32, // load existing mutable resource into resource struct func (self *ResourceHandler) loadResource(name string, refresh bool) (*resource, error) { + // if the resource is not known to this session we must load it // if refresh is set, we force load - rsrc := self.getResource(name) if rsrc == nil || refresh { rsrc = &resource{} // make sure our name is safe to use - validname, err := toSafeName(name) - if err != nil { - return nil, err + if !isSafeName(name) { + return nil, fmt.Errorf("Invalid name '%s'") } - rsrc.name = validname - rsrc.nameHash = self.validator.nameHash(validname) + rsrc.name = &name + rsrc.nameHash = self.nameHash(name) // get the root info chunk and update the cached value chunk, err := self.Get(Key(rsrc.nameHash[:])) @@ -356,7 +355,7 @@ func (self *ResourceHandler) loadResource(name string, refresh bool) (*resource, return nil, err } - // sanity check for chunk data + // minimum sanity check for chunk data if len(chunk.SData) != indexSize { return nil, fmt.Errorf("Invalid chunk length %d, should be %d", len(chunk.SData), indexSize) } @@ -372,46 +371,58 @@ func (self *ResourceHandler) loadResource(name string, refresh bool) (*resource, } // update mutable resource index map with specified content -func (self *ResourceHandler) updateResourceIndex(rsrc *resource, chunk *Chunk, indexname *string) (*resource, error) { +func (self *ResourceHandler) updateResourceIndex(rsrc *resource, chunk *Chunk) (*resource, error) { - // update our rsrcs entry map - signature, period, version, name, data, err := parseUpdate(chunk.SData) - if rsrc.name != name { + // retrieve metadata from chunk data and check that it matches this mutable resource + signature, period, version, name, data, err := self.parseUpdate(chunk.SData) + if *rsrc.name != name { return nil, fmt.Errorf("Update belongs to '%s', but have '%s'", name, rsrc.name) } - self.hashLock.Lock() - self.hasher.Reset() - self.hasher.Write(chunk.Key[:]) - self.hasher.Write(data) - digest := self.hasher.Sum(nil) - self.hashLock.Unlock() - _, err = getAddressFromDataSig(common.BytesToHash(digest), signature) - if err != nil { - return nil, fmt.Errorf("Invalid signature: %v", err) + // only check signature if validator is present + if self.validator != nil { + digest := self.keyDataHash(chunk.Key, data) + _, err = getAddressFromDataSig(digest, *signature) + if err != nil { + return nil, fmt.Errorf("Invalid signature: %v", err) + } } + + // update our rsrcs entry map rsrc.lastPeriod = period rsrc.version = version rsrc.updated = time.Now() rsrc.data = make([]byte, len(data)) copy(rsrc.data, data) - log.Debug("Resource synced", "name", rsrc.name, "key", chunk.Key, "period", rsrc.lastPeriod, "version", rsrc.version) - self.setResource(*indexname, rsrc) + log.Debug("Resource synced", "name", *rsrc.name, "key", chunk.Key, "period", rsrc.lastPeriod, "version", rsrc.version) + self.setResource(*rsrc.name, rsrc) return rsrc, nil } -func parseUpdate(chunkdata []byte) (signature Signature, period uint32, version uint32, name string, data []byte, err error) { - copy(signature[:], chunkdata[:signatureLength]) - cursor := signatureLength +// retrieve update metadata from chunk data +// mirrors newUpdateChunk() +func (self *ResourceHandler) parseUpdate(chunkdata []byte) (signature *Signature, period uint32, version uint32, name string, data []byte, err error) { + cursor := 0 + + // omit signatures if we have no validator + var sigoffset int + if self.validator != nil { + signature = &Signature{} + copy(signature[:], chunkdata[:signatureLength]) + sigoffset = signatureLength + cursor = sigoffset + } + headerlength := binary.LittleEndian.Uint16(chunkdata[cursor : cursor+2]) if int(headerlength+2) > len(chunkdata) { - return emptySignature, 0, 0, "", nil, fmt.Errorf("Reported header length %d longer than actual data length %d", headerlength, len(chunkdata)) + err = fmt.Errorf("Reported header length %d longer than actual data length %d", headerlength, len(chunkdata)) + return } cursor += 2 period = binary.LittleEndian.Uint32(chunkdata[cursor : cursor+4]) cursor += 4 version = binary.LittleEndian.Uint32(chunkdata[cursor : cursor+4]) cursor += 4 - namelength := int(headerlength) - cursor + signatureLength + 2 + namelength := int(headerlength) - cursor + sigoffset + 2 name = string(chunkdata[cursor : cursor+namelength]) cursor += namelength data = make([]byte, len(chunkdata)-cursor) @@ -425,16 +436,24 @@ func parseUpdate(chunkdata []byte) (signature Signature, period uint32, version // It is the caller's responsibility to make sure that this data is not stale. // // A resource update cannot span chunks, and thus has max length 4096 -func (self *ResourceHandler) Update(indexname string, data []byte) (Key, error) { +func (self *ResourceHandler) Update(name string, data []byte) (Key, error) { + + var sigoffset int + if self.validator != nil { + sigoffset = signatureLength + } // get the cached information - rsrc := self.getResource(indexname) + rsrc := self.getResource(name) + if rsrc == nil { + return nil, fmt.Errorf("Resource object not in index") + } if !rsrc.isSynced() { return nil, fmt.Errorf("Resource object not in sync") } // an update can be only one chunk long - datalimit := self.chunkSize() - int64(signatureLength-len(self.resources[indexname].name)-8) + datalimit := self.chunkSize() - int64(sigoffset-len(name)-8) if int64(len(data)) > datalimit { return nil, fmt.Errorf("Data overflow: %d / %d bytes", len(data), datalimit) } @@ -449,7 +468,7 @@ func (self *ResourceHandler) Update(indexname string, data []byte) (Key, error) // if we already have an update for this block then increment version // (resource object MUST be in sync for version to be correct) var version uint32 - if self.hasUpdate(indexname, nextperiod) { + if self.hasUpdate(name, nextperiod) { version = rsrc.version } version++ @@ -457,32 +476,36 @@ func (self *ResourceHandler) Update(indexname string, data []byte) (Key, error) // calculate the chunk key key := self.resourceHash(nextperiod, version, rsrc.nameHash) - // sign the data hash with the key - digest := self.keyDataHash(key, data) - signature, err := self.validator.sign(digest) - if err != nil { - return nil, err + var signature *Signature + if self.validator != nil { + // sign the data hash with the key + digest := self.keyDataHash(key, data) + sig, err := self.validator.sign(digest) + if err != nil { + return nil, err + } + signature = &sig + + // get the address of the signer (which also checks that it's a valid signature) + addr, err := getAddressFromDataSig(digest, *signature) + if err != nil { + return nil, fmt.Errorf("Invalid data/signature: %v", err) + } + + // check if the signer has access to update + ok, err := self.validator.checkAccess(name, addr) + if err != nil { + return nil, err + } else if !ok { + return nil, fmt.Errorf("Address %x does not have access to update %s", addr, name) + } } - // get the address of the signer (which also checks that it's a valid signature) - addr, err := getAddressFromDataSig(digest, signature) - if err != nil { - return nil, fmt.Errorf("Invalid data/signature: %v", err) - } - - // check if the signer has access to update - ok, err := self.validator.checkAccess(indexname, addr) - if err != nil { - return nil, err - } else if !ok { - return nil, fmt.Errorf("Address %x does not have access to update %s", addr, indexname) - } - - chunk := newUpdateChunk(key, signature, nextperiod, version, self.resources[indexname].name, data) + chunk := newUpdateChunk(key, signature, nextperiod, version, name, data) // send the chunk self.Put(chunk) - log.Trace("resource update", "name", rsrc.name, "key", key, "currentblock", currentblock, "lastperiod", nextperiod, "version", version, "data", chunk.SData) + log.Trace("resource update", "name", name, "key", key, "currentblock", currentblock, "lastperiod", nextperiod, "version", version, "data", chunk.SData) // update our resources map entry and return the new key rsrc.lastPeriod = nextperiod @@ -492,38 +515,6 @@ func (self *ResourceHandler) Update(indexname string, data []byte) (Key, error) return key, nil } -func newUpdateChunk(key Key, signature Signature, period uint32, version uint32, name string, data []byte) *Chunk { - // create the update chunk - // prepend version and period to allow reverse lookups - headerlength := uint16(len(name) + 4 + 4) - - chunk := NewChunk(key, nil) - chunk.SData = make([]byte, signatureLength+int(headerlength)+2+len(data)) - - cursor := 0 - copy(chunk.SData, signature[:]) - cursor += signatureLength - - // data header length does NOT include the header length prefix bytes themselves - binary.LittleEndian.PutUint16(chunk.SData[cursor:], headerlength) - cursor += 2 - - binary.LittleEndian.PutUint32(chunk.SData[cursor:], period) - cursor += 4 - - binary.LittleEndian.PutUint32(chunk.SData[cursor:], version) - cursor += 4 - - namebytes := []byte(name) - copy(chunk.SData[cursor:], namebytes) - cursor += len(namebytes) - - copy(chunk.SData[cursor:], data) - - chunk.Size = int64(len(chunk.SData)) - return chunk -} - // Closes the datastore. // Always call this at shutdown to avoid data corruption. func (self *ResourceHandler) Close() { @@ -543,10 +534,12 @@ func (self *ResourceHandler) getBlock() (uint64, error) { return strconv.ParseUint(currentblock, 10, 64) } +// Calculate the period index (aka major version number) from a given block number func (self *ResourceHandler) BlockToPeriod(name string, blocknumber uint64) uint32 { return getNextPeriod(self.resources[name].startBlock, blocknumber, self.resources[name].frequency) } +// Calculate the block number from a given period index (aka major version number) func (self *ResourceHandler) PeriodToBlock(name string, period uint32) uint64 { return self.resources[name].startBlock + (uint64(period) * self.resources[name].frequency) } @@ -564,8 +557,9 @@ func (self *ResourceHandler) setResource(name string, rsrc *resource) { self.resources[name] = rsrc } +// used for chunk keys func (self *ResourceHandler) resourceHash(period uint32, version uint32, namehash common.Hash) Key { - // format is: hash(namehash|period|version) + // format is: hash(period|version|namehash) self.hashLock.Lock() defer self.hashLock.Unlock() self.hasher.Reset() @@ -578,6 +572,13 @@ func (self *ResourceHandler) resourceHash(period uint32, version uint32, namehas return self.hasher.Sum(nil) } +func (self *ResourceHandler) hasUpdate(name string, period uint32) bool { + if self.resources[name].lastPeriod == period { + return true + } + return false +} + func getAddressFromDataSig(datahash common.Hash, signature Signature) (common.Address, error) { pub, err := crypto.SigToPub(datahash.Bytes(), signature[:]) if err != nil { @@ -586,8 +587,45 @@ func getAddressFromDataSig(datahash common.Hash, signature Signature) (common.Ad return crypto.PubkeyToAddress(*pub), nil } -func (self *ResourceHandler) hasUpdate(name string, period uint32) bool { - return self.resources[name].lastPeriod == period +// create an update chunk +func newUpdateChunk(key Key, signature *Signature, period uint32, version uint32, name string, data []byte) *Chunk { + + // no signatures if no validator + var sigoffset int + if signature != nil { + sigoffset = signatureLength + } + + // prepend version and period to allow reverse lookups + headerlength := uint16(len(name) + 4 + 4) + + chunk := NewChunk(key, nil) + chunk.SData = make([]byte, sigoffset+int(headerlength)+2+len(data)) + + cursor := 0 + if signature != nil { + copy(chunk.SData, (*signature)[:]) + cursor += signatureLength + } + + // data header length does NOT include the header length prefix bytes themselves + binary.LittleEndian.PutUint16(chunk.SData[cursor:], headerlength) + cursor += 2 + + binary.LittleEndian.PutUint32(chunk.SData[cursor:], period) + cursor += 4 + + binary.LittleEndian.PutUint32(chunk.SData[cursor:], version) + cursor += 4 + + namebytes := []byte(name) + copy(chunk.SData[cursor:], namebytes) + cursor += len(namebytes) + + copy(chunk.SData[cursor:], data) + + chunk.Size = int64(len(chunk.SData)) + return chunk } // \TODO chunkSize is a workaround until the ChunkStore interface exports a method to get the chunk size directly @@ -640,8 +678,7 @@ func getNextPeriod(start uint64, current uint64, frequency uint64) uint32 { return uint32(period + 1) } -func toSafeName(name string) (string, error) { - // make sure our ens identifier is idna safe +func ToSafeName(name string) (string, error) { validname, err := idna.ToASCII(name) if err != nil { return "", err @@ -649,6 +686,22 @@ func toSafeName(name string) (string, error) { return validname, nil } +// check that name identifiers contain valid bytes +func isSafeName(name string) bool { + if name == "" { + return false + } + validname, err := idna.ToASCII(name) + if err != nil { + return false + } + if validname != name { + return false + } + return true +} + +// convenience for creating signature hashes of update data func (self *ResourceHandler) keyDataHash(key Key, data []byte) common.Hash { self.hashLock.Lock() defer self.hashLock.Unlock() diff --git a/swarm/storage/resource_ens.go b/swarm/storage/resource_ens.go index ccd4ed5344..0a4500309d 100644 --- a/swarm/storage/resource_ens.go +++ b/swarm/storage/resource_ens.go @@ -12,9 +12,9 @@ type baseValidator struct { signFunc SignFunc } -func (b *baseValidator) sign(datahash common.Hash) (Signature, error) { +func (b *baseValidator) sign(datahash common.Hash) (signature Signature, err error) { if b.signFunc == nil { - return emptySignature, fmt.Errorf("No signature function") + return signature, fmt.Errorf("No signature function") } return b.signFunc(datahash) } @@ -22,8 +22,7 @@ func (b *baseValidator) sign(datahash common.Hash) (Signature, error) { // ENS validation of mutable resource owners type ENSValidator struct { *baseValidator - api *ens.ENS - hashlength int + api *ens.ENS } func NewENSValidator(contractaddress common.Address, backend bind.ContractBackend, transactOpts *bind.TransactOpts, signFunc SignFunc) (*ENSValidator, error) { @@ -37,7 +36,6 @@ func NewENSValidator(contractaddress common.Address, backend bind.ContractBacken if err != nil { return nil, err } - validator.hashlength = len(ens.EnsNode(dbDirName).Bytes()) return validator, nil } @@ -52,29 +50,3 @@ func (self *ENSValidator) checkAccess(name string, address common.Address) (bool func (self *ENSValidator) nameHash(name string) common.Hash { return ens.EnsNode(name) } - -// Default fallthrough validation of mutable resource ownership -type GenericValidator struct { - *baseValidator - hashFunc func(string) common.Hash - hashlength int -} - -func NewGenericValidator(hashFunc func(string) common.Hash, signFunc SignFunc) *GenericValidator { - return &GenericValidator{ - baseValidator: &baseValidator{ - signFunc: signFunc, - }, - hashFunc: hashFunc, - hashlength: len(hashFunc(dbDirName).Bytes()), - } - -} - -func (self *GenericValidator) checkAccess(name string, address common.Address) (bool, error) { - return true, nil -} - -func (self *GenericValidator) nameHash(name string) common.Hash { - return self.hashFunc(name) -} diff --git a/swarm/storage/resource_test.go b/swarm/storage/resource_test.go index f645522054..ffa0eec758 100644 --- a/swarm/storage/resource_test.go +++ b/swarm/storage/resource_test.go @@ -15,8 +15,6 @@ import ( "testing" "time" - "golang.org/x/net/idna" - "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind/backends" "github.com/ethereum/go-ethereum/common" @@ -35,10 +33,16 @@ var ( resourceFrequency = uint64(42) cleanF func() domainName = "føø.bar" + safeName string ) func init() { + var err error log.Root().SetHandler(log.CallerFileHandler(log.LvlFilterHandler(log.LvlTrace, log.StreamHandler(os.Stderr, log.TerminalFormat(true))))) + safeName, err = ToSafeName(domainName) + if err != nil { + panic(err) + } } // simulated backend does not have the blocknumber call @@ -70,20 +74,20 @@ func TestResourceReverse(t *testing.T) { period := uint32(4) version := uint32(2) - // set up rpc and create resourcehandler - rh, _, signer, teardownTest, err := setupTest(nil, nil) + // signer containing private key + signer, err := newTestSigner() if err != nil { - teardownTest(t, err) + t.Fatal(err) } - // create a new resource - validname, err := idna.ToASCII(domainName) + // set up rpc and create resourcehandler + rh, _, _, teardownTest, err := setupTest(nil, newTestValidator(signer.signContent)) if err != nil { teardownTest(t, err) } // generate a hash for block 4200 version 1 - key := rh.resourceHash(period, version, rh.validator.nameHash(validname)) + key := rh.resourceHash(period, version, rh.nameHash(safeName)) // generate some bogus data for the chunk and sign it data := make([]byte, 8) @@ -99,17 +103,18 @@ func TestResourceReverse(t *testing.T) { teardownTest(t, err) } - chunk := newUpdateChunk(key, sig, period, version, validname, data) + chunk := newUpdateChunk(key, &sig, period, version, safeName, data) // check that we can recover the owner account from the update chunk's signature - checksig, checkperiod, checkversion, checkname, checkdata, err := parseUpdate(chunk.SData) + checksig, checkperiod, checkversion, checkname, checkdata, err := rh.parseUpdate(chunk.SData) checkdigest := rh.keyDataHash(chunk.Key, checkdata) - recoveredaddress, err := getAddressFromDataSig(checkdigest, checksig) + recoveredaddress, err := getAddressFromDataSig(checkdigest, *checksig) if err != nil { - teardownTest(t, err) + teardownTest(t, fmt.Errorf("Retrieve address from signature fail: %v", err)) } originaladdress := crypto.PubkeyToAddress(signer.privKey.PublicKey) + // check that the metadata retrieved from the chunk matches what we gave it if recoveredaddress != originaladdress { teardownTest(t, fmt.Errorf("addresses dont match: %x != %x", originaladdress, recoveredaddress)) } @@ -123,8 +128,8 @@ func TestResourceReverse(t *testing.T) { if version != checkversion { teardownTest(t, fmt.Errorf("Expected version '%d', was '%d'", version, checkversion)) } - if validname != checkname { - teardownTest(t, fmt.Errorf("Expected name '%s', was '%s'", validname, checkname)) + if safeName != checkname { + teardownTest(t, fmt.Errorf("Expected name '%s', was '%s'", safeName, checkname)) } if !bytes.Equal(data, checkdata) { teardownTest(t, fmt.Errorf("Expectedn data '%x', was '%x'", data, checkdata)) @@ -145,17 +150,16 @@ func TestResourceHandler(t *testing.T) { } // create a new resource - resourcevalidname, err := idna.ToASCII(domainName) if err != nil { teardownTest(t, err) } - _, err = rh.NewResource(domainName, resourceFrequency, false) + _, err = rh.NewResource(safeName, resourceFrequency) if err != nil { teardownTest(t, err) } // check that the new resource is stored correctly - namehash := rh.validator.nameHash(resourcevalidname) + namehash := rh.nameHash(safeName) chunk, err := rh.ChunkStore.(*resourceChunkStore).localStore.(*LocalStore).memStore.Get(Key(namehash[:])) if err != nil { teardownTest(t, err) @@ -175,7 +179,7 @@ func TestResourceHandler(t *testing.T) { resourcekey := make(map[string]Key) fwdBlocks(int(resourceFrequency/2), backend) data := []byte("blinky") - resourcekey["blinky"], err = rh.Update(domainName, data) + resourcekey["blinky"], err = rh.Update(safeName, data) if err != nil { teardownTest(t, err) } @@ -183,7 +187,7 @@ func TestResourceHandler(t *testing.T) { // update on first period fwdBlocks(int(resourceFrequency/2), backend) data = []byte("pinky") - resourcekey["pinky"], err = rh.Update(domainName, data) + resourcekey["pinky"], err = rh.Update(safeName, data) if err != nil { teardownTest(t, err) } @@ -191,7 +195,7 @@ func TestResourceHandler(t *testing.T) { // update on second period fwdBlocks(int(resourceFrequency), backend) data = []byte("inky") - resourcekey["inky"], err = rh.Update(domainName, data) + resourcekey["inky"], err = rh.Update(safeName, data) if err != nil { teardownTest(t, err) } @@ -199,7 +203,7 @@ func TestResourceHandler(t *testing.T) { // update just after second period fwdBlocks(1, backend) data = []byte("clyde") - resourcekey["clyde"], err = rh.Update(domainName, data) + resourcekey["clyde"], err = rh.Update(safeName, data) if err != nil { teardownTest(t, err) } @@ -211,43 +215,44 @@ func TestResourceHandler(t *testing.T) { fwdBlocks(int(resourceFrequency*2)-1, backend) rh2, err := NewResourceHandler(datadir, &testCloudStore{}, rh.rpcClient, nil) - _, err = rh2.LookupLatest(domainName, true) + _, err = rh2.LookupLatest(safeName, true) if err != nil { teardownTest(t, err) } // last update should be "clyde", version two, blockheight startblocknumber + (resourcefrequency * 3) - if !bytes.Equal(rh2.resources[domainName].data, []byte("clyde")) { - teardownTest(t, fmt.Errorf("resource data was %v, expected %v", rh2.resources[domainName].data, []byte("clyde"))) + if !bytes.Equal(rh2.resources[safeName].data, []byte("clyde")) { + teardownTest(t, fmt.Errorf("resource data was %v, expected %v", rh2.resources[safeName].data, []byte("clyde"))) } - if rh2.resources[domainName].version != 2 { - teardownTest(t, fmt.Errorf("resource version was %d, expected 2", rh2.resources[domainName].version)) + if rh2.resources[safeName].version != 2 { + teardownTest(t, fmt.Errorf("resource version was %d, expected 2", rh2.resources[safeName].version)) } - if rh2.resources[domainName].lastPeriod != 3 { - teardownTest(t, fmt.Errorf("resource period was %d, expected 3", rh2.resources[domainName].lastPeriod)) + if rh2.resources[safeName].lastPeriod != 3 { + teardownTest(t, fmt.Errorf("resource period was %d, expected 3", rh2.resources[safeName].lastPeriod)) } + log.Debug("Latest lookup", "period", rh2.resources[safeName].lastPeriod, "version", rh2.resources[safeName].version, "data", rh2.resources[safeName].data) // specific block, latest version - rsrc, err := rh2.LookupHistorical(domainName, 3, true) + rsrc, err := rh2.LookupHistorical(safeName, 3, true) if err != nil { teardownTest(t, err) } - // check data if !bytes.Equal(rsrc.data, []byte("clyde")) { teardownTest(t, fmt.Errorf("resource data (historical) was %v, expected %v", rh2.resources[domainName].data, []byte("clyde"))) } + log.Debug("Historical lookup", "period", rh2.resources[safeName].lastPeriod, "version", rh2.resources[safeName].version, "data", rh2.resources[safeName].data) // specific block, specific version - rsrc, err = rh2.LookupVersion(domainName, 3, 1, true) + rsrc, err = rh2.LookupVersion(safeName, 3, 1, true) if err != nil { teardownTest(t, err) } - // check data if !bytes.Equal(rsrc.data, []byte("inky")) { teardownTest(t, fmt.Errorf("resource data (historical) was %v, expected %v", rh2.resources[domainName].data, []byte("inky"))) } + log.Debug("Specific version lookup", "period", rh2.resources[safeName].lastPeriod, "version", rh2.resources[safeName].version, "data", rh2.resources[safeName].data) teardownTest(t, nil) } @@ -266,7 +271,7 @@ func TestResourceENSOwner(t *testing.T) { transactOpts := bind.NewKeyedTransactor(signer.privKey) // set up ENS sim - domainparts := strings.Split(domainName, ".") + domainparts := strings.Split(safeName, ".") contractAddr, contractbackend, err := setupENS(addr, transactOpts, domainparts[0], domainparts[1]) if err != nil { t.Fatal(err) @@ -284,14 +289,14 @@ func TestResourceENSOwner(t *testing.T) { } // create new resource when we are owner = ok - _, err = rh.NewResource(domainName, resourceFrequency, true) + _, err = rh.NewResource(safeName, resourceFrequency) if err != nil { teardownTest(t, fmt.Errorf("Create resource fail: %v", err)) } data := []byte("foo") // update resource when we are owner = ok - _, err = rh.Update(domainName, data) + _, err = rh.Update(safeName, data) if err != nil { teardownTest(t, fmt.Errorf("Update resource fail: %v", err)) } @@ -302,7 +307,7 @@ func TestResourceENSOwner(t *testing.T) { teardownTest(t, err) } rh.validator.(*ENSValidator).signFunc = signertwo.signContent - _, err = rh.Update(domainName, data) + _, err = rh.Update(safeName, data) if err == nil { teardownTest(t, fmt.Errorf("Expected resource update fail due to owner mismatch")) } @@ -331,15 +336,6 @@ func setupTest(contractbackend bind.ContractBackend, validator ResourceValidator } } - if validator == nil { - // create a new signer, which creates the private key - signer, err = newTestSigner() - if err != nil { - return - } - validator = NewGenericValidator(testHashFunc, signer.signContent) - } - // temp datadir datadir, err = ioutil.TempDir("", "rh") if err != nil { @@ -431,12 +427,7 @@ func setupENS(addr common.Address, transactOpts *bind.TransactOpts, sub string, return contractAddress, contractBackend, nil } -func testHashFunc(name string) common.Hash { - testHasher.Reset() - testHasher.Write([]byte(name)) - return common.BytesToHash(testHasher.Sum(nil)) -} - +// implementation of an external signer to pass to validator type testSigner struct { privKey *ecdsa.PrivateKey hasher SwarmHash @@ -453,6 +444,7 @@ func newTestSigner() (*testSigner, error) { }, nil } +// matches the SignFunc type func (self *testSigner) signContent(data common.Hash) (signature Signature, err error) { signaturebytes, err := crypto.Sign(data.Bytes(), self.privKey) if err != nil { @@ -473,3 +465,31 @@ func (c *testCloudStore) Deliver(*Chunk) { func (c *testCloudStore) Retrieve(*Chunk) { } + +// Default fallthrough validation of mutable resource ownership +type testValidator struct { + *baseValidator + hashFunc func(string) common.Hash +} + +func newTestValidator(signFunc SignFunc) *testValidator { + return &testValidator{ + baseValidator: &baseValidator{ + signFunc: signFunc, + }, + hashFunc: func(name string) common.Hash { + testHasher.Reset() + testHasher.Write([]byte(name)) + return common.BytesToHash(testHasher.Sum(nil)) + }, + } + +} + +func (self *testValidator) checkAccess(name string, address common.Address) (bool, error) { + return true, nil +} + +func (self *testValidator) nameHash(name string) common.Hash { + return self.hashFunc(name) +} From d224cd119cd85b0bde00844e6595c04923242d0b Mon Sep 17 00:00:00 2001 From: lash Date: Sat, 20 Jan 2018 18:35:21 +0100 Subject: [PATCH 6/6] swarm/storage: Add store timeout --- swarm/storage/resource.go | 45 ++++++++++++++++++++++------------ swarm/storage/resource_test.go | 3 --- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/swarm/storage/resource.go b/swarm/storage/resource.go index 342727ca5b..3d6c653716 100644 --- a/swarm/storage/resource.go +++ b/swarm/storage/resource.go @@ -17,10 +17,11 @@ import ( ) const ( - signatureLength = 65 - indexSize = 16 - dbDirName = "resource" - chunkSize = 4096 // temporary until we implement DPA in the resourcehandler + signatureLength = 65 + indexSize = 16 + dbDirName = "resource" + chunkSize = 4096 // temporary until we implement DPA in the resourcehandler + defaultStoreTimeout = 4000 * time.Millisecond ) type Signature [signatureLength]byte @@ -121,6 +122,7 @@ type ResourceHandler struct { resourceLock sync.RWMutex hasher SwarmHash nameHash nameHashFunc + storeTimeout time.Duration } // Create or open resource update chunk store @@ -141,11 +143,12 @@ func NewResourceHandler(datadir string, cloudStore CloudStore, rpcClient *rpc.Cl } rh := &ResourceHandler{ - ChunkStore: newResourceChunkStore(path, hashfunc, localStore, cloudStore), - rpcClient: rpcClient, - resources: make(map[string]*resource), - hasher: hashfunc(), - validator: validator, + ChunkStore: newResourceChunkStore(path, hashfunc, localStore, cloudStore), + rpcClient: rpcClient, + resources: make(map[string]*resource), + hasher: hashfunc(), + validator: validator, + storeTimeout: defaultStoreTimeout, } if rh.validator != nil { @@ -344,7 +347,7 @@ func (self *ResourceHandler) loadResource(name string, refresh bool) (*resource, rsrc = &resource{} // make sure our name is safe to use if !isSafeName(name) { - return nil, fmt.Errorf("Invalid name '%s'") + return nil, fmt.Errorf("Invalid name '%s'", name) } rsrc.name = &name rsrc.nameHash = self.nameHash(name) @@ -376,7 +379,7 @@ func (self *ResourceHandler) updateResourceIndex(rsrc *resource, chunk *Chunk) ( // retrieve metadata from chunk data and check that it matches this mutable resource signature, period, version, name, data, err := self.parseUpdate(chunk.SData) if *rsrc.name != name { - return nil, fmt.Errorf("Update belongs to '%s', but have '%s'", name, rsrc.name) + return nil, fmt.Errorf("Update belongs to '%s', but have '%s'", name, *rsrc.name) } // only check signature if validator is present if self.validator != nil { @@ -400,9 +403,10 @@ func (self *ResourceHandler) updateResourceIndex(rsrc *resource, chunk *Chunk) ( // retrieve update metadata from chunk data // mirrors newUpdateChunk() -func (self *ResourceHandler) parseUpdate(chunkdata []byte) (signature *Signature, period uint32, version uint32, name string, data []byte, err error) { +func (self *ResourceHandler) parseUpdate(chunkdata []byte) (*Signature, uint32, uint32, string, []byte, error) { + var err error cursor := 0 - + var signature *Signature // omit signatures if we have no validator var sigoffset int if self.validator != nil { @@ -415,8 +419,13 @@ func (self *ResourceHandler) parseUpdate(chunkdata []byte) (signature *Signature headerlength := binary.LittleEndian.Uint16(chunkdata[cursor : cursor+2]) if int(headerlength+2) > len(chunkdata) { err = fmt.Errorf("Reported header length %d longer than actual data length %d", headerlength, len(chunkdata)) - return + return nil, 0, 0, "", nil, err } + + var period uint32 + var version uint32 + var name string + var data []byte cursor += 2 period = binary.LittleEndian.Uint32(chunkdata[cursor : cursor+4]) cursor += 4 @@ -427,7 +436,7 @@ func (self *ResourceHandler) parseUpdate(chunkdata []byte) (signature *Signature cursor += namelength data = make([]byte, len(chunkdata)-cursor) copy(data, chunkdata[cursor:]) - return + return signature, period, version, name, data, err } // Adds an actual data update @@ -505,6 +514,12 @@ func (self *ResourceHandler) Update(name string, data []byte) (Key, error) { // send the chunk self.Put(chunk) + timeout := time.NewTimer(self.storeTimeout) + select { + case <-chunk.dbStored: + case <-timeout.C: + + } log.Trace("resource update", "name", name, "key", key, "currentblock", currentblock, "lastperiod", nextperiod, "version", version, "data", chunk.SData) // update our resources map entry and return the new key diff --git a/swarm/storage/resource_test.go b/swarm/storage/resource_test.go index ffa0eec758..d130709428 100644 --- a/swarm/storage/resource_test.go +++ b/swarm/storage/resource_test.go @@ -150,9 +150,6 @@ func TestResourceHandler(t *testing.T) { } // create a new resource - if err != nil { - teardownTest(t, err) - } _, err = rh.NewResource(safeName, resourceFrequency) if err != nil { teardownTest(t, err)