opensighub 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
opensighub/__init__.py ADDED
File without changes
opensighub/cli.py ADDED
@@ -0,0 +1,488 @@
1
+ # SPDX-FileCopyrightText: 2026 Linutronix GmbH
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import argparse
6
+ import logging
7
+ import multiprocessing
8
+ import sys
9
+ from collections.abc import Callable
10
+ from dataclasses import dataclass, field
11
+ from importlib.metadata import version
12
+ from pathlib import Path
13
+
14
+ import yaml
15
+ from platformdirs import user_config_path
16
+
17
+ from opensighub import setup
18
+ from opensighub.config import Config
19
+ from opensighub.debian import DebianSigningJob, DebianSigningProcessor
20
+ from opensighub.signers import (
21
+ Hab4Sign,
22
+ LinuxModuleSign,
23
+ OpteeTaSign,
24
+ RpiSign,
25
+ SigningPool,
26
+ SwuSign,
27
+ SwuSignJob,
28
+ UefiSign,
29
+ UefiSignJob,
30
+ UefiVariableSign,
31
+ UefiVariableSignJob,
32
+ )
33
+ from opensighub.util import MultiprocessingCertCache, OpensighubError
34
+
35
+ DEFAULT_CONFIG_PATH = user_config_path("opensighub") / "config.yaml"
36
+
37
+
38
+ @dataclass
39
+ class SigningRunBase:
40
+ config: Path
41
+ output: Path
42
+ parallel: int
43
+ force_overwrite: bool
44
+
45
+ def processor_factory(
46
+ self, config: Config, cert_cache: MultiprocessingCertCache
47
+ ) -> Callable[[], None]:
48
+ raise NotImplementedError
49
+
50
+
51
+ @dataclass
52
+ class DebianRun(SigningRunBase):
53
+ jobs: list[DebianSigningJob]
54
+ run_sbuild: bool = False
55
+ sbuild_args: list[str] = field(default_factory=list)
56
+
57
+ def processor_factory(
58
+ self, config: Config, cert_cache: MultiprocessingCertCache
59
+ ) -> Callable[[], None]:
60
+ debian_processor = DebianSigningProcessor(
61
+ config,
62
+ Path(self.output),
63
+ self.run_sbuild,
64
+ self.sbuild_args,
65
+ self.force_overwrite,
66
+ )
67
+ worker = SigningPool(
68
+ UefiSign(cert_cache, config.uefi, self.force_overwrite) if config.uefi else None,
69
+ None, # UefiVariableSign
70
+ None, # SwuSign
71
+ LinuxModuleSign(cert_cache, config.kernel_modules) if config.kernel_modules else None,
72
+ Hab4Sign(cert_cache, config.hab4) if config.hab4 else None,
73
+ OpteeTaSign(cert_cache, config.optee_ta) if config.optee_ta else None,
74
+ RpiSign(cert_cache, config.rpi) if config.rpi else None,
75
+ parallel=self.parallel,
76
+ )
77
+
78
+ def process():
79
+ for job in self.jobs:
80
+ debian_processor.process(job, worker)
81
+ debian_processor.cleanup()
82
+
83
+ return process
84
+
85
+
86
+ @dataclass
87
+ class UefiVariableRun(SigningRunBase):
88
+ jobs: list[UefiVariableSignJob]
89
+
90
+ def processor_factory(
91
+ self, config: Config, cert_cache: MultiprocessingCertCache
92
+ ) -> Callable[[], None]:
93
+ pool = SigningPool(
94
+ None, # uefi_signer
95
+ UefiVariableSign(cert_cache, config.uefi) if config.uefi else None,
96
+ None, # swu_signer
97
+ None, # linux_module_signer
98
+ None, # hab4_signer
99
+ None, # optee_ta_signer
100
+ None, # rpi_signer
101
+ parallel=self.parallel,
102
+ )
103
+
104
+ def process():
105
+ pool.sign(self.jobs)
106
+
107
+ return process
108
+
109
+
110
+ @dataclass
111
+ class SwuRun(SigningRunBase):
112
+ jobs: list[SwuSignJob]
113
+
114
+ def processor_factory(
115
+ self, config: Config, cert_cache: MultiprocessingCertCache
116
+ ) -> Callable[[], None]:
117
+ pool = SigningPool(
118
+ None, # uefi_signer
119
+ None, # UefiVariableSign
120
+ SwuSign(cert_cache, config.swu) if config.swu else None,
121
+ None, # linux_module_signer
122
+ None, # hab4_signer
123
+ None, # optee_ta_signer
124
+ None, # rpi_signer
125
+ parallel=self.parallel,
126
+ )
127
+
128
+ def process():
129
+ pool.sign(self.jobs)
130
+
131
+ return process
132
+
133
+
134
+ @dataclass
135
+ class EfiBinaryRun(SigningRunBase):
136
+ jobs: list[UefiSignJob]
137
+
138
+ def processor_factory(
139
+ self, config: Config, cert_cache: MultiprocessingCertCache
140
+ ) -> Callable[[], None]:
141
+ pool = SigningPool(
142
+ UefiSign(cert_cache, config.uefi, self.force_overwrite) if config.uefi else None,
143
+ None, # uefi_variable_signer
144
+ None, # swu_signer
145
+ None, # linux_module_signer
146
+ None, # hab4_signer
147
+ None, # optee_ta_signer
148
+ None, # rpi_signer
149
+ parallel=self.parallel,
150
+ )
151
+
152
+ def process():
153
+ pool.sign(self.jobs)
154
+
155
+ return process
156
+
157
+
158
+ @dataclass
159
+ class SetupRun:
160
+ config: Path
161
+ setup_command: str | None
162
+
163
+
164
+ debian_example = """examples:
165
+
166
+ To read configuration from /etc/osh/config.yaml from, download and sign the
167
+ architecture specific (amd64) signed-template Debian package
168
+ linux-image-amd64-signed-template version 6.12.41-1, and output a source package
169
+ tree with detached signatures under /tmp/signed:
170
+
171
+ osh --config /etc/osh/config.yaml --output /tmp/signed debsign \\
172
+ --archive debian_org --suite trixie --version 6.12.41-1 \\
173
+ --architecture amd64 \\
174
+ --build \\
175
+ linux-image-amd64-signed-template -- --no-clean-source
176
+
177
+ The exact type of signatures (EFI in case of the linux kernel) and to-be-signed
178
+ files (boot/vmlinuz in case of the linux kernel) is determined by a files.json
179
+ included in signed-template. A final sbuild run will attach the signatures and
180
+ create the signed /boot/vmlinuz. Extra arguments after a literal '--' are passed
181
+ through to that sbuild call verbatim.
182
+ """
183
+
184
+
185
+ uefivarsign_example = """examples:
186
+
187
+ To read configuration from /etc/osh/config.yaml and sign the data blob mydata.bin
188
+ as UEFI variable named mydata
189
+
190
+ osh --config /etc/osh/config.yaml --output /tmp/signed uefivarsign \\
191
+ mydata1:mydata1.bin mydata2:mydata2.bin
192
+
193
+ Optional details for the signing process (like attributes to attach to mydata variable
194
+ or which GUID to assign) will be looked up in config.yaml.
195
+ """
196
+
197
+ swusign_example = """examples:
198
+
199
+ To read configuration from /etc/osh/config.yaml and sign the swu file my.swu
200
+
201
+ osh --config /etc/osh/config.yaml --output /tmp/signed swusign \\
202
+ my.swu
203
+ """
204
+
205
+ efibinarysign_example = """examples:
206
+
207
+ To read configuration from /etc/osh/config.yaml and sign the (U)EFI PE/COFF
208
+ binaries uki.efi and vmlinuz, writing signed binaries /tmp/signed/uki.efi and
209
+ /tmp/signed/vmlinuz that can be booted or verified directly, e.g. with
210
+ 'sbverify --cert cert.pem /tmp/signed/uki.efi':
211
+
212
+ osh --config /etc/osh/config.yaml --output /tmp/signed efibinarysign \\
213
+ uki.efi vmlinuz
214
+
215
+ To instead produce detached signatures (e.g. /tmp/signed/vmlinuz.sig), as used
216
+ by the Debian signing flow where the signature is attached later during the
217
+ package build, pass --detached:
218
+
219
+ osh --config /etc/osh/config.yaml --output /tmp/signed efibinarysign \\
220
+ --detached vmlinuz
221
+ """
222
+
223
+
224
+ class PassthroughParser(argparse.ArgumentParser):
225
+ def __init__(self, *args, passthrough: bool = False, **kwargs):
226
+ self._passthrough = passthrough
227
+ super().__init__(*args, **kwargs)
228
+
229
+ def parse_known_args(self, args=None, namespace=None):
230
+ passthrough_args: list[str] = []
231
+ if self._passthrough and args and "--" in args:
232
+ sep = args.index("--")
233
+ args, passthrough_args = args[:sep], args[sep + 1 :]
234
+ namespace, extras = super().parse_known_args(args, namespace)
235
+ if self._passthrough:
236
+ namespace.passthrough_args = passthrough_args
237
+ return namespace, extras
238
+
239
+
240
+ def parse_args(arg_list: list[str] | None = None) -> SigningRunBase | SetupRun:
241
+ parser = argparse.ArgumentParser(
242
+ description="Sign artifacts or packages according to various schemes."
243
+ )
244
+ parser.add_argument(
245
+ "-v", "--version", action="version", version=f"%(prog)s {version('opensighub')}"
246
+ )
247
+ parser.add_argument(
248
+ "-c",
249
+ "--config",
250
+ default=str(DEFAULT_CONFIG_PATH),
251
+ help=f"Config file to load. Defaults to {DEFAULT_CONFIG_PATH}.",
252
+ )
253
+ parser.add_argument(
254
+ "-p", "--parallel", help="Number of concurrent signing operations.", type=int, default=5
255
+ )
256
+ parser.add_argument(
257
+ "-o",
258
+ "--output",
259
+ default=".",
260
+ help="Directory where to place signed files. Defaults to current working directory.",
261
+ )
262
+ parser.add_argument(
263
+ "-y",
264
+ "--yes",
265
+ action="store_true",
266
+ help="Answer y/n confirmation prompts with 'y' instead of asking or aborting.",
267
+ )
268
+ sub_parsers = parser.add_subparsers(dest="command", parser_class=PassthroughParser)
269
+ debsign_parser = sub_parsers.add_parser(
270
+ "debsign",
271
+ description="Sign a package from an apt archive.",
272
+ epilog=debian_example,
273
+ formatter_class=argparse.RawDescriptionHelpFormatter,
274
+ passthrough=True,
275
+ )
276
+ debsign_parser.add_argument(
277
+ "--archive",
278
+ help="Refers to archive mapping from config file. osh uses it to build "
279
+ "sources.list entries to download signed-template and dependencies.",
280
+ )
281
+ debsign_parser.add_argument(
282
+ "--suite",
283
+ help="The apt archive may have multiple suites. This options selects "
284
+ "the codename of a suite, e.g. bookworm or trixie, "
285
+ "where to download the signed-template and dependencies.",
286
+ )
287
+ debsign_parser.add_argument(
288
+ "--version",
289
+ help="The apt archive may contain multiple versions of a "
290
+ "signed-template. This options specifies the version to download.",
291
+ )
292
+ debsign_parser.add_argument(
293
+ "--architecture",
294
+ help="The apt archive may contain a signed-template (and dependencies) "
295
+ "for multiple architectures side by side. This selects the architecture "
296
+ "to download. Values are the same as for sbuild (1) --host=archtiecture.",
297
+ )
298
+ debsign_parser.add_argument(
299
+ "templates",
300
+ nargs="+",
301
+ help="One or more Debian signed-template binary packages. For each, a "
302
+ "sub directory with the name of the new signed source package name "
303
+ "as per debian/changelog will be created under the output directory.",
304
+ )
305
+ debsign_parser.add_argument(
306
+ "--build",
307
+ action="store_true",
308
+ help="Build the signed source package using sbuild to final deb. Default: False. "
309
+ "Extra arguments after a literal '--' are passed through to sbuild.",
310
+ )
311
+ uefi_parser = sub_parsers.add_parser(
312
+ "uefivarsign",
313
+ description="Sign arbitrary data blob as UEFI authenticated variable. "
314
+ "The signed output file name is calculated by appending '.auth' to the variable name. "
315
+ "Optional details for signing a variable can be configured in the config file.",
316
+ epilog=uefivarsign_example,
317
+ formatter_class=argparse.RawDescriptionHelpFormatter,
318
+ )
319
+ uefi_parser.add_argument(
320
+ "variables",
321
+ nargs="+",
322
+ help="One or more variable:blob pairs. Well known variable names are db, dbx, pk, kek, "
323
+ "but custom names are also supported. Blob paths are absolut, or relative to the "
324
+ "current working directory.",
325
+ )
326
+ swu_parser = sub_parsers.add_parser(
327
+ "swusign",
328
+ description="Sign or resign an existing SW-Update file generated for swupdate. "
329
+ "The signed output file name is stored in the output directory using the given input file name. ",
330
+ epilog=swusign_example,
331
+ formatter_class=argparse.RawDescriptionHelpFormatter,
332
+ )
333
+ swu_parser.add_argument("swu", help="The swu file to sign")
334
+ efibinary_parser = sub_parsers.add_parser(
335
+ "efibinarysign",
336
+ description="Sign one or more (U)EFI PE/COFF binaries (e.g. uki.efi, "
337
+ "vmlinuz) with sbsign. By default the signature is embedded into the "
338
+ "binary (matching sbsign's default), producing a binary that can be "
339
+ "booted or verified directly; it is written using the input file name "
340
+ "in the output directory. With --detached a detached signature is "
341
+ "produced instead, named by appending '.sig' to the input file name.",
342
+ epilog=efibinarysign_example,
343
+ formatter_class=argparse.RawDescriptionHelpFormatter,
344
+ )
345
+ efibinary_parser.add_argument(
346
+ "--detached",
347
+ action="store_true",
348
+ help="Produce a detached '.sig' signature file instead of embedding the "
349
+ "signature into the binary. Useful for the Debian signing flow where the "
350
+ "signature is attached later during the package build.",
351
+ )
352
+ efibinary_parser.add_argument(
353
+ "binaries",
354
+ nargs="+",
355
+ help="One or more (U)EFI binaries to sign. Paths are absolute, or "
356
+ "relative to the current working directory.",
357
+ )
358
+ setup_parser = sub_parsers.add_parser(
359
+ "setup",
360
+ description="Set up user-local environment for osh to help getting started.",
361
+ )
362
+ setup_sub_parsers = setup_parser.add_subparsers(dest="setup_command")
363
+ setup_sub_parsers.add_parser(
364
+ "softhsm",
365
+ description="Set up an isolated, user-local SoftHSM token for test purpose.",
366
+ )
367
+ setup_sub_parsers.add_parser(
368
+ "testkeys",
369
+ description="Generate a self-signed test key in the local SoftHSM token for test purpose"
370
+ " and suitable configuration file.",
371
+ )
372
+ args = parser.parse_args(arg_list)
373
+ if args.command == "setup":
374
+ return SetupRun(config=Path(args.config), setup_command=args.setup_command)
375
+ if args.command == "debsign" and args.passthrough_args and not args.build:
376
+ parser.error("arguments after '--' require debsign --build")
377
+ if args.command == "swusign":
378
+ outfile = Path(args.output) / Path(args.swu).name
379
+ return SwuRun(
380
+ config=Path(args.config),
381
+ output=outfile,
382
+ jobs=[SwuSignJob(Path(args.swu), outfile)],
383
+ parallel=args.parallel,
384
+ force_overwrite=args.yes,
385
+ )
386
+ if args.command == "efibinarysign":
387
+ detached = args.detached
388
+ return EfiBinaryRun(
389
+ config=Path(args.config),
390
+ output=Path(args.output),
391
+ jobs=[
392
+ UefiSignJob(
393
+ artifact=Path(binary),
394
+ signed_artifact=Path(args.output)
395
+ / (Path(binary).name + ".sig" if detached else Path(binary).name),
396
+ detached=detached,
397
+ )
398
+ for binary in args.binaries
399
+ ],
400
+ parallel=args.parallel,
401
+ force_overwrite=args.yes,
402
+ )
403
+ if args.command == "uefivarsign":
404
+ return UefiVariableRun(
405
+ config=Path(args.config),
406
+ output=Path(args.output),
407
+ jobs=[
408
+ UefiVariableSignJob(
409
+ name, Path(blob), (Path(args.output) / name).with_suffix(".auth")
410
+ )
411
+ for name, blob in (v.split(":") for v in args.variables)
412
+ ],
413
+ parallel=args.parallel,
414
+ force_overwrite=args.yes,
415
+ )
416
+ elif args.command == "debsign":
417
+ return DebianRun(
418
+ config=Path(args.config),
419
+ output=Path(args.output),
420
+ jobs=[
421
+ DebianSigningJob(
422
+ signing_template=template,
423
+ version=args.version,
424
+ architecture=args.architecture,
425
+ suite_codename=args.suite,
426
+ archive_id=args.archive,
427
+ )
428
+ for template in args.templates
429
+ ],
430
+ parallel=args.parallel,
431
+ force_overwrite=args.yes,
432
+ run_sbuild=args.build,
433
+ sbuild_args=args.passthrough_args,
434
+ )
435
+ raise NotImplementedError
436
+
437
+
438
+ def sign_main(run_config: SigningRunBase):
439
+ logger = logging.getLogger("opensighub")
440
+
441
+ if not run_config or run_config is NotImplementedError:
442
+ return
443
+
444
+ try:
445
+ with open(run_config.config) as fp:
446
+ cfg_dict = yaml.safe_load(fp)
447
+ except OSError as e:
448
+ raise OpensighubError(f"Could not read config file: {e}") from e
449
+ config = Config.from_dict(cfg_dict)
450
+
451
+ logger.setLevel(config.log_level)
452
+
453
+ with multiprocessing.Manager() as manager:
454
+ shared_data = manager.dict()
455
+ shared_data_lock = manager.Lock()
456
+ with MultiprocessingCertCache(shared_data, shared_data_lock) as cert_cache:
457
+ process = run_config.processor_factory(config, cert_cache)
458
+ process()
459
+
460
+
461
+ def run_setup(run_config: SetupRun):
462
+ if run_config.setup_command == "softhsm":
463
+ setup.setup_local_token(run_config.config)
464
+ elif run_config.setup_command == "testkeys":
465
+ setup.setup_testenv_keys(run_config.config)
466
+ else:
467
+ raise NotImplementedError
468
+
469
+
470
+ def main():
471
+ logging.basicConfig(format="%(message)s")
472
+ logging.getLogger("opensighub").setLevel(logging.INFO)
473
+
474
+ run_config = parse_args()
475
+ setup.enable_local_softhsm2(run_config.config)
476
+
477
+ try:
478
+ if isinstance(run_config, SetupRun):
479
+ run_setup(run_config)
480
+ return
481
+ sign_main(run_config)
482
+ except OpensighubError as e:
483
+ print(f"osh: error: {e}", file=sys.stderr)
484
+ sys.exit(1)
485
+
486
+
487
+ if __name__ == "__main__":
488
+ main()