From e1e867cdf9875e060aed1a5a5a91a0fb39ba1a4f Mon Sep 17 00:00:00 2001 From: lash Date: Thu, 18 Jan 2018 01:13:13 +0100 Subject: [PATCH] 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 {