mirror of
https://github.com/token2/snapd.git
synced 2026-03-13 11:15:47 -07:00
2051 lines
66 KiB
Go
2051 lines
66 KiB
Go
// -*- Mode: Go; indent-tabs-mode: t -*-
|
|
|
|
/*
|
|
* Copyright (C) 2021 Canonical Ltd
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License version 3 as
|
|
* published by the Free Software Foundation.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
package boot_test
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
. "gopkg.in/check.v1"
|
|
|
|
"github.com/snapcore/snapd/asserts"
|
|
"github.com/snapcore/snapd/boot"
|
|
"github.com/snapcore/snapd/boot/boottest"
|
|
"github.com/snapcore/snapd/bootloader"
|
|
"github.com/snapcore/snapd/bootloader/bootloadertest"
|
|
"github.com/snapcore/snapd/secboot"
|
|
"github.com/snapcore/snapd/seed"
|
|
"github.com/snapcore/snapd/snap"
|
|
"github.com/snapcore/snapd/testutil"
|
|
"github.com/snapcore/snapd/timings"
|
|
)
|
|
|
|
type baseSystemsSuite struct {
|
|
baseBootenvSuite
|
|
}
|
|
|
|
func (s *baseSystemsSuite) SetUpTest(c *C) {
|
|
s.baseBootenvSuite.SetUpTest(c)
|
|
c.Assert(os.MkdirAll(boot.InitramfsUbuntuBootDir, 0755), IsNil)
|
|
c.Assert(os.MkdirAll(boot.InitramfsUbuntuSeedDir, 0755), IsNil)
|
|
}
|
|
|
|
type systemsSuite struct {
|
|
baseSystemsSuite
|
|
|
|
uc20dev boot.Device
|
|
|
|
runKernelBf bootloader.BootFile
|
|
recoveryKernelBf bootloader.BootFile
|
|
seedKernelSnap *seed.Snap
|
|
seedGadgetSnap *seed.Snap
|
|
}
|
|
|
|
var _ = Suite(&systemsSuite{})
|
|
|
|
func (s *systemsSuite) mockTrustedBootloaderWithAssetAndChains(c *C, runKernelBf, recoveryKernelBf bootloader.BootFile) *bootloadertest.MockTrustedAssetsBootloader {
|
|
mockAssetsCache(c, s.rootdir, "trusted", []string{
|
|
"asset-asset-hash-1",
|
|
})
|
|
|
|
mtbl := bootloadertest.Mock("trusted", s.bootdir).WithTrustedAssets()
|
|
mtbl.TrustedAssetsList = []string{"asset-1"}
|
|
mtbl.StaticCommandLine = "static cmdline"
|
|
mtbl.BootChainList = []bootloader.BootFile{
|
|
bootloader.NewBootFile("", "asset", bootloader.RoleRunMode),
|
|
runKernelBf,
|
|
}
|
|
mtbl.RecoveryBootChainList = []bootloader.BootFile{
|
|
bootloader.NewBootFile("", "asset", bootloader.RoleRecovery),
|
|
recoveryKernelBf,
|
|
}
|
|
bootloader.Force(mtbl)
|
|
return mtbl
|
|
}
|
|
|
|
func (s *systemsSuite) SetUpTest(c *C) {
|
|
s.baseBootenvSuite.SetUpTest(c)
|
|
|
|
restore := boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error { return nil })
|
|
s.AddCleanup(restore)
|
|
|
|
s.uc20dev = boottest.MockUC20Device("", nil)
|
|
|
|
// run kernel
|
|
s.runKernelBf = bootloader.NewBootFile("/var/lib/snapd/snap/pc-kernel_500.snap",
|
|
"kernel.efi", bootloader.RoleRunMode)
|
|
// seed (recovery) kernel
|
|
s.recoveryKernelBf = bootloader.NewBootFile("/var/lib/snapd/seed/snaps/pc-kernel_1.snap",
|
|
"kernel.efi", bootloader.RoleRecovery)
|
|
|
|
s.seedKernelSnap = mockKernelSeedSnap(snap.R(1))
|
|
s.seedGadgetSnap = mockGadgetSeedSnap(c, nil)
|
|
}
|
|
|
|
func (s *systemsSuite) TestSetTryRecoverySystemEncrypted(c *C) {
|
|
mockAssetsCache(c, s.rootdir, "trusted", []string{
|
|
"asset-asset-hash-1",
|
|
})
|
|
|
|
mtbl := s.mockTrustedBootloaderWithAssetAndChains(c, s.runKernelBf, s.recoveryKernelBf)
|
|
bootloader.Force(mtbl)
|
|
defer bootloader.Force(nil)
|
|
|
|
// system is encrypted
|
|
s.stampSealedKeys(c, s.rootdir)
|
|
|
|
model := s.uc20dev.Model()
|
|
|
|
modeenv := &boot.Modeenv{
|
|
Mode: "run",
|
|
// keep this comment to make old gofmt happy
|
|
CurrentRecoverySystems: []string{"20200825"},
|
|
GoodRecoverySystems: []string{"20200825"},
|
|
CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{
|
|
"asset": []string{"asset-hash-1"},
|
|
},
|
|
CurrentTrustedBootAssets: boot.BootAssetsMap{
|
|
"asset": []string{"asset-hash-1"},
|
|
},
|
|
CurrentKernels: []string{"pc-kernel_500.snap"},
|
|
|
|
Model: model.Model(),
|
|
BrandID: model.BrandID(),
|
|
Grade: string(model.Grade()),
|
|
ModelSignKeyID: model.SignKeyID(),
|
|
}
|
|
c.Assert(modeenv.WriteTo(""), IsNil)
|
|
|
|
var readSeedSeenLabels []string
|
|
restore := boot.MockSeedReadSystemEssential(func(seedDir, label string, essentialTypes []snap.Type, tm timings.Measurer) (*asserts.Model, []*seed.Snap, error) {
|
|
// the mock bootloader can only mock a single recovery boot
|
|
// chain, so pretend both seeds use the same kernel, but keep track of the labels
|
|
readSeedSeenLabels = append(readSeedSeenLabels, label)
|
|
return model, []*seed.Snap{s.seedKernelSnap, s.seedGadgetSnap}, nil
|
|
})
|
|
defer restore()
|
|
|
|
resealCalls := 0
|
|
restore = boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error {
|
|
resealCalls++
|
|
// bootloader variables have already been modified
|
|
c.Check(mtbl.SetBootVarsCalls, Equals, 1)
|
|
c.Assert(params, NotNil)
|
|
c.Assert(params.ModelParams, HasLen, 1)
|
|
switch resealCalls {
|
|
case 1:
|
|
c.Check(params.KeyFiles, DeepEquals, []string{
|
|
filepath.Join(boot.InitramfsBootEncryptionKeyDir, "ubuntu-data.sealed-key"),
|
|
})
|
|
c.Assert(params.ModelParams[0].KernelCmdlines, DeepEquals, []string{
|
|
"snapd_recovery_mode=recover snapd_recovery_system=1234 static cmdline",
|
|
"snapd_recovery_mode=recover snapd_recovery_system=20200825 static cmdline",
|
|
"snapd_recovery_mode=run static cmdline",
|
|
})
|
|
return nil
|
|
case 2:
|
|
c.Check(params.KeyFiles, DeepEquals, []string{
|
|
filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-data.recovery.sealed-key"),
|
|
filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-save.recovery.sealed-key"),
|
|
})
|
|
c.Assert(params.ModelParams[0].KernelCmdlines, DeepEquals, []string{
|
|
"snapd_recovery_mode=recover snapd_recovery_system=20200825 static cmdline",
|
|
})
|
|
return nil
|
|
default:
|
|
c.Errorf("unexpected call to secboot.ResealKeys with count %v", resealCalls)
|
|
return fmt.Errorf("unexpected call")
|
|
}
|
|
})
|
|
defer restore()
|
|
|
|
err := boot.SetTryRecoverySystem(s.uc20dev, "1234")
|
|
c.Assert(err, IsNil)
|
|
|
|
vars, err := mtbl.GetBootVars("try_recovery_system", "recovery_system_status")
|
|
c.Assert(err, IsNil)
|
|
c.Check(vars, DeepEquals, map[string]string{
|
|
"try_recovery_system": "1234",
|
|
"recovery_system_status": "try",
|
|
})
|
|
// run and recovery keys
|
|
c.Check(resealCalls, Equals, 2)
|
|
c.Check(readSeedSeenLabels, DeepEquals, []string{
|
|
"20200825", "1234", // current recovery systems for run key
|
|
"20200825", // good recovery systems for recovery keys
|
|
})
|
|
|
|
modeenvRead, err := boot.ReadModeenv("")
|
|
c.Assert(err, IsNil)
|
|
c.Check(modeenvRead.DeepEqual(&boot.Modeenv{
|
|
Mode: "run",
|
|
// keep this comment to make old gofmt happy
|
|
CurrentRecoverySystems: []string{"20200825", "1234"},
|
|
GoodRecoverySystems: []string{"20200825"},
|
|
CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{
|
|
"asset": []string{"asset-hash-1"},
|
|
},
|
|
CurrentTrustedBootAssets: boot.BootAssetsMap{
|
|
"asset": []string{"asset-hash-1"},
|
|
},
|
|
CurrentKernels: []string{"pc-kernel_500.snap"},
|
|
|
|
Model: model.Model(),
|
|
BrandID: model.BrandID(),
|
|
Grade: string(model.Grade()),
|
|
ModelSignKeyID: model.SignKeyID(),
|
|
}), Equals, true)
|
|
}
|
|
|
|
func (s *systemsSuite) TestSetTryRecoverySystemRemodelEncrypted(c *C) {
|
|
mockAssetsCache(c, s.rootdir, "trusted", []string{
|
|
"asset-asset-hash-1",
|
|
})
|
|
|
|
mtbl := s.mockTrustedBootloaderWithAssetAndChains(c, s.runKernelBf, s.recoveryKernelBf)
|
|
bootloader.Force(mtbl)
|
|
defer bootloader.Force(nil)
|
|
|
|
// system is encrypted
|
|
s.stampSealedKeys(c, s.rootdir)
|
|
|
|
model := s.uc20dev.Model()
|
|
newModel := boottest.MakeMockUC20Model(map[string]interface{}{
|
|
"model": "my-new-model",
|
|
})
|
|
|
|
modeenv := &boot.Modeenv{
|
|
Mode: "run",
|
|
// keep this comment to make old gofmt happy
|
|
CurrentRecoverySystems: []string{"20200825"},
|
|
GoodRecoverySystems: []string{"20200825"},
|
|
CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{
|
|
"asset": []string{"asset-hash-1"},
|
|
},
|
|
CurrentTrustedBootAssets: boot.BootAssetsMap{
|
|
"asset": []string{"asset-hash-1"},
|
|
},
|
|
CurrentKernels: []string{"pc-kernel_500.snap"},
|
|
|
|
Model: model.Model(),
|
|
BrandID: model.BrandID(),
|
|
Grade: string(model.Grade()),
|
|
ModelSignKeyID: model.SignKeyID(),
|
|
}
|
|
c.Assert(modeenv.WriteTo(""), IsNil)
|
|
|
|
var readSeedSeenLabels []string
|
|
restore := boot.MockSeedReadSystemEssential(func(seedDir, label string, essentialTypes []snap.Type, tm timings.Measurer) (*asserts.Model, []*seed.Snap, error) {
|
|
// the mock bootloader can only mock a single recovery boot
|
|
// chain, so pretend both seeds use the same kernel, but keep track of the labels
|
|
readSeedSeenLabels = append(readSeedSeenLabels, label)
|
|
systemModel := model
|
|
if label == "1234" {
|
|
systemModel = newModel
|
|
}
|
|
return systemModel, []*seed.Snap{s.seedKernelSnap, s.seedGadgetSnap}, nil
|
|
})
|
|
defer restore()
|
|
|
|
resealCalls := 0
|
|
restore = boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error {
|
|
resealCalls++
|
|
// bootloader variables have already been modified
|
|
c.Check(mtbl.SetBootVarsCalls, Equals, 1)
|
|
c.Assert(params, NotNil)
|
|
switch resealCalls {
|
|
case 1:
|
|
c.Assert(params.ModelParams, HasLen, 2)
|
|
c.Check(params.KeyFiles, DeepEquals, []string{
|
|
filepath.Join(boot.InitramfsBootEncryptionKeyDir, "ubuntu-data.sealed-key"),
|
|
})
|
|
c.Assert(params.ModelParams[0].KernelCmdlines, DeepEquals, []string{
|
|
"snapd_recovery_mode=recover snapd_recovery_system=20200825 static cmdline",
|
|
"snapd_recovery_mode=run static cmdline",
|
|
})
|
|
c.Assert(params.ModelParams[1].KernelCmdlines, DeepEquals, []string{
|
|
"snapd_recovery_mode=recover snapd_recovery_system=1234 static cmdline",
|
|
"snapd_recovery_mode=run static cmdline",
|
|
})
|
|
return nil
|
|
case 2:
|
|
c.Assert(params.ModelParams, HasLen, 1)
|
|
c.Check(params.KeyFiles, DeepEquals, []string{
|
|
filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-data.recovery.sealed-key"),
|
|
filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-save.recovery.sealed-key"),
|
|
})
|
|
c.Assert(params.ModelParams[0].KernelCmdlines, DeepEquals, []string{
|
|
"snapd_recovery_mode=recover snapd_recovery_system=20200825 static cmdline",
|
|
})
|
|
return nil
|
|
default:
|
|
c.Errorf("unexpected call to secboot.ResealKeys with count %v", resealCalls)
|
|
return fmt.Errorf("unexpected call")
|
|
}
|
|
})
|
|
defer restore()
|
|
|
|
// a remodel will pass the new device
|
|
newUC20Device := boottest.MockUC20Device("run", newModel)
|
|
err := boot.SetTryRecoverySystem(newUC20Device, "1234")
|
|
c.Assert(err, IsNil)
|
|
|
|
vars, err := mtbl.GetBootVars("try_recovery_system", "recovery_system_status")
|
|
c.Assert(err, IsNil)
|
|
c.Check(vars, DeepEquals, map[string]string{
|
|
"try_recovery_system": "1234",
|
|
"recovery_system_status": "try",
|
|
})
|
|
// run and recovery keys
|
|
c.Check(resealCalls, Equals, 2)
|
|
c.Check(readSeedSeenLabels, DeepEquals, []string{
|
|
"20200825", "1234", // current recovery systems for run key and current model from modeenv
|
|
"20200825", "1234", // current recovery systems for run key and try model from modeenv
|
|
"20200825", // good recovery systems for recovery keys
|
|
})
|
|
|
|
modeenvRead, err := boot.ReadModeenv("")
|
|
c.Assert(err, IsNil)
|
|
c.Check(modeenvRead, testutil.JsonEquals, boot.Modeenv{
|
|
Mode: "run",
|
|
// keep this comment to make old gofmt happy
|
|
CurrentRecoverySystems: []string{"20200825", "1234"},
|
|
GoodRecoverySystems: []string{"20200825"},
|
|
CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{
|
|
"asset": []string{"asset-hash-1"},
|
|
},
|
|
CurrentTrustedBootAssets: boot.BootAssetsMap{
|
|
"asset": []string{"asset-hash-1"},
|
|
},
|
|
CurrentKernels: []string{"pc-kernel_500.snap"},
|
|
|
|
Model: model.Model(),
|
|
BrandID: model.BrandID(),
|
|
Grade: string(model.Grade()),
|
|
ModelSignKeyID: model.SignKeyID(),
|
|
|
|
TryModel: newModel.Model(),
|
|
TryBrandID: newModel.BrandID(),
|
|
TryGrade: string(newModel.Grade()),
|
|
TryModelSignKeyID: newModel.SignKeyID(),
|
|
})
|
|
}
|
|
|
|
func (s *systemsSuite) TestSetTryRecoverySystemSimple(c *C) {
|
|
mtbl := bootloadertest.Mock("trusted", s.bootdir).WithTrustedAssets()
|
|
bootloader.Force(mtbl)
|
|
defer bootloader.Force(nil)
|
|
|
|
model := s.uc20dev.Model()
|
|
modeenv := &boot.Modeenv{
|
|
Mode: "run",
|
|
// keep this comment to make old gofmt happy
|
|
CurrentRecoverySystems: []string{"20200825"},
|
|
|
|
Model: model.Model(),
|
|
BrandID: model.BrandID(),
|
|
Grade: string(model.Grade()),
|
|
ModelSignKeyID: model.SignKeyID(),
|
|
}
|
|
c.Assert(modeenv.WriteTo(""), IsNil)
|
|
|
|
restore := boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error {
|
|
return fmt.Errorf("unexpected call")
|
|
})
|
|
s.AddCleanup(restore)
|
|
|
|
err := boot.SetTryRecoverySystem(s.uc20dev, "1234")
|
|
c.Assert(err, IsNil)
|
|
|
|
vars, err := mtbl.GetBootVars("try_recovery_system", "recovery_system_status")
|
|
c.Assert(err, IsNil)
|
|
c.Check(vars, DeepEquals, map[string]string{
|
|
"try_recovery_system": "1234",
|
|
"recovery_system_status": "try",
|
|
})
|
|
|
|
modeenvRead, err := boot.ReadModeenv("")
|
|
c.Assert(err, IsNil)
|
|
c.Check(modeenvRead, testutil.JsonEquals, boot.Modeenv{
|
|
Mode: "run",
|
|
// keep this comment to make old gofmt happy
|
|
CurrentRecoverySystems: []string{"20200825", "1234"},
|
|
|
|
Model: model.Model(),
|
|
BrandID: model.BrandID(),
|
|
Grade: string(model.Grade()),
|
|
ModelSignKeyID: model.SignKeyID(),
|
|
})
|
|
}
|
|
|
|
func (s *systemsSuite) TestSetTryRecoverySystemSetBootVarsErr(c *C) {
|
|
mtbl := bootloadertest.Mock("trusted", s.bootdir).WithTrustedAssets()
|
|
bootloader.Force(mtbl)
|
|
defer bootloader.Force(nil)
|
|
|
|
modeenv := &boot.Modeenv{
|
|
Mode: "run",
|
|
// keep this comment to make old gofmt happy
|
|
CurrentRecoverySystems: []string{"20200825"},
|
|
}
|
|
c.Assert(modeenv.WriteTo(""), IsNil)
|
|
|
|
restore := boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error {
|
|
return fmt.Errorf("unexpected call")
|
|
})
|
|
s.AddCleanup(restore)
|
|
|
|
mtbl.BootVars = map[string]string{
|
|
"try_recovery_system": "mock",
|
|
"recovery_system_status": "mock",
|
|
}
|
|
mtbl.SetErrFunc = func() error {
|
|
switch mtbl.SetBootVarsCalls {
|
|
case 1:
|
|
return fmt.Errorf("set boot vars fails")
|
|
case 2:
|
|
// called during cleanup
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("unexpected call")
|
|
}
|
|
}
|
|
|
|
err := boot.SetTryRecoverySystem(s.uc20dev, "1234")
|
|
c.Assert(err, ErrorMatches, "set boot vars fails")
|
|
|
|
// cleared
|
|
vars, err := mtbl.GetBootVars("try_recovery_system", "recovery_system_status")
|
|
c.Assert(err, IsNil)
|
|
c.Check(vars, DeepEquals, map[string]string{
|
|
"try_recovery_system": "",
|
|
"recovery_system_status": "",
|
|
})
|
|
|
|
modeenvRead, err := boot.ReadModeenv("")
|
|
c.Assert(err, IsNil)
|
|
// modeenv is unchanged
|
|
c.Check(modeenvRead.DeepEqual(modeenv), Equals, true)
|
|
}
|
|
|
|
func (s *systemsSuite) TestSetTryRecoverySystemCleanupOnErrorBeforeReseal(c *C) {
|
|
mockAssetsCache(c, s.rootdir, "trusted", []string{
|
|
"asset-asset-hash-1",
|
|
})
|
|
|
|
mtbl := s.mockTrustedBootloaderWithAssetAndChains(c, s.runKernelBf, s.recoveryKernelBf)
|
|
defer bootloader.Force(nil)
|
|
|
|
// system is encrypted
|
|
s.stampSealedKeys(c, s.rootdir)
|
|
|
|
model := s.uc20dev.Model()
|
|
|
|
modeenv := &boot.Modeenv{
|
|
Mode: "run",
|
|
// keep this comment to make old gofmt happy
|
|
CurrentRecoverySystems: []string{"20200825"},
|
|
CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{
|
|
"asset": []string{"asset-hash-1"},
|
|
},
|
|
|
|
CurrentTrustedBootAssets: boot.BootAssetsMap{
|
|
"asset": []string{"asset-hash-1"},
|
|
},
|
|
|
|
Model: model.Model(),
|
|
BrandID: model.BrandID(),
|
|
Grade: string(model.Grade()),
|
|
ModelSignKeyID: model.SignKeyID(),
|
|
}
|
|
c.Assert(modeenv.WriteTo(""), IsNil)
|
|
|
|
readSeedCalls := 0
|
|
cleanupTriggered := false
|
|
restore := boot.MockSeedReadSystemEssential(func(seedDir, label string, essentialTypes []snap.Type, tm timings.Measurer) (*asserts.Model, []*seed.Snap, error) {
|
|
readSeedCalls++
|
|
// this is the reseal cleanup path
|
|
switch readSeedCalls {
|
|
case 1:
|
|
// called for the first system
|
|
c.Assert(label, Equals, "20200825")
|
|
c.Check(mtbl.SetBootVarsCalls, Equals, 1)
|
|
return model, []*seed.Snap{s.seedKernelSnap, s.seedGadgetSnap}, nil
|
|
case 2:
|
|
// called for the 'try' system
|
|
c.Assert(label, Equals, "1234")
|
|
// modeenv is updated first
|
|
modeenvRead, err := boot.ReadModeenv("")
|
|
c.Assert(err, IsNil)
|
|
c.Check(modeenvRead.CurrentRecoverySystems, DeepEquals, []string{
|
|
"20200825", "1234",
|
|
})
|
|
c.Check(mtbl.SetBootVarsCalls, Equals, 1)
|
|
// we are triggering the cleanup by returning an error now
|
|
cleanupTriggered = true
|
|
return nil, nil, fmt.Errorf("seed read essential fails")
|
|
case 3:
|
|
// (cleanup) recovery boot chains for run key, called
|
|
// for the first system only
|
|
fallthrough
|
|
case 4:
|
|
// (cleanup) recovery boot chains for recovery keys
|
|
c.Assert(label, Equals, "20200825")
|
|
// boot variables already updated
|
|
c.Check(mtbl.SetBootVarsCalls, Equals, 2)
|
|
return model, []*seed.Snap{s.seedKernelSnap, s.seedGadgetSnap}, nil
|
|
default:
|
|
return nil, nil, fmt.Errorf("unexpected call %v", readSeedCalls)
|
|
}
|
|
})
|
|
defer restore()
|
|
|
|
resealCalls := 0
|
|
restore = boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error {
|
|
resealCalls++
|
|
if cleanupTriggered {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("unexpected call")
|
|
})
|
|
defer restore()
|
|
|
|
err := boot.SetTryRecoverySystem(s.uc20dev, "1234")
|
|
c.Assert(err, ErrorMatches, ".*: seed read essential fails")
|
|
|
|
// failed after the call to read the 'try' system seed
|
|
c.Check(readSeedCalls, Equals, 4)
|
|
// called twice during cleanup for run and recovery keys
|
|
c.Check(resealCalls, Equals, 2)
|
|
|
|
modeenvRead, err := boot.ReadModeenv("")
|
|
c.Assert(err, IsNil)
|
|
// modeenv is unchanged
|
|
c.Check(modeenvRead.DeepEqual(modeenv), Equals, true)
|
|
// bootloader variables have been cleared
|
|
vars, err := mtbl.GetBootVars("try_recovery_system", "recovery_system_status")
|
|
c.Assert(err, IsNil)
|
|
c.Check(vars, DeepEquals, map[string]string{
|
|
"try_recovery_system": "",
|
|
"recovery_system_status": "",
|
|
})
|
|
}
|
|
|
|
func (s *systemsSuite) TestSetTryRecoverySystemCleanupOnErrorAfterReseal(c *C) {
|
|
mockAssetsCache(c, s.rootdir, "trusted", []string{
|
|
"asset-asset-hash-1",
|
|
})
|
|
|
|
mtbl := s.mockTrustedBootloaderWithAssetAndChains(c, s.runKernelBf, s.recoveryKernelBf)
|
|
bootloader.Force(mtbl)
|
|
defer bootloader.Force(nil)
|
|
|
|
// system is encrypted
|
|
s.stampSealedKeys(c, s.rootdir)
|
|
|
|
model := s.uc20dev.Model()
|
|
|
|
modeenv := &boot.Modeenv{
|
|
Mode: "run",
|
|
// keep this comment to make old gofmt happy
|
|
CurrentRecoverySystems: []string{"20200825"},
|
|
CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{
|
|
"asset": []string{"asset-hash-1"},
|
|
},
|
|
|
|
CurrentTrustedBootAssets: boot.BootAssetsMap{
|
|
"asset": []string{"asset-hash-1"},
|
|
},
|
|
|
|
Model: model.Model(),
|
|
BrandID: model.BrandID(),
|
|
Grade: string(model.Grade()),
|
|
ModelSignKeyID: model.SignKeyID(),
|
|
}
|
|
c.Assert(modeenv.WriteTo(""), IsNil)
|
|
|
|
readSeedCalls := 0
|
|
restore := boot.MockSeedReadSystemEssential(func(seedDir, label string, essentialTypes []snap.Type, tm timings.Measurer) (*asserts.Model, []*seed.Snap, error) {
|
|
readSeedCalls++
|
|
// this is the reseal cleanup path
|
|
|
|
switch readSeedCalls {
|
|
case 1:
|
|
// called for the first system
|
|
c.Assert(label, Equals, "20200825")
|
|
c.Check(mtbl.SetBootVarsCalls, Equals, 1)
|
|
return model, []*seed.Snap{s.seedKernelSnap, s.seedGadgetSnap}, nil
|
|
case 2:
|
|
// called for the 'try' system
|
|
c.Assert(label, Equals, "1234")
|
|
// modeenv is updated first
|
|
modeenvRead, err := boot.ReadModeenv("")
|
|
c.Assert(err, IsNil)
|
|
c.Check(modeenvRead.CurrentRecoverySystems, DeepEquals, []string{
|
|
"20200825", "1234",
|
|
})
|
|
c.Check(mtbl.SetBootVarsCalls, Equals, 1)
|
|
// still good
|
|
return model, []*seed.Snap{s.seedKernelSnap, s.seedGadgetSnap}, nil
|
|
case 3:
|
|
// recovery boot chains for a good recovery system
|
|
c.Check(mtbl.SetBootVarsCalls, Equals, 1)
|
|
fallthrough
|
|
case 4:
|
|
// (cleanup) recovery boot chains for run key, called
|
|
// for the first system only
|
|
fallthrough
|
|
case 5:
|
|
// (cleanup) recovery boot chains for recovery keys
|
|
c.Assert(label, Equals, "20200825")
|
|
// boot variables already updated
|
|
if readSeedCalls >= 4 {
|
|
c.Check(mtbl.SetBootVarsCalls, Equals, 2)
|
|
}
|
|
return model, []*seed.Snap{s.seedKernelSnap, s.seedGadgetSnap}, nil
|
|
default:
|
|
return nil, nil, fmt.Errorf("unexpected call %v", readSeedCalls)
|
|
}
|
|
})
|
|
defer restore()
|
|
|
|
resealCalls := 0
|
|
restore = boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error {
|
|
resealCalls++
|
|
switch resealCalls {
|
|
case 1:
|
|
// attempt to reseal the run key
|
|
return fmt.Errorf("reseal fails")
|
|
case 2, 3:
|
|
// reseal of run and recovery keys
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("unexpected call")
|
|
|
|
}
|
|
})
|
|
defer restore()
|
|
|
|
err := boot.SetTryRecoverySystem(s.uc20dev, "1234")
|
|
c.Assert(err, ErrorMatches, "cannot reseal the encryption key: reseal fails")
|
|
|
|
// failed after the call to read the 'try' system seed
|
|
c.Check(readSeedCalls, Equals, 5)
|
|
// called 3 times, once when mocked failure occurs, twice during cleanup
|
|
// for run and recovery keys
|
|
c.Check(resealCalls, Equals, 3)
|
|
|
|
modeenvRead, err := boot.ReadModeenv("")
|
|
c.Assert(err, IsNil)
|
|
// modeenv is unchanged
|
|
c.Check(modeenvRead.DeepEqual(modeenv), Equals, true)
|
|
// bootloader variables have been cleared
|
|
vars, err := mtbl.GetBootVars("try_recovery_system", "recovery_system_status")
|
|
c.Assert(err, IsNil)
|
|
c.Check(vars, DeepEquals, map[string]string{
|
|
"try_recovery_system": "",
|
|
"recovery_system_status": "",
|
|
})
|
|
}
|
|
|
|
func (s *systemsSuite) TestSetTryRecoverySystemCleanupError(c *C) {
|
|
mockAssetsCache(c, s.rootdir, "trusted", []string{
|
|
"asset-asset-hash-1",
|
|
})
|
|
|
|
mtbl := s.mockTrustedBootloaderWithAssetAndChains(c, s.runKernelBf, s.recoveryKernelBf)
|
|
bootloader.Force(mtbl)
|
|
defer bootloader.Force(nil)
|
|
|
|
// system is encrypted
|
|
s.stampSealedKeys(c, s.rootdir)
|
|
|
|
model := s.uc20dev.Model()
|
|
modeenv := &boot.Modeenv{
|
|
Mode: "run",
|
|
// keep this comment to make old gofmt happy
|
|
CurrentRecoverySystems: []string{"20200825"},
|
|
CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{
|
|
"asset": []string{"asset-hash-1"},
|
|
},
|
|
|
|
CurrentTrustedBootAssets: boot.BootAssetsMap{
|
|
"asset": []string{"asset-hash-1"},
|
|
},
|
|
|
|
Model: model.Model(),
|
|
BrandID: model.BrandID(),
|
|
Grade: string(model.Grade()),
|
|
ModelSignKeyID: model.SignKeyID(),
|
|
}
|
|
c.Assert(modeenv.WriteTo(""), IsNil)
|
|
|
|
readSeedCalls := 0
|
|
restore := boot.MockSeedReadSystemEssential(func(seedDir, label string, essentialTypes []snap.Type, tm timings.Measurer) (*asserts.Model, []*seed.Snap, error) {
|
|
readSeedCalls++
|
|
// this is the reseal cleanup path
|
|
c.Logf("call %v label %v", readSeedCalls, label)
|
|
switch readSeedCalls {
|
|
case 1:
|
|
// called for the first system
|
|
c.Assert(label, Equals, "20200825")
|
|
return s.uc20dev.Model(), []*seed.Snap{s.seedKernelSnap, s.seedGadgetSnap}, nil
|
|
case 2:
|
|
// called for the 'try' system
|
|
c.Assert(label, Equals, "1234")
|
|
// still good
|
|
return s.uc20dev.Model(), []*seed.Snap{s.seedKernelSnap, s.seedGadgetSnap}, nil
|
|
case 3:
|
|
// recovery boot chains for a good recovery system
|
|
fallthrough
|
|
case 4:
|
|
// (cleanup) recovery boot chains for run key, called
|
|
// for the first system only
|
|
fallthrough
|
|
case 5:
|
|
// (cleanup) recovery boot chains for recovery keys
|
|
c.Check(label, Equals, "20200825")
|
|
return s.uc20dev.Model(), []*seed.Snap{s.seedKernelSnap, s.seedGadgetSnap}, nil
|
|
default:
|
|
return nil, nil, fmt.Errorf("unexpected call %v", readSeedCalls)
|
|
}
|
|
})
|
|
defer restore()
|
|
|
|
resealCalls := 0
|
|
restore = boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error {
|
|
resealCalls++
|
|
switch resealCalls {
|
|
case 1:
|
|
return fmt.Errorf("reseal fails")
|
|
case 2, 3:
|
|
// reseal of run and recovery keys
|
|
return fmt.Errorf("reseal in cleanup fails too")
|
|
default:
|
|
return fmt.Errorf("unexpected call")
|
|
|
|
}
|
|
})
|
|
defer restore()
|
|
|
|
err := boot.SetTryRecoverySystem(s.uc20dev, "1234")
|
|
c.Assert(err, ErrorMatches, `cannot reseal the encryption key: reseal fails \(cleanup failed: cannot reseal the encryption key: reseal in cleanup fails too\)`)
|
|
|
|
// failed after the call to read the 'try' system seed
|
|
c.Check(readSeedCalls, Equals, 5)
|
|
// called twice, once when enabling the try system, once on cleanup
|
|
c.Check(resealCalls, Equals, 2)
|
|
|
|
modeenvRead, err := boot.ReadModeenv("")
|
|
c.Assert(err, IsNil)
|
|
// modeenv is unchanged
|
|
c.Check(modeenvRead.DeepEqual(modeenv), Equals, true)
|
|
// bootloader variables have been cleared regardless of reseal failing
|
|
vars, err := mtbl.GetBootVars("try_recovery_system", "recovery_system_status")
|
|
c.Assert(err, IsNil)
|
|
c.Check(vars, DeepEquals, map[string]string{
|
|
"try_recovery_system": "",
|
|
"recovery_system_status": "",
|
|
})
|
|
}
|
|
|
|
func (s *systemsSuite) testInspectRecoverySystemOutcomeHappy(c *C, mtbl *bootloadertest.MockTrustedAssetsBootloader, expectedOutcome boot.TryRecoverySystemOutcome, expectedErr string) {
|
|
bootloader.Force(mtbl)
|
|
defer bootloader.Force(nil)
|
|
|
|
// system is encrypted
|
|
s.stampSealedKeys(c, s.rootdir)
|
|
|
|
restore := boot.MockSeedReadSystemEssential(func(seedDir, label string, essentialTypes []snap.Type, tm timings.Measurer) (*asserts.Model, []*seed.Snap, error) {
|
|
return nil, nil, fmt.Errorf("unexpected call")
|
|
})
|
|
defer restore()
|
|
|
|
restore = boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error {
|
|
return fmt.Errorf("unexpected call")
|
|
})
|
|
defer restore()
|
|
|
|
outcome, label, err := boot.InspectTryRecoverySystemOutcome(s.uc20dev)
|
|
if expectedErr == "" {
|
|
c.Assert(err, IsNil)
|
|
} else {
|
|
c.Assert(err, ErrorMatches, expectedErr)
|
|
}
|
|
c.Check(outcome, Equals, expectedOutcome)
|
|
switch outcome {
|
|
case boot.TryRecoverySystemOutcomeSuccess, boot.TryRecoverySystemOutcomeFailure:
|
|
c.Check(label, Equals, "1234")
|
|
default:
|
|
c.Check(label, Equals, "")
|
|
}
|
|
}
|
|
|
|
func (s *systemsSuite) TestInspectRecoverySystemOutcomeHappySuccess(c *C) {
|
|
triedVars := map[string]string{
|
|
"recovery_system_status": "tried",
|
|
"try_recovery_system": "1234",
|
|
}
|
|
mtbl := s.mockTrustedBootloaderWithAssetAndChains(c, s.runKernelBf, s.recoveryKernelBf)
|
|
err := mtbl.SetBootVars(triedVars)
|
|
c.Assert(err, IsNil)
|
|
|
|
m := boot.Modeenv{
|
|
Mode: boot.ModeRun,
|
|
// keep this comment to make old gofmt happy
|
|
CurrentRecoverySystems: []string{"29112019", "1234"},
|
|
}
|
|
err = m.WriteTo("")
|
|
c.Assert(err, IsNil)
|
|
|
|
s.testInspectRecoverySystemOutcomeHappy(c, mtbl, boot.TryRecoverySystemOutcomeSuccess, "")
|
|
|
|
vars, err := mtbl.GetBootVars("try_recovery_system", "recovery_system_status")
|
|
c.Assert(err, IsNil)
|
|
c.Check(vars, DeepEquals, triedVars)
|
|
}
|
|
|
|
func (s *systemsSuite) TestInspectRecoverySystemOutcomeFailureMissingSystemInModeenv(c *C) {
|
|
triedVars := map[string]string{
|
|
"recovery_system_status": "tried",
|
|
"try_recovery_system": "1234",
|
|
}
|
|
mtbl := s.mockTrustedBootloaderWithAssetAndChains(c, s.runKernelBf, s.recoveryKernelBf)
|
|
err := mtbl.SetBootVars(triedVars)
|
|
c.Assert(err, IsNil)
|
|
|
|
m := boot.Modeenv{
|
|
Mode: boot.ModeRun,
|
|
// we don't have the tried recovery system in the modeenv
|
|
CurrentRecoverySystems: []string{"29112019"},
|
|
}
|
|
err = m.WriteTo("")
|
|
c.Assert(err, IsNil)
|
|
|
|
s.testInspectRecoverySystemOutcomeHappy(c, mtbl, boot.TryRecoverySystemOutcomeFailure, `recovery system "1234" was tried, but is not present in the modeenv CurrentRecoverySystems`)
|
|
|
|
vars, err := mtbl.GetBootVars("try_recovery_system", "recovery_system_status")
|
|
c.Assert(err, IsNil)
|
|
c.Check(vars, DeepEquals, triedVars)
|
|
}
|
|
|
|
func (s *systemsSuite) TestInspectRecoverySystemOutcomeHappyFailure(c *C) {
|
|
tryVars := map[string]string{
|
|
"recovery_system_status": "try",
|
|
"try_recovery_system": "1234",
|
|
}
|
|
mtbl := s.mockTrustedBootloaderWithAssetAndChains(c, s.runKernelBf, s.recoveryKernelBf)
|
|
err := mtbl.SetBootVars(tryVars)
|
|
c.Assert(err, IsNil)
|
|
s.testInspectRecoverySystemOutcomeHappy(c, mtbl, boot.TryRecoverySystemOutcomeFailure, "")
|
|
|
|
vars, err := mtbl.GetBootVars("try_recovery_system", "recovery_system_status")
|
|
c.Assert(err, IsNil)
|
|
c.Check(vars, DeepEquals, tryVars)
|
|
}
|
|
|
|
func (s *systemsSuite) TestInspectRecoverySystemOutcomeNotTried(c *C) {
|
|
notTriedVars := map[string]string{
|
|
"recovery_system_status": "",
|
|
"try_recovery_system": "",
|
|
}
|
|
mtbl := s.mockTrustedBootloaderWithAssetAndChains(c, s.runKernelBf, s.recoveryKernelBf)
|
|
err := mtbl.SetBootVars(notTriedVars)
|
|
c.Assert(err, IsNil)
|
|
s.testInspectRecoverySystemOutcomeHappy(c, mtbl, boot.TryRecoverySystemOutcomeNoneTried, "")
|
|
}
|
|
|
|
func (s *systemsSuite) TestInspectRecoverySystemOutcomeInconsistentBogusStatus(c *C) {
|
|
badVars := map[string]string{
|
|
"recovery_system_status": "foo",
|
|
"try_recovery_system": "1234",
|
|
}
|
|
mtbl := s.mockTrustedBootloaderWithAssetAndChains(c, s.runKernelBf, s.recoveryKernelBf)
|
|
err := mtbl.SetBootVars(badVars)
|
|
c.Assert(err, IsNil)
|
|
s.testInspectRecoverySystemOutcomeHappy(c, mtbl, boot.TryRecoverySystemOutcomeInconsistent, `unexpected recovery system status "foo"`)
|
|
vars, err := mtbl.GetBootVars("try_recovery_system", "recovery_system_status")
|
|
c.Assert(err, IsNil)
|
|
c.Check(vars, DeepEquals, badVars)
|
|
}
|
|
|
|
func (s *systemsSuite) TestInspectRecoverySystemOutcomeInconsistentBadLabel(c *C) {
|
|
badVars := map[string]string{
|
|
"recovery_system_status": "tried",
|
|
"try_recovery_system": "",
|
|
}
|
|
mtbl := s.mockTrustedBootloaderWithAssetAndChains(c, s.runKernelBf, s.recoveryKernelBf)
|
|
err := mtbl.SetBootVars(badVars)
|
|
c.Assert(err, IsNil)
|
|
s.testInspectRecoverySystemOutcomeHappy(c, mtbl, boot.TryRecoverySystemOutcomeInconsistent, `try recovery system is unset but status is "tried"`)
|
|
vars, err := mtbl.GetBootVars("try_recovery_system", "recovery_system_status")
|
|
c.Assert(err, IsNil)
|
|
c.Check(vars, DeepEquals, badVars)
|
|
}
|
|
|
|
func (s *systemsSuite) TestInspectRecoverySystemOutcomeInconsistentUnexpectedLabel(c *C) {
|
|
badVars := map[string]string{
|
|
"recovery_system_status": "",
|
|
"try_recovery_system": "1234",
|
|
}
|
|
mtbl := s.mockTrustedBootloaderWithAssetAndChains(c, s.runKernelBf, s.recoveryKernelBf)
|
|
err := mtbl.SetBootVars(badVars)
|
|
c.Assert(err, IsNil)
|
|
s.testInspectRecoverySystemOutcomeHappy(c, mtbl, boot.TryRecoverySystemOutcomeInconsistent, `unexpected recovery system status ""`)
|
|
vars, err := mtbl.GetBootVars("try_recovery_system", "recovery_system_status")
|
|
c.Assert(err, IsNil)
|
|
c.Check(vars, DeepEquals, badVars)
|
|
}
|
|
|
|
type clearRecoverySystemTestCase struct {
|
|
systemLabel string
|
|
tryModel *asserts.Model
|
|
resealErr error
|
|
expectedErr string
|
|
}
|
|
|
|
func (s *systemsSuite) testClearRecoverySystem(c *C, mtbl *bootloadertest.MockTrustedAssetsBootloader, tc clearRecoverySystemTestCase) {
|
|
mockAssetsCache(c, s.rootdir, "trusted", []string{
|
|
"asset-asset-hash-1",
|
|
})
|
|
|
|
bootloader.Force(mtbl)
|
|
defer bootloader.Force(nil)
|
|
|
|
// system is encrypted
|
|
s.stampSealedKeys(c, s.rootdir)
|
|
|
|
model := s.uc20dev.Model()
|
|
|
|
modeenv := &boot.Modeenv{
|
|
Mode: "run",
|
|
// keep this comment to make old gofmt happy
|
|
CurrentRecoverySystems: []string{"20200825"},
|
|
GoodRecoverySystems: []string{"20200825"},
|
|
CurrentKernels: []string{},
|
|
CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{
|
|
"asset": []string{"asset-hash-1"},
|
|
},
|
|
|
|
CurrentTrustedBootAssets: boot.BootAssetsMap{
|
|
"asset": []string{"asset-hash-1"},
|
|
},
|
|
|
|
Model: model.Model(),
|
|
BrandID: model.BrandID(),
|
|
Grade: string(model.Grade()),
|
|
ModelSignKeyID: model.SignKeyID(),
|
|
}
|
|
if tc.systemLabel != "" {
|
|
modeenv.CurrentRecoverySystems = append(modeenv.CurrentRecoverySystems, tc.systemLabel)
|
|
}
|
|
if tc.tryModel != nil {
|
|
modeenv.TryModel = tc.tryModel.Model()
|
|
modeenv.TryBrandID = tc.tryModel.BrandID()
|
|
modeenv.TryGrade = string(tc.tryModel.Grade())
|
|
modeenv.TryModelSignKeyID = tc.tryModel.SignKeyID()
|
|
}
|
|
c.Assert(modeenv.WriteTo(""), IsNil)
|
|
|
|
var readSeedSeenLabels []string
|
|
restore := boot.MockSeedReadSystemEssential(func(seedDir, label string, essentialTypes []snap.Type, tm timings.Measurer) (*asserts.Model, []*seed.Snap, error) {
|
|
// the mock bootloader can only mock a single recovery boot
|
|
// chain, so pretend both seeds use the same kernel, but keep track of the labels
|
|
readSeedSeenLabels = append(readSeedSeenLabels, label)
|
|
return model, []*seed.Snap{s.seedKernelSnap, s.seedGadgetSnap}, nil
|
|
})
|
|
defer restore()
|
|
|
|
resealCalls := 0
|
|
restore = boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error {
|
|
resealCalls++
|
|
c.Assert(params, NotNil)
|
|
c.Assert(params.ModelParams, HasLen, 1)
|
|
switch resealCalls {
|
|
case 1:
|
|
c.Check(params.KeyFiles, DeepEquals, []string{
|
|
filepath.Join(boot.InitramfsBootEncryptionKeyDir, "ubuntu-data.sealed-key"),
|
|
})
|
|
return tc.resealErr
|
|
case 2:
|
|
c.Check(params.KeyFiles, DeepEquals, []string{
|
|
filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-data.recovery.sealed-key"),
|
|
filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-save.recovery.sealed-key"),
|
|
})
|
|
c.Assert(params.ModelParams[0].KernelCmdlines, DeepEquals, []string{
|
|
"snapd_recovery_mode=recover snapd_recovery_system=20200825 static cmdline",
|
|
})
|
|
return nil
|
|
default:
|
|
c.Errorf("unexpected call to secboot.ResealKeys with count %v", resealCalls)
|
|
return fmt.Errorf("unexpected call")
|
|
}
|
|
})
|
|
defer restore()
|
|
|
|
err := boot.ClearTryRecoverySystem(s.uc20dev, tc.systemLabel)
|
|
if tc.expectedErr == "" {
|
|
c.Assert(err, IsNil)
|
|
} else {
|
|
c.Assert(err, ErrorMatches, tc.expectedErr)
|
|
}
|
|
|
|
// only one seed system accessed
|
|
c.Check(readSeedSeenLabels, DeepEquals, []string{"20200825", "20200825"})
|
|
if tc.resealErr == nil {
|
|
// called twice, for run and recovery keys
|
|
c.Check(resealCalls, Equals, 2)
|
|
} else {
|
|
// fails on run key
|
|
c.Check(resealCalls, Equals, 1)
|
|
}
|
|
|
|
modeenvRead, err := boot.ReadModeenv("")
|
|
c.Assert(err, IsNil)
|
|
// modeenv systems list has one entry only
|
|
c.Check(modeenvRead, testutil.JsonEquals, boot.Modeenv{
|
|
Mode: "run",
|
|
// keep this comment to make old gofmt happy
|
|
CurrentRecoverySystems: []string{"20200825"},
|
|
GoodRecoverySystems: []string{"20200825"},
|
|
CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{
|
|
"asset": []string{"asset-hash-1"},
|
|
},
|
|
|
|
CurrentTrustedBootAssets: boot.BootAssetsMap{
|
|
"asset": []string{"asset-hash-1"},
|
|
},
|
|
|
|
Model: model.Model(),
|
|
BrandID: model.BrandID(),
|
|
Grade: string(model.Grade()),
|
|
ModelSignKeyID: model.SignKeyID(),
|
|
|
|
// try model if set, has been cleared
|
|
})
|
|
}
|
|
|
|
func (s *systemsSuite) TestClearRecoverySystemHappy(c *C) {
|
|
setVars := map[string]string{
|
|
"recovery_system_status": "try",
|
|
"try_recovery_system": "1234",
|
|
}
|
|
mtbl := s.mockTrustedBootloaderWithAssetAndChains(c, s.runKernelBf, s.recoveryKernelBf)
|
|
err := mtbl.SetBootVars(setVars)
|
|
c.Assert(err, IsNil)
|
|
|
|
s.testClearRecoverySystem(c, mtbl, clearRecoverySystemTestCase{systemLabel: "1234"})
|
|
// bootloader variables have been cleared
|
|
vars, err := mtbl.GetBootVars("try_recovery_system", "recovery_system_status")
|
|
c.Assert(err, IsNil)
|
|
c.Check(vars, DeepEquals, map[string]string{
|
|
"try_recovery_system": "",
|
|
"recovery_system_status": "",
|
|
})
|
|
}
|
|
|
|
func (s *systemsSuite) TestClearRecoverySystemTriedHappy(c *C) {
|
|
setVars := map[string]string{
|
|
"recovery_system_status": "tried",
|
|
"try_recovery_system": "1234",
|
|
}
|
|
mtbl := s.mockTrustedBootloaderWithAssetAndChains(c, s.runKernelBf, s.recoveryKernelBf)
|
|
err := mtbl.SetBootVars(setVars)
|
|
c.Assert(err, IsNil)
|
|
|
|
s.testClearRecoverySystem(c, mtbl, clearRecoverySystemTestCase{systemLabel: "1234"})
|
|
// bootloader variables have been cleared
|
|
vars, err := mtbl.GetBootVars("try_recovery_system", "recovery_system_status")
|
|
c.Assert(err, IsNil)
|
|
c.Check(vars, DeepEquals, map[string]string{
|
|
"try_recovery_system": "",
|
|
"recovery_system_status": "",
|
|
})
|
|
}
|
|
|
|
func (s *systemsSuite) TestClearRecoverySystemInconsistentStateHappy(c *C) {
|
|
setVars := map[string]string{
|
|
"recovery_system_status": "foo",
|
|
"try_recovery_system": "",
|
|
}
|
|
mtbl := s.mockTrustedBootloaderWithAssetAndChains(c, s.runKernelBf, s.recoveryKernelBf)
|
|
err := mtbl.SetBootVars(setVars)
|
|
c.Assert(err, IsNil)
|
|
|
|
s.testClearRecoverySystem(c, mtbl, clearRecoverySystemTestCase{systemLabel: "1234"})
|
|
// bootloader variables have been cleared
|
|
vars, err := mtbl.GetBootVars("try_recovery_system", "recovery_system_status")
|
|
c.Assert(err, IsNil)
|
|
c.Check(vars, DeepEquals, map[string]string{
|
|
"try_recovery_system": "",
|
|
"recovery_system_status": "",
|
|
})
|
|
}
|
|
|
|
func (s *systemsSuite) TestClearRecoverySystemInconsistentNoLabelHappy(c *C) {
|
|
setVars := map[string]string{
|
|
"recovery_system_status": "this-will-be-gone",
|
|
"try_recovery_system": "this-too",
|
|
}
|
|
mtbl := s.mockTrustedBootloaderWithAssetAndChains(c, s.runKernelBf, s.recoveryKernelBf)
|
|
err := mtbl.SetBootVars(setVars)
|
|
c.Assert(err, IsNil)
|
|
|
|
// clear without passing the system label, just clears the relevant boot
|
|
// variables
|
|
const noLabel = ""
|
|
s.testClearRecoverySystem(c, mtbl, clearRecoverySystemTestCase{systemLabel: noLabel})
|
|
// bootloader variables have been cleared
|
|
vars, err := mtbl.GetBootVars("try_recovery_system", "recovery_system_status")
|
|
c.Assert(err, IsNil)
|
|
c.Check(vars, DeepEquals, map[string]string{
|
|
"try_recovery_system": "",
|
|
"recovery_system_status": "",
|
|
})
|
|
}
|
|
|
|
func (s *systemsSuite) TestClearRecoverySystemRemodelHappy(c *C) {
|
|
setVars := map[string]string{
|
|
"recovery_system_status": "try",
|
|
"try_recovery_system": "1234",
|
|
}
|
|
mtbl := s.mockTrustedBootloaderWithAssetAndChains(c, s.runKernelBf, s.recoveryKernelBf)
|
|
err := mtbl.SetBootVars(setVars)
|
|
c.Assert(err, IsNil)
|
|
|
|
s.testClearRecoverySystem(c, mtbl, clearRecoverySystemTestCase{
|
|
systemLabel: "1234",
|
|
tryModel: boottest.MakeMockUC20Model(map[string]interface{}{
|
|
"tryModelodel": "my-new-model",
|
|
}),
|
|
})
|
|
// bootloader variables have been cleared
|
|
vars, err := mtbl.GetBootVars("try_recovery_system", "recovery_system_status")
|
|
c.Assert(err, IsNil)
|
|
c.Check(vars, DeepEquals, map[string]string{
|
|
"try_recovery_system": "",
|
|
"recovery_system_status": "",
|
|
})
|
|
}
|
|
|
|
func (s *systemsSuite) TestClearRecoverySystemResealFails(c *C) {
|
|
setVars := map[string]string{
|
|
"recovery_system_status": "try",
|
|
"try_recovery_system": "1234",
|
|
}
|
|
mtbl := s.mockTrustedBootloaderWithAssetAndChains(c, s.runKernelBf, s.recoveryKernelBf)
|
|
err := mtbl.SetBootVars(setVars)
|
|
c.Assert(err, IsNil)
|
|
|
|
s.testClearRecoverySystem(c, mtbl, clearRecoverySystemTestCase{
|
|
systemLabel: "1234",
|
|
resealErr: fmt.Errorf("reseal fails"),
|
|
expectedErr: "cannot reseal the encryption key: reseal fails",
|
|
})
|
|
// bootloader variables have been cleared
|
|
vars, err := mtbl.GetBootVars("try_recovery_system", "recovery_system_status")
|
|
c.Assert(err, IsNil)
|
|
// variables were cleared
|
|
c.Check(vars, DeepEquals, map[string]string{
|
|
"try_recovery_system": "",
|
|
"recovery_system_status": "",
|
|
})
|
|
}
|
|
|
|
func (s *systemsSuite) TestClearRecoverySystemSetBootVarsFails(c *C) {
|
|
setVars := map[string]string{
|
|
"recovery_system_status": "try",
|
|
"try_recovery_system": "1234",
|
|
}
|
|
mtbl := s.mockTrustedBootloaderWithAssetAndChains(c, s.runKernelBf, s.recoveryKernelBf)
|
|
err := mtbl.SetBootVars(setVars)
|
|
c.Assert(err, IsNil)
|
|
mtbl.SetErr = fmt.Errorf("set boot vars fails")
|
|
|
|
s.testClearRecoverySystem(c, mtbl, clearRecoverySystemTestCase{
|
|
systemLabel: "1234",
|
|
expectedErr: "set boot vars fails",
|
|
})
|
|
}
|
|
|
|
func (s *systemsSuite) TestClearRecoverySystemReboot(c *C) {
|
|
setVars := map[string]string{
|
|
"recovery_system_status": "try",
|
|
"try_recovery_system": "1234",
|
|
}
|
|
mtbl := s.mockTrustedBootloaderWithAssetAndChains(c, s.runKernelBf, s.recoveryKernelBf)
|
|
err := mtbl.SetBootVars(setVars)
|
|
c.Assert(err, IsNil)
|
|
|
|
mockAssetsCache(c, s.rootdir, "trusted", []string{
|
|
"asset-asset-hash-1",
|
|
})
|
|
|
|
bootloader.Force(mtbl)
|
|
defer bootloader.Force(nil)
|
|
|
|
// system is encrypted
|
|
s.stampSealedKeys(c, s.rootdir)
|
|
|
|
model := s.uc20dev.Model()
|
|
|
|
modeenv := &boot.Modeenv{
|
|
Mode: "run",
|
|
// keep this comment to make old gofmt happy
|
|
CurrentRecoverySystems: []string{"20200825", "1234"},
|
|
CurrentKernels: []string{},
|
|
CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{
|
|
"asset": []string{"asset-hash-1"},
|
|
},
|
|
|
|
CurrentTrustedBootAssets: boot.BootAssetsMap{
|
|
"asset": []string{"asset-hash-1"},
|
|
},
|
|
|
|
Model: model.Model(),
|
|
BrandID: model.BrandID(),
|
|
Grade: string(model.Grade()),
|
|
ModelSignKeyID: model.SignKeyID(),
|
|
}
|
|
c.Assert(modeenv.WriteTo(""), IsNil)
|
|
|
|
var readSeedSeenLabels []string
|
|
restore := boot.MockSeedReadSystemEssential(func(seedDir, label string, essentialTypes []snap.Type, tm timings.Measurer) (*asserts.Model, []*seed.Snap, error) {
|
|
// the mock bootloader can only mock a single recovery boot
|
|
// chain, so pretend both seeds use the same kernel, but keep track of the labels
|
|
readSeedSeenLabels = append(readSeedSeenLabels, label)
|
|
return model, []*seed.Snap{s.seedKernelSnap, s.seedGadgetSnap}, nil
|
|
})
|
|
defer restore()
|
|
|
|
resealCalls := 0
|
|
restore = boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error {
|
|
resealCalls++
|
|
c.Assert(params, NotNil)
|
|
c.Assert(params.ModelParams, HasLen, 1)
|
|
switch resealCalls {
|
|
case 1:
|
|
c.Check(params.KeyFiles, DeepEquals, []string{
|
|
filepath.Join(boot.InitramfsBootEncryptionKeyDir, "ubuntu-data.sealed-key"),
|
|
})
|
|
panic("reseal panic")
|
|
case 2:
|
|
c.Check(params.KeyFiles, DeepEquals, []string{
|
|
filepath.Join(boot.InitramfsBootEncryptionKeyDir, "ubuntu-data.sealed-key"),
|
|
})
|
|
return nil
|
|
case 3:
|
|
c.Check(params.KeyFiles, DeepEquals, []string{
|
|
filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-data.recovery.sealed-key"),
|
|
filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-save.recovery.sealed-key"),
|
|
})
|
|
c.Assert(params.ModelParams[0].KernelCmdlines, DeepEquals, []string{
|
|
"snapd_recovery_mode=recover snapd_recovery_system=20200825 static cmdline",
|
|
})
|
|
return nil
|
|
default:
|
|
c.Errorf("unexpected call to secboot.ResealKeys with count %v", resealCalls)
|
|
return fmt.Errorf("unexpected call")
|
|
|
|
}
|
|
})
|
|
defer restore()
|
|
|
|
checkGoodState := func() {
|
|
// modeenv was already written
|
|
modeenvRead, err := boot.ReadModeenv("")
|
|
c.Assert(err, IsNil)
|
|
// modeenv systems list has one entry only
|
|
c.Check(modeenvRead.CurrentRecoverySystems, DeepEquals, []string{
|
|
"20200825",
|
|
})
|
|
// bootloader variables have been cleared already
|
|
vars, err := mtbl.GetBootVars("try_recovery_system", "recovery_system_status")
|
|
c.Assert(err, IsNil)
|
|
// variables were cleared
|
|
c.Check(vars, DeepEquals, map[string]string{
|
|
"try_recovery_system": "",
|
|
"recovery_system_status": "",
|
|
})
|
|
}
|
|
|
|
c.Assert(func() {
|
|
boot.ClearTryRecoverySystem(s.uc20dev, "1234")
|
|
}, PanicMatches, "reseal panic")
|
|
// only one seed system accessed
|
|
c.Check(readSeedSeenLabels, DeepEquals, []string{"20200825", "20200825"})
|
|
// panicked on run key
|
|
c.Check(resealCalls, Equals, 1)
|
|
checkGoodState()
|
|
|
|
mtbl.SetErrFunc = func() error {
|
|
panic("set boot vars panic")
|
|
}
|
|
c.Assert(func() {
|
|
boot.ClearTryRecoverySystem(s.uc20dev, "1234")
|
|
}, PanicMatches, "set boot vars panic")
|
|
// we did not reach resealing yet
|
|
c.Check(resealCalls, Equals, 1)
|
|
checkGoodState()
|
|
|
|
mtbl.SetErrFunc = nil
|
|
err = boot.ClearTryRecoverySystem(s.uc20dev, "1234")
|
|
c.Assert(err, IsNil)
|
|
checkGoodState()
|
|
}
|
|
|
|
type recoverySystemGoodTestCase struct {
|
|
systemLabelAddToCurrent bool
|
|
systemLabelAddToGood bool
|
|
triedSystems []string
|
|
|
|
resealRecoveryKeyErr error
|
|
resealRecoveryKeyDuringCleanupErr error
|
|
resealCalls int
|
|
expectedErr string
|
|
|
|
readSeedSystems []string
|
|
expectedCurrentSystemsList []string
|
|
expectedGoodSystemsList []string
|
|
}
|
|
|
|
func (s *systemsSuite) testPromoteTriedRecoverySystem(c *C, systemLabel string, tc recoverySystemGoodTestCase) {
|
|
mtbl := s.mockTrustedBootloaderWithAssetAndChains(c, s.runKernelBf, s.recoveryKernelBf)
|
|
mockAssetsCache(c, s.rootdir, "trusted", []string{
|
|
"asset-asset-hash-1",
|
|
})
|
|
|
|
bootloader.Force(mtbl)
|
|
defer bootloader.Force(nil)
|
|
|
|
// system is encrypted
|
|
s.stampSealedKeys(c, s.rootdir)
|
|
|
|
model := s.uc20dev.Model()
|
|
|
|
modeenv := &boot.Modeenv{
|
|
Mode: "run",
|
|
// keep this comment to make old gofmt happy
|
|
CurrentRecoverySystems: []string{"20200825"},
|
|
GoodRecoverySystems: []string{"20200825"},
|
|
CurrentKernels: []string{},
|
|
CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{
|
|
"asset": []string{"asset-hash-1"},
|
|
},
|
|
|
|
CurrentTrustedBootAssets: boot.BootAssetsMap{
|
|
"asset": []string{"asset-hash-1"},
|
|
},
|
|
|
|
Model: model.Model(),
|
|
BrandID: model.BrandID(),
|
|
Grade: string(model.Grade()),
|
|
ModelSignKeyID: model.SignKeyID(),
|
|
}
|
|
if tc.systemLabelAddToCurrent {
|
|
modeenv.CurrentRecoverySystems = append(modeenv.CurrentRecoverySystems, systemLabel)
|
|
}
|
|
if tc.systemLabelAddToGood {
|
|
modeenv.GoodRecoverySystems = append(modeenv.GoodRecoverySystems, systemLabel)
|
|
}
|
|
|
|
c.Assert(modeenv.WriteTo(""), IsNil)
|
|
|
|
var readSeedSeenLabels []string
|
|
restore := boot.MockSeedReadSystemEssential(func(seedDir, label string, essentialTypes []snap.Type, tm timings.Measurer) (*asserts.Model, []*seed.Snap, error) {
|
|
// the mock bootloader can only mock a single recovery boot
|
|
// chain, so pretend both seeds use the same kernel, but keep track of the labels
|
|
readSeedSeenLabels = append(readSeedSeenLabels, label)
|
|
return model, []*seed.Snap{s.seedKernelSnap, s.seedGadgetSnap}, nil
|
|
})
|
|
defer restore()
|
|
|
|
resealCalls := 0
|
|
restore = boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error {
|
|
resealCalls++
|
|
c.Assert(params, NotNil)
|
|
c.Assert(params.ModelParams, HasLen, 1)
|
|
switch resealCalls {
|
|
case 1:
|
|
c.Check(params.KeyFiles, DeepEquals, []string{
|
|
filepath.Join(boot.InitramfsBootEncryptionKeyDir, "ubuntu-data.sealed-key"),
|
|
})
|
|
c.Assert(params.ModelParams[0].KernelCmdlines, DeepEquals, []string{
|
|
fmt.Sprintf("snapd_recovery_mode=recover snapd_recovery_system=%s static cmdline", systemLabel),
|
|
"snapd_recovery_mode=recover snapd_recovery_system=20200825 static cmdline",
|
|
})
|
|
return nil
|
|
case 2:
|
|
c.Check(params.KeyFiles, DeepEquals, []string{
|
|
filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-data.recovery.sealed-key"),
|
|
filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-save.recovery.sealed-key"),
|
|
})
|
|
c.Assert(params.ModelParams[0].KernelCmdlines, DeepEquals, []string{
|
|
fmt.Sprintf("snapd_recovery_mode=recover snapd_recovery_system=%s static cmdline", systemLabel),
|
|
"snapd_recovery_mode=recover snapd_recovery_system=20200825 static cmdline",
|
|
})
|
|
return tc.resealRecoveryKeyErr
|
|
case 3:
|
|
// run key boot chain is unchanged, so only recovery key boot chain is resealed
|
|
if tc.resealRecoveryKeyErr == nil {
|
|
c.Errorf("unexpected call to secboot.ResealKeys with count %v", resealCalls)
|
|
return fmt.Errorf("unexpected call")
|
|
}
|
|
c.Check(params.KeyFiles, DeepEquals, []string{
|
|
filepath.Join(boot.InitramfsBootEncryptionKeyDir, "ubuntu-data.sealed-key"),
|
|
})
|
|
c.Assert(params.ModelParams[0].KernelCmdlines, DeepEquals, []string{
|
|
"snapd_recovery_mode=recover snapd_recovery_system=20200825 static cmdline",
|
|
})
|
|
return nil
|
|
case 4:
|
|
if tc.resealRecoveryKeyErr == nil {
|
|
c.Errorf("unexpected call to secboot.ResealKeys with count %v", resealCalls)
|
|
return fmt.Errorf("unexpected call")
|
|
}
|
|
c.Check(params.KeyFiles, DeepEquals, []string{
|
|
filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-data.recovery.sealed-key"),
|
|
filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-save.recovery.sealed-key"),
|
|
})
|
|
c.Assert(params.ModelParams[0].KernelCmdlines, DeepEquals, []string{
|
|
"snapd_recovery_mode=recover snapd_recovery_system=20200825 static cmdline",
|
|
})
|
|
return tc.resealRecoveryKeyDuringCleanupErr
|
|
default:
|
|
c.Errorf("unexpected call to secboot.ResealKeys with count %v", resealCalls)
|
|
return fmt.Errorf("unexpected call")
|
|
}
|
|
})
|
|
defer restore()
|
|
|
|
err := boot.PromoteTriedRecoverySystem(s.uc20dev, systemLabel, tc.triedSystems)
|
|
if tc.expectedErr == "" {
|
|
c.Assert(err, IsNil)
|
|
} else {
|
|
c.Assert(err, ErrorMatches, tc.expectedErr)
|
|
}
|
|
c.Check(readSeedSeenLabels, DeepEquals, tc.readSeedSystems)
|
|
c.Check(resealCalls, Equals, tc.resealCalls)
|
|
|
|
modeenvRead, err := boot.ReadModeenv("")
|
|
c.Assert(err, IsNil)
|
|
c.Check(modeenvRead.GoodRecoverySystems, DeepEquals, tc.expectedGoodSystemsList)
|
|
c.Check(modeenvRead.CurrentRecoverySystems, DeepEquals, tc.expectedCurrentSystemsList)
|
|
}
|
|
|
|
func (s *systemsSuite) TestPromoteTriedRecoverySystemHappy(c *C) {
|
|
s.testPromoteTriedRecoverySystem(c, "1234", recoverySystemGoodTestCase{
|
|
triedSystems: []string{"1234"},
|
|
|
|
resealCalls: 2,
|
|
|
|
readSeedSystems: []string{
|
|
// run key
|
|
"20200825", "1234",
|
|
// recovery keys
|
|
"20200825", "1234",
|
|
},
|
|
|
|
expectedCurrentSystemsList: []string{"20200825", "1234"},
|
|
expectedGoodSystemsList: []string{"20200825", "1234"},
|
|
})
|
|
}
|
|
|
|
func (s *systemsSuite) TestPromoteTriedRecoverySystemInCurrent(c *C) {
|
|
s.testPromoteTriedRecoverySystem(c, "1234", recoverySystemGoodTestCase{
|
|
triedSystems: []string{"1234"},
|
|
systemLabelAddToCurrent: true,
|
|
resealCalls: 2,
|
|
|
|
readSeedSystems: []string{
|
|
// run key
|
|
"20200825", "1234",
|
|
// recovery keys
|
|
"20200825", "1234",
|
|
},
|
|
expectedCurrentSystemsList: []string{"20200825", "1234"},
|
|
expectedGoodSystemsList: []string{"20200825", "1234"},
|
|
})
|
|
}
|
|
|
|
func (s *systemsSuite) TestPromoteTriedRecoverySystemPresentEverywhere(c *C) {
|
|
s.testPromoteTriedRecoverySystem(c, "1234", recoverySystemGoodTestCase{
|
|
triedSystems: []string{"1234"},
|
|
systemLabelAddToCurrent: true,
|
|
systemLabelAddToGood: true,
|
|
|
|
resealCalls: 2,
|
|
|
|
readSeedSystems: []string{
|
|
// run key
|
|
"20200825", "1234",
|
|
// recovery keys
|
|
"20200825", "1234",
|
|
},
|
|
expectedCurrentSystemsList: []string{"20200825", "1234"},
|
|
expectedGoodSystemsList: []string{"20200825", "1234"},
|
|
})
|
|
}
|
|
|
|
func (s *systemsSuite) TestPromoteTriedRecoverySystemResealFails(c *C) {
|
|
s.testPromoteTriedRecoverySystem(c, "1234", recoverySystemGoodTestCase{
|
|
triedSystems: []string{"1234"},
|
|
resealRecoveryKeyErr: fmt.Errorf("recovery key reseal mock failure"),
|
|
// no failure during cleanup
|
|
resealRecoveryKeyDuringCleanupErr: nil,
|
|
|
|
resealCalls: 4,
|
|
|
|
expectedErr: `cannot reseal the fallback encryption keys: recovery key reseal mock failure`,
|
|
|
|
readSeedSystems: []string{
|
|
// run key
|
|
"20200825", "1234",
|
|
// recovery keys
|
|
"20200825", "1234",
|
|
// cleanup run key reseal (the seed system is still in
|
|
// current-recovery-systems)
|
|
"20200825",
|
|
// cleanup recovery keys
|
|
"20200825",
|
|
},
|
|
expectedCurrentSystemsList: []string{"20200825"},
|
|
expectedGoodSystemsList: []string{"20200825"},
|
|
})
|
|
}
|
|
|
|
func (s *systemsSuite) TestPromoteTriedRecoverySystemResealUndoFails(c *C) {
|
|
s.testPromoteTriedRecoverySystem(c, "1234", recoverySystemGoodTestCase{
|
|
triedSystems: []string{"1234"},
|
|
resealRecoveryKeyErr: fmt.Errorf("recovery key reseal mock failure"),
|
|
resealRecoveryKeyDuringCleanupErr: fmt.Errorf("recovery key reseal mock fail in cleanup"),
|
|
|
|
resealCalls: 4,
|
|
|
|
expectedErr: `cannot reseal the fallback encryption keys: recovery key reseal mock failure \(cleanup failed: cannot reseal the fallback encryption keys: recovery key reseal mock fail in cleanup\)`,
|
|
|
|
readSeedSystems: []string{
|
|
// run key
|
|
"20200825", "1234",
|
|
// recovery keys
|
|
"20200825", "1234",
|
|
// cleanup run key reseal (the seed system is still in
|
|
// current-recovery-systems)
|
|
// cleanup run key
|
|
"20200825",
|
|
// cleanup recovery keys
|
|
"20200825",
|
|
},
|
|
expectedCurrentSystemsList: []string{"20200825"},
|
|
expectedGoodSystemsList: []string{"20200825"},
|
|
})
|
|
}
|
|
|
|
func (s *systemsSuite) TestPromoteTriedRecoverySystemNotTried(c *C) {
|
|
s.testPromoteTriedRecoverySystem(c, "1234", recoverySystemGoodTestCase{
|
|
triedSystems: []string{"not-here"},
|
|
|
|
expectedErr: `system has not been successfully tried`,
|
|
|
|
expectedCurrentSystemsList: []string{"20200825"},
|
|
expectedGoodSystemsList: []string{"20200825"},
|
|
})
|
|
|
|
// also works if tried systems list is nil
|
|
s.testPromoteTriedRecoverySystem(c, "1234", recoverySystemGoodTestCase{
|
|
triedSystems: nil,
|
|
|
|
expectedErr: `system has not been successfully tried`,
|
|
|
|
expectedCurrentSystemsList: []string{"20200825"},
|
|
expectedGoodSystemsList: []string{"20200825"},
|
|
})
|
|
}
|
|
|
|
type recoverySystemDropTestCase struct {
|
|
systemLabelAddToCurrent bool
|
|
systemLabelAddToGood bool
|
|
|
|
resealRecoveryKeyErr error
|
|
resealCalls int
|
|
expectedErr string
|
|
|
|
expectedCurrentSystemsList []string
|
|
expectedGoodSystemsList []string
|
|
}
|
|
|
|
func (s *systemsSuite) testDropRecoverySystem(c *C, systemLabel string, tc recoverySystemDropTestCase) {
|
|
mtbl := s.mockTrustedBootloaderWithAssetAndChains(c, s.runKernelBf, s.recoveryKernelBf)
|
|
mockAssetsCache(c, s.rootdir, "trusted", []string{
|
|
"asset-asset-hash-1",
|
|
})
|
|
|
|
bootloader.Force(mtbl)
|
|
defer bootloader.Force(nil)
|
|
|
|
// system is encrypted
|
|
s.stampSealedKeys(c, s.rootdir)
|
|
|
|
model := s.uc20dev.Model()
|
|
|
|
modeenv := &boot.Modeenv{
|
|
Mode: "run",
|
|
// keep this comment to make old gofmt happy
|
|
CurrentRecoverySystems: []string{"20200825"},
|
|
GoodRecoverySystems: []string{"20200825"},
|
|
CurrentKernels: []string{},
|
|
CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{
|
|
"asset": []string{"asset-hash-1"},
|
|
},
|
|
|
|
CurrentTrustedBootAssets: boot.BootAssetsMap{
|
|
"asset": []string{"asset-hash-1"},
|
|
},
|
|
|
|
Model: model.Model(),
|
|
BrandID: model.BrandID(),
|
|
Grade: string(model.Grade()),
|
|
ModelSignKeyID: model.SignKeyID(),
|
|
}
|
|
if tc.systemLabelAddToCurrent {
|
|
modeenv.CurrentRecoverySystems = append(modeenv.CurrentRecoverySystems, systemLabel)
|
|
}
|
|
if tc.systemLabelAddToGood {
|
|
modeenv.GoodRecoverySystems = append(modeenv.GoodRecoverySystems, systemLabel)
|
|
}
|
|
|
|
c.Assert(modeenv.WriteTo(""), IsNil)
|
|
|
|
var readSeedSeenLabels []string
|
|
restore := boot.MockSeedReadSystemEssential(func(seedDir, label string, essentialTypes []snap.Type, tm timings.Measurer) (*asserts.Model, []*seed.Snap, error) {
|
|
// the mock bootloader can only mock a single recovery boot
|
|
// chain, so pretend both seeds use the same kernel, but keep track of the labels
|
|
readSeedSeenLabels = append(readSeedSeenLabels, label)
|
|
return model, []*seed.Snap{s.seedKernelSnap, s.seedGadgetSnap}, nil
|
|
})
|
|
defer restore()
|
|
|
|
resealCalls := 0
|
|
restore = boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error {
|
|
resealCalls++
|
|
c.Assert(params, NotNil)
|
|
c.Assert(params.ModelParams, HasLen, 1)
|
|
switch resealCalls {
|
|
case 1:
|
|
c.Check(params.KeyFiles, DeepEquals, []string{
|
|
filepath.Join(boot.InitramfsBootEncryptionKeyDir, "ubuntu-data.sealed-key"),
|
|
})
|
|
c.Assert(params.ModelParams[0].KernelCmdlines, DeepEquals, []string{
|
|
"snapd_recovery_mode=recover snapd_recovery_system=20200825 static cmdline",
|
|
})
|
|
return nil
|
|
case 2:
|
|
c.Check(params.KeyFiles, DeepEquals, []string{
|
|
filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-data.recovery.sealed-key"),
|
|
filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-save.recovery.sealed-key"),
|
|
})
|
|
c.Assert(params.ModelParams[0].KernelCmdlines, DeepEquals, []string{
|
|
"snapd_recovery_mode=recover snapd_recovery_system=20200825 static cmdline",
|
|
})
|
|
return tc.resealRecoveryKeyErr
|
|
default:
|
|
c.Errorf("unexpected call to secboot.ResealKeys with count %v", resealCalls)
|
|
return fmt.Errorf("unexpected call")
|
|
}
|
|
})
|
|
defer restore()
|
|
|
|
err := boot.DropRecoverySystem(s.uc20dev, systemLabel)
|
|
if tc.expectedErr == "" {
|
|
c.Assert(err, IsNil)
|
|
} else {
|
|
c.Assert(err, ErrorMatches, tc.expectedErr)
|
|
}
|
|
c.Check(readSeedSeenLabels, DeepEquals, []string{"20200825", "20200825"})
|
|
c.Check(resealCalls, Equals, tc.resealCalls)
|
|
|
|
modeenvRead, err := boot.ReadModeenv("")
|
|
c.Assert(err, IsNil)
|
|
// current is unchanged
|
|
c.Check(modeenvRead.GoodRecoverySystems, DeepEquals, tc.expectedCurrentSystemsList)
|
|
c.Check(modeenvRead.CurrentRecoverySystems, DeepEquals, tc.expectedGoodSystemsList)
|
|
}
|
|
|
|
func (s *systemsSuite) TestDropRecoverySystemHappy(c *C) {
|
|
s.testDropRecoverySystem(c, "1234", recoverySystemDropTestCase{
|
|
systemLabelAddToCurrent: true,
|
|
systemLabelAddToGood: true,
|
|
resealCalls: 2,
|
|
|
|
expectedGoodSystemsList: []string{"20200825"},
|
|
expectedCurrentSystemsList: []string{"20200825"},
|
|
})
|
|
}
|
|
|
|
func (s *systemsSuite) TestDropRecoverySystemAlreadyGoneFromBoth(c *C) {
|
|
s.testDropRecoverySystem(c, "1234", recoverySystemDropTestCase{
|
|
systemLabelAddToCurrent: false,
|
|
systemLabelAddToGood: false,
|
|
resealCalls: 2,
|
|
|
|
expectedGoodSystemsList: []string{"20200825"},
|
|
expectedCurrentSystemsList: []string{"20200825"},
|
|
})
|
|
}
|
|
|
|
func (s *systemsSuite) TestDropRecoverySystemAlreadyGoneOne(c *C) {
|
|
s.testDropRecoverySystem(c, "1234", recoverySystemDropTestCase{
|
|
systemLabelAddToCurrent: true,
|
|
systemLabelAddToGood: false,
|
|
resealCalls: 2,
|
|
|
|
expectedGoodSystemsList: []string{"20200825"},
|
|
expectedCurrentSystemsList: []string{"20200825"},
|
|
})
|
|
}
|
|
|
|
func (s *systemsSuite) TestDropRecoverySystemResealErr(c *C) {
|
|
s.testDropRecoverySystem(c, "1234", recoverySystemDropTestCase{
|
|
systemLabelAddToCurrent: true,
|
|
systemLabelAddToGood: false,
|
|
resealCalls: 2,
|
|
resealRecoveryKeyErr: fmt.Errorf("mocked error"),
|
|
expectedErr: `cannot reseal the fallback encryption keys: mocked error`,
|
|
|
|
expectedGoodSystemsList: []string{"20200825"},
|
|
expectedCurrentSystemsList: []string{"20200825"},
|
|
})
|
|
}
|
|
|
|
func (s *systemsSuite) TestMarkRecoveryCapableSystemHappy(c *C) {
|
|
rbl := bootloadertest.Mock("recovery", c.MkDir()).RecoveryAware()
|
|
bootloader.Force(rbl)
|
|
|
|
err := boot.MarkRecoveryCapableSystem("1234")
|
|
c.Assert(err, IsNil)
|
|
vars, err := rbl.GetBootVars("snapd_good_recovery_systems")
|
|
c.Assert(err, IsNil)
|
|
c.Check(vars, DeepEquals, map[string]string{
|
|
"snapd_good_recovery_systems": "1234",
|
|
})
|
|
// try the same system again
|
|
err = boot.MarkRecoveryCapableSystem("1234")
|
|
c.Assert(err, IsNil)
|
|
vars, err = rbl.GetBootVars("snapd_good_recovery_systems")
|
|
c.Assert(err, IsNil)
|
|
c.Check(vars, DeepEquals, map[string]string{
|
|
// still a single entry
|
|
"snapd_good_recovery_systems": "1234",
|
|
})
|
|
|
|
// try something new
|
|
err = boot.MarkRecoveryCapableSystem("4567")
|
|
c.Assert(err, IsNil)
|
|
vars, err = rbl.GetBootVars("snapd_good_recovery_systems")
|
|
c.Assert(err, IsNil)
|
|
c.Check(vars, DeepEquals, map[string]string{
|
|
// entry added
|
|
"snapd_good_recovery_systems": "1234,4567",
|
|
})
|
|
|
|
// try adding the old one again
|
|
err = boot.MarkRecoveryCapableSystem("1234")
|
|
c.Assert(err, IsNil)
|
|
vars, err = rbl.GetBootVars("snapd_good_recovery_systems")
|
|
c.Assert(err, IsNil)
|
|
c.Check(vars, DeepEquals, map[string]string{
|
|
// system got moved to the end of the list
|
|
"snapd_good_recovery_systems": "4567,1234",
|
|
})
|
|
|
|
// and the new one again
|
|
err = boot.MarkRecoveryCapableSystem("4567")
|
|
c.Assert(err, IsNil)
|
|
vars, err = rbl.GetBootVars("snapd_good_recovery_systems")
|
|
c.Assert(err, IsNil)
|
|
c.Check(vars, DeepEquals, map[string]string{
|
|
// and it became the last entry
|
|
"snapd_good_recovery_systems": "1234,4567",
|
|
})
|
|
}
|
|
|
|
func (s *systemsSuite) TestMarkRecoveryCapableSystemAlwaysLast(c *C) {
|
|
rbl := bootloadertest.Mock("recovery", c.MkDir()).RecoveryAware()
|
|
bootloader.Force(rbl)
|
|
|
|
err := rbl.SetBootVars(map[string]string{
|
|
"snapd_good_recovery_systems": "1234,2222",
|
|
})
|
|
c.Assert(err, IsNil)
|
|
|
|
err = boot.MarkRecoveryCapableSystem("1234")
|
|
c.Assert(err, IsNil)
|
|
vars, err := rbl.GetBootVars("snapd_good_recovery_systems")
|
|
c.Assert(err, IsNil)
|
|
c.Check(vars, DeepEquals, map[string]string{
|
|
"snapd_good_recovery_systems": "2222,1234",
|
|
})
|
|
|
|
err = rbl.SetBootVars(map[string]string{
|
|
"snapd_good_recovery_systems": "1111,1234,2222",
|
|
})
|
|
c.Assert(err, IsNil)
|
|
err = boot.MarkRecoveryCapableSystem("1234")
|
|
c.Assert(err, IsNil)
|
|
vars, err = rbl.GetBootVars("snapd_good_recovery_systems")
|
|
c.Assert(err, IsNil)
|
|
c.Check(vars, DeepEquals, map[string]string{
|
|
"snapd_good_recovery_systems": "1111,2222,1234",
|
|
})
|
|
|
|
err = rbl.SetBootVars(map[string]string{
|
|
"snapd_good_recovery_systems": "1111,2222",
|
|
})
|
|
c.Assert(err, IsNil)
|
|
err = boot.MarkRecoveryCapableSystem("1234")
|
|
c.Assert(err, IsNil)
|
|
vars, err = rbl.GetBootVars("snapd_good_recovery_systems")
|
|
c.Assert(err, IsNil)
|
|
c.Check(vars, DeepEquals, map[string]string{
|
|
"snapd_good_recovery_systems": "1111,2222,1234",
|
|
})
|
|
}
|
|
|
|
func (s *systemsSuite) TestMarkRecoveryCapableSystemErr(c *C) {
|
|
rbl := bootloadertest.Mock("recovery", c.MkDir()).RecoveryAware()
|
|
bootloader.Force(rbl)
|
|
|
|
err := boot.MarkRecoveryCapableSystem("1234")
|
|
c.Assert(err, IsNil)
|
|
vars, err := rbl.GetBootVars("snapd_good_recovery_systems")
|
|
c.Assert(err, IsNil)
|
|
c.Check(vars, DeepEquals, map[string]string{
|
|
"snapd_good_recovery_systems": "1234",
|
|
})
|
|
|
|
rbl.SetErr = fmt.Errorf("mocked error")
|
|
err = boot.MarkRecoveryCapableSystem("4567")
|
|
c.Assert(err, ErrorMatches, "mocked error")
|
|
vars, err = rbl.GetBootVars("snapd_good_recovery_systems")
|
|
c.Assert(err, IsNil)
|
|
c.Check(vars, DeepEquals, map[string]string{
|
|
// mocked error is returned after variable is set
|
|
"snapd_good_recovery_systems": "1234,4567",
|
|
})
|
|
|
|
// but mocked panic happens earlier
|
|
rbl.SetMockToPanic("SetBootVars")
|
|
c.Assert(func() { boot.MarkRecoveryCapableSystem("9999") },
|
|
PanicMatches, "mocked reboot panic in SetBootVars")
|
|
vars, err = rbl.GetBootVars("snapd_good_recovery_systems")
|
|
c.Assert(err, IsNil)
|
|
c.Check(vars, DeepEquals, map[string]string{
|
|
"snapd_good_recovery_systems": "1234,4567",
|
|
})
|
|
|
|
}
|
|
|
|
func (s *systemsSuite) TestMarkRecoveryCapableSystemNonRecoveryAware(c *C) {
|
|
bl := bootloadertest.Mock("recovery", c.MkDir())
|
|
bootloader.Force(bl)
|
|
|
|
err := boot.MarkRecoveryCapableSystem("1234")
|
|
c.Assert(err, IsNil)
|
|
c.Check(bl.SetBootVarsCalls, Equals, 0)
|
|
}
|
|
|
|
type initramfsMarkTryRecoverySystemSuite struct {
|
|
baseSystemsSuite
|
|
|
|
bl *bootloadertest.MockBootloader
|
|
}
|
|
|
|
var _ = Suite(&initramfsMarkTryRecoverySystemSuite{})
|
|
|
|
func (s *initramfsMarkTryRecoverySystemSuite) SetUpTest(c *C) {
|
|
s.baseSystemsSuite.SetUpTest(c)
|
|
|
|
s.bl = bootloadertest.Mock("bootloader", s.bootdir)
|
|
bootloader.Force(s.bl)
|
|
s.AddCleanup(func() { bootloader.Force(nil) })
|
|
}
|
|
|
|
func (s *initramfsMarkTryRecoverySystemSuite) testMarkRecoverySystemForRun(c *C, outcome boot.TryRecoverySystemOutcome, expectingStatus string) {
|
|
err := s.bl.SetBootVars(map[string]string{
|
|
"recovery_system_status": "try",
|
|
"try_recovery_system": "1234",
|
|
})
|
|
c.Assert(err, IsNil)
|
|
err = boot.EnsureNextBootToRunModeWithTryRecoverySystemOutcome(outcome)
|
|
c.Assert(err, IsNil)
|
|
|
|
expectedVars := map[string]string{
|
|
"snapd_recovery_mode": "run",
|
|
"snapd_recovery_system": "",
|
|
|
|
"recovery_system_status": expectingStatus,
|
|
"try_recovery_system": "1234",
|
|
}
|
|
|
|
vars, err := s.bl.GetBootVars("snapd_recovery_mode", "snapd_recovery_system",
|
|
"recovery_system_status", "try_recovery_system")
|
|
c.Assert(err, IsNil)
|
|
c.Check(vars, DeepEquals, expectedVars)
|
|
|
|
err = s.bl.SetBootVars(map[string]string{
|
|
// the status is overwritten, even if it's completely bogus
|
|
"recovery_system_status": "foobar",
|
|
"try_recovery_system": "1234",
|
|
})
|
|
c.Assert(err, IsNil)
|
|
|
|
err = boot.EnsureNextBootToRunModeWithTryRecoverySystemOutcome(outcome)
|
|
c.Assert(err, IsNil)
|
|
|
|
vars, err = s.bl.GetBootVars("snapd_recovery_mode", "snapd_recovery_system",
|
|
"recovery_system_status", "try_recovery_system")
|
|
c.Assert(err, IsNil)
|
|
c.Check(vars, DeepEquals, expectedVars)
|
|
}
|
|
|
|
func (s *initramfsMarkTryRecoverySystemSuite) TestMarkTryRecoverySystemSuccess(c *C) {
|
|
s.testMarkRecoverySystemForRun(c, boot.TryRecoverySystemOutcomeSuccess, "tried")
|
|
}
|
|
|
|
func (s *initramfsMarkTryRecoverySystemSuite) TestMarkRecoverySystemFailure(c *C) {
|
|
s.testMarkRecoverySystemForRun(c, boot.TryRecoverySystemOutcomeFailure, "try")
|
|
}
|
|
|
|
func (s *initramfsMarkTryRecoverySystemSuite) TestMarkRecoverySystemBogus(c *C) {
|
|
s.testMarkRecoverySystemForRun(c, boot.TryRecoverySystemOutcomeInconsistent, "")
|
|
}
|
|
|
|
func (s *initramfsMarkTryRecoverySystemSuite) TestMarkRecoverySystemErr(c *C) {
|
|
s.bl.SetErr = fmt.Errorf("set fails")
|
|
err := boot.EnsureNextBootToRunModeWithTryRecoverySystemOutcome(boot.TryRecoverySystemOutcomeSuccess)
|
|
c.Assert(err, ErrorMatches, "set fails")
|
|
err = boot.EnsureNextBootToRunModeWithTryRecoverySystemOutcome(boot.TryRecoverySystemOutcomeFailure)
|
|
c.Assert(err, ErrorMatches, "set fails")
|
|
err = boot.EnsureNextBootToRunModeWithTryRecoverySystemOutcome(boot.TryRecoverySystemOutcomeInconsistent)
|
|
c.Assert(err, ErrorMatches, "set fails")
|
|
}
|
|
|
|
func (s *initramfsMarkTryRecoverySystemSuite) TestTryingRecoverySystemUnset(c *C) {
|
|
err := s.bl.SetBootVars(map[string]string{
|
|
"recovery_system_status": "try",
|
|
// system is unset
|
|
"try_recovery_system": "",
|
|
})
|
|
c.Assert(err, IsNil)
|
|
isTry, err := boot.InitramfsIsTryingRecoverySystem("1234")
|
|
c.Assert(err, ErrorMatches, `try recovery system is unset but status is "try"`)
|
|
c.Check(boot.IsInconsistentRecoverySystemState(err), Equals, true)
|
|
c.Check(isTry, Equals, false)
|
|
}
|
|
|
|
func (s *initramfsMarkTryRecoverySystemSuite) TestTryingRecoverySystemBogus(c *C) {
|
|
err := s.bl.SetBootVars(map[string]string{
|
|
"recovery_system_status": "foobar",
|
|
"try_recovery_system": "1234",
|
|
})
|
|
c.Assert(err, IsNil)
|
|
isTry, err := boot.InitramfsIsTryingRecoverySystem("1234")
|
|
c.Assert(err, ErrorMatches, `unexpected recovery system status "foobar"`)
|
|
c.Check(boot.IsInconsistentRecoverySystemState(err), Equals, true)
|
|
c.Check(isTry, Equals, false)
|
|
|
|
// errors out even if try recovery system label is unset
|
|
err = s.bl.SetBootVars(map[string]string{
|
|
"recovery_system_status": "no-label",
|
|
"try_recovery_system": "",
|
|
})
|
|
c.Assert(err, IsNil)
|
|
isTry, err = boot.InitramfsIsTryingRecoverySystem("1234")
|
|
c.Assert(err, ErrorMatches, `unexpected recovery system status "no-label"`)
|
|
c.Check(boot.IsInconsistentRecoverySystemState(err), Equals, true)
|
|
c.Check(isTry, Equals, false)
|
|
}
|
|
|
|
func (s *initramfsMarkTryRecoverySystemSuite) TestTryingRecoverySystemNoTryingStatus(c *C) {
|
|
err := s.bl.SetBootVars(map[string]string{
|
|
"recovery_system_status": "",
|
|
"try_recovery_system": "",
|
|
})
|
|
c.Assert(err, IsNil)
|
|
isTry, err := boot.InitramfsIsTryingRecoverySystem("1234")
|
|
c.Assert(err, IsNil)
|
|
c.Check(isTry, Equals, false)
|
|
|
|
err = s.bl.SetBootVars(map[string]string{
|
|
// status is checked first
|
|
"recovery_system_status": "",
|
|
"try_recovery_system": "1234",
|
|
})
|
|
c.Assert(err, IsNil)
|
|
isTry, err = boot.InitramfsIsTryingRecoverySystem("1234")
|
|
c.Assert(err, IsNil)
|
|
c.Check(isTry, Equals, false)
|
|
}
|
|
|
|
func (s *initramfsMarkTryRecoverySystemSuite) TestTryingRecoverySystemSameSystem(c *C) {
|
|
// the usual scenario
|
|
err := s.bl.SetBootVars(map[string]string{
|
|
"recovery_system_status": "try",
|
|
"try_recovery_system": "1234",
|
|
})
|
|
c.Assert(err, IsNil)
|
|
isTry, err := boot.InitramfsIsTryingRecoverySystem("1234")
|
|
c.Assert(err, IsNil)
|
|
c.Check(isTry, Equals, true)
|
|
|
|
// pretend the system has already been tried
|
|
err = s.bl.SetBootVars(map[string]string{
|
|
"recovery_system_status": "tried",
|
|
"try_recovery_system": "1234",
|
|
})
|
|
c.Assert(err, IsNil)
|
|
isTry, err = boot.InitramfsIsTryingRecoverySystem("1234")
|
|
c.Assert(err, IsNil)
|
|
c.Check(isTry, Equals, true)
|
|
}
|
|
|
|
func (s *initramfsMarkTryRecoverySystemSuite) TestRecoverySystemSuccessDifferent(c *C) {
|
|
// other system
|
|
err := s.bl.SetBootVars(map[string]string{
|
|
"recovery_system_status": "try",
|
|
"try_recovery_system": "9999",
|
|
})
|
|
c.Assert(err, IsNil)
|
|
isTry, err := boot.InitramfsIsTryingRecoverySystem("1234")
|
|
c.Assert(err, IsNil)
|
|
c.Check(isTry, Equals, false)
|
|
|
|
// same when the other system has already been tried
|
|
err = s.bl.SetBootVars(map[string]string{
|
|
"recovery_system_status": "tried",
|
|
"try_recovery_system": "9999",
|
|
})
|
|
c.Assert(err, IsNil)
|
|
isTry, err = boot.InitramfsIsTryingRecoverySystem("1234")
|
|
c.Assert(err, IsNil)
|
|
c.Check(isTry, Equals, false)
|
|
}
|