Refactor handling of hmac state packing

This refactors the hmac state token to take control of the layerUploadState
json message, which has been removed from the storage backend. It also moves
away from the concept of a LayerUploadStateStore callback object, which was
short-lived. This allows for upload offset to be managed by the web application
logic in the face of an inconsistent backend. By controlling the upload offset
externally, we reduce the possibility of misreporting upload state to a client.

We may still want to modify the way this works after getting production
experience.

Signed-off-by: Stephen J Day <stephen.day@docker.com>
This commit is contained in:
Stephen J Day 2015-01-08 14:59:15 -08:00
parent 8f57e05016
commit fdcfc56f7b
3 changed files with 87 additions and 84 deletions

72
docs/hmac.go Normal file
View file

@ -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
}

View file

@ -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)
}

View file

@ -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
}