1package data
2
3import (
4	"bytes"
5	"crypto/sha256"
6	"crypto/sha512"
7	"crypto/subtle"
8	"encoding/hex"
9	"fmt"
10	"hash"
11	"io"
12	"io/ioutil"
13	"path"
14	"strings"
15	"time"
16
17	"github.com/docker/go/canonical/json"
18	"github.com/sirupsen/logrus"
19	"github.com/theupdateframework/notary"
20)
21
22// GUN is a Globally Unique Name. It is used to identify trust collections.
23// An example usage of this is for container image repositories.
24// For example: myregistry.io/myuser/myimage
25type GUN string
26
27func (g GUN) String() string {
28	return string(g)
29}
30
31// RoleName type for specifying role
32type RoleName string
33
34func (r RoleName) String() string {
35	return string(r)
36}
37
38// Parent provides the parent path role from the provided child role
39func (r RoleName) Parent() RoleName {
40	return RoleName(path.Dir(r.String()))
41}
42
43// MetadataRoleMapToStringMap generates a map string of bytes from a map RoleName of bytes
44func MetadataRoleMapToStringMap(roles map[RoleName][]byte) map[string][]byte {
45	metadata := make(map[string][]byte)
46	for k, v := range roles {
47		metadata[k.String()] = v
48	}
49	return metadata
50}
51
52// NewRoleList generates an array of RoleName objects from a slice of strings
53func NewRoleList(roles []string) []RoleName {
54	var roleNames []RoleName
55	for _, role := range roles {
56		roleNames = append(roleNames, RoleName(role))
57	}
58	return roleNames
59}
60
61// RolesListToStringList generates an array of string objects from a slice of roles
62func RolesListToStringList(roles []RoleName) []string {
63	var roleNames []string
64	for _, role := range roles {
65		roleNames = append(roleNames, role.String())
66	}
67	return roleNames
68}
69
70// SigAlgorithm for types of signatures
71type SigAlgorithm string
72
73func (k SigAlgorithm) String() string {
74	return string(k)
75}
76
77const defaultHashAlgorithm = "sha256"
78
79// NotaryDefaultExpiries is the construct used to configure the default expiry times of
80// the various role files.
81var NotaryDefaultExpiries = map[RoleName]time.Duration{
82	CanonicalRootRole:      notary.NotaryRootExpiry,
83	CanonicalTargetsRole:   notary.NotaryTargetsExpiry,
84	CanonicalSnapshotRole:  notary.NotarySnapshotExpiry,
85	CanonicalTimestampRole: notary.NotaryTimestampExpiry,
86}
87
88// Signature types
89const (
90	EDDSASignature       SigAlgorithm = "eddsa"
91	RSAPSSSignature      SigAlgorithm = "rsapss"
92	RSAPKCS1v15Signature SigAlgorithm = "rsapkcs1v15"
93	ECDSASignature       SigAlgorithm = "ecdsa"
94	PyCryptoSignature    SigAlgorithm = "pycrypto-pkcs#1 pss"
95)
96
97// Key types
98const (
99	ED25519Key   = "ed25519"
100	RSAKey       = "rsa"
101	RSAx509Key   = "rsa-x509"
102	ECDSAKey     = "ecdsa"
103	ECDSAx509Key = "ecdsa-x509"
104)
105
106// TUFTypes is the set of metadata types
107var TUFTypes = map[RoleName]string{
108	CanonicalRootRole:      "Root",
109	CanonicalTargetsRole:   "Targets",
110	CanonicalSnapshotRole:  "Snapshot",
111	CanonicalTimestampRole: "Timestamp",
112}
113
114// ValidTUFType checks if the given type is valid for the role
115func ValidTUFType(typ string, role RoleName) bool {
116	if ValidRole(role) {
117		// All targets delegation roles must have
118		// the valid type is for targets.
119		if role == "" {
120			// role is unknown and does not map to
121			// a type
122			return false
123		}
124		if strings.HasPrefix(role.String(), CanonicalTargetsRole.String()+"/") {
125			role = CanonicalTargetsRole
126		}
127	}
128	// most people will just use the defaults so have this optimal check
129	// first. Do comparison just in case there is some unknown vulnerability
130	// if a key and value in the map differ.
131	if v, ok := TUFTypes[role]; ok {
132		return typ == v
133	}
134	return false
135}
136
137// Signed is the high level, partially deserialized metadata object
138// used to verify signatures before fully unpacking, or to add signatures
139// before fully packing
140type Signed struct {
141	Signed     *json.RawMessage `json:"signed"`
142	Signatures []Signature      `json:"signatures"`
143}
144
145// SignedCommon contains the fields common to the Signed component of all
146// TUF metadata files
147type SignedCommon struct {
148	Type    string    `json:"_type"`
149	Expires time.Time `json:"expires"`
150	Version int       `json:"version"`
151}
152
153// SignedMeta is used in server validation where we only need signatures
154// and common fields
155type SignedMeta struct {
156	Signed     SignedCommon `json:"signed"`
157	Signatures []Signature  `json:"signatures"`
158}
159
160// Signature is a signature on a piece of metadata
161type Signature struct {
162	KeyID     string       `json:"keyid"`
163	Method    SigAlgorithm `json:"method"`
164	Signature []byte       `json:"sig"`
165	IsValid   bool         `json:"-"`
166}
167
168// Files is the map of paths to file meta container in targets and delegations
169// metadata files
170type Files map[string]FileMeta
171
172// Hashes is the map of hash type to digest created for each metadata
173// and target file
174type Hashes map[string][]byte
175
176// NotaryDefaultHashes contains the default supported hash algorithms.
177var NotaryDefaultHashes = []string{notary.SHA256, notary.SHA512}
178
179// FileMeta contains the size and hashes for a metadata or target file. Custom
180// data can be optionally added.
181type FileMeta struct {
182	Length int64            `json:"length"`
183	Hashes Hashes           `json:"hashes"`
184	Custom *json.RawMessage `json:"custom,omitempty"`
185}
186
187// Equals returns true if the other FileMeta object is equivalent to this one
188func (f FileMeta) Equals(o FileMeta) bool {
189	if o.Length != f.Length || len(f.Hashes) != len(f.Hashes) {
190		return false
191	}
192	if f.Custom == nil && o.Custom != nil || f.Custom != nil && o.Custom == nil {
193		return false
194	}
195	// we don't care if these are valid hashes, just that they are equal
196	for key, val := range f.Hashes {
197		if !bytes.Equal(val, o.Hashes[key]) {
198			return false
199		}
200	}
201	if f.Custom == nil && o.Custom == nil {
202		return true
203	}
204	fBytes, err := f.Custom.MarshalJSON()
205	if err != nil {
206		return false
207	}
208	oBytes, err := o.Custom.MarshalJSON()
209	if err != nil {
210		return false
211	}
212	return bytes.Equal(fBytes, oBytes)
213}
214
215// CheckHashes verifies all the checksums specified by the "hashes" of the payload.
216func CheckHashes(payload []byte, name string, hashes Hashes) error {
217	cnt := 0
218
219	// k, v indicate the hash algorithm and the corresponding value
220	for k, v := range hashes {
221		switch k {
222		case notary.SHA256:
223			checksum := sha256.Sum256(payload)
224			if subtle.ConstantTimeCompare(checksum[:], v) == 0 {
225				return ErrMismatchedChecksum{alg: notary.SHA256, name: name, expected: hex.EncodeToString(v)}
226			}
227			cnt++
228		case notary.SHA512:
229			checksum := sha512.Sum512(payload)
230			if subtle.ConstantTimeCompare(checksum[:], v) == 0 {
231				return ErrMismatchedChecksum{alg: notary.SHA512, name: name, expected: hex.EncodeToString(v)}
232			}
233			cnt++
234		}
235	}
236
237	if cnt == 0 {
238		return ErrMissingMeta{Role: name}
239	}
240
241	return nil
242}
243
244// CompareMultiHashes verifies that the two Hashes passed in can represent the same data.
245// This means that both maps must have at least one key defined for which they map, and no conflicts.
246// Note that we check the intersection of map keys, which adds support for non-default hash algorithms in notary
247func CompareMultiHashes(hashes1, hashes2 Hashes) error {
248	// First check if the two hash structures are valid
249	if err := CheckValidHashStructures(hashes1); err != nil {
250		return err
251	}
252	if err := CheckValidHashStructures(hashes2); err != nil {
253		return err
254	}
255	// Check if they have at least one matching hash, and no conflicts
256	cnt := 0
257	for hashAlg, hash1 := range hashes1 {
258
259		hash2, ok := hashes2[hashAlg]
260		if !ok {
261			continue
262		}
263
264		if subtle.ConstantTimeCompare(hash1[:], hash2[:]) == 0 {
265			return fmt.Errorf("mismatched %s checksum", hashAlg)
266		}
267		// If we reached here, we had a match
268		cnt++
269	}
270
271	if cnt == 0 {
272		return fmt.Errorf("at least one matching hash needed")
273	}
274
275	return nil
276}
277
278// CheckValidHashStructures returns an error, or nil, depending on whether
279// the content of the hashes is valid or not.
280func CheckValidHashStructures(hashes Hashes) error {
281	cnt := 0
282
283	for k, v := range hashes {
284		switch k {
285		case notary.SHA256:
286			if len(v) != sha256.Size {
287				return ErrInvalidChecksum{alg: notary.SHA256}
288			}
289			cnt++
290		case notary.SHA512:
291			if len(v) != sha512.Size {
292				return ErrInvalidChecksum{alg: notary.SHA512}
293			}
294			cnt++
295		}
296	}
297
298	if cnt == 0 {
299		return fmt.Errorf("at least one supported hash needed")
300	}
301
302	return nil
303}
304
305// NewFileMeta generates a FileMeta object from the reader, using the
306// hash algorithms provided
307func NewFileMeta(r io.Reader, hashAlgorithms ...string) (FileMeta, error) {
308	if len(hashAlgorithms) == 0 {
309		hashAlgorithms = []string{defaultHashAlgorithm}
310	}
311	hashes := make(map[string]hash.Hash, len(hashAlgorithms))
312	for _, hashAlgorithm := range hashAlgorithms {
313		var h hash.Hash
314		switch hashAlgorithm {
315		case notary.SHA256:
316			h = sha256.New()
317		case notary.SHA512:
318			h = sha512.New()
319		default:
320			return FileMeta{}, fmt.Errorf("Unknown hash algorithm: %s", hashAlgorithm)
321		}
322		hashes[hashAlgorithm] = h
323		r = io.TeeReader(r, h)
324	}
325	n, err := io.Copy(ioutil.Discard, r)
326	if err != nil {
327		return FileMeta{}, err
328	}
329	m := FileMeta{Length: n, Hashes: make(Hashes, len(hashes))}
330	for hashAlgorithm, h := range hashes {
331		m.Hashes[hashAlgorithm] = h.Sum(nil)
332	}
333	return m, nil
334}
335
336// Delegations holds a tier of targets delegations
337type Delegations struct {
338	Keys  Keys    `json:"keys"`
339	Roles []*Role `json:"roles"`
340}
341
342// NewDelegations initializes an empty Delegations object
343func NewDelegations() *Delegations {
344	return &Delegations{
345		Keys:  make(map[string]PublicKey),
346		Roles: make([]*Role, 0),
347	}
348}
349
350// These values are recommended TUF expiry times.
351var defaultExpiryTimes = map[RoleName]time.Duration{
352	CanonicalRootRole:      notary.Year,
353	CanonicalTargetsRole:   90 * notary.Day,
354	CanonicalSnapshotRole:  7 * notary.Day,
355	CanonicalTimestampRole: notary.Day,
356}
357
358// SetDefaultExpiryTimes allows one to change the default expiries.
359func SetDefaultExpiryTimes(times map[RoleName]time.Duration) {
360	for key, value := range times {
361		if _, ok := defaultExpiryTimes[key]; !ok {
362			logrus.Errorf("Attempted to set default expiry for an unknown role: %s", key.String())
363			continue
364		}
365		defaultExpiryTimes[key] = value
366	}
367}
368
369// DefaultExpires gets the default expiry time for the given role
370func DefaultExpires(role RoleName) time.Time {
371	if d, ok := defaultExpiryTimes[role]; ok {
372		return time.Now().Add(d)
373	}
374	var t time.Time
375	return t.UTC().Round(time.Second)
376}
377
378type unmarshalledSignature Signature
379
380// UnmarshalJSON does a custom unmarshalling of the signature JSON
381func (s *Signature) UnmarshalJSON(data []byte) error {
382	uSignature := unmarshalledSignature{}
383	err := json.Unmarshal(data, &uSignature)
384	if err != nil {
385		return err
386	}
387	uSignature.Method = SigAlgorithm(strings.ToLower(string(uSignature.Method)))
388	*s = Signature(uSignature)
389	return nil
390}
391