Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"

Check warning on line 44 in cmd/snap-bootstrap/cmd_initramfs_mounts_cvm.go

View check run for this annotation

Codecov / codecov/patch

cmd/snap-bootstrap/cmd_initramfs_mounts_cvm.go#L43-L44

Added lines #L43 - L44 were not covered by tests
}

// 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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably a good idea to have a doc comment attached to this one

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added.

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

Check warning on line 54 in cmd/snap-bootstrap/cmd_initramfs_mounts_cvm.go

View check run for this annotation

Codecov / codecov/patch

cmd/snap-bootstrap/cmd_initramfs_mounts_cvm.go#L53-L54

Added lines #L53 - L54 were not covered by tests

// 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
}

Check warning on line 61 in cmd/snap-bootstrap/cmd_initramfs_mounts_cvm.go

View check run for this annotation

Codecov / codecov/patch

cmd/snap-bootstrap/cmd_initramfs_mounts_cvm.go#L60-L61

Added lines #L60 - L61 were not covered by tests

// Mount rootfs
if err := secbootProvisionForCVM(boot.InitramfsUbuntuSeedDir); err != nil {
return err
}

Check warning on line 66 in cmd/snap-bootstrap/cmd_initramfs_mounts_cvm.go

View check run for this annotation

Codecov / codecov/patch

cmd/snap-bootstrap/cmd_initramfs_mounts_cvm.go#L65-L66

Added lines #L65 - L66 were not covered by tests
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
}

Check warning on line 74 in cmd/snap-bootstrap/cmd_initramfs_mounts_cvm.go

View check run for this annotation

Codecov / codecov/patch

cmd/snap-bootstrap/cmd_initramfs_mounts_cvm.go#L73-L74

Added lines #L73 - L74 were not covered by tests
fsckSystemdOpts := &systemdMountOptions{
NeedsFsck: true,
Ephemeral: true,
}
if err := doSystemdMount(unlockRes.FsDevice, boot.InitramfsDataDir, fsckSystemdOpts); err != nil {
return err
}

Check warning on line 81 in cmd/snap-bootstrap/cmd_initramfs_mounts_cvm.go

View check run for this annotation

Codecov / codecov/patch

cmd/snap-bootstrap/cmd_initramfs_mounts_cvm.go#L80-L81

Added lines #L80 - L81 were not covered by tests

// 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
}

Check warning on line 94 in cmd/snap-bootstrap/cmd_initramfs_mounts_cvm.go

View check run for this annotation

Codecov / codecov/patch

cmd/snap-bootstrap/cmd_initramfs_mounts_cvm.go#L93-L94

Added lines #L93 - L94 were not covered by tests
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())
}

Check warning on line 99 in cmd/snap-bootstrap/cmd_initramfs_mounts_cvm.go

View check run for this annotation

Codecov / codecov/patch

cmd/snap-bootstrap/cmd_initramfs_mounts_cvm.go#L96-L99

Added lines #L96 - L99 were not covered by tests

// 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
}

Check warning on line 104 in cmd/snap-bootstrap/cmd_initramfs_mounts_cvm.go

View check run for this annotation

Codecov / codecov/patch

cmd/snap-bootstrap/cmd_initramfs_mounts_cvm.go#L103-L104

Added lines #L103 - L104 were not covered by tests

// 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,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we move cvmEncPart to this file too?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I missed that one. Added.

},
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
Loading