diff --git a/docs/man1/bmaptool.1 b/docs/man1/bmaptool.1 index 5093b48..05faf6c 100644 --- a/docs/man1/bmaptool.1 +++ b/docs/man1/bmaptool.1 @@ -194,6 +194,18 @@ integrity and publisher. If this option is not specified, \fIbmaptool\fR tries to automatically discover the signature file. .RE +.PP +\-\-fingerprint FINGERPRINT +.RS 2 +The GPG fingerprint which you expect to have signed the bmap file. +.RE + +.PP +\-\-keyring KEYRING +.RS 2 +Path to the GPG keyring that will be used when verifying GPG signatures. +.RE + .PP \-\-nobmap .RS 2 diff --git a/src/bmaptool/CLI.py b/src/bmaptool/CLI.py index a15050a..ae53b77 100644 --- a/src/bmaptool/CLI.py +++ b/src/bmaptool/CLI.py @@ -139,7 +139,12 @@ class Signature(NamedTuple): uid: str -def verify_bmap_signature_gpgme(bmap_obj, detached_sig): +def verify_bmap_signature_gpgme(bmap_obj, detached_sig, keyring): + if keyring: + error_out( + "Python gpgme binding is not able to verify " + "signatures against a custom keyring." + ) try: import gpg except ImportError: @@ -187,8 +192,17 @@ def fpr2uid(fpr): ] -def verify_bmap_signature_gpgbin(bmap_obj, detached_sig, gpgargv): +def verify_bmap_signature_gpgbin(bmap_obj, detached_sig, gpgargv, keyring): with tempfile.TemporaryDirectory(suffix=".bmaptool.gnupg") as td: + if keyring: + if gpgargv[0] == "gpg": + gpgargv.extend( + [ + f"--homedir={td}", + "--no-default-keyring", + ] + ) + gpgargv.append(f"--keyring={keyring}") if detached_sig: with open(f"{td}/sig", "wb") as f: shutil.copyfileobj(detached_sig, f) @@ -237,13 +251,13 @@ def verify_bmap_signature_gpgbin(bmap_obj, detached_sig, gpgargv): ] -def verify_bmap_signature_gpgv(bmap_obj, detached_sig): +def verify_bmap_signature_gpgv(bmap_obj, detached_sig, keyring): return verify_bmap_signature_gpgbin( - bmap_obj, detached_sig, ["gpgv", "--output=-", "--status-fd=2"] + bmap_obj, detached_sig, ["gpgv", "--output=-", "--status-fd=2"], keyring ) -def verify_bmap_signature_gpg(bmap_obj, detached_sig): +def verify_bmap_signature_gpg(bmap_obj, detached_sig, keyring): return verify_bmap_signature_gpgbin( bmap_obj, detached_sig, @@ -257,6 +271,7 @@ def verify_bmap_signature_gpg(bmap_obj, detached_sig): "-", "--status-fd=2", ], + keyring, ) @@ -317,6 +332,10 @@ def _add_ext(p, ext): detached_sig = TransRead.TransRead(_add_ext(bmap_path, ".sig")) except TransRead.Error: # No detached signatures found + if args.fingerprint: + error_out("no signature found but --fingerprint given") + if args.keyring: + error_out("no signature found but --keyring given") return None log.info("discovered signature file for bmap '%s'" % detached_sig.name) @@ -327,12 +346,16 @@ def _add_ext(p, ext): "gpgv": verify_bmap_signature_gpgv, } have_method = set() - try: - import gpg - have_method.add("gpgme") - except ImportError: - pass + if not args.keyring: + # The python gpgme binding is not able to verify against a custom + # keyring. Only try this method if we have no keyring. + try: + import gpg + + have_method.add("gpgme") + except ImportError: + pass if shutil.which("gpg") is not None: have_method.add("gpg") if shutil.which("gpgv") is not None: @@ -342,10 +365,10 @@ def _add_ext(p, ext): error_out("Cannot verify GPG signature without GPG") for method in ["gpgme", "gpgv", "gpg"]: - log.info(f"Trying to verify signature using {method}") if method not in have_method: continue - plaintext, sigs = methods[method](bmap_obj, detached_sig) + log.info(f"Trying to verify signature using {method}") + plaintext, sigs = methods[method](bmap_obj, detached_sig, args.keyring) break bmap_obj.seek(0) @@ -359,6 +382,12 @@ def _add_ext(p, ext): "contain any valid signatures" ) else: + if args.fingerprint and args.fingerprint not in [sig.fpr for sig in sigs]: + error_out( + f"requested fingerprint {args.fingerprint} " + "did not sign the bmap file. Only have these sigs: " + + ("".join([f"\n * {sig.fpr}" for sig in sigs])) + ) for sig in sigs: if sig.valid: log.info( @@ -575,6 +604,12 @@ def copy_command(args): if args.bmap_sig and args.no_sig_verify: error_out("--bmap-sig and --no-sig-verify cannot be used together") + if args.no_sig_verify and args.keyring: + error_out("--no-sig-verify and --keyring cannot be used together") + + if args.no_sig_verify and args.fingerprint: + error_out("--no-sig-verify and --fingerprint cannot be used together") + image_obj, dest_obj, bmap_obj, bmap_path, image_size, dest_is_blkdev = open_files( args ) @@ -808,6 +843,14 @@ def parse_arguments(): text = "do not verify bmap file GPG signature" parser_copy.add_argument("--no-sig-verify", action="store_true", help=text) + # The --keyring option + text = "the GPG keyring to verify the GPG signature on the bmap file" + parser_copy.add_argument("--keyring", help=text) + + # The --fingerprint option + text = "the GPG fingerprint that is expected to have signed the bmap file" + parser_copy.add_argument("--fingerprint", help=text) + # The --no-verify option text = "do not verify the data checksum while writing" parser_copy.add_argument("--no-verify", action="store_true", help=text) diff --git a/tests/test_CLI.py b/tests/test_CLI.py index 9f09e86..86e9cf0 100644 --- a/tests/test_CLI.py +++ b/tests/test_CLI.py @@ -61,6 +61,56 @@ def test_valid_signature(self): b"successfully verified bmap file signature", completed_process.stderr ) + def test_valid_signature_fingerprint(self): + assert testkeys["correct"].fpr is not None + completed_process = subprocess.run( + [ + "bmaptool", + "copy", + "--bmap", + "tests/test-data/signatures/test.image.bmap.v2.0correct.asc", + "--fingerprint", + testkeys["correct"].fpr, + "tests/test-data/test.image.gz", + self.tmpfile, + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + self.assertEqual(completed_process.returncode, 0) + self.assertEqual(completed_process.stdout, b"") + self.assertIn( + b"successfully verified bmap file signature", completed_process.stderr + ) + + def test_valid_signature_fingerprint_keyring(self): + assert testkeys["correct"].fpr is not None + completed_process = subprocess.run( + [ + "bmaptool", + "copy", + "--bmap", + "tests/test-data/signatures/test.image.bmap.v2.0correct.asc", + "--fingerprint", + testkeys["correct"].fpr, + "--keyring", + testkeys["correct"].gnupghome + ".keyring", + "tests/test-data/test.image.gz", + self.tmpfile, + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + # should work without GNUPGHOME set because we supply --keyring + env={k: v for k, v in os.environ.items() if k != "GNUPGHOME"}, + ) + self.assertEqual(completed_process.returncode, 0) + self.assertEqual(completed_process.stdout, b"") + self.assertIn( + b"successfully verified bmap file signature", completed_process.stderr + ) + def test_unknown_signer(self): completed_process = subprocess.run( [ @@ -141,6 +191,29 @@ def test_clearsign(self): b"successfully verified bmap file signature", completed_process.stderr ) + def test_fingerprint_without_signature(self): + assert testkeys["correct"].fpr is not None + completed_process = subprocess.run( + [ + "bmaptool", + "copy", + "--bmap", + "tests/test-data/test.image.bmap.v2.0", + "--fingerprint", + testkeys["correct"].fpr, + "tests/test-data/test.image.gz", + self.tmpfile, + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + self.assertEqual(completed_process.returncode, 1) + self.assertEqual(completed_process.stdout, b"") + self.assertIn( + b"no signature found but --fingerprint given", completed_process.stderr + ) + def setUp(self): try: import gpg @@ -160,8 +233,6 @@ def setUp(self): certify=True, ) key.fpr = dmkey.fpr - with open(f"{key.gnupghome}.keyring", "wb") as f: - f.write(context.key_export_minimal()) for bmapv in ["2.0", "1.4"]: testp = "tests/test-data" imbn = "test.image.bmap.v" @@ -184,6 +255,10 @@ def setUp(self): bmapcontent, mode=gpg.constants.sig.mode.DETACH ) detsigf.write(signed_data) + # the file supplied to gpgv via --keyring must not be armored + context.armor = False + with open(f"{key.gnupghome}.keyring", "wb") as f: + f.write(context.key_export_minimal()) self.tmpfile = tempfile.mkstemp(prefix="testfile_", dir=".")[1] os.environ["GNUPGHOME"] = testkeys["correct"].gnupghome