access_test.go 16 KB

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