blob: f046b6a0f3e816c86dcd5b08c8f6a2067e4970af [file] [log] [blame]
package client
import (
"bytes"
"encoding/json"
"errors"
"io"
"io/ioutil"
"github.com/flynn/go-tuf/data"
"github.com/flynn/go-tuf/util"
"github.com/flynn/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
)
// LocalStore is local storage for downloaded top-level metadata.
type LocalStore interface {
// 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
}
// 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 int
targetsVer int
snapshotVer int
timestampVer int
// 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
}
func NewClient(local LocalStore, remote RemoteStore) *Client {
return &Client{
local: local,
remote: remote,
}
}
// 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.
func (c *Client) Init(rootKeys []*data.Key, threshold int) error {
if len(rootKeys) < threshold {
return ErrInsufficientKeys
}
rootJSON, err := c.downloadMetaUnsafe("root.json", defaultRootDownloadLimit)
if err != nil {
return err
}
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
}
}
}
role := &data.Role{Threshold: threshold, KeyIDs: rootKeyIDs}
if err := c.db.AddRole("root", role); err != nil {
return err
}
if err := c.decodeRoot(rootJSON); err != nil {
return err
}
return c.local.SetMeta("root.json", rootJSON)
}
// Update downloads and verifies remote metadata and returns updated targets.
//
// It performs the update part of "The client application" workflow from
// section 5.1 of the TUF spec:
//
// https://github.com/theupdateframework/tuf/blob/v0.9.9/docs/tuf-spec.txt#L714
func (c *Client) Update() (data.TargetFiles, error) {
return c.update(false)
}
func (c *Client) update(latestRoot bool) (data.TargetFiles, error) {
// Always start the update using local metadata
if err := c.getLocalMeta(); err != nil {
if _, ok := err.(verify.ErrExpired); ok {
if !latestRoot {
return c.updateWithLatestRoot(nil)
}
// this should not be reached as if the latest root has
// been downloaded and it is expired, updateWithLatestRoot
// should not have continued the update
return nil, err
}
if latestRoot && isErrRoleThreshold(err) {
// Root was updated with new keys, so our local metadata is no
// longer validating. Read only the versions from the local metadata
// and re-download everything.
if err := c.getRootAndLocalVersionsUnsafe(); err != nil {
return nil, err
}
} else {
return nil, err
}
}
// Get timestamp.json, extract snapshot.json file meta and save the
// timestamp.json locally
timestampJSON, err := c.downloadMetaUnsafe("timestamp.json", defaultTimestampDownloadLimit)
if err != nil {
return nil, err
}
snapshotMeta, err := c.decodeTimestamp(timestampJSON)
if err != nil {
// ErrRoleThreshold could indicate timestamp keys have been
// revoked, so retry with the latest root.json
if isDecodeFailedWithErrRoleThreshold(err) && !latestRoot {
return c.updateWithLatestRoot(nil)
}
return nil, err
}
if err := c.local.SetMeta("timestamp.json", timestampJSON); err != nil {
return nil, err
}
// Return ErrLatestSnapshot if we already have the latest snapshot.json
if c.hasMetaFromTimestamp("snapshot.json", snapshotMeta) {
return nil, ErrLatestSnapshot{c.snapshotVer}
}
// Get snapshot.json, then extract root.json and targets.json file meta.
//
// The snapshot.json is only saved locally after checking root.json and
// targets.json so that it will be re-downloaded on subsequent updates
// if this update fails.
snapshotJSON, err := c.downloadMetaFromTimestamp("snapshot.json", snapshotMeta)
if err != nil {
return nil, err
}
rootMeta, targetsMeta, err := c.decodeSnapshot(snapshotJSON)
if err != nil {
// ErrRoleThreshold could indicate snapshot keys have been
// revoked, so retry with the latest root.json
if isDecodeFailedWithErrRoleThreshold(err) && !latestRoot {
return c.updateWithLatestRoot(nil)
}
return nil, err
}
// If we don't have the root.json, download it, save it in local
// storage and restart the update
if !c.hasMetaFromSnapshot("root.json", rootMeta) {
return c.updateWithLatestRoot(&rootMeta)
}
// If we don't have the targets.json, download it, determine updated
// targets and save targets.json in local storage
var updatedTargets data.TargetFiles
if !c.hasMetaFromSnapshot("targets.json", targetsMeta) {
targetsJSON, err := c.downloadMetaFromSnapshot("targets.json", targetsMeta)
if err != nil {
return nil, err
}
updatedTargets, err = c.decodeTargets(targetsJSON)
if err != nil {
return nil, err
}
if err := c.local.SetMeta("targets.json", targetsJSON); err != nil {
return nil, err
}
}
// Save the snapshot.json now it has been processed successfully
if err := c.local.SetMeta("snapshot.json", snapshotJSON); err != nil {
return nil, err
}
return updatedTargets, nil
}
func (c *Client) updateWithLatestRoot(m *data.SnapshotFileMeta) (data.TargetFiles, error) {
var rootJSON json.RawMessage
var err error
if m == nil {
rootJSON, err = c.downloadMetaUnsafe("root.json", defaultRootDownloadLimit)
} else {
rootJSON, err = c.downloadMetaFromSnapshot("root.json", *m)
}
if err != nil {
return nil, err
}
if err := c.decodeRoot(rootJSON); err != nil {
return nil, err
}
if err := c.local.SetMeta("root.json", rootJSON); err != nil {
return nil, err
}
return c.update(true)
}
// 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.
func (c *Client) getLocalMeta() error {
meta, err := c.local.GetMeta()
if err != nil {
return err
}
if rootJSON, ok := meta["root.json"]; ok {
// 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
}
c.db = verify.NewDB()
for id, k := range root.Keys {
if err := c.db.AddKey(id, k); err != nil {
// FIXME(TUF-0.9) Ignore unknown keyids, which
// can happen during the transition to TUF-1.0.
if _, ok := err.(verify.ErrWrongID); !ok {
return err
}
}
}
for name, role := range root.Roles {
if err := c.db.AddRole(name, role); err != nil {
return err
}
}
if err := c.db.Verify(s, "root", 0); err != nil {
return err
}
c.consistentSnapshot = root.ConsistentSnapshot
} else {
return ErrNoRootKeys
}
if snapshotJSON, ok := meta["snapshot.json"]; ok {
snapshot := &data.Snapshot{}
if err := c.db.UnmarshalTrusted(snapshotJSON, snapshot, "snapshot"); err != nil {
return err
}
c.snapshotVer = snapshot.Version
}
if targetsJSON, ok := meta["targets.json"]; ok {
targets := &data.Targets{}
if err := c.db.UnmarshalTrusted(targetsJSON, targets, "targets"); err != nil {
return err
}
c.targetsVer = targets.Version
// FIXME(TUF-0.9) temporarily support files with leading path separators.
// c.targets = targets.Targets
c.loadTargets(targets.Targets)
}
if timestampJSON, ok := meta["timestamp.json"]; ok {
timestamp := &data.Timestamp{}
if err := c.db.UnmarshalTrusted(timestampJSON, timestamp, "timestamp"); err != nil {
return err
}
c.timestampVer = timestamp.Version
}
c.localMeta = meta
return 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))
}
// getRootAndLocalVersionsUnsafe decodes the versions stored in the local
// metadata without verifying signatures to protect against downgrade attacks
// when the root is replaced and contains new keys. It also sets the local meta
// cache to only contain the local root metadata.
func (c *Client) getRootAndLocalVersionsUnsafe() error {
type versionData struct {
Signed struct {
Version int
}
}
meta, err := c.local.GetMeta()
if err != nil {
return err
}
getVersion := func(name string) (int, error) {
m, ok := meta[name]
if !ok {
return 0, nil
}
var data versionData
if err := json.Unmarshal(m, &data); err != nil {
return 0, err
}
return data.Signed.Version, nil
}
c.timestampVer, err = getVersion("timestamp.json")
if err != nil {
return err
}
c.snapshotVer, err = getVersion("snapshot.json")
if err != nil {
return err
}
c.targetsVer, err = getVersion("targets.json")
if err != nil {
return err
}
root, ok := meta["root.json"]
if !ok {
return errors.New("tuf: missing local root after downloading, this should not be possible")
}
c.localMeta = map[string]json.RawMessage{"root.json": root}
return nil
}
// 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 int, 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
}
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
}
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.SnapshotFileMeta, data.SnapshotFileMeta, error) {
snapshot := &data.Snapshot{}
if err := c.db.Unmarshal(b, snapshot, "snapshot", c.snapshotVer); err != nil {
return data.SnapshotFileMeta{}, data.SnapshotFileMeta{}, ErrDecodeFailed{"snapshot.json", err}
}
c.snapshotVer = snapshot.Version
return snapshot.Meta["root.json"], snapshot.Meta["targets.json"], 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{}
if err := c.db.Unmarshal(b, targets, "targets", c.targetsVer); err != nil {
return nil, ErrDecodeFailed{"targets.json", err}
}
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 = targets.Version
// 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}
}
c.timestampVer = timestamp.Version
return timestamp.Meta["snapshot.json"], nil
}
// hasSnapshotMeta checks whether local metadata has the given meta
func (c *Client) hasMetaFromSnapshot(name string, m data.SnapshotFileMeta) bool {
b, ok := c.localMeta[name]
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
}
// hasTargetsMeta checks whether local metadata has the given snapshot meta
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
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
// * The target does not exist in remote storage
// * 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
}
}
// return ErrUnknownTarget if the file is not in the local targets.json
normalizedName := util.NormalizeTarget(name)
localMeta, ok := c.targets[normalizedName]
if !ok {
return ErrUnknownTarget{name}
}
// 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
}
// Target returns the target metadata for a specific target if it
// exists. If it does not, ErrNotFound will be returned.
func (c *Client) Target(name string) (data.TargetFileMeta, error) {
m, err := c.Targets()
if err != nil {
return data.TargetFileMeta{}, err
}
target, ok := m[util.NormalizeTarget(name)]
if ok {
return target, nil
}
return data.TargetFileMeta{}, ErrNotFound{name}
}
// Targets returns the complete list of available 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
}