access_test.go 14 KB

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