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