github.com/anonymouse64/snapd@v0.0.0-20210824153203-04c4c42d842d/boot/modeenv.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2019-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  	"io"
    27  	"os"
    28  	"path/filepath"
    29  	"reflect"
    30  	"sort"
    31  	"strings"
    32  
    33  	"github.com/mvo5/goconfigparser"
    34  
    35  	"github.com/snapcore/snapd/asserts"
    36  	"github.com/snapcore/snapd/dirs"
    37  	"github.com/snapcore/snapd/osutil"
    38  	"github.com/snapcore/snapd/release"
    39  	"github.com/snapcore/snapd/secboot"
    40  )
    41  
    42  type bootAssetsMap map[string][]string
    43  
    44  // bootCommandLines is a list of kernel command lines. The command lines are
    45  // marshalled as JSON as a comma can be present in the module parameters.
    46  type bootCommandLines []string
    47  
    48  // Modeenv is a file on UC20 that provides additional information
    49  // about the current mode (run,recover,install)
    50  type Modeenv struct {
    51  	Mode           string `key:"mode"`
    52  	RecoverySystem string `key:"recovery_system"`
    53  	// CurrentRecoverySystems is a list of labels corresponding to recovery
    54  	// systems that have been tested or are in the process of being tried,
    55  	// thus only the run key is resealed for these systems.
    56  	CurrentRecoverySystems []string `key:"current_recovery_systems"`
    57  	// GoodRecoverySystems is a list of labels corresponding to recovery
    58  	// systems that were tested and are prepared to use for recovering.
    59  	// The fallback keys are resealed for these systems.
    60  	GoodRecoverySystems []string `key:"good_recovery_systems"`
    61  	Base                string   `key:"base"`
    62  	TryBase             string   `key:"try_base"`
    63  	BaseStatus          string   `key:"base_status"`
    64  	CurrentKernels      []string `key:"current_kernels"`
    65  	// Model, BrandID, Grade, SignKeyID describe the properties of current
    66  	// device model.
    67  	Model          string `key:"model"`
    68  	BrandID        string `key:"model,secondary"`
    69  	Grade          string `key:"grade"`
    70  	ModelSignKeyID string `key:"model_sign_key_id"`
    71  	// TryModel, TryBrandID, TryGrade, TrySignKeyID describe the properties
    72  	// of the candidate model.
    73  	TryModel          string `key:"try_model"`
    74  	TryBrandID        string `key:"try_model,secondary"`
    75  	TryGrade          string `key:"try_grade"`
    76  	TryModelSignKeyID string `key:"try_model_sign_key_id"`
    77  	// BootFlags is the set of boot flags. Whether this applies for the current
    78  	// or next boot is not indicated in the modeenv. When the modeenv is read in
    79  	// the initramfs these flags apply to the current boot and are copied into
    80  	// a file in /run that userspace should read instead of reading from this
    81  	// key. When setting boot flags for the next boot, then this key will be
    82  	// written to and used by the initramfs after rebooting.
    83  	BootFlags []string `key:"boot_flags"`
    84  	// CurrentTrustedBootAssets is a map of a run bootloader's asset names to
    85  	// a list of hashes of the asset contents. Typically the first entry in
    86  	// the list is a hash of an asset the system currently boots with (or is
    87  	// expected to have booted with). The second entry, if present, is the
    88  	// hash of an entry added when an update of the asset was being applied
    89  	// and will become the sole entry after a successful boot.
    90  	CurrentTrustedBootAssets bootAssetsMap `key:"current_trusted_boot_assets"`
    91  	// CurrentTrustedRecoveryBootAssetsMap is a map of a recovery bootloader's
    92  	// asset names to a list of hashes of the asset contents. Used similarly
    93  	// to CurrentTrustedBootAssets.
    94  	CurrentTrustedRecoveryBootAssets bootAssetsMap `key:"current_trusted_recovery_boot_assets"`
    95  	// CurrentKernelCommandLines is a list of the expected kernel command
    96  	// lines when booting into run mode. It will typically only be one
    97  	// element for normal operations, but may contain two elements during
    98  	// update scenarios.
    99  	CurrentKernelCommandLines bootCommandLines `key:"current_kernel_command_lines"`
   100  	// TODO:UC20 add a per recovery system list of kernel command lines
   101  
   102  	// read is set to true when a modenv was read successfully
   103  	read bool
   104  
   105  	// originRootdir is set to the root whence the modeenv was
   106  	// read from, and where it will be written back to
   107  	originRootdir string
   108  
   109  	// extrakeys is all the keys in the modeenv we read from the file but don't
   110  	// understand, we keep track of this so that if we read a new modeenv with
   111  	// extra keys and need to rewrite it, we will write those new keys as well
   112  	extrakeys map[string]string
   113  }
   114  
   115  var modeenvKnownKeys = make(map[string]bool)
   116  
   117  func init() {
   118  	st := reflect.TypeOf(Modeenv{})
   119  	num := st.NumField()
   120  	for i := 0; i < num; i++ {
   121  		f := st.Field(i)
   122  		if f.PkgPath != "" {
   123  			// unexported
   124  			continue
   125  		}
   126  		key := f.Tag.Get("key")
   127  		if key == "" {
   128  			panic(fmt.Sprintf("modeenv %s field has no key tag", f.Name))
   129  		}
   130  		const secondaryModifier = ",secondary"
   131  		if strings.HasSuffix(key, secondaryModifier) {
   132  			// secondary field in a group fields
   133  			// corresponding to one file key
   134  			key := key[:len(key)-len(secondaryModifier)]
   135  			if !modeenvKnownKeys[key] {
   136  				panic(fmt.Sprintf("modeenv %s field marked as secondary for not yet defined key %q", f.Name, key))
   137  			}
   138  			continue
   139  		}
   140  		if modeenvKnownKeys[key] {
   141  			panic(fmt.Sprintf("modeenv key %q repeated on %s", key, f.Name))
   142  		}
   143  		modeenvKnownKeys[key] = true
   144  	}
   145  }
   146  
   147  func modeenvFile(rootdir string) string {
   148  	if rootdir == "" {
   149  		rootdir = dirs.GlobalRootDir
   150  	}
   151  	return dirs.SnapModeenvFileUnder(rootdir)
   152  }
   153  
   154  // ReadModeenv attempts to read the modeenv file at
   155  // <rootdir>/var/iib/snapd/modeenv.
   156  func ReadModeenv(rootdir string) (*Modeenv, error) {
   157  	modeenvPath := modeenvFile(rootdir)
   158  	cfg := goconfigparser.New()
   159  	cfg.AllowNoSectionHeader = true
   160  	if err := cfg.ReadFile(modeenvPath); err != nil {
   161  		return nil, err
   162  	}
   163  
   164  	// TODO:UC20: should we check these errors and try to do something?
   165  	m := Modeenv{
   166  		read:          true,
   167  		originRootdir: rootdir,
   168  		extrakeys:     make(map[string]string),
   169  	}
   170  	unmarshalModeenvValueFromCfg(cfg, "recovery_system", &m.RecoverySystem)
   171  	unmarshalModeenvValueFromCfg(cfg, "current_recovery_systems", &m.CurrentRecoverySystems)
   172  	unmarshalModeenvValueFromCfg(cfg, "good_recovery_systems", &m.GoodRecoverySystems)
   173  	unmarshalModeenvValueFromCfg(cfg, "boot_flags", &m.BootFlags)
   174  
   175  	unmarshalModeenvValueFromCfg(cfg, "mode", &m.Mode)
   176  	if m.Mode == "" {
   177  		return nil, fmt.Errorf("internal error: mode is unset")
   178  	}
   179  	unmarshalModeenvValueFromCfg(cfg, "base", &m.Base)
   180  	unmarshalModeenvValueFromCfg(cfg, "base_status", &m.BaseStatus)
   181  	unmarshalModeenvValueFromCfg(cfg, "try_base", &m.TryBase)
   182  
   183  	// current_kernels is a comma-delimited list in a string
   184  	unmarshalModeenvValueFromCfg(cfg, "current_kernels", &m.CurrentKernels)
   185  	var bm modeenvModel
   186  	unmarshalModeenvValueFromCfg(cfg, "model", &bm)
   187  	m.BrandID = bm.brandID
   188  	m.Model = bm.model
   189  	// expect the caller to validate the grade
   190  	unmarshalModeenvValueFromCfg(cfg, "grade", &m.Grade)
   191  	unmarshalModeenvValueFromCfg(cfg, "model_sign_key_id", &m.ModelSignKeyID)
   192  	var tryBm modeenvModel
   193  	unmarshalModeenvValueFromCfg(cfg, "try_model", &tryBm)
   194  	m.TryBrandID = tryBm.brandID
   195  	m.TryModel = tryBm.model
   196  	unmarshalModeenvValueFromCfg(cfg, "try_grade", &m.TryGrade)
   197  	unmarshalModeenvValueFromCfg(cfg, "try_model_sign_key_id", &m.TryModelSignKeyID)
   198  
   199  	unmarshalModeenvValueFromCfg(cfg, "current_trusted_boot_assets", &m.CurrentTrustedBootAssets)
   200  	unmarshalModeenvValueFromCfg(cfg, "current_trusted_recovery_boot_assets", &m.CurrentTrustedRecoveryBootAssets)
   201  	unmarshalModeenvValueFromCfg(cfg, "current_kernel_command_lines", &m.CurrentKernelCommandLines)
   202  
   203  	// save all the rest of the keys we don't understand
   204  	keys, err := cfg.Options("")
   205  	if err != nil {
   206  		return nil, err
   207  	}
   208  	for _, k := range keys {
   209  		if !modeenvKnownKeys[k] {
   210  			val, err := cfg.Get("", k)
   211  			if err != nil {
   212  				return nil, err
   213  			}
   214  			m.extrakeys[k] = val
   215  		}
   216  	}
   217  
   218  	return &m, nil
   219  }
   220  
   221  // deepEqual compares two modeenvs to ensure they are textually the same. It
   222  // does not consider whether the modeenvs were read from disk or created purely
   223  // in memory. It also does not sort or otherwise mutate any sub-objects,
   224  // performing simple strict verification of sub-objects.
   225  func (m *Modeenv) deepEqual(m2 *Modeenv) bool {
   226  	b, err := json.Marshal(m)
   227  	if err != nil {
   228  		return false
   229  	}
   230  	b2, err := json.Marshal(m2)
   231  	if err != nil {
   232  		return false
   233  	}
   234  	return bytes.Equal(b, b2)
   235  }
   236  
   237  // Copy will make a deep copy of a Modeenv.
   238  func (m *Modeenv) Copy() (*Modeenv, error) {
   239  	// to avoid hard-coding all fields here and manually copying everything, we
   240  	// take the easy way out and serialize to json then re-import into a
   241  	// empty Modeenv
   242  	b, err := json.Marshal(m)
   243  	if err != nil {
   244  		return nil, err
   245  	}
   246  	m2 := &Modeenv{}
   247  	err = json.Unmarshal(b, m2)
   248  	if err != nil {
   249  		return nil, err
   250  	}
   251  
   252  	// manually copy the unexported fields as they won't be in the JSON
   253  	m2.read = m.read
   254  	m2.originRootdir = m.originRootdir
   255  	return m2, nil
   256  }
   257  
   258  // Write outputs the modeenv to the file where it was read, only valid on
   259  // modeenv that has been read.
   260  func (m *Modeenv) Write() error {
   261  	if m.read {
   262  		return m.WriteTo(m.originRootdir)
   263  	}
   264  	return fmt.Errorf("internal error: must use WriteTo with modeenv not read from disk")
   265  }
   266  
   267  // WriteTo outputs the modeenv to the file at <rootdir>/var/lib/snapd/modeenv.
   268  func (m *Modeenv) WriteTo(rootdir string) error {
   269  	modeenvPath := modeenvFile(rootdir)
   270  
   271  	if err := os.MkdirAll(filepath.Dir(modeenvPath), 0755); err != nil {
   272  		return err
   273  	}
   274  	buf := bytes.NewBuffer(nil)
   275  	if m.Mode == "" {
   276  		return fmt.Errorf("internal error: mode is unset")
   277  	}
   278  	marshalModeenvEntryTo(buf, "mode", m.Mode)
   279  	marshalModeenvEntryTo(buf, "recovery_system", m.RecoverySystem)
   280  	marshalModeenvEntryTo(buf, "current_recovery_systems", m.CurrentRecoverySystems)
   281  	marshalModeenvEntryTo(buf, "good_recovery_systems", m.GoodRecoverySystems)
   282  	marshalModeenvEntryTo(buf, "boot_flags", m.BootFlags)
   283  	marshalModeenvEntryTo(buf, "base", m.Base)
   284  	marshalModeenvEntryTo(buf, "try_base", m.TryBase)
   285  	marshalModeenvEntryTo(buf, "base_status", m.BaseStatus)
   286  	marshalModeenvEntryTo(buf, "current_kernels", strings.Join(m.CurrentKernels, ","))
   287  	if m.Model != "" || m.Grade != "" {
   288  		if m.Model == "" {
   289  			return fmt.Errorf("internal error: model is unset")
   290  		}
   291  		if m.BrandID == "" {
   292  			return fmt.Errorf("internal error: brand is unset")
   293  		}
   294  		marshalModeenvEntryTo(buf, "model", &modeenvModel{brandID: m.BrandID, model: m.Model})
   295  	}
   296  	// TODO: complain when grade or key are unset
   297  	marshalModeenvEntryTo(buf, "grade", m.Grade)
   298  	marshalModeenvEntryTo(buf, "model_sign_key_id", m.ModelSignKeyID)
   299  	if m.TryModel != "" || m.TryGrade != "" {
   300  		if m.TryModel == "" {
   301  			return fmt.Errorf("internal error: try model is unset")
   302  		}
   303  		if m.TryBrandID == "" {
   304  			return fmt.Errorf("internal error: try brand is unset")
   305  		}
   306  		marshalModeenvEntryTo(buf, "try_model", &modeenvModel{brandID: m.TryBrandID, model: m.TryModel})
   307  	}
   308  	marshalModeenvEntryTo(buf, "try_grade", m.TryGrade)
   309  	marshalModeenvEntryTo(buf, "try_model_sign_key_id", m.TryModelSignKeyID)
   310  	marshalModeenvEntryTo(buf, "current_trusted_boot_assets", m.CurrentTrustedBootAssets)
   311  	marshalModeenvEntryTo(buf, "current_trusted_recovery_boot_assets", m.CurrentTrustedRecoveryBootAssets)
   312  	marshalModeenvEntryTo(buf, "current_kernel_command_lines", m.CurrentKernelCommandLines)
   313  
   314  	// write all the extra keys at the end
   315  	// sort them for test convenience
   316  	extraKeys := make([]string, 0, len(m.extrakeys))
   317  	for k := range m.extrakeys {
   318  		extraKeys = append(extraKeys, k)
   319  	}
   320  	sort.Strings(extraKeys)
   321  	for _, k := range extraKeys {
   322  		marshalModeenvEntryTo(buf, k, m.extrakeys[k])
   323  	}
   324  
   325  	if err := osutil.AtomicWriteFile(modeenvPath, buf.Bytes(), 0644, 0); err != nil {
   326  		return err
   327  	}
   328  	return nil
   329  }
   330  
   331  // modelForSealing is a helper type that implements
   332  // github.com/snapcore/secboot.SnapModel interface.
   333  type modelForSealing struct {
   334  	brandID        string
   335  	model          string
   336  	grade          asserts.ModelGrade
   337  	modelSignKeyID string
   338  }
   339  
   340  // dummy to verify interface match
   341  var _ secboot.ModelForSealing = (*modelForSealing)(nil)
   342  
   343  func (m *modelForSealing) BrandID() string           { return m.brandID }
   344  func (m *modelForSealing) SignKeyID() string         { return m.modelSignKeyID }
   345  func (m *modelForSealing) Model() string             { return m.model }
   346  func (m *modelForSealing) Grade() asserts.ModelGrade { return m.grade }
   347  func (m *modelForSealing) Series() string            { return release.Series }
   348  
   349  // modelUniqueID returns a unique ID which can be used as a map index of the
   350  // provided model.
   351  func modelUniqueID(m secboot.ModelForSealing) string {
   352  	return fmt.Sprintf("%s/%s,%s,%s", m.BrandID(), m.Model(), m.Grade(), m.SignKeyID())
   353  }
   354  
   355  // ModelForSealing returns a wrapper implementing
   356  // github.com/snapcore/secboot.SnapModel interface which describes the current
   357  // model.
   358  func (m *Modeenv) ModelForSealing() secboot.ModelForSealing {
   359  	return &modelForSealing{
   360  		brandID:        m.BrandID,
   361  		model:          m.Model,
   362  		grade:          asserts.ModelGrade(m.Grade),
   363  		modelSignKeyID: m.ModelSignKeyID,
   364  	}
   365  }
   366  
   367  // TryModelForSealing returns a wrapper implementing
   368  // github.com/snapcore/secboot.SnapModel interface which describes the candidate
   369  // or try model.
   370  func (m *Modeenv) TryModelForSealing() secboot.ModelForSealing {
   371  	return &modelForSealing{
   372  		brandID:        m.TryBrandID,
   373  		model:          m.TryModel,
   374  		grade:          asserts.ModelGrade(m.TryGrade),
   375  		modelSignKeyID: m.TryModelSignKeyID,
   376  	}
   377  }
   378  
   379  func (m *Modeenv) setModel(model *asserts.Model) {
   380  	m.Model = model.Model()
   381  	m.BrandID = model.BrandID()
   382  	m.Grade = string(model.Grade())
   383  	m.ModelSignKeyID = model.SignKeyID()
   384  }
   385  
   386  func (m *Modeenv) setTryModel(model *asserts.Model) {
   387  	m.TryModel = model.Model()
   388  	m.TryBrandID = model.BrandID()
   389  	m.TryGrade = string(model.Grade())
   390  	m.TryModelSignKeyID = model.SignKeyID()
   391  }
   392  
   393  func (m *Modeenv) clearTryModel() {
   394  	m.TryModel = ""
   395  	m.TryBrandID = ""
   396  	m.TryGrade = ""
   397  	m.TryModelSignKeyID = ""
   398  }
   399  
   400  type modeenvValueMarshaller interface {
   401  	MarshalModeenvValue() (string, error)
   402  }
   403  
   404  type modeenvValueUnmarshaller interface {
   405  	UnmarshalModeenvValue(value string) error
   406  }
   407  
   408  // marshalModeenvEntryTo marshals to out what as value for an entry
   409  // with the given key. If what is empty this is a no-op.
   410  func marshalModeenvEntryTo(out io.Writer, key string, what interface{}) error {
   411  	var asString string
   412  	switch v := what.(type) {
   413  	case string:
   414  		if v == "" {
   415  			return nil
   416  		}
   417  		asString = v
   418  	case []string:
   419  		if len(v) == 0 {
   420  			return nil
   421  		}
   422  		asString = asModeenvStringList(v)
   423  	default:
   424  		if vm, ok := what.(modeenvValueMarshaller); ok {
   425  			marshalled, err := vm.MarshalModeenvValue()
   426  			if err != nil {
   427  				return fmt.Errorf("cannot marshal value for key %q: %v", key, err)
   428  			}
   429  			asString = marshalled
   430  		} else if jm, ok := what.(json.Marshaler); ok {
   431  			marshalled, err := jm.MarshalJSON()
   432  			if err != nil {
   433  				return fmt.Errorf("cannot marshal value for key %q as JSON: %v", key, err)
   434  			}
   435  			asString = string(marshalled)
   436  			if asString == "null" {
   437  				//  no need to keep nulls in the modeenv
   438  				return nil
   439  			}
   440  		} else {
   441  			return fmt.Errorf("internal error: cannot marshal unsupported type %T value %v for key %q", what, what, key)
   442  		}
   443  	}
   444  	_, err := fmt.Fprintf(out, "%s=%s\n", key, asString)
   445  	return err
   446  }
   447  
   448  // unmarshalModeenvValueFromCfg unmarshals the value of the entry with
   449  // th given key to dest. If there's no such entry dest might be left
   450  // empty.
   451  func unmarshalModeenvValueFromCfg(cfg *goconfigparser.ConfigParser, key string, dest interface{}) error {
   452  	if dest == nil {
   453  		return fmt.Errorf("internal error: cannot unmarshal to nil")
   454  	}
   455  	kv, _ := cfg.Get("", key)
   456  
   457  	switch v := dest.(type) {
   458  	case *string:
   459  		*v = kv
   460  	case *[]string:
   461  		*v = splitModeenvStringList(kv)
   462  	default:
   463  		if vm, ok := v.(modeenvValueUnmarshaller); ok {
   464  			if err := vm.UnmarshalModeenvValue(kv); err != nil {
   465  				return fmt.Errorf("cannot unmarshal modeenv value %q to %T: %v", kv, dest, err)
   466  			}
   467  			return nil
   468  		} else if jm, ok := v.(json.Unmarshaler); ok {
   469  			if len(kv) == 0 {
   470  				// leave jm empty
   471  				return nil
   472  			}
   473  			if err := jm.UnmarshalJSON([]byte(kv)); err != nil {
   474  				return fmt.Errorf("cannot unmarshal modeenv value %q as JSON to %T: %v", kv, dest, err)
   475  			}
   476  			return nil
   477  		}
   478  		return fmt.Errorf("internal error: cannot unmarshal value %q for unsupported type %T", kv, dest)
   479  	}
   480  	return nil
   481  }
   482  
   483  func splitModeenvStringList(v string) []string {
   484  	if v == "" {
   485  		return nil
   486  	}
   487  	split := strings.Split(v, ",")
   488  	// drop empty strings
   489  	nonEmpty := make([]string, 0, len(split))
   490  	for _, one := range split {
   491  		if one != "" {
   492  			nonEmpty = append(nonEmpty, one)
   493  		}
   494  	}
   495  	if len(nonEmpty) == 0 {
   496  		return nil
   497  	}
   498  	return nonEmpty
   499  }
   500  
   501  func asModeenvStringList(v []string) string {
   502  	return strings.Join(v, ",")
   503  }
   504  
   505  type modeenvModel struct {
   506  	brandID, model string
   507  }
   508  
   509  func (m *modeenvModel) MarshalModeenvValue() (string, error) {
   510  	return fmt.Sprintf("%s/%s", m.brandID, m.model), nil
   511  }
   512  
   513  func (m *modeenvModel) UnmarshalModeenvValue(brandSlashModel string) error {
   514  	if bsmSplit := strings.SplitN(brandSlashModel, "/", 2); len(bsmSplit) == 2 {
   515  		if bsmSplit[0] != "" && bsmSplit[1] != "" {
   516  			m.brandID = bsmSplit[0]
   517  			m.model = bsmSplit[1]
   518  		}
   519  	}
   520  	return nil
   521  }
   522  
   523  func (b bootAssetsMap) MarshalJSON() ([]byte, error) {
   524  	asMap := map[string][]string(b)
   525  	return json.Marshal(asMap)
   526  }
   527  
   528  func (b *bootAssetsMap) UnmarshalJSON(data []byte) error {
   529  	var asMap map[string][]string
   530  	if err := json.Unmarshal(data, &asMap); err != nil {
   531  		return err
   532  	}
   533  	*b = bootAssetsMap(asMap)
   534  	return nil
   535  }
   536  
   537  func (s bootCommandLines) MarshalJSON() ([]byte, error) {
   538  	return json.Marshal([]string(s))
   539  }
   540  
   541  func (s *bootCommandLines) UnmarshalJSON(data []byte) error {
   542  	var asList []string
   543  	if err := json.Unmarshal(data, &asList); err != nil {
   544  		return err
   545  	}
   546  	*s = bootCommandLines(asList)
   547  	return nil
   548  }