forked from notaryproject/notary
-
Notifications
You must be signed in to change notification settings - Fork 0
/
tuf.go
1072 lines (970 loc) · 34.7 KB
/
tuf.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Package tuf defines the core TUF logic around manipulating a repo.
package tuf
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/sirupsen/logrus"
"github.com/theupdateframework/notary"
"github.com/theupdateframework/notary/tuf/data"
"github.com/theupdateframework/notary/tuf/signed"
"github.com/theupdateframework/notary/tuf/utils"
)
// ErrSigVerifyFail - signature verification failed
type ErrSigVerifyFail struct{}
func (e ErrSigVerifyFail) Error() string {
return "Error: Signature verification failed"
}
// ErrMetaExpired - metadata file has expired
type ErrMetaExpired struct{}
func (e ErrMetaExpired) Error() string {
return "Error: Metadata has expired"
}
// ErrLocalRootExpired - the local root file is out of date
type ErrLocalRootExpired struct{}
func (e ErrLocalRootExpired) Error() string {
return "Error: Local Root Has Expired"
}
// ErrNotLoaded - attempted to access data that has not been loaded into
// the repo. This means specifically that the relevant JSON file has not
// been loaded.
type ErrNotLoaded struct {
Role data.RoleName
}
func (err ErrNotLoaded) Error() string {
return fmt.Sprintf("%s role has not been loaded", err.Role)
}
// StopWalk - used by visitor functions to signal WalkTargets to stop walking
type StopWalk struct{}
// Repo is an in memory representation of the TUF Repo.
// It operates at the data.Signed level, accepting and producing
// data.Signed objects. Users of a Repo are responsible for
// fetching raw JSON and using the Set* functions to populate
// the Repo instance.
type Repo struct {
Root *data.SignedRoot
Targets map[data.RoleName]*data.SignedTargets
Snapshot *data.SignedSnapshot
Timestamp *data.SignedTimestamp
cryptoService signed.CryptoService
// Because Repo is a mutable structure, these keep track of what the root
// role was when a root is set on the repo (as opposed to what it might be
// after things like AddBaseKeys and RemoveBaseKeys have been called on it).
// If we know what the original was, we'll if and how to handle root
// rotations.
originalRootRole data.BaseRole
}
// NewRepo initializes a Repo instance with a CryptoService.
// If the Repo will only be used for reading, the CryptoService
// can be nil.
func NewRepo(cryptoService signed.CryptoService) *Repo {
return &Repo{
Targets: make(map[data.RoleName]*data.SignedTargets),
cryptoService: cryptoService,
}
}
// AddBaseKeys is used to add keys to the role in root.json
func (tr *Repo) AddBaseKeys(role data.RoleName, keys ...data.PublicKey) error {
if tr.Root == nil {
return ErrNotLoaded{Role: data.CanonicalRootRole}
}
ids := []string{}
for _, k := range keys {
// Store only the public portion
tr.Root.Signed.Keys[k.ID()] = k
tr.Root.Signed.Roles[role].KeyIDs = append(tr.Root.Signed.Roles[role].KeyIDs, k.ID())
ids = append(ids, k.ID())
}
tr.Root.Dirty = true
// also, whichever role was switched out needs to be re-signed
// root has already been marked dirty.
switch role {
case data.CanonicalSnapshotRole:
if tr.Snapshot != nil {
tr.Snapshot.Dirty = true
}
case data.CanonicalTargetsRole:
if target, ok := tr.Targets[data.CanonicalTargetsRole]; ok {
target.Dirty = true
}
case data.CanonicalTimestampRole:
if tr.Timestamp != nil {
tr.Timestamp.Dirty = true
}
}
return nil
}
// ReplaceBaseKeys is used to replace all keys for the given role with the new keys
func (tr *Repo) ReplaceBaseKeys(role data.RoleName, keys ...data.PublicKey) error {
r, err := tr.GetBaseRole(role)
if err != nil {
return err
}
err = tr.RemoveBaseKeys(role, r.ListKeyIDs()...)
if err != nil {
return err
}
return tr.AddBaseKeys(role, keys...)
}
// RemoveBaseKeys is used to remove keys from the roles in root.json
func (tr *Repo) RemoveBaseKeys(role data.RoleName, keyIDs ...string) error {
if tr.Root == nil {
return ErrNotLoaded{Role: data.CanonicalRootRole}
}
var keep []string
toDelete := make(map[string]struct{})
emptyStruct := struct{}{}
// remove keys from specified role
for _, k := range keyIDs {
toDelete[k] = emptyStruct
}
oldKeyIDs := tr.Root.Signed.Roles[role].KeyIDs
for _, rk := range oldKeyIDs {
if _, ok := toDelete[rk]; !ok {
keep = append(keep, rk)
}
}
tr.Root.Signed.Roles[role].KeyIDs = keep
// also, whichever role had keys removed needs to be re-signed
// root has already been marked dirty.
tr.markRoleDirty(role)
// determine which keys are no longer in use by any roles
for roleName, r := range tr.Root.Signed.Roles {
if roleName == role {
continue
}
for _, rk := range r.KeyIDs {
if _, ok := toDelete[rk]; ok {
delete(toDelete, rk)
}
}
}
// Remove keys no longer in use by any roles, except for root keys.
// Root private keys must be kept in tr.cryptoService to be able to sign
// for rotation, and root certificates must be kept in tr.Root.SignedKeys
// because we are not necessarily storing them elsewhere (tuf.Repo does not
// depend on certs.Manager, that is an upper layer), and without storing
// the certificates in their x509 form we are not able to do the
// util.CanonicalKeyID conversion.
if role != data.CanonicalRootRole {
for k := range toDelete {
delete(tr.Root.Signed.Keys, k)
tr.cryptoService.RemoveKey(k)
}
}
tr.Root.Dirty = true
return nil
}
func (tr *Repo) markRoleDirty(role data.RoleName) {
switch role {
case data.CanonicalSnapshotRole:
if tr.Snapshot != nil {
tr.Snapshot.Dirty = true
}
case data.CanonicalTargetsRole:
if target, ok := tr.Targets[data.CanonicalTargetsRole]; ok {
target.Dirty = true
}
case data.CanonicalTimestampRole:
if tr.Timestamp != nil {
tr.Timestamp.Dirty = true
}
}
}
// GetBaseRole gets a base role from this repo's metadata
func (tr *Repo) GetBaseRole(name data.RoleName) (data.BaseRole, error) {
if !data.ValidRole(name) {
return data.BaseRole{}, data.ErrInvalidRole{Role: name, Reason: "invalid base role name"}
}
if tr.Root == nil {
return data.BaseRole{}, ErrNotLoaded{data.CanonicalRootRole}
}
// Find the role data public keys for the base role from TUF metadata
baseRole, err := tr.Root.BuildBaseRole(name)
if err != nil {
return data.BaseRole{}, err
}
return baseRole, nil
}
// GetDelegationRole gets a delegation role from this repo's metadata, walking from the targets role down to the delegation itself
func (tr *Repo) GetDelegationRole(name data.RoleName) (data.DelegationRole, error) {
if !data.IsDelegation(name) {
return data.DelegationRole{}, data.ErrInvalidRole{Role: name, Reason: "invalid delegation name"}
}
if tr.Root == nil {
return data.DelegationRole{}, ErrNotLoaded{data.CanonicalRootRole}
}
_, ok := tr.Root.Signed.Roles[data.CanonicalTargetsRole]
if !ok {
return data.DelegationRole{}, ErrNotLoaded{data.CanonicalTargetsRole}
}
// Traverse target metadata, down to delegation itself
// Get all public keys for the base role from TUF metadata
_, ok = tr.Targets[data.CanonicalTargetsRole]
if !ok {
return data.DelegationRole{}, ErrNotLoaded{data.CanonicalTargetsRole}
}
// Start with top level roles in targets. Walk the chain of ancestors
// until finding the desired role, or we run out of targets files to search.
var foundRole *data.DelegationRole
buildDelegationRoleVisitor := func(tgt *data.SignedTargets, validRole data.DelegationRole) interface{} {
// Try to find the delegation and build a DelegationRole structure
for _, role := range tgt.Signed.Delegations.Roles {
if role.Name == name {
delgRole, err := tgt.BuildDelegationRole(name)
if err != nil {
return err
}
// Check all public key certificates in the role for expiry
// Currently we do not reject expired delegation keys but warn if they might expire soon or have already
for _, pubKey := range delgRole.Keys {
certFromKey, err := utils.LoadCertFromPEM(pubKey.Public())
if err != nil {
continue
}
//Don't check the delegation certificate expiry once added, use the TUF role expiry instead
if err := utils.ValidateCertificate(certFromKey, false); err != nil {
return err
}
}
foundRole = &delgRole
return StopWalk{}
}
}
return nil
}
// Walk to the parent of this delegation, since that is where its role metadata exists
err := tr.WalkTargets("", name.Parent(), buildDelegationRoleVisitor)
if err != nil {
return data.DelegationRole{}, err
}
// We never found the delegation. In the context of this repo it is considered
// invalid. N.B. it may be that it existed at one point but an ancestor has since
// been modified/removed.
if foundRole == nil {
return data.DelegationRole{}, data.ErrInvalidRole{Role: name, Reason: "delegation does not exist"}
}
return *foundRole, nil
}
// GetAllLoadedRoles returns a list of all role entries loaded in this TUF repo, could be empty
func (tr *Repo) GetAllLoadedRoles() []*data.Role {
var res []*data.Role
if tr.Root == nil {
// if root isn't loaded, we should consider we have no loaded roles because we can't
// trust any other state that might be present
return res
}
for name, rr := range tr.Root.Signed.Roles {
res = append(res, &data.Role{
RootRole: *rr,
Name: name,
})
}
for _, delegate := range tr.Targets {
for _, r := range delegate.Signed.Delegations.Roles {
res = append(res, r)
}
}
return res
}
// Walk to parent, and either create or update this delegation. We can only create a new delegation if we're given keys
// Ensure all updates are valid, by checking against parent ancestor paths and ensuring the keys meet the role threshold.
func delegationUpdateVisitor(roleName data.RoleName, addKeys data.KeyList, removeKeys, addPaths, removePaths []string, clearAllPaths bool, newThreshold int) walkVisitorFunc {
return func(tgt *data.SignedTargets, validRole data.DelegationRole) interface{} {
var err error
// Validate the changes underneath this restricted validRole for adding paths, reject invalid path additions
if len(addPaths) != len(data.RestrictDelegationPathPrefixes(validRole.Paths, addPaths)) {
return data.ErrInvalidRole{Role: roleName, Reason: "invalid paths to add to role"}
}
// Try to find the delegation and amend it using our changelist
var delgRole *data.Role
for _, role := range tgt.Signed.Delegations.Roles {
if role.Name == roleName {
// Make a copy and operate on this role until we validate the changes
keyIDCopy := make([]string, len(role.KeyIDs))
copy(keyIDCopy, role.KeyIDs)
pathsCopy := make([]string, len(role.Paths))
copy(pathsCopy, role.Paths)
delgRole = &data.Role{
RootRole: data.RootRole{
KeyIDs: keyIDCopy,
Threshold: role.Threshold,
},
Name: role.Name,
Paths: pathsCopy,
}
delgRole.RemovePaths(removePaths)
if clearAllPaths {
delgRole.Paths = []string{}
}
delgRole.AddPaths(addPaths)
delgRole.RemoveKeys(removeKeys)
break
}
}
// We didn't find the role earlier, so create it.
if addKeys == nil {
addKeys = data.KeyList{} // initialize to empty list if necessary so calling .IDs() below won't panic
}
if delgRole == nil {
delgRole, err = data.NewRole(roleName, newThreshold, addKeys.IDs(), addPaths)
if err != nil {
return err
}
}
// Add the key IDs to the role and the keys themselves to the parent
for _, k := range addKeys {
if !utils.StrSliceContains(delgRole.KeyIDs, k.ID()) {
delgRole.KeyIDs = append(delgRole.KeyIDs, k.ID())
}
}
// Make sure we have a valid role still
if len(delgRole.KeyIDs) < delgRole.Threshold {
logrus.Warnf("role %s has fewer keys than its threshold of %d; it will not be usable until keys are added to it", delgRole.Name, delgRole.Threshold)
}
// NOTE: this closure CANNOT error after this point, as we've committed to editing the SignedTargets metadata in the repo object.
// Any errors related to updating this delegation must occur before this point.
// If all of our changes were valid, we should edit the actual SignedTargets to match our copy
for _, k := range addKeys {
tgt.Signed.Delegations.Keys[k.ID()] = k
}
foundAt := utils.FindRoleIndex(tgt.Signed.Delegations.Roles, delgRole.Name)
if foundAt < 0 {
tgt.Signed.Delegations.Roles = append(tgt.Signed.Delegations.Roles, delgRole)
} else {
tgt.Signed.Delegations.Roles[foundAt] = delgRole
}
tgt.Dirty = true
utils.RemoveUnusedKeys(tgt)
return StopWalk{}
}
}
// UpdateDelegationKeys updates the appropriate delegations, either adding
// a new delegation or updating an existing one. If keys are
// provided, the IDs will be added to the role (if they do not exist
// there already), and the keys will be added to the targets file.
func (tr *Repo) UpdateDelegationKeys(roleName data.RoleName, addKeys data.KeyList, removeKeys []string, newThreshold int) error {
if !data.IsDelegation(roleName) {
return data.ErrInvalidRole{Role: roleName, Reason: "not a valid delegated role"}
}
parent := roleName.Parent()
if err := tr.VerifyCanSign(parent); err != nil {
return err
}
// check the parent role's metadata
_, ok := tr.Targets[parent]
if !ok { // the parent targetfile may not exist yet - if not, then create it
var err error
_, err = tr.InitTargets(parent)
if err != nil {
return err
}
}
// Walk to the parent of this delegation, since that is where its role metadata exists
// We do not have to verify that the walker reached its desired role in this scenario
// since we've already done another walk to the parent role in VerifyCanSign, and potentially made a targets file
return tr.WalkTargets("", roleName.Parent(), delegationUpdateVisitor(roleName, addKeys, removeKeys, []string{}, []string{}, false, newThreshold))
}
// PurgeDelegationKeys removes the provided canonical key IDs from all delegations
// present in the subtree rooted at role. The role argument must be provided in a wildcard
// format, i.e. targets/* would remove the key from all delegations in the repo
func (tr *Repo) PurgeDelegationKeys(role data.RoleName, removeKeys []string) error {
if !data.IsWildDelegation(role) {
return data.ErrInvalidRole{
Role: role,
Reason: "only wildcard roles can be used in a purge",
}
}
removeIDs := make(map[string]struct{})
for _, id := range removeKeys {
removeIDs[id] = struct{}{}
}
start := role.Parent()
tufIDToCanon := make(map[string]string)
purgeKeys := func(tgt *data.SignedTargets, validRole data.DelegationRole) interface{} {
var (
deleteCandidates []string
err error
)
for id, key := range tgt.Signed.Delegations.Keys {
var (
canonID string
ok bool
)
if canonID, ok = tufIDToCanon[id]; !ok {
canonID, err = utils.CanonicalKeyID(key)
if err != nil {
return err
}
tufIDToCanon[id] = canonID
}
if _, ok := removeIDs[canonID]; ok {
deleteCandidates = append(deleteCandidates, id)
}
}
if len(deleteCandidates) == 0 {
// none of the interesting keys were present. We're done with this role
return nil
}
// now we know there are changes, check if we'll be able to sign them in
if err := tr.VerifyCanSign(validRole.Name); err != nil {
logrus.Warnf(
"role %s contains keys being purged but you do not have the necessary keys present to sign it; keys will not be purged from %s or its immediate children",
validRole.Name,
validRole.Name,
)
return nil
}
// we know we can sign in the changes, delete the keys
for _, id := range deleteCandidates {
delete(tgt.Signed.Delegations.Keys, id)
}
// delete candidate keys from all roles.
for _, role := range tgt.Signed.Delegations.Roles {
role.RemoveKeys(deleteCandidates)
if len(role.KeyIDs) < role.Threshold {
logrus.Warnf("role %s has fewer keys than its threshold of %d; it will not be usable until keys are added to it", role.Name, role.Threshold)
}
}
tgt.Dirty = true
return nil
}
return tr.WalkTargets("", start, purgeKeys)
}
// UpdateDelegationPaths updates the appropriate delegation's paths.
// It is not allowed to create a new delegation.
func (tr *Repo) UpdateDelegationPaths(roleName data.RoleName, addPaths, removePaths []string, clearPaths bool) error {
if !data.IsDelegation(roleName) {
return data.ErrInvalidRole{Role: roleName, Reason: "not a valid delegated role"}
}
parent := roleName.Parent()
if err := tr.VerifyCanSign(parent); err != nil {
return err
}
// check the parent role's metadata
_, ok := tr.Targets[parent]
if !ok { // the parent targetfile may not exist yet
// if not, this is an error because a delegation must exist to edit only paths
return data.ErrInvalidRole{Role: roleName, Reason: "no valid delegated role exists"}
}
// Walk to the parent of this delegation, since that is where its role metadata exists
// We do not have to verify that the walker reached its desired role in this scenario
// since we've already done another walk to the parent role in VerifyCanSign
err := tr.WalkTargets("", parent, delegationUpdateVisitor(roleName, data.KeyList{}, []string{}, addPaths, removePaths, clearPaths, notary.MinThreshold))
if err != nil {
return err
}
return nil
}
// DeleteDelegation removes a delegated targets role from its parent
// targets object. It also deletes the delegation from the snapshot.
// DeleteDelegation will only make use of the role Name field.
func (tr *Repo) DeleteDelegation(roleName data.RoleName) error {
if !data.IsDelegation(roleName) {
return data.ErrInvalidRole{Role: roleName, Reason: "not a valid delegated role"}
}
parent := roleName.Parent()
if err := tr.VerifyCanSign(parent); err != nil {
return err
}
// delete delegated data from Targets map and Snapshot - if they don't
// exist, these are no-op
delete(tr.Targets, roleName)
tr.Snapshot.DeleteMeta(roleName)
p, ok := tr.Targets[parent]
if !ok {
// if there is no parent metadata (the role exists though), then this
// is as good as done.
return nil
}
foundAt := utils.FindRoleIndex(p.Signed.Delegations.Roles, roleName)
if foundAt >= 0 {
var roles []*data.Role
// slice out deleted role
roles = append(roles, p.Signed.Delegations.Roles[:foundAt]...)
if foundAt+1 < len(p.Signed.Delegations.Roles) {
roles = append(roles, p.Signed.Delegations.Roles[foundAt+1:]...)
}
p.Signed.Delegations.Roles = roles
utils.RemoveUnusedKeys(p)
p.Dirty = true
} // if the role wasn't found, it's a good as deleted
return nil
}
// InitRoot initializes an empty root file with the 4 core roles passed to the
// method, and the consistent flag.
func (tr *Repo) InitRoot(root, timestamp, snapshot, targets data.BaseRole, consistent bool) error {
rootRoles := make(map[data.RoleName]*data.RootRole)
rootKeys := make(map[string]data.PublicKey)
for _, r := range []data.BaseRole{root, timestamp, snapshot, targets} {
rootRoles[r.Name] = &data.RootRole{
Threshold: r.Threshold,
KeyIDs: r.ListKeyIDs(),
}
for kid, k := range r.Keys {
rootKeys[kid] = k
}
}
r, err := data.NewRoot(rootKeys, rootRoles, consistent)
if err != nil {
return err
}
tr.Root = r
tr.originalRootRole = root
return nil
}
// InitTargets initializes an empty targets, and returns the new empty target
func (tr *Repo) InitTargets(role data.RoleName) (*data.SignedTargets, error) {
if !data.IsDelegation(role) && role != data.CanonicalTargetsRole {
return nil, data.ErrInvalidRole{
Role: role,
Reason: fmt.Sprintf("role is not a valid targets role name: %s", role.String()),
}
}
targets := data.NewTargets()
tr.Targets[role] = targets
return targets, nil
}
// InitSnapshot initializes a snapshot based on the current root and targets
func (tr *Repo) InitSnapshot() error {
if tr.Root == nil {
return ErrNotLoaded{Role: data.CanonicalRootRole}
}
root, err := tr.Root.ToSigned()
if err != nil {
return err
}
if _, ok := tr.Targets[data.CanonicalTargetsRole]; !ok {
return ErrNotLoaded{Role: data.CanonicalTargetsRole}
}
targets, err := tr.Targets[data.CanonicalTargetsRole].ToSigned()
if err != nil {
return err
}
snapshot, err := data.NewSnapshot(root, targets)
if err != nil {
return err
}
tr.Snapshot = snapshot
return nil
}
// InitTimestamp initializes a timestamp based on the current snapshot
func (tr *Repo) InitTimestamp() error {
snap, err := tr.Snapshot.ToSigned()
if err != nil {
return err
}
timestamp, err := data.NewTimestamp(snap)
if err != nil {
return err
}
tr.Timestamp = timestamp
return nil
}
// TargetMeta returns the FileMeta entry for the given path in the
// targets file associated with the given role. This may be nil if
// the target isn't found in the targets file.
func (tr Repo) TargetMeta(role data.RoleName, path string) *data.FileMeta {
if t, ok := tr.Targets[role]; ok {
if m, ok := t.Signed.Targets[path]; ok {
return &m
}
}
return nil
}
// TargetDelegations returns a slice of Roles that are valid publishers
// for the target path provided.
func (tr Repo) TargetDelegations(role data.RoleName, path string) []*data.Role {
var roles []*data.Role
if t, ok := tr.Targets[role]; ok {
for _, r := range t.Signed.Delegations.Roles {
if r.CheckPaths(path) {
roles = append(roles, r)
}
}
}
return roles
}
// VerifyCanSign returns nil if the role exists and we have at least one
// signing key for the role, false otherwise. This does not check that we have
// enough signing keys to meet the threshold, since we want to support the use
// case of multiple signers for a role. It returns an error if the role doesn't
// exist or if there are no signing keys.
func (tr *Repo) VerifyCanSign(roleName data.RoleName) error {
var (
role data.BaseRole
err error
canonicalKeyIDs []string
)
// we only need the BaseRole part of a delegation because we're just
// checking KeyIDs
if data.IsDelegation(roleName) {
r, err := tr.GetDelegationRole(roleName)
if err != nil {
return err
}
role = r.BaseRole
} else {
role, err = tr.GetBaseRole(roleName)
}
if err != nil {
return data.ErrInvalidRole{Role: roleName, Reason: "does not exist"}
}
for keyID, k := range role.Keys {
check := []string{keyID}
if canonicalID, err := utils.CanonicalKeyID(k); err == nil {
check = append(check, canonicalID)
canonicalKeyIDs = append(canonicalKeyIDs, canonicalID)
}
for _, id := range check {
p, _, err := tr.cryptoService.GetPrivateKey(id)
if err == nil && p != nil {
return nil
}
}
}
return signed.ErrNoKeys{KeyIDs: canonicalKeyIDs}
}
// used for walking the targets/delegations tree, potentially modifying the underlying SignedTargets for the repo
type walkVisitorFunc func(*data.SignedTargets, data.DelegationRole) interface{}
// WalkTargets will apply the specified visitor function to iteratively walk the targets/delegation metadata tree,
// until receiving a StopWalk. The walk starts from the base "targets" role, and searches for the correct targetPath and/or rolePath
// to call the visitor function on. Any roles passed into skipRoles will be excluded from the walk, as well as roles in those subtrees
func (tr *Repo) WalkTargets(targetPath string, rolePath data.RoleName, visitTargets walkVisitorFunc, skipRoles ...data.RoleName) error {
// Start with the base targets role, which implicitly has the "" targets path
targetsRole, err := tr.GetBaseRole(data.CanonicalTargetsRole)
if err != nil {
return err
}
// Make the targets role have the empty path, when we treat it as a delegation role
roles := []data.DelegationRole{
{
BaseRole: targetsRole,
Paths: []string{""},
},
}
for len(roles) > 0 {
role := roles[0]
roles = roles[1:]
// Check the role metadata
signedTgt, ok := tr.Targets[role.Name]
if !ok {
// The role meta doesn't exist in the repo so continue onward
continue
}
// We're at a prefix of the desired role subtree, so add its delegation role children and continue walking
if strings.HasPrefix(rolePath.String(), role.Name.String()+"/") {
roles = append(roles, signedTgt.GetValidDelegations(role)...)
continue
}
// Determine whether to visit this role or not:
// If the paths validate against the specified targetPath and the role is empty or is a path in the subtree.
// Also check if we are choosing to skip visiting this role on this walk (see ListTargets and GetTargetByName priority)
if isValidPath(targetPath, role) && isAncestorRole(role.Name, rolePath) && !utils.RoleNameSliceContains(skipRoles, role.Name) {
// If we had matching path or role name, visit this target and determine whether or not to keep walking
res := visitTargets(signedTgt, role)
switch typedRes := res.(type) {
case StopWalk:
// If the visitor function signalled a stop, return nil to finish the walk
return nil
case nil:
// If the visitor function signalled to continue, add this role's delegation to the walk
roles = append(roles, signedTgt.GetValidDelegations(role)...)
case error:
// Propagate any errors from the visitor
return typedRes
default:
// Return out with an error if we got a different result
return fmt.Errorf("unexpected return while walking: %v", res)
}
}
}
return nil
}
// helper function that returns whether the candidateChild role name is an ancestor or equal to the candidateAncestor role name
// Will return true if given an empty candidateAncestor role name
// The HasPrefix check is for determining whether the role name for candidateChild is a child (direct or further down the chain)
// of candidateAncestor, for ex: candidateAncestor targets/a and candidateChild targets/a/b/c
func isAncestorRole(candidateChild data.RoleName, candidateAncestor data.RoleName) bool {
return candidateAncestor.String() == "" || candidateAncestor == candidateChild || strings.HasPrefix(candidateChild.String(), candidateAncestor.String()+"/")
}
// helper function that returns whether the delegation Role is valid against the given path
// Will return true if given an empty candidatePath
func isValidPath(candidatePath string, delgRole data.DelegationRole) bool {
return candidatePath == "" || delgRole.CheckPaths(candidatePath)
}
// AddTargets will attempt to add the given targets specifically to
// the directed role. If the metadata for the role doesn't exist yet,
// AddTargets will create one.
func (tr *Repo) AddTargets(role data.RoleName, targets data.Files) (data.Files, error) {
cantSignErr := tr.VerifyCanSign(role)
if _, ok := cantSignErr.(data.ErrInvalidRole); ok {
return nil, cantSignErr
}
var needSign bool
// check existence of the role's metadata
_, ok := tr.Targets[role]
if !ok { // the targetfile may not exist yet - if not, then create it
var err error
_, err = tr.InitTargets(role)
if err != nil {
return nil, err
}
}
addedTargets := make(data.Files)
addTargetVisitor := func(targetPath string, targetMeta data.FileMeta) func(*data.SignedTargets, data.DelegationRole) interface{} {
return func(tgt *data.SignedTargets, validRole data.DelegationRole) interface{} {
// We've already validated the role's target path in our walk, so just modify the metadata
if targetMeta.Equals(tgt.Signed.Targets[targetPath]) {
// Also add to our new addedTargets map because this target was "added" successfully
addedTargets[targetPath] = targetMeta
return StopWalk{}
}
needSign = true
if cantSignErr == nil {
tgt.Signed.Targets[targetPath] = targetMeta
tgt.Dirty = true
// Also add to our new addedTargets map to keep track of every target we've added successfully
addedTargets[targetPath] = targetMeta
}
return StopWalk{}
}
}
// Walk the role tree while validating the target paths, and add all of our targets
for path, target := range targets {
tr.WalkTargets(path, role, addTargetVisitor(path, target))
if needSign && cantSignErr != nil {
return nil, cantSignErr
}
}
if len(addedTargets) != len(targets) {
return nil, fmt.Errorf("Could not add all targets")
}
return nil, nil
}
// RemoveTargets removes the given target (paths) from the given target role (delegation)
func (tr *Repo) RemoveTargets(role data.RoleName, targets ...string) error {
cantSignErr := tr.VerifyCanSign(role)
if _, ok := cantSignErr.(data.ErrInvalidRole); ok {
return cantSignErr
}
var needSign bool
removeTargetVisitor := func(targetPath string) func(*data.SignedTargets, data.DelegationRole) interface{} {
return func(tgt *data.SignedTargets, validRole data.DelegationRole) interface{} {
// We've already validated the role path in our walk, so just modify the metadata
// We don't check against the target path against the valid role paths because it's
// possible we got into an invalid state and are trying to fix it
if _, needSign = tgt.Signed.Targets[targetPath]; needSign && cantSignErr == nil {
delete(tgt.Signed.Targets, targetPath)
tgt.Dirty = true
}
return StopWalk{}
}
}
// if the role exists but metadata does not yet, then our work is done
_, ok := tr.Targets[role]
if ok {
for _, path := range targets {
tr.WalkTargets("", role, removeTargetVisitor(path))
if needSign && cantSignErr != nil {
return cantSignErr
}
}
}
return nil
}
// UpdateSnapshot updates the FileMeta for the given role based on the Signed object
func (tr *Repo) UpdateSnapshot(role data.RoleName, s *data.Signed) error {
jsonData, err := json.Marshal(s)
if err != nil {
return err
}
meta, err := data.NewFileMeta(bytes.NewReader(jsonData), data.NotaryDefaultHashes...)
if err != nil {
return err
}
tr.Snapshot.Signed.Meta[role.String()] = meta
tr.Snapshot.Dirty = true
return nil
}
// UpdateTimestamp updates the snapshot meta in the timestamp based on the Signed object
func (tr *Repo) UpdateTimestamp(s *data.Signed) error {
jsonData, err := json.Marshal(s)
if err != nil {
return err
}
meta, err := data.NewFileMeta(bytes.NewReader(jsonData), data.NotaryDefaultHashes...)
if err != nil {
return err
}
tr.Timestamp.Signed.Meta[data.CanonicalSnapshotRole.String()] = meta
tr.Timestamp.Dirty = true
return nil
}
// SignRoot signs the root, using all keys from the "root" role (i.e. currently trusted)
// as well as available keys used to sign the previous version, if the public part is
// carried in tr.Root.Keys and the private key is available (i.e. probably previously
// trusted keys, to allow rollover). If there are any errors, attempt to put root
// back to the way it was (so version won't be incremented, for instance).
// Extra signing keys can be added to support older clients
func (tr *Repo) SignRoot(expires time.Time, extraSigningKeys data.KeyList) (*data.Signed, error) {
logrus.Debug("signing root...")
// duplicate root and attempt to modify it rather than the existing root
rootBytes, err := tr.Root.MarshalJSON()
if err != nil {
return nil, err
}
tempRoot := data.SignedRoot{}
if err := json.Unmarshal(rootBytes, &tempRoot); err != nil {
return nil, err
}
currRoot, err := tr.GetBaseRole(data.CanonicalRootRole)
if err != nil {
return nil, err
}
var rolesToSignWith []data.BaseRole
// If the root role (root keys or root threshold) has changed, sign with the
// previous root role keys
if !tr.originalRootRole.Equals(currRoot) {
rolesToSignWith = append(rolesToSignWith, tr.originalRootRole)
}
tempRoot.Signed.Expires = expires
tempRoot.Signed.Version++
rolesToSignWith = append(rolesToSignWith, currRoot)
signed, err := tempRoot.ToSigned()
if err != nil {
return nil, err
}
signed, err = tr.sign(signed, rolesToSignWith, extraSigningKeys)
if err != nil {
return nil, err
}
tr.Root = &tempRoot
tr.Root.Signatures = signed.Signatures
tr.originalRootRole = currRoot
return signed, nil
}
func oldRootVersionName(version int) string {
return fmt.Sprintf("%s.%v", data.CanonicalRootRole, version)
}
// SignTargets signs the targets file for the given top level or delegated targets role
func (tr *Repo) SignTargets(role data.RoleName, expires time.Time) (*data.Signed, error) {
logrus.Debugf("sign targets called for role %s", role)
if _, ok := tr.Targets[role]; !ok {
return nil, data.ErrInvalidRole{
Role: role,
Reason: "SignTargets called with non-existent targets role",
}
}
tr.Targets[role].Signed.Expires = expires
tr.Targets[role].Signed.Version++
signed, err := tr.Targets[role].ToSigned()
if err != nil {
logrus.Debug("errored getting targets data.Signed object")
return nil, err
}
var targets data.BaseRole
if role == data.CanonicalTargetsRole {
targets, err = tr.GetBaseRole(role)
} else {
tr, err := tr.GetDelegationRole(role)
if err != nil {
return nil, err
}
targets = tr.BaseRole
}
if err != nil {
return nil, err
}
signed, err = tr.sign(signed, []data.BaseRole{targets}, nil)
if err != nil {
logrus.Debug("errored signing ", role)
return nil, err
}
tr.Targets[role].Signatures = signed.Signatures
return signed, nil
}
// SignSnapshot updates the snapshot based on the current targets and root then signs it
func (tr *Repo) SignSnapshot(expires time.Time) (*data.Signed, error) {
logrus.Debug("signing snapshot...")
signedRoot, err := tr.Root.ToSigned()
if err != nil {
return nil, err
}
err = tr.UpdateSnapshot(data.CanonicalRootRole, signedRoot)
if err != nil {
return nil, err
}
tr.Root.Dirty = false // root dirty until changes captures in snapshot
for role, targets := range tr.Targets {