|
|
@@ -39,6 +39,7 @@ import (
|
|
|
"encoding/json"
|
|
|
"errors"
|
|
|
"fmt"
|
|
|
+ "io"
|
|
|
"net"
|
|
|
"net/url"
|
|
|
"path"
|
|
|
@@ -727,10 +728,6 @@ type PaveFile struct {
|
|
|
// Registry describes a set of OSL files.
|
|
|
type Registry struct {
|
|
|
FileSpecs []*OSLFileSpec
|
|
|
-
|
|
|
- // The following fields are ephemeral state.
|
|
|
-
|
|
|
- oslIDLookup map[string]*OSLFileSpec
|
|
|
}
|
|
|
|
|
|
// An OSLFileSpec includes an ID which is used to reference the
|
|
|
@@ -739,7 +736,7 @@ type Registry struct {
|
|
|
//
|
|
|
// The MD5Sum field is a checksum of the contents of the OSL file
|
|
|
// to be used to skip redownloading previously downloaded files.
|
|
|
-// MD5 is not cryptogrpahically secure and this checksum is not
|
|
|
+// 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.
|
|
|
type OSLFileSpec struct {
|
|
|
@@ -915,7 +912,7 @@ func makeOSLFileSpec(
|
|
|
// is derived from the master key and OSL ID. This deterministic
|
|
|
// derivation ensures that repeated paves of the same OSL
|
|
|
// with the same ID and same content yields the same MD5Sum
|
|
|
- // to avoid wastful downloads.
|
|
|
+ // to avoid wasteful downloads.
|
|
|
|
|
|
fileKey := deriveKeyHKDF(
|
|
|
scheme.MasterKey,
|
|
|
@@ -1054,6 +1051,72 @@ func divideKeyWithSeedSpecSLOKs(
|
|
|
}, nil
|
|
|
}
|
|
|
|
|
|
+// reassembleKey recursively traverses a KeyShares tree, determining
|
|
|
+// whether there exists suffient SLOKs to reassemble the root key and
|
|
|
+// performing the key assembly as required.
|
|
|
+func (keyShares *KeyShares) reassembleKey(lookup SLOKLookup, unboxKey bool) (bool, []byte, error) {
|
|
|
+
|
|
|
+ if (len(keyShares.SLOKIDs) > 0 && len(keyShares.KeyShares) > 0) ||
|
|
|
+ (len(keyShares.SLOKIDs) > 0 && len(keyShares.SLOKIDs) != len(keyShares.BoxedShares)) ||
|
|
|
+ (len(keyShares.KeyShares) > 0 && len(keyShares.KeyShares) != len(keyShares.BoxedShares)) {
|
|
|
+ return false, nil, common.ContextError(errors.New("unexpected KeyShares format"))
|
|
|
+ }
|
|
|
+
|
|
|
+ shareCount := 0
|
|
|
+ var shares [][]byte
|
|
|
+ if unboxKey {
|
|
|
+ // Note: shamirCombine infers share indices from slice offset, so the full
|
|
|
+ // keyShares.Total slots are allocated and missing shares are left nil.
|
|
|
+ shares = make([][]byte, len(keyShares.BoxedShares))
|
|
|
+ }
|
|
|
+ if len(keyShares.SLOKIDs) > 0 {
|
|
|
+ for i := 0; i < len(keyShares.SLOKIDs) && shareCount < keyShares.Threshold; i++ {
|
|
|
+ slokKey := lookup(keyShares.SLOKIDs[i])
|
|
|
+ if slokKey == nil {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ shareCount += 1
|
|
|
+ if unboxKey {
|
|
|
+ share, err := unbox(slokKey, keyShares.BoxedShares[i])
|
|
|
+ if err != nil {
|
|
|
+ return false, nil, common.ContextError(err)
|
|
|
+ }
|
|
|
+ shares[i] = share
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ for i := 0; i < len(keyShares.KeyShares) && shareCount < keyShares.Threshold; i++ {
|
|
|
+ ok, key, err := keyShares.KeyShares[i].reassembleKey(lookup, unboxKey)
|
|
|
+ if err != nil {
|
|
|
+ return false, nil, common.ContextError(err)
|
|
|
+ }
|
|
|
+ if !ok {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ shareCount += 1
|
|
|
+ if unboxKey {
|
|
|
+ share, err := unbox(key, keyShares.BoxedShares[i])
|
|
|
+ if err != nil {
|
|
|
+ return false, nil, common.ContextError(err)
|
|
|
+ }
|
|
|
+ shares[i] = share
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if shareCount < keyShares.Threshold {
|
|
|
+ return false, nil, nil
|
|
|
+ }
|
|
|
+
|
|
|
+ if !unboxKey {
|
|
|
+ return true, nil, nil
|
|
|
+ }
|
|
|
+
|
|
|
+ joinedKey := shamirCombine(shares)
|
|
|
+
|
|
|
+ return true, joinedKey, nil
|
|
|
+}
|
|
|
+
|
|
|
// GetOSLRegistryURL returns the URL for an OSL registry. Clients
|
|
|
// call this when fetching the registry from out-of-band
|
|
|
// distribution sites.
|
|
|
@@ -1096,194 +1159,169 @@ func GetOSLFilename(baseDirectory string, oslID []byte) string {
|
|
|
baseDirectory, fmt.Sprintf(OSL_FILENAME_FORMAT, hex.EncodeToString(oslID)))
|
|
|
}
|
|
|
|
|
|
-// UnpackRegistry validates and loads a JSON encoded OSL registry.
|
|
|
-func UnpackRegistry(
|
|
|
- registryPackage []byte, signingPublicKey string) (*Registry, []byte, error) {
|
|
|
-
|
|
|
- encodedRegistry, err := common.ReadAuthenticatedDataPackage(
|
|
|
- registryPackage, true, signingPublicKey)
|
|
|
- if err != nil {
|
|
|
- return nil, nil, common.ContextError(err)
|
|
|
- }
|
|
|
-
|
|
|
- registryJSON, err := base64.StdEncoding.DecodeString(encodedRegistry)
|
|
|
- if err != nil {
|
|
|
- return nil, nil, common.ContextError(err)
|
|
|
- }
|
|
|
+// SLOKLookup is a callback to lookup SLOK keys by ID.
|
|
|
+type SLOKLookup func([]byte) []byte
|
|
|
|
|
|
- registry, err := LoadRegistry(registryJSON)
|
|
|
- return registry, registryJSON, err
|
|
|
+// RegistryStreamer authenticates and processes a JSON encoded OSL registry.
|
|
|
+// The streamer processes the registry without loading the entire file
|
|
|
+// into memory, parsing each OSL file spec in turn and returning those
|
|
|
+// OSL file specs for which the client has sufficient SLOKs to reassemble
|
|
|
+// the OSL key and decrypt.
|
|
|
+//
|
|
|
+// At this stage, SLOK reassembly simply does SLOK ID lookups and threshold
|
|
|
+// counting and does not derive keys for every OSL. This allows the client
|
|
|
+// to defer key derivation until NewOSLReader for cases where it has not
|
|
|
+// already imported the OSL.
|
|
|
+//
|
|
|
+// The client's propagation channel ID is used implicitly: it determines the
|
|
|
+// base URL used to download the registry and OSL files. If the client has
|
|
|
+// seeded SLOKs from a propagation channel ID different than the one associated
|
|
|
+// with its present base URL, they will not appear in the registry and not
|
|
|
+// be used.
|
|
|
+type RegistryStreamer struct {
|
|
|
+ jsonDecoder *json.Decoder
|
|
|
+ lookup SLOKLookup
|
|
|
}
|
|
|
|
|
|
-// LoadRegistry loads a JSON encoded OSL registry.
|
|
|
-// Clients call this to process downloaded registry files.
|
|
|
-func LoadRegistry(registryJSON []byte) (*Registry, error) {
|
|
|
+// NewRegistryStreamer creates a new RegistryStreamer.
|
|
|
+func NewRegistryStreamer(
|
|
|
+ registryFileContent io.ReadSeeker,
|
|
|
+ signingPublicKey string,
|
|
|
+ lookup SLOKLookup) (*RegistryStreamer, error) {
|
|
|
|
|
|
- var registry Registry
|
|
|
- err := json.Unmarshal(registryJSON, ®istry)
|
|
|
+ payloadReader, err := common.NewAuthenticatedDataPackageReader(
|
|
|
+ registryFileContent, signingPublicKey)
|
|
|
if err != nil {
|
|
|
return nil, common.ContextError(err)
|
|
|
}
|
|
|
|
|
|
- registry.oslIDLookup = make(map[string]*OSLFileSpec)
|
|
|
- for _, fileSpec := range registry.FileSpecs {
|
|
|
- registry.oslIDLookup[string(fileSpec.ID)] = fileSpec
|
|
|
- }
|
|
|
+ base64Decoder := base64.NewDecoder(base64.StdEncoding, payloadReader)
|
|
|
|
|
|
- return ®istry, nil
|
|
|
-}
|
|
|
+ // A json.Decoder is used to stream the JSON payload, which
|
|
|
+ // is expected to be of the following form, corresponding
|
|
|
+ // to the Registry struct type:
|
|
|
+ //
|
|
|
+ // {"FileSpecs" : [{...}, {...}, ..., {...}]}
|
|
|
|
|
|
-// SLOKLookup is a callback to lookup SLOK keys by ID.
|
|
|
-type SLOKLookup func([]byte) []byte
|
|
|
+ jsonDecoder := json.NewDecoder(base64Decoder)
|
|
|
|
|
|
-// GetSeededOSLIDs examines each OSL in the registry and returns a list for
|
|
|
-// which the client has sufficient SLOKs to reassemble the OSL key and
|
|
|
-// decrypt. This function simply does SLOK ID lookups and threshold counting
|
|
|
-// and does not derive keys for every OSL.
|
|
|
-// The client is responsible for using the resulting list of OSL IDs to fetch
|
|
|
-// the OSL files and process.
|
|
|
-//
|
|
|
-// The client's propagation channel ID is used implicitly: it determines the
|
|
|
-// base URL used to download the registry and OSL files. If the client has
|
|
|
-// seeded SLOKs from a propagation channel ID different than the one associated
|
|
|
-// with its present base URL, they will not appear in the registry and not
|
|
|
-// be used.
|
|
|
-//
|
|
|
-// SLOKLookup is called to determine which SLOKs are seeded with the client.
|
|
|
-// errorLogger is a callback to log errors; GetSeededOSLIDs will continue to
|
|
|
-// process each candidate OSL even in the case of an error processing a
|
|
|
-// particular one.
|
|
|
-func (registry *Registry) GetSeededOSLIDs(lookup SLOKLookup, errorLogger func(error)) [][]byte {
|
|
|
-
|
|
|
- var OSLIDs [][]byte
|
|
|
- for _, fileSpec := range registry.FileSpecs {
|
|
|
- ok, _, err := fileSpec.KeyShares.reassembleKey(lookup, false)
|
|
|
- if err != nil {
|
|
|
- errorLogger(err)
|
|
|
- continue
|
|
|
- }
|
|
|
- if ok {
|
|
|
- OSLIDs = append(OSLIDs, fileSpec.ID)
|
|
|
- }
|
|
|
+ err = expectJSONDelimiter(jsonDecoder, "{")
|
|
|
+ if err != nil {
|
|
|
+ return nil, common.ContextError(err)
|
|
|
}
|
|
|
|
|
|
- return OSLIDs
|
|
|
-}
|
|
|
-
|
|
|
-// GetOSLMD5Sum returns the MD5 checksum for the specified OSL.
|
|
|
-func (registry *Registry) GetOSLMD5Sum(oslID []byte) ([]byte, error) {
|
|
|
+ token, err := jsonDecoder.Token()
|
|
|
+ if err != nil {
|
|
|
+ return nil, common.ContextError(err)
|
|
|
+ }
|
|
|
+ if name, ok := token.(string); !ok || name != "FileSpecs" {
|
|
|
+ return nil, common.ContextError(
|
|
|
+ fmt.Errorf("unexpected name: %s", name))
|
|
|
+ }
|
|
|
|
|
|
- fileSpec, ok := registry.oslIDLookup[string(oslID)]
|
|
|
- if !ok {
|
|
|
- return nil, common.ContextError(errors.New("unknown OSL ID"))
|
|
|
+ err = expectJSONDelimiter(jsonDecoder, "[")
|
|
|
+ if err != nil {
|
|
|
+ return nil, common.ContextError(err)
|
|
|
}
|
|
|
|
|
|
- return fileSpec.MD5Sum, nil
|
|
|
+ return &RegistryStreamer{
|
|
|
+ jsonDecoder: jsonDecoder,
|
|
|
+ lookup: lookup,
|
|
|
+ }, nil
|
|
|
}
|
|
|
|
|
|
-// reassembleKey recursively traverses a KeyShares tree, determining
|
|
|
-// whether there exists suffient SLOKs to reassemble the root key and
|
|
|
-// performing the key assembly as required.
|
|
|
-func (keyShares *KeyShares) reassembleKey(lookup SLOKLookup, unboxKey bool) (bool, []byte, error) {
|
|
|
+// Next returns the next OSL file spec that the client
|
|
|
+// has sufficient SLOKs to decrypt. The client calls
|
|
|
+// NewOSLReader with the file spec to process that OSL.
|
|
|
+// Next returns nil at EOF.
|
|
|
+func (s *RegistryStreamer) Next() (*OSLFileSpec, error) {
|
|
|
|
|
|
- if (len(keyShares.SLOKIDs) > 0 && len(keyShares.KeyShares) > 0) ||
|
|
|
- (len(keyShares.SLOKIDs) > 0 && len(keyShares.SLOKIDs) != len(keyShares.BoxedShares)) ||
|
|
|
- (len(keyShares.KeyShares) > 0 && len(keyShares.KeyShares) != len(keyShares.BoxedShares)) {
|
|
|
- return false, nil, common.ContextError(errors.New("unexpected KeyShares format"))
|
|
|
- }
|
|
|
+ for {
|
|
|
+ if s.jsonDecoder.More() {
|
|
|
|
|
|
- shareCount := 0
|
|
|
- var shares [][]byte
|
|
|
- if unboxKey {
|
|
|
- // Note: shamirCombine infers share indices from slice offset, so the full
|
|
|
- // keyShares.Total slots are allocated and missing shares are left nil.
|
|
|
- shares = make([][]byte, len(keyShares.BoxedShares))
|
|
|
- }
|
|
|
- if len(keyShares.SLOKIDs) > 0 {
|
|
|
- for i := 0; i < len(keyShares.SLOKIDs) && shareCount < keyShares.Threshold; i++ {
|
|
|
- slokKey := lookup(keyShares.SLOKIDs[i])
|
|
|
- if slokKey == nil {
|
|
|
- continue
|
|
|
+ var fileSpec OSLFileSpec
|
|
|
+ err := s.jsonDecoder.Decode(&fileSpec)
|
|
|
+ if err != nil {
|
|
|
+ return nil, common.ContextError(err)
|
|
|
}
|
|
|
- shareCount += 1
|
|
|
- if unboxKey {
|
|
|
- share, err := unbox(slokKey, keyShares.BoxedShares[i])
|
|
|
- if err != nil {
|
|
|
- return false, nil, common.ContextError(err)
|
|
|
- }
|
|
|
- shares[i] = share
|
|
|
+
|
|
|
+ ok, _, err := fileSpec.KeyShares.reassembleKey(s.lookup, false)
|
|
|
+ if err != nil {
|
|
|
+ return nil, common.ContextError(err)
|
|
|
}
|
|
|
- }
|
|
|
- } else {
|
|
|
- for i := 0; i < len(keyShares.KeyShares) && shareCount < keyShares.Threshold; i++ {
|
|
|
- ok, key, err := keyShares.KeyShares[i].reassembleKey(lookup, unboxKey)
|
|
|
+
|
|
|
+ if ok {
|
|
|
+ return &fileSpec, nil
|
|
|
+ }
|
|
|
+
|
|
|
+ } else {
|
|
|
+
|
|
|
+ // Expect the end of the FileSpecs array.
|
|
|
+ err := expectJSONDelimiter(s.jsonDecoder, "]")
|
|
|
if err != nil {
|
|
|
- return false, nil, common.ContextError(err)
|
|
|
+ return nil, common.ContextError(err)
|
|
|
}
|
|
|
- if !ok {
|
|
|
- continue
|
|
|
+
|
|
|
+ // Expect the end of the Registry object.
|
|
|
+ err = expectJSONDelimiter(s.jsonDecoder, "}")
|
|
|
+ if err != nil {
|
|
|
+ return nil, common.ContextError(err)
|
|
|
}
|
|
|
- shareCount += 1
|
|
|
- if unboxKey {
|
|
|
- share, err := unbox(key, keyShares.BoxedShares[i])
|
|
|
- if err != nil {
|
|
|
- return false, nil, common.ContextError(err)
|
|
|
- }
|
|
|
- shares[i] = share
|
|
|
+
|
|
|
+ // Expect the end of the registry content.
|
|
|
+ _, err = s.jsonDecoder.Token()
|
|
|
+ if err != io.EOF {
|
|
|
+ return nil, common.ContextError(err)
|
|
|
}
|
|
|
+
|
|
|
+ return nil, nil
|
|
|
}
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
- if shareCount < keyShares.Threshold {
|
|
|
- return false, nil, nil
|
|
|
+func expectJSONDelimiter(jsonDecoder *json.Decoder, delimiter string) error {
|
|
|
+ token, err := jsonDecoder.Token()
|
|
|
+ if err != nil {
|
|
|
+ return common.ContextError(err)
|
|
|
}
|
|
|
-
|
|
|
- if !unboxKey {
|
|
|
- return true, nil, nil
|
|
|
+ if delim, ok := token.(json.Delim); !ok || delim.String() != delimiter {
|
|
|
+ return common.ContextError(
|
|
|
+ fmt.Errorf("unexpected delimiter: %s", delim.String()))
|
|
|
}
|
|
|
-
|
|
|
- joinedKey := shamirCombine(shares)
|
|
|
-
|
|
|
- return true, joinedKey, nil
|
|
|
+ return nil
|
|
|
}
|
|
|
|
|
|
-// UnpackOSL reassembles the key for the OSL specified by oslID and uses
|
|
|
-// that key to decrypt oslFileContents, validate the authenticated package,
|
|
|
-// and extract the payload.
|
|
|
-// Clients will call UnpackOSL for OSLs indicated by GetSeededOSLIDs along
|
|
|
-// with their downloaded content.
|
|
|
-// SLOKLookup is called to determine which SLOKs are seeded with the client.
|
|
|
-func (registry *Registry) UnpackOSL(
|
|
|
+// NewOSLReader decrypts, authenticates and streams an OSL payload.
|
|
|
+func NewOSLReader(
|
|
|
+ oslFileContent io.ReadSeeker,
|
|
|
+ fileSpec *OSLFileSpec,
|
|
|
lookup SLOKLookup,
|
|
|
- oslID []byte,
|
|
|
- oslFileContents []byte,
|
|
|
- signingPublicKey string) (string, error) {
|
|
|
-
|
|
|
- fileSpec, ok := registry.oslIDLookup[string(oslID)]
|
|
|
- if !ok {
|
|
|
- return "", common.ContextError(errors.New("unknown OSL ID"))
|
|
|
- }
|
|
|
+ signingPublicKey string) (io.Reader, error) {
|
|
|
|
|
|
ok, fileKey, err := fileSpec.KeyShares.reassembleKey(lookup, true)
|
|
|
if err != nil {
|
|
|
- return "", common.ContextError(err)
|
|
|
+ return nil, common.ContextError(err)
|
|
|
}
|
|
|
if !ok {
|
|
|
- return "", common.ContextError(errors.New("unseeded OSL"))
|
|
|
+ return nil, common.ContextError(errors.New("unseeded OSL"))
|
|
|
}
|
|
|
|
|
|
- dataPackage, err := unbox(fileKey, oslFileContents)
|
|
|
- if err != nil {
|
|
|
- return "", common.ContextError(err)
|
|
|
+ if len(fileKey) != 32 {
|
|
|
+ return nil, common.ContextError(errors.New("invalid key length"))
|
|
|
}
|
|
|
|
|
|
- oslPayload, err := common.ReadAuthenticatedDataPackage(
|
|
|
- dataPackage, true, signingPublicKey)
|
|
|
+ var nonce [24]byte
|
|
|
+ var key [32]byte
|
|
|
+ copy(key[:], fileKey)
|
|
|
+
|
|
|
+ unboxer, err := secretbox.NewOpenReadSeeker(oslFileContent, &nonce, &key)
|
|
|
if err != nil {
|
|
|
- return "", common.ContextError(err)
|
|
|
+ return nil, common.ContextError(err)
|
|
|
}
|
|
|
|
|
|
- return oslPayload, nil
|
|
|
+ return common.NewAuthenticatedDataPackageReader(
|
|
|
+ unboxer,
|
|
|
+ signingPublicKey)
|
|
|
}
|
|
|
|
|
|
// deriveKeyHKDF implements HKDF-Expand as defined in https://tools.ietf.org/html/rfc5869
|
|
|
@@ -1355,7 +1393,7 @@ func shamirCombine(shares [][]byte) []byte {
|
|
|
}
|
|
|
|
|
|
// box is a helper wrapper for secretbox.Seal.
|
|
|
-// A constant nonce is used, which is secure so long as
|
|
|
+// A constant nonce is used, which is secure so long as
|
|
|
// each key is used to encrypt only one message.
|
|
|
func box(key, plaintext []byte) ([]byte, error) {
|
|
|
if len(key) != 32 {
|