Wren Linux

wren-linux / extras / security-key.sh

#!/bin/bash
# A hardware security key (YubiKey, Nitrokey, SoloKey, Google Titan, any FIDO2
# key): its secret is made inside the chip and never leaves it, and it answers
# only when you touch it, so malware can neither copy it nor use it for you.
#  - Linux (udev 244 and later) lets you, at the keyboard, use such a key by
#    itself: no rules from the maker are needed. Other users get none.
#  - SSH: a key whose file is only a handle, every login needing the key and a
#    touch (ssh-keygen -t ed25519-sk; tools/vps-client.sh --ssh-key --security-key).
#  - Websites: two-factor login (WebAuthn) works in Firefox run as you (firefox-esr,
#    or firefox-unsafe with the sandboxed Firefox installed). The sandboxed
#    Firefox runs as other users; if you say yes, a udev rule lets them (the
#    group wren-fx) reach the key too (data/security-key-firefox.rules).
#  - sudo, if you say yes (or later: --sudo): your password AND a touch of the
#    key (pam_u2f). The keys it takes are listed in /etc/wren/u2f_keys, a file
#    only root can change, so nothing running as you can add one of its own. It
#    is tried in a PAM service of its own first, and the change to sudo undoes
#    itself after 3 minutes (or at the next boot, if the computer restarts
#    first) unless sudo then works with your password and a touch. Register a second key as a backup: lose them all and sudo is closed
#    to you (docs/extras/security-key.md says how to get back in).
# Usage: bash security-key.sh [--remove | --manual | --sudo | --sudo-off]
#   --sudo       sudo asks for your password AND a touch of the key (registers it)
#   --sudo-off   sudo asks for your password alone again (then --sudo registers
#                another key, a backup, say)
# summary: Hardware security keys (FIDO2): SSH, web, sudo
# security: most
# category: devices
# files: data/security-key-firefox.rules
# status: apt:fido2-tools apt:pamu2fcfg
. "$(dirname -- "${BASH_SOURCE[0]}")/../lib.sh"
if [ "${1:-}" = --manual ]; then print_manual; exit; fi
need_user

# (a test puts these in a folder of its own)
ROOT=${WREN_SK_ROOT:-}
RULE=$ROOT/etc/udev/rules.d/72-wren-security-key-firefox.rules
KEYS=$ROOT/etc/wren/u2f_keys
TEST_PAM=$ROOT/etc/pam.d/wren-u2f-test
SUDO_PAM=$ROOT/etc/pam.d/sudo
PENDING=$ROOT/var/lib/wren/u2f-pending
UNDO=$ROOT/etc/systemd/system/wren-u2f-undo.service
MARK='# Wren Linux extras/security-key.sh: sudo takes your password and a touch of your key'
U2F="auth required pam_u2f.so authfile=$KEYS origin=pam://wren appid=pam://wren cue nouserok"
ME=$(id -un)

