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