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