diff --git a/docs/imagecustomizer/verity-signing-sample/10-mountbootpartition.conf b/docs/imagecustomizer/verity-signing-sample/10-mountbootpartition.conf new file mode 100644 index 000000000..00cb71824 --- /dev/null +++ b/docs/imagecustomizer/verity-signing-sample/10-mountbootpartition.conf @@ -0,0 +1 @@ +add_dracutmodules+=" mountbootpartition " \ No newline at end of file diff --git a/docs/imagecustomizer/verity-signing-sample/90mountbootpartition/module-setup.sh b/docs/imagecustomizer/verity-signing-sample/90mountbootpartition/module-setup.sh new file mode 100755 index 000000000..46c7a54eb --- /dev/null +++ b/docs/imagecustomizer/verity-signing-sample/90mountbootpartition/module-setup.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# called by dracut +check() { + return 255 +} + +# called by dracut +depends() { + return 0 +} + +# called by dracut +installkernel() { + return 0 +} + +# called by dracut +install() { + # install utilities + inst_multiple lsblk umount + # generate udev rule - i.e. schedule things post udev settlement + inst_hook pre-udev 30 "$moddir/mountbootpartition-genrules.sh" + # script to run post udev to mout + inst_script "$moddir/mountbootpartition.sh" "/sbin/mountbootpartition" + # script runs early on when systemd is initialized... + if dracut_module_included "systemd-initrd"; then + inst_script "$moddir/mountbootpartition-generator.sh" "$systemdutildir"/system-generators/dracut-mountbootpartition-generator + fi + dracut_need_initqueue +} diff --git a/docs/imagecustomizer/verity-signing-sample/90mountbootpartition/mountbootpartition-generator.sh b/docs/imagecustomizer/verity-signing-sample/90mountbootpartition/mountbootpartition-generator.sh new file mode 100755 index 000000000..d0c64b731 --- /dev/null +++ b/docs/imagecustomizer/verity-signing-sample/90mountbootpartition/mountbootpartition-generator.sh @@ -0,0 +1,79 @@ +#!/bin/sh + +set -x +set -e + +echo "Running mountbootpartition-generator.sh" > /dev/kmsg + +# type getarg > /dev/null 2>&1 || . /lib/dracut-lib.sh + +function updateVeritySetupUnit () { + systemdDropInDir=/etc/systemd/system + verityDropInDir=$systemdDropInDir/systemd-veritysetup@root.service.d + + mkdir -p $verityDropInDir + verityConfiguration=$verityDropInDir/verity-azl-extension.conf + + cat < $verityConfiguration +[Unit] +After=bootmountmonitor.service +Requires=bootmountmonitor.service +EOF + + chmod 644 $verityConfiguration + chown root:root $verityConfiguration +} + +# ----------------------------------------------------------------------------- +function createBootPartitionMonitorScript () { + local bootPartitionMonitorCmd=$1 + local semaphorefile=$2 + + cat < $bootPartitionMonitorCmd +#!/bin/sh +while [ ! -e "$semaphorefile" ]; do + echo "Waiting for $semaphorefile to exist..." + sleep 1 +done +EOF + chmod +x $bootPartitionMonitorCmd +} + +# ----------------------------------------------------------------------------- +function createBootPartitionMonitorUnit() { + local bootPartitionMonitorCmd=$1 + + bootMountMonitorName="bootmountmonitor.service" + systemdDropInDir=/etc/systemd/system + bootMountMonitorDir=$systemdDropInDir + bootMountMonitorUnitFile=$bootMountMonitorDir/$bootMountMonitorName + + cat < $bootMountMonitorUnitFile +[Unit] +Description=bootpartitionmounter +DefaultDependencies=no + +[Service] +Type=oneshot +ExecStart=$bootPartitionMonitorCmd +RemainAfterExit=yes + +[Install] +WantedBy=multi-user.target +EOF +} + +# ----------------------------------------------------------------------------- + +updateVeritySetupUnit + +systemdScriptsDir=/usr/local/bin +bootPartitionMonitorCmd=$systemdScriptsDir/boot-partition-monitor.sh +semaphorefile=/run/boot-parition-mount-ready.sem + +mkdir -p $systemdScriptsDir + +createBootPartitionMonitorScript $bootPartitionMonitorCmd $semaphorefile +createBootPartitionMonitorUnit $bootPartitionMonitorCmd + +echo "mountbootpartition-generator.sh completed successfully." > /dev/kmsg diff --git a/docs/imagecustomizer/verity-signing-sample/90mountbootpartition/mountbootpartition-genrules.sh b/docs/imagecustomizer/verity-signing-sample/90mountbootpartition/mountbootpartition-genrules.sh new file mode 100755 index 000000000..36d7fcfba --- /dev/null +++ b/docs/imagecustomizer/verity-signing-sample/90mountbootpartition/mountbootpartition-genrules.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +echo "Running mountbootpartition-genrules.sh" > /dev/kmsg + +# this gets called after all devices have settled. +/sbin/initqueue --finished --onetime --unique /sbin/mountbootpartition > /dev/kmsg diff --git a/docs/imagecustomizer/verity-signing-sample/90mountbootpartition/mountbootpartition.sh b/docs/imagecustomizer/verity-signing-sample/90mountbootpartition/mountbootpartition.sh new file mode 100755 index 000000000..cd8323197 --- /dev/null +++ b/docs/imagecustomizer/verity-signing-sample/90mountbootpartition/mountbootpartition.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +echo "Running mountbootpartition.sh" + +type getarg > /dev/null 2>&1 || . /lib/dracut-lib.sh + +bootPartitionUuid=$(getarg pre.verity.mount) + +if [[ "$bootPartitionUuid" == "" ]]; then + exit 0 +fi + +mkdir -p /boot +mount -U $bootPartitionUuid /boot + +echo "done" > /run/boot-parition-mount-ready.sem diff --git a/docs/imagecustomizer/verity-signing-sample/verity-test.yaml b/docs/imagecustomizer/verity-signing-sample/verity-test.yaml new file mode 100644 index 000000000..06ff0d7e6 --- /dev/null +++ b/docs/imagecustomizer/verity-signing-sample/verity-test.yaml @@ -0,0 +1,96 @@ +storage: + bootType: efi + disks: + - partitionTableType: gpt + maxSize: 5120M + partitions: + - id: esp + type: esp + start: 1M + end: 9M + + - id: boot + start: 9M + end: 1024M + + - id: root + start: 1024M + end: 3072M + + - id: roothash + start: 3072M + end: 3200M + + - id: var + start: 3200M + + verity: + - id: verityroot + name: root + dataDeviceId: root + hashDeviceId: roothash + corruptionOption: panic + + filesystems: + - deviceId: esp + type: fat32 + mountPoint: + path: /boot/efi + options: umask=0077 + + - deviceId: boot + type: ext4 + mountPoint: + path: /boot + + - deviceId: verityroot + type: ext4 + mountPoint: + path: / + options: ro + + - deviceId: var + type: ext4 + mountPoint: + path: /var + +os: + resetBootLoaderType: hard-reset + selinux: + mode: disabled + + kernelCommandLine: + extraCommandLine: + - "rd.info" + - "console=tty0" + - "console=ttyS0" + + packages: + install: + - openssh-server + - veritysetup + - vim + - lvm2 + + additionalFiles: + # 90mountbootpartition + - source: 90mountbootpartition/module-setup.sh + destination: /usr/lib/dracut/modules.d/90mountbootpartition/module-setup.sh + permissions: "755" + - source: 90mountbootpartition/mountbootpartition-generator.sh + destination: /usr/lib/dracut/modules.d/90mountbootpartition/mountbootpartition-generator.sh + permissions: "755" + - source: 90mountbootpartition/mountbootpartition-genrules.sh + destination: /usr/lib/dracut/modules.d/90mountbootpartition/mountbootpartition-genrules.sh + permissions: "755" + - source: 90mountbootpartition/mountbootpartition.sh + destination: /usr/lib/dracut/modules.d/90mountbootpartition/mountbootpartition.sh + permissions: "755" + # ensure mountbootpartition is included + - source: 10-mountbootpartition.conf + destination: /etc/dracut.conf.d/10-mountbootpartition.conf + permissions: "755" + + services: + enable: + - sshd diff --git a/docs/imagecustomizer/verity.md b/docs/imagecustomizer/verity.md index f3f42465f..67cb02803 100644 --- a/docs/imagecustomizer/verity.md +++ b/docs/imagecustomizer/verity.md @@ -224,3 +224,170 @@ os: disable: - systemd-growfs-root ``` + +### Signing Verity Hashes + +To sign verity hashes, we need to: + +- Invoke the Azure Linux Image Customizer to: + - Configure dm-verity to check for signed hashes. + - Calculate the hashes and export them. +- Sign the exported hashes. +- Invoke the Azure Linux Image Customizer to: + - Re-inject the exported hashes into the image. + +The following commadline switches are used to achieve that: + +- First Invocation Switches: + - `--output-verity-hashes` + - Exports the dm-verity calculated root hashes. + - Each exported hash will be stored in a separate text file where its + name is the verity device concatenated with 'hash'. + - The exported hash files will be placed in the folder specified by the + value of `--output-verity-hashes-dir` + - `--output-verity-hashes-dir` + - Specifies where to saved the exported hashes. + - `--require-signed-rootfs-root-hash` + - When specified, the rootfs signed hash is expected to be at + `/boot/.hash.sig`. If absent or not signed + properly, the rootfs verity device verification will fail. + - `--require-signed-root-hashes` + - When specified, all verity devices will be required to have signed root + hashes (rootfs, containers, etc). + +For testing, the user may choose to export the hashes without requiring +signatures. + +- Second Invocation Switches: + - `--input-signed-verity-hashes-files [..]` + - The list of files to import and place on the boot partition at `/boot`. + - Each file name must be on the form `.hash.sig`. + + +```bash +imageCustomizerPath="./imagecustomizer" +inputConfigFile="./verity-test.yaml" +inputImage="./core-3.0.20241216.vhdx" +buildDir="./build" + +outputFormat="qcow2" +outputBaseName="verity-$(date +'%Y%m%d-%H%M').$outputFormat" +outputDir="./output" +verityImage="$outputDir/$outputBaseName" + +hashFilesDir="./temp/root-hashes" +hashFile="$hashFilesDir/root.hash" + +rm -rf $hashFilesDir +sudo $imageCustomizerPath \ + --config-file "$inputConfigFile" \ + --image-file "$inputImage" \ + --build-dir "$buildDir" \ + --output-image-format "$outputFormat" \ + --output-image-file "$verityImage" \ + --output-verity-hashes \ + --output-verity-hashes-dir "$hashFilesDir" \ + --require-signed-rootfs-root-hash \ + --require-signed-root-hashes \ + --log-level "$logLevel" + +signedHashFilesDir="./temp/signed-root-hashes" +signedHashFile="$signedHashFilesDir/$(basename $hashFile).sig" + +sudo chown $USER:$USER $hashFilesDir +sudo chown $USER:$USER $hashFile + +# sign the hash files +cp "$hashFile" "$signedHashFile" +echo "...signed..." > "$signedHashFile" + +# inject the file back +signedVerityImage=$outputDir/signed-$outputBaseName +emptyConfig=/home/george/temp/empty-config.yaml +echo "iso:" > $emptyConfig + +sudo $imageCustomizerPath \ + --config-file "$emptyConfig" \ + --image-file "$verityImage" \ + --build-dir "$buildDir" \ + --output-image-format "$outputFormat" \ + --output-image-file "$signedVerityImage" \ + --input-signed-verity-hashes-files "$signedHashFile" \ + --log-level "$logLevel" +``` + +```bash +imageCustomizerPath="./imagecustomizer" +inputConfigFile="./verity-test.yaml" +inputImage="./core-3.0.20241216.vhdx" +buildDir="./build" + +keyFile=~./key.pem +certFile=~./cert.pem + +outputFormat="qcow2" +outputBaseName="verity-$(date +'%Y%m%d-%H%M').$outputFormat" +outputDir="./output" +verityImage="$outputDir/$outputBaseName" + +hashFilesDir="./temp/root-hashes" +hashFile="$hashFilesDir/root.hash" + +rm -rf $hashFilesDir +sudo $imageCustomizerPath \ + --config-file "$inputConfigFile" \ + --image-file "$inputImage" \ + --build-dir "$buildDir" \ + --output-image-format "$outputFormat" \ + --output-image-file "$verityImage" \ + --output-verity-hashes \ + --output-verity-hashes-dir "$hashFilesDir" \ + --require-signed-rootfs-root-hash \ + --require-signed-root-hashes \ + --log-level "$logLevel" + +echo "Generated: $verityImage" + +sudo chown $USER:$USER $unsignedHashFile +sudo chown $USER:$USER $unsignedHashDir + +# sign the generated hash +signedHashDir=./root-hashes-signed +signedHashFile=$signedHashDir/root.hash.sig + +sudo rm -rf $signedHashDir +mkdir -p $signedHashDir + +inputFileStripped=$unsignedHashFile-stripped + +rootHash=$(cat $unsignedHashFile) +echo ${rootHash} | tr -d '\n' > $inputFileStripped + +openssl smime \ + -sign \ + -nocerts \ + -noattr \ + -binary \ + -in $inputFileStripped \ + -inkey $keyFile \ + -signer $certFile \ + -outform der \ + -out $signedHashFile + +# generate the final image +signedVerityImage=$outputDir/signed-$outputBaseName + +emptyConfig=./empty-config.yaml +echo "iso:" > $emptyConfig + +sudo $imageCustomizerPath \ + --config-file $emptyConfig \ + --image-file $verityImage \ + --build-dir $buildDir \ + --output-image-format $outputFormat \ + --output-image-file $signedVerityImage \ + --input-signed-verity-hashes-files $signedHashFile \ + --log-level $logLevel + + echo "Generated: $signedVerityImage" +``` \ No newline at end of file diff --git a/toolkit/tools/imagecustomizer/main.go b/toolkit/tools/imagecustomizer/main.go index e8c159e3a..b79bf13b2 100644 --- a/toolkit/tools/imagecustomizer/main.go +++ b/toolkit/tools/imagecustomizer/main.go @@ -27,7 +27,12 @@ var ( rpmSources = app.Flag("rpm-source", "Path to a RPM repo config file or a directory containing RPMs.").Strings() disableBaseImageRpmRepos = app.Flag("disable-base-image-rpm-repos", "Disable the base image's RPM repos as an RPM source").Bool() enableShrinkFilesystems = app.Flag("shrink-filesystems", "Enable shrinking of filesystems to minimum size. Supports ext2, ext3, ext4 filesystem types.").Bool() + requireSignedRootfsRootHash = app.Flag("require-signed-rootfs-root-hash", "Requires that the verity root hash of the rootfs is signed.").Bool() + requireSignedRootHashes = app.Flag("require-signed-root-hashes", "Requires that all root hashes are signed.").Bool() outputPXEArtifactsDir = app.Flag("output-pxe-artifacts-dir", "Create a directory with customized image PXE booting artifacts. '--output-image-format' must be set to 'iso'.").String() + outputVerityHashes = app.Flag("output-verity-hashes", "Save the root hash value of each verity target device in a text file.").Bool() + outputVerityHashesDir = app.Flag("output-verity-hashes-dir", "The directory where the verity root hash files will be saved to.").String() + inputSignedVerityHashes = app.Flag("input-signed-verity-hashes-files", "A list of one or more signed verity root hash files.").Strings() logFlags = exe.SetupLogFlags(app) profFlags = exe.SetupProfileFlags(app) timestampFile = app.Flag("timestamp-file", "File that stores timestamps for this program.").String() @@ -72,7 +77,8 @@ func customizeImage() error { err = imagecustomizerlib.CustomizeImageWithConfigFile(*buildDir, *configFile, *imageFile, *rpmSources, *outputImageFile, *outputImageFormat, *outputSplitPartitionsFormat, *outputPXEArtifactsDir, - !*disableBaseImageRpmRepos, *enableShrinkFilesystems) + !*disableBaseImageRpmRepos, *requireSignedRootfsRootHash, *requireSignedRootHashes, *outputVerityHashes, + *outputVerityHashesDir, *inputSignedVerityHashes, *enableShrinkFilesystems) if err != nil { return err } diff --git a/toolkit/tools/imagecustomizerapi/mountidentifiertype.go b/toolkit/tools/imagecustomizerapi/mountidentifiertype.go index 8b1d9618e..1330d90be 100644 --- a/toolkit/tools/imagecustomizerapi/mountidentifiertype.go +++ b/toolkit/tools/imagecustomizerapi/mountidentifiertype.go @@ -18,6 +18,8 @@ const ( // MountIdentifierTypePartLabel mounts this partition via the GPT PARTLABEL MountIdentifierTypePartLabel MountIdentifierType = "part-label" + MountIdentifierTypeDeviceMapper MountIdentifierType = "device-mapper" + // MountIdentifierTypeDefault uses the default type, which is PARTUUID. MountIdentifierTypeDefault MountIdentifierType = "" ) diff --git a/toolkit/tools/pkg/imagecustomizerlib/customizeos.go b/toolkit/tools/pkg/imagecustomizerlib/customizeos.go index 1db96d28d..02d23e344 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/customizeos.go +++ b/toolkit/tools/pkg/imagecustomizerlib/customizeos.go @@ -11,27 +11,31 @@ import ( func doOsCustomizations(buildDir string, baseConfigPath string, config *imagecustomizerapi.Config, imageConnection *ImageConnection, rpmsSources []string, useBaseImageRpmRepos bool, partitionsCustomized bool, - imageUuid string) error { + imageUuid string, onlyAddFiles bool) error { var err error imageChroot := imageConnection.Chroot() buildTime := time.Now().Format("2006-01-02T15:04:05Z") - resolvConf, err := overrideResolvConf(imageChroot) - if err != nil { - return err - } + var resolvConf resolvConfInfo - err = addRemoveAndUpdatePackages(buildDir, baseConfigPath, config.OS, imageChroot, rpmsSources, - useBaseImageRpmRepos) - if err != nil { - return err - } + if !onlyAddFiles { + resolvConf, err = overrideResolvConf(imageChroot) + if err != nil { + return err + } - err = UpdateHostname(config.OS.Hostname, imageChroot) - if err != nil { - return err + err = addRemoveAndUpdatePackages(buildDir, baseConfigPath, config.OS, imageChroot, rpmsSources, + useBaseImageRpmRepos) + if err != nil { + return err + } + + err = UpdateHostname(config.OS.Hostname, imageChroot) + if err != nil { + return err + } } err = copyAdditionalDirs(baseConfigPath, config.OS.AdditionalDirs, imageChroot) @@ -44,77 +48,79 @@ func doOsCustomizations(buildDir string, baseConfigPath string, config *imagecus return err } - err = AddOrUpdateUsers(config.OS.Users, baseConfigPath, imageChroot) - if err != nil { - return err - } - - err = EnableOrDisableServices(config.OS.Services, imageChroot) - if err != nil { - return err - } + if !onlyAddFiles { + err = AddOrUpdateUsers(config.OS.Users, baseConfigPath, imageChroot) + if err != nil { + return err + } - err = LoadOrDisableModules(config.OS.Modules, imageChroot.RootDir()) - if err != nil { - return err - } + err = EnableOrDisableServices(config.OS.Services, imageChroot) + if err != nil { + return err + } - err = addCustomizerRelease(imageChroot, ToolVersion, buildTime, imageUuid) - if err != nil { - return err - } + err = LoadOrDisableModules(config.OS.Modules, imageChroot.RootDir()) + if err != nil { + return err + } - err = handleBootLoader(baseConfigPath, config, imageConnection) - if err != nil { - return err - } + err = addCustomizerRelease(imageChroot, ToolVersion, buildTime, imageUuid) + if err != nil { + return err + } - selinuxMode, err := handleSELinux(config.OS.SELinux.Mode, config.OS.ResetBootLoaderType, - imageChroot) - if err != nil { - return err - } + err = handleBootLoader(baseConfigPath, config, imageConnection) + if err != nil { + return err + } - overlayUpdated, err := enableOverlays(config.OS.Overlays, selinuxMode, imageChroot) - if err != nil { - return err - } + selinuxMode, err := handleSELinux(config.OS.SELinux.Mode, config.OS.ResetBootLoaderType, + imageChroot) + if err != nil { + return err + } - verityUpdated, err := enableVerityPartition(config.Storage.Verity, imageChroot) - if err != nil { - return err - } + overlayUpdated, err := enableOverlays(config.OS.Overlays, selinuxMode, imageChroot) + if err != nil { + return err + } - if partitionsCustomized || overlayUpdated || verityUpdated { - err = regenerateInitrd(imageChroot) + verityUpdated, err := enableVerityPartition(config.Storage.Verity, imageChroot) if err != nil { return err } - } - err = runUserScripts(baseConfigPath, config.Scripts.PostCustomization, "postCustomization", imageChroot) - if err != nil { - return err - } + if partitionsCustomized || overlayUpdated || verityUpdated { + err = regenerateInitrd(imageChroot) + if err != nil { + return err + } + } - err = restoreResolvConf(resolvConf, imageChroot) - if err != nil { - return err - } + err = runUserScripts(baseConfigPath, config.Scripts.PostCustomization, "postCustomization", imageChroot) + if err != nil { + return err + } - err = selinuxSetFiles(selinuxMode, imageChroot) - if err != nil { - return err - } + err = restoreResolvConf(resolvConf, imageChroot) + if err != nil { + return err + } - err = runUserScripts(baseConfigPath, config.Scripts.FinalizeCustomization, "finalizeCustomization", imageChroot) - if err != nil { - return err - } + err = selinuxSetFiles(selinuxMode, imageChroot) + if err != nil { + return err + } - err = checkForInstalledKernel(imageChroot) - if err != nil { - return err + err = runUserScripts(baseConfigPath, config.Scripts.FinalizeCustomization, "finalizeCustomization", imageChroot) + if err != nil { + return err + } + + err = checkForInstalledKernel(imageChroot) + if err != nil { + return err + } } return nil diff --git a/toolkit/tools/pkg/imagecustomizerlib/customizeverity.go b/toolkit/tools/pkg/imagecustomizerlib/customizeverity.go index 668ca14b9..dfd963f10 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/customizeverity.go +++ b/toolkit/tools/pkg/imagecustomizerlib/customizeverity.go @@ -5,15 +5,21 @@ package imagecustomizerlib import ( "fmt" + "os" "path/filepath" "github.com/microsoft/azurelinux/toolkit/tools/imagecustomizerapi" "github.com/microsoft/azurelinux/toolkit/tools/imagegen/diskutils" "github.com/microsoft/azurelinux/toolkit/tools/internal/file" "github.com/microsoft/azurelinux/toolkit/tools/internal/logger" + "github.com/microsoft/azurelinux/toolkit/tools/internal/ptrutils" "github.com/microsoft/azurelinux/toolkit/tools/internal/safechroot" ) +const ( + veritySignedRootHashFilesDir = "/boot" +) + func enableVerityPartition(verity []imagecustomizerapi.Verity, imageChroot *safechroot.Chroot, ) (bool, error) { var err error @@ -107,6 +113,7 @@ func prepareGrubConfigForVerity(imageChroot *safechroot.Chroot) error { func updateGrubConfigForVerity(rootfsVerity imagecustomizerapi.Verity, rootHash string, grubCfgFullPath string, partIdToPartUuid map[string]string, partitions []diskutils.PartitionInfo, + provideRootHashSignatureArgument string, requireRootHashSignatureArgument string, bootPartitionUuid string, ) error { var err error @@ -133,6 +140,9 @@ func updateGrubConfigForVerity(rootfsVerity imagecustomizerapi.Verity, rootHash fmt.Sprintf("systemd.verity_root_data=%s", formattedDataPartition), fmt.Sprintf("systemd.verity_root_hash=%s", formattedHashPartition), fmt.Sprintf("systemd.verity_root_options=%s", formattedCorruptionOption), + fmt.Sprintf("%s", provideRootHashSignatureArgument), + fmt.Sprintf("%s", requireRootHashSignatureArgument), + fmt.Sprintf("pre.verity.mount=%s", bootPartitionUuid), } grub2Config, err := file.Read(grubCfgFullPath) @@ -258,3 +268,59 @@ func validateVerityDependencies(imageChroot *safechroot.Chroot) error { return nil } + +func generateSignedRootHashArtifacts(deviceId string, deviceRootHash string, outputVerityHashes bool, outputVerityHashesDir string, + requireSignedRootfsRootHash bool, requireSignedRootHashes bool, +) (provideRootHashSignatureArgument string, requireRootHashSignatureArgument string, err error) { + + if !outputVerityHashes { + return "", "", nil + } + + rootHashFile := deviceId + ".hash" + rootHashFileLocalPath := filepath.Join(outputVerityHashesDir, rootHashFile) + rootHashSignedFileImagePath := filepath.Join("/boot", rootHashFile+".sig") + + err = os.MkdirAll(outputVerityHashesDir, os.ModePerm) + if err != nil { + return "", "", fmt.Errorf("failed to create root hashes directory (%s):\n%w", outputVerityHashesDir, err) + } + err = file.Write(deviceRootHash, rootHashFileLocalPath) + if err != nil { + return "", "", fmt.Errorf("failed to write root hash to %s:\n%w", rootHashFileLocalPath, err) + } + + // ToDo: how do we handle multiple verity device? + if requireSignedRootfsRootHash { + provideRootHashSignatureArgument = "systemd.verity_root_options=root-hash-signature=" + rootHashSignedFileImagePath + } + if requireSignedRootHashes { + requireRootHashSignatureArgument = "dm_verity.require_signatures=1" + } + + logger.Log.Debugf("---- debug ---- rootHashSignedFileImagePath=(%s)", rootHashSignedFileImagePath) + logger.Log.Debugf("---- debug ---- provideRootHashSignatureArgument =(%s)", provideRootHashSignatureArgument) + logger.Log.Debugf("---- debug ---- requireRootHashSignatureArgument =(%s)", requireRootHashSignatureArgument) + + return provideRootHashSignatureArgument, requireRootHashSignatureArgument, err +} + +func generateSignedRootHashConfiguration(signedRootHashFiles []string) (imagecustomizerapi.AdditionalFileList, error) { + additionalFiles := imagecustomizerapi.AdditionalFileList{} + for _, localFile := range signedRootHashFiles { + + imageFile := filepath.Join(veritySignedRootHashFilesDir, filepath.Base(localFile)) + + logger.Log.Debugf("---- debug ---- - src = %s", localFile) + logger.Log.Debugf("---- debug ---- dst = %s", imageFile) + + additionalFile := imagecustomizerapi.AdditionalFile{ + Destination: imageFile, + Source: localFile, + // ToDo: what permissions should we use? + Permissions: ptrutils.PtrTo(imagecustomizerapi.FilePermissions(0o755)), + } + additionalFiles = append(additionalFiles, additionalFile) + } + return additionalFiles, nil +} diff --git a/toolkit/tools/pkg/imagecustomizerlib/grubcfgutils.go b/toolkit/tools/pkg/imagecustomizerlib/grubcfgutils.go index f6886a985..23ae8856e 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/grubcfgutils.go +++ b/toolkit/tools/pkg/imagecustomizerlib/grubcfgutils.go @@ -825,7 +825,8 @@ func regenerateInitrd(imageChroot *safechroot.Chroot) error { if mkinitrdExists { return shell.ExecuteLiveWithErr(1, "mkinitrd") } else { - return shell.ExecuteLiveWithErr(1, "dracut", "--force", "--regenerate-all") + return shell.ExecuteLiveWithErr(1, "dracut", "--force", "--regenerate-all", + "--include", "/usr/lib/locale", "/usr/lib/locale") } }) if err != nil { diff --git a/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go b/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go index 4d757ead0..721c2771a 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go +++ b/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go @@ -54,9 +54,13 @@ type ImageCustomizerParameters struct { buildDirAbs string // input image - inputImageFile string - inputImageFormat string - inputIsIso bool + inputImageFile string + inputImageFormat string + inputIsIso bool + inputSignedVerityHashes []string + + requireSignedRootfsRootHash bool + requireSignedRootHashes bool // configurations configPath string @@ -77,13 +81,17 @@ type ImageCustomizerParameters struct { outputImageDir string outputImageBase string outputPXEArtifactsDir string + outputVerityHashes bool + outputVerityHashesDir string } func createImageCustomizerParameters(buildDir string, inputImageFile string, configPath string, config *imagecustomizerapi.Config, - useBaseImageRpmRepos bool, rpmsSources []string, enableShrinkFilesystems bool, outputSplitPartitionsFormat string, - outputImageFormat string, outputImageFile string, outputPXEArtifactsDir string) (*ImageCustomizerParameters, error) { + useBaseImageRpmRepos bool, rpmsSources []string, enableShrinkFilesystems bool, requireSignedRootfsRootHash bool, + requireSignedRootHashes bool, outputVerityHashes bool, outputVerityHashesDir string, inputSignedVerityHashes []string, + outputSplitPartitionsFormat string, outputImageFormat string, outputImageFile string, outputPXEArtifactsDir string, +) (*ImageCustomizerParameters, error) { ic := &ImageCustomizerParameters{} @@ -101,6 +109,10 @@ func createImageCustomizerParameters(buildDir string, ic.inputImageFile = inputImageFile ic.inputImageFormat = strings.TrimLeft(filepath.Ext(inputImageFile), ".") ic.inputIsIso = ic.inputImageFormat == ImageFormatIso + ic.inputSignedVerityHashes = inputSignedVerityHashes + + ic.requireSignedRootfsRootHash = requireSignedRootfsRootHash + ic.requireSignedRootHashes = requireSignedRootHashes // configuration ic.configPath = configPath @@ -129,6 +141,8 @@ func createImageCustomizerParameters(buildDir string, ic.outputImageBase = strings.TrimSuffix(filepath.Base(outputImageFile), filepath.Ext(outputImageFile)) ic.outputImageDir = filepath.Dir(outputImageFile) ic.outputPXEArtifactsDir = outputPXEArtifactsDir + ic.outputVerityHashes = outputVerityHashes + ic.outputVerityHashesDir = outputVerityHashesDir if ic.outputImageFormat != "" && !ic.outputIsIso { err = validateImageFormat(ic.outputImageFormat) @@ -176,7 +190,8 @@ func createImageCustomizerParameters(buildDir string, func CustomizeImageWithConfigFile(buildDir string, configFile string, imageFile string, rpmsSources []string, outputImageFile string, outputImageFormat string, outputSplitPartitionsFormat string, outputPXEArtifactsDir string, - useBaseImageRpmRepos bool, enableShrinkFilesystems bool, + useBaseImageRpmRepos bool, requireSignedRootfsRootHash bool, requireSignedRootHashes bool, outputVerityHashes bool, + outputVerityHashesDir string, inputSignedVerityHashes []string, enableShrinkFilesystems bool, ) error { var err error @@ -196,7 +211,8 @@ func CustomizeImageWithConfigFile(buildDir string, configFile string, imageFile } err = CustomizeImage(buildDir, absBaseConfigPath, &config, imageFile, rpmsSources, outputImageFile, outputImageFormat, - outputSplitPartitionsFormat, outputPXEArtifactsDir, useBaseImageRpmRepos, enableShrinkFilesystems) + outputSplitPartitionsFormat, outputPXEArtifactsDir, useBaseImageRpmRepos, requireSignedRootfsRootHash, + requireSignedRootHashes, outputVerityHashes, outputVerityHashesDir, inputSignedVerityHashes, enableShrinkFilesystems) if err != nil { return err } @@ -215,7 +231,8 @@ func cleanUp(ic *ImageCustomizerParameters) error { func CustomizeImage(buildDir string, baseConfigPath string, config *imagecustomizerapi.Config, imageFile string, rpmsSources []string, outputImageFile string, outputImageFormat string, outputSplitPartitionsFormat string, - outputPXEArtifactsDir string, useBaseImageRpmRepos bool, enableShrinkFilesystems bool, + outputPXEArtifactsDir string, useBaseImageRpmRepos bool, requireSignedRootfsRootHash bool, requireSignedRootHashes bool, + outputVerityHashes bool, outputVerityHashesDir string, inputSignedVerityHashes []string, enableShrinkFilesystems bool, ) error { err := validateConfig(baseConfigPath, config, rpmsSources, useBaseImageRpmRepos) if err != nil { @@ -224,7 +241,8 @@ func CustomizeImage(buildDir string, baseConfigPath string, config *imagecustomi imageCustomizerParameters, err := createImageCustomizerParameters(buildDir, imageFile, baseConfigPath, config, - useBaseImageRpmRepos, rpmsSources, enableShrinkFilesystems, outputSplitPartitionsFormat, + useBaseImageRpmRepos, rpmsSources, enableShrinkFilesystems, requireSignedRootfsRootHash, + requireSignedRootHashes, outputVerityHashes, outputVerityHashesDir, inputSignedVerityHashes, outputSplitPartitionsFormat, outputImageFormat, outputImageFile, outputPXEArtifactsDir) if err != nil { return fmt.Errorf("failed to create image customizer parameters object:\n%w", err) @@ -333,6 +351,20 @@ func customizeOSContents(ic *ImageCustomizerParameters) error { return nil } + // Are we being invoked to inject signed verity hashes? + if len(ic.inputSignedVerityHashes) != 0 { + if ic.config.OS != nil { + // todo: add other exclusions... + return fmt.Errorf("cannot define both --input-signed-verity-hashes and OS configuration.") + } + var err error + ic.config.OS = &imagecustomizerapi.OS{} + ic.config.OS.AdditionalFiles, err = generateSignedRootHashConfiguration(ic.inputSignedVerityHashes) + if err != nil { + return fmt.Errorf("failed to generate configuration for signed root hash files") + } + } + // The code beyond this point assumes the OS object is always present. To // change the code to check before every usage whether the OS object is // present or not will lead to a messy mix of if statements that do not @@ -348,10 +380,11 @@ func customizeOSContents(ic *ImageCustomizerParameters) error { // The presence of this type indicates that dm-verity has been enabled on the base image. If dm-verity is not enabled, // the verity hash device should not be assigned this type. We do not support customization on verity enabled base // images at this time because such modifications would compromise the integrity and security mechanisms enforced by dm-verity. - err := checkDmVerityEnabled(ic.rawImageFile) - if err != nil { - return err - } + + // err := checkDmVerityEnabled(ic.rawImageFile) + // if err != nil { + // return err + // } // Customize the partitions. partitionsCustomized, newRawImageFile, partIdToPartUuid, err := customizePartitions(ic.buildDirAbs, @@ -368,8 +401,9 @@ func customizeOSContents(ic *ImageCustomizerParameters) error { } // Customize the raw image file. + onlyAddFiles := len(ic.inputSignedVerityHashes) != 0 err = customizeImageHelper(ic.buildDirAbs, ic.configPath, ic.config, ic.rawImageFile, ic.rpmsSources, - ic.useBaseImageRpmRepos, partitionsCustomized, imageUuidStr) + ic.useBaseImageRpmRepos, partitionsCustomized, imageUuidStr, onlyAddFiles) if err != nil { return err } @@ -384,7 +418,8 @@ func customizeOSContents(ic *ImageCustomizerParameters) error { if len(ic.config.Storage.Verity) > 0 { // Customize image for dm-verity, setting up verity metadata and security features. - err = customizeVerityImageHelper(ic.buildDirAbs, ic.configPath, ic.config, ic.rawImageFile, partIdToPartUuid) + err = customizeVerityImageHelper(ic.buildDirAbs, ic.configPath, ic.config, ic.rawImageFile, partIdToPartUuid, + ic.requireSignedRootfsRootHash, ic.requireSignedRootHashes, ic.outputVerityHashes, ic.outputVerityHashesDir) if err != nil { return err } @@ -673,10 +708,8 @@ func validatePackageLists(baseConfigPath string, config *imagecustomizerapi.OS, func customizeImageHelper(buildDir string, baseConfigPath string, config *imagecustomizerapi.Config, rawImageFile string, rpmsSources []string, useBaseImageRpmRepos bool, partitionsCustomized bool, - imageUuidStr string, + imageUuidStr string, onlyAddFiles bool, ) error { - logger.Log.Debugf("Customizing OS") - imageConnection, err := connectToExistingImage(rawImageFile, buildDir, "imageroot", true) if err != nil { return err @@ -692,7 +725,7 @@ func customizeImageHelper(buildDir string, baseConfigPath string, config *imagec // Do the actual customizations. err = doOsCustomizations(buildDir, baseConfigPath, config, imageConnection, rpmsSources, - useBaseImageRpmRepos, partitionsCustomized, imageUuidStr) + useBaseImageRpmRepos, partitionsCustomized, imageUuidStr, onlyAddFiles) // Out of disk space errors can be difficult to diagnose. // So, warn about any partitions with low free space. @@ -755,7 +788,8 @@ func shrinkFilesystemsHelper(buildImageFile string, verity []imagecustomizerapi. } func customizeVerityImageHelper(buildDir string, baseConfigPath string, config *imagecustomizerapi.Config, - buildImageFile string, partIdToPartUuid map[string]string, + buildImageFile string, partIdToPartUuid map[string]string, requireSignedRootfsRootHash bool, requireSignedRootHashes bool, + outputVerityHashes bool, outputVerityHashesDir string, ) error { var err error @@ -818,6 +852,18 @@ func customizeVerityImageHelper(buildDir string, baseConfigPath string, config * return err } + logger.Log.Debugf("---- debug --- boot partition - Name = (%s)", bootPartition.Name) + logger.Log.Debugf("---- debug --- boot partition - Path = (%s)", bootPartition.Path) + logger.Log.Debugf("---- debug --- boot partition - PartitionTypeUuid = (%s)", bootPartition.PartitionTypeUuid) + logger.Log.Debugf("---- debug --- boot partition - FileSystemType = (%s)", bootPartition.FileSystemType) + logger.Log.Debugf("---- debug --- boot partition - Uuid = (%s)", bootPartition.Uuid) + logger.Log.Debugf("---- debug --- boot partition - PartUuid = (%s)", bootPartition.PartUuid) + logger.Log.Debugf("---- debug --- boot partition - Mountpoint = (%s)", bootPartition.Mountpoint) + logger.Log.Debugf("---- debug --- boot partition - PartLabel = (%s)", bootPartition.PartLabel) + logger.Log.Debugf("---- debug --- boot partition - Type = (%s)", bootPartition.Type) + + // mount -U 9bb90123-2744-49e4-a49c-090bcba96ae8 /run/my-boot/ + bootPartitionTmpDir := filepath.Join(buildDir, tmpParitionDirName) // Temporarily mount the partition. bootPartitionMount, err := safemount.NewMount(bootPartition.Path, bootPartitionTmpDir, bootPartition.FileSystemType, 0, "", true) @@ -831,7 +877,14 @@ func customizeVerityImageHelper(buildDir string, baseConfigPath string, config * return fmt.Errorf("failed to stat file (%s):\n%w", grubCfgFullPath, err) } - err = updateGrubConfigForVerity(rootfsVerity, rootHash, grubCfgFullPath, partIdToPartUuid, diskPartitions) + provideRootHashSignatureArgument, requireRootHashSignatureArgument, err := generateSignedRootHashArtifacts(rootfsVerity.DataDeviceId, rootHash, outputVerityHashes, + outputVerityHashesDir, requireSignedRootfsRootHash, requireSignedRootHashes) + if err != nil { + return err + } + + err = updateGrubConfigForVerity(rootfsVerity, rootHash, grubCfgFullPath, partIdToPartUuid, diskPartitions, + provideRootHashSignatureArgument, requireRootHashSignatureArgument, bootPartition.Uuid) if err != nil { return err } diff --git a/toolkit/tools/pkg/imagecustomizerlib/partitionutils.go b/toolkit/tools/pkg/imagecustomizerlib/partitionutils.go index 4aef95858..eea3a3766 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/partitionutils.go +++ b/toolkit/tools/pkg/imagecustomizerlib/partitionutils.go @@ -149,7 +149,7 @@ func findRootfsPartition(diskPartitions []diskutils.PartitionInfo, buildDir stri } // Temporarily mount the partition. - partitionMount, err := safemount.NewMount(diskPartition.Path, tmpDir, diskPartition.FileSystemType, 0, + partitionMount, err := safemount.NewMount(diskPartition.Path, tmpDir, diskPartition.FileSystemType, unix.MS_RDONLY, "", true) if err != nil { return nil, fmt.Errorf("failed to mount partition (%s):\n%w", diskPartition.Path, err) @@ -192,7 +192,7 @@ func findMountsFromRootfs(rootfsPartition *diskutils.PartitionInfo, diskPartitio tmpDir := filepath.Join(buildDir, tmpParitionDirName) // Temporarily mount the rootfs partition so that the fstab file can be read. - rootfsPartitionMount, err := safemount.NewMount(rootfsPartition.Path, tmpDir, rootfsPartition.FileSystemType, 0, "", + rootfsPartitionMount, err := safemount.NewMount(rootfsPartition.Path, tmpDir, rootfsPartition.FileSystemType, unix.MS_RDONLY, "", true) if err != nil { return nil, fmt.Errorf("failed to mount rootfs partition (%s):\n%w", rootfsPartition.Path, err) @@ -238,13 +238,17 @@ func fstabEntriesToMountPoints(fstabEntries []diskutils.FstabEntry, diskPartitio // Convert fstab entries into mount points. var mountPoints []*safechroot.MountPoint - var foundRoot bool for _, fstabEntry := range filteredFstabEntries { source, err := findSourcePartition(fstabEntry.Source, diskPartitions) if err != nil { return nil, err } + // ToDo: device mapper returns an empty string + if source == "" { + continue + } + // Unset read-only flag so that read-only partitions can be customized. vfsOptions := fstabEntry.VfsOptions & ^diskutils.MountFlags(unix.MS_RDONLY) @@ -253,8 +257,6 @@ func fstabEntriesToMountPoints(fstabEntries []diskutils.FstabEntry, diskPartitio mountPoint = safechroot.NewPreDefaultsMountPoint( source, fstabEntry.Target, fstabEntry.FsType, uintptr(vfsOptions), fstabEntry.FsOptions) - - foundRoot = true } else { mountPoint = safechroot.NewMountPoint( source, fstabEntry.Target, fstabEntry.FsType, @@ -264,10 +266,6 @@ func fstabEntriesToMountPoints(fstabEntries []diskutils.FstabEntry, diskPartitio mountPoints = append(mountPoints, mountPoint) } - if !foundRoot { - return nil, fmt.Errorf("image has invalid fstab file: no root partition found") - } - return mountPoints, nil } @@ -310,9 +308,14 @@ func findSourcePartitionHelper(source string, return imagecustomizerapi.MountIdentifierTypeDefault, diskutils.PartitionInfo{}, 0, err } - partition, partitionIndex, err := findPartition(mountIdType, mountId, partitions) - if err != nil { - return imagecustomizerapi.MountIdentifierTypeDefault, diskutils.PartitionInfo{}, 0, err + var partition diskutils.PartitionInfo + var partitionIndex int + + if mountIdType != imagecustomizerapi.MountIdentifierTypeDeviceMapper { + partition, partitionIndex, err = findPartition(mountIdType, mountId, partitions) + if err != nil { + return imagecustomizerapi.MountIdentifierTypeDefault, diskutils.PartitionInfo{}, 0, err + } } return mountIdType, partition, partitionIndex, nil @@ -368,6 +371,11 @@ func parseSourcePartition(source string) (imagecustomizerapi.MountIdentifierType return imagecustomizerapi.MountIdentifierTypePartLabel, partLabel, nil } + deviceMapperValue, isDeviceMapper := strings.CutPrefix(source, "/dev/mapper") + if isDeviceMapper { + return imagecustomizerapi.MountIdentifierTypeDeviceMapper, deviceMapperValue, nil + } + err := fmt.Errorf("unknown fstab source type (%s)", source) return imagecustomizerapi.MountIdentifierTypeDefault, "", err }