Repository: QubesOS/qubes-antievilmaid Branch: main Commit: c085d776c499 Files: 24 Total size: 81.9 KB Directory structure: gitextract_b_nyytam/ ├── .gitignore ├── .gitlab-ci.yml ├── .qubesbuilder ├── 90anti-evil-maid/ │ ├── anti-evil-maid-check-mount-devs │ ├── anti-evil-maid-unseal │ ├── hosts │ └── module-setup.sh ├── Makefile.builder ├── README ├── anti-evil-maid.spec.in ├── etc/ │ ├── anti-evil-maid.conf │ ├── dracut.conf.d/ │ │ └── anti-evil-maid.conf │ └── grub.d/ │ └── 19_linux_xen_tboot ├── sbin/ │ ├── anti-evil-maid-install │ ├── anti-evil-maid-lib │ ├── anti-evil-maid-lib-tpm1 │ ├── anti-evil-maid-lib-tpm2 │ ├── anti-evil-maid-seal │ └── anti-evil-maid-tpm-setup ├── systemd/ │ └── system/ │ ├── anti-evil-maid-check-mount-devs.service │ ├── anti-evil-maid-seal.service │ ├── anti-evil-maid-unseal.service │ └── tcsd.service.d/ │ └── anti-evil-maid-seal.conf └── version ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ pkgs/ ================================================ FILE: .gitlab-ci.yml ================================================ checks:shellcheck: stage: checks tags: - docker script: - cd sbin/ - shellcheck -s bash * ../etc/anti-evil-maid.conf - cd ../90anti-evil-maid/ - shellcheck -s bash anti-evil-maid-unseal ../sbin/anti-evil-maid-lib* include: - file: /r4.2/gitlab-base.yml project: QubesOS/qubes-continuous-integration - file: /r4.2/gitlab-host.yml project: QubesOS/qubes-continuous-integration - file: /r4.3/gitlab-base.yml project: QubesOS/qubes-continuous-integration - file: /r4.3/gitlab-host.yml project: QubesOS/qubes-continuous-integration ================================================ FILE: .qubesbuilder ================================================ host: rpm: build: - anti-evil-maid.spec ================================================ FILE: 90anti-evil-maid/anti-evil-maid-check-mount-devs ================================================ #!/bin/bash # shellcheck disable=SC1091 . /lib/dracut-lib.sh # this cannot contain -u option because it causes an error inside # /lib/dracut-lib.sh set -eo pipefail shopt -s expand_aliases function check_device() { local sysfs_path recursion_limit dm_name dm_target slave sysfs_path="$1" recursion_limit="${2:-10}" if [ -r "$sysfs_path/dm/name" ]; then dm_name=$(cat "$sysfs_path"/dm/name) dm_target=$(dmsetup table "$dm_name" | cut -d ' ' -f 3) # This also ensures that the dm table have only single entry if [ "$dm_target" = "crypt" ]; then return 0 elif [ -n "$(ls -A "$sysfs_path"/slaves)" ] && [ "$recursion_limit" -gt 0 ]; then for slave in "$sysfs_path"/slaves/*; do if ! check_device "$slave" "$(( recursion_limit - 1 ))"; then return 1 fi done return 0 else return 1 fi else return 1 fi } root_name="$(getarg root)" if echo "$root_name" | grep -q = ; then root_matches=$(blkid -t "$root_name" | wc -l) if [ "$root_matches" -gt 1 ]; then die "AEM: multiple devices matching $root_name found, aborting!" fi root_dev=$(blkid -o device -t "$root_name") else root_dev=$root_name fi root_devid=$(lsblk -dnr -o MAJ:MIN "$root_dev") if ! check_device /sys/dev/block/"$root_devid"; then die "AEM: (bogus?) root device found not encrypted!" fi for lv in $(getarg rd.lvm.lv); do if [ -e /dev/"$lv" ]; then devid=$(lsblk -dnr -o MAJ:MIN /dev/"$lv") if ! check_device /sys/dev/block/"$devid"; then die "AEM: (bogus?) device /dev/$lv found not encrypted!" fi fi done exit 0 ================================================ FILE: 90anti-evil-maid/anti-evil-maid-unseal ================================================ #!/bin/bash set -euo pipefail shopt -s expand_aliases # Anti Evil Maid for dracut by Invisible Things Lab # Copyright (C) 2010 Joanna Rutkowska # # Mount our device, read the sealed secret blobs, initialize TPM # and finally try to unseal the secrets and display them to the user MNT=/anti-evil-maid UNSEALED_SECRET=/tmp/unsealed-secret LUKS_HEADER_DUMP=/tmp/luks-header-dump LUKS_PCR=13 PLYMOUTH_MESSAGES=() plymouth_message() { if [ "${#PLYMOUTH_MESSAGES[@]}" -eq 0 ]; then # add vertical "padding" to avoid printing messages over plymouth's # prompt help plymouth message --text="" plymouth message --text="" plymouth message --text="" fi plymouth message --text="$*" PLYMOUTH_MESSAGES+=("$*") } plymouth_messages_hide() { for m in "${PLYMOUTH_MESSAGES[@]}"; do plymouth hide-message --text="$m" done } # shellcheck source=../sbin/anti-evil-maid-lib . anti-evil-maid-lib # find AEM device UUID=$(getparams aem.uuid) DEV=/dev/disk/by-uuid/$UUID udevadm trigger udevadm settle waitfor -b "$DEV" n=$(lsblk -nr -o UUID | grep -Fixc "$UUID") || true if [ "$n" != 1 ]; then message "Error: found ${n:-?} devices with UUID $UUID" exit 1 fi LABEL=$(lsblk -dnr -o LABEL "$DEV") if [[ "$LABEL" != "$LABEL_PREFIX"* ]]; then message "AEM boot device $DEV has wrong label: $LABEL" exit 1 fi # mount AEM device log "Mounting $DEV (\"$LABEL\")..." mkdir -p "$MNT" mount -t ext4 -o ro "$DEV" "$MNT" # this way an error prior to unmounting will keep the device usable after initrd trap 'umount "$MNT"' EXIT # setup TPM & copy secrets to initrd tmpfs log "Initializing TPM..." modprobe tpm_tis validatetpm || exit 1 ip link set dev lo up mkdir -p "${TPMS_DIR%/*}" log "Copying sealed AEM secrets..." cp -Tr "$MNT/aem/${TPMS_DIR##*/}" "${TPMS_DIR}" tpmstartinitrdservices SEALED_SECRET_TXT=$TPM_DIR/$LABEL/secret.txt.sealed2 SEALED_SECRET_KEY=$TPM_DIR/$LABEL/secret.key.sealed2 SEALED_SECRET_OTP=$TPM_DIR/$LABEL/secret.otp.sealed2 SEALED_SECRET_FRE=$TPM_DIR/$LABEL/secret.fre.sealed2 # unmount AEM device log "Unmounting $DEV (\"$LABEL\")..." umount "$MNT" # remove umount trap set after mount trap - EXIT if [ "$(blockdev --getro "$DEV")" = 1 ]; then message "You should now unplug the AEM device if it is intentionally read-only." fi # Extend PCR with LUKS header(s) getluksuuids | sort -u | while read -r luksid; do waitfor -b "/dev/disk/by-uuid/$luksid" cryptsetup luksHeaderBackup "/dev/disk/by-uuid/$luksid" \ --header-backup-file "$LUKS_HEADER_DUMP" luks_header_hash=$(hashfile "$LUKS_HEADER_DUMP") log "Extending PCR $LUKS_PCR, value $luks_header_hash, device $luksid..." tpmpcrextend "$LUKS_PCR" "$luks_header_hash" done # cache suffix and SRK password, if applicable mkdir -p "$CACHE_DIR" echo "${LABEL##"$LABEL_PREFIX"}" >"$SUFFIX_CACHE" Z=$(tpmzsrk) if [ -n "$Z" ]; then true >"$SRK_PASSWORD_CACHE" else for _ in 1 2 3; do log "Prompting for SRK password..." if systemd-ask-password --timeout=0 \ "TPM SRK password to unseal the secret(s)" \ > "$SRK_PASSWORD_CACHE" && checksrkpass; then log "Correct SRK password" break fi log "Wrong SRK password" done fi # check freshness token log "Unsealing freshness token..." if tpmunsealdata "$Z" "$SEALED_SECRET_FRE" "$UNSEALED_SECRET" \ "$TPM_DIR/$LABEL"; then log "Freshness token unsealed." true >"$CACHE_DIR/unseal-success" else log "Freshness token unsealing failed!" log "This is expected during the first boot from a particular" log "AEM media or after updating any of the boot components or" log "changing their configuration." exit 1 fi if checkfreshness "$UNSEALED_SECRET"; then log "Freshness token valid, continuing." else log "Freshness token invalid!" exit 1 fi # unseal & show OTP if provisioned # unseal & decrypt key file unless the user switches to text secret mode if [ -e "$SEALED_SECRET_OTP" ]; then alias otp=true else alias otp=false fi if otp; then log "Unsealing TOTP shared secret seed..." if tpmunsealdata "$Z" "$SEALED_SECRET_OTP" "$UNSEALED_SECRET" \ "$TPM_DIR/$LABEL"; then log "TOTP secret unsealed." message "" message "Never type in your key file password unless the code below is correct!" message "" seed=$(cat "$UNSEALED_SECRET") last= { trap 'plymouth_messages_hide; exit' TERM while :; do now=$(date +%s) if [ -z "$last" ] || { [ "$((now % 30))" = 0 ] && [ "$last" != "$now" ]; }; then code=$(oathtool --totp -b "$seed") message "[ $(date) ] TOTP code: $code" last=$now fi sleep 0.1 done } & totp_loop_pid=$! if tpmunsealdata "$Z" "$SEALED_SECRET_KEY" "$UNSEALED_SECRET" \ "$TPM_DIR/$LABEL"; then for _ in 1 2 3; do pass=$(systemd-ask-password --timeout=0 \ 'LUKS key file password (or "t" to show text secret)') if [ "$pass" = "t" ]; then alias otp=false break fi if scrypt dec -P "$UNSEALED_SECRET" /tmp/aem-keyfile \ <<<"$pass"; then log "Correct LUKS key file password" # dracut "90crypt" module will parse the # rd.luks.key=/tmp/aem-keyfile # kernel cmdline arg and attempt to use it; # this file is deleted on root switch # along with everything in /tmp break else log "Wrong LUKS key file password" fi done fi kill "$totp_loop_pid" fi fi # unseal text secret if ! otp; then log "Unsealing text secret..." if tpmunsealdata "$Z" "$SEALED_SECRET_TXT" "$UNSEALED_SECRET" \ "$TPM_DIR/$LABEL"; then { message "" message "$(cat "$UNSEALED_SECRET" 2>/dev/null)" message "" } 2>&1 # don't put the secret into the journal message "Never type in your disk password unless the secret above is correct!" waitforenter fi plymouth_messages_hide clear fi # prevent sealing service from starting if user unplugged # the (supposedly read-only) AEM device if [ ! -b "$DEV" ]; then rm -rf "$CACHE_DIR" fi ================================================ FILE: 90anti-evil-maid/hosts ================================================ 127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4 ::1 localhost localhost.localdomain localhost6 localhost6.localdomain6 ================================================ FILE: 90anti-evil-maid/module-setup.sh ================================================ #!/bin/bash check() { which tpm_unsealdata tpm2_unseal >/dev/null 2>&1 || return 1 } #depends() { #} installkernel() { instmods tpm_tis tpm_crb } install() { inst_script "$moddir"/anti-evil-maid-unseal /sbin/anti-evil-maid-unseal inst_script "$moddir"/anti-evil-maid-check-mount-devs /sbin/anti-evil-maid-check-mount-devs inst $systemdsystemunitdir/cryptsetup-pre.target dracut_install \ /usr/sbin/anti-evil-maid-lib* \ base32 \ blockdev \ clear \ cryptsetup \ cut \ date \ file \ /usr/share/misc/magic \ grep \ head \ install \ killall \ lsblk \ oathtool \ printf \ scrypt \ sed \ seq \ sha1sum \ sort \ tail \ tcsd \ trousers_changer_identify \ tee \ tpm_id \ tpm2_id \ tpm_nvinfo \ tpm_nvread \ tpm_nvread_stdout \ tpm_pcr_extend \ tpm_sealdata \ tpm_unsealdata \ tpm_z_srk \ tpm2_z_srk \ tr \ uniq \ wc \ xargs \ xxd # TPM2-related: # tpm2-tools dracut_install \ tpm2_changeauth \ tpm2_create \ tpm2_createprimary \ tpm2_evictcontrol \ tpm2_flushcontext \ tpm2_load \ tpm2_nvdefine \ tpm2_nvread \ tpm2_nvreadpublic \ tpm2_nvundefine \ tpm2_nvwrite \ tpm2_nvwritelock \ tpm2_pcrextend \ tpm2_pcrread \ tpm2_policycommandcode \ tpm2_startauthsession \ tpm2_unseal # other utilities dracut_install \ mktemp \ openssl \ sha256sum # such tpm2-tss libraries must be listed explicitly because they are # discovered at runtime instead of being linked to during build dracut_install \ /usr/lib64/libtss2-tcti-device.so.0* dracut_install \ $systemdsystemunitdir/anti-evil-maid-unseal.service \ $systemdsystemunitdir/anti-evil-maid-check-mount-devs.service \ $systemdsystemunitdir/initrd.target.wants/anti-evil-maid-unseal.service \ $systemdsystemunitdir/initrd.target.requires/anti-evil-maid-check-mount-devs.service # all this crap below is needed for tcsd to start properly... dracut_install ip inst_simple "$moddir"/hosts /etc/hosts touch "$initdir/etc/"{passwd,shadow,group} chmod 0644 "$initdir/etc/"{passwd,group} chmod 0640 "$initdir/etc/shadow" for name in root tss; do for file in /etc/{passwd,group}; do if ! grep -q "^$name:" "$initdir/$file"; then grep "^$name:" "$file" >> "$initdir/$file" fi done if ! grep -q "^$name:" "$initdir/etc/shadow"; then echo "$name:*:::::::" >> "$initdir/etc/shadow" fi done } ================================================ FILE: Makefile.builder ================================================ ifeq ($(PACKAGE_SET),dom0) RPM_SPEC_FILES := anti-evil-maid.spec endif ================================================ FILE: README ================================================ Intro ====== Anti Evil Maid is an implementation of a TPM-based dynamic (Intel TXT) trusted boot for dracut/initramfs-based OSes (Fedora, Qubes, etc.) with a primary goal to prevent Evil Maid attacks. In short, AEM relies on TPM and a feature found in Intel's vPro CPUs (TXT) to detect tampering of various boot components. For more information and discussion about potential attacks see: http://blog.invisiblethings.org/2011/09/07/anti-evil-maid.html (Note that this article is somewhat outdated, e.g. AEM uses Intel TXT now.) Requirements and security notes ("before you start") ==================================================== * Only TPM version 1.2 is currently supported. It may be possible to configure your 2.0 TPM to emulate the 1.2 interface. * AEM is not compatible with (U)EFI boot. Legacy boot is required. * If you are using LUKS with LVM, you must encrypt the whole volume group instead of each volume, or else AEM will fail to boot. * You MUST set a TPM owner password * Unless you're installing AEM to internal disk, TPM SRK password SHOULD NOT be set (otherwise tboot will not be able to check whether critical parts of RAM were not altered during S3 sleep). Please be aware that by installing to internal disk and setting a TPM SRK password, your RAM WILL NOT be protected against tampering when your laptop is suspended, so make sure it is completely shut down any time an attacker might gain physical access. * When RAM tampering is detected on wake-up from S3 sleep, the default tboot policy only prints a warning into its log (`sudo txt-stat`) so even if you checked it immediately after each wake-up, the attacker might already have exfiltrated your login passphrase. Fix this by either using two-factor for desktop login (weak) or create stronger Launch Control Policy (LCP) and Verified Launch Policy (LCP) and write them into TPM NVRAM -- especially make sure that tboot will forcefully halt/reboot the platform if RAM tampering is detected. See tboot docs for more information (`/usr/share/doc/tboot`). Creating a small NVRAM area for tboot to write last error code might be a good idea for debugging crashes on S3 suspend/resume: `sudo tpmnv_defindex -i 0x20000002 -s 8 -pv 0 -rl 0x07 -wl 0x07 -p ` * Be aware that Intel TXT is vulnerable to System Management Mode (SMM) exploits like overwriting System Management Interrupt (SMI) handlers. Since SMM code is stored in platform firmware (BIOS) which usually is updatable (and thus can be overwritten by an attacker), it is quite an attractive target (and not just for the NSA). This can be fixed by integrating an SMI Transfer Monitor (STM) into the platform (but this, again, relies on the the same BIOS vendor who wrote a buggy SMM code to safely implement STM). Additionally, STM does not appear to be widely available yet (STM specification released mid-2015 by Intel). You can check whether your platform includes STM: `sudo txt-stat | grep -iA 1 stm` Seeing "stm: 0" and "stm_hash" being all zeros means you DO NOT have STM. Either way, BIOS is now part of your Trusted Computing Base (TCB) and you need to prevent attackers with physical access from modifying it. Good luck. Some hints: connect the write protect pin on BIOS flash chip to ground (prevents attacker from booting their own software which would bypass BIOS protections and overwrite it) and make sure physically accessing the chip will be tamper-evident by eg. covering the screws holding laptop body together in glitter and taking high-res photos, then examining before each use. * You might want to consider assessing the firmware security of your platform using an automated tool such as CHIPSEC: https://github.com/chipsec/chipsec * To recap -- you need to fully trust: * CPU (Intel, since we're depending on TXT) * sometimes over-optimizes for performance at the cost of security, see eg. Meltdown/Spectre, cache attacks against SGX enclaves, ... * TPM (various vendors) * few known attacks sniffing and injecting commands on the LPC bus; differential power analysis; buggy RSA key generation code * note that any potential TPM exploits (should) have no means of compromising your system directly -- a TPM under attacker's control can only be used to hide the fact that a compromise has occurred (ie. defeating the whole AEM feature) * BIOS (a few vendors) * it's full of holes! * that the attacker cannot get physically inside your laptop without you noticing (see the glitter hint above) Upgrading to AEM v4 =================== If you have an existing AEM installation, there are a few steps required after updating AEM packages to version >= 4.0 (available since Qubes R4). The easiest way to upgrade is to completely reset the TPM and start from scratch and re-create all existing AEM devices. Should you want to migrate without resetting the TPM (in case you're using it for something else besides Qubes AEM), you can manually replicate the steps taken in the TPM setup script (/usr/sbin/anti-evil-maid-tpm-setup). Note that you still need to re-create all provisioned AEM media afterwards. Otherwise, perform a TPM reset (via BIOS) and skip to the "Installation" section below. Installation ============= The instructions below assume Qubes OS. 1) Enable TPM in BIOS. Also enable TXT if there is an option for it. 2) Install and Verify TPM support under your OS/Dom0. a) Install anti-evil-maid packages (in Dom0 on Qubes). It will install all the required dependencies and tools. # qubes-dom0-update anti-evil-maid b) Verify kernel support for TPM: # cat /sys/class/tpm/tpm0/pcrs If you see something like this: PCR-00: 67 DC B4 8C AB 8D C7 9B 28 84 D9 15 69 DE 82 F2 F0 E1 2A D8 PCR-01: 11 75 9A 19 E5 BD E8 4E DA 1C 01 EC 53 87 FD 50 18 E1 94 1E PCR-02: 4B 43 98 82 65 04 E9 F4 14 78 26 F9 ED EA 92 91 6D FD AF D5 PCR-03: B2 A8 3B 0E BF 2F 83 74 29 9A 5B 2B DF C3 1E A9 55 AD 72 36 PCR-04: 93 33 4E 81 A6 9C 80 54 D6 87 C7 FD 76 7C 6F 4C 70 FC C6 73 (...) ... then your TPM is supported by your kernel. If your tpm has already been owned in the past, you can reset it by running tpm_clear -z, powering your computer off, and then resetting TPM in the BIOS (e.g.: TPM Authentication Reset). c) Initialize the TPM for use with AEM # anti-evil-maid-tpm-setup -z In case you want to install AEM to an internal disk, an SRK password must be set up in order for AEM to be secure. The SRK password can be set up by NOT passing the "-z" option to the above command. Should you not anticipate future need for internal AEM boot device and want to use external media only, use the "-z" option. If you later decide to provision AEM on the internal drive, create an SRK password first: # tpm_changeownerauth -s You will need to copy & paste the randomly-generated TPM owner password from the /var/lib/anti-evil-maid/tpm-owner-pw file. Existing AEM media will _not_ need to be re-sealed. 3) Setup Anti Evil Maid a) SINIT module You should download the SINIT module required for your system. Intel documented the required SINIT module depending on your CPU platform in: http://software.intel.com/en-us/articles/intel-trusted-execution-technology But DO NOT download the modules besides the SINIT/RACM from here. The download links provide old versions and broken binaries. You can then download the module and unzip it. All the modules could be downloaded from: https://cdrdv2.intel.com/v1/dl/getContent/630744?wapkw=intel%20txt%20sinit%20acm%20revocation%20 Find the module fitting to your platform and rename it to the names mentioned on the intel website link. Also, make sure you have the latest RACM update, if available (2nd & 3rd gen): https://software.intel.com/system/files/article/183305/intel-txt-sinit-acm-revocation-tools-guide-rev1-0_2.pdf It's possible to use 3rd gen SINIT/RACM on 2nd gen platforms. In fact, the only RACM available at the time of writing is for the 3rd gen, while the 2nd gen platforms were also affected by the buffer overflow bug in old SINIT version. Finally, you should retrieve the BIN file inside /boot in dom0. E.g., run from dom0: $ sudo -s # qvm-run --pass-io vm_name_containing_bin_file 'cat /home/user/path_to_sinit/name_of_sinit_file.BIN' > /boot/name_of_sinit_file.BIN NOTE: The SINIT files are digitally signed by Intel. While there is no easy way to verify their integrity after downloading (and after copying to Dom0), still, the operation of placing such a file into Dom0's /boot filesystem should be reasonably safe to do -- after all the file should not be processed by any software in Dom0, and only by the SENTER instruction of the processes, which, we hope, correctly verifies the signature before executing it... b) Create an Anti Evil Maid device: # anti-evil-maid-install -h Please note that each AEM device you provision should have a unique filesystem label suffix (use the '-s' option). You may safely re-use suffixes for destroyed devices. Installation directly onto a (truly) read-only media (such as a CD-R) is not supported. You can, however, copy the contents of an existing RW media onto RO media after the initial sealing takes place. Physical write-protect switches on USB sticks are fine (install AEM in RW mode, then flip the switch and proceed to use as RO media). Remember to always pull out the RO media when your text secret or TOTP code is displayed! Failing to do that will result in invalidation of freshness token in the TPM memory and the AEM media will fail to perform verified boot next time, falling back to non-AEM-protected mode. For example, to install on the internal boot partition (assuming that it is /dev/sda1): # anti-evil-maid-install /dev/sda1 Or better, create an external AEM boot device (in this case an SD card): # anti-evil-maid-install /dev/mmcblk0p1 Alternatively, a multi-factor authentication AEM boot device can be created, which provides additional protection against shoulder surfing and video surveillance -- with the above setups, if an attacker sees your disk encryption password as you're typing it then they can simply steal and decrypt your computer. Long story short, if you ever need to boot your AEM-protected computer in public or anywhere a camera may be hidden, this is the thing you want to use. However, this setup requires an external boot media (eg. USB stick or memory card) for maximum protection and owning a suitable two-factor authentication device supporting time-based one-time passwords (TOTP). There are apps available for Android/Apple smartphones (Google Authenticator, FreeOTP Authenticator) and Windows Phone (Microsoft Authenticator). You can also use a dedicated hardware token, either a reseedable one (letting the AEM installer generate a seed for you), or a manufacturer-seeded token (you will need to store the seed in /var/lib/anti-evil-maid/aem/secret.otp in base32 format, then run the AEM installer). The command to set this all up is simple: # anti-evil-maid-install -m /dev/sdb1 This will automatically generate a TOTP seed and display it as a QR code for you to enroll on your 2FA device (if it doesn't have a camera, there's also text version for manual entry). Once a successful TOTP seed enrollment is verified (you need to enter the 6-digit code displayed on your 2FA device), a LUKS key file will be randomly generated and encrypted by a password of your choice (make sure you're not using this password for anything else). This key file will then get added to your encrypted drive's LUKS key slot. For more details, see the associated qubes-devel mailing list thread: https://groups.google.com/d/topic/qubes-devel/8cAjSyg1msM/discussion In case you would like to install multi-factor AEM on internal disk, beware that keyboard observation attacks cannot be prevented! Plus, you still need to have TPM SRK password set. The only advantage over plain static text secret is, of course, that there's not static secret **shown on the screen** to observe (ie. cover the keyboard while typing AEM passwords). If you've chosen to install AEM on an external device (and not the internal drive), you should then remove the internal boot partition from dom0's /etc/fstab, never mount it again in dom0, and never boot from it again, because an attacker might modify it to exploit GRUB or dom0 filesystem drivers. Note: If you choose to use a USB device (e.g., a flash drive) as your AEM device and you previously created a USB qube, then you may have to unhide your USB controller from dom0: 1. Open the file `/etc/default/grub` in dom0. 2. Find the line that begins with `GRUB_CMDLINE_LINUX`. 3. If present, remove `rd.qubes.hide_all_usb` from that line. 4. Save and close the file. 5. Run the command `grub2-mkconfig -o /boot/grub2/grub.cfg` in dom0. 6. Reboot. c) Create a secret text (note: it cannot be larger than 255 bytes): Note: This step is unnecessary if using the multi-factor auth setup, but can serve as a fallback option in case you ever find yourself temporarily not having access to your 2FA device (eg. smartphone or hardware TOTP token). # cat >/var/lib/anti-evil-maid/aem/secret.txt <>/etc/default/grub # grub2-mkconfig -o /boot/grub2/grub.cfg Then go to step 4.a again. A discussion of this problem can be found at http://thread.gmane.org/gmane.comp.boot-loaders.tboot.devel/610/focus=611 and by searching for "min_ram" in the qubes mailing lists. c) Now, every time you boot your system (from your Anti Evil Maid stick) you should see your secret text or TOTP code displayed *before* you enter your LUKS disk encryption or key file passphrase. Xen/kernel/BIOS/firmware upgrades ================================== After Xen, kernel, BIOS, or firmware upgrades, you will need to reboot and enter your disk decryption passphrase even though you can't see your secret. Please note that you will see a `Freshness toekn unsealing failed!` error. It (along with your AEM secrets) will be resealed again automatically later in the boot process (see step 4.a). Some additional things that can cause AEM secrets and freshness token to fail to unseal (non-exhaustive list): * changing the LUKS header of the encrypted root partition * modifying the initrd (adding/removing files or just re-generating it) * changing kernel commandline parameters in GRUB What to do in case of compromise ================================ For a discussion of potential attacks against Anti Evil Maid, see the article referenced at the beginning. AEM media copied/stolen by an attacker -------------------------------------- If you have your system up and running or have an extra AEM media using which you can boot the system before the attacker can get to it: * `sudo -s` in dom0 * `. /usr/sbin/anti-evil-maid-lib` (note the dot at the beginning) * `revokefreshness ` (where `` is the missing label suffix) In case you do not remember the used media label suffix, take a look at `/var/lib/anti-evil-maid/`. If the particular media had no special suffix set (i.e. if the subdirectory is just named `aem`), use `revokefreshness ""`. Alternatively, `resetfreshness` will wipe all freshness tokens from the TPM (thus invalidating all enrolled AEM media and forcing you to boot an unverified system). As a last resort, you can attempt to reset the TPM from BIOS (with similar effect to `resetfreshness`). Otherwise, it's game over. Someone saw my LUKS passphrase ------------------------------ You should've installed AEM in multi-factor mode. If you're fairly confident the attacker does not possess a bitwise copy of your encrypted Qubes OS drive, change the LUKS passphrase: * determine the path to LUKS-encrypted disk (usually /dev/sda2) * add a new password with `sudo cryptsetup luksAddKey ` * remove old one with `sudo cryptsetup luksRemoveKey ` As these actions will change the LUKS header, which is fed into one of the TPM PCRs upon each AEM boot, all the existing AEM media will get invalidated (ie. fall back to unverified boot). Beware that solid-state devices will most likely NOT overwrite the LUKS header, but rather write the new one into another memory cell (due to wear leveling algorithms designed to prolong SSD life). If you're worried about this and have recent-enough backups (as you always should), perform an ATA secure erase of the whole SSD using a live CD and then reinstall Qubes OS. https://ata.wiki.kernel.org/index.php/ATA_Secure_Erase Someone saw my text secret -------------------------- Again, if you're going to boot your Qubes OS in public places, using multi-factor AEM is strongly recommended. Assuming the attacker haven't yet had access to your computer (in order to install a compromised bootloader which will show the correct secret but record your LUKS passphrase), simply changing the text secret and re-creating affected AEM media (if you used same secret for multiple ones) will do. The text secrets are stored in `/var/lib/anti-evil-maid/aem/secret.txt` Make sure to never trust the old text secret ever again! TODO: write up more scenarios and how to recover, best practices ================================================ FILE: anti-evil-maid.spec.in ================================================ Name: anti-evil-maid Version: @VERSION@ Release: 1%{?dist} Summary: Anti Evil Maid for initramfs-based systems. Requires: dracut grub2-tools parted tboot tpm-tools Requires: tpm-extra >= 4.0.0 Requires: trousers-changer >= 4.0.0 Requires: systemd >= 227 Requires: coreutils >= 8.25-2 Requires: scrypt qrencode oathtool Requires: tpm2-tools openssl Requires(post): dracut grub2-tools tboot systemd Obsoletes: anti-evil-maid-dracut Vendor: Invisible Things Lab License: GPL URL: http://www.qubes-os.org Source0: %{name}-%{version}.tar.gz BuildArch: noarch %description Anti Evil Maid for initramfs-based systems. %prep %setup -q %install mkdir -p $RPM_BUILD_ROOT/usr cp -r sbin $RPM_BUILD_ROOT/usr mkdir -p $RPM_BUILD_ROOT/usr/share/doc/anti-evil-maid cp README $RPM_BUILD_ROOT/usr/share/doc/anti-evil-maid cp -r etc $RPM_BUILD_ROOT mkdir -p $RPM_BUILD_ROOT/mnt/anti-evil-maid mkdir -p $RPM_BUILD_ROOT/var/lib/anti-evil-maid mkdir -p $RPM_BUILD_ROOT/usr/lib/dracut/modules.d cp -r 90anti-evil-maid $RPM_BUILD_ROOT/usr/lib/dracut/modules.d/ mkdir -p $RPM_BUILD_ROOT/usr/lib cp -r systemd $RPM_BUILD_ROOT/usr/lib %files /usr/sbin/anti-evil-maid-install /usr/sbin/anti-evil-maid-lib /usr/sbin/anti-evil-maid-lib-tpm1 /usr/sbin/anti-evil-maid-lib-tpm2 /usr/sbin/anti-evil-maid-seal /usr/sbin/anti-evil-maid-tpm-setup /usr/share/doc/anti-evil-maid/README /usr/lib/systemd/system/anti-evil-maid-seal.service /usr/lib/systemd/system/tcsd.service.d/anti-evil-maid-seal.conf /usr/lib/systemd/system/basic.target.wants/anti-evil-maid-seal.service /etc/anti-evil-maid.conf /etc/grub.d/19_linux_xen_tboot %dir /mnt/anti-evil-maid %dir /var/lib/anti-evil-maid /etc/dracut.conf.d/anti-evil-maid.conf /usr/lib/dracut/modules.d/90anti-evil-maid /usr/lib/systemd/system/anti-evil-maid-unseal.service /usr/lib/systemd/system/anti-evil-maid-check-mount-devs.service /usr/lib/systemd/system/initrd.target.wants/anti-evil-maid-unseal.service /usr/lib/systemd/system/initrd.target.requires/anti-evil-maid-check-mount-devs.service %define tboot_grub /etc/grub.d/20_linux_tboot /etc/grub.d/20_linux_xen_tboot %define refresh \ dracut --regenerate-all --force \ grub2-mkconfig -o /boot/grub2/grub.cfg \ systemctl daemon-reload %post chmod -x %tboot_grub %refresh %postun if [ "$1" = 0 ]; then %refresh chmod -f +x %tboot_grub || true fi %triggerin -- tboot chmod -x %tboot_grub %changelog @CHANGELOG@ ================================================ FILE: etc/anti-evil-maid.conf ================================================ # List of PCRs -- but note that Qubes DOESN'T USE TrustedGRUB: # # 0-3: (SRTM) BIOS, option ROMs, platform config # 4: (SRTM) MBR # 5-7: (SRTM) OEM specific, probably safe to skip # 8,9: (SRTM) TrustedGRUB1 stage2 # 12: (SRTM) Xen/kernel params passed by TrustedGRUB1 # 13: LUKS header(s) # 14: (SRTM) Xen/kernel/initrd loaded by TrustedGRUB1 # 17-19: (DRTM) TBoot # # SRTM = Static Root of Trust Measurement # DRTM = Dynamic Root of Trust Measurement (Intel TXT) # shellcheck disable=SC2034 SEAL="--pcr 13 --pcr 17 --pcr 18 --pcr 19" ================================================ FILE: etc/dracut.conf.d/anti-evil-maid.conf ================================================ add_dracutmodules+=" anti-evil-maid " ================================================ FILE: etc/grub.d/19_linux_xen_tboot ================================================ #! /bin/sh set -e # grub-mkconfig helper script. # Copyright (C) 2006,2007,2008,2009,2010 Free Software Foundation, Inc. # # GRUB is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # GRUB is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with GRUB. If not, see . prefix="/usr" exec_prefix="/usr" datarootdir="${prefix}/share" . "/usr/share/grub/grub-mkconfig_lib" export TEXTDOMAIN=grub export TEXTDOMAINDIR="${datarootdir}/locale" CLASS="--class gnu-linux --class gnu --class os --class xen" if [ "x${GRUB_DISTRIBUTOR}" = "x" ] ; then OS="$(sed 's, release .*$,,g' /etc/system-release)" else OS="${GRUB_DISTRIBUTOR}" CLASS="--class $(echo ${GRUB_DISTRIBUTOR} | tr 'A-Z' 'a-z' | cut -d' ' -f1) ${CLASS}" fi # loop-AES arranges things so that /dev/loop/X can be our root device, but # the initrds that Linux uses don't like that. case ${GRUB_DEVICE} in /dev/loop/*|/dev/loop[0-9]) GRUB_DEVICE=`losetup ${GRUB_DEVICE} | sed -e "s/^[^(]*(\([^)]\+\)).*/\1/"` ;; esac if [ "x${GRUB_DEVICE_UUID}" = "x" ] || [ "x${GRUB_DISABLE_LINUX_UUID}" = "xtrue" ] \ || ! test -e "/dev/disk/by-uuid/${GRUB_DEVICE_UUID}" \ || uses_abstraction "${GRUB_DEVICE}" lvm; then LINUX_ROOT_DEVICE=${GRUB_DEVICE} else LINUX_ROOT_DEVICE=UUID=${GRUB_DEVICE_UUID} fi # Allow overriding GRUB_CMDLINE_LINUX and GRUB_CMDLINE_LINUX_DEFAULT. if [ "${GRUB_CMDLINE_LINUX_XEN_REPLACE}" ]; then GRUB_CMDLINE_LINUX="${GRUB_CMDLINE_LINUX_XEN_REPLACE}" fi if [ "${GRUB_CMDLINE_LINUX_XEN_REPLACE_DEFAULT}" ]; then GRUB_CMDLINE_LINUX_DEFAULT="${GRUB_CMDLINE_LINUX_XEN_REPLACE_DEFAULT}" fi GRUBFS="`${grub_probe} --device ${GRUB_DEVICE} --target=fs 2>/dev/null || true`" if [ x"$GRUBFS" = x ]; then GRUBFS="$(stat -f --printf=%T /)" fi case x"$GRUBFS" in xbtrfs) rootsubvol="`make_system_path_relative_to_its_root /`" rootsubvol="${rootsubvol#/}" if [ "x${rootsubvol}" != x ]; then GRUB_CMDLINE_LINUX="rootflags=subvol=${rootsubvol} ${GRUB_CMDLINE_LINUX}" fi;; xzfs) rpool=`${grub_probe} --device ${GRUB_DEVICE} --target=fs_label 2>/dev/null || true` bootfs="`make_system_path_relative_to_its_root / | sed -e "s,@$,,"`" LINUX_ROOT_DEVICE="ZFS=${rpool}${bootfs}" ;; esac title_correction_code= linux_entry () { os="$1" version="$2" xen_version="$3" type="$4" args="$5" xen_args="$6" if [ -z "$boot_device_id" ]; then boot_device_id="$(grub_get_device_id "${GRUB_DEVICE}")" fi if [ x$type != xsimple ] ; then if [ x$type = xrecovery ] ; then title="$(gettext_printf "AEM %s boot, with Xen %s and Linux %s (recovery mode)" "${os}" "${xen_version}" "${version}")" else title="$(gettext_printf "AEM %s boot, with Xen %s and Linux %s" "${os}" "${xen_version}" "${version}")" fi replacement_title="$(echo "Advanced options with AEM boot for ${OS}" | sed 's,>,>>,g')>$(echo "$title" | sed 's,>,>>,g')" if [ x"Xen ${xen_version}>$title" = x"$GRUB_ACTUAL_DEFAULT" ]; then quoted="$(echo "$GRUB_ACTUAL_DEFAULT" | grub_quote)" title_correction_code="${title_correction_code}if [ \"x\$default\" = '$quoted' ]; then default='$(echo "$replacement_title" | grub_quote)'; fi;" grub_warn "$(gettext_printf "Please don't use old title \`%s' for GRUB_DEFAULT, use \`%s' (for versions before 2.00) or \`%s' (for 2.00 or later)" "$GRUB_ACTUAL_DEFAULT" "$replacement_title" "gnulinux-advanced-$boot_device_id>gnulinux-$version-$type-$boot_device_id")" fi echo "menuentry '$(echo "$title" | grub_quote)' ${CLASS} \$menuentry_id_option 'xen-gnulinux-$version-$type-$boot_device_id' {" | sed "s/^/$submenu_indentation/" else title="$(gettext_printf "AEM %s, with Xen hypervisor" "${os}")" echo "menuentry '$(echo "$title" | grub_quote)' ${CLASS} \$menuentry_id_option 'xen-gnulinux-simple-$boot_device_id' {" | sed "s/^/$submenu_indentation/" fi if [ x$type != xrecovery ] ; then save_default_entry | grub_add_tab | sed "s/^/$submenu_indentation/" fi if [ -z "${prepare_boot_cache}" ]; then prepare_boot_cache="$(prepare_grub_to_access_device ${GRUB_DEVICE_BOOT} | grub_add_tab)" fi printf '%s\n' "${prepare_boot_cache}" | sed "s/^/$submenu_indentation/" tmessage="$(gettext_printf "Loading tboot ...")" xmessage="$(gettext_printf "Loading Xen %s ..." ${xen_version})" lmessage="$(gettext_printf "Loading Linux %s ..." ${version})" sed "s/^/$submenu_indentation/" << EOF echo '$(echo "$tmessage" | grub_quote)' multiboot /tboot.gz placeholder logging=memory,serial ${GRUB_CMDLINE_TBOOT} echo '$(echo "$xmessage" | grub_quote)' if [ "\$grub_platform" = "pc" -o "\$grub_platform" = "" ]; then xen_rm_opts= else xen_rm_opts="no-real-mode edd=off" fi module ${rel_xen_dirname}/${xen_basename} placeholder ${xen_args} \${xen_rm_opts} echo '$(echo "$lmessage" | grub_quote)' module ${rel_dirname}/${basename} placeholder root=${linux_root_device_thisversion} ro ${args} aem.uuid=${GRUB_DEVICE_BOOT_UUID} rd.luks.key=/tmp/aem-keyfile rd.luks.crypttab=no EOF if test -n "${initrd}" ; then # TRANSLATORS: ramdisk isn't identifier. Should be translated. message="$(gettext_printf "Loading initial ramdisk ...")" sed "s/^/$submenu_indentation/" << EOF echo '$(echo "$message" | grub_quote)' module ${rel_dirname}/${initrd} EOF fi if test -n "${sinit_module_list}" ; then for i in ${sinit_module_list} ; do sinit_module=`basename $i` message="$(gettext_printf "Loading SINIT module %s ..." ${sinit_module})" sed "s/^/$submenu_indentation/" << EOF echo '$message' module /${sinit_module} EOF done fi sed "s/^/$submenu_indentation/" << EOF } EOF } linux_list=`for i in /boot/vmlinu[xz]-* /vmlinu[xz]-* /boot/kernel-*; do if grub_file_is_not_garbage "$i"; then basename=$(basename $i) version=$(echo $basename | sed -e "s,^[^0-9]*-,,g") dirname=$(dirname $i) config= for j in "${dirname}/config-${version}" "${dirname}/config-${alt_version}" "/etc/kernels/kernel-config-${version}" ; do if test -e "${j}" ; then config="${j}" break fi done if (grep -qx "CONFIG_XEN_DOM0=y" "${config}" 2> /dev/null || grep -qx "CONFIG_XEN_PRIVILEGED_GUEST=y" "${config}" 2> /dev/null); then echo -n "$i " ; fi fi done` if [ "x${linux_list}" = "x" ] ; then exit 0 fi file_is_not_sym () { case "$1" in */xen-syms-*) return 1;; *) return 0;; esac } xen_list=`for i in /boot/xen*; do if grub_file_is_not_garbage "$i" && file_is_not_sym "$i" ; then echo -n "$i " ; fi done` sinit_module_list=`for i in /boot/*SINIT*.BIN; do if grub_file_is_not_garbage "$i"; then echo "$i" fi done` prepare_boot_cache= boot_device_id= title_correction_code= machine=`uname -m` case "$machine" in i?86) GENKERNEL_ARCH="x86" ;; mips|mips64) GENKERNEL_ARCH="mips" ;; mipsel|mips64el) GENKERNEL_ARCH="mipsel" ;; arm*) GENKERNEL_ARCH="arm" ;; *) GENKERNEL_ARCH="$machine" ;; esac echo "if [ -d /aem/ ]; then" # Extra indentation to add to menu entries in a submenu. We're not in a submenu # yet, so it's empty. In a submenu it will be equal to '\t' (one tab). submenu_indentation="" is_first_entry=true while [ "x${xen_list}" != "x" ] ; do list="${linux_list}" current_xen=`version_find_latest $xen_list` xen_basename=`basename ${current_xen}` xen_dirname=`dirname ${current_xen}` rel_xen_dirname=`make_system_path_relative_to_its_root $xen_dirname` xen_version=`echo $xen_basename | sed -e "s,.gz$,,g;s,^xen-,,g"` if [ -z "$boot_device_id" ]; then boot_device_id="$(grub_get_device_id "${GRUB_DEVICE}")" fi if [ "x$is_first_entry" != xtrue ]; then echo " submenu '$(gettext_printf "Xen hypervisor, version %s with AEM boot" "${xen_version}" | grub_quote)' \$menuentry_id_option 'xen-hypervisor-$xen_version-$boot_device_id' {" fi while [ "x$list" != "x" ] ; do linux=`version_find_latest $list` gettext_printf "Found linux image: %s\n" "$linux" >&2 basename=`basename $linux` dirname=`dirname $linux` rel_dirname=`make_system_path_relative_to_its_root $dirname` version=`echo $basename | sed -e "s,^[^0-9]*-,,g"` alt_version=`echo $version | sed -e "s,\.old$,,g"` linux_root_device_thisversion="${LINUX_ROOT_DEVICE}" initrd= for i in "initrd.img-${version}" "initrd-${version}.img" "initrd-${version}.gz" \ "initrd-${version}" "initramfs-${version}.img" \ "initrd.img-${alt_version}" "initrd-${alt_version}.img" \ "initrd-${alt_version}" "initramfs-${alt_version}.img" \ "initramfs-genkernel-${version}" \ "initramfs-genkernel-${alt_version}" \ "initramfs-genkernel-${GENKERNEL_ARCH}-${version}" \ "initramfs-genkernel-${GENKERNEL_ARCH}-${alt_version}" ; do if test -e "${dirname}/${i}" ; then initrd="$i" break fi done if test -n "${initrd}" ; then gettext_printf "Found initrd image: %s\n" "${dirname}/${initrd}" >&2 else # "UUID=" magic is parsed by initrds. Since there's no initrd, it can't work here. linux_root_device_thisversion=${GRUB_DEVICE} fi if [ "x$is_first_entry" = xtrue ]; then linux_entry "${OS}" "${version}" "${xen_version}" simple \ "${GRUB_CMDLINE_LINUX} ${GRUB_CMDLINE_LINUX_DEFAULT}" "${GRUB_CMDLINE_XEN} ${GRUB_CMDLINE_XEN_DEFAULT}" submenu_indentation="$grub_tab$grub_tab" if [ -z "$boot_device_id" ]; then boot_device_id="$(grub_get_device_id "${GRUB_DEVICE}")" fi # TRANSLATORS: %s is replaced with an OS name echo "submenu '$(gettext_printf "Advanced options with AEM boot for %s (with Xen hypervisor)" "${OS}" | grub_quote)' \$menuentry_id_option 'gnulinux-advanced-$boot_device_id' {" echo " submenu '$(gettext_printf "Xen hypervisor, version %s" "${xen_version}" | grub_quote)' \$menuentry_id_option 'xen-hypervisor-$xen_version-$boot_device_id' {" fi is_first_entry=false linux_entry "${OS}" "${version}" "${xen_version}" advanced \ "${GRUB_CMDLINE_LINUX} ${GRUB_CMDLINE_LINUX_DEFAULT}" "${GRUB_CMDLINE_XEN} ${GRUB_CMDLINE_XEN_DEFAULT}" if [ "x${GRUB_DISABLE_RECOVERY}" != "xtrue" ]; then linux_entry "${OS}" "${version}" "${xen_version}" recovery \ "single ${GRUB_CMDLINE_LINUX}" "${GRUB_CMDLINE_XEN}" fi list=`echo $list | tr ' ' '\n' | grep -vx $linux | tr '\n' ' '` done if [ x"$is_first_entry" != xtrue ]; then echo ' }' fi xen_list=`echo $xen_list | tr ' ' '\n' | grep -vx $current_xen | tr '\n' ' '` done # If at least one kernel was found, then we need to # add a closing '}' for the submenu command. if [ x"$is_first_entry" != xtrue ]; then echo '}' fi echo "$title_correction_code" echo fi ================================================ FILE: sbin/anti-evil-maid-install ================================================ #!/bin/bash set -euo pipefail shopt -s expand_aliases . anti-evil-maid-lib LABEL_SUFFIX_CHARS=0-9a-zA-Z=.- BOOT_DIR=/boot GRUB_DIR=$BOOT_DIR/grub2 GRUB_CFG=$GRUB_DIR/grub.cfg validatetpm || exit 1 usage() { cat <] [-F] [-m] Installs Anti Evil Maid to your system's boot partition, or to a different storage device (e.g. an SD card or a USB stick). Arguments: -s: gets labeled "$LABEL_PREFIX" can be composed of 0-13 characters from the alphabet $LABEL_SUFFIX_CHARS It defaults to 's current suffix, if any, or the empty string otherwise. Each of your AEM installations must have a unique suffix. This suffix has no particular meaning, except that you can let it end in .rm=1 or .rm=0 to hint that is removable or fixed, respectively, no matter what the Linux kernel detects. -F: passed on to mkfs.ext4 (don't ask for confirmation, etc.) -m: set up a multi-factor auth AEM media Using time-based one time password and a LUKS key file, provides resistance to shoulder surfing and video surveillance based passphrase snooping. Examples: Install on the system's boot partition (assuming that it is /dev/sda1), and label its current filesystem "$LABEL_PREFIX": anti-evil-maid-install /dev/sda1 Install on an SD card's first partition, replacing its data with a new ext4 filesystem labeled "$LABEL_PREFIX.sd", and make it bootable: anti-evil-maid-install -s .sd /dev/mmcblk0p1 Install MFA-enabled AEM on USB stick's first partition, overwriting it with a new ext4 filesystem and marking it bootable: anti-evil-maid-install -m /dev/sdb1 END exit 1 } # check invocation alias mfa=false LABEL_SUFFIX= F=() while getopts s:Fhm opt; do case "$opt" in s) LABEL_SUFFIX=$OPTARG ;; F) F=( -F ) ;; m) alias mfa=true ;; *) usage ;; esac done # shellcheck disable=SC2102 case "$LABEL_SUFFIX" in *[!$LABEL_SUFFIX_CHARS]*|??????????????*) usage; esac LABEL=$LABEL_PREFIX$LABEL_SUFFIX shift $((OPTIND - 1)) case $# in 1) PART_DEV=$1 ;; *) usage ;; esac if [ "$(id -ur)" != 0 ]; then log "This command must be run as root!" exit 1 fi if [ -z "$(getluksuuids)" ]; then log "Anti Evil Maid requires encrypted disk!" exit 1 fi tpmstartservices # examine device BOOT_MAJMIN=$(mountpoint -d "$BOOT_DIR") || BOOT_MAJMIN= PART_DEV_MAJMIN=$(lsblk -dnr -o MAJ:MIN "$PART_DEV") if external "$PART_DEV" && [ "$BOOT_MAJMIN" != "$PART_DEV_MAJMIN" ]; then alias replace=true else alias replace=false fi WHOLE_DEV=$(lsblk -dnp -o PKNAME "$PART_DEV") if [ ! -b "$WHOLE_DEV" ] || [ "$WHOLE_DEV" == "$PART_DEV" ]; then log "Couldn't find parent device: $WHOLE_DEV" exit 1 fi PART_DEV_REAL=$(readlink -f "$PART_DEV") PART_NUM=${PART_DEV_REAL##*[!0-9]} if ! [ "$PART_NUM" -gt 0 ]; then log "Couldn't extract partition number: $PART_NUM" exit 1 fi # MFA-specific checks if mfa && ! external "$PART_DEV"; then log "WARNING: Installing MFA AEM on the same disk" log "as Qubes OS will NOT provide any resistance" log "against keyboard observation during boot!" log "Additionally, compromise recovery using" log "freshness token revocation will be a lot" log "less feasible." waitforenter elif mfa && ! removable "$PART_DEV" "$LABEL" ; then log "WARNING: Installing MFA AEM on an internal" log "disk will NOT provide any resistance" log "against keyboard observation during boot!" log "Additionally, compromise recovery using" log "freshness token revocation will be a lot" log "less feasible." log "You can safely ignore this warning if the" log "device in question is, in fact, removable." waitforenter fi # This check (instead of a more obvious 'mountpoint $BOOT_DIR') should work # even in unusual setups without any internal boot partition at all: if [ ! -e "$GRUB_CFG" ]; then log "Couldn't find boot files at $BOOT_DIR" exit 1 fi # keep old label unless overridden explicitly OLD_LABEL=$(lsblk -dnr -o LABEL "$PART_DEV") || OLD_LABEL= case "$OLD_LABEL" in "$LABEL_PREFIX"*) if [ -z "${LABEL_SUFFIX+set}" ]; then LABEL=$OLD_LABEL fi esac # create and/or label fs if replace; then log "Creating new ext4 filesystem labeled $LABEL" mkfs.ext4 "${F[@]}" -L "$LABEL" "$PART_DEV" else log "Labeling filesystem $LABEL" e2label "$PART_DEV" "$LABEL" fi # move secrets if label changed if [ -n "$OLD_LABEL" ] && [ -e "$AEM_DIR/$OLD_LABEL" ] && [ ! -e "$AEM_DIR/$LABEL" ]; then mv -v "$AEM_DIR/$OLD_LABEL" "$AEM_DIR/$LABEL" fi # add the AEM media being created to the freshness database if suffixtoslot "$LABEL_SUFFIX" >/dev/null; then log "WARNING: (possibly another) AEM media with the same" log "label suffix is already enrolled in the freshness token" log "database! Overwriting will result in the old AEM media" log "failing to perform a successful AEM boot. If you're" log "simply reinstalling on the same device or intentionally" log "replacing an old AEM media that was lost/destroyed/etc.," log "it is safe to continue." read -r -p "Proceed? [y/N] " response case "$response" in y|Y) echo "continuing..." ;; *) exit ;; esac else assignslottosuffix "$LABEL_SUFFIX" slot=$(suffixtoslot "$LABEL_SUFFIX") log "Assigned slot $slot to this AEM media" fi # MFA: generate a TOTP seed if mfa && [ ! -e "$AEM_DIR/$LABEL/secret.otp" ]; then log "Generating new 160-bit TOTP seed" mkdir -p "$AEM_DIR/$LABEL" otp_secret=$(head -c 20 /dev/random | base32 -w 0 | tr -d =) echo "$otp_secret" > "$AEM_DIR/$LABEL/secret.otp" # create an ANSI text QR code and show it in the terminal otp_uri="otpauth://totp/${LABEL}?secret=${otp_secret}" echo -n "$otp_uri" | qrencode -t ansiutf8 log "Please scan the above QR code with your OTP device." # display the text form of secret to user, too # shellcheck disable=SC2001 human_readable_secret="$(echo "$otp_secret" | sed 's/\(....\)/\1\ /g')" log "Alternatively, you may manually enter the following" log "secret into your OTP device:" log " $human_readable_secret" if timedatectl status | grep -q 'RTC in local TZ: yes'; then log "" log "WARNING: Your computer's RTC (real-time clock) is set" log "to store time in local timezone. This will cause wrong" log "TOTP codes to be generated during AEM boot. Please fix" log "this by running (as root):" log " timedatectl set-local-rtc 0" waitforenter fi # check whether secret was provisioned correctly log "" log "After you have set up your OTP device, please enter" log "the code displayed on your device and press " log "to continue." log "" totp_tries=3 for try in $(seq $totp_tries); do read -r -p "Code: " if ! oathtool --totp -b "$otp_secret" "$REPLY" >/dev/null; then log "Entered TOTP code is invalid!" if [ "$try" -lt $totp_tries ]; then log "Please check clock synchronization." log "If you made mistake while manually entering the secret," log "remove the added token, repeat the process & try again." log "" else log "Aborting AEM setup..." exit 1 fi else break fi done log "TOTP code matches, continuing AEM setup." fi # MFA: generate and enroll a LUKS key file if not already present if mfa && [ ! -e "$AEM_DIR/$LABEL/secret.key" ]; then log "Generating new LUKS key file" rawkey=$(mktemp) head -c 64 /dev/random > "$rawkey" log "Encrypting key file" mkdir -p "$AEM_DIR/$LABEL" scrypt enc "$rawkey" "$AEM_DIR/$LABEL/secret.key" for uuid in $(getluksuuids); do dev=/dev/disk/by-uuid/$uuid devname=$(readlink -f "$dev") log "Adding key file to new key slot for $devname (UUID $uuid)" cryptsetup luksAddKey "$dev" "$rawkey" done log "Shredding the unencrypted key file" shred -zu "$rawkey" fi # mount if CUR_MNT=$(devtomnt "$PART_DEV") && [ -n "$CUR_MNT" ]; then PART_MNT=$CUR_MNT else CUR_MNT= PART_MNT=/mnt/anti-evil-maid/$LABEL log "Mounting at $PART_MNT" mkdir -p "$PART_MNT" mount "$PART_DEV" "$PART_MNT" fi # sync mkdir -p "$PART_MNT/aem" synctpms "$LABEL" "$PART_MNT" mkdir -p "$AEM_DIR/$LABEL" # make device bootable if replace; then log "Setting bootable flag" parted -s "$WHOLE_DEV" set "$PART_NUM" boot on log "Copying boot files" find "$BOOT_DIR" -maxdepth 1 -type f ! -name 'initramfs-*.img' \ -exec cp {} "$PART_MNT" \; # TODO: If dracut is configured for no-hostonly mode (so we don't have to # worry about picking up loaded kernel modules), just copy each initramfs # instead of regenerating it for img in "$BOOT_DIR"/initramfs-*.img; do ver=${img%.img} ver=${ver##*initramfs-} log "Generating initramfs for kernel $ver" dracut --force "$PART_MNT/${img##*/}" "$ver" done log "Copying GRUB themes" dst=$PART_MNT/${GRUB_DIR#"$BOOT_DIR"/} mkdir "$dst" cp -r "$GRUB_DIR/themes" "$dst" log "Installing GRUB" grub2-install --boot-directory="$PART_MNT" "$WHOLE_DEV" log "Bind mounting $PART_MNT at $BOOT_DIR" mount --bind "$PART_MNT" "$BOOT_DIR" fi log "Generating GRUB configuration" grub2-mkconfig -o "$GRUB_CFG" if replace; then log "Unmounting bind mounted $BOOT_DIR" umount "$BOOT_DIR" fi if [ -z "$CUR_MNT" ]; then log "Unmounting $PART_MNT" umount "$PART_MNT" fi ================================================ FILE: sbin/anti-evil-maid-lib ================================================ LABEL_PREFIX=aem SYSFS_TPM_DIR=/sys/class/tpm/tpm0 AEM_DIR=/var/lib/anti-evil-maid TPM_DIR=/var/lib/tpm TPMS_DIR=${TPM_DIR}s CACHE_DIR=/run/anti-evil-maid SRK_PASSWORD_CACHE=$CACHE_DIR/srk-password # shellcheck disable=SC2034 SUFFIX_CACHE=$CACHE_DIR/suffix TPM_OWNER_PASSWORD_FILE=$AEM_DIR/tpm-owner-pw TPM_FRESHNESS_PASSWORD_FILE=$AEM_DIR/tpm-freshness-pw TPM_FRESHNESS_INDEX="0x454d" TPM_FRESHNESS_SLOTS=8 # work with or without plymouth if command plymouth --ping 2>/dev/null; then alias plymouth_active=true alias message=plymouth_message else alias plymouth=: alias plymouth_active=false alias message=log fi getparams() { _CMDLINE=${_CMDLINE-$(cat /proc/cmdline)} for _param in $_CMDLINE; do for _key; do case "$_param" in "$_key"=*) printf '%s\n' "${_param#*=}" break esac done done } getluksuuids() { getparams rd.luks.uuid rd_LUKS_UUID | sed s/^luks-// } log() { echo "${0##*/}: $1" >&2 } hex() { xxd -ps | tr -dc 0-9a-f } unhex() { tr -dc 0-9a-f | xxd -ps -r } waitfor() { case $# in 2) _file=$2; _what=connected ;; 3) _file=$3; _what=removed ;; *) return 1 ;; esac if [ "$@" ]; then return fi message "Waiting for $_file to be $_what..." plymouth pause-progress until [ "$@" ]; do sleep 0.1 done plymouth unpause-progress message "$_file $_what" } waitforenter() { msg='Press to continue...' if plymouth_active; then message "$msg" plymouth watch-keystroke --keys=$'\n' else systemd-ask-password --timeout=0 --echo=no "$msg" >/dev/null fi } suffixtoslotfile() { echo "$AEM_DIR/$LABEL_PREFIX$1/tpm-freshness-slot" } suffixtoslot() { # returns the slot number assigned to the AEM media given its label suffix # as the first argument _slotfile=$(suffixtoslotfile "$1") cat "$_slotfile" 2>/dev/null } assignslottosuffix() { # assigns an unused freshness slot number (if available) to an AEM # media identified by its label suffix (passed as the first argument) _slotfile=$(suffixtoslotfile "$1") rm -f "$_slotfile" _slotfilesglob=$(suffixtoslotfile '*') _lastslot=$((TPM_FRESHNESS_SLOTS - 1)) _freeslot=$( { cat "$_slotfilesglob" 2>/dev/null || true seq 0 $_lastslot } | sort -n | uniq -u | head -n 1 ) if [ -z "$_freeslot" ]; then message "No more freshness token slots available!" return 1 fi mkdir -p "${_slotfile%/*}" echo "$_freeslot" >> "$_slotfile" } synctpms() { _label=${1:?} _mnt=${2:?} message "Syncing to $_mnt" _mnt_tpms_dir=$_mnt/aem/${TPMS_DIR##*/} rm -rf "$_mnt_tpms_dir" _ids=$(ls "$TPMS_DIR") for _id in $_ids; do mkdir -p "$_mnt_tpms_dir/$_id" # this file is used only with TPM1 if [ -f "$TPMS_DIR/$_id/system.data" ]; then cp "$TPMS_DIR/$_id/system.data" "$_mnt_tpms_dir/$_id" fi if [ -d "$TPMS_DIR/$_id/$_label" ]; then cp -r "$TPMS_DIR/$_id/$_label" "$_mnt_tpms_dir/$_id" fi done } devtomnt() { lsblk -dnr -o MOUNTPOINT "$1" 2>/dev/null | sed 's/%/\\x25/g' | xargs -0 printf } topdev() { lsblk -snrp -o KNAME "$1" | tail -n 1 } external() { _aem_whole=$(topdev "$1") for _luks_uuid in $(getluksuuids); do _luks_whole=$(topdev "/dev/disk/by-uuid/$_luks_uuid") if [ "$_aem_whole" = "$_luks_whole" ]; then return 1 fi done return 0 } removable() { _rm="$(lsblk -dnr -o RM "$1") ${2-$(lsblk -dnr -o LABEL "$1")}" case "$_rm" in *.rm=[01]) _rm=${_rm##*=} ;; *) _rm=${_rm%% *} ;; esac [ "$_rm" = 1 ] } validatetpm() { # makes sure TPM is there and can be used, determines TPM version if [ ! -d "$SYSFS_TPM_DIR" ]; then message "$SYSFS_TPM_DIR isn't present" return 1 fi _tpm_version=$(cat "$SYSFS_TPM_DIR/tpm_version_major") if [ -z "$_tpm_version" ]; then message "Failed to determine the version of the TPM" return 1 fi if [ "$_tpm_version" -eq 1 ]; then # shellcheck source=../sbin/anti-evil-maid-lib-tpm1 source /sbin/anti-evil-maid-lib-tpm1 return 0 fi if [ "$_tpm_version" -eq 2 ]; then # shellcheck source=../sbin/anti-evil-maid-lib-tpm2 source /sbin/anti-evil-maid-lib-tpm2 return 0 fi message "Unexpected TPM version: $_tpm_version" return 1 } ================================================ FILE: sbin/anti-evil-maid-lib-tpm1 ================================================ tpmid() { tpm_id } tpmzsrk() { tpm_z_srk } checktpmnvram() { # checks whether the TPM NVRAM area is defined # NOTE: tpm_nvinfo does not return non-zero if requested index # is not a defined NVRAM area so we need to parse if ! tpm_nvinfo -i "$TPM_FRESHNESS_INDEX" | grep -q 'AUTHWRITE'; then return 1 fi } createtpmnvram() { # create the world-readable/AUTHWRITE TPM NVRAM area to hold up to # TPM_FRESHNESS_SLOTS anti-replay freshness token hashes; # takes TPM owner password as an agument if [ ! -e "$TPM_FRESHNESS_PASSWORD_FILE" ]; then message "Generating TPM NVRAM area AUTHWRITE password" head -c 16 /dev/random | hex > "$TPM_FRESHNESS_PASSWORD_FILE" fi _pw=$(cat "$TPM_FRESHNESS_PASSWORD_FILE") if ! tpm_nvdefine -i "$TPM_FRESHNESS_INDEX" \ -s $((TPM_FRESHNESS_SLOTS * 20)) \ -p AUTHWRITE --pwda="$_pw" --pwdo="$1"; then return 1 fi } hashfile() { # computes hash of a file passed as the only argument _path=$1 sha1sum "$_path" | cut -d ' ' -f 1 } checkfreshness() { # check whether hash of an usealed freshness token (file path # given as an argument) is contained in TPM NVRAM area _hash=$(hashfile "$1") _lastslot=$((TPM_FRESHNESS_SLOTS - 1)) for _i in $(seq 0 $_lastslot); do _slot=$(tpm_nvread_stdout -i "$TPM_FRESHNESS_INDEX" \ -n "$((_i * 20))" -s 20 | hex) if [ "$_hash" == "$_slot" ]; then return 0 fi done message "Freshness token does not match any slot in TPM NVRAM!" return 1 } updatefreshness() { # takes a path to the new freshness token as an argument and # stores its sha1 hash in the appropriate freshness token slot # of the TPM NVRAM area; second argument is the AEM boot device # label suffix if [ ! -e "$TPM_FRESHNESS_PASSWORD_FILE" ]; then message "TPM NVRAM area AUTHWRITE password file does not exist!" return 1 fi if ! _slot=$(suffixtoslot "$2"); then message "Suffix '$2' not in DB, attempting to create..." if ! _slot=$(assignslottosuffix "$2"); then message "Failed to add suffix '$2' into DB!" return 1 fi fi _pw=$(cat "$TPM_FRESHNESS_PASSWORD_FILE") hashfile "$1" | unhex \ | tpm_nvwrite_stdin -i "$TPM_FRESHNESS_INDEX" \ -n "$((_slot * 20))" -s 20 --password="$_pw" } revokefreshness() { # invalidates the freshness token of a specified AEM media (by its # label suffix _suff=$1 if _slot=$(suffixtoslot "$_suff"); then message "Revoking freshness token for AEM media w/ suffix '$_suff'..." _pw=$(cat "$TPM_FRESHNESS_PASSWORD_FILE") if tpm_nvwrite -i "$TPM_FRESHNESS_INDEX" \ -n "$((_slot * 20))" -s 20 \ --password="$_pw" -m "0xff"; then message "Done." else message "Failed!" fi else message "AEM device with label suffix '$_suff' not found in DB!" fi } resetfreshness() { # invalidates ALL freshness tokens message "Invalidating **ALL** freshness tokens..." _pw=$(cat "$TPM_FRESHNESS_PASSWORD_FILE") if tpm_nvwrite -i "$TPM_FRESHNESS_INDEX" \ -s "$((TPM_FRESHNESS_SLOTS * 20))" \ --password="$_pw" -m "0xff"; then message "Done." else message "Failed!" fi } destroytpmnvram() { # releases the TPM NVRAM area; TPM owner pw as first argument tpm_nvrelease -i "$TPM_FRESHNESS_INDEX" --pwdo="$1" } listbadpcrs() { # prints those standard PCRs configured to be used via $SEAL which haven't # been extended, output is empty there are no such PCRs _pcrs=$(printf %s "$SEAL" | grep -Eo '\b1[3789]\b') || true grep -E "^PCR-(${_pcrs//$'\n'/|}):( 00| FF){20}" "$SYSFS_TPM_DIR"/pcrs } tpmowned() { # checks whether TPM is already owned, signals results with exit code [ "$(cat "$SYSFS_TPM_DIR"/owned)" -ne 0 ] } provisiontpmid() { # stores TPM ID into an NVRAM entry _tpm_id_index=$(tpm_id -i) _opw=$(cat "$TPM_OWNER_PASSWORD_FILE") # create a write-once NVRAM area tpm_nvdefine -i "$_tpm_id_index" -s 20 -p "WRITEDEFINE|WRITEALL" \ --pwdo="$_opw" # generate a random ID and write it into NVRAM head -c 20 /dev/random | tpm_nvwrite_stdin -i "$_tpm_id_index" -s 20 # lock the area to prevent non-owners from changing ID tpm_nvwrite -i "$_tpm_id_index" -s 0 } postprovisioning() { # takes care of updating /var/lib/tpms after a successful provisioning by # provisiontpmid _tpmid=$(tpm_id) mkdir -p "/var/lib/tpms/$_tpmid" systemctl stop tcsd mv "$TPMS_DIR"/unknown/* "$TPMS_DIR/$_tpmid/" rm -rf "$TPMS_DIR/unknown" systemctl start tcsd } checksrkpass() { # checks whether contents of $SRK_PASSWORD_CACHE file is a valid SRK # password, signals result with exit code tpm_sealdata -i /dev/null -o /dev/null < "$SRK_PASSWORD_CACHE" } tpmpcrextend() { # extends a PCR with a hash value of a suitable type _pcr=$1 _hash=$2 tpm_pcr_extend "$_pcr" "$_hash" } tpmsealprepare() { # does necessary preparations before the use of tpmsealdata, accepts path # to media-specific storage of sealed data true # nothing to do for TPM1 } tpmsealdata() { # seals source specified by second argument into destination specified by # the third one, non-empty first argument signifies empty SRK password, the # forth argument specifies path to AEM media-specific storage _nosrkpass=() if [ -n "$1" ]; then _nosrkpass=( -z ) fi _input=$2 _output=$3 # shellcheck disable=SC2086 if [ ! -t 0 ]; then cat "$SRK_PASSWORD_CACHE"; fi | tpm_sealdata "${_nosrkpass[@]}" $SEAL -i "$_input" -o "$_output" } tpmunsealdata() { # unseals source specified by second argument into destination specified by # the third one, non-empty first argument signifies empty SRK password, the # forth argument specifies path to AEM media-specific storage _nosrkpass=() if [ -n "$1" ]; then _nosrkpass=( -z ) fi _infile=$2 _outfile=$3 tpm_unsealdata "${_nosrkpass[@]}" -i "$_infile" -o "$_outfile" \ < "$SRK_PASSWORD_CACHE" } tpmtakeownership() { # takes ownership of the TPM, accepts owner and SRK passwords in this order _opw=$1 _srkpw=$2 _lines=( "$_opw" "$_opw" ) _nosrkpass=() if [ -n "$_srkpw" ]; then _lines+=( "$_srkpw" "$_srkpw" ) else _nosrkpass=( -z ) fi printf '%s\n' "${_lines[@]}" | notty env LC_ALL=C tpm_takeownership "${_nosrkpass[@]}" \ 2> >(grep -vF "Confirm password:" >&2) } tpmresetdalock() { notty tpm_resetdalock <"$TPM_OWNER_PASSWORD_FILE" } tpmstartservices() { systemctl start tcsd } tpmstartinitrdservices() { trousers_changer_identify # it forks tcsd } tpmrestartservices() { systemctl restart tcsd } ================================================ FILE: sbin/anti-evil-maid-lib-tpm2 ================================================ # Value recommended by TCG TPM v2.0 Provisioning Guidance # https://trustedcomputinggroup.org/wp-content/uploads/TCG-TPM-v2.0-Provisioning-Guidance-Published-v1r1.pdf # in Table 2 TPM2_SRK_HANDLE=0x81000001 # this is necessary for anti-evil-maid-seal to not try to use tarbmd TCTI which # has large timeouts for trying to connect with tpm2-abrmd which isn't running export TPM2TOOLS_TCTI="device:/dev/tpm0" # make sure we're not leaving any temporary state in the TPM (some very # unobvious commands create sessions/objects), this way if tpm2-abrmd will be # used later, its idea about the initial contents of the TPM being empty will # be correct (otherwise you can get "out of memory", but tpm2_getcap won't show # anything and tpm2_flushcontext won't clean anything unless $TPM2TOOLS_TCTI # is set as above) trap 'tpm2_flushcontext -tls' EXIT tpmid() { tpm2_id } tpmzsrk() { tpm2_z_srk } checktpmnvram() { # checks whether the TPM NVRAM area is defined # NOTE: tpm2_nvreadpublic returns all defined NV indices so we need to parse tpm2_nvreadpublic | grep "$TPM_FRESHNESS_INDEX" -A 7 | grep -q 'authwrite' } createtpmnvram() { # create the world-readable/AUTHWRITE TPM NVRAM area to hold up to # TPM_FRESHNESS_SLOTS anti-replay freshness token hashes; # takes TPM owner password as an agument if [ ! -e "$TPM_FRESHNESS_PASSWORD_FILE" ]; then message "Generating TPM NVRAM area AUTHWRITE password" head -c 16 /dev/random | hex > "$TPM_FRESHNESS_PASSWORD_FILE" fi _opw=$1 _pw=$(cat "$TPM_FRESHNESS_PASSWORD_FILE") _session="$(mktemp)" _policy="$(mktemp)" tpm2_startauthsession -S "$_session" tpm2_policycommandcode -Q -S "$_session" -L "$_policy" \ TPM2_CC_NV_Read tpm2_flushcontext "$_session" rm "$_session" tpm2_nvdefine -Q "$TPM_FRESHNESS_INDEX" \ -L "$_policy" \ -a "policyread|authread|authwrite" \ -s $((TPM_FRESHNESS_SLOTS * 32)) \ -P "$_opw" -p "$_pw" rm "$_policy" } hashfile() { # computes hash of a file passed as the only argument _path=$1 sha256sum "$_path" | cut -d ' ' -f 1 } checkfreshness() { # check whether hash of an usealed freshness token (file path # given as an argument) is contained in TPM NVRAM area _hash=$(hashfile "$1") _lastslot=$((TPM_FRESHNESS_SLOTS - 1)) tpm2_startauthsession -S "$CACHE_DIR/session" --policy-session tpm2_policycommandcode -Q -S "$CACHE_DIR/session" TPM2_CC_NV_Read for _i in $(seq 0 $_lastslot); do _slot=$(tpm2_nvread "$TPM_FRESHNESS_INDEX" \ -P "session:$CACHE_DIR/session" \ --offset="$((_i * 32))" -s 32 | hex) if [ "$_hash" == "$_slot" ]; then tpm2_flushcontext "$CACHE_DIR/session" return 0 fi done tpm2_flushcontext "$CACHE_DIR/session" message "Freshness token does not match any slot in TPM NVRAM!" return 1 } updatefreshness() { # takes a path to the new freshness token as an argument and # stores its hash in the appropriate freshness token slot # of the TPM NVRAM area; second argument is the AEM boot device # label suffix _file=$1 if [ ! -e "$TPM_FRESHNESS_PASSWORD_FILE" ]; then message "TPM NVRAM area AUTHWRITE password file does not exist!" return 1 fi if ! _slot=$(suffixtoslot "$2"); then message "Suffix '$2' not in DB, attempting to create..." if ! _slot=$(assignslottosuffix "$2"); then message "Failed to add suffix '$2' into DB!" return 1 fi fi _pw=$(cat "$TPM_FRESHNESS_PASSWORD_FILE") hashfile "$_file" | unhex | tpm2_nvwrite "$TPM_FRESHNESS_INDEX" -i - \ --offset "$((_slot * 32))" -P "$_pw" } ffbytestream() { _count=$1 tr '\0' '\377' < /dev/zero | dd bs="$_count" count=1 } revokefreshness() { # invalidates the freshness token of a specified AEM media (by its # label suffix _suff=$1 if _slot=$(suffixtoslot "$_suff"); then message "Revoking freshness token for AEM media w/ suffix '$_suff'..." _pw=$(cat "$TPM_FRESHNESS_PASSWORD_FILE") if ffbytestream 32 | tpm2_nvwrite "$TPM_FRESHNESS_INDEX" \ --offset "$((_slot * 32))" \ -P "$_pw" -i - ; then message "Done." else message "Failed!" fi else message "AEM device with label suffix '$_suff' not found in DB!" fi } resetfreshness() { # invalidates ALL freshness tokens message "Invalidating **ALL** freshness tokens..." _pw=$(cat "$TPM_FRESHNESS_PASSWORD_FILE") if ffbytestream "$((TPM_FRESHNESS_SLOTS * 32))" | tpm2_nvwrite "$TPM_FRESHNESS_INDEX" \ -P "$_pw" -i - ; then message "Done." else message "Failed!" fi } destroytpmnvram() { # releases the TPM NVRAM area; TPM owner pw as first argument _pw=$1 tpm2_nvundefine "$TPM_FRESHNESS_INDEX" -C owner -P "$_pw" } listbadpcrs() { # prints those standard PCRs configured to be used via $SEAL which haven't # been extended, output is empty there are no such PCRs _pcrs=$(printf %s "$SEAL" | grep -Eo '\b1[3789]\b') || true tpm2_pcrread "sha256:${_pcrs//$'\n'/,}" | grep -E ": 0x((00){32}|(FF){32})" } tpmowned() { # checks whether TPM is already owned, signals results with exit code ! tpm2_changeauth --quiet -c owner 2>/dev/null } provisiontpmid() { # stores TPM ID into an NVRAM entry _tpm_id_index=$(tpm2_id -i) _opw=$(cat "$TPM_OWNER_PASSWORD_FILE") # create a write-once and read-by-anyone NVRAM area _session="$(mktemp)" _policy="$(mktemp)" tpm2_startauthsession -S "$_session" tpm2_policycommandcode -Q -S "$_session" -L "$_policy" TPM2_CC_NV_Read tpm2_flushcontext "$_session" rm "$_session" tpm2_nvdefine -Q -s 20 -P "$_opw" -L "$_policy" "$_tpm_id_index" \ -a "policyread|writedefine|writeall|ownerwrite|ownerread" rm "$_policy" # generate a random ID and write it into NVRAM head -c 20 /dev/random | tpm2_nvwrite -Q -C o -P "$_opw" -i - "$_tpm_id_index" # lock the area to prevent changing the ID even by the owner tpm2_nvwritelock -C o -P "$_opw" "$_tpm_id_index" } postprovisioning() { # takes care of updating /var/lib/tpms after a successful provisioning by # provisiontpmid _tpmid=$(tpm2_id) mkdir -p "/var/lib/tpms/$_tpmid" } checksrkpass() { # checks whether contents of $SRK_PASSWORD_CACHE file is a valid SRK # password, signals result with exit code # `echo` is needed because empty input doesn't work and `echo` it provides # '\n' echo | tpm2_create -Q -C "$TPM2_SRK_HANDLE" -i - \ -P "str:$(cat "$SRK_PASSWORD_CACHE")" } tpmpcrextend() { # extends a PCR with a hash value of a suitable type _pcr=$1 _hash=$2 tpm2_pcrextend "$_pcr:sha256=$_hash" } tpmsealprepare() { # does necessary preparations before the use of tpmsealdata, accepts path # to media-specific storage of sealed data _dir=$1 # this recreates a sealing key on every run to pick up configuration/PCR # changes if there were any _pcrs=$(printf %s "$SEAL" | grep -Eo '\b[0-9]+\b') _pcrs=sha256:${_pcrs//$'\n'/,} _policy="$(mktemp)" _session="$(mktemp)" # make a suitable PCR policy tpm2_startauthsession -S "$_session" tpm2_policypcr -Q -S "$_session" -l "$_pcrs" -L "$_policy" tpm2_flushcontext "$_session" rm "$_session" tpm2_flushcontext -t || return 1 # make a key for sealing head -c 16 /dev/random | tpm2_create -Q -C "$TPM2_SRK_HANDLE" \ -P "str:$(cat "$SRK_PASSWORD_CACHE")" \ -L "$_policy" -i - -u "$_dir/key.pub" -r "$_dir/key.priv" echo "$_pcrs" > "$_dir/key.pcrs" rm "$_policy" } tpmsealdata() { # seals source specified by second argument into destination specified by # the third one, non-empty first argument signifies empty SRK password, the # forth argument specifies path to AEM media-specific storage _infile=$2 _outfile=$3 _dir=$4 _ctx="$(mktemp)" tpm2_load -Q -C "$TPM2_SRK_HANDLE" -P "str:$(cat "$SRK_PASSWORD_CACHE")" \ --private "$_dir/key.priv" --public "$_dir/key.pub" \ -c "$_ctx" || return 1 tpm2_unseal -Q -c "$_ctx" -p "pcr:$_pcrs" | hex | openssl enc -aes-256-ctr -pbkdf2 -e \ -kfile - -in "$_infile" -out "$_outfile" || return 1 tpm2_flushcontext -t || return 1 rm "$_ctx" } tpmunsealdata() { # unseals source specified by second argument into destination specified by # the third one, non-empty first argument signifies empty SRK password, the # forth argument specifies path to AEM media-specific storage # # there is not need to handle the first argument, empty $SRK_PASSWORD_CACHE # file will do _infile=$2 _outfile=$3 _dir=$4 _pcrs=$(cat "$_dir/key.pcrs") _ctx="$(mktemp)" tpm2_load -Q -C "$TPM2_SRK_HANDLE" -P "str:$(cat "$SRK_PASSWORD_CACHE")" \ --private "$_dir/key.priv" --public "$_dir/key.pub" \ -c "$_ctx" 2>/dev/null || return 1 tpm2_unseal -Q -c "$_ctx" -p "pcr:$_pcrs" | hex | openssl enc -aes-256-ctr -pbkdf2 -d \ -kfile - -in "$_infile" -out "$_outfile" || return 1 tpm2_flushcontext -t || return 1 rm "$_ctx" } tpmtakeownership() { # takes ownership of the TPM, accepts owner and SRK passwords in this order _opw=$1 _srkpw=$2 tpm2_changeauth --quiet -c owner "$_opw" # use the same password for lockout handle tpm2_changeauth --quiet -c lockout "$_opw" _srkctx="$(mktemp)" tpm2_createprimary -Q --hierarchy=o \ --key-context="$_srkctx" \ --key-auth="$_srkpw" \ -P "$_opw" # make SRK key persistent tpm2_evictcontrol -Q -C o -P "$_opw" -c "$_srkctx" "$TPM2_SRK_HANDLE" rm "$_srkctx" } tpmresetdalock() { tpm2_dictionarylockout -p "$(cat "$TPM_OWNER_PASSWORD_FILE")" \ --clear-lockout } tpmstartservices() { trousers_changer_migrate || true trousers_changer_identify } tpmstartinitrdservices() { trousers_changer_identify } tpmrestartservices() { trousers_changer_migrate 2>/dev/null || true trousers_changer_identify 2>/dev/null } ================================================ FILE: sbin/anti-evil-maid-seal ================================================ #!/bin/bash set -euo pipefail -o errtrace shopt -s expand_aliases alias plymouth_message="plymouth message --text" source anti-evil-maid-lib trap 'rm -rf "$CACHE_DIR"' EXIT validatetpm || exit 1 # Listing foo.service in anti-evil-maid-seal.service's Requires= and After= # would cause it to always be started (even when not booting in AEM mode or # when sealing is unnecessary) due to the way systemd evaluates conditions. # Putting the systemctl command in ExecStartPre= is also insufficient: The # user might want to run this script manually after changing the secret(s). tpmstartservices if [ ! -e "$SUFFIX_CACHE" ] && [ $# -ne 1 ]; then message "AEM media suffix cache file does not exist" message "and you didn't specify a suffix as the" message "first positional argument to this script." exit 1 fi # scream loudly if sealing fails for some reason # (eg. AEM media read-only) _failure() { message "Failed to seal secrets (error @ line $1)!" waitforenter exit 1 } trap '_failure $LINENO' ERR # define sealing and device variables # shellcheck source=../etc/anti-evil-maid.conf source /etc/anti-evil-maid.conf tpmresetdalock || true Z=$(tpmzsrk) LABEL_SUFFIX=${1-$(cat "$SUFFIX_CACHE")} LABEL=$LABEL_PREFIX$LABEL_SUFFIX case $# in 0) DEV=/dev/disk/by-uuid/$(getparams aem.uuid) ;; 1) DEV=/dev/disk/by-label/$LABEL ;; *) exit 1 ;; esac # ensure that all standard PCRs configured to be used have been extended bad_pcrs=$(listbadpcrs) || true if [ -n "$bad_pcrs" ]; then message "PCR sanity check failed!" message "Bad PCRs:"$'\n'"$bad_pcrs" message "See /usr/share/doc/anti-evil-maid/README for details." exit 1 fi # regenerate the freshness token and store its hash in TPM head -c 20 /dev/random > "$AEM_DIR/$LABEL/secret.fre" updatefreshness "$AEM_DIR/$LABEL/secret.fre" "$LABEL_SUFFIX" # seal and save secret(s) to root partition mkdir -p "$TPM_DIR/$LABEL" tpmsealprepare "$TPM_DIR/$LABEL" SEALED=0 for ext in txt key otp fre; do input=$AEM_DIR/$LABEL/secret.$ext output=$TPM_DIR/$LABEL/secret.$ext.sealed2 if [ ! -e "$input" ]; then message "Absent $input" elif tpmsealdata "$Z" "$input" "$output" "$TPM_DIR/$LABEL"; then rm -f "${output%2}" SEALED=$((SEALED + 1)) message "Sealed $input using $SEAL" else message "Failed $input" fi done if [ "$SEALED" = 0 ]; then exit 1 fi # mount device waitfor -b "$DEV" if CUR_MNT=$(devtomnt "$DEV") && [ -n "$CUR_MNT" ]; then MNT=$CUR_MNT else CUR_MNT= MNT=/mnt/anti-evil-maid/$LABEL mkdir -p "$MNT" mount "$DEV" "$MNT" fi # copy secret(s) to device synctpms "$LABEL" "$MNT" # unmount device if [ -z "$CUR_MNT" ]; then umount "$MNT" if external "$DEV" && removable "$DEV"; then waitfor ! -b "$DEV" fi fi ================================================ FILE: sbin/anti-evil-maid-tpm-setup ================================================ #!/bin/bash set -euo pipefail shopt -s expand_aliases source anti-evil-maid-lib validatetpm || exit 1 if ! { [ $# = 0 ] || { [ $# = 1 ] && [ "$1" = "-z" ]; }; } then echo "Usage: ${0##*/} [-z]" exit 1 fi if [ "$(id -ur)" != 0 ]; then log "This command must be run as root!" exit 1 fi if tpmowned; then log "You must reset/clear your TPM chip first!" exit 1 fi # - take ownership of TPM OWNERPW=$(head -c 16 /dev/random | hex) srkpw= if [ $# = 0 ]; then # set an SRK password for try in 1 2 3; do read -r -s -p "Choose SRK password: " srkpw echo read -r -s -p "Confirm SRK password: " srkpw2 echo [ "$srkpw" != "$srkpw2" ] || break log "Passwords didn't match" [ "$try" != 3 ] || exit 1 done fi tpmrestartservices log "Taking ownership of the TPM..." tpmtakeownership "$OWNERPW" "$srkpw" echo "$OWNERPW" >"$TPM_OWNER_PASSWORD_FILE" # - generate NVRAM ID alias provisioned_id=false if [[ $(tpmid 2>/dev/null) == "unknown" ]]; then # TPM reset does not clear NVRAM, reusing old ID is fine though log "Creating TPM ID..." provisiontpmid alias provisioned_id=true fi # - create freshness token area if checktpmnvram; then # delete old freshness area as the old access password is most likely lost # (in case it isn't, the area will simply get recreated with the same pw) log "Deleting old freshness token NVRAM area..." destroytpmnvram "$OWNERPW" fi log "Creating freshness token NVRAM area..." createtpmnvram "$OWNERPW" # - update TPMs directory after provisioning if provisioned_id; then postprovisioning fi ================================================ FILE: systemd/system/anti-evil-maid-check-mount-devs.service ================================================ [Unit] Description=Anti Evil Maid system mount dev check DefaultDependencies=no ConditionKernelCommandLine=aem.uuid Before=initrd-root-fs.target sysroot.mount swap.target After=dracut-initqueue.service After=cryptsetup.target [Service] Type=oneshot RemainAfterExit=no ExecStart=/sbin/anti-evil-maid-check-mount-devs StandardOutput=journal+console StandardError=journal+console ================================================ FILE: systemd/system/anti-evil-maid-seal.service ================================================ [Unit] Description=Anti Evil Maid sealing DefaultDependencies=false Requires=local-fs.target After=local-fs.target plymouth-start.service Before=basic.target ConditionKernelCommandLine=aem.uuid ConditionPathIsDirectory=/run/anti-evil-maid [Service] ExecStart=/usr/sbin/anti-evil-maid-seal Type=oneshot StandardOutput=journal+console StandardError=inherit TimeoutStartSec=300 [Install] WantedBy=basic.target ================================================ FILE: systemd/system/anti-evil-maid-unseal.service ================================================ [Unit] Description=Anti Evil Maid unsealing DefaultDependencies=no Wants=cryptsetup-pre.target Before=cryptsetup-pre.target After=plymouth-start.service ConditionKernelCommandLine=aem.uuid [Service] Type=oneshot RemainAfterExit=no ExecStart=/sbin/anti-evil-maid-unseal StandardInput=null StandardOutput=tty StandardError=journal+console TimeoutStartSec=300 ================================================ FILE: systemd/system/tcsd.service.d/anti-evil-maid-seal.conf ================================================ # start early enough for anti-evil-maid-seal.service [Unit] DefaultDependencies=false # for trousers_changer_identify: Requires=local-fs.target After=local-fs.target ================================================ FILE: version ================================================ 4.2.1