access_test.go 16 KB

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