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