diff --git a/docs/development/img.md b/docs/development/img.md index 6e81417..95cb178 100644 --- a/docs/development/img.md +++ b/docs/development/img.md @@ -301,12 +301,20 @@ The boot menu cannot edit the entries: their kernel command line, root hash incl part of the UKI. With Secure Boot off, a UEFI shell can still start the UKI with another command line (see [What Rite Live proves](#what-rite-live-proves)). -The session starts on its own as the `rite` user. Its start screen shows the `rite` -version, the ceremony definition with its SHA-256, where the outputs go, and the `rite run` -command for the ceremony. Pressing Enter starts `rite run` with that ceremony, and -`rite run` asks for any parameter the ceremony leaves open. For other flags, such as -`--dry-run` or `--role`, the operator types the word `shell` at the prompt and runs the -command by hand. +Before the session, the setup screen lets the operator choose a keyboard layout and a +time zone. US and UTC are the defaults. The layout is loaded and tested before it is +kept. The layout applies to the text console and both terminals; the time zone applies +to times shown by the session, including `date`. Transcripts always use UTC. The +displayed time comes from the machine clock. If it is wrong, power off from the setup +screen and correct it in the firmware settings. Setup runs once per boot. The session +start screen shows the selected layout and time zone. + +The session then starts as the `rite` user. Its start screen shows the `rite` version, +the ceremony definition and its SHA-256, where outputs go, and the `rite run` command +for the ceremony. It also shows the keyboard layout and time zone in use. Pressing +Enter starts `rite run` with that ceremony, and `rite run` asks for any parameter the +ceremony leaves open. For other flags, such as `--dry-run` or `--role`, the operator +types the word `shell` at the prompt and runs the command by hand. When the outputs partition is missing or does not mount, the start screen says so and offers no Enter shortcut: the ceremony's outputs would be lost at power off, so running it @@ -339,8 +347,6 @@ driver. A ceremony that needs an air gap establishes it physically, by leaving o cable, and records it as a witnessed confirmation step, such as `verify_air_gap` in `examples/showcase/demo.rite.yaml`. -The keyboard layout is US. - ### Known hardware issues - **Secure Boot.** The setting is named differently from one firmware to another. On ASUS @@ -447,7 +453,8 @@ any interaction with the boot process. Observation does not prove what booted. | `img/mkosi/mkosi.extra/` | Files copied into the image, at their paths in it | | `.../efi/loader/loader.conf` | Boot menu: timeout, no editing | | `.../usr/lib/systemd/system/rite-session.service`, `rite-shell.service`, `.../etc/pam.d/rite-session` | The session on tty1 and the shell on tty2, run as `rite` | -| `.../usr/lib/rite/rite-kiosk` | Starts the session or the shell in cage and foot, or on the text console | +| `.../usr/lib/systemd/system/rite-setup.service`, `.../usr/lib/rite/rite-setup`, `setup-values` | The setup screen: keyboard layout and time zone, chosen before the session | +| `.../usr/lib/rite/rite-kiosk` | Starts the session or the shell in cage and foot, or on the text console, with the layout and zone chosen at setup | | `.../usr/lib/rite/rite-session`, `rite-shell` | The operator's start screen, and the second terminal | | `.../usr/lib/systemd/system/rite-ceremony-load.service`, `.../usr/lib/rite/load-ceremony` | Copies the ceremony layer into memory and records its hash | | `.../usr/lib/rite/mount-outputs`, `boot-partition` | Mounts `RITE-OUT` at `/rite/outputs`; finds a partition on the boot disk | diff --git a/img/mkosi/mkosi.conf b/img/mkosi/mkosi.conf index a832262..f6f3d84 100644 --- a/img/mkosi/mkosi.conf +++ b/img/mkosi/mkosi.conf @@ -168,6 +168,12 @@ Packages= # The boot loader and the UKI stub, which mkosi takes from the image. systemd-boot-efi + # rite-setup uses whiptail for menus, ckbcomp to convert XKB layouts, + # and loadkeys to load the resulting console keymap. + whiptail + console-setup + kbd + # Pager, for man and the shell. less diff --git a/img/mkosi/mkosi.extra/usr/lib/rite/rite-kiosk b/img/mkosi/mkosi.extra/usr/lib/rite/rite-kiosk index c4795a7..9afe3cb 100755 --- a/img/mkosi/mkosi.extra/usr/lib/rite/rite-kiosk +++ b/img/mkosi/mkosi.extra/usr/lib/rite/rite-kiosk @@ -9,6 +9,21 @@ program=$1 +# Apply the settings chosen in rite-setup: cage reads the XKB variables and +# session programs read TZ. The settings file is root-owned before the session +# starts. If it is absent or invalid, the session uses US and UTC. +# shellcheck disable=SC1091 +source /usr/lib/rite/setup-values +if [[ -r $SETTINGS ]]; then + while IFS='=' read -r key value; do + case $key in + layout) valid_layout "$value" && export XKB_DEFAULT_LAYOUT=$value ;; + variant) valid_variant "$value" && export XKB_DEFAULT_VARIANT=$value ;; + timezone) valid_zone "$value" && export TZ=$value ;; + esac + done < "$SETTINGS" +fi + if ! grep -qwF 'rite.console' /proc/cmdline; then # cage takes the display, which only the terminal on screen can do: the # second terminal starts cage when the operator first switches to it. diff --git a/img/mkosi/mkosi.extra/usr/lib/rite/rite-session b/img/mkosi/mkosi.extra/usr/lib/rite/rite-session index 425f772..103c963 100755 --- a/img/mkosi/mkosi.extra/usr/lib/rite/rite-session +++ b/img/mkosi/mkosi.extra/usr/lib/rite/rite-session @@ -69,6 +69,12 @@ else echo "WARNING: no RITE-OUT partition. Outputs in $OUTPUTS are lost at power off." fi echo "" +# Set by rite-kiosk from the choice made in rite-setup. +# shellcheck disable=SC1091 +source /usr/lib/rite/setup-values +echo "Keyboard: $(layout_name "${XKB_DEFAULT_LAYOUT:-us}" "${XKB_DEFAULT_VARIANT:-}")" +echo "Time zone: ${TZ:-UTC}" +echo "" echo "Terminals: Ctrl+Alt+F2 opens a second terminal, Ctrl+Alt+F1 returns here." echo "" diff --git a/img/mkosi/mkosi.extra/usr/lib/rite/rite-setup b/img/mkosi/mkosi.extra/usr/lib/rite/rite-setup new file mode 100755 index 0000000..b260ba5 --- /dev/null +++ b/img/mkosi/mkosi.extra/usr/lib/rite/rite-setup @@ -0,0 +1,198 @@ +#!/bin/bash +# /usr/lib/rite/rite-setup +# The keyboard layout and time zone, chosen from menus on tty1 before the +# sessions start. Nothing typed by the operator becomes a setting. A layout +# is loaded into the console and tested before it is kept. The settings go to +# /run/rite-setup/settings, where rite-kiosk reads them for cage (XKB) and +# the session programs (TZ). If setup fails, the session uses US and UTC. +# This script always exits 0. + +# Ctrl-C, Ctrl-\ and Ctrl-Z do nothing: a stopped script would hold the boot. +trap '' INT QUIT TSTP + +# shellcheck disable=SC1091 +source /usr/lib/rite/setup-values + +COUNTRIES=/usr/share/zoneinfo/iso3166.tab +TITLE="Rite Live" + +layout=us +variant= +zone=UTC + +# whiptail draws on stdout and prints the choice on stderr; the swap returns +# the choice on stdout and draws on stderr, which is the terminal. +ask() { + whiptail --title "$TITLE" "$@" 3>&1 1>&2 2>&3 +} + +message() { + ask --msgbox "$1" 10 64 +} + +save() { + if ! printf 'layout=%s\nvariant=%s\ntimezone=%s\n' "$layout" "$variant" "$zone" \ + > "$SETTINGS.new" || ! mv -f "$SETTINGS.new" "$SETTINGS"; then + rm -f "$SETTINGS.new" "$SETTINGS" + layout=us + variant= + zone=UTC + load_keymap us "" + message "Could not save the settings. The sessions will use US and UTC." + fi +} + +# Loads layout $1, variant $2 into the console keymap, which every terminal +# shares. +load_keymap() { + local args=(-layout "$1") status + [[ -n $2 ]] && args+=(-variant "$2") + ask --infobox "Loading the keyboard layout..." 7 50 + ckbcomp "${args[@]}" 2>/dev/null | loadkeys -q 2>/dev/null + status=("${PIPESTATUS[@]}") + [[ ${status[0]} -eq 0 && ${status[1]} -eq 0 ]] +} + +# ckbcomp warns and falls back to the base layout for an unknown variant. +variant_listed() { + [[ -z $2 || -n $(xkb_description "$1" "$2") ]] +} + +# Every layout of the XKB rules, then its variants. Prints "layout variant". +other_layout() { + local name rest items=() choice + while read -r name rest; do + items+=("$name" "$rest") + done < <(xkb_section layout) + choice=$(ask --notags --menu "Keyboard layout:" 22 72 14 "${items[@]}") || return 1 + + # A variant line reads "name layout: description". + items=("-" "No variant") + while read -r name rest; do + [[ ${rest%%:*} == "$choice" ]] && items+=("$name" "${rest#*: }") + done < <(xkb_section variant) + if [[ ${#items[@]} -eq 2 ]]; then + echo "$choice" + return 0 + fi + rest=$(ask --notags --menu "Variant for $(layout_name "$choice" ""):" 22 72 14 \ + "${items[@]}") || return 1 + [[ $rest == - ]] && rest="" + echo "$choice $rest" +} + +change_keyboard() { + local choice new_layout new_variant new_name current_name typed + # Widely used layouts, by their names in the XKB rules. + choice=$(ask --notags --default-item "$layout${variant:+ $variant}" \ + --menu "Keyboard layout:" 20 60 11 \ + "gb" "English (UK)" \ + "us" "English (US)" \ + "fr" "French" \ + "ch fr" "French (Switzerland)" \ + "de" "German" \ + "ch" "German (Switzerland)" \ + "it" "Italian" \ + "jp" "Japanese" \ + "br" "Portuguese (Brazil)" \ + "es" "Spanish" \ + "other" "Other layouts...") || return 0 + if [[ $choice == other ]]; then + choice=$(other_layout) || return 0 + fi + read -r new_layout new_variant <<< "$choice" + new_name=$(layout_name "$new_layout" "$new_variant") + current_name=$(layout_name "$layout" "$variant") + if ! valid_layout "$new_layout" || ! valid_variant "$new_variant" \ + || ! variant_listed "$new_layout" "$new_variant"; then + message "Layout $new_name is not supported. Keeping $current_name." + return 0 + fi + + if ! load_keymap "$new_layout" "$new_variant"; then + if load_keymap "$layout" "$variant"; then + message "Could not load $new_name. Keeping $current_name." + else + message "Could not load $new_name or restore $current_name. The keyboard may not work. Power off and restart." + fi + return 0 + fi + typed=$(ask --inputbox "Type anything to check the layout, then press Enter." \ + 8 60) || typed="" + if ask --yesno "You typed: + + $typed + +Keep this layout: $new_name?" 12 60; then + layout=$new_layout + variant=$new_variant + save + elif ! load_keymap "$layout" "$variant"; then + # The console still has the new layout: keep it, so that the settings + # and the start screen say what the keyboard does. + layout=$new_layout + variant=$new_variant + save + message "Could not restore $current_name. Keeping $new_name." + fi +} + +# UTC or a region, then a zone of that region, each shown with its country. +change_zone() { + local choice items=("UTC" "") code name comment + local -A country + while IFS= read -r name; do + items+=("$name" "") + done < <(grep -v '^#' "$ZONE_TAB" | cut -f3 | cut -d/ -f1 | sort -u) + choice=$(ask --default-item "${zone%%/*}" --menu "Choose a region:" 20 50 12 \ + "${items[@]}") || return 0 + + if [[ $choice != UTC ]]; then + while IFS=$'\t' read -r code name; do + country[$code]=$name + done < <(grep -v '^#' "$COUNTRIES") + items=() + while IFS=$'\t' read -r code _ name comment; do + [[ $name == "$choice"/* ]] || continue + items+=("$name" "${country[$code]:-$code}${comment:+, $comment}") + done < <(grep -v '^#' "$ZONE_TAB" | sort -t$'\t' -k3) + choice=$(ask --default-item "$zone" --menu "Choose a time zone:" 22 76 14 \ + "${items[@]}") || return 0 + fi + if ! valid_zone "$choice"; then + message "Time zone $choice is not supported. Keeping $zone." + return 0 + fi + zone=$choice + save +} + +while true; do + choice=$(ask --notags --nocancel --default-item start --menu "\ +Keyboard: $(layout_name "$layout" "$variant") +Time zone: $zone +Time: $(date -u '+%Y-%m-%d %H:%M UTC'), $(TZ=$zone date '+%H:%M %Z') local + +Check the clock against a watch. +If it is wrong, power off and correct it in the firmware settings." 19 76 5 \ + "start" "Continue" \ + "keyboard" "Change the keyboard layout" \ + "zone" "Change the time zone" \ + "time" "Refresh the time" \ + "poweroff" "Power off") || choice=start + case $choice in + keyboard) change_keyboard ;; + zone) change_zone ;; + time) ;; + poweroff) + if ask --defaultno --yesno "Power off the machine?" 8 40; then + # polkit lets the rite user power off (10-rite.rules). + systemctl poweroff && exit 0 + message "Could not power off the machine. Try again." + fi + ;; + *) break ;; + esac +done +clear +exit 0 diff --git a/img/mkosi/mkosi.extra/usr/lib/rite/setup-values b/img/mkosi/mkosi.extra/usr/lib/rite/setup-values new file mode 100644 index 0000000..2bc7d1c --- /dev/null +++ b/img/mkosi/mkosi.extra/usr/lib/rite/setup-values @@ -0,0 +1,56 @@ +# shellcheck shell=bash +# /usr/lib/rite/setup-values +# The keyboard layout and time zone values: their checks and the names the +# screens show. Sourced by rite-setup, which writes them, and by rite-kiosk and +# rite-session, which read them back. + +# Used by the scripts that source this file. +# shellcheck disable=SC2034 +SETTINGS=/run/rite-setup/settings +XKB_RULES=/usr/share/X11/xkb/rules/evdev.lst +ZONE_TAB=/usr/share/zoneinfo/zone.tab + +valid_layout() { + [[ $1 =~ ^[a-z0-9_]+$ ]] +} + +# Some variants have capitals: de T3, jp OADG109A. +valid_variant() { + [[ -z $1 || $1 =~ ^[A-Za-z0-9_][A-Za-z0-9_-]*$ ]] +} + +# UTC, or a zone of zone.tab, which lists one or more zones per country. +valid_zone() { + [[ $1 == UTC ]] && return 0 + [[ $1 =~ ^[A-Za-z0-9_+-]+(/[A-Za-z0-9_+-]+)*$ ]] \ + && grep -v '^#' "$ZONE_TAB" | cut -f3 | grep -qxF -- "$1" +} + +# Prints the entries of section $1 of the XKB rules ("layout", "variant"), one +# per line: the name, then the description. A variant's description starts +# with its layout: "fr ch: French (Switzerland)". +xkb_section() { + awk -v want="$1" '/^! / { section = $2; next } NF && section == want { + sub(/^ +/, ""); print }' "$XKB_RULES" +} + +# Prints the description of layout $1 with variant $2 from the XKB rules, such +# as "French (Switzerland)", or nothing if the rules do not list them. awk reads +# the file itself: a lookup that stops early must not leave a writer on a closed +# pipe, since systemd services ignore SIGPIPE. +xkb_description() { + awk -v layout="$1" -v variant="$2" ' + /^! / { section = $2; next } + variant == "" && section == "layout" && $1 == layout { + sub(/^ *[^ ]+ +/, ""); print; exit } + variant != "" && section == "variant" && $1 == variant && $2 == layout ":" { + sub(/^ *[^ ]+ +[^ ]+ +/, ""); print; exit }' "$XKB_RULES" +} + +# Prints the name of layout $1 with variant $2, or the codes if the XKB rules +# do not list it. +layout_name() { + local name + name=$(xkb_description "$1" "$2") + echo "${name:-$1${2:+ ($2)}}" +} diff --git a/img/mkosi/mkosi.extra/usr/lib/systemd/system-preset/10-rite.preset b/img/mkosi/mkosi.extra/usr/lib/systemd/system-preset/10-rite.preset index 080a2cc..894880e 100644 --- a/img/mkosi/mkosi.extra/usr/lib/systemd/system-preset/10-rite.preset +++ b/img/mkosi/mkosi.extra/usr/lib/systemd/system-preset/10-rite.preset @@ -1,6 +1,7 @@ # Units of the Live system. mkosi applies the presets at the end of the # build (systemctl preset-all); Debian's default enables every other unit, # systemd-networkd and systemd-resolved among them. +enable rite-setup.service enable rite-session.service enable rite-shell.service enable rite-ceremony-load.service diff --git a/img/mkosi/mkosi.extra/usr/lib/systemd/system/rite-setup.service b/img/mkosi/mkosi.extra/usr/lib/systemd/system/rite-setup.service new file mode 100644 index 0000000..9726fae --- /dev/null +++ b/img/mkosi/mkosi.extra/usr/lib/systemd/system/rite-setup.service @@ -0,0 +1,40 @@ +# The keyboard layout and time zone, chosen on tty1 before the sessions start +# (rite-setup), as the rite user. The sessions start whether setup succeeds or +# not. On failure, the stop command removes the settings and restores the US +# console keymap. +[Unit] +Description=Rite keyboard layout and time zone +After=systemd-user-sessions.service +Before=rite-session.service rite-shell.service +ConditionPathExists=/dev/tty1 + +[Service] +Type=oneshot +# Once per boot. +RemainAfterExit=yes +# Kernel messages would print over the menus. +ExecStartPre=+/usr/bin/dmesg --console-off +ExecStart=/usr/lib/rite/rite-setup +# Restore kernel messages and protect settings before the sessions start. +# On failure, the stop command also restores the US keymap. +ExecStartPost=+/usr/bin/dmesg --console-on +ExecStartPost=+/usr/bin/chown -R root:root /run/rite-setup +ExecStopPost=+/usr/bin/dmesg --console-on +ExecStopPost=+/usr/bin/chown -R root:root /run/rite-setup +ExecStopPost=+/bin/sh -c 'test "$SERVICE_RESULT" = success || { rm -f /run/rite-setup/settings; ckbcomp us | loadkeys -q; }' +User=rite +# Loading the console keymap takes CAP_SYS_TTY_CONFIG, even on the process's +# own terminal. +AmbientCapabilities=CAP_SYS_TTY_CONFIG +RuntimeDirectory=rite-setup +RuntimeDirectoryPreserve=yes +TimeoutStartSec=infinity +TTYPath=/dev/tty1 +TTYReset=yes +TTYVHangup=yes +StandardInput=tty +StandardOutput=tty +StandardError=tty + +[Install] +WantedBy=multi-user.target diff --git a/img/mkosi/mkosi.postinst.chroot b/img/mkosi/mkosi.postinst.chroot index ee892a7..5fed228 100755 --- a/img/mkosi/mkosi.postinst.chroot +++ b/img/mkosi/mkosi.postinst.chroot @@ -31,6 +31,11 @@ systemctl mask getty@tty1.service getty@tty2.service # for them at the console. systemctl mask systemd-firstboot.service +# rite-setup loads the console keymap the operator chooses. Mask +# console-setup.service so it cannot replace that keymap after setup, and +# keyboard-setup.service, whose US keymap setup replaces anyway. +systemctl mask keyboard-setup.service console-setup.service + # systemd would mount the EFI system partition read-write on first access to # /boot or /efi. The system needs nothing there after boot, and a mount can # change the partition's bytes, which the medium hash of build-img covers.