github.com/psiphon-labs/psiphon-tunnel-core@v2.0.28+incompatible/psiphon/common/osl/osl_test.go (about) 1 /* 2 * Copyright (c) 2016, Psiphon Inc. 3 * All rights reserved. 4 * 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation, either version 3 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package osl 21 22 import ( 23 "bytes" 24 "encoding/base64" 25 "encoding/hex" 26 "fmt" 27 "io/ioutil" 28 "net" 29 "testing" 30 "time" 31 32 "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common" 33 ) 34 35 func TestOSL(t *testing.T) { 36 37 configJSONTemplate := ` 38 { 39 "Schemes" : [ 40 { 41 "Epoch" : "%s", 42 43 "Regions" : ["US", "CA"], 44 45 "PropagationChannelIDs" : ["2995DB0C968C59C4F23E87988D9C0D41", "E742C25A6D8BA8C17F37E725FA628569", "B4A780E67695595FA486E9B900EA7335"], 46 47 "MasterKey" : "wFuSbqU/pJ/35vRmoM8T9ys1PgDa8uzJps1Y+FNKa5U=", 48 49 "SeedSpecs" : [ 50 { 51 "Description": "spec1", 52 "ID" : "IXHWfVgWFkEKvgqsjmnJuN3FpaGuCzQMETya+DSQvsk=", 53 "UpstreamSubnets" : ["192.168.0.0/16", "172.16.0.0/12"], 54 "Targets" : 55 { 56 "BytesRead" : 1, 57 "BytesWritten" : 1, 58 "PortForwardDurationNanoseconds" : 1 59 } 60 }, 61 { 62 "Description": "spec2", 63 "ID" : "qvpIcORLE2Pi5TZmqRtVkEp+OKov0MhfsYPLNV7FYtI=", 64 "UpstreamSubnets" : ["192.168.0.0/16", "10.0.0.0/8"], 65 "Targets" : 66 { 67 "BytesRead" : 10, 68 "BytesWritten" : 10, 69 "PortForwardDurationNanoseconds" : 10 70 } 71 }, 72 { 73 "Description": "spec3", 74 "ID" : "ts5LInjFHbVKX+/C5/bSJqUh+cLT5kJy92TZGLvAtPU=", 75 "UpstreamSubnets" : ["100.64.0.0/10"], 76 "Targets" : 77 { 78 "BytesRead" : 100, 79 "BytesWritten" : 100, 80 "PortForwardDurationNanoseconds" : 100 81 } 82 } 83 ], 84 85 "SeedSpecThreshold" : 2, 86 87 "SeedPeriodNanoseconds" : 5000000, 88 89 "SeedPeriodKeySplits": [ 90 { 91 "Total": 10, 92 "Threshold": 5 93 }, 94 { 95 "Total": 10, 96 "Threshold": 5 97 } 98 ] 99 }, 100 { 101 "Epoch" : "%s", 102 103 "Regions" : ["US", "CA"], 104 105 "PropagationChannelIDs" : ["36F1CF2DF1250BF0C7BA0629CE3DC657", "B4A780E67695595FA486E9B900EA7335"], 106 107 "MasterKey" : "fcyQy8JSxLXHt/Iom9Qj9wMnSjrsccTiiSPEsJicet4=", 108 109 "SeedSpecs" : [ 110 { 111 "Description": "spec1", 112 "ID" : "NXY0/4lqMxx5XIszIhMbwHobH/qb2Gl0Bw/OGndc1vM=", 113 "UpstreamSubnets" : ["192.168.0.0/16", "172.16.0.0/12"], 114 "Targets" : 115 { 116 "BytesRead" : 1, 117 "BytesWritten" : 1, 118 "PortForwardDurationNanoseconds" : 1 119 } 120 }, 121 { 122 "Description": "spec2", 123 "ID" : "o78G6muv3idtbQKXoU05tF6gTlQj1LHmNe0eUWkZGxs=", 124 "UpstreamSubnets" : ["192.168.0.0/16", "10.0.0.0/8"], 125 "Targets" : 126 { 127 "BytesRead" : 10, 128 "BytesWritten" : 10, 129 "PortForwardDurationNanoseconds" : 10 130 } 131 }, 132 { 133 "Description": "spec3", 134 "ID" : "1DlAvJYpoSEfcqMXYBV7bDEtYu3LCQO39ISD5tmi8Uo=", 135 "UpstreamSubnets" : ["100.64.0.0/10"], 136 "Targets" : 137 { 138 "BytesRead" : 0, 139 "BytesWritten" : 0, 140 "PortForwardDurationNanoseconds" : 0 141 } 142 } 143 ], 144 145 "SeedSpecThreshold" : 2, 146 147 "SeedPeriodNanoseconds" : 5000000, 148 149 "SeedPeriodKeySplits": [ 150 { 151 "Total": 100, 152 "Threshold": 25 153 } 154 ] 155 } 156 ] 157 } 158 ` 159 seedPeriod := 5 * time.Millisecond // "SeedPeriodNanoseconds" : 5000000 160 now := time.Now().UTC() 161 epoch := now.Add(-seedPeriod).Truncate(seedPeriod) 162 epochStr := epoch.Format(time.RFC3339Nano) 163 configJSON := fmt.Sprintf(configJSONTemplate, epochStr, epochStr) 164 165 // The first scheme requires sufficient activity within 5/10 5 millisecond 166 // periods and 5/10 50 millisecond longer periods. The second scheme requires 167 // sufficient activity within 25/100 5 millisecond periods. 168 169 config, err := LoadConfig([]byte(configJSON)) 170 if err != nil { 171 t.Fatalf("LoadConfig failed: %s", err) 172 } 173 174 t.Run("ineligible client, sufficient transfer", func(t *testing.T) { 175 176 clientSeedState := config.NewClientSeedState("US", "C5E8D2EDFD093B50D8D65CF59D0263CA", nil) 177 178 seedPortForward := clientSeedState.NewClientSeedPortForward(net.ParseIP("192.168.0.1")) 179 180 if seedPortForward != nil { 181 t.Fatalf("expected nil client seed port forward") 182 } 183 }) 184 185 // This clientSeedState is used across multiple tests. 186 signalIssueSLOKs := make(chan struct{}, 1) 187 clientSeedState := config.NewClientSeedState("US", "2995DB0C968C59C4F23E87988D9C0D41", signalIssueSLOKs) 188 189 t.Run("eligible client, no transfer", func(t *testing.T) { 190 191 if len(clientSeedState.GetSeedPayload().SLOKs) != 0 { 192 t.Fatalf("expected 0 SLOKs, got %d", len(clientSeedState.GetSeedPayload().SLOKs)) 193 } 194 }) 195 196 t.Run("eligible client, insufficient transfer", func(t *testing.T) { 197 198 clientSeedState.NewClientSeedPortForward(net.ParseIP("10.0.0.1")).UpdateProgress(5, 5, 5) 199 200 if len(clientSeedState.GetSeedPayload().SLOKs) != 0 { 201 t.Fatalf("expected 0 SLOKs, got %d", len(clientSeedState.GetSeedPayload().SLOKs)) 202 } 203 }) 204 205 rolloverToNextSLOKTime := func() { 206 // Rollover to the next SLOK time, so accrued data transfer will be reset. 207 now := time.Now().UTC() 208 time.Sleep(now.Add(seedPeriod).Truncate(seedPeriod).Sub(now)) 209 } 210 211 t.Run("eligible client, insufficient transfer after rollover", func(t *testing.T) { 212 213 rolloverToNextSLOKTime() 214 215 clientSeedState.NewClientSeedPortForward(net.ParseIP("10.0.0.1")).UpdateProgress(5, 5, 5) 216 217 if len(clientSeedState.GetSeedPayload().SLOKs) != 0 { 218 t.Fatalf("expected 0 SLOKs, got %d", len(clientSeedState.GetSeedPayload().SLOKs)) 219 } 220 }) 221 222 t.Run("eligible client, sufficient transfer, one port forward", func(t *testing.T) { 223 224 rolloverToNextSLOKTime() 225 226 clientSeedPortForward := clientSeedState.NewClientSeedPortForward(net.ParseIP("10.0.0.1")) 227 228 clientSeedPortForward.UpdateProgress(5, 5, 5) 229 230 clientSeedPortForward.UpdateProgress(5, 5, 5) 231 232 select { 233 case <-signalIssueSLOKs: 234 default: 235 t.Fatalf("expected issue SLOKs signal") 236 } 237 238 if len(clientSeedState.GetSeedPayload().SLOKs) != 1 { 239 t.Fatalf("expected 1 SLOKs, got %d", len(clientSeedState.GetSeedPayload().SLOKs)) 240 } 241 }) 242 243 t.Run("eligible client, sufficient transfer, multiple port forwards", func(t *testing.T) { 244 245 rolloverToNextSLOKTime() 246 247 clientSeedState.NewClientSeedPortForward(net.ParseIP("10.0.0.1")).UpdateProgress(5, 5, 5) 248 249 clientSeedState.NewClientSeedPortForward(net.ParseIP("10.0.0.1")).UpdateProgress(5, 5, 5) 250 251 select { 252 case <-signalIssueSLOKs: 253 default: 254 t.Fatalf("expected issue SLOKs signal") 255 } 256 257 // Expect 2 SLOKS: 1 new, and 1 remaining in payload. 258 if len(clientSeedState.GetSeedPayload().SLOKs) != 2 { 259 t.Fatalf("expected 2 SLOKs, got %d", len(clientSeedState.GetSeedPayload().SLOKs)) 260 } 261 }) 262 263 t.Run("eligible client, sufficient transfer multiple SLOKs", func(t *testing.T) { 264 265 rolloverToNextSLOKTime() 266 267 clientSeedState.NewClientSeedPortForward(net.ParseIP("192.168.0.1")).UpdateProgress(5, 5, 5) 268 269 clientSeedState.NewClientSeedPortForward(net.ParseIP("10.0.0.1")).UpdateProgress(5, 5, 5) 270 271 select { 272 case <-signalIssueSLOKs: 273 default: 274 t.Fatalf("expected issue SLOKs signal") 275 } 276 277 // Expect 4 SLOKS: 2 new, and 2 remaining in payload. 278 if len(clientSeedState.GetSeedPayload().SLOKs) != 4 { 279 t.Fatalf("expected 4 SLOKs, got %d", len(clientSeedState.GetSeedPayload().SLOKs)) 280 } 281 }) 282 283 t.Run("clear payload", func(t *testing.T) { 284 clientSeedState.ClearSeedPayload() 285 286 if len(clientSeedState.GetSeedPayload().SLOKs) != 0 { 287 t.Fatalf("expected 0 SLOKs, got %d", len(clientSeedState.GetSeedPayload().SLOKs)) 288 } 289 }) 290 291 t.Run("no transfer required", func(t *testing.T) { 292 293 rolloverToNextSLOKTime() 294 295 clientSeedState := config.NewClientSeedState("US", "36F1CF2DF1250BF0C7BA0629CE3DC657", nil) 296 297 if len(clientSeedState.GetSeedPayload().SLOKs) != 1 { 298 t.Fatalf("expected 1 SLOKs, got %d", len(clientSeedState.GetSeedPayload().SLOKs)) 299 } 300 }) 301 302 t.Run("concurrent schemes", func(t *testing.T) { 303 304 rolloverToNextSLOKTime() 305 306 clientSeedState := config.NewClientSeedState("US", "B4A780E67695595FA486E9B900EA7335", nil) 307 308 clientSeedPortForward := clientSeedState.NewClientSeedPortForward(net.ParseIP("192.168.0.1")) 309 310 clientSeedPortForward.UpdateProgress(10, 10, 10) 311 312 if len(clientSeedState.GetSeedPayload().SLOKs) != 5 { 313 t.Fatalf("expected 5 SLOKs, got %d", len(clientSeedState.GetSeedPayload().SLOKs)) 314 } 315 }) 316 317 signingPublicKey, signingPrivateKey, err := common.GenerateAuthenticatedDataPackageKeys() 318 if err != nil { 319 t.Fatalf("GenerateAuthenticatedDataPackageKeys failed: %s", err) 320 } 321 322 pavedRegistries := make(map[string][]byte) 323 pavedOSLFileContents := make(map[string]map[string][]byte) 324 325 t.Run("pave OSLs", func(t *testing.T) { 326 327 // Pave sufficient OSLs to cover simulated elapsed time of all test cases. 328 endTime := epoch.Add(1000 * seedPeriod) 329 330 // In actual deployment, paved files for each propagation channel ID 331 // are dropped in distinct distribution sites. 332 for _, propagationChannelID := range []string{ 333 "2995DB0C968C59C4F23E87988D9C0D41", 334 "E742C25A6D8BA8C17F37E725FA628569", 335 "36F1CF2DF1250BF0C7BA0629CE3DC657"} { 336 337 // Dummy server entry payloads will be the OSL ID, which the following 338 // tests use to verify that the correct OSL file decrypts successfully. 339 paveServerEntries := make(map[string][]string) 340 for _, scheme := range config.Schemes { 341 342 oslDuration := scheme.GetOSLDuration() 343 344 oslTime := scheme.epoch 345 for oslTime.Before(endTime) { 346 347 firstSLOKRef := &slokReference{ 348 PropagationChannelID: propagationChannelID, 349 SeedSpecID: string(scheme.SeedSpecs[0].ID), 350 Time: oslTime, 351 } 352 firstSLOK := scheme.deriveSLOK(firstSLOKRef) 353 oslID := firstSLOK.ID 354 paveServerEntries[hex.EncodeToString(oslID)] = 355 []string{base64.StdEncoding.EncodeToString(oslID)} 356 357 oslTime = oslTime.Add(oslDuration) 358 } 359 } 360 361 // Note: these options are exercised in remoteServerList_test.go 362 omitMD5SumsSchemes := []int{} 363 omitEmptyOSLsSchemes := []int{} 364 365 firstPaveFiles, err := config.Pave( 366 time.Time{}, 367 endTime, 368 propagationChannelID, 369 signingPublicKey, 370 signingPrivateKey, 371 paveServerEntries, 372 omitMD5SumsSchemes, 373 omitEmptyOSLsSchemes, 374 nil) 375 if err != nil { 376 t.Fatalf("Pave failed: %s", err) 377 } 378 379 offsetPaveFiles, err := config.Pave( 380 epoch.Add(500*seedPeriod+seedPeriod/2), 381 endTime, 382 propagationChannelID, 383 signingPublicKey, 384 signingPrivateKey, 385 paveServerEntries, 386 omitMD5SumsSchemes, 387 omitEmptyOSLsSchemes, 388 nil) 389 if err != nil { 390 t.Fatalf("Pave failed: %s", err) 391 } 392 393 paveFiles, err := config.Pave( 394 time.Time{}, 395 endTime, 396 propagationChannelID, 397 signingPublicKey, 398 signingPrivateKey, 399 paveServerEntries, 400 omitMD5SumsSchemes, 401 omitEmptyOSLsSchemes, 402 nil) 403 if err != nil { 404 t.Fatalf("Pave failed: %s", err) 405 } 406 407 // Check that the paved file name matches the name the client will look for. 408 409 if len(paveFiles) < 1 || paveFiles[len(paveFiles)-1].Name != GetOSLRegistryURL("") { 410 t.Fatalf("invalid registry pave file") 411 } 412 413 // Check that the content of two paves is the same: all the crypto should be 414 // deterministic. 415 416 for index, paveFile := range paveFiles { 417 if paveFile.Name != firstPaveFiles[index].Name { 418 t.Fatalf("pave name mismatch") 419 } 420 if !bytes.Equal(paveFile.Contents, firstPaveFiles[index].Contents) { 421 t.Fatalf("pave content mismatch") 422 } 423 } 424 425 // Check that the output of a pave using an unaligned offset from epoch 426 // produces a subset of OSLs with the same IDs and content: the OSL and 427 // SLOK time slots must align. 428 429 if len(offsetPaveFiles) >= len(paveFiles) { 430 t.Fatalf("unexpected pave size") 431 } 432 433 for _, offsetPaveFile := range offsetPaveFiles { 434 found := false 435 for _, paveFile := range paveFiles { 436 if offsetPaveFile.Name == paveFile.Name { 437 if offsetPaveFile.Name != GetOSLRegistryURL("") && 438 !bytes.Equal(offsetPaveFile.Contents, paveFile.Contents) { 439 t.Fatalf("pave content mismatch") 440 } 441 found = true 442 break 443 } 444 } 445 if !found { 446 t.Fatalf("pave name missing") 447 } 448 } 449 450 // Use the paved content in the following tests. 451 452 pavedRegistries[propagationChannelID] = paveFiles[len(paveFiles)-1].Contents 453 454 pavedOSLFileContents[propagationChannelID] = make(map[string][]byte) 455 for _, paveFile := range paveFiles[0:] { 456 pavedOSLFileContents[propagationChannelID][paveFile.Name] = paveFile.Contents 457 } 458 } 459 }) 460 461 if len(pavedRegistries) != 3 { 462 // Previous subtest failed. Following tests cannot be completed, so abort. 463 t.Fatalf("pave failed") 464 } 465 466 // To ensure SLOKs are issued at precise time periods, the following tests 467 // bypass ClientSeedState and derive SLOKs directly. 468 469 expandRanges := func(ranges ...[2]int) []int { 470 a := make([]int, 0) 471 for _, r := range ranges { 472 for n := r[0]; n <= r[1]; n++ { 473 a = append(a, n) 474 } 475 } 476 return a 477 } 478 479 singleSplitPropagationChannelID := "36F1CF2DF1250BF0C7BA0629CE3DC657" 480 singleSplitScheme := config.Schemes[1] 481 482 doubleSplitPropagationChannelID := "2995DB0C968C59C4F23E87988D9C0D41" 483 doubleSplitScheme := config.Schemes[0] 484 485 keySplitTestCases := []struct { 486 description string 487 propagationChannelID string 488 scheme *Scheme 489 issueSLOKTimePeriods []int 490 issueSLOKSeedSpecIndexes []int 491 expectedOSLCount int 492 }{ 493 { 494 "single split scheme: insufficient SLOK periods", 495 singleSplitPropagationChannelID, 496 singleSplitScheme, 497 expandRanges([2]int{0, 23}), 498 []int{0, 1}, 499 0, 500 }, 501 { 502 "single split scheme: insufficient SLOK seed specs", 503 singleSplitPropagationChannelID, 504 singleSplitScheme, 505 expandRanges([2]int{0, 23}), 506 []int{0}, 507 0, 508 }, 509 { 510 "single split scheme: sufficient SLOKs", 511 singleSplitPropagationChannelID, 512 singleSplitScheme, 513 expandRanges([2]int{0, 24}), 514 []int{0, 1}, 515 1, 516 }, 517 { 518 "single split scheme: sufficient SLOKs (alternative seed specs)", 519 singleSplitPropagationChannelID, 520 singleSplitScheme, 521 expandRanges([2]int{0, 24}), 522 []int{1, 2}, 523 1, 524 }, 525 { 526 "single split scheme: more than sufficient SLOKs", 527 singleSplitPropagationChannelID, 528 singleSplitScheme, 529 expandRanges([2]int{0, 49}), 530 []int{0, 1}, 531 1, 532 }, 533 { 534 "double split scheme: insufficient SLOK periods", 535 doubleSplitPropagationChannelID, 536 doubleSplitScheme, 537 expandRanges([2]int{0, 4}, [2]int{10, 14}, [2]int{20, 24}, [2]int{30, 34}, [2]int{40, 43}), 538 []int{0, 1}, 539 0, 540 }, 541 { 542 "double split scheme: insufficient SLOK period spread", 543 doubleSplitPropagationChannelID, 544 doubleSplitScheme, 545 expandRanges([2]int{0, 25}), 546 []int{0, 1}, 547 0, 548 }, 549 { 550 "double split scheme: insufficient SLOK seed specs", 551 doubleSplitPropagationChannelID, 552 doubleSplitScheme, 553 expandRanges([2]int{0, 4}, [2]int{10, 14}, [2]int{20, 24}, [2]int{30, 34}, [2]int{40, 44}), 554 []int{0}, 555 0, 556 }, 557 { 558 "double split scheme: sufficient SLOKs", 559 doubleSplitPropagationChannelID, 560 doubleSplitScheme, 561 expandRanges([2]int{0, 4}, [2]int{10, 14}, [2]int{20, 24}, [2]int{30, 34}, [2]int{40, 44}), 562 []int{0, 1}, 563 1, 564 }, 565 { 566 "double split scheme: sufficient SLOKs (alternative seed specs)", 567 doubleSplitPropagationChannelID, 568 doubleSplitScheme, 569 expandRanges([2]int{0, 4}, [2]int{10, 14}, [2]int{20, 24}, [2]int{30, 34}, [2]int{40, 44}), 570 []int{1, 2}, 571 1, 572 }, 573 } 574 575 for _, testCase := range keySplitTestCases { 576 t.Run(testCase.description, func(t *testing.T) { 577 578 slokMap := make(map[string][]byte) 579 580 for _, timePeriod := range testCase.issueSLOKTimePeriods { 581 for _, seedSpecIndex := range testCase.issueSLOKSeedSpecIndexes { 582 583 slok := testCase.scheme.deriveSLOK( 584 &slokReference{ 585 PropagationChannelID: testCase.propagationChannelID, 586 SeedSpecID: string(testCase.scheme.SeedSpecs[seedSpecIndex].ID), 587 Time: epoch.Add(time.Duration(timePeriod) * seedPeriod), 588 }) 589 590 slokMap[string(slok.ID)] = slok.Key 591 592 } 593 } 594 595 startTime := time.Now() 596 597 lookupSLOKs := func(slokID []byte) []byte { 598 return slokMap[string(slokID)] 599 } 600 601 registryStreamer, err := NewRegistryStreamer( 602 bytes.NewReader(pavedRegistries[testCase.propagationChannelID]), 603 signingPublicKey, 604 lookupSLOKs) 605 if err != nil { 606 t.Fatalf("NewRegistryStreamer failed: %s", err) 607 } 608 609 seededOSLCount := 0 610 611 for { 612 613 fileSpec, err := registryStreamer.Next() 614 if err != nil { 615 t.Fatalf("Next failed: %s", err) 616 } 617 618 if fileSpec == nil { 619 break 620 } 621 622 seededOSLCount += 1 623 624 oslFileContents, ok := 625 pavedOSLFileContents[testCase.propagationChannelID][GetOSLFileURL("", fileSpec.ID)] 626 if !ok { 627 t.Fatalf("unknown OSL file name") 628 } 629 630 payloadReader, err := NewOSLReader( 631 bytes.NewReader(oslFileContents), 632 fileSpec, 633 lookupSLOKs, 634 signingPublicKey) 635 if err != nil { 636 t.Fatalf("NewOSLReader failed: %s", err) 637 } 638 639 payload, err := ioutil.ReadAll(payloadReader) 640 if err != nil { 641 t.Fatalf("ReadAll failed: %s", err) 642 } 643 644 // The decrypted OSL should contain its own ID. 645 if string(payload) != base64.StdEncoding.EncodeToString(fileSpec.ID) { 646 t.Fatalf("unexpected OSL file contents") 647 } 648 } 649 650 t.Logf("registry size: %d", len(pavedRegistries[testCase.propagationChannelID])) 651 t.Logf("SLOK count: %d", len(slokMap)) 652 t.Logf("seeded OSL count: %d", seededOSLCount) 653 t.Logf("elapsed time: %s", time.Since(startTime)) 654 655 if seededOSLCount != testCase.expectedOSLCount { 656 t.Fatalf("expected %d OSLs got %d", testCase.expectedOSLCount, seededOSLCount) 657 } 658 }) 659 } 660 }