Skip to content

Commit

Permalink
c/snap-bootstrap: split CVM related functionality in separate files (#…
Browse files Browse the repository at this point in the history
…14789)

* c/snap-bootstrap: split CVM related functionality in separate files

* snap-bootstrap: add doc for CVM mode and address comment
  • Loading branch information
sespiros authored Dec 5, 2024
1 parent d3134e6 commit e9ca771
Show file tree
Hide file tree
Showing 4 changed files with 284 additions and 203 deletions.
75 changes: 0 additions & 75 deletions cmd/snap-bootstrap/cmd_initramfs_mounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@ var (
snap.TypeSnapd: "snapd",
}

secbootProvisionForCVM func(initramfsUbuntuSeedDir string) error
secbootMeasureSnapSystemEpochWhenPossible func() error
secbootMeasureSnapModelWhenPossible func(findModel func() (*asserts.Model, error)) error
secbootUnlockVolumeUsingSealedKeyIfEncrypted func(disk disks.Disk, name string, encryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error)
Expand Down Expand Up @@ -1905,80 +1904,6 @@ func maybeMountSave(disk disks.Disk, rootdir string, encrypted bool, mountOpts *
return true, nil
}

// XXX: workaround for the lack of model in CVM systems
type genericCVMModel struct{}

func (*genericCVMModel) Classic() bool {
return true
}

func (*genericCVMModel) Grade() asserts.ModelGrade {
return "signed"
}

func generateMountsModeRunCVM(mst *initramfsMountsState) error {
// Mount ESP as UbuntuSeedDir which has UEFI label
if err := mountNonDataPartitionMatchingKernelDisk(boot.InitramfsUbuntuSeedDir, "UEFI"); err != nil {
return err
}

// get the disk that we mounted the ESP from as a reference
// point for future mounts
disk, err := disks.DiskFromMountPoint(boot.InitramfsUbuntuSeedDir, nil)
if err != nil {
return err
}

// Mount rootfs
if err := secbootProvisionForCVM(boot.InitramfsUbuntuSeedDir); err != nil {
return err
}
runModeCVMKey := filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "cloudimg-rootfs.sealed-key")
opts := &secboot.UnlockVolumeUsingSealedKeyOptions{
AllowRecoveryKey: true,
}
unlockRes, err := secbootUnlockVolumeUsingSealedKeyIfEncrypted(disk, "cloudimg-rootfs", runModeCVMKey, opts)
if err != nil {
return err
}
fsckSystemdOpts := &systemdMountOptions{
NeedsFsck: true,
Ephemeral: true,
}
if err := doSystemdMount(unlockRes.FsDevice, boot.InitramfsDataDir, fsckSystemdOpts); err != nil {
return err
}

// Verify that cloudimg-rootfs comes from where we expect it to
diskOpts := &disks.Options{}
if unlockRes.IsEncrypted {
// then we need to specify that the data mountpoint is
// expected to be a decrypted device
diskOpts.IsDecryptedDevice = true
}

matches, err := disk.MountPointIsFromDisk(boot.InitramfsDataDir, diskOpts)
if err != nil {
return err
}
if !matches {
// failed to verify that cloudimg-rootfs mountpoint
// comes from the same disk as ESP
return fmt.Errorf("cannot validate boot: cloudimg-rootfs mountpoint is expected to be from disk %s but is not", disk.Dev())
}

// Unmount ESP because otherwise unmounting is racy and results in booted systems without ESP
if err := doSystemdMount("", boot.InitramfsUbuntuSeedDir, &systemdMountOptions{Umount: true, Ephemeral: true}); err != nil {
return err
}

// There is no real model on a CVM device but minimal model
// information is required by the later code
mst.SetVerifiedBootModel(&genericCVMModel{})

return nil
}

