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:
parent
8f57e05016
commit
fdcfc56f7b
3 changed files with 87 additions and 84 deletions
72
docs/hmac.go
Normal file
72
docs/hmac.go
Normal 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
|
||||||
|
}
|
|
@ -1,12 +1,8 @@
|
||||||
package registry
|
package registry
|
||||||
|
|
||||||
import (
|
import "testing"
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/docker/distribution/storage"
|
var layerUploadStates = []layerUploadState{
|
||||||
)
|
|
||||||
|
|
||||||
var layerUploadStates = []storage.LayerUploadState{
|
|
||||||
{
|
{
|
||||||
Name: "hello",
|
Name: "hello",
|
||||||
UUID: "abcd-1234-qwer-0987",
|
UUID: "abcd-1234-qwer-0987",
|
||||||
|
@ -47,15 +43,15 @@ var secrets = []string{
|
||||||
// TestLayerUploadTokens constructs stateTokens from LayerUploadStates and
|
// TestLayerUploadTokens constructs stateTokens from LayerUploadStates and
|
||||||
// validates that the tokens can be used to reconstruct the proper upload state.
|
// validates that the tokens can be used to reconstruct the proper upload state.
|
||||||
func TestLayerUploadTokens(t *testing.T) {
|
func TestLayerUploadTokens(t *testing.T) {
|
||||||
tokenProvider := newHMACTokenProvider("supersecret")
|
secret := hmacKey("supersecret")
|
||||||
|
|
||||||
for _, testcase := range layerUploadStates {
|
for _, testcase := range layerUploadStates {
|
||||||
token, err := tokenProvider.layerUploadStateToToken(testcase)
|
token, err := secret.packUploadState(testcase)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
lus, err := tokenProvider.layerUploadStateFromToken(token)
|
lus, err := secret.unpackUploadState(token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -68,39 +64,39 @@ func TestLayerUploadTokens(t *testing.T) {
|
||||||
// only if they share the same secret.
|
// only if they share the same secret.
|
||||||
func TestHMACValidation(t *testing.T) {
|
func TestHMACValidation(t *testing.T) {
|
||||||
for _, secret := range secrets {
|
for _, secret := range secrets {
|
||||||
tokenProvider1 := newHMACTokenProvider(secret)
|
secret1 := hmacKey(secret)
|
||||||
tokenProvider2 := newHMACTokenProvider(secret)
|
secret2 := hmacKey(secret)
|
||||||
badTokenProvider := newHMACTokenProvider("DifferentSecret")
|
badSecret := hmacKey("DifferentSecret")
|
||||||
|
|
||||||
for _, testcase := range layerUploadStates {
|
for _, testcase := range layerUploadStates {
|
||||||
token, err := tokenProvider1.layerUploadStateToToken(testcase)
|
token, err := secret1.packUploadState(testcase)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
lus, err := tokenProvider2.layerUploadStateFromToken(token)
|
lus, err := secret2.unpackUploadState(token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
assertLayerUploadStateEquals(t, testcase, lus)
|
assertLayerUploadStateEquals(t, testcase, lus)
|
||||||
|
|
||||||
_, err = badTokenProvider.layerUploadStateFromToken(token)
|
_, err = badSecret.unpackUploadState(token)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected token provider to fail at retrieving state from token: %s", token)
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = tokenProvider1.layerUploadStateFromToken(badToken)
|
_, err = secret1.unpackUploadState(badToken)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected token provider to fail at retrieving state from token: %s", badToken)
|
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 {
|
if err == nil {
|
||||||
t.Fatalf("Expected token provider to fail at retrieving state from token: %s", badToken)
|
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 {
|
if expected.Name != received.Name {
|
||||||
t.Fatalf("Expected Name=%q, Received Name=%q", expected.Name, received.Name)
|
t.Fatalf("Expected Name=%q, Received Name=%q", expected.Name, received.Name)
|
||||||
}
|
}
|
|
@ -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
|
|
||||||
}
|
|
Loading…
Reference in a new issue