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