func generateMountsModeRun(mst *initramfsMountsState) error {
// 1. mount ubuntu-boot
if err := mountNonDataPartitionMatchingKernelDisk(boot.InitramfsUbuntuBootDir, "ubuntu-boot"); err != nil {
Expand Down
111 changes: 111 additions & 0 deletions cmd/snap-bootstrap/cmd_initramfs_mounts_cvm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2019-2024 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 main

import (
"fmt"
"path/filepath"

"github.com/snapcore/snapd/asserts"
"github.com/snapcore/snapd/boot"
"github.com/snapcore/snapd/osutil/disks"
"github.com/snapcore/snapd/secboot"
)

var (
secbootProvisionForCVM func(initramfsUbuntuSeedDir string) error
)

// XXX: workaround for the lack of model in CVM systems
type genericCVMModel struct{}

func (*genericCVMModel) Classic() bool {
return true
}

func (*genericCVMModel) Grade() asserts.ModelGrade {
return "signed"
}

// generateMountsModeRunCVM is used to generate mounts for the special "cloudimg-rootfs" mode which
// mounts the rootfs from a partition on the disk rather than a base snap. It supports TPM-backed FDE
// for the rootfs partition using a sealed key from the seed partition.
func generateMountsModeRunCVM(mst *initramfsMountsState) error {
// Mount ESP as UbuntuSeedDir which has UEFI label
if err := mountNonDataPartitionMatchingKernelDisk(boot.InitramfsUbuntuSeedDir, "UEFI"); err != nil {
return err
}

// get the disk that we mounted the ESP from as a reference
// point for future mounts
disk, err := disks.DiskFromMountPoint(boot.InitramfsUbuntuSeedDir, nil)
if err != nil {
return err
}

// Mount rootfs
if err := secbootProvisionForCVM(boot.InitramfsUbuntuSeedDir); err != nil {
return err
}
runModeCVMKey := filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "cloudimg-rootfs.sealed-key")
opts := &secboot.UnlockVolumeUsingSealedKeyOptions{
AllowRecoveryKey: true,
}
unlockRes, err := secbootUnlockVolumeUsingSealedKeyIfEncrypted(disk, "cloudimg-rootfs", runModeCVMKey, opts)
if err != nil {
return err
}
fsckSystemdOpts := &systemdMountOptions{
NeedsFsck: true,
Ephemeral: true,
}
if err := doSystemdMount(unlockRes.FsDevice, boot.InitramfsDataDir, fsckSystemdOpts); err != nil {
return err
}

// Verify that cloudimg-rootfs comes from where we expect it to
diskOpts := &disks.Options{}
if unlockRes.IsEncrypted {
// then we need to specify that the data mountpoint is
// expected to be a decrypted device
diskOpts.IsDecryptedDevice = true
}

matches, err := disk.MountPointIsFromDisk(boot.InitramfsDataDir, diskOpts)
if err != nil {
return err
}
if !matches {
// failed to verify that cloudimg-rootfs mountpoint
// comes from the same disk as ESP
return fmt.Errorf("cannot validate boot: cloudimg-rootfs mountpoint is expected to be from disk %s but is not", disk.Dev())
}

// Unmount ESP because otherwise unmounting is racy and results in booted systems without ESP
if err := doSystemdMount("", boot.InitramfsUbuntuSeedDir, &systemdMountOptions{Umount: true, Ephemeral: true}); err != nil {
return err
}

// There is no real model on a CVM device but minimal model
// information is required by the later code
mst.SetVerifiedBootModel(&genericCVMModel{})

return nil
}
173 changes: 173 additions & 0 deletions cmd/snap-bootstrap/cmd_initramfs_mounts_cvm_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2019-2024 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 main_test

import (
"fmt"
"path/filepath"

. "gopkg.in/check.v1"

"github.com/snapcore/snapd/boot"
main "github.com/snapcore/snapd/cmd/snap-bootstrap"
"github.com/snapcore/snapd/osutil/disks"
"github.com/snapcore/snapd/secboot"
"github.com/snapcore/snapd/testutil"
)

var (
cvmEncPart = disks.Partition{
FilesystemLabel: "cloudimg-rootfs-enc",
PartitionUUID: "cloudimg-rootfs-enc-partuuid",
KernelDeviceNode: "/dev/sda1",
}

defaultCVMDisk = &disks.MockDiskMapping{
Structure: []disks.Partition{
seedPart,
cvmEncPart,
},
DiskHasPartitions: true,
DevNum: "defaultCVMDev",
}
)

type initramfsCVMMountsSuite struct {
baseInitramfsMountsSuite
}

var _ = Suite(&initramfsCVMMountsSuite{})

func (s *initramfsCVMMountsSuite) SetUpTest(c *C) {
s.baseInitramfsMountsSuite.SetUpTest(c)
s.AddCleanup(main.MockSecbootProvisionForCVM(func(_ string) error {
return nil
}))
}

func (s *initramfsCVMMountsSuite) TestInitramfsMountsRunCVMModeHappy(c *C) {
s.mockProcCmdlineContent(c, "snapd_recovery_mode=cloudimg-rootfs")

restore := main.MockPartitionUUIDForBootedKernelDisk("specific-ubuntu-seed-partuuid")
defer restore()

restore = disks.MockMountPointDisksToPartitionMapping(
map[disks.Mountpoint]*disks.MockDiskMapping{
{Mountpoint: boot.InitramfsUbuntuSeedDir}: defaultCVMDisk,
{Mountpoint: boot.InitramfsDataDir, IsDecryptedDevice: true}: defaultCVMDisk,
},
)
defer restore()

// don't do anything from systemd-mount, we verify the arguments passed at
// the end with cmd.Calls
cmd := testutil.MockCommand(c, "systemd-mount", ``)
defer cmd.Restore()

// mock that in turn, /run/mnt/ubuntu-boot, /run/mnt/ubuntu-seed, etc. are
// mounted
n := 0
restore = main.MockOsutilIsMounted(func(where string) (bool, error) {
n++
switch n {
// first call for each mount returns false, then returns true, this
// tests in the case where systemd is racy / inconsistent and things
// aren't mounted by the time systemd-mount returns
case 1, 2:
c.Assert(where, Equals, boot.InitramfsUbuntuSeedDir)
case 3, 4:
c.Assert(where, Equals, boot.InitramfsDataDir)
case 5, 6:
c.Assert(where, Equals, boot.InitramfsUbuntuSeedDir)
default:
c.Errorf("unexpected IsMounted check on %s", where)
return false, fmt.Errorf("unexpected IsMounted check on %s", where)
}
return n%2 == 0, nil
})
defer restore()

// Mock the call to TPMCVM, to ensure that TPM provisioning is
// done before unlock attempt
provisionTPMCVMCalled := false
restore = main.MockSecbootProvisionForCVM(func(_ string) error {
// Ensure this function is only called once
c.Assert(provisionTPMCVMCalled, Equals, false)
provisionTPMCVMCalled = true
return nil
})
defer restore()

cloudimgActivated := false
restore = main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(func(disk disks.Disk, name string, sealedEncryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) {
c.Assert(provisionTPMCVMCalled, Equals, true)
c.Assert(name, Equals, "cloudimg-rootfs")
c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-seed/device/fde/cloudimg-rootfs.sealed-key"))
c.Assert(opts.AllowRecoveryKey, Equals, true)
c.Assert(opts.WhichModel, IsNil)

cloudimgActivated = true
// return true because we are using an encrypted device
return happyUnlocked("cloudimg-rootfs", secboot.UnlockedWithSealedKey), nil
})
defer restore()

_, err := main.Parser().ParseArgs([]string{"initramfs-mounts"})
c.Assert(err, IsNil)
c.Check(s.Stdout.String(), Equals, "")

// 2 per mountpoint + 1 more for cross check
c.Assert(n, Equals, 5)

// failed to use mockSystemdMountSequence way of asserting this
// note that other test cases also mix & match using
// mockSystemdMountSequence & DeepEquals
c.Assert(cmd.Calls(), DeepEquals, [][]string{
{
"systemd-mount",
"/dev/disk/by-partuuid/specific-ubuntu-seed-partuuid",
boot.InitramfsUbuntuSeedDir,
"--no-pager",
"--no-ask-password",
"--fsck=yes",
"--options=private",
"--property=Before=initrd-fs.target",
},
{
"systemd-mount",
"/dev/mapper/cloudimg-rootfs-random",
boot.InitramfsDataDir,
"--no-pager",
"--no-ask-password",
"--fsck=yes",
},
{
"systemd-mount",
boot.InitramfsUbuntuSeedDir,
"--umount",
"--no-pager",
"--no-ask-password",
"--fsck=no",
},
})

c.Check(provisionTPMCVMCalled, Equals, true)
c.Check(cloudimgActivated, Equals, true)
}
Loading

0 comments on commit e9ca771

Please sign in to comment.