diff --git a/docs/hmac.go b/docs/hmac.go new file mode 100644 index 00000000..d2470087 --- /dev/null +++ b/docs/hmac.go @@ -0,0 +1,72 @@ +package registry + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + "time" +) + +// layerUploadState captures the state serializable state of the layer upload. +type layerUploadState struct { + // name is the primary repository under which the layer will be linked. + Name string + + // UUID identifies the upload. + UUID string + + // offset contains the current progress of the upload. + Offset int64 + + // StartedAt is the original start time of the upload. + StartedAt time.Time +} + +type hmacKey string + +// unpackUploadState unpacks and validates the layer upload state from the +// token, using the hmacKey secret. +func (secret hmacKey) unpackUploadState(token string) (layerUploadState, error) { + var state layerUploadState + + tokenBytes, err := base64.URLEncoding.DecodeString(token) + if err != nil { + return state, err + } + mac := hmac.New(sha256.New, []byte(secret)) + + if len(tokenBytes) < mac.Size() { + return state, fmt.Errorf("Invalid token") + } + + macBytes := tokenBytes[:mac.Size()] + messageBytes := tokenBytes[mac.Size():] + + mac.Write(messageBytes) + if !hmac.Equal(mac.Sum(nil), macBytes) { + return state, fmt.Errorf("Invalid token") + } + + if err := json.Unmarshal(messageBytes, &state); err != nil { + return state, err + } + + return state, nil +} + +// packUploadState packs the upload state signed with and hmac digest using +// the hmacKey secret, encoding to url safe base64. The resulting token can be +// used to share data with minimized risk of external tampering. +func (secret hmacKey) packUploadState(lus layerUploadState) (string, error) { + mac := hmac.New(sha256.New, []byte(secret)) + p, err := json.Marshal(lus) + if err != nil { + return "", err + } + + mac.Write(p) + + return base64.URLEncoding.EncodeToString(append(mac.Sum(nil), p...)), nil +} diff --git a/docs/tokens_test.go b/docs/hmac_test.go similarity index 69% rename from docs/tokens_test.go rename to docs/hmac_test.go index a447438a..5ad60f61 100644 --- a/docs/tokens_test.go +++ b/docs/hmac_test.go @@ -1,12 +1,8 @@ package registry -import ( - "testing" +import "testing" - "github.com/docker/distribution/storage" -) - -var layerUploadStates = []storage.LayerUploadState{ +var layerUploadStates = []layerUploadState{ { Name: "hello", UUID: "abcd-1234-qwer-0987", @@ -47,15 +43,15 @@ var secrets = []string{ // TestLayerUploadTokens constructs stateTokens from LayerUploadStates and // validates that the tokens can be used to reconstruct the proper upload state. func TestLayerUploadTokens(t *testing.T) { - tokenProvider := newHMACTokenProvider("supersecret") + secret := hmacKey("supersecret") for _, testcase := range layerUploadStates { - token, err := tokenProvider.layerUploadStateToToken(testcase) + token, err := secret.packUploadState(testcase) if err != nil { t.Fatal(err) } - lus, err := tokenProvider.layerUploadStateFromToken(token) + lus, err := secret.unpackUploadState(token) if err != nil { t.Fatal(err) } @@ -68,39 +64,39 @@ func TestLayerUploadTokens(t *testing.T) { // only if they share the same secret. func TestHMACValidation(t *testing.T) { for _, secret := range secrets { - tokenProvider1 := newHMACTokenProvider(secret) - tokenProvider2 := newHMACTokenProvider(secret) - badTokenProvider := newHMACTokenProvider("DifferentSecret") + secret1 := hmacKey(secret) + secret2 := hmacKey(secret) + badSecret := hmacKey("DifferentSecret") for _, testcase := range layerUploadStates { - token, err := tokenProvider1.layerUploadStateToToken(testcase) + token, err := secret1.packUploadState(testcase) if err != nil { t.Fatal(err) } - lus, err := tokenProvider2.layerUploadStateFromToken(token) + lus, err := secret2.unpackUploadState(token) if err != nil { t.Fatal(err) } assertLayerUploadStateEquals(t, testcase, lus) - _, err = badTokenProvider.layerUploadStateFromToken(token) + _, err = badSecret.unpackUploadState(token) if err == nil { t.Fatalf("Expected token provider to fail at retrieving state from token: %s", token) } - badToken, err := badTokenProvider.layerUploadStateToToken(testcase) + badToken, err := badSecret.packUploadState(lus) if err != nil { t.Fatal(err) } - _, err = tokenProvider1.layerUploadStateFromToken(badToken) + _, err = secret1.unpackUploadState(badToken) if err == nil { t.Fatalf("Expected token provider to fail at retrieving state from token: %s", badToken) } - _, err = tokenProvider2.layerUploadStateFromToken(badToken) + _, err = secret2.unpackUploadState(badToken) if err == nil { t.Fatalf("Expected token provider to fail at retrieving state from token: %s", badToken) } @@ -108,7 +104,7 @@ func TestHMACValidation(t *testing.T) { } } -func assertLayerUploadStateEquals(t *testing.T, expected storage.LayerUploadState, received storage.LayerUploadState) { +func assertLayerUploadStateEquals(t *testing.T, expected layerUploadState, received layerUploadState) { if expected.Name != received.Name { t.Fatalf("Expected Name=%q, Received Name=%q", expected.Name, received.Name) } diff --git a/docs/tokens.go b/docs/tokens.go deleted file mode 100644 index 276b896e..00000000 --- a/docs/tokens.go +++ /dev/null @@ -1,65 +0,0 @@ -package registry - -import ( - "crypto/hmac" - "crypto/sha256" - "encoding/base64" - "encoding/json" - "fmt" - - "github.com/docker/distribution/storage" -) - -// tokenProvider contains methods for serializing and deserializing state from token strings. -type tokenProvider interface { - // layerUploadStateFromToken retrieves the LayerUploadState for a given state token. - layerUploadStateFromToken(stateToken string) (storage.LayerUploadState, error) - - // layerUploadStateToToken returns a token string representing the given LayerUploadState. - layerUploadStateToToken(layerUploadState storage.LayerUploadState) (string, error) -} - -type hmacTokenProvider struct { - secret string -} - -func newHMACTokenProvider(secret string) tokenProvider { - return &hmacTokenProvider{secret: secret} -} - -// layerUploadStateFromToken deserializes the given HMAC stateToken and validates the prefix HMAC -func (ts *hmacTokenProvider) layerUploadStateFromToken(stateToken string) (storage.LayerUploadState, error) { - var lus storage.LayerUploadState - - tokenBytes, err := base64.URLEncoding.DecodeString(stateToken) - if err != nil { - return lus, err - } - mac := hmac.New(sha256.New, []byte(ts.secret)) - - if len(tokenBytes) < mac.Size() { - return lus, fmt.Errorf("Invalid token") - } - - macBytes := tokenBytes[:mac.Size()] - messageBytes := tokenBytes[mac.Size():] - - mac.Write(messageBytes) - if !hmac.Equal(mac.Sum(nil), macBytes) { - return lus, fmt.Errorf("Invalid token") - } - - if err := json.Unmarshal(messageBytes, &lus); err != nil { - return lus, err - } - - return lus, nil -} - -// layerUploadStateToToken serializes the given LayerUploadState to JSON with an HMAC prepended -func (ts *hmacTokenProvider) layerUploadStateToToken(lus storage.LayerUploadState) (string, error) { - mac := hmac.New(sha256.New, []byte(ts.secret)) - stateJSON := fmt.Sprintf("{\"Name\": \"%s\", \"UUID\": \"%s\", \"Offset\": %d}", lus.Name, lus.UUID, lus.Offset) - mac.Write([]byte(stateJSON)) - return base64.URLEncoding.EncodeToString(append(mac.Sum(nil), stateJSON...)), nil -}