Incomplete work on -q
[signduterre.git] / signduterre.py
blobbc775fe1981d8575c0569648a6497fb9d0f44c39
1 #!/usr/bin/python
2 manual = """
3 Signature-du-Terroir
4 Construct a signature of the installed software state or check the integrity of the installation
5 using a previously made signature.
7 Usage: signduterre.py [options] FILE1 FILE2 ...
9 Options:
10 -h, --help show this help message and exit
11 -s HEX, --salt=HEX Enter salt in cleartext. If not given, a hexadecimal
12 salt will be suggested. The SUGGESTED[=N] keyword will
13 cause the selection of the suggested string. N is the
14 number of salts generated (default N=1)
15 -p TEXT, --passphrase=TEXT
16 Enter passphrase in cleartext. If not given, a cleartext
17 passphrase will be suggested. The keyword SUGGESTED
18 will cause the suggested passphrase to be used.
19 Entering the name of an existing file will cause it to
20 be read and a random passphrase found in the file will
21 be used (creating a signature), or they will all be used
22 in sequence (--check-file).
23 -c FILE, --check-file=FILE
24 Check contents of file (output of previous run)
25 -i FILE, --input-file=FILE
26 Use names from input-file (one filename per line)
27 -u USER, --user=USER Execute $(cmd) as USER, default 'nobody' (root/sudo
28 only)
29 -S, --Status For each file, add a line with unvarying file status
30 information: st_mode, st_ino, st_dev, st_uid, st_gid,
31 and st_size (like the '?' prefix, default False)
32 -t, --total-only Only print the total hash, must be checked BEFORE
33 running --detail (default True)
34 -d, --detailed-view Print hashes of individual files, must be checked
35 AFTER running --total-only check (default False)
36 -e, --execute Interpret $(cmd) (default False)
37 -n, --no-execute Explicitely do NOT Interpret ${ENV} and $(cmd)
38 -m, --manual Print the manual and exit
39 -r, --release-notes Print the release notes and exit
40 -l, --license Print license text and exit
41 -v, --verbose Print more information
42 -q, --quiet Print minimal information (hide filenames)
44 FILE1 FILE2 ...
45 Names and paths of one or more files to be checked. Any name starting with a '$', eg, $PATH, will be
46 interpreted as an environmental variable or command according to the bash conventions:
47 '$ENV' and '${ENV}' as variables, '$(cmd;cmd...)' as system commands (bash --restricted -c 'cmd;cmd...' PID).
48 Where PID the current Process ID is (available as positional parameter $0). Do not forget to enclose the
49 arguments in single ''-quotes! The commands are scanned for unwanted characters (eg, ') and these are removed.
50 The use of '$(cmd;cmd...)' requires explicit use of the -e or --execute option.
52 If executed as root or sudo, $(cmd;cmd...) will be executed as 'sudo -H -u <user>' which defaults to
53 --user nobody ('--user root' is at your own risk). This will obviously not work when invoked as non-root/sudo.
54 --user root is necessary when you need to check privileged information, eg,
55 you want to check the MBR with '$(dd if=/dev/hda bs=512 count=1 | od -X)'
56 However, as you might use --check-file with files you did not create yourself, it is important to
57 be warned if commands are to be executed.
59 Interpretation of $() ONLY works if the -e or --execute options are entered. signduterre.py can easily
60 be adapted to automatically use the setting in the check-file. However, this is deemed insecure and
61 commented out in the distribution version.
63 The -n or --no-execute option explicitely supress the interpretation of $(cmd) arguments.
65 Meta information from lstat on files is signed when the filename is preceded by a '?'. '?./signduterre.py' will
66 extract (st_mode, st_ino, st_dev, st_nlinks, st_uid, st_gid, st_size) and hash a line of these data (visible with --verbose).
67 The --Status option will automatically add such a line in front of every file. Note that '?' is implied for directories.
68 both '/' and '?/' produce a hash of, eg,:
69 lstat(/) = [st_mode=041775, st_ino=2, st_dev=234881026, st_uid=0, st_gid=80, st_size=1360]
70 Note that nlinks of a directory include every file in the directory, so this will check whether files have been added
71 to a directory.
73 Signature-du-Terroir
75 A very simple security application to test for the integrity of files and "states" in a computer installation.
76 signduterre.py constructs a signature of the current system state and checks installation state with a previously made signature.
77 The files are hashed with a passphrase to allow detection of compromised systems while running on the same system.
78 The signature checking can be subverted, but the flexibillity of signduterre.py and the fact that the output of any command
79 can be tested should hamper automated root-kit attacks.
81 signduterre.py writes a total SHA-256 hash to STDOUT of all the files and commands entered as arguments. It can also write a
82 hash for each individual file (insecure). The output of a signature can be send to a file and later used to
83 check with --check-file. Hashes are calculated with a hashed salt + passphrase sequence pre-pended to create
84 unpredictable hashes. This procedure ensures that an attacker does not know whether or not the correct passphrase
85 has been entered. An attacker can only know when to supply the requested hash values if she knows
86 the passphrase or has copies available of all the tested files and output of commands to calculate the hashes
87 on the fly.
89 SECURITY WARNINGS:
91 When run on a compromised system, signduterre.py can be subverted if the attacker keeps a copy of all the files and
92 reroutes the open() and lstat() functions, or simply delegating signduterre.py to a chroot jail with the original system.
93 In principle, signduterre.py only checks whether the computer responds identically to when the sinature file was made.
94 There is no theoretic barrier against a compromised computer perfectly simulating the original system when tested,
95 but behaving adversely at other times. Except for running from clean boot media (USB?), I know of no theoretical
96 sound solution to this problem ;-)
98 However, this scenario assumes the use of unlimited resources and time. Inside a limited, real computer system, the
99 attacker must make compromises on what can and what cannot be simulated with the available time and hardware. The idea
100 behind signduterre.py is to "ask difficult questions" that increase the cost of simulating the original system
101 high enough to make detection of successful attacks likely. But signduterre.py simply intended to raise the bar.
102 One point is to store the times needed to create the original hashes. This timing can later be used to see whether
103 the new timings are reasonable. If the same hardware takes considerably longer to perform the same calculations,
104 or needs a much longer delay before it starts, the tester might want to see where this time is spent.
106 Signature-du-Terroir works on the assumption that any attacker in control of a compromised system cannot
107 predict whether the passphrase entered is correct or not. An attacker can always intercept the in- and output of
108 signduterre. When running a --check-file, this means the program can be made to print out OK irrespective of the
109 tests. A safe use of signduterre.py is to start with a random number of incorrect passphrases and see whether they fail.
110 Repeat:
111 THE CORRECT USE OF signduterre.py IS TO ENTER A RANDOM NUMBER OF INCORRECT PASSPHRASES FOR EACH TEST
112 AND SEE WHETHER IT FAILS EVERY TIME!
114 On a compromised system, signduterre.py's detailed file testing (--detailed-view) is easily subverted. With a
115 matched file hash, the attacker will know that the correct passphrase has been entered and can print out the
116 stored hashes or 'ok's for the rest of the checks. So if the attacker keeps any entry in the signature file
117 uncompromised, she can intercept the output, test the password on the unchanged entry and substitute the
118 requested hashes for the output if the hash of that entry matches.
120 When checking for root-kits and other malware, it is safest to compare the signature files from a different,
121 clean, system. But then you would not need signduterre.py anyway.
122 If you have to work on the system itself, only use the -t or --total-only options to create
123 signatures with a total hash and without individual file hashes. Such a signature can be used to check
124 whether the system is unchanged. Another signature file WITH A DIFFERENT PASSPHRASE can then be used to
125 identify the individual files that have changed. If a detailed signature file has the same passphrase,
126 an attacker could use that other file to read the individual file hashes to check whether the correct
127 passphrase was entered.
129 Using the --check-file option in itself is perfectly UNsafe. An attacker simply has to print out 'OK' and
130 to defeat the check. This attack can be foiled by making it unpredictable when signduterre.py should return
131 'OK'. This can be done by using a list of salts or passphrases where only one of them (or none!) is correct.
132 Any attacker will have to guess when to return 'OK'.
134 As generating and entering wrong passphrases and salts is tedious, it is to be expected that users will
135 take shortcuts. To assist users, the '--salt SUGGESTED=<N>' option will generate a number N of salts. When
136 checking, each of these salts is tried in turn. An attacker that is unable to simulate the uncompromised
137 system will have to guess which one of the salts is the correct one, and whether or not the passphrase
138 is correct. This increases the chances of detecting compromised systems.
140 The '--passphrase SUGGESTED=N' option will generate and print N passphrases. One of these is chosen at
141 random for the signature. The number of the chosen passphrase is printed on STDERR with the passwords.
142 When checking a file, the stored passphrases can be read in again, either by entering the passphrase
143 file after the --passphrase option ('--passphrase <passphrase file>'), or directly from the --check-file.
144 signduterre.py will print out the result for each of the passphrases.
146 Note, that storing passphrases in a file and feeding it to signduterre.py is MUCH less secure than just
147 typing them in. Moreover, it might completely defeat the purpose of signduterre.py. If future experiences
148 cast any more doubt on the security ofthis option, it will be removed.
150 For those who want to know more about what an "ideal attacker" can do, see:
151 Ken Thompson "Reflections on Trusting Trust"
152 http://www.acm.org/classics/sep95/
154 David A Wheeler "Countering Trusting Trust through Diverse Double-Compiling"
155 http://www.acsa-admin.org/2005/abstracts/47.html
157 and the discussion of these at Bruce Schneier's 'Countering "Trusting Trust"'
158 http://www.schneier.com/blog/archives/2006/01/countering_trus.html
160 Manual
162 The intent of signduterre.py is to ensure that the signature cannot be subverted even if the system has been compromised
163 by an attacker that has obtained root control over the computer and any existing signature files.
165 signduterre.py asks for a passphrase which is PRE-pended to every file before the hash is constructed (unless the
166 passphrase is entered with an option). As long as the passphrase is not compromised, the hashes cannot
167 be reconstructed. A randomly generated, unpadded base-64 encoded 16 Byte password (ie, ~22 characters) is suggested in
168 interactive use. If '--passphrase SUGGESTED' is entered on the command line as the salt, the suggested
169 value will be used. This value is printed to STDERR (the screen or 2) for safe keeping. Please, make sure
170 you store the printed passphrase. For instance:
171 python signduterre.py -p SUGGESTED -s SUGGESTED /boot/* /sbin/* /bin/* \\
172 2> Signature_`date "+%Y%m%d_%H-%M-%S"`.pwd > Signature_`date "+%Y%m%d_%H-%M-%S"`.txt
173 will store the passphrase (and all error messages) in a file like 'Signature_20090630_11-14-03.pwd'
174 and the checik-file in 'Signature_20090630_11-14-03.txt'.
176 It is not secure to store files with the passphrase on the system you want to check. However, you could
177 pipe STDERR to some safe site.
179 Good passphrases are difficult to remember, so their plaintext form should be protected. To protect the
180 passphrase against rainbow and brute force attacks, the passphrase is concatenated to a salt phrase and
181 hashed before use (SHA-256).
183 The salt phrase is requested when constructing a signature. In interactive use, an 8 byte hexadecimal
184 (= 16 character) salt from /dev/urandom is suggested. If '--salt SUGGESTED' is entered on the command line
185 as the salt, the suggested value will be used. The salt is printed in plaintext to the output.
186 The salt will make it more difficult to determine whether the same passphrase has been used to create
187 different signatures.
189 At the bottom, a 'TOTAL HASH' line will be printed that hashes all the lines printed for the files. This includes
190 the file names as printed on the hash lines. It is not inconceivable that existing signature files could have been
191 compromised in ways that might be missed when checking the signature. The total hash will point out such changes.
193 Examples:
194 > python signduterre.py --execute --detail --salt 436a73e3 --passphrase liauwefa3251EWC signduterre.py /sbin/* /bin/* \\
195 /usr/bin/find /usr/bin/file /usr/bin/python* '${PATH}' > Signature_`date "+%Y%m%d_%H-%M-%S"`.txt
197 Prints a signature to the file Signature_20090625_14-31-54.txt (with your date). The signature contains the
198 SHA-256 hashes of the files, signduterre.py, /sbin/*, /bin/*, /usr/bin/find, /usr/bin/file, /usr/bin/python*,
199 and a hash of the PATH environment variable.
201 > python signduterre.py --execute --detail --salt SUGGESTED --passphrase SUGGESTED --Status --detailed-view \\
202 signduterre.py /sbin/* /bin/* /usr/bin/find /usr/bin/file /usr/bin/python* '${PATH}' \\
203 2> Signature_`date "+%Y%m%d_%H-%M-%S"`.pwd > Signature_`date "+%Y%m%d_%H-%M-%S"`.txt
205 Prints a signature to the system Signature_20090625_14-31-54.txt (with your date) and the automatically generated
206 password to Signature_20090625_14-31-54.pwd (with your date). The salt will be automatically determined.
207 The signature contains the SHA-256 hashes of the file status and file contents of signduterre.py, /sbin/*,
208 /bin/*, /usr/bin/find, /usr/bin/file, /usr/bin/python* on separate lines, and a hash of the PATH environment variable.
210 > python signduterre.py --execute --passphrase liauwefa3251EWC -c Signature_20090625_14-31-54.txt
212 Will check the files and PATH variable from the signature file Signature_20090625_14-31-54.txt.
214 > python signduterre.py --passphrase liauwefa3251EWC -c Signature_20090625_14-31-54.txt
215 > python signduterre.py --passphrase liauwefa3251EWC -c Signature_20090625_14-31-54.txt --no-execute
217 Will both fail if Signature_20090625_14-31-54.txt contains a $(cmd) entry. The --no-execute
218 option is default and prevents the execute option (if reading the execute optionfrom the signature file
219 has been activated).
221 > python signduterre.py signduterre.py --salt SUGGESTED -passphrase SUGGESTED=20 signduterre.py \\
222 &> Signature_`date "+%Y%m%d_%H-%M-%S"`.txt
224 Will generate and print 20 passphrases and print a signature using one randomly chosen passphrase
225 from these 20. Everything is written to a single file 'Signature_20090630_16-44-34.txt'.
226 On some systems, this means that the passphrases are written in between the hashes messing up the
227 file names. This will obviously make the output file useless.
229 > python signduterre.py signduterre.py -c Signature_20090630_16-44-34.txt
231 Will check all 20 passphrases generated before from the Signature file and print the results.
233 > sudo python signduterre.py -u root -s SUGGESTED -p SUGGESTED -v -e -t \\
234 '$(ls -LCid /proc/$0/root /proc/$0/exe|sed "s|/$0/|/PID/|g")' '$(dd if=/dev/hda bs=512 count=1 | od -X)' \\
235 >Signature_`date "+%Y%m%d_%H-%M-%S"`.txt
237 Will hash the inode numbers of the effective root directory (eg, chroot) and the executable (python)
238 together with the contents of the MBR (Master Boot Record) in Hex. It uses suggested salt and passphrase.
239 Accessing /dev/hda is only possible when root, so the command is entered with sudo and --user root.
243 license = """
244 Signature-du-Terroir
245 Construct a signature of the installed software state or check a previously made signature.
247 copyright 2009, R.J.J.H. van Son
249 This program is free software: you can redistribute it and/or modify
250 it under the terms of the GNU General Public License as published by
251 the Free Software Foundation, either version 3 of the License, or
252 (at your option) any later version.
254 This program is distributed in the hope that it will be useful,
255 but WITHOUT ANY WARRANTY; without even the implied warranty of
256 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
257 GNU General Public License for more details.
259 You should have received a copy of the GNU General Public License
260 along with this program. If not, see <http://www.gnu.org/licenses/>.
261 """;
263 # Note that only release notes are put here
264 # See git repository for detailed change comments:
265 # git clone git://repo.or.cz/signduterre.git
266 releasenotes = """
267 20090713 - Added --quiet option
268 20090712 - moved from /dev/random to /dev/urandom
269 20090702 - Replaced -g with -p SUGGESTED[=N]
270 20090702 - Generating and testing lists of random salts
271 20090701 - Release v0.2
272 20090630 - Generating and testing random passphrases
273 20090630 - --execute works on $(cmd) only, nlinks in ?path and ? implied for directories
274 20090630 - Ported to Python 3.0
276 20090628 - Release v0.1b
277 20090628 - Added release-notes
279 20090626 - Release v0.1a
280 20090626 - Initial commit to Git
281 """;
283 import sys;
284 import os;
285 import stat;
286 import subprocess;
287 # if sys.stdout.isatty(): import readline;
288 import binascii;
289 import hashlib;
290 import re;
291 import time;
292 from optparse import OptionParser;
293 import base64;
294 import random;
295 import struct;
297 # Limit the characters that can be used in $(cmd) commands
298 not_allowed_chars = re.compile('[^\w\ \.\/\"\|\;\,\-\$\[\]\{\}\(\)\@\`\!\*]');
300 programname = "Signature-du-Terroir";
301 version = "0.2";
303 print("# Program: "+programname + " version " + version + "\n");
305 parser = OptionParser()
306 parser.add_option("-s", "--salt", metavar="HEX",
307 dest="salt", default=False,
308 help="Enter salt in cleartext. If not given, a hexadecimal salt will be suggested. The SUGGESTED[=N] keyword will cause the selection of the suggested string. N is the number of salts generated (default N=1)")
309 parser.add_option("-p", "--passphrase", metavar="TEXT",
310 dest="passphrase", default=False,
311 help="Enter passphrase in cleartext, the keyword SUGGESTED will cause the suggested passphrase to be used. Entering the name of an existing file will cause it to be read and a random passphrase found in the file will be used (creating a signature), or they will all be used in sequence (--check-file).")
312 parser.add_option("-c", "--check-file",
313 dest="check", default=False, metavar="FILE",
314 help="Check contents of file (output of previous run)")
315 parser.add_option("-i", "--input-file",
316 dest="input", default=False, metavar="FILE",
317 help="Use names from input-file (one filename per line)")
318 parser.add_option("-u", "--user",
319 dest="user", default="nobody", metavar="USER",
320 help="Execute $(cmd) as USER, default 'nobody' (root/sudo only)")
321 parser.add_option("-S", "--Status",
322 dest="status", default=False, action="store_true",
323 help="For each file, add a line with unvarying file status information: st_mode, st_ino, st_dev, st_uid, st_gid, and st_size (like the '?' prefix, default False)")
324 parser.add_option("-t", "--total-only",
325 dest="total", default=False, action="store_true",
326 help="Only print the total hash, must be checked BEFORE running --detail (default True)")
327 parser.add_option("-d", "--detailed-view",
328 dest="detail", default=False, action="store_true",
329 help="Print hashes of individual files, must be checked AFTER running --total-only check (default False)")
330 parser.add_option("-e", "--execute",
331 dest="execute", default=False, action="store_true",
332 help="Interpret $(cmd) (default False)")
333 parser.add_option("-n", "--no-execute",
334 dest="noexecute", default=False, action="store_true",
335 help="Explicitely do NOT Interpret ${ENV} and $(cmd)")
336 parser.add_option("-m", "--manual",
337 dest="manual", default=False, action="store_true",
338 help="Print the manual and exit")
339 parser.add_option("-r", "--release-notes",
340 dest="releasenotes", default=False, action="store_true",
341 help="Print the release notes and exit")
342 parser.add_option("-l", "--license",
343 dest="license", default=False, action="store_true",
344 help="Print license text and exit")
345 parser.add_option("-v", "--verbose",
346 dest="verbose", default=False, action="store_true",
347 help="Print more information")
348 parser.add_option("-q", "--quiet",
349 dest="quiet", default=False, action="store_true",
350 help="Print minimal information (hide filenames)")
352 (options, check_filenames) = parser.parse_args();
353 # Print license
354 if options.license:
355 print (license, file=sys.stderr);
356 exit(0);
357 # Print manual
358 if options.manual:
359 print (manual, file=sys.stderr);
360 exit(0);
361 # Print manual
362 if options.releasenotes:
363 print ("Version: "+version, file=sys.stderr);
364 print (releasenotes, file=sys.stderr);
365 exit(0);
367 my_salt = options.salt;
368 my_passphrase = options.passphrase;
369 my_check = options.check;
370 my_status = options.status;
371 my_verbose = options.verbose and not options.quiet;
372 my_quiet = options.quiet;
373 execute = options.execute;
374 noexecute = options.noexecute;
375 input_file = options.input;
377 # Set total-only with the correct default
378 total_only = True;
379 total_only = not options.detail;
380 if options.total: total_only = options.total;
381 if my_check: total_only = False;
383 my_user = options.user;
384 # Things might be executed as another user
385 user_change = '';
386 if os.getuid() == 0:
387 user_change = 'sudo -H -u '+my_user+' ';
388 print("User: "+my_user);
390 # Execute option
391 if execute:
392 text_execute = "True";
393 else:
394 text_execute = "False";
396 if execute: print("Execute system commands: "+text_execute+"\n");
398 # --quiet option
399 if my_quiet: print("Quiet: True\n");
401 # Measure time intervals
402 start_time = time.time();
404 dev_random = open("/dev/urandom", 'rb');
406 # Read input-file
407 if input_file:
408 with open(input_file, 'r') as i:
409 for line in i:
410 # Clean up filename
411 current_filename = re.sub('[^\w\-\.\/\$\{\(\)\}]', '', line);
412 check_filenames.append(current_filename);
414 stat_list = [];
415 for x in check_filenames:
416 if os.path.isdir(x):
417 x = '?'+x;
418 if my_status and not x.startswith(('?', '$')):
419 stat_list.append('?'+x);
420 stat_list.append(x);
421 check_filenames = stat_list;
423 # Read the check file
424 passphrase_list = [];
425 salt_list = [];
426 check_hashes = {};
427 total_hash = "";
428 if my_check:
429 print("# Checking: "+my_check+"\n");
430 arg_list = check_filenames;
431 check_filenames = [];
432 with open(my_check, 'r') as c:
433 for line in c:
434 match = re.search("Execute system commands:\s+(True|False)", line);
435 if match != None:
436 # Uncomment the next line if you want automatic --execute from the check-file (DANGEROUS)
437 # execute = match.group(1).upper() == 'TRUE';
438 continue;
440 match = re.search("Quiet:\s+(True|False)", line);
441 if match != None:
442 my_quiet = match.group(1).upper() == 'TRUE';
443 if my_quiet: my_verbose = False;
444 continue;
446 match = re.search("Salt\:\s+\'([\w]*)\'", line);
447 if match != None:
448 salt_list.append(match.group(1));
449 continue;
451 match = re.search("User\:\s+\'([\w]*)\'", line);
452 if match != None:
453 # Uncomment the next line if you want automatic --user from the check-file (DANGEROUS)
454 # my_user = match.group(1);
455 continue;
457 match = re.search("Passphrase\:\s+\'([^\']*)\'", line);
458 if match != None:
459 passphrase_list.append(match.group(1));
460 continue;
462 match = re.search("^\s*([a-f0-9]+)\s+\*(TOTAL HASH)\s*$", line)
463 if match != None:
464 total_hash = match.group(1);
465 continue;
467 match = re.search("^\s*([a-f0-9\-]+)\s+\*\[([0-9]+)\]\s*$", line)
468 if match != None:
469 filenumber = int(match.group(2));
470 # Watch out, arguments count from 0
471 check_filenames.append(arg_list[filenumber - 1]);
472 check_hashes['['+match.group(2)+']'] = match.group(1);
473 continue;
475 match = re.search("^\s*([a-f0-9\-]+)\s+\*(.*)\s*$", line)
476 if match != None:
477 check_filenames.append(match.group(2));
478 check_hashes[match.group(2)] = match.group(1);
479 continue;
481 # Seed Pseudo Random Number Generator
482 seed = dev_random.read(16);
483 random.seed(seed);
485 # Read suggested salts from /dev/(u)random if needed
486 if my_salt:
487 if my_salt.startswith('SUGGESTED'):
488 N=1;
489 match = re.search("([0-9][0-9]*)$", my_salt);
490 if match != None:
491 N = int(match.group(1));
492 for i in range(0,N):
493 salt = dev_random.read(8);
494 salt_list.append(str(binascii.hexlify(salt), 'ascii'));
495 else:
496 salt_list.append(my_salt);
497 elif len(salt_list) == 0:
498 salt = dev_random.read(8);
499 sys.stderr.write("Enter salt (suggest \'"+str(binascii.hexlify(salt), 'ascii')+"\'): ");
500 new_salt = input();
501 if not new_salt: new_salt = str(binascii.hexlify(salt), 'ascii');
502 salt_list.append(new_salt);
504 for my_salt in salt_list:
505 print("Salt: \'"+my_salt+"\'");
507 # Get passphrase
508 if my_passphrase and os.path.isfile(my_passphrase):
509 with open(my_passphrase, 'r') as file:
510 for line in file:
511 match = re.search("Passphrase\:\s+\'([^\']*)\'", line);
512 if match != None:
513 passphrase_list.append(match.group(1));
514 elif not my_passphrase and len(passphrase_list) == 0:
515 suggest_passphrase = dev_random.read(16);
516 sys.stderr.write("Enter passphrase (suggest \'"+str(base64.b64encode(suggest_passphrase), 'ascii').rstrip('=')+"\'): ");
517 # How kan we make this unreadable on input?
518 current_passphrase = input();
519 if not current_passphrase:
520 current_passphrase = str(base64.b64encode(suggest_passphrase), 'ascii').rstrip('=');
521 print("Passphrase: \'"+current_passphrase+"\'", file=sys.stderr);
522 passphrase_list.append(current_passphrase);
523 elif my_passphrase.startswith('SUGGESTED'):
524 N = 1;
525 match = re.search("([0-9][0-9]*)$", my_passphrase);
526 if match != None:
527 N = int(match.group(1));
528 j = int(random.random()*N);
529 for i in range(0, N):
530 suggest_passphrase = dev_random.read(16);
531 current_passphrase = str(base64.b64encode(suggest_passphrase), 'ascii').rstrip('=');
532 print("Passphrase: \'"+current_passphrase+"\'", file=sys.stderr);
533 passphrase_list.append(current_passphrase);
534 else:
535 passphrase_list.append(my_passphrase);
537 if not my_check:
538 j = int(random.random()*len(passphrase_list));
539 passphrase_list = [passphrase_list[j]];
540 print("# Selected passphrase:", j+1, file=sys.stderr);
541 j = int(random.random()*len(salt_list));
542 salt_list = [salt_list[j]];
543 print("# Selected salt:", j+1, file=sys.stderr);
545 # Close /dev/(u)random
546 dev_random.close;
548 end_time = time.time();
549 print("# Preparation time:", end_time - start_time, "seconds\n\n# Start signature");
551 pnum = 1;
552 snum = 1;
553 corrpnum = 0;
554 corrsnum = 0;
555 for my_passphrase in passphrase_list:
556 snum = 1;
557 for my_salt in salt_list:
558 file_argnum = 0;
559 start_time = time.time();
560 # Construct the passphrase hash
561 passphrase = hashlib.sha256();
563 passphrase.update(bytes(my_salt, encoding='ascii'));
564 passphrase.update(bytes(my_passphrase, encoding='ascii'));
566 # Create prefix which is a hash of the salt+passphrase
567 prefix = passphrase.hexdigest();
569 # Create signature and write output
570 if noexecute: execute = False; # Doubly make sure that NOTHING is executed if required
571 totalhash = hashlib.sha256();
572 totalhash.update(bytes(prefix, encoding='ascii'));
573 for org_filename in check_filenames:
574 # Create file hash object
575 filehash = hashlib.sha256();
576 filehash.update(bytes(prefix, encoding='ascii'));
577 # Remove []
578 filename = org_filename.strip('[');
579 filename = filename.rstrip(']');
580 print(filename, file=sys.stderr);
581 # Use system variables and commands
582 if filename.startswith('$'):
583 # Commands $(command)
584 match = re.search('^\$([\(\{]?)([^\)\}]+)[\)\}]?$', filename);
585 if match != None:
586 if match.group(1) == '(':
587 if not execute :
588 error_message = "Executable argument \'"+filename+"\' only allowed with the --execute flag";
589 print (error_message, file=sys.stderr);
590 if not sys.stdout.isatty(): print(error_message);
591 exit(1);
592 current_command = not_allowed_chars.sub(" ", match.group(2));
593 current_command_line = user_change+"bash --restricted -c \'"+current_command+"\' "+str(os.getpid());
594 if my_verbose:
595 print ("#", current_command_line);
596 (status, b) = subprocess.getstatusoutput(current_command_line);
597 if status != 0:
598 print ('$('+current_command+')'+"\n"+b, file=sys.stderr);
599 exit(status);
600 else:
601 current_var = not_allowed_chars.sub(" ", match.group(2));
602 if my_verbose:
603 print ("# echo $"+ current_var);
604 b = os.environ[current_var];
605 filehash.update(bytes(bytes(b, encoding='utf8')));
606 # lstat() meta information
607 elif filename.startswith('?'):
608 filestat = os.lstat(filename.lstrip('?'));
609 b = 'lstat('+filename.lstrip('?')+') = [st_mode='+str(oct(filestat.st_mode))+', st_ino='+str(filestat.st_ino)+', st_dev='+str(filestat.st_dev)+', st_nlink='+str(filestat.st_nlink)+', st_uid='+str(filestat.st_uid)+', st_gid='+str(filestat.st_gid)+', st_size='+str(filestat.st_size)+']';
610 filehash.update(bytes(b, encoding='utf8'));
611 if my_verbose:
612 print ("# "+ b);
613 # Use file
614 else:
615 # open and read the file
616 with open(filename, 'rb') as file:
617 for b in file:
618 filehash.update(b);
620 current_digest = filehash.hexdigest();
621 print_name = filename;
622 if my_quiet or org_filename.startswith('['):
623 file_argnum += 1;
624 print_name = '['+str(file_argnum)+']';
625 current_hash_line = current_digest+" *"+print_name
626 totalhash.update(bytes(current_hash_line, encoding='ascii'));
628 # Be careful to use this ONLY after totalhash has been updated!
629 if total_only: current_hash_line = (len(current_digest)*'-')+" *"+print_name;
631 # Write output
632 if not my_check:
633 print(current_hash_line);
634 elif check_hashes[print_name] == (len(current_digest)*'-'):
635 print(check_hashes[print_name]+" *"+print_name);
636 elif current_digest != check_hashes[filename]:
637 print("DIFFERENT: "+current_hash_line);
638 else:
639 print("ok"+" *"+filename);
641 # Handle total hash
642 current_total_digest = totalhash.hexdigest();
643 current_total_digest_line = current_total_digest+" *"+"TOTAL HASH";
644 end_time = time.time();
645 print("# \n# Total hash - Time to completion:", end_time - start_time, "seconds");
646 if not my_check:
647 print(current_total_digest_line+"\n");
648 elif current_total_digest != total_hash:
649 print("DIFFERENT: "+current_total_digest_line+"\n");
650 else:
651 match_number = "";
652 if len(passphrase_list) > 1 or len(salt_list): match_number = " #"
653 if len(passphrase_list) > 1: match_number += " passphrase no: "+str(pnum);
654 if len(salt_list) > 1: match_number += " salt no: "+str(snum);
655 print("OK"+" *"+"TOTAL HASH"+match_number+"\n");
656 corrsnum = snum;
657 corrpnum = pnum;
658 snum += 1;
659 pnum += 1;
661 if len(passphrase_list) > 1:
662 if corrpnum > 0:
663 print("Passphrase entry:",corrpnum,"matched");
664 else:
665 print("No passphrase entry matched!");
666 if len(salt_list) > 1:
667 if corrpnum > 0:
668 if corrsnum > 0:
669 print("Salt entry:",corrsnum,"matched");
670 else:
671 print("No salt entry matched!");
672 else:
673 print("No entry matched");