docs/tuf/client/client.go

547 lines
16 KiB
Go

package client
import (
"encoding/json"
"fmt"
"path"
"github.com/Sirupsen/logrus"
"github.com/docker/notary"
tuf "github.com/docker/notary/tuf"
"github.com/docker/notary/tuf/data"
"github.com/docker/notary/tuf/signed"
"github.com/docker/notary/tuf/store"
"github.com/docker/notary/tuf/utils"
)
// Client is a usability wrapper around a raw TUF repo
type Client struct {
local *tuf.Repo
remote store.RemoteStore
cache store.MetadataStore
}
// NewClient initialized a Client with the given repo, remote source of content, and cache
func NewClient(local *tuf.Repo, remote store.RemoteStore, cache store.MetadataStore) *Client {
return &Client{
local: local,
remote: remote,
cache: cache,
}
}
// Update performs an update to the TUF repo as defined by the TUF spec
func (c *Client) Update() error {
// 1. Get timestamp
// a. If timestamp error (verification, expired, etc...) download new root and return to 1.
// 2. Check if local snapshot is up to date
// a. If out of date, get updated snapshot
// i. If snapshot error, download new root and return to 1.
// 3. Check if root correct against snapshot
// a. If incorrect, download new root and return to 1.
// 4. Iteratively download and search targets and delegations to find target meta
logrus.Debug("updating TUF client")
err := c.update()
if err != nil {
logrus.Debug("Error occurred. Root will be downloaded and another update attempted")
if err := c.downloadRoot(); err != nil {
logrus.Debug("Client Update (Root):", err)
return err
}
// If we error again, we now have the latest root and just want to fail
// out as there's no expectation the problem can be resolved automatically
logrus.Debug("retrying TUF client update")
return c.update()
}
return nil
}
func (c *Client) update() error {
err := c.downloadTimestamp()
if err != nil {
logrus.Debugf("Client Update (Timestamp): %s", err.Error())
return err
}
err = c.downloadSnapshot()
if err != nil {
logrus.Debugf("Client Update (Snapshot): %s", err.Error())
return err
}
err = c.checkRoot()
if err != nil {
// In this instance the root has not expired base on time, but is
// expired based on the snapshot dictating a new root has been produced.
logrus.Debug(err)
return err
}
// will always need top level targets at a minimum
err = c.downloadTargets(data.CanonicalTargetsRole)
if err != nil {
logrus.Debugf("Client Update (Targets): %s", err.Error())
return err
}
return nil
}
// checkRoot determines if the hash, and size are still those reported
// in the snapshot file. It will also check the expiry, however, if the
// hash and size in snapshot are unchanged but the root file has expired,
// there is little expectation that the situation can be remedied.
func (c Client) checkRoot() error {
role := data.CanonicalRootRole
size := c.local.Snapshot.Signed.Meta[role].Length
expectedHashes := c.local.Snapshot.Signed.Meta[role].Hashes
raw, err := c.cache.GetMeta("root", size)
if err != nil {
return err
}
if err := data.CheckHashes(raw, expectedHashes); err != nil {
return fmt.Errorf("Cached root hashes did not match snapshot root hashes")
}
if int64(len(raw)) != size {
return fmt.Errorf("Cached root size did not match snapshot size")
}
root := &data.SignedRoot{}
err = json.Unmarshal(raw, root)
if err != nil {
return ErrCorruptedCache{file: "root.json"}
}
if signed.IsExpired(root.Signed.Expires) {
return tuf.ErrLocalRootExpired{}
}
return nil
}
// downloadRoot is responsible for downloading the root.json
func (c *Client) downloadRoot() error {
logrus.Debug("Downloading Root...")
role := data.CanonicalRootRole
// We can't read an exact size for the root metadata without risking getting stuck in the TUF update cycle
// since it's possible that downloading timestamp/snapshot metadata may fail due to a signature mismatch
var size int64 = -1
// We could not expect what the "snapshot" meta has specified.
//
// In some old clients, there is only the "sha256",
// but both "sha256" and "sha512" in the newer ones.
//
// And possibly more in the future.
var expectedHashes data.Hashes
if c.local.Snapshot != nil {
if prevRootMeta, ok := c.local.Snapshot.Signed.Meta[role]; ok {
size = prevRootMeta.Length
expectedHashes = prevRootMeta.Hashes
}
}
// if we're bootstrapping we may not have a cached root, an
// error will result in the "previous root version" being
// interpreted as 0.
var download bool
var err error
var cachedRoot []byte
old := &data.Signed{}
version := 0
// Due to the same reason, we don't really know how many hashes are there.
if len(expectedHashes) != 0 {
// can only trust cache if we have an expected sha256(for example) to trust
cachedRoot, err = c.cache.GetMeta(role, size)
}
if cachedRoot == nil || err != nil {
logrus.Debug("didn't find a cached root, must download")
download = true
} else {
if err := data.CheckHashes(cachedRoot, expectedHashes); err != nil {
logrus.Debug("cached root's hash didn't match expected, must download")
download = true
}
err := json.Unmarshal(cachedRoot, old)
if err == nil {
root, err := data.RootFromSigned(old)
if err == nil {
version = root.Signed.Version
} else {
logrus.Debug("couldn't parse Signed part of cached root, must download")
download = true
}
} else {
logrus.Debug("couldn't parse cached root, must download")
download = true
}
}
var s *data.Signed
var raw []byte
if download {
// use consistent download if we have the checksum.
raw, s, err = c.downloadSigned(role, size, expectedHashes)
if err != nil {
return err
}
} else {
logrus.Debug("using cached root")
s = old
}
if err := c.verifyRoot(role, s, version); err != nil {
return err
}
if download {
logrus.Debug("caching downloaded root")
// Now that we have accepted new root, write it to cache
if err = c.cache.SetMeta(role, raw); err != nil {
logrus.Errorf("Failed to write root to local cache: %s", err.Error())
}
}
return nil
}
func (c Client) verifyRoot(role string, s *data.Signed, minVersion int) error {
// this will confirm that the root has been signed by the old root role
// with the root keys we bootstrapped with.
// Still need to determine if there has been a root key update and
// confirm signature with new root key
logrus.Debug("verifying root with existing keys")
rootRole, err := c.local.GetBaseRole(role)
if err != nil {
logrus.Debug("no previous root role loaded")
return err
}
// Verify using the rootRole loaded from the known root.json
if err = signed.Verify(s, rootRole, minVersion); err != nil {
logrus.Debug("root did not verify with existing keys")
return err
}
logrus.Debug("updating known root roles and keys")
root, err := data.RootFromSigned(s)
if err != nil {
logrus.Error(err.Error())
return err
}
// replace the existing root.json with the new one (just in memory, we
// have another validation step before we fully accept the new root)
err = c.local.SetRoot(root)
if err != nil {
logrus.Error(err.Error())
return err
}
// Verify the new root again having loaded the rootRole out of this new
// file (verifies self-referential integrity)
// TODO(endophage): be more intelligent and only re-verify if we detect
// there has been a change in root keys
logrus.Debug("verifying root with updated keys")
rootRole, err = c.local.GetBaseRole(role)
if err != nil {
logrus.Debug("root role with new keys not loaded")
return err
}
err = signed.Verify(s, rootRole, minVersion)
if err != nil {
logrus.Debug("root did not verify with new keys")
return err
}
logrus.Debug("successfully verified root")
return nil
}
// downloadTimestamp is responsible for downloading the timestamp.json
// Timestamps are special in that we ALWAYS attempt to download and only
// use cache if the download fails (and the cache is still valid).
func (c *Client) downloadTimestamp() error {
logrus.Debug("Downloading Timestamp...")
role := data.CanonicalTimestampRole
// We may not have a cached timestamp if this is the first time
// we're interacting with the repo. This will result in the
// version being 0
var (
old *data.Signed
ts *data.SignedTimestamp
version = 0
)
cachedTS, err := c.cache.GetMeta(role, notary.MaxTimestampSize)
if err == nil {
cached := &data.Signed{}
err := json.Unmarshal(cachedTS, cached)
if err == nil {
ts, err := data.TimestampFromSigned(cached)
if err == nil {
version = ts.Signed.Version
}
old = cached
}
}
// unlike root, targets and snapshot, always try and download timestamps
// from remote, only using the cache one if we couldn't reach remote.
raw, s, err := c.downloadSigned(role, notary.MaxTimestampSize, nil)
if err == nil {
ts, err = c.verifyTimestamp(s, version)
if err == nil {
logrus.Debug("successfully verified downloaded timestamp")
c.cache.SetMeta(role, raw)
c.local.SetTimestamp(ts)
return nil
}
}
if old == nil {
// couldn't retrieve valid data from server and don't have unmarshallable data in cache.
logrus.Debug("no cached timestamp available")
return err
}
logrus.Debug(err.Error())
logrus.Warn("Error while downloading remote metadata, using cached timestamp - this might not be the latest version available remotely")
ts, err = c.verifyTimestamp(old, version)
if err != nil {
return err
}
logrus.Debug("successfully verified cached timestamp")
c.local.SetTimestamp(ts)
return nil
}
// verifies that a timestamp is valid, and returned the SignedTimestamp object to add to the tuf repo
func (c *Client) verifyTimestamp(s *data.Signed, minVersion int) (*data.SignedTimestamp, error) {
timestampRole, err := c.local.GetBaseRole(data.CanonicalTimestampRole)
if err != nil {
logrus.Debug("no timestamp role loaded")
return nil, err
}
if err := signed.Verify(s, timestampRole, minVersion); err != nil {
return nil, err
}
return data.TimestampFromSigned(s)
}
// downloadSnapshot is responsible for downloading the snapshot.json
func (c *Client) downloadSnapshot() error {
logrus.Debug("Downloading Snapshot...")
role := data.CanonicalSnapshotRole
if c.local.Timestamp == nil {
return tuf.ErrNotLoaded{Role: data.CanonicalTimestampRole}
}
size := c.local.Timestamp.Signed.Meta[role].Length
expectedHashes := c.local.Timestamp.Signed.Meta[role].Hashes
if len(expectedHashes) == 0 {
return data.ErrMissingMeta{Role: data.CanonicalSnapshotRole}
}
var download bool
old := &data.Signed{}
version := 0
raw, err := c.cache.GetMeta(role, size)
if raw == nil || err != nil {
logrus.Debug("no snapshot in cache, must download")
download = true
} else {
// file may have been tampered with on disk. Always check the hash!
if err := data.CheckHashes(raw, expectedHashes); err != nil {
logrus.Debug("hash of snapshot in cache did not match expected hash, must download")
download = true
}
err := json.Unmarshal(raw, old)
if err == nil {
snap, err := data.SnapshotFromSigned(old)
if err == nil {
version = snap.Signed.Version
} else {
logrus.Debug("Could not parse Signed part of snapshot, must download")
download = true
}
} else {
logrus.Debug("Could not parse snapshot, must download")
download = true
}
}
var s *data.Signed
if download {
raw, s, err = c.downloadSigned(role, size, expectedHashes)
if err != nil {
return err
}
} else {
logrus.Debug("using cached snapshot")
s = old
}
snapshotRole, err := c.local.GetBaseRole(role)
if err != nil {
logrus.Debug("no snapshot role loaded")
return err
}
err = signed.Verify(s, snapshotRole, version)
if err != nil {
return err
}
logrus.Debug("successfully verified snapshot")
snap, err := data.SnapshotFromSigned(s)
if err != nil {
return err
}
c.local.SetSnapshot(snap)
if download {
err = c.cache.SetMeta(role, raw)
if err != nil {
logrus.Errorf("Failed to write snapshot to local cache: %s", err.Error())
}
}
return nil
}
// downloadTargets downloads all targets and delegated targets for the repository.
// It uses a pre-order tree traversal as it's necessary to download parents first
// to obtain the keys to validate children.
func (c *Client) downloadTargets(role string) error {
logrus.Debug("Downloading Targets...")
stack := utils.NewStack()
stack.Push(role)
for !stack.Empty() {
role, err := stack.PopString()
if err != nil {
return err
}
if c.local.Snapshot == nil {
return tuf.ErrNotLoaded{Role: data.CanonicalSnapshotRole}
}
snap := c.local.Snapshot.Signed
root := c.local.Root.Signed
s, err := c.getTargetsFile(role, snap.Meta, root.ConsistentSnapshot)
if err != nil {
if _, ok := err.(data.ErrMissingMeta); ok && role != data.CanonicalTargetsRole {
// if the role meta hasn't been published,
// that's ok, continue
continue
}
logrus.Error("Error getting targets file:", err)
return err
}
t, err := data.TargetsFromSigned(s, role)
if err != nil {
return err
}
err = c.local.SetTargets(role, t)
if err != nil {
return err
}
// push delegated roles contained in the targets file onto the stack
for _, r := range t.Signed.Delegations.Roles {
if path.Dir(r.Name) == role {
// only load children that are direct 1st generation descendants
// of the role we've just downloaded
stack.Push(r.Name)
}
}
}
return nil
}
func (c *Client) downloadSigned(role string, size int64, expectedHashes data.Hashes) ([]byte, *data.Signed, error) {
rolePath := utils.ConsistentName(role, expectedHashes["sha256"])
raw, err := c.remote.GetMeta(rolePath, size)
if err != nil {
return nil, nil, err
}
if expectedHashes != nil {
if err := data.CheckHashes(raw, expectedHashes); err != nil {
return nil, nil, ErrChecksumMismatch{role: role}
}
}
s := &data.Signed{}
err = json.Unmarshal(raw, s)
if err != nil {
return nil, nil, err
}
return raw, s, nil
}
func (c Client) getTargetsFile(role string, snapshotMeta data.Files, consistent bool) (*data.Signed, error) {
// require role exists in snapshots
roleMeta, ok := snapshotMeta[role]
if !ok {
return nil, data.ErrMissingMeta{Role: role}
}
expectedHashes := snapshotMeta[role].Hashes
if len(expectedHashes) == 0 {
return nil, data.ErrMissingMeta{Role: role}
}
// try to get meta file from content addressed cache
var download bool
old := &data.Signed{}
version := 0
raw, err := c.cache.GetMeta(role, roleMeta.Length)
if err != nil || raw == nil {
logrus.Debugf("Couldn't not find cached %s, must download", role)
download = true
} else {
// file may have been tampered with on disk. Always check the hash!
if err := data.CheckHashes(raw, expectedHashes); err != nil {
download = true
}
err := json.Unmarshal(raw, old)
if err == nil {
targ, err := data.TargetsFromSigned(old, role)
if err == nil {
version = targ.Signed.Version
} else {
download = true
}
} else {
download = true
}
}
size := snapshotMeta[role].Length
var s *data.Signed
if download {
raw, s, err = c.downloadSigned(role, size, expectedHashes)
if err != nil {
return nil, err
}
} else {
logrus.Debug("using cached ", role)
s = old
}
var targetOrDelgRole data.BaseRole
if data.IsDelegation(role) {
delgRole, err := c.local.GetDelegationRole(role)
if err != nil {
logrus.Debugf("no %s delegation role loaded", role)
return nil, err
}
targetOrDelgRole = delgRole.BaseRole
} else {
targetOrDelgRole, err = c.local.GetBaseRole(role)
if err != nil {
logrus.Debugf("no %s role loaded", role)
return nil, err
}
}
if err = signed.Verify(s, targetOrDelgRole, version); err != nil {
return nil, err
}
logrus.Debugf("successfully verified %s", role)
if download {
// if we error when setting meta, we should continue.
err = c.cache.SetMeta(role, raw)
if err != nil {
logrus.Errorf("Failed to write %s to local cache: %s", role, err.Error())
}
}
return s, nil
}