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