Skip to content

Commit 2bbda32

Browse files
joschJPEWdev
authored andcommitted
add --keyring and --fingerprint options
These options allow one to pass a custom keyring with --keyring as well as panning the fingerprint using --fingerprint. This is for example useful in situations where a vendor wants to give users access to updated system images and for that purpose, ships a custom keyring with their installations. Using the --keyring option, the user can use GPG to verify that the updated system image they downloaded indeed comes from the expected source (even more so when using the --fingerprint option together with it) without having to add the key into their own personal GPG keyring. Furthermore, this is useful in scripts which call bmaptool programmatically. For example, an updater script could come shipped with an embedded GPG keyring and then call bmaptool with this keyring (and optionally with a known --fingerprint) to facilitate GPG-verified automatic updates.
1 parent 16eec70 commit 2bbda32

File tree

3 files changed

+144
-14
lines changed

3 files changed

+144
-14
lines changed

docs/man1/bmaptool.1

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,18 @@ integrity and publisher. If this option is not specified, \fIbmaptool\fR
194194
tries to automatically discover the signature file.
195195
.RE
196196

197+
.PP
198+
\-\-fingerprint FINGERPRINT
199+
.RS 2
200+
The GPG fingerprint which you expect to have signed the bmap file.
201+
.RE
202+
203+
.PP
204+
\-\-keyring KEYRING
205+
.RS 2
206+
Path to the GPG keyring that will be used when verifying GPG signatures.
207+
.RE
208+
197209
.PP
198210
\-\-nobmap
199211
.RS 2

src/bmaptool/CLI.py

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,12 @@ class Signature(NamedTuple):
139139
uid: str
140140

141141

142-
def verify_bmap_signature_gpgme(bmap_obj, detached_sig):
142+
def verify_bmap_signature_gpgme(bmap_obj, detached_sig, keyring):
143+
if keyring:
144+
error_out(
145+
"Python gpgme binding is not able to verify "
146+
"signatures against a custom keyring."
147+
)
143148
try:
144149
import gpg
145150
except ImportError:
@@ -187,8 +192,17 @@ def fpr2uid(fpr):
187192
]
188193

189194

190-
def verify_bmap_signature_gpgbin(bmap_obj, detached_sig, gpgargv):
195+
def verify_bmap_signature_gpgbin(bmap_obj, detached_sig, gpgargv, keyring):
191196
with tempfile.TemporaryDirectory(suffix=".bmaptool.gnupg") as td:
197+
if keyring:
198+
if gpgargv[0] == "gpg":
199+
gpgargv.extend(
200+
[
201+
f"--homedir={td}",
202+
"--no-default-keyring",
203+
]
204+
)
205+
gpgargv.append(f"--keyring={keyring}")
192206
if detached_sig:
193207
with open(f"{td}/sig", "wb") as f:
194208
shutil.copyfileobj(detached_sig, f)
@@ -237,13 +251,13 @@ def verify_bmap_signature_gpgbin(bmap_obj, detached_sig, gpgargv):
237251
]
238252

239253

240-
def verify_bmap_signature_gpgv(bmap_obj, detached_sig):
254+
def verify_bmap_signature_gpgv(bmap_obj, detached_sig, keyring):
241255
return verify_bmap_signature_gpgbin(
242-
bmap_obj, detached_sig, ["gpgv", "--output=-", "--status-fd=2"]
256+
bmap_obj, detached_sig, ["gpgv", "--output=-", "--status-fd=2"], keyring
243257
)
244258

245259

246-
def verify_bmap_signature_gpg(bmap_obj, detached_sig):
260+
def verify_bmap_signature_gpg(bmap_obj, detached_sig, keyring):
247261
return verify_bmap_signature_gpgbin(
248262
bmap_obj,
249263
detached_sig,
@@ -257,6 +271,7 @@ def verify_bmap_signature_gpg(bmap_obj, detached_sig):
257271
"-",
258272
"--status-fd=2",
259273
],
274+
keyring,
260275
)
261276

