diff --git a/tuf/data/snapshot.go b/tuf/data/snapshot.go index f13951ca83..8871b64355 100644 --- a/tuf/data/snapshot.go +++ b/tuf/data/snapshot.go @@ -2,6 +2,7 @@ package data import ( "bytes" + "fmt" "time" "github.com/Sirupsen/logrus" @@ -23,6 +24,27 @@ type Snapshot struct { 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 // and targets objects 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 -func (sp SignedSnapshot) ToSigned() (*Signed, error) { - s, err := json.MarshalCanonical(sp.Signed) +func (sp *SignedSnapshot) ToSigned() (*Signed, error) { + s, err := defaultSerializer.MarshalCanonical(sp.Signed) if err != nil { return nil, err } @@ -88,6 +110,15 @@ func (sp *SignedSnapshot) AddMeta(role string, meta FileMeta) { 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 // exist in the snapshot, it's a noop. 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 func SnapshotFromSigned(s *Signed) (*SignedSnapshot, error) { sp := Snapshot{} - err := json.Unmarshal(s.Signed, &sp) - if err != nil { + if err := json.Unmarshal(s.Signed, &sp); err != nil { + return nil, err + } + if err := isValidSnapshot(sp); err != nil { return nil, err } sigs := make([]Signature, len(s.Signatures)) diff --git a/tuf/data/snapshot_test.go b/tuf/data/snapshot_test.go index 3dc4668665..881f273059 100644 --- a/tuf/data/snapshot_test.go +++ b/tuf/data/snapshot_test.go @@ -1,28 +1,175 @@ package data import ( + "bytes" + rjson "encoding/json" + "reflect" "testing" + "time" - "github.com/stretchr/testify/assert" + cjson "github.com/docker/go/canonical/json" + "github.com/stretchr/testify/require" ) -func TestDeleteMeta(t *testing.T) { - sp := &SignedSnapshot{ - Signatures: make([]Signature, 0), +func validSnapshotTemplate() *SignedSnapshot { + return &SignedSnapshot{ Signed: Snapshot{ - Type: TUFTypes["snapshot"], - Version: 0, - Expires: DefaultExpires("snapshot"), - Meta: Files{ + Type: "Snapshot", Version: 1, Expires: time.Now(), Meta: Files{ CanonicalRootRole: FileMeta{}, CanonicalTargetsRole: FileMeta{}, - }, + }}, + Signatures: []Signature{ + {KeyID: "key1", Method: "method1", Signature: []byte("hello")}, }, } - _, ok := sp.Signed.Meta["root"] - assert.True(t, ok) - sp.DeleteMeta("root") - _, ok = sp.Signed.Meta["root"] - assert.False(t, ok) - assert.True(t, sp.Dirty) +} + +func TestSnapshotToSignedMarshalsSignedPortionWithCanonicalJSON(t *testing.T) { + sn := SignedSnapshot{Signed: Snapshot{Type: "Snapshot", Version: 1, Expires: time.Now()}} + signedCanonical, err := sn.ToSigned() + 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) }