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