Added bug report for URLs, adapted bug report about ssh
[signduterre.git] / signduterre.py
blob9f08fcff16894e6b087bbe17d8da6881cb20d68f
1 #!/usr/bin/python3
3 # ToC
4 # 1. DOCUMENTATION
5 # 2. IMPORT & INITIALIZATION
6 # 3. OPTION HANDLING
7 # 4. ARGUMENT PROCESSING
8 # 5. SIGNATURE CREATION AND CHECKING
10 #############################################################################
11 # #
12 # DOCUMENTATION #
13 # #
14 #############################################################################
16 # The full manual can be printed as:
17 # - HTML: Replace '[[[' by < and ']]]' by >, protect <, > brackets in text
18 # - plain text long: Remove everything between '[[[' and ']]]'
19 # - plain text short: as long, but remove text between [[[LONG]]] and [[[/LONG]]]
20 # - makefile: Print only the text between [[[pre make=<label>]]] and [[[/pre]]],
21 # remove '\\\n' and replace '^\$' by "\t". Add label and grouped labels
22 # and a 'clean' action to complete functional makefile
24 manual = """
25 [[[!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"]]][[[html]]][[[header]]][[[title]]]Signature-du-Terroir[[[/title]]][[[/header]]][[[body]]][[[h1]]]Signature-du-Terroir[[[/h1]]][[[p]]]
26 Construct a signature of the installed software state or check the integrity of the installation
27 using a previously made signature.
28 [[[/p]]][[[p]]]
29 Usage: signduterre.py [options] FILE1 FILE2 ...
30 [[[/p]]][[[p]]]
31 Options:[[[/p]]][[[pre]]]
32 -h, --help show this help message and exit
33 -s HEX, --salt=HEX Enter salt in cleartext. If not given, a hexadecimal
34 salt will be suggested. The SUGGESTED[=N] keyword will
35 cause the selection of the suggested string. N is the
36 number of salts generated (default N=1). If N>1, all
37 will be printed and a random one will be used to
38 generate the signature (selection printed to STDERR).
39 -a, --all-salts-pattern
40 Use all salts in sequence, randomly replace salts with
41 incorrect ones in the output to create a pattern of
42 failing hashes indicated by a corresponding integer
43 number. Depends on '--salt SUGGESTED=N'. Implies
44 --total-only.
45 -p TEXT, --passphrase=TEXT
46 Enter passphrase in cleartext, the keyword
47 SUGGESTED[=N] will cause the suggested passphrase to
48 be used. If N>1, N passphrases will be printed to
49 STDERR and a random one will be used (selection
50 printed to STDERR). Entering the name of an existing
51 file (or '-' for STDIN) will cause it to be read and a
52 random passphrase found in the file will be used
53 (creating a signature), or they will all be used in
54 sequence (--check-file).
55 -c FILE, --check-file=FILE
56 Check contents with the output of a previous run from
57 file or STDIN ('-'). Except when the --quiet option is
58 given, the previous output will contain all
59 information needed for the program, but not the
60 passphrase and the --execute option.
61 -i FILE, --input-file=FILE
62 Use names from FILE or STDIN ('-'), use one filename
63 per line.
64 -o FILE, --output-file=FILE
65 Print to FILE instead of STDOUT.
66 --file-source=PATH Read all files from PATH. The PATH-string is prepended
67 to every plain file-path that is read for a signature.
68 Remote files can be checked with
69 'ssh://<user>@<host>[/path]'. The option overrules
70 any File source specification in the --check-file.
71 -P FILE, --Private-file=FILE
72 Print private information (passwords etc.) to FILE
73 instead of STDERR.
74 -u USER, --user=USER Execute $(cmd) as USER, default 'nobody' (root/sudo
75 only)
76 -S, --Status For each file, add a line with unvarying file status
77 information: st_mode, st_ino, st_dev, st_uid, st_gid,
78 and st_size (like the '?' prefix, default False)
79 --Status-values=MODE Status values to print for --Status, default MODE is
80 'fmidugs' (file, mode, inode, device, uid, gid, size).
81 Also available (n)l(inks) a(time), (m)t(ime), and
82 c(time).
83 -t, --total-only Only print the total hash, unsets --detailed-view
84 (default True)
85 -d, --detailed-view Print hashes of individual files, is unset by --total-
86 only (default False)
87 -e, --execute Interpret $(cmd) (default False)
88 --execute-args=ARGS Arguments for the $(cmd) commands ($1 ....)
89 -n, --no-execute Explicitely do NOT Interpret $(cmd)
90 --print-hexdump Print hexadecimal dump of input bytes to STDERR for
91 debugging purposes
92 -m, --manual Print a short version of the manual and exit
93 --manual-long Print the long version of the manual and exit
94 --manual-html Print the manual in HTML format and exit
95 --manual-make Print the examples in the manual as a makefile and
96 exit
97 -r, --release-notes Print the release notes and exit
98 -l, --license Print license text and exit
99 -v, --verbose Print more information on output
100 -q, --quiet Print minimal information (hide filenames). If the
101 output is used with --check-file, the command line
102 options and arguments must be repeated.
103 [[[/pre]]][[[p]]]
104 FILE1 FILE2 ...
105 Names and paths of one or more files to be checked. All file arguments in SdT accept '-' as the STDIN file
106 (ie, piped data). Can use ssh://<user>@<host>/path pseudo-URLs for checking files at remote sites.
107 [[[/p]]][[[p]]]
108 Any name starting with a '$', eg, $PATH, will be interpreted as an environmental variable or a command
109 according to the bash conventions: '$ENV' and '${ENV}' as variables, '$(cmd;cmd...)' as system commands
110 (bash --restricted -c 'cmd;cmd...' PID). Where PID the current Process ID is (available as positional
111 parameter $0). Other parameters can be entered with the --execute-args option ($1 etc). Do not forget to
112 enclose the arguments in single ''-quotes! The commands are scanned for unwanted characters and these
113 are removed (eg, ' and \\, however, escaping $ is allowed, eg, '\\$1'). The use of '$(cmd;cmd...)'
114 requires explicit use of the -e or --execute option.
115 [[[/p]]][[[p]]]
116 If executed as root or sudo, $(cmd;cmd...) will be executed as 'sudo -H -u <user>' which defaults to
117 --user nobody ('--user root' is at your own risk). This will obviously not work when invoked as non-root/sudo.
118 --user root is necessary when you need to check privileged information, eg, you want to check the MBR with
119 '$(dd if=/dev/hda bs=512 count=1 status=noxfer | od -X)'
120 However, as you might use --check-file with files you did not create yourself, it is important to
121 be warned if commands are to be executed.
122 [[[/p]]][[[p]]]
123 Interpretation of $() ONLY works if the -e or --execute options are entered. signduterre.py can easily
124 be adapted to automatically use the setting in the check-file. However, this is deemed insecure and
125 commented out in the distribution version.
126 [[[/p]]][[[p]]]
127 The -n or --no-execute option explicitely supress the interpretation of $(cmd) arguments.
128 [[[/p]]][[[p]]]
129 Meta information from stat() on files is signed when the filename is preceded by a '?'. '?./signduterre.py' will
130 extract (st_mode, st_ino, st_dev, st_nlinks, st_uid, st_gid, st_size) and hash a line of these data (visible
131 with --verbose). The --Status option will automatically add such a line in front of every file. Note that '?'
132 is implied for directories. Both '/' and '?/' produce a hash of, eg,:
133 [[[/p]]][[[pre]]]
134 stat(/) = [st_mode=041775, st_ino=2, st_dev=234881026, st_uid=0, st_gid=80, st_size=1360]
135 [[[/pre]]][[[p]]]
136 The --Status-values=<mode> option selects which status values will be used: f(ile), m(ode), i(node),
137 d(evice), u(id), g(id), s(ize), (n)l(inks), a(time), (m)t(ime), and c(time). Default is
138 --Status-values='fmidugs'. Note that nlinks of a directory include every file in the directory, so this
139 option can check whether files have been added to a directory.
140 [[[/p]]][[[p]]]
141 Arguments enclosed in []-brackets will be hidden in the output. That is, '[/proc/self/exe]' will show up as
142 '[1]' in the output (or '[n]' with n the number of the hidden argument), equivalent to the use of the
143 --quiet option. This means the hidden arguments must be entered again when using the --check-file (-c)
144 option.
145 [[[/p]]][[[p]]]
146 Signature-du-Terroir
147 [[[/p]]][[[p]]]
148 A very simple tool to generate a signature that can be used to test the integrity of files and "states" in
149 a running installation. signduterre.py constructs a signature of the current system state and checks
150 installation state with a previously made signature. The files are hashed with a passphrase to allow detection
151 of compromised systems while running on the same system. The signature checking can be subverted, but the
152 flexibillity of signduterre.py and the fact that the output of any command can be tested should hamper
153 automated root-kit attacks.
154 [[[/p]]][[[p]]]
155 signduterre.py writes a total SHA-256 hash to STDOUT of all the files and commands entered as arguments. It
156 can also write a hash for each individual file (insecure). The output of a signature can be send to a file and
157 later used to check with --check-file. Hashes are calculated with a hashed salt + passphrase sequence
158 pre-pended to create unpredictable hashes. This procedure ensures that an attacker does not know whether or
159 not the correct passphrase has been entered. An attacker can only know when to supply the requested hash
160 values if she knows the passphrase or has copies available of all the tested files and output of commands to
161 calculate the hashes on the fly.
162 [[[/p]]][[[LONG]]][[[p]]]
163 The Problem
164 [[[/p]]][[[p]]]
165 The problem SdT tries to solve is how to test whether your system has been compromised when you can only use
166 the potentially compromised system? The solution is to store a password encrypted signature (or fingerprint)
167 of your system when you are sure it is in a good state. Then you check whether the system can still
168 distinguish between correct and incorrect passwords when it regenerates the signature. The trick is to use
169 the right data (ie, questions) to generate the signature.
170 [[[/p]]][[[p]]]
171 The underlying idea is that some bits have to be changed to compromise a system. That is, program
172 files have been altered, settings and accounts changed, new processes are running or existing processes
173 altered. The most common situation is that some system programs have been changed to hide the traces of
174 the attack. For instance, the [[[i]]]ls[[[/i]]], [[[i]]]find[[[/i]]], and [[[i]]]stat[[[/i]]] commands are altered to hide the existence of new files
175 and programs, and the [[[i]]]netstat[[[/i]]] and [[[i]]]ps[[[/i]]] commands or the [[[i]]]/proc[[[/i]]] pseudo file system are changed to hide the
176 malicious processes that are running. Such wholescale adaptations of running systems can be executed
177 using standard, off-the-shelf application suits, so called rootkits. There are applications that can
178 detect common (known) rootkits and other malicious programs, eg, [[[i]]]chkrootkit[[[/i]]] ([[[a
179 href="http://www.chkrootkit.org/"]]]www.chkrootkit.org[[[/a]]]) and
180 [[[i]]]rootkit hunter[[[/i]]] ([[[a
181 href="http://www.rootkit.nl"]]]www.rootkit.nl[[[/a]]]). However, these rootkit detectors also use existing commands on the
182 potentially compromised system, so a rootkit can hide from them too.
183 [[[/p]]][[[p]]]
184 There are two obvious directions to guard against rootkits. One is to continuously run a process that
185 looks for attempts to install a rootkit and other malicious activities. The other is to take a snapshot
186 of the system in a known good state, and then flag changes in relevant areas, eg, like [[[i]]]Tripwire[[[/i]]]
187 ([[[a href="http://sourceforge.net/projects/tripwire/"
188 ]]]http://sourceforge.net/projects/tripwire/[[[/a]]]) and [[[i]]]Radmind[[[/i]]] ([[[a
189 href="http://rsug.itd.umich.edu/software/radmind/"
190 ]]]http://rsug.itd.umich.edu/software/radmind/[[[/a]]]).
191 Signature-du-Terroir takes the second route, it creates a signature of a set of relevant files and
192 command output, and checks later whether these have not been changed. However, when running such a test
193 on a compromised system, the attacker can theoretically "fool" any (automated) test. In practise, time
194 and other precious resources will limit what an attacker can accomplish. The idea is to raise the bar
195 for rootkits high enough to make them not worthwhile. SdT tries to make using signatures easy (cheap)
196 and subverting it difficult (expensive).
197 [[[/p]]][[[p]]]
198 As an illustration of the problem SdT treis to solve, take the [[[i]]]sha256sum[[[/i]]] command which generates file
199 hashes (signatures) using the SHA256 algorithm. Hashes can be generated and checked with this command:
200 [[[/p]]][[[pre]]]
201 # Use of sha256sum to check integrity of ps and ls commands
202 $ sha256sum /bin/ps /bin/ls > ps-ls.sh256
203 $ sha256sum -c ps-ls.sh256
204 [[[/pre]]][[[p]]]
205 A compromised file will show up as FAILED. This is ok for unintentional changes to the files. However, a
206 malicious attacker could easily replace [[[i]]]/usr/bin/sha256sum[[[/i]]] with a program that would replace the hash of
207 malicious replacements of these files with the hash sums of the original files. There are three easy ways
208 of doing that. Either simply say 'ok' when checking the file, print out the stored old hash value whenever
209 an altered file is requested by name, or look for the hash of the new, malicious replacement and print out
210 the old hash sum instead. The former two are easy to circumvent, the last one is somewhat less easy.
211 [[[/p]]][[[p]]]
212 The first solution to these avoidance strategies is to generate the signatures with a passphrase and random
213 string (salt). As long as the attacker does not know the passphrase, the only way to subvert SdT is to store
214 the original bits in the files and calculate the signature the moment SdT is called. As the attacker does
215 not know when the correct password or salt is entered, it is not possible to simply answer OK or repeat the
216 stored earlier results instead of calculating them de-novo.
217 [[[/p]]][[[p]]]
218 To be able to serve up the original bits, instead of the bits used on the compromised system, when asked
219 for the hashes, the attacker must divert attempts to read the files by SdT, but not at other moments.
220 There are many ways to do this, eg, running python in a chroot-jail, changing python itself, changing other
221 programs. To accommodate these diversion strategies, SdT allows to read data from each and every command
222 that can supply it. So, a binary file can be entered by name, with eg, cat, dd, perl, python, ruby, or read
223 from the [[[i]]]/proc[[[/i]]] system (if it is a running process), or from STDIN or shell subprocesses. For instance,
224 to protect against running in a chroot-jail, the inode number and device of the root directory can be read
225 from [[[i]]]/proc/self/root[[[/i]]], or [[[i]]]/proc/<PID>/root[[[/i]]], or simply from [[[i]]]/[[[/i]]].
226 [[[/p]]][[[/LONG]]][[[p]]]
227 Signature creation: Passphrases, salts, and hashes
228 [[[/p]]][[[p]]]
229 Good passphrases are difficult to remember, so their plaintext form should be protected. To protect the
230 passphrase against rainbow and brute force attacks, the passphrase is concatenated to a salt phrase and
231 hashed before use (SHA-256).
232 [[[/p]]][[[p]]]
233 The salt phrase is requested when constructing a signature. In interactive use, an 8 byte hexadecimal
234 (= 16 character) salt from [[[i]]]/dev/urandom[[[/i]]] is suggested. If '--salt SUGGESTED' is entered on the command line
235 as the salt, the suggested value will be used. The salt is printed in plaintext to the output. The salt will
236 make it more difficult to determine whether the same passphrase has been used to create different signatures.
237 [[[/p]]][[[p]]]
238 At the bottom, a 'TOTAL HASH' line will be printed that hashes all the lines printed for the files. This
239 includes the file names as printed on the hash lines. It is not inconceivable that existing signature files
240 could have been compromised in ways that might be missed when checking the signature. The total hash will
241 point out such changes.
242 [[[/p]]][[[p]]]
243 SECURITY
244 [[[/p]]][[[LONG]]][[[p]]]
245 When run on a compromised system, signduterre.py can be subverted if the attacker keeps a copy of all the
246 files and command outputs, and reroutes the open() and stat() functions, or simply delegating signduterre.py
247 to a chroot jail with the original system. In principle, signduterre.py only checks whether the computer
248 responds identically to when the signature file was made. There is no theoretic barrier against a compromised
249 computer perfectly simulating the original system when tested, but behaving adversely at other times. Except
250 for running from clean boot media (USB?), I know of no theoretical sound solution to this problem.
251 [[[/p]]][[[p]]]
252 However, this scenario assumes the use of unlimited resources and time. Inside a limited, real computer system,
253 the attacker must make compromises on what can and what cannot be simulated with the available time and
254 hardware. The idea behind signduterre.py is to "ask difficult questions" that increase the cost of simulating
255 the original system high enough to make detection of successful attacks likely.signduterre.py simply intends
256 to raise the bar high enoug. One point is to store the times needed to create the original hashes. This timing
257 can later be used to see whether the new timings are reasonable. If the same hardware takes considerably
258 longer to perform the same calculations, or needs a much longer delay before it starts, the tester might want
259 to see where this time is spent.
260 [[[/p]]][[[/LONG]]][[[p]]]
261 Signature-du-Terroir works on the assumption that any attacker in control of a compromised system cannot
262 predict whether the passphrase entered is correct or not. An attacker can always intercept the in- and output
263 of signduterre. When running with --check-file, this means the program can be made to print out OK
264 irrespective of the tests. A safe use of signduterre.py is to start with a random number of incorrect
265 passphrases and see whether they fail. Alternatively, and easier, is to add a number of unused salts
266 to the check-file and let the attacker guess which one is correct.
267 [[[/p]]][[[p]]]
268 THE CORRECT USE OF signduterre.py IS TO ENTER A RANDOM NUMBER OF INCORRECT PASSPHRASES OR SALTS FOR EACH
269 TEST AND SEE WHETHER IT FAILS AT THE CORRECT INSTANCES!
270 [[[/p]]][[[p]]]
271 On a compromised system, signduterre.py's detailed file testing (--detailed-view) is easily subverted. With a
272 matched file hash, the attacker will know that the correct passphrase has been entered and can print out the
273 stored hashes or 'ok's for the rest of the checks. So if the attacker keeps any entry in the signature file
274 uncompromised, she can intercept the output, test the password on the unchanged entry and substitute the
275 requested hashes for the output if the hash of that entry matches.
276 [[[/p]]][[[LONG]]][[[p]]]
277 When checking for root-kits and other malware, it is safest to compare the signature files from a different,
278 clean, system. But then you would not need signduterre.py anyway. If you have to work on the system itself,
279 only use the -t or --total-only options to create signatures with a total hash and without individual file
280 hashes. Such a signature can be used to check whether the system is unchanged. Another signature file WITH A
281 DIFFERENT PASSPHRASE can then be used to identify the individual files that have changed. If a detailed
282 signature file has the same passphrase, an attacker could use that other file to read the individual file
283 hashes to check whether the correct passphrase was entered.
284 [[[/p]]][[[/LONG]]][[[p]]]
285 Using the --check-file option in itself is UNsafe. An attacker simply has to print out 'OK' to defeat the
286 check. This attack can be foiled by making it unpredictable when signduterre.py should return 'OK'. This can
287 be done by using a list of salts or passphrases where only one of them (or none!) is correct. Any attacker
288 will have to guess when to return 'OK'.
289 [[[/p]]][[[LONG]]][[[p]]]
290 As generating and entering wrong passphrases and salts is tedious, users have to be supported in correct use
291 of SdT. To assist users, the '--salt SUGGESTED=<N>' option will generate a number N of salts. When
292 checking, each of these salts is tried in turn. An attacker that is unable to simulate the uncompromised
293 system will have to guess which one of the salts is the correct one, and whether or not the passphrase
294 is correct. This increases the chances of detecting compromised systems. If this is not enough guess
295 work, the '-a', '--all-salts-pattern' option will use all salts in sequence to generate total hashes,
296 but random salts will be changed in the output. This generates a pattern of failed salt tests. This pattern
297 is translated into a bit pattern and printed as an integer ([Fail, Fail, OK, Fail, OK, OK, Fail, OK]
298 = 00101101 (least significant first) = 10110100 (unsigned bin) = 180). On creation of a signature, this
299 number is printed to STDERR, on checking (--check-file) it is printed to STDOUT (note that the number
300 will never become 0 or all Fail). So for '--salt SUGGESTED=<N> --all-salts-pattern' the probability of
301 guessing the correct output goes from 1/N to 1/(2^N - 1). Note that '--all-salts-pattern' will work,
302 but is pointless, without '--salt SUGGESTED=<N>' with N>1.
303 [[[/p]]][[[p]]]
304 The '--passphrase SUGGESTED=N' option will generate and print N passphrases. One of these is chosen at
305 random for the signature. The number of the chosen passphrase is printed on STDERR with the passwords.
306 When checking a file, the stored passphrases can be read in again, either by entering the passphrase
307 file after the --passphrase option ('--passphrase <passphrase file>'), or directly from the --check-file.
308 signduterre.py will print out the result for each of the passphrases.
309 [[[/p]]][[[p]]]
310 Note, that storing passphrases in a file and feeding it to signduterre.py is MUCH less secure than just
311 typing them in. Moreover, it might completely defeat the purpose of signduterre.py. If future experiences
312 cast any more doubt on the security of this option, it will be removed.
313 [[[/p]]][[[p]]]
314 For those who want to know more about what an "ideal attacker" can do, see:[[[br]]]
315 Ken Thompson "Reflections on Trusting Trust"[[[br]]]
316 [[[a href="http://cm.bell-labs.com/who/ken/trust.html"]]]http://cm.bell-labs.com/who/ken/trust.html[[[/a]]][[[br]]]
317 [[[a href="http://www.ece.cmu.edu/~ganger/712.fall02/papers/p761-thompson.pdf"]]]http://www.ece.cmu.edu/~ganger/712.fall02/papers/p761-thompson.pdf[[[/a]]]
318 [[[/p]]][[[p]]]
319 David A Wheeler "Countering Trusting Trust through Diverse Double-Compiling"[[[br]]]
320 [[[a href="http://www.acsa-admin.org/2005/abstracts/47.html"]]]http://www.acsa-admin.org/2005/abstracts/47.html[[[/a]]]
321 [[[/p]]][[[p]]]
322 and the discussion of these at Bruce Schneier's 'Countering "Trusting Trust"'[[[br]]]
323 [[[a href="http://www.schneier.com/blog/archives/2006/01/countering_trus.html"]]]http://www.schneier.com/blog/archives/2006/01/countering_trus.html[[[/a]]]
324 [[[/p]]][[[/LONG]]][[[p]]]
325 Manual
326 [[[/p]]][[[p]]]
327 The intent of signduterre.py is to ensure that the signature cannot be subverted even if the system has been
328 compromised by an attacker that has obtained root control over the computer and any existing signature files.
329 [[[/p]]][[[p]]]
330 signduterre.py asks for a passphrase which is PRE-pended to every file before the hash is constructed (unless
331 the passphrase is entered with an option). As long as the passphrase is not compromised, the hashes cannot
332 be reconstructed. A randomly generated, unpadded base-64 encoded 16 Byte password (ie, ~22 characters) is
333 suggested in interactive use. If '--passphrase SUGGESTED' is entered on the command line or no passphrase is
334 enetered when asked, the suggested value will be used. This value is printed to STDERR (the screen or 2) for
335 safe keeping. Please, make sure you store the printed passphrase. For instance:
336 [[[/p]]][[[pre make=example1]]]
337 # make: example1
338 # Simple system sanity test using the 'which' command to establish the paths
339 $ python3 signduterre.py --passphrase SUGGESTED --salt SUGGESTED --detailed-view \\
340 `which python3 bash ps ls find stat` 2> test-20090630_11-14-03.pwd > test-20090630_11-14-03.sdt
341 $ python3 signduterre.py --passphrase test-20090630_11-14-03.pwd --check-file test-20090630_11-14-03.sdt
342 [[[/pre]]][[[p]]]
343 The first command will store the passphrase (and all error messages) in a file 'Signature_20090630_11-14-03.pwd'
344 and the check-file in 'Signature_20090630_11-14-03.sdt'. The second line will test the signature.
345 The signature will be made of the files used for the commands python3, bash, ps, ls, find, and stat.
346 These files are found using the 'which' command.
347 [[[/p]]][[[p]]]
348 Working with remote systems
349 [[[/p]]][[[p]]]
350 It is not secure to store files with the passphrase on the system you want to check. However, you could
351 pipe STDERR to some safe site.
352 [[[/p]]][[[pre]]]
353 # Send passphrase over ssh tunnel to safe site
354 $ python3 signduterre.py --passphrase SUGGESTED --salt SUGGESTED `which bash python3` \\
355 -o test-safe-store.sdt 2>&1 | ssh user@safe.host.site 'dd of=/home/user/safe/test-safe-store.pwd status=noxfer'
356 [[[/pre]]][[[p]]]
357 As the security of the passphrases is important and off-site storrage of files is often prudent or convenient,
358 this tunneling construct has been automated in all in- and output as a pseudo-URL: 'ssh://<user>@<host></path>',
359 eg, 'ssh://user@safe.host.site/home/user/safe/test-safe-store.pwd'. It is not possible to enter a
360 password in such a pseudo-URL, so the automatical login into the host system must be configured in SSH.[[[br /]]]
361 [[[em]]]Note: There are severe security risks involved when using SSH to login into another system if the
362 originating system is compromised[[[/em]]].
363 [[[/p]]][[[p]]]
364 The pseudo-url can be used with the [[[i]]]--output-file, --Private-file, --input-file, --check-file, --passphrase[[[/i]]]
365 options as well as for the actual file and $(cmd) arguments used to determine the signatures. The latter
366 allows to check files on remote systems, or to repeat a check from a remote system using the [[[i]]]--file-source[[[/i]]]
367 option (only works with plain files and $(cmd), not for directories, --Status, or ${} arguments). For instance:
368 [[[/p]]][[[pre]]]
369 # Use ssh:// pseudo-url to send passphrase to safe.host.site
370 $ python3 signduterre.py --passphrase SUGGESTED --salt SUGGESTED `which bash python3` \\
371 -o ssh://user@safe.host.site/home/user/safe/test-safe-store.sdt \\
372 -P ssh://user@safe.host.site/home/user/safe/test-safe-store.pwd
373 # Check files on remote compromised.host.site while running test program on safe.host.site
374 $ python3 signduterre.py --passphrase test-safe-store.pwd --check-file test-safe-store.sdt \\
375 --file-source ssh://user@compromised.host.site
376 [[[/pre]]][[[p]]]
377 To execute a remote $(cmd) argument, write $(ssh://<user>@<host>/cmd). Be aware that too many nested "-quotes might cause
378 problems. When using a --file-source argument that starts with 'ssh://', the $(cmd) commands are internally rewritten
379 into the above form. Note that no <path> argument will be used.
380 [[[/p]]][[[p]]]
381 The next example uses the ssh:// pseudo-URL to read the data in an alternative way on [[[i]]]localhost[[[/i]]]. Obviously, storing the
382 plain text passphrase on the same system makes it a rather pointless excersize. The example only works if your
383 '~/.ssh/id_dsa.pub' or '~/.ssh/id_rsa.pub' file has been appended to '~/.ssh/authorized_keys' and you used ssh-add or
384 another application to open the key.
385 [[[/p]]][[[pre make=ssh1]]]
386 # make: ssh1
387 # Use ssh:// pseudo-url to read data in an alternative way
388 $ python3 signduterre.py --passphrase SUGGESTED --salt SUGGESTED -v -d -e `which dd` '$(cat `which dd`)' \\
389 -o test-safe-store.sdt \\
390 -P ssh://`whoami`@localhost${PWD}/test-safe-store.pwd
391 # check files the standard way
392 $ python3 signduterre.py -e --passphrase ssh://`whoami`@localhost${PWD}/test-safe-store.pwd --check-file test-safe-store.sdt
393 # Check files using ssh on localhost
394 $ python3 signduterre.py -e --passphrase ssh://`whoami`@localhost${PWD}/test-safe-store.pwd --check-file test-safe-store.sdt \\
395 --file-source ssh://`whoami`@localhost
396 [[[/pre]]][[[p]]]
397 Examples:[[[/p]]][[[pre make=example2]]]
398 # make: example2
399 # Self test of root directory, python, and signduterre.py using the 'which' command to establish the paths
400 $ python3 signduterre.py --detailed-view --salt 436a73e3 --passphrase liauwefa3251EWC -o test-self.sdt \\
401 / `which python3 signduterre.py`
402 $ python3 signduterre.py --passphrase liauwefa3251EWC -c test-self.sdt
403 [[[/pre]]][[[LONG]]][[[p]]]
404 Write a signature to the file test-self.sdt and test it with the --check-file option. The signature contains
405 the SHA-256 hashes of the files, [[[i]]]/usr/bin/python3[[[/i]]], [[[i]]]signduterre.py[[[/i]]], and the status information on the root
406 directory. The salt '436a73e3' and passphrase 'liauwefa3251EWC' are used.
407 [[[/p]]][[[/LONG]]][[[pre make=procfs1]]]
408 # make: procfs1
409 # Self test of root directory, python, and signduterre.py using the the /proc file system
410 $ python3 signduterre.py --detailed-view --salt SUGGESTED --passphrase liauwefa3251EWC -o test-self_proc.sdt \\
411 /proc/self/root /proc/self/exe `which signduterre.py`
412 $ python3 signduterre.py --passphrase liauwefa3251EWC --check-file test-self_proc.sdt
413 [[[/pre]]][[[LONG]]][[[p]]]
414 Write a signature to the file test-self_proc.sdt and test it with the --check-file option. The signature
415 contains the SHA-256 hashes of the same files as above, [[[i]]]/usr/bin/python3[[[/i]]], [[[i]]]signduterre.py[[[/i]]], and the status
416 information on the root directory. However, the python executable and the root directory are now accessed
417 through the [[[i]]]/proc[[[/i]]] file system. The suggested salt is used (written to test-self_proc.sdt) and the passphrase
418 is (again) 'liauwefa3251EWC'.
419 [[[/p]]][[[/LONG]]][[[pre make=example3]]]
420 # make: example3
421 # Test of supporting commands for chkrootkit
422 $ python3 signduterre.py --execute --total-only --salt SUGGESTED=8 --passphrase SUGGESTED --Status \\
423 --output-file=test-chkrootkit.sdt --Private-file=test-chkrootkit.pwd \\
424 signduterre.py `which bash awk cut egrep find head id ls netstat ps strings sed uname`
425 $ python3 signduterre.py --execute --passphrase test-chkrootkit.pwd --check-file test-chkrootkit.sdt
426 [[[/pre]]][[[LONG]]][[[p]]]
427 Writes a signature of the requested files to test-chkrootkit.sdt (signature) and private information to
428 test-chkrootkit.pwd (password and selected salt) and checks it in the next line. The files are those of
429 commands required by the [[[i]]]chkrootkit[[[/i]]] program (http://www.chkrootkit.org/), with bash added. The 'which'
430 command will give the paths for the commands. Eight salts are generated, of which only 1 is actually
431 used. When checking, the correct salt should match. This prevents a compromised program from simply
432 printing out OK tot he check. A more comprehensive evation of guessing the correct salt can be obtained
433 by using the '--all-salts-pattern' option.
434 [[[/p]]][[[/LONG]]][[[pre make=procfs2]]]
435 # make: procfs2
436 # Simply lump all "system" files, the PATH environment variable and the first 2 columns of the output of lsmod
437 $ python3 signduterre.py --execute --detail --salt SUGGESTED --passphrase liauwefa3251EWC --Status --total-only \\
438 signduterre.py /sbin/* /bin/* /usr/bin/find /usr/bin/stat /usr/bin/python3 '${PATH}' \\
439 '$(lsmod | awk "{print \$1, \$2}")' > test-20090625_14-31-54.sdt
441 # Failing check due to missing --execute option
442 $ python3 signduterre.py --passphrase liauwefa3251EWC -c test-20090625_14-31-54.sdt
443 $ python3 signduterre.py --passphrase liauwefa3251EWC -c test-20090625_14-31-54.sdt --no-execute
445 # Successful check
446 $ python3 signduterre.py --execute --passphrase liauwefa3251EWC --check-file test-20090625_14-31-54.sdt
447 [[[/pre]]][[[LONG]]][[[p]]]
448 Prints a signature to the system test-20090625_14-31-54.sdt and the automatically generated password to
449 test-20090625_14-31-54.pwd. The salt will be automatically determined. The signature contains the SHA-256
450 hashes of the file status and file contents of [[[i]]]signduterre.py, /sbin/*, /bin/*, /usr/bin/find,
451 /usr/bin/file, /usr/bin/python*[[[/i]]] on separate lines, and a hash of the PATH environment variable. Do not
452 display the hash of every single file, which could be insecure, but only the total hash.
453 The first two checks will both fail if test-20090625_14-31-54.sdt contains a $(cmd) entry.
454 The --no-execute option is default and prevents the execute option (if reading the execute option from the
455 signature file has been activated). The last check will succeed (if the files have not been changed).
456 [[[/p]]][[[/LONG]]][[[pre make=example4]]]
457 # make: example4
458 # Use a list of generated passphrases
459 $ python3 signduterre.py --salt SUGGESTED --passphrase SUGGESTED=20 signduterre.py \\
460 2> test-20090630_16-44-34.pwd > test-20090630_16-44-34.sdt
461 $ python3 signduterre.py -p test-20090630_16-44-34.pwd -c test-20090630_16-44-34.sdt
462 [[[/pre]]][[[LONG]]][[[p]]]
463 Will generate and print 20 passphrases and print a signature using one randomly chosen passphrase from these
464 20. Everything is written to the files 'test-20090630_16-44-34.pwd' and 'test-20090630_16-44-34.sdt'.
465 Such file names can easily be generated with 'test-`date "+%Y%m%d_%H-%M-%S"`.sdt'.
466 The next command will check all 20 passphrases generated before from the Signature file and print the results.
467 [[[/p]]][[[/LONG]]][[[pre make=example5]]]
468 # make: example5
469 # Use a list of generated salts with a pattern of correct salts
470 $ python3 signduterre.py --salt SUGGESTED=16 --passphrase SUGGESTED --all-salts-pattern \\
471 -P test-salt-pattern.pwd -o test-salt-pattern.sdt `which bash stat find ls ps id uname awk gawk perl`
472 $ python3 signduterre.py -p test-salt-pattern.pwd -c test-salt-pattern.sdt
473 # Compare to salt pattern number to the one from the check-file
474 $ cat test-salt-pattern.pwd
475 [[[/pre]]][[[LONG]]][[[p]]]
476 As the previous, but with a pattern of random correct and incorrect salts. The salt pattern number
477 indicates which salts were and were not correct.
478 [[[/p]]][[[/LONG]]][[[pre make=sudo1]]]
479 # make: sudo1
480 # Check MBR and current root directory (sudo and root user)
481 $ sudo python3 signduterre.py -u root -s SUGGESTED -p SUGGESTED --Status-values='i' -v -e -t \\
482 --output-file test-boot-sector.sdt --Private-file test-boot-sector.pwd --execute-args=sda \\
483 '?/proc/self/root' `which dd` '$(dd if=/dev/$1 bs=512 count=1 status=noxfer | od -X)'
484 $ sudo python3 signduterre.py -u root -e -p test-boot-sector.pwd -c test-boot-sector.sdt
485 [[[/pre]]][[[LONG]]][[[p]]]
486 Will hash the inode numbers of the effective root directory (eg, chroot) and the executable (python)
487 together with the contents of the MBR (Master Boot Record) on [[[i]]]/dev/sda[[[/i]]] in Hex. It uses suggested salt and
488 passphrase. Accessing [[[i]]]/dev/sda[[[/i]]] is only possible when [[[i]]]root[[[/i]]], so the command is entered with [[[i]]]sudo[[[/i]]] and
489 '--user root'. Use the '--print-execute' option if you want to check the output of the [[[i]]]dd[[[/i]]] command.
490 [[[/p]]][[[p]]]
491 The main problem with intrusion detection by comparing file contents is the ability of an attacker
492 to redirect attempts to read a compromised file to a stored copy of the original. So, [[[i]]]sha256sum[[[/i]]] or
493 python could be changed to read [[[i]]]'/home/attacker/old/ps'[[[/i]]] when the argument was [[[i]]]'/bin/ps'[[[/i]]]. This would
494 foil any scheme that depends on entering file names in programs. An answer to this threat is to
495 read the bytes in files in as many ways as possible. Therefor, forcing an attacker to change many
496 files which itself would increase the probability of detection of the attack. The following command
497 will read the same (test) file, and generate identical hashes, in many different ways.
498 [[[/p]]][[[/LONG]]][[[pre make=example6]]]
499 # make: example6
500 # Example generating identical signatures of the same text file in different ways
501 $ dd if=signduterre.py status=noxfer 2>/dev/null | \\
502 python3 signduterre.py -v -d -s 1234567890abcdef -p poiuytrewq \\
503 --execute --execute-args='signduterre.py' \\
504 signduterre.py - \\
505 '$(cat $1)' \\
506 '$(grep "" $1)' \\
507 '$(awk "{print}" $1)' \\
508 '$(cut -f 1-100 $1)' \\
509 '$(perl -ane "{print \$_}" $1)' \\
510 '$(python3 -c "import sys;f=open(sys.argv[1]);sys.stdout.buffer.write(f.buffer.read())" $1;)' \\
511 '$(ruby -e "f=open(ARGV[0]);print f.read();" $1;)'
512 [[[/pre]]][[[LONG]]][[[p]]]
513 These "commands" do not always return the same bytes (awk), or any bytes at all (grep), from a text
514 file as when used with a binary file. However, if the commands can print the bytes unaltered, the
515 signatures will be identical. That is, the following arguments will work on a binary file:
516 [[[/p]]][[[/LONG]]][[[pre make=example6]]]
517 # make: example6
518 # Example generating identical signatures of the same file in different ways, now for binary files
519 $ dd if=/bin/bash status=noxfer 2>/dev/null | \\
520 python3 signduterre.py -v -d -s 1234567890abcdef -p poiuytrewq \\
521 --execute --execute-args='/bin/bash' \\
522 /bin/bash - \\
523 '$(cat $1)' \\
524 '$(perl -ane "{print \$_}" $1)' \\
525 '$(python3 -c "import sys;f=open(sys.argv[1]);sys.stdout.buffer.write(f.buffer.read())" $1;)' \\
526 '$(ruby -e "f=open(ARGV[0]);print f.read();" $1;)'
527 [[[/pre]]][[[LONG]]][[[p]]]
528 Will generate the same identical signatures for [[[i]]]/bin/bash[[[/i]]], [[[i]]]STDIN[[[/i]]], [[[i]]]'$(cat /bin/bash)'[[[/i]]] etc.
529 There are obviously many more ways to read out the bytes from the disk or memory. The main point
530 being that it should be difficult to predict for an attacker which commands must be compromised
531 to hide changes in the system.
532 [[[/p]]][[[ /long]]][[[p]]]
533 The examples can be run as a makefile using make. Use one of the following commands:
534 [[[/p]]][[[pre]]]
535 # General examples, use them all
536 python3 signduterre.py --manual-make |make -f - example
537 # Linux specific examples using the second procfs example
538 python3 signduterre.py --manual-make |make -f - procfs2
539 # Examples requiring sudo, using first
540 python3 signduterre.py --manual-make | sudo make -f - sudo1
541 [[[/pre]]][[[p]]]
542 Known Bugs:
543 [[[/p]]][[[p]]]
544 - Reading files from STDIN (-) does not work if ssh:// has been used before as input for,
545 eg, file arguments, --check-file or --passphrase
546 [[[/p]]][[[pre make=sshbug1]]]
547 # make: sshbug1
548 # '-' stdin before ssh:// is fine
549 $ dd if=/bin/ps 2>/dev/null | python3.0 signduterre.py -edv -p SUGGESTED -s SUGGESTED \
550 /bin/ps - ssh://`whoami`@localhost/bin/ps
551 # '-' stdin after ssh:// FAILs
552 $ dd if=/bin/ps 2>/dev/null | python3.0 signduterre.py -edv -p SUGGESTED -s SUGGESTED \
553 /bin/ps ssh://`whoami`@localhost/bin/ps -
554 [[[/pre]]][[[p]]]
555 - Reading URLs as file arguments should work when Python treats URLs identical
556 to file descriptors. For the technically inclined:
557 when [[[br /]]][[[tt]]]with urllib.request.urlopen(url) as f:[[[/tt]]] [[[br /]]]works, URLs can be
558 entered where ever file paths can be entered..
559 [[[/p]]][[[/body]]][[[/html]]]
562 license = """
563 Signature-du-Terroir
564 Construct a signature of the installed software state or check a previously made signature.
566 copyright 2009, R.J.J.H. van Son
568 This program is free software: you can redistribute it and/or modify
569 it under the terms of the GNU General Public License as published by
570 the Free Software Foundation, either version 3 of the License, or
571 (at your option) any later version.
573 This program is distributed in the hope that it will be useful,
574 but WITHOUT ANY WARRANTY; without even the implied warranty of
575 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
576 GNU General Public License for more details.
578 You should have received a copy of the GNU General Public License
579 along with this program. If not, see <http://www.gnu.org/licenses/>.
580 """;
582 # Note that only release notes are put here
583 # See git repository for detailed change comments:
584 # git clone git://repo.or.cz/signduterre.git
585 # http://repo.or.cz/w/signduterre.git
586 releasenotes = """
587 20090811 - Release candidate v0.5RC
588 20090811 - Implemented ssh tunnel for commands
589 20090810 - Added --file-source=PATH option
590 20090810 - Added ssh tunnel for all file i/o (ssh://...)
591 20090807 - DIFFERENT became FAIL in --check-file
592 20090730 - Release v0.4
593 20090724 - Added '--all-salts-pattern' and HTML formatting in manual
594 20090723 - Added URL support for all files. Does not yet work due to bug in Python 3.0
595 20090723 - Added '-' for STDIN
596 20090717 - Added --execute-args
597 20090716 - Release v0.3
598 20090713 - Added --quiet option
599 20090712 - moved from /dev/random to /dev/urandom
600 20090702 - Replaced -g with -p SUGGESTED[=N]
601 20090702 - Generating and testing lists of random salts
602 20090701 - Release v0.2
603 20090630 - Generating and testing random passphrases
604 20090630 - --execute works on $(cmd) only, nlinks in ?path and ? implied for directories
605 20090630 - Ported to Python 3.0
607 20090628 - Release v0.1b
608 20090628 - Added release-notes
610 20090626 - Release v0.1a
611 20090626 - Initial commit to Git
612 """;
614 #############################################################################
616 # IMPORT & INITIALIZATION #
618 #############################################################################
620 import sys;
621 import os;
622 import subprocess;
623 import stat;
624 import subprocess;
625 # if sys.stdout.isatty(): import readline;
626 import binascii;
627 import hashlib;
628 import re;
629 import time;
630 from optparse import OptionParser;
631 import base64;
632 import random;
633 import struct;
634 import urllib.request;
635 import urllib.error;
637 # Limit the characters that can be used in $(cmd) commands
638 # Only allow the escape of '$'
639 not_allowed_chars = re.compile('([^\w\ \.\/\"\|\;\:\,\-\$\[\]\{\}\(\)\@\`\!\*\=\\\\]|([\\\\]+([^\$\"\\\\]|$)))');
641 programname = "Signature-du-Terroir";
642 version = "0.5RC";
644 # Open files or pipes for in/output, use mode = 'b' if binary is needed
645 def open_infile(filename, mode):
646 if filename == '-':
647 return sys.stdin;
648 elif filename.lower().find('ssh://') > -1:
649 match = re.search('(?i)ssh://([^/]+)/(.*)$', filename);
650 tunnel_command = 'ssh '+match.group(1)+' "dd if=/'+match.group(2)+' status=noxfer "';
651 if 'b' in mode:
652 pipe = subprocess.Popen(tunnel_command, shell=True, stdout=subprocess.PIPE);
653 else:
654 pipe = subprocess.Popen(tunnel_command, shell=True, stdout=subprocess.PIPE, universal_newlines=True);
655 return pipe.stdout;
656 elif filename.find('://') > -1:
657 print("URL:", filename, file=current_private);
658 return urllib.request.urlopen(filename);
659 else:
660 if not os.path.isfile(filename):
661 print(filename, "does not exist", file=sys.stderr)
662 quit();
663 return open(filename, mode);
665 def open_outfile(filename, mode):
666 if filename == '-':
667 return sys.stdout;
668 elif filename.lower().find('ssh://') > -1:
669 match = re.search('(?i)ssh://([^/]+)/(.*)$', filename);
670 tunnel_command = 'ssh '+match.group(1)+' "dd of=/'+match.group(2)+' status=noxfer "';
671 if 'b' in mode:
672 pipe = subprocess.Popen(tunnel_command, shell=True, stdin=subprocess.PIPE);
673 else:
674 pipe = subprocess.Popen(tunnel_command, shell=True, stdin=subprocess.PIPE, universal_newlines=True);
675 return pipe.stdin;
676 elif filename.find('://') > -1:
677 print("URL:", filename, file=current_private);
678 return urllib.request.urlopen(filename);
679 else :
680 return open(filename, mode);
682 current_outfile = sys.stdout;
683 current_private = sys.stderr;
685 #############################################################################
687 # OPTION HANDLING #
689 #############################################################################
691 parser = OptionParser()
692 parser.add_option("-s", "--salt", metavar="HEX",
693 dest="salt", default=False,
694 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).")
695 parser.add_option("-a", "--all-salts-pattern",
696 dest="allsalts", default=False, action="store_true",
697 help="Use all salts in sequence, randomly replace salts with incorrect ones in the output to create a pattern of failing hashes indicated by a corresponding integer number. Depends on '--salt SUGGESTED=N'. Implies --total-only.")
698 parser.add_option("-p", "--passphrase", metavar="TEXT",
699 dest="passphrase", default=False,
700 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 (or '-' for STDIN) 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).")
701 parser.add_option("-c", "--check-file",
702 dest="check", default=False, metavar="FILE",
703 help="Check contents with the output of a previous run from file or STDIN ('-'). 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.")
704 parser.add_option("-i", "--input-file",
705 dest="input", default=False, metavar="FILE",
706 help="Use names from FILE or STDIN ('-'), use one filename per line.")
707 parser.add_option("-o", "--output-file",
708 dest="output", default=False, metavar="FILE",
709 help="Print to FILE instead of STDOUT.")
710 parser.add_option("--file-source",
711 dest="filesource", default=False, metavar="PATH",
712 help="Read all files from PATH. The PATH-string is prepended to every plain file-path that is read for a signature. Remote files can be checked with 'ssh://<user>@<host>[/path]'. The option overrules any File source specification in the --check-file.")
713 parser.add_option("-P", "--Private-file",
714 dest="private", default=False, metavar="FILE",
715 help="Print private information (passwords etc.) to FILE instead of STDERR.")
716 parser.add_option("-u", "--user",
717 dest="user", default="nobody", metavar="USER",
718 help="Execute $(cmd) as USER, default 'nobody' (root/sudo only)")
719 parser.add_option("-S", "--Status",
720 dest="status", default=False, action="store_true",
721 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)")
722 parser.add_option("--Status-values",
723 dest="statusvalues", default="fmidugs", metavar="MODE",
724 help="Status values to print for --Status, default MODE is 'fmidugs' (file, mode, inode, device, uid, gid, size). Also available (n)l(inks) a(time), (m)t(ime), and c(time).")
725 parser.add_option("-t", "--total-only",
726 dest="total", default=False, action="store_true",
727 help="Only print the total hash, unsets --detailed-view (default True)")
728 parser.add_option("-d", "--detailed-view",
729 dest="detail", default=False, action="store_true",
730 help="Print hashes of individual files, is unset by --total-only (default False)")
731 parser.add_option("-e", "--execute",
732 dest="execute", default=False, action="store_true",
733 help="Interpret $(cmd) (default False)")
734 parser.add_option("--execute-args",
735 dest="executeargs", default='', metavar="ARGS",
736 help="Arguments for the $(cmd) commands ($1 ....)")
737 parser.add_option("-n", "--no-execute",
738 dest="noexecute", default=False, action="store_true",
739 help="Explicitely do NOT Interpret $(cmd)")
740 parser.add_option("--print-hexdump",
741 dest="printhexdump", default=False, action="store_true",
742 help="Print hexadecimal dump of input bytes to STDERR for debugging purposes")
743 parser.add_option("-m", "--manual",
744 dest="manual", default=False, action="store_true",
745 help="Print a short version of the manual and exit")
746 parser.add_option("--manual-long",
747 dest="manuallong", default=False, action="store_true",
748 help="Print the long version of the manual and exit")
749 parser.add_option("--manual-html",
750 dest="manualhtml", default=False, action="store_true",
751 help="Print the manual in HTML format and exit")
752 parser.add_option("--manual-make",
753 dest="manualmake", default=False, action="store_true",
754 help="Print the examples in the manual as a makefile and exit")
755 parser.add_option("-r", "--release-notes",
756 dest="releasenotes", default=False, action="store_true",
757 help="Print the release notes and exit")
758 parser.add_option("-l", "--license",
759 dest="license", default=False, action="store_true",
760 help="Print license text and exit")
761 parser.add_option("-v", "--verbose",
762 dest="verbose", default=False, action="store_true",
763 help="Print more information on output")
764 parser.add_option("-q", "--quiet",
765 dest="quiet", default=False, action="store_true",
766 help="Print minimal information (hide filenames). If the output is used with --check-file, the command line options and arguments must be repeated.")
768 (options, check_filenames) = parser.parse_args();
771 # Start with opening any non-default output files
772 my_output = False;
773 if options.output:
774 current_outfile = open_outfile(options.output, 'w');
775 my_output = options.output;
777 my_private = False;
778 if options.private:
779 current_private = open_outfile(options.private, 'w');
780 my_private = options.private;
782 print("# Program: "+programname + " version " + version, file=current_outfile);
783 print("#", time.strftime("%Y/%m/%d %H:%M:%S", time.localtime()), "("+time.tzname[0]+")\n", file=current_outfile);
785 # Print license
786 if options.license:
787 print (license, file=sys.stderr);
788 exit(0);
789 # Print manual
790 if options.manual or options.manuallong:
791 cleartext_manual = re.sub(r"(?i)\[\[\[\s*(/?)\s*LONG\s*\]\]\]", r'[[[\1LONG]]]', manual);
792 if not options.manuallong:
793 currentstart = cleartext_manual.find('[[[LONG]]]');
794 while currentstart > -1:
795 currentend = cleartext_manual.find('[[[/LONG]]]', currentstart)+len('[[[/LONG]]]');
796 (firstpart, secondpart) = cleartext_manual.split(cleartext_manual[currentstart:currentend]);
797 cleartext_manual = firstpart+secondpart;
798 currentstart = cleartext_manual.find('[[[LONG]]]');
799 htmltags = re.compile('\[\[\[[^\]]*\]\]\]');
800 cleartext_manual = htmltags.sub('', cleartext_manual);
801 print (cleartext_manual, file=sys.stdout);
802 exit(0);
803 # Print HTML manual
804 if options.manualhtml:
805 protleftanglesbracks = re.compile('\<');
806 protrightanglesbracks = re.compile('\>');
807 leftanglesbracks = re.compile('\[\[\[');
808 rightanglesbracks = re.compile('\]\]\]');
809 html_manual = re.sub(r"(?i)\[\[\[\s*(/?)\s*LONG\s*\]\]\]", '', manual);
810 html_manual = protleftanglesbracks.sub('&lt;', html_manual);
811 html_manual = protrightanglesbracks.sub('&gt;', html_manual);
812 html_manual = leftanglesbracks.sub('<', html_manual);
813 html_manual = rightanglesbracks.sub('>', html_manual);
814 print (html_manual, file=sys.stdout);
815 exit(0);
816 # Print manual examples as makefile
817 if options.manualmake:
818 make_manual = re.sub("\$ ", "\t", manual);
819 make_manual = re.sub("\#", "\t#", make_manual);
820 make_manual = re.sub(r"\\\s*\n", '', make_manual);
821 make_manual = re.sub(r"\$", r'$$', make_manual);
822 # Protect "single" [ brackets
823 make_manual = re.sub(r"(^|[^\[])\[([^\[]|$)", r"\1&#91;\2", make_manual);
824 extrexamples = re.compile(r"\[\[\[pre\s+make\=?([a-zA-Z]*)([0-9]*)\s*\]\]\]\n([^\[]*)\n\[\[\[/pre\s*\]\]\]", re.IGNORECASE|re.MULTILINE|re.DOTALL);
825 exampleiter = extrexamples.finditer(make_manual);
826 makefile_list = [];
827 group_list={};
828 group_list['all'] = "all: ";
829 for match in exampleiter:
830 # We had to convert any '[' in the command. Now convert them back.
831 command_text = re.sub(r"\&\#91\;", '[', match.group(3));
832 makefile_list.append(match.group(1)+match.group(2)+":\n"+command_text);
833 if len(match.group(2)) > 0:
834 if not match.group(1) in group_list.keys():
835 group_list[match.group(1)] = match.group(1)+": ";
836 group_list['all'] += match.group(1)+" ";
837 group_list[match.group(1)] += match.group(1)+match.group(2)+" ";
838 else:
839 group_list['all'] += match.group(1)+" ";
840 print("help: \n\t@echo 'Use \"make -f - "+re.sub(':', '', group_list['all'])+"\"'", file=sys.stdout);
841 for group in group_list:
842 print(group_list[group]+"\n", file=sys.stdout);
843 makefile_list.sort()
844 previous_cat = 'NOT A VALUE';
845 for line in makefile_list:
846 (category, commands) = line.split(':\n');
847 if category != previous_cat:
848 previous_cat = category;
849 print("\n"+previous_cat+":", file=sys.stdout);
850 print(commands, file=sys.stdout);
851 # Clean option
852 print("\nclean:\n\trm test-*.sdt test-*.pwd", file=sys.stdout);
853 exit(0);
854 # Print release notes
855 if options.releasenotes:
856 print ("Version: "+version, file=sys.stderr);
857 print (releasenotes, file=sys.stderr);
858 exit(0);
860 my_salt = options.salt;
861 my_allsalts = options.allsalts;
862 my_passphrase = options.passphrase;
863 my_check = options.check;
864 my_status = options.status;
865 my_statusvalues = options.statusvalues;
866 my_verbose = options.verbose and not options.quiet;
867 my_quiet = options.quiet;
868 execute = options.execute;
869 execute_args = options.executeargs;
870 if options.noexecute: execute = False;
871 input_file = options.input;
872 my_filesource = options.filesource;
873 if my_filesource: print("File source: '"+my_filesource+"'\n", file=current_outfile);
875 # Set total-only with the correct default
876 total_only = True;
877 total_only = not options.detail;
878 if options.total: total_only = options.total;
879 if my_allsalts: total_only = my_allsalts; # All alts pattern only sensible with total-only
880 if my_check: total_only = False;
882 my_user = options.user;
883 # Things might be executed as another user
884 user_change = '';
885 if os.getuid() == 0:
886 user_change = 'sudo -H -u '+my_user+' ';
887 if not my_quiet: print("User: "+my_user, file=current_outfile);
889 # Execute option
890 if execute:
891 text_execute = "True";
892 else:
893 text_execute = "False";
895 if execute and not my_quiet:
896 print("Execute system commands: "+text_execute+"\n", file=current_outfile);
897 if execute_args != '': print("Execute arguments: '"+execute_args+"'\n", file=current_outfile);
899 # --quiet option
900 if my_quiet: print("Quiet: True\n", file=current_outfile);
902 # --quiet option
903 if my_statusvalues != 'fmidugs': print("Status-values: '"+my_statusvalues+"'\n", file=current_outfile);
905 #############################################################################
907 # ARGUMENT PROCESSING #
909 #############################################################################
911 # Measure time intervals
912 start_time = time.time();
914 dev_random = open("/dev/urandom", 'rb');
916 # Read the check file
917 passphrase_list = [];
918 salt_list = [];
919 check_hashes = {};
920 total_hash_list = [];
921 if my_check:
922 highest_arg_used = 0;
923 print("# Checking: "+my_check+"\n", file=current_outfile);
924 arg_list = check_filenames;
925 check_filenames = [];
926 with open_infile(my_check, 'r') as c:
927 for line in c:
928 match = re.search("Execute system commands:\s+(True|False)", line);
929 if match != None:
930 # Uncomment the next line if you want automatic --execute from the check-file (DANGEROUS)
931 # execute = match.group(1).upper() == 'TRUE';
932 continue;
934 match = re.search("Execute arguments:\s+\'([\w\$\s\-\+\/]*)\'", line);
935 if match != None:
936 execute_args = match.group(1);
937 continue;
939 match = re.search("Quiet:\s+(True|False)", line);
940 if match != None:
941 my_quiet = match.group(1).upper() == 'TRUE';
942 if my_quiet: my_verbose = False;
943 continue;
945 match = re.search("File source:\s+\'([\w\-\./\@\:]+)\'", line);
946 if not my_filesource and match != None:
947 my_filesource = match.group(1);
948 continue;
950 match = re.search("Salt\s*\+\s*TOTAL HASH\s*\:\s+\'([\w]*)\'\s+\'([\w]*)\'", line);
951 if match != None:
952 salt_list.append(match.group(1));
953 total_hash_list.append(match.group(2));
954 my_allsalts = True; # Salt+TOTAL HASH imples all-salts-pattern
955 continue;
957 match = re.search("Salt\:\s+\'([\w]*)\'", line);
958 if match != None:
959 salt_list.append(match.group(1));
960 continue;
962 match = re.search("Salt\s*\+\s*TOTAL HASH\s*\:\s+\'([\w]+)\'\s+\'([a-f0-9]+)\'", line);
963 if match != None:
964 salt_list.append(match.group(1));
965 total_hash_list.append(match.group(2));
966 continue;
968 match = re.search("User\:\s+\'([\w]*)\'", line);
969 if match != None:
970 # Uncomment the next line if you want automatic --user from the check-file (DANGEROUS)
971 # my_user = match.group(1);
972 continue;
974 match = re.search("Passphrase\:\s+\'([^\']*)\'", line);
975 if match != None:
976 passphrase_list.append(match.group(1));
977 continue;
979 match = re.search("Status-values\:\s+\'([\w]*)\'", line);
980 if match != None:
981 my_statusvalues = match.group(1);
982 continue;
984 match = re.search("^\s*([a-f0-9]+)\s+\*(TOTAL HASH)\s*$", line)
985 if match != None:
986 total_hash_list.append(match.group(1));
987 continue;
989 match = re.search("^\s*([a-f0-9\-]+)\s+\*\[([0-9]+)\]\s*$", line)
990 if match != None:
991 filenumber = int(match.group(2));
992 if filenumber > highest_arg_used: highest_arg_used = filenumber;
993 # Watch out, arguments count from 0
994 check_filenames.append(arg_list[filenumber - 1]);
995 check_hashes['['+match.group(2)+']'] = match.group(1);
996 continue;
998 match = re.search("^\s*([a-f0-9\-]+)\s+\*(.*)\s*$", line)
999 if match != None:
1000 check_filenames.append(match.group(2));
1001 # Catch --execute error as early as possible
1002 if match.group(2).startswith('$(') and not execute:
1003 error_message = "Executable argument \'"+match.group(2)+"\' only allowed with the --execute flag";
1004 print (error_message, file=sys.stderr);
1005 if not sys.stdout.isatty(): print(error_message, file=current_outfile);
1006 exit(0);
1007 check_hashes[match.group(2)] = match.group(1);
1008 continue;
1009 for i in range(highest_arg_used, len(arg_list)):
1010 check_filenames.append(arg_list[i]);
1011 check_hashes['['+str(i+1)+']'] = (64*'-');
1013 # Read input-file
1014 if input_file:
1015 with open_infile(input_file, 'r') as i:
1016 for line in i:
1017 # Clean up filename
1018 current_filename = re.sub('[^\w\-\.\/\$\{\(\)\}\?\[\]]', '', line);
1019 check_filenames.append(current_filename);
1020 if my_check: check_hashes['['+str(i+1)+']'] = (64*'-');
1022 stat_list = [];
1023 for x in check_filenames:
1024 if os.path.isdir(x):
1025 x = '?'+x;
1026 if my_status and not x.startswith(('?', '$')):
1027 stat_list.append('?'+x);
1028 stat_list.append(x);
1029 check_filenames = stat_list;
1031 # Seed Pseudo Random Number Generator
1032 seed = dev_random.read(16);
1033 random.seed(seed);
1035 # Read suggested salts from /dev/(u)random if needed
1036 if my_salt:
1037 if my_salt.startswith('SUGGESTED'):
1038 N=1;
1039 match = re.search("([0-9][0-9]*)$", my_salt);
1040 if match != None:
1041 N = int(match.group(1));
1042 for i in range(0,N):
1043 salt = dev_random.read(8);
1044 salt_list.append(str(binascii.hexlify(salt), 'ascii'));
1045 else:
1046 salt_list.append(my_salt);
1047 elif len(salt_list) == 0:
1048 salt = dev_random.read(8);
1049 sys.stderr.write("Enter salt (suggest \'"+str(binascii.hexlify(salt), 'ascii')+"\'): ");
1050 new_salt = input();
1051 if not new_salt: new_salt = str(binascii.hexlify(salt), 'ascii');
1052 salt_list.append(new_salt);
1054 # If not combining salts with TOTAL HASH, print salts now
1055 if not my_allsalts:
1056 for my_salt in salt_list:
1057 print("Salt: \'"+my_salt+"\'", file=current_outfile);
1059 # Get passphrase
1060 if my_passphrase and(my_passphrase == '-' or my_passphrase.find("://") > -1 or os.path.isfile(my_passphrase)):
1061 with open_infile(my_passphrase, 'r') as file:
1062 for line in file:
1063 match = re.search("Passphrase\:\s+\'([^\']*)\'", line);
1064 if match != None:
1065 passphrase_list.append(match.group(1));
1066 elif not my_passphrase and len(passphrase_list) == 0:
1067 suggest_passphrase = dev_random.read(16);
1068 sys.stderr.write("Enter passphrase (suggest \'"+str(base64.b64encode(suggest_passphrase), 'ascii').rstrip('=')+"\'): ");
1069 # How kan we make this unreadable on input?
1070 current_passphrase = input();
1071 if not current_passphrase:
1072 current_passphrase = str(base64.b64encode(suggest_passphrase), 'ascii').rstrip('=');
1073 print("Passphrase: \'"+current_passphrase+"\'", file=current_private);
1074 passphrase_list.append(current_passphrase);
1075 elif my_passphrase.startswith('SUGGESTED'):
1076 N = 1;
1077 match = re.search("([0-9][0-9]*)$", my_passphrase);
1078 if match != None:
1079 N = int(match.group(1));
1080 j = int(random.random()*N);
1081 for i in range(0, N):
1082 suggest_passphrase = dev_random.read(16);
1083 current_passphrase = str(base64.b64encode(suggest_passphrase), 'ascii').rstrip('=');
1084 print("Passphrase: \'"+current_passphrase+"\'", file=current_private);
1085 passphrase_list.append(current_passphrase);
1086 else:
1087 passphrase_list.append(my_passphrase);
1089 selected_salt = 1;
1090 fail_fraction = 0.5;
1091 if not my_check:
1092 if len(passphrase_list) > 1:
1093 j = int(random.random()*len(passphrase_list));
1094 passphrase_list = [passphrase_list[j]];
1095 print("# Selected passphrase:", j+1, file=current_private);
1096 if len(salt_list) > 1:
1097 j = int(random.random()*len(salt_list));
1098 # Make sure at least 1 salt will match and print the selection if only one is used
1099 selected_salt = j+1;
1100 if not my_allsalts:
1101 salt_list = [salt_list[selected_salt-1]];
1102 print("# Selected salt:", selected_salt, file=current_private);
1103 else:
1104 salt_N = len(salt_list);
1105 fail_fraction = (salt_N/2.0)/(salt_N - 1);
1106 else:
1107 fail_fraction = 0;
1109 # Close /dev/(u)random
1110 dev_random.close;
1112 #############################################################################
1114 # SIGNATURE CREATION AND CHECKING #
1116 #############################################################################
1118 end_time = time.time();
1119 print("# Preparation time:", end_time - start_time, "seconds\n", file=current_outfile);
1121 pnum = 1;
1122 snum = 1;
1123 corrpnum = 0;
1124 corrsnum = 0;
1125 matched_salt_pattern = -1;
1126 salt_pattern_number = -1;
1128 for my_passphrase in passphrase_list:
1129 snum = 1;
1130 # Initialize salt pattern
1131 if my_allsalts:
1132 salt_pattern_number = 0;
1133 current_salt_power = 1;
1135 for my_salt in salt_list:
1136 print("# Start signature: ", end='', file=current_outfile);
1137 if len(passphrase_list) > 1: print("passphrase -", pnum, end='', file=current_outfile);
1138 if len(salt_list) > 1: print(" salt -", snum, end='', file=current_outfile);
1139 print("", file=current_outfile);
1141 # Should everything be printed?
1142 print_verbose = my_verbose and not (my_allsalts and snum > 1);
1144 file_argnum = 0;
1145 start_time = time.time();
1146 # Construct the passphrase hash
1147 passphrase = hashlib.sha256();
1149 passphrase.update(bytes(my_salt, encoding='ascii'));
1150 passphrase.update(bytes(my_passphrase, encoding='ascii'));
1152 # Create prefix which is a hash of the salt+passphrase
1153 prefix = passphrase.hexdigest();
1155 # Create signature and write output
1156 totalhash = hashlib.sha256();
1157 totalhash.update(bytes(prefix, encoding='ascii'));
1158 for org_filename in check_filenames:
1159 # Create file hash object
1160 filehash = hashlib.sha256();
1161 filehash.update(bytes(prefix, encoding='ascii'));
1162 # Remove []
1163 filename = org_filename.strip('[').rstrip(']');
1164 # Use system variables and commands
1165 if filename.startswith('$'):
1166 # Commands $(command)
1167 match = re.search('^\$\((.+)\)$', filename);
1168 if match != None:
1169 if not execute :
1170 error_message = "Executable argument \'"+filename+"\' only allowed with the --execute flag";
1171 print (error_message, file=sys.stderr);
1172 if not sys.stdout.isatty(): print(error_message, file=current_outfile);
1173 exit(0);
1174 # Clean up command
1175 current_command = not_allowed_chars.sub(" ", match.group(1));
1176 # Insert remote ssh:// if necessary
1177 if my_filesource and current_command.find('://') == -1 :
1178 match = re.search(r"(?i)ssh://([^/]+)", my_filesource);
1179 current_command = "ssh://"+match.group(1)+"/"+current_command;
1180 # Expand remote ssh://
1181 current_host = None;
1182 current_executable = None;
1183 if current_command.startswith("ssh://"):
1184 match = re.search(r"(?i)ssh://([^/]+)/(.*)$", current_command);
1185 if match != None:
1186 current_host = match.group(1);
1187 current_command = match.group(2);
1188 current_executable = "/usr/bin/ssh";
1189 # Create command line
1190 current_command_line = user_change+"bash --restricted -c \'"+current_command+"\' "+str(os.getpid())+" "+execute_args;
1191 # Add ssh command
1192 if current_executable:
1193 current_command_line = re.sub(r'\"', r'\\"', current_command_line);
1194 current_command_line = current_executable+' '+current_host+' "'+current_command_line+'"';
1195 # Print command
1196 if print_verbose:
1197 print("#", current_command_line, file=current_outfile);
1198 # Spawn command and open a pipe to the output
1199 pipe = subprocess.Popen(current_command_line, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE);
1200 for b in pipe.stdout:
1201 if type(b).__name__ == 'str':
1202 b = bytes(b, encoding='utf8');
1203 filehash.update(b);
1204 if options.printhexdump: # For debugging commands
1205 print(str(binascii.hexlify(b), 'ascii'), file=sys.stderr);
1206 # See whether there was an error
1207 pipe.wait();
1208 if pipe.returncode:
1209 error_message = pipe.stderr.read();
1210 print('$('+current_command+')', "\n", str(error_message, encoding='UTF8'), file=sys.stderr);
1211 exit(pipe.returncode);
1212 # ${ENV} environment variables
1213 match = re.search('^\$[\{]?([^\}\(\)]+)[\}]?$', filename);
1214 if match != None:
1215 current_var = not_allowed_chars.sub(" ", match.group(1));
1216 if print_verbose:
1217 print("# echo $"+ current_var, file=current_outfile);
1218 b = os.environ[current_var];
1219 filehash.update(bytes(b, encoding='utf8'));
1220 # lstat() meta information
1221 elif filename.startswith('?'):
1222 if not os.path.exists(filename.lstrip('?')):
1223 print(filename, "does not exist", file=sys.stderr)
1224 quit();
1225 filestat = os.stat(filename.lstrip('?'));
1226 if my_statusvalues == "": my_statusvalues = 'fmidlugs'
1227 b = "";
1228 if 'f' in my_statusvalues:
1229 b += 'stat('+filename.lstrip('?')+') = '
1230 b += '[';
1231 if 'm' in my_statusvalues:
1232 b += 'st_mode='+str(oct(filestat.st_mode))+', ';
1233 if 'i' in my_statusvalues:
1234 b += 'st_ino='+str(filestat.st_ino)+', ';
1235 if 'd' in my_statusvalues:
1236 b += 'st_dev='+str(filestat.st_dev)+', '
1237 if 'l' in my_statusvalues:
1238 b += 'st_nlink='+str(filestat.st_nlink)+', '
1239 if 'u' in my_statusvalues:
1240 b += 'st_uid='+str(filestat.st_uid)+', '
1241 if 'g' in my_statusvalues:
1242 b += 'st_gid='+str(filestat.st_gid)+', '
1243 if 's' in my_statusvalues:
1244 b += 'st_size='+str(filestat.st_size)+', '
1245 if 'a' in my_statusvalues:
1246 b += 'st_atime='+str(filestat.st_atime)+', '
1247 if 't' in my_statusvalues:
1248 b += 'st_mtime='+str(filestat.st_mtime)+', '
1249 if 'c' in my_statusvalues:
1250 b += 'st_ctime='+str(filestat.st_ctime);
1252 b = b.rstrip(', ') + ']';
1253 filehash.update(bytes(b, encoding='utf8'));
1254 if print_verbose:
1255 print ("# "+ b, file=current_outfile);
1256 # Use file
1257 else:
1258 # open and read the file
1259 if filename != '-' and filename.find('://') == -1 and not os.path.exists(filename):
1260 print(filename, "does not exist", file=sys.stderr)
1261 quit();
1263 if filename == '-':
1264 total_bytes = 0;
1265 for b in sys.stdin.buffer:
1266 total_bytes += len(b);
1267 if type(b).__name__ == 'str':
1268 b = bytes(b, encoding='utf8');
1269 filehash.update(b);
1270 if options.printhexdump: # For debugging commands
1271 print(str(binascii.hexlify(b), 'ascii'), file=sys.stderr);
1272 if total_bytes == 0:
1273 print("ERROR: No bytes read from STDIN. Processing aborted.", file=sys.stderr);
1274 quit(1);
1275 else:
1276 current_filename = filename;
1277 if my_filesource and current_filename.find('://') == -1 :
1278 current_filename = my_filesource+current_filename;
1279 with open_infile(current_filename, 'rb') as file:
1280 for b in file:
1281 if type(b).__name__ == 'str':
1282 b = bytes(b, encoding='utf8');
1283 filehash.update(b);
1284 if options.printhexdump: # For debugging commands
1285 print(str(binascii.hexlify(b), 'ascii'), file=sys.stderr);
1287 current_digest = filehash.hexdigest();
1288 print_name = filename;
1289 if my_quiet or org_filename.startswith('['):
1290 file_argnum += 1;
1291 print_name = '['+str(file_argnum)+']';
1292 current_hash_line = current_digest+" *"+print_name
1293 totalhash.update(bytes(current_hash_line, encoding='ascii'));
1295 # Be careful to use this ONLY after totalhash has been updated!
1296 if total_only:
1297 current_hash_line = (len(current_digest)*'-')+" *"+print_name;
1299 # Write output
1300 if not my_check:
1301 if not (my_quiet and total_only) and not (my_allsalts and snum > 1):
1302 print(current_hash_line, file=current_outfile);
1303 elif not (my_quiet or my_allsalts):
1304 if check_hashes[print_name] == (len(current_digest)*'-'):
1305 # Suppress redundant output of empty, ----, lines
1306 if snum <= 1 and pnum <= 1:
1307 print(check_hashes[print_name]+" *"+print_name, file=current_outfile);
1308 elif current_digest != check_hashes[print_name]:
1309 print("FAILED: "+current_hash_line, file=current_outfile);
1310 else:
1311 print("ok"+" *"+print_name, file=current_outfile);
1313 # Handle total hash
1314 current_total_digest = totalhash.hexdigest();
1315 # Write (in)correct salts with the TOTAL HASH
1316 if my_allsalts:
1317 output_salt = my_salt;
1318 j = random.random();
1319 # Randomly create an incorrect salt for failed output
1320 if not my_check:
1321 if j < fail_fraction and snum != selected_salt:
1322 salt = dev_random.read(8);
1323 output_salt = str(binascii.hexlify(salt), 'ascii');
1324 else:
1325 salt_pattern_number += current_salt_power;
1326 current_total_digest_line = "Salt+TOTAL HASH: '"+output_salt+"' '"+current_total_digest+"'";
1327 else: # Standard TOTAL HASH line
1328 current_total_digest_line = current_total_digest+" *"+"TOTAL HASH";
1329 end_time = time.time();
1330 print("# \n# Total hash - Time to completion:", end_time - start_time, "seconds", file=current_outfile);
1331 total_hash_num = 0;
1332 if my_allsalts: total_hash_num = snum-1; # Current TOTAL HASH number of more are used
1333 if not my_check:
1334 print(current_total_digest_line+"\n", file=current_outfile);
1335 elif current_total_digest != total_hash_list[total_hash_num]:
1336 if not my_allsalts: print("FAILED: "+current_total_digest_line+"\n", file=current_outfile);
1337 else:
1338 if my_allsalts: salt_pattern_number += current_salt_power; # Update salt bit pattern
1339 match_number = "";
1340 if len(passphrase_list) > 1 or len(salt_list): match_number = " #"
1341 if len(passphrase_list) > 1: match_number += " passphrase no: "+str(pnum);
1342 if len(salt_list) > 1: match_number += " salt no: "+str(snum);
1343 if not my_allsalts: print("OK"+" *"+"TOTAL HASH"+match_number+"\n", file=current_outfile);
1344 corrsnum = snum;
1345 corrpnum = pnum;
1346 snum += 1;
1347 if my_allsalts: current_salt_power *= 2; # Update current bit position in salt pattern
1348 if my_check and corrpnum == pnum: matched_salt_pattern = salt_pattern_number;
1349 pnum += 1;
1351 if my_check and len(passphrase_list) > 1:
1352 if corrpnum > 0:
1353 print("Passphrase entry:",corrpnum,"matched", file=current_outfile);
1354 else:
1355 print("No passphrase entry matched!", file=current_outfile);
1356 if my_check and (not my_allsalts) and len(salt_list) > 1:
1357 if corrpnum > 0:
1358 if corrsnum > 0:
1359 print("Salt entry:",corrsnum,"matched", file=current_outfile);
1360 else:
1361 print("No salt entry matched!", file=current_outfile);
1362 else:
1363 print("No entry matched", file=current_outfile);
1364 # Print salt bit patterns
1365 elif my_check and my_allsalts:
1366 print("Salt pattern number:", matched_salt_pattern, file=current_outfile);
1367 elif not my_check and my_allsalts:
1368 print("# Salt pattern number:", salt_pattern_number, file=current_private);
1370 # Close output files if necessary
1371 if my_output and my_output != '-':
1372 current_outfile.close();
1373 if my_private and my_private != '-':
1374 current_private.close();