max / alloy
1 file changed,
+94 insertions,
-2 deletions
| @@ -626,6 +626,27 @@ | |||
| 626 | 626 | .arg(partition) | |
| 627 | 627 | } | |
| 628 | 628 | ||
| 629 | + | /// The kernel command line `--block-setup tpm2-luks` writes, verbatim. | |
| 630 | + | /// | |
| 631 | + | /// Read off an installed disk's boot entry in a VM 2026-08-09, not from bootc's | |
| 632 | + | /// source, so it is a fact about the bootc being shipped rather than about the | |
| 633 | + | /// one being read. It is deleted whole and replaced by [`LUKS_PROMPTING_KARG`]; | |
| 634 | + | /// see the deploy stage in [`install_plan`] for why. | |
| 635 | + | /// | |
| 636 | + | /// **If bootc changes this string the delete stops matching and stops saying | |
| 637 | + | /// so.** `--karg-delete` is content with removing nothing. The install would | |
| 638 | + | /// then carry both this argument and the replacement, and go back to producing | |
| 639 | + | /// machines that cannot be unlocked by hand. That is the cost of the approach | |
| 640 | + | /// and the reason the string is stated once, here, rather than inline. | |
| 641 | + | const LUKS_HEADLESS_KARG: &str = "luks.options=tpm2-device=auto,headless=true"; | |
| 642 | + | ||
| 643 | + | /// The same argument with `headless=true` gone, which is the whole change. | |
| 644 | + | /// | |
| 645 | + | /// The TPM is still tried first and still opens the disk unattended on a | |
| 646 | + | /// machine whose TPM answers. What changes is only what happens when it does | |
| 647 | + | /// not: a prompt, rather than a failed unit. | |
| 648 | + | const LUKS_PROMPTING_KARG: &str = "luks.options=tpm2-device=auto"; | |
| 649 | + | ||
| 629 | 650 | /// The filesystem label bootc gives the separate `/boot` partition. | |
| 630 | 651 | /// | |
| 631 | 652 | /// bootc creates that partition only when the root is encrypted: the bootloader | |
| @@ -2620,6 +2641,34 @@ | |||
| 2620 | 2641 | // is a permanently unreadable disk. | |
| 2621 | 2642 | if encrypt { | |
| 2622 | 2643 | install = install.args(["--block-setup", "tpm2-luks"]); | |
| 2644 | + | // And then undo the half of that flag which makes the two | |
| 2645 | + | // enrollments below unreachable. | |
| 2646 | + | // | |
| 2647 | + | // `--block-setup tpm2-luks` writes the kernel command line | |
| 2648 | + | // [`LUKS_HEADLESS_KARG`], and `headless=true` means never | |
| 2649 | + | // prompt. When the TPM does not satisfy — a cleared TPM, a | |
| 2650 | + | // replaced board, the disk read in another machine, which is | |
| 2651 | + | // the entire case [`enroll_plan`] exists for — | |
| 2652 | + | // systemd-cryptsetup fails the unit instead of asking, and the | |
| 2653 | + | // boot is over with no way to type either secret. | |
| 2654 | + | // | |
| 2655 | + | // That is the invariant [`acceptance_checks`] already asserts, | |
| 2656 | + | // undone one argument later: the install refuses to finish when | |
| 2657 | + | // the header has no slot but the TPM's, and then shipped a | |
| 2658 | + | // machine where that slot could never be used. Measured in a VM | |
| 2659 | + | // 2026-08-09: the install completes, the passphrase opens the | |
| 2660 | + | // volume from another machine, and the machine itself stops at | |
| 2661 | + | // "Failed to start systemd-cryptsetup@luks-...". Deleting this | |
| 2662 | + | // one option from the boot entry, and nothing else, makes the | |
| 2663 | + | // same disk prompt, unlock and reach the greeter. | |
| 2664 | + | // | |
| 2665 | + | // Deleted whole and rewritten rather than edited, because | |
| 2666 | + | // --karg-delete matches a kernel argument exactly and | |
| 2667 | + | // `luks.options` is one argument with a comma-separated value. | |
| 2668 | + | // That makes it bootc's exact string this depends on, which is | |
| 2669 | + | // why [`LUKS_HEADLESS_KARG`] is named and stated once. | |
| 2670 | + | install = install.args(["--karg-delete", LUKS_HEADLESS_KARG]); | |
| 2671 | + | install = install.args(["--karg", LUKS_PROMPTING_KARG]); | |
| 2623 | 2672 | } | |
| 2624 | 2673 | install.arg(disk) | |
| 2625 | 2674 | }), | |
| @@ -5940,6 +5989,45 @@ | |||
| 5940 | 5989 | assert!(Stage::Run(unmount(TARGET_MOUNT)).display().contains("-R")); | |
| 5941 | 5990 | } | |
| 5942 | 5991 | ||
| 5992 | + | // The replacement has to be the deleted argument minus `headless=true` and | |
| 5993 | + | // nothing else. Written as a derivation rather than as two literals: two | |
| 5994 | + | // hand-written strings can drift apart, and the drift that matters here is | |
| 5995 | + | // silent, since a delete that no longer matches removes nothing and says | |
| 5996 | + | // nothing. | |
| 5997 | + | #[test] | |
| 5998 | + | fn the_prompting_karg_is_the_headless_one_with_the_flag_removed() { | |
| 5999 | + | assert_eq!( | |
| 6000 | + | LUKS_HEADLESS_KARG.replace(",headless=true", ""), | |
| 6001 | + | LUKS_PROMPTING_KARG, | |
| 6002 | + | ); | |
| 6003 | + | } | |
| 6004 | + | ||
| 6005 | + | // An encrypted deploy has to both delete and replace. Deleting alone leaves | |
| 6006 | + | // the volume with no tpm2-device option and no unattended unlock; replacing | |
| 6007 | + | // alone leaves both arguments on the command line. | |
| 6008 | + | #[test] | |
| 6009 | + | fn the_encrypted_deploy_replaces_the_headless_luks_karg() { | |
| 6010 | + | let shown = deploy_line(true); | |
| 6011 | + | assert!( | |
| 6012 | + | shown.contains(&format!("--karg-delete {LUKS_HEADLESS_KARG}")), | |
| 6013 | + | "{shown}", | |
| 6014 | + | ); | |
| 6015 | + | assert!( | |
| 6016 | + | shown.contains(&format!("--karg {LUKS_PROMPTING_KARG}")), | |
| 6017 | + | "{shown}" | |
| 6018 | + | ); | |
| 6019 | + | } | |
| 6020 | + | ||
| 6021 | + | // And touches neither when there is no LUKS volume to unlock. The karg | |
| 6022 | + | // belongs to --block-setup, so an unencrypted install never carries it and | |
| 6023 | + | // a delete against it would be noise on the summary the user reads. | |
| 6024 | + | #[test] | |
| 6025 | + | fn a_plain_deploy_says_nothing_about_luks_kargs() { | |
| 6026 | + | let shown = deploy_line(false); | |
| 6027 | + | assert!(!shown.contains("--karg"), "{shown}"); | |
| 6028 | + | assert!(!shown.contains("luks.options"), "{shown}"); | |
| 6029 | + | } | |
| 6030 | + | ||
| 5943 | 6031 | // Both slots bootc wipes, enrolled against the container rather than the | |
| 5944 | 6032 | // mapper device: the header is on the partition. | |
| 5945 | 6033 | #[test] | |
| @@ -6033,12 +6121,16 @@ | |||
| 6033 | 6121 | // --block-setup because the fixture takes the offered default and | |
| 6034 | 6122 | // encrypts. It is on the same line and there for the same reason: it | |
| 6035 | 6123 | // decides how the machine behaves for its whole life, and it is not | |
| 6036 | - | // reversible after this command. | |
| 6124 | + | // reversible after this command. The two karg arguments after it are | |
| 6125 | + | // that flag's correction, and belong on the same line for the same | |
| 6126 | + | // reason: they decide whether a machine whose TPM stops answering can | |
| 6127 | + | // be unlocked by hand at all. | |
| 6037 | 6128 | assert_eq!( | |
| 6038 | 6129 | shown[0], | |
| 6039 | 6130 | format!( | |
| 6040 | 6131 | "bootc install to-disk --wipe --target-imgref {UPDATE_IMAGE} \ | |
| 6041 | - | --block-setup tpm2-luks /dev/sda" | |
| 6132 | + | --block-setup tpm2-luks --karg-delete {LUKS_HEADLESS_KARG} \ | |
| 6133 | + | --karg {LUKS_PROMPTING_KARG} /dev/sda" | |
| 6042 | 6134 | ), | |
| 6043 | 6135 | ); | |
| 6044 | 6136 | assert_eq!(shown[1], "udevadm settle"); |