This repository has been archived by the owner on Mar 7, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 4
/
trustedmetadata.go
351 lines (327 loc) · 13.7 KB
/
trustedmetadata.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
// Copyright 2022-2023 VMware, Inc.
//
// This product is licensed to you under the BSD-2 license (the "License").
// You may not use this product except in compliance with the BSD-2 License.
// This product may include a number of subcomponents with separate copyright
// notices and license terms. Your use of these subcomponents is subject to
// the terms and conditions of the subcomponent's license, as noted in the
// LICENSE file.
//
// SPDX-License-Identifier: BSD-2-Clause
package trustedmetadata
import (
"fmt"
"time"
"github.com/rdimitrov/go-tuf-metadata/metadata"
)
// TrustedMetadata struct for storing trusted metadata
type TrustedMetadata struct {
Root *metadata.Metadata[metadata.RootType]
Snapshot *metadata.Metadata[metadata.SnapshotType]
Timestamp *metadata.Metadata[metadata.TimestampType]
Targets map[string]*metadata.Metadata[metadata.TargetsType]
RefTime time.Time
}
// New creates a new TrustedMetadata instance which ensures that the
// collection of metadata in it is valid and trusted through the whole
// client update workflow. It provides easy ways to update the metadata
// with the caller making decisions on what is updated
func New(rootData []byte) (*TrustedMetadata, error) {
res := &TrustedMetadata{
Targets: map[string]*metadata.Metadata[metadata.TargetsType]{},
RefTime: time.Now().UTC(),
}
// load and validate the local root metadata
// valid initial trusted root metadata is required
err := res.loadTrustedRoot(rootData)
if err != nil {
return nil, err
}
return res, nil
}
// UpdateRoot verifies and loads “rootData“ as new root metadata.
// Note that an expired intermediate root is considered valid: expiry is
// only checked for the final root in UpdateTimestamp()
func (trusted *TrustedMetadata) UpdateRoot(rootData []byte) (*metadata.Metadata[metadata.RootType], error) {
log := metadata.GetLogger()
if trusted.Timestamp != nil {
return nil, metadata.ErrRuntime{Msg: "cannot update root after timestamp"}
}
log.Info("Updating root")
// generate root metadata
newRoot, err := metadata.Root().FromBytes(rootData)
if err != nil {
return nil, err
}
// check metadata type matches root
if newRoot.Signed.Type != metadata.ROOT {
return nil, metadata.ErrRepository{Msg: fmt.Sprintf("expected %s, got %s", metadata.ROOT, newRoot.Signed.Type)}
}
// verify that new root is signed by trusted root
err = trusted.Root.VerifyDelegate(metadata.ROOT, newRoot)
if err != nil {
return nil, err
}
// verify version
if newRoot.Signed.Version != trusted.Root.Signed.Version+1 {
return nil, metadata.ErrBadVersionNumber{Msg: fmt.Sprintf("bad version number, expected %d, got %d", trusted.Root.Signed.Version+1, newRoot.Signed.Version)}
}
// verify that new root is signed by itself
err = newRoot.VerifyDelegate(metadata.ROOT, newRoot)
if err != nil {
return nil, err
}
// save root if verified
trusted.Root = newRoot
log.Info("Updated root", "version", trusted.Root.Signed.Version)
return trusted.Root, nil
}
// UpdateTimestamp verifies and loads “timestampData“ as new timestamp metadata.
// Note that an intermediate timestamp is allowed to be expired. "TrustedMetadata"
// will error in this case but the intermediate timestamp will be loaded.
// This way a newer timestamp can still be loaded (and the intermediate
// timestamp will be used for rollback protection). Expired timestamp will
// prevent loading snapshot metadata.
func (trusted *TrustedMetadata) UpdateTimestamp(timestampData []byte) (*metadata.Metadata[metadata.TimestampType], error) {
log := metadata.GetLogger()
if trusted.Snapshot != nil {
return nil, metadata.ErrRuntime{Msg: "cannot update timestamp after snapshot"}
}
// client workflow 5.3.10: Make sure final root is not expired.
if trusted.Root.Signed.IsExpired(trusted.RefTime) {
// no need to check for 5.3.11 (fast forward attack recovery):
// timestamp/snapshot can not yet be loaded at this point
return nil, metadata.ErrExpiredMetadata{Msg: "final root.json is expired"}
}
log.Info("Updating timestamp")
newTimestamp, err := metadata.Timestamp().FromBytes(timestampData)
if err != nil {
return nil, err
}
// check metadata type matches timestamp
if newTimestamp.Signed.Type != metadata.TIMESTAMP {
return nil, metadata.ErrRepository{Msg: fmt.Sprintf("expected %s, got %s", metadata.TIMESTAMP, newTimestamp.Signed.Type)}
}
// verify that new timestamp is signed by trusted root
err = trusted.Root.VerifyDelegate(metadata.TIMESTAMP, newTimestamp)
if err != nil {
return nil, err
}
// if an existing trusted timestamp is updated,
// check for a rollback attack
if trusted.Timestamp != nil {
// prevent rolling back timestamp version
if newTimestamp.Signed.Version < trusted.Timestamp.Signed.Version {
return nil, metadata.ErrBadVersionNumber{Msg: fmt.Sprintf("new timestamp version %d must be >= %d", newTimestamp.Signed.Version, trusted.Timestamp.Signed.Version)}
}
// keep using old timestamp if versions are equal
if newTimestamp.Signed.Version == trusted.Timestamp.Signed.Version {
log.Info("New timestamp version equals the old one", "new", newTimestamp.Signed.Version, "old", trusted.Timestamp.Signed.Version)
return nil, metadata.ErrEqualVersionNumber{Msg: fmt.Sprintf("new timestamp version %d equals the old one %d", newTimestamp.Signed.Version, trusted.Timestamp.Signed.Version)}
}
// prevent rolling back snapshot version
snapshotMeta := trusted.Timestamp.Signed.Meta[fmt.Sprintf("%s.json", metadata.SNAPSHOT)]
newSnapshotMeta := newTimestamp.Signed.Meta[fmt.Sprintf("%s.json", metadata.SNAPSHOT)]
if newSnapshotMeta.Version < snapshotMeta.Version {
return nil, metadata.ErrBadVersionNumber{Msg: fmt.Sprintf("new snapshot version %d must be >= %d", newSnapshotMeta.Version, snapshotMeta.Version)}
}
}
// expiry not checked to allow old timestamp to be used for rollback
// protection of new timestamp: expiry is checked in UpdateSnapshot()
// save root if verified
trusted.Timestamp = newTimestamp
log.Info("Updated timestamp", "version", trusted.Timestamp.Signed.Version)
// timestamp is loaded: error if it is not valid _final_ timestamp
err = trusted.checkFinalTimestamp()
if err != nil {
return nil, err
}
// all okay
return trusted.Timestamp, nil
}
// checkFinalTimestamp verifies if trusted timestamp is not expired
func (trusted *TrustedMetadata) checkFinalTimestamp() error {
if trusted.Timestamp.Signed.IsExpired(trusted.RefTime) {
return metadata.ErrExpiredMetadata{Msg: "timestamp.json is expired"}
}
return nil
}
// UpdateSnapshot verifies and loads “snapshotData“ as new snapshot metadata.
// Note that an intermediate snapshot is allowed to be expired and version
// is allowed to not match timestamp meta version: TrustedMetadata
// will error for case of expired metadata or when using bad versions but the
// intermediate snapshot will be loaded. This way a newer snapshot can still
// be loaded (and the intermediate snapshot will be used for rollback protection).
// Expired snapshot or snapshot that does not match timestamp meta version will
// prevent loading targets.
func (trusted *TrustedMetadata) UpdateSnapshot(snapshotData []byte, isTrusted bool) (*metadata.Metadata[metadata.SnapshotType], error) {
log := metadata.GetLogger()
if trusted.Timestamp == nil {
return nil, metadata.ErrRuntime{Msg: "cannot update snapshot before timestamp"}
}
if trusted.Targets[metadata.TARGETS] != nil {
return nil, metadata.ErrRuntime{Msg: "cannot update snapshot after targets"}
}
log.Info("Updating snapshot")
// snapshot cannot be loaded if final timestamp is expired
err := trusted.checkFinalTimestamp()
if err != nil {
return nil, err
}
snapshotMeta := trusted.Timestamp.Signed.Meta[fmt.Sprintf("%s.json", metadata.SNAPSHOT)]
// verify non-trusted data against the hashes in timestamp, if any.
// trusted snapshot data has already been verified once.
if !isTrusted {
err = snapshotMeta.VerifyLengthHashes(snapshotData)
if err != nil {
return nil, err
}
}
newSnapshot, err := metadata.Snapshot().FromBytes(snapshotData)
if err != nil {
return nil, err
}
// check metadata type matches snapshot
if newSnapshot.Signed.Type != metadata.SNAPSHOT {
return nil, metadata.ErrRepository{Msg: fmt.Sprintf("expected %s, got %s", metadata.SNAPSHOT, newSnapshot.Signed.Type)}
}
// verify that new snapshot is signed by trusted root
err = trusted.Root.VerifyDelegate(metadata.SNAPSHOT, newSnapshot)
if err != nil {
return nil, err
}
// version not checked against meta version to allow old snapshot to be
// used in rollback protection: it is checked when targets is updated
// if an existing trusted snapshot is updated, check for rollback attack
if trusted.Snapshot != nil {
for name, info := range trusted.Snapshot.Signed.Meta {
newFileInfo, ok := newSnapshot.Signed.Meta[name]
// prevent removal of any metadata in meta
if !ok {
return nil, metadata.ErrRepository{Msg: fmt.Sprintf("new snapshot is missing info for %s", name)}
}
// prevent rollback of any metadata versions
if newFileInfo.Version < info.Version {
return nil, metadata.ErrBadVersionNumber{Msg: fmt.Sprintf("expected %s version %d, got %d", name, newFileInfo.Version, info.Version)}
}
}
}
// expiry not checked to allow old snapshot to be used for rollback
// protection of new snapshot: it is checked when targets is updated
trusted.Snapshot = newSnapshot
log.Info("Updated snapshot", "version", trusted.Snapshot.Signed.Version)
// snapshot is loaded, but we error if it's not valid _final_ snapshot
err = trusted.checkFinalSnapshot()
if err != nil {
return nil, err
}
// all okay
return trusted.Snapshot, nil
}
// checkFinalSnapshot verifies if it's not expired and snapshot version matches timestamp meta version
func (trusted *TrustedMetadata) checkFinalSnapshot() error {
if trusted.Snapshot.Signed.IsExpired(trusted.RefTime) {
return metadata.ErrExpiredMetadata{Msg: "snapshot.json is expired"}
}
snapshotMeta := trusted.Timestamp.Signed.Meta[fmt.Sprintf("%s.json", metadata.SNAPSHOT)]
if trusted.Snapshot.Signed.Version != snapshotMeta.Version {
return metadata.ErrBadVersionNumber{Msg: fmt.Sprintf("expected %d, got %d", snapshotMeta.Version, trusted.Snapshot.Signed.Version)}
}
return nil
}
// UpdateTargets verifies and loads “targetsData“ as new top-level targets metadata.
func (trusted *TrustedMetadata) UpdateTargets(targetsData []byte) (*metadata.Metadata[metadata.TargetsType], error) {
return trusted.UpdateDelegatedTargets(targetsData, metadata.TARGETS, metadata.ROOT)
}
// UpdateDelegatedTargets verifies and loads “targetsData“ as new metadata for target “roleName“
func (trusted *TrustedMetadata) UpdateDelegatedTargets(targetsData []byte, roleName, delegatorName string) (*metadata.Metadata[metadata.TargetsType], error) {
log := metadata.GetLogger()
var ok bool
if trusted.Snapshot == nil {
return nil, metadata.ErrRuntime{Msg: "cannot load targets before snapshot"}
}
// targets cannot be loaded if final snapshot is expired or its version
// does not match meta version in timestamp
err := trusted.checkFinalSnapshot()
if err != nil {
return nil, err
}
// check if delegator metadata is present
if delegatorName == metadata.ROOT {
if trusted.Root != nil {
ok = true
} else {
ok = false
}
} else {
_, ok = trusted.Targets[delegatorName]
}
if !ok {
return nil, metadata.ErrRuntime{Msg: "cannot load targets before delegator"}
}
log.Info("Updating delegated role", "role", roleName, "delegator", delegatorName)
// Verify against the hashes in snapshot, if any
meta, ok := trusted.Snapshot.Signed.Meta[fmt.Sprintf("%s.json", roleName)]
if !ok {
return nil, metadata.ErrRepository{Msg: fmt.Sprintf("snapshot does not contain information for %s", roleName)}
}
err = meta.VerifyLengthHashes(targetsData)
if err != nil {
return nil, err
}
newDelegate, err := metadata.Targets().FromBytes(targetsData)
if err != nil {
return nil, err
}
// check metadata type matches targets
if newDelegate.Signed.Type != metadata.TARGETS {
return nil, metadata.ErrRepository{Msg: fmt.Sprintf("expected %s, got %s", metadata.TARGETS, newDelegate.Signed.Type)}
}
// get delegator metadata and verify the new delegatee
if delegatorName == metadata.ROOT {
err = trusted.Root.VerifyDelegate(roleName, newDelegate)
if err != nil {
return nil, err
}
} else {
err = trusted.Targets[delegatorName].VerifyDelegate(roleName, newDelegate)
if err != nil {
return nil, err
}
}
// check versions
if newDelegate.Signed.Version != meta.Version {
return nil, metadata.ErrBadVersionNumber{Msg: fmt.Sprintf("expected %s version %d, got %d", roleName, meta.Version, newDelegate.Signed.Version)}
}
// check expiration
if newDelegate.Signed.IsExpired(trusted.RefTime) {
return nil, metadata.ErrExpiredMetadata{Msg: fmt.Sprintf("new %s is expired", roleName)}
}
trusted.Targets[roleName] = newDelegate
log.Info("Updated role", "role", roleName, "version", trusted.Targets[roleName].Signed.Version)
return trusted.Targets[roleName], nil
}
// loadTrustedRoot verifies and loads "data" as trusted root metadata.
// Note that an expired initial root is considered valid: expiry is
// only checked for the final root in “UpdateTimestamp()“.
func (trusted *TrustedMetadata) loadTrustedRoot(rootData []byte) error {
log := metadata.GetLogger()
// generate root metadata
newRoot, err := metadata.Root().FromBytes(rootData)
if err != nil {
return err
}
// check metadata type matches root
if newRoot.Signed.Type != metadata.ROOT {
return metadata.ErrRepository{Msg: fmt.Sprintf("expected %s, got %s", metadata.ROOT, newRoot.Signed.Type)}
}
// verify root by itself
err = newRoot.VerifyDelegate(metadata.ROOT, newRoot)
if err != nil {
return err
}
// save root if verified
trusted.Root = newRoot
log.Info("Loaded trusted root", "version", trusted.Root.Signed.Version)
return nil
}