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