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 }