summaryrefslogtreecommitdiffstats
path: root/data
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 17:39:05 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 17:39:05 +0000
commitd3911883df1e317b23fa12be7e1c7b45f74d630a (patch)
tree154cebf32f39b26b8a88e9bb359c57288d629e3b /data
parentInitial commit. (diff)
downloadgolang-github-theupdateframework-go-tuf-d3911883df1e317b23fa12be7e1c7b45f74d630a.tar.xz
golang-github-theupdateframework-go-tuf-d3911883df1e317b23fa12be7e1c7b45f74d630a.zip
Adding upstream version 0.6.1.upstream/0.6.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'data')
-rw-r--r--data/hex_bytes.go42
-rw-r--r--data/hex_bytes_test.go44
-rw-r--r--data/types.go348
-rw-r--r--data/types_test.go287
4 files changed, 721 insertions, 0 deletions
diff --git a/data/hex_bytes.go b/data/hex_bytes.go
new file mode 100644
index 0000000..ec20041
--- /dev/null
+++ b/data/hex_bytes.go
@@ -0,0 +1,42 @@
+package data
+
+import (
+ "crypto/sha256"
+ "encoding/hex"
+ "errors"
+)
+
+type HexBytes []byte
+
+func (b *HexBytes) UnmarshalJSON(data []byte) error {
+ if len(data) < 2 || len(data)%2 != 0 || data[0] != '"' || data[len(data)-1] != '"' {
+ return errors.New("tuf: invalid JSON hex bytes")
+ }
+ res := make([]byte, hex.DecodedLen(len(data)-2))
+ _, err := hex.Decode(res, data[1:len(data)-1])
+ if err != nil {
+ return err
+ }
+ *b = res
+ return nil
+}
+
+func (b HexBytes) MarshalJSON() ([]byte, error) {
+ res := make([]byte, hex.EncodedLen(len(b))+2)
+ res[0] = '"'
+ res[len(res)-1] = '"'
+ hex.Encode(res[1:], b)
+ return res, nil
+}
+
+func (b HexBytes) String() string {
+ return hex.EncodeToString(b)
+}
+
+// 4.5. File formats: targets.json and delegated target roles:
+// ...each target path, when hashed with the SHA-256 hash function to produce
+// a 64-byte hexadecimal digest (HEX_DIGEST)...
+func PathHexDigest(s string) string {
+ b := sha256.Sum256([]byte(s))
+ return hex.EncodeToString(b[:])
+}
diff --git a/data/hex_bytes_test.go b/data/hex_bytes_test.go
new file mode 100644
index 0000000..8c11623
--- /dev/null
+++ b/data/hex_bytes_test.go
@@ -0,0 +1,44 @@
+package data
+
+import (
+ "encoding/json"
+ "testing"
+
+ . "gopkg.in/check.v1"
+)
+
+// Hook up gocheck into the "go test" runner.
+func Test(t *testing.T) { TestingT(t) }
+
+type HexBytesSuite struct{}
+
+var _ = Suite(&HexBytesSuite{})
+
+func (HexBytesSuite) TestUnmarshalJSON(c *C) {
+ var data HexBytes
+ err := json.Unmarshal([]byte(`"666f6f"`), &data)
+ c.Assert(err, IsNil)
+ c.Assert(string(data), Equals, "foo")
+}
+
+func (HexBytesSuite) TestUnmarshalJSONError(c *C) {
+ var data HexBytes
+
+ // uneven length
+ err := json.Unmarshal([]byte(`"a"`), &data)
+ c.Assert(err, Not(IsNil))
+
+ // invalid hex
+ err = json.Unmarshal([]byte(`"zz"`), &data)
+ c.Assert(err, Not(IsNil))
+
+ // wrong type
+ err = json.Unmarshal([]byte("6"), &data)
+ c.Assert(err, Not(IsNil))
+}
+
+func (HexBytesSuite) TestMarshalJSON(c *C) {
+ data, err := json.Marshal(HexBytes("foo"))
+ c.Assert(err, IsNil)
+ c.Assert(data, DeepEquals, []byte(`"666f6f"`))
+}
diff --git a/data/types.go b/data/types.go
new file mode 100644
index 0000000..eb00489
--- /dev/null
+++ b/data/types.go
@@ -0,0 +1,348 @@
+package data
+
+import (
+ "bytes"
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "path"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/secure-systems-lab/go-securesystemslib/cjson"
+)
+
+type KeyType string
+
+type KeyScheme string
+
+type HashAlgorithm string
+
+const (
+ KeyIDLength = sha256.Size * 2
+
+ KeyTypeEd25519 KeyType = "ed25519"
+ // From version 1.0.32, the reference implementation defines 'ecdsa',
+ // not 'ecdsa-sha2-nistp256' for NIST P-256 curves.
+ KeyTypeECDSA_SHA2_P256 KeyType = "ecdsa"
+ KeyTypeECDSA_SHA2_P256_OLD_FMT KeyType = "ecdsa-sha2-nistp256"
+ KeyTypeRSASSA_PSS_SHA256 KeyType = "rsa"
+
+ KeySchemeEd25519 KeyScheme = "ed25519"
+ KeySchemeECDSA_SHA2_P256 KeyScheme = "ecdsa-sha2-nistp256"
+ KeySchemeRSASSA_PSS_SHA256 KeyScheme = "rsassa-pss-sha256"
+
+ HashAlgorithmSHA256 HashAlgorithm = "sha256"
+ HashAlgorithmSHA512 HashAlgorithm = "sha512"
+)
+
+var (
+ HashAlgorithms = []HashAlgorithm{HashAlgorithmSHA256, HashAlgorithmSHA512}
+ ErrPathsAndPathHashesSet = errors.New("tuf: failed validation of delegated target: paths and path_hash_prefixes are both set")
+)
+
+type Signed struct {
+ Signed json.RawMessage `json:"signed"`
+ Signatures []Signature `json:"signatures"`
+}
+
+type Signature struct {
+ KeyID string `json:"keyid"`
+ Signature HexBytes `json:"sig"`
+}
+
+type PublicKey struct {
+ Type KeyType `json:"keytype"`
+ Scheme KeyScheme `json:"scheme"`
+ Algorithms []HashAlgorithm `json:"keyid_hash_algorithms,omitempty"`
+ Value json.RawMessage `json:"keyval"`
+
+ ids []string
+ idOnce sync.Once
+}
+
+type PrivateKey struct {
+ Type KeyType `json:"keytype"`
+ Scheme KeyScheme `json:"scheme,omitempty"`
+ Algorithms []HashAlgorithm `json:"keyid_hash_algorithms,omitempty"`
+ Value json.RawMessage `json:"keyval"`
+}
+
+func (k *PublicKey) IDs() []string {
+ k.idOnce.Do(func() {
+ data, err := cjson.EncodeCanonical(k)
+ if err != nil {
+ panic(fmt.Errorf("tuf: error creating key ID: %w", err))
+ }
+ digest := sha256.Sum256(data)
+ k.ids = []string{hex.EncodeToString(digest[:])}
+ })
+ return k.ids
+}
+
+func (k *PublicKey) ContainsID(id string) bool {
+ for _, keyid := range k.IDs() {
+ if id == keyid {
+ return true
+ }
+ }
+ return false
+}
+
+func DefaultExpires(role string) time.Time {
+ var t time.Time
+ switch role {
+ case "root":
+ t = time.Now().AddDate(1, 0, 0)
+ case "snapshot":
+ t = time.Now().AddDate(0, 0, 7)
+ case "timestamp":
+ t = time.Now().AddDate(0, 0, 1)
+ default:
+ // targets and delegated targets
+ t = time.Now().AddDate(0, 3, 0)
+ }
+ return t.UTC().Round(time.Second)
+}
+
+type Root struct {
+ Type string `json:"_type"`
+ SpecVersion string `json:"spec_version"`
+ Version int64 `json:"version"`
+ Expires time.Time `json:"expires"`
+ Keys map[string]*PublicKey `json:"keys"`
+ Roles map[string]*Role `json:"roles"`
+ Custom *json.RawMessage `json:"custom,omitempty"`
+
+ ConsistentSnapshot bool `json:"consistent_snapshot"`
+}
+
+func NewRoot() *Root {
+ return &Root{
+ Type: "root",
+ SpecVersion: "1.0",
+ Expires: DefaultExpires("root"),
+ Keys: make(map[string]*PublicKey),
+ Roles: make(map[string]*Role),
+ ConsistentSnapshot: true,
+ }
+}
+
+func (r *Root) AddKey(key *PublicKey) bool {
+ changed := false
+ for _, id := range key.IDs() {
+ if _, ok := r.Keys[id]; !ok {
+ changed = true
+ r.Keys[id] = key
+ }
+ }
+ return changed
+}
+
+type Role struct {
+ KeyIDs []string `json:"keyids"`
+ Threshold int `json:"threshold"`
+}
+
+func (r *Role) AddKeyIDs(ids []string) bool {
+ roleIDs := make(map[string]struct{})
+ for _, id := range r.KeyIDs {
+ roleIDs[id] = struct{}{}
+ }
+ changed := false
+ for _, id := range ids {
+ if _, ok := roleIDs[id]; !ok {
+ changed = true
+ r.KeyIDs = append(r.KeyIDs, id)
+ }
+ }
+ return changed
+}
+
+type Files map[string]TargetFileMeta
+
+type Hashes map[string]HexBytes
+
+func (f Hashes) HashAlgorithms() []string {
+ funcs := make([]string, 0, len(f))
+ for name := range f {
+ funcs = append(funcs, name)
+ }
+ return funcs
+}
+
+type metapathFileMeta struct {
+ Length int64 `json:"length,omitempty"`
+ Hashes Hashes `json:"hashes,omitempty"`
+ Version int64 `json:"version"`
+ Custom *json.RawMessage `json:"custom,omitempty"`
+}
+
+// SnapshotFileMeta is the meta field of a snapshot
+// Note: Contains a `custom` field
+type SnapshotFileMeta metapathFileMeta
+
+type SnapshotFiles map[string]SnapshotFileMeta
+
+type Snapshot struct {
+ Type string `json:"_type"`
+ SpecVersion string `json:"spec_version"`
+ Version int64 `json:"version"`
+ Expires time.Time `json:"expires"`
+ Meta SnapshotFiles `json:"meta"`
+ Custom *json.RawMessage `json:"custom,omitempty"`
+}
+
+func NewSnapshot() *Snapshot {
+ return &Snapshot{
+ Type: "snapshot",
+ SpecVersion: "1.0",
+ Expires: DefaultExpires("snapshot"),
+ Meta: make(SnapshotFiles),
+ }
+}
+
+type FileMeta struct {
+ Length int64 `json:"length"`
+ Hashes Hashes `json:"hashes"`
+}
+
+type TargetFiles map[string]TargetFileMeta
+
+type TargetFileMeta struct {
+ FileMeta
+ Custom *json.RawMessage `json:"custom,omitempty"`
+}
+
+func (f TargetFileMeta) HashAlgorithms() []string {
+ return f.FileMeta.Hashes.HashAlgorithms()
+}
+
+type Targets struct {
+ Type string `json:"_type"`
+ SpecVersion string `json:"spec_version"`
+ Version int64 `json:"version"`
+ Expires time.Time `json:"expires"`
+ Targets TargetFiles `json:"targets"`
+ Delegations *Delegations `json:"delegations,omitempty"`
+ Custom *json.RawMessage `json:"custom,omitempty"`
+}
+
+// Delegations represents the edges from a parent Targets role to one or more
+// delegated target roles. See spec v1.0.19 section 4.5.
+type Delegations struct {
+ Keys map[string]*PublicKey `json:"keys"`
+ Roles []DelegatedRole `json:"roles"`
+}
+
+// DelegatedRole describes a delegated role, including what paths it is
+// reponsible for. See spec v1.0.19 section 4.5.
+type DelegatedRole struct {
+ Name string `json:"name"`
+ KeyIDs []string `json:"keyids"`
+ Threshold int `json:"threshold"`
+ Terminating bool `json:"terminating"`
+ PathHashPrefixes []string `json:"path_hash_prefixes,omitempty"`
+ Paths []string `json:"paths"`
+}
+
+// MatchesPath evaluates whether the path patterns or path hash prefixes match
+// a given file. This determines whether a delegated role is responsible for
+// signing and verifying the file.
+func (d *DelegatedRole) MatchesPath(file string) (bool, error) {
+ if err := d.validatePaths(); err != nil {
+ return false, err
+ }
+
+ for _, pattern := range d.Paths {
+ if matched, _ := path.Match(pattern, file); matched {
+ return true, nil
+ }
+ }
+
+ pathHash := PathHexDigest(file)
+ for _, hashPrefix := range d.PathHashPrefixes {
+ if strings.HasPrefix(pathHash, hashPrefix) {
+ return true, nil
+ }
+ }
+
+ return false, nil
+}
+
+// validatePaths enforces the spec
+// https://theupdateframework.github.io/specification/v1.0.19/index.html#file-formats-targets
+// 'role MUST specify only one of the "path_hash_prefixes" or "paths"'
+// Marshalling and unmarshalling JSON will fail and return
+// ErrPathsAndPathHashesSet if both fields are set and not empty.
+func (d *DelegatedRole) validatePaths() error {
+ if len(d.PathHashPrefixes) > 0 && len(d.Paths) > 0 {
+ return ErrPathsAndPathHashesSet
+ }
+
+ return nil
+}
+
+// MarshalJSON is called when writing the struct to JSON. We validate prior to
+// marshalling to ensure that an invalid delegated role can not be serialized
+// to JSON.
+func (d *DelegatedRole) MarshalJSON() ([]byte, error) {
+ type delegatedRoleAlias DelegatedRole
+
+ if err := d.validatePaths(); err != nil {
+ return nil, err
+ }
+
+ return json.Marshal((*delegatedRoleAlias)(d))
+}
+
+// UnmarshalJSON is called when reading the struct from JSON. We validate once
+// unmarshalled to ensure that an error is thrown if an invalid delegated role
+// is read.
+func (d *DelegatedRole) UnmarshalJSON(b []byte) error {
+ type delegatedRoleAlias DelegatedRole
+
+ // Prepare decoder
+ dec := json.NewDecoder(bytes.NewReader(b))
+
+ // Unmarshal delegated role
+ if err := dec.Decode((*delegatedRoleAlias)(d)); err != nil {
+ return err
+ }
+
+ return d.validatePaths()
+}
+
+func NewTargets() *Targets {
+ return &Targets{
+ Type: "targets",
+ SpecVersion: "1.0",
+ Expires: DefaultExpires("targets"),
+ Targets: make(TargetFiles),
+ }
+}
+
+type TimestampFileMeta metapathFileMeta
+
+type TimestampFiles map[string]TimestampFileMeta
+
+type Timestamp struct {
+ Type string `json:"_type"`
+ SpecVersion string `json:"spec_version"`
+ Version int64 `json:"version"`
+ Expires time.Time `json:"expires"`
+ Meta TimestampFiles `json:"meta"`
+ Custom *json.RawMessage `json:"custom,omitempty"`
+}
+
+func NewTimestamp() *Timestamp {
+ return &Timestamp{
+ Type: "timestamp",
+ SpecVersion: "1.0",
+ Expires: DefaultExpires("timestamp"),
+ Meta: make(TimestampFiles),
+ }
+}
diff --git a/data/types_test.go b/data/types_test.go
new file mode 100644
index 0000000..b9523fa
--- /dev/null
+++ b/data/types_test.go
@@ -0,0 +1,287 @@
+package data
+
+import (
+ "encoding/json"
+ "testing"
+
+ "github.com/secure-systems-lab/go-securesystemslib/cjson"
+ "github.com/stretchr/testify/assert"
+ . "gopkg.in/check.v1"
+)
+
+const (
+ // This public key is from the TUF specs:
+ //
+ // https://github.com/theupdateframework/specification
+ //
+ public = `"72378e5bc588793e58f81c8533da64a2e8f1565c1fcc7f253496394ffc52542c"`
+ keyid10 = "1bf1c6e3cdd3d3a8420b19199e27511999850f4b376c4547b2f32fba7e80fca3"
+ keyid10algos = "506a349b85945d0d99c7289c3f0f1f6c550218089d1d38a3f64824db31e827ac"
+)
+
+type TypesSuite struct{}
+
+var _ = Suite(&TypesSuite{})
+
+type ed25519Public struct {
+ PublicKey HexBytes `json:"public"`
+}
+
+func (TypesSuite) TestKeyIDs(c *C) {
+ var hexbytes HexBytes
+ err := json.Unmarshal([]byte(public), &hexbytes)
+ c.Assert(err, IsNil)
+ keyValBytes, err := json.Marshal(ed25519Public{PublicKey: hexbytes})
+ c.Assert(err, IsNil)
+
+ key := PublicKey{
+ Type: KeyTypeEd25519,
+ Scheme: KeySchemeEd25519,
+ Value: keyValBytes,
+ }
+ c.Assert(key.IDs(), DeepEquals, []string{keyid10})
+
+ key = PublicKey{
+ Type: KeyTypeEd25519,
+ Scheme: KeySchemeEd25519,
+ Algorithms: HashAlgorithms,
+ Value: keyValBytes,
+ }
+ c.Assert(key.IDs(), DeepEquals, []string{keyid10algos})
+}
+
+func (TypesSuite) TestRootAddKey(c *C) {
+ var hexbytes HexBytes
+ err := json.Unmarshal([]byte(public), &hexbytes)
+ c.Assert(err, IsNil)
+ keyValBytes, err := json.Marshal(ed25519Public{PublicKey: hexbytes})
+ c.Assert(err, IsNil)
+
+ key := &PublicKey{
+ Type: KeyTypeEd25519,
+ Scheme: KeySchemeEd25519,
+ Value: keyValBytes,
+ }
+
+ root := NewRoot()
+
+ c.Assert(root.AddKey(key), Equals, true)
+ c.Assert(root.AddKey(key), Equals, false)
+}
+
+func (TypesSuite) TestRoleAddKeyIDs(c *C) {
+ var hexbytes HexBytes
+ err := json.Unmarshal([]byte(public), &hexbytes)
+ c.Assert(err, IsNil)
+ keyValBytes, err := json.Marshal(ed25519Public{PublicKey: hexbytes})
+ c.Assert(err, IsNil)
+
+ key := &PublicKey{
+ Type: KeyTypeEd25519,
+ Scheme: KeySchemeEd25519,
+ Value: keyValBytes,
+ }
+
+ role := &Role{}
+ c.Assert(role.KeyIDs, HasLen, 0)
+
+ c.Assert(role.AddKeyIDs(key.IDs()), Equals, true)
+ c.Assert(role.KeyIDs, DeepEquals, []string{keyid10})
+
+ // Adding the key again doesn't modify the array.
+ c.Assert(role.AddKeyIDs(key.IDs()), Equals, false)
+ c.Assert(role.KeyIDs, DeepEquals, []string{keyid10})
+
+ // Add another key.
+ key = &PublicKey{
+ Type: KeyTypeEd25519,
+ Scheme: KeySchemeEd25519,
+ Algorithms: HashAlgorithms,
+ Value: keyValBytes,
+ }
+
+ // Adding the key again doesn't modify the array.
+ c.Assert(role.AddKeyIDs(key.IDs()), Equals, true)
+ c.Assert(role.KeyIDs, DeepEquals, []string{keyid10, keyid10algos})
+}
+
+func TestDelegatedRolePathMatch(t *testing.T) {
+ var tts = []struct {
+ testName string
+ pathPatterns []string
+ pathHashPrefixes []string
+ file string
+ shouldMatch bool
+ }{
+ {
+ testName: "no path",
+ file: "licence.txt",
+ },
+ {
+ testName: "match path *",
+ pathPatterns: []string{"null", "targets/*.tgz"},
+ file: "targets/foo.tgz",
+ shouldMatch: true,
+ },
+ {
+ testName: "does not match path *",
+ pathPatterns: []string{"null", "targets/*.tgz"},
+ file: "targets/foo.txt",
+ shouldMatch: false,
+ },
+ {
+ testName: "match path ?",
+ pathPatterns: []string{"foo-version-?.tgz"},
+ file: "foo-version-a.tgz",
+ shouldMatch: true,
+ },
+ {
+ testName: "does not match ?",
+ pathPatterns: []string{"foo-version-?.tgz"},
+ file: "foo-version-alpha.tgz",
+ shouldMatch: false,
+ },
+ // picked from https://github.com/theupdateframework/tuf/blob/30ba6e9f9ab25e0370e29ce574dada2d8809afa0/tests/test_updater.py#L1726-L1734
+ {
+ testName: "match hash prefix",
+ pathHashPrefixes: []string{"badd", "8baf"},
+ file: "/file3.txt",
+ shouldMatch: true,
+ },
+ {
+ testName: "does not match hash prefix",
+ pathHashPrefixes: []string{"badd"},
+ file: "/file3.txt",
+ shouldMatch: false,
+ },
+ {
+ testName: "hash prefix first char",
+ pathHashPrefixes: []string{"2"},
+ file: "/a/b/c/file_d.txt",
+ shouldMatch: true,
+ },
+ {
+ testName: "full hash prefix",
+ pathHashPrefixes: []string{"34c85d1ee84f61f10d7dc633472a49096ed87f8f764bd597831eac371f40ac39"},
+ file: "/e/f/g.txt",
+ shouldMatch: true,
+ },
+ }
+ for _, tt := range tts {
+ t.Run(tt.testName, func(t *testing.T) {
+ d := DelegatedRole{
+ Paths: tt.pathPatterns,
+ PathHashPrefixes: tt.pathHashPrefixes,
+ }
+ assert.NoError(t, d.validatePaths())
+
+ matchesPath, err := d.MatchesPath(tt.file)
+ assert.NoError(t, err)
+ assert.Equal(t, tt.shouldMatch, matchesPath)
+ })
+
+ }
+}
+
+func TestDelegatedRoleJSON(t *testing.T) {
+ var tts = []struct {
+ testName string
+ d *DelegatedRole
+ rawCJSON string
+ }{{
+ testName: "all fields with hashes",
+ d: &DelegatedRole{
+ Name: "n1",
+ KeyIDs: []string{"k1"},
+ Threshold: 5,
+ Terminating: true,
+ PathHashPrefixes: []string{"8f"},
+ },
+ rawCJSON: `{"keyids":["k1"],"name":"n1","path_hash_prefixes":["8f"],"paths":null,"terminating":true,"threshold":5}`,
+ },
+ {
+ testName: "paths only",
+ d: &DelegatedRole{
+ Name: "n2",
+ KeyIDs: []string{"k1", "k3"},
+ Threshold: 12,
+ Paths: []string{"*.txt"},
+ },
+ rawCJSON: `{"keyids":["k1","k3"],"name":"n2","paths":["*.txt"],"terminating":false,"threshold":12}`,
+ },
+ {
+ testName: "default",
+ d: &DelegatedRole{},
+ rawCJSON: `{"keyids":null,"name":"","paths":null,"terminating":false,"threshold":0}`,
+ },
+ }
+
+ for _, tt := range tts {
+ t.Run(tt.testName, func(t *testing.T) {
+ b, err := cjson.EncodeCanonical(tt.d)
+ assert.NoError(t, err)
+ assert.Equal(t, tt.rawCJSON, string(b))
+
+ newD := &DelegatedRole{}
+ err = json.Unmarshal(b, newD)
+ assert.NoError(t, err)
+ assert.Equal(t, tt.d, newD)
+ })
+ }
+}
+
+func TestDelegatedRoleUnmarshalErr(t *testing.T) {
+ targetsWithBothMatchers := []byte(`{"keyids":null,"name":"","paths":["*.txt"],"path_hash_prefixes":["8f"],"terminating":false,"threshold":0}`)
+ var d DelegatedRole
+ assert.Equal(t, ErrPathsAndPathHashesSet, json.Unmarshal(targetsWithBothMatchers, &d))
+
+ // test for type errors
+ err := json.Unmarshal([]byte(`{"keyids":"a"}`), &d)
+ assert.Equal(t, "keyids", err.(*json.UnmarshalTypeError).Field)
+}
+
+func TestCustomField(t *testing.T) {
+ testCustomJSON := json.RawMessage([]byte(`{"test":true}`))
+
+ root := Root{
+ Type: "root",
+ SpecVersion: "1.0",
+ Keys: make(map[string]*PublicKey),
+ Roles: make(map[string]*Role),
+ ConsistentSnapshot: true,
+ Custom: &testCustomJSON,
+ }
+ rootJSON, err := json.Marshal(&root)
+ assert.NoError(t, err)
+ assert.Equal(t, []byte("{\"_type\":\"root\",\"spec_version\":\"1.0\",\"version\":0,\"expires\":\"0001-01-01T00:00:00Z\",\"keys\":{},\"roles\":{},\"custom\":{\"test\":true},\"consistent_snapshot\":true}"), rootJSON)
+
+ targets := Targets{
+ Type: "targets",
+ SpecVersion: "1.0",
+ Targets: make(TargetFiles),
+ Custom: &testCustomJSON,
+ }
+ targetsJSON, err := json.Marshal(&targets)
+ assert.NoError(t, err)
+ assert.Equal(t, []byte("{\"_type\":\"targets\",\"spec_version\":\"1.0\",\"version\":0,\"expires\":\"0001-01-01T00:00:00Z\",\"targets\":{},\"custom\":{\"test\":true}}"), targetsJSON)
+
+ snapshot := Snapshot{
+ Type: "snapshot",
+ SpecVersion: "1.0",
+ Meta: make(SnapshotFiles),
+ Custom: &testCustomJSON,
+ }
+ snapshotJSON, err := json.Marshal(&snapshot)
+ assert.NoError(t, err)
+ assert.Equal(t, []byte("{\"_type\":\"snapshot\",\"spec_version\":\"1.0\",\"version\":0,\"expires\":\"0001-01-01T00:00:00Z\",\"meta\":{},\"custom\":{\"test\":true}}"), snapshotJSON)
+
+ timestamp := Timestamp{
+ Type: "timestamp",
+ SpecVersion: "1.0",
+ Meta: make(TimestampFiles),
+ Custom: &testCustomJSON,
+ }
+ timestampJSON, err := json.Marshal(&timestamp)
+ assert.NoError(t, err)
+ assert.Equal(t, []byte("{\"_type\":\"timestamp\",\"spec_version\":\"1.0\",\"version\":0,\"expires\":\"0001-01-01T00:00:00Z\",\"meta\":{},\"custom\":{\"test\":true}}"), timestampJSON)
+}