Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/man1/bmaptool.1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
67 changes: 55 additions & 12 deletions src/bmaptool/CLI.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand All @@ -257,6 +271,7 @@ def verify_bmap_signature_gpg(bmap_obj, detached_sig):
"-",
"--status-fd=2",
],
keyring,
)


Expand Down Expand Up @@ -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)
Expand All @@ -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:
Expand All @@ -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)

Expand All @@ -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(
Expand Down Expand Up @@ -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
)
Expand Down Expand Up @@ -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)
Expand Down
79 changes: 77 additions & 2 deletions tests/test_CLI.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
[
Expand Down Expand Up @@ -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
Expand All @@ -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"
Expand All @@ -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
Expand Down