github.com/divan/go-ethereum@v1.8.14-0.20180820134928-1de9ada4016d/cmd/swarm/access_test.go (about)

     1  // Copyright 2018 The go-ethereum Authors
     2  // This file is part of go-ethereum.
     3  //
     4  // go-ethereum is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // go-ethereum is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU General Public License
    15  // along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
    16  package main
    17  
    18  import (
    19  	"bytes"
    20  	"crypto/rand"
    21  	"encoding/hex"
    22  	"encoding/json"
    23  	"io"
    24  	"io/ioutil"
    25  	gorand "math/rand"
    26  	"net/http"
    27  	"os"
    28  	"path/filepath"
    29  	"strings"
    30  	"testing"
    31  	"time"
    32  
    33  	"github.com/ethereum/go-ethereum/crypto"
    34  	"github.com/ethereum/go-ethereum/crypto/sha3"
    35  	"github.com/ethereum/go-ethereum/log"
    36  	"github.com/ethereum/go-ethereum/swarm/api"
    37  	swarm "github.com/ethereum/go-ethereum/swarm/api/client"
    38  )
    39  
    40  // TestAccessPassword tests for the correct creation of an ACT manifest protected by a password.
    41  // The test creates bogus content, uploads it encrypted, then creates the wrapping manifest with the Access entry
    42  // The parties participating - node (publisher), uploads to second node then disappears. Content which was uploaded
    43  // is then fetched through 2nd node. since the tested code is not key-aware - we can just
    44  // fetch from the 2nd node using HTTP BasicAuth
    45  func TestAccessPassword(t *testing.T) {
    46  	cluster := newTestCluster(t, 1)
    47  	defer cluster.Shutdown()
    48  	proxyNode := cluster.Nodes[0]
    49  
    50  	// create a tmp file
    51  	tmp, err := ioutil.TempDir("", "swarm-test")
    52  	if err != nil {
    53  		t.Fatal(err)
    54  	}
    55  	defer os.RemoveAll(tmp)
    56  
    57  	// write data to file
    58  	data := "notsorandomdata"
    59  	dataFilename := filepath.Join(tmp, "data.txt")
    60  
    61  	err = ioutil.WriteFile(dataFilename, []byte(data), 0666)
    62  	if err != nil {
    63  		t.Fatal(err)
    64  	}
    65  
    66  	hashRegexp := `[a-f\d]{128}`
    67  
    68  	// upload the file with 'swarm up' and expect a hash
    69  	up := runSwarm(t,
    70  		"--bzzapi",
    71  		proxyNode.URL, //it doesn't matter through which node we upload content
    72  		"up",
    73  		"--encrypt",
    74  		dataFilename)
    75  	_, matches := up.ExpectRegexp(hashRegexp)
    76  	up.ExpectExit()
    77  
    78  	if len(matches) < 1 {
    79  		t.Fatal("no matches found")
    80  	}
    81  
    82  	ref := matches[0]
    83  
    84  	password := "smth"
    85  	passwordFilename := filepath.Join(tmp, "password.txt")
    86  
    87  	err = ioutil.WriteFile(passwordFilename, []byte(password), 0666)
    88  	if err != nil {
    89  		t.Fatal(err)
    90  	}
    91  
    92  	up = runSwarm(t,
    93  		"access",
    94  		"new",
    95  		"pass",
    96  		"--dry-run",
    97  		"--password",
    98  		passwordFilename,
    99  		ref,
   100  	)
   101  
   102  	_, matches = up.ExpectRegexp(".+")
   103  	up.ExpectExit()
   104  
   105  	if len(matches) == 0 {
   106  		t.Fatalf("stdout not matched")
   107  	}
   108  
   109  	var m api.Manifest
   110  
   111  	err = json.Unmarshal([]byte(matches[0]), &m)
   112  	if err != nil {
   113  		t.Fatalf("unmarshal manifest: %v", err)
   114  	}
   115  
   116  	if len(m.Entries) != 1 {
   117  		t.Fatalf("expected one manifest entry, got %v", len(m.Entries))
   118  	}
   119  
   120  	e := m.Entries[0]
   121  
   122  	ct := "application/bzz-manifest+json"
   123  	if e.ContentType != ct {
   124  		t.Errorf("expected %q content type, got %q", ct, e.ContentType)
   125  	}
   126  
   127  	if e.Access == nil {
   128  		t.Fatal("manifest access is nil")
   129  	}
   130  
   131  	a := e.Access
   132  
   133  	if a.Type != "pass" {
   134  		t.Errorf(`got access type %q, expected "pass"`, a.Type)
   135  	}
   136  	if len(a.Salt) < 32 {
   137  		t.Errorf(`got salt with length %v, expected not less the 32 bytes`, len(a.Salt))
   138  	}
   139  	if a.KdfParams == nil {
   140  		t.Fatal("manifest access kdf params is nil")
   141  	}
   142  
   143  	client := swarm.NewClient(cluster.Nodes[0].URL)
   144  
   145  	hash, err := client.UploadManifest(&m, false)
   146  	if err != nil {
   147  		t.Fatal(err)
   148  	}
   149  
   150  	httpClient := &http.Client{}
   151  
   152  	url := cluster.Nodes[0].URL + "/" + "bzz:/" + hash
   153  	response, err := httpClient.Get(url)
   154  	if err != nil {
   155  		t.Fatal(err)
   156  	}
   157  	if response.StatusCode != http.StatusUnauthorized {
   158  		t.Fatal("should be a 401")
   159  	}
   160  	authHeader := response.Header.Get("WWW-Authenticate")
   161  	if authHeader == "" {
   162  		t.Fatal("should be something here")
   163  	}
   164  
   165  	req, err := http.NewRequest(http.MethodGet, url, nil)
   166  	if err != nil {
   167  		t.Fatal(err)
   168  	}
   169  	req.SetBasicAuth("", password)
   170  
   171  	response, err = http.DefaultClient.Do(req)
   172  	if err != nil {
   173  		t.Fatal(err)
   174  	}
   175  	defer response.Body.Close()
   176  
   177  	if response.StatusCode != http.StatusOK {
   178  		t.Errorf("expected status %v, got %v", http.StatusOK, response.StatusCode)
   179  	}
   180  	d, err := ioutil.ReadAll(response.Body)
   181  	if err != nil {
   182  		t.Fatal(err)
   183  	}
   184  	if string(d) != data {
   185  		t.Errorf("expected decrypted data %q, got %q", data, string(d))
   186  	}
   187  
   188  	wrongPasswordFilename := filepath.Join(tmp, "password-wrong.txt")
   189  
   190  	err = ioutil.WriteFile(wrongPasswordFilename, []byte("just wr0ng"), 0666)
   191  	if err != nil {
   192  		t.Fatal(err)
   193  	}
   194  
   195  	//download file with 'swarm down' with wrong password
   196  	up = runSwarm(t,
   197  		"--bzzapi",
   198  		proxyNode.URL,
   199  		"down",
   200  		"bzz:/"+hash,
   201  		tmp,
   202  		"--password",
   203  		wrongPasswordFilename)
   204  
   205  	_, matches = up.ExpectRegexp("unauthorized")
   206  	if len(matches) != 1 && matches[0] != "unauthorized" {
   207  		t.Fatal(`"unauthorized" not found in output"`)
   208  	}
   209  	up.ExpectExit()
   210  }
   211  
   212  // TestAccessPK tests for the correct creation of an ACT manifest between two parties (publisher and grantee).
   213  // The test creates bogus content, uploads it encrypted, then creates the wrapping manifest with the Access entry
   214  // The parties participating - node (publisher), uploads to second node (which is also the grantee) then disappears.
   215  // Content which was uploaded is then fetched through the grantee's http proxy. Since the tested code is private-key aware,
   216  // the test will fail if the proxy's given private key is not granted on the ACT.
   217  func TestAccessPK(t *testing.T) {
   218  	// Setup Swarm and upload a test file to it
   219  	cluster := newTestCluster(t, 1)
   220  	defer cluster.Shutdown()
   221  
   222  	// create a tmp file
   223  	tmp, err := ioutil.TempFile("", "swarm-test")
   224  	if err != nil {
   225  		t.Fatal(err)
   226  	}
   227  	defer tmp.Close()
   228  	defer os.Remove(tmp.Name())
   229  
   230  	// write data to file
   231  	data := "notsorandomdata"
   232  	_, err = io.WriteString(tmp, data)
   233  	if err != nil {
   234  		t.Fatal(err)
   235  	}
   236  
   237  	hashRegexp := `[a-f\d]{128}`
   238  
   239  	// upload the file with 'swarm up' and expect a hash
   240  	up := runSwarm(t,
   241  		"--bzzapi",
   242  		cluster.Nodes[0].URL,
   243  		"up",
   244  		"--encrypt",
   245  		tmp.Name())
   246  	_, matches := up.ExpectRegexp(hashRegexp)
   247  	up.ExpectExit()
   248  
   249  	if len(matches) < 1 {
   250  		t.Fatal("no matches found")
   251  	}
   252  
   253  	ref := matches[0]
   254  
   255  	pk := cluster.Nodes[0].PrivateKey
   256  	granteePubKey := crypto.CompressPubkey(&pk.PublicKey)
   257  
   258  	publisherDir, err := ioutil.TempDir("", "swarm-account-dir-temp")
   259  	if err != nil {
   260  		t.Fatal(err)
   261  	}
   262  
   263  	passFile, err := ioutil.TempFile("", "swarm-test")
   264  	if err != nil {
   265  		t.Fatal(err)
   266  	}
   267  	defer passFile.Close()
   268  	defer os.Remove(passFile.Name())
   269  	_, err = io.WriteString(passFile, testPassphrase)
   270  	if err != nil {
   271  		t.Fatal(err)
   272  	}
   273  	_, publisherAccount := getTestAccount(t, publisherDir)
   274  	up = runSwarm(t,
   275  		"--bzzaccount",
   276  		publisherAccount.Address.String(),
   277  		"--password",
   278  		passFile.Name(),
   279  		"--datadir",
   280  		publisherDir,
   281  		"--bzzapi",
   282  		cluster.Nodes[0].URL,
   283  		"access",
   284  		"new",
   285  		"pk",
   286  		"--dry-run",
   287  		"--grant-key",
   288  		hex.EncodeToString(granteePubKey),
   289  		ref,
   290  	)
   291  
   292  	_, matches = up.ExpectRegexp(".+")
   293  	up.ExpectExit()
   294  
   295  	if len(matches) == 0 {
   296  		t.Fatalf("stdout not matched")
   297  	}
   298  
   299  	var m api.Manifest
   300  
   301  	err = json.Unmarshal([]byte(matches[0]), &m)
   302  	if err != nil {
   303  		t.Fatalf("unmarshal manifest: %v", err)
   304  	}
   305  
   306  	if len(m.Entries) != 1 {
   307  		t.Fatalf("expected one manifest entry, got %v", len(m.Entries))
   308  	}
   309  
   310  	e := m.Entries[0]
   311  
   312  	ct := "application/bzz-manifest+json"
   313  	if e.ContentType != ct {
   314  		t.Errorf("expected %q content type, got %q", ct, e.ContentType)
   315  	}
   316  
   317  	if e.Access == nil {
   318  		t.Fatal("manifest access is nil")
   319  	}
   320  
   321  	a := e.Access
   322  
   323  	if a.Type != "pk" {
   324  		t.Errorf(`got access type %q, expected "pk"`, a.Type)
   325  	}
   326  	if len(a.Salt) < 32 {
   327  		t.Errorf(`got salt with length %v, expected not less the 32 bytes`, len(a.Salt))
   328  	}
   329  	if a.KdfParams != nil {
   330  		t.Fatal("manifest access kdf params should be nil")
   331  	}
   332  
   333  	client := swarm.NewClient(cluster.Nodes[0].URL)
   334  
   335  	hash, err := client.UploadManifest(&m, false)
   336  	if err != nil {
   337  		t.Fatal(err)
   338  	}
   339  
   340  	httpClient := &http.Client{}
   341  
   342  	url := cluster.Nodes[0].URL + "/" + "bzz:/" + hash
   343  	response, err := httpClient.Get(url)
   344  	if err != nil {
   345  		t.Fatal(err)
   346  	}
   347  	if response.StatusCode != http.StatusOK {
   348  		t.Fatal("should be a 200")
   349  	}
   350  	d, err := ioutil.ReadAll(response.Body)
   351  	if err != nil {
   352  		t.Fatal(err)
   353  	}
   354  	if string(d) != data {
   355  		t.Errorf("expected decrypted data %q, got %q", data, string(d))
   356  	}
   357  }
   358  
   359  // TestAccessACT tests the e2e creation, uploading and downloading of an ACT type access control
   360  // the test fires up a 3 node cluster, then randomly picks 2 nodes which will be acting as grantees to the data
   361  // set. the third node should fail decoding the reference as it will not be granted access. the publisher uploads through
   362  // one of the nodes then disappears.
   363  func TestAccessACT(t *testing.T) {
   364  	// Setup Swarm and upload a test file to it
   365  	cluster := newTestCluster(t, 3)
   366  	defer cluster.Shutdown()
   367  
   368  	var uploadThroughNode = cluster.Nodes[0]
   369  	client := swarm.NewClient(uploadThroughNode.URL)
   370  
   371  	r1 := gorand.New(gorand.NewSource(time.Now().UnixNano()))
   372  	nodeToSkip := r1.Intn(3) // a number between 0 and 2 (node indices in `cluster`)
   373  	// create a tmp file
   374  	tmp, err := ioutil.TempFile("", "swarm-test")
   375  	if err != nil {
   376  		t.Fatal(err)
   377  	}
   378  	defer tmp.Close()
   379  	defer os.Remove(tmp.Name())
   380  
   381  	// write data to file
   382  	data := "notsorandomdata"
   383  	_, err = io.WriteString(tmp, data)
   384  	if err != nil {
   385  		t.Fatal(err)
   386  	}
   387  
   388  	hashRegexp := `[a-f\d]{128}`
   389  
   390  	// upload the file with 'swarm up' and expect a hash
   391  	up := runSwarm(t,
   392  		"--bzzapi",
   393  		cluster.Nodes[0].URL,
   394  		"up",
   395  		"--encrypt",
   396  		tmp.Name())
   397  	_, matches := up.ExpectRegexp(hashRegexp)
   398  	up.ExpectExit()
   399  
   400  	if len(matches) < 1 {
   401  		t.Fatal("no matches found")
   402  	}
   403  
   404  	ref := matches[0]
   405  	grantees := []string{}
   406  	for i, v := range cluster.Nodes {
   407  		if i == nodeToSkip {
   408  			continue
   409  		}
   410  		pk := v.PrivateKey
   411  		granteePubKey := crypto.CompressPubkey(&pk.PublicKey)
   412  		grantees = append(grantees, hex.EncodeToString(granteePubKey))
   413  	}
   414  
   415  	granteesPubkeyListFile, err := ioutil.TempFile("", "grantees-pubkey-list.csv")
   416  	if err != nil {
   417  		t.Fatal(err)
   418  	}
   419  
   420  	_, err = granteesPubkeyListFile.WriteString(strings.Join(grantees, "\n"))
   421  	if err != nil {
   422  		t.Fatal(err)
   423  	}
   424  
   425  	defer granteesPubkeyListFile.Close()
   426  	defer os.Remove(granteesPubkeyListFile.Name())
   427  
   428  	publisherDir, err := ioutil.TempDir("", "swarm-account-dir-temp")
   429  	if err != nil {
   430  		t.Fatal(err)
   431  	}
   432  
   433  	passFile, err := ioutil.TempFile("", "swarm-test")
   434  	if err != nil {
   435  		t.Fatal(err)
   436  	}
   437  	defer passFile.Close()
   438  	defer os.Remove(passFile.Name())
   439  	_, err = io.WriteString(passFile, testPassphrase)
   440  	if err != nil {
   441  		t.Fatal(err)
   442  	}
   443  
   444  	_, publisherAccount := getTestAccount(t, publisherDir)
   445  	up = runSwarm(t,
   446  		"--bzzaccount",
   447  		publisherAccount.Address.String(),
   448  		"--password",
   449  		passFile.Name(),
   450  		"--datadir",
   451  		publisherDir,
   452  		"--bzzapi",
   453  		cluster.Nodes[0].URL,
   454  		"access",
   455  		"new",
   456  		"act",
   457  		"--grant-keys",
   458  		granteesPubkeyListFile.Name(),
   459  		ref,
   460  	)
   461  
   462  	_, matches = up.ExpectRegexp(`[a-f\d]{64}`)
   463  	up.ExpectExit()
   464  
   465  	if len(matches) == 0 {
   466  		t.Fatalf("stdout not matched")
   467  	}
   468  	hash := matches[0]
   469  	m, _, err := client.DownloadManifest(hash)
   470  	if err != nil {
   471  		t.Fatalf("unmarshal manifest: %v", err)
   472  	}
   473  
   474  	if len(m.Entries) != 1 {
   475  		t.Fatalf("expected one manifest entry, got %v", len(m.Entries))
   476  	}
   477  
   478  	e := m.Entries[0]
   479  
   480  	ct := "application/bzz-manifest+json"
   481  	if e.ContentType != ct {
   482  		t.Errorf("expected %q content type, got %q", ct, e.ContentType)
   483  	}
   484  
   485  	if e.Access == nil {
   486  		t.Fatal("manifest access is nil")
   487  	}
   488  
   489  	a := e.Access
   490  
   491  	if a.Type != "act" {
   492  		t.Fatalf(`got access type %q, expected "act"`, a.Type)
   493  	}
   494  	if len(a.Salt) < 32 {
   495  		t.Fatalf(`got salt with length %v, expected not less the 32 bytes`, len(a.Salt))
   496  	}
   497  	if a.KdfParams != nil {
   498  		t.Fatal("manifest access kdf params should be nil")
   499  	}
   500  
   501  	httpClient := &http.Client{}
   502  
   503  	// all nodes except the skipped node should be able to decrypt the content
   504  	for i, node := range cluster.Nodes {
   505  		log.Debug("trying to fetch from node", "node index", i)
   506  
   507  		url := node.URL + "/" + "bzz:/" + hash
   508  		response, err := httpClient.Get(url)
   509  		if err != nil {
   510  			t.Fatal(err)
   511  		}
   512  		log.Debug("got response from node", "response code", response.StatusCode)
   513  
   514  		if i == nodeToSkip {
   515  			log.Debug("reached node to skip", "status code", response.StatusCode)
   516  
   517  			if response.StatusCode != http.StatusUnauthorized {
   518  				t.Fatalf("should be a 401")
   519  			}
   520  
   521  			continue
   522  		}
   523  
   524  		if response.StatusCode != http.StatusOK {
   525  			t.Fatal("should be a 200")
   526  		}
   527  		d, err := ioutil.ReadAll(response.Body)
   528  		if err != nil {
   529  			t.Fatal(err)
   530  		}
   531  		if string(d) != data {
   532  			t.Errorf("expected decrypted data %q, got %q", data, string(d))
   533  		}
   534  	}
   535  }
   536  
   537  // TestKeypairSanity is a sanity test for the crypto scheme for ACT. it asserts the correct shared secret according to
   538  // the specs at https://github.com/ethersphere/swarm-docs/blob/eb857afda906c6e7bb90d37f3f334ccce5eef230/act.md
   539  func TestKeypairSanity(t *testing.T) {
   540  	salt := make([]byte, 32)
   541  	if _, err := io.ReadFull(rand.Reader, salt); err != nil {
   542  		t.Fatalf("reading from crypto/rand failed: %v", err.Error())
   543  	}
   544  	sharedSecret := "a85586744a1ddd56a7ed9f33fa24f40dd745b3a941be296a0d60e329dbdb896d"
   545  
   546  	for i, v := range []struct {
   547  		publisherPriv string
   548  		granteePub    string
   549  	}{
   550  		{
   551  			publisherPriv: "ec5541555f3bc6376788425e9d1a62f55a82901683fd7062c5eddcc373a73459",
   552  			granteePub:    "0226f213613e843a413ad35b40f193910d26eb35f00154afcde9ded57479a6224a",
   553  		},
   554  		{
   555  			publisherPriv: "70c7a73011aa56584a0009ab874794ee7e5652fd0c6911cd02f8b6267dd82d2d",
   556  			granteePub:    "02e6f8d5e28faaa899744972bb847b6eb805a160494690c9ee7197ae9f619181db",
   557  		},
   558  	} {
   559  		b, _ := hex.DecodeString(v.granteePub)
   560  		granteePub, _ := crypto.DecompressPubkey(b)
   561  		publisherPrivate, _ := crypto.HexToECDSA(v.publisherPriv)
   562  
   563  		ssKey, err := api.NewSessionKeyPK(publisherPrivate, granteePub, salt)
   564  		if err != nil {
   565  			t.Fatal(err)
   566  		}
   567  
   568  		hasher := sha3.NewKeccak256()
   569  		hasher.Write(salt)
   570  		shared, err := hex.DecodeString(sharedSecret)
   571  		if err != nil {
   572  			t.Fatal(err)
   573  		}
   574  		hasher.Write(shared)
   575  		sum := hasher.Sum(nil)
   576  
   577  		if !bytes.Equal(ssKey, sum) {
   578  			t.Fatalf("%d: got a session key mismatch", i)
   579  		}
   580  	}
   581  }