mirror of https://github.com/docker/docs.git
Add snapshot metadata validation to the snapshot data structure
Signed-off-by: Ying Li <ying.li@docker.com>
This commit is contained in:
parent
8335d194ce
commit
4cae84bb29
|
@ -2,6 +2,7 @@ package data
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
"github.com/Sirupsen/logrus"
|
||||||
|
@ -23,6 +24,27 @@ type Snapshot struct {
|
||||||
Meta Files `json:"meta"`
|
Meta Files `json:"meta"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isValidSnapshot returns an error, or nil, depending on whether the content of the struct
|
||||||
|
// is valid for snapshot metadata. This does not check signatures or expiry, just that
|
||||||
|
// the metadata content is valid.
|
||||||
|
func isValidSnapshot(s Snapshot) error {
|
||||||
|
expectedType := TUFTypes[CanonicalSnapshotRole]
|
||||||
|
if s.Type != expectedType {
|
||||||
|
return ErrInvalidMeta{
|
||||||
|
Role: CanonicalSnapshotRole, Msg: fmt.Sprintf("expected type %s, not %s", expectedType, s.Type)}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, role := range []string{CanonicalRootRole, CanonicalTargetsRole} {
|
||||||
|
if _, ok := s.Meta[role]; !ok {
|
||||||
|
return ErrInvalidMeta{
|
||||||
|
Role: CanonicalSnapshotRole,
|
||||||
|
Msg: fmt.Sprintf("missing %s checksum information", role),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// NewSnapshot initilizes a SignedSnapshot with a given top level root
|
// NewSnapshot initilizes a SignedSnapshot with a given top level root
|
||||||
// and targets objects
|
// and targets objects
|
||||||
func NewSnapshot(root *Signed, targets *Signed) (*SignedSnapshot, error) {
|
func NewSnapshot(root *Signed, targets *Signed) (*SignedSnapshot, error) {
|
||||||
|
@ -64,8 +86,8 @@ func (sp *SignedSnapshot) hashForRole(role string) []byte {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToSigned partially serializes a SignedSnapshot for further signing
|
// ToSigned partially serializes a SignedSnapshot for further signing
|
||||||
func (sp SignedSnapshot) ToSigned() (*Signed, error) {
|
func (sp *SignedSnapshot) ToSigned() (*Signed, error) {
|
||||||
s, err := json.MarshalCanonical(sp.Signed)
|
s, err := defaultSerializer.MarshalCanonical(sp.Signed)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -88,6 +110,15 @@ func (sp *SignedSnapshot) AddMeta(role string, meta FileMeta) {
|
||||||
sp.Dirty = true
|
sp.Dirty = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetMeta gets the metadata for a particular role, returning an error if it's
|
||||||
|
// not found
|
||||||
|
func (sp *SignedSnapshot) GetMeta(role string) (*FileMeta, error) {
|
||||||
|
if meta, ok := sp.Signed.Meta[role]; ok {
|
||||||
|
return &meta, nil
|
||||||
|
}
|
||||||
|
return nil, ErrMissingMeta{Role: role}
|
||||||
|
}
|
||||||
|
|
||||||
// DeleteMeta removes a role from the snapshot. If the role doesn't
|
// DeleteMeta removes a role from the snapshot. If the role doesn't
|
||||||
// exist in the snapshot, it's a noop.
|
// exist in the snapshot, it's a noop.
|
||||||
func (sp *SignedSnapshot) DeleteMeta(role string) {
|
func (sp *SignedSnapshot) DeleteMeta(role string) {
|
||||||
|
@ -97,11 +128,22 @@ func (sp *SignedSnapshot) DeleteMeta(role string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalJSON returns the serialized form of SignedSnapshot as bytes
|
||||||
|
func (sp *SignedSnapshot) MarshalJSON() ([]byte, error) {
|
||||||
|
signed, err := sp.ToSigned()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return defaultSerializer.Marshal(signed)
|
||||||
|
}
|
||||||
|
|
||||||
// SnapshotFromSigned fully unpacks a Signed object into a SignedSnapshot
|
// SnapshotFromSigned fully unpacks a Signed object into a SignedSnapshot
|
||||||
func SnapshotFromSigned(s *Signed) (*SignedSnapshot, error) {
|
func SnapshotFromSigned(s *Signed) (*SignedSnapshot, error) {
|
||||||
sp := Snapshot{}
|
sp := Snapshot{}
|
||||||
err := json.Unmarshal(s.Signed, &sp)
|
if err := json.Unmarshal(s.Signed, &sp); err != nil {
|
||||||
if err != nil {
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := isValidSnapshot(sp); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
sigs := make([]Signature, len(s.Signatures))
|
sigs := make([]Signature, len(s.Signatures))
|
||||||
|
|
|
@ -1,28 +1,175 @@
|
||||||
package data
|
package data
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
rjson "encoding/json"
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
cjson "github.com/docker/go/canonical/json"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDeleteMeta(t *testing.T) {
|
func validSnapshotTemplate() *SignedSnapshot {
|
||||||
sp := &SignedSnapshot{
|
return &SignedSnapshot{
|
||||||
Signatures: make([]Signature, 0),
|
|
||||||
Signed: Snapshot{
|
Signed: Snapshot{
|
||||||
Type: TUFTypes["snapshot"],
|
Type: "Snapshot", Version: 1, Expires: time.Now(), Meta: Files{
|
||||||
Version: 0,
|
|
||||||
Expires: DefaultExpires("snapshot"),
|
|
||||||
Meta: Files{
|
|
||||||
CanonicalRootRole: FileMeta{},
|
CanonicalRootRole: FileMeta{},
|
||||||
CanonicalTargetsRole: FileMeta{},
|
CanonicalTargetsRole: FileMeta{},
|
||||||
},
|
}},
|
||||||
|
Signatures: []Signature{
|
||||||
|
{KeyID: "key1", Method: "method1", Signature: []byte("hello")},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
_, ok := sp.Signed.Meta["root"]
|
}
|
||||||
assert.True(t, ok)
|
|
||||||
sp.DeleteMeta("root")
|
func TestSnapshotToSignedMarshalsSignedPortionWithCanonicalJSON(t *testing.T) {
|
||||||
_, ok = sp.Signed.Meta["root"]
|
sn := SignedSnapshot{Signed: Snapshot{Type: "Snapshot", Version: 1, Expires: time.Now()}}
|
||||||
assert.False(t, ok)
|
signedCanonical, err := sn.ToSigned()
|
||||||
assert.True(t, sp.Dirty)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
canonicalSignedPortion, err := cjson.MarshalCanonical(sn.Signed)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
castedCanonical := rjson.RawMessage(canonicalSignedPortion)
|
||||||
|
|
||||||
|
// don't bother testing regular JSON because it might not be different
|
||||||
|
|
||||||
|
require.True(t, bytes.Equal(signedCanonical.Signed, castedCanonical),
|
||||||
|
"expected %v == %v", signedCanonical.Signed, castedCanonical)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSnapshotToSignCopiesSignatures(t *testing.T) {
|
||||||
|
sn := SignedSnapshot{
|
||||||
|
Signed: Snapshot{Type: "Snapshot", Version: 2, Expires: time.Now()},
|
||||||
|
Signatures: []Signature{
|
||||||
|
{KeyID: "key1", Method: "method1", Signature: []byte("hello")},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
signed, err := sn.ToSigned()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.True(t, reflect.DeepEqual(sn.Signatures, signed.Signatures),
|
||||||
|
"expected %v == %v", sn.Signatures, signed.Signatures)
|
||||||
|
|
||||||
|
sn.Signatures[0].KeyID = "changed"
|
||||||
|
require.False(t, reflect.DeepEqual(sn.Signatures, signed.Signatures),
|
||||||
|
"expected %v != %v", sn.Signatures, signed.Signatures)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSnapshotToSignedMarshallingErrorsPropagated(t *testing.T) {
|
||||||
|
setDefaultSerializer(errorSerializer{})
|
||||||
|
defer setDefaultSerializer(canonicalJSON{})
|
||||||
|
sn := SignedSnapshot{
|
||||||
|
Signed: Snapshot{Type: "Snapshot", Version: 2, Expires: time.Now()},
|
||||||
|
}
|
||||||
|
_, err := sn.ToSigned()
|
||||||
|
require.EqualError(t, err, "bad")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSnapshotMarshalJSONMarshalsSignedWithRegularJSON(t *testing.T) {
|
||||||
|
sn := SignedSnapshot{
|
||||||
|
Signed: Snapshot{Type: "Snapshot", Version: 1, Expires: time.Now()},
|
||||||
|
Signatures: []Signature{
|
||||||
|
{KeyID: "key1", Method: "method1", Signature: []byte("hello")},
|
||||||
|
{KeyID: "key2", Method: "method2", Signature: []byte("there")},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
serialized, err := sn.MarshalJSON()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
signed, err := sn.ToSigned()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// don't bother testing canonical JSON because it might not be different
|
||||||
|
|
||||||
|
regular, err := rjson.Marshal(signed)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.True(t, bytes.Equal(serialized, regular),
|
||||||
|
"expected %v != %v", serialized, regular)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSnapshotMarshalJSONMarshallingErrorsPropagated(t *testing.T) {
|
||||||
|
setDefaultSerializer(errorSerializer{})
|
||||||
|
defer setDefaultSerializer(canonicalJSON{})
|
||||||
|
sn := SignedSnapshot{
|
||||||
|
Signed: Snapshot{Type: "Snapshot", Version: 2, Expires: time.Now()},
|
||||||
|
}
|
||||||
|
_, err := sn.MarshalJSON()
|
||||||
|
require.EqualError(t, err, "bad")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SnapshotFromSigned succeeds if the snapshot is valid, and copies the signatures
|
||||||
|
// rather than assigns them
|
||||||
|
func TestSnapshotFromSignedCopiesSignatures(t *testing.T) {
|
||||||
|
signed, err := validSnapshotTemplate().ToSigned()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
signedSnapshot, err := SnapshotFromSigned(signed)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
signed.Signatures[0] = Signature{KeyID: "key3", Method: "method3", Signature: []byte("world")}
|
||||||
|
|
||||||
|
require.Equal(t, "key3", signed.Signatures[0].KeyID)
|
||||||
|
require.Equal(t, "key1", signedSnapshot.Signatures[0].KeyID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the root or targets metadata is missing, the snapshot metadata fails to validate
|
||||||
|
// and thus fails to convert into a SignedSnapshot
|
||||||
|
func TestSnapshotFromSignedValidatesMeta(t *testing.T) {
|
||||||
|
for _, roleName := range []string{CanonicalRootRole, CanonicalTargetsRole} {
|
||||||
|
sn := validSnapshotTemplate()
|
||||||
|
|
||||||
|
// no root meta
|
||||||
|
delete(sn.Signed.Meta, roleName)
|
||||||
|
s, err := sn.ToSigned()
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = SnapshotFromSigned(s)
|
||||||
|
require.IsType(t, ErrInvalidMeta{}, err)
|
||||||
|
|
||||||
|
// add some extra metadata to make sure it's not failing because the metadata
|
||||||
|
// is empty
|
||||||
|
sn.Signed.Meta[CanonicalSnapshotRole] = FileMeta{}
|
||||||
|
s, err = sn.ToSigned()
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = SnapshotFromSigned(s)
|
||||||
|
require.IsType(t, ErrInvalidMeta{}, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type must be "Snapshot"
|
||||||
|
func TestSnapshotFromSignedValidatesRoleType(t *testing.T) {
|
||||||
|
sn := validSnapshotTemplate()
|
||||||
|
|
||||||
|
for _, invalid := range []string{" Snapshot", CanonicalSnapshotRole, "TIMESTAMP"} {
|
||||||
|
sn.Signed.Type = invalid
|
||||||
|
s, err := sn.ToSigned()
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = SnapshotFromSigned(s)
|
||||||
|
require.IsType(t, ErrInvalidMeta{}, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sn = validSnapshotTemplate()
|
||||||
|
sn.Signed.Type = "Snapshot"
|
||||||
|
s, err := sn.ToSigned()
|
||||||
|
require.NoError(t, err)
|
||||||
|
sSnapshot, err := SnapshotFromSigned(s)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "Snapshot", sSnapshot.Signed.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMeta returns the checksum, or an error if it is missing.
|
||||||
|
func TestSnapshotGetMeta(t *testing.T) {
|
||||||
|
ts := validSnapshotTemplate()
|
||||||
|
f, err := ts.GetMeta(CanonicalRootRole)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.IsType(t, &FileMeta{}, f)
|
||||||
|
|
||||||
|
// now one that doesn't exist
|
||||||
|
f, err = ts.GetMeta("targets/a")
|
||||||
|
require.Error(t, err)
|
||||||
|
require.IsType(t, ErrMissingMeta{}, err)
|
||||||
|
require.Nil(t, f)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue