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  }