1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
|
def shipped_openpgp_keys
shipped_gpg_keys = $vm.execute_successfully('gpg --batch --with-colons --fingerprint --list-key', :user => LIVE_USER).stdout
openpgp_fingerprints = shipped_gpg_keys.scan(/^fpr:::::::::([A-Z0-9]+):$/).flatten
return openpgp_fingerprints
end
Then /^the OpenPGP keys shipped with Tails will be valid for the next (\d+) months$/ do |months|
invalid = Array.new
shipped_openpgp_keys.each do |key|
begin
step "the shipped OpenPGP key #{key} will be valid for the next #{months} months"
rescue Test::Unit::AssertionFailedError
invalid << key
next
end
end
assert(invalid.empty?, "The following key(s) will not be valid in #{months} months: #{invalid.join(', ')}")
end
Then /^the shipped (?:Debian repository key|OpenPGP key ([A-Z0-9]+)) will be valid for the next (\d+) months$/ do |fingerprint, max_months|
if fingerprint
cmd = 'gpg'
user = LIVE_USER
else
fingerprint = TAILS_DEBIAN_REPO_KEY
cmd = 'apt-key adv'
user = 'root'
end
shipped_sig_key_info = $vm.execute_successfully("#{cmd} --batch --list-key #{fingerprint}", :user => user).stdout
m = /\[expire[ds]: ([0-9-]*)\]/.match(shipped_sig_key_info)
if m
expiration_date = Date.parse(m[1])
assert((expiration_date << max_months.to_i) > DateTime.now,
"The shipped key #{fingerprint} will not be valid #{max_months} months from now.")
end
end
Then /^the live user has been setup by live\-boot$/ do
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|
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
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
Then /^no unexpected services are listening for network connections$/ do
for line in $vm.execute_successfully("ss -ltupn").stdout.chomp.split("\n") do
splitted = line.split(/[[:blank:]]+/)
proto = splitted[0]
next unless ['tcp', 'udp'].include?(proto)
laddr, lport = splitted[4].split(":")
proc = /users:\(\("([^"]+)"/.match(splitted[6])[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
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
assert($vm.execute("modinfo vboxguest").success?,
"The vboxguest module is not available.")
end
Then /^the support documentation page opens in Tor Browser$/ do
if @language == 'German'
expected_title = 'Tails - Hilfe & Support'
expected_heading = 'Die Dokumentation durchsuchen'
else
expected_title = 'Tails - Support'
expected_heading = 'Search the documentation'
end
step "\"#{expected_title}\" has loaded in the Tor Browser"
headings = @torbrowser
.child(expected_title, roleName: 'document frame')
.children(roleName: 'heading')
assert(
headings.any? { |heading| heading.text == expected_heading }
)
end
Given /^I plug and mount a USB drive containing a sample PNG$/ do
@png_dir = share_host_files(Dir.glob("#{MISC_FILES_DIR}/*.png"))
end
Then /^MAT can clean some sample PNG file$/ do
for png_on_host in Dir.glob("#{MISC_FILES_DIR}/*.png") do
png_name = File.basename(png_on_host)
png_on_guest = "/home/#{LIVE_USER}/#{png_name}"
step "I copy \"#{@png_dir}/#{png_name}\" to \"#{png_on_guest}\" as user \"#{LIVE_USER}\""
raw_check_cmd = "grep --quiet --fixed-strings --text " +
"'Created with GIMP' '#{png_on_guest}'"
assert($vm.execute(raw_check_cmd, user: LIVE_USER).success?,
'The comment is not present in the PNG')
check_before = $vm.execute_successfully("mat --check '#{png_on_guest}'",
:user => LIVE_USER).stdout
assert(check_before.include?("#{png_on_guest} is not clean"),
"MAT failed to see that '#{png_on_host}' is dirty")
$vm.execute_successfully("mat '#{png_on_guest}'", :user => LIVE_USER)
check_after = $vm.execute_successfully("mat --check '#{png_on_guest}'",
:user => LIVE_USER).stdout
assert(check_after.include?("#{png_on_guest} is clean"),
"MAT failed to clean '#{png_on_host}'")
assert($vm.execute(raw_check_cmd, user: LIVE_USER).failure?,
'The comment is still present in the PNG')
$vm.execute_successfully("rm '#{png_on_guest}'")
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
def get_seccomp_status(process)
assert($vm.has_process?(process), "Process #{process} not running.")
pid = $vm.pidof(process)[0]
status = $vm.file_content("/proc/#{pid}/status")
return status.match(/^Seccomp:\s+([0-9])/)[1].chomp.to_i
end
def get_apparmor_status(pid)
apparmor_status = $vm.file_content("/proc/#{pid}/attr/current").chomp
if apparmor_status.include?(')')
# matches something like /usr/sbin/cupsd (enforce)
# and only returns what's in the parentheses
return apparmor_status.match(/[^\s]+\s+\((.+)\)$/)[1].chomp
else
return apparmor_status
end
end
Then /^the running process "(.+)" is confined with AppArmor in (complain|enforce) mode$/ do |process, mode|
assert($vm.has_process?(process), "Process #{process} not running.")
pid = $vm.pidof(process)[0]
assert_equal(mode, get_apparmor_status(pid))
end
Then /^the running process "(.+)" is confined with Seccomp in (filter|strict) mode$/ do |process,mode|
status = get_seccomp_status(process)
if mode == 'strict'
assert_equal(1, status, "#{process} not confined with Seccomp in strict mode")
elsif mode == 'filter'
assert_equal(2, status, "#{process} not confined with Seccomp in filter mode")
else
raise "Unsupported mode #{mode} passed"
end
end
Then /^tails-debugging-info is not susceptible to symlink attacks$/ do
secret_file = '/secret'
secret_contents = 'T0P S3Cr1t -- 3yEs oN1y'
$vm.file_append(secret_file, secret_contents)
$vm.execute_successfully("chmod u=rw,go= #{secret_file}")
$vm.execute_successfully("chown root:root #{secret_file}")
script_path = '/usr/local/sbin/tails-debugging-info'
script_lines = $vm.file_content(script_path).split("\n")
script_lines.grep(/^debug_file\s+/).each do |line|
_, user, debug_file = line.split
# root can always mount symlink attacks
next if user == 'root'
# Remove quoting around the file
debug_file.gsub!(/["']/, '')
# Skip files that do not exist, or cannot be removed (e.g. the
# ones in /proc).
next if not($vm.execute("rm #{debug_file}").success?)
# Check what would happen *if* the amnesia user managed to replace
# the debugging file with a symlink to the secret.
$vm.execute_successfully("ln -s #{secret_file} #{debug_file}")
$vm.execute_successfully("chown --no-dereference #{LIVE_USER}:#{LIVE_USER} #{debug_file}")
if $vm.execute("sudo /usr/local/sbin/tails-debugging-info | " +
"grep '#{secret_contents}'",
:user => LIVE_USER).success?
raise "The secret was leaked by tails-debugging-info via '#{debug_file}'"
end
# Remove the secret so it cannot possibly interfere with the
# following iterations (even though it should not).
$vm.execute_successfully("echo > #{debug_file}")
end
end
When /^I disable all networking in the Tails Greeter$/ do
open_greeter_additional_settings()
@screen.wait_and_click('TailsGreeterNetworkConnection.png', 30)
@screen.wait_and_click('TailsGreeterDisableAllNetworking.png', 10)
@screen.wait_and_click("TailsGreeterAdditionalSettingsAdd.png", 10)
end
Then /^the Tor Status icon tells me that Tor is( not)? usable$/ do |not_usable|
picture = not_usable ? 'TorStatusNotUsable' : 'TorStatusUsable'
@screen.find("#{picture}.png")
end
|