-
Notifications
You must be signed in to change notification settings - Fork 8
add download method for updater #20
Changes from 5 commits
ae040de
203b45b
6b03158
b3171bd
36fce67
94e828b
7e86c84
a9553ef
5cdb39a
61f29df
1368a25
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| package tuf | ||
|
|
||
| import ( | ||
| "io" | ||
| "net/http" | ||
| "net/url" | ||
| "path" | ||
|
|
||
| "github.com/pkg/errors" | ||
| ) | ||
|
|
||
| // Client is a TUF client. | ||
| type Client struct { | ||
| // Client wraps the private repoMan type which contains the actual | ||
| // methods for working with TUF repositories. In the future it might | ||
| // be worthwile to export the repoMan type as Client instead, but | ||
| // wrapping it reduces the amount of present refactoring work. | ||
| manager *repoMan | ||
| } | ||
|
|
||
| func NewClient(settings *Settings) (*Client, error) { | ||
| if settings.MaxResponseSize == 0 { | ||
| settings.MaxResponseSize = defaultMaxResponseSize | ||
| } | ||
| // check to see if Notary server is available | ||
| notary, err := newNotaryRepo(settings) | ||
| if err != nil { | ||
| return nil, errors.Wrap(err, "creating notary client") | ||
| } | ||
| err = notary.ping() | ||
| if err != nil { | ||
| return nil, errors.Wrap(err, "pinging notary server failed") | ||
| } | ||
| localRepo, err := newLocalRepo(settings.LocalRepoPath) | ||
| if err != nil { | ||
| return nil, errors.New("creating local tuf role repo") | ||
| } | ||
| // store intermediate state until all validation succeeds, then write | ||
| // changed roles to non-volitile storage | ||
| manager := newRepoMan(localRepo, notary, settings, notary.client) | ||
| return &Client{manager: manager}, nil | ||
| } | ||
|
|
||
| func (c *Client) Update() (files map[targetNameType]FileIntegrityMeta, latest bool, err error) { | ||
| latest, err = c.manager.refresSafe() | ||
| if err != nil { | ||
| return nil, latest, errors.Wrap(err, "refreshing state") | ||
| } | ||
|
|
||
| if err := c.manager.save(getTag()); err != nil { | ||
| return nil, latest, errors.Wrap(err, "unable to save tuf repo state") | ||
| } | ||
|
|
||
| files = c.manager.getLocalTargets() | ||
| return files, latest, nil | ||
| } | ||
|
|
||
| func (c *Client) Download(targetName string, destination io.Writer) error { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Download securely copies a target to an io.Writer, doing all the checks on that file. |
||
| files := c.manager.getLocalTargets() | ||
| currentMeta, ok := files[targetNameType(targetName)] | ||
| if !ok { | ||
| return errors.Errorf("targetName %s not found", targetName) | ||
| } | ||
| mirrorURL, err := url.Parse(c.manager.settings.MirrorURL) | ||
| if err != nil { | ||
| return errors.Wrap(err, "parse mirror url for download") | ||
| } | ||
|
|
||
| mirrorURL.Path = path.Join(mirrorURL.Path, c.manager.settings.GUN, targetName) | ||
| request, err := http.NewRequest(http.MethodGet, mirrorURL.String(), nil) | ||
| if err != nil { | ||
| return errors.Wrapf(err, "creating request to %s", mirrorURL.String()) | ||
| } | ||
| request.Header.Add(cacheControl, cachePolicyNoStore) | ||
|
|
||
| resp, err := c.manager.client.Do(request) | ||
| if err != nil { | ||
| return errors.Wrap(err, "fetching target from mirror") | ||
| } | ||
| defer resp.Body.Close() | ||
|
|
||
| if resp.StatusCode != http.StatusOK { | ||
| return errors.Errorf("get target returned %q", resp.Status) | ||
| } | ||
|
|
||
| stream := io.LimitReader(resp.Body, int64(currentMeta.Length)) | ||
| if err := currentMeta.verify(io.TeeReader(stream, destination)); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| func (c *Client) Stop() { | ||
| c.manager.Stop() | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -111,15 +111,74 @@ type repoMan struct { | |
| snapshot *Snapshot | ||
| targets *Targets | ||
| client *http.Client | ||
|
|
||
| actionc chan func() | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the Snapshot, Timestamp, Targets and Root fields on this structure need to be safe for concurrency. To accomplish that, I created a function type channel which I use in the new refresh method, avoiding race conditions and ensuring files are updated/read in order. |
||
| quit chan chan struct{} | ||
| } | ||
|
|
||
| func (rs *repoMan) Stop() { | ||
| quit := make(chan struct{}) | ||
| rs.quit <- quit | ||
| <-quit | ||
| } | ||
|
|
||
| func (rs *repoMan) refresSafe() (bool, error) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. need to remove the old |
||
| errc := make(chan error) | ||
| var isLatest bool | ||
| rs.actionc <- func() { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. example action channel function. |
||
| root, err := rs.refreshRoot() | ||
| if err != nil { | ||
| errc <- errors.Wrap(err, "refreshing root") | ||
| return | ||
| } | ||
| rs.root = root | ||
| timestamp, err := rs.refreshTimestamp(root) | ||
| if err != nil { | ||
| errc <- errors.Wrap(err, "refreshing timestamp") | ||
| return | ||
| } | ||
| rs.timestamp = timestamp | ||
| snapshot, err := rs.refreshSnapshot(root, timestamp) | ||
| if err != nil { | ||
| errc <- errors.Wrap(err, "refreshing snapshot") | ||
| return | ||
| } | ||
| rs.snapshot = snapshot | ||
| targets, latest, err := rs.refreshTargets(root, snapshot) | ||
| if err != nil { | ||
| errc <- errors.Wrap(err, "refreshing targets") | ||
| return | ||
| } | ||
| isLatest = latest | ||
| rs.targets = targets | ||
| errc <- nil | ||
| } | ||
| return isLatest, <-errc | ||
| } | ||
|
|
||
| func (rs *repoMan) loop() { | ||
| for { | ||
| select { | ||
| case f := <-rs.actionc: | ||
| f() | ||
| case quit := <-rs.quit: | ||
| close(quit) | ||
| return | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func newRepoMan(repo persistentRepo, notary remoteRepo, settings *Settings, client *http.Client) *repoMan { | ||
| return &repoMan{ | ||
| man := &repoMan{ | ||
| settings: settings, | ||
| repo: repo, | ||
| notary: notary, | ||
| client: client, | ||
| actionc: make(chan func()), | ||
| quit: make(chan chan struct{}), | ||
| } | ||
| go man.loop() | ||
| return man | ||
| } | ||
|
|
||
| func (rs *repoMan) save(backupTag string) (err error) { | ||
|
|
@@ -258,11 +317,19 @@ func (rs *repoMan) refresh() (string, error) { | |
| return "", errors.Wrap(err, "refreshing snapshot") | ||
| } | ||
| rs.snapshot = snapshot | ||
| targets, stagingPath, err := rs.refreshTargets(root, snapshot) | ||
| targets, latest, err := rs.refreshTargets(root, snapshot) | ||
| if err != nil { | ||
| return "", errors.Wrap(err, "refreshing targets") | ||
| } | ||
| rs.targets = targets | ||
| var stagingPath string | ||
| if !latest { | ||
| sp, err := rs.stageTarget(targets.Signed.Targets) | ||
| if err != nil { | ||
| return "", errors.Wrap(err, "downloading updated target") | ||
| } | ||
| stagingPath = sp | ||
| } | ||
| return stagingPath, nil | ||
| } | ||
|
|
||
|
|
@@ -450,7 +517,7 @@ func (rs *repoMan) refreshSnapshot(root *Root, timestamp *Timestamp) (*Snapshot, | |
| } | ||
|
|
||
| // Targets processing section 5.4 through 5.5.2 in the TUF spec | ||
| func (rs *repoMan) refreshTargets(root *Root, snapshot *Snapshot) (*Targets, string, error) { | ||
| func (rs *repoMan) refreshTargets(root *Root, snapshot *Snapshot) (*Targets, bool, error) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the previous method, refreshTargets downloaded the settings.Target. There's a problem here, that someone calling |
||
| // 4. **Download and check the top-level targets metadata file**, up to either | ||
| // the number of bytes specified in the snapshot metadata file, or some | ||
| // Z number of bytes. The value for Z is set by the authors of the application | ||
|
|
@@ -467,9 +534,10 @@ func (rs *repoMan) refreshTargets(root *Root, snapshot *Snapshot) (*Targets, str | |
| // number of this metadata file MUST match the snapshot metadata. This is | ||
| // done, in part, to prevent a mix-and-match attack by man-in-the-middle | ||
| // attackers. | ||
| latest := true | ||
| fim, ok := snapshot.Signed.Meta[roleTargets] | ||
| if !ok { | ||
| return nil, "", errors.New("missing target metadata in snapshot") | ||
| return nil, latest, errors.New("missing target metadata in snapshot") | ||
| } | ||
| var opts []func() interface{} | ||
| opts = append(opts, expectedSize(int64(fim.Length))) | ||
|
|
@@ -483,7 +551,7 @@ func (rs *repoMan) refreshTargets(root *Root, snapshot *Snapshot) (*Targets, str | |
| } | ||
| current, err := rs.notary.targets(opts...) | ||
| if err != nil { | ||
| return nil, "", errors.Wrap(err, "retrieving timestamp from notary") | ||
| return nil, latest, errors.Wrap(err, "retrieving timestamp from notary") | ||
| } | ||
| // 4.2. **Check for an arbitrary software attack.** This metadata file MUST | ||
| // have been signed by a threshold of keys specified in the latest root | ||
|
|
@@ -492,32 +560,27 @@ func (rs *repoMan) refreshTargets(root *Root, snapshot *Snapshot) (*Targets, str | |
| threshold := root.Signed.Roles[roleTargets].Threshold | ||
| err = rs.verifySignatures(current.Signed, keys, current.Signatures, threshold) | ||
| if err != nil { | ||
| return nil, "", errors.Wrap(err, "signature verification for targets failed") | ||
| return nil, latest, errors.Wrap(err, "signature verification for targets failed") | ||
| } | ||
| previous, err := rs.repo.targets() | ||
| if err != nil { | ||
| return nil, "", errors.Wrap(err, "fetching local targets") | ||
| return nil, latest, errors.Wrap(err, "fetching local targets") | ||
| } | ||
| // 4.3. **Check for a rollback attack.** The version number of the previous | ||
| // targets metadata file, if any, MUST be less than or equal to the version | ||
| // number of this targets metadata file. | ||
| if previous.Signed.Version > current.Signed.Version { | ||
| return nil, "", errRollbackAttack | ||
| return nil, latest, errRollbackAttack | ||
| } | ||
| // 4.4. **Check for a freeze attack.** The latest known time should be lower | ||
| // than the expiration timestamp in this metadata file. | ||
| if time.Now().After(current.Signed.Expires) { | ||
| return nil, "", errFreezeAttack | ||
| return nil, latest, errFreezeAttack | ||
| } | ||
| // If we have a new version of the target download it. | ||
| var stagedPath string | ||
| if current.Signed.Version > previous.Signed.Version { | ||
| stagedPath, err = rs.stageTarget(current.Signed.Targets) | ||
| if err != nil { | ||
| return nil, "", errors.Wrap(err, "staging targets") | ||
| } | ||
| latest = false | ||
| } | ||
| return current, stagedPath, nil | ||
| return current, latest, nil | ||
| } | ||
|
|
||
| func (rs *repoMan) getKeys(r *Root, sigs []Signature) map[keyID]Key { | ||
|
|
@@ -616,6 +679,16 @@ func (rs *repoMan) downloadTarget(client *http.Client, target targetNameType, fi | |
| return stagingFile, nil | ||
| } | ||
|
|
||
| func (rs *repoMan) getLocalTargets() map[targetNameType]FileIntegrityMeta { | ||
| files := make(chan map[targetNameType]FileIntegrityMeta) | ||
| rs.actionc <- func() { | ||
| if rs.targets != nil { | ||
| files <- rs.targets.Signed.Targets | ||
| } | ||
| } | ||
| return <-files | ||
| } | ||
|
|
||
| func (rs *repoMan) verifySignatures(role marshaller, keys map[keyID]Key, sigs []Signature, threshold int) error { | ||
| // just in case, make sure threshold is not zero as this would mean we're not checking any sigs | ||
| if threshold <= 0 { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// update refreshes local state, returning all updated targets.