summaryrefslogtreecommitdiffstats
path: root/tests/openpgp/tofu
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 16:14:06 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 16:14:06 +0000
commiteee068778cb28ecf3c14e1bf843a95547d72c42d (patch)
tree0e07b30ddc5ea579d682d5dbe57998200d1c9ab7 /tests/openpgp/tofu
parentInitial commit. (diff)
downloadgnupg2-eee068778cb28ecf3c14e1bf843a95547d72c42d.tar.xz
gnupg2-eee068778cb28ecf3c14e1bf843a95547d72c42d.zip
Adding upstream version 2.2.40.upstream/2.2.40upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rwxr-xr-xtests/openpgp/tofu.scm420
-rw-r--r--tests/openpgp/tofu/conflicting/1C005AF3-1.txtbin0 -> 342 bytes
-rw-r--r--tests/openpgp/tofu/conflicting/1C005AF3-2.txtbin0 -> 338 bytes
-rw-r--r--tests/openpgp/tofu/conflicting/1C005AF3-3.txtbin0 -> 339 bytes
-rw-r--r--tests/openpgp/tofu/conflicting/1C005AF3-4.txtbin0 -> 338 bytes
-rw-r--r--tests/openpgp/tofu/conflicting/1C005AF3-5.txtbin0 -> 339 bytes
-rw-r--r--tests/openpgp/tofu/conflicting/1C005AF3-secret.gpgbin0 -> 2537 bytes
-rw-r--r--tests/openpgp/tofu/conflicting/1C005AF3.gpgbin0 -> 1235 bytes
-rw-r--r--tests/openpgp/tofu/conflicting/B662E42F-1.txtbin0 -> 340 bytes
-rw-r--r--tests/openpgp/tofu/conflicting/B662E42F-2.txtbin0 -> 339 bytes
-rw-r--r--tests/openpgp/tofu/conflicting/B662E42F-3.txtbin0 -> 342 bytes
-rw-r--r--tests/openpgp/tofu/conflicting/B662E42F-4.txtbin0 -> 340 bytes
-rw-r--r--tests/openpgp/tofu/conflicting/B662E42F-5.txt1
-rw-r--r--tests/openpgp/tofu/conflicting/B662E42F-secret.gpgbin0 -> 2537 bytes
-rw-r--r--tests/openpgp/tofu/conflicting/B662E42F.gpgbin0 -> 1235 bytes
-rw-r--r--tests/openpgp/tofu/conflicting/BE04EB2B-1.txtbin0 -> 340 bytes
-rw-r--r--tests/openpgp/tofu/conflicting/BE04EB2B-2.txtbin0 -> 342 bytes
-rw-r--r--tests/openpgp/tofu/conflicting/BE04EB2B-3.txtbin0 -> 340 bytes
-rw-r--r--tests/openpgp/tofu/conflicting/BE04EB2B-4.txtbin0 -> 342 bytes
-rw-r--r--tests/openpgp/tofu/conflicting/BE04EB2B-5.txtbin0 -> 340 bytes
-rw-r--r--tests/openpgp/tofu/conflicting/BE04EB2B-secret.gpgbin0 -> 2537 bytes
-rw-r--r--tests/openpgp/tofu/conflicting/BE04EB2B.gpgbin0 -> 1235 bytes
-rw-r--r--tests/openpgp/tofu/cross-sigs/871C2247-1.gpgbin0 -> 1173 bytes
-rw-r--r--tests/openpgp/tofu/cross-sigs/871C2247-1.txtbin0 -> 321 bytes
-rw-r--r--tests/openpgp/tofu/cross-sigs/871C2247-2.gpgbin0 -> 1460 bytes
-rw-r--r--tests/openpgp/tofu/cross-sigs/871C2247-2.txtbin0 -> 333 bytes
-rw-r--r--tests/openpgp/tofu/cross-sigs/871C2247-3.gpgbin0 -> 1800 bytes
-rw-r--r--tests/openpgp/tofu/cross-sigs/871C2247-3.txtbin0 -> 334 bytes
-rw-r--r--tests/openpgp/tofu/cross-sigs/871C2247-4.gpgbin0 -> 2087 bytes
-rw-r--r--tests/openpgp/tofu/cross-sigs/871C2247-secret.gpgbin0 -> 2475 bytes
-rw-r--r--tests/openpgp/tofu/cross-sigs/EC38277E-1.gpgbin0 -> 1171 bytes
-rw-r--r--tests/openpgp/tofu/cross-sigs/EC38277E-1.txtbin0 -> 321 bytes
-rw-r--r--tests/openpgp/tofu/cross-sigs/EC38277E-2.gpgbin0 -> 1458 bytes
-rw-r--r--tests/openpgp/tofu/cross-sigs/EC38277E-2.txtbin0 -> 334 bytes
-rw-r--r--tests/openpgp/tofu/cross-sigs/EC38277E-3.txtbin0 -> 334 bytes
-rw-r--r--tests/openpgp/tofu/cross-sigs/EC38277E-secret.gpgbin0 -> 2473 bytes
-rw-r--r--tests/openpgp/tofu/cross-sigs/README79
37 files changed, 500 insertions, 0 deletions
diff --git a/tests/openpgp/tofu.scm b/tests/openpgp/tofu.scm
new file mode 100755
index 0000000..cd4b4c7
--- /dev/null
+++ b/tests/openpgp/tofu.scm
@@ -0,0 +1,420 @@
+#!/usr/bin/env gpgscm
+
+;; Copyright (C) 2016 g10 Code GmbH
+;;
+;; This file is part of GnuPG.
+;;
+;; GnuPG is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation; either version 3 of the License, or
+;; (at your option) any later version.
+;;
+;; GnuPG is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with this program; if not, see <http://www.gnu.org/licenses/>.
+
+(load (in-srcdir "tests" "openpgp" "defs.scm"))
+(load (with-path "time.scm"))
+(setup-environment)
+
+(define GPGTIME 1480943782)
+
+;; Generate a --faked-system-time parameter for a particular offset.
+(define (faketime delta)
+ (string-append "--faked-system-time=" (number->string (+ GPGTIME delta))))
+
+;; Redefine GPG without --always-trust and a fixed time.
+(define GPG `(,(tool 'gpg) --no-permission-warning ,(faketime 0)))
+
+(catch (skip "Tofu not supported")
+ (call-check `(,@GPG --trust-model=tofu --list-config)))
+
+(let ((trust-model (gpg-config 'gpg "trust-model")))
+ (trust-model::update "tofu"))
+
+(define KEYS '("1C005AF3" "BE04EB2B" "B662E42F"))
+
+;; Import the test keys.
+(for-each (lambda (keyid)
+ (call-check `(,@GPG --import
+ ,(in-srcdir "tests" "openpgp" "tofu" "conflicting"
+ (string-append keyid ".gpg"))))
+ (catch (fail "Missing key" keyid)
+ (call-check `(,@GPG --list-keys ,keyid))))
+ KEYS)
+
+;; Get tofu policy for KEYID. Any remaining arguments are simply
+;; passed to GPG.
+;;
+;; This function only supports keys with a single user id.
+(define (getpolicy keyid . args)
+ (let ((policy
+ (list-ref (assoc "tfs" (gpg-with-colons
+ `(--with-tofu-info
+ ,@args
+ --list-keys ,keyid))) 5)))
+ (unless (member policy '("auto" "good" "unknown" "bad" "ask"))
+ (fail "Bad policy:" policy))
+ policy))
+
+;; Check that KEYID's tofu policy matches EXPECTED-POLICY. Any
+;; remaining arguments are simply passed to GPG.
+;;
+;; This function only supports keys with a single user id.
+(define (checkpolicy keyid expected-policy . args)
+ (let ((policy (apply getpolicy `(,keyid ,@args))))
+ (unless (string=? policy expected-policy)
+ (fail keyid ": Expected policy to be" expected-policy
+ "but got" policy))))
+
+;; Set key KEYID's policy to POLICY. Any remaining arguments are
+;; passed as options to gpg.
+(define (setpolicy keyid policy . args)
+ (call-check `(,@GPG ,@args
+ --tofu-policy ,policy ,keyid)))
+
+(info "Checking tofu policies and trust...")
+
+;; Carefully remove the TOFU db.
+(catch '() (unlink (path-join GNUPGHOME "tofu.db")))
+
+;; Verify a message. There should be no conflict and the trust
+;; policy should be set to auto.
+(call-check `(,@GPG --verify ,(in-srcdir "tests" "openpgp" "tofu" "conflicting" "1C005AF3-1.txt")))
+
+(checkpolicy "1C005AF3" "auto")
+;; Check default trust.
+(checktrust "1C005AF3" "m")
+
+;; Trust should be derived lazily. Thus, if the policy is set to
+;; auto and we change --tofu-default-policy, then the trust should
+;; change as well. Try it.
+(checktrust "1C005AF3" "f" '--tofu-default-policy=good)
+(checktrust "1C005AF3" "-" '--tofu-default-policy=unknown)
+(checktrust "1C005AF3" "n" '--tofu-default-policy=bad)
+(checktrust "1C005AF3" "q" '--tofu-default-policy=ask)
+
+;; Change the policy to something other than auto and make sure the
+;; policy and the trust are correct.
+(for-each-p
+ "Setting a fixed policy..."
+ (lambda (policy)
+ (let ((expected-trust
+ (cond
+ ((string=? "good" policy) "f")
+ ((string=? "unknown" policy) "-")
+ (else "n"))))
+ (setpolicy "1C005AF3" policy)
+
+ ;; Since we have a fixed policy, the trust level shouldn't
+ ;; change if we change the default policy.
+ (for-each-p
+ ""
+ (lambda (default-policy)
+ (checkpolicy "1C005AF3" policy
+ '--tofu-default-policy default-policy)
+ (checktrust "1C005AF3" expected-trust
+ '--tofu-default-policy default-policy))
+ '("auto" "good" "unknown" "bad" "ask"))))
+ '("good" "unknown" "bad"))
+
+;; At the end, 1C005AF3's policy should be bad.
+(checkpolicy "1C005AF3" "bad")
+
+;; 1C005AF3 and BE04EB2B conflict. A policy setting of "auto"
+;; (BE04EB2B's state) will result in an effective policy of ask. But,
+;; a policy setting of "bad" will result in an effective policy of
+;; bad.
+(setpolicy "BE04EB2B" "auto")
+(checkpolicy "BE04EB2B" "ask")
+(checkpolicy "1C005AF3" "bad")
+
+;; 1C005AF3, B662E42F, and BE04EB2B conflict. We change BE04EB2B's
+;; policy to auto and leave 1C005AF3's policy at bad. This conflict
+;; should cause BE04EB2B's effective policy to be ask (since it is
+;; auto), but not affect 1C005AF3's policy.
+(setpolicy "BE04EB2B" "auto")
+(checkpolicy "BE04EB2B" "ask")
+(call-check `(,@GPG --verify ,(in-srcdir "tests" "openpgp" "tofu" "conflicting" "B662E42F-1.txt")))
+(checkpolicy "BE04EB2B" "ask")
+(checkpolicy "1C005AF3" "bad")
+(checkpolicy "B662E42F" "ask")
+
+;; Check that the stats are emitted correctly.
+
+(display "Checking TOFU stats...\n")
+
+(define (check-counts keyid expected-sigs expected-sig-days
+ expected-encs expected-enc-days . args)
+ (let*
+ ((tfs (assoc "tfs"
+ (gpg-with-colons
+ `(--with-tofu-info ,@args --list-keys ,keyid))))
+ (sigs (string->number (list-ref tfs 3)))
+ (sig-days (string->number (list-ref tfs 11)))
+ (encs (string->number (list-ref tfs 4)))
+ (enc-days (string->number (list-ref tfs 12)))
+ )
+ ; (display keyid) (display ": ") (display tfs) (display "\n")
+ (unless (= sigs expected-sigs)
+ (fail keyid ": # signatures (" sigs ") does not match expected"
+ "# signatures (" expected-sigs ").\n"))
+ (unless (= sig-days expected-sig-days)
+ (fail keyid ": # signature days (" sig-days ")"
+ "does not match expected"
+ "# signature days (" expected-sig-days ").\n"))
+ (unless (= encs expected-encs)
+ (fail keyid ": # encryptions (" encs ") does not match expected"
+ "# encryptions (" expected-encs ").\n"))
+ (unless (= enc-days expected-enc-days)
+ (fail keyid ": # encryption days (" encs ")"
+ "does not match expected"
+ "# encryption days (" expected-enc-days ").\n"))
+ ))
+
+;; Carefully remove the TOFU db.
+(catch '() (unlink (path-join GNUPGHOME "tofu.db")))
+
+(check-counts "1C005AF3" 0 0 0 0)
+(check-counts "BE04EB2B" 0 0 0 0)
+(check-counts "B662E42F" 0 0 0 0)
+
+;; Verify a message. The signature count should increase by 1.
+(call-check `(,@GPG --verify ,(in-srcdir "tests" "openpgp" "tofu" "conflicting" "1C005AF3-1.txt")))
+
+(check-counts "1C005AF3" 1 1 0 0)
+
+;; Verify the same message. The signature count should remain the
+;; same.
+(call-check `(,@GPG --verify ,(in-srcdir "tests" "openpgp" "tofu" "conflicting" "1C005AF3-1.txt")))
+(check-counts "1C005AF3" 1 1 0 0)
+
+;; Verify another message.
+(call-check `(,@GPG --verify ,(in-srcdir "tests" "openpgp" "tofu" "conflicting" "1C005AF3-2.txt")))
+(check-counts "1C005AF3" 2 1 0 0)
+
+;; Verify another message.
+(call-check `(,@GPG --verify ,(in-srcdir "tests" "openpgp" "tofu" "conflicting" "1C005AF3-3.txt")))
+(check-counts "1C005AF3" 3 1 0 0)
+
+;; Verify a message from a different sender. The signature count
+;; should increase by 1 for that key.
+(call-check `(,@GPG --verify ,(in-srcdir "tests" "openpgp" "tofu" "conflicting" "BE04EB2B-1.txt")))
+(check-counts "1C005AF3" 3 1 0 0)
+(check-counts "BE04EB2B" 1 1 0 0)
+(check-counts "B662E42F" 0 0 0 0)
+
+;; Verify another message on a new day. (Recall: we are interested in
+;; when the message was first verified, not when the signer claimed
+;; that it was signed.)
+(call-check `(,@GPG ,(faketime (days->seconds 2))
+ --verify ,(in-srcdir "tests" "openpgp" "tofu" "conflicting" "1C005AF3-4.txt")))
+(check-counts "1C005AF3" 4 2 0 0)
+(check-counts "BE04EB2B" 1 1 0 0)
+(check-counts "B662E42F" 0 0 0 0)
+
+;; And another.
+(call-check `(,@GPG ,(faketime (days->seconds 2))
+ --verify ,(in-srcdir "tests" "openpgp" "tofu" "conflicting" "1C005AF3-5.txt")))
+(check-counts "1C005AF3" 5 2 0 0)
+(check-counts "BE04EB2B" 1 1 0 0)
+(check-counts "B662E42F" 0 0 0 0)
+
+;; Another, but for a different key.
+(call-check `(,@GPG ,(faketime (days->seconds 2))
+ --verify ,(in-srcdir "tests" "openpgp" "tofu" "conflicting" "BE04EB2B-2.txt")))
+(check-counts "1C005AF3" 5 2 0 0)
+(check-counts "BE04EB2B" 2 2 0 0)
+(check-counts "B662E42F" 0 0 0 0)
+
+;; And add a third day.
+(call-check `(,@GPG ,(faketime (days->seconds 4))
+ --verify ,(in-srcdir "tests" "openpgp" "tofu" "conflicting" "BE04EB2B-3.txt")))
+(check-counts "1C005AF3" 5 2 0 0)
+(check-counts "BE04EB2B" 3 3 0 0)
+(check-counts "B662E42F" 0 0 0 0)
+
+(call-check `(,@GPG ,(faketime (days->seconds 4))
+ --verify ,(in-srcdir "tests" "openpgp" "tofu" "conflicting" "BE04EB2B-4.txt")))
+(check-counts "1C005AF3" 5 2 0 0)
+(check-counts "BE04EB2B" 4 3 0 0)
+(check-counts "B662E42F" 0 0 0 0)
+
+;; Check that we detect the following attack:
+;;
+;; Alice and Bob each have a key and cross sign them. Bob then adds a
+;; new user id, "Alice". TOFU should now detect a conflict, because
+;; Alice only signed Bob's "Bob" user id.
+
+(display "Checking cross sigs...\n")
+(define GPG `(,(tool 'gpg) --no-permission-warning
+ --faked-system-time=1476304861))
+
+;; Carefully remove the TOFU db.
+(catch '() (unlink (path-join GNUPGHOME "tofu.db")))
+
+(define DIR "tofu/cross-sigs")
+;; The test keys.
+(define KEYA "1938C3A0E4674B6C217AC0B987DB2814EC38277E")
+(define KEYB "DC463A16E42F03240D76E8BA8B48C6BD871C2247")
+(define KEYIDA (substring KEYA (- (string-length KEYA) 8)))
+(define KEYIDB (substring KEYB (- (string-length KEYB) 8)))
+
+(define (verify-messages)
+ (for-each
+ (lambda (key)
+ (for-each
+ (lambda (i)
+ (let ((fn (in-srcdir "tests" "openpgp" DIR (string-append key "-" i ".txt"))))
+ (call-check `(,@GPG --verify ,fn))))
+ (list "1" "2")))
+ (list KEYIDA KEYIDB)))
+
+;; Import the public keys.
+(display " > Two keys. ")
+(call-check `(,@GPG --import ,(in-srcdir "tests" "openpgp" DIR (string-append KEYIDA "-1.gpg"))))
+(call-check `(,@GPG --import ,(in-srcdir "tests" "openpgp" DIR (string-append KEYIDB "-1.gpg"))))
+;; Make sure the tofu engine registers the keys.
+(verify-messages)
+(display "<\n")
+
+;; Since there is no conflict, the policy should be auto.
+(checkpolicy KEYA "auto")
+(checkpolicy KEYB "auto")
+
+;; Import the cross sigs.
+(display " > Adding cross signatures. ")
+(call-check `(,@GPG --import ,(in-srcdir "tests" "openpgp" DIR (string-append KEYIDA "-2.gpg"))))
+(call-check `(,@GPG --import ,(in-srcdir "tests" "openpgp" DIR (string-append KEYIDB "-2.gpg"))))
+(verify-messages)
+(display "<\n")
+
+;; There is still no conflict, so the policy shouldn't have changed.
+(checkpolicy KEYA "auto")
+(checkpolicy KEYB "auto")
+
+;; Import the conflicting user id.
+(display " > Adding conflicting user id. ")
+(call-check `(,@GPG --import ,(in-srcdir "tests" "openpgp" DIR (string-append KEYIDB "-3.gpg"))))
+(verify-messages)
+(display "<\n")
+
+(checkpolicy KEYA "ask")
+(checkpolicy KEYB "ask")
+
+;; Import Alice's signature on the conflicting user id. Since there
+;; is now a cross signature, we should revert to the default policy.
+(display " > Adding cross signature on user id. ")
+(call-check `(,@GPG --import ,(in-srcdir "tests" "openpgp" DIR (string-append KEYIDB "-4.gpg"))))
+(verify-messages)
+(display "<\n")
+
+(checkpolicy KEYA "auto")
+(checkpolicy KEYB "auto")
+
+;; Remove the keys.
+(call-check `(,@GPG --delete-key ,KEYA))
+(call-check `(,@GPG --delete-key ,KEYB))
+
+
+;; Check that we detect the following attack:
+;;
+;; Alice has an ultimately trusted key and she signs Bob's key. Then
+;; Bob adds a new user id, "Alice". TOFU should now detect a
+;; conflict, because Alice only signed Bob's "Bob" user id.
+;;
+;;
+;; The Alice key:
+;; pub rsa2048 2016-10-11 [SC]
+;; 1938C3A0E4674B6C217AC0B987DB2814EC38277E
+;; uid [ultimate] Spy Cow <spy@cow.com>
+;; sub rsa2048 2016-10-11 [E]
+;;
+;; The Bob key:
+;;
+;; pub rsa2048 2016-10-11 [SC]
+;; DC463A16E42F03240D76E8BA8B48C6BD871C2247
+;; uid [ full ] Spy R. Cow <spy@cow.com>
+;; uid [ full ] Spy R. Cow <spy@cow.de>
+;; sub rsa2048 2016-10-11 [E]
+
+(display "Checking UTK sigs...\n")
+(define GPG `(,(tool 'gpg) --no-permission-warning
+ --faked-system-time=1476304861))
+
+;; Carefully remove the TOFU db.
+(catch '() (unlink (path-join GNUPGHOME "tofu.db")))
+
+(define DIR "tofu/cross-sigs")
+;; The test keys.
+(define KEYA "1938C3A0E4674B6C217AC0B987DB2814EC38277E")
+(define KEYB "DC463A16E42F03240D76E8BA8B48C6BD871C2247")
+(define KEYIDA (substring KEYA (- (string-length KEYA) 8)))
+(define KEYIDB (substring KEYB (- (string-length KEYB) 8)))
+
+(define (verify-messages)
+ (for-each
+ (lambda (key)
+ (for-each
+ (lambda (i)
+ (let ((fn (in-srcdir "tests" "openpgp" DIR (string-append key "-" i ".txt"))))
+ (call-check `(,@GPG --verify ,fn))))
+ (list "1" "2")))
+ (list KEYIDA KEYIDB)))
+
+;; Import the public keys.
+(display " > Two keys. ")
+(call-check `(,@GPG --import ,(in-srcdir "tests" "openpgp" DIR (string-append KEYIDA "-1.gpg"))))
+(call-check `(,@GPG --import ,(in-srcdir "tests" "openpgp" DIR (string-append KEYIDB "-1.gpg"))))
+(display "<\n")
+
+(checkpolicy KEYA "auto")
+(checkpolicy KEYB "auto")
+
+;; Import the cross sigs.
+(display " > Adding cross signatures. ")
+(call-check `(,@GPG --import ,(in-srcdir "tests" "openpgp" DIR (string-append KEYIDA "-2.gpg"))))
+(call-check `(,@GPG --import ,(in-srcdir "tests" "openpgp" DIR (string-append KEYIDB "-2.gpg"))))
+(display "<\n")
+
+(checkpolicy KEYA "auto")
+(checkpolicy KEYB "auto")
+
+;; Make KEYA ultimately trusted.
+(display (string-append " > Marking " KEYA " as ultimately trusted. "))
+(pipe:do
+ (pipe:echo (string-append KEYA ":6:\n"))
+ (pipe:gpg `(--import-ownertrust)))
+(display "<\n")
+
+;; An ultimately trusted key's policy is good.
+(checkpolicy KEYA "good")
+;; A key signed by a UTK for which there is no policy gets the default
+;; policy of good.
+(checkpolicy KEYB "good")
+
+;; Import the conflicting user id.
+(display " > Adding conflicting user id. ")
+(call-check `(,@GPG --import ,(in-srcdir "tests" "openpgp" DIR (string-append KEYIDB "-3.gpg"))))
+(verify-messages)
+(display "<\n")
+
+(checkpolicy KEYA "good")
+(checkpolicy KEYB "ask")
+
+;; Import Alice's signature on the conflicting user id.
+(display " > Adding cross signature on user id. ")
+(call-check `(,@GPG --import ,(in-srcdir "tests" "openpgp" DIR (string-append KEYIDB "-4.gpg"))))
+(verify-messages)
+(display "<\n")
+
+(checkpolicy KEYA "good")
+(checkpolicy KEYB "good")
+
+;; Remove the keys.
+(call-check `(,@GPG --delete-key ,KEYA))
+(call-check `(,@GPG --delete-key ,KEYB))
diff --git a/tests/openpgp/tofu/conflicting/1C005AF3-1.txt b/tests/openpgp/tofu/conflicting/1C005AF3-1.txt
new file mode 100644
index 0000000..dba581d
--- /dev/null
+++ b/tests/openpgp/tofu/conflicting/1C005AF3-1.txt
Binary files differ
diff --git a/tests/openpgp/tofu/conflicting/1C005AF3-2.txt b/tests/openpgp/tofu/conflicting/1C005AF3-2.txt
new file mode 100644
index 0000000..fde9fb8
--- /dev/null
+++ b/tests/openpgp/tofu/conflicting/1C005AF3-2.txt
Binary files differ
diff --git a/tests/openpgp/tofu/conflicting/1C005AF3-3.txt b/tests/openpgp/tofu/conflicting/1C005AF3-3.txt
new file mode 100644
index 0000000..e6aa4ac
--- /dev/null
+++ b/tests/openpgp/tofu/conflicting/1C005AF3-3.txt
Binary files differ
diff --git a/tests/openpgp/tofu/conflicting/1C005AF3-4.txt b/tests/openpgp/tofu/conflicting/1C005AF3-4.txt
new file mode 100644
index 0000000..6a14891
--- /dev/null
+++ b/tests/openpgp/tofu/conflicting/1C005AF3-4.txt
Binary files differ
diff --git a/tests/openpgp/tofu/conflicting/1C005AF3-5.txt b/tests/openpgp/tofu/conflicting/1C005AF3-5.txt
new file mode 100644
index 0000000..12fb5fb
--- /dev/null
+++ b/tests/openpgp/tofu/conflicting/1C005AF3-5.txt
Binary files differ
diff --git a/tests/openpgp/tofu/conflicting/1C005AF3-secret.gpg b/tests/openpgp/tofu/conflicting/1C005AF3-secret.gpg
new file mode 100644
index 0000000..5f1e78a
--- /dev/null
+++ b/tests/openpgp/tofu/conflicting/1C005AF3-secret.gpg
Binary files differ
diff --git a/tests/openpgp/tofu/conflicting/1C005AF3.gpg b/tests/openpgp/tofu/conflicting/1C005AF3.gpg
new file mode 100644
index 0000000..7a75011
--- /dev/null
+++ b/tests/openpgp/tofu/conflicting/1C005AF3.gpg
Binary files differ
diff --git a/tests/openpgp/tofu/conflicting/B662E42F-1.txt b/tests/openpgp/tofu/conflicting/B662E42F-1.txt
new file mode 100644
index 0000000..c39056c
--- /dev/null
+++ b/tests/openpgp/tofu/conflicting/B662E42F-1.txt
Binary files differ
diff --git a/tests/openpgp/tofu/conflicting/B662E42F-2.txt b/tests/openpgp/tofu/conflicting/B662E42F-2.txt
new file mode 100644
index 0000000..a96ef9f
--- /dev/null
+++ b/tests/openpgp/tofu/conflicting/B662E42F-2.txt
Binary files differ
diff --git a/tests/openpgp/tofu/conflicting/B662E42F-3.txt b/tests/openpgp/tofu/conflicting/B662E42F-3.txt
new file mode 100644
index 0000000..2e6e81b
--- /dev/null
+++ b/tests/openpgp/tofu/conflicting/B662E42F-3.txt
Binary files differ
diff --git a/tests/openpgp/tofu/conflicting/B662E42F-4.txt b/tests/openpgp/tofu/conflicting/B662E42F-4.txt
new file mode 100644
index 0000000..470882f
--- /dev/null
+++ b/tests/openpgp/tofu/conflicting/B662E42F-4.txt
Binary files differ
diff --git a/tests/openpgp/tofu/conflicting/B662E42F-5.txt b/tests/openpgp/tofu/conflicting/B662E42F-5.txt
new file mode 100644
index 0000000..21d54bc
--- /dev/null
+++ b/tests/openpgp/tofu/conflicting/B662E42F-5.txt
@@ -0,0 +1 @@
+%[}I i\,  b,&khӌ)eech&q0l;Sɵ`K>}iw5?VF+' |]$/jmJߖTjY<i6%|aGVkg4e<akB UoffZ<U[hJen뤙 f~[dm"v?P3}=}#"^j-Tҁ՜+/q=&>̬%kzNlʳמ:7z\o޵sJs1 \ No newline at end of file
diff --git a/tests/openpgp/tofu/conflicting/B662E42F-secret.gpg b/tests/openpgp/tofu/conflicting/B662E42F-secret.gpg
new file mode 100644
index 0000000..7362ded
--- /dev/null
+++ b/tests/openpgp/tofu/conflicting/B662E42F-secret.gpg
Binary files differ
diff --git a/tests/openpgp/tofu/conflicting/B662E42F.gpg b/tests/openpgp/tofu/conflicting/B662E42F.gpg
new file mode 100644
index 0000000..6c07520
--- /dev/null
+++ b/tests/openpgp/tofu/conflicting/B662E42F.gpg
Binary files differ
diff --git a/tests/openpgp/tofu/conflicting/BE04EB2B-1.txt b/tests/openpgp/tofu/conflicting/BE04EB2B-1.txt
new file mode 100644
index 0000000..1b3de47
--- /dev/null
+++ b/tests/openpgp/tofu/conflicting/BE04EB2B-1.txt
Binary files differ
diff --git a/tests/openpgp/tofu/conflicting/BE04EB2B-2.txt b/tests/openpgp/tofu/conflicting/BE04EB2B-2.txt
new file mode 100644
index 0000000..f4f5487
--- /dev/null
+++ b/tests/openpgp/tofu/conflicting/BE04EB2B-2.txt
Binary files differ
diff --git a/tests/openpgp/tofu/conflicting/BE04EB2B-3.txt b/tests/openpgp/tofu/conflicting/BE04EB2B-3.txt
new file mode 100644
index 0000000..7451073
--- /dev/null
+++ b/tests/openpgp/tofu/conflicting/BE04EB2B-3.txt
Binary files differ
diff --git a/tests/openpgp/tofu/conflicting/BE04EB2B-4.txt b/tests/openpgp/tofu/conflicting/BE04EB2B-4.txt
new file mode 100644
index 0000000..f15496d
--- /dev/null
+++ b/tests/openpgp/tofu/conflicting/BE04EB2B-4.txt
Binary files differ
diff --git a/tests/openpgp/tofu/conflicting/BE04EB2B-5.txt b/tests/openpgp/tofu/conflicting/BE04EB2B-5.txt
new file mode 100644
index 0000000..39078f1
--- /dev/null
+++ b/tests/openpgp/tofu/conflicting/BE04EB2B-5.txt
Binary files differ
diff --git a/tests/openpgp/tofu/conflicting/BE04EB2B-secret.gpg b/tests/openpgp/tofu/conflicting/BE04EB2B-secret.gpg
new file mode 100644
index 0000000..5d393aa
--- /dev/null
+++ b/tests/openpgp/tofu/conflicting/BE04EB2B-secret.gpg
Binary files differ
diff --git a/tests/openpgp/tofu/conflicting/BE04EB2B.gpg b/tests/openpgp/tofu/conflicting/BE04EB2B.gpg
new file mode 100644
index 0000000..787b238
--- /dev/null
+++ b/tests/openpgp/tofu/conflicting/BE04EB2B.gpg
Binary files differ
diff --git a/tests/openpgp/tofu/cross-sigs/871C2247-1.gpg b/tests/openpgp/tofu/cross-sigs/871C2247-1.gpg
new file mode 100644
index 0000000..f706f70
--- /dev/null
+++ b/tests/openpgp/tofu/cross-sigs/871C2247-1.gpg
Binary files differ
diff --git a/tests/openpgp/tofu/cross-sigs/871C2247-1.txt b/tests/openpgp/tofu/cross-sigs/871C2247-1.txt
new file mode 100644
index 0000000..0bdc1fc
--- /dev/null
+++ b/tests/openpgp/tofu/cross-sigs/871C2247-1.txt
Binary files differ
diff --git a/tests/openpgp/tofu/cross-sigs/871C2247-2.gpg b/tests/openpgp/tofu/cross-sigs/871C2247-2.gpg
new file mode 100644
index 0000000..0b2485f
--- /dev/null
+++ b/tests/openpgp/tofu/cross-sigs/871C2247-2.gpg
Binary files differ
diff --git a/tests/openpgp/tofu/cross-sigs/871C2247-2.txt b/tests/openpgp/tofu/cross-sigs/871C2247-2.txt
new file mode 100644
index 0000000..4d3aaaa
--- /dev/null
+++ b/tests/openpgp/tofu/cross-sigs/871C2247-2.txt
Binary files differ
diff --git a/tests/openpgp/tofu/cross-sigs/871C2247-3.gpg b/tests/openpgp/tofu/cross-sigs/871C2247-3.gpg
new file mode 100644
index 0000000..eb2c435
--- /dev/null
+++ b/tests/openpgp/tofu/cross-sigs/871C2247-3.gpg
Binary files differ
diff --git a/tests/openpgp/tofu/cross-sigs/871C2247-3.txt b/tests/openpgp/tofu/cross-sigs/871C2247-3.txt
new file mode 100644
index 0000000..9b2d49d
--- /dev/null
+++ b/tests/openpgp/tofu/cross-sigs/871C2247-3.txt
Binary files differ
diff --git a/tests/openpgp/tofu/cross-sigs/871C2247-4.gpg b/tests/openpgp/tofu/cross-sigs/871C2247-4.gpg
new file mode 100644
index 0000000..9c98ec1
--- /dev/null
+++ b/tests/openpgp/tofu/cross-sigs/871C2247-4.gpg
Binary files differ
diff --git a/tests/openpgp/tofu/cross-sigs/871C2247-secret.gpg b/tests/openpgp/tofu/cross-sigs/871C2247-secret.gpg
new file mode 100644
index 0000000..a87c61b
--- /dev/null
+++ b/tests/openpgp/tofu/cross-sigs/871C2247-secret.gpg
Binary files differ
diff --git a/tests/openpgp/tofu/cross-sigs/EC38277E-1.gpg b/tests/openpgp/tofu/cross-sigs/EC38277E-1.gpg
new file mode 100644
index 0000000..e6becec
--- /dev/null
+++ b/tests/openpgp/tofu/cross-sigs/EC38277E-1.gpg
Binary files differ
diff --git a/tests/openpgp/tofu/cross-sigs/EC38277E-1.txt b/tests/openpgp/tofu/cross-sigs/EC38277E-1.txt
new file mode 100644
index 0000000..92236be
--- /dev/null
+++ b/tests/openpgp/tofu/cross-sigs/EC38277E-1.txt
Binary files differ
diff --git a/tests/openpgp/tofu/cross-sigs/EC38277E-2.gpg b/tests/openpgp/tofu/cross-sigs/EC38277E-2.gpg
new file mode 100644
index 0000000..d26bd54
--- /dev/null
+++ b/tests/openpgp/tofu/cross-sigs/EC38277E-2.gpg
Binary files differ
diff --git a/tests/openpgp/tofu/cross-sigs/EC38277E-2.txt b/tests/openpgp/tofu/cross-sigs/EC38277E-2.txt
new file mode 100644
index 0000000..b4013d3
--- /dev/null
+++ b/tests/openpgp/tofu/cross-sigs/EC38277E-2.txt
Binary files differ
diff --git a/tests/openpgp/tofu/cross-sigs/EC38277E-3.txt b/tests/openpgp/tofu/cross-sigs/EC38277E-3.txt
new file mode 100644
index 0000000..9b2d49d
--- /dev/null
+++ b/tests/openpgp/tofu/cross-sigs/EC38277E-3.txt
Binary files differ
diff --git a/tests/openpgp/tofu/cross-sigs/EC38277E-secret.gpg b/tests/openpgp/tofu/cross-sigs/EC38277E-secret.gpg
new file mode 100644
index 0000000..1839e3a
--- /dev/null
+++ b/tests/openpgp/tofu/cross-sigs/EC38277E-secret.gpg
Binary files differ
diff --git a/tests/openpgp/tofu/cross-sigs/README b/tests/openpgp/tofu/cross-sigs/README
new file mode 100644
index 0000000..439962b
--- /dev/null
+++ b/tests/openpgp/tofu/cross-sigs/README
@@ -0,0 +1,79 @@
+# How I generate the keys and messages to verify:
+
+# Generate and export two non-conflicting keys.
+gpg --quick-gen-key 'Spy Cow <spy@cow.com>'
+gpg --quick-gen-key 'Spy R. Cow <spy@cow.de>'
+
+KEYIDA=1938C3A0E4674B6C217AC0B987DB2814EC38277E
+KEYIDB=DC463A16E42F03240D76E8BA8B48C6BD871C2247
+
+for KEYID in $KEYIDA $KEYIDB
+do
+ gpg --export $KEYID > tofu-$KEYID.gpg
+ gpg --export-secret-keys $KEYID > tofu-$KEYID-secret.gpg
+done
+
+# Sign some data.
+echo foo | gpg --default-key $KEYIDA -s > tofu-$KEYIDA-1.txt
+echo foo | gpg --default-key $KEYIDB -s > tofu-$KEYIDB-1.txt
+
+# Again, but with an issuer.
+echo foo | gpg --default-key "<spy@cow.com>" -s > tofu-$KEYIDA-2.txt
+echo foo | gpg --default-key "<spy@cow.de>" -s > tofu-$KEYIDB-2.txt
+
+# Have A sign B and vice versa.
+gpg --default-key $KEYIDA --quick-sign $KEYIDB
+gpg --default-key $KEYIDB --quick-sign $KEYIDA
+
+gpg --export $KEYIDA > tofu-$KEYIDA-2.gpg
+gpg --export $KEYIDB > tofu-$KEYIDB-2.gpg
+
+# Cause A and B to conflict.
+gpg --quick-adduid $KEYIDB 'Spy R. Cow <spy@cow.com>'
+gpg --export $KEYIDB > tofu-$KEYIDB-3.gpg
+
+echo foo | gpg --default-key "<spy@cow.com>" -s > tofu-$KEYIDA-3.txt
+echo foo | gpg --default-key "<spy@cow.com>" -s > tofu-$KEYIDB-3.txt
+
+# Have A sign B's conflicting user id.
+gpg --default-key $KEYIDA --quick-sign $KEYIDB
+gpg --export $KEYIDB > tofu-$KEYIDB-4.gpg
+
+exit 0
+
+# In a new directory (so the keys are not ultimately trusted).
+
+D=~/neal/work/gpg/test
+echo 'trust-model tofu+pgp' > gpg.conf
+gpg --import $D/tofu-$KEYIDA.gpg
+gpg --import $D/tofu-$KEYIDB.gpg
+gpg -k
+
+gpg --verify $D/tofu-$KEYIDA-1.txt
+gpg --verify $D/tofu-$KEYIDB-1.txt
+# With an issuer.
+gpg --verify $D/tofu-$KEYIDA-2.txt
+gpg --verify $D/tofu-$KEYIDB-2.txt
+
+# Import the cross signatures.
+gpg --import $D/tofu-$KEYIDA-2.gpg
+gpg --import $D/tofu-$KEYIDB-2.gpg
+gpg -k
+
+gpg --verify $D/tofu-$KEYIDA-1.txt
+gpg --verify $D/tofu-$KEYIDB-1.txt
+# With an issuer.
+gpg --verify $D/tofu-$KEYIDA-2.txt
+gpg --verify $D/tofu-$KEYIDB-2.txt
+
+
+gpg --status-fd=1 --batch --verify $D/tofu-$KEYIDA-3.txt | grep TRUST_UNDEFINED
+gpg --status-fd=1 --batch --verify $D/tofu-$KEYIDB-3.txt | grep TRUST_UNDEFINED
+
+# Import the conflicting user id.
+gpg --import $D/tofu-$KEYIDB-3.gpg
+gpg -k
+
+# Import the cross signature, which should remove the conflict.
+gpg --import $D/tofu-$KEYIDB-4.gpg
+gpg -k