Removed redundant and incorrect character cleaning check from --input-file
[signduterre.git] / signduterre.py
blob480d8486b7b748cd34ccdc977089ddac6ea9b798
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 original <, > 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]'. A shell command that
70 prints out the file can be entered as '$(<cmd>)'. The
71 filepath will be substituted for any '{}' string in
72 the command, or appended tot the command (without
73 white-space). The option overrules any File source
74 specification in the --check-file.
75 -P FILE, --Private-file=FILE
76 Print private information (passwords etc.) to FILE
77 instead of STDERR.
78 -u USER, --user=USER Execute $(cmd) as USER, default 'nobody' (root/sudo
79 only)
80 -S, --Status For each file, add a line with unvarying file status
81 information: st_mode, st_ino, st_dev, st_uid, st_gid,
82 and st_size (like the '?' prefix, default False)
83 --Status-values=MODE Status values to print for --Status, default MODE is
84 'fmidugs' (file, mode, inode, device, uid, gid, size).
85 Also available (n)l(inks) a(time), (m)t(ime), and
86 c(time).
87 -t, --total-only Only print the total hash, unsets --detailed-view
88 (default True)
89 -d, --detailed-view Print hashes of individual files, is unset by --total-
90 only (default False)
91 -e, --execute Interpret $(cmd) (default False)
92 --execute-args=ARGS Arguments for the $(cmd) commands ($1 ....)
93 -n, --no-execute Explicitely do NOT Interpret $(cmd)
94 --import=FILE Import python modules (comma separated list without extension)
95 --print-textdump Print printable character+hexadecimal dump of input
96 bytes to STDERR for debugging purposes
97 --message=TEXT Add a comment message about the test
98 -m, --manual Print a short version of the manual and exit
99 --manual-long Print the long version of the manual and exit
100 --manual-html Print the manual in HTML format and exit
101 --manual-make Print the examples in the manual as a makefile and
102 exit
103 -r, --release-notes Print the release notes and exit
104 -l, --license Print license text and exit
105 -v, --verbose Print more information on output
106 -q, --quiet Print minimal information (hide filenames). If the
107 output is used with --check-file, the command line
108 options and arguments must be repeated.
109 [[[/pre]]][[[p]]]
110 FILE1 FILE2 ...
111 Names and paths of one or more files to be checked. All file arguments in SdT accept '-' as the STDIN file
112 (ie, piped data). Can use ssh://<user>@<host>/path pseudo-URLs for checking files at remote sites. Arguments
113 of any type can take an appended range parameter '[<start>:<end>:<offset>]' or '[<start>:+<length>:<offset>]'.
114 <offset>+<start> bytes are skipped and only <length>=<end>-<start> bytes are written. Leaving out the second
115 argument, ie, '[<start>:]', means all bytes after <start> to the end of the file or stream are used. The
116 ':<offset>' argument is optional. All <start>, <end>, <length>, and <offset> arguments are eval(uat)ed by
117 the Python interpreter as expressions. This means ranges can be can be entered in decimal (default),
118 hexadecimal (0x0000..), octal (0o0000..), and binary (0B0000..) representations.
119 [[[/p]]][[[p]]]
120 Single expression byte rangess can each be enclosed in ()-brackets, and this is obligatory if a ':' character
121 is used in an expression. It is allowed to use balanced []-brackets in expressions inside the byte range
122 slices. Python evaluation takes place INSIDE the current Python environment. This means there is access to
123 all imported modules including those imported with the --import option.[[[br /]]]
124 THIS COULD BE A SECURITY CONCERN AND THE '--execute' OPTION IS REQUIRED TO CALL ANY FUNCTION IN BYTE RANGE
125 SLICE EXPRESSIONS
126 [[[/p]]][[[p]]]
127 Any name starting with a '$', eg, $PATH, will be interpreted as an environmental variable or a command
128 according to the bash conventions: '$ENV' and '${ENV}' as variables, '$(cmd;cmd...)' as system commands
129 (bash --restricted -c 'cmd;cmd...' PID). Where PID the current Process ID is (available as positional
130 parameter $0). Other parameters can be entered with the --execute-args option ($1 etc). Do not forget to
131 enclose the arguments in single ''-quotes! The commands are scanned for unwanted characters and these
132 are removed (eg, ' and \\, however, escaping $ is allowed, eg, '\\$1'). The use of '$(cmd;cmd...)'
133 requires explicit use of the -e or --execute option.
134 [[[/p]]][[[p]]]
135 Note that byte range slices '$(cmd)[<start>:<end>]' do work, but only [[[em]]]after[[[/em]]] the command
136 has completed. So, the file version, '/dev/kmem[0xc04838a0:+88]', will simply use 88 bytes as in
137 '$(dd if=/dev/kmem bs=1 skip=3225958560 count=88)'. However, '$(dd if=/dev/kmem bs=1)[0xc04838a0:+88]' will
138 [[[em]]]first[[[/em]]] read all of /dev/kmem, and only then extract the 88 bytes. In general, this is not
139 the desired procedure (/dev/kmem contains all of the physical RAM). Note that the remote
140 '--file-soource=ssh://...' option preserves the file slice behavior as the file reads are changed into the
141 equivalent 'dd skip=<start> count=<length>' commands.
142 [[[/p]]][[[p]]]
143 Any string '@(python code)' will be evaluated as python 3 code. The '--execute' option is obligatory.
144 Note that the outer ()-brackets are removed. You can extend the program by importing modules with the
145 '--import <module>,<module>,....' option. The python code will be interpreted as a function body,
146 complete with obligatory return statement(s), and wrapped in a function definition. This function will
147 be executed in a separate namespace and the 'return'ed value will be exported and hashed. The current PID
148 is available as 'argv[0]' and the --execute-args argument values are available as list elements 'argv[1]',
149 'argv[2]', etc. @() statements are executed inside the running signduterre program and cannot be used to
150 querry a remote system with ssh:// pseudo-URL constructs.
151 [[[/p]]][[[p]]]
152 If executed as root or sudo, $(cmd;cmd...) will be executed as 'sudo -H -u <user>' which defaults to
153 --user nobody ('--user root' is at your own risk). This will obviously not work when invoked as non-root/sudo.
154 --user root is necessary when you need to check privileged information, eg, you want to check the MBR with
155 '$(dd if=/dev/hda bs=512 count=1 | od -X)'
156 However, as you might use --check-file with files you did not create yourself, it is important to
157 be warned if commands are to be executed.
158 [[[/p]]][[[p]]]
159 Interpretation of $() ONLY works if the -e or --execute options are entered. signduterre.py can easily
160 be adapted to automatically use the setting in the check-file. However, this is deemed insecure and
161 commented out in the distribution version.
162 [[[/p]]][[[p]]]
163 The -n or --no-execute option explicitely supress the interpretation of $(cmd) arguments.
164 [[[/p]]][[[p]]]
165 Meta information from stat() on files is signed when the filename is preceded by a '?'. '?./signduterre.py' will
166 extract (st_mode, st_ino, st_dev, st_nlinks, st_uid, st_gid, st_size) and hash a line of these data (visible
167 with --verbose). The --Status option will automatically add such a line in front of every file. Note that '?'
168 is implied for directories. Both '/' and '?/' produce a hash of, eg,:
169 [[[/p]]][[[pre]]]
170 stat(/) = [st_mode=041775, st_ino=2, st_dev=234881026, st_uid=0, st_gid=80, st_size=1360]
171 [[[/pre]]][[[p]]]
172 The --Status-values=<mode> option selects which status values will be used: f(ile), m(ode), i(node),
173 d(evice), u(id), g(id), s(ize), (n)l(inks), a(time), (m)t(ime), and c(time). Default is
174 --Status-values='fmidugs'. Note that nlinks of a directory include every file in the directory, so this
175 option can check whether files have been added to a directory.
176 [[[/p]]][[[p]]]
177 Arguments enclosed in []-brackets will be hidden in the output. That is, '[/proc/self/exe]' will show up as
178 '[1]' in the output (or '[n]' with n the number of the hidden argument), equivalent to the use of the
179 --quiet option. This means the hidden arguments must be entered again when using the --check-file (-c)
180 option.
181 [[[/p]]][[[h1 align="CENTER"]]]
182 Signature-du-Terroir
183 [[[/h1]]][[[p]]]
184 A very simple tool to generate a signature that can be used to test the integrity of files and "states" in
185 a running installation. signduterre.py constructs a signature of the current system state and checks
186 installation state with a previously made signature. The files are hashed with a passphrase to allow detection
187 of compromised systems while running on the same system. The signature checking can be subverted, but the
188 flexibillity of signduterre.py and the fact that the output of any command can be tested should hamper
189 automated root-kit attacks.
190 [[[/p]]][[[p]]]
191 signduterre.py writes a total SHA-256 hash to STDOUT of all the files and commands entered as arguments. It
192 can also write a hash for each individual file (insecure). The output of a signature can be send to a file and
193 later used to check with --check-file. Hashes are calculated with a hashed salt + passphrase sequence
194 pre-pended to create unpredictable hashes. This procedure ensures that an attacker does not know whether or
195 not the correct passphrase has been entered. An attacker can only know when to supply the requested hash
196 values if she knows the passphrase or has copies available of all the tested files and output of commands to
197 calculate the hashes on the fly.
198 [[[/p]]][[[LONG]]][[[h2]]]
199 The Problem
200 [[[/h2]]][[[p]]]
201 The problem SdT tries to solve is how to test whether your system has been compromised when you can only use
202 the potentially compromised system? The solution is to store a password encrypted signature (or fingerprint)
203 of your system when you are sure it is in a good state. Then you check whether the system can still
204 distinguish between correct and incorrect passwords when it regenerates the signature. The trick is to use
205 the right data (ie, questions) to generate the signature.
206 [[[/p]]][[[p]]]
207 The underlying idea is that some bits have to be changed to compromise a system. That is, program
208 files have been altered, settings and accounts changed, new processes are running or existing processes
209 altered. The most common situation is that some system programs have been changed to hide the traces of
210 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
211 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
212 malicious processes that are running. Such wholescale adaptations of running systems can be executed
213 using standard, off-the-shelf application suits, so called rootkits. There are applications that can
214 detect common (known) rootkits and other malicious programs, eg, [[[i]]]chkrootkit[[[/i]]] ([[[a
215 href="http://www.chkrootkit.org/"]]]www.chkrootkit.org[[[/a]]]) and
216 [[[i]]]rootkit hunter[[[/i]]] ([[[a
217 href="http://www.rootkit.nl"]]]www.rootkit.nl[[[/a]]]). However, these rootkit detectors also use existing commands on the
218 potentially compromised system, so a rootkit can hide from them too.
219 [[[/p]]][[[p]]]
220 There are two obvious directions to guard against rootkits. One is to continuously run a process that
221 looks for attempts to install a rootkit and other malicious activities. The other is to take a snapshot
222 of the system in a known good state, and then flag changes in relevant areas, eg, like [[[i]]]Tripwire[[[/i]]]
223 ([[[a href="http://sourceforge.net/projects/tripwire/"
224 ]]]http://sourceforge.net/projects/tripwire/[[[/a]]]) and [[[i]]]Radmind[[[/i]]] ([[[a
225 href="http://rsug.itd.umich.edu/software/radmind/"
226 ]]]http://rsug.itd.umich.edu/software/radmind/[[[/a]]]).
227 Signature-du-Terroir takes the second route, it creates a signature of a set of relevant files and
228 command output, and checks later whether these have not been changed. However, when running such a test
229 on a compromised system, the attacker can theoretically "fool" any (automated) test. In practise, time
230 and other precious resources will limit what an attacker can accomplish. The idea is to raise the bar
231 for rootkits high enough to make them not worthwhile. SdT tries to make using signatures easy (cheap)
232 and subverting it difficult (expensive).
233 [[[/p]]][[[p]]]
234 As an illustration of the problem SdT treis to solve, take the [[[i]]]sha256sum[[[/i]]] command which generates file
235 hashes (signatures) using the SHA256 algorithm. Hashes can be generated and checked with this command:
236 [[[/p]]][[[pre]]]
237 # Use of sha256sum to check integrity of ps and ls commands
238 $ sha256sum /bin/ps /bin/ls > ps-ls.sh256
239 $ sha256sum -c ps-ls.sh256
240 [[[/pre]]][[[p]]]
241 A compromised file will show up as FAILED. This is ok for unintentional changes to the files. However, a
242 malicious attacker could easily replace [[[i]]]/usr/bin/sha256sum[[[/i]]] with a program that would replace the hash of
243 malicious replacements of these files with the hash sums of the original files. There are three easy ways
244 of doing that. Either simply say 'ok' when checking the file, print out the stored old hash value whenever
245 an altered file is requested by name, or look for the hash of the new, malicious replacement and print out
246 the old hash sum instead. The former two are easy to circumvent, the last one is somewhat less easy.
247 [[[/p]]][[[p]]]
248 The first solution to these avoidance strategies is to generate the signatures with a passphrase and random
249 string (salt). As long as the attacker does not know the passphrase, the only way to subvert SdT is to store
250 the original bits in the files and calculate the signature the moment SdT is called. As the attacker does
251 not know when the correct password or salt is entered, it is not possible to simply answer OK or repeat the
252 stored earlier results instead of calculating them de-novo.
253 [[[/p]]][[[p]]]
254 To be able to serve up the original bits, instead of the bits used on the compromised system, when asked
255 for the hashes, the attacker must divert attempts to read the files by SdT, but not at other moments.
256 There are many ways to do this, eg, running python in a chroot-jail, changing python itself, changing other
257 programs. To accommodate these diversion strategies, SdT allows to read data from each and every command
258 that can supply it. So, a binary file can be entered by name, with eg, cat, dd, perl, python, ruby, or read
259 from the [[[i]]]/proc[[[/i]]] system (if it is a running process), or from STDIN or shell subprocesses. For instance,
260 to protect against running in a chroot-jail, the inode number and device of the root directory can be read
261 from [[[i]]]/proc/self/root[[[/i]]], or [[[i]]]/proc/<PID>/root[[[/i]]], or simply from [[[i]]]/[[[/i]]].
262 [[[/p]]][[[/LONG]]][[[h2]]]
263 Signature creation: Passphrases, salts, and hashes
264 [[[/h2]]][[[p]]]
265 Good passphrases are difficult to remember, so their plaintext form should be protected. To protect the
266 passphrase against rainbow and brute force attacks, the passphrase is concatenated to a salt phrase and
267 hashed before use (SHA-256).
268 [[[/p]]][[[p]]]
269 The salt phrase is requested when constructing a signature. In interactive use, an 8 byte hexadecimal
270 (= 16 character) salt from [[[i]]]/dev/urandom[[[/i]]] is suggested. If '--salt SUGGESTED' is entered on the command line
271 as the salt, the suggested value will be used. The salt is printed in plaintext to the output. The salt will
272 make it more difficult to determine whether the same passphrase has been used to create different signatures.
273 [[[/p]]][[[p]]]
274 At the bottom, a 'TOTAL HASH' line will be printed that hashes all the lines printed for the files. This
275 includes the file names as printed on the hash lines. It is not inconceivable that existing signature files
276 could have been compromised in ways that might be missed when checking the signature. The total hash will
277 point out such changes.
278 [[[/p]]][[[h3]]]
279 SECURITY
280 [[[/h3]]][[[LONG]]][[[p]]]
281 When run on a compromised system, signduterre.py can be subverted if the attacker keeps a copy of all the
282 files and command outputs, and reroutes the open() and stat() functions, or simply delegating signduterre.py
283 to a chroot jail with the original system. In principle, signduterre.py only checks whether the computer
284 responds identically to when the signature file was made. There is no theoretic barrier against a compromised
285 computer perfectly simulating the original system when tested, but behaving adversely at other times. Except
286 for running from clean boot media (USB?), I know of no theoretical sound solution to this problem.
287 [[[/p]]][[[p]]]
288 However, this scenario assumes the use of unlimited resources and time. Inside a limited, real computer system,
289 the attacker must make compromises on what can and what cannot be simulated with the available time and
290 hardware. The idea behind signduterre.py is to "ask difficult questions" that increase the cost of simulating
291 the original system high enough to make detection of successful attacks likely.signduterre.py simply intends
292 to raise the bar high enoug. One point is to store the times needed to create the original hashes. This timing
293 can later be used to see whether the new timings are reasonable. If the same hardware takes considerably
294 longer to perform the same calculations, or needs a much longer delay before it starts, the tester might want
295 to see where this time is spent.
296 [[[/p]]][[[/LONG]]][[[p]]]
297 Signature-du-Terroir works on the assumption that any attacker in control of a compromised system cannot
298 predict whether the passphrase entered is correct or not. An attacker can always intercept the in- and output
299 of signduterre. When running with --check-file, this means the program can be made to print out OK
300 irrespective of the tests. A safe use of signduterre.py is to start with a random number of incorrect
301 passphrases and see whether they fail. Alternatively, and easier, is to add a number of unused salts
302 to the check-file and let the attacker guess which one is correct.
303 [[[/p]]][[[p]]]
304 THE CORRECT USE OF signduterre.py IS TO ENTER A RANDOM NUMBER OF INCORRECT PASSPHRASES OR SALTS FOR EACH
305 TEST AND SEE WHETHER IT FAILS AT THE CORRECT INSTANCES!
306 [[[/p]]][[[p]]]
307 On a compromised system, signduterre.py's detailed file testing (--detailed-view) is easily subverted. With a
308 matched file hash, the attacker will know that the correct passphrase has been entered and can print out the
309 stored hashes or 'ok's for the rest of the checks. So if the attacker keeps any entry in the signature file
310 uncompromised, she can intercept the output, test the password on the unchanged entry and substitute the
311 requested hashes for the output if the hash of that entry matches.
312 [[[/p]]][[[LONG]]][[[p]]]
313 When checking for root-kits and other malware, it is safest to compare the signature files from a different,
314 clean, system. But then you would not need signduterre.py anyway. If you have to work on the system itself,
315 only use the -t or --total-only options to create signatures with a total hash and without individual file
316 hashes. Such a signature can be used to check whether the system is unchanged. Another signature file WITH A
317 DIFFERENT PASSPHRASE can then be used to identify the individual files that have changed. If a detailed
318 signature file has the same passphrase, an attacker could use that other file to read the individual file
319 hashes to check whether the correct passphrase was entered.
320 [[[/p]]][[[/LONG]]][[[p]]]
321 Using the --check-file option in itself is UNsafe. An attacker simply has to print out 'OK' to defeat the
322 check. This attack can be foiled by making it unpredictable when signduterre.py should return 'OK'. This can
323 be done by using a list of salts or passphrases where only one of them (or none!) is correct. Any attacker
324 will have to guess when to return 'OK'.
325 [[[/p]]][[[LONG]]][[[p]]]
326 As generating and entering wrong passphrases and salts is tedious, users have to be supported in correct use
327 of SdT. To assist users, the '--salt SUGGESTED=<N>' option will generate a number N of salts. When
328 checking, each of these salts is tried in turn. An attacker that is unable to simulate the uncompromised
329 system will have to guess which one of the salts is the correct one, and whether or not the passphrase
330 is correct. This increases the chances of detecting compromised systems. If this is not enough guess
331 work, the '-a', '--all-salts-pattern' option will use all salts in sequence to generate total hashes,
332 but random salts will be changed in the output. This generates a pattern of failed salt tests. This pattern
333 is translated into a bit pattern and printed as an integer ([Fail, Fail, OK, Fail, OK, OK, Fail, OK]
334 = 00101101 (least significant first) = 10110100 (unsigned bin) = 180). On creation of a signature, this
335 number is printed to STDERR, on checking (--check-file) it is printed to STDOUT (note that the number
336 will never become 0 or all Fail). So for '--salt SUGGESTED=<N> --all-salts-pattern' the probability of
337 guessing the correct output goes from 1/N to 1/(2^N - 1). Note that '--all-salts-pattern' will work,
338 but is pointless, without '--salt SUGGESTED=<N>' with N>1.
339 [[[/p]]][[[p]]]
340 The '--passphrase SUGGESTED=N' option will generate and print N passphrases. One of these is chosen at
341 random for the signature. The number of the chosen passphrase is printed on STDERR with the passwords.
342 When checking a file, the stored passphrases can be read in again, either by entering the passphrase
343 file after the --passphrase option ('--passphrase <passphrase file>'), or directly from the --check-file.
344 signduterre.py will print out the result for each of the passphrases.
345 [[[/p]]][[[p]]]
346 Note, that storing passphrases in a file and feeding it to signduterre.py is MUCH less secure than just
347 typing them in. Moreover, it might completely defeat the purpose of signduterre.py. If future experiences
348 cast any more doubt on the security of this option, it will be removed.
349 [[[/p]]][[[p]]]
350 For those who want to know more about what an "ideal attacker" can do, see:[[[br]]]
351 Ken Thompson "Reflections on Trusting Trust"[[[br]]]
352 [[[a href="http://cm.bell-labs.com/who/ken/trust.html"]]]http://cm.bell-labs.com/who/ken/trust.html[[[/a]]][[[br]]]
353 [[[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]]]
354 [[[/p]]][[[p]]]
355 David A Wheeler "Countering Trusting Trust through Diverse Double-Compiling"[[[br]]]
356 [[[a href="http://www.acsa-admin.org/2005/abstracts/47.html"]]]http://www.acsa-admin.org/2005/abstracts/47.html[[[/a]]]
357 [[[/p]]][[[p]]]
358 and the discussion of these at Bruce Schneier's 'Countering "Trusting Trust"'[[[br]]]
359 [[[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]]]
360 [[[/p]]][[[/LONG]]][[[h2]]]
361 Manual
362 [[[/h2]]][[[p]]]
363 The intent of signduterre.py is to ensure that the signature cannot be subverted even if the system has been
364 compromised by an attacker that has obtained root control over the computer and any existing signature files.
365 [[[/p]]][[[p]]]
366 signduterre.py asks for a passphrase which is PRE-pended to every file before the hash is constructed (unless
367 the passphrase is entered with an option). As long as the passphrase is not compromised, the hashes cannot
368 be reconstructed. A randomly generated, unpadded base-64 encoded 16 Byte password (ie, ~22 characters) is
369 suggested in interactive use. If '--passphrase SUGGESTED' is entered on the command line or no passphrase is
370 entered when asked, the suggested value will be used. This value is printed to STDERR (the screen or 2) for
371 safe keeping. Please, make sure you store the printed passphrase. For instance:
372 [[[/p]]][[[pre make=example1]]]
373 # make: example1
374 # Simple system sanity test using the 'which' command to establish the paths
375 $ python3 signduterre.py --passphrase SUGGESTED --salt SUGGESTED --detailed-view \\
376 `which python3 bash ps ls find stat` 2> test-20090630_11-14-03.pwd > test-20090630_11-14-03.sdt
377 $ python3 signduterre.py --passphrase test-20090630_11-14-03.pwd --check-file test-20090630_11-14-03.sdt
378 [[[/pre]]][[[p]]]
379 The first command will store the passphrase (and all error messages) in a file 'Signature_20090630_11-14-03.pwd'
380 and the check-file in 'Signature_20090630_11-14-03.sdt'. The second line will test the signature.
381 The signature will be made of the files used for the commands python3, bash, ps, ls, find, and stat.
382 These files are found using the 'which' command.
383 [[[/p]]][[[h2]]]
384 Working with remote systems
385 [[[/h2]]][[[p]]]
386 It is not secure to store files with the passphrase on the system you want to check. However, you could
387 pipe STDERR to some safe site.
388 [[[/p]]][[[pre]]]
389 # Send passphrase over ssh tunnel to safe site
390 $ python3 signduterre.py --passphrase SUGGESTED --salt SUGGESTED `which bash python3` \\
391 -o test-safe-store.sdt 2>&1 | ssh user@safe.host.site 'dd of=/home/user/safe/test-safe-store.pwd'
392 [[[/pre]]][[[p]]]
393 As the security of the passphrases is important and off-site storrage of files is often prudent or convenient,
394 this tunneling construct has been automated in all in- and output as a pseudo-URL: 'ssh://<user>@<host></path>',
395 eg, 'ssh://user@safe.host.site/home/user/safe/test-safe-store.pwd'. It is not possible to enter a
396 password in such a pseudo-URL, so the automatical login into the host system must be configured in SSH.[[[br /]]]
397 [[[em]]]Note: There are severe security risks involved when using SSH to login into another system if the
398 originating system is compromised[[[/em]]].
399 [[[/p]]][[[p]]]
400 The pseudo-url can be used with the [[[i]]]--output-file, --Private-file, --input-file, --check-file, --passphrase[[[/i]]]
401 options as well as for the actual file, ${ENV}, and $(cmd) arguments used to determine the signatures. The latter
402 allows to check files on remote systems, or to repeat a check from a remote system using the [[[i]]]--file-source[[[/i]]]
403 option (only works with plain files, ${ENV}, and $(cmd), not for @(python code), directories, or --Status arguments).
404 For instance:
405 [[[/p]]][[[pre]]]
406 # Use ssh:// pseudo-url to send passphrase to safe.host.site
407 $ python3 signduterre.py --passphrase SUGGESTED --salt SUGGESTED `which bash python3` \\
408 -o ssh://user@safe.host.site/home/user/safe/test-safe-store.sdt \\
409 -P ssh://user@safe.host.site/home/user/safe/test-safe-store.pwd
410 # Check files on remote compromised.host.site while running test program on safe.host.site
411 $ python3 signduterre.py --passphrase test-safe-store.pwd --check-file test-safe-store.sdt \\
412 --file-source ssh://user@compromised.host.site
413 [[[/pre]]][[[p]]]
414 To execute a remote $(cmd) argument, write $(ssh://<user>@<host>/cmd). Be aware that nested "-quotes might cause
415 problems. ${ENV} can be written as ${ssh://<user>@<host>/ENV}. When using a --file-source argument that starts
416 with 'ssh://', the $(cmd) and ${ENV} commands are internally rewritten into the above form. In both forms,
417 as well as the arguments entered with --execute-args, any '$' and '"' symbols are protected by '\$' and '\"' to
418 be evaluated at the host system, as they would be evaluated locally by the ssh command line. This might not
419 always work out as planned, so take care when using these pseudo-URLS. Note that no <path> argument will be used.
420 [[[/p]]][[[p]]]
421 The next example uses the ssh:// pseudo-URL to read the data in an alternative way on [[[i]]]localhost[[[/i]]]. Obviously, storing the
422 plain text passphrase on the same system makes it a rather pointless excersize. The example only works if your have
423 (open)SSH server and clients installed and appended the '~/.ssh/id_dsa.pub' or '~/.ssh/id_rsa.pub' file to '~/.ssh/authorized_keys',
424 and you used ssh-add or another application to open the key.
425 [[[/p]]][[[pre make=ssh1]]]
426 # make: ssh1
427 # Use ssh:// pseudo-url to read data in an alternative way
428 $ python3 signduterre.py --passphrase SUGGESTED --salt SUGGESTED -v -d -e `which dd` '$(cat `which dd`)' \\
429 -o test-safe-store.sdt \\
430 -P ssh://`whoami`@localhost${PWD}/test-safe-store.pwd
431 # check files the standard way
432 $ python3 signduterre.py -e --passphrase ssh://`whoami`@localhost${PWD}/test-safe-store.pwd --check-file test-safe-store.sdt
433 # Check files using ssh on localhost
434 $ python3 signduterre.py -e --passphrase ssh://`whoami`@localhost${PWD}/test-safe-store.pwd --check-file test-safe-store.sdt \\
435 --file-source ssh://`whoami`@localhost
436 [[[/pre]]][[[h2]]]
437 Examples:[[[/h2]]][[[pre make=example2]]]
438 # make: example2
439 # Self test of root directory, python, and signduterre.py using the 'which' command to establish the paths
440 $ python3 signduterre.py --detailed-view --salt 436a73e3 --passphrase liauwefa3251EWC -o test-self.sdt \\
441 / `which python3 signduterre.py`
442 $ python3 signduterre.py --passphrase liauwefa3251EWC -c test-self.sdt
443 [[[/pre]]][[[LONG]]][[[p]]]
444 Write a signature to the file test-self.sdt and test it with the --check-file option. The signature contains
445 the SHA-256 hashes of the files, [[[i]]]/usr/bin/python3[[[/i]]], [[[i]]]signduterre.py[[[/i]]], and the status information on the root
446 directory. The salt '436a73e3' and passphrase 'liauwefa3251EWC' are used.
447 [[[/p]]][[[/LONG]]][[[pre make=procfs1]]]
448 # make: procfs1
449 # Self test of root directory, python, and signduterre.py using the the /proc file system
450 $ python3 signduterre.py --detailed-view --salt SUGGESTED --passphrase liauwefa3251EWC -o test-self_proc.sdt \\
451 /proc/self/root /proc/self/exe `which signduterre.py`
452 $ python3 signduterre.py --passphrase liauwefa3251EWC --check-file test-self_proc.sdt
453 [[[/pre]]][[[LONG]]][[[p]]]
454 Write a signature to the file test-self_proc.sdt and test it with the --check-file option. The signature
455 contains the SHA-256 hashes of the same files as above, [[[i]]]/usr/bin/python3[[[/i]]], [[[i]]]signduterre.py[[[/i]]], and the status
456 information on the root directory. However, the python executable and the root directory are now accessed
457 through the [[[i]]]/proc[[[/i]]] file system. The suggested salt is used (written to test-self_proc.sdt) and the passphrase
458 is (again) 'liauwefa3251EWC'.
459 [[[/p]]][[[/LONG]]][[[pre make=example3]]]
460 # make: example3
461 # Test of supporting commands for chkrootkit
462 $ python3 signduterre.py --execute --total-only --salt SUGGESTED=8 --passphrase SUGGESTED --Status \\
463 --output-file=test-chkrootkit.sdt --Private-file=test-chkrootkit.pwd \\
464 signduterre.py `which bash awk cut egrep find head id ls netstat ps strings sed uname`
465 $ python3 signduterre.py --execute --passphrase test-chkrootkit.pwd --check-file test-chkrootkit.sdt
466 [[[/pre]]][[[LONG]]][[[p]]]
467 Writes a signature of the requested files to test-chkrootkit.sdt (signature) and private information to
468 test-chkrootkit.pwd (password and selected salt) and checks it in the next line. The files are those of
469 commands required by the [[[i]]]chkrootkit[[[/i]]] program (http://www.chkrootkit.org/), with bash added. The 'which'
470 command will give the paths for the commands. Eight salts are generated, of which only 1 is actually
471 used. When checking, the correct salt should match. This prevents a compromised program from simply
472 printing out OK tot he check. A more comprehensive evation of guessing the correct salt can be obtained
473 by using the '--all-salts-pattern' option.
474 [[[/p]]][[[/LONG]]][[[pre make=procfs2]]]
475 # make: procfs2
476 # Simply lump all "system" files, the PATH environment variable and the first 2 columns of the output of lsmod
477 $ python3 signduterre.py --execute --detail --salt SUGGESTED --passphrase liauwefa3251EWC --Status --total-only \\
478 signduterre.py /sbin/* /bin/* /usr/bin/find /usr/bin/stat /usr/bin/python3 '${PATH}' \\
479 '$(lsmod | awk "{print \$1, \$2}")' > test-20090625_14-31-54.sdt
481 # Failing check due to missing --execute option
482 $ python3 signduterre.py --passphrase liauwefa3251EWC -c test-20090625_14-31-54.sdt
483 $ python3 signduterre.py --passphrase liauwefa3251EWC -c test-20090625_14-31-54.sdt --no-execute
485 # Successful check
486 $ python3 signduterre.py --execute --passphrase liauwefa3251EWC --check-file test-20090625_14-31-54.sdt
487 [[[/pre]]][[[LONG]]][[[p]]]
488 Prints a signature to the system test-20090625_14-31-54.sdt and the automatically generated password to
489 test-20090625_14-31-54.pwd. The salt will be automatically determined. The signature contains the SHA-256
490 hashes of the file status and file contents of [[[i]]]signduterre.py, /sbin/*, /bin/*, /usr/bin/find,
491 /usr/bin/file, /usr/bin/python*[[[/i]]] on separate lines, and a hash of the PATH environment variable. Do not
492 display the hash of every single file, which could be insecure, but only the total hash.
493 The first two checks will both fail if test-20090625_14-31-54.sdt contains a $(cmd) entry.
494 The --no-execute option is default and prevents the execute option (if reading the execute option from the
495 signature file has been activated). The last check will succeed (if the files have not been changed).
496 [[[/p]]][[[/LONG]]][[[pre make=example4]]]
497 # make: example4
498 # Use a list of generated passphrases
499 $ python3 signduterre.py --salt SUGGESTED --passphrase SUGGESTED=20 signduterre.py \\
500 2> test-20090630_16-44-34.pwd > test-20090630_16-44-34.sdt
501 $ python3 signduterre.py -p test-20090630_16-44-34.pwd -c test-20090630_16-44-34.sdt
502 [[[/pre]]][[[LONG]]][[[p]]]
503 Will generate and print 20 passphrases and print a signature using one randomly chosen passphrase from these
504 20. Everything is written to the files 'test-20090630_16-44-34.pwd' and 'test-20090630_16-44-34.sdt'.
505 Such file names can easily be generated with 'test-`date "+%Y%m%d_%H-%M-%S"`.sdt'.
506 The next command will check all 20 passphrases generated before from the Signature file and print the results.
507 [[[/p]]][[[/LONG]]][[[pre make=example5]]]
508 # make: example5
509 # Use a list of generated salts with a pattern of correct salts
510 $ python3 signduterre.py --salt SUGGESTED=16 --passphrase SUGGESTED --all-salts-pattern \\
511 -P test-salt-pattern.pwd -o test-salt-pattern.sdt `which bash stat find ls ps id uname awk gawk perl`
512 $ python3 signduterre.py -p test-salt-pattern.pwd -c test-salt-pattern.sdt
513 # Compare to salt pattern number to the one from the check-file
514 $ cat test-salt-pattern.pwd
515 [[[/pre]]][[[LONG]]][[[p]]]
516 As the previous, but with a pattern of random correct and incorrect salts. The salt pattern number
517 indicates which salts were and were not correct.
518 [[[/p]]][[[/LONG]]][[[pre make=sudo1]]]
519 # make: sudo1
520 # Check MBR and current root directory (sudo and root user)
521 $ sudo python3 signduterre.py -u root -s SUGGESTED -p SUGGESTED --Status-values='i' -v -e -t \\
522 --output-file test-boot-sector.sdt --Private-file test-boot-sector.pwd --execute-args=sda \\
523 '?/proc/self/root' `which dd` '$(dd if=/dev/$1 bs=512 count=1 | od -X)'
524 $ sudo python3 signduterre.py -u root -e -p test-boot-sector.pwd -c test-boot-sector.sdt
525 [[[/pre]]][[[LONG]]][[[p]]]
526 Will hash the inode numbers of the effective root directory (eg, chroot) and the executable (python)
527 together with the contents of the MBR (Master Boot Record) on [[[i]]]/dev/sda[[[/i]]] in Hex. It uses suggested salt and
528 passphrase. Accessing [[[i]]]/dev/sda[[[/i]]] is only possible when [[[i]]]root[[[/i]]], so the command is entered with [[[i]]]sudo[[[/i]]] and
529 '--user root'. Use the '--print-execute' option if you want to check the output of the [[[i]]]dd[[[/i]]] command.
530 [[[/p]]][[[p]]]
531 The main problem with intrusion detection by comparing file contents is the ability of an attacker
532 to redirect attempts to read a compromised file to a stored copy of the original. So, [[[i]]]sha256sum[[[/i]]] or
533 python could be changed to read [[[i]]]'/home/attacker/old/ps'[[[/i]]] when the argument was [[[i]]]'/bin/ps'[[[/i]]]. This would
534 foil any scheme that depends on entering file names in programs. An answer to this threat is to
535 read the bytes in files in as many ways as possible. Therefor, forcing an attacker to change many
536 files which itself would increase the probability of detection of the attack. The following command
537 will read the same (test) file, and generate identical hashes, in many different ways.
538 [[[/p]]][[[/LONG]]][[[pre make=example6]]]
539 # make: example6
540 # Example generating identical signatures of the same text file in different ways
541 $ dd if=signduterre.py 2>/dev/null | \\
542 python3 signduterre.py -v -d -s 1234567890abcdef -p poiuytrewq \\
543 --execute --execute-args='signduterre.py' \\
544 signduterre.py - \\
545 '$(cat $1)' \\
546 '$(grep "" $1)' \\
547 '$(awk "{print}" $1)' \\
548 '$(cut -f 1-100 $1)' \\
549 '$(perl -ane "{print \$_}" $1)' \\
550 '$(python3 -c "import sys;f=open(sys.argv[1]);sys.stdout.buffer.write(f.buffer.read())" $1;)' \\
551 '$(ruby -e "f=open(ARGV[0]);print f.read();" $1;)'
552 [[[/pre]]][[[LONG]]][[[p]]]
553 These "commands" do not always return the same bytes (awk), or any bytes at all (grep), from a text
554 file as when used with a binary file. However, if the commands can print the bytes unaltered, the
555 signatures will be identical. That is, the following arguments will work on a binary file:
556 [[[/p]]][[[/LONG]]][[[pre make=example6]]]
557 # make: example6
558 # Example generating identical signatures of the same file in different ways, now for binary files
559 $ dd if=/bin/bash 2>/dev/null | \\
560 python3 signduterre.py -v -d -s 1234567890abcdef -p poiuytrewq \\
561 --execute --execute-args='/bin/bash' \\
562 /bin/bash - \\
563 '$(cat $1)' \\
564 '$(perl -ane "{print \$_}" $1)' \\
565 '$(python3 -c "import sys;f=open(sys.argv[1]);sys.stdout.buffer.write(f.buffer.read())" $1;)' \\
566 '$(ruby -e "f=open(ARGV[0]);print f.read();" $1;)'
567 [[[/pre]]][[[LONG]]][[[p]]]
568 Will generate the same identical signatures for [[[i]]]/bin/bash[[[/i]]], [[[i]]]STDIN[[[/i]]], [[[i]]]'$(cat /bin/bash)'[[[/i]]] etc.
569 There are obviously many more ways to read out the bytes from the disk or memory. The main point
570 being that it should be difficult to predict for an attacker which commands must be compromised
571 to hide changes in the system.
572 [[[/p]]][[[/long]]][[[p]]]
573 In case of a real compromised system, it is conceivable that the signatures will need to be checked using known
574 good statically linked programs, eg, cat or dd from a cyptographically secured container like ecryptfs or an
575 encrypted loopback device. An existing signature can be tested against such statically linked programs using
576 the "--file-source '$(<cmd>)'" option. In this option, the plain file path will be substituted for every
577 occurence of the string '{}' in the command. If no '{}' is present in the command, the file will simply be
578 appended to the command. So, '$(/bin/dd if=)' is equivalent to '$(/bin/dd if={})' and '$(/bin/cat )' is
579 equivalent to '$(/bin/cat {})'. Note the trailing space in '$(/bin/cat )'.
580 [[[/p]]][[[pre make=example7]]]
581 # make: example7
582 # Create standard signature
583 $ python3 signduterre.py --passphrase SUGGESTED --salt SUGGESTED --detailed-view --verbose \\
584 `which python3 bash ps ls find stat lsof` 2> test-20090825_14_48-23.pwd > test-20090825_14_48-23.sdt
585 # Standard check
586 $ python3 signduterre.py --passphrase test-20090825_14_48-23.pwd --check-file test-20090825_14_48-23.sdt -v
587 # Example generating identical signatures checking with --file-source $(dd if=)
588 $ python3 signduterre.py --passphrase test-20090825_14_48-23.pwd --check-file test-20090825_14_48-23.sdt -v \\
589 --execute --file-source '$(dd if=)'
590 # Example generating identical signatures checking with --file-source $(cat ) (note the space between 'cat' and ')')
591 $ python3 signduterre.py --passphrase test-20090825_14_48-23.pwd --check-file test-20090825_14_48-23.sdt -v \\
592 --execute --file-source '$(cat )'
593 # Example generating identical signatures checking with --file-source $(perl -e "{open(F, \"<{}\");print <F>;};)
594 $ python3 signduterre.py --passphrase test-20090825_14_48-23.pwd --check-file test-20090825_14_48-23.sdt -v \\
595 --execute --file-source '$(perl -e "{open(F, \\"<{}\\");print <F>;};")'
596 [[[/pre]]][[[p]]]
597 The integrity of a running 'cat' command can be checked with module proc_PID that will create a signature of all
598 files loaded with the 'cat' command from the information in the /proc/<pid>/maps file using the inode numbers
599 (sudo only). Debugfs will read the blocks directly from the medium using the inode tables without using the filenames.
600 [[[/p]]][[[pre make=sudo2]]]
601 # make: sudo2
602 # Use module proc_PID to check the integrety of 'cat' and all libraries loaded with it
603 # Check out the workings of proc_PID.py with '$ python3 proc_PID.py'
604 # The actual output of the module used in the signature can be inspected with --print-textdump
605 $ sudo python3 signduterre.py -p poiuytrewq --salt SUGGESTED --detailed-view \\
606 --verbose --execute -u root -o test-proc_PID.sdt --import proc_PID \\
607 '@(return proc_PID.paths("cat","inode"))' '@(return proc_PID.fileSHA("cat", mainprefix))' \\
608 '@(return proc_PID.inodeSHA("cat", "", mainprefix))' '@(return proc_PID.mapsSHA("cat", mainprefix))'
609 # Check the results
610 $ sudo python3 signduterre.py -p poiuytrewq --detailed-view --verbose --execute -u root \\
611 --check-file test-proc_PID.sdt --import proc_PID
612 [[[/pre]]][[[p]]]
613 Check only the Table of Contents of this file using a byte range slice
614 [[[/p]]][[[pre make=example8]]]
615 # make: example8
616 # Create standard signature
617 $ python3 signduterre.py --passphrase kdfuhgcriu --salt SUGGESTED --detailed-view --verbose \\
618 'signduterre.py[0o7:+136:0xf]' -o test-slices.sdt -P test-slices.pwd --print-textdump
619 # Standard check
620 $ python3 signduterre.py --passphrase kdfuhgcriu --detailed-view --verbose \\
621 -c test-slices.sdt --print-textdump
622 [[[/pre]]][[[p]]]
623 The examples can be run as a makefile using make. Use one of the following commands:
624 [[[/p]]][[[pre]]]
625 # General examples, use them all
626 python3 signduterre.py --manual-make |make -f - example
627 # Linux specific examples using the second procfs example
628 python3 signduterre.py --manual-make |make -f - procfs2
629 # Examples requiring sudo, using first
630 python3 signduterre.py --manual-make | sudo make -f - sudo1
631 [[[/pre]]][[[h2]]]
632 Known Bugs:
633 [[[/h2]]][[[p]]]
634 - Reading files from STDIN (-) does not work if ssh:// has been used before as input for,
635 eg, file arguments, --check-file or --passphrase
636 [[[/p]]][[[pre make=sshbug1]]]
637 # make: sshbug1
638 # '-' stdin before ssh:// is fine
639 $ dd if=/bin/ps 2>/dev/null | python3.0 signduterre.py -edv -p SUGGESTED -s SUGGESTED \
640 /bin/ps - ssh://`whoami`@localhost/bin/ps
641 # '-' stdin after ssh:// FAILs
642 $ dd if=/bin/ps 2>/dev/null | python3.0 signduterre.py -edv -p SUGGESTED -s SUGGESTED \
643 /bin/ps ssh://`whoami`@localhost/bin/ps -
644 [[[/pre]]][[[p]]]
645 - Reading URLs as file arguments should work when Python treats URLs identical
646 to file descriptors. For the technically inclined:
647 when:[[[br /]]]
648 [[[tt]]]with urllib.request.urlopen(url) as f:[[[/tt]]][[[br /]]]
649 works, URLs can be entered where ever file paths can be entered..
650 [[[/p]]][[[/body]]][[[/html]]]
653 license = """
654 Signature-du-Terroir
655 Construct a signature of the installed software state or check a previously made signature.
657 copyright 2009, R.J.J.H. van Son
659 This program is free software: you can redistribute it and/or modify
660 it under the terms of the GNU General Public License as published by
661 the Free Software Foundation, either version 3 of the License, or
662 (at your option) any later version.
664 This program is distributed in the hope that it will be useful,
665 but WITHOUT ANY WARRANTY; without even the implied warranty of
666 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
667 GNU General Public License for more details.
669 You should have received a copy of the GNU General Public License
670 along with this program. If not, see <http://www.gnu.org/licenses/>.
671 """;
673 # Note that only release notes are put here
674 # See git repository for detailed change comments:
675 # git clone git://repo.or.cz/signduterre.git
676 # http://repo.or.cz/w/signduterre.git
677 releasenotes = """
678 20090926 - Opened work on v0.6a
679 20090926 - Added expressions to byte range slices
680 20090922 - Release v0.6
681 20090922 - Regression testing
682 20090916 - Completed work on import of modules and documentation thereof
683 20090829 - Added optional offset to [<start>:<end>:<offset>] byte slices
684 20090826 - Added [<start>:<end>] byte slices for every argument
685 20090825 - Added --source-file $(cmd) as substitute file readers
686 20090820 - Release v0.6RC
687 20090820 - Added extensibility, or plugins, with functional @(python code) execution
688 20090817 - Replaced --print-hexdump by --print-textdump
689 20090817 - Implemented ssh:// with ${ENV}
690 20090814 - Release v0.5RC
691 20090811 - Implemented ssh tunnel for commands
692 20090810 - Added --file-source=PATH option
693 20090810 - Added ssh tunnel for all file i/o (ssh://...)
694 20090807 - DIFFERENT became FAIL in --check-file
695 20090730 - Release v0.4
696 20090724 - Added '--all-salts-pattern' and HTML formatting in manual
697 20090723 - Added URL support for all files. Does not yet work due to bug in Python 3.0
698 20090723 - Added '-' for STDIN
699 20090717 - Added --execute-args
700 20090716 - Release v0.3
701 20090713 - Added --quiet option
702 20090712 - moved from /dev/random to /dev/urandom
703 20090702 - Replaced -g with -p SUGGESTED[=N]
704 20090702 - Generating and testing lists of random salts
705 20090701 - Release v0.2
706 20090630 - Generating and testing random passphrases
707 20090630 - --execute works on $(cmd) only, nlinks in ?path and ? implied for directories
708 20090630 - Ported to Python 3.0
710 20090628 - Release v0.1b
711 20090628 - Added release-notes
713 20090626 - Release v0.1a
714 20090626 - Initial commit to Git
715 """;
717 #############################################################################
719 # IMPORT & INITIALIZATION #
721 #############################################################################
723 import sys;
724 import os;
725 import subprocess;
726 import stat;
727 import subprocess;
728 # if sys.stdout.isatty(): import readline;
729 import binascii;
730 import hashlib;
731 import re;
732 import time;
733 from optparse import OptionParser;
734 import base64;
735 import random;
736 import struct;
737 import urllib.request;
738 import urllib.error;
740 # Limit the characters that can be used in $(cmd) commands
741 # Only allow the escape of '$'
742 not_allowed_chars = re.compile('([^\w\ \.\/\"\|\;\:\,\-\$\[\]\{\}\(\)\@\`\!\*\=\\\\\<\>]|([\\\\]+([^\$\"\\\\]|$)))');
744 programname = "Signature-du-Terroir";
745 version = "0.6a";
748 # Open files or pipes for in/output, use mode = 'b' if binary is needed
749 def open_infile(filename, mode):
750 if filename == '-':
751 return sys.stdin;
752 elif filename.lower().find('ssh://') > -1:
753 match = re.search('(?i)ssh://([^/]+)/(.*)$', filename);
754 tunnel_command = 'ssh '+match.group(1)+' "dd if=/'+match.group(2)+' "';
755 if 'b' in mode:
756 pipe = subprocess.Popen(tunnel_command, shell=True, stdout=subprocess.PIPE);
757 else:
758 pipe = subprocess.Popen(tunnel_command, shell=True, stdout=subprocess.PIPE, universal_newlines=True);
759 return pipe.stdout;
760 elif filename.find('://') > -1:
761 print("URL:", filename, file=current_private);
762 return urllib.request.urlopen(filename);
763 else:
764 if not os.path.isfile(filename):
765 print(filename, "does not exist", file=sys.stderr)
766 quit();
767 return open(filename, mode);
769 def open_outfile(filename, mode):
770 if filename == '-':
771 return sys.stdout;
772 elif filename.lower().find('ssh://') > -1:
773 match = re.search('(?i)ssh://([^/]+)/(.*)$', filename);
774 tunnel_command = 'ssh '+match.group(1)+' "dd of=/'+match.group(2)+' "';
775 if 'b' in mode:
776 pipe = subprocess.Popen(tunnel_command, shell=True, stdin=subprocess.PIPE);
777 else:
778 pipe = subprocess.Popen(tunnel_command, shell=True, stdin=subprocess.PIPE, universal_newlines=True);
779 return pipe.stdin;
780 elif filename.find('://') > -1:
781 print("URL:", filename, file=current_private);
782 return urllib.request.urlopen(filename);
783 else :
784 return open(filename, mode);
786 current_outfile = sys.stdout;
787 current_private = sys.stderr;
789 # Determine which kind of argument you have
790 def arg_is_shell_command (argument):
791 return_value = False;
792 if argument.startswith('$(') and argument.endswith(')'):
793 return_value = argument[2:-1];
794 return return_value;
796 def arg_is_env (argument): # -> Env/False
797 return_value = False;
798 if arg_is_shell_command(argument): return return_value;
799 match = re.match(r'\$\{?([^\}]+)\}?$', argument);
800 if match != None:
801 return_value = match.group(1);
802 return return_value;
804 def arg_is_python_script (argument): # -> Script/False
805 return_value = False;
806 if argument.startswith('@(') and argument.endswith(')'):
807 return_value = argument[2:-1];
808 return return_value;
810 def arg_is_hidden (argument): # -> Exposed arg/False
811 return_value = False;
812 if argument.startswith('[') and argument.endswith(']'):
813 return_value = argument[1:-1];
814 return return_value;
816 def arg_is_tunnel (argument): # -> True/False
817 return argument.find('ssh://') > -1;
819 def arg_is_stat (argument): # -> Path/False
820 return_value = False;
821 if argument.startswith('?'):
822 return_value = argument[1:];
823 return return_value;
825 def arg_is_stdin (argument): # -> True/False
826 return argument == '-';
828 def arg_is_URL (argument): # -> True/False
829 return re.search(r'\://', argument) and not arg_is_tunnel(argument);
831 def arg_is_dir (argument): # -> True/False
832 return os.path.isdir(argument);
834 # Plain files are defined as NOT something else
835 def arg_is_plain_file (argument): # -> True/False
836 not_file = False;
837 not_file = not_file or arg_is_env(argument)
838 not_file = not_file or arg_is_shell_command (argument);
839 not_file = not_file or arg_is_python_script (argument);
840 not_file = not_file or arg_is_tunnel (argument);
841 not_file = not_file or arg_is_stdin (argument);
842 # not_file = not_file or arg_is_URL (argument);
843 not_file = not_file or arg_is_dir (argument);
844 not_file = not_file or arg_is_stat (argument);
846 return not not_file;
848 # Supportive functions
849 # Convert Hexadecimal, 0XFFFF, Octal, 0o7777, Binary, 0B111, and decimal, 9999, strings to int
850 def convertString2Int (number):
851 result = 0;
852 # If this is a number
853 if number > ' ' and re.search(r'(?i)[^oxb0-9A-F]', number) == None:
854 # Is it hexadecimal?
855 if number.upper().startswith('0X') or re.search(r'(?i)[A-F]', number) != None:
856 result=int(number, 16);
857 # Is it octal
858 elif number.upper().startswith('0O'):
859 result=int(number, 8);
860 # Is it binary?
861 elif number.upper().startswith('0B'):
862 result=int(number, 2);
863 # Then it must be decimal
864 else:
865 result = int(number);
866 return result;
868 #############################################################################
870 # OPTION HANDLING #
872 #############################################################################
874 parser = OptionParser()
875 parser.add_option("-s", "--salt", metavar="HEX",
876 dest="salt", default=False,
877 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).")
878 parser.add_option("-a", "--all-salts-pattern",
879 dest="allsalts", default=False, action="store_true",
880 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.")
881 parser.add_option("-p", "--passphrase", metavar="TEXT",
882 dest="passphrase", default=False,
883 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).")
884 parser.add_option("-c", "--check-file",
885 dest="check", default=False, metavar="FILE",
886 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.")
887 parser.add_option("-i", "--input-file",
888 dest="input", default=False, metavar="FILE",
889 help="Use names from FILE or STDIN ('-'), use one filename per line.")
890 parser.add_option("-o", "--output-file",
891 dest="output", default=False, metavar="FILE",
892 help="Print to FILE instead of STDOUT.")
893 parser.add_option("--file-source",
894 dest="filesource", default=False, metavar="PATH",
895 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]'. "
896 + "A shell command that prints out the file can be entered as '$(<cmd>)'. The filepath will be substituted for any '{}' string in the command, or appended tot the command (without white-space). "
897 + "The option overrules any File source specification in the --check-file.")
898 parser.add_option("-P", "--Private-file",
899 dest="private", default=False, metavar="FILE",
900 help="Print private information (passwords etc.) to FILE instead of STDERR.")
901 parser.add_option("-u", "--user",
902 dest="user", default="nobody", metavar="USER",
903 help="Execute $(cmd) as USER, default 'nobody' (root/sudo only)")
904 parser.add_option("-S", "--Status",
905 dest="status", default=False, action="store_true",
906 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)")
907 parser.add_option("--Status-values",
908 dest="statusvalues", default="fmidugs", metavar="MODE",
909 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).")
910 parser.add_option("-t", "--total-only",
911 dest="total", default=False, action="store_true",
912 help="Only print the total hash, unsets --detailed-view (default True)")
913 parser.add_option("-d", "--detailed-view",
914 dest="detail", default=False, action="store_true",
915 help="Print hashes of individual files, is unset by --total-only (default False)")
916 parser.add_option("-e", "--execute",
917 dest="execute", default=False, action="store_true",
918 help="Interpret $(cmd) (default False)")
919 parser.add_option("--execute-args",
920 dest="executeargs", default='', metavar="ARGS",
921 help="Arguments for the $(cmd) commands ($1 ....)")
922 parser.add_option("-n", "--no-execute",
923 dest="noexecute", default=False, action="store_true",
924 help="Explicitely do NOT Interpret $(cmd)")
925 parser.add_option("--import",
926 dest="importfile", default='', metavar="FILE",
927 help="Import python modules (comma separated list)")
928 parser.add_option("--print-textdump",
929 dest="printtextdump", default=False, action="store_true",
930 help="Print printable character+hexadecimal dump of input bytes to STDERR for debugging purposes")
931 parser.add_option("--message",
932 dest="message", default='', metavar="TEXT",
933 help="Add a comment message about the test")
934 parser.add_option("-m", "--manual",
935 dest="manual", default=False, action="store_true",
936 help="Print a short version of the manual and exit")
937 parser.add_option("--manual-long",
938 dest="manuallong", default=False, action="store_true",
939 help="Print the long version of the manual and exit")
940 parser.add_option("--manual-html",
941 dest="manualhtml", default=False, action="store_true",
942 help="Print the manual in HTML format and exit")
943 parser.add_option("--manual-make",
944 dest="manualmake", default=False, action="store_true",
945 help="Print the examples in the manual as a makefile and exit")
946 parser.add_option("-r", "--release-notes",
947 dest="releasenotes", default=False, action="store_true",
948 help="Print the release notes and exit")
949 parser.add_option("-l", "--license",
950 dest="license", default=False, action="store_true",
951 help="Print license text and exit")
952 parser.add_option("-v", "--verbose",
953 dest="verbose", default=False, action="store_true",
954 help="Print more information on output")
955 parser.add_option("-q", "--quiet",
956 dest="quiet", default=False, action="store_true",
957 help="Print minimal information (hide filenames). If the output is used with --check-file, the command line options and arguments must be repeated.")
959 (options, check_filenames) = parser.parse_args();
962 # Start with opening any non-default output files
963 my_output = False;
964 if options.output:
965 current_outfile = open_outfile(options.output, 'w');
966 my_output = options.output;
968 my_private = False;
969 if options.private:
970 current_private = open_outfile(options.private, 'w');
971 my_private = options.private;
973 print("# Program: "+programname + " version " + version, file=current_outfile);
974 print("#", time.strftime("%Y/%m/%d %H:%M:%S", time.localtime()), "("+time.tzname[0]+")\n", file=current_outfile);
976 # Print license
977 if options.license:
978 print (license, file=sys.stderr);
979 exit(0);
980 # Print manual
981 if options.manual or options.manuallong:
982 cleartext_manual = re.sub(r"(?i)\[\[\[\s*(/?)\s*LONG\s*\]\]\]", r'[[[\1LONG]]]', manual);
983 if not options.manuallong:
984 currentstart = cleartext_manual.find('[[[LONG]]]');
985 while currentstart > -1:
986 currentend = cleartext_manual.find('[[[/LONG]]]', currentstart)+len('[[[/LONG]]]');
987 (firstpart, secondpart) = cleartext_manual.split(cleartext_manual[currentstart:currentend]);
988 cleartext_manual = firstpart+secondpart;
989 currentstart = cleartext_manual.find('[[[LONG]]]');
990 htmltags = re.compile('\[\[\[[^\]]*\]\]\]');
991 cleartext_manual = htmltags.sub('', cleartext_manual);
992 print (cleartext_manual, file=sys.stdout);
993 exit(0);
994 # Print HTML manual
995 if options.manualhtml:
996 protleftanglesbracks = re.compile('\<');
997 protrightanglesbracks = re.compile('\>');
998 leftanglesbracks = re.compile('\[\[\[');
999 rightanglesbracks = re.compile('\]\]\]');
1000 html_manual = re.sub(r"(?i)\[\[\[\s*(/?)\s*LONG\s*\]\]\]", '', manual);
1001 html_manual = protleftanglesbracks.sub('&lt;', html_manual);
1002 html_manual = protrightanglesbracks.sub('&gt;', html_manual);
1003 html_manual = leftanglesbracks.sub('<', html_manual);
1004 html_manual = rightanglesbracks.sub('>', html_manual);
1005 print (html_manual, file=sys.stdout);
1006 exit(0);
1007 # Print manual examples as makefile
1008 if options.manualmake:
1009 make_manual = re.sub("\$ ", "\t", manual);
1010 make_manual = re.sub("\#", "\t#", make_manual);
1011 make_manual = re.sub(r"\\\s*\n", '', make_manual);
1012 make_manual = re.sub(r"\$", r'$$', make_manual);
1013 # Protect "single" [ brackets
1014 make_manual = re.sub(r"(^|[^\[])\[([^\[]|$)", r"\1&#91;\2", make_manual);
1015 extrexamples = re.compile(r"\[\[\[pre\s+make\=?([a-zA-Z]*)([0-9]*)\s*\]\]\]\n([^\[]*)\n\[\[\[/pre\s*\]\]\]", re.IGNORECASE|re.MULTILINE|re.DOTALL);
1016 exampleiter = extrexamples.finditer(make_manual);
1017 makefile_list = [];
1018 group_list={};
1019 group_list['all'] = "all: ";
1020 for match in exampleiter:
1021 # We had to convert any '[' in the command. Now convert them back.
1022 command_text = re.sub(r"\&\#91\;", '[', match.group(3));
1023 makefile_list.append(match.group(1)+match.group(2)+":\n"+command_text);
1024 if len(match.group(2)) > 0:
1025 if not match.group(1) in group_list.keys():
1026 group_list[match.group(1)] = match.group(1)+": ";
1027 group_list['all'] += match.group(1)+" ";
1028 group_list[match.group(1)] += match.group(1)+match.group(2)+" ";
1029 else:
1030 group_list['all'] += match.group(1)+" ";
1031 print("help: \n\t@echo 'Use \"make -f - "+re.sub(':', '', group_list['all'])+"\"'", file=sys.stdout);
1032 for group in group_list:
1033 print(group_list[group]+"\n", file=sys.stdout);
1034 makefile_list.sort()
1035 previous_cat = 'NOT A VALUE';
1036 for line in makefile_list:
1037 (category, commands) = line.split(':\n');
1038 if category != previous_cat:
1039 previous_cat = category;
1040 print("\n"+previous_cat+":", file=sys.stdout);
1041 print(commands, file=sys.stdout);
1042 # Clean option
1043 print("\nclean:\n\trm test-*.sdt test-*.pwd", file=sys.stdout);
1044 exit(0);
1045 # Print release notes
1046 if options.releasenotes:
1047 print ("Version: "+version, file=sys.stderr);
1048 print (releasenotes, file=sys.stderr);
1049 exit(0);
1051 my_salt = options.salt;
1052 my_allsalts = options.allsalts;
1053 my_passphrase = options.passphrase;
1054 my_check = options.check;
1055 my_status = options.status;
1056 my_statusvalues = options.statusvalues;
1057 my_verbose = options.verbose and not options.quiet;
1058 my_quiet = options.quiet;
1059 execute = options.execute;
1060 execute_args = options.executeargs;
1061 if options.noexecute: execute = False;
1062 input_file = options.input;
1063 my_filesource = options.filesource;
1064 if my_filesource: print("File source: '"+my_filesource+"'\n", file=current_outfile);
1065 my_message = options.message;
1066 my_importfiles = options.importfile;
1067 if my_importfiles:
1068 for module_name in my_importfiles.split(','):
1069 var_module = __import__(module_name);
1070 print("Import: '"+my_importfiles+"'\n", file=current_outfile);
1073 # Set total-only with the correct default
1074 total_only = True;
1075 total_only = not options.detail;
1076 if options.total: total_only = options.total;
1077 if my_allsalts: total_only = my_allsalts; # All alts pattern only sensible with total-only
1078 if my_check: total_only = False;
1080 my_user = options.user;
1081 # Things might be executed as another user
1082 user_change = '';
1083 if os.getuid() == 0:
1084 user_change = 'sudo -H -u '+my_user+' ';
1085 if not my_quiet: print("User: "+my_user, file=current_outfile);
1087 # Execute option
1088 if execute:
1089 text_execute = "True";
1090 else:
1091 text_execute = "False";
1093 if execute and not my_quiet:
1094 print("Execute system commands: "+text_execute+"\n", file=current_outfile);
1095 if execute_args != '': print("Execute arguments: '"+execute_args+"'\n", file=current_outfile);
1097 # --quiet option
1098 if my_quiet: print("Quiet: True\n", file=current_outfile);
1100 # --Status-values option
1101 if my_statusvalues != 'fmidugs': print("Status-values: '"+my_statusvalues+"'\n", file=current_outfile);
1103 # --message option
1104 if len(my_message) > 0: print("Message: '''"+my_message+"'''\n", file=current_outfile);
1106 #############################################################################
1108 # ARGUMENT PROCESSING #
1110 #############################################################################
1112 # Measure time intervals
1113 start_time = time.time();
1115 dev_random = open("/dev/urandom", 'rb');
1117 # Read the check file
1118 passphrase_list = [];
1119 salt_list = [];
1120 check_hashes = {};
1121 total_hash_list = [];
1122 if my_check:
1123 highest_arg_used = 0;
1124 print("# Checking: "+my_check+"\n", file=current_outfile);
1125 arg_list = check_filenames;
1126 check_filenames = [];
1127 with open_infile(my_check, 'r') as c:
1128 for line in c:
1129 match = re.search(r"Execute system commands:\s+(True|False)", line);
1130 if match != None:
1131 # Uncomment the next line if you want automatic --execute from the check-file (DANGEROUS)
1132 # execute = match.group(1).upper() == 'TRUE';
1133 continue;
1135 match = re.search(r"Execute arguments:\s+\'([\w\$\s\-\+\/]*)\'", line);
1136 if match != None:
1137 execute_args = match.group(1);
1138 continue;
1140 match = re.search(r"Quiet:\s+(True|False)", line);
1141 if match != None:
1142 my_quiet = match.group(1).upper() == 'TRUE';
1143 if my_quiet: my_verbose = False;
1144 continue;
1146 match = re.search(r"File source:\s+\'([\w\ \-\+\`\"\[\]\{\}\@\$\=\:\/\(\)\<\>\.\,\;\?\*\&\^\%\\]+)\'", line);
1147 if not my_filesource and match != None:
1148 my_filesource = match.group(1);
1149 continue;
1151 match = re.search(r"Salt\s*\+\s*TOTAL HASH\s*\:\s+\'([\w]*)\'\s+\'([\w]*)\'", line);
1152 if match != None:
1153 salt_list.append(match.group(1));
1154 total_hash_list.append(match.group(2));
1155 my_allsalts = True; # Salt+TOTAL HASH imples all-salts-pattern
1156 continue;
1158 match = re.search(r"Salt\:\s+\'([\w]*)\'", line);
1159 if match != None:
1160 salt_list.append(match.group(1));
1161 continue;
1163 match = re.search("Salt\s*\+\s*TOTAL HASH\s*\:\s+\'([\w]+)\'\s+\'([a-f0-9]+)\'", line);
1164 if match != None:
1165 salt_list.append(match.group(1));
1166 total_hash_list.append(match.group(2));
1167 continue;
1169 match = re.search("User\:\s+\'([\w]*)\'", line);
1170 if match != None:
1171 # Uncomment the next line if you want automatic --user from the check-file (DANGEROUS)
1172 # my_user = match.group(1);
1173 continue;
1175 match = re.search("Passphrase\:\s+\'([^\']*)\'", line);
1176 if match != None:
1177 passphrase_list.append(match.group(1));
1178 continue;
1180 match = re.search("Status-values\:\s+\'([\w]*)\'", line);
1181 if match != None:
1182 my_statusvalues = match.group(1);
1183 continue;
1185 match = re.search(r"Import\:\s+\'([^\']*)\'", line);
1186 if match != None:
1187 my_importfiles = match.group(1);
1188 continue;
1190 match = re.search("Message\:\s+\'\'\'(.*)$", line);
1191 if match != None:
1192 my_message = match.group(1);
1193 my_message = my_message[0:my_message.find("'''")];
1194 print("Message: '''"+my_message+"'''\n", file=current_outfile);
1195 continue;
1197 match = re.search("^\s*([a-f0-9]+)\s+\*(TOTAL HASH)\s*$", line)
1198 if match != None:
1199 total_hash_list.append(match.group(1));
1200 continue;
1202 match = re.search("^\s*([a-f0-9\-]+)\s+\*\[([0-9]+)\]\s*$", line)
1203 if match != None:
1204 filenumber = int(match.group(2));
1205 if filenumber > highest_arg_used: highest_arg_used = filenumber;
1206 # Watch out, arguments count from 0
1207 check_filenames.append(arg_list[filenumber - 1]);
1208 check_hashes['['+match.group(2)+']'] = match.group(1);
1209 continue;
1211 match = re.search("^\s*([a-f0-9\-]+)\s+\*(.*)\s*$", line)
1212 if match != None:
1213 check_filenames.append(match.group(2));
1214 # Catch --execute error as early as possible
1215 if match.group(2).startswith('$(') and not execute:
1216 error_message = "Executable argument \'"+match.group(2)+"\' only allowed with the --execute flag";
1217 print (error_message, file=sys.stderr);
1218 if not sys.stdout.isatty(): print(error_message, file=current_outfile);
1219 exit(0);
1220 check_hashes[match.group(2)] = match.group(1);
1221 continue;
1222 for i in range(highest_arg_used, len(arg_list)):
1223 check_filenames.append(arg_list[i]);
1224 check_hashes['['+str(i+1)+']'] = (64*'-');
1226 # Read input-file
1227 if input_file:
1228 with open_infile(input_file, 'r') as i:
1229 for line in i:
1230 # Clean up filename
1231 current_filename = line.lstrip(' \n').rstrip(' \n');
1232 check_filenames.append(current_filename);
1233 if my_check: check_hashes['['+str(i+1)+']'] = (64*'-');
1235 stat_list = [];
1236 for x in check_filenames:
1237 if os.path.isdir(x):
1238 x = '?'+x;
1239 if my_status and not x.startswith(('?', '$')):
1240 stat_list.append('?'+x);
1241 stat_list.append(x);
1242 check_filenames = stat_list;
1244 # Seed Pseudo Random Number Generator
1245 seed = dev_random.read(16);
1246 random.seed(seed);
1248 # Read suggested salts from /dev/(u)random if needed
1249 if my_salt:
1250 if my_salt.startswith('SUGGESTED'):
1251 N=1;
1252 match = re.search("([0-9][0-9]*)$", my_salt);
1253 if match != None:
1254 N = int(match.group(1));
1255 for i in range(0,N):
1256 salt = dev_random.read(8);
1257 salt_list.append(str(binascii.hexlify(salt), 'ascii'));
1258 else:
1259 salt_list.append(my_salt);
1260 elif len(salt_list) == 0:
1261 salt = dev_random.read(8);
1262 sys.stderr.write("Enter salt (suggest \'"+str(binascii.hexlify(salt), 'ascii')+"\'): ");
1263 new_salt = input();
1264 if not new_salt: new_salt = str(binascii.hexlify(salt), 'ascii');
1265 salt_list.append(new_salt);
1267 # If not combining salts with TOTAL HASH, print salts now
1268 if not my_allsalts:
1269 for my_salt in salt_list:
1270 print("Salt: \'"+my_salt+"\'", file=current_outfile);
1272 # Get passphrase
1273 if my_passphrase and(my_passphrase == '-' or my_passphrase.find("://") > -1 or os.path.isfile(my_passphrase)):
1274 with open_infile(my_passphrase, 'r') as file:
1275 for line in file:
1276 match = re.search("Passphrase\:\s+\'([^\']*)\'", line);
1277 if match != None:
1278 passphrase_list.append(match.group(1));
1279 elif not my_passphrase and len(passphrase_list) == 0:
1280 suggest_passphrase = dev_random.read(16);
1281 suggest_string = "";
1282 if not my_check:
1283 suggest_string = "(suggest \'"+str(base64.b64encode(suggest_passphrase), 'ascii').rstrip('=')+"\')";
1284 sys.stderr.write("Enter passphrase"+suggest_string+": ");
1285 # How kan we make this unreadable on input?
1286 current_passphrase = input();
1287 if not current_passphrase:
1288 current_passphrase = str(base64.b64encode(suggest_passphrase), 'ascii').rstrip('=');
1289 print("Passphrase: \'"+current_passphrase+"\'", file=current_private);
1290 passphrase_list.append(current_passphrase);
1291 elif my_passphrase.startswith('SUGGESTED'):
1292 N = 1;
1293 match = re.search("([0-9][0-9]*)$", my_passphrase);
1294 if match != None:
1295 N = int(match.group(1));
1296 j = int(random.random()*N);
1297 for i in range(0, N):
1298 suggest_passphrase = dev_random.read(16);
1299 current_passphrase = str(base64.b64encode(suggest_passphrase), 'ascii').rstrip('=');
1300 print("Passphrase: \'"+current_passphrase+"\'", file=current_private);
1301 passphrase_list.append(current_passphrase);
1302 else:
1303 passphrase_list.append(my_passphrase);
1305 selected_salt = 1;
1306 fail_fraction = 0.5;
1307 if not my_check:
1308 if len(passphrase_list) > 1:
1309 j = int(random.random()*len(passphrase_list));
1310 passphrase_list = [passphrase_list[j]];
1311 print("# Selected passphrase:", j+1, file=current_private);
1312 if len(salt_list) > 1:
1313 j = int(random.random()*len(salt_list));
1314 # Make sure at least 1 salt will match and print the selection if only one is used
1315 selected_salt = j+1;
1316 if not my_allsalts:
1317 salt_list = [salt_list[selected_salt-1]];
1318 print("# Selected salt:", selected_salt, file=current_private);
1319 else:
1320 salt_N = len(salt_list);
1321 fail_fraction = (salt_N/2.0)/(salt_N - 1);
1322 else:
1323 fail_fraction = 0;
1325 # Close /dev/(u)random
1326 dev_random.close;
1328 #############################################################################
1330 # SIGNATURE CREATION AND CHECKING #
1332 #############################################################################
1334 end_time = time.time();
1335 print("# Preparation time:", end_time - start_time, "seconds\n", file=current_outfile);
1337 pnum = 1;
1338 snum = 1;
1339 corrpnum = 0;
1340 corrsnum = 0;
1341 matched_salt_pattern = -1;
1342 salt_pattern_number = -1;
1344 for my_passphrase in passphrase_list:
1345 snum = 1;
1346 # Initialize salt pattern
1347 if my_allsalts:
1348 salt_pattern_number = 0;
1349 current_salt_power = 1;
1351 for my_salt in salt_list:
1352 print("# Start signature: ", end='', file=current_outfile);
1353 if len(passphrase_list) > 1: print("passphrase -", pnum, end='', file=current_outfile);
1354 if len(salt_list) > 1: print(" salt -", snum, end='', file=current_outfile);
1355 print("", file=current_outfile);
1357 # Should everything be printed?
1358 print_verbose = my_verbose and not (my_allsalts and snum > 1);
1360 file_argnum = 0;
1361 start_time = time.time();
1362 # Construct the passphrase hash
1363 passphrase = hashlib.sha256();
1365 passphrase.update(bytes(my_salt, encoding='ascii'));
1366 passphrase.update(bytes(my_passphrase, encoding='ascii'));
1368 # Create prefix which is a hash of the salt+passphrase
1369 prefix = passphrase.hexdigest();
1371 ##########################################
1373 # Create signature and write output #
1375 ##########################################
1377 totalhash = hashlib.sha256();
1378 totalhash.update(bytes(prefix, encoding='ascii'));
1379 for org_filename in check_filenames:
1380 # Create file hash object
1381 filehash = hashlib.sha256();
1382 filehash.update(bytes(prefix, encoding='ascii'));
1383 # Remove []
1384 filename = org_filename;
1385 if org_filename.startswith('[') and org_filename.endswith(']'):
1386 filename = filename[1:len(filename)-1];
1387 # Select input bytes to use for signature
1388 input_start = 0;
1389 input_length = 0;
1390 if filename.endswith(']'):
1391 input_offset = 0;
1392 # Find the matching open bracket
1393 finalCloseBracket = filename.rfind(']', 0, len(filename));
1394 closeBracket = finalCloseBracket;
1395 openBracket = filename.rfind('[', 0, closeBracket);
1396 while filename.rfind(']', openBracket, closeBracket) > -1:
1397 openBracket = filename.rfind('[', 0, openBracket);
1398 closeBracket = filename.rfind(']', openBracket, closeBracket);
1399 interval = filename[openBracket+1:finalCloseBracket];
1400 filename = filename[0:openBracket];
1401 (new_start, new_end, new_offset) = ('', '', '');
1402 # split interval
1403 intervalList = [];
1404 interval = interval.lstrip(' ').rstrip(' ')
1405 while len(interval) > 0:
1406 if interval.endswith(')'):
1407 closeBracket = len(interval) - 1;
1408 openBracket = interval.rfind('(', 0, closeBracket);
1409 while interval.rfind(')', openBracket, closeBracket) > -1:
1410 openBracket = interval.rfind('(', 0, openBracket);
1411 closeBracket = interval.rfind(')', openBracket, closeBracket);
1412 while openBracket > 0 and interval[openBracket-1:openBracket] != ':':
1413 openBracket -= 1;
1414 intervalList.append(interval[openBracket:len(interval)]);
1415 interval = interval[0:openBracket-1];
1416 elif interval.rfind(':') > -1:
1417 colonPoint = interval.rfind(':');
1418 intervalList.append(interval[colonPoint+1:len(interval)]);
1419 interval = interval[0:colonPoint];
1420 else:
1421 intervalList.append(interval);
1422 interval = "";
1423 # Add missing offset
1424 intervalList.reverse();
1425 if len(intervalList) < 3: intervalList.append('0');
1426 (new_start, new_end, new_offset) = intervalList;
1427 if new_end.lstrip(' ').startswith('+'): new_end = new_start+new_end;
1428 if len(new_start) > 0 and len(new_end) > 0:
1429 # Execute priviledges or not even builtins
1430 if execute:
1431 input_start = eval(new_start);
1432 input_end = eval(new_end);
1433 input_offset = eval(new_offset);
1434 else: # not even builtins
1435 input_start = eval(new_start, {"__builtins__":None}, {});
1436 input_end = eval(new_end, {"__builtins__":None}, {});
1437 input_offset = eval(new_offset, {"__builtins__":None}, {});
1438 input_length = input_end - input_start;
1439 if input_start >=0 and input_offset > 0 : input_start += input_offset;
1441 # Preprocessing filename to include "external" file sources
1442 if my_filesource:
1443 # Insert different file reader as a shell command
1444 if arg_is_shell_command(my_filesource) and arg_is_plain_file(filename):
1445 current_source_command = arg_is_shell_command(my_filesource);
1446 if current_source_command.find('{}') > -1:
1447 current_source_command = re.sub(r'\{\}', filename, current_source_command);
1448 else:
1449 current_source_command += filename;
1450 filename = "$("+current_source_command+")";
1451 # Insert remote ssh:// if necessary and not allready a tunnel
1452 elif arg_is_tunnel(my_filesource) and not arg_is_tunnel(filename):
1453 if arg_is_env(filename) :
1454 match = re.search(r"(?i)ssh://([^/]+)", my_filesource);
1455 if match != None: filename = "${ssh://"+match.group(1)+"/"+arg_is_env(filename)+"}";
1456 elif arg_is_shell_command(filename):
1457 match = re.search(r"(?i)ssh://([^/]+)", my_filesource);
1458 if match != None: filename = "$(ssh://"+match.group(1)+"/"+arg_is_shell_command(filename)+")";
1459 elif arg_is_plain_file(filename):
1460 filename = my_filesource+filename;
1461 # Handle file tunnels: convert 'ssh://<user>@<host>/<path>' to
1462 # $(ssh://<user>@<host>/dd if=/<path>)
1463 if arg_is_tunnel(filename) and filename.startswith("ssh://"):
1464 match = re.search(r"(?i)ssh://([^/]+)/(.*)$", filename);
1465 if match != None:
1466 host = match.group(1);
1467 path = match.group(2);
1468 if arg_is_stat(path):
1469 print("Error: status not possible in tunnels - "+filename);
1470 else:
1471 if input_start > 0 or input_length > 0:
1472 filename = '$(ssh://'+host+'/dd if=/'+path+' bs=1 skip='+str(input_start)+' count='+str(input_length)+')';
1473 # The start and end have been dealt with, remove them
1474 input_start = 0;
1475 input_length = 0;
1476 else:
1477 filename = '$(ssh://'+host+'/dd if=/'+path+')';
1478 print(filename);
1479 # Use python @() constructs
1480 orig_command_filename = filename;
1481 if arg_is_python_script(filename):
1482 if not execute:
1483 error_message = "Executable argument \'"+filename+"\' only allowed with the --execute flag";
1484 print (error_message, file=sys.stderr);
1485 exit(1);
1486 # Check for dangerous constructs
1487 if re.search(r'\bimport\b', filename):
1488 print("Error: Import statement not allowed, please use --import option", file=sys.stderr);
1489 exit(1);
1490 # Construct a code wrapper around the user supplied code
1491 script_input = arg_is_python_script(filename);
1492 statement_string = "";
1493 if my_importfiles: statement_string += "import "+re.sub(',', ', ', my_importfiles)+';\nglobal mainprefix;\n';
1494 statement_string += "def sdt_exec_code():\n\t"+re.sub(r'\\n', "\n\t", re.sub(r'\\t', '\t', script_input));
1495 # Compile and execute code in a limited namespace
1496 user_code = compile(statement_string+"\nsdt_export_result = sdt_exec_code()\n", '<user code>', 'exec');
1497 userdict = {'sdt_export_result' : None};
1498 userdict['mainprefix'] = prefix;
1499 argvlist = [];
1500 argvlist.append(os.getpid());
1501 for value in execute_args.split():
1502 argvlist.append(value);
1503 userdict['argv'] = argvlist;
1504 # Execute code
1505 exec(user_code, userdict);
1506 # Use result
1507 b = None;
1508 if userdict['sdt_export_result']:
1509 b = userdict['sdt_export_result'];
1510 else:
1511 b = None;
1512 print("Error: '"+filename+"' did not deliver any result", file=sys.stderr);
1513 exit(1);
1514 if input_start > 0:
1515 b = b[input_start:];
1516 if input_length > 0:
1517 b = b[0:input_length];
1518 filehash.update(bytes(b, encoding='utf8'));
1519 if options.printtextdump: # For debugging commands
1520 print(str(b));
1521 # Use system variables and commands
1522 # ${ENV} environment variables
1523 if arg_is_env(filename):
1524 current_var = not_allowed_chars.sub(" ", arg_is_env(filename));
1525 if print_verbose:
1526 print("# echo $"+ current_var, file=current_outfile);
1527 # Redirect env query to other system
1528 if current_var.startswith("ssh://"):
1529 match = re.search(r"(?i)(ssh://[^/]+/)(.*)$", current_var);
1530 if match != None:
1531 current_ssh = match.group(1);
1532 current_command = r"echo -n ${"+match.group(2)+"}";
1533 filename = '$('+current_ssh+current_command+')';
1534 # Current system
1535 else:
1536 b = os.environ[current_var];
1537 if input_start > 0:
1538 b = b[input_start:];
1539 if input_length > 0:
1540 b = b[0:input_length];
1541 filehash.update(bytes(b, encoding='utf8'));
1542 if options.printtextdump: # For debugging commands
1543 print(str(b));
1544 # Commands $(command)
1545 if arg_is_shell_command(filename):
1546 if not execute :
1547 error_message = "Executable argument \'"+filename+"\' only allowed with the --execute flag";
1548 print (error_message, file=sys.stderr);
1549 exit(0);
1550 # Clean up command
1551 current_command = not_allowed_chars.sub(" ", arg_is_shell_command(filename));
1552 # Expand remote ssh://
1553 current_host = None;
1554 current_executable = None;
1555 current_args = execute_args;
1556 if current_command.startswith("ssh://"):
1557 match = re.search(r"(?i)ssh://([^/]+)/(.*)$", current_command);
1558 if match != None:
1559 current_host = match.group(1);
1560 current_command = match.group(2);
1561 # Protect $, ", and backslashes
1562 current_command = re.sub(r'\\', r'\\', current_command);
1563 current_command = re.sub(r'\$', r'\$', current_command);
1564 current_executable = "/usr/bin/ssh";
1565 current_args = re.sub(r'\$', r'\$', current_args);
1566 current_args = re.sub(r'\"', r'\"', current_args);
1567 # Create command line
1568 current_command_line = user_change+"bash --restricted -c \'"+current_command+"\' "+str(os.getpid())+" "+current_args;
1569 # Add ssh command
1570 if current_executable:
1571 current_command_line = re.sub(r'\"', r'\\"', current_command_line);
1572 current_command_line = current_executable+' '+current_host+' "'+current_command_line+'"';
1573 # Print command
1574 if print_verbose:
1575 print("#", current_command_line, file=current_outfile);
1576 # Spawn command and open a pipe to the output
1577 pipe = subprocess.Popen(current_command_line, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE);
1578 if input_start > 0:
1579 pipe.stdout.read(input_start);
1580 bytes_processed = 0;
1581 for b in pipe.stdout:
1582 if input_length > 0:
1583 if bytes_processed >= input_length:
1584 break;
1585 elif bytes_processed + len(b) <= input_length:
1586 bytes_processed += len(b);
1587 else:
1588 b = b[0:(input_length - bytes_processed)];
1589 bytes_processed = input_length;
1590 if type(b).__name__ == 'str':
1591 b = bytes(b, encoding='utf8');
1592 filehash.update(b);
1593 if options.printtextdump: # For debugging commands
1594 print(str(b));
1595 # See whether there was an error
1596 pipe.wait();
1597 if pipe.returncode:
1598 error_message = pipe.stderr.read();
1599 print('$('+current_command+')', "\n", str(error_message, encoding='UTF8'), file=sys.stderr);
1600 exit(pipe.returncode);
1601 # stat() meta information
1602 if arg_is_stat(filename):
1603 if not os.path.exists(arg_is_stat(filename)):
1604 print(filename, "does not exist", file=sys.stderr)
1605 quit();
1606 filestat = os.stat(arg_is_stat(filename));
1607 if my_statusvalues == "": my_statusvalues = 'fmidlugs'
1608 b = "";
1609 if 'f' in my_statusvalues:
1610 b += 'stat('+filename.lstrip('?')+') = '
1611 b += '[';
1612 if 'm' in my_statusvalues:
1613 b += 'st_mode='+str(oct(filestat.st_mode))+', ';
1614 if 'i' in my_statusvalues:
1615 b += 'st_ino='+str(filestat.st_ino)+', ';
1616 if 'd' in my_statusvalues:
1617 b += 'st_dev='+str(filestat.st_dev)+', '
1618 if 'l' in my_statusvalues:
1619 b += 'st_nlink='+str(filestat.st_nlink)+', '
1620 if 'u' in my_statusvalues:
1621 b += 'st_uid='+str(filestat.st_uid)+', '
1622 if 'g' in my_statusvalues:
1623 b += 'st_gid='+str(filestat.st_gid)+', '
1624 if 's' in my_statusvalues:
1625 b += 'st_size='+str(filestat.st_size)+', '
1626 if 'a' in my_statusvalues:
1627 b += 'st_atime='+str(filestat.st_atime)+', '
1628 if 't' in my_statusvalues:
1629 b += 'st_mtime='+str(filestat.st_mtime)+', '
1630 if 'c' in my_statusvalues:
1631 b += 'st_ctime='+str(filestat.st_ctime);
1633 b = b.rstrip(', ') + ']';
1634 # Not sure whether this makes sense at all
1635 if input_start > 0:
1636 b = b[input_start:];
1637 if input_length > 0:
1638 b = b[0:input_length];
1639 filehash.update(bytes(b, encoding='utf8'));
1640 if print_verbose:
1641 print ("# "+ b, file=current_outfile);
1642 if options.printtextdump: # For debugging commands
1643 print(str(b));
1645 # STDIN
1646 if arg_is_stdin(filename):
1647 total_bytes = 0;
1648 if input_start > 0:
1649 sys.stdin.buffer.read(input_start);
1650 bytes_processed = 0;
1651 for b in sys.stdin.buffer:
1652 if input_length > 0:
1653 if bytes_processed >= input_length:
1654 break;
1655 elif bytes_processed + len(b) <= input_length:
1656 bytes_processed += len(b);
1657 else:
1658 b = b[0:(input_length - bytes_processed)];
1659 bytes_processed = input_length;
1660 total_bytes += len(b);
1661 if type(b).__name__ == 'str':
1662 b = bytes(b, encoding='utf8');
1663 filehash.update(b);
1664 if options.printtextdump: # For debugging commands
1665 print(str(b));
1666 if total_bytes == 0:
1667 print("ERROR: No bytes read from STDIN. Processing aborted.", file=sys.stderr);
1668 quit(1);
1670 # Use plain file
1671 if arg_is_plain_file(filename):
1672 # Open and read the file
1673 with open_infile(filename, 'rb') as file:
1674 if input_start > 0:
1675 file.seek(input_start);
1676 bytes_processed = 0;
1677 for b in file:
1678 if input_length > 0:
1679 if bytes_processed >= input_length:
1680 break;
1681 elif bytes_processed + len(b) <= input_length:
1682 bytes_processed += len(b);
1683 else:
1684 b = b[0:(input_length - bytes_processed)];
1685 bytes_processed = input_length;
1686 if type(b).__name__ == 'str':
1687 b = bytes(b, encoding='utf8');
1688 filehash.update(b);
1689 if options.printtextdump: # For debugging commands
1690 print(str(b));
1692 # Print the signature of the current argument
1693 current_digest = filehash.hexdigest();
1694 print_name = org_filename;
1695 if my_quiet or arg_is_hidden(org_filename):
1696 file_argnum += 1;
1697 print_name = '['+str(file_argnum)+']';
1698 current_hash_line = current_digest+" *"+print_name
1699 # Add current signature to total signature (including the argument!)
1700 totalhash.update(bytes(current_hash_line, encoding='ascii'));
1702 # Be careful to use this ONLY after totalhash has been updated!
1703 if total_only:
1704 current_hash_line = (len(current_digest)*'-')+" *"+print_name;
1706 # Write output
1707 if not my_check:
1708 if not (my_quiet and total_only) and not (my_allsalts and snum > 1):
1709 print(current_hash_line, file=current_outfile);
1710 elif not (my_quiet or my_allsalts):
1711 if check_hashes[print_name] == (len(current_digest)*'-'):
1712 # Suppress redundant output of empty, ----, lines
1713 if snum <= 1 and pnum <= 1:
1714 print(check_hashes[print_name]+" *"+print_name, file=current_outfile);
1715 elif current_digest != check_hashes[print_name]:
1716 print("FAILED: "+current_hash_line, file=current_outfile);
1717 else:
1718 print("ok"+" *"+print_name, file=current_outfile);
1720 # Handle total hash
1721 current_total_digest = totalhash.hexdigest();
1722 # Write (in)correct salts with the TOTAL HASH
1723 if my_allsalts:
1724 output_salt = my_salt;
1725 j = random.random();
1726 # Randomly create an incorrect salt for failed output
1727 if not my_check:
1728 if j < fail_fraction and snum != selected_salt:
1729 salt = dev_random.read(8);
1730 output_salt = str(binascii.hexlify(salt), 'ascii');
1731 else:
1732 salt_pattern_number += current_salt_power;
1733 current_total_digest_line = "Salt+TOTAL HASH: '"+output_salt+"' '"+current_total_digest+"'";
1734 else: # Standard TOTAL HASH line
1735 current_total_digest_line = current_total_digest+" *"+"TOTAL HASH";
1736 end_time = time.time();
1737 print("# \n# Total hash - Time to completion:", end_time - start_time, "seconds", file=current_outfile);
1738 total_hash_num = 0;
1739 if my_allsalts: total_hash_num = snum-1; # Current TOTAL HASH number of more are used
1740 if not my_check:
1741 print(current_total_digest_line+"\n", file=current_outfile);
1742 elif current_total_digest != total_hash_list[total_hash_num]:
1743 if not my_allsalts: print("FAILED: "+current_total_digest_line+"\n", file=current_outfile);
1744 else:
1745 if my_allsalts: salt_pattern_number += current_salt_power; # Update salt bit pattern
1746 match_number = "";
1747 if len(passphrase_list) > 1 or len(salt_list): match_number = " #"
1748 if len(passphrase_list) > 1: match_number += " passphrase no: "+str(pnum);
1749 if len(salt_list) > 1: match_number += " salt no: "+str(snum);
1750 if not my_allsalts: print("OK"+" *"+"TOTAL HASH"+match_number+"\n", file=current_outfile);
1751 corrsnum = snum;
1752 corrpnum = pnum;
1753 snum += 1;
1754 if my_allsalts: current_salt_power *= 2; # Update current bit position in salt pattern
1755 if my_check and corrpnum == pnum: matched_salt_pattern = salt_pattern_number;
1756 pnum += 1;
1758 if my_check and len(passphrase_list) > 1:
1759 if corrpnum > 0:
1760 print("Passphrase entry:",corrpnum,"matched", file=current_outfile);
1761 else:
1762 print("No passphrase entry matched!", file=current_outfile);
1763 if my_check and (not my_allsalts) and len(salt_list) > 1:
1764 if corrpnum > 0:
1765 if corrsnum > 0:
1766 print("Salt entry:",corrsnum,"matched", file=current_outfile);
1767 else:
1768 print("No salt entry matched!", file=current_outfile);
1769 else:
1770 print("No entry matched", file=current_outfile);
1771 # Print salt bit patterns
1772 elif my_check and my_allsalts:
1773 print("Salt pattern number:", matched_salt_pattern, file=current_outfile);
1774 elif not my_check and my_allsalts:
1775 print("# Salt pattern number:", salt_pattern_number, file=current_private);
1777 # Close output files if necessary
1778 if my_output and my_output != '-':
1779 current_outfile.close();
1780 if my_private and my_private != '-':
1781 current_private.close();