1#!/usr/bin/env gpgscm
2
3;; Copyright (C) 2016 g10 Code GmbH
4;;
5;; This file is part of GnuPG.
6;;
7;; GnuPG is free software; you can redistribute it and/or modify
8;; it under the terms of the GNU General Public License as published by
9;; the Free Software Foundation; either version 3 of the License, or
10;; (at your option) any later version.
11;;
12;; GnuPG is distributed in the hope that it will be useful,
13;; but WITHOUT ANY WARRANTY; without even the implied warranty of
14;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15;; GNU General Public License for more details.
16;;
17;; You should have received a copy of the GNU General Public License
18;; along with this program; if not, see <http://www.gnu.org/licenses/>.
19
20(load (in-srcdir "tests" "openpgp" "defs.scm"))
21(load (with-path "time.scm"))
22(setup-environment)
23
24(define GPGTIME 1480943782)
25
26;; Generate a --faked-system-time parameter for a particular offset.
27(define (faketime delta)
28  (string-append "--faked-system-time=" (number->string (+ GPGTIME delta))))
29
30;; Redefine GPG without --always-trust and a fixed time.
31(define GPG `(,(tool 'gpg) --no-permission-warning ,(faketime 0)))
32
33(catch (skip "Tofu not supported")
34       (call-check `(,@GPG --trust-model=tofu --list-config)))
35
36(let ((trust-model (gpg-config 'gpg "trust-model")))
37  (trust-model::update "tofu"))
38
39(define KEYS '("1C005AF3" "BE04EB2B" "B662E42F"))
40
41;; Import the test keys.
42(for-each (lambda (keyid)
43            (call-check `(,@GPG --import
44                                ,(in-srcdir "tests" "openpgp" "tofu" "conflicting"
45                                            (string-append keyid ".gpg"))))
46	    (catch (fail "Missing key" keyid)
47		   (call-check `(,@GPG --list-keys ,keyid))))
48	  KEYS)
49
50;; Get tofu policy for KEYID.  Any remaining arguments are simply
51;; passed to GPG.
52;;
53;; This function only supports keys with a single user id.
54(define (getpolicy keyid . args)
55  (let ((policy
56	 (list-ref (assoc "tfs" (gpg-with-colons
57				 `(--with-tofu-info
58				   ,@args
59				   --list-keys ,keyid))) 5)))
60    (unless (member policy '("auto" "good" "unknown" "bad" "ask"))
61	    (fail "Bad policy:" policy))
62    policy))
63
64;; Check that KEYID's tofu policy matches EXPECTED-POLICY.  Any
65;; remaining arguments are simply passed to GPG.
66;;
67;; This function only supports keys with a single user id.
68(define (checkpolicy keyid expected-policy . args)
69  (let ((policy (apply getpolicy `(,keyid ,@args))))
70    (unless (string=? policy expected-policy)
71	    (fail keyid ": Expected policy to be" expected-policy
72		   "but got" policy))))
73
74;; Set key KEYID's policy to POLICY.  Any remaining arguments are
75;; passed as options to gpg.
76(define (setpolicy keyid policy . args)
77  (call-check `(,@GPG ,@args
78		      --tofu-policy ,policy ,keyid)))
79
80(info "Checking tofu policies and trust...")
81
82;; Carefully remove the TOFU db.
83(catch '() (unlink (path-join GNUPGHOME "tofu.db")))
84
85;; Verify a message.  There should be no conflict and the trust
86;; policy should be set to auto.
87(call-check `(,@GPG --verify ,(in-srcdir "tests" "openpgp" "tofu" "conflicting" "1C005AF3-1.txt")))
88
89(checkpolicy "1C005AF3" "auto")
90;; Check default trust.
91(checktrust "1C005AF3" "m")
92
93;; Trust should be derived lazily.  Thus, if the policy is set to
94;; auto and we change --tofu-default-policy, then the trust should
95;; change as well.  Try it.
96(checktrust "1C005AF3" "f" '--tofu-default-policy=good)
97(checktrust "1C005AF3" "-" '--tofu-default-policy=unknown)
98(checktrust "1C005AF3" "n" '--tofu-default-policy=bad)
99(checktrust "1C005AF3" "q" '--tofu-default-policy=ask)
100
101;; Change the policy to something other than auto and make sure the
102;; policy and the trust are correct.
103(for-each-p
104 "Setting a fixed policy..."
105 (lambda (policy)
106   (let ((expected-trust
107	  (cond
108	   ((string=? "good" policy) "f")
109	   ((string=? "unknown" policy) "-")
110	   (else "n"))))
111     (setpolicy "1C005AF3" policy)
112
113     ;; Since we have a fixed policy, the trust level shouldn't
114     ;; change if we change the default policy.
115     (for-each-p
116      ""
117      (lambda (default-policy)
118	(checkpolicy "1C005AF3" policy
119		     '--tofu-default-policy default-policy)
120	(checktrust "1C005AF3" expected-trust
121		    '--tofu-default-policy default-policy))
122      '("auto" "good" "unknown" "bad" "ask"))))
123 '("good" "unknown" "bad"))
124
125;; At the end, 1C005AF3's policy should be bad.
126(checkpolicy "1C005AF3" "bad")
127
128;; 1C005AF3 and BE04EB2B conflict.  A policy setting of "auto"
129;; (BE04EB2B's state) will result in an effective policy of ask.  But,
130;; a policy setting of "bad" will result in an effective policy of
131;; bad.
132(setpolicy "BE04EB2B" "auto")
133(checkpolicy "BE04EB2B" "ask")
134(checkpolicy "1C005AF3" "bad")
135
136;; 1C005AF3, B662E42F, and BE04EB2B conflict.  We change BE04EB2B's
137;; policy to auto and leave 1C005AF3's policy at bad.  This conflict
138;; should cause BE04EB2B's effective policy to be ask (since it is
139;; auto), but not affect 1C005AF3's policy.
140(setpolicy "BE04EB2B" "auto")
141(checkpolicy "BE04EB2B" "ask")
142(call-check `(,@GPG --verify ,(in-srcdir "tests" "openpgp" "tofu" "conflicting" "B662E42F-1.txt")))
143(checkpolicy "BE04EB2B" "ask")
144(checkpolicy "1C005AF3" "bad")
145(checkpolicy "B662E42F" "ask")
146
147;; Check that the stats are emitted correctly.
148
149(display "Checking TOFU stats...\n")
150
151(define (check-counts keyid expected-sigs expected-sig-days
152                      expected-encs expected-enc-days . args)
153  (let*
154      ((tfs (assoc "tfs"
155                   (gpg-with-colons
156                    `(--with-tofu-info ,@args --list-keys ,keyid))))
157       (sigs (string->number (list-ref tfs 3)))
158       (sig-days (string->number (list-ref tfs 11)))
159       (encs (string->number (list-ref tfs 4)))
160       (enc-days (string->number (list-ref tfs 12)))
161       )
162    ; (display keyid) (display ": ") (display tfs) (display "\n")
163    (unless (= sigs expected-sigs)
164            (fail keyid ": # signatures (" sigs ") does not match expected"
165                   "# signatures (" expected-sigs ").\n"))
166    (unless (= sig-days expected-sig-days)
167            (fail keyid ": # signature days (" sig-days ")"
168                  "does not match expected"
169                  "# signature days (" expected-sig-days ").\n"))
170    (unless (= encs expected-encs)
171            (fail keyid ": # encryptions (" encs ") does not match expected"
172                   "# encryptions (" expected-encs ").\n"))
173    (unless (= enc-days expected-enc-days)
174            (fail keyid ": # encryption days (" encs ")"
175                  "does not match expected"
176                  "# encryption days (" expected-enc-days ").\n"))
177    ))
178
179;; Carefully remove the TOFU db.
180(catch '() (unlink (path-join GNUPGHOME "tofu.db")))
181
182(check-counts "1C005AF3" 0 0 0 0)
183(check-counts "BE04EB2B" 0 0 0 0)
184(check-counts "B662E42F" 0 0 0 0)
185
186;; Verify a message.  The signature count should increase by 1.
187(call-check `(,@GPG --verify ,(in-srcdir "tests" "openpgp" "tofu" "conflicting" "1C005AF3-1.txt")))
188
189(check-counts "1C005AF3" 1 1 0 0)
190
191;; Verify the same message.  The signature count should remain the
192;; same.
193(call-check `(,@GPG --verify ,(in-srcdir "tests" "openpgp" "tofu" "conflicting" "1C005AF3-1.txt")))
194(check-counts "1C005AF3" 1 1 0 0)
195
196;; Verify another message.
197(call-check `(,@GPG --verify ,(in-srcdir "tests" "openpgp" "tofu" "conflicting" "1C005AF3-2.txt")))
198(check-counts "1C005AF3" 2 1 0 0)
199
200;; Verify another message.
201(call-check `(,@GPG --verify ,(in-srcdir "tests" "openpgp" "tofu" "conflicting" "1C005AF3-3.txt")))
202(check-counts "1C005AF3" 3 1 0 0)
203
204;; Verify a message from a different sender.  The signature count
205;; should increase by 1 for that key.
206(call-check `(,@GPG --verify ,(in-srcdir "tests" "openpgp" "tofu" "conflicting" "BE04EB2B-1.txt")))
207(check-counts "1C005AF3" 3 1 0 0)
208(check-counts "BE04EB2B" 1 1 0 0)
209(check-counts "B662E42F" 0 0 0 0)
210
211;; Verify another message on a new day.  (Recall: we are interested in
212;; when the message was first verified, not when the signer claimed
213;; that it was signed.)
214(call-check `(,@GPG ,(faketime (days->seconds 2))
215		    --verify ,(in-srcdir "tests" "openpgp" "tofu" "conflicting" "1C005AF3-4.txt")))
216(check-counts "1C005AF3" 4 2 0 0)
217(check-counts "BE04EB2B" 1 1 0 0)
218(check-counts "B662E42F" 0 0 0 0)
219
220;; And another.
221(call-check `(,@GPG ,(faketime (days->seconds 2))
222		    --verify ,(in-srcdir "tests" "openpgp" "tofu" "conflicting" "1C005AF3-5.txt")))
223(check-counts "1C005AF3" 5 2 0 0)
224(check-counts "BE04EB2B" 1 1 0 0)
225(check-counts "B662E42F" 0 0 0 0)
226
227;; Another, but for a different key.
228(call-check `(,@GPG ,(faketime (days->seconds 2))
229		    --verify ,(in-srcdir "tests" "openpgp" "tofu" "conflicting" "BE04EB2B-2.txt")))
230(check-counts "1C005AF3" 5 2 0 0)
231(check-counts "BE04EB2B" 2 2 0 0)
232(check-counts "B662E42F" 0 0 0 0)
233
234;; And add a third day.
235(call-check `(,@GPG ,(faketime (days->seconds 4))
236		    --verify ,(in-srcdir "tests" "openpgp" "tofu" "conflicting" "BE04EB2B-3.txt")))
237(check-counts "1C005AF3" 5 2 0 0)
238(check-counts "BE04EB2B" 3 3 0 0)
239(check-counts "B662E42F" 0 0 0 0)
240
241(call-check `(,@GPG ,(faketime (days->seconds 4))
242		    --verify ,(in-srcdir "tests" "openpgp" "tofu" "conflicting" "BE04EB2B-4.txt")))
243(check-counts "1C005AF3" 5 2 0 0)
244(check-counts "BE04EB2B" 4 3 0 0)
245(check-counts "B662E42F" 0 0 0 0)
246
247;; Check that we detect the following attack:
248;;
249;; Alice and Bob each have a key and cross sign them.  Bob then adds a
250;; new user id, "Alice".  TOFU should now detect a conflict, because
251;; Alice only signed Bob's "Bob" user id.
252
253(display "Checking cross sigs...\n")
254(define GPG `(,(tool 'gpg) --no-permission-warning
255	      --faked-system-time=1476304861))
256
257;; Carefully remove the TOFU db.
258(catch '() (unlink (path-join GNUPGHOME "tofu.db")))
259
260(define DIR "tofu/cross-sigs")
261;; The test keys.
262(define KEYA "1938C3A0E4674B6C217AC0B987DB2814EC38277E")
263(define KEYB "DC463A16E42F03240D76E8BA8B48C6BD871C2247")
264(define KEYIDA (substring KEYA (- (string-length KEYA) 8)))
265(define KEYIDB (substring KEYB (- (string-length KEYB) 8)))
266
267(define (verify-messages)
268  (for-each
269   (lambda (key)
270     (for-each
271      (lambda (i)
272        (let ((fn (in-srcdir "tests" "openpgp" DIR (string-append key "-" i ".txt"))))
273          (call-check `(,@GPG --verify ,fn))))
274      (list "1" "2")))
275   (list KEYIDA KEYIDB)))
276
277;; Import the public keys.
278(display "    > Two keys. ")
279(call-check `(,@GPG --import ,(in-srcdir "tests" "openpgp" DIR (string-append KEYIDA "-1.gpg"))))
280(call-check `(,@GPG --import ,(in-srcdir "tests" "openpgp" DIR (string-append KEYIDB "-1.gpg"))))
281;; Make sure the tofu engine registers the keys.
282(verify-messages)
283(display "<\n")
284
285;; Since there is no conflict, the policy should be auto.
286(checkpolicy KEYA "auto")
287(checkpolicy KEYB "auto")
288
289;; Import the cross sigs.
290(display "    > Adding cross signatures. ")
291(call-check `(,@GPG --import ,(in-srcdir "tests" "openpgp" DIR (string-append KEYIDA "-2.gpg"))))
292(call-check `(,@GPG --import ,(in-srcdir "tests" "openpgp" DIR (string-append KEYIDB "-2.gpg"))))
293(verify-messages)
294(display "<\n")
295
296;; There is still no conflict, so the policy shouldn't have changed.
297(checkpolicy KEYA "auto")
298(checkpolicy KEYB "auto")
299
300;; Import the conflicting user id.
301(display "    > Adding conflicting user id. ")
302(call-check `(,@GPG --import ,(in-srcdir "tests" "openpgp" DIR (string-append KEYIDB "-3.gpg"))))
303(verify-messages)
304(display "<\n")
305
306(checkpolicy KEYA "ask")
307(checkpolicy KEYB "ask")
308
309;; Import Alice's signature on the conflicting user id.  Since there
310;; is now a cross signature, we should revert to the default policy.
311(display "    > Adding cross signature on user id. ")
312(call-check `(,@GPG --import ,(in-srcdir "tests" "openpgp" DIR (string-append KEYIDB "-4.gpg"))))
313(verify-messages)
314(display "<\n")
315
316(checkpolicy KEYA "auto")
317(checkpolicy KEYB "auto")
318
319;; Remove the keys.
320(call-check `(,@GPG --delete-key ,KEYA))
321(call-check `(,@GPG --delete-key ,KEYB))
322
323
324;; Check that we detect the following attack:
325;;
326;; Alice has an ultimately trusted key and she signs Bob's key.  Then
327;; Bob adds a new user id, "Alice".  TOFU should now detect a
328;; conflict, because Alice only signed Bob's "Bob" user id.
329;;
330;;
331;; The Alice key:
332;;   pub   rsa2048 2016-10-11 [SC]
333;;         1938C3A0E4674B6C217AC0B987DB2814EC38277E
334;;   uid           [ultimate] Spy Cow <spy@cow.com>
335;;   sub   rsa2048 2016-10-11 [E]
336;;
337;; The Bob key:
338;;
339;;   pub   rsa2048 2016-10-11 [SC]
340;;         DC463A16E42F03240D76E8BA8B48C6BD871C2247
341;;   uid           [  full  ] Spy R. Cow <spy@cow.com>
342;;   uid           [  full  ] Spy R. Cow <spy@cow.de>
343;;   sub   rsa2048 2016-10-11 [E]
344
345(display "Checking UTK sigs...\n")
346(define GPG `(,(tool 'gpg) --no-permission-warning
347	      --faked-system-time=1476304861))
348
349;; Carefully remove the TOFU db.
350(catch '() (unlink (path-join GNUPGHOME "tofu.db")))
351
352(define DIR "tofu/cross-sigs")
353;; The test keys.
354(define KEYA "1938C3A0E4674B6C217AC0B987DB2814EC38277E")
355(define KEYB "DC463A16E42F03240D76E8BA8B48C6BD871C2247")
356(define KEYIDA (substring KEYA (- (string-length KEYA) 8)))
357(define KEYIDB (substring KEYB (- (string-length KEYB) 8)))
358
359(define (verify-messages)
360  (for-each
361   (lambda (key)
362     (for-each
363      (lambda (i)
364        (let ((fn (in-srcdir "tests" "openpgp" DIR (string-append key "-" i ".txt"))))
365          (call-check `(,@GPG --verify ,fn))))
366      (list "1" "2")))
367   (list KEYIDA KEYIDB)))
368
369;; Import the public keys.
370(display "    > Two keys. ")
371(call-check `(,@GPG --import ,(in-srcdir "tests" "openpgp" DIR (string-append KEYIDA "-1.gpg"))))
372(call-check `(,@GPG --import ,(in-srcdir "tests" "openpgp" DIR (string-append KEYIDB "-1.gpg"))))
373(display "<\n")
374
375(checkpolicy KEYA "auto")
376(checkpolicy KEYB "auto")
377
378;; Import the cross sigs.
379(display "    > Adding cross signatures. ")
380(call-check `(,@GPG --import ,(in-srcdir "tests" "openpgp" DIR (string-append KEYIDA "-2.gpg"))))
381(call-check `(,@GPG --import ,(in-srcdir "tests" "openpgp" DIR (string-append KEYIDB "-2.gpg"))))
382(display "<\n")
383
384(checkpolicy KEYA "auto")
385(checkpolicy KEYB "auto")
386
387;; Make KEYA ultimately trusted.
388(display (string-append "    > Marking " KEYA " as ultimately trusted. "))
389(pipe:do
390 (pipe:echo (string-append KEYA ":6:\n"))
391 (pipe:gpg `(--import-ownertrust)))
392(display "<\n")
393
394;; An ultimately trusted key's policy is good.
395(checkpolicy KEYA "good")
396;; A key signed by a UTK for which there is no policy gets the default
397;; policy of good.
398(checkpolicy KEYB "good")
399
400;; Import the conflicting user id.
401(display "    > Adding conflicting user id. ")
402(call-check `(,@GPG --import ,(in-srcdir "tests" "openpgp" DIR (string-append KEYIDB "-3.gpg"))))
403(verify-messages)
404(display "<\n")
405
406(checkpolicy KEYA "good")
407(checkpolicy KEYB "ask")
408
409;; Import Alice's signature on the conflicting user id.
410(display "    > Adding cross signature on user id. ")
411(call-check `(,@GPG --import ,(in-srcdir "tests" "openpgp" DIR (string-append KEYIDB "-4.gpg"))))
412(verify-messages)
413(display "<\n")
414
415(checkpolicy KEYA "good")
416(checkpolicy KEYB "good")
417
418;; Remove the keys.
419(call-check `(,@GPG --delete-key ,KEYA))
420(call-check `(,@GPG --delete-key ,KEYB))
421