From e9ca77117b088df4d28111761905e23b7d43c33b Mon Sep 17 00:00:00 2001 From: Spyros Seimenis Date: Thu, 5 Dec 2024 09:55:57 +0200 Subject: [PATCH] c/snap-bootstrap: split CVM related functionality in separate files (#14789) * c/snap-bootstrap: split CVM related functionality in separate files * snap-bootstrap: add doc for CVM mode and address comment --- cmd/snap-bootstrap/cmd_initramfs_mounts.go | 75 -------- .../cmd_initramfs_mounts_cvm.go | 111 +++++++++++ .../cmd_initramfs_mounts_cvm_test.go | 173 ++++++++++++++++++ .../cmd_initramfs_mounts_test.go | 128 ------------- 4 files changed, 284 insertions(+), 203 deletions(-) create mode 100644 cmd/snap-bootstrap/cmd_initramfs_mounts_cvm.go create mode 100644 cmd/snap-bootstrap/cmd_initramfs_mounts_cvm_test.go diff --git a/cmd/snap-bootstrap/cmd_initramfs_mounts.go b/cmd/snap-bootstrap/cmd_initramfs_mounts.go index edadde3fb86..cdbbdb6730a 100644 --- a/cmd/snap-bootstrap/cmd_initramfs_mounts.go +++ b/cmd/snap-bootstrap/cmd_initramfs_mounts.go @@ -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) @@ -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 { diff --git a/cmd/snap-bootstrap/cmd_initramfs_mounts_cvm.go b/cmd/snap-bootstrap/cmd_initramfs_mounts_cvm.go new file mode 100644 index 00000000000..b7a0c6ca09e --- /dev/null +++ b/cmd/snap-bootstrap/cmd_initramfs_mounts_cvm.go @@ -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 . + * + */ + +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 +} diff --git a/cmd/snap-bootstrap/cmd_initramfs_mounts_cvm_test.go b/cmd/snap-bootstrap/cmd_initramfs_mounts_cvm_test.go new file mode 100644 index 00000000000..09ea09cb171 --- /dev/null +++ b/cmd/snap-bootstrap/cmd_initramfs_mounts_cvm_test.go @@ -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 . + * + */ + +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) +} diff --git a/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go b/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go index 91fa6daef6b..ee771833176 100644 --- a/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go +++ b/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go @@ -177,12 +177,6 @@ var ( KernelDeviceNode: "/dev/sda5", } - cvmEncPart = disks.Partition{ - FilesystemLabel: "cloudimg-rootfs-enc", - PartitionUUID: "cloudimg-rootfs-enc-partuuid", - KernelDeviceNode: "/dev/sda1", - } - // a boot disk without ubuntu-save defaultBootDisk = &disks.MockDiskMapping{ Structure: []disks.Partition{ @@ -227,15 +221,6 @@ var ( DevNum: "defaultEncDev", } - defaultCVMDisk = &disks.MockDiskMapping{ - Structure: []disks.Partition{ - seedPart, - cvmEncPart, - }, - DiskHasPartitions: true, - DevNum: "defaultCVMDev", - } - // a boot disk without ubuntu-seed, which can happen for classic defaultNoSeedWithSaveDisk = &disks.MockDiskMapping{ Structure: []disks.Partition{ @@ -387,9 +372,6 @@ func (s *baseInitramfsMountsSuite) SetUpTest(c *C) { // by default mock that we don't have UEFI vars, etc. to get the booted // kernel partition partition uuid s.AddCleanup(main.MockPartitionUUIDForBootedKernelDisk("")) - s.AddCleanup(main.MockSecbootProvisionForCVM(func(_ string) error { - return nil - })) s.AddCleanup(main.MockSecbootMeasureSnapSystemEpochWhenPossible(func() error { return nil })) @@ -2447,116 +2429,6 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRunModeEncryptedDataHappy(c *C c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, "run-model-measured"), testutil.FilePresent) } -func (s *initramfsMountsSuite) 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) -} - func (s *initramfsMountsSuite) TestInitramfsMountsRunModeEncryptedDataUnhappyNoSave(c *C) { s.mockProcCmdlineContent(c, "snapd_recovery_mode=run")