gopkg.in/ubuntu-core/snappy.v0@v0.0.0-20210902073436-25a8614f10a6/boot/bootchain.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  	"bytes"
    24  	"encoding/json"
    25  	"fmt"
    26  	"os"
    27  	"path/filepath"
    28  	"sort"
    29  
    30  	"github.com/snapcore/snapd/asserts"
    31  	"github.com/snapcore/snapd/bootloader"
    32  	"github.com/snapcore/snapd/dirs"
    33  	"github.com/snapcore/snapd/osutil"
    34  	"github.com/snapcore/snapd/secboot"
    35  )
    36  
    37  // TODO:UC20 add a doc comment when this is stabilized
    38  type bootChain struct {
    39  	BrandID        string             `json:"brand-id"`
    40  	Model          string             `json:"model"`
    41  	Grade          asserts.ModelGrade `json:"grade"`
    42  	ModelSignKeyID string             `json:"model-sign-key-id"`
    43  	AssetChain     []bootAsset        `json:"asset-chain"`
    44  	Kernel         string             `json:"kernel"`
    45  	// KernelRevision is the revision of the kernel snap. It is empty if
    46  	// kernel is unasserted, in which case always reseal.
    47  	KernelRevision string   `json:"kernel-revision"`
    48  	KernelCmdlines []string `json:"kernel-cmdlines"`
    49  
    50  	kernelBootFile bootloader.BootFile
    51  }
    52  
    53  func (b *bootChain) modelForSealing() *modelForSealing {
    54  	return &modelForSealing{
    55  		brandID:        b.BrandID,
    56  		model:          b.Model,
    57  		grade:          b.Grade,
    58  		modelSignKeyID: b.ModelSignKeyID,
    59  	}
    60  }
    61  
    62  // TODO:UC20 add a doc comment when this is stabilized
    63  type bootAsset struct {
    64  	Role   bootloader.Role `json:"role"`
    65  	Name   string          `json:"name"`
    66  	Hashes []string        `json:"hashes"`
    67  }
    68  
    69  func bootAssetLess(b, other *bootAsset) bool {
    70  	byRole := b.Role < other.Role
    71  	byName := b.Name < other.Name
    72  	// sort order: role -> name -> hash list (len -> lexical)
    73  	if b.Role != other.Role {
    74  		return byRole
    75  	}
    76  	if b.Name != other.Name {
    77  		return byName
    78  	}
    79  	return stringListsLess(b.Hashes, other.Hashes)
    80  }
    81  
    82  func stringListsEqual(sl1, sl2 []string) bool {
    83  	if len(sl1) != len(sl2) {
    84  		return false
    85  	}
    86  	for i := range sl1 {
    87  		if sl1[i] != sl2[i] {
    88  			return false
    89  		}
    90  	}
    91  	return true
    92  }
    93  
    94  func stringListsLess(sl1, sl2 []string) bool {
    95  	if len(sl1) != len(sl2) {
    96  		return len(sl1) < len(sl2)
    97  	}
    98  	for idx := range sl1 {
    99  		if sl1[idx] < sl2[idx] {
   100  			return true
   101  		}
   102  	}
   103  	return false
   104  }
   105  
   106  func toPredictableBootAsset(b *bootAsset) *bootAsset {
   107  	if b == nil {
   108  		return nil
   109  	}
   110  	newB := *b
   111  	if b.Hashes != nil {
   112  		newB.Hashes = make([]string, len(b.Hashes))
   113  		copy(newB.Hashes, b.Hashes)
   114  		sort.Strings(newB.Hashes)
   115  	}
   116  	return &newB
   117  }
   118  
   119  func toPredictableBootChain(b *bootChain) *bootChain {
   120  	if b == nil {
   121  		return nil
   122  	}
   123  	newB := *b
   124  	if b.AssetChain != nil {
   125  		newB.AssetChain = make([]bootAsset, len(b.AssetChain))
   126  		for i := range b.AssetChain {
   127  			newB.AssetChain[i] = *toPredictableBootAsset(&b.AssetChain[i])
   128  		}
   129  	}
   130  	if b.KernelCmdlines != nil {
   131  		newB.KernelCmdlines = make([]string, len(b.KernelCmdlines))
   132  		copy(newB.KernelCmdlines, b.KernelCmdlines)
   133  		sort.Strings(newB.KernelCmdlines)
   134  	}
   135  	return &newB
   136  }
   137  
   138  func predictableBootAssetsEqual(b1, b2 []bootAsset) bool {
   139  	b1JSON, err := json.Marshal(b1)
   140  	if err != nil {
   141  		return false
   142  	}
   143  	b2JSON, err := json.Marshal(b2)
   144  	if err != nil {
   145  		return false
   146  	}
   147  	return bytes.Equal(b1JSON, b2JSON)
   148  }
   149  
   150  func predictableBootAssetsLess(b1, b2 []bootAsset) bool {
   151  	if len(b1) != len(b2) {
   152  		return len(b1) < len(b2)
   153  	}
   154  	for i := range b1 {
   155  		if bootAssetLess(&b1[i], &b2[i]) {
   156  			return true
   157  		}
   158  	}
   159  	return false
   160  }
   161  
   162  type byBootChainOrder []bootChain
   163  
   164  func (b byBootChainOrder) Len() int      { return len(b) }
   165  func (b byBootChainOrder) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
   166  func (b byBootChainOrder) Less(i, j int) bool {
   167  	// sort by model info
   168  	if b[i].BrandID != b[j].BrandID {
   169  		return b[i].BrandID < b[j].BrandID
   170  	}
   171  	if b[i].Model != b[j].Model {
   172  		return b[i].Model < b[j].Model
   173  	}
   174  	if b[i].Grade != b[j].Grade {
   175  		return b[i].Grade < b[j].Grade
   176  	}
   177  	if b[i].ModelSignKeyID != b[j].ModelSignKeyID {
   178  		return b[i].ModelSignKeyID < b[j].ModelSignKeyID
   179  	}
   180  	// then boot assets
   181  	if !predictableBootAssetsEqual(b[i].AssetChain, b[j].AssetChain) {
   182  		return predictableBootAssetsLess(b[i].AssetChain, b[j].AssetChain)
   183  	}
   184  	// then kernel
   185  	if b[i].Kernel != b[j].Kernel {
   186  		return b[i].Kernel < b[j].Kernel
   187  	}
   188  	if b[i].KernelRevision != b[j].KernelRevision {
   189  		return b[i].KernelRevision < b[j].KernelRevision
   190  	}
   191  	// and last kernel command lines
   192  	if !stringListsEqual(b[i].KernelCmdlines, b[j].KernelCmdlines) {
   193  		return stringListsLess(b[i].KernelCmdlines, b[j].KernelCmdlines)
   194  	}
   195  	return false
   196  }
   197  
   198  type predictableBootChains []bootChain
   199  
   200  // hasUnrevisionedKernels returns true if any of the chains have an
   201  // unrevisioned kernel. Revisions will not be set for unasserted
   202  // kernels.
   203  func (pbc predictableBootChains) hasUnrevisionedKernels() bool {
   204  	for i := range pbc {
   205  		if pbc[i].KernelRevision == "" {
   206  			return true
   207  		}
   208  	}
   209  	return false
   210  }
   211  
   212  func toPredictableBootChains(chains []bootChain) predictableBootChains {
   213  	if chains == nil {
   214  		return nil
   215  	}
   216  	predictableChains := make([]bootChain, len(chains))
   217  	for i := range chains {
   218  		predictableChains[i] = *toPredictableBootChain(&chains[i])
   219  	}
   220  	sort.Sort(byBootChainOrder(predictableChains))
   221  	return predictableChains
   222  }
   223  
   224  type bootChainEquivalence int
   225  
   226  const (
   227  	bootChainEquivalent   bootChainEquivalence = 0
   228  	bootChainDifferent    bootChainEquivalence = 1
   229  	bootChainUnrevisioned bootChainEquivalence = -1
   230  )
   231  
   232  // predictableBootChainsEqualForReseal returns bootChainEquivalent
   233  // when boot chains are equivalent for reseal. If the boot chains
   234  // are clearly different it returns bootChainDifferent.
   235  // If it would return bootChainEquivalent but the chains contain
   236  // unrevisioned kernels it will return bootChainUnrevisioned.
   237  func predictableBootChainsEqualForReseal(pb1, pb2 predictableBootChains) bootChainEquivalence {
   238  	pb1JSON, err := json.Marshal(pb1)
   239  	if err != nil {
   240  		return bootChainDifferent
   241  	}
   242  	pb2JSON, err := json.Marshal(pb2)
   243  	if err != nil {
   244  		return bootChainDifferent
   245  	}
   246  	if bytes.Equal(pb1JSON, pb2JSON) {
   247  		if pb1.hasUnrevisionedKernels() {
   248  			return bootChainUnrevisioned
   249  		}
   250  		return bootChainEquivalent
   251  	}
   252  	return bootChainDifferent
   253  }
   254  
   255  // bootAssetsToLoadChains generates a list of load chains covering given boot
   256  // assets sequence. At the end of each chain, adds an entry for the kernel boot
   257  // file.
   258  func bootAssetsToLoadChains(assets []bootAsset, kernelBootFile bootloader.BootFile, roleToBlName map[bootloader.Role]string) ([]*secboot.LoadChain, error) {
   259  	// kernel is added after all the assets
   260  	addKernelBootFile := len(assets) == 0
   261  	if addKernelBootFile {
   262  		return []*secboot.LoadChain{secboot.NewLoadChain(kernelBootFile)}, nil
   263  	}
   264  
   265  	thisAsset := assets[0]
   266  	blName := roleToBlName[thisAsset.Role]
   267  	if blName == "" {
   268  		return nil, fmt.Errorf("internal error: no bootloader name for boot asset role %q", thisAsset.Role)
   269  	}
   270  	var chains []*secboot.LoadChain
   271  	for _, hash := range thisAsset.Hashes {
   272  		var bf bootloader.BootFile
   273  		var next []*secboot.LoadChain
   274  		var err error
   275  
   276  		p := filepath.Join(
   277  			dirs.SnapBootAssetsDir,
   278  			trustedAssetCacheRelPath(blName, thisAsset.Name, hash))
   279  		if !osutil.FileExists(p) {
   280  			return nil, fmt.Errorf("file %s not found in boot assets cache", p)
   281  		}
   282  		bf = bootloader.NewBootFile(
   283  			"", // asset comes from the filesystem, not a snap
   284  			p,
   285  			thisAsset.Role,
   286  		)
   287  		next, err = bootAssetsToLoadChains(assets[1:], kernelBootFile, roleToBlName)
   288  		if err != nil {
   289  			return nil, err
   290  		}
   291  		chains = append(chains, secboot.NewLoadChain(bf, next...))
   292  	}
   293  	return chains, nil
   294  }
   295  
   296  // predictableBootChainsWrapperForStorage wraps the boot chains so
   297  // that we do not store the arrays directly as JSON and we can add
   298  // other information
   299  type predictableBootChainsWrapperForStorage struct {
   300  	ResealCount int                   `json:"reseal-count"`
   301  	BootChains  predictableBootChains `json:"boot-chains"`
   302  }
   303  
   304  func readBootChains(path string) (pbc predictableBootChains, resealCount int, err error) {
   305  	inf, err := os.Open(path)
   306  	if err != nil {
   307  		if os.IsNotExist(err) {
   308  			return nil, 0, nil
   309  		}
   310  		return nil, 0, fmt.Errorf("cannot open existing boot chains data file: %v", err)
   311  	}
   312  	defer inf.Close()
   313  	var wrapped predictableBootChainsWrapperForStorage
   314  	if err := json.NewDecoder(inf).Decode(&wrapped); err != nil {
   315  		return nil, 0, fmt.Errorf("cannot read boot chains data: %v", err)
   316  	}
   317  	return wrapped.BootChains, wrapped.ResealCount, nil
   318  }
   319  
   320  func writeBootChains(pbc predictableBootChains, path string, resealCount int) error {
   321  	if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
   322  		return fmt.Errorf("cannot create device fde state directory: %v", err)
   323  	}
   324  	outf, err := osutil.NewAtomicFile(path, 0600, 0, osutil.NoChown, osutil.NoChown)
   325  	if err != nil {
   326  		return fmt.Errorf("cannot create a temporary boot chains file: %v", err)
   327  	}
   328  	// becomes noop when the file is committed
   329  	defer outf.Cancel()
   330  
   331  	wrapped := predictableBootChainsWrapperForStorage{
   332  		ResealCount: resealCount,
   333  		BootChains:  pbc,
   334  	}
   335  	if err := json.NewEncoder(outf).Encode(wrapped); err != nil {
   336  		return fmt.Errorf("cannot write boot chains data: %v", err)
   337  	}
   338  	return outf.Commit()
   339  }