summaryrefslogtreecommitdiffstats
path: root/features
diff options
context:
space:
mode:
authorTails developers <amnesia@boum.org>2014-12-19 00:40:08 +0100
committerHolger Levsen <holger@layer-acht.org>2014-12-21 09:45:40 +0100
commit51680b6ebb645d37ebdfcd122ca163b3a638aefa (patch)
tree337e128d2eac3cbc89ecbacf38851bfa33469cd5 /features
parent44bab3c86ca3d95837f4c50cc535206352385a46 (diff)
downloadjenkins.debian.net-51680b6ebb645d37ebdfcd122ca163b3a638aefa.tar.xz
files copied from https://git-tails.immerda.ch/tails - many thanks to the tails developers for their nice work and documentation of it - these files have been released under the GNU General Public License version 3 or (at your option) any later version
features/images has been omitted
Diffstat (limited to 'features')
-rw-r--r--features/apt.feature34
-rw-r--r--features/build.feature75
-rw-r--r--features/checks.feature57
-rw-r--r--features/dhcp.feature32
-rw-r--r--features/domains/default.xml64
-rw-r--r--features/domains/default_net.xml13
-rw-r--r--features/domains/disk.xml5
-rw-r--r--features/domains/fs_share.xml6
-rw-r--r--features/domains/storage_pool.xml6
-rw-r--r--features/domains/volume.xml14
-rw-r--r--features/encryption.feature31
-rw-r--r--features/erase_memory.feature61
-rw-r--r--features/evince.feature53
-rw-r--r--features/firewall_leaks.feature37
-rw-r--r--features/i2p.feature33
-rw-r--r--features/misc_files/sample.pdfbin0 -> 22347 bytes
-rw-r--r--features/misc_files/sample.tex8
-rw-r--r--features/pidgin.feature71
-rw-r--r--features/root_access_control.feature44
-rw-r--r--features/step_definitions/apt.rb80
-rw-r--r--features/step_definitions/build.rb71
-rw-r--r--features/step_definitions/checks.rb143
-rw-r--r--features/step_definitions/common_steps.rb687
-rw-r--r--features/step_definitions/dhcp.rb20
-rw-r--r--features/step_definitions/encryption.rb139
-rw-r--r--features/step_definitions/erase_memory.rb172
-rw-r--r--features/step_definitions/evince.rb20
-rw-r--r--features/step_definitions/firewall_leaks.rb60
-rw-r--r--features/step_definitions/i2p.rb60
-rw-r--r--features/step_definitions/pidgin.rb188
-rw-r--r--features/step_definitions/root_access_control.rb45
-rw-r--r--features/step_definitions/time_syncing.rb20
-rw-r--r--features/step_definitions/torified_browsing.rb12
-rw-r--r--features/step_definitions/torified_gnupg.rb54
-rw-r--r--features/step_definitions/totem.rb50
-rw-r--r--features/step_definitions/truecrypt.rb12
-rw-r--r--features/step_definitions/unsafe_browser.rb154
-rw-r--r--features/step_definitions/untrusted_partitions.rb35
-rw-r--r--features/step_definitions/usb.rb492
-rw-r--r--features/step_definitions/windows_camouflage.rb10
-rw-r--r--features/support/config.rb34
-rw-r--r--features/support/env.rb53
-rw-r--r--features/support/extra_hooks.rb45
-rw-r--r--features/support/helpers/display_helper.rb51
-rw-r--r--features/support/helpers/exec_helper.rb61
-rw-r--r--features/support/helpers/firewall_helper.rb100
-rw-r--r--features/support/helpers/misc_helpers.rb121
-rw-r--r--features/support/helpers/net_helper.rb42
-rw-r--r--features/support/helpers/sikuli_helper.rb145
-rw-r--r--features/support/helpers/storage_helper.rb143
-rw-r--r--features/support/helpers/vm_helper.rb426
-rw-r--r--features/support/hooks.rb156
-rw-r--r--features/time_syncing.feature41
-rw-r--r--features/torified_browsing.feature35
-rw-r--r--features/torified_gnupg.feature31
-rw-r--r--features/totem.feature59
-rw-r--r--features/truecrypt.feature12
-rw-r--r--features/unsafe_browser.feature47
-rw-r--r--features/untrusted_partitions.feature41
-rw-r--r--features/usb_install.feature274
-rw-r--r--features/windows_camouflage.feature36
61 files changed, 5121 insertions, 0 deletions
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
new file mode 100644
index 00000000..d0cc9502
--- /dev/null
+++ b/features/misc_files/sample.pdf
Binary files differ
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