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