EmbeddedSigner 0.1.1__tar.gz

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.
@@ -0,0 +1,5 @@
1
+ """Expose the :class:`EmbedSigner` orchestrator."""
2
+
3
+ from ._embed_signer import EmbedSigner
4
+
5
+ __all__ = ["EmbedSigner"]
@@ -0,0 +1,341 @@
1
+ """Embed XMP metadata and sign media payloads with Swarmauri plugins."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ from collections.abc import Callable, Mapping, Sequence
7
+ from dataclasses import dataclass
8
+ from importlib import metadata
9
+ from pathlib import Path
10
+ from typing import Any, MutableMapping, Optional
11
+ from urllib.parse import parse_qs, urlparse
12
+
13
+ from EmbedXMP import EmbedXMP
14
+ from MediaSigner import MediaSigner
15
+ from swarmauri_core.crypto.types import KeyRef
16
+ from swarmauri_core.key_providers.IKeyProvider import IKeyProvider
17
+ from swarmauri_core.signing.types import Signature
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class _ParsedKeyRef:
22
+ provider: Optional[str]
23
+ kid: str
24
+ version: Optional[int]
25
+
26
+
27
+ class EmbedSigner:
28
+ """High-level helper that embeds XMP metadata and produces signatures."""
29
+
30
+ def __init__(
31
+ self,
32
+ *,
33
+ embedder: Optional[EmbedXMP] = None,
34
+ signer: Optional[MediaSigner] = None,
35
+ key_provider: Optional[IKeyProvider] = None,
36
+ key_provider_name: Optional[str] = None,
37
+ provider_plugins: Optional[
38
+ Mapping[str, Callable[[], IKeyProvider] | type[IKeyProvider]]
39
+ ] = None,
40
+ eager_import: bool = True,
41
+ ) -> None:
42
+ self._embedder = embedder or EmbedXMP(eager_import=eager_import)
43
+ self._provider_factories: MutableMapping[
44
+ str, Callable[[], IKeyProvider] | type[IKeyProvider]
45
+ ] = self._discover_provider_factories()
46
+ if provider_plugins:
47
+ self._provider_factories.update(dict(provider_plugins))
48
+ self._providers: MutableMapping[str, IKeyProvider] = {}
49
+ self._default_provider: Optional[IKeyProvider] = None
50
+ self._default_provider_name: Optional[str] = None
51
+
52
+ if key_provider is not None:
53
+ self._default_provider = key_provider
54
+ self._default_provider_name = key_provider.__class__.__name__
55
+ elif key_provider_name is not None:
56
+ self._default_provider = self._instantiate_provider(key_provider_name)
57
+ self._default_provider_name = key_provider_name
58
+
59
+ provider_for_signer = self._default_provider
60
+ self._signer = signer or MediaSigner(key_provider=provider_for_signer)
61
+
62
+ # ------------------------------------------------------------------
63
+ @staticmethod
64
+ def _discover_provider_factories() -> MutableMapping[
65
+ str, Callable[[], IKeyProvider] | type[IKeyProvider]
66
+ ]:
67
+ factories: MutableMapping[
68
+ str, Callable[[], IKeyProvider] | type[IKeyProvider]
69
+ ] = {}
70
+ try:
71
+ entries = metadata.entry_points(group="swarmauri.key_providers")
72
+ except TypeError: # pragma: no cover - Python <3.10 fallback
73
+ entries = metadata.entry_points()["swarmauri.key_providers"]
74
+ for entry in entries:
75
+ try:
76
+ provider_cls = entry.load()
77
+ except (
78
+ Exception
79
+ ): # pragma: no cover - defensive against broken entry points
80
+ continue
81
+ if not inspect.isclass(provider_cls) and not callable(provider_cls):
82
+ continue
83
+ factories[entry.name] = provider_cls
84
+ return factories
85
+
86
+ def _instantiate_provider(self, name: str) -> IKeyProvider:
87
+ if name in self._providers:
88
+ return self._providers[name]
89
+ factory = self._provider_factories.get(name)
90
+ if factory is None:
91
+ raise ValueError(f"Unknown key provider '{name}'")
92
+ provider: IKeyProvider
93
+ if inspect.isclass(factory):
94
+ provider = factory() # type: ignore[call-arg]
95
+ else:
96
+ provider = factory()
97
+ self._providers[name] = provider
98
+ return provider
99
+
100
+ def _get_provider(self, name: Optional[str]) -> IKeyProvider:
101
+ if name is None:
102
+ if self._default_provider is not None:
103
+ return self._default_provider
104
+ if self._default_provider_name is not None:
105
+ return self._instantiate_provider(self._default_provider_name)
106
+ raise ValueError(
107
+ "No default key provider configured; include a provider name in the key reference."
108
+ )
109
+ if name == self._default_provider_name and self._default_provider is not None:
110
+ return self._default_provider
111
+ return self._instantiate_provider(name)
112
+
113
+ # ------------------------------------------------------------------
114
+ @staticmethod
115
+ def _parse_key_reference(value: str) -> _ParsedKeyRef:
116
+ parsed = urlparse(value)
117
+ if parsed.scheme:
118
+ provider = parsed.scheme
119
+ kid = (parsed.netloc + parsed.path).lstrip("/")
120
+ query = parse_qs(parsed.query, keep_blank_values=False)
121
+ version = None
122
+ if "version" in query and query["version"]:
123
+ try:
124
+ version = int(query["version"][0])
125
+ except (TypeError, ValueError) as exc:
126
+ raise ValueError(
127
+ f"Invalid version component in key reference '{value}'"
128
+ ) from exc
129
+ return _ParsedKeyRef(provider=provider, kid=kid, version=version)
130
+ token = value
131
+ provider_name = None
132
+ if ":" in token and not token.startswith("{"):
133
+ provider_name, _, token = token.partition(":")
134
+ kid, sep, version_token = token.partition("@")
135
+ version_number = None
136
+ if sep:
137
+ try:
138
+ version_number = int(version_token)
139
+ except ValueError as exc:
140
+ raise ValueError(
141
+ f"Invalid version component in key reference '{value}'"
142
+ ) from exc
143
+ if not kid:
144
+ raise ValueError("Key reference must include a key identifier")
145
+ return _ParsedKeyRef(provider=provider_name, kid=kid, version=version_number)
146
+
147
+ async def _resolve_key(
148
+ self, key: KeyRef | Mapping[str, Any] | str
149
+ ) -> KeyRef | Mapping[str, Any]:
150
+ if isinstance(key, Mapping):
151
+ return key
152
+ if isinstance(key, KeyRef):
153
+ return key
154
+ if isinstance(key, str):
155
+ ref = self._parse_key_reference(key)
156
+ provider = self._get_provider(ref.provider)
157
+ try:
158
+ resolved = await provider.get_key_by_ref(ref.kid, include_secret=True)
159
+ except NotImplementedError:
160
+ resolved = None
161
+ except AttributeError: # pragma: no cover - provider without method
162
+ resolved = None
163
+ if resolved is None:
164
+ resolved = await provider.get_key(
165
+ ref.kid, version=ref.version, include_secret=True
166
+ )
167
+ return resolved
168
+ raise TypeError(
169
+ "key must be a Mapping, KeyRef, or string reference understood by EmbedSigner"
170
+ )
171
+
172
+ # ------------------------------------------------------------------
173
+ def embed_bytes(
174
+ self, data: bytes, xmp_xml: str, *, path: str | Path | None = None
175
+ ) -> bytes:
176
+ """Embed XMP metadata into *data* using the configured handlers."""
177
+
178
+ ref = str(path) if path is not None else None
179
+ return self._embedder.embed(data, xmp_xml, ref)
180
+
181
+ def read_xmp(self, data: bytes, *, path: str | Path | None = None) -> str | None:
182
+ """Read XMP metadata from *data* using the configured handlers."""
183
+
184
+ ref = str(path) if path is not None else None
185
+ return self._embedder.read(data, ref)
186
+
187
+ def remove_xmp(self, data: bytes, *, path: str | Path | None = None) -> bytes:
188
+ """Remove XMP metadata from *data* using the configured handlers."""
189
+
190
+ ref = str(path) if path is not None else None
191
+ return self._embedder.remove(data, ref)
192
+
193
+ def embed_file(
194
+ self,
195
+ path: str | Path,
196
+ xmp_xml: str,
197
+ *,
198
+ write_back: bool = True,
199
+ output: str | Path | None = None,
200
+ ) -> bytes:
201
+ """Embed XMP metadata directly into a file on disk."""
202
+
203
+ file_path = Path(path)
204
+ updated = self.embed_bytes(file_path.read_bytes(), xmp_xml, path=file_path)
205
+ target = Path(output) if output is not None else file_path
206
+ if write_back:
207
+ target.write_bytes(updated)
208
+ return updated
209
+
210
+ def read_xmp_file(self, path: str | Path) -> str | None:
211
+ """Read XMP metadata from the file located at *path*."""
212
+
213
+ file_path = Path(path)
214
+ return self.read_xmp(file_path.read_bytes(), path=file_path)
215
+
216
+ def remove_xmp_file(
217
+ self,
218
+ path: str | Path,
219
+ *,
220
+ write_back: bool = False,
221
+ output: str | Path | None = None,
222
+ ) -> bytes:
223
+ """Remove XMP metadata from a file, optionally persisting the result."""
224
+
225
+ file_path = Path(path)
226
+ updated = self.remove_xmp(file_path.read_bytes(), path=file_path)
227
+ target = Path(output) if output is not None else file_path
228
+ if write_back:
229
+ target.write_bytes(updated)
230
+ return updated
231
+
232
+ # ------------------------------------------------------------------
233
+ async def sign_bytes(
234
+ self,
235
+ fmt: str,
236
+ key: KeyRef | Mapping[str, Any] | str,
237
+ payload: bytes,
238
+ *,
239
+ attached: bool = True,
240
+ alg: Optional[str] = None,
241
+ signer_opts: Optional[Mapping[str, Any]] = None,
242
+ ) -> Sequence[Signature]:
243
+ """Produce signatures for *payload* using the requested signer format."""
244
+
245
+ resolved_key = await self._resolve_key(key)
246
+ opts: dict[str, Any] = dict(signer_opts or {})
247
+ opts.setdefault("attached", attached)
248
+ return await self._signer.sign_bytes(
249
+ fmt, resolved_key, payload, alg=alg, opts=opts
250
+ )
251
+
252
+ async def embed_and_sign_bytes(
253
+ self,
254
+ data: bytes,
255
+ *,
256
+ fmt: str,
257
+ xmp_xml: str,
258
+ key: KeyRef | Mapping[str, Any] | str,
259
+ path: str | Path | None = None,
260
+ attached: bool = True,
261
+ alg: Optional[str] = None,
262
+ signer_opts: Optional[Mapping[str, Any]] = None,
263
+ ) -> tuple[bytes, Sequence[Signature]]:
264
+ """Embed metadata into *data* and sign the updated payload."""
265
+
266
+ embedded = self.embed_bytes(data, xmp_xml, path=path)
267
+ signatures = await self.sign_bytes(
268
+ fmt,
269
+ key,
270
+ embedded,
271
+ attached=attached,
272
+ alg=alg,
273
+ signer_opts=signer_opts,
274
+ )
275
+ return embedded, signatures
276
+
277
+ async def embed_and_sign_file(
278
+ self,
279
+ path: str | Path,
280
+ *,
281
+ fmt: str,
282
+ xmp_xml: str,
283
+ key: KeyRef | Mapping[str, Any] | str,
284
+ attached: bool = True,
285
+ alg: Optional[str] = None,
286
+ signer_opts: Optional[Mapping[str, Any]] = None,
287
+ write_back: bool = False,
288
+ ) -> tuple[bytes, Sequence[Signature]]:
289
+ """Embed metadata into *path* and optionally persist the signed bytes."""
290
+
291
+ file_path = Path(path)
292
+ embedded, signatures = await self.embed_and_sign_bytes(
293
+ file_path.read_bytes(),
294
+ fmt=fmt,
295
+ xmp_xml=xmp_xml,
296
+ key=key,
297
+ path=file_path,
298
+ attached=attached,
299
+ alg=alg,
300
+ signer_opts=signer_opts,
301
+ )
302
+ if write_back:
303
+ file_path.write_bytes(embedded)
304
+ return embedded, signatures
305
+
306
+ async def sign_file(
307
+ self,
308
+ path: str | Path,
309
+ *,
310
+ fmt: str,
311
+ key: KeyRef | Mapping[str, Any] | str,
312
+ attached: bool = True,
313
+ alg: Optional[str] = None,
314
+ signer_opts: Optional[Mapping[str, Any]] = None,
315
+ ) -> Sequence[Signature]:
316
+ """Sign the contents of *path* and return the generated signatures."""
317
+
318
+ file_path = Path(path)
319
+ payload = file_path.read_bytes()
320
+ return await self.sign_bytes(
321
+ fmt,
322
+ key,
323
+ payload,
324
+ attached=attached,
325
+ alg=alg,
326
+ signer_opts=signer_opts,
327
+ )
328
+
329
+ # ------------------------------------------------------------------
330
+ def supported_embed_handlers(self) -> Sequence[str]:
331
+ """Return the names of registered XMP handlers."""
332
+
333
+ return tuple(
334
+ handler.__class__.__name__
335
+ for handler in getattr(self._embedder, "_handlers", [])
336
+ )
337
+
338
+ def supported_signers(self) -> Sequence[str]:
339
+ """Expose signer formats advertised by the underlying :class:`MediaSigner`."""
340
+
341
+ return tuple(self._signer.supported_formats())
@@ -0,0 +1,325 @@
1
+ """Command line interface for the :mod:`EmbeddedSigner` plugin."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import asyncio
7
+ import base64
8
+ import json
9
+ import sys
10
+ from pathlib import Path
11
+ from typing import Any, Mapping, Sequence
12
+
13
+ from ._embed_signer import EmbedSigner
14
+
15
+
16
+ def _load_xmp_from_args(args: argparse.Namespace) -> str:
17
+ if args.xmp is not None and args.xmp_file is not None:
18
+ raise SystemExit("Specify either --xmp or --xmp-file, not both.")
19
+ if args.xmp is not None:
20
+ return args.xmp
21
+ if args.xmp_file is not None:
22
+ return Path(args.xmp_file).read_text(encoding="utf-8")
23
+ raise SystemExit("An XMP payload is required for this command.")
24
+
25
+
26
+ def _resolve_key_argument(args: argparse.Namespace) -> Any:
27
+ provided = [
28
+ value
29
+ for value in (args.key_ref, args.key_json, args.key_file)
30
+ if value is not None
31
+ ]
32
+ if len(provided) != 1:
33
+ raise SystemExit("Provide exactly one of --key-ref, --key-json, or --key-file.")
34
+ if args.key_ref:
35
+ return args.key_ref
36
+ if args.key_json:
37
+ try:
38
+ return json.loads(args.key_json)
39
+ except json.JSONDecodeError as exc: # pragma: no cover - defensive
40
+ raise SystemExit(f"Invalid JSON passed to --key-json: {exc}") from exc
41
+ if args.key_file:
42
+ return json.loads(Path(args.key_file).read_text(encoding="utf-8"))
43
+ raise SystemExit("A key reference or JSON description must be supplied.")
44
+
45
+
46
+ def _serialise_signature(signature: Mapping[str, Any]) -> Mapping[str, Any]:
47
+ payload: dict[str, Any] = dict(signature)
48
+ artifact = payload.get("artifact")
49
+ if isinstance(artifact, (bytes, bytearray)):
50
+ payload["artifact"] = base64.b64encode(artifact).decode("ascii")
51
+ payload.setdefault("artifact_encoding", "base64")
52
+ return payload
53
+
54
+
55
+ def _create_signer(args: argparse.Namespace) -> EmbedSigner:
56
+ provider_name = getattr(args, "key_provider", None)
57
+ return EmbedSigner(key_provider_name=provider_name)
58
+
59
+
60
+ def _command_embed(args: argparse.Namespace) -> int:
61
+ signer = _create_signer(args)
62
+ xmp_xml = _load_xmp_from_args(args)
63
+ src = Path(args.input)
64
+ payload = src.read_bytes()
65
+ updated = signer.embed_bytes(payload, xmp_xml, path=src)
66
+ target = Path(args.output) if args.output else src
67
+ target.write_bytes(updated)
68
+ return 0
69
+
70
+
71
+ def _command_read(args: argparse.Namespace) -> int:
72
+ signer = _create_signer(args)
73
+ src = Path(args.input)
74
+ metadata = signer.read_xmp_file(src)
75
+ if metadata is None:
76
+ return 1
77
+ sys.stdout.write(metadata)
78
+ if not metadata.endswith("\n"):
79
+ sys.stdout.write("\n")
80
+ return 0
81
+
82
+
83
+ def _command_remove(args: argparse.Namespace) -> int:
84
+ signer = _create_signer(args)
85
+ src = Path(args.input)
86
+ updated = signer.remove_xmp_file(src)
87
+ target = Path(args.output) if args.output else src
88
+ target.write_bytes(updated)
89
+ return 0
90
+
91
+
92
+ async def _async_sign(
93
+ signer: EmbedSigner,
94
+ *,
95
+ src: Path,
96
+ fmt: str,
97
+ key: Any,
98
+ attached: bool,
99
+ alg: str | None,
100
+ opts: Mapping[str, Any] | None,
101
+ ) -> Sequence[Mapping[str, Any]]:
102
+ signatures = await signer.sign_file(
103
+ src,
104
+ fmt=fmt,
105
+ key=key,
106
+ attached=attached,
107
+ alg=alg,
108
+ signer_opts=opts,
109
+ )
110
+ return [_serialise_signature(sig) for sig in signatures]
111
+
112
+
113
+ def _command_sign(args: argparse.Namespace) -> int:
114
+ signer = _create_signer(args)
115
+ src = Path(args.input)
116
+ key = _resolve_key_argument(args)
117
+ opts: dict[str, Any] | None = None
118
+ if args.option:
119
+ opts = {}
120
+ for token in args.option:
121
+ name, _, value = token.partition("=")
122
+ if not name:
123
+ raise SystemExit("Signer options must use the form name=value.")
124
+ opts[name] = value
125
+ signatures = asyncio.run(
126
+ _async_sign(
127
+ signer,
128
+ src=src,
129
+ fmt=args.format,
130
+ key=key,
131
+ attached=not args.detached,
132
+ alg=args.alg,
133
+ opts=opts,
134
+ )
135
+ )
136
+ output = json.dumps(signatures, indent=2)
137
+ if args.output:
138
+ Path(args.output).write_text(output, encoding="utf-8")
139
+ else:
140
+ sys.stdout.write(output + "\n")
141
+ return 0
142
+
143
+
144
+ async def _async_embed_sign(
145
+ signer: EmbedSigner,
146
+ *,
147
+ src: Path,
148
+ fmt: str,
149
+ xmp_xml: str,
150
+ key: Any,
151
+ attached: bool,
152
+ alg: str | None,
153
+ opts: Mapping[str, Any] | None,
154
+ write_back: bool,
155
+ output: Path | None,
156
+ ) -> Sequence[Mapping[str, Any]]:
157
+ embedded, signatures = await signer.embed_and_sign_file(
158
+ src,
159
+ fmt=fmt,
160
+ xmp_xml=xmp_xml,
161
+ key=key,
162
+ attached=attached,
163
+ alg=alg,
164
+ signer_opts=opts,
165
+ write_back=write_back and output is None,
166
+ )
167
+ if output is not None:
168
+ output.write_bytes(embedded)
169
+ return [_serialise_signature(sig) for sig in signatures]
170
+
171
+
172
+ def _command_embed_sign(args: argparse.Namespace) -> int:
173
+ signer = _create_signer(args)
174
+ src = Path(args.input)
175
+ xmp_xml = _load_xmp_from_args(args)
176
+ key = _resolve_key_argument(args)
177
+ opts: dict[str, Any] | None = None
178
+ if args.option:
179
+ opts = {}
180
+ for token in args.option:
181
+ name, _, value = token.partition("=")
182
+ if not name:
183
+ raise SystemExit("Signer options must use the form name=value.")
184
+ opts[name] = value
185
+ output_path = Path(args.output) if args.output else None
186
+ signatures = asyncio.run(
187
+ _async_embed_sign(
188
+ signer,
189
+ src=src,
190
+ fmt=args.format,
191
+ xmp_xml=xmp_xml,
192
+ key=key,
193
+ attached=not args.detached,
194
+ alg=args.alg,
195
+ opts=opts,
196
+ write_back=args.write_back,
197
+ output=output_path,
198
+ )
199
+ )
200
+ output = json.dumps(signatures, indent=2)
201
+ if args.signature_output:
202
+ Path(args.signature_output).write_text(output, encoding="utf-8")
203
+ else:
204
+ sys.stdout.write(output + "\n")
205
+ return 0
206
+
207
+
208
+ def build_parser() -> argparse.ArgumentParser:
209
+ parser = argparse.ArgumentParser(description="Embed XMP metadata and sign media.")
210
+ parser.add_argument(
211
+ "--key-provider",
212
+ dest="key_provider",
213
+ help="Default key provider to use when resolving key references.",
214
+ )
215
+ subparsers = parser.add_subparsers(dest="command", required=True)
216
+
217
+ embed_parser = subparsers.add_parser("embed", help="Embed XMP into a file")
218
+ embed_parser.add_argument("input", help="Path to the media file")
219
+ embed_parser.add_argument("--xmp", help="Inline XMP XML payload")
220
+ embed_parser.add_argument("--xmp-file", help="Read the XMP payload from a file")
221
+ embed_parser.add_argument("--output", help="Write the embedded media to this path")
222
+ embed_parser.set_defaults(func=_command_embed)
223
+
224
+ read_parser = subparsers.add_parser("read", help="Read XMP metadata from a file")
225
+ read_parser.add_argument("input", help="Path to the media file")
226
+ read_parser.set_defaults(func=_command_read)
227
+
228
+ remove_parser = subparsers.add_parser(
229
+ "remove", help="Strip XMP metadata from a file"
230
+ )
231
+ remove_parser.add_argument("input", help="Path to the media file")
232
+ remove_parser.add_argument("--output", help="Write the stripped media to this path")
233
+ remove_parser.set_defaults(func=_command_remove)
234
+
235
+ sign_parser = subparsers.add_parser("sign", help="Generate signatures for a file")
236
+ sign_parser.add_argument("input", help="Path to the media file")
237
+ sign_parser.add_argument("--format", required=True, help="MediaSigner format name")
238
+ sign_parser.add_argument("--key-ref", dest="key_ref", help="Key reference string")
239
+ sign_parser.add_argument(
240
+ "--key-json",
241
+ dest="key_json",
242
+ help="Inline JSON description of the signing key",
243
+ )
244
+ sign_parser.add_argument(
245
+ "--key-file",
246
+ dest="key_file",
247
+ help="Path to a JSON file describing the signing key",
248
+ )
249
+ sign_parser.add_argument(
250
+ "--option",
251
+ action="append",
252
+ help="Additional signer option in the form name=value",
253
+ )
254
+ sign_parser.add_argument("--alg", help="Explicit algorithm identifier")
255
+ sign_parser.add_argument(
256
+ "--detached",
257
+ action="store_true",
258
+ help="Produce detached signatures when supported",
259
+ )
260
+ sign_parser.add_argument(
261
+ "--output", help="Write the generated signatures to a file"
262
+ )
263
+ sign_parser.set_defaults(func=_command_sign)
264
+
265
+ embed_sign_parser = subparsers.add_parser(
266
+ "embed-sign", help="Embed metadata and sign the updated file"
267
+ )
268
+ embed_sign_parser.add_argument("input", help="Path to the media file")
269
+ embed_sign_parser.add_argument("--xmp", help="Inline XMP XML payload")
270
+ embed_sign_parser.add_argument(
271
+ "--xmp-file", help="Read the XMP payload from a file"
272
+ )
273
+ embed_sign_parser.add_argument(
274
+ "--format", required=True, help="MediaSigner format name"
275
+ )
276
+ embed_sign_parser.add_argument(
277
+ "--key-ref", dest="key_ref", help="Key reference string"
278
+ )
279
+ embed_sign_parser.add_argument(
280
+ "--key-json",
281
+ dest="key_json",
282
+ help="Inline JSON description of the signing key",
283
+ )
284
+ embed_sign_parser.add_argument(
285
+ "--key-file",
286
+ dest="key_file",
287
+ help="Path to a JSON file describing the signing key",
288
+ )
289
+ embed_sign_parser.add_argument(
290
+ "--option",
291
+ action="append",
292
+ help="Additional signer option in the form name=value",
293
+ )
294
+ embed_sign_parser.add_argument("--alg", help="Explicit algorithm identifier")
295
+ embed_sign_parser.add_argument(
296
+ "--detached",
297
+ action="store_true",
298
+ help="Produce detached signatures when supported",
299
+ )
300
+ embed_sign_parser.add_argument(
301
+ "--write-back",
302
+ action="store_true",
303
+ help="Persist embedded bytes back to the original input file",
304
+ )
305
+ embed_sign_parser.add_argument(
306
+ "--output",
307
+ help="Write embedded bytes to this path instead of the input file",
308
+ )
309
+ embed_sign_parser.add_argument(
310
+ "--signature-output",
311
+ help="Write generated signatures to this JSON file",
312
+ )
313
+ embed_sign_parser.set_defaults(func=_command_embed_sign)
314
+
315
+ return parser
316
+
317
+
318
+ def main(argv: Sequence[str] | None = None) -> int:
319
+ parser = build_parser()
320
+ args = parser.parse_args(argv)
321
+ return args.func(args)
322
+
323
+
324
+ if __name__ == "__main__": # pragma: no cover - CLI entry point
325
+ sys.exit(main())