262277

@@ -317,6 +332,10 @@ def _add_ext(p, ext):
317332
detached_sig = TransRead.TransRead(_add_ext(bmap_path, ".sig"))
318333
except TransRead.Error:
319334
# No detached signatures found
335+
if args.fingerprint:
336+
error_out("no signature found but --fingerprint given")
337+
if args.keyring:
338+
error_out("no signature found but --keyring given")
320339
return None
321340

322341
log.info("discovered signature file for bmap '%s'" % detached_sig.name)
@@ -327,12 +346,16 @@ def _add_ext(p, ext):
327346
"gpgv": verify_bmap_signature_gpgv,
328347
}
329348
have_method = set()
330-
try:
331-
import gpg
332349

333-
have_method.add("gpgme")
334-
except ImportError:
335-
pass
350+
if not args.keyring:
351+
# The python gpgme binding is not able to verify against a custom
352+
# keyring. Only try this method if we have no keyring.
353+
try:
354+
import gpg
355+
356+
have_method.add("gpgme")
357+
except ImportError:
358+
pass
336359
if shutil.which("gpg") is not None:
337360
have_method.add("gpg")
338361
if shutil.which("gpgv") is not None:
@@ -342,10 +365,10 @@ def _add_ext(p, ext):
342365
error_out("Cannot verify GPG signature without GPG")
343366

344367
for method in ["gpgme", "gpgv", "gpg"]:
345-
log.info(f"Trying to verify signature using {method}")
346368
if method not in have_method:
347369
continue
348-
plaintext, sigs = methods[method](bmap_obj, detached_sig)
370+
log.info(f"Trying to verify signature using {method}")
371+
plaintext, sigs = methods[method](bmap_obj, detached_sig, args.keyring)
349372
break
350373
bmap_obj.seek(0)
351374

@@ -359,6 +382,12 @@ def _add_ext(p, ext):
359382
"contain any valid signatures"
360383
)
361384
else:
385+
if args.fingerprint and args.fingerprint not in [sig.fpr for sig in sigs]:
386+
error_out(
387+
f"requested fingerprint {args.fingerprint} "
388+
"did not sign the bmap file. Only have these sigs: "
389+
+ ("".join([f"\n * {sig.fpr}" for sig in sigs]))
390+
)
362391
for sig in sigs:
363392
if sig.valid:
364393
log.info(
@@ -575,6 +604,12 @@ def copy_command(args):
575604
if args.bmap_sig and args.no_sig_verify:
576605
error_out("--bmap-sig and --no-sig-verify cannot be used together")
577606

607+
if args.no_sig_verify and args.keyring:
608+
error_out("--no-sig-verify and --keyring cannot be used together")
609+
610+
if args.no_sig_verify and args.fingerprint:
611+
error_out("--no-sig-verify and --fingerprint cannot be used together")
612+
578613
image_obj, dest_obj, bmap_obj, bmap_path, image_size, dest_is_blkdev = open_files(
579614
args
580615
)
@@ -808,6 +843,14 @@ def parse_arguments():
808843
text = "do not verify bmap file GPG signature"
809844
parser_copy.add_argument("--no-sig-verify", action="store_true", help=text)
810845

846+
# The --keyring option
847+
text = "the GPG keyring to verify the GPG signature on the bmap file"
848+
parser_copy.add_argument("--keyring", help=text)
849+
850+
# The --fingerprint option
851+
text = "the GPG fingerprint that is expected to have signed the bmap file"
852+
parser_copy.add_argument("--fingerprint", help=text)
853+
811854
# The --no-verify option
812855
text = "do not verify the data checksum while writing"
813856
parser_copy.add_argument("--no-verify", action="store_true", help=text)

tests/test_CLI.py

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,56 @@ def test_valid_signature(self):
6161
b"successfully verified bmap file signature", completed_process.stderr
6262
)
6363

