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 1 commit
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
108 changes: 108 additions & 0 deletions cmd/snap-bootstrap/cmd_initramfs_mounts_cvm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// -*- 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
}

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 51 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#L50-L51

Added lines #L50 - L51 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 58 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#L57-L58

Added lines #L57 - L58 were not covered by tests

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

Check warning on line 63 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#L62-L63

Added lines #L62 - L63 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 71 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#L70-L71

Added lines #L70 - L71 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 78 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#L77-L78

Added lines #L77 - L78 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 91 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#L90-L91

Added lines #L90 - L91 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 96 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-L96

Added lines #L93 - L96 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 101 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#L100-L101

Added lines #L100 - L101 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
}
167 changes: 167 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,167 @@
// -*- 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 (
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