# sudo_on: the key for sudo, after registering it and trying it
sudo_on() {
    command -v pamu2fcfg >/dev/null || die "pamu2fcfg is missing: run this script without --sudo first."
    if sudo grep -qxF "$MARK" "$SUDO_PAM" 2>/dev/null; then
        note "sudo takes your key already. To register another (a backup) key, run --sudo-off, then --sudo."
        return 0
    fi
    say "sudo: your password and a touch of your key"
    note "Every sudo then needs both, so a program that learned your password still can't"
    note "use it. Lose every registered key and sudo is closed to you: register a backup key"
    note "now if you have one (docs/extras/security-key.md says how to get back in without)."
    ask "Set it up now (plug the key in first)?" n || { note "Later: wrenctl install security-key --sudo"; return 0; }
    local line more new
    note "Touch the key when it blinks."
    # what pamu2fcfg says, one line: "NAME:KEY,..." and, for each more key, ":KEY,..."
    local entry='[A-Za-z0-9+/=,*_-]+'
    line=$(pamu2fcfg -u "$ME" -o pam://wren -i pam://wren) || die "The key didn't answer; nothing was changed."
    [[ $line =~ ^"$ME":$entry$ ]] || die "pamu2fcfg said something unexpected; nothing was changed."
    while ask "Register another key, as a backup (swap the keys now)?" y; do
        note "Touch the other key when it blinks."
        if more=$(pamu2fcfg -n -o pam://wren -i pam://wren) && [[ $more =~ ^:$entry$ ]]; then
            line+=$more
        else
            warn "That key didn't answer (or said something unexpected); it wasn't added."
        fi
    done
    # The list, root's: your line replaced, other users' kept
    new=$(mktemp)
    { sudo cat "$KEYS" 2>/dev/null | grep -v "^$ME:" || true; printf '%s\n' "$line"; } >"$new"
    sudo install -d -o root -g root -m 0755 "${KEYS%/*}"
    sudo install -o root -g root -m 0644 "$new" "$KEYS"
    rm -f "$new"

    say "Trying it, in a PAM service of its own (sudo isn't changed yet)"
    printf '# Written by Wren Linux extras/security-key.sh, to try the key before sudo uses it\n%s\naccount required pam_permit.so\n' \
        "${U2F% nouserok}" | sudo tee "$TEST_PAM" >/dev/null
    note "Touch the key when it blinks."
    if ! pamtester wren-u2f-test "$ME" authenticate; then
        sudo rm -f "$TEST_PAM"
        die "The key wasn't accepted, so sudo was left as it was."
    fi
    sudo rm -f "$TEST_PAM"

    say "sudo takes it now; it undoes itself in 3 minutes unless sudo works with it"
    sudo cp -p "$SUDO_PAM" "$SUDO_PAM.wren-old"
    sudo install -d -o root -g root -m 0755 "${PENDING%/*}"
    sudo touch "$PENDING"
    local undo_cmd="if [ -e $PENDING ]; then cp -p $SUDO_PAM.wren-old $SUDO_PAM; rm -f $PENDING; logger -t wren 'security key: sudo put back, not confirmed'; fi"
    # in 3 minutes, and at the next boot if the computer restarts before that
    sudo install -d -o root -g root -m 0755 "${UNDO%/*}"
    printf '%s\n' '# Written by Wren Linux extras/security-key.sh: a sudo change that was' \
        "# never confirmed is undone at the next boot." '[Unit]' \
        'Description=Wren Linux: undo a sudo change that was not confirmed' "ConditionPathExists=$PENDING" '' \
        '[Service]' 'Type=oneshot' "ExecStart=/bin/sh -c \"$undo_cmd\"" '' '[Install]' 'WantedBy=multi-user.target' |
        sudo tee "$UNDO" >/dev/null
    sudo systemctl daemon-reload
    sudo systemctl enable wren-u2f-undo.service
    sudo systemd-run --quiet --unit=wren-u2f-rollback --on-active=3min /bin/sh -c "$undo_cmd"
    new=$(mktemp)
    # right after the password (common-auth): both are needed
    awk -v mark="$MARK" -v line="$U2F" '{ print } /^@include common-auth/ && !done { print mark; print line; done = 1 }' "$SUDO_PAM" >"$new"
    grep -qxF "$U2F" "$new" || { rm -f "$new"; sudo rm -f "$PENDING"; die "$SUDO_PAM has no @include common-auth line; it was left as it was."; }
    sudo install -o root -g root -m 0644 "$new" "$SUDO_PAM"
    rm -f "$new"
    note "Now sudo asks your password, then wants a touch of the key:"
    sudo -k
    if sudo rm -f "$PENDING"; then
        sudo systemctl stop wren-u2f-rollback.timer 2>/dev/null || true
        sudo systemctl disable wren-u2f-undo.service 2>/dev/null || true
        sudo rm -f "$SUDO_PAM.wren-old" "$UNDO"
        note "Confirmed: sudo takes your password and your key."
    else
        warn "That didn't work: in 3 minutes sudo takes your password alone again, by itself."
        exit 1
    fi
}

sudo_off() {
    if ! sudo grep -qxF "$MARK" "$SUDO_PAM" 2>/dev/null; then note "sudo doesn't take the key; nothing to undo."; return 0; fi
    local new
    new=$(mktemp)
    grep -vxF -e "$MARK" -e "$U2F" "$SUDO_PAM" >"$new"
    sudo install -o root -g root -m 0644 "$new" "$SUDO_PAM"
    rm -f "$new"
    # your keys out of the list (other users' stay)
    new=$(mktemp)
    sudo cat "$KEYS" 2>/dev/null | grep -v "^$ME:" >"$new" || true
    if [ -s "$new" ]; then sudo install -o root -g root -m 0644 "$new" "$KEYS"; else sudo rm -f "$KEYS"; fi
    rm -f "$new"
    note "sudo takes your password alone again."
}

case ${1:-} in
    --sudo) sudo_on; exit ;;
    --sudo-off) sudo_off; exit ;;
    --remove)
        sudo_off
        sudo rm -f "$RULE" "$TEST_PAM" "$UNDO"
        sudo udevadm control --reload-rules 2>/dev/null || true
        apt_purge fido2-tools pamu2fcfg libpam-u2f pamtester
        note "The keys themselves are untouched; SSH keys made with them stay in ~/.ssh."
        exit ;;
    "") ;;
    *) die "Usage: bash security-key.sh [--remove | --manual | --sudo | --sudo-off]" ;;
esac

say "Security keys (FIDO2)"
# libfido2: what ssh and the tools talk to the key with; pamtester: tries
# the sudo setting before sudo uses it
apt_install libfido2-1 fido2-tools libpam-u2f pamu2fcfg pamtester acl
note "Linux lets you, at the keyboard, use the key by itself; other users get none."

say "Website logins in the sandboxed Firefox"
note "Two-factor login with the key (WebAuthn) works in Firefox run as you. The sandboxed Firefox"
note "runs as other users, which can't reach the key unless you let them: then any of its"
note "profiles can ask for a login with it, and each one still needs a touch."
if ask "Let the sandboxed Firefox use the key for website logins?" y; then
    sudo install -o root -g root -m 0644 "$WREN_DIR/data/security-key-firefox.rules" "$RULE"
    sudo udevadm control --reload-rules 2>/dev/null || true
    sudo udevadm trigger --subsystem-match=hidraw --action=change 2>/dev/null || true
    note "Done: plug the key in (again) and it works there too."
else
    sudo rm -f "$RULE"
    sudo udevadm control --reload-rules 2>/dev/null || true
fi

say "SSH with the key"
note "ssh-keygen -t ed25519-sk -f ~/.ssh/NAME   (the file is only a handle: every login needs the key)"
note "For the Wren Linux VPS (from a git copy): bash $WREN_DIR/tools/vps-client.sh --ssh-key --security-key"

sudo_on