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

[RFC] Add support for systemd initcpio setups #25

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
*.pkg.tar.gz
*.pkg.tar.xz
*.pkg.tar.zst
2 changes: 2 additions & 0 deletions 95-smartcard.rules
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
SUBSYSTEM=="usb", ATTRS{idVendor}=="1050", ENV{ID_SMARTCARD_READER}=="?*", SYMLINK+="ykccid"

44 changes: 39 additions & 5 deletions PKGBUILD
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
pkgname=initramfs-scencrypt
pkgdesc="initramfs hook that adds PGP smartcard support for LUKS FDE"
pkgver=1.8
pkgver=2.0
pkgrel=1
license=(MIT)
arch=(any)
depends=(gnupg)
install=${pkgname}.install
source=(scencrypt-hook
scencrypt-install
systemd-initramfs-gpg-init.service
systemd-initramfs-gpg-init
[email protected]
systemd-gpg-decrypt
[email protected]
cryptsetup-gpg-dropin-generator
scencrypt-migrate
95-smartcard.rules
README.md)

build() {
Expand All @@ -16,15 +24,41 @@ build() {

package() {
mkdir -p "${pkgdir}/usr/lib/initcpio/hooks"
install -oroot -m0755 "${srcdir}/scencrypt-hook" "${pkgdir}/usr/lib/initcpio/hooks/scencrypt"

mkdir -p "${pkgdir}/usr/lib/initcpio/install"
install -oroot -m0755 "${srcdir}/scencrypt-install" "${pkgdir}/usr/lib/initcpio/install/scencrypt"

mkdir -p "${pkgdir}/usr/lib/systemd"
install -oroot -m0755 "${srcdir}/systemd-initramfs-gpg-init" "${pkgdir}/usr/lib/systemd/systemd-initramfs-gpg-init"
install -oroot -m0755 "${srcdir}/systemd-gpg-decrypt" "${pkgdir}/usr/lib/systemd/systemd-gpg-decrypt"

mkdir -p "${pkgdir}/usr/lib/systemd/system-generators"
install -oroot -m0755 "${srcdir}/cryptsetup-gpg-dropin-generator" "${pkgdir}/usr/lib/systemd/system-generators/cryptsetup-gpg-dropin-generator"

mkdir -p "${pkgdir}/usr/lib/systemd/system"
install -oroot -m0644 "${srcdir}/systemd-initramfs-gpg-init.service" "${pkgdir}/usr/lib/systemd/system/systemd-initramfs-gpg-init.service"
install -oroot -m0644 "${srcdir}/[email protected]" "${pkgdir}/usr/lib/systemd/system/[email protected]"
install -oroot -m0644 "${srcdir}/[email protected]" "${pkgdir}/usr/lib/systemd/system/[email protected]"

mkdir -p "${pkgdir}/usr/lib/initcpio/udev"
install -oroot -m0644 "${srcdir}/95-smartcard.rules" "${pkgdir}/usr/lib/initcpio/udev/95-smartcard.rules"

cp "${srcdir}/scencrypt-hook" "${pkgdir}/usr/lib/initcpio/hooks/scencrypt"
cp "${srcdir}/scencrypt-install" "${pkgdir}/usr/lib/initcpio/install/scencrypt"

mkdir -p "${pkgdir}/usr/share/doc/${pkgname}"
cp "${srcdir}/README.md" "${pkgdir}/usr/share/doc/${pkgname}/"

mkdir -p "${pkgdir}/usr/bin"
install -oroot -m0755 "${srcdir}/scencrypt-migrate" "${pkgdir}/usr/bin/scencrypt-migrate"
}

md5sums=('SKIP'
sha256sums=('SKIP'
'SKIP'
'SKIP'
'SKIP'
'SKIP'
'SKIP'
'SKIP'
'SKIP'
'SKIP'
'SKIP'
'SKIP')
26 changes: 21 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,33 @@ Use this hook at your own risk. It's highly recommended to have a backup key som
1. `sudo cryptsetup luksAddKey /dev/your_luks_device /dev/shm/disk.bin`
1. `shred -u -n1 /dev/shm/disk.bin` to delete the decrypted `disk.bin` file from memory.
1. Edit `/etc/crypttab` to include your encrypted device. The line will look somewhat like:
`arch_crypt /dev/your_luks_device /home/you/disk.bin.gpg discard`
1. Edit `/etc/mkinitcpio.conf` and replace the `encrypt` hook with `scencrypt`. Do not leave both `encrypt` and `scencrypt` enabled.
1. Make sure `root` has a GPG keychain with your public key (e.g. `gpg --export -a 0x13C7C0BA66FB8DC7 > ~/pub.gpg`, `sudo su`, `gpg --import /home/<user>/pub.gpg`
`arch_crypt /dev/your_luks_device none pgp-keyfile=/etc/disk-keys/disk.bin.gpg,discard`
1. Edit `/etc/mkinitcpio.conf` and add the `scencrypt` hook.
If you are using a traditional initramfs, **do not** leave both `encrypt` and `scencrypt` enabled.
If you are using systemd in your rootfs, you **must** leave sd-encrypt enabled.
1. Export public keys (when using a smartcard) or private keys (when using software decryption) into a `.asc` file in `/etc/initcpio/gpg/public-keys.d`.
1. Run `mkinitcpio -p linux`. If there are no errors, reboot with your smart card plugged in to find out if it works.
1. (Optional) `sudo cryptSetup luksRemoveKey /dev/your_luks_device` and type the passphrase you added when you were installing Arch. This will remove the old passphrase so that only your GPG-encrypted key file can unseal the disk.
1. (Optional) Remove previously-used passphrases so that only your GPG-encrypted key file can unseal the disk. Optionally, enroll a recovery key that is saved offline.

# Migration from v1.x

To facilitate compatibility with `systemd-cryptsetup`, version 2.x requires the PGP key file to be passed in the fourth column as `pgp-keyfile=...` rather than the third column in `crypttab`.

A script called `scencrypt-migrate` is included which will automatically rewrite your crypttab (and save a backup in `/etc/crypttab.old`, of course). While the non-systemd version of the hook attempts to support the old format, this will not remain supported and may be subject to cleanup in the future.

The `scencrypt-migrate` script will also attempt to export your PGP keys to the new storage location. Be advised that it only attempts to export public keys, so if your private key is normally bundled into the initramfs, you'll need to export this manually.

Once your `crypttab` has been migrated, you can switch to a systemd-based initramfs by replacing the `udev` hook with `systemd`, and adding the `sd-encrypt` hook after `scencrypt`.

# Technical details

The hook works by copying your encrypted key file to the initramfs, decrypting it in memory, passing it to LUKS to unseal the disk, and then using `shred` to overwrite it in memory.

Behind the scenes, `gpg` starts `scdaemon`, which talks to `pcscd` and `pinentry-tty` to get your PIN and pass it to the card along with the payload for decryption. The private key itself is held securely on the smartcard - it cannot be released even with the PIN on hand. But the decryption is quick because the payload is small. Once the disk is mounted, the smartcard can safely be removed from the system - the result of the decryption is merely a "user key" that LUKS uses to decrypt the volume's master key. There is an excellent [white paper](http://clemens.endorphin.org/nmihde/nmihde-A4-ds.pdf) written by one of the original LUKS authors detailing LUKS's extensive anti-forensic hardening.
Behind the scenes, `gpg` starts `scdaemon`, which talks to `pcscd` and `pinentry-tty` to get your PIN and pass it to the card along with the payload for decryption.

For installations using a systemd-based initramfs, the process is a little more complicated, because the PIN comes in through `systemd-ask-password` and a series of units and dependencies is used to allow the same keyfile to be used for multiple disks while only asking for the PIN once.

The private key itself is held securely on the smartcard - it cannot be released even with the PIN on hand. But the decryption is quick because the payload is small. Once the disk is mounted, the smartcard can safely be removed from the system - the result of the decryption is merely a "key encryption key" (KEK) that LUKS uses to decrypt the real data encryption key (DEK). There is an excellent [white paper](http://clemens.endorphin.org/nmihde/nmihde-A4-ds.pdf) written by one of the original LUKS authors detailing LUKS's extensive anti-forensic hardening.

The hook will prefer `cryptkey=` kernel cmdline argument if present. It uses the same options as the stock `encrypt` hook, refer to the `cryptsetup` package for details. This allows you to use `kexec` without having to re-insert your YubiKey. For this to work you can kexec-load a `initrd` which contains the plain key file. For security reasons that initrd shall only reside in RAM. Have a look at [kexec-example.sh](kexec-example.sh).

Expand Down
84 changes: 84 additions & 0 deletions cryptsetup-gpg-dropin-generator
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#!/bin/sh

set -x

NORMAL_GENERATOR_DIR=${1:-/run/systemd/generator}
EARLY_GENERATOR_DIR=$2
LATE_GENERATOR_DIR=$3
SYSTEM_GENERATOR_DIR="${NORMAL_GENERATOR_DIR}"
UNIT_SOURCE_DIR=/usr/lib/systemd/system

if [ -z "$PATH" ]; then
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
fi

condlink() {
if test -h "$2" ; then
rm -f "$2"
fi
if test -e "$2"; then
echo "cannot create symbolic link $2: file exists" >&2
return 1
fi
ln -s "$1" "$2"
}

generate_dropin() {
local mapped_name="$1"
local escaped_name="`systemd-escape "$mapped_name"`"
local cryptsetup_unit="systemd-cryptsetup@${escaped_name}.service"
local password_file="$2"
local password_escaped="`systemd-escape --path "${password_file}"`"
local password_unit="systemd-gpg-decrypt@${password_escaped}.service"
local keyfile_unit="systemd-cryptsetup-pgp-keyfile@${escaped_name}.service"

mkdir -p "$SYSTEM_GENERATOR_DIR/${password_unit}.d"
cat <<EOF > "$SYSTEM_GENERATOR_DIR/${password_unit}.d/10-path.conf"
[Service]
Environment=INPUT_FILE=${password_file}

EOF

mkdir -p "$SYSTEM_GENERATOR_DIR/${keyfile_unit}.d"
cat <<EOF > "$SYSTEM_GENERATOR_DIR/${keyfile_unit}.d/10-parent.conf"
[Unit]
After=${password_unit}

[Service]
Environment=KEY_NAME=${password_escaped}
EOF

mkdir -p "$SYSTEM_GENERATOR_DIR/${password_unit}.requires"
condlink "${UNIT_SOURCE_DIR}/systemd-initramfs-gpg-init.service" "$SYSTEM_GENERATOR_DIR/${password_unit}.requires/systemd-initramfs-gpg-init.service"

mkdir -p "$SYSTEM_GENERATOR_DIR/${keyfile_unit}.requires"
condlink "${UNIT_SOURCE_DIR}/[email protected]" "$SYSTEM_GENERATOR_DIR/${keyfile_unit}.requires/${password_unit}"

mkdir -p "$SYSTEM_GENERATOR_DIR/${cryptsetup_unit}.wants"
condlink "${UNIT_SOURCE_DIR}/[email protected]" "$SYSTEM_GENERATOR_DIR/${cryptsetup_unit}.wants/${keyfile_unit}"

mkdir -p "$SYSTEM_GENERATOR_DIR/cryptsetup-pre.target.wants"
condlink "${UNIT_SOURCE_DIR}/[email protected]" "$SYSTEM_GENERATOR_DIR/cryptsetup-pre.target.wants/${keyfile_unit}"
condlink "${UNIT_SOURCE_DIR}/systemd-initramfs-gpg-init.service" "$SYSTEM_GENERATOR_DIR/cryptsetup-pre.target.wants/systemd-initramfs-gpg-init.service"
}

if test "$SYSTEMD_IN_INITRD" != "1"; then
exit 0
fi

while read line; do
line="${line%#*}"
IFS=$' \t' read mapped_name device keyfile options <<EOF
$line
EOF
for opt in ${options//,/ }; do
case "$opt" in
pgp-keyfile=*)
generate_dropin "$mapped_name" "${opt:12}"
;;
*)
: ;;
esac
done
done < /etc/crypttab

15 changes: 13 additions & 2 deletions initramfs-scencrypt.install
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,19 @@ post_install() {
echo " >> Make sure to add your encrypted disks to /etc/crypttab. There is"
echo " >> no support for cryptdevice=... on the kernel command line."
echo ""
echo " >> Key file path must end in .gpg and the private key must be"
echo " >> accessible by root."
echo -e " \e[0;1m>> \e[31;1mTHIS VERSION BREAKS BACKWARD COMPATIBILITY!"
echo -e " \e[0;1m>> Run the \e[36;1mscencrypt-migrate\e[0;1m command to automatically "
echo -e " \e[0;1m>> update your /etc/crypttab and key storage.\e[0m"
echo ""
echo " >> For more information, read /usr/share/doc/initramfs-scencrypt/README.md"
}

post_upgrade() {
echo " >> initramfs-scencrypt - READ ME!"
echo ""
echo -e " \e[0;1m>> \e[31;1mTHIS VERSION BREAKS BACKWARD COMPATIBILITY!"
echo -e " \e[0;1m>> Run the \e[36;1mscencrypt-migrate\e[0;1m command to automatically "
echo -e " \e[0;1m>> update your /etc/crypttab and key storage.\e[0m"
echo ""
echo " >> For more information, read /usr/share/doc/initramfs-scencrypt/README.md"
}
70 changes: 50 additions & 20 deletions scencrypt-hook
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ retry() {
}

run_hook() {
INIT="`readlink -f /proc/1/exe`"
if test "${INIT##*/}" = "systemd" ; then
# we do not run under systemd
return 0
fi
modprobe -a -q dm-crypt >/dev/null 2>&1
[ "${quiet}" = "y" ] && CSQUIET=">/dev/null"

Expand Down Expand Up @@ -99,30 +104,38 @@ EOF
$line
EOF

# First, parse the key_spec field. The legacy behavior was to use the first
# argument in this column as the key path, and treat it as a PGP keyfile if
# the filename ends with ".gpg".
IFS=: read key_file keyarg1 keyarg2 <<EOF
$key_spec
EOF
# handle case of no key file
if [ "$key_file" = "-" -o "$key_file" = "none" ]; then
key_file=
elif [ -r "${ckeyfile}" ]; then
# plain key file via cmdline cryptkey=
echo "Using plain key file specified in cryptkey="
key_file="${ckeyfile}"
elif [ -c "${key_file}" ]; then
# key file is a character device
length=${keyarg1:-32}
dd if=$key_file of=/keyfile.bin bs=1 count=$length >/dev/null 2>&1
key_file=/keyfile.bin
elif [ -b "${key_file}" ]; then
echo "ERROR: Key files on block devices are not supported yet."
key_file=
elif [ -r "${key_file}" -a "${key_file%.gpg}" != "${key_file}" ]; then

key_file_is_pgp=no
if test "${key_file%.gpg}" != "${key_file}"; then
key_file_is_pgp=yes
fi

# Now check the options column - the "new" way of specifying the key file is
# with the `pgp-keyfile=` option in the fourth column. This is for consistency
# with other FDE hooks.
IFS="$IFS_BACKUP"
for opt in ${options//,/ }; do
case "$opt" in
pgp-keyfile=*)
key_file_is_pgp=yes
key_file=${opt:12} ;;
*)
: ;;
esac
done

if test -r "${key_file}" -a "$key_file_is_pgp" = "yes"; then
# /.gnupg is where the scdaemon socket lives
test -d /.gnupg || mkdir -p /.gnupg
chmod -R go-rwx /.gnupg /etc/initcpio/gpg

# store the key at a known path. this allows the same key to be
# store the key at a known path. this allows the same key to be
# used for multiple disks and only have to decrypt once.
key_dest_path=/etc/initcpio/gpg/key_${key_file//\//S}

Expand All @@ -132,10 +145,10 @@ EOF
else
# we need to decrypt.

# test communication with card - this is also needed for
# test communication with card - this is also needed for
# decryption to work at all
retry 60 "Waiting for the smartcard to be inserted
(or press enter to falling back to passphrase)" card_status
(or press enter to use passphrase instead)" card_status

# now attempt to decrypt
if decrypt_file "${key_file}" "${key_dest_path}"; then
Expand All @@ -148,6 +161,21 @@ EOF
key_file=
fi
fi
elif [ "$key_file" = "-" -o "$key_file" = "none" ]; then
# handle case of no key file
key_file=
elif [ -r "${ckeyfile}" ]; then
# plain key file via cmdline cryptkey=
echo "Using plain key file specified in cryptkey="
key_file="${ckeyfile}"
elif [ -c "${key_file}" ]; then
# key file is a character device
length=${keyarg1:-32}
dd if=$key_file of=/keyfile.bin bs=1 count=$length >/dev/null 2>&1
key_file=/keyfile.bin
elif [ -b "${key_file}" ]; then
echo "ERROR: Key files on block devices are not supported yet."
key_file=
elif [ -r "${key_file}" ]; then
cp "${key_file}" /keyfile.bin
key_file=/keyfile.bin
Expand All @@ -163,8 +191,10 @@ EOF
discard)
luksoptions="$luksoptions --allow-discards"
;;
pgp-keyfile=*)
: ;;
*)
echo "Warning: ignoring unknown crypttab option: $option"
echo "Warning: ignoring unknown crypttab option: $option" ;;
esac
done

Expand Down
Loading