checks.rb 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. def shipped_openpgp_keys
  2. shipped_gpg_keys = $vm.execute_successfully('gpg --batch --with-colons --fingerprint --list-key', :user => LIVE_USER).stdout
  3. openpgp_fingerprints = shipped_gpg_keys.scan(/^fpr:::::::::([A-Z0-9]+):$/).flatten
  4. return openpgp_fingerprints
  5. end
  6. Then /^the OpenPGP keys shipped with Tails will be valid for the next (\d+) months$/ do |months|
  7. invalid = Array.new
  8. shipped_openpgp_keys.each do |key|
  9. begin
  10. step "the shipped OpenPGP key #{key} will be valid for the next #{months} months"
  11. rescue Test::Unit::AssertionFailedError
  12. invalid << key
  13. next
  14. end
  15. end
  16. assert(invalid.empty?, "The following key(s) will not be valid in #{months} months: #{invalid.join(', ')}")
  17. end
  18. Then /^the shipped (?:Debian repository key|OpenPGP key ([A-Z0-9]+)) will be valid for the next (\d+) months$/ do |fingerprint, max_months|
  19. if fingerprint
  20. cmd = 'gpg'
  21. user = LIVE_USER
  22. else
  23. fingerprint = TAILS_DEBIAN_REPO_KEY
  24. cmd = 'apt-key adv'
  25. user = 'root'
  26. end
  27. shipped_sig_key_info = $vm.execute_successfully("#{cmd} --batch --list-key #{fingerprint}", :user => user).stdout
  28. m = /\[expire[ds]: ([0-9-]*)\]/.match(shipped_sig_key_info)
  29. if m
  30. expiration_date = Date.parse(m[1])
  31. assert((expiration_date << max_months.to_i) > DateTime.now,
  32. "The shipped key #{fingerprint} will not be valid #{max_months} months from now.")
  33. end
  34. end
  35. Then /^I double-click the Report an Error launcher on the desktop$/ do
  36. @screen.wait_and_double_click('DesktopReportAnError.png', 30)
  37. end
  38. Then /^the live user has been setup by live\-boot$/ do
  39. assert($vm.execute("test -e /var/lib/live/config/user-setup").success?,
  40. "live-boot failed its user-setup")
  41. actual_username = $vm.execute(". /etc/live/config/username.conf; " +
  42. "echo $LIVE_USERNAME").stdout.chomp
  43. assert_equal(LIVE_USER, actual_username)
  44. end
  45. Then /^the live user is a member of only its own group and "(.*?)"$/ do |groups|
  46. expected_groups = groups.split(" ") << LIVE_USER
  47. actual_groups = $vm.execute("groups #{LIVE_USER}").stdout.chomp.sub(/^#{LIVE_USER} : /, "").split(" ")
  48. unexpected = actual_groups - expected_groups
  49. missing = expected_groups - actual_groups
  50. assert_equal(0, unexpected.size,
  51. "live user in unexpected groups #{unexpected}")
  52. assert_equal(0, missing.size,
  53. "live user not in expected groups #{missing}")
  54. end
  55. Then /^the live user owns its home dir and it has normal permissions$/ do
  56. home = "/home/#{LIVE_USER}"
  57. assert($vm.execute("test -d #{home}").success?,
  58. "The live user's home doesn't exist or is not a directory")
  59. owner = $vm.execute("stat -c %U:%G #{home}").stdout.chomp
  60. perms = $vm.execute("stat -c %a #{home}").stdout.chomp
  61. assert_equal("#{LIVE_USER}:#{LIVE_USER}", owner)
  62. assert_equal("700", perms)
  63. end
  64. Then /^no unexpected services are listening for network connections$/ do
  65. netstat_cmd = $vm.execute("netstat -ltupn")
  66. assert netstat_cmd.success?
  67. for line in netstat_cmd.stdout.chomp.split("\n") do
  68. splitted = line.split(/[[:blank:]]+/)
  69. proto = splitted[0]
  70. if proto == "tcp"
  71. proc_index = 6
  72. elsif proto == "udp"
  73. proc_index = 5
  74. else
  75. next
  76. end
  77. laddr, lport = splitted[3].split(":")
  78. proc = splitted[proc_index].split("/")[1]
  79. # Services listening on loopback is not a threat
  80. if /127(\.[[:digit:]]{1,3}){3}/.match(laddr).nil?
  81. if SERVICES_EXPECTED_ON_ALL_IFACES.include? [proc, laddr, lport] or
  82. SERVICES_EXPECTED_ON_ALL_IFACES.include? [proc, laddr, "*"]
  83. puts "Service '#{proc}' is listening on #{laddr}:#{lport} " +
  84. "but has an exception"
  85. else
  86. raise "Unexpected service '#{proc}' listening on #{laddr}:#{lport}"
  87. end
  88. end
  89. end
  90. end
  91. When /^Tails has booted a 32-bit kernel$/ do
  92. assert(! $vm.execute("uname -r | grep -qs 'amd64$'").success?,
  93. "Tails has not booted a 32-bit kernel.")
  94. end
  95. When /^Tails has booted a 64-bit kernel$/ do
  96. assert($vm.execute("uname -r | grep -qs 'amd64$'").success?,
  97. "Tails has not booted a 64-bit kernel.")
  98. end
  99. Then /^there is no screenshot in the live user's Pictures directory$/ do
  100. pictures_directory = "/home/#{LIVE_USER}/Pictures"
  101. assert($vm.execute(
  102. "find '#{pictures_directory}' -name 'Screenshot*.png' -maxdepth 1"
  103. ).stdout.empty?,
  104. "Existing screenshots were found in the live user's Pictures directory.")
  105. end
  106. Then /^a screenshot is saved to the live user's Pictures directory$/ do
  107. pictures_directory = "/home/#{LIVE_USER}/Pictures"
  108. try_for(10, :msg=> "No screenshot was created in #{pictures_directory}") do
  109. !$vm.execute(
  110. "find '#{pictures_directory}' -name 'Screenshot*.png' -maxdepth 1"
  111. ).stdout.empty?
  112. end
  113. end
  114. Then /^the VirtualBox guest modules are available$/ do
  115. assert($vm.execute("modinfo vboxguest").success?,
  116. "The vboxguest module is not available.")
  117. end
  118. Given /^I setup a filesystem share containing a sample PNG$/ do
  119. shared_png_dir_on_host = "#{$config["TMPDIR"]}/shared_png_dir"
  120. @shared_png_dir_on_guest = "/tmp/shared_png_dir"
  121. FileUtils.mkdir_p(shared_png_dir_on_host)
  122. Dir.glob("#{MISC_FILES_DIR}/*.png") do |png_file|
  123. FileUtils.cp(png_file, shared_png_dir_on_host)
  124. end
  125. add_after_scenario_hook { FileUtils.rm_r(shared_png_dir_on_host) }
  126. $vm.add_share(shared_png_dir_on_host, @shared_png_dir_on_guest)
  127. end
  128. Then /^the support documentation page opens in Tor Browser$/ do
  129. if @language == 'German'
  130. expected_title = 'Tails - Hilfe & Support'
  131. expected_heading = 'Die Dokumentation durchsuchen'
  132. else
  133. expected_title = 'Tails - Support'
  134. expected_heading = 'Search the documentation'
  135. end
  136. step "\"#{expected_title}\" has loaded in the Tor Browser"
  137. headings = Dogtail::Application.new('Firefox')
  138. .child(expected_title, roleName: 'document frame')
  139. .children(roleName: 'heading')
  140. assert(
  141. headings.any? { |heading| heading.text == expected_heading }
  142. )
  143. end
  144. Then /^MAT can clean some sample PNG file$/ do
  145. for png_on_host in Dir.glob("#{MISC_FILES_DIR}/*.png") do
  146. png_name = File.basename(png_on_host)
  147. png_on_guest = "/home/#{LIVE_USER}/#{png_name}"
  148. step "I copy \"#{@shared_png_dir_on_guest}/#{png_name}\" to \"#{png_on_guest}\" as user \"#{LIVE_USER}\""
  149. raw_check_cmd = "grep --quiet --fixed-strings --text " +
  150. "'Created with GIMP' '#{png_on_guest}'"
  151. assert($vm.execute(raw_check_cmd, user: LIVE_USER).success?,
  152. 'The comment is not present in the PNG')
  153. check_before = $vm.execute_successfully("mat --check '#{png_on_guest}'",
  154. :user => LIVE_USER).stdout
  155. assert(check_before.include?("#{png_on_guest} is not clean"),
  156. "MAT failed to see that '#{png_on_host}' is dirty")
  157. $vm.execute_successfully("mat '#{png_on_guest}'", :user => LIVE_USER)
  158. check_after = $vm.execute_successfully("mat --check '#{png_on_guest}'",
  159. :user => LIVE_USER).stdout
  160. assert(check_after.include?("#{png_on_guest} is clean"),
  161. "MAT failed to clean '#{png_on_host}'")
  162. assert($vm.execute(raw_check_cmd, user: LIVE_USER).failure?,
  163. 'The comment is still present in the PNG')
  164. $vm.execute_successfully("rm '#{png_on_guest}'")
  165. end
  166. end
  167. Then /^AppArmor is enabled$/ do
  168. assert($vm.execute("aa-status").success?, "AppArmor is not enabled")
  169. end
  170. Then /^some AppArmor profiles are enforced$/ do
  171. assert($vm.execute("aa-status --enforced").stdout.chomp.to_i > 0,
  172. "No AppArmor profile is enforced")
  173. end
  174. def get_seccomp_status(process)
  175. assert($vm.has_process?(process), "Process #{process} not running.")
  176. pid = $vm.pidof(process)[0]
  177. status = $vm.file_content("/proc/#{pid}/status")
  178. return status.match(/^Seccomp:\s+([0-9])/)[1].chomp.to_i
  179. end
  180. def get_apparmor_status(pid)
  181. apparmor_status = $vm.file_content("/proc/#{pid}/attr/current").chomp
  182. if apparmor_status.include?(')')
  183. # matches something like /usr/sbin/cupsd (enforce)
  184. # and only returns what's in the parentheses
  185. return apparmor_status.match(/[^\s]+\s+\((.+)\)$/)[1].chomp
  186. else
  187. return apparmor_status
  188. end
  189. end
  190. Then /^the running process "(.+)" is confined with AppArmor in (complain|enforce) mode$/ do |process, mode|
  191. if process == 'i2p'
  192. $vm.execute_successfully('service i2p status')
  193. pid = $vm.file_content('/run/i2p/i2p.pid').chomp
  194. else
  195. assert($vm.has_process?(process), "Process #{process} not running.")
  196. pid = $vm.pidof(process)[0]
  197. end
  198. assert_equal(mode, get_apparmor_status(pid))
  199. end
  200. Then /^the running process "(.+)" is confined with Seccomp in (filter|strict) mode$/ do |process,mode|
  201. status = get_seccomp_status(process)
  202. if mode == 'strict'
  203. assert_equal(1, status, "#{process} not confined with Seccomp in strict mode")
  204. elsif mode == 'filter'
  205. assert_equal(2, status, "#{process} not confined with Seccomp in filter mode")
  206. else
  207. raise "Unsupported mode #{mode} passed"
  208. end
  209. end
  210. Then /^tails-debugging-info is not susceptible to symlink attacks$/ do
  211. secret_file = '/secret'
  212. secret_contents = 'T0P S3Cr1t -- 3yEs oN1y'
  213. $vm.file_append(secret_file, secret_contents)
  214. $vm.execute_successfully("chmod u=rw,go= #{secret_file}")
  215. $vm.execute_successfully("chown root:root #{secret_file}")
  216. script_path = '/usr/local/sbin/tails-debugging-info'
  217. script_lines = $vm.file_content(script_path).split("\n")
  218. script_lines.grep(/^debug_file\s+/).each do |line|
  219. _, user, debug_file = line.split
  220. # root can always mount symlink attacks
  221. next if user == 'root'
  222. # Remove quoting around the file
  223. debug_file.gsub!(/["']/, '')
  224. # Skip files that do not exist, or cannot be removed (e.g. the
  225. # ones in /proc).
  226. next if not($vm.execute("rm #{debug_file}").success?)
  227. # Check what would happen *if* the amnesia user managed to replace
  228. # the debugging file with a symlink to the secret.
  229. $vm.execute_successfully("ln -s #{secret_file} #{debug_file}")
  230. $vm.execute_successfully("chown --no-dereference #{LIVE_USER}:#{LIVE_USER} #{debug_file}")
  231. if $vm.execute("sudo /usr/local/sbin/tails-debugging-info | " +
  232. "grep '#{secret_contents}'",
  233. :user => LIVE_USER).success?
  234. raise "The secret was leaked by tails-debugging-info via '#{debug_file}'"
  235. end
  236. # Remove the secret so it cannot possibly interfere with the
  237. # following iterations (even though it should not).
  238. $vm.execute_successfully("echo > #{debug_file}")
  239. end
  240. end
  241. When /^I disable all networking in the Tails Greeter$/ do
  242. begin
  243. @screen.click('TailsGreeterDisableAllNetworking.png')
  244. rescue FindFailed
  245. @screen.type(Sikuli::Key.PAGE_DOWN)
  246. @screen.click('TailsGreeterDisableAllNetworking.png')
  247. end
  248. end
  249. Then /^the Tor Status icon tells me that Tor is( not)? usable$/ do |not_usable|
  250. picture = not_usable ? 'TorStatusNotUsable' : 'TorStatusUsable'
  251. @screen.find("#{picture}.png")
  252. end