diff options
62 files changed, 5317 insertions, 0 deletions
diff --git a/bin/run_test_suite b/bin/run_test_suite new file mode 100755 index 00000000..cf7abeb9 --- /dev/null +++ b/bin/run_test_suite @@ -0,0 +1,196 @@ +#!/bin/sh + +set -e +set -u + +NAME=$(basename ${0}) + +usage() { + echo "Usage: $NAME [OPTION]... [FEATURE]... +Sets up an appropriate environment and tests FEATUREs (all by default). Note +that this script must be run from the Tails source directory root. + +Options for '@product' features: + --capture FILE Captures the test session into FILE using VP8 encoding. + Requires ffmpeg and libvpx1. + --debug Display various debugging information while running the + test suite. + --pause-on-fail On failure, pause test suite until pressing Enter. This is + useful for investigating the state of the VM guest to see + exactly why a test failed. + --keep-snapshots Don't ever delete the background snapshots. This can a big + time saver when debugging new features. + --retry-find Print a warning whenever Sikuli fails to find an image + and allow *one* retry after pressing ENTER. This is useful + for updating outdated images. + --temp-dir Directory where various temporary files are written + during a test, e.g. VM snapshots and memory dumps, + failure screenshots, pcap files and disk images + (default is /tmp/TailsToaster). + --view Shows the test session in a windows. Requires x11vnc + and xtightvncviewer. + --vnc-server-only Starts a VNC server for the test session. Requires x11vnc. + --iso IMAGE Test '@product' features using IMAGE. If none is given, + the ISO with most recent creation date (according to the + ISO's label) in the current directory will be used. + --old-iso IMAGE For some '@product' features (e.g. usb_install) we need + an older version of Tails, which this options sets to + IMAGE. If none is given, the ISO with the least recent + creation date will be used. + +Note that '@source' features has no relevant options. +" +} + +error() { + echo "${NAME}: error: ${*}" >&2 + usage + exit 1 +} + +check_dependency() { + if ! which "${1}" >/dev/null && \ + ! dpkg -s "${1}" 2>/dev/null | grep -q "^Status:.*installed"; then + error "'${1}' is missing, please install it and run again. Aborting..." + fi +} + +display_in_use() { + [ -e "/tmp/.X${1#:}-lock" ] || [ -e "/tmp/.X11-unix/X${1#:}" ] +} + +next_free_display() { + display_nr=0 + while display_in_use ":${display_nr}"; do + display_nr=$((display_nr+1)) + done + echo ":${display_nr}" +} + +start_xvfb() { + Xvfb $TARGET_DISPLAY -screen 0 1024x768x24+32 >/dev/null 2>&1 & + XVFB_PID=$! + trap "kill -0 ${XVFB_PID} 2>/dev/null && kill -9 ${XVFB_PID}; \ + rm -f /tmp/.X${TARGET_DISPLAY#:}-lock" EXIT + # Wait for Xvfb to run on TARGET_DISPLAY + until display_in_use $TARGET_DISPLAY; do + sleep 1 + done + echo "Virtual X framebuffer started on display ${TARGET_DISPLAY}" + # Hide the mouse cursor so it won't mess up Sikuli's screen scanning + unclutter -display $TARGET_DISPLAY -root -idle 0 >/dev/null 2>&1 & +} + +start_vnc_server() { + check_dependency x11vnc + VNC_SERVER_PORT="$(x11vnc -listen localhost -display ${TARGET_DISPLAY} \ + -bg -nopw 2>&1 | \ + grep -m 1 "^PORT=[0-9]\+" | sed 's/^PORT=//')" + echo "VNC server running on: localhost:${VNC_SERVER_PORT}" +} + +start_vnc_viewer() { + check_dependency xtightvncviewer + xtightvncviewer -viewonly localhost:${VNC_SERVER_PORT} 1>/dev/null 2>&1 & +} + +capture_session() { + echo "Capturing guest display into ${CAPTURE_FILE}" + ffmpeg -f x11grab -s 1024x768 -r 15 -i ${TARGET_DISPLAY}.0 -an \ + -vcodec libvpx -y "${CAPTURE_FILE}" >/dev/null 2>&1 & +} + +# main script + +CAPTURE_FILE= +VNC_VIEWER= +VNC_SERVER= +DEBUG= +PAUSE_ON_FAIL= +KEEP_SNAPSHOTS= +SIKULI_RETRY_FINDFAILED= +TEMP_DIR= +ISO= +OLD_ISO= + +LONGOPTS="view,vnc-server-only,capture:,help,temp-dir:,keep-snapshots,retry-find,iso:,old-iso:,debug,pause-on-fail" +OPTS=$(getopt -o "" --longoptions $LONGOPTS -n "${NAME}" -- "$@") +eval set -- "$OPTS" +while [ $# -gt 0 ]; do + case $1 in + --view) + VNC_VIEWER=yes + VNC_SERVER=yes + ;; + --vnc-server-only) + VNC_VIEWER= + VNC_SERVER=yes + ;; + --capture) + shift + CAPTURE_FILE="$1" + ;; + --debug) + export DEBUG="yes" + ;; + --pause-on-fail) + export PAUSE_ON_FAIL="yes" + ;; + --keep-snapshots) + export KEEP_SNAPSHOTS="yes" + ;; + --retry-find) + export SIKULI_RETRY_FINDFAILED="yes" + ;; + --temp-dir) + shift + export TEMP_DIR="$(readlink -f $1)" + ;; + --iso) + shift + export ISO="$(readlink -f $1)" + ;; + --old-iso) + shift + export OLD_ISO="$(readlink -f $1)" + ;; + --help) + usage + exit 0 + ;; + --) + shift + break + ;; + esac + shift +done + +for dep in ffmpeg git libvirt-bin libvirt-dev libavcodec-extra-53 libvpx1 \ + virt-viewer libsikuli-script-java ovmf tcpdump xvfb; do + check_dependency "${dep}" +done + +TARGET_DISPLAY=$(next_free_display) + +start_xvfb + +if [ -n "${CAPTURE_FILE}" ]; then + capture_session +fi +if [ -n "${VNC_SERVER}" ]; then + start_vnc_server +fi +if [ -n "${VNC_VIEWER}" ]; then + start_vnc_viewer +fi + +export JAVA_HOME="/usr/lib/jvm/java-7-openjdk-amd64" +export SIKULI_HOME="/usr/share/java" +export DISPLAY=${TARGET_DISPLAY} +check_dependency cucumber +if [ -z "${*}" ]; then + cucumber --format ExtraHooks::Pretty features +else + cucumber --format ExtraHooks::Pretty features/step_definitions features/support ${*} +fi diff --git a/features/apt.feature b/features/apt.feature new file mode 100644 index 00000000..e86d3c60 --- /dev/null +++ b/features/apt.feature @@ -0,0 +1,34 @@ +@product +Feature: Installing packages through APT + As a Tails user + when I set an administration password in Tails Greeter + I should be able to install packages using APT and Synaptic + and all Internet traffic should flow only through Tor. + + Background: + Given a computer + And I capture all network traffic + And I start the computer + And the computer boots Tails + And I enable more Tails Greeter options + And I set sudo password "asdf" + And I log in to a new session + And GNOME has started + And Tor is ready + And all notifications have disappeared + And available upgrades have been checked + And I save the state so the background can be restored next scenario + + Scenario: APT sources are configured correctly + Then the only hosts in APT sources are "ftp.us.debian.org,security.debian.org,backports.debian.org,deb.tails.boum.org,deb.torproject.org,mozilla.debian.net" + + Scenario: Install packages using apt-get + When I update APT using apt-get + Then I should be able to install a package using apt-get + And all Internet traffic has only flowed through Tor + + Scenario: Install packages using Synaptic + When I start Synaptic + And I update APT using Synaptic + Then I should be able to install a package using Synaptic + And all Internet traffic has only flowed through Tor diff --git a/features/build.feature b/features/build.feature new file mode 100644 index 00000000..4cc0b650 --- /dev/null +++ b/features/build.feature @@ -0,0 +1,75 @@ +@source +Feature: custom APT sources to build branches + As a Tails developer, when I build Tails, I'd be happy if + the proper APT sources were automatically picked depending + on which Git branch I am working on. + + Scenario: build from an untagged stable branch + Given I am working on the stable branch + And last released version mentioned in debian/changelog is 1.0 + And Tails 1.0 has not been released yet + When I run tails-custom-apt-sources + Then I should see the 'stable' suite + Then I should not see the '1.0' suite + + Scenario: build from a tagged stable branch + Given Tails 0.10 has been released + And last released version mentioned in debian/changelog is 0.10 + And I am working on the stable branch + When I run tails-custom-apt-sources + Then I should see the '0.10' suite + + Scenario: build from a bugfix branch for a stable release + Given Tails 0.10 has been released + And last released version mentioned in debian/changelog is 0.10 + And I am working on the bugfix/disable_gdomap branch based on 0.10 + When I run tails-custom-apt-sources + Then I should see the '0.10' suite + And I should see the 'bugfix-disable-gdomap' suite + + Scenario: build from an untagged testing branch + Given I am working on the testing branch + And last released version mentioned in debian/changelog is 0.11 + And Tails 0.11 has not been released yet + When I run tails-custom-apt-sources + Then I should see the 'testing' suite + And I should not see the '0.11' suite + + Scenario: build from a tagged testing branch + Given I am working on the testing branch + And last released version mentioned in debian/changelog is 0.11 + And Tails 0.11 has been released + When I run tails-custom-apt-sources + Then I should see the '0.11' suite + And I should not see the 'testing' suite + + Scenario: build a release candidate from a tagged testing branch + Given I am working on the testing branch + And Tails 0.11 has been released + And last released version mentioned in debian/changelog is 0.12~rc1 + And Tails 0.12-rc1 has been tagged + When I run tails-custom-apt-sources + Then I should see the '0.12-rc1' suite + And I should not see the 'testing' suite + + Scenario: build from the devel branch + Given I am working on the devel branch + When I run tails-custom-apt-sources + Then I should see the 'devel' suite + + Scenario: build from the experimental branch + Given I am working on the experimental branch + When I run tails-custom-apt-sources + Then I should see the 'experimental' suite + + Scenario: build from a feature branch based on devel + Given I am working on the feature/icedove branch based on devel + When I run tails-custom-apt-sources + Then I should see the 'devel' suite + And I should see the 'feature-icedove' suite + + Scenario: build from a feature branch based on devel with dots in its name + Given I am working on the feature/live-boot-3.x branch based on devel + When I run tails-custom-apt-sources + Then I should see the 'devel' suite + And I should see the 'feature-live-boot-3.x' suite diff --git a/features/checks.feature b/features/checks.feature new file mode 100644 index 00000000..277bdb99 --- /dev/null +++ b/features/checks.feature @@ -0,0 +1,57 @@ +@product +Feature: Various checks + + Background: + Given a computer + And I start Tails from DVD with network unplugged and I login + And I save the state so the background can be restored next scenario + + Scenario: AppArmor is enabled and has enforced profiles + Then AppArmor is enabled + And some AppArmor profiles are enforced + + Scenario: VirtualBox guest modules are available + When Tails has booted a 64-bit kernel + Then the VirtualBox guest modules are available + + Scenario: The shipped Tails signing key is up-to-date + Given the network is plugged + And Tor is ready + And all notifications have disappeared + Then the shipped Tails signing key is not outdated + + Scenario: The live user is setup correctly + Then the live user has been setup by live-boot + And the live user is a member of only its own group and "audio cdrom dialout floppy video plugdev netdev fuse scanner lp lpadmin vboxsf" + And the live user owns its home dir and it has normal permissions + + Scenario: No initial network + Given I wait between 30 and 60 seconds + When the network is plugged + And Tor is ready + And all notifications have disappeared + And the time has synced + And process "vidalia" is running within 30 seconds + + Scenario: No unexpected network services + When the network is plugged + And Tor is ready + Then no unexpected services are listening for network connections + + Scenario: The emergency shutdown applet can shutdown Tails + When I request a shutdown using the emergency shutdown applet + Then Tails eventually shuts down + + Scenario: The emergency shutdown applet can reboot Tails + When I request a reboot using the emergency shutdown applet + Then Tails eventually restarts + + # We ditch the background snapshot for this scenario since we cannot + # add a filesystem share to a live VM so it would have to be in the + # background above. However, there's a bug that seems to make shares + # impossible to have after a snapshot restore. + Scenario: MAT can clean a PDF file + Given a computer + And I setup a filesystem share containing a sample PDF + And I start Tails from DVD with network unplugged and I login + Then MAT can clean some sample PDF file diff --git a/features/dhcp.feature b/features/dhcp.feature new file mode 100644 index 00000000..c15ae0c1 --- /dev/null +++ b/features/dhcp.feature @@ -0,0 +1,32 @@ +@product +Feature: Getting a DHCP lease without leaking too much information + As a Tails user + when I connect to a network with a DHCP server + I should be able to connect to the Internet + and the hostname should not have been leaked on the network. + + Scenario: Getting a DHCP lease with the default NetworkManager connection + Given a computer + And I capture all network traffic + And I start the computer + And the computer boots Tails + And I log in to a new session + And GNOME has started + And Tor is ready + And all notifications have disappeared + And available upgrades have been checked + Then the hostname should not have been leaked on the network + + Scenario: Getting a DHCP lease with a manually configured NetworkManager connection + Given a computer + And I capture all network traffic + And I start the computer + And the computer boots Tails + And I log in to a new session + And GNOME has started + And Tor is ready + And all notifications have disappeared + And available upgrades have been checked + And I add a wired DHCP NetworkManager connection called "manually-added-con" + And I switch to the "manually-added-con" NetworkManager connection + Then the hostname should not have been leaked on the network diff --git a/features/domains/default.xml b/features/domains/default.xml new file mode 100644 index 00000000..3d25576f --- /dev/null +++ b/features/domains/default.xml @@ -0,0 +1,64 @@ +<domain type='kvm' xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'> + <name>TailsToaster</name> + <memory unit='KiB'>1310720</memory> + <currentMemory unit='KiB'>1310720</currentMemory> + <vcpu>1</vcpu> + <os> + <type arch='x86_64' machine='pc-0.15'>hvm</type> + <boot dev='cdrom'/> + </os> + <features> + <acpi/> + <apic/> + <pae/> + </features> + <clock offset='utc'/> + <on_poweroff>destroy</on_poweroff> + <on_reboot>restart</on_reboot> + <on_crash>restart</on_crash> + <devices> + <emulator>/usr/bin/qemu-system-x86_64</emulator> + <disk type='file' device='cdrom'> + <driver name='qemu' type='raw'/> + <source file=''/> + <target dev='hdc' bus='ide'/> + <readonly/> + <address type='drive' controller='0' bus='1' target='0' unit='0'/> + </disk> + <controller type='usb' index='0' model='ich9-ehci1'> + <address type='pci' domain='0x0000' bus='0x00' slot='0x05' function='0x7'/> + </controller> + <controller type='usb' index='0' model='ich9-uhci1'> + <master startport='0'/> + <address type='pci' domain='0x0000' bus='0x00' slot='0x05' function='0x0' multifunction='on'/> + </controller> + <controller type='ide' index='0'> + <address type='pci' domain='0x0000' bus='0x00' slot='0x01' function='0x1'/> + </controller> + <interface type='network'> + <mac address='52:54:00:ac:dd:ee'/> + <source network='TailsToasterNet'/> + <model type='virtio'/> + <address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x0'/> + <link state='up'/> + </interface> + <serial type='tcp'> + <source mode="bind" host='127.0.0.1' service='1337'/> + <target port='0'/> + </serial> + <input type='tablet' bus='usb'/> + <input type='mouse' bus='ps2'/> + <graphics type='vnc' port='-1' autoport='yes'/> + <sound model='ich6'> + <address type='pci' domain='0x0000' bus='0x00' slot='0x04' function='0x0'/> + </sound> + <video> + <model type='qxl' vram='9216' heads='1'/> + <address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x0'/> + </video> + <memballoon model='virtio'> + <address type='pci' domain='0x0000' bus='0x00' slot='0x06' function='0x0'/> + </memballoon> + </devices> +</domain> + diff --git a/features/domains/default_net.xml b/features/domains/default_net.xml new file mode 100644 index 00000000..d37935b8 --- /dev/null +++ b/features/domains/default_net.xml @@ -0,0 +1,13 @@ +<network> + <name>TailsToasterNet</name> + <forward mode='nat'/> + <bridge name='virbr10' stp='on' delay='0' /> + <ip address='10.2.1.1' netmask='255.255.255.0'> + <dhcp> + <range start='10.2.1.2' end='10.2.1.254' /> + <host mac="52:54:00:ac:dd:ee" name="amnesia" ip="10.2.1.2" /> + </dhcp> + </ip> + <ip family="ipv6" address="fc00::1" prefix="7" /> +</network> + diff --git a/features/domains/disk.xml b/features/domains/disk.xml new file mode 100644 index 00000000..8193fea3 --- /dev/null +++ b/features/domains/disk.xml @@ -0,0 +1,5 @@ +<disk type='file' device='disk'> + <driver name='qemu' type=''/> + <source file=''/> + <target dev='' bus=''/> +</disk> diff --git a/features/domains/fs_share.xml b/features/domains/fs_share.xml new file mode 100644 index 00000000..718755ea --- /dev/null +++ b/features/domains/fs_share.xml @@ -0,0 +1,6 @@ +<filesystem type='mount' accessmode='passthrough'> + <driver type='path' wrpolicy='immediate'/> + <source dir=''/> + <target dir=''/> + <readonly/> +</filesystem> diff --git a/features/domains/storage_pool.xml b/features/domains/storage_pool.xml new file mode 100644 index 00000000..3e12f8b6 --- /dev/null +++ b/features/domains/storage_pool.xml @@ -0,0 +1,6 @@ +<pool type="dir"> + <name>TailsToasterStorage</name> + <target> + <path></path> + </target> +</pool> diff --git a/features/domains/volume.xml b/features/domains/volume.xml new file mode 100644 index 00000000..9159c268 --- /dev/null +++ b/features/domains/volume.xml @@ -0,0 +1,14 @@ +<volume> + <name></name> + <allocation>0</allocation> + <capacity unit="b"></capacity> + <target> + <path></path> + <format type='qcow2'/> + <permissions> + <owner></owner> + <group></group> + <mode>0774</mode> + </permissions> + </target> +</volume> diff --git a/features/encryption.feature b/features/encryption.feature new file mode 100644 index 00000000..2f30d2ad --- /dev/null +++ b/features/encryption.feature @@ -0,0 +1,31 @@ +@product +Feature: Encryption and verification using GnuPG + As a Tails user + I want to be able to easily encrypt and sign messages using GnuPG + And decrypt and verify GnuPG blocks + + Background: + Given a computer + And I start Tails from DVD with network unplugged and I login + And I generate an OpenPGP key named "test" with password "asdf" + And I save the state so the background can be restored next scenario + + Scenario: Encryption and decryption using Tails OpenPGP Applet + When I type a message into gedit + And I encrypt the message using my OpenPGP key + Then I can decrypt the encrypted message + + Scenario: Signing and verification using Tails OpenPGP Applet + When I type a message into gedit + And I sign the message using my OpenPGP key + Then I can verify the message's signature + + Scenario: Encryption/signing and decryption/verification using Tails OpenPGP Applet + When I type a message into gedit + And I both encrypt and sign the message using my OpenPGP key + Then I can decrypt and verify the encrypted message + + Scenario: Symmetric encryption and decryption using Tails OpenPGP Applet + When I type a message into gedit + And I symmetrically encrypt the message with password "asdf" + Then I can decrypt the encrypted message diff --git a/features/erase_memory.feature b/features/erase_memory.feature new file mode 100644 index 00000000..56d3a402 --- /dev/null +++ b/features/erase_memory.feature @@ -0,0 +1,61 @@ +@product +Feature: System memory erasure on shutdown + As a Tails user + when I shutdown Tails + I want the system memory to be free from sensitive data. + + Scenario: Anti-test: no memory erasure on a modern computer + Given a computer + And the computer is a modern 64-bit system + And the computer has 8 GiB of RAM + And I set Tails to boot with options "debug=wipemem" + And I start Tails from DVD with network unplugged and I login + Then the PAE kernel is running + And at least 8 GiB of RAM was detected + And process "memlockd" is running + And process "udev-watchdog" is running + When I fill the guest's memory with a known pattern without verifying + And I reboot without wiping the memory + Then I find many patterns in the guest's memory + + Scenario: Memory erasure on a modern computer + Given a computer + And the computer is a modern 64-bit system + And the computer has 8 GiB of RAM + And I set Tails to boot with options "debug=wipemem" + And I start Tails from DVD with network unplugged and I login + Then the PAE kernel is running + And at least 8 GiB of RAM was detected + And process "memlockd" is running + And process "udev-watchdog" is running + When I fill the guest's memory with a known pattern + And I shutdown and wait for Tails to finish wiping the memory + Then I find very few patterns in the guest's memory + + Scenario: Anti-test: no memory erasure on an old computer + Given a computer + And the computer is an old pentium without the PAE extension + And the computer has 8 GiB of RAM + And I set Tails to boot with options "debug=wipemem" + And I start Tails from DVD with network unplugged and I login + Then the non-PAE kernel is running + And at least 3500 MiB of RAM was detected + And process "memlockd" is running + And process "udev-watchdog" is running + When I fill the guest's memory with a known pattern without verifying + And I reboot without wiping the memory + Then I find many patterns in the guest's memory + + Scenario: Memory erasure on an old computer + Given a computer + And the computer is an old pentium without the PAE extension + And the computer has 8 GiB of RAM + And I set Tails to boot with options "debug=wipemem" + And I start Tails from DVD with network unplugged and I login + And the non-PAE kernel is running + And at least 3500 MiB of RAM was detected + And process "memlockd" is running + And process "udev-watchdog" is running + When I fill the guest's memory with a known pattern + And I shutdown and wait for Tails to finish wiping the memory + Then I find very few patterns in the guest's memory diff --git a/features/evince.feature b/features/evince.feature new file mode 100644 index 00000000..fe687f3a --- /dev/null +++ b/features/evince.feature @@ -0,0 +1,53 @@ +@product +Feature: Using Evince + As a Tails user + I want to view and print PDF files in Evince + And AppArmor should prevent Evince from doing dangerous things + + Background: + Given a computer + And I start Tails from DVD with network unplugged and I login + And I save the state so the background can be restored next scenario + + Scenario: I can view and print a PDF file stored in /usr/share + When I open "/usr/share/cups/data/default-testpage.pdf" with Evince + Then I see "CupsTestPage.png" after at most 10 seconds + And I can print the current document to "/home/amnesia/output.pdf" + + Scenario: I can view and print a PDF file stored in non-persistent /home/amnesia + Given I copy "/usr/share/cups/data/default-testpage.pdf" to "/home/amnesia" as user "amnesia" + When I open "/home/amnesia/default-testpage.pdf" with Evince + Then I see "CupsTestPage.png" after at most 10 seconds + And I can print the current document to "/home/amnesia/output.pdf" + + Scenario: I cannot view a PDF file stored in non-persistent /home/amnesia/.gnupg + Given I copy "/usr/share/cups/data/default-testpage.pdf" to "/home/amnesia/.gnupg" as user "amnesia" + When I try to open "/home/amnesia/.gnupg/default-testpage.pdf" with Evince + Then I see "EvinceUnableToOpen.png" after at most 10 seconds + + @keep_volumes + Scenario: Installing Tails on a USB drive, creating a persistent partition, copying PDF files to it + Given the USB drive "current" contains Tails with persistence configured and password "asdf" + And a computer + And I start Tails from USB drive "current" with network unplugged and I login with persistence password "asdf" + And I copy "/usr/share/cups/data/default-testpage.pdf" to "/home/amnesia/Persistent" as user "amnesia" + Then the file "/home/amnesia/Persistent/default-testpage.pdf" exists + And I copy "/usr/share/cups/data/default-testpage.pdf" to "/home/amnesia/.gnupg" as user "amnesia" + Then the file "/home/amnesia/.gnupg/default-testpage.pdf" exists + And I shutdown Tails and wait for the computer to power off + + @keep_volumes + Scenario: I can view and print a PDF file stored in persistent /home/amnesia/Persistent + Given a computer + And I start Tails from USB drive "current" with network unplugged and I login with persistence password "asdf" + When I open "/home/amnesia/Persistent/default-testpage.pdf" with Evince + Then I see "CupsTestPage.png" after at most 10 seconds + And I can print the current document to "/home/amnesia/Persistent/output.pdf" + + @keep_volumes + Scenario: I cannot view a PDF file stored in persistent /home/amnesia/.gnupg + Given a computer + When I start Tails from USB drive "current" with network unplugged and I login with persistence password "asdf" + And I try to open "/home/amnesia/.gnupg/default-testpage.pdf" with Evince + Then I see "EvinceUnableToOpen.png" after at most 10 seconds + diff --git a/features/firewall_leaks.feature b/features/firewall_leaks.feature new file mode 100644 index 00000000..775c6e13 --- /dev/null +++ b/features/firewall_leaks.feature @@ -0,0 +1,37 @@ +@product +Feature: + As a Tails developer + I want to ensure that the automated test suite detects firewall leaks reliably + + Background: + Given a computer + And I capture all network traffic + And I start the computer + And the computer boots Tails + And I log in to a new session + And Tor is ready + And all notifications have disappeared + And available upgrades have been checked + And all Internet traffic has only flowed through Tor + And I save the state so the background can be restored next scenario + + Scenario: Detecting IPv4 TCP leaks from the Unsafe Browser + When I successfully start the Unsafe Browser + And I open the address "https://check.torproject.org" in the Unsafe Browser + And I see "UnsafeBrowserTorCheckFail.png" after at most 60 seconds + Then the firewall leak detector has detected IPv4 TCP leaks + + Scenario: Detecting IPv4 TCP leaks of TCP DNS lookups + Given I disable Tails' firewall + When I do a TCP DNS lookup of "torproject.org" + Then the firewall leak detector has detected IPv4 TCP leaks + + Scenario: Detecting IPv4 non-TCP leaks (UDP) of UDP DNS lookups + Given I disable Tails' firewall + When I do a UDP DNS lookup of "torproject.org" + Then the firewall leak detector has detected IPv4 non-TCP leaks + + Scenario: Detecting IPv4 non-TCP (ICMP) leaks of ping + Given I disable Tails' firewall + When I send some ICMP pings + Then the firewall leak detector has detected IPv4 non-TCP leaks diff --git a/features/i2p.feature b/features/i2p.feature new file mode 100644 index 00000000..fc4cdf01 --- /dev/null +++ b/features/i2p.feature @@ -0,0 +1,33 @@ +@product +Feature: I2P + As a Tails user + I *might* want to use I2P + + Scenario: I2P is disabled by default + Given a computer + And I start the computer + And the computer boots Tails + And I log in to a new session + And GNOME has started + And Tor is ready + And all notifications have disappeared + Then the I2P Browser desktop file is not present + And the I2P Browser sudo rules are not present + And the I2P firewall rules are disabled + + Scenario: I2P is enabled when the "i2p" boot parameter is added + Given a computer + And I set Tails to boot with options "i2p" + And I start the computer + And the computer boots Tails + And I log in to a new session + And GNOME has started + And Tor is ready + And I2P is running + And the I2P router console is ready + And all notifications have disappeared + Then the I2P Browser desktop file is present + And the I2P Browser sudo rules are enabled + And the I2P firewall rules are enabled + When I start the I2P Browser through the GNOME menu + Then I see "I2P_router_console.png" after at most 60 seconds diff --git a/features/misc_files/sample.pdf b/features/misc_files/sample.pdf Binary files differnew file mode 100644 index 00000000..d0cc9502 --- /dev/null +++ b/features/misc_files/sample.pdf diff --git a/features/misc_files/sample.tex b/features/misc_files/sample.tex new file mode 100644 index 00000000..043faaec --- /dev/null +++ b/features/misc_files/sample.tex @@ -0,0 +1,8 @@ +\documentclass[12pt]{article} +\title{Sample PDF document} +\author{Tails developers} +\date{March 12, 2013} +\begin{document} +\maketitle +Does this PDF still have metadata? +\end{document} diff --git a/features/pidgin.feature b/features/pidgin.feature new file mode 100644 index 00000000..51d4a776 --- /dev/null +++ b/features/pidgin.feature @@ -0,0 +1,71 @@ +@product +Feature: Chatting anonymously using Pidgin + As a Tails user + when I chat using Pidgin + I should be able to use OTR + And I should be able to persist my Pidgin configuration + And AppArmor should prevent Pidgin from doing dangerous things + And all Internet traffic should flow only through Tor + + Background: + Given a computer + And I capture all network traffic + When I start Tails from DVD and I login + Then Pidgin has the expected accounts configured with random nicknames + And I save the state so the background can be restored next scenario + + Scenario: Connecting to the #tails IRC channel with the pre-configured account + When I start Pidgin through the GNOME menu + Then I see Pidgin's account manager window + When I activate the "irc.oftc.net" Pidgin account + And I close Pidgin's account manager window + Then Pidgin successfully connects to the "irc.oftc.net" account + And I can join the "#tails" channel on "irc.oftc.net" + And all Internet traffic has only flowed through Tor + + Scenario: Adding a certificate to Pidgin + And I start Pidgin through the GNOME menu + And I see Pidgin's account manager window + And I close Pidgin's account manager window + Then I can add a certificate from the "/home/amnesia" directory to Pidgin + + Scenario: Failing to add a certificate to Pidgin + And I start Pidgin through the GNOME menu + And I see Pidgin's account manager window + And I close Pidgin's account manager window + Then I cannot add a certificate from the "/home/amnesia/.gnupg" directory to Pidgin + + @keep_volumes + Scenario: Using a persistent Pidgin configuration + Given the USB drive "current" contains Tails with persistence configured and password "asdf" + And a computer + And I start Tails from USB drive "current" and I login with persistence password "asdf" + When I start Pidgin through the GNOME menu + Then I see Pidgin's account manager window + # And I generate an OTR key for the default Pidgin account + And I take note of the configured Pidgin accounts + # And I take note of the OTR key for Pidgin's "irc.oftc.net" account + And I shutdown Tails and wait for the computer to power off + Given a computer + And I capture all network traffic + And I start Tails from USB drive "current" and I login with persistence password "asdf" + And Pidgin has the expected persistent accounts configured + # And Pidgin has the expected persistent OTR keys + When I start Pidgin through the GNOME menu + Then I see Pidgin's account manager window + When I activate the "irc.oftc.net" Pidgin account + And I close Pidgin's account manager window + Then Pidgin successfully connects to the "irc.oftc.net" account + And I can join the "#tails" channel on "irc.oftc.net" + And all Internet traffic has only flowed through Tor + # Exercise Pidgin AppArmor profile with persistence enabled. + # This should really be in dedicated scenarios, but it would be + # too costly to set up the virtual USB drive with persistence more + # than once in this feature. + And I cannot add a certificate from the "/home/amnesia/.gnupg" directory to Pidgin + When I close Pidgin's certificate import failure dialog + And I close Pidgin's certificate manager + Then I cannot add a certificate from the "/live/persistence/TailsData_unlocked/gnupg" directory to Pidgin + When I close Pidgin's certificate import failure dialog + And I close Pidgin's certificate manager + Then I can add a certificate from the "/home/amnesia" directory to Pidgin diff --git a/features/root_access_control.feature b/features/root_access_control.feature new file mode 100644 index 00000000..9aa45de8 --- /dev/null +++ b/features/root_access_control.feature @@ -0,0 +1,44 @@ +@product +Feature: Root access control enforcement + As a Tails user + when I set an administration password in Tails Greeter + I can use the password for attaining administrative privileges. + But when I do not set an administration password + I should not be able to attain administration privileges at all. + + Background: + Given a computer + And the network is unplugged + And I start the computer + And the computer boots Tails + And I save the state so the background can be restored next scenario + + Scenario: If an administrative password is set in Tails Greeter the live user should be able to run arbitrary commands with administrative privileges. + Given I enable more Tails Greeter options + And I set sudo password "asdf" + And I log in to a new session + And Tails Greeter has dealt with the sudo password + Then I should be able to run administration commands as the live user + + Scenario: If no administrative password is set in Tails Greeter the live user should not be able to run arbitrary commands administrative privileges. + Given I log in to a new session + And Tails Greeter has dealt with the sudo password + Then I should not be able to run administration commands as the live user with the "" password + And I should not be able to run administration commands as the live user with the "amnesia" password + And I should not be able to run administration commands as the live user with the "live" password + + Scenario: If an administrative password is set in Tails Greeter the live user should be able to get administrative privileges through PolicyKit + Given I enable more Tails Greeter options + And I set sudo password "asdf" + And I log in to a new session + And Tails Greeter has dealt with the sudo password + And GNOME has started + And running a command as root with pkexec requires PolicyKit administrator privileges + Then I should be able to run a command as root with pkexec + + Scenario: If no administrative password is set in Tails Greeter the live user should not be able to get administrative privileges through PolicyKit with the standard passwords. + Given I log in to a new session + And Tails Greeter has dealt with the sudo password + And GNOME has started + And running a command as root with pkexec requires PolicyKit administrator privileges + Then I should not be able to run a command as root with pkexec and the standard passwords diff --git a/features/step_definitions/apt.rb b/features/step_definitions/apt.rb new file mode 100644 index 00000000..a5492054 --- /dev/null +++ b/features/step_definitions/apt.rb @@ -0,0 +1,80 @@ +require 'uri' + +Given /^the only hosts in APT sources are "([^"]*)"$/ do |hosts_str| + next if @skip_steps_while_restoring_background + hosts = hosts_str.split(',') + @vm.file_content("/etc/apt/sources.list /etc/apt/sources.list.d/*").chomp.each_line { |line| + next if ! line.start_with? "deb" + source_host = URI(line.split[1]).host + if !hosts.include?(source_host) + raise "Bad APT source '#{line}'" + end + } +end + +When /^I update APT using apt-get$/ do + next if @skip_steps_while_restoring_background + Timeout::timeout(30*60) do + cmd = @vm.execute("echo #{@sudo_password} | " + + "sudo -S apt-get update", $live_user) + if !cmd.success? + STDERR.puts cmd.stderr + end + end +end + +Then /^I should be able to install a package using apt-get$/ do + next if @skip_steps_while_restoring_background + package = "cowsay" + Timeout::timeout(120) do + cmd = @vm.execute("echo #{@sudo_password} | " + + "sudo -S apt-get install #{package}", $live_user) + if !cmd.success? + STDERR.puts cmd.stderr + end + end + step "package \"#{package}\" is installed" +end + +When /^I update APT using Synaptic$/ do + next if @skip_steps_while_restoring_background + # Upon start the interface will be frozen while Synaptic loads the + # package list. Since the frozen GUI is so similar to the unfrozen + # one there's no easy way to reliably wait for the latter. Hence we + # spam reload until it's performed, which is easier to detect. + try_for(60, :msg => "Failed to reload the package list in Synaptic") { + @screen.type("r", Sikuli::KeyModifier.CTRL) + @screen.find('SynapticReloadPrompt.png') + } + @screen.waitVanish('SynapticReloadPrompt.png', 30*60) +end + +Then /^I should be able to install a package using Synaptic$/ do + next if @skip_steps_while_restoring_background + package = "cowsay" + # We do this after a Reload, so the interface will be frozen until + # the package list has been loaded + try_for(60, :msg => "Failed to open the Synaptic 'Find' window") { + @screen.type("f", Sikuli::KeyModifier.CTRL) # Find key + @screen.find('SynapticSearch.png') + } + @screen.type(package + Sikuli::Key.ENTER) + @screen.wait_and_click('SynapticCowsaySearchResult.png', 20) + sleep 5 + @screen.type("i", Sikuli::KeyModifier.CTRL) # Mark for installation + sleep 5 + @screen.type("p", Sikuli::KeyModifier.CTRL) # Apply + @screen.wait('SynapticApplyPrompt.png', 60) + @screen.type("a", Sikuli::KeyModifier.ALT) # Verify apply + @screen.wait('SynapticChangesAppliedPrompt.png', 120) + step "package \"#{package}\" is installed" +end + +When /^I start Synaptic$/ do + next if @skip_steps_while_restoring_background + @screen.wait_and_click("GnomeApplicationsMenu.png", 10) + @screen.wait_and_click("GnomeApplicationsSystem.png", 10) + @screen.wait_and_click("GnomeApplicationsAdministration.png", 10) + @screen.wait_and_click("GnomeApplicationsSynaptic.png", 20) + deal_with_polkit_prompt('SynapticPolicyKitAuthPrompt.png', @sudo_password) +end diff --git a/features/step_definitions/build.rb b/features/step_definitions/build.rb new file mode 100644 index 00000000..2e597a4d --- /dev/null +++ b/features/step_definitions/build.rb @@ -0,0 +1,71 @@ +Given /^Tails ([[:alnum:].]+) has been released$/ do |version| + create_git unless git_exists? + + fatal_system "git checkout --quiet stable" + old_entries = File.open('debian/changelog') { |f| f.read } + File.open('debian/changelog', 'w') do |changelog| + changelog.write(<<END_OF_CHANGELOG) +tails (#{version}) stable; urgency=low + + * New upstream release. + + -- Tails developers <tails@boum.org> Tue, 31 Jan 2012 15:12:57 +0100 + +#{old_entries} +END_OF_CHANGELOG + end + fatal_system "git commit --quiet debian/changelog -m 'Release #{version}'" + fatal_system "git tag '#{version}'" +end + +Given /^Tails ([[:alnum:].-]+) has been tagged$/ do |version| + fatal_system "git tag '#{version}'" +end + +Given /^Tails ([[:alnum:].]+) has not been released yet$/ do |version| + !File.exists? ".git/refs/tags/#{version}" +end + +Given /^last released version mentioned in debian\/changelog is ([[:alnum:]~.]+)$/ do |version| + last = `dpkg-parsechangelog | awk '/^Version: / { print $2 }'`.strip + raise StandardError.new('dpkg-parsechangelog failed.') if $? != 0 + + if last != version + fatal_system "debchange -v '#{version}' 'New upstream release'" + end +end + +Given %r{I am working on the ([[:alnum:]./_-]+) branch$} do |branch| + create_git unless git_exists? + + current_branch = `git branch | awk '/^\*/ { print $2 }'`.strip + raise StandardError.new('git-branch failed.') if $? != 0 + + if current_branch != branch + fatal_system "git checkout --quiet '#{branch}'" + end +end + +Given %r{I am working on the ([[:alnum:]./_-]+) branch based on ([[:alnum:]./_-]+)$} do |branch, base| + create_git unless git_exists? + + current_branch = `git branch | awk '/^\*/ { print $2 }'`.strip + raise StandardError.new('git-branch failed.') if $? != 0 + + if current_branch != branch + fatal_system "git checkout --quiet -b '#{branch}' '#{base}'" + end +end + +When /^I run ([[:alnum:]-]+)$/ do |command| + @output = `#{File.expand_path("../../../auto/scripts/#{command}", __FILE__)}` + raise StandardError.new("#{command} failed. Exit code: #{$?}") if $? != 0 +end + +Then /^I should see the ['"]?([[:alnum:].-]+)['"]? suite$/ do |suite| + @output.should have_suite(suite) +end + +Then /^I should not see the ['"]?([[:alnum:].-]+)['"]? suite$/ do |suite| + @output.should_not have_suite(suite) +end diff --git a/features/step_definitions/checks.rb b/features/step_definitions/checks.rb new file mode 100644 index 00000000..76cfe670 --- /dev/null +++ b/features/step_definitions/checks.rb @@ -0,0 +1,143 @@ +Then /^the shipped Tails signing key is not outdated$/ do + # "old" here is w.r.t. the one we fetch from Tails' website + next if @skip_steps_while_restoring_background + sig_key_fingerprint = "0D24B36AA9A2A651787876451202821CBE2CD9C1" + fresh_sig_key = "/tmp/tails-signing.key" + tmp_keyring = "/tmp/tmp-keyring.gpg" + key_url = "https://tails.boum.org/tails-signing.key" + @vm.execute("curl --silent --socks5-hostname localhost:9062 " + + "#{key_url} -o #{fresh_sig_key}", $live_user) + @vm.execute("gpg --batch --no-default-keyring --keyring #{tmp_keyring} " + + "--import #{fresh_sig_key}", $live_user) + fresh_sig_key_info = + @vm.execute("gpg --batch --no-default-keyring --keyring #{tmp_keyring} " + + "--list-key #{sig_key_fingerprint}", $live_user).stdout + shipped_sig_key_info = @vm.execute("gpg --batch --list-key #{sig_key_fingerprint}", + $live_user).stdout + assert_equal(fresh_sig_key_info, shipped_sig_key_info, + "The Tails signing key shipped inside Tails is outdated:\n" + + "Shipped key:\n" + + shipped_sig_key_info + + "Newly fetched key from #{key_url}:\n" + + fresh_sig_key_info) +end + +Then /^the live user has been setup by live\-boot$/ do + next if @skip_steps_while_restoring_background + assert(@vm.execute("test -e /var/lib/live/config/user-setup").success?, + "live-boot failed its user-setup") + actual_username = @vm.execute(". /etc/live/config/username.conf; " + + "echo $LIVE_USERNAME").stdout.chomp + assert_equal($live_user, actual_username) +end + +Then /^the live user is a member of only its own group and "(.*?)"$/ do |groups| + next if @skip_steps_while_restoring_background + expected_groups = groups.split(" ") << $live_user + actual_groups = @vm.execute("groups #{$live_user}").stdout.chomp.sub(/^#{$live_user} : /, "").split(" ") + unexpected = actual_groups - expected_groups + missing = expected_groups - actual_groups + assert_equal(0, unexpected.size, + "live user in unexpected groups #{unexpected}") + assert_equal(0, missing.size, + "live user not in expected groups #{missing}") +end + +Then /^the live user owns its home dir and it has normal permissions$/ do + next if @skip_steps_while_restoring_background + home = "/home/#{$live_user}" + assert(@vm.execute("test -d #{home}").success?, + "The live user's home doesn't exist or is not a directory") + owner = @vm.execute("stat -c %U:%G #{home}").stdout.chomp + perms = @vm.execute("stat -c %a #{home}").stdout.chomp + assert_equal("#{$live_user}:#{$live_user}", owner) + assert_equal("700", perms) +end + +Given /^I wait between (\d+) and (\d+) seconds$/ do |min, max| + next if @skip_steps_while_restoring_background + time = rand(max.to_i - min.to_i + 1) + min.to_i + puts "Slept for #{time} seconds" + sleep(time) +end + +Then /^no unexpected services are listening for network connections$/ do + next if @skip_steps_while_restoring_background + netstat_cmd = @vm.execute("netstat -ltupn") + assert netstat_cmd.success? + for line in netstat_cmd.stdout.chomp.split("\n") do + splitted = line.split(/[[:blank:]]+/) + proto = splitted[0] + if proto == "tcp" + proc_index = 6 + elsif proto == "udp" + proc_index = 5 + else + next + end + laddr, lport = splitted[3].split(":") + proc = splitted[proc_index].split("/")[1] + # Services listening on loopback is not a threat + if /127(\.[[:digit:]]{1,3}){3}/.match(laddr).nil? + if $services_expected_on_all_ifaces.include? [proc, laddr, lport] or + $services_expected_on_all_ifaces.include? [proc, laddr, "*"] + puts "Service '#{proc}' is listening on #{laddr}:#{lport} " + + "but has an exception" + else + raise "Unexpected service '#{proc}' listening on #{laddr}:#{lport}" + end + end + end +end + +When /^Tails has booted a 64-bit kernel$/ do + next if @skip_steps_while_restoring_background + assert(@vm.execute("uname -r | grep -qs 'amd64$'").success?, + "Tails has not booted a 64-bit kernel.") +end + +Then /^the VirtualBox guest modules are available$/ do + next if @skip_steps_while_restoring_background + assert(@vm.execute("modinfo vboxguest").success?, + "The vboxguest module is not available.") +end + +def shared_pdf_dir_on_guest + "/tmp/shared_pdf_dir" +end + +Given /^I setup a filesystem share containing a sample PDF$/ do + next if @skip_steps_while_restoring_background + @vm.add_share($misc_files_dir, shared_pdf_dir_on_guest) +end + +Then /^MAT can clean some sample PDF file$/ do + next if @skip_steps_while_restoring_background + for pdf_on_host in Dir.glob("#{$misc_files_dir}/*.pdf") do + pdf_name = File.basename(pdf_on_host) + pdf_on_guest = "/home/#{$live_user}/#{pdf_name}" + step "I copy \"#{shared_pdf_dir_on_guest}/#{pdf_name}\" to \"#{pdf_on_guest}\" as user \"#{$live_user}\"" + @vm.execute("mat --display '#{pdf_on_guest}'", + $live_user).stdout + check_before = @vm.execute("mat --check '#{pdf_on_guest}'", + $live_user).stdout + if check_before.include?("#{pdf_on_guest} is clean") + STDERR.puts "warning: '#{pdf_on_host}' is already clean so it is a " + + "bad candidate for testing MAT" + end + @vm.execute("mat '#{pdf_on_guest}'", $live_user) + check_after = @vm.execute("mat --check '#{pdf_on_guest}'", + $live_user).stdout + assert(check_after.include?("#{pdf_on_guest} is clean"), + "MAT failed to clean '#{pdf_on_host}'") + end +end + +Then /^AppArmor is enabled$/ do + assert(@vm.execute("aa-status").success?, "AppArmor is not enabled") +end + +Then /^some AppArmor profiles are enforced$/ do + assert(@vm.execute("aa-status --enforced").stdout.chomp.to_i > 0, + "No AppArmor profile is enforced") +end diff --git a/features/step_definitions/common_steps.rb b/features/step_definitions/common_steps.rb new file mode 100644 index 00000000..532aa4fd --- /dev/null +++ b/features/step_definitions/common_steps.rb @@ -0,0 +1,687 @@ +require 'fileutils' + +def post_vm_start_hook + # Sometimes the first click is lost (presumably it's used to give + # focus to virt-viewer or similar) so we do that now rather than + # having an important click lost. The point we click should be + # somewhere where no clickable elements generally reside. + @screen.click_point(@screen.w, @screen.h/2) +end + +def activate_filesystem_shares + # XXX-9p: First of all, filesystem shares cannot be mounted while we + # do a snapshot save+restore, so unmounting+remounting them seems + # like a good idea. However, the 9p modules get into a broken state + # during the save+restore, so we also would like to unload+reload + # them, but loading of 9pnet_virtio fails after a restore with + # "probe of virtio2 failed with error -2" (in dmesg) which makes the + # shares unavailable. Hence we leave this code commented for now. + #for mod in ["9pnet_virtio", "9p"] do + # @vm.execute("modprobe #{mod}") + #end + + @vm.list_shares.each do |share| + @vm.execute("mkdir -p #{share}") + @vm.execute("mount -t 9p -o trans=virtio #{share} #{share}") + end +end + +def deactivate_filesystem_shares + @vm.list_shares.each do |share| + @vm.execute("umount #{share}") + end + + # XXX-9p: See XXX-9p above + #for mod in ["9p", "9pnet_virtio"] do + # @vm.execute("modprobe -r #{mod}") + #end +end + +def restore_background + @vm.restore_snapshot($background_snapshot) + @vm.wait_until_remote_shell_is_up + post_vm_start_hook + + # XXX-9p: See XXX-9p above + #activate_filesystem_shares + + # The guest's Tor's circuits' states are likely to get out of sync + # with the other relays, so we ensure that we have fresh circuits. + # Time jumps and incorrect clocks also confuses Tor in many ways. + if @vm.has_network? + if @vm.execute("service tor status").success? + @vm.execute("service tor stop") + @vm.execute("rm -f /var/log/tor/log") + @vm.execute("killall vidalia") + @vm.host_to_guest_time_sync + @vm.execute("service tor start") + wait_until_tor_is_working + @vm.spawn("/usr/local/sbin/restart-vidalia") + end + end +end + +Given /^a computer$/ do + @vm.destroy if @vm + @vm = VM.new($vm_xml_path, $x_display) +end + +Given /^the computer has (\d+) ([[:alpha:]]+) of RAM$/ do |size, unit| + next if @skip_steps_while_restoring_background + @vm.set_ram_size(size, unit) +end + +Given /^the computer is set to boot from the Tails DVD$/ do + next if @skip_steps_while_restoring_background + @vm.set_cdrom_boot($tails_iso) +end + +Given /^the computer is set to boot from (.+?) drive "(.+?)"$/ do |type, name| + next if @skip_steps_while_restoring_background + @vm.set_disk_boot(name, type.downcase) +end + +Given /^I plug ([[:alpha:]]+) drive "([^"]+)"$/ do |bus, name| + next if @skip_steps_while_restoring_background + @vm.plug_drive(name, bus.downcase) + if @vm.is_running? + step "drive \"#{name}\" is detected by Tails" + end +end + +Then /^drive "([^"]+)" is detected by Tails$/ do |name| + next if @skip_steps_while_restoring_background + if @vm.is_running? + try_for(10, :msg => "Drive '#{name}' is not detected by Tails") { + @vm.disk_detected?(name) + } + else + STDERR.puts "Cannot tell if drive '#{name}' is detected by Tails: " + + "Tails is not running" + end +end + +Given /^the network is plugged$/ do + next if @skip_steps_while_restoring_background + @vm.plug_network +end + +Given /^the network is unplugged$/ do + next if @skip_steps_while_restoring_background + @vm.unplug_network +end + +Given /^I capture all network traffic$/ do + # Note: We don't want skip this particular stpe if + # @skip_steps_while_restoring_background is set since it starts + # something external to the VM state. + @sniffer = Sniffer.new("TestSniffer", @vm.net.bridge_name) + @sniffer.capture +end + +Given /^I set Tails to boot with options "([^"]*)"$/ do |options| + next if @skip_steps_while_restoring_background + @boot_options = options +end + +When /^I start the computer$/ do + next if @skip_steps_while_restoring_background + assert(!@vm.is_running?, + "Trying to start a VM that is already running") + @vm.start + post_vm_start_hook +end + +Given /^I start Tails from DVD(| with network unplugged) and I login$/ do |network_unplugged| + # we don't @skip_steps_while_restoring_background as we're only running + # other steps, that are taking care of it *if* they have to + step "the computer is set to boot from the Tails DVD" + if network_unplugged.empty? + step "the network is plugged" + else + step "the network is unplugged" + end + step "I start the computer" + step "the computer boots Tails" + step "I log in to a new session" + step "Tails seems to have booted normally" + if network_unplugged.empty? + step "Tor is ready" + step "all notifications have disappeared" + step "available upgrades have been checked" + else + step "all notifications have disappeared" + end +end + +Given /^I start Tails from (.+?) drive "(.+?)"(| with network unplugged) and I login(| with(| read-only) persistence password "([^"]+)")$/ do |drive_type, drive_name, network_unplugged, persistence_on, persistence_ro, persistence_pwd| + # we don't @skip_steps_while_restoring_background as we're only running + # other steps, that are taking care of it *if* they have to + step "the computer is set to boot from #{drive_type} drive \"#{drive_name}\"" + if network_unplugged.empty? + step "the network is plugged" + else + step "the network is unplugged" + end + step "I start the computer" + step "the computer boots Tails" + if ! persistence_on.empty? + assert(! persistence_pwd.empty?, "A password must be provided when enabling persistence") + if persistence_ro.empty? + step "I enable persistence with password \"#{persistence_pwd}\"" + else + step "I enable read-only persistence with password \"#{persistence_pwd}\"" + end + end + step "I log in to a new session" + step "Tails seems to have booted normally" + if network_unplugged.empty? + step "Tor is ready" + step "all notifications have disappeared" + step "available upgrades have been checked" + else + step "all notifications have disappeared" + end +end + +When /^I power off the computer$/ do + next if @skip_steps_while_restoring_background + assert(@vm.is_running?, + "Trying to power off an already powered off VM") + @vm.power_off +end + +When /^I cold reboot the computer$/ do + next if @skip_steps_while_restoring_background + step "I power off the computer" + step "I start the computer" +end + +When /^I destroy the computer$/ do + next if @skip_steps_while_restoring_background + @vm.destroy +end + +Given /^the computer (re)?boots Tails$/ do |reboot| + next if @skip_steps_while_restoring_background + + case @os_loader + when "UEFI" + assert(!reboot, "Testing of reboot with UEFI enabled is not implemented") + bootsplash = 'TailsBootSplashUEFI.png' + bootsplash_tab_msg = 'TailsBootSplashTabMsgUEFI.png' + boot_timeout = 30 + else + if reboot + bootsplash = 'TailsBootSplashPostReset.png' + bootsplash_tab_msg = 'TailsBootSplashTabMsgPostReset.png' + boot_timeout = 120 + else + bootsplash = 'TailsBootSplash.png' + bootsplash_tab_msg = 'TailsBootSplashTabMsg.png' + boot_timeout = 30 + end + end + + @screen.wait(bootsplash, boot_timeout) + @screen.wait(bootsplash_tab_msg, 10) + @screen.type(Sikuli::Key.TAB) + @screen.waitVanish(bootsplash_tab_msg, 1) + + @screen.type(" autotest_never_use_this_option #{@boot_options}" + + Sikuli::Key.ENTER) + @screen.wait('TailsGreeter.png', 30*60) + @vm.wait_until_remote_shell_is_up + activate_filesystem_shares +end + +Given /^I log in to a new session$/ do + next if @skip_steps_while_restoring_background + @screen.wait_and_click('TailsGreeterLoginButton.png', 10) +end + +Given /^I enable more Tails Greeter options$/ do + next if @skip_steps_while_restoring_background + match = @screen.find('TailsGreeterMoreOptions.png') + @screen.click(match.getCenter.offset(match.w/2, match.h*2)) + @screen.wait_and_click('TailsGreeterForward.png', 10) + @screen.wait('TailsGreeterLoginButton.png', 20) +end + +Given /^I set sudo password "([^"]*)"$/ do |password| + @sudo_password = password + next if @skip_steps_while_restoring_background + @screen.wait("TailsGreeterAdminPassword.png", 20) + @screen.type(@sudo_password) + @screen.type(Sikuli::Key.TAB) + @screen.type(@sudo_password) +end + +Given /^Tails Greeter has dealt with the sudo password$/ do + next if @skip_steps_while_restoring_background + f1 = "/etc/sudoers.d/tails-greeter" + f2 = "#{f1}-no-password-lecture" + try_for(20) { + @vm.execute("test -e '#{f1}' -o -e '#{f2}'").success? + } +end + +Given /^GNOME has started$/ do + next if @skip_steps_while_restoring_background + case @theme + when "windows" + desktop_started_picture = 'WindowsStartButton.png' + else + desktop_started_picture = 'GnomeApplicationsMenu.png' + end + @screen.wait(desktop_started_picture, 180) +end + +Then /^Tails seems to have booted normally$/ do + next if @skip_steps_while_restoring_background + step "GNOME has started" +end + +Given /^Tor is ready$/ do + next if @skip_steps_while_restoring_background + @screen.wait("GnomeTorIsReady.png", 300) + @screen.waitVanish("GnomeTorIsReady.png", 15) + + # Having seen the "Tor is ready" notification implies that Tor has + # built a circuit, but let's check it directly to be on the safe side. + step "Tor has built a circuit" + + step "the time has synced" +end + +Given /^Tor has built a circuit$/ do + next if @skip_steps_while_restoring_background + wait_until_tor_is_working +end + +Given /^the time has synced$/ do + next if @skip_steps_while_restoring_background + ["/var/run/tordate/done", "/var/run/htpdate/success"].each do |file| + try_for(300) { @vm.execute("test -e #{file}").success? } + end +end + +Given /^available upgrades have been checked$/ do + next if @skip_steps_while_restoring_background + try_for(300) { + @vm.execute("test -e '/var/run/tails-upgrader/checked_upgrades'").success? + } +end + +Given /^the Tor Browser has started$/ do + next if @skip_steps_while_restoring_background + case @theme + when "windows" + tor_browser_picture = "WindowsTorBrowserWindow.png" + else + tor_browser_picture = "TorBrowserWindow.png" + end + + @screen.wait(tor_browser_picture, 60) +end + +Given /^the Tor Browser has started and loaded the startup page$/ do + next if @skip_steps_while_restoring_background + step "the Tor Browser has started" + @screen.wait("TorBrowserStartupPage.png", 120) +end + +Given /^the Tor Browser has started in offline mode$/ do + next if @skip_steps_while_restoring_background + @screen.wait("TorBrowserOffline.png", 60) +end + +Given /^I add a bookmark to eff.org in the Tor Browser$/ do + next if @skip_steps_while_restoring_background + url = "https://www.eff.org" + step "I open the address \"#{url}\" in the Tor Browser" + @screen.wait("TorBrowserOffline.png", 5) + @screen.type("d", Sikuli::KeyModifier.CTRL) + @screen.wait("TorBrowserBookmarkPrompt.png", 10) + @screen.type(url + Sikuli::Key.ENTER) +end + +Given /^the Tor Browser has a bookmark to eff.org$/ do + next if @skip_steps_while_restoring_background + @screen.type("b", Sikuli::KeyModifier.ALT) + @screen.wait("TorBrowserEFFBookmark.png", 10) +end + +Given /^all notifications have disappeared$/ do + next if @skip_steps_while_restoring_background + case @theme + when "windows" + notification_picture = "WindowsNotificationX.png" + else + notification_picture = "GnomeNotificationX.png" + end + @screen.waitVanish(notification_picture, 60) +end + +Given /^I save the state so the background can be restored next scenario$/ do + if @skip_steps_while_restoring_background + assert(File.size?($background_snapshot), + "We have been skipping steps but there is no snapshot to restore") + else + # To be sure we run the feature from scratch we remove any + # leftover snapshot that wasn't removed. + if File.exist?($background_snapshot) + File.delete($background_snapshot) + end + # Workaround: when libvirt takes ownership of the snapshot it may + # become unwritable for the user running this script so it cannot + # be removed during clean up. + FileUtils.touch($background_snapshot) + FileUtils.chmod(0666, $background_snapshot) + + # Snapshots cannot be saved while filesystem shares are mounted + # XXX-9p: See XXX-9p above. + #deactivate_filesystem_shares + + @vm.save_snapshot($background_snapshot) + end + restore_background + # Now we stop skipping steps from the snapshot restore. + @skip_steps_while_restoring_background = false +end + +Then /^I see "([^"]*)" after at most (\d+) seconds$/ do |image, time| + next if @skip_steps_while_restoring_background + @screen.wait(image, time.to_i) +end + +Then /^all Internet traffic has only flowed through Tor$/ do + next if @skip_steps_while_restoring_background + leaks = FirewallLeakCheck.new(@sniffer.pcap_file, get_tor_relays) + if !leaks.empty? + if !leaks.ipv4_tcp_leaks.empty? + puts "The following IPv4 TCP non-Tor Internet hosts were contacted:" + puts leaks.ipv4_tcp_leaks.join("\n") + puts + end + if !leaks.ipv4_nontcp_leaks.empty? + puts "The following IPv4 non-TCP Internet hosts were contacted:" + puts leaks.ipv4_nontcp_leaks.join("\n") + puts + end + if !leaks.ipv6_leaks.empty? + puts "The following IPv6 Internet hosts were contacted:" + puts leaks.ipv6_leaks.join("\n") + puts + end + if !leaks.nonip_leaks.empty? + puts "Some non-IP packets were sent\n" + end + save_pcap_file + raise "There were network leaks!" + end +end + +Given /^I enter the sudo password in the gksu prompt$/ do + next if @skip_steps_while_restoring_background + @screen.wait('GksuAuthPrompt.png', 60) + sleep 1 # wait for weird fade-in to unblock the "Ok" button + @screen.type(@sudo_password) + @screen.type(Sikuli::Key.ENTER) + @screen.waitVanish('GksuAuthPrompt.png', 10) +end + +Given /^I enter the sudo password in the pkexec prompt$/ do + next if @skip_steps_while_restoring_background + step "I enter the \"#{@sudo_password}\" password in the pkexec prompt" +end + +def deal_with_polkit_prompt (image, password) + @screen.wait(image, 60) + sleep 1 # wait for weird fade-in to unblock the "Ok" button + @screen.type(password) + @screen.type(Sikuli::Key.ENTER) + @screen.waitVanish(image, 10) +end + +Given /^I enter the "([^"]*)" password in the pkexec prompt$/ do |password| + next if @skip_steps_while_restoring_background + deal_with_polkit_prompt('PolicyKitAuthPrompt.png', password) +end + +Given /^process "([^"]+)" is running$/ do |process| + next if @skip_steps_while_restoring_background + assert(@vm.has_process?(process), + "Process '#{process}' is not running") +end + +Given /^process "([^"]+)" is running within (\d+) seconds$/ do |process, time| + next if @skip_steps_while_restoring_background + try_for(time.to_i, :msg => "Process '#{process}' is not running after " + + "waiting for #{time} seconds") do + @vm.has_process?(process) + end +end + +Given /^process "([^"]+)" is not running$/ do |process| + next if @skip_steps_while_restoring_background + assert(!@vm.has_process?(process), + "Process '#{process}' is running") +end + +Given /^I kill the process "([^"]+)"$/ do |process| + next if @skip_steps_while_restoring_background + @vm.execute("killall #{process}") + try_for(10, :msg => "Process '#{process}' could not be killed") { + !@vm.has_process?(process) + } +end + +Then /^Tails eventually shuts down$/ do + next if @skip_steps_while_restoring_background + nr_gibs_of_ram = (detected_ram_in_MiB.to_f/(2**10)).ceil + timeout = nr_gibs_of_ram*5*60 + try_for(timeout, :msg => "VM is still running after #{timeout} seconds") do + ! @vm.is_running? + end +end + +Then /^Tails eventually restarts$/ do + next if @skip_steps_while_restoring_background + nr_gibs_of_ram = (detected_ram_in_MiB.to_f/(2**10)).ceil + @screen.wait('TailsBootSplashPostReset.png', nr_gibs_of_ram*5*60) +end + +Given /^I shutdown Tails and wait for the computer to power off$/ do + next if @skip_steps_while_restoring_background + @vm.execute("poweroff") + step 'Tails eventually shuts down' +end + +When /^I request a shutdown using the emergency shutdown applet$/ do + next if @skip_steps_while_restoring_background + @screen.hide_cursor + @screen.wait_and_click('TailsEmergencyShutdownButton.png', 10) + @screen.hide_cursor + @screen.wait_and_click('TailsEmergencyShutdownHalt.png', 10) +end + +When /^I warm reboot the computer$/ do + next if @skip_steps_while_restoring_background + @vm.execute("reboot") +end + +When /^I request a reboot using the emergency shutdown applet$/ do + next if @skip_steps_while_restoring_background + @screen.hide_cursor + @screen.wait_and_click('TailsEmergencyShutdownButton.png', 10) + @screen.hide_cursor + @screen.wait_and_click('TailsEmergencyShutdownReboot.png', 10) +end + +Given /^package "([^"]+)" is installed$/ do |package| + next if @skip_steps_while_restoring_background + assert(@vm.execute("dpkg -s '#{package}' 2>/dev/null | grep -qs '^Status:.*installed$'").success?, + "Package '#{package}' is not installed") +end + +When /^I start the Tor Browser$/ do + next if @skip_steps_while_restoring_background + case @theme + when "windows" + step 'I click the start menu' + @screen.wait_and_click("WindowsApplicationsInternet.png", 10) + @screen.wait_and_click("WindowsApplicationsTorBrowser.png", 10) + else + @screen.wait_and_click("GnomeApplicationsMenu.png", 10) + @screen.wait_and_click("GnomeApplicationsInternet.png", 10) + @screen.wait_and_click("GnomeApplicationsTorBrowser.png", 10) + end +end + +When /^I start the Tor Browser in offline mode$/ do + next if @skip_steps_while_restoring_background + step "I start the Tor Browser" + case @theme + when "windows" + @screen.wait_and_click("WindowsTorBrowserOfflinePrompt.png", 10) + @screen.click("WindowsTorBrowserOfflinePromptStart.png") + else + @screen.wait_and_click("TorBrowserOfflinePrompt.png", 10) + @screen.click("TorBrowserOfflinePromptStart.png") + end +end + +def xul_app_shared_lib_check(pid, chroot) + expected_absent_tbb_libs = ['libnssdbm3.so'] + absent_tbb_libs = [] + unwanted_native_libs = [] + tbb_libs = @vm.execute_successfully( + ". /usr/local/lib/tails-shell-library/tor-browser.sh; " + + "ls -1 #{chroot}${TBB_INSTALL}/Browser/*.so" + ).stdout.split + firefox_pmap_info = @vm.execute("pmap #{pid}").stdout + for lib in tbb_libs do + lib_name = File.basename lib + if not /\W#{lib}$/.match firefox_pmap_info + absent_tbb_libs << lib_name + end + native_libs = @vm.execute_successfully( + "find /usr/lib /lib -name \"#{lib_name}\"" + ).stdout.split + for native_lib in native_libs do + if /\W#{native_lib}$"/.match firefox_pmap_info + unwanted_native_libs << lib_name + end + end + end + absent_tbb_libs -= expected_absent_tbb_libs + assert(absent_tbb_libs.empty? && unwanted_native_libs.empty?, + "The loaded shared libraries for the firefox process are not the " + + "way we expect them.\n" + + "Expected TBB libs that are absent: #{absent_tbb_libs}\n" + + "Native libs that we don't want: #{unwanted_native_libs}") +end + +Then /^(.*) uses all expected TBB shared libraries$/ do |application| + next if @skip_steps_while_restoring_background + binary = @vm.execute_successfully( + '. /usr/local/lib/tails-shell-library/tor-browser.sh; ' + + 'echo ${TBB_INSTALL}/Browser/firefox' + ).stdout.chomp + case application + when "the Tor Browser" + user = $live_user + cmd_regex = "#{binary} .* -profile /home/#{user}/\.tor-browser/profile\.default" + chroot = "" + when "the Unsafe Browser" + user = "clearnet" + cmd_regex = "#{binary} .* -profile /home/#{user}/\.tor-browser/profile\.default" + chroot = "/var/lib/unsafe-browser/chroot" + when "Tor Launcher" + user = "tor-launcher" + cmd_regex = "#{binary} -app /home/#{user}/\.tor-launcher/tor-launcher-standalone/application\.ini" + chroot = "" + else + raise "Invalid browser or XUL application: #{application}" + end + pid = @vm.execute_successfully("pgrep --uid #{user} --full --exact '#{cmd_regex}'").stdout.chomp + assert(/\A\d+\z/.match(pid), "It seems like #{application} is not running") + xul_app_shared_lib_check(pid, chroot) +end + +Given /^I add a wired DHCP NetworkManager connection called "([^"]+)"$/ do |con_name| + next if @skip_steps_while_restoring_background + con_content = <<EOF +[802-3-ethernet] +duplex=full + +[connection] +id=#{con_name} +uuid=bbc60668-1be0-11e4-a9c6-2f1ce0e75bf1 +type=802-3-ethernet +timestamp=1395406011 + +[ipv6] +method=auto + +[ipv4] +method=auto +EOF + con_content.split("\n").each do |line| + @vm.execute("echo '#{line}' >> /tmp/NM.#{con_name}") + end + @vm.execute("install -m 0600 '/tmp/NM.#{con_name}' '/etc/NetworkManager/system-connections/#{con_name}'") + try_for(10) { + nm_con_list = @vm.execute("nmcli --terse --fields NAME con list").stdout + nm_con_list.split("\n").include? "#{con_name}" + } +end + +Given /^I switch to the "([^"]+)" NetworkManager connection$/ do |con_name| + next if @skip_steps_while_restoring_background + @vm.execute("nmcli con up id #{con_name}") + try_for(60) { + @vm.execute("nmcli --terse --fields NAME,STATE con status").stdout.chomp == "#{con_name}:activated" + } +end + +When /^I start and focus GNOME Terminal$/ do + next if @skip_steps_while_restoring_background + @screen.wait_and_click("GnomeApplicationsMenu.png", 10) + @screen.wait_and_click("GnomeApplicationsAccessories.png", 10) + @screen.wait_and_click("GnomeApplicationsTerminal.png", 20) + @screen.wait_and_click('GnomeTerminalWindow.png', 20) +end + +When /^I run "([^"]+)" in GNOME Terminal$/ do |command| + next if @skip_steps_while_restoring_background + step "I start and focus GNOME Terminal" + @screen.type(command + Sikuli::Key.ENTER) +end + +When /^the file "([^"]+)" exists$/ do |file| + next if @skip_steps_while_restoring_background + assert(@vm.file_exist?(file)) +end + +When /^I copy "([^"]+)" to "([^"]+)" as user "([^"]+)"$/ do |source, destination, user| + next if @skip_steps_while_restoring_background + c = @vm.execute("cp \"#{source}\" \"#{destination}\"", $live_user) + assert(c.success?, "Failed to copy file:\n#{c.stdout}\n#{c.stderr}") +end + +Given /^the USB drive "([^"]+)" contains Tails with persistence configured and password "([^"]+)"$/ do |drive, password| + step "a computer" + step "I start Tails from DVD with network unplugged and I login" + step "I create a new 4 GiB USB drive named \"#{drive}\"" + step "I plug USB drive \"#{drive}\"" + step "I \"Clone & Install\" Tails to USB drive \"#{drive}\"" + step "there is no persistence partition on USB drive \"#{drive}\"" + step "I shutdown Tails and wait for the computer to power off" + step "a computer" + step "I start Tails from USB drive \"#{drive}\" with network unplugged and I login" + step "I create a persistent partition with password \"#{password}\"" + step "a Tails persistence partition with password \"#{password}\" exists on USB drive \"#{drive}\"" + step "I shutdown Tails and wait for the computer to power off" +end diff --git a/features/step_definitions/dhcp.rb b/features/step_definitions/dhcp.rb new file mode 100644 index 00000000..78ee8f2d --- /dev/null +++ b/features/step_definitions/dhcp.rb @@ -0,0 +1,20 @@ +Then /^the hostname should not have been leaked on the network$/ do + next if @skip_steps_while_restoring_background + hostname = @vm.execute("hostname").stdout.chomp + packets = PacketFu::PcapFile.new.file_to_array(:filename => @sniffer.pcap_file) + packets.each do |p| + # if PacketFu::TCPPacket.can_parse?(p) + # ipv4_tcp_packets << PacketFu::TCPPacket.parse(p) + if PacketFu::IPPacket.can_parse?(p) + payload = PacketFu::IPPacket.parse(p).payload + elsif PacketFu::IPv6Packet.can_parse?(p) + payload = PacketFu::IPv6Packet.parse(p).payload + else + save_pcap_file + raise "Found something in the pcap file that either is non-IP, or cannot be parsed" + end + if payload.match(hostname) + raise "Hostname leak detected" + end + end +end diff --git a/features/step_definitions/encryption.rb b/features/step_definitions/encryption.rb new file mode 100644 index 00000000..404890ae --- /dev/null +++ b/features/step_definitions/encryption.rb @@ -0,0 +1,139 @@ +Given /^I generate an OpenPGP key named "([^"]+)" with password "([^"]+)"$/ do |name, pwd| + @passphrase = pwd + @key_name = name + next if @skip_steps_while_restoring_background + gpg_key_recipie = <<EOF + Key-Type: RSA + Key-Length: 4096 + Subkey-Type: RSA + Subkey-Length: 4096 + Name-Real: #{@key_name} + Name-Comment: Blah + Name-Email: #{@key_name}@test.org + Expire-Date: 0 + Passphrase: #{pwd} + %commit +EOF + gpg_key_recipie.split("\n").each do |line| + @vm.execute("echo '#{line}' >> /tmp/gpg_key_recipie", $live_user) + end + c = @vm.execute("gpg --batch --gen-key < /tmp/gpg_key_recipie", $live_user) + assert(c.success?, "Failed to generate OpenPGP key:\n#{c.stderr}") +end + +When /^I type a message into gedit$/ do + next if @skip_steps_while_restoring_background + @screen.wait_and_click("GnomeApplicationsMenu.png", 10) + @screen.wait_and_click("GnomeApplicationsAccessories.png", 10) + @screen.wait_and_click("GnomeApplicationsGedit.png", 20) + @screen.wait_and_click("GeditWindow.png", 10) + sleep 0.5 + @screen.type("ATTACK AT DAWN") +end + +def maybe_deal_with_pinentry + begin + @screen.wait_and_click("PinEntryPrompt.png", 3) + sleep 1 + @screen.type(@passphrase + Sikuli::Key.ENTER) + rescue FindFailed + # The passphrase was cached or we wasn't prompted at all (e.g. when + # only encrypting to a public key) + end +end + +def encrypt_sign_helper + @screen.wait_and_click("GeditWindow.png", 10) + @screen.type("a", Sikuli::KeyModifier.CTRL) + sleep 0.5 + @screen.click("GpgAppletIconNormal.png") + sleep 2 + @screen.type("k") + @screen.wait_and_click("GpgAppletChooseKeyWindow.png", 30) + sleep 0.5 + yield + maybe_deal_with_pinentry + @screen.wait_and_click("GeditWindow.png", 10) + sleep 0.5 + @screen.type("n", Sikuli::KeyModifier.CTRL) + sleep 0.5 + @screen.type("v", Sikuli::KeyModifier.CTRL) +end + +def decrypt_verify_helper(icon) + @screen.wait_and_click("GeditWindow.png", 10) + @screen.type("a", Sikuli::KeyModifier.CTRL) + sleep 0.5 + @screen.click(icon) + sleep 2 + @screen.type("d") + maybe_deal_with_pinentry + @screen.wait("GpgAppletResults.png", 10) + @screen.wait("GpgAppletResultsMsg.png", 10) +end + +When /^I encrypt the message using my OpenPGP key$/ do + next if @skip_steps_while_restoring_background + encrypt_sign_helper do + @screen.type(@key_name + Sikuli::Key.ENTER + Sikuli::Key.ENTER) + end +end + +Then /^I can decrypt the encrypted message$/ do + next if @skip_steps_while_restoring_background + decrypt_verify_helper("GpgAppletIconEncrypted.png") + @screen.wait("GpgAppletResultsEncrypted.png", 10) +end + +When /^I sign the message using my OpenPGP key$/ do + next if @skip_steps_while_restoring_background + encrypt_sign_helper do + @screen.type(Sikuli::Key.TAB + Sikuli::Key.DOWN + Sikuli::Key.ENTER) + @screen.wait("PinEntryPrompt.png", 10) + @screen.type(@passphrase + Sikuli::Key.ENTER) + end +end + +Then /^I can verify the message's signature$/ do + next if @skip_steps_while_restoring_background + decrypt_verify_helper("GpgAppletIconSigned.png") + @screen.wait("GpgAppletResultsSigned.png", 10) +end + +When /^I both encrypt and sign the message using my OpenPGP key$/ do + next if @skip_steps_while_restoring_background + encrypt_sign_helper do + @screen.type(@key_name + Sikuli::Key.ENTER) + @screen.type(Sikuli::Key.TAB + Sikuli::Key.DOWN + Sikuli::Key.ENTER) + @screen.wait("PinEntryPrompt.png", 10) + @screen.type(@passphrase + Sikuli::Key.ENTER) + end +end + +Then /^I can decrypt and verify the encrypted message$/ do + next if @skip_steps_while_restoring_background + decrypt_verify_helper("GpgAppletIconEncrypted.png") + @screen.wait("GpgAppletResultsEncrypted.png", 10) + @screen.wait("GpgAppletResultsSigned.png", 10) +end + +When /^I symmetrically encrypt the message with password "([^"]+)"$/ do |pwd| + @passphrase = pwd + next if @skip_steps_while_restoring_background + @screen.wait_and_click("GeditWindow.png", 10) + @screen.type("a", Sikuli::KeyModifier.CTRL) + sleep 0.5 + @screen.click("GpgAppletIconNormal.png") + sleep 2 + @screen.type("p") + @screen.wait("PinEntryPrompt.png", 10) + @screen.type(@passphrase + Sikuli::Key.ENTER) + sleep 1 + @screen.wait("PinEntryPrompt.png", 10) + @screen.type(@passphrase + Sikuli::Key.ENTER) + @screen.wait_and_click("GeditWindow.png", 10) + sleep 0.5 + @screen.type("n", Sikuli::KeyModifier.CTRL) + sleep 0.5 + @screen.type("v", Sikuli::KeyModifier.CTRL) +end diff --git a/features/step_definitions/erase_memory.rb b/features/step_definitions/erase_memory.rb new file mode 100644 index 00000000..171f997c --- /dev/null +++ b/features/step_definitions/erase_memory.rb @@ -0,0 +1,172 @@ +Given /^the computer is a modern 64-bit system$/ do + next if @skip_steps_while_restoring_background + @vm.set_arch("x86_64") + @vm.drop_hypervisor_feature("nonpae") + @vm.add_hypervisor_feature("pae") +end + +Given /^the computer is an old pentium without the PAE extension$/ do + next if @skip_steps_while_restoring_background + @vm.set_arch("i686") + @vm.drop_hypervisor_feature("pae") + # libvirt claim the following feature doesn't exit even though + # it's listed in the hvm i686 capabilities... +# @vm.add_hypervisor_feature("nonpae") + # ... so we use a workaround until we can figure this one out. + @vm.disable_pae_workaround +end + +def which_kernel + kernel_path = @vm.execute("/usr/local/bin/tails-get-bootinfo kernel").stdout.chomp + return File.basename(kernel_path) +end + +Given /^the PAE kernel is running$/ do + next if @skip_steps_while_restoring_background + kernel = which_kernel + assert_equal("vmlinuz2", kernel) +end + +Given /^the non-PAE kernel is running$/ do + next if @skip_steps_while_restoring_background + kernel = which_kernel + assert_equal("vmlinuz", kernel) +end + +def used_ram_in_MiB + return @vm.execute("free -m | awk '/^-\\/\\+ buffers\\/cache:/ { print $3 }'").stdout.chomp.to_i +end + +def detected_ram_in_MiB + return @vm.execute("free -m | awk '/^Mem:/ { print $2 }'").stdout.chomp.to_i +end + +Given /^at least (\d+) ([[:alpha:]]+) of RAM was detected$/ do |min_ram, unit| + @detected_ram_m = detected_ram_in_MiB + next if @skip_steps_while_restoring_background + puts "Detected #{@detected_ram_m} MiB of RAM" + min_ram_m = convert_to_MiB(min_ram.to_i, unit) + # All RAM will not be reported by `free`, so we allow a 196 MB gap + gap = convert_to_MiB(196, "MiB") + assert(@detected_ram_m + gap >= min_ram_m, "Didn't detect enough RAM") +end + +def pattern_coverage_in_guest_ram + dump = "#{$tmp_dir}/memdump" + # Workaround: when dumping the guest's memory via core_dump(), libvirt + # will create files that only root can read. We therefore pre-create + # them with more permissible permissions, which libvirt will preserve + # (although it will change ownership) so that the user running the + # script can grep the dump for the fillram pattern, and delete it. + if File.exist?(dump) + File.delete(dump) + end + FileUtils.touch(dump) + FileUtils.chmod(0666, dump) + @vm.domain.core_dump(dump) + patterns = IO.popen("grep -c 'wipe_didnt_work' #{dump}").gets.to_i + File.delete dump + # Pattern is 16 bytes long + patterns_b = patterns*16 + patterns_m = convert_to_MiB(patterns_b, 'b') + coverage = patterns_b.to_f/convert_to_bytes(@detected_ram_m.to_f, 'MiB') + puts "Pattern coverage: #{"%.3f" % (coverage*100)}% (#{patterns_m} MiB)" + return coverage +end + +Given /^I fill the guest's memory with a known pattern(| without verifying)$/ do |dont_verify| + verify = dont_verify.empty? + next if @skip_steps_while_restoring_background + + # Free some more memory by dropping the caches etc. + @vm.execute("echo 3 > /proc/sys/vm/drop_caches") + + # The (guest) kernel may freeze when approaching full memory without + # adjusting the OOM killer and memory overcommitment limitations. + [ + "echo 256 > /proc/sys/vm/min_free_kbytes", + "echo 2 > /proc/sys/vm/overcommit_memory", + "echo 97 > /proc/sys/vm/overcommit_ratio", + "echo 1 > /proc/sys/vm/oom_kill_allocating_task", + "echo 0 > /proc/sys/vm/oom_dump_tasks" + ].each { |c| @vm.execute(c) } + + # The remote shell is sometimes OOM killed when we fill the memory, + # and since we depend on it after the memory fill we try to prevent + # that from happening. + pid = @vm.pidof("autotest_remote_shell.py")[0] + @vm.execute("echo -17 > /proc/#{pid}/oom_adj") + + used_mem_before_fill = used_ram_in_MiB + + # To be sure that we fill all memory we run one fillram instance + # for each GiB of detected memory, rounded up. We also kill all instances + # after the first one has finished, i.e. when the memory is full, + # since the others otherwise may continue re-filling the same memory + # unnecessarily. + instances = (@detected_ram_m.to_f/(2**10)).ceil + instances.times { @vm.spawn('/usr/local/sbin/fillram; killall fillram') } + # We make sure that the filling has started... + try_for(10, { :msg => "fillram didn't start" }) { + @vm.has_process?("fillram") + } + STDERR.print "Memory fill progress: " + ram_usage = "" + remove_chars = 0 + # ... and that it finishes + try_for(instances*2*60, { :msg => "fillram didn't complete, probably the VM crashed" }) do + used_ram = used_ram_in_MiB + remove_chars = ram_usage.size + ram_usage = "%3d%% " % ((used_ram.to_f/@detected_ram_m)*100) + STDERR.print "\b"*remove_chars + ram_usage + ! @vm.has_process?("fillram") + end + STDERR.print "\b"*remove_chars + "finished.\n" + if verify + coverage = pattern_coverage_in_guest_ram() + # Let's aim for having the pattern cover at least 80% of the free RAM. + # More would be good, but it seems like OOM kill strikes around 90%, + # and we don't want this test to fail all the time. + min_coverage = ((@detected_ram_m - used_mem_before_fill).to_f / + @detected_ram_m.to_f)*0.75 + assert(coverage > min_coverage, + "#{"%.3f" % (coverage*100)}% of the memory is filled with the " + + "pattern, but more than #{"%.3f" % (min_coverage*100)}% was expected") + end +end + +Then /^I find very few patterns in the guest's memory$/ do + next if @skip_steps_while_restoring_background + coverage = pattern_coverage_in_guest_ram() + max_coverage = 0.005 + assert(coverage < max_coverage, + "#{"%.3f" % (coverage*100)}% of the memory is filled with the " + + "pattern, but less than #{"%.3f" % (max_coverage*100)}% was expected") +end + +Then /^I find many patterns in the guest's memory$/ do + next if @skip_steps_while_restoring_background + coverage = pattern_coverage_in_guest_ram() + min_coverage = 0.7 + assert(coverage > min_coverage, + "#{"%.3f" % (coverage*100)}% of the memory is filled with the " + + "pattern, but more than #{"%.3f" % (min_coverage*100)}% was expected") +end + +When /^I reboot without wiping the memory$/ do + next if @skip_steps_while_restoring_background + @vm.reset + @screen.wait('TailsBootSplashPostReset.png', 30) +end + +When /^I shutdown and wait for Tails to finish wiping the memory$/ do + next if @skip_steps_while_restoring_background + @vm.execute("halt") + nr_gibs_of_ram = (@detected_ram_m.to_f/(2**10)).ceil + try_for(nr_gibs_of_ram*5*60, { :msg => "memory wipe didn't finish, probably the VM crashed" }) do + # We spam keypresses to prevent console blanking from hiding the + # image we're waiting for + @screen.type(" ") + @screen.wait('MemoryWipeCompleted.png') + end +end diff --git a/features/step_definitions/evince.rb b/features/step_definitions/evince.rb new file mode 100644 index 00000000..d9bb42c1 --- /dev/null +++ b/features/step_definitions/evince.rb @@ -0,0 +1,20 @@ +When /^I(?:| try to) open "([^"]+)" with Evince$/ do |filename| + next if @skip_steps_while_restoring_background + step "I run \"evince #{filename}\" in GNOME Terminal" +end + +Then /^I can print the current document to "([^"]+)"$/ do |output_file| + next if @skip_steps_while_restoring_background + @screen.type("p", Sikuli::KeyModifier.CTRL) + @screen.wait("EvincePrintDialog.png", 10) + @screen.wait_and_click("EvincePrintToFile.png", 10) + @screen.wait_and_double_click("EvincePrintOutputFile.png", 10) + @screen.hide_cursor + @screen.wait("EvincePrintOutputFileSelected.png", 10) + # Only the file's basename is selected by double-clicking, + # so we type only the desired file's basename to replace it + @screen.type(output_file.sub(/[.]pdf$/, '') + Sikuli::Key.ENTER) + try_for(10, :msg => "The document was not printed to #{output_file}") { + @vm.file_exist?(output_file) + } +end diff --git a/features/step_definitions/firewall_leaks.rb b/features/step_definitions/firewall_leaks.rb new file mode 100644 index 00000000..79ae0de3 --- /dev/null +++ b/features/step_definitions/firewall_leaks.rb @@ -0,0 +1,60 @@ +Then(/^the firewall leak detector has detected (.*?) leaks$/) do |type| + next if @skip_steps_while_restoring_background + leaks = FirewallLeakCheck.new(@sniffer.pcap_file, get_tor_relays) + case type.downcase + when 'ipv4 tcp' + if leaks.ipv4_tcp_leaks.empty? + save_pcap_file + raise "Couldn't detect any IPv4 TCP leaks" + end + when 'ipv4 non-tcp' + if leaks.ipv4_nontcp_leaks.empty? + save_pcap_file + raise "Couldn't detect any IPv4 non-TCP leaks" + end + when 'ipv6' + if leaks.ipv6_leaks.empty? + save_pcap_file + raise "Couldn't detect any IPv6 leaks" + end + when 'non-ip' + if leaks.nonip_leaks.empty? + save_pcap_file + raise "Couldn't detect any non-IP leaks" + end + else + raise "Incorrect packet type '#{type}'" + end +end + +Given(/^I disable Tails' firewall$/) do + next if @skip_steps_while_restoring_background + @vm.execute("/usr/local/sbin/do_not_ever_run_me") + iptables = @vm.execute("iptables -L -n -v").stdout.chomp.split("\n") + for line in iptables do + if !line[/Chain (INPUT|OUTPUT|FORWARD) \(policy ACCEPT/] and + !line[/pkts[[:blank:]]+bytes[[:blank:]]+target/] and + !line.empty? + raise "The Tails firewall was not successfully disabled:\n#{iptables}" + end + end +end + +When(/^I do a TCP DNS lookup of "(.*?)"$/) do |host| + next if @skip_steps_while_restoring_background + lookup = @vm.execute("host -T #{host} #{$some_dns_server}", $live_user) + assert(lookup.success?, "Failed to resolve #{host}:\n#{lookup.stdout}") +end + +When(/^I do a UDP DNS lookup of "(.*?)"$/) do |host| + next if @skip_steps_while_restoring_background + lookup = @vm.execute("host #{host} #{$some_dns_server}", $live_user) + assert(lookup.success?, "Failed to resolve #{host}:\n#{lookup.stdout}") +end + +When(/^I send some ICMP pings$/) do + next if @skip_steps_while_restoring_background + # We ping an IP address to avoid a DNS lookup + ping = @vm.execute("ping -c 5 #{$some_dns_server}", $live_user) + assert(ping.success?, "Failed to ping #{$some_dns_server}:\n#{ping.stderr}") +end diff --git a/features/step_definitions/i2p.rb b/features/step_definitions/i2p.rb new file mode 100644 index 00000000..0b8a8d3c --- /dev/null +++ b/features/step_definitions/i2p.rb @@ -0,0 +1,60 @@ +Given /^I2P is running$/ do + next if @skip_steps_while_restoring_background + try_for(30) do + @vm.execute('service i2p status').success? + end +end + +Given /^the I2P router console is ready$/ do + next if @skip_steps_while_restoring_background + try_for(60) do + @vm.execute('. /usr/local/lib/tails-shell-library/i2p.sh; ' + + 'i2p_router_console_is_ready').success? + end +end + +When /^I start the I2P Browser through the GNOME menu$/ do + next if @skip_steps_while_restoring_background + @screen.wait_and_click("GnomeApplicationsMenu.png", 10) + @screen.wait_and_click("GnomeApplicationsInternet.png", 10) + @screen.wait_and_click("GnomeApplicationsI2PBrowser.png", 20) +end + +Then /^the I2P Browser desktop file is (|not )present$/ do |mode| + next if @skip_steps_while_restoring_background + file = '/usr/share/applications/i2p-browser.desktop' + if mode == '' + assert(@vm.execute("test -e #{file}").success?) + elsif mode == 'not ' + assert(@vm.execute("! test -e #{file}").success?) + else + raise "Unsupported mode passed: '#{mode}'" + end +end + +Then /^the I2P Browser sudo rules are (enabled|not present)$/ do |mode| + next if @skip_steps_while_restoring_background + file = '/etc/sudoers.d/zzz_i2pbrowser' + if mode == 'enabled' + assert(@vm.execute("test -e #{file}").success?) + elsif mode == 'not present' + assert(@vm.execute("! test -e #{file}").success?) + else + raise "Unsupported mode passed: '#{mode}'" + end +end + +Then /^the I2P firewall rules are (enabled|disabled)$/ do |mode| + next if @skip_steps_while_restoring_background + i2p_username = 'i2psvc' + i2p_uid = @vm.execute("getent passwd #{i2p_username} | awk -F ':' '{print $3}'").stdout.chomp + accept_rules = @vm.execute("iptables -L -n -v | grep -E '^\s+[0-9]+\s+[0-9]+\s+ACCEPT.*owner UID match #{i2p_uid}$'").stdout + accept_rules_count = accept_rules.lines.count + if mode == 'enabled' + assert_equal(13, accept_rules_count) + elsif mode == 'disabled' + assert_equal(0, accept_rules_count) + else + raise "Unsupported mode passed: '#{mode}'" + end +end diff --git a/features/step_definitions/pidgin.rb b/features/step_definitions/pidgin.rb new file mode 100644 index 00000000..23b48e26 --- /dev/null +++ b/features/step_definitions/pidgin.rb @@ -0,0 +1,188 @@ +def configured_pidgin_accounts + accounts = [] + xml = REXML::Document.new(@vm.file_content('$HOME/.purple/accounts.xml', + $live_user)) + xml.elements.each("account/account") do |e| + account = e.elements["name"].text + account_name, network = account.split("@") + protocol = e.elements["protocol"].text + port = e.elements["settings/setting[@name='port']"].text + nickname = e.elements["settings/setting[@name='username']"].text + real_name = e.elements["settings/setting[@name='realname']"].text + accounts.push({ + 'name' => account_name, + 'network' => network, + 'protocol' => protocol, + 'port' => port, + 'nickname' => nickname, + 'real_name' => real_name, + }) + end + + return accounts +end + +def chan_image (account, channel, image) + images = { + 'irc.oftc.net' => { + '#tails' => { + 'roaster' => 'PidginTailsChannelEntry', + 'conversation_tab' => 'PidginTailsConversationTab', + 'welcome' => 'PidginTailsChannelWelcome', + } + } + } + return images[account][channel][image] + ".png" +end + +def default_chan (account) + chans = { + 'irc.oftc.net' => '#tails', + } + return chans[account] +end + +def pidgin_otr_keys + return @vm.file_content('$HOME/.purple/otr.private_key', $live_user) +end + +Given /^Pidgin has the expected accounts configured with random nicknames$/ do + next if @skip_steps_while_restoring_background + expected = [ + ["irc.oftc.net", "prpl-irc", "6697"], + ["127.0.0.1", "prpl-irc", "6668"], + ] + configured_pidgin_accounts.each() do |account| + assert(account['nickname'] != "XXX_NICK_XXX", "Nickname was no randomised") + assert_equal(account['nickname'], account['real_name'], + "Nickname and real name are not identical: " + + account['nickname'] + " vs. " + account['real_name']) + assert_equal(account['name'], account['nickname'], + "Account name and nickname are not identical: " + + account['name'] + " vs. " + account['nickname']) + candidate = [account['network'], account['protocol'], account['port']] + assert(expected.include?(candidate), "Unexpected account: #{candidate}") + expected.delete(candidate) + end + assert(expected.empty?, "These Pidgin accounts are not configured: " + + "#{expected}") +end + +When /^I start Pidgin through the GNOME menu$/ do + next if @skip_steps_while_restoring_background + @screen.wait_and_click("GnomeApplicationsMenu.png", 10) + @screen.wait_and_click("GnomeApplicationsInternet.png", 10) + @screen.wait_and_click("GnomeApplicationsPidgin.png", 20) +end + +When /^I open Pidgin's account manager window$/ do + next if @skip_steps_while_restoring_background + @screen.type("a", Sikuli::KeyModifier.CTRL) # shortcut for "manage accounts" + step "I see Pidgin's account manager window" +end + +When /^I see Pidgin's account manager window$/ do + next if @skip_steps_while_restoring_background + @screen.wait("PidginAccountWindow.png", 20) +end + +When /^I close Pidgin's account manager window$/ do + next if @skip_steps_while_restoring_background + @screen.wait_and_click("PidginAccountManagerCloseButton.png", 10) +end + +When /^I activate the "([^"]+)" Pidgin account$/ do |account| + next if @skip_steps_while_restoring_background + @screen.click("PidginAccount_#{account}.png") + @screen.type(Sikuli::Key.LEFT + Sikuli::Key.SPACE) + # wait for the Pidgin to be connecting, otherwise sometimes the step + # that closes the account management dialog happens before the account + # is actually enabled + @screen.wait("PidginConnecting.png", 5) +end + +Then /^Pidgin successfully connects to the "([^"]+)" account$/ do |account| + next if @skip_steps_while_restoring_background + expected_channel_entry = chan_image(account, default_chan(account), 'roaster') + @screen.wait(expected_channel_entry, 60) +end + +Then /^I can join the "([^"]+)" channel on "([^"]+)"$/ do |channel, account| + next if @skip_steps_while_restoring_background + @screen.doubleClick( chan_image(account, channel, 'roaster')) + @screen.wait_and_click(chan_image(account, channel, 'conversation_tab'), 10) + @screen.wait( chan_image(account, channel, 'welcome'), 10) +end + +Then /^I take note of the configured Pidgin accounts$/ do + next if @skip_steps_while_restoring_background + @persistent_pidgin_accounts = configured_pidgin_accounts +end + +Then /^I take note of the OTR key for Pidgin's "([^"]+)" account$/ do |account_name| + next if @skip_steps_while_restoring_background + @persistent_pidgin_otr_keys = pidgin_otr_keys +end + +Then /^Pidgin has the expected persistent accounts configured$/ do + next if @skip_steps_while_restoring_background + current_accounts = configured_pidgin_accounts + assert(current_accounts <=> @persistent_pidgin_accounts, + "Currently configured Pidgin accounts do not match the persistent ones:\n" + + "Current:\n#{current_accounts}\n" + + "Persistent:\n#{@persistent_pidgin_accounts}" + ) +end + +Then /^Pidgin has the expected persistent OTR keys$/ do + next if @skip_steps_while_restoring_background + assert_equal(pidgin_otr_keys, @persistent_pidgin_otr_keys) +end + +def pidgin_add_certificate_from (cert_file) + # Here, we need a certificate that is not already in the NSS database + step "I copy \"/usr/share/ca-certificates/spi-inc.org/spi-cacert-2008.crt\" to \"#{cert_file}\" as user \"amnesia\"" + + @screen.wait_and_click('PidginToolsMenu.png', 10) + @screen.wait_and_click('PidginCertificatesMenuItem.png', 10) + @screen.wait('PidginCertificateManagerDialog.png', 10) + @screen.wait_and_click('PidginCertificateAddButton.png', 10) + begin + @screen.wait_and_click('GtkFileChooserDesktopButton.png', 10) + rescue FindFailed + # The first time we're run, the file chooser opens in the Recent + # view, so we have to browse a directory before we can use the + # "Type file name" button. But on subsequent runs, the file + # chooser is already in the Desktop directory, so we don't need to + # do anything. Hence, this noop exception handler. + end + @screen.wait_and_click('GtkFileTypeFileNameButton.png', 10) + @screen.type("l", Sikuli::KeyModifier.ALT) # "Location" field + @screen.type(cert_file + Sikuli::Key.ENTER) +end + +Then /^I can add a certificate from the "([^"]+)" directory to Pidgin$/ do |cert_dir| + next if @skip_steps_while_restoring_background + pidgin_add_certificate_from("#{cert_dir}/test.crt") + @screen.wait('PidginCertificateAddHostnameDialog.png', 10) + @screen.type("XXX test XXX" + Sikuli::Key.ENTER) + @screen.wait('PidginCertificateTestItem.png', 10) +end + +Then /^I cannot add a certificate from the "([^"]+)" directory to Pidgin$/ do |cert_dir| + next if @skip_steps_while_restoring_background + pidgin_add_certificate_from("#{cert_dir}/test.crt") + @screen.wait('PidginCertificateImportFailed.png', 10) +end + +When /^I close Pidgin's certificate manager$/ do + @screen.type(Sikuli::Key.ESC) + # @screen.wait_and_click('PidginCertificateManagerClose.png', 10) + @screen.waitVanish('PidginCertificateManagerDialog.png', 10) +end + +When /^I close Pidgin's certificate import failure dialog$/ do + @screen.type(Sikuli::Key.ESC) + # @screen.wait_and_click('PidginCertificateManagerClose.png', 10) + @screen.waitVanish('PidginCertificateImportFailed.png', 10) +end diff --git a/features/step_definitions/root_access_control.rb b/features/step_definitions/root_access_control.rb new file mode 100644 index 00000000..aaebb0df --- /dev/null +++ b/features/step_definitions/root_access_control.rb @@ -0,0 +1,45 @@ +Then /^I should be able to run administration commands as the live user$/ do + next if @skip_steps_while_restoring_background + stdout = @vm.execute("echo #{@sudo_password} | sudo -S whoami", $live_user).stdout + actual_user = stdout.sub(/^\[sudo\] password for #{$live_user}: /, "").chomp + assert_equal("root", actual_user, "Could not use sudo") +end + +Then /^I should not be able to run administration commands as the live user with the "([^"]*)" password$/ do |password| + next if @skip_steps_while_restoring_background + stderr = @vm.execute("echo #{password} | sudo -S whoami", $live_user).stderr + sudo_failed = stderr.include?("The administration password is disabled") || stderr.include?("is not allowed to execute") + assert(sudo_failed, "The administration password is not disabled:" + stderr) +end + +When /^running a command as root with pkexec requires PolicyKit administrator privileges$/ do + next if @skip_steps_while_restoring_background + action = 'org.freedesktop.policykit.exec' + action_details = @vm.execute("pkaction --verbose --action-id #{action}").stdout + assert(action_details[/\s+implicit any:\s+auth_admin$/], + "Expected 'auth_admin' for 'any':\n#{action_details}") + assert(action_details[/\s+implicit inactive:\s+auth_admin$/], + "Expected 'auth_admin' for 'inactive':\n#{action_details}") + assert(action_details[/\s+implicit active:\s+auth_admin$/], + "Expected 'auth_admin' for 'active':\n#{action_details}") +end + +Then /^I should be able to run a command as root with pkexec$/ do + next if @skip_steps_while_restoring_background + step "I run \"pkexec touch /root/pkexec-test\" in GNOME Terminal" + step 'I enter the sudo password in the pkexec prompt' + try_for(10, :msg => 'The /root/pkexec-test file was not created.') { + @vm.execute('ls /root/pkexec-test').success? + } +end + +Then /^I should not be able to run a command as root with pkexec and the standard passwords$/ do + next if @skip_steps_while_restoring_background + step "I run \"pkexec touch /root/pkexec-test\" in GNOME Terminal" + ['', 'live'].each do |password| + step "I enter the \"#{password}\" password in the pkexec prompt" + @screen.wait('PolicyKitAuthFailure.png', 20) + end + step "I enter the \"amnesia\" password in the pkexec prompt" + @screen.wait('PolicyKitAuthCompleteFailure.png', 20) +end diff --git a/features/step_definitions/time_syncing.rb b/features/step_definitions/time_syncing.rb new file mode 100644 index 00000000..161a4162 --- /dev/null +++ b/features/step_definitions/time_syncing.rb @@ -0,0 +1,20 @@ +When /^I set the system time to "([^"]+)"$/ do |time| + next if @skip_steps_while_restoring_background + @vm.execute("date -s '#{time}'") +end + +When /^I bump the system time with "([^"]+)"$/ do |timediff| + next if @skip_steps_while_restoring_background + @vm.execute("date -s 'now #{timediff}'") +end + +Then /^Tails clock is less than (\d+) minutes incorrect$/ do |max_diff_mins| + next if @skip_steps_while_restoring_background + guest_time_str = @vm.execute("date --rfc-2822").stdout.chomp + guest_time = Time.rfc2822(guest_time_str) + host_time = Time.now + diff = (host_time - guest_time).abs + assert(diff < max_diff_mins.to_i*60, + "The guest's clock is off by #{diff} seconds (#{guest_time})") + puts "Time was #{diff} seconds off" +end diff --git a/features/step_definitions/torified_browsing.rb b/features/step_definitions/torified_browsing.rb new file mode 100644 index 00000000..770fda52 --- /dev/null +++ b/features/step_definitions/torified_browsing.rb @@ -0,0 +1,12 @@ +When /^I open a new tab in the Tor Browser$/ do + next if @skip_steps_while_restoring_background + @screen.click("TorBrowserNewTabButton.png") +end + +When /^I open the address "([^"]*)" in the Tor Browser$/ do |address| + next if @skip_steps_while_restoring_background + step "I open a new tab in the Tor Browser" + @screen.click("TorBrowserAddressBar.png") + sleep 0.5 + @screen.type(address + Sikuli::Key.ENTER) +end diff --git a/features/step_definitions/torified_gnupg.rb b/features/step_definitions/torified_gnupg.rb new file mode 100644 index 00000000..5a1462ce --- /dev/null +++ b/features/step_definitions/torified_gnupg.rb @@ -0,0 +1,54 @@ +When /^the "([^"]*)" OpenPGP key is not in the live user's public keyring$/ do |keyid| + next if @skip_steps_while_restoring_background + assert(!@vm.execute("gpg --batch --list-keys '#{keyid}'", $live_user).success?, + "The '#{keyid}' key is in the live user's public keyring.") +end + +When /^I fetch the "([^"]*)" OpenPGP key using the GnuPG CLI$/ do |keyid| + next if @skip_steps_while_restoring_background + @gnupg_recv_key_res = @vm.execute( + "gpg --batch --recv-key '#{keyid}'", + $live_user) +end + +When /^the GnuPG fetch is successful$/ do + next if @skip_steps_while_restoring_background + assert(@gnupg_recv_key_res.success?, + "gpg keyserver fetch failed:\n#{@gnupg_recv_key_res.stderr}") +end + +When /^GnuPG uses the configured keyserver$/ do + next if @skip_steps_while_restoring_background + assert(@gnupg_recv_key_res.stderr[$configured_keyserver_hostname], + "GnuPG's stderr did not mention keyserver #{$configured_keyserver_hostname}") +end + +When /^the "([^"]*)" key is in the live user's public keyring after at most (\d+) seconds$/ do |keyid, delay| + next if @skip_steps_while_restoring_background + try_for(delay.to_f, :msg => "The '#{keyid}' key is not in the live user's public keyring") { + @vm.execute("gpg --batch --list-keys '#{keyid}'", $live_user).success? + } +end + +When /^I start Seahorse$/ do + next if @skip_steps_while_restoring_background + @screen.wait_and_click("GnomeApplicationsMenu.png", 10) + @screen.wait_and_click("GnomeApplicationsSystem.png", 10) + @screen.wait_and_click("GnomeApplicationsPreferences.png", 10) + @screen.wait_and_click("GnomeApplicationsSeahorse.png", 10) +end + +When /^I fetch the "([^"]*)" OpenPGP key using Seahorse$/ do |keyid| + next if @skip_steps_while_restoring_background + step "I start Seahorse" + @screen.wait("SeahorseWindow.png", 10) + @screen.type("r", Sikuli::KeyModifier.ALT) # Menu: "Remote" -> + @screen.type("f") # "Find Remote Keys...". + @screen.wait("SeahorseFindKeysWindow.png", 10) + # Seahorse doesn't seem to support searching for fingerprints + @screen.type(keyid + Sikuli::Key.ENTER) + @screen.wait("SeahorseFoundKeyResult.png", 5*60) + @screen.type(Sikuli::Key.DOWN) # Select first item in result menu + @screen.type("f", Sikuli::KeyModifier.ALT) # Menu: "File" -> + @screen.type("i") # "Import" +end diff --git a/features/step_definitions/totem.rb b/features/step_definitions/totem.rb new file mode 100644 index 00000000..d125f4ec --- /dev/null +++ b/features/step_definitions/totem.rb @@ -0,0 +1,50 @@ +def shared_video_dir_on_guest + "/tmp/shared_video_dir" +end + +Given /^I create sample videos$/ do + next if @skip_steps_while_restoring_background + fatal_system("ffmpeg -loop 1 -t 30 -f image2 " + + "-i 'features/images/TailsBootSplash.png' " + + "-an -vcodec libx264 -y " + + "'#{$misc_files_dir}/video.mp4' >/dev/null 2>&1") +end + +Given /^I setup a filesystem share containing sample videos$/ do + next if @skip_steps_while_restoring_background + @vm.add_share($misc_files_dir, shared_video_dir_on_guest) +end + +Given /^I copy the sample videos to "([^"]+)" as user "([^"]+)"$/ do |destination, user| + next if @skip_steps_while_restoring_background + for video_on_host in Dir.glob("#{$misc_files_dir}/*.mp4") do + video_name = File.basename(video_on_host) + src_on_guest = "#{shared_video_dir_on_guest}/#{video_name}" + dst_on_guest = "#{destination}/#{video_name}" + step "I copy \"#{src_on_guest}\" to \"#{dst_on_guest}\" as user \"amnesia\"" + end +end + +When /^I start Totem through the GNOME menu$/ do + next if @skip_steps_while_restoring_background + @screen.wait_and_click("GnomeApplicationsMenu.png", 10) + @screen.wait_and_click("GnomeApplicationsSoundVideo.png", 10) + @screen.wait_and_click("GnomeApplicationsTotem.png", 20) + @screen.wait_and_click("TotemMainWindow.png", 20) +end + +When /^I load the "([^"]+)" URL in Totem$/ do |url| + next if @skip_steps_while_restoring_background + @screen.type("l", Sikuli::KeyModifier.CTRL) + @screen.wait("TotemOpenUrlDialog.png", 10) + @screen.type(url + Sikuli::Key.ENTER) +end + +When /^I(?:| try to) open "([^"]+)" with Totem$/ do |filename| + next if @skip_steps_while_restoring_background + step "I run \"totem #{filename}\" in GNOME Terminal" +end + +When /^I close Totem$/ do + step 'I kill the process "totem"' +end diff --git a/features/step_definitions/truecrypt.rb b/features/step_definitions/truecrypt.rb new file mode 100644 index 00000000..bc8591bc --- /dev/null +++ b/features/step_definitions/truecrypt.rb @@ -0,0 +1,12 @@ +When /^I start TrueCrypt through the GNOME menu$/ do + next if @skip_steps_while_restoring_background + @screen.wait_and_click("GnomeApplicationsMenu.png", 10) + @screen.wait_and_click("GnomeApplicationsAccessories.png", 10) + @screen.wait_and_click("GnomeApplicationsTrueCrypt.png", 20) +end + +When /^I deal with the removal warning prompt$/ do + next if @skip_steps_while_restoring_background + @screen.wait("TrueCryptRemovalWarning.png", 60) + @screen.type(Sikuli::Key.ENTER) +end diff --git a/features/step_definitions/unsafe_browser.rb b/features/step_definitions/unsafe_browser.rb new file mode 100644 index 00000000..86f1c165 --- /dev/null +++ b/features/step_definitions/unsafe_browser.rb @@ -0,0 +1,154 @@ +When /^I see and accept the Unsafe Browser start verification$/ do + next if @skip_steps_while_restoring_background + @screen.wait("UnsafeBrowserStartVerification.png", 30) + @screen.type("l", Sikuli::KeyModifier.ALT) +end + +Then /^I see the Unsafe Browser start notification and wait for it to close$/ do + next if @skip_steps_while_restoring_background + @screen.wait("UnsafeBrowserStartNotification.png", 30) + @screen.waitVanish("UnsafeBrowserStartNotification.png", 10) +end + +Then /^the Unsafe Browser has started$/ do + next if @skip_steps_while_restoring_background + @screen.wait("UnsafeBrowserHomepage.png", 360) +end + +Then /^the Unsafe Browser has a red theme$/ do + next if @skip_steps_while_restoring_background + @screen.wait("UnsafeBrowserRedTheme.png", 10) +end + +Then /^the Unsafe Browser shows a warning as its start page$/ do + next if @skip_steps_while_restoring_background + @screen.wait("UnsafeBrowserStartPage.png", 10) +end + +When /^I start the Unsafe Browser$/ do + next if @skip_steps_while_restoring_background + @screen.wait_and_click("GnomeApplicationsMenu.png", 10) + @screen.wait_and_click("GnomeApplicationsInternet.png", 10) + @screen.wait_and_click("GnomeApplicationsUnsafeBrowser.png", 20) +end + +When /^I successfully start the Unsafe Browser$/ do + next if @skip_steps_while_restoring_background + step "I start the Unsafe Browser" + step "I see and accept the Unsafe Browser start verification" + step "I see the Unsafe Browser start notification and wait for it to close" + step "the Unsafe Browser has started" +end + +Then /^I see a warning about another instance already running$/ do + next if @skip_steps_while_restoring_background + @screen.wait('UnsafeBrowserWarnAlreadyRunning.png', 10) +end + +When /^I close the Unsafe Browser$/ do + next if @skip_steps_while_restoring_background + @screen.type("q", Sikuli::KeyModifier.CTRL) +end + +Then /^I see the Unsafe Browser stop notification$/ do + next if @skip_steps_while_restoring_background + @screen.wait('UnsafeBrowserStopNotification.png', 20) + @screen.waitVanish('UnsafeBrowserStopNotification.png', 10) +end + +Then /^I can start the Unsafe Browser again$/ do + next if @skip_steps_while_restoring_background + step "I start the Unsafe Browser" +end + +When /^I open a new tab in the Unsafe Browser$/ do + next if @skip_steps_while_restoring_background + @screen.wait_and_click("UnsafeBrowserWindow.png", 10) + @screen.type("t", Sikuli::KeyModifier.CTRL) +end + +When /^I open the address "([^"]*)" in the Unsafe Browser$/ do |address| + next if @skip_steps_while_restoring_background + step "I open a new tab in the Unsafe Browser" + @screen.type("l", Sikuli::KeyModifier.CTRL) + sleep 0.5 + @screen.type(address + Sikuli::Key.ENTER) +end + +# Workaround until the TBB shows the menu bar by default +# https://lists.torproject.org/pipermail/tor-qa/2014-October/000478.html +def show_unsafe_browser_menu_bar + try_for(15, :msg => "Failed to show the menu bar") do + @screen.type("h", Sikuli::KeyModifier.ALT) + @screen.find('UnsafeBrowserEditMenu.png') + end +end + +Then /^I cannot configure the Unsafe Browser to use any local proxies$/ do + next if @skip_steps_while_restoring_background + @screen.wait_and_click("UnsafeBrowserWindow.png", 10) + # First we open the proxy settings page to prepare it with the + # correct open tabs for the loop below. + show_unsafe_browser_menu_bar + @screen.hover('UnsafeBrowserEditMenu.png') + @screen.wait_and_click('UnsafeBrowserEditPreferences.png', 10) + @screen.wait('UnsafeBrowserPreferencesWindow.png', 10) + @screen.wait_and_click('UnsafeBrowserAdvancedSettings.png', 10) + @screen.wait_and_click('UnsafeBrowserNetworkTab.png', 10) + sleep 0.5 + @screen.type(Sikuli::Key.ESC) +# @screen.waitVanish('UnsafeBrowserPreferences.png', 10) + sleep 0.5 + + http_proxy = 'x' # Alt+x is the shortcut to select http proxy + socks_proxy = 'c' # Alt+c for socks proxy + no_proxy = 'y' # Alt+y for no proxy + + # Note: the loop below depends on that http_proxy is done after any + # other proxy types since it will set "Use this proxy server for all + # protocols", which will make the other proxy types unselectable. + proxies = [[socks_proxy, 9050], + [socks_proxy, 9061], + [socks_proxy, 9062], + [socks_proxy, 9150], + [http_proxy, 8118], + [no_proxy, 0]] + + proxies.each do |proxy| + proxy_type = proxy[0] + proxy_port = proxy[1] + + @screen.hide_cursor + + # Open proxy settings and select manual proxy configuration + show_unsafe_browser_menu_bar + @screen.hover('UnsafeBrowserEditMenu.png') + @screen.wait_and_click('UnsafeBrowserEditPreferences.png', 10) + @screen.wait('UnsafeBrowserPreferencesWindow.png', 10) + @screen.type("e", Sikuli::KeyModifier.ALT) + @screen.wait('UnsafeBrowserProxySettings.png', 10) + @screen.type("m", Sikuli::KeyModifier.ALT) + + # Configure the proxy + @screen.type(proxy_type, Sikuli::KeyModifier.ALT) # Select correct proxy type + @screen.type("127.0.0.1" + Sikuli::Key.TAB + "#{proxy_port}") if proxy_type != no_proxy + # For http proxy we set "Use this proxy server for all protocols" + @screen.type("s", Sikuli::KeyModifier.ALT) if proxy_type == http_proxy + + # Close settings + @screen.type(Sikuli::Key.ENTER) +# @screen.waitVanish('UnsafeBrowserProxySettings.png', 10) + sleep 0.5 + @screen.type(Sikuli::Key.ESC) +# @screen.waitVanish('UnsafeBrowserPreferences.png', 10) + sleep 0.5 + + # Test that the proxy settings work as they should + step "I open the address \"https://check.torproject.org\" in the Unsafe Browser" + if proxy_type == no_proxy + @screen.wait('UnsafeBrowserTorCheckFail.png', 60) + else + @screen.wait('UnsafeBrowserProxyRefused.png', 60) + end + end +end diff --git a/features/step_definitions/untrusted_partitions.rb b/features/step_definitions/untrusted_partitions.rb new file mode 100644 index 00000000..de2e0a70 --- /dev/null +++ b/features/step_definitions/untrusted_partitions.rb @@ -0,0 +1,35 @@ +Given /^I create a (\d+) ([[:alpha:]]+) disk named "([^"]+)"$/ do |size, unit, name| + next if @skip_steps_while_restoring_background + @vm.storage.create_new_disk(name, {:size => size, :unit => unit, + :type => "raw"}) +end + +Given /^I create a ([[:alpha:]]+) label on disk "([^"]+)"$/ do |type, name| + next if @skip_steps_while_restoring_background + @vm.storage.disk_mklabel(name, type) +end + +Given /^I create a ([[:alnum:]]+) filesystem on disk "([^"]+)"$/ do |type, name| + next if @skip_steps_while_restoring_background + @vm.storage.disk_mkpartfs(name, type) +end + +Given /^I cat an ISO hybrid of the Tails image to disk "([^"]+)"$/ do |name| + next if @skip_steps_while_restoring_background + disk_path = @vm.storage.disk_path(name) + tails_iso_hybrid = "#{$tmp_dir}/#{File.basename($tails_iso)}" + begin + cmd_helper("cp '#{$tails_iso}' '#{tails_iso_hybrid}'") + cmd_helper("isohybrid '#{tails_iso_hybrid}' --entry 4 --type 0x1c") + cmd_helper("dd if='#{tails_iso_hybrid}' of='#{disk_path}' conv=notrunc") + ensure + cmd_helper("rm -f '#{tails_iso_hybrid}'") + end +end + +Then /^drive "([^"]+)" is not mounted$/ do |name| + next if @skip_steps_while_restoring_background + dev = @vm.disk_dev(name) + assert(!@vm.execute("grep -qs '^#{dev}' /proc/mounts").success?, + "an untrusted partition from drive '#{name}' was automounted") +end diff --git a/features/step_definitions/usb.rb b/features/step_definitions/usb.rb new file mode 100644 index 00000000..f9f17ea2 --- /dev/null +++ b/features/step_definitions/usb.rb @@ -0,0 +1,492 @@ +def persistent_mounts + { + "cups-configuration" => "/etc/cups", + "nm-system-connections" => "/etc/NetworkManager/system-connections", + "claws-mail" => "/home/#{$live_user}/.claws-mail", + "gnome-keyrings" => "/home/#{$live_user}/.gnome2/keyrings", + "gnupg" => "/home/#{$live_user}/.gnupg", + "bookmarks" => "/home/#{$live_user}/.mozilla/firefox/bookmarks", + "pidgin" => "/home/#{$live_user}/.purple", + "openssh-client" => "/home/#{$live_user}/.ssh", + "Persistent" => "/home/#{$live_user}/Persistent", + "apt/cache" => "/var/cache/apt/archives", + "apt/lists" => "/var/lib/apt/lists", + } +end + +def persistent_volumes_mountpoints + @vm.execute("ls -1 -d /live/persistence/*_unlocked/").stdout.chomp.split +end + +Given /^I create a new (\d+) ([[:alpha:]]+) USB drive named "([^"]+)"$/ do |size, unit, name| + next if @skip_steps_while_restoring_background + @vm.storage.create_new_disk(name, {:size => size, :unit => unit}) +end + +Given /^I clone USB drive "([^"]+)" to a new USB drive "([^"]+)"$/ do |from, to| + next if @skip_steps_while_restoring_background + @vm.storage.clone_to_new_disk(from, to) +end + +Given /^I unplug USB drive "([^"]+)"$/ do |name| + next if @skip_steps_while_restoring_background + @vm.unplug_drive(name) +end + +Given /^the computer is set to boot from the old Tails DVD$/ do + next if @skip_steps_while_restoring_background + @vm.set_cdrom_boot($old_tails_iso) +end + +Given /^the computer is set to boot in UEFI mode$/ do + next if @skip_steps_while_restoring_background + @vm.set_os_loader('UEFI') + @os_loader = 'UEFI' +end + +class ISOHybridUpgradeNotSupported < StandardError +end + +def usb_install_helper(name) + @screen.wait('USBCreateLiveUSB.png', 10) + + # Here we'd like to select USB drive using #{name}, but Sikuli's + # OCR seems to be too unreliable. +# @screen.wait('USBTargetDevice.png', 10) +# match = @screen.find('USBTargetDevice.png') +# region_x = match.x +# region_y = match.y + match.h +# region_w = match.w*3 +# region_h = match.h*2 +# ocr = Sikuli::Region.new(region_x, region_y, region_w, region_h).text +# STDERR.puts ocr +# # Unfortunately this results in almost garbage, like "|]dev/sdm" +# # when it should be /dev/sda1 + + @screen.wait_and_click('USBCreateLiveUSB.png', 10) + if @screen.exists("USBSuggestsInstall.png") + raise ISOHybridUpgradeNotSupported + end + @screen.wait('USBCreateLiveUSBConfirmWindow.png', 10) + @screen.wait_and_click('USBCreateLiveUSBConfirmYes.png', 10) + @screen.wait('USBInstallationComplete.png', 60*60) +end + +When /^I start Tails Installer$/ do + next if @skip_steps_while_restoring_background + @screen.wait_and_click("GnomeApplicationsMenu.png", 10) + @screen.wait_and_click("GnomeApplicationsTails.png", 10) + @screen.wait_and_click("GnomeApplicationsTailsInstaller.png", 20) +end + +When /^I "Clone & Install" Tails to USB drive "([^"]+)"$/ do |name| + next if @skip_steps_while_restoring_background + step "I start Tails Installer" + @screen.wait_and_click('USBCloneAndInstall.png', 30) + usb_install_helper(name) +end + +When /^I "Clone & Upgrade" Tails to USB drive "([^"]+)"$/ do |name| + next if @skip_steps_while_restoring_background + step "I start Tails Installer" + @screen.wait_and_click('USBCloneAndUpgrade.png', 30) + usb_install_helper(name) +end + +When /^I try a "Clone & Upgrade" Tails to USB drive "([^"]+)"$/ do |name| + next if @skip_steps_while_restoring_background + begin + step "I \"Clone & Upgrade\" Tails to USB drive \"#{name}\"" + rescue ISOHybridUpgradeNotSupported + # this is what we expect + else + raise "The USB installer should not succeed" + end +end + +When /^I am suggested to do a "Clone & Install"$/ do + next if @skip_steps_while_restoring_background + @screen.find("USBSuggestsInstall.png") +end + +def shared_iso_dir_on_guest + "/tmp/shared_iso_dir" +end + +Given /^I setup a filesystem share containing the Tails ISO$/ do + next if @skip_steps_while_restoring_background + @vm.add_share(File.dirname($tails_iso), shared_iso_dir_on_guest) +end + +When /^I do a "Upgrade from ISO" on USB drive "([^"]+)"$/ do |name| + next if @skip_steps_while_restoring_background + step "I start Tails Installer" + @screen.wait_and_click('USBUpgradeFromISO.png', 10) + @screen.wait('USBUseLiveSystemISO.png', 10) + match = @screen.find('USBUseLiveSystemISO.png') + @screen.click(match.getCenter.offset(0, match.h*2)) + @screen.wait('USBSelectISO.png', 10) + @screen.wait_and_click('GnomeFileDiagTypeFilename.png', 10) + iso = "#{shared_iso_dir_on_guest}/#{File.basename($tails_iso)}" + @screen.type(iso + Sikuli::Key.ENTER) + usb_install_helper(name) +end + +Given /^I enable all persistence presets$/ do + next if @skip_steps_while_restoring_background + @screen.wait('PersistenceWizardPresets.png', 20) + # Mark first non-default persistence preset + @screen.type(Sikuli::Key.TAB*2) + # Check all non-default persistence presets + 12.times do + @screen.type(Sikuli::Key.SPACE + Sikuli::Key.TAB) + end + @screen.wait_and_click('PersistenceWizardSave.png', 10) + @screen.wait('PersistenceWizardDone.png', 20) + @screen.type(Sikuli::Key.F4, Sikuli::KeyModifier.ALT) +end + +Given /^I create a persistent partition with password "([^"]+)"$/ do |pwd| + next if @skip_steps_while_restoring_background + @screen.wait_and_click("GnomeApplicationsMenu.png", 10) + @screen.wait_and_click("GnomeApplicationsTails.png", 10) + @screen.wait_and_click("GnomeApplicationsConfigurePersistentVolume.png", 20) + @screen.wait('PersistenceWizardWindow.png', 40) + @screen.wait('PersistenceWizardStart.png', 20) + @screen.type(pwd + "\t" + pwd + Sikuli::Key.ENTER) + @screen.wait('PersistenceWizardPresets.png', 300) + step "I enable all persistence presets" +end + +def check_part_integrity(name, dev, usage, type, scheme, label) + info = @vm.execute("udisks --show-info #{dev}").stdout + info_split = info.split("\n partition:\n") + dev_info = info_split[0] + part_info = info_split[1] + assert(dev_info.match("^ usage: +#{usage}$"), + "Unexpected device field 'usage' on USB drive '#{name}', '#{dev}'") + assert(dev_info.match("^ type: +#{type}$"), + "Unexpected device field 'type' on USB drive '#{name}', '#{dev}'") + assert(part_info.match("^ scheme: +#{scheme}$"), + "Unexpected partition scheme on USB drive '#{name}', '#{dev}'") + assert(part_info.match("^ label: +#{label}$"), + "Unexpected partition label on USB drive '#{name}', '#{dev}'") +end + +def tails_is_installed_helper(name, tails_root, loader) + dev = @vm.disk_dev(name) + "1" + check_part_integrity(name, dev, "filesystem", "vfat", "gpt", "Tails") + + target_root = "/mnt/new" + @vm.execute("mkdir -p #{target_root}") + @vm.execute("mount #{dev} #{target_root}") + + c = @vm.execute("diff -qr '#{tails_root}/live' '#{target_root}/live'") + assert(c.success?, + "USB drive '#{name}' has differences in /live:\n#{c.stdout}") + + syslinux_files = @vm.execute("ls -1 #{target_root}/syslinux").stdout.chomp.split + # We deal with these files separately + ignores = ["syslinux.cfg", "exithelp.cfg", "ldlinux.sys"] + for f in syslinux_files - ignores do + c = @vm.execute("diff -q '#{tails_root}/#{loader}/#{f}' " + + "'#{target_root}/syslinux/#{f}'") + assert(c.success?, "USB drive '#{name}' has differences in " + + "'/syslinux/#{f}'") + end + + # The main .cfg is named differently vs isolinux + c = @vm.execute("diff -q '#{tails_root}/#{loader}/#{loader}.cfg' " + + "'#{target_root}/syslinux/syslinux.cfg'") + assert(c.success?, "USB drive '#{name}' has differences in " + + "'/syslinux/syslinux.cfg'") + + @vm.execute("umount #{target_root}") + @vm.execute("sync") +end + +Then /^the running Tails is installed on USB drive "([^"]+)"$/ do |target_name| + next if @skip_steps_while_restoring_background + loader = boot_device_type == "usb" ? "syslinux" : "isolinux" + tails_is_installed_helper(target_name, "/lib/live/mount/medium", loader) +end + +Then /^the ISO's Tails is installed on USB drive "([^"]+)"$/ do |target_name| + next if @skip_steps_while_restoring_background + iso = "#{shared_iso_dir_on_guest}/#{File.basename($tails_iso)}" + iso_root = "/mnt/iso" + @vm.execute("mkdir -p #{iso_root}") + @vm.execute("mount -o loop #{iso} #{iso_root}") + tails_is_installed_helper(target_name, iso_root, "isolinux") + @vm.execute("umount #{iso_root}") +end + +Then /^there is no persistence partition on USB drive "([^"]+)"$/ do |name| + next if @skip_steps_while_restoring_background + data_part_dev = @vm.disk_dev(name) + "2" + assert(!@vm.execute("test -b #{data_part_dev}").success?, + "USB drive #{name} has a partition '#{data_part_dev}'") +end + +Then /^a Tails persistence partition with password "([^"]+)" exists on USB drive "([^"]+)"$/ do |pwd, name| + next if @skip_steps_while_restoring_background + dev = @vm.disk_dev(name) + "2" + check_part_integrity(name, dev, "crypto", "crypto_LUKS", "gpt", "TailsData") + + # The LUKS container may already be opened, e.g. by udisks after + # we've run tails-persistence-setup. + c = @vm.execute("ls -1 /dev/mapper/") + if c.success? + for candidate in c.stdout.split("\n") + luks_info = @vm.execute("cryptsetup status #{candidate}") + if luks_info.success? and luks_info.stdout.match("^\s+device:\s+#{dev}$") + luks_dev = "/dev/mapper/#{candidate}" + break + end + end + end + if luks_dev.nil? + c = @vm.execute("echo #{pwd} | cryptsetup luksOpen #{dev} #{name}") + assert(c.success?, "Couldn't open LUKS device '#{dev}' on drive '#{name}'") + luks_dev = "/dev/mapper/#{name}" + end + + # Adapting check_part_integrity() seems like a bad idea so here goes + info = @vm.execute("udisks --show-info #{luks_dev}").stdout + assert info.match("^ cleartext luks device:$") + assert info.match("^ usage: +filesystem$") + assert info.match("^ type: +ext[34]$") + assert info.match("^ label: +TailsData$") + + mount_dir = "/mnt/#{name}" + @vm.execute("mkdir -p #{mount_dir}") + c = @vm.execute("mount #{luks_dev} #{mount_dir}") + assert(c.success?, + "Couldn't mount opened LUKS device '#{dev}' on drive '#{name}'") + + @vm.execute("umount #{mount_dir}") + @vm.execute("sync") + @vm.execute("cryptsetup luksClose #{name}") +end + +Given /^I enable persistence with password "([^"]+)"$/ do |pwd| + next if @skip_steps_while_restoring_background + @screen.wait('TailsGreeterPersistence.png', 10) + @screen.type(Sikuli::Key.SPACE) + @screen.wait('TailsGreeterPersistencePassphrase.png', 10) + match = @screen.find('TailsGreeterPersistencePassphrase.png') + @screen.click(match.getCenter.offset(match.w*2, match.h/2)) + @screen.type(pwd) +end + +def tails_persistence_enabled? + persistence_state_file = "/var/lib/live/config/tails.persistence" + return @vm.execute("test -e '#{persistence_state_file}'").success? && + @vm.execute('. #{persistence_state_file} && ' + + 'test "$TAILS_PERSISTENCE_ENABLED" = true').success? +end + +Given /^persistence is enabled$/ do + next if @skip_steps_while_restoring_background + try_for(120, :msg => "Persistence is disabled") do + tails_persistence_enabled? + end + # Check that all persistent directories are mounted + mount = @vm.execute("mount").stdout.chomp + for _, dir in persistent_mounts do + assert(mount.include?("on #{dir} "), + "Persistent directory '#{dir}' is not mounted") + end +end + +Given /^persistence is disabled$/ do + next if @skip_steps_while_restoring_background + assert(!tails_persistence_enabled?, "Persistence is enabled") +end + +Given /^I enable read-only persistence with password "([^"]+)"$/ do |pwd| + step "I enable persistence with password \"#{pwd}\"" + next if @skip_steps_while_restoring_background + @screen.wait_and_click('TailsGreeterPersistenceReadOnly.png', 10) +end + +def boot_device + # Approach borrowed from + # config/chroot_local_includes/lib/live/config/998-permissions + boot_dev_id = @vm.execute("udevadm info --device-id-of-file=/lib/live/mount/medium").stdout.chomp + boot_dev = @vm.execute("readlink -f /dev/block/'#{boot_dev_id}'").stdout.chomp + return boot_dev +end + +def boot_device_type + # Approach borrowed from + # config/chroot_local_includes/lib/live/config/998-permissions + boot_dev_info = @vm.execute("udevadm info --query=property --name='#{boot_device}'").stdout.chomp + boot_dev_type = (boot_dev_info.split("\n").select { |x| x.start_with? "ID_BUS=" })[0].split("=")[1] + return boot_dev_type +end + +Then /^Tails is running from USB drive "([^"]+)"$/ do |name| + next if @skip_steps_while_restoring_background + assert_equal("usb", boot_device_type) + actual_dev = boot_device + # The boot partition differs between a "normal" install using the + # USB installer and isohybrid installations + expected_dev_normal = @vm.disk_dev(name) + "1" + expected_dev_isohybrid = @vm.disk_dev(name) + "4" + assert(actual_dev == expected_dev_normal || + actual_dev == expected_dev_isohybrid, + "We are running from device #{actual_dev}, but for USB drive " + + "'#{name}' we expected to run from either device " + + "#{expected_dev_normal} (when installed via the USB installer) " + + "or #{expected_dev_normal} (when installed from an isohybrid)") +end + +Then /^the boot device has safe access rights$/ do + next if @skip_steps_while_restoring_background + + super_boot_dev = boot_device.sub(/[[:digit:]]+$/, "") + devs = @vm.execute("ls -1 #{super_boot_dev}*").stdout.chomp.split + assert(devs.size > 0, "Could not determine boot device") + all_users = @vm.execute("cut -d':' -f1 /etc/passwd").stdout.chomp.split + all_users_with_groups = all_users.collect do |user| + groups = @vm.execute("groups #{user}").stdout.chomp.sub(/^#{user} : /, "").split(" ") + [user, groups] + end + for dev in devs do + dev_owner = @vm.execute("stat -c %U #{dev}").stdout.chomp + dev_group = @vm.execute("stat -c %G #{dev}").stdout.chomp + dev_perms = @vm.execute("stat -c %a #{dev}").stdout.chomp + assert_equal("root", dev_owner) + assert(dev_group == "disk" || dev_group == "root", + "Boot device '#{dev}' owned by group '#{dev_group}', expected " + + "'disk' or 'root'.") + assert_equal("1660", dev_perms) + for user, groups in all_users_with_groups do + next if user == "root" + assert(!(groups.include?(dev_group)), + "Unprivileged user '#{user}' is in group '#{dev_group}' which " + + "owns boot device '#{dev}'") + end + end + + info = @vm.execute("udisks --show-info #{super_boot_dev}").stdout + assert(info.match("^ system internal: +1$"), + "Boot device '#{super_boot_dev}' is not system internal for udisks") +end + +Then /^persistent filesystems have safe access rights$/ do + persistent_volumes_mountpoints.each do |mountpoint| + fs_owner = @vm.execute("stat -c %U #{mountpoint}").stdout.chomp + fs_group = @vm.execute("stat -c %G #{mountpoint}").stdout.chomp + fs_perms = @vm.execute("stat -c %a #{mountpoint}").stdout.chomp + assert_equal("root", fs_owner) + assert_equal("root", fs_group) + assert_equal('775', fs_perms) + end +end + +Then /^persistence configuration files have safe access rights$/ do + persistent_volumes_mountpoints.each do |mountpoint| + assert(@vm.execute("test -e #{mountpoint}/persistence.conf").success?, + "#{mountpoint}/persistence.conf does not exist, while it should") + assert(@vm.execute("test ! -e #{mountpoint}/live-persistence.conf").success?, + "#{mountpoint}/live-persistence.conf does exist, while it should not") + @vm.execute( + "ls -1 #{mountpoint}/persistence.conf #{mountpoint}/live-*.conf" + ).stdout.chomp.split.each do |f| + file_owner = @vm.execute("stat -c %U '#{f}'").stdout.chomp + file_group = @vm.execute("stat -c %G '#{f}'").stdout.chomp + file_perms = @vm.execute("stat -c %a '#{f}'").stdout.chomp + assert_equal("tails-persistence-setup", file_owner) + assert_equal("tails-persistence-setup", file_group) + assert_equal("600", file_perms) + end + end +end + +Then /^persistent directories have safe access rights$/ do + next if @skip_steps_while_restoring_background + expected_perms = "700" + persistent_volumes_mountpoints.each do |mountpoint| + # We also want to check that dotfiles' source has safe permissions + all_persistent_dirs = persistent_mounts.clone + all_persistent_dirs["dotfiles"] = "/home/#{$live_user}/" + persistent_mounts.each do |src, dest| + next unless dest.start_with?("/home/#{$live_user}/") + f = "#{mountpoint}/#{src}" + next unless @vm.execute("test -d #{f}").success? + file_perms = @vm.execute("stat -c %a '#{f}'").stdout.chomp + assert_equal(expected_perms, file_perms) + end + end +end + +When /^I write some files expected to persist$/ do + next if @skip_steps_while_restoring_background + persistent_mounts.each do |_, dir| + owner = @vm.execute("stat -c %U #{dir}").stdout.chomp + assert(@vm.execute("touch #{dir}/XXX_persist", user=owner).success?, + "Could not create file in persistent directory #{dir}") + end +end + +When /^I remove some files expected to persist$/ do + next if @skip_steps_while_restoring_background + persistent_mounts.each do |_, dir| + owner = @vm.execute("stat -c %U #{dir}").stdout.chomp + assert(@vm.execute("rm #{dir}/XXX_persist", user=owner).success?, + "Could not remove file in persistent directory #{dir}") + end +end + +When /^I write some files not expected to persist$/ do + next if @skip_steps_while_restoring_background + persistent_mounts.each do |_, dir| + owner = @vm.execute("stat -c %U #{dir}").stdout.chomp + assert(@vm.execute("touch #{dir}/XXX_gone", user=owner).success?, + "Could not create file in persistent directory #{dir}") + end +end + +Then /^the expected persistent files are present in the filesystem$/ do + next if @skip_steps_while_restoring_background + persistent_mounts.each do |_, dir| + assert(@vm.execute("test -e #{dir}/XXX_persist").success?, + "Could not find expected file in persistent directory #{dir}") + assert(!@vm.execute("test -e #{dir}/XXX_gone").success?, + "Found file that should not have persisted in persistent directory #{dir}") + end +end + +Then /^only the expected files should persist on USB drive "([^"]+)"$/ do |name| + next if @skip_steps_while_restoring_background + step "a computer" + step "the computer is set to boot from USB drive \"#{name}\"" + step "the network is unplugged" + step "I start the computer" + step "the computer boots Tails" + step "I enable read-only persistence with password \"asdf\"" + step "I log in to a new session" + step "persistence is enabled" + step "GNOME has started" + step "all notifications have disappeared" + step "the expected persistent files are present in the filesystem" + step "I shutdown Tails and wait for the computer to power off" +end + +When /^I delete the persistent partition$/ do + next if @skip_steps_while_restoring_background + @screen.wait_and_click("GnomeApplicationsMenu.png", 10) + @screen.wait_and_click("GnomeApplicationsTails.png", 10) + @screen.wait_and_click("GnomeApplicationsDeletePersistentVolume.png", 20) + @screen.wait("PersistenceWizardWindow.png", 40) + @screen.wait("PersistenceWizardDeletionStart.png", 20) + @screen.type(" ") + @screen.wait("PersistenceWizardDone.png", 120) +end + +Then /^Tails has started in UEFI mode$/ do + assert(@vm.execute("test -d /sys/firmware/efi").success?, + "/sys/firmware/efi does not exist") + end diff --git a/features/step_definitions/windows_camouflage.rb b/features/step_definitions/windows_camouflage.rb new file mode 100644 index 00000000..82ccd8c8 --- /dev/null +++ b/features/step_definitions/windows_camouflage.rb @@ -0,0 +1,10 @@ +Given /^I enable Microsoft Windows camouflage$/ do + @theme = "windows" + next if @skip_steps_while_restoring_background + @screen.wait_and_click("TailsGreeterWindowsCamouflage.png", 10) +end + +When /^I click the start menu$/ do + next if @skip_steps_while_restoring_background + @screen.wait_and_click("WindowsStartButton.png", 10) +end diff --git a/features/support/config.rb b/features/support/config.rb new file mode 100644 index 00000000..b5f6fcd9 --- /dev/null +++ b/features/support/config.rb @@ -0,0 +1,34 @@ +require 'fileutils' +require "#{Dir.pwd}/features/support/helpers/misc_helpers.rb" + +# Dynamic +$tails_iso = ENV['ISO'] || get_newest_iso +$old_tails_iso = ENV['OLD_ISO'] || get_oldest_iso +$tmp_dir = ENV['TEMP_DIR'] || "/tmp/TailsToaster" +$vm_xml_path = ENV['VM_XML_PATH'] || "#{Dir.pwd}/features/domains" +$misc_files_dir = "#{Dir.pwd}/features/misc_files" +$keep_snapshots = !ENV['KEEP_SNAPSHOTS'].nil? +$x_display = ENV['DISPLAY'] +$debug = !ENV['DEBUG'].nil? +$pause_on_fail = !ENV['PAUSE_ON_FAIL'].nil? +$time_at_start = Time.now +$live_user = cmd_helper(". config/chroot_local-includes/etc/live/config.d/username.conf; echo ${LIVE_USERNAME}").chomp +$sikuli_retry_findfailed = !ENV['SIKULI_RETRY_FINDFAILED'].nil? + +# Static +$configured_keyserver_hostname = 'hkps.pool.sks-keyservers.net' +$services_expected_on_all_ifaces = + [ + ["cupsd", "0.0.0.0", "631"], + ["dhclient", "0.0.0.0", "*"] + ] +$tor_authorities = + # List grabbed from Tor's sources, src/or/config.c:~750. + [ + "128.31.0.39", "86.59.21.38", "194.109.206.212", + "82.94.251.203", "76.73.17.194", "212.112.245.170", + "193.23.244.244", "208.83.223.34", "171.25.193.9", + "154.35.32.5" + ] +# OpenDNS +$some_dns_server = "208.67.222.222" diff --git a/features/support/env.rb b/features/support/env.rb new file mode 100644 index 00000000..523a1d1c --- /dev/null +++ b/features/support/env.rb @@ -0,0 +1,53 @@ +require 'rubygems' +require "#{Dir.pwd}/features/support/extra_hooks.rb" +require 'time' +require 'rspec' + +def fatal_system(str) + unless system(str) + raise StandardError.new("Command exited with #{$?}") + end +end + +def git_exists? + File.exists? '.git' +end + +def create_git + Dir.mkdir 'debian' + File.open('debian/changelog', 'w') do |changelog| + changelog.write(<<END_OF_CHANGELOG) +tails (0) stable; urgency=low + + * First release. + + -- Tails developers <tails@boum.org> Mon, 30 Jan 2012 01:00:00 +0000 +END_OF_CHANGELOG + end + + fatal_system "git init --quiet" + fatal_system "git config user.email 'tails@boum.org'" + fatal_system "git config user.name 'Tails developers'" + fatal_system "git add debian/changelog" + fatal_system "git commit --quiet debian/changelog -m 'First release'" + fatal_system "git branch -M stable" + fatal_system "git branch testing stable" + fatal_system "git branch devel stable" + fatal_system "git branch experimental devel" +end + +RSpec::Matchers.define :have_suite do |suite| + match do |string| + # e.g.: `deb http://deb.tails.boum.org/ 0.10 main contrib non-free` + %r{^deb +http://deb\.tails\.boum\.org/ +#{Regexp.escape(suite)} main}.match(string) + end + failure_message_for_should do |string| + "expected the sources to include #{suite}\nCurrent sources : #{string}" + end + failure_message_for_should_not do |string| + "expected the sources to exclude #{suite}\nCurrent sources : #{string}" + end + description do + "expected an output with #{suite}" + end +end diff --git a/features/support/extra_hooks.rb b/features/support/extra_hooks.rb new file mode 100644 index 00000000..a8addb35 --- /dev/null +++ b/features/support/extra_hooks.rb @@ -0,0 +1,45 @@ +require 'cucumber/formatter/pretty' + +# Sort of inspired by Cucumber::RbSupport::RbHook, but really we just +# want an object with a 'tag_expressions' attribute to make +# accept_hook?() (used below) happy. +class SimpleHook + attr_reader :tag_expressions + + def initialize(tag_expressions, proc) + @tag_expressions = tag_expressions + @proc = proc + end + + def invoke(arg) + @proc.call(arg) + end +end + +def BeforeFeature(*tag_expressions, &block) + $before_feature_hooks ||= [] + $before_feature_hooks << SimpleHook.new(tag_expressions, block) +end + +def AfterFeature(*tag_expressions, &block) + $after_feature_hooks ||= [] + $after_feature_hooks << SimpleHook.new(tag_expressions, block) +end + +module ExtraHooks + class Pretty < Cucumber::Formatter::Pretty + def before_feature(feature) + for hook in $before_feature_hooks do + hook.invoke(feature) if feature.accept_hook?(hook) + end + super if defined?(super) + end + + def after_feature(feature) + for hook in $after_feature_hooks do + hook.invoke(feature) if feature.accept_hook?(hook) + end + super if defined?(super) + end + end +end diff --git a/features/support/helpers/display_helper.rb b/features/support/helpers/display_helper.rb new file mode 100644 index 00000000..354935f0 --- /dev/null +++ b/features/support/helpers/display_helper.rb @@ -0,0 +1,51 @@ + +class Display + + def initialize(domain, x_display) + @domain = domain + @x_display = x_display + end + + def start + start_virtviewer(@domain) + # We wait for the display to be active to not lose actions + # (e.g. key presses via sikuli) that come immediately after + # starting (or restoring) a vm + try_for(20, { :delay => 0.1, :msg => "virt-viewer failed to start"}) { + active? + } + end + + def stop + stop_virtviewer + end + + def restart + stop_virtviewer + start_virtviewer(@domain) + end + + def start_virtviewer(domain) + # virt-viewer forks, so we cannot (easily) get the child pid + # and use it in active? and stop_virtviewer below... + IO.popen(["virt-viewer", "-d", + "-f", + "-r", + "-c", "qemu:///system", + ["--display=", @x_display].join(''), + domain, + "&"].join(' ')) + end + + def active? + p = IO.popen("xprop -display #{@x_display} " + + "-name '#{@domain} (1) - Virt Viewer' 2>/dev/null") + Process.wait(p.pid) + p.close + $? == 0 + end + + def stop_virtviewer + system("killall virt-viewer") + end +end diff --git a/features/support/helpers/exec_helper.rb b/features/support/helpers/exec_helper.rb new file mode 100644 index 00000000..b0d3a9cd --- /dev/null +++ b/features/support/helpers/exec_helper.rb @@ -0,0 +1,61 @@ +require 'json' +require 'socket' + +class VMCommand + + attr_reader :cmd, :returncode, :stdout, :stderr + + def initialize(vm, cmd, options = {}) + @cmd = cmd + @returncode, @stdout, @stderr = VMCommand.execute(vm, cmd, options) + end + + def VMCommand.wait_until_remote_shell_is_up(vm, timeout = 30) + begin + Timeout::timeout(timeout) do + VMCommand.execute(vm, "true", { :user => "root", :spawn => false }) + end + rescue Timeout::Error + raise "Remote shell seems to be down" + end + end + + # The parameter `cmd` cannot contain newlines. Separate multiple + # commands using ";" instead. + # If `:spawn` is false the server will block until it has finished + # executing `cmd`. If it's true the server won't block, and the + # response will always be [0, "", ""] (only used as an + # ACK). execute() will always block until a response is received, + # though. Spawning is useful when starting processes in the + # background (or running scripts that does the same) like the + # vidalia-wrapper, or any application we want to interact with. + def VMCommand.execute(vm, cmd, options = {}) + options[:user] ||= "root" + options[:spawn] ||= false + type = options[:spawn] ? "spawn" : "call" + socket = TCPSocket.new("127.0.0.1", vm.get_remote_shell_port) + STDERR.puts "#{type}ing as #{options[:user]}: #{cmd}" if $debug + begin + socket.puts(JSON.dump([type, options[:user], cmd])) + s = socket.readline(sep = "\0").chomp("\0") + ensure + socket.close + end + STDERR.puts "#{type} returned: #{s}" if $debug + begin + return JSON.load(s) + rescue JSON::ParserError + # The server often returns something unparsable for the very + # first execute() command issued after a VM start/restore + # (generally from wait_until_remote_shell_is_up()) presumably + # because the TCP -> serial link isn't properly setup yet. All + # will be well after that initial hickup, so we just retry. + return VMCommand.execute(vm, cmd, options) + end + end + + def success? + return @returncode == 0 + end + +end diff --git a/features/support/helpers/firewall_helper.rb b/features/support/helpers/firewall_helper.rb new file mode 100644 index 00000000..400965a5 --- /dev/null +++ b/features/support/helpers/firewall_helper.rb @@ -0,0 +1,100 @@ +require 'packetfu' +require 'ipaddr' + +# Extent IPAddr with a private/public address space checks +class IPAddr + PrivateIPv4Ranges = [ + IPAddr.new("10.0.0.0/8"), + IPAddr.new("172.16.0.0/12"), + IPAddr.new("192.168.0.0/16"), + IPAddr.new("255.255.255.255/32") + ] + + PrivateIPv6Ranges = [ + IPAddr.new("fc00::/7"), # private + ] + + def private? + if self.ipv4? + PrivateIPv4Ranges.each do |ipr| + return true if ipr.include?(self) + end + return false + else + PrivateIPv6Ranges.each do |ipr| + return true if ipr.include?(self) + end + return false + end + end + + def public? + !private? + end +end + +class FirewallLeakCheck + attr_reader :ipv4_tcp_leaks, :ipv4_nontcp_leaks, :ipv6_leaks, :nonip_leaks + + def initialize(pcap_file, tor_relays) + packets = PacketFu::PcapFile.new.file_to_array(:filename => pcap_file) + @tor_relays = tor_relays + ipv4_tcp_packets = [] + ipv4_nontcp_packets = [] + ipv6_packets = [] + nonip_packets = [] + packets.each do |p| + if PacketFu::TCPPacket.can_parse?(p) + ipv4_tcp_packets << PacketFu::TCPPacket.parse(p) + elsif PacketFu::IPPacket.can_parse?(p) + ipv4_nontcp_packets << PacketFu::IPPacket.parse(p) + elsif PacketFu::IPv6Packet.can_parse?(p) + ipv6_packets << PacketFu::IPv6Packet.parse(p) + elsif PacketFu::Packet.can_parse?(p) + nonip_packets << PacketFu::Packet.parse(p) + else + save_pcap_file + raise "Found something in the pcap file that cannot be parsed" + end + end + ipv4_tcp_hosts = get_public_hosts_from_ippackets ipv4_tcp_packets + tor_nodes = Set.new(get_all_tor_contacts) + @ipv4_tcp_leaks = ipv4_tcp_hosts.select{|host| !tor_nodes.member?(host)} + @ipv4_nontcp_leaks = get_public_hosts_from_ippackets ipv4_nontcp_packets + @ipv6_leaks = get_public_hosts_from_ippackets ipv6_packets + @nonip_leaks = nonip_packets + end + + # Returns a list of all unique non-LAN destination IP addresses + # found in `packets`. + def get_public_hosts_from_ippackets(packets) + hosts = [] + packets.each do |p| + candidate = nil + if p.kind_of?(PacketFu::IPPacket) + candidate = p.ip_daddr + elsif p.kind_of?(PacketFu::IPv6Packet) + candidate = p.ipv6_header.ipv6_daddr + else + save_pcap_file + raise "Expected an IP{v4,v6} packet, but got something else:\n" + + p.peek_format + end + if candidate != nil and IPAddr.new(candidate).public? + hosts << candidate + end + end + hosts.uniq + end + + # Returns an array of all Tor relays and authorities, i.e. all + # Internet hosts Tails ever should contact. + def get_all_tor_contacts + @tor_relays + $tor_authorities + end + + def empty? + @ipv4_tcp_leaks.empty? and @ipv4_nontcp_leaks.empty? and @ipv6_leaks.empty? and @nonip_leaks.empty? + end + +end diff --git a/features/support/helpers/misc_helpers.rb b/features/support/helpers/misc_helpers.rb new file mode 100644 index 00000000..caf64b80 --- /dev/null +++ b/features/support/helpers/misc_helpers.rb @@ -0,0 +1,121 @@ +require 'date' +require 'timeout' +require 'test/unit' + +# Make all the assert_* methods easily accessible in any context. +include Test::Unit::Assertions + +def assert_vmcommand_success(p, msg = nil) + assert(p.success?, msg.nil? ? "Command failed: #{p.cmd}\n" + \ + "error code: #{p.returncode}\n" \ + "stderr: #{p.stderr}" : \ + msg) +end + +# Call block (ignoring any exceptions it may throw) repeatedly with one +# second breaks until it returns true, or until `t` seconds have +# passed when we throw Timeout::Error. As a precondition, the code +# block cannot throw Timeout::Error. +def try_for(t, options = {}) + options[:delay] ||= 1 + begin + Timeout::timeout(t) do + loop do + begin + return true if yield + rescue Timeout::Error => e + if options[:msg] + raise RuntimeError, options[:msg], caller + else + raise e + end + rescue Exception + # noop + end + sleep options[:delay] + end + end + rescue Timeout::Error => e + if options[:msg] + raise RuntimeError, options[:msg], caller + else + raise e + end + end +end + +def wait_until_tor_is_working + try_for(240) { @vm.execute( + '. /usr/local/lib/tails-shell-library/tor.sh; tor_is_working').success? } +end + +def convert_bytes_mod(unit) + case unit + when "bytes", "b" then mod = 1 + when "KB" then mod = 10**3 + when "k", "KiB" then mod = 2**10 + when "MB" then mod = 10**6 + when "M", "MiB" then mod = 2**20 + when "GB" then mod = 10**9 + when "G", "GiB" then mod = 2**30 + when "TB" then mod = 10**12 + when "T", "TiB" then mod = 2**40 + else + raise "invalid memory unit '#{unit}'" + end + return mod +end + +def convert_to_bytes(size, unit) + return (size*convert_bytes_mod(unit)).to_i +end + +def convert_to_MiB(size, unit) + return (size*convert_bytes_mod(unit) / (2**20)).to_i +end + +def convert_from_bytes(size, unit) + return size.to_f/convert_bytes_mod(unit).to_f +end + +def cmd_helper(cmd) + IO.popen(cmd + " 2>&1") do |p| + out = p.readlines.join("\n") + p.close + ret = $? + assert_equal(0, ret, "Command failed (returned #{ret}): #{cmd}:\n#{out}") + return out + end +end + +def tails_iso_creation_date(path) + label = cmd_helper("/sbin/blkid -p -s LABEL -o value #{path}") + assert(label[/^TAILS \d+(\.\d+)+(~rc\d+)? - \d+$/], + "Got invalid label '#{label}' from Tails image '#{path}'") + return label[/\d+$/] +end + +def sort_isos_by_creation_date + Dir.glob("#{Dir.pwd}/*.iso").sort_by {|f| tails_iso_creation_date(f)} +end + +def get_newest_iso + return sort_isos_by_creation_date.last +end + +def get_oldest_iso + return sort_isos_by_creation_date.first +end + +# This command will grab all router IP addresses from the Tor +# consensus in the VM. +def get_tor_relays + cmd = 'awk "/^r/ { print \$6 }" /var/lib/tor/cached-microdesc-consensus' + @vm.execute(cmd).stdout.chomp.split("\n") +end + +def save_pcap_file + pcap_copy = "#{$tmp_dir}/pcap_with_leaks-#{DateTime.now}" + FileUtils.cp(@sniffer.pcap_file, pcap_copy) + puts "Full network capture available at: #{pcap_copy}" +end diff --git a/features/support/helpers/net_helper.rb b/features/support/helpers/net_helper.rb new file mode 100644 index 00000000..29119195 --- /dev/null +++ b/features/support/helpers/net_helper.rb @@ -0,0 +1,42 @@ +# +# Sniffer is a very dumb wrapper to start and stop tcpdumps instances, possibly +# with customized filters. Captured traffic is stored in files whose name +# depends on the sniffer name. The resulting captured packets for each sniffers +# can be accessed as an array through its `packets` method. +# +# Use of more rubyish internal ways to sniff a network like with pcap-able gems +# is waaay to much resource consumming, notmuch reliable and soooo slow. Let's +# not bother too much with that. :) +# +# Should put all that in a Module. + +class Sniffer + + attr_reader :name, :pcap_file, :pid + + def initialize(name, bridge_name) + @name = name + @bridge_name = bridge_name + @bridge_mac = File.open("/sys/class/net/#{@bridge_name}/address", "rb").read.chomp + @pcap_file = "#{$tmp_dir}/#{name}.pcap" + end + + def capture(filter="not ether src host #{@bridge_mac} and not ether proto \\arp and not ether proto \\rarp") + job = IO.popen("/usr/sbin/tcpdump -n -i #{@bridge_name} -w #{@pcap_file} -U '#{filter}' >/dev/null 2>&1") + @pid = job.pid + end + + def stop + begin + Process.kill("TERM", @pid) + rescue + # noop + end + end + + def clear + if File.exist?(@pcap_file) + File.delete(@pcap_file) + end + end +end diff --git a/features/support/helpers/sikuli_helper.rb b/features/support/helpers/sikuli_helper.rb new file mode 100644 index 00000000..f6211be7 --- /dev/null +++ b/features/support/helpers/sikuli_helper.rb @@ -0,0 +1,145 @@ +require 'rjb' +require 'rjbextension' +$LOAD_PATH << ENV['SIKULI_HOME'] +require 'sikuli-script.jar' +Rjb::load + +package_members = [ + "org.sikuli.script.Finder", + "org.sikuli.script.Key", + "org.sikuli.script.KeyModifier", + "org.sikuli.script.Location", + "org.sikuli.script.Match", + "org.sikuli.script.Pattern", + "org.sikuli.script.Region", + "org.sikuli.script.Screen", + "org.sikuli.script.Settings", + ] + +translations = Hash[ + "org.sikuli.script", "Sikuli", + ] + +for p in package_members + imported_class = Rjb::import(p) + package, ignore, class_name = p.rpartition(".") + next if ! translations.include? package + mod_name = translations[package] + mod = mod_name.split("::").inject(Object) do |parent_obj, child_name| + if parent_obj.const_defined?(child_name, false) + parent_obj.const_get(child_name, false) + else + child_obj = Module.new + parent_obj.const_set(child_name, child_obj) + end + end + mod.const_set(class_name, imported_class) +end + +def findfailed_hook(pic) + STDERR.puts "" + STDERR.puts "FindFailed for: #{pic}" + STDERR.puts "" + STDERR.puts "Update the image and press RETURN to retry" + STDIN.gets +end + +# Since rjb imports Java classes without creating a corresponding +# Ruby class (it's just an instance of Rjb_JavaProxy) we can't +# monkey patch any class, so additional methods must be added +# to each Screen object. +# +# All Java classes' methods are immediately available in the proxied +# Ruby classes, but care has to be given to match their type. For a +# list of methods, see: <http://doc.sikuli.org/javadoc/index.html>. +# The type "PRSML" is a union of Pattern, Region, Screen, Match and +# Location. +# +# Also, due to limitations in Ruby's syntax we can't do: +# def Sikuli::Screen.new +# so we work around it with the following vairable. +sikuli_script_proxy = Sikuli::Screen +$_original_sikuli_screen_new ||= Sikuli::Screen.method :new + +def sikuli_script_proxy.new(*args) + s = $_original_sikuli_screen_new.call(*args) + + if $sikuli_retry_findfailed + # The usage of `_invoke()` below exemplifies how one can wrap + # around Java objects' methods when they're imported using RJB. It + # isn't pretty. The seconds argument is the parameter signature, + # which can be obtained by creating the intended Java object using + # RJB, and then calling its `java_methods` method. + + def s.wait(pic, time) + self._invoke('wait', 'Ljava.lang.Object;D', pic, time) + rescue FindFailed => e + findfailed_hook(pic) + self._invoke('wait', 'Ljava.lang.Object;D', pic, time) + end + + def s.find(pic) + self._invoke('find', 'Ljava.lang.Object;', pic) + rescue FindFailed => e + findfailed_hook(pic) + self._invoke('find', 'Ljava.lang.Object;', pic) + end + + def s.waitVanish(pic, time) + self._invoke('waitVanish', 'Ljava.lang.Object;D', pic, time) + rescue FindFailed => e + findfailed_hook(pic) + self._invoke('waitVanish', 'Ljava.lang.Object;D', pic, time) + end + + def s.click(pic) + self._invoke('click', 'Ljava.lang.Object;', pic) + rescue FindFailed => e + findfailed_hook(pic) + self._invoke('click', 'Ljava.lang.Object;', pic) + end + end + + def s.click_point(x, y) + self.click(Sikuli::Location.new(x, y)) + end + + def s.wait_and_click(pic, time) + self.click(self.wait(pic, time)) + end + + def s.wait_and_double_click(pic, time) + self.doubleClick(self.wait(pic, time)) + end + + def s.hover_point(x, y) + self.hover(Sikuli::Location.new(x, y)) + end + + def s.hide_cursor + self.hover_point(self.w, self.h/2) + end + + s +end + +# Configure sikuli +java.lang.System.setProperty("SIKULI_IMAGE_PATH", "#{Dir.pwd}/features/images/") + +# ruby and rjb doesn't play well together when it comes to static +# fields (and possibly methods) so we instantiate and access the field +# via objects instead. It actually works inside this file, but when +# it's required from "outside", and the file has been completely +# required, ruby's require method complains that the method for the +# field accessor is missing. +sikuli_settings = Sikuli::Settings.new +sikuli_settings.OcrDataPath = $tmp_dir +# sikuli_ruby, which we used before, defaulted to 0.9 minimum +# similarity, so all our current images are adapted to that value. +# Also, Sikuli's default of 0.7 is simply too low (many false +# positives). +sikuli_settings.MinSimilarity = 0.9 +sikuli_settings.ActionLogs = $debug +sikuli_settings.DebugLogs = $debug +sikuli_settings.InfoLogs = $debug +sikuli_settings.ProfileLogs = $debug diff --git a/features/support/helpers/storage_helper.rb b/features/support/helpers/storage_helper.rb new file mode 100644 index 00000000..80a1e1e0 --- /dev/null +++ b/features/support/helpers/storage_helper.rb @@ -0,0 +1,143 @@ +# Helper class for manipulating VM storage *volumes*, i.e. it deals +# only with creation of images and keeps a name => volume path lookup +# table (plugging drives or getting info of plugged devices is done in +# the VM class). We'd like better coupling, but given the ridiculous +# disconnect between Libvirt::StoragePool and Libvirt::Domain (hint: +# they have nothing with each other to do whatsoever) it's what makes +# sense. + +require 'libvirt' +require 'rexml/document' +require 'etc' + +class VMStorage + + @@virt = nil + + def initialize(virt, xml_path) + @@virt ||= virt + @xml_path = xml_path + pool_xml = REXML::Document.new(File.read("#{@xml_path}/storage_pool.xml")) + pool_name = pool_xml.elements['pool/name'].text + begin + @pool = @@virt.lookup_storage_pool_by_name(pool_name) + rescue Libvirt::RetrieveError + # There's no pool with that name, so we don't have to clear it + else + VMStorage.clear_storage_pool(@pool) + end + @pool_path = "#{$tmp_dir}/#{pool_name}" + pool_xml.elements['pool/target/path'].text = @pool_path + @pool = @@virt.define_storage_pool_xml(pool_xml.to_s) + @pool.build + @pool.create + @pool.refresh + end + + def VMStorage.clear_storage_pool_volumes(pool) + was_not_active = !pool.active? + if was_not_active + pool.create + end + pool.list_volumes.each do |vol_name| + vol = pool.lookup_volume_by_name(vol_name) + vol.delete + end + if was_not_active + pool.destroy + end + rescue + # Some of the above operations can fail if the pool's path was + # deleted by external means; let's ignore that. + end + + def VMStorage.clear_storage_pool(pool) + VMStorage.clear_storage_pool_volumes(pool) + pool.destroy if pool.active? + pool.undefine + end + + def clear_pool + VMStorage.clear_storage_pool(@pool) + end + + def clear_volumes + VMStorage.clear_storage_pool_volumes(@pool) + end + + def create_new_disk(name, options = {}) + options[:size] ||= 2 + options[:unit] ||= "GiB" + options[:type] ||= "qcow2" + begin + old_vol = @pool.lookup_volume_by_name(name) + rescue Libvirt::RetrieveError + # noop + else + old_vol.delete + end + uid = Etc::getpwnam("libvirt-qemu").uid + gid = Etc::getgrnam("libvirt-qemu").gid + vol_xml = REXML::Document.new(File.read("#{@xml_path}/volume.xml")) + vol_xml.elements['volume/name'].text = name + size_b = convert_to_bytes(options[:size].to_f, options[:unit]) + vol_xml.elements['volume/capacity'].text = size_b.to_s + vol_xml.elements['volume/target/format'].attributes["type"] = options[:type] + vol_xml.elements['volume/target/path'].text = "#{@pool_path}/#{name}" + vol_xml.elements['volume/target/permissions/owner'].text = uid.to_s + vol_xml.elements['volume/target/permissions/group'].text = gid.to_s + vol = @pool.create_volume_xml(vol_xml.to_s) + @pool.refresh + end + + def clone_to_new_disk(from, to) + begin + old_to_vol = @pool.lookup_volume_by_name(to) + rescue Libvirt::RetrieveError + # noop + else + old_to_vol.delete + end + from_vol = @pool.lookup_volume_by_name(from) + xml = REXML::Document.new(from_vol.xml_desc) + pool_path = REXML::Document.new(@pool.xml_desc).elements['pool/target/path'].text + xml.elements['volume/name'].text = to + xml.elements['volume/target/path'].text = "#{pool_path}/#{to}" + @pool.create_volume_xml_from(xml.to_s, from_vol) + end + + def disk_format(name) + vol = @pool.lookup_volume_by_name(name) + vol_xml = REXML::Document.new(vol.xml_desc) + return vol_xml.elements['volume/target/format'].attributes["type"] + end + + def disk_path(name) + @pool.lookup_volume_by_name(name).path + end + + # We use parted for the disk_mk* functions since it can format + # partitions "inside" the super block device; mkfs.* need a + # partition device (think /dev/sdaX), so we'd have to use something + # like losetup or kpartx, which would require administrative + # privileges. These functions only work for raw disk images. + + # TODO: We should switch to guestfish/libguestfs (which has + # ruby-bindings) so we could use qcow2 instead of raw, and more + # easily use LVM volumes. + + # For type, see label-type for mklabel in parted(8) + def disk_mklabel(name, type) + assert_equal("raw", disk_format(name)) + path = disk_path(name) + cmd_helper("/sbin/parted -s '#{path}' mklabel #{type}") + end + + # For fstype, see fs-type for mkfs in parted(8) + def disk_mkpartfs(name, fstype) + assert(disk_format(name), "raw") + path = disk_path(name) + cmd_helper("/sbin/parted -s '#{path}' mkpartfs primary '#{fstype}' 0% 100%") + end + +end diff --git a/features/support/helpers/vm_helper.rb b/features/support/helpers/vm_helper.rb new file mode 100644 index 00000000..2b5ad291 --- /dev/null +++ b/features/support/helpers/vm_helper.rb @@ -0,0 +1,426 @@ +require 'libvirt' +require 'rexml/document' + +class VM + + # These class attributes will be lazily initialized during the first + # instantiation: + # This is the libvirt connection, of which we only want one and + # which can persist for different VM instances (even in parallel) + @@virt = nil + # This is a storage helper that deals with volume manipulation. The + # storage it deals with persists across VMs, by necessity. + @@storage = nil + + def VM.storage + return @@storage + end + + def storage + return @@storage + end + + attr_reader :domain, :display, :ip, :net + + def initialize(xml_path, x_display) + @@virt ||= Libvirt::open("qemu:///system") + @xml_path = xml_path + default_domain_xml = File.read("#{@xml_path}/default.xml") + update_domain(default_domain_xml) + default_net_xml = File.read("#{@xml_path}/default_net.xml") + update_net(default_net_xml) + @display = Display.new(@domain_name, x_display) + set_cdrom_boot($tails_iso) + plug_network + # unlike the domain and net the storage pool should survive VM + # teardown (so a new instance can use e.g. a previously created + # USB drive), so we only create a new one if there is none. + @@storage ||= VMStorage.new(@@virt, xml_path) + rescue Exception => e + clean_up_net + clean_up_domain + raise e + end + + def update_domain(xml) + domain_xml = REXML::Document.new(xml) + @domain_name = domain_xml.elements['domain/name'].text + clean_up_domain + @domain = @@virt.define_domain_xml(xml) + end + + def update_net(xml) + net_xml = REXML::Document.new(xml) + @net_name = net_xml.elements['network/name'].text + @ip = net_xml.elements['network/ip/dhcp/host/'].attributes['ip'] + clean_up_net + @net = @@virt.define_network_xml(xml) + @net.create + end + + def clean_up_domain + begin + domain = @@virt.lookup_domain_by_name(@domain_name) + domain.destroy if domain.active? + domain.undefine + rescue + end + end + + def clean_up_net + begin + net = @@virt.lookup_network_by_name(@net_name) + net.destroy if net.active? + net.undefine + rescue + end + end + + def set_network_link_state(state) + domain_xml = REXML::Document.new(@domain.xml_desc) + domain_xml.elements['domain/devices/interface/link'].attributes['state'] = state + if is_running? + @domain.update_device(domain_xml.elements['domain/devices/interface'].to_s) + else + update_domain(domain_xml.to_s) + end + end + + def plug_network + set_network_link_state('up') + end + + def unplug_network + set_network_link_state('down') + end + + def set_cdrom_tray_state(state) + domain_xml = REXML::Document.new(@domain.xml_desc) + domain_xml.elements.each('domain/devices/disk') do |e| + if e.attribute('device').to_s == "cdrom" + e.elements['target'].attributes['tray'] = state + if is_running? + @domain.update_device(e.to_s) + else + update_domain(domain_xml.to_s) + end + end + end + end + + def eject_cdrom + set_cdrom_tray_state('open') + end + + def close_cdrom + set_cdrom_tray_state('closed') + end + + def set_boot_device(dev) + if is_running? + raise "boot settings can only be set for inactive vms" + end + domain_xml = REXML::Document.new(@domain.xml_desc) + domain_xml.elements['domain/os/boot'].attributes['dev'] = dev + update_domain(domain_xml.to_s) + end + + def set_cdrom_image(image) + domain_xml = REXML::Document.new(@domain.xml_desc) + domain_xml.elements.each('domain/devices/disk') do |e| + if e.attribute('device').to_s == "cdrom" + if ! e.elements['source'] + e.add_element('source') + end + e.elements['source'].attributes['file'] = image + if is_running? + @domain.update_device(e.to_s, Libvirt::Domain::DEVICE_MODIFY_FORCE) + else + update_domain(domain_xml.to_s) + end + end + end + end + + def remove_cdrom + set_cdrom_image('') + end + + def set_cdrom_boot(image) + if is_running? + raise "boot settings can only be set for inactice vms" + end + set_boot_device('cdrom') + set_cdrom_image(image) + close_cdrom + end + + def plug_drive(name, type) + # Get the next free /dev/sdX on guest + used_devs = [] + domain_xml = REXML::Document.new(@domain.xml_desc) + domain_xml.elements.each('domain/devices/disk/target') do |e| + used_devs <<= e.attribute('dev').to_s + end + letter = 'a' + dev = "sd" + letter + while used_devs.include? dev + letter = (letter[0].ord + 1).chr + dev = "sd" + letter + end + assert letter <= 'z' + + xml = REXML::Document.new(File.read("#{@xml_path}/disk.xml")) + xml.elements['disk/source'].attributes['file'] = @@storage.disk_path(name) + xml.elements['disk/driver'].attributes['type'] = @@storage.disk_format(name) + xml.elements['disk/target'].attributes['dev'] = dev + xml.elements['disk/target'].attributes['bus'] = type + if type == "usb" + xml.elements['disk/target'].attributes['removable'] = 'on' + end + + if is_running? + @domain.attach_device(xml.to_s) + else + domain_xml = REXML::Document.new(@domain.xml_desc) + domain_xml.elements['domain/devices'].add_element(xml) + update_domain(domain_xml.to_s) + end + end + + def disk_xml_desc(name) + domain_xml = REXML::Document.new(@domain.xml_desc) + domain_xml.elements.each('domain/devices/disk') do |e| + begin + if e.elements['source'].attribute('file').to_s == @@storage.disk_path(name) + return e.to_s + end + rescue + next + end + end + return nil + end + + def unplug_drive(name) + xml = disk_xml_desc(name) + @domain.detach_device(xml) + end + + def disk_dev(name) + xml = REXML::Document.new(disk_xml_desc(name)) + return "/dev/" + xml.elements['disk/target'].attribute('dev').to_s + end + + def disk_detected?(name) + return execute("test -b #{disk_dev(name)}").success? + end + + def set_disk_boot(name, type) + if is_running? + raise "boot settings can only be set for inactive vms" + end + plug_drive(name, type) + set_boot_device('hd') + # For some reason setting the boot device doesn't prevent cdrom + # boot unless it's empty + remove_cdrom + end + + # XXX-9p: Shares don't work together with snapshot save+restore. See + # XXX-9p in common_steps.rb for more information. + def add_share(source, tag) + if is_running? + raise "shares can only be added to inactice vms" + end + xml = REXML::Document.new(File.read("#{@xml_path}/fs_share.xml")) + xml.elements['filesystem/source'].attributes['dir'] = source + xml.elements['filesystem/target'].attributes['dir'] = tag + domain_xml = REXML::Document.new(@domain.xml_desc) + domain_xml.elements['domain/devices'].add_element(xml) + update_domain(domain_xml.to_s) + end + + def list_shares + list = [] + domain_xml = REXML::Document.new(@domain.xml_desc) + domain_xml.elements.each('domain/devices/filesystem') do |e| + list << e.elements['target'].attribute('dir').to_s + end + return list + end + + def set_ram_size(size, unit = "KiB") + raise "System memory can only be added to inactice vms" if is_running? + domain_xml = REXML::Document.new(@domain.xml_desc) + domain_xml.elements['domain/memory'].text = size + domain_xml.elements['domain/memory'].attributes['unit'] = unit + domain_xml.elements['domain/currentMemory'].text = size + domain_xml.elements['domain/currentMemory'].attributes['unit'] = unit + update_domain(domain_xml.to_s) + end + + def get_ram_size_in_bytes + domain_xml = REXML::Document.new(@domain.xml_desc) + unit = domain_xml.elements['domain/memory'].attribute('unit').to_s + size = domain_xml.elements['domain/memory'].text.to_i + return convert_to_bytes(size, unit) + end + + def set_arch(arch) + raise "System architecture can only be set to inactice vms" if is_running? + domain_xml = REXML::Document.new(@domain.xml_desc) + domain_xml.elements['domain/os/type'].attributes['arch'] = arch + update_domain(domain_xml.to_s) + end + + def add_hypervisor_feature(feature) + raise "Hypervisor features can only be added to inactice vms" if is_running? + domain_xml = REXML::Document.new(@domain.xml_desc) + domain_xml.elements['domain/features'].add_element(feature) + update_domain(domain_xml.to_s) + end + + def drop_hypervisor_feature(feature) + raise "Hypervisor features can only be fropped from inactice vms" if is_running? + domain_xml = REXML::Document.new(@domain.xml_desc) + domain_xml.elements['domain/features'].delete_element(feature) + update_domain(domain_xml.to_s) + end + + def disable_pae_workaround + # add_hypervisor_feature("nonpae") results in a libvirt error, and + # drop_hypervisor_feature("pae") alone won't disable pae. Hence we + # use this workaround. + xml = <<EOF + <qemu:commandline xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'> + <qemu:arg value='-cpu'/> + <qemu:arg value='pentium,-pae'/> + </qemu:commandline> +EOF + domain_xml = REXML::Document.new(@domain.xml_desc) + domain_xml.elements['domain'].add_element(REXML::Document.new(xml)) + update_domain(domain_xml.to_s) + end + + def set_os_loader(type) + if is_running? + raise "boot settings can only be set for inactice vms" + end + if type == 'UEFI' + domain_xml = REXML::Document.new(@domain.xml_desc) + domain_xml.elements['domain/os'].add_element(REXML::Document.new( + '<loader>/usr/share/ovmf/OVMF.fd</loader>' + )) + update_domain(domain_xml.to_s) + else + raise "unsupported OS loader type" + end + end + + def is_running? + begin + return @domain.active? + rescue + return false + end + end + + def execute(cmd, user = "root") + return VMCommand.new(self, cmd, { :user => user, :spawn => false }) + end + + def execute_successfully(cmd, user = "root") + p = execute(cmd, user) + assert_vmcommand_success(p) + return p + end + + def spawn(cmd, user = "root") + return VMCommand.new(self, cmd, { :user => user, :spawn => true }) + end + + def wait_until_remote_shell_is_up(timeout = 30) + VMCommand.wait_until_remote_shell_is_up(self, timeout) + end + + def host_to_guest_time_sync + host_time= DateTime.now.strftime("%s").to_s + execute("date -s '@#{host_time}'").success? + end + + def has_network? + return execute("/sbin/ifconfig eth0 | grep -q 'inet addr'").success? + end + + def has_process?(process) + return execute("pidof -x -o '%PPID' " + process).success? + end + + def pidof(process) + return execute("pidof -x -o '%PPID' " + process).stdout.chomp.split + end + + def file_exist?(file) + execute("test -e #{file}").success? + end + + def file_content(file, user = 'root') + # We don't quote #{file} on purpose: we sometimes pass environment variables + # or globs that we want to be interpreted by the shell. + cmd = execute("cat #{file}", user) + assert(cmd.success?, + "Could not cat '#{file}':\n#{cmd.stdout}\n#{cmd.stderr}") + return cmd.stdout + end + + def save_snapshot(path) + @domain.save(path) + @display.stop + end + + def restore_snapshot(path) + # Clean up current domain so its snapshot can be restored + clean_up_domain + Libvirt::Domain::restore(@@virt, path) + @domain = @@virt.lookup_domain_by_name(@domain_name) + @display.start + end + + def start + return if is_running? + @domain.create + @display.start + end + + def reset + # ruby-libvirt 0.4 does not support the reset method. + # XXX: Once we use Jessie, use @domain.reset instead. + system("virsh -c qemu:///system reset " + @domain_name) if is_running? + end + + def power_off + @domain.destroy if is_running? + @display.stop + end + + def destroy + clean_up_domain + clean_up_net + power_off + end + + def take_screenshot(description) + @display.take_screenshot(description) + end + + def get_remote_shell_port + domain_xml = REXML::Document.new(@domain.xml_desc) + domain_xml.elements.each('domain/devices/serial') do |e| + if e.attribute('type').to_s == "tcp" + return e.elements['source'].attribute('service').to_s.to_i + end + end + end + +end diff --git a/features/support/hooks.rb b/features/support/hooks.rb new file mode 100644 index 00000000..2f2f98c1 --- /dev/null +++ b/features/support/hooks.rb @@ -0,0 +1,156 @@ +require 'fileutils' +require 'time' +require 'tmpdir' + +# For @product tests +#################### + +def delete_snapshot(snapshot) + if snapshot and File.exist?(snapshot) + File.delete(snapshot) + end +rescue Errno::EACCES => e + STDERR.puts "Couldn't delete background snapshot: #{e.to_s}" +end + +def delete_all_snapshots + Dir.glob("#{$tmp_dir}/*.state").each do |snapshot| + delete_snapshot(snapshot) + end +end + +BeforeFeature('@product') do |feature| + if File.exist?($tmp_dir) + if !File.directory?($tmp_dir) + raise "Temporary directory '#{$tmp_dir}' exists but is not a " + + "directory" + end + if !File.owned?($tmp_dir) + raise "Temporary directory '#{$tmp_dir}' must be owned by the " + + "current user" + end + FileUtils.chmod(0755, $tmp_dir) + else + begin + Dir.mkdir($tmp_dir) + rescue Errno::EACCES => e + raise "Cannot create temporary directory: #{e.to_s}" + end + end + delete_all_snapshots if !$keep_snapshots + if $tails_iso.nil? + raise "No Tails ISO image specified, and none could be found in the " + + "current directory" + end + if File.exist?($tails_iso) + # Workaround: when libvirt takes ownership of the ISO image it may + # become unreadable for the live user inside the guest in the + # host-to-guest share used for some tests. + + if !File.world_readable?($tails_iso) + if File.owned?($tails_iso) + File.chmod(0644, $tails_iso) + else + raise "warning: the Tails ISO image must be world readable or be " + + "owned by the current user to be available inside the guest " + + "VM via host-to-guest shares, which is required by some tests" + end + end + else + raise "The specified Tails ISO image '#{$tails_iso}' does not exist" + end + puts "Testing ISO image: #{File.basename($tails_iso)}" + base = File.basename(feature.file, ".feature").to_s + $background_snapshot = "#{$tmp_dir}/#{base}_background.state" +end + +AfterFeature('@product') do + delete_snapshot($background_snapshot) if !$keep_snapshots + VM.storage.clear_volumes if VM.storage +end + +BeforeFeature('@product', '@old_iso') do + if $old_tails_iso.nil? + raise "No old Tails ISO image specified, and none could be found in the " + + "current directory" + end + if !File.exist?($old_tails_iso) + raise "The specified old Tails ISO image '#{$old_tails_iso}' does not exist" + end + if $tails_iso == $old_tails_iso + raise "The old Tails ISO is the same as the Tails ISO we're testing" + end + puts "Using old ISO image: #{File.basename($old_tails_iso)}" +end + +# BeforeScenario +Before('@product') do + @screen = Sikuli::Screen.new + if File.size?($background_snapshot) + @skip_steps_while_restoring_background = true + else + @skip_steps_while_restoring_background = false + end + @theme = "gnome" + @os_loader = "MBR" +end + +# AfterScenario +After('@product') do |scenario| + if (scenario.status != :passed) + time_of_fail = Time.now - $time_at_start + secs = "%02d" % (time_of_fail % 60) + mins = "%02d" % ((time_of_fail / 60) % 60) + hrs = "%02d" % (time_of_fail / (60*60)) + STDERR.puts "Scenario failed at time #{hrs}:#{mins}:#{secs}" + base = File.basename(scenario.feature.file, ".feature").to_s + tmp = @screen.capture.getFilename + out = "#{$tmp_dir}/#{base}-#{DateTime.now}.png" + FileUtils.mv(tmp, out) + STDERR.puts("Took screenshot \"#{out}\"") + if $pause_on_fail + STDERR.puts "" + STDERR.puts "Press ENTER to continue running the test suite" + STDIN.gets + end + end + if @sniffer + @sniffer.stop + @sniffer.clear + end + @vm.destroy if @vm +end + +After('@product', '~@keep_volumes') do + VM.storage.clear_volumes +end + +# For @source tests +################### + +# BeforeScenario +Before('@source') do + @orig_pwd = Dir.pwd + @git_clone = Dir.mktmpdir 'tails-apt-tests' + Dir.chdir @git_clone +end + +# AfterScenario +After('@source') do + Dir.chdir @orig_pwd + FileUtils.remove_entry_secure @git_clone +end + + +# Common +######## + +BeforeFeature('@product', '@source') do |feature| + raise "Feature #{feature.file} is tagged both @product and @source, " + + "which is an impossible combination" +end + +at_exit do + delete_all_snapshots if !$keep_snapshots + VM.storage.clear_pool if VM.storage +end diff --git a/features/time_syncing.feature b/features/time_syncing.feature new file mode 100644 index 00000000..a32d5a70 --- /dev/null +++ b/features/time_syncing.feature @@ -0,0 +1,41 @@ +@product +Feature: Time syncing + As a Tails user + I want Tor to work properly + And for that I need a reasonably accurate system clock + + Background: + Given a computer + And I start Tails from DVD with network unplugged and I login + And I save the state so the background can be restored next scenario + + Scenario: Clock with host's time + When the network is plugged + And Tor is ready + Then Tails clock is less than 5 minutes incorrect + + Scenario: Clock is one day in the past + When I bump the system time with "-1 day" + And the network is plugged + And Tor is ready + Then Tails clock is less than 5 minutes incorrect + + Scenario: Clock way in the past + When I set the system time to "01 Jan 2000 12:34:56" + And the network is plugged + And Tor is ready + Then Tails clock is less than 5 minutes incorrect + + Scenario: Clock is one day in the future + When I bump the system time with "+1 day" + And the network is plugged + And Tor is ready + Then Tails clock is less than 5 minutes incorrect + + Scenario: Clock way in the future + When I set the system time to "01 Jan 2020 12:34:56" + And the network is plugged + And Tor is ready + Then Tails clock is less than 5 minutes incorrect + +# Scenario: Clock vs Tor consensus' valid-{after,until} etc. diff --git a/features/torified_browsing.feature b/features/torified_browsing.feature new file mode 100644 index 00000000..cc9ddf1a --- /dev/null +++ b/features/torified_browsing.feature @@ -0,0 +1,35 @@ +@product +Feature: Browsing the web using the Tor Browser + As a Tails user + when I browse the web using the Tor Browser + all Internet traffic should flow only through Tor + + Background: + Given a computer + And I capture all network traffic + And I start the computer + And the computer boots Tails + And I log in to a new session + And GNOME has started + And Tor is ready + And available upgrades have been checked + And all notifications have disappeared + And I save the state so the background can be restored next scenario + + Scenario: The Tor Browser uses TBB's shared libraries + When I start the Tor Browser + And the Tor Browser has started + Then the Tor Browser uses all expected TBB shared libraries + + Scenario: Opening check.torproject.org in the Tor Browser shows the green onion and the congratulations message + When I start the Tor Browser + And the Tor Browser has started and loaded the startup page + And I open the address "https://check.torproject.org" in the Tor Browser + Then I see "TorBrowserTorCheck.png" after at most 180 seconds + And all Internet traffic has only flowed through Tor + + Scenario: The Tor Browser should not have any plugins enabled + When I start the Tor Browser + And the Tor Browser has started and loaded the startup page + And I open the address "about:plugins" in the Tor Browser + Then I see "TorBrowserNoPlugins.png" after at most 60 seconds diff --git a/features/torified_gnupg.feature b/features/torified_gnupg.feature new file mode 100644 index 00000000..a5820681 --- /dev/null +++ b/features/torified_gnupg.feature @@ -0,0 +1,31 @@ +@product +Feature: Keyserver interaction with GnuPG + As a Tails user + when I interact with keyservers using various GnuPG tools + the configured keyserver must be used + and all Internet traffic should flow only through Tor. + + Background: + Given a computer + And I capture all network traffic + And I start the computer + And the computer boots Tails + And I log in to a new session + And GNOME has started + And Tor is ready + And all notifications have disappeared + And available upgrades have been checked + And the "10CC5BC7" OpenPGP key is not in the live user's public keyring + And I save the state so the background can be restored next scenario + + Scenario: Fetching OpenPGP keys using GnuPG should work and be done over Tor. + When I fetch the "10CC5BC7" OpenPGP key using the GnuPG CLI + Then GnuPG uses the configured keyserver + And the GnuPG fetch is successful + And the "10CC5BC7" key is in the live user's public keyring after at most 120 seconds + And all Internet traffic has only flowed through Tor + + Scenario: Fetching OpenPGP keys using Seahorse should work and be done over Tor. + When I fetch the "10CC5BC7" OpenPGP key using Seahorse + Then the "10CC5BC7" key is in the live user's public keyring after at most 120 seconds + And all Internet traffic has only flowed through Tor diff --git a/features/totem.feature b/features/totem.feature new file mode 100644 index 00000000..2729b273 --- /dev/null +++ b/features/totem.feature @@ -0,0 +1,59 @@ +@product +Feature: Using Totem + As a Tails user + I want to watch local and remote videos in Totem + And AppArmor should prevent Totem from doing dangerous things + And all Internet traffic should flow only through Tor + + # We cannot use Background to save a snapshot of an already booted + # Tails here, due to bugs with filesystem shares vs. snapshots, as + # explained in checks.feature. + + Background: + Given I create sample videos + + Scenario: Watching a MP4 video stored on the non-persistent filesystem + Given a computer + And I setup a filesystem share containing sample videos + And I start Tails from DVD with network unplugged and I login + And I copy the sample videos to "/home/amnesia" as user "amnesia" + When I open "/home/amnesia/video.mp4" with Totem + Then I see "SampleLocalMp4VideoFrame.png" after at most 10 seconds + Given I close Totem + And I copy the sample videos to "/home/amnesia/.gnupg" as user "amnesia" + When I try to open "/home/amnesia/.gnupg/video.mp4" with Totem + Then I see "TotemUnableToOpen.png" after at most 10 seconds + + Scenario: Watching a WebM video over HTTPS, with and without the command-line + Given a computer + And I capture all network traffic + And I start Tails from DVD and I login + When I open "https://webm.html5.org/test.webm" with Totem + Then I see "SampleRemoteWebMVideoFrame.png" after at most 10 seconds + When I close Totem + And I start Totem through the GNOME menu + When I load the "https://webm.html5.org/test.webm" URL in Totem + Then I see "SampleRemoteWebMVideoFrame.png" after at most 10 seconds + And all Internet traffic has only flowed through Tor + + @keep_volumes + Scenario: Installing Tails on a USB drive, creating a persistent partition, copying video files to it + Given the USB drive "current" contains Tails with persistence configured and password "asdf" + And a computer + And I setup a filesystem share containing sample videos + And I start Tails from USB drive "current" with network unplugged and I login with persistence password "asdf" + And I copy the sample videos to "/home/amnesia/Persistent" as user "amnesia" + And I copy the sample videos to "/home/amnesia/.gnupg" as user "amnesia" + And I shutdown Tails and wait for the computer to power off + + @keep_volumes + Scenario: Watching a MP4 video stored on the persistent volume + Given a computer + And I start Tails from USB drive "current" with network unplugged and I login with persistence password "asdf" + And the file "/home/amnesia/Persistent/video.mp4" exists + When I open "/home/amnesia/Persistent/video.mp4" with Totem + Then I see "SampleLocalMp4VideoFrame.png" after at most 10 seconds + Given I close Totem + And the file "/home/amnesia/.gnupg/video.mp4" exists + When I try to open "/home/amnesia/.gnupg/video.mp4" with Totem + Then I see "TotemUnableToOpen.png" after at most 10 seconds diff --git a/features/truecrypt.feature b/features/truecrypt.feature new file mode 100644 index 00000000..db4cb5b0 --- /dev/null +++ b/features/truecrypt.feature @@ -0,0 +1,12 @@ +@product +Feature: TrueCrypt + As a Tails user + I *might* want to use TrueCrypt + + Scenario: TrueCrypt starts + Given a computer + And I set Tails to boot with options "truecrypt" + And I start Tails from DVD with network unplugged and I login + When I start TrueCrypt through the GNOME menu + And I deal with the removal warning prompt + Then I see "TrueCryptWindow.png" after at most 60 seconds diff --git a/features/unsafe_browser.feature b/features/unsafe_browser.feature new file mode 100644 index 00000000..80d91af1 --- /dev/null +++ b/features/unsafe_browser.feature @@ -0,0 +1,47 @@ +@product +Feature: Browsing the web using the Unsafe Browser + As a Tails user + when I browse the web using the Unsafe Browser + I should have direct access to the web + + Background: + Given a computer + And I start the computer + And the computer boots Tails + And I log in to a new session + And GNOME has started + And Tor is ready + And all notifications have disappeared + And available upgrades have been checked + And I save the state so the background can be restored next scenario + + Scenario: Starting the Unsafe Browser works as it should. + When I successfully start the Unsafe Browser + Then the Unsafe Browser has a red theme + And the Unsafe Browser shows a warning as its start page + And the Unsafe Browser uses all expected TBB shared libraries + + Scenario: Closing the Unsafe Browser shows a stop notification. + When I successfully start the Unsafe Browser + And I close the Unsafe Browser + Then I see the Unsafe Browser stop notification + + Scenario: Starting a second instance of the Unsafe Browser results in an error message being shown. + When I successfully start the Unsafe Browser + And I start the Unsafe Browser + Then I see a warning about another instance already running + + Scenario: The Unsafe Browser cannot be restarted before the previous instance has been cleaned up. + When I successfully start the Unsafe Browser + And I close the Unsafe Browser + And I start the Unsafe Browser + Then I see a warning about another instance already running + + Scenario: Opening check.torproject.org in the Unsafe Browser shows the red onion and a warning message. + When I successfully start the Unsafe Browser + And I open the address "https://check.torproject.org" in the Unsafe Browser + Then I see "UnsafeBrowserTorCheckFail.png" after at most 60 seconds + + Scenario: The Unsafe Browser cannot be configured to use Tor and other local proxies. + When I successfully start the Unsafe Browser + Then I cannot configure the Unsafe Browser to use any local proxies diff --git a/features/untrusted_partitions.feature b/features/untrusted_partitions.feature new file mode 100644 index 00000000..816fbe7e --- /dev/null +++ b/features/untrusted_partitions.feature @@ -0,0 +1,41 @@ +@product +Feature: Untrusted partitions + As a Tails user + I don't want to touch other media than the one Tails runs from + + @keep_volumes + Scenario: Tails can boot from live systems stored on hard drives + Given a computer + And I create a 2 GiB disk named "live_hd" + And I cat an ISO hybrid of the Tails image to disk "live_hd" + And the computer is set to boot from ide drive "live_hd" + And I set Tails to boot with options "live-media=" + And I start Tails from DVD with network unplugged and I login + Then Tails seems to have booted normally + + Scenario: Tails booting from a DVD does not use live systems stored on hard drives + Given a computer + And I plug ide drive "live_hd" + And I start Tails from DVD with network unplugged and I login + Then drive "live_hd" is detected by Tails + And drive "live_hd" is not mounted + + Scenario: Booting Tails does not automount untrusted ext2 partitions + Given a computer + And I create a 100 MiB disk named "gpt_ext2" + And I create a gpt label on disk "gpt_ext2" + And I create a ext2 filesystem on disk "gpt_ext2" + And I plug ide drive "gpt_ext2" + And I start Tails from DVD with network unplugged and I login + Then drive "gpt_ext2" is detected by Tails + And drive "gpt_ext2" is not mounted + + Scenario: Booting Tails does not automount untrusted fat32 partitions + Given a computer + And I create a 100 MiB disk named "msdos_fat32" + And I create a msdos label on disk "msdos_fat32" + And I create a fat32 filesystem on disk "msdos_fat32" + And I plug ide drive "msdos_fat32" + And I start Tails from DVD with network unplugged and I login + Then drive "msdos_fat32" is detected by Tails + And drive "msdos_fat32" is not mounted diff --git a/features/usb_install.feature b/features/usb_install.feature new file mode 100644 index 00000000..b40ca93b --- /dev/null +++ b/features/usb_install.feature @@ -0,0 +1,274 @@ +@product @old_iso +Feature: Installing Tails to a USB drive, upgrading it, and using persistence + As a Tails user + I may want to install Tails to a USB drive + and upgrade it to new Tails versions + and use persistence + + @keep_volumes + Scenario: Installing Tails to a pristine USB drive + Given a computer + And I start Tails from DVD with network unplugged and I login + And I create a new 4 GiB USB drive named "current" + And I plug USB drive "current" + And I "Clone & Install" Tails to USB drive "current" + Then the running Tails is installed on USB drive "current" + But there is no persistence partition on USB drive "current" + And I unplug USB drive "current" + + @keep_volumes + Scenario: Booting Tails from a USB drive in UEFI mode + Given a computer + And the computer is set to boot in UEFI mode + When I start Tails from USB drive "current" with network unplugged and I login + Then the boot device has safe access rights + And Tails is running from USB drive "current" + And the boot device has safe access rights + And Tails has started in UEFI mode + + @keep_volumes + Scenario: Booting Tails from a USB drive without a persistent partition and creating one + Given a computer + And I start Tails from USB drive "current" with network unplugged and I login + Then the boot device has safe access rights + And Tails is running from USB drive "current" + And the boot device has safe access rights + And there is no persistence partition on USB drive "current" + And I create a persistent partition with password "asdf" + Then a Tails persistence partition with password "asdf" exists on USB drive "current" + And I shutdown Tails and wait for the computer to power off + + @keep_volumes + Scenario: Booting Tails from a USB drive with a disabled persistent partition + Given a computer + And I start Tails from USB drive "current" with network unplugged and I login + Then Tails is running from USB drive "current" + And the boot device has safe access rights + And persistence is disabled + But a Tails persistence partition with password "asdf" exists on USB drive "current" + + @keep_volumes + Scenario: Persistent browser bookmarks + Given a computer + And the computer is set to boot from USB drive "current" + And the network is unplugged + When I start the computer + And the computer boots Tails + And Tails is running from USB drive "current" + And the boot device has safe access rights + And I enable persistence with password "asdf" + And I log in to a new session + And GNOME has started + And all notifications have disappeared + And persistence is enabled + And persistent filesystems have safe access rights + And persistence configuration files have safe access rights + And persistent directories have safe access rights + And I start the Tor Browser in offline mode + And the Tor Browser has started in offline mode + And I add a bookmark to eff.org in the Tor Browser + And I warm reboot the computer + And the computer reboots Tails + And I enable read-only persistence with password "asdf" + And I log in to a new session + And GNOME has started + And I start the Tor Browser in offline mode + And the Tor Browser has started in offline mode + Then the Tor Browser has a bookmark to eff.org + + @keep_volumes + Scenario: Writing files to a read/write-enabled persistent partition + Given a computer + And I start Tails from USB drive "current" with network unplugged and I login with persistence password "asdf" + Then Tails is running from USB drive "current" + And the boot device has safe access rights + And persistence is enabled + And I write some files expected to persist + And persistent filesystems have safe access rights + And persistence configuration files have safe access rights + And persistent directories have safe access rights + And I shutdown Tails and wait for the computer to power off + Then only the expected files should persist on USB drive "current" + + @keep_volumes + Scenario: Writing files to a read-only-enabled persistent partition + Given a computer + And I start Tails from USB drive "current" with network unplugged and I login with read-only persistence password "asdf" + Then Tails is running from USB drive "current" + And the boot device has safe access rights + And persistence is enabled + And I write some files not expected to persist + And I remove some files expected to persist + And I shutdown Tails and wait for the computer to power off + Then only the expected files should persist on USB drive "current" + + @keep_volumes + Scenario: Deleting a Tails persistent partition + Given a computer + And I start Tails from USB drive "current" with network unplugged and I login + Then Tails is running from USB drive "current" + And the boot device has safe access rights + And persistence is disabled + But a Tails persistence partition with password "asdf" exists on USB drive "current" + And all notifications have disappeared + When I delete the persistent partition + Then there is no persistence partition on USB drive "current" + + @keep_volumes + Scenario: Installing an old version of Tails to a pristine USB drive + Given a computer + And the computer is set to boot from the old Tails DVD + And the network is unplugged + And I start the computer + When the computer boots Tails + And I log in to a new session + And GNOME has started + And all notifications have disappeared + And I create a new 4 GiB USB drive named "old" + And I plug USB drive "old" + And I "Clone & Install" Tails to USB drive "old" + Then the running Tails is installed on USB drive "old" + But there is no persistence partition on USB drive "old" + And I unplug USB drive "old" + + @keep_volumes + Scenario: Creating a persistent partition with the old Tails USB installation + Given a computer + And I start Tails from USB drive "old" with network unplugged and I login + Then Tails is running from USB drive "old" + And I create a persistent partition with password "asdf" + Then a Tails persistence partition with password "asdf" exists on USB drive "old" + And I shutdown Tails and wait for the computer to power off + + @keep_volumes + Scenario: Writing files to a read/write-enabled persistent partition with the old Tails USB installation + Given a computer + And I start Tails from USB drive "old" with network unplugged and I login with persistence password "asdf" + Then Tails is running from USB drive "old" + And persistence is enabled + And I write some files expected to persist + And persistent filesystems have safe access rights + And persistence configuration files have safe access rights + And persistent directories have safe access rights + And I shutdown Tails and wait for the computer to power off + Then only the expected files should persist on USB drive "old" + + @keep_volumes + Scenario: Upgrading an old Tails USB installation from a Tails DVD + Given a computer + And I clone USB drive "old" to a new USB drive "to_upgrade" + And I start Tails from DVD with network unplugged and I login + And I plug USB drive "to_upgrade" + And I "Clone & Upgrade" Tails to USB drive "to_upgrade" + Then the running Tails is installed on USB drive "to_upgrade" + And I unplug USB drive "to_upgrade" + + @keep_volumes + Scenario: Booting Tails from a USB drive upgraded from DVD with persistence enabled + Given a computer + And I start Tails from USB drive "to_upgrade" with network unplugged and I login with persistence password "asdf" + Then Tails is running from USB drive "to_upgrade" + And the boot device has safe access rights + And the expected persistent files are present in the filesystem + And persistent directories have safe access rights + + @keep_volumes + Scenario: Upgrading an old Tails USB installation from another Tails USB drive + Given a computer + And I clone USB drive "old" to a new USB drive "to_upgrade" + And I start Tails from USB drive "current" with network unplugged and I login + Then Tails is running from USB drive "current" + And the boot device has safe access rights + And I plug USB drive "to_upgrade" + And I "Clone & Upgrade" Tails to USB drive "to_upgrade" + Then the running Tails is installed on USB drive "to_upgrade" + And I unplug USB drive "to_upgrade" + And I unplug USB drive "current" + + @keep_volumes + Scenario: Booting Tails from a USB drive upgraded from USB with persistence enabled + Given a computer + And I start Tails from USB drive "to_upgrade" with network unplugged and I login with persistence password "asdf" + Then persistence is enabled + And Tails is running from USB drive "to_upgrade" + And the boot device has safe access rights + And the expected persistent files are present in the filesystem + And persistent directories have safe access rights + + @keep_volumes + Scenario: Upgrading an old Tails USB installation from an ISO image, running on the old version + Given a computer + And I clone USB drive "old" to a new USB drive "to_upgrade" + And I setup a filesystem share containing the Tails ISO + When I start Tails from USB drive "old" with network unplugged and I login + And I plug USB drive "to_upgrade" + And I do a "Upgrade from ISO" on USB drive "to_upgrade" + Then the ISO's Tails is installed on USB drive "to_upgrade" + And I unplug USB drive "to_upgrade" + + @keep_volumes + Scenario: Upgrading an old Tails USB installation from an ISO image, running on the new version + Given a computer + And I clone USB drive "old" to a new USB drive "to_upgrade" + And I setup a filesystem share containing the Tails ISO + And I start Tails from DVD with network unplugged and I login + And I plug USB drive "to_upgrade" + And I do a "Upgrade from ISO" on USB drive "to_upgrade" + Then the ISO's Tails is installed on USB drive "to_upgrade" + And I unplug USB drive "to_upgrade" + + Scenario: Booting a USB drive upgraded from ISO with persistence enabled + Given a computer + And I start Tails from USB drive "to_upgrade" with network unplugged and I login with persistence password "asdf" + Then persistence is enabled + And Tails is running from USB drive "to_upgrade" + And the boot device has safe access rights + And the expected persistent files are present in the filesystem + And persistent directories have safe access rights + + @keep_volumes + Scenario: Installing Tails to a USB drive with an MBR partition table but no partitions + Given a computer + And I create a 4 GiB disk named "mbr" + And I create a msdos label on disk "mbr" + And I start Tails from DVD with network unplugged and I login + And I plug USB drive "mbr" + And I "Clone & Install" Tails to USB drive "mbr" + Then the running Tails is installed on USB drive "mbr" + But there is no persistence partition on USB drive "mbr" + And I unplug USB drive "mbr" + + Scenario: Booting a USB drive that originally had an empty MBR partition table + Given a computer + And I start Tails from USB drive "mbr" with network unplugged and I login + Then Tails is running from USB drive "mbr" + And the boot device has safe access rights + And there is no persistence partition on USB drive "mbr" + + @keep_volumes + Scenario: Cat:ing a Tails isohybrid to a USB drive and booting it + Given a computer + And I create a 4 GiB disk named "isohybrid" + And I cat an ISO hybrid of the Tails image to disk "isohybrid" + And I start Tails from USB drive "isohybrid" with network unplugged and I login + Then Tails is running from USB drive "isohybrid" + + @keep_volumes + Scenario: Try upgrading but end up installing Tails to a USB drive containing a Tails isohybrid installation + Given a computer + And I start Tails from DVD with network unplugged and I login + And I plug USB drive "isohybrid" + And I try a "Clone & Upgrade" Tails to USB drive "isohybrid" + But I am suggested to do a "Clone & Install" + And I kill the process "liveusb-creator" + And I "Clone & Install" Tails to USB drive "isohybrid" + Then the running Tails is installed on USB drive "isohybrid" + But there is no persistence partition on USB drive "isohybrid" + And I unplug USB drive "isohybrid" + + Scenario: Booting a USB drive that originally had a isohybrid installation + Given a computer + And I start Tails from USB drive "isohybrid" with network unplugged and I login + Then Tails is running from USB drive "isohybrid" + And the boot device has safe access rights + And there is no persistence partition on USB drive "isohybrid" diff --git a/features/windows_camouflage.feature b/features/windows_camouflage.feature new file mode 100644 index 00000000..1c2fa526 --- /dev/null +++ b/features/windows_camouflage.feature @@ -0,0 +1,36 @@ +@product +Feature: Microsoft Windows Camouflage + As a Tails user + when I select the Microsoft Windows Camouflage in Tails Greeter + I should be presented with a Microsoft Windows like environment + + Background: + Given a computer + And the network is unplugged + And I start the computer + And the computer boots Tails + And I enable more Tails Greeter options + And I enable Microsoft Windows camouflage + And I log in to a new session + And GNOME has started + And all notifications have disappeared + And I save the state so the background can be restored next scenario + + Scenario: I should be presented with a Microsoft Windows like desktop + Then I see "WindowsDesktop.png" after at most 10 seconds + And I see "WindowsStartButton.png" after at most 10 seconds + And I see "WindowsSysTray.png" after at most 10 seconds + + Scenario: Windows should appear like those in Microsoft Windows + When the network is plugged + And Tor is ready + And all notifications have disappeared + And available upgrades have been checked + And I start the Tor Browser + Then I see "WindowsTorBrowserWindow.png" after at most 120 seconds + And I see "WindowsTorBrowserTaskBar.png" after at most 10 seconds + And I see "WindowsWindowButtons.png" after at most 10 seconds + + Scenario: The panel menu should look like Microsoft Windows's start menu + When I click the start menu + Then I see "WindowsStartMenu.png" after at most 10 seconds |