github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/boot/assets.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2020 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package boot
    21  
    22  import (
    23  	"crypto"
    24  	"encoding/hex"
    25  	"errors"
    26  	"fmt"
    27  	"io"
    28  	"os"
    29  	"path/filepath"
    30  
    31  	_ "golang.org/x/crypto/sha3"
    32  
    33  	"github.com/snapcore/snapd/asserts"
    34  	"github.com/snapcore/snapd/bootloader"
    35  	"github.com/snapcore/snapd/dirs"
    36  	"github.com/snapcore/snapd/gadget"
    37  	"github.com/snapcore/snapd/logger"
    38  	"github.com/snapcore/snapd/osutil"
    39  	"github.com/snapcore/snapd/secboot"
    40  	"github.com/snapcore/snapd/strutil"
    41  )
    42  
    43  type trustedAssetsCache struct {
    44  	cacheDir string
    45  	hash     crypto.Hash
    46  }
    47  
    48  func newTrustedAssetsCache(cacheDir string) *trustedAssetsCache {
    49  	return &trustedAssetsCache{cacheDir: cacheDir, hash: crypto.SHA3_384}
    50  }
    51  
    52  func (c *trustedAssetsCache) tempAssetRelPath(blName, assetName string) string {
    53  	return filepath.Join(blName, assetName+".temp")
    54  }
    55  
    56  func (c *trustedAssetsCache) pathInCache(part string) string {
    57  	return filepath.Join(c.cacheDir, part)
    58  }
    59  
    60  func trustedAssetCacheRelPath(blName, assetName, assetHash string) string {
    61  	return filepath.Join(blName, fmt.Sprintf("%s-%s", assetName, assetHash))
    62  }
    63  
    64  // fileHash calculates the hash of an arbitrary file using the same hash method
    65  // as the cache.
    66  func (c *trustedAssetsCache) fileHash(name string) (string, error) {
    67  	digest, _, err := osutil.FileDigest(name, c.hash)
    68  	if err != nil {
    69  		return "", err
    70  	}
    71  	return hex.EncodeToString(digest), nil
    72  }
    73  
    74  // Add entry for a new named asset owned by a particular bootloader, with the
    75  // binary content of the located at a given path. The cache ensures that only
    76  // one entry for given tuple of (bootloader name, asset name, content-hash)
    77  // exists in the cache.
    78  func (c *trustedAssetsCache) Add(assetPath, blName, assetName string) (*trackedAsset, error) {
    79  	if err := os.MkdirAll(c.pathInCache(blName), 0755); err != nil {
    80  		return nil, fmt.Errorf("cannot create cache directory: %v", err)
    81  	}
    82  
    83  	// input
    84  	inf, err := os.Open(assetPath)
    85  	if err != nil {
    86  		return nil, fmt.Errorf("cannot open asset file: %v", err)
    87  	}
    88  	defer inf.Close()
    89  	// temporary output
    90  	tempPath := c.pathInCache(c.tempAssetRelPath(blName, assetName))
    91  	outf, err := osutil.NewAtomicFile(tempPath, 0644, 0, osutil.NoChown, osutil.NoChown)
    92  	if err != nil {
    93  		return nil, fmt.Errorf("cannot create temporary cache file: %v", err)
    94  	}
    95  	defer outf.Cancel()
    96  
    97  	// copy and hash at the same time
    98  	h := c.hash.New()
    99  	tr := io.TeeReader(inf, h)
   100  	if _, err := io.Copy(outf, tr); err != nil {
   101  		return nil, fmt.Errorf("cannot copy trusted asset to cache: %v", err)
   102  	}
   103  	hashStr := hex.EncodeToString(h.Sum(nil))
   104  	cacheKey := trustedAssetCacheRelPath(blName, assetName, hashStr)
   105  
   106  	ta := &trackedAsset{
   107  		blName: blName,
   108  		name:   assetName,
   109  		hash:   hashStr,
   110  	}
   111  
   112  	targetName := c.pathInCache(cacheKey)
   113  	if osutil.FileExists(targetName) {
   114  		// asset is already cached
   115  		return ta, nil
   116  	}
   117  	// commit under a new name
   118  	if err := outf.CommitAs(targetName); err != nil {
   119  		return nil, fmt.Errorf("cannot commit file to assets cache: %v", err)
   120  	}
   121  	return ta, nil
   122  }
   123  
   124  func (c *trustedAssetsCache) Remove(blName, assetName, hashStr string) error {
   125  	cacheKey := trustedAssetCacheRelPath(blName, assetName, hashStr)
   126  	if err := os.Remove(c.pathInCache(cacheKey)); err != nil && !os.IsNotExist(err) {
   127  		return err
   128  	}
   129  	return nil
   130  }
   131  
   132  // CopyBootAssetsCacheToRoot copies the boot assets cache to a corresponding
   133  // location under a new root directory.
   134  func CopyBootAssetsCacheToRoot(dstRoot string) error {
   135  	if !osutil.IsDirectory(dirs.SnapBootAssetsDir) {
   136  		// nothing to copy
   137  		return nil
   138  	}
   139  
   140  	newCacheRoot := dirs.SnapBootAssetsDirUnder(dstRoot)
   141  	if err := os.MkdirAll(newCacheRoot, 0755); err != nil {
   142  		return fmt.Errorf("cannot create cache directory under new root: %v", err)
   143  	}
   144  	err := filepath.Walk(dirs.SnapBootAssetsDir, func(path string, info os.FileInfo, err error) error {
   145  		if err != nil {
   146  			return err
   147  		}
   148  		relPath, err := filepath.Rel(dirs.SnapBootAssetsDir, path)
   149  		if err != nil {
   150  			return err
   151  		}
   152  		if info.IsDir() {
   153  			if err := os.MkdirAll(filepath.Join(newCacheRoot, relPath), info.Mode()); err != nil {
   154  				return fmt.Errorf("cannot recreate cache directory %q: %v", relPath, err)
   155  			}
   156  			return nil
   157  		}
   158  		if !info.Mode().IsRegular() {
   159  			return fmt.Errorf("unsupported non-file entry %q mode %v", relPath, info.Mode())
   160  		}
   161  		if err := osutil.CopyFile(path, filepath.Join(newCacheRoot, relPath), osutil.CopyFlagPreserveAll); err != nil {
   162  			return fmt.Errorf("cannot copy boot asset cache file %q: %v", relPath, err)
   163  		}
   164  		return nil
   165  	})
   166  	return err
   167  }
   168  
   169  // ErrObserverNotApplicable indicates that observer is not applicable for use
   170  // with the model.
   171  var ErrObserverNotApplicable = errors.New("observer not applicable")
   172  
   173  // TrustedAssetsInstallObserverForModel returns a new trusted assets observer
   174  // for use during installation of the run mode system, provided the device model
   175  // supports secure boot. Otherwise, nil and ErrObserverNotApplicable is
   176  // returned.
   177  func TrustedAssetsInstallObserverForModel(model *asserts.Model, gadgetDir string) (*TrustedAssetsInstallObserver, error) {
   178  	if model.Grade() == asserts.ModelGradeUnset {
   179  		// no need to observe updates when assets are not managed
   180  		return nil, ErrObserverNotApplicable
   181  	}
   182  	if gadgetDir == "" {
   183  		return nil, fmt.Errorf("internal error: gadget dir not provided")
   184  	}
   185  
   186  	return &TrustedAssetsInstallObserver{
   187  		model:     model,
   188  		cache:     newTrustedAssetsCache(dirs.SnapBootAssetsDir),
   189  		gadgetDir: gadgetDir,
   190  	}, nil
   191  }
   192  
   193  type trackedAsset struct {
   194  	blName, name, hash string
   195  }
   196  
   197  func isAssetAlreadyTracked(bam bootAssetsMap, newAsset *trackedAsset) bool {
   198  	return isAssetHashTrackedInMap(bam, newAsset.name, newAsset.hash)
   199  }
   200  
   201  func isAssetHashTrackedInMap(bam bootAssetsMap, assetName, assetHash string) bool {
   202  	if bam == nil {
   203  		return false
   204  	}
   205  	hashes, ok := bam[assetName]
   206  	if !ok {
   207  		return false
   208  	}
   209  	return strutil.ListContains(hashes, assetHash)
   210  }
   211  
   212  // TrustedAssetsInstallObserver tracks the installation of trusted boot assets.
   213  type TrustedAssetsInstallObserver struct {
   214  	model                 *asserts.Model
   215  	gadgetDir             string
   216  	cache                 *trustedAssetsCache
   217  	blName                string
   218  	trustedAssets         []string
   219  	trackedAssets         bootAssetsMap
   220  	trackedRecoveryAssets bootAssetsMap
   221  	encryptionKey         secboot.EncryptionKey
   222  }
   223  
   224  // Observe observes the operation related to the content of a given gadget
   225  // structure. In particular, the TrustedAssetsInstallObserver tracks writing of
   226  // managed boot assets, such as the bootloader binary which is measured as part
   227  // of the secure boot.
   228  //
   229  // Implements gadget.ContentObserver.
   230  func (o *TrustedAssetsInstallObserver) Observe(op gadget.ContentOperation, affectedStruct *gadget.LaidOutStructure, root, relativeTarget string, data *gadget.ContentChange) (bool, error) {
   231  	if affectedStruct.Role != gadget.SystemBoot {
   232  		// only care about system-boot
   233  		return true, nil
   234  	}
   235  
   236  	if o.blName == "" {
   237  		// we have no information about the bootloader yet
   238  		bl, err := bootloader.ForGadget(o.gadgetDir, root, &bootloader.Options{Role: bootloader.RoleRunMode, NoSlashBoot: true})
   239  		if err != nil {
   240  			return false, fmt.Errorf("cannot find bootloader: %v", err)
   241  		}
   242  		o.blName = bl.Name()
   243  		tbl, ok := bl.(bootloader.TrustedAssetsBootloader)
   244  		if !ok {
   245  			return true, nil
   246  		}
   247  		trustedAssets, err := tbl.TrustedAssets()
   248  		if err != nil {
   249  			return false, fmt.Errorf("cannot list %q bootloader trusted assets: %v", bl.Name(), err)
   250  		}
   251  		o.trustedAssets = trustedAssets
   252  	}
   253  	if len(o.trustedAssets) == 0 || !strutil.ListContains(o.trustedAssets, relativeTarget) {
   254  		// not one of the trusted assets
   255  		return true, nil
   256  	}
   257  	ta, err := o.cache.Add(data.After, o.blName, filepath.Base(relativeTarget))
   258  	if err != nil {
   259  		return false, err
   260  	}
   261  	// during installation, modeenv is written out later, at this point we
   262  	// only care that the same file may appear multiple times in gadget
   263  	// structure content, so make sure we are not tracking it yet
   264  	if !isAssetAlreadyTracked(o.trackedAssets, ta) {
   265  		if o.trackedAssets == nil {
   266  			o.trackedAssets = bootAssetsMap{}
   267  		}
   268  		if len(o.trackedAssets[ta.name]) > 0 {
   269  			return false, fmt.Errorf("cannot reuse asset name %q", ta.name)
   270  		}
   271  		o.trackedAssets[ta.name] = append(o.trackedAssets[ta.name], ta.hash)
   272  	}
   273  	return true, nil
   274  }
   275  
   276  // ObserveExistingTrustedRecoveryAssets observes existing trusted assets of a
   277  // recovery bootloader located inside a given root directory.
   278  func (o *TrustedAssetsInstallObserver) ObserveExistingTrustedRecoveryAssets(recoveryRootDir string) error {
   279  	bl, err := bootloader.Find(recoveryRootDir, &bootloader.Options{
   280  		Role: bootloader.RoleRecovery,
   281  	})
   282  	if err != nil {
   283  		return fmt.Errorf("cannot identify recovery system bootloader: %v", err)
   284  	}
   285  	tbl, ok := bl.(bootloader.TrustedAssetsBootloader)
   286  	if !ok {
   287  		// not a trusted assets bootloader
   288  		return nil
   289  	}
   290  	trustedAssets, err := tbl.TrustedAssets()
   291  	if err != nil {
   292  		return fmt.Errorf("cannot list %q recovery bootloader trusted assets: %v", bl.Name(), err)
   293  	}
   294  	for _, trustedAsset := range trustedAssets {
   295  		ta, err := o.cache.Add(filepath.Join(recoveryRootDir, trustedAsset), bl.Name(), filepath.Base(trustedAsset))
   296  		if err != nil {
   297  			return err
   298  		}
   299  		if !isAssetAlreadyTracked(o.trackedRecoveryAssets, ta) {
   300  			if o.trackedRecoveryAssets == nil {
   301  				o.trackedRecoveryAssets = bootAssetsMap{}
   302  			}
   303  			if len(o.trackedRecoveryAssets[ta.name]) > 0 {
   304  				return fmt.Errorf("cannot reuse recovery asset name %q", ta.name)
   305  			}
   306  			o.trackedRecoveryAssets[ta.name] = append(o.trackedRecoveryAssets[ta.name], ta.hash)
   307  		}
   308  	}
   309  	return nil
   310  }
   311  
   312  func (o *TrustedAssetsInstallObserver) currentTrustedBootAssetsMap() bootAssetsMap {
   313  	return o.trackedAssets
   314  }
   315  
   316  func (o *TrustedAssetsInstallObserver) currentTrustedRecoveryBootAssetsMap() bootAssetsMap {
   317  	return o.trackedRecoveryAssets
   318  }
   319  
   320  func (o *TrustedAssetsInstallObserver) ChosenEncryptionKey(key secboot.EncryptionKey) {
   321  	o.encryptionKey = key
   322  }
   323  
   324  // TrustedAssetsUpdateObserverForModel returns a new trusted assets observer for
   325  // tracking changes to the measured boot assets during gadget updates, provided
   326  // the device model supports secure boot. Otherwise, nil and ErrObserverNotApplicable is
   327  // returned.
   328  func TrustedAssetsUpdateObserverForModel(model *asserts.Model) (*TrustedAssetsUpdateObserver, error) {
   329  	if model.Grade() == asserts.ModelGradeUnset {
   330  		// no need to observe updates when assets are not managed
   331  		return nil, ErrObserverNotApplicable
   332  	}
   333  
   334  	return &TrustedAssetsUpdateObserver{
   335  		cache: newTrustedAssetsCache(dirs.SnapBootAssetsDir),
   336  	}, nil
   337  }
   338  
   339  // TrustedAssetsUpdateObserver tracks the updates of trusted boot assets and
   340  // attempts to reseal when needed.
   341  type TrustedAssetsUpdateObserver struct {
   342  	cache *trustedAssetsCache
   343  
   344  	bootBootloader    bootloader.Bootloader
   345  	bootTrustedAssets []string
   346  	changedAssets     []*trackedAsset
   347  
   348  	seedBootloader    bootloader.Bootloader
   349  	seedTrustedAssets []string
   350  	seedChangedAssets []*trackedAsset
   351  
   352  	modeenv *Modeenv
   353  }
   354  
   355  func findMaybeTrustedAssetsBootloader(root string, opts *bootloader.Options) (foundBl bootloader.Bootloader, trustedAssets []string, err error) {
   356  	foundBl, err = bootloader.Find(root, opts)
   357  	if err != nil {
   358  		return nil, nil, fmt.Errorf("cannot find bootloader: %v", err)
   359  	}
   360  	tbl, ok := foundBl.(bootloader.TrustedAssetsBootloader)
   361  	if !ok {
   362  		return foundBl, nil, nil
   363  	}
   364  	trustedAssets, err = tbl.TrustedAssets()
   365  	if err != nil {
   366  		return nil, nil, fmt.Errorf("cannot list %q bootloader trusted assets: %v", foundBl.Name(), err)
   367  	}
   368  	return foundBl, trustedAssets, nil
   369  }
   370  
   371  // Observe observes the operation related to the update or rollback of the
   372  // content of a given gadget structure. In particular, the
   373  // TrustedAssetsUpdateObserver tracks updates of managed boot assets, such as
   374  // the bootloader binary which is measured as part of the secure boot.
   375  //
   376  // Implements gadget.ContentUpdateObserver.
   377  func (o *TrustedAssetsUpdateObserver) Observe(op gadget.ContentOperation, affectedStruct *gadget.LaidOutStructure, root, relativeTarget string, data *gadget.ContentChange) (bool, error) {
   378  	var whichBootloader bootloader.Bootloader
   379  	var whichAssets []string
   380  	var err error
   381  	var isRecovery bool
   382  
   383  	switch affectedStruct.Role {
   384  	case gadget.SystemBoot:
   385  		if o.bootBootloader == nil {
   386  			o.bootBootloader, o.bootTrustedAssets, err = findMaybeTrustedAssetsBootloader(root, &bootloader.Options{
   387  				Role:        bootloader.RoleRunMode,
   388  				NoSlashBoot: true,
   389  			})
   390  			if err != nil {
   391  				return false, err
   392  			}
   393  		}
   394  		whichBootloader = o.bootBootloader
   395  		whichAssets = o.bootTrustedAssets
   396  	case gadget.SystemSeed:
   397  		if o.seedBootloader == nil {
   398  			o.seedBootloader, o.seedTrustedAssets, err = findMaybeTrustedAssetsBootloader(root, &bootloader.Options{
   399  				Role: bootloader.RoleRecovery,
   400  			})
   401  			if err != nil {
   402  				return false, err
   403  			}
   404  		}
   405  		whichBootloader = o.seedBootloader
   406  		whichAssets = o.seedTrustedAssets
   407  		isRecovery = true
   408  	default:
   409  		// only system-seed and system-boot are of interest
   410  		return true, nil
   411  	}
   412  	if len(whichAssets) == 0 || !strutil.ListContains(whichAssets, relativeTarget) {
   413  		// not one of the trusted assets
   414  		return true, nil
   415  	}
   416  	if o.modeenv == nil {
   417  		// we've hit a trusted asset, so a modeenv is needed now too
   418  		o.modeenv, err = ReadModeenv("")
   419  		if err != nil {
   420  			return false, fmt.Errorf("cannot load modeenv: %v", err)
   421  		}
   422  	}
   423  	switch op {
   424  	case gadget.ContentUpdate:
   425  		return o.observeUpdate(whichBootloader, isRecovery, root, relativeTarget, data)
   426  	case gadget.ContentRollback:
   427  		return o.observeRollback(whichBootloader, isRecovery, root, relativeTarget, data)
   428  	default:
   429  		// we only care about update and rollback actions
   430  		return false, nil
   431  	}
   432  }
   433  
   434  func (o *TrustedAssetsUpdateObserver) observeUpdate(bl bootloader.Bootloader, recovery bool, root, relativeTarget string, change *gadget.ContentChange) (bool, error) {
   435  	modeenvBefore, err := o.modeenv.Copy()
   436  	if err != nil {
   437  		return false, fmt.Errorf("cannot copy modeenv: %v", err)
   438  	}
   439  
   440  	// we may be running after a mid-update reboot, where a successful boot
   441  	// would have trimmed the tracked assets hash lists to contain only the
   442  	// asset we booted with
   443  
   444  	var taBefore *trackedAsset
   445  	if change.Before != "" {
   446  		// make sure that the original copy is present in the cache if
   447  		// it existed
   448  		taBefore, err = o.cache.Add(change.Before, bl.Name(), filepath.Base(relativeTarget))
   449  		if err != nil {
   450  			return false, err
   451  		}
   452  	}
   453  
   454  	ta, err := o.cache.Add(change.After, bl.Name(), filepath.Base(relativeTarget))
   455  	if err != nil {
   456  		return false, err
   457  	}
   458  
   459  	trustedAssets := &o.modeenv.CurrentTrustedBootAssets
   460  	changedAssets := &o.changedAssets
   461  	if recovery {
   462  		trustedAssets = &o.modeenv.CurrentTrustedRecoveryBootAssets
   463  		changedAssets = &o.seedChangedAssets
   464  	}
   465  	// keep track of the change for cancellation purpose
   466  	*changedAssets = append(*changedAssets, ta)
   467  
   468  	if *trustedAssets == nil {
   469  		*trustedAssets = bootAssetsMap{}
   470  	}
   471  
   472  	if taBefore != nil && !isAssetAlreadyTracked(*trustedAssets, taBefore) {
   473  		// make sure that the boot asset that was was in the filesystem
   474  		// before the update, is properly tracked until either a
   475  		// successful boot or the update is canceled
   476  		// the original asset hash is listed first
   477  		(*trustedAssets)[taBefore.name] = append([]string{taBefore.hash}, (*trustedAssets)[taBefore.name]...)
   478  	}
   479  
   480  	if !isAssetAlreadyTracked(*trustedAssets, ta) {
   481  		if len((*trustedAssets)[ta.name]) > 1 {
   482  			// we expect at most 2 different blobs for a given asset
   483  			// name, the current one and one that will be installed
   484  			// during an update; more entries indicates that the
   485  			// same asset name is used multiple times with different
   486  			// content
   487  			return false, fmt.Errorf("cannot reuse asset name %q", ta.name)
   488  		}
   489  		(*trustedAssets)[ta.name] = append((*trustedAssets)[ta.name], ta.hash)
   490  	}
   491  
   492  	if o.modeenv.deepEqual(modeenvBefore) {
   493  		return true, nil
   494  	}
   495  	if err := o.modeenv.Write(); err != nil {
   496  		return false, fmt.Errorf("cannot write modeeenv: %v", err)
   497  	}
   498  	return true, nil
   499  }
   500  
   501  func (o *TrustedAssetsUpdateObserver) observeRollback(bl bootloader.Bootloader, recovery bool, root, relativeTarget string, data *gadget.ContentChange) (bool, error) {
   502  	trustedAssets := &o.modeenv.CurrentTrustedBootAssets
   503  	otherTrustedAssets := o.modeenv.CurrentTrustedRecoveryBootAssets
   504  	if recovery {
   505  		trustedAssets = &o.modeenv.CurrentTrustedRecoveryBootAssets
   506  		otherTrustedAssets = o.modeenv.CurrentTrustedBootAssets
   507  	}
   508  
   509  	assetName := filepath.Base(relativeTarget)
   510  	hashList, ok := (*trustedAssets)[assetName]
   511  	if !ok || len(hashList) == 0 {
   512  		// asset not tracked in modeenv
   513  		return true, nil
   514  	}
   515  
   516  	// new assets are appended to the list
   517  	expectedOldHash := hashList[0]
   518  	// sanity check, make sure that the current file is what we expect
   519  	newlyAdded := false
   520  	ondiskHash, err := o.cache.fileHash(filepath.Join(root, relativeTarget))
   521  	if err != nil {
   522  		// file may not exist if it was added by the update, that's ok
   523  		if !os.IsNotExist(err) {
   524  			return false, fmt.Errorf("cannot calculate the digest of current asset: %v", err)
   525  		}
   526  		newlyAdded = true
   527  		if len(hashList) > 1 {
   528  			// we have more than 1 hash of the asset, so we expected
   529  			// a previous revision to be restored, but got nothing
   530  			// instead
   531  			return false, fmt.Errorf("tracked asset %q is unexpectedly missing from disk",
   532  				assetName)
   533  		}
   534  	} else {
   535  		if ondiskHash != expectedOldHash {
   536  			// this is unexpected, a different file exists on disk?
   537  			return false, fmt.Errorf("unexpected content of existing asset %q", relativeTarget)
   538  		}
   539  	}
   540  
   541  	newHash := ""
   542  	if len(hashList) == 1 {
   543  		if newlyAdded {
   544  			newHash = hashList[0]
   545  		}
   546  	} else {
   547  		newHash = hashList[1]
   548  	}
   549  	if newHash != "" && !isAssetHashTrackedInMap(otherTrustedAssets, assetName, newHash) {
   550  		// asset revision is not used used elsewhere, we can remove it from the cache
   551  		if err := o.cache.Remove(bl.Name(), assetName, newHash); err != nil {
   552  			// XXX: should this be a log instead?
   553  			return false, fmt.Errorf("cannot remove unused boot asset %v:%v: %v", assetName, newHash, err)
   554  		}
   555  	}
   556  
   557  	// update modeenv content
   558  	if !newlyAdded {
   559  		(*trustedAssets)[assetName] = hashList[:1]
   560  	} else {
   561  		delete(*trustedAssets, assetName)
   562  	}
   563  
   564  	if err := o.modeenv.Write(); err != nil {
   565  		return false, fmt.Errorf("cannot write modeeenv: %v", err)
   566  	}
   567  
   568  	return false, nil
   569  }
   570  
   571  // BeforeWrite is called when the update process has been staged for execution.
   572  func (o *TrustedAssetsUpdateObserver) BeforeWrite() error {
   573  	// TODO:UC20:
   574  	// - reseal with a given state of modeenv
   575  	return nil
   576  }
   577  
   578  func (o *TrustedAssetsUpdateObserver) canceledUpdate(recovery bool) {
   579  	trustedAssets := &o.modeenv.CurrentTrustedBootAssets
   580  	otherTrustedAssets := o.modeenv.CurrentTrustedRecoveryBootAssets
   581  	changedAssets := o.changedAssets
   582  	if recovery {
   583  		trustedAssets = &o.modeenv.CurrentTrustedRecoveryBootAssets
   584  		otherTrustedAssets = o.modeenv.CurrentTrustedBootAssets
   585  		changedAssets = o.seedChangedAssets
   586  	}
   587  
   588  	if len(*trustedAssets) == 0 {
   589  		return
   590  	}
   591  
   592  	for _, changed := range changedAssets {
   593  		hashList, ok := (*trustedAssets)[changed.name]
   594  		if !ok || len(hashList) == 0 {
   595  			// not tracked already, nothing to do
   596  			continue
   597  		}
   598  		if len(hashList) == 1 {
   599  			currentAssetHash := hashList[0]
   600  			if currentAssetHash != changed.hash {
   601  				// assets list has already been trimmed, nothing
   602  				// to do
   603  				continue
   604  			} else {
   605  				// asset was newly added
   606  				delete(*trustedAssets, changed.name)
   607  			}
   608  		} else {
   609  			// asset updates were appended to the list
   610  			(*trustedAssets)[changed.name] = hashList[:1]
   611  		}
   612  		if !isAssetHashTrackedInMap(otherTrustedAssets, changed.name, changed.hash) {
   613  			// asset revision is not used used elsewhere, we can remove it from the cache
   614  			if err := o.cache.Remove(changed.blName, changed.name, changed.hash); err != nil {
   615  				logger.Noticef("cannot remove unused boot asset %v:%v: %v", changed.name, changed.hash, err)
   616  			}
   617  		}
   618  	}
   619  }
   620  
   621  // Canceled is called when the update has been canceled, or if changes
   622  // were written and the update has been reverted.
   623  func (o *TrustedAssetsUpdateObserver) Canceled() error {
   624  	if o.modeenv == nil {
   625  		// modeenv wasn't even loaded yet, meaning none of the boot
   626  		// assets was updated
   627  		return nil
   628  	}
   629  	for _, isRecovery := range []bool{false, true} {
   630  		o.canceledUpdate(isRecovery)
   631  	}
   632  
   633  	if err := o.modeenv.Write(); err != nil {
   634  		return fmt.Errorf("cannot write modeeenv: %v", err)
   635  	}
   636  
   637  	// TODO:UC20:
   638  	// - reseal with a given state of modeenv
   639  	return nil
   640  }
   641  
   642  func observeSuccessfulBootAssetsForBootloader(m *Modeenv, root string, opts *bootloader.Options) (drop []*trackedAsset, err error) {
   643  	trustedAssetsMap := &m.CurrentTrustedBootAssets
   644  	otherTrustedAssetsMap := m.CurrentTrustedRecoveryBootAssets
   645  	whichBootloader := "run mode"
   646  	if opts != nil && opts.Role == bootloader.RoleRecovery {
   647  		trustedAssetsMap = &m.CurrentTrustedRecoveryBootAssets
   648  		otherTrustedAssetsMap = m.CurrentTrustedBootAssets
   649  		whichBootloader = "recovery"
   650  	}
   651  
   652  	if len(*trustedAssetsMap) == 0 {
   653  		// bootloader may have trusted assets, but we are not tracking
   654  		// any for the boot process
   655  		return nil, nil
   656  	}
   657  
   658  	// let's find the bootloader first
   659  	bl, trustedAssets, err := findMaybeTrustedAssetsBootloader(root, opts)
   660  	if err != nil {
   661  		return nil, err
   662  	}
   663  	if len(trustedAssets) == 0 {
   664  		// not a trusted assets bootloader, nothing to do
   665  		return nil, nil
   666  	}
   667  
   668  	cache := newTrustedAssetsCache(dirs.SnapBootAssetsDir)
   669  	for _, trustedAsset := range trustedAssets {
   670  		assetName := filepath.Base(trustedAsset)
   671  
   672  		// find the hash of the file on disk
   673  		assetHash, err := cache.fileHash(filepath.Join(root, trustedAsset))
   674  		if err != nil && !os.IsNotExist(err) {
   675  			return nil, fmt.Errorf("cannot calculate the digest of existing trusted asset: %v", err)
   676  		}
   677  		if assetHash == "" {
   678  			// no trusted asset on disk, but we booted nonetheless,
   679  			// at least log something
   680  			logger.Noticef("system booted without %v bootloader trusted asset %q", whichBootloader, trustedAsset)
   681  			// given that asset names cannot be reused, clear the
   682  			// boot assets map for the current bootloader
   683  			delete(*trustedAssetsMap, assetName)
   684  			continue
   685  		}
   686  
   687  		// this is what we booted with
   688  		bootedWith := []string{assetHash}
   689  		// one of these was expected during boot
   690  		hashList := (*trustedAssetsMap)[assetName]
   691  
   692  		assetFound := false
   693  		// find out if anything needs to be dropped
   694  		for _, hash := range hashList {
   695  			if hash == assetHash {
   696  				assetFound = true
   697  				continue
   698  			}
   699  			if !isAssetHashTrackedInMap(otherTrustedAssetsMap, assetName, hash) {
   700  				// asset can be dropped
   701  				drop = append(drop, &trackedAsset{
   702  					blName: bl.Name(),
   703  					name:   assetName,
   704  					hash:   hash,
   705  				})
   706  			}
   707  		}
   708  
   709  		if !assetFound {
   710  			// unexpected, we have booted with an asset whose hash
   711  			// is not listed among the ones we expect
   712  
   713  			// TODO:UC20: try to restore the asset from cache
   714  			return nil, fmt.Errorf("system booted with unexpected %v bootloader asset %q hash %v", whichBootloader, trustedAsset, assetHash)
   715  		}
   716  
   717  		// update the list of what we booted with
   718  		(*trustedAssetsMap)[assetName] = bootedWith
   719  
   720  	}
   721  	return drop, nil
   722  }
   723  
   724  // observeSuccessfulBootAssets observes the state of the trusted boot assets
   725  // after a successful boot. Returns a modified modeenv reflecting a new state,
   726  // and a list of assets that can be dropped from the cache.
   727  func observeSuccessfulBootAssets(m *Modeenv) (newM *Modeenv, drop []*trackedAsset, err error) {
   728  	newM, err = m.Copy()
   729  	if err != nil {
   730  		return nil, nil, err
   731  	}
   732  
   733  	for _, bl := range []struct {
   734  		root string
   735  		opts *bootloader.Options
   736  	}{
   737  		{
   738  			// ubuntu-boot bootloader
   739  			root: InitramfsUbuntuBootDir,
   740  			opts: &bootloader.Options{Role: bootloader.RoleRunMode, NoSlashBoot: true},
   741  		}, {
   742  			// ubuntu-seed bootloader
   743  			root: InitramfsUbuntuSeedDir,
   744  			opts: &bootloader.Options{Role: bootloader.RoleRecovery, NoSlashBoot: true},
   745  		},
   746  	} {
   747  		dropForBootloader, err := observeSuccessfulBootAssetsForBootloader(newM, bl.root, bl.opts)
   748  		if err != nil {
   749  			return nil, nil, err
   750  		}
   751  		drop = append(drop, dropForBootloader...)
   752  	}
   753  	return newM, drop, nil
   754  }