Commit 1b0d3845 authored by lash's avatar lash Committed by GitHub

Introduce soc validator (#347)

parent fa22e982
......@@ -7,7 +7,7 @@ require (
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd // indirect
github.com/coreos/go-semver v0.3.0
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect
github.com/ethersphere/bmt v0.1.1
github.com/ethersphere/bmt v0.1.2
github.com/gogo/protobuf v1.3.1
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
github.com/golang/mock v1.4.3 // indirect
......
......@@ -108,6 +108,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/ethersphere/bmt v0.1.1 h1:vwHSJwnDyzJ0fqP3YQBDk+/vqdAfulfRGJesQ5kL2ps=
github.com/ethersphere/bmt v0.1.1/go.mod h1:fqRBDmYwn3lX2MH4lkImXQgFWeNP8ikLkS/hgi/HRws=
github.com/ethersphere/bmt v0.1.2 h1:FEuvQY9xuK+rDp3VwDVyde8T396Matv/u9PdtKa2r9Q=
github.com/ethersphere/bmt v0.1.2/go.mod h1:fqRBDmYwn3lX2MH4lkImXQgFWeNP8ikLkS/hgi/HRws=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
......
// Copyright 2020 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package contains convenience methods and validator for content-addressed chunks
package content
import (
"encoding/binary"
"errors"
"fmt"
"github.com/ethersphere/bee/pkg/swarm"
bmtlegacy "github.com/ethersphere/bmt/legacy"
)
// NewChunk creates a new content-addressed single-span chunk.
// The length of the chunk data is set as the span.
func NewChunk(data []byte) (swarm.Chunk, error) {
return NewChunkWithSpan(data, int64(len(data)))
}
// NewChunkWithSpan creates a new content-addressed chunk from given data and span.
func NewChunkWithSpan(data []byte, span int64) (swarm.Chunk, error) {
if len(data) > swarm.ChunkSize {
return nil, errors.New("max chunk size exceeded")
}
if span < swarm.ChunkSize && span != int64(len(data)) {
return nil, fmt.Errorf("single-span chunk size mismatch; span is %d, chunk data length %d", span, len(data))
}
bmtPool := bmtlegacy.NewTreePool(swarm.NewHasher, swarm.Branches, bmtlegacy.PoolSize)
hasher := bmtlegacy.New(bmtPool)
// execute hash, compare and return result
spanBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(spanBytes, uint64(span))
err := hasher.SetSpanBytes(spanBytes)
if err != nil {
return nil, err
}
_, err = hasher.Write(data)
if err != nil {
return nil, err
}
s := hasher.Sum(nil)
payload := append(spanBytes, data...)
address := swarm.NewAddress(s)
return swarm.NewChunk(address, payload), nil
}
// NewChunkWithSpanBytes deserializes a content-addressed chunk from separate
// data and span byte slices.
func NewChunkWithSpanBytes(data, spanBytes []byte) (swarm.Chunk, error) {
bmtPool := bmtlegacy.NewTreePool(swarm.NewHasher, swarm.Branches, bmtlegacy.PoolSize)
hasher := bmtlegacy.New(bmtPool)
// execute hash, compare and return result
err := hasher.SetSpanBytes(spanBytes)
if err != nil {
return nil, err
}
_, err = hasher.Write(data)
if err != nil {
return nil, err
}
s := hasher.Sum(nil)
payload := append(spanBytes, data...)
address := swarm.NewAddress(s)
return swarm.NewChunk(address, payload), nil
}
// contentChunkFromBytes deserializes a content-addressed chunk.
func contentChunkFromBytes(chunkData []byte) (swarm.Chunk, error) {
if len(chunkData) < swarm.SpanSize {
return nil, errors.New("shorter than minimum length")
}
return NewChunkWithSpanBytes(chunkData[8:], chunkData[:8])
}
// Copyright 2020 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package content_test
import (
"encoding/binary"
"testing"
"github.com/ethersphere/bee/pkg/content"
"github.com/ethersphere/bee/pkg/swarm"
)
// TestChunkWithSpan verifies creation of content addressed chunk from
// byte data.
func TestChunk(t *testing.T) {
bmtHashOfFoo := "2387e8e7d8a48c2a9339c97c1dc3461a9a7aa07e994c5cb8b38fd7c1b3e6ea48"
address := swarm.MustParseHexAddress(bmtHashOfFoo)
c, err := content.NewChunk([]byte("foo"))
if err != nil {
t.Fatal(err)
}
if !address.Equal(c.Address()) {
t.Fatal("address mismatch")
}
}
// TestChunkWithSpan verifies creation of content addressed chunk from
// payload data and span in integer form.
func TestChunkWithSpan(t *testing.T) {
bmtHashOfFoo := "2387e8e7d8a48c2a9339c97c1dc3461a9a7aa07e994c5cb8b38fd7c1b3e6ea48"
address := swarm.MustParseHexAddress(bmtHashOfFoo)
data := []byte("foo")
c, err := content.NewChunkWithSpan(data, int64(len(data)))
if err != nil {
t.Fatal(err)
}
if !address.Equal(c.Address()) {
t.Fatal("address mismatch")
}
}
// TestChunkWithSpanBytes verifies creation of content addressed chunk from
// payload data and span in byte form.
func TestChunkWithSpanBytes(t *testing.T) {
bmtHashOfFoo := "2387e8e7d8a48c2a9339c97c1dc3461a9a7aa07e994c5cb8b38fd7c1b3e6ea48"
address := swarm.MustParseHexAddress(bmtHashOfFoo)
data := []byte("foo")
span := len(data)
spanBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(spanBytes, uint64(span))
c, err := content.NewChunkWithSpanBytes(data, spanBytes)
if err != nil {
t.Fatal(err)
}
if !address.Equal(c.Address()) {
t.Fatal("address mismatch")
}
}
......@@ -2,56 +2,32 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package validator contains file-oriented chunk validation implementations
package validator
package content
import (
"encoding/binary"
"hash"
"github.com/ethersphere/bee/pkg/swarm"
bmtlegacy "github.com/ethersphere/bmt/legacy"
"golang.org/x/crypto/sha3"
)
var _ swarm.ChunkValidator = (*ContentAddressValidator)(nil)
func hashFunc() hash.Hash {
return sha3.NewLegacyKeccak256()
}
// ContentAddressValidator validates that the address of a given chunk
// is the content address of its contents
// is the content address of its contents.
type ContentAddressValidator struct {
}
// New constructs a new ContentAddressValidator
// NewContentAddressValidator constructs a new ContentAddressValidator
func NewContentAddressValidator() swarm.ChunkValidator {
return &ContentAddressValidator{}
}
// Validate performs the validation check
// Validate performs the validation check.
func (v *ContentAddressValidator) Validate(ch swarm.Chunk) (valid bool) {
p := bmtlegacy.NewTreePool(hashFunc, swarm.Branches, bmtlegacy.PoolSize)
hasher := bmtlegacy.New(p)
// prepare data
data := ch.Data()
address := ch.Address()
span := binary.LittleEndian.Uint64(data[:8])
// execute hash, compare and return result
hasher.Reset()
err := hasher.SetSpan(int64(span))
chunkData := ch.Data()
rch, err := contentChunkFromBytes(chunkData)
if err != nil {
return false
}
_, err = hasher.Write(data[8:])
if err != nil {
return false
}
s := hasher.Sum(nil)
return address.Equal(swarm.NewAddress(s))
address := ch.Address()
return address.Equal(rch.Address())
}
// Copyright 2020 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package validator_test
package content_test
import (
"encoding/binary"
"testing"
"github.com/ethersphere/bee/pkg/content"
"github.com/ethersphere/bee/pkg/swarm"
"github.com/ethersphere/bee/pkg/validator"
)
// TestContentAddressValidator checks that the validator evaluates correctly
......@@ -16,7 +16,7 @@ import (
func TestContentAddressValidator(t *testing.T) {
// instantiate validator
validator := validator.NewContentAddressValidator()
validator := content.NewContentAddressValidator()
// generate address from pre-generated hex of 'foo' from legacy bmt
bmtHashOfFoo := "2387e8e7d8a48c2a9339c97c1dc3461a9a7aa07e994c5cb8b38fd7c1b3e6ea48"
......
......@@ -17,6 +17,10 @@ import (
"golang.org/x/crypto/sha3"
)
const (
AddressSize = 20
)
// NewOverlayAddress constructs a Swarm Address from ECDSA private key.
func NewOverlayAddress(p ecdsa.PublicKey, networkID uint64) (swarm.Address, error) {
ethAddr, err := NewEthereumAddress(p)
......
......@@ -19,6 +19,7 @@ import (
"github.com/ethersphere/bee/pkg/accounting"
"github.com/ethersphere/bee/pkg/addressbook"
"github.com/ethersphere/bee/pkg/api"
"github.com/ethersphere/bee/pkg/content"
"github.com/ethersphere/bee/pkg/crypto"
"github.com/ethersphere/bee/pkg/debugapi"
"github.com/ethersphere/bee/pkg/hive"
......@@ -45,7 +46,6 @@ import (
"github.com/ethersphere/bee/pkg/storage"
"github.com/ethersphere/bee/pkg/tags"
"github.com/ethersphere/bee/pkg/tracing"
"github.com/ethersphere/bee/pkg/validator"
ma "github.com/multiformats/go-multiaddr"
"github.com/sirupsen/logrus"
"golang.org/x/sync/errgroup"
......@@ -254,7 +254,7 @@ func NewBee(o Options) (*Bee, error) {
return nil, fmt.Errorf("retrieval service: %w", err)
}
ns := netstore.New(storer, retrieve, validator.NewContentAddressValidator())
ns := netstore.New(storer, retrieve, content.NewContentAddressValidator())
retrieve.SetStorer(ns)
......
package soc
var (
ToSignDigest = toSignDigest
RecoverAddress = recoverAddress
)
// Copyright 2020 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package swarm contains most basic and general Swarm concepts.
package soc
import (
"bytes"
"errors"
"fmt"
"github.com/ethersphere/bee/pkg/content"
"github.com/ethersphere/bee/pkg/crypto"
"github.com/ethersphere/bee/pkg/swarm"
)
const (
IdSize = 32
SignatureSize = 65
AddressSize = crypto.AddressSize
minChunkSize = IdSize + SignatureSize + swarm.SpanSize
)
// Id is a soc identifier
type Id []byte
// Owner is a wrapper that enforces valid length address of soc owner.
type Owner struct {
address []byte
}
// NewOwner creates a new Owner.
func NewOwner(address []byte) (*Owner, error) {
if len(address) != AddressSize {
return nil, fmt.Errorf("invalid address %x", address)
}
return &Owner{
address: address,
}, nil
}
// Soc wraps a single soc.
type Soc struct {
id Id
signature []byte
signer crypto.Signer
owner *Owner
Chunk swarm.Chunk
}
// NewChunk is a convenience function to create a single-owner chunk ready to be sent
// on the network.
func NewChunk(id Id, ch swarm.Chunk, signer crypto.Signer) (swarm.Chunk, error) {
s := New(id, ch)
err := s.AddSigner(signer)
if err != nil {
return nil, err
}
return s.ToChunk()
}
// New creates a new Soc representation from arbitrary soc id and
// a content-addressed chunk.
//
// By default the span of the soc data is set to the length
// of the payload.
func New(id Id, ch swarm.Chunk) *Soc {
return &Soc{
id: id,
Chunk: ch,
}
}
// WithOwnerAddress provides the possibility of setting the ethereum
// address for the owner of an soc in the absence of a signer.
func (s *Soc) WithOwnerAddress(ownerAddress *Owner) *Soc {
s.owner = ownerAddress
return s
}
// AddSigner currently sets a single signer for the soc.
//
// This method will overwrite any value set with WithOwnerAddress with
// the address derived from the given signer.
func (s *Soc) AddSigner(signer crypto.Signer) error {
publicKey, err := signer.PublicKey()
if err != nil {
return err
}
ownerAddressBytes, err := crypto.NewEthereumAddress(*publicKey)
if err != nil {
return err
}
ownerAddress, err := NewOwner(ownerAddressBytes)
if err != nil {
return err
}
s.signer = signer
s.owner = ownerAddress
return nil
}
// OwnerAddress returns the ethereum address of the signer of the Chunk.
func (s *Soc) OwnerAddress() []byte {
return s.owner.address
}
// Address returns the soc Chunk address.
func (s *Soc) Address() (swarm.Address, error) {
return CreateAddress(s.id, s.owner)
}
// FromChunk recreates an Soc representation from swarm.Chunk data.
func FromChunk(sch swarm.Chunk) (*Soc, error) {
chunkData := sch.Data()
if len(chunkData) < minChunkSize {
return nil, errors.New("less than minimum length")
}
// add all the data fields
s := &Soc{}
cursor := 0
s.id = chunkData[cursor : cursor+IdSize]
cursor += IdSize
s.signature = chunkData[cursor : cursor+SignatureSize]
cursor += SignatureSize
spanBytes := chunkData[cursor : cursor+swarm.SpanSize]
cursor += swarm.SpanSize
ch, err := content.NewChunkWithSpanBytes(chunkData[cursor:], spanBytes)
if err != nil {
return nil, err
}
toSignBytes, err := toSignDigest(s.id, ch.Address().Bytes())
if err != nil {
return nil, err
}
// recover owner information
recoveredEthereumAddress, err := recoverAddress(s.signature, toSignBytes)
if err != nil {
return nil, err
}
owner, err := NewOwner(recoveredEthereumAddress)
if err != nil {
return nil, err
}
s.owner = owner
s.Chunk = ch
return s, nil
}
// ToChunk generates a signed chunk payload ready for submission to the swarm network.
//
// The method will fail if no signer has been added.
func (s *Soc) ToChunk() (swarm.Chunk, error) {
var err error
if s.signer == nil {
return nil, errors.New("signer missing")
}
// generate the data to sign
toSignBytes, err := toSignDigest(s.id, s.Chunk.Address().Bytes())
if err != nil {
return nil, err
}
// sign the chunk
signature, err := s.signer.Sign(toSignBytes)
if err != nil {
return nil, err
}
// prepare the payload
buf := bytes.NewBuffer(nil)
buf.Write(s.id)
buf.Write(signature)
buf.Write(s.Chunk.Data())
// create chunk
socAddress, err := s.Address()
if err != nil {
return nil, err
}
return swarm.NewChunk(socAddress, buf.Bytes()), nil
}
// toSignDigest creates a digest suitable for signing to represent the soc.
func toSignDigest(id Id, sum []byte) ([]byte, error) {
h := swarm.NewHasher()
_, err := h.Write(id)
if err != nil {
return nil, err
}
_, err = h.Write(sum)
if err != nil {
return nil, err
}
return h.Sum(nil), nil
}
// CreateAddress creates a new soc address from the soc id and the ethereum address of the signer
func CreateAddress(id Id, owner *Owner) (swarm.Address, error) {
h := swarm.NewHasher()
_, err := h.Write(id)
if err != nil {
return swarm.ZeroAddress, err
}
_, err = h.Write(owner.address)
if err != nil {
return swarm.ZeroAddress, err
}
sum := h.Sum(nil)
return swarm.NewAddress(sum), nil
}
// recoverOwner returns the ethereum address of the owner of an soc.
func recoverAddress(signature, digest []byte) ([]byte, error) {
recoveredPublicKey, err := crypto.Recover(signature, digest)
if err != nil {
return nil, err
}
recoveredEthereumAddress, err := crypto.NewEthereumAddress(*recoveredPublicKey)
if err != nil {
return nil, err
}
return recoveredEthereumAddress, nil
}
// Copyright 2020 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package swarm contains most basic and general Swarm concepts.
package soc_test
import (
"bytes"
"encoding/binary"
"testing"
"github.com/ethersphere/bee/pkg/content"
"github.com/ethersphere/bee/pkg/crypto"
"github.com/ethersphere/bee/pkg/soc"
"github.com/ethersphere/bee/pkg/swarm"
)
// TestToChunk verifies that the chunk create from the Soc object
// corresponds to the soc spec.
func TestToChunk(t *testing.T) {
privKey, err := crypto.GenerateSecp256k1Key()
if err != nil {
t.Fatal(err)
}
signer := crypto.NewDefaultSigner(privKey)
id := make([]byte, 32)
payload := []byte("foo")
ch, err := content.NewChunk(payload)
if err != nil {
t.Fatal(err)
}
sch, err := soc.NewChunk(id, ch, signer)
if err != nil {
t.Fatal(err)
}
chunkData := sch.Data()
// verify that id, signature, payload is in place
cursor := 0
if !bytes.Equal(chunkData[cursor:cursor+soc.IdSize], id) {
t.Fatal("id mismatch")
}
cursor += soc.IdSize
signature := chunkData[cursor : cursor+soc.SignatureSize]
cursor += soc.SignatureSize
spanBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(spanBytes, uint64(len(payload)))
if !bytes.Equal(chunkData[cursor:cursor+swarm.SpanSize], spanBytes) {
t.Fatal("span mismatch")
}
cursor += swarm.SpanSize
if !bytes.Equal(chunkData[cursor:], payload) {
t.Fatal("payload mismatch")
}
// get the public key of the signer that was used
publicKey, err := signer.PublicKey()
if err != nil {
t.Fatal(err)
}
ethereumAddress, err := crypto.NewEthereumAddress(*publicKey)
if err != nil {
t.Fatal(err)
}
toSignBytes, err := soc.ToSignDigest(id, ch.Address().Bytes())
if err != nil {
t.Fatal(err)
}
// verify owner match
recoveredEthereumAddress, err := soc.RecoverAddress(signature, toSignBytes)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(recoveredEthereumAddress, ethereumAddress) {
t.Fatalf("address mismatch %x %x", recoveredEthereumAddress, ethereumAddress)
}
}
// TestFromChunk verifies that valid chunk data deserializes to
// a fully populated Chunk object.
func TestFromChunk(t *testing.T) {
privKey, err := crypto.GenerateSecp256k1Key()
if err != nil {
t.Fatal(err)
}
signer := crypto.NewDefaultSigner(privKey)
id := make([]byte, 32)
payload := []byte("foo")
ch, err := content.NewChunk(payload)
if err != nil {
t.Fatal(err)
}
sch, err := soc.NewChunk(id, ch, signer)
if err != nil {
t.Fatal(err)
}
u2, err := soc.FromChunk(sch)
if err != nil {
t.Fatal(err)
}
// owner matching means the address was successfully recovered from
// payload and signature
publicKey, err := signer.PublicKey()
if err != nil {
t.Fatal(err)
}
ownerEthereumAddress, err := crypto.NewEthereumAddress(*publicKey)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(ownerEthereumAddress, u2.OwnerAddress()) {
t.Fatalf("owner address mismatch %x %x", ownerEthereumAddress, u2.OwnerAddress())
}
}
// Copyright 2020 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package soc
import (
"github.com/ethersphere/bee/pkg/swarm"
)
var _ swarm.ChunkValidator = (*SocValidator)(nil)
// SocVaildator validates that the address of a given chunk
// is a single-owner chunk.
type SocValidator struct {
}
// NewSocValidator creates a new SocValidator.
func NewSocValidator() swarm.ChunkValidator {
return &SocValidator{}
}
// Validate performs the validation check.
func (v *SocValidator) Validate(ch swarm.Chunk) (valid bool) {
s, err := FromChunk(ch)
if err != nil {
return false
}
address, err := s.Address()
if err != nil {
return false
}
return ch.Address().Equal(address)
}
// Copyright 2020 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package swarm contains most basic and general Swarm concepts.
package soc_test
import (
"encoding/binary"
"testing"
"github.com/ethersphere/bee/pkg/crypto"
"github.com/ethersphere/bee/pkg/soc"
"github.com/ethersphere/bee/pkg/swarm"
)
// TestSocValidator verifies that the validator can detect both
// valid soc chunks, as well as chunks with invalid data and invalid
// address.
func TestSocValidator(t *testing.T) {
id := make([]byte, soc.IdSize)
privKey, err := crypto.GenerateSecp256k1Key()
if err != nil {
t.Fatal(err)
}
signer := crypto.NewDefaultSigner(privKey)
bmtHashOfFoo := "2387e8e7d8a48c2a9339c97c1dc3461a9a7aa07e994c5cb8b38fd7c1b3e6ea48"
address := swarm.MustParseHexAddress(bmtHashOfFoo)
foo := "foo"
fooLength := len(foo)
fooBytes := make([]byte, 8+fooLength)
binary.LittleEndian.PutUint64(fooBytes, uint64(fooLength))
copy(fooBytes[8:], foo)
ch := swarm.NewChunk(address, fooBytes)
sch, err := soc.NewChunk(id, ch, signer)
if err != nil {
t.Fatal(err)
}
// check valid chunk
v := soc.NewSocValidator()
if !v.Validate(sch) {
t.Fatal("valid chunk evaluates to invalid")
}
// check invalid data
sch.Data()[0] = 0x01
if v.Validate(sch) {
t.Fatal("chunk with invalid data evaluates to valid")
}
// check invalid address
sch.Data()[0] = 0x00
wrongAddressBytes := sch.Address().Bytes()
wrongAddressBytes[0] ^= wrongAddressBytes[0]
wrongAddress := swarm.NewAddress(wrongAddressBytes)
sch = swarm.NewChunk(wrongAddress, sch.Data())
if v.Validate(sch) {
t.Fatal("chunk with invalid address evaluates to valid")
}
}
......@@ -15,13 +15,13 @@ import (
)
const (
SpanSize = 8
SectionSize = 32
Branches = 128
ChunkSize = SectionSize * Branches
HashSize = 32
MaxPO uint8 = 15
MaxBins = MaxPO + 1
SpanSize = 8
ChunkWithSpanSize = ChunkSize + SpanSize
)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment