| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997 |
- package client
- import (
- "bytes"
- "encoding/hex"
- "encoding/json"
- "io"
- "io/ioutil"
- "github.com/DataDog/go-tuf/data"
- "github.com/DataDog/go-tuf/internal/roles"
- "github.com/DataDog/go-tuf/util"
- "github.com/DataDog/go-tuf/verify"
- )
- const (
- // This is the upper limit in bytes we will use to limit the download
- // size of the root/timestamp roles, since we might not don't know how
- // big it is.
- defaultRootDownloadLimit = 512000
- defaultTimestampDownloadLimit = 16384
- defaultMaxDelegations = 32
- defaultMaxRootRotations = 1e3
- )
- // LocalStore is local storage for downloaded top-level metadata.
- type LocalStore interface {
- io.Closer
- // GetMeta returns top-level metadata from local storage. The keys are
- // in the form `ROLE.json`, with ROLE being a valid top-level role.
- GetMeta() (map[string]json.RawMessage, error)
- // SetMeta persists the given top-level metadata in local storage, the
- // name taking the same format as the keys returned by GetMeta.
- SetMeta(name string, meta json.RawMessage) error
- // DeleteMeta deletes a given metadata.
- DeleteMeta(name string) error
- }
- // RemoteStore downloads top-level metadata and target files from a remote
- // repository.
- type RemoteStore interface {
- // GetMeta downloads the given metadata from remote storage.
- //
- // `name` is the filename of the metadata (e.g. "root.json")
- //
- // `err` is ErrNotFound if the given file does not exist.
- //
- // `size` is the size of the stream, -1 indicating an unknown length.
- GetMeta(name string) (stream io.ReadCloser, size int64, err error)
- // GetTarget downloads the given target file from remote storage.
- //
- // `path` is the path of the file relative to the root of the remote
- // targets directory (e.g. "/path/to/file.txt").
- //
- // `err` is ErrNotFound if the given file does not exist.
- //
- // `size` is the size of the stream, -1 indicating an unknown length.
- GetTarget(path string) (stream io.ReadCloser, size int64, err error)
- }
- // Client provides methods for fetching updates from a remote repository and
- // downloading remote target files.
- type Client struct {
- local LocalStore
- remote RemoteStore
- // The following four fields represent the versions of metatdata either
- // from local storage or from recently downloaded metadata
- rootVer int64
- targetsVer int64
- snapshotVer int64
- timestampVer int64
- // targets is the list of available targets, either from local storage
- // or from recently downloaded targets metadata
- targets data.TargetFiles
- // localMeta is the raw metadata from local storage and is used to
- // check whether remote metadata is present locally
- localMeta map[string]json.RawMessage
- // db is a key DB used for verifying metadata
- db *verify.DB
- // consistentSnapshot indicates whether the remote storage is using
- // consistent snapshots (as specified in root.json)
- consistentSnapshot bool
- // MaxDelegations limits by default the number of delegations visited for any
- // target
- MaxDelegations int
- // MaxRootRotations limits the number of downloaded roots in 1.0.19 root updater
- MaxRootRotations int
- }
- func NewClient(local LocalStore, remote RemoteStore) *Client {
- return &Client{
- local: local,
- remote: remote,
- MaxDelegations: defaultMaxDelegations,
- MaxRootRotations: defaultMaxRootRotations,
- }
- }
- // Init initializes a local repository.
- //
- // The latest root.json is fetched from remote storage, verified using rootKeys
- // and threshold, and then saved in local storage. It is expected that rootKeys
- // were securely distributed with the software being updated.
- //
- // Deprecated: Use c.InitLocal and c.Update to initialize a local repository.
- func (c *Client) Init(rootKeys []*data.PublicKey, threshold int) error {
- if len(rootKeys) < threshold {
- return ErrInsufficientKeys
- }
- rootJSON, err := c.downloadMetaUnsafe("root.json", defaultRootDownloadLimit)
- if err != nil {
- return err
- }
- // create a new key database, and add all the public `rootKeys` to it.
- c.db = verify.NewDB()
- rootKeyIDs := make([]string, 0, len(rootKeys))
- for _, key := range rootKeys {
- for _, id := range key.IDs() {
- rootKeyIDs = append(rootKeyIDs, id)
- if err := c.db.AddKey(id, key); err != nil {
- return err
- }
- }
- }
- // add a mock "root" role that trusts the passed in key ids. These keys
- // will be used to verify the `root.json` we just fetched.
- role := &data.Role{Threshold: threshold, KeyIDs: rootKeyIDs}
- if err := c.db.AddRole("root", role); err != nil {
- return err
- }
- // verify that the new root is valid.
- if err := c.decodeRoot(rootJSON); err != nil {
- return err
- }
- return c.local.SetMeta("root.json", rootJSON)
- }
- // InitLocal initializes a local repository from root metadata.
- //
- // The root's keys are extracted from the root and saved in local storage.
- // Root expiration is not checked.
- // It is expected that rootJSON was securely distributed with the software
- // being updated.
- func (c *Client) InitLocal(rootJSON []byte) error {
- err := c.loadAndVerifyRootMeta(rootJSON, true /*ignoreExpiredCheck*/)
- if err != nil {
- return err
- }
- return c.local.SetMeta("root.json", rootJSON)
- }
- // Update downloads and verifies remote metadata and returns updated targets.
- // It always performs root update (5.2 and 5.3) section of the v1.0.19 spec.
- //
- // https://DataDog.github.io/specification/v1.0.19/index.html#load-trusted-root
- func (c *Client) Update() (data.TargetFiles, error) {
- if err := c.UpdateRoots(); err != nil {
- if _, ok := err.(verify.ErrExpired); ok {
- // For backward compatibility, we wrap the ErrExpired inside
- // ErrDecodeFailed.
- return nil, ErrDecodeFailed{"root.json", err}
- }
- return nil, err
- }
- // Load trusted metadata files, if any, and verify them against the latest root
- c.getLocalMeta()
- // 5.4.1 - Download the timestamp metadata
- timestampJSON, err := c.downloadMetaUnsafe("timestamp.json", defaultTimestampDownloadLimit)
- if err != nil {
- return nil, err
- }
- // 5.4.(2,3 and 4) - Verify timestamp against various attacks
- // Returns the extracted snapshot metadata
- snapshotMeta, err := c.decodeTimestamp(timestampJSON)
- if err != nil {
- return nil, err
- }
- // 5.4.5 - Persist the timestamp metadata
- if err := c.local.SetMeta("timestamp.json", timestampJSON); err != nil {
- return nil, err
- }
- // 5.5.1 - Download snapshot metadata
- // 5.5.2 and 5.5.4 - Check against timestamp role's snapshot hash and version
- snapshotJSON, err := c.downloadMetaFromTimestamp("snapshot.json", snapshotMeta)
- if err != nil {
- return nil, err
- }
- // 5.5.(3,5 and 6) - Verify snapshot against various attacks
- // Returns the extracted metadata files
- snapshotMetas, err := c.decodeSnapshot(snapshotJSON)
- if err != nil {
- return nil, err
- }
- // 5.5.7 - Persist snapshot metadata
- if err := c.local.SetMeta("snapshot.json", snapshotJSON); err != nil {
- return nil, err
- }
- // If we don't have the targets.json, download it, determine updated
- // targets and save targets.json in local storage
- var updatedTargets data.TargetFiles
- targetsMeta := snapshotMetas["targets.json"]
- if !c.hasMetaFromSnapshot("targets.json", targetsMeta) {
- // 5.6.1 - Download the top-level targets metadata file
- // 5.6.2 and 5.6.4 - Check against snapshot role's targets hash and version
- targetsJSON, err := c.downloadMetaFromSnapshot("targets.json", targetsMeta)
- if err != nil {
- return nil, err
- }
- // 5.6.(3 and 5) - Verify signatures and check against freeze attack
- updatedTargets, err = c.decodeTargets(targetsJSON)
- if err != nil {
- return nil, err
- }
- // 5.6.6 - Persist targets metadata
- if err := c.local.SetMeta("targets.json", targetsJSON); err != nil {
- return nil, err
- }
- }
- return updatedTargets, nil
- }
- func (c *Client) UpdateRoots() error {
- // https://DataDog.github.io/specification/v1.0.19/index.html#load-trusted-root
- // 5.2 Load the trusted root metadata file. We assume that a good,
- // trusted copy of this file was shipped with the package manager
- // or software updater using an out-of-band process.
- if err := c.loadAndVerifyLocalRootMeta( /*ignoreExpiredCheck=*/ true); err != nil {
- return err
- }
- m, err := c.local.GetMeta()
- if err != nil {
- return err
- }
- type KeyInfo struct {
- KeyIDs map[string]bool
- Threshold int
- }
- // Prepare for 5.3.11: If the timestamp and / or snapshot keys have been rotated,
- // then delete the trusted timestamp and snapshot metadata files.
- getKeyInfo := func(role string) KeyInfo {
- keyIDs := make(map[string]bool)
- for k := range c.db.GetRole(role).KeyIDs {
- keyIDs[k] = true
- }
- return KeyInfo{keyIDs, c.db.GetRole(role).Threshold}
- }
- // The nonRootKeyInfo looks like this:
- // {
- // "timestamp": {KeyIDs={"KEYID1": true, "KEYID2": true}, Threshold=2},
- // "snapshot": {KeyIDs={"KEYID3": true}, Threshold=1},
- // "targets": {KeyIDs={"KEYID4": true, "KEYID5": true, "KEYID6": true}, Threshold=1}
- // }
- nonRootKeyInfo := map[string]KeyInfo{"timestamp": {}, "snapshot": {}, "targets": {}}
- for k := range nonRootKeyInfo {
- nonRootKeyInfo[k] = getKeyInfo(k)
- }
- // 5.3.1 Temorarily turn on the consistent snapshots in order to download
- // versioned root metadata files as described next.
- consistentSnapshot := c.consistentSnapshot
- c.consistentSnapshot = true
- nRootMetadata := m["root.json"]
- // https://DataDog.github.io/specification/v1.0.19/index.html#update-root
- // 5.3.1 Since it may now be signed using entirely different keys,
- // the client MUST somehow be able to establish a trusted line of
- // continuity to the latest set of keys (see § 6.1 Key
- // management and migration). To do so, the client MUST
- // download intermediate root metadata files, until the
- // latest available one is reached. Therefore, it MUST
- // temporarily turn on consistent snapshots in order to
- // download versioned root metadata files as described next.
- // This loop returns on error or breaks after downloading the lastest root metadata.
- // 5.3.2 Let N denote the version number of the trusted root metadata file.
- for i := 0; i < c.MaxRootRotations; i++ {
- // 5.3.3 Try downloading version nPlusOne of the root metadata file.
- // NOTE: as a side effect, we do update c.rootVer to nPlusOne between iterations.
- nPlusOne := c.rootVer + 1
- nPlusOneRootPath := util.VersionedPath("root.json", nPlusOne)
- nPlusOneRootMetadata, err := c.downloadMetaUnsafe(nPlusOneRootPath, defaultRootDownloadLimit)
- if err != nil {
- if _, ok := err.(ErrMissingRemoteMetadata); ok {
- // stop when the next root can't be downloaded
- break
- }
- return err
- }
- // 5.3.4 Check for an arbitrary software attack.
- // 5.3.4.1 Check that N signed N+1
- nPlusOneRootMetadataSigned, err := c.verifyRoot(nRootMetadata, nPlusOneRootMetadata)
- if err != nil {
- return err
- }
- // 5.3.4.2 check that N+1 signed itself.
- if _, err := c.verifyRoot(nPlusOneRootMetadata, nPlusOneRootMetadata); err != nil {
- // 5.3.6 Note that the expiration of the new (intermediate) root
- // metadata file does not matter yet, because we will check for
- // it in step 5.3.10.
- return err
- }
- // 5.3.5 Check for a rollback attack. Here, we check that nPlusOneRootMetadataSigned.version == nPlusOne.
- if nPlusOneRootMetadataSigned.Version != nPlusOne {
- return verify.ErrWrongVersion{
- Given: nPlusOneRootMetadataSigned.Version,
- Expected: nPlusOne,
- }
- }
- // 5.3.7 Set the trusted root metadata file to the new root metadata file.
- c.rootVer = nPlusOneRootMetadataSigned.Version
- // NOTE: following up on 5.3.1, we want to always have consistent snapshots on for the duration
- // of root rotation. AFTER the rotation is over, we will set it to the value of the last root.
- consistentSnapshot = nPlusOneRootMetadataSigned.ConsistentSnapshot
- // 5.3.8 Persist root metadata. The client MUST write the file to non-volatile storage as FILENAME.EXT (e.g. root.json).
- // NOTE: Internally, setMeta stores metadata in LevelDB in a persistent manner.
- if err := c.local.SetMeta("root.json", nPlusOneRootMetadata); err != nil {
- return err
- }
- nRootMetadata = nPlusOneRootMetadata
- // 5.3.9 Repeat steps 5.3.2 to 5.3.9
- } // End of the for loop.
- // 5.3.10 Check for a freeze attack.
- // NOTE: This will check for any, including freeze, attack.
- if err := c.loadAndVerifyLocalRootMeta( /*ignoreExpiredCheck=*/ false); err != nil {
- return err
- }
- countDeleted := func(s1 map[string]bool, s2 map[string]bool) int {
- c := 0
- for k := range s1 {
- if _, ok := s2[k]; !ok {
- c++
- }
- }
- return c
- }
- // 5.3.11 To recover from fast-forward attack, certain metadata files need
- // to be deleted if a threshold of keys are revoked.
- // List of metadata that should be deleted per role if a threshold of keys
- // are revoked:
- // (based on the ongoing PR: https://github.com/mnm678/specification/tree/e50151d9df632299ddea364c4f44fe8ca9c10184)
- // timestamp -> delete timestamp.json
- // snapshot -> delete timestamp.json and snapshot.json
- // targets -> delete snapshot.json and targets.json
- //
- // nonRootKeyInfo contains the keys and thresholds from root.json
- // that were on disk before the root update process begins.
- for topLevelRolename := range nonRootKeyInfo {
- // ki contains the keys and thresholds from the latest downloaded root.json.
- ki := getKeyInfo(topLevelRolename)
- if countDeleted(nonRootKeyInfo[topLevelRolename].KeyIDs, ki.KeyIDs) >= nonRootKeyInfo[topLevelRolename].Threshold {
- deleteMeta := map[string][]string{
- "timestamp": {"timestamp.json"},
- "snapshot": {"timestamp.json", "snapshot.json"},
- "targets": {"snapshot.json", "targets.json"},
- }
- for _, r := range deleteMeta[topLevelRolename] {
- c.local.DeleteMeta(r)
- }
- }
- }
- // 5.3.12 Set whether consistent snapshots are used as per the trusted root metadata file.
- c.consistentSnapshot = consistentSnapshot
- return nil
- }
- // getLocalMeta decodes and verifies metadata from local storage.
- // The verification of local files is purely for consistency, if an attacker
- // has compromised the local storage, there is no guarantee it can be trusted.
- // Before trying to load the metadata files, it clears the in-memory copy of the local metadata.
- // This is to insure that all of the loaded metadata files at the end are indeed verified by the latest root.
- // If some of the metadata files fail to load it will proceed with trying to load the rest,
- // but still return an error at the end, if such occurred. Otherwise returns nil.
- func (c *Client) getLocalMeta() error {
- var retErr error
- loadFailed := false
- // Clear the in-memory copy of the local metadata. The goal is to reload and take into account
- // only the metadata files that are verified by the latest root. Otherwise, their content should
- // be ignored.
- c.localMeta = make(map[string]json.RawMessage)
- // Load the latest root meta
- if err := c.loadAndVerifyLocalRootMeta( /*ignoreExpiredCheck=*/ false); err != nil {
- return err
- }
- // Load into memory the existing meta, if any, from the local storage
- meta, err := c.local.GetMeta()
- if err != nil {
- return nil
- }
- // Verify the top-level metadata (timestamp, snapshot and targets) against the latest root and load it, if okay
- if timestampJSON, ok := meta["timestamp.json"]; ok {
- timestamp := &data.Timestamp{}
- if err := c.db.UnmarshalTrusted(timestampJSON, timestamp, "timestamp"); err != nil {
- loadFailed = true
- retErr = err
- } else {
- c.localMeta["timestamp.json"] = meta["timestamp.json"]
- c.timestampVer = timestamp.Version
- }
- }
- if snapshotJSON, ok := meta["snapshot.json"]; ok {
- snapshot := &data.Snapshot{}
- if err := c.db.UnmarshalTrusted(snapshotJSON, snapshot, "snapshot"); err != nil {
- loadFailed = true
- retErr = err
- } else {
- c.localMeta["snapshot.json"] = meta["snapshot.json"]
- c.snapshotVer = snapshot.Version
- }
- }
- if targetsJSON, ok := meta["targets.json"]; ok {
- targets := &data.Targets{}
- if err := c.db.UnmarshalTrusted(targetsJSON, targets, "targets"); err != nil {
- loadFailed = true
- retErr = err
- } else {
- c.localMeta["targets.json"] = meta["targets.json"]
- c.targetsVer = targets.Version
- // FIXME(TUF-0.9) temporarily support files with leading path separators.
- // c.targets = targets.Targets
- c.loadTargets(targets.Targets)
- }
- }
- for fileName := range meta {
- if roles.IsDelegatedTargetsManifest(fileName) {
- c.localMeta[fileName] = meta[fileName]
- }
- }
- if loadFailed {
- // If any of the metadata failed to be verified, return the reason for that failure
- return retErr
- }
- return nil
- }
- // loadAndVerifyLocalRootMeta decodes and verifies root metadata from
- // local storage and loads the top-level keys. This method first clears
- // the DB for top-level keys and then loads the new keys.
- func (c *Client) loadAndVerifyLocalRootMeta(ignoreExpiredCheck bool) error {
- meta, err := c.local.GetMeta()
- if err != nil {
- return err
- }
- rootJSON, ok := meta["root.json"]
- if !ok {
- return ErrNoRootKeys
- }
- return c.loadAndVerifyRootMeta(rootJSON, ignoreExpiredCheck)
- }
- // loadAndVerifyRootMeta decodes and verifies root metadata and loads the top-level keys.
- // This method first clears the DB for top-level keys and then loads the new keys.
- func (c *Client) loadAndVerifyRootMeta(rootJSON []byte, ignoreExpiredCheck bool) error {
- // unmarshal root.json without verifying as we need the root
- // keys first
- s := &data.Signed{}
- if err := json.Unmarshal(rootJSON, s); err != nil {
- return err
- }
- root := &data.Root{}
- if err := json.Unmarshal(s.Signed, root); err != nil {
- return err
- }
- ndb := verify.NewDB()
- for id, k := range root.Keys {
- if err := ndb.AddKey(id, k); err != nil {
- // TUF is considering in TAP-12 removing the
- // requirement that the keyid hash algorithm be derived
- // from the public key. So to be forwards compatible,
- // we ignore `ErrWrongID` errors.
- //
- // TAP-12: https://github.com/DataDog/taps/blob/master/tap12.md
- if _, ok := err.(verify.ErrWrongID); !ok {
- return err
- }
- }
- }
- for name, role := range root.Roles {
- if err := ndb.AddRole(name, role); err != nil {
- return err
- }
- }
- // Any trusted local root metadata version must be greater than 0.
- if ignoreExpiredCheck {
- if err := ndb.VerifyIgnoreExpiredCheck(s, "root", 0); err != nil {
- return err
- }
- } else {
- if err := ndb.Verify(s, "root", 0); err != nil {
- return err
- }
- }
- c.consistentSnapshot = root.ConsistentSnapshot
- c.rootVer = root.Version
- c.db = ndb
- return nil
- }
- // verifyRoot verifies Signed section of the bJSON
- // using verification keys in aJSON.
- func (c *Client) verifyRoot(aJSON []byte, bJSON []byte) (*data.Root, error) {
- aSigned := &data.Signed{}
- if err := json.Unmarshal(aJSON, aSigned); err != nil {
- return nil, err
- }
- aRoot := &data.Root{}
- if err := json.Unmarshal(aSigned.Signed, aRoot); err != nil {
- return nil, err
- }
- bSigned := &data.Signed{}
- if err := json.Unmarshal(bJSON, bSigned); err != nil {
- return nil, err
- }
- bRoot := &data.Root{}
- if err := json.Unmarshal(bSigned.Signed, bRoot); err != nil {
- return nil, err
- }
- ndb := verify.NewDB()
- for id, k := range aRoot.Keys {
- if err := ndb.AddKey(id, k); err != nil {
- // TUF is considering in TAP-12 removing the
- // requirement that the keyid hash algorithm be derived
- // from the public key. So to be forwards compatible,
- // we ignore `ErrWrongID` errors.
- //
- // TAP-12: https://github.com/DataDog/taps/blob/master/tap12.md
- if _, ok := err.(verify.ErrWrongID); !ok {
- return nil, err
- }
- }
- }
- for name, role := range aRoot.Roles {
- if err := ndb.AddRole(name, role); err != nil {
- return nil, err
- }
- }
- if err := ndb.VerifySignatures(bSigned, "root"); err != nil {
- return nil, err
- }
- return bRoot, nil
- }
- // FIXME(TUF-0.9) TUF is considering removing support for target files starting
- // with a leading path separator. In order to be backwards compatible, we'll
- // just remove leading separators for now.
- func (c *Client) loadTargets(targets data.TargetFiles) {
- c.targets = make(data.TargetFiles)
- for name, meta := range targets {
- c.targets[name] = meta
- c.targets[util.NormalizeTarget(name)] = meta
- }
- }
- // downloadMetaUnsafe downloads top-level metadata from remote storage without
- // verifying it's length and hashes (used for example to download timestamp.json
- // which has unknown size). It will download at most maxMetaSize bytes.
- func (c *Client) downloadMetaUnsafe(name string, maxMetaSize int64) ([]byte, error) {
- r, size, err := c.remote.GetMeta(name)
- if err != nil {
- if IsNotFound(err) {
- return nil, ErrMissingRemoteMetadata{name}
- }
- return nil, ErrDownloadFailed{name, err}
- }
- defer r.Close()
- // return ErrMetaTooLarge if the reported size is greater than maxMetaSize
- if size > maxMetaSize {
- return nil, ErrMetaTooLarge{name, size, maxMetaSize}
- }
- // although the size has been checked above, use a LimitReader in case
- // the reported size is inaccurate, or size is -1 which indicates an
- // unknown length
- return ioutil.ReadAll(io.LimitReader(r, maxMetaSize))
- }
- // remoteGetFunc is the type of function the download method uses to download
- // remote files
- type remoteGetFunc func(string) (io.ReadCloser, int64, error)
- // downloadHashed tries to download the hashed prefixed version of the file.
- func (c *Client) downloadHashed(file string, get remoteGetFunc, hashes data.Hashes) (io.ReadCloser, int64, error) {
- // try each hashed path in turn, and either return the contents,
- // try the next one if a 404 is returned, or return an error
- for _, path := range util.HashedPaths(file, hashes) {
- r, size, err := get(path)
- if err != nil {
- if IsNotFound(err) {
- continue
- }
- return nil, 0, err
- }
- return r, size, nil
- }
- return nil, 0, ErrNotFound{file}
- }
- // download downloads the given target file from remote storage using the get
- // function, adding hashes to the path if consistent snapshots are in use
- func (c *Client) downloadTarget(file string, get remoteGetFunc, hashes data.Hashes) (io.ReadCloser, int64, error) {
- if c.consistentSnapshot {
- return c.downloadHashed(file, get, hashes)
- } else {
- return get(file)
- }
- }
- // downloadVersionedMeta downloads top-level metadata from remote storage and
- // verifies it using the given file metadata.
- func (c *Client) downloadMeta(name string, version int64, m data.FileMeta) ([]byte, error) {
- r, size, err := func() (io.ReadCloser, int64, error) {
- if c.consistentSnapshot {
- path := util.VersionedPath(name, version)
- r, size, err := c.remote.GetMeta(path)
- if err == nil {
- return r, size, nil
- }
- return nil, 0, err
- } else {
- return c.remote.GetMeta(name)
- }
- }()
- if err != nil {
- if IsNotFound(err) {
- return nil, ErrMissingRemoteMetadata{name}
- }
- return nil, err
- }
- defer r.Close()
- // return ErrWrongSize if the reported size is known and incorrect
- var stream io.Reader
- if m.Length != 0 {
- if size >= 0 && size != m.Length {
- return nil, ErrWrongSize{name, size, m.Length}
- }
- // wrap the data in a LimitReader so we download at most m.Length bytes
- stream = io.LimitReader(r, m.Length)
- } else {
- stream = r
- }
- return ioutil.ReadAll(stream)
- }
- func (c *Client) downloadMetaFromSnapshot(name string, m data.SnapshotFileMeta) ([]byte, error) {
- b, err := c.downloadMeta(name, m.Version, m.FileMeta)
- if err != nil {
- return nil, err
- }
- meta, err := util.GenerateSnapshotFileMeta(bytes.NewReader(b), m.HashAlgorithms()...)
- if err != nil {
- return nil, err
- }
- // 5.6.2 and 5.6.4 - Check against snapshot role's targets hash and version
- if err := util.SnapshotFileMetaEqual(meta, m); err != nil {
- return nil, ErrDownloadFailed{name, err}
- }
- return b, nil
- }
- func (c *Client) downloadMetaFromTimestamp(name string, m data.TimestampFileMeta) ([]byte, error) {
- b, err := c.downloadMeta(name, m.Version, m.FileMeta)
- if err != nil {
- return nil, err
- }
- meta, err := util.GenerateTimestampFileMeta(bytes.NewReader(b), m.HashAlgorithms()...)
- if err != nil {
- return nil, err
- }
- // 5.5.2 and 5.5.4 - Check against timestamp role's snapshot hash and version
- if err := util.TimestampFileMetaEqual(meta, m); err != nil {
- return nil, ErrDownloadFailed{name, err}
- }
- return b, nil
- }
- // decodeRoot decodes and verifies root metadata.
- func (c *Client) decodeRoot(b json.RawMessage) error {
- root := &data.Root{}
- if err := c.db.Unmarshal(b, root, "root", c.rootVer); err != nil {
- return ErrDecodeFailed{"root.json", err}
- }
- c.rootVer = root.Version
- c.consistentSnapshot = root.ConsistentSnapshot
- return nil
- }
- // decodeSnapshot decodes and verifies snapshot metadata, and returns the new
- // root and targets file meta.
- func (c *Client) decodeSnapshot(b json.RawMessage) (data.SnapshotFiles, error) {
- snapshot := &data.Snapshot{}
- // 5.5.(3 and 6) - Verify it's signed correctly and it's not expired
- if err := c.db.Unmarshal(b, snapshot, "snapshot", c.snapshotVer); err != nil {
- return data.SnapshotFiles{}, ErrDecodeFailed{"snapshot.json", err}
- }
- // 5.5.5 - Check for top-level targets rollback attack
- // Verify explicitly that current targets meta version is less than or equal to the new one
- if snapshot.Meta["targets.json"].Version < c.targetsVer {
- return data.SnapshotFiles{}, verify.ErrLowVersion{Actual: snapshot.Meta["targets.json"].Version, Current: c.targetsVer}
- }
- // 5.5.5 - Get the local/trusted snapshot metadata, if any, and check all target metafiles against rollback attack
- // In case the local snapshot metadata was not verified by the keys in the latest root during getLocalMeta(),
- // snapshot.json won't be present in c.localMeta and thus this check will not be processed.
- if snapshotJSON, ok := c.localMeta["snapshot.json"]; ok {
- currentSnapshot := &data.Snapshot{}
- if err := c.db.UnmarshalTrusted(snapshotJSON, currentSnapshot, "snapshot"); err != nil {
- return data.SnapshotFiles{}, err
- }
- // 5.5.5 - Check for rollback attacks in both top-level and delegated targets roles (note that the Meta object includes both)
- for path, local := range currentSnapshot.Meta {
- if newMeta, ok := snapshot.Meta[path]; ok {
- // 5.5.5 - Check for rollback attack
- if newMeta.Version < local.Version {
- return data.SnapshotFiles{}, verify.ErrLowVersion{Actual: newMeta.Version, Current: local.Version}
- }
- } else {
- // 5.5.5 - Abort the update if a target file has been removed from the new snapshot file
- return data.SnapshotFiles{}, verify.ErrMissingTargetFile
- }
- }
- }
- // At this point we can trust the new snapshot, the top-level targets, and any delegated targets versions it refers to
- // so we can update the client's trusted versions and proceed with persisting the new snapshot metadata
- // c.snapshotVer was already set when we verified the timestamp metadata
- c.targetsVer = snapshot.Meta["targets.json"].Version
- return snapshot.Meta, nil
- }
- // decodeTargets decodes and verifies targets metadata, sets c.targets and
- // returns updated targets.
- func (c *Client) decodeTargets(b json.RawMessage) (data.TargetFiles, error) {
- targets := &data.Targets{}
- // 5.6.(3 and 5) - Verify signatures and check against freeze attack
- if err := c.db.Unmarshal(b, targets, "targets", c.targetsVer); err != nil {
- return nil, ErrDecodeFailed{"targets.json", err}
- }
- // Generate a list with the updated targets
- updatedTargets := make(data.TargetFiles)
- for path, meta := range targets.Targets {
- if local, ok := c.targets[path]; ok {
- if err := util.TargetFileMetaEqual(local, meta); err == nil {
- continue
- }
- }
- updatedTargets[path] = meta
- }
- // c.targetsVer was already updated when we verified the snapshot metadata
- // FIXME(TUF-0.9) temporarily support files with leading path separators.
- // c.targets = targets.Targets
- c.loadTargets(targets.Targets)
- return updatedTargets, nil
- }
- // decodeTimestamp decodes and verifies timestamp metadata, and returns the
- // new snapshot file meta.
- func (c *Client) decodeTimestamp(b json.RawMessage) (data.TimestampFileMeta, error) {
- timestamp := &data.Timestamp{}
- if err := c.db.Unmarshal(b, timestamp, "timestamp", c.timestampVer); err != nil {
- return data.TimestampFileMeta{}, ErrDecodeFailed{"timestamp.json", err}
- }
- // 5.4.3.2 - Check for snapshot rollback attack
- // Verify that the current snapshot meta version is less than or equal to the new one
- if timestamp.Meta["snapshot.json"].Version < c.snapshotVer {
- return data.TimestampFileMeta{}, verify.ErrLowVersion{Actual: timestamp.Meta["snapshot.json"].Version, Current: c.snapshotVer}
- }
- // At this point we can trust the new timestamp and the snaphost version it refers to
- // so we can update the client's trusted versions and proceed with persisting the new timestamp
- c.timestampVer = timestamp.Version
- c.snapshotVer = timestamp.Meta["snapshot.json"].Version
- return timestamp.Meta["snapshot.json"], nil
- }
- // hasMetaFromSnapshot checks whether local metadata has the given meta
- func (c *Client) hasMetaFromSnapshot(name string, m data.SnapshotFileMeta) bool {
- _, ok := c.localMetaFromSnapshot(name, m)
- return ok
- }
- // localMetaFromSnapshot returns localmetadata if it matches the snapshot
- func (c *Client) localMetaFromSnapshot(name string, m data.SnapshotFileMeta) (json.RawMessage, bool) {
- b, ok := c.localMeta[name]
- if !ok {
- return nil, false
- }
- meta, err := util.GenerateSnapshotFileMeta(bytes.NewReader(b), m.HashAlgorithms()...)
- if err != nil {
- return nil, false
- }
- err = util.SnapshotFileMetaEqual(meta, m)
- return b, err == nil
- }
- // hasTargetsMeta checks whether local metadata has the given snapshot meta
- //
- //lint:ignore U1000 unused
- func (c *Client) hasTargetsMeta(m data.SnapshotFileMeta) bool {
- b, ok := c.localMeta["targets.json"]
- if !ok {
- return false
- }
- meta, err := util.GenerateSnapshotFileMeta(bytes.NewReader(b), m.HashAlgorithms()...)
- if err != nil {
- return false
- }
- err = util.SnapshotFileMetaEqual(meta, m)
- return err == nil
- }
- // hasSnapshotMeta checks whether local metadata has the given meta
- //
- //lint:ignore U1000 unused
- func (c *Client) hasMetaFromTimestamp(name string, m data.TimestampFileMeta) bool {
- b, ok := c.localMeta[name]
- if !ok {
- return false
- }
- meta, err := util.GenerateTimestampFileMeta(bytes.NewReader(b), m.HashAlgorithms()...)
- if err != nil {
- return false
- }
- err = util.TimestampFileMetaEqual(meta, m)
- return err == nil
- }
- type Destination interface {
- io.Writer
- Delete() error
- }
- // Download downloads the given target file from remote storage into dest.
- //
- // dest will be deleted and an error returned in the following situations:
- //
- // - The target does not exist in the local targets.json
- // - Failed to fetch the chain of delegations accessible from local snapshot.json
- // - The target does not exist in any targets
- // - Metadata cannot be generated for the downloaded data
- // - Generated metadata does not match local metadata for the given file
- func (c *Client) Download(name string, dest Destination) (err error) {
- // delete dest if there is an error
- defer func() {
- if err != nil {
- dest.Delete()
- }
- }()
- // populate c.targets from local storage if not set
- if c.targets == nil {
- if err := c.getLocalMeta(); err != nil {
- return err
- }
- }
- normalizedName := util.NormalizeTarget(name)
- localMeta, ok := c.targets[normalizedName]
- if !ok {
- // search in delegations
- localMeta, err = c.getTargetFileMeta(normalizedName)
- if err != nil {
- return err
- }
- }
- // get the data from remote storage
- r, size, err := c.downloadTarget(normalizedName, c.remote.GetTarget, localMeta.Hashes)
- if err != nil {
- return err
- }
- defer r.Close()
- // return ErrWrongSize if the reported size is known and incorrect
- if size >= 0 && size != localMeta.Length {
- return ErrWrongSize{name, size, localMeta.Length}
- }
- // wrap the data in a LimitReader so we download at most localMeta.Length bytes
- stream := io.LimitReader(r, localMeta.Length)
- // read the data, simultaneously writing it to dest and generating metadata
- actual, err := util.GenerateTargetFileMeta(io.TeeReader(stream, dest), localMeta.HashAlgorithms()...)
- if err != nil {
- return ErrDownloadFailed{name, err}
- }
- // check the data has the correct length and hashes
- if err := util.TargetFileMetaEqual(actual, localMeta); err != nil {
- if e, ok := err.(util.ErrWrongLength); ok {
- return ErrWrongSize{name, e.Actual, e.Expected}
- }
- return ErrDownloadFailed{name, err}
- }
- return nil
- }
- func (c *Client) VerifyDigest(digest string, digestAlg string, length int64, path string) error {
- localMeta, ok := c.targets[path]
- if !ok {
- return ErrUnknownTarget{Name: path, SnapshotVersion: c.snapshotVer}
- }
- actual := data.FileMeta{Length: length, Hashes: make(data.Hashes, 1)}
- var err error
- actual.Hashes[digestAlg], err = hex.DecodeString(digest)
- if err != nil {
- return err
- }
- if err := util.TargetFileMetaEqual(data.TargetFileMeta{FileMeta: actual}, localMeta); err != nil {
- if e, ok := err.(util.ErrWrongLength); ok {
- return ErrWrongSize{path, e.Actual, e.Expected}
- }
- return ErrDownloadFailed{path, err}
- }
- return nil
- }
- // Target returns the target metadata for a specific target if it
- // exists, searching from top-level level targets then through
- // all delegations. If it does not, ErrNotFound will be returned.
- func (c *Client) Target(name string) (data.TargetFileMeta, error) {
- target, err := c.getTargetFileMeta(util.NormalizeTarget(name))
- if err == nil {
- return target, nil
- }
- if _, ok := err.(ErrUnknownTarget); ok {
- return data.TargetFileMeta{}, ErrNotFound{name}
- }
- return data.TargetFileMeta{}, err
- }
- // Targets returns the complete list of available top-level targets.
- func (c *Client) Targets() (data.TargetFiles, error) {
- // populate c.targets from local storage if not set
- if c.targets == nil {
- if err := c.getLocalMeta(); err != nil {
- return nil, err
- }
- }
- return c.targets, nil
- }
|