Преглед изворни кода

Add support for fileless OSL mechanisms

Rod Hynes пре 7 месеци
родитељ
комит
d1c20289ed
2 измењених фајлова са 155 додато и 10 уклоњено
  1. 115 7
      psiphon/common/osl/osl.go
  2. 40 3
      psiphon/common/osl/osl_test.go

+ 115 - 7
psiphon/common/osl/osl.go

@@ -86,6 +86,10 @@ type Scheme struct {
 	// specified in UTC and must be a multiple of SeedPeriodNanoseconds.
 	Epoch string
 
+	// PaveDataOSLCount indicates how many active OSLs GetPaveData should
+	// return. Must be must be > 0 when using GetPaveData.
+	PaveDataOSLCount int
+
 	// Regions is a list of client country codes this scheme applies to.
 	// If empty, the scheme applies to all regions.
 	Regions []string
@@ -790,10 +794,13 @@ type Registry struct {
 // MD5 is not cryptographically secure and this checksum is not
 // relied upon for OSL verification. MD5 is used for compatibility
 // with out-of-band distribution hosts.
+//
+// OSLFileSpec supports compact CBOR encoding for use in alternative,
+// fileless mechanisms.
 type OSLFileSpec struct {
-	ID        []byte
-	KeyShares *KeyShares
-	MD5Sum    []byte
+	ID        []byte     `cbor:"1,keyasint,omitempty"`
+	KeyShares *KeyShares `cbor:"2,keyasint,omitempty"`
+	MD5Sum    []byte     `cbor:"3,keyasint,omitempty"`
 }
 
 // KeyShares is a tree data structure which describes the
@@ -802,11 +809,14 @@ type OSLFileSpec struct {
 // are required to reconstruct the secret key. The keys for BoxedShares
 // are either SLOKs (referenced by SLOK ID) or random keys that are
 // themselves split as described in child KeyShares.
+//
+// KeyShares supports compact CBOR encoding for use in alternative,
+// fileless mechanisms.
 type KeyShares struct {
-	Threshold   int
-	BoxedShares [][]byte
-	SLOKIDs     [][]byte
-	KeyShares   []*KeyShares
+	Threshold   int          `cbor:"1,keyasint,omitempty"`
+	BoxedShares [][]byte     `cbor:"2,keyasint,omitempty"`
+	SLOKIDs     [][]byte     `cbor:"3,keyasint,omitempty"`
+	KeyShares   []*KeyShares `cbor:"4,keyasint,omitempty"`
 }
 
 type PaveLogInfo struct {
@@ -996,6 +1006,84 @@ func (config *Config) CurrentOSLIDs(schemeIndex int) (map[string]string, error)
 	return OSLIDs, nil
 }
 
+// PaveData is the per-OSL data used by Pave, for use in alternative, fileless
+// mechanisms, such as proof-of-knowledge of keys. PaveData.FileSpec is the
+// OSL FileSpec that would be paved into the registry file, and
+// PaveData.FileKey is the key that would be used to encrypted OSL files.
+type PaveData struct {
+	FileSpec *OSLFileSpec
+	FileKey  []byte
+}
+
+// GetPaveData returns, for each propagation channel ID in the specified
+// scheme, the list of OSL PaveData for the Config.PaveDataOSLCount most
+// recent OSLs from now. GetPaveData is the equivilent of Pave that is for
+// use in alternative, fileless mechanisms, such as proof-of-knowledge of
+// keys
+func (config *Config) GetPaveData(schemeIndex int) (map[string][]*PaveData, error) {
+
+	config.ReloadableFile.RLock()
+	defer config.ReloadableFile.RUnlock()
+
+	if schemeIndex < 0 || schemeIndex >= len(config.Schemes) {
+		return nil, errors.TraceNew("invalid scheme index")
+	}
+
+	scheme := config.Schemes[schemeIndex]
+
+	oslDuration := scheme.GetOSLDuration()
+
+	// Using PaveDataOSLCount, initialize startTime and EndTime values that
+	// are similar to the Pave inputs. As in Pave, logic in the following
+	// loop will align these time values to actual OSL periods.
+
+	if scheme.PaveDataOSLCount < 1 {
+		return nil, errors.TraceNew("invalid OSL count")
+	}
+	endTime := time.Now()
+	startTime := endTime.Add(-time.Duration(scheme.PaveDataOSLCount) * oslDuration)
+	if startTime.Before(scheme.epoch) {
+		startTime = scheme.epoch
+	}
+
+	allPaveData := make(map[string][]*PaveData)
+
+	for _, propagationChannelID := range scheme.PropagationChannelIDs {
+
+		if !common.Contains(scheme.PropagationChannelIDs, propagationChannelID) {
+			return nil, errors.TraceNew("invalid propagationChannelID")
+		}
+
+		var paveData []*PaveData
+
+		oslTime := scheme.epoch
+
+		if !startTime.IsZero() && !startTime.Before(scheme.epoch) {
+			for oslTime.Before(startTime) {
+				oslTime = oslTime.Add(oslDuration)
+			}
+		}
+
+		for !oslTime.After(endTime) {
+
+			firstSLOKTime := oslTime
+			fileKey, fileSpec, err := makeOSLFileSpec(
+				scheme, propagationChannelID, firstSLOKTime)
+			if err != nil {
+				return nil, errors.Trace(err)
+			}
+
+			paveData = append(paveData, &PaveData{FileSpec: fileSpec, FileKey: fileKey})
+
+			oslTime = oslTime.Add(oslDuration)
+		}
+
+		allPaveData[propagationChannelID] = paveData
+	}
+
+	return allPaveData, nil
+}
+
 // makeOSLFileSpec creates an OSL file key, splits it according to the
 // scheme's key splits, and sets the OSL ID as its first SLOK ID. The
 // returned key is used to encrypt the OSL payload and then discarded;
@@ -1487,6 +1575,26 @@ func NewOSLReader(
 		signingPublicKey)
 }
 
+// ReassembleOSLKey returns a reassembled OSL key, for use in alternative,
+// fileless mechanisms, such as proof-of-knowledge of keys.
+func ReassembleOSLKey(
+	fileSpec *OSLFileSpec,
+	lookup SLOKLookup) (bool, []byte, error) {
+
+	ok, fileKey, err := fileSpec.KeyShares.reassembleKey(lookup, true)
+	if err != nil {
+		return false, nil, errors.Trace(err)
+	}
+	if !ok {
+		return false, nil, nil
+	}
+	if len(fileKey) != KEY_LENGTH_BYTES {
+		return false, nil, errors.TraceNew("invalid key length")
+	}
+
+	return true, fileKey, nil
+}
+
 // zeroReader reads an unlimited stream of zeroes.
 type zeroReader struct {
 }

+ 40 - 3
psiphon/common/osl/osl_test.go

@@ -40,6 +40,8 @@ func TestOSL(t *testing.T) {
     {
       "Epoch" : "%s",
 
+      "PaveDataOSLCount" : %d,
+
       "Regions" : ["US", "CA"],
 
       "PropagationChannelIDs" : ["2995DB0C968C59C4F23E87988D9C0D41", "E742C25A6D8BA8C17F37E725FA628569", "B4A780E67695595FA486E9B900EA7335"],
@@ -101,6 +103,8 @@ func TestOSL(t *testing.T) {
     {
       "Epoch" : "%s",
 
+      "PaveDataOSLCount" : %d,
+
       "Regions" : ["US", "CA"],
 
       "PropagationChannelIDs" : ["36F1CF2DF1250BF0C7BA0629CE3DC657", "B4A780E67695595FA486E9B900EA7335"],
@@ -157,11 +161,14 @@ func TestOSL(t *testing.T) {
   ]
 }
 `
+	// Pave sufficient OSLs to cover simulated elapsed time of all test cases.
+	paveOSLCount := 1000
+
 	seedPeriod := 5 * time.Millisecond // "SeedPeriodNanoseconds" : 5000000
 	now := time.Now().UTC()
 	epoch := now.Add(-seedPeriod).Truncate(seedPeriod)
 	epochStr := epoch.Format(time.RFC3339Nano)
-	configJSON := fmt.Sprintf(configJSONTemplate, epochStr, epochStr)
+	configJSON := fmt.Sprintf(configJSONTemplate, epochStr, paveOSLCount, epochStr, paveOSLCount)
 
 	// The first scheme requires sufficient activity within 5/10 5 millisecond
 	// periods and 5/10 50 millisecond longer periods. The second scheme requires
@@ -387,8 +394,7 @@ func TestOSL(t *testing.T) {
 
 	t.Run("pave OSLs", func(t *testing.T) {
 
-		// Pave sufficient OSLs to cover simulated elapsed time of all test cases.
-		endTime := epoch.Add(1000 * seedPeriod)
+		endTime := epoch.Add(time.Duration(paveOSLCount) * seedPeriod)
 
 		// In actual deployment, paved files for each propagation channel ID
 		// are dropped in distinct distribution sites.
@@ -569,6 +575,7 @@ func TestOSL(t *testing.T) {
 			[]int{0},
 			0,
 		},
+
 		{
 			"single split scheme: sufficient SLOKs",
 			singleSplitPropagationChannelID,
@@ -718,6 +725,36 @@ func TestOSL(t *testing.T) {
 			if seededOSLCount != testCase.expectedOSLCount {
 				t.Fatalf("expected %d OSLs got %d", testCase.expectedOSLCount, seededOSLCount)
 			}
+
+			// Test the alternative, file-less API.
+
+			seededOSLCount = 0
+			for schemeIndex := range len(config.Schemes) {
+				schemePaveData, err := config.GetPaveData(schemeIndex)
+				if err != nil {
+					t.Fatalf("GetPaveData failed: %s", err)
+				}
+				propagationChannelPaveData, ok := schemePaveData[testCase.propagationChannelID]
+				if !ok {
+					continue
+				}
+				for _, paveData := range propagationChannelPaveData {
+					ok, reassembledKey, err := ReassembleOSLKey(paveData.FileSpec, lookupSLOKs)
+					if err != nil {
+						t.Fatalf("ReassembleOSLKey failed: %s", err)
+					}
+					if ok {
+						if !bytes.Equal(reassembledKey, paveData.FileKey) {
+							t.Fatalf("unexpected reassembled key")
+						}
+						seededOSLCount++
+					}
+				}
+			}
+
+			if seededOSLCount != testCase.expectedOSLCount {
+				t.Fatalf("expected %d OSLs got %d", testCase.expectedOSLCount, seededOSLCount)
+			}
 		})
 	}
 }