64+
def test_valid_signature_fingerprint(self):
65+
assert testkeys["correct"].fpr is not None
66+
completed_process = subprocess.run(
67+
[
68+
"bmaptool",
69+
"copy",
70+
"--bmap",
71+
"tests/test-data/signatures/test.image.bmap.v2.0correct.asc",
72+
"--fingerprint",
73+
testkeys["correct"].fpr,
74+
"tests/test-data/test.image.gz",
75+
self.tmpfile,
76+
],
77+
stdout=subprocess.PIPE,
78+
stderr=subprocess.PIPE,
79+
check=False,
80+
)
81+
self.assertEqual(completed_process.returncode, 0)
82+
self.assertEqual(completed_process.stdout, b"")
83+
self.assertIn(
84+
b"successfully verified bmap file signature", completed_process.stderr
85+
)
86+
87+
def test_valid_signature_fingerprint_keyring(self):
88+
assert testkeys["correct"].fpr is not None
89+
completed_process = subprocess.run(
90+
[
91+
"bmaptool",
92+
"copy",
93+
"--bmap",
94+
"tests/test-data/signatures/test.image.bmap.v2.0correct.asc",
95+
"--fingerprint",
96+
testkeys["correct"].fpr,
97+
"--keyring",
98+
testkeys["correct"].gnupghome + ".keyring",
99+
"tests/test-data/test.image.gz",
100+
self.tmpfile,
101+
],
102+
stdout=subprocess.PIPE,
103+
stderr=subprocess.PIPE,
104+
check=False,
105+
# should work without GNUPGHOME set because we supply --keyring
106+
env={k: v for k, v in os.environ.items() if k != "GNUPGHOME"},
107+
)
108+
self.assertEqual(completed_process.returncode, 0)
109+
self.assertEqual(completed_process.stdout, b"")
110+
self.assertIn(
111+
b"successfully verified bmap file signature", completed_process.stderr
112+
)
113+
64114
def test_unknown_signer(self):
65115
completed_process = subprocess.run(
66116
[
@@ -141,6 +191,29 @@ def test_clearsign(self):
141191
b"successfully verified bmap file signature", completed_process.stderr
142192
)
143193

194+
def test_fingerprint_without_signature(self):
195+
assert testkeys["correct"].fpr is not None
196+
completed_process = subprocess.run(
197+
[
198+
"bmaptool",
199+
"copy",
200+
"--bmap",
201+
"tests/test-data/test.image.bmap.v2.0",
202+
"--fingerprint",
203+
testkeys["correct"].fpr,
204+
"tests/test-data/test.image.gz",
205+
self.tmpfile,
206+
],
207+
stdout=subprocess.PIPE,
208+
stderr=subprocess.PIPE,
209+
check=False,
210+
)
211+
self.assertEqual(completed_process.returncode, 1)
212+
self.assertEqual(completed_process.stdout, b"")
213+
self.assertIn(
214+
b"no signature found but --fingerprint given", completed_process.stderr
215+
)
216+
144217
def setUp(self):
145218
try:
146219
import gpg
@@ -160,8 +233,6 @@ def setUp(self):
160233
certify=True,
161234
)
162235
key.fpr = dmkey.fpr
163-
with open(f"{key.gnupghome}.keyring", "wb") as f:
164-
f.write(context.key_export_minimal())
165236
for bmapv in ["2.0", "1.4"]:
166237
testp = "tests/test-data"
167238
imbn = "test.image.bmap.v"
@@ -184,6 +255,10 @@ def setUp(self):
184255
bmapcontent, mode=gpg.constants.sig.mode.DETACH
185256
)
186257
detsigf.write(signed_data)
258+
# the file supplied to gpgv via --keyring must not be armored
259+
context.armor = False
260+
with open(f"{key.gnupghome}.keyring", "wb") as f:
261+
f.write(context.key_export_minimal())
187262

188263
self.tmpfile = tempfile.mkstemp(prefix="testfile_", dir=".")[1]
189264
os.environ["GNUPGHOME"] = testkeys["correct"].gnupghome

0 commit comments

Comments
 (0)