swarm/storage: Check ENS ownership on resource create

This commit is contained in:
lash 2018-01-14 08:54:14 +01:00
parent dfb45e382c
commit 91cfddd375
3 changed files with 79 additions and 37 deletions

View file

@ -40,10 +40,6 @@ type resource struct {
// The update scheme is built on swarm chunks with chunk keys following
// a predictable, versionable pattern.
//
// The data of the chunk contains the content hash of the version in question.
// In order to be valid, the hash is signed by the owner of the ENS record
// of the mutable resource.
//
// Updates are defined to be periodic in nature, where periods are
// expressed in terms of number of blocks.
//
@ -56,7 +52,7 @@ type resource struct {
// The root entry tells the requester from when the mutable resource was
// first added (block number) and in which block number to look for the
// actual updates. Thus, a resource update for identifier "foo.bar"
// actual updates. Thus, a resource update for identifier "føø.bar"
// starting at block 4200 with frequency 42 will have updates on block 4242,
// 4284, 4326 and so on.
//
@ -66,27 +62,28 @@ type resource struct {
//
// 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 actual ENS entry (or manifest entry) instead.
// and pointed to by an external resource (ENS or manifest entry)
//
// 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|blocknumber|version)
// sha256(namehash|period|version)
//
// The blocknumber here is the next block period after the current block
// calculated from the start block and frequency of the resource update.
// Using our previous example, this means that an update made at block 4285,
// and even 4284, will have 4326 as the block number.
// The period is (currentblock - startblock) / frequency
//
// Using our previous example, this means that a period 3 will have 4326 as
// the block number.
//
// If more than one update is made to the same block number, incremental
// version numbers are used successively.
//
// A lookup agent need only know the identifier name in order to get the versions
//
// the data itself is prefixed with a signed hash of the data. The sigining key
// is used to verify the authenticity of the update, for example by looking
// up the ownership of the namehash in ENS and comparing to the address derived
// from it
// the chunk data is: sign(resourcedata)|resourcedata
// the resourcedata is: headerlength|period|version|name|data
//
// 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
//
// NOTE: the following is yet to be implemented
// The resource update chunks will be stored in the swarm, but receive special
@ -94,8 +91,6 @@ type resource struct {
// 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.
//
// TODO: signature validation
type ResourceHandler interface {
ChunkStore
NewResource(name string, frequency uint64) (*resource, error)

View file

@ -2,24 +2,31 @@ package storage
import (
"crypto/ecdsa"
"fmt"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/contracts/ens"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/rpc"
)
// Implements Mutable Resources as offchain ENS resolvers
//
// The data part of the update is forced to be a valid ENS content hash
//
// Also, the ENSResourceHandler only allows creation and update of
// Resources from the ENS owner's address
//
type ENSResourceHandler struct {
ResourceHandler
ethapi *ethclient.Client
*RawResourceHandler
addr common.Address
ensapi *ens.ENS
}
func NewENSResourceHandler(privKey *ecdsa.PrivateKey, datadir string, cloudStore CloudStore, rpcClient *rpc.Client, ensAddr common.Address) (*ENSResourceHandler, error) {
func NewENSResourceHandler(privKey *ecdsa.PrivateKey, datadir string, cloudStore CloudStore, rpcClient *rpc.Client, backend bind.ContractBackend, ensAddr common.Address) (*ENSResourceHandler, error) {
transactOpts := bind.NewKeyedTransactor(privKey)
ethapi := ethclient.NewClient(rpcClient)
ensinstance, err := ens.NewENS(transactOpts, ensAddr, ethapi)
ensinstance, err := ens.NewENS(transactOpts, ensAddr, backend)
if err != nil {
return nil, err
}
@ -32,8 +39,19 @@ func NewENSResourceHandler(privKey *ecdsa.PrivateKey, datadir string, cloudStore
}
return &ENSResourceHandler{
ResourceHandler: rh,
ethapi: ethclient.NewClient(rpcClient),
RawResourceHandler: rh,
addr: crypto.PubkeyToAddress(privKey.PublicKey),
ensapi: ensinstance,
}, nil
}
func (self *ENSResourceHandler) NewResource(name string, frequency uint64) (*resource, error) {
owneraddr, err := self.ensapi.Owner(self.RawResourceHandler.nameHashFunc(name))
if err != nil {
return nil, fmt.Errorf("ENS error: %v", err)
}
if owneraddr != self.addr {
return nil, fmt.Errorf("not owner")
}
return self.RawResourceHandler.NewResource(name, frequency)
}

View file

@ -41,6 +41,8 @@ func init() {
log.Root().SetHandler(log.CallerFileHandler(log.LvlFilterHandler(log.LvlTrace, log.StreamHandler(os.Stderr, log.TerminalFormat(true)))))
}
// simulated backend does not have the blocknumber call
// so we use this wrapper to fake returning the block count
type fakeBackend struct {
*backends.SimulatedBackend
blocknumber uint64
@ -317,14 +319,21 @@ func TestResourceHandler(t *testing.T) {
}
func TestResourceENS(t *testing.T) {
func TestResourceENSNew(t *testing.T) {
// privkey for signing updates
// privkey for ens owner
privkey, err := crypto.GenerateKey()
if err != nil {
return
}
// privkey for signing updates
privkeytwo, err := crypto.GenerateKey()
if err != nil {
return
}
// set up ENS sim
domainparts := strings.Split(domainName, ".")
addr, contractbackend, err := setupENS(privkey, domainparts[0], domainparts[1])
if err != nil {
@ -336,14 +345,31 @@ func TestResourceENS(t *testing.T) {
teardownTest(t, err)
}
// create new resource when we are owner = ok
_, err = rh.NewResource(domainName, 42)
if err != nil {
teardownTest(t, err)
}
// create new resource when we are NOT owner = !ok
rawrh := rh.(*ENSResourceHandler)
rawrh.privKey = privkeytwo
rawrh.addr = crypto.PubkeyToAddress(privkeytwo.PublicKey)
_, err = rawrh.NewResource(domainName, 42)
if err == nil {
teardownTest(t, fmt.Errorf("Expected resource create fail due to owner mismatch"))
}
teardownTest(t, nil)
}
// fast-forward blockheight
func fwdBlocks(count int, backend *fakeBackend) {
for i := 0; i < count; i++ {
backend.Commit()
}
}
func setupTest(privkey *ecdsa.PrivateKey, contractbackend bind.ContractBackend, ensaddr common.Address) (rh ResourceHandler, datadir string, err error, teardown func(*testing.T, error)) {
var fsClean func()
@ -394,8 +420,9 @@ func setupTest(privkey *ecdsa.PrivateKey, contractbackend bind.ContractBackend,
return
}
// choose if with ens or not
if ensaddr != zeroAddr {
rh, err = NewENSResourceHandler(privkey, datadir, &testCloudStore{}, rpcclient, ensaddr)
rh, err = NewENSResourceHandler(privkey, datadir, &testCloudStore{}, rpcclient, contractbackend, ensaddr)
} else {
rh, err = NewRawResourceHandler(privkey, datadir, &testCloudStore{}, rpcclient, nil)
}
@ -408,7 +435,11 @@ func setupTest(privkey *ecdsa.PrivateKey, contractbackend bind.ContractBackend,
return
}
// Set up simulated ENS backend for use with ENSResourceHandler tests
func setupENS(privkey *ecdsa.PrivateKey, sub string, top string) (common.Address, bind.ContractBackend, error) {
// create the domain hash values to pass to the ENS contract methods
var tophash [32]byte
var subhash [32]byte
hashfunc.Reset()
@ -417,17 +448,22 @@ func setupENS(privkey *ecdsa.PrivateKey, sub string, top string) (common.Address
hashfunc.Reset()
hashfunc.Write([]byte(sub))
copy(subhash[:], hashfunc.Sum(nil))
// private key -> address is owner of domain
addr := crypto.PubkeyToAddress(privkey.PublicKey)
// initialize contract backend and deploy
transactOpts := bind.NewKeyedTransactor(privkey)
contractBackend := &fakeBackend{
SimulatedBackend: backends.NewSimulatedBackend(core.GenesisAlloc{addr: {Balance: big.NewInt(1000000000)}}),
}
transactOpts := bind.NewKeyedTransactor(privkey)
ensAddr, _, ensinstance, err := contract.DeployENS(transactOpts, contractBackend)
if err != nil {
return zeroAddr, nil, fmt.Errorf("can't deploy: %v", err)
}
// update the registry for the correct owner address
if _, err = ensinstance.SetOwner(transactOpts, [32]byte{}, addr); err != nil {
return zeroAddr, nil, fmt.Errorf("can't setowner: %v", err)
}
@ -443,13 +479,6 @@ func setupENS(privkey *ecdsa.PrivateKey, sub string, top string) (common.Address
}
contractBackend.Commit()
nodeowner, err := ensinstance.Owner(&bind.CallOpts{}, ens.EnsNode(strings.Join([]string{sub, top}, ".")))
if err != nil {
return zeroAddr, nil, fmt.Errorf("can't retrieve owner: %v", err)
} else if !bytes.Equal(nodeowner.Bytes(), addr.Bytes()) {
return zeroAddr, nil, fmt.Errorf("retrieved owner doesn't match; expected '%x', got '%x'", addr, nodeowner)
}
return ensAddr, contractBackend, nil
}