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