pyhanko-cli 0.1.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.
@@ -0,0 +1,144 @@
1
+ import asyncio
2
+ from datetime import datetime
3
+ from typing import List, Optional
4
+
5
+ import click
6
+ import tzlocal
7
+ from asn1crypto import pem
8
+ from pyhanko.cli._ctx import CLIContext
9
+ from pyhanko.cli.commands.signing.utils import get_text_params, open_for_signing
10
+ from pyhanko.cli.plugin_api import SigningCommandPlugin
11
+ from pyhanko.cli.runtime import pyhanko_exception_manager
12
+ from pyhanko.cli.utils import readable_file, writable_file
13
+ from pyhanko.pdf_utils.rw_common import PdfHandler
14
+ from pyhanko.sign import PdfSigner, fields
15
+ from pyhanko.sign.signers.pdf_cms import (
16
+ PdfCMSSignedAttributes,
17
+ select_suitable_signing_md,
18
+ )
19
+ from pyhanko.sign.timestamps import HTTPTimeStamper
20
+
21
+
22
+ def _ensure_field_visible(
23
+ handler: PdfHandler, name: Optional[str], will_create: bool
24
+ ):
25
+ # verify if the resulting signature will be a visible one
26
+ prefix = "You seem to be trying to create a visible signature"
27
+ try:
28
+ fq_name, _, field_ref = next(
29
+ fields.enumerate_sig_fields(handler, with_name=name)
30
+ )
31
+ sig_annot = fields.get_sig_field_annot(sig_field=field_ref.get_object())
32
+ w, h = fields.annot_width_height(sig_annot)
33
+ if not w or not h:
34
+ raise click.ClickException(
35
+ f"{prefix}, but the field '{fq_name}' in the PDF is not a "
36
+ f"visible one. Please specify another field name if you "
37
+ f"need a visible signature."
38
+ )
39
+ except StopIteration:
40
+ if not name:
41
+ raise click.ClickException(
42
+ f"{prefix}, but the PDF did not contain any signature fields, "
43
+ f"and you did not specify a bounding box. "
44
+ f"Please specify the field as "
45
+ f"--field \"PAGE/X1,Y1,X2,Y2/NAME\" to "
46
+ f"create a visible signature field at the coordinates provided."
47
+ )
48
+ elif not will_create:
49
+ raise click.ClickException(
50
+ f"{prefix}, but the field '{name}' does not exist in the PDF "
51
+ f"file, and you did not specify a bounding box. "
52
+ f"Please specify the field as "
53
+ f"--field \"PAGE/X1,Y1,X2,Y2/{name}\" to "
54
+ f"create a visible signature field at the coordinates provided."
55
+ )
56
+
57
+
58
+ def _callback_logic(
59
+ plugin: SigningCommandPlugin, infile: str, outfile: str, **kwargs
60
+ ):
61
+ ctx = click.get_current_context()
62
+ cli_ctx: CLIContext = ctx.obj
63
+
64
+ timestamp_url: Optional[str] = cli_ctx.timestamp_url
65
+ if timestamp_url is not None:
66
+ timestamper = HTTPTimeStamper(timestamp_url)
67
+ else:
68
+ timestamper = None
69
+ with plugin.create_signer(cli_ctx, **kwargs) as signer:
70
+ pdf_sig_settings = cli_ctx.sig_settings
71
+
72
+ if pdf_sig_settings is None:
73
+ # detached sig
74
+ if timestamper is not None:
75
+ # signed TS
76
+ timestamp_attr = None
77
+ else:
78
+ # in this case, embed the signing time as a signed attr
79
+ timestamp_attr = datetime.now(tz=tzlocal.get_localzone())
80
+
81
+ with open(infile, 'rb') as inf:
82
+ cert = signer.signing_cert
83
+ assert cert is not None
84
+ signature_job = signer.async_sign_general_data(
85
+ inf,
86
+ select_suitable_signing_md(cert.public_key),
87
+ timestamper=timestamper,
88
+ signed_attr_settings=PdfCMSSignedAttributes(
89
+ signing_time=timestamp_attr
90
+ ),
91
+ )
92
+ signature = asyncio.run(signature_job)
93
+
94
+ output_bytes = signature.dump()
95
+ if cli_ctx.detach_pem:
96
+ output_bytes = pem.armor('PKCS7', output_bytes)
97
+
98
+ with open(outfile, 'wb') as out:
99
+ out.write(output_bytes)
100
+ else:
101
+ with open_for_signing(
102
+ infile_path=infile, lenient=cli_ctx.lenient
103
+ ) as w:
104
+ if cli_ctx.ux.visible_signature_desired:
105
+ _ensure_field_visible(
106
+ w,
107
+ pdf_sig_settings.field_name,
108
+ cli_ctx.new_field_spec is not None,
109
+ )
110
+ result = PdfSigner(
111
+ pdf_sig_settings,
112
+ signer=signer,
113
+ timestamper=timestamper,
114
+ stamp_style=cli_ctx.stamp_style,
115
+ new_field_spec=cli_ctx.new_field_spec,
116
+ ).sign_pdf(
117
+ w,
118
+ existing_fields_only=cli_ctx.existing_fields_only,
119
+ appearance_text_params=get_text_params(ctx),
120
+ )
121
+
122
+ with open(outfile, 'wb') as outf:
123
+ buf = result.getbuffer()
124
+ outf.write(buf)
125
+ buf.release()
126
+
127
+
128
+ def command_from_plugin(plugin: SigningCommandPlugin) -> click.Command:
129
+ def _callback(*, infile: str, outfile: str, **kwargs):
130
+ with pyhanko_exception_manager():
131
+ _callback_logic(plugin, infile, outfile, **kwargs)
132
+
133
+ params: List[click.Parameter] = [
134
+ click.Argument(('infile',), type=readable_file),
135
+ click.Argument(('outfile',), type=writable_file),
136
+ ]
137
+ params.extend(plugin.click_extra_arguments())
138
+ params.extend(plugin.click_options())
139
+ return click.Command(
140
+ name=plugin.subcommand_name,
141
+ callback=_callback,
142
+ help=plugin.help_summary,
143
+ params=params,
144
+ )
@@ -0,0 +1,260 @@
1
+ import contextlib
2
+ import getpass
3
+ from typing import ContextManager, List, Optional
4
+
5
+ import click
6
+ from pyhanko.cli._ctx import CLIContext
7
+ from pyhanko.cli._trust import grab_certs
8
+ from pyhanko.cli.config import CLIConfig
9
+ from pyhanko.cli.plugin_api import SigningCommandPlugin, register_signing_plugin
10
+ from pyhanko.cli.utils import _warn_empty_passphrase, logger, readable_file
11
+ from pyhanko.config.errors import ConfigurationError
12
+ from pyhanko.config.local_keys import (
13
+ PemDerSignatureConfig,
14
+ PKCS12SignatureConfig,
15
+ )
16
+ from pyhanko.sign.signers.pdf_cms import (
17
+ Signer,
18
+ signer_from_p12_config,
19
+ signer_from_pemder_config,
20
+ )
21
+
22
+ __all__ = ['PemderPlugin', 'PKCS12Plugin']
23
+
24
+
25
+ class KeyFileConfigWrapper:
26
+ def __init__(self, config: CLIConfig):
27
+ config_dict = config.raw_config
28
+ self.pemder_setups = config_dict.get('pemder-setups', {})
29
+ self.pkcs12_setups = config_dict.get('pkcs12-setups', {})
30
+
31
+ def get_pkcs12_config(self, name):
32
+ try:
33
+ setup = self.pkcs12_setups[name]
34
+ except KeyError:
35
+ raise ConfigurationError(f"There's no PKCS#12 setup named '{name}'")
36
+ return PKCS12SignatureConfig.from_config(setup)
37
+
38
+ def get_pemder_config(self, name):
39
+ try:
40
+ setup = self.pemder_setups[name]
41
+ except KeyError:
42
+ raise ConfigurationError(f"There's no PEM/DER setup named '{name}'")
43
+ return PemDerSignatureConfig.from_config(setup)
44
+
45
+
46
+ class PemderPlugin(SigningCommandPlugin):
47
+ subcommand_name = 'pemder'
48
+ help_summary = 'read key material from PEM/DER files'
49
+
50
+ def click_options(self) -> List[click.Option]:
51
+ return [
52
+ click.Option(
53
+ ('--key',),
54
+ help='file containing the private key (PEM/DER)',
55
+ type=readable_file,
56
+ required=False,
57
+ ),
58
+ click.Option(
59
+ ('--cert',),
60
+ help='file containing the signer\'s certificate ' '(PEM/DER)',
61
+ type=readable_file,
62
+ required=False,
63
+ ),
64
+ click.Option(
65
+ ('--chain',),
66
+ type=readable_file,
67
+ multiple=True,
68
+ help='file(s) containing the chain of trust for the '
69
+ 'signer\'s certificate (PEM/DER). May be '
70
+ 'passed multiple times.',
71
+ ),
72
+ click.Option(
73
+ ('--pemder-setup',),
74
+ type=str,
75
+ required=False,
76
+ help='name of preconfigured PEM/DER profile (overrides all '
77
+ 'other options)',
78
+ ),
79
+ # TODO allow reading the passphrase from a specific file descriptor
80
+ # (for advanced scripting setups)
81
+ click.Option(
82
+ ('--passfile',),
83
+ help='file containing the passphrase ' 'for the private key',
84
+ required=False,
85
+ type=click.File('r'),
86
+ show_default='stdin',
87
+ ),
88
+ click.Option(
89
+ ('--no-pass',),
90
+ help='assume the private key file is unencrypted',
91
+ type=bool,
92
+ is_flag=True,
93
+ default=False,
94
+ show_default=True,
95
+ ),
96
+ ]
97
+
98
+ def create_signer(
99
+ self, context: CLIContext, **kwargs
100
+ ) -> ContextManager[Signer]:
101
+ @contextlib.contextmanager
102
+ def _m():
103
+ yield _pemder_signer(context, **kwargs)
104
+
105
+ return _m()
106
+
107
+
108
+ def _pemder_signer(
109
+ ctx: CLIContext,
110
+ key,
111
+ cert,
112
+ chain,
113
+ pemder_setup,
114
+ passfile,
115
+ no_pass,
116
+ ):
117
+ if pemder_setup:
118
+ cli_config = ctx.config
119
+ if cli_config is None:
120
+ raise click.ClickException(
121
+ "The --pemder-setup option requires a configuration file"
122
+ )
123
+ try:
124
+ pemder_config = KeyFileConfigWrapper(cli_config).get_pemder_config(
125
+ pemder_setup
126
+ )
127
+ except ConfigurationError as e:
128
+ msg = f"Error while reading PEM/DER setup {pemder_setup}"
129
+ logger.error(msg, exc_info=e)
130
+ raise click.ClickException(msg)
131
+ elif not (key and cert):
132
+ raise click.ClickException(
133
+ "Either both the --key and --cert options, or the --pemder-setup "
134
+ "option must be provided."
135
+ )
136
+ else:
137
+ pemder_config = PemDerSignatureConfig(
138
+ key_file=key,
139
+ cert_file=cert,
140
+ other_certs=grab_certs(chain),
141
+ prefer_pss=ctx.prefer_pss,
142
+ )
143
+
144
+ if pemder_config.key_passphrase is not None:
145
+ passphrase = pemder_config.key_passphrase
146
+ elif passfile is not None:
147
+ passphrase = passfile.readline().strip().encode('utf-8')
148
+ passfile.close()
149
+ elif pemder_config.prompt_passphrase and not no_pass:
150
+ passphrase = getpass.getpass(prompt='Key passphrase: ').encode('utf-8')
151
+ if not passphrase:
152
+ _warn_empty_passphrase()
153
+ passphrase = None
154
+ else:
155
+ passphrase = None
156
+
157
+ return signer_from_pemder_config(
158
+ pemder_config, provided_key_passphrase=passphrase
159
+ )
160
+
161
+
162
+ class PKCS12Plugin(SigningCommandPlugin):
163
+ subcommand_name = 'pkcs12'
164
+ help_summary = 'read key material from PKCS#12 files'
165
+
166
+ def click_extra_arguments(self) -> List[click.Argument]:
167
+ return [click.Argument(('pfx',), type=readable_file, required=False)]
168
+
169
+ def click_options(self) -> List[click.Option]:
170
+ return [
171
+ click.Option(
172
+ ('--p12-setup',),
173
+ type=str,
174
+ required=False,
175
+ help='name of preconfigured PKCS#12 profile (overrides all '
176
+ 'other options)',
177
+ ),
178
+ click.Option(
179
+ ('--chain',),
180
+ type=readable_file,
181
+ multiple=True,
182
+ help='PEM/DER file(s) containing extra certificates to embed '
183
+ '(e.g. chain of trust not embedded in the PKCS#12 file)'
184
+ 'May be passed multiple times.',
185
+ ),
186
+ click.Option(
187
+ ('--passfile',),
188
+ help='file containing the passphrase for the PKCS#12 file.',
189
+ required=False,
190
+ type=click.File('r'),
191
+ show_default='stdin',
192
+ ),
193
+ click.Option(
194
+ ('--no-pass',),
195
+ help='assume the PKCS#12 file is unencrypted',
196
+ type=bool,
197
+ is_flag=True,
198
+ default=False,
199
+ show_default=True,
200
+ ),
201
+ ]
202
+
203
+ def create_signer(
204
+ self, context: CLIContext, **kwargs
205
+ ) -> ContextManager[Signer]:
206
+ @contextlib.contextmanager
207
+ def _m():
208
+ yield _pkcs12_signer(context, **kwargs)
209
+
210
+ return _m()
211
+
212
+
213
+ def _pkcs12_signer(ctx: CLIContext, pfx, chain, passfile, p12_setup, no_pass):
214
+ # TODO add sanity check in case the user gets the arg order wrong
215
+ # (now it fails with a gnarly DER decoding error, which is not very
216
+ # user-friendly)
217
+ if p12_setup:
218
+ cli_config: Optional[CLIConfig] = ctx.config
219
+ if cli_config is None:
220
+ raise click.ClickException(
221
+ "The --p12-setup option requires a configuration file"
222
+ )
223
+ try:
224
+ pkcs12_config = KeyFileConfigWrapper(cli_config).get_pkcs12_config(
225
+ p12_setup
226
+ )
227
+ except ConfigurationError as e:
228
+ msg = f"Error while reading PKCS#12 config {p12_setup}"
229
+ logger.error(msg, exc_info=e)
230
+ raise click.ClickException(msg)
231
+ elif not pfx:
232
+ raise click.ClickException(
233
+ "Either the PFX argument or the --p12-setup option "
234
+ "must be provided."
235
+ )
236
+ else:
237
+ pkcs12_config = PKCS12SignatureConfig(
238
+ pfx_file=pfx,
239
+ other_certs=grab_certs(chain),
240
+ prefer_pss=ctx.prefer_pss,
241
+ )
242
+
243
+ if pkcs12_config.pfx_passphrase is not None:
244
+ passphrase = pkcs12_config.pfx_passphrase
245
+ elif passfile is not None:
246
+ passphrase = passfile.readline().strip().encode('utf-8')
247
+ passfile.close()
248
+ elif pkcs12_config.prompt_passphrase and not no_pass:
249
+ passphrase = getpass.getpass(prompt='PKCS#12 passphrase: ').encode(
250
+ 'utf-8'
251
+ )
252
+ if not passphrase:
253
+ _warn_empty_passphrase()
254
+ passphrase = None
255
+ else:
256
+ passphrase = None
257
+
258
+ return signer_from_p12_config(
259
+ pkcs12_config, provided_pfx_passphrase=passphrase
260
+ )
@@ -0,0 +1,58 @@
1
+ import getpass
2
+ from typing import BinaryIO, Optional
3
+
4
+ import click
5
+ from pyhanko.pdf_utils import crypt
6
+ from pyhanko.pdf_utils.crypt import AuthStatus
7
+ from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter
8
+
9
+
10
+ class OpenForSigning:
11
+ def __init__(self, infile_path: str, lenient: bool):
12
+ self.infile_path = infile_path
13
+ self.lenient = lenient
14
+ self.handle: Optional[BinaryIO] = None
15
+
16
+ def __enter__(self) -> IncrementalPdfFileWriter:
17
+ self.handle = infile = open(self.infile_path, 'rb')
18
+ writer = IncrementalPdfFileWriter(infile, strict=not self.lenient)
19
+
20
+ # TODO make this an option higher up the tree
21
+ if writer.prev.encrypted:
22
+ sh = writer.prev.security_handler
23
+ if isinstance(sh, crypt.StandardSecurityHandler):
24
+ pdf_pass = getpass.getpass(
25
+ prompt='Password for encrypted file \'%s\': '
26
+ % self.infile_path
27
+ )
28
+ auth = writer.encrypt(pdf_pass)
29
+ if auth.status == AuthStatus.FAILED:
30
+ raise click.ClickException(
31
+ "Invalid password for encrypted file"
32
+ )
33
+ elif isinstance(sh, crypt.PubKeySecurityHandler):
34
+ raise click.ClickException(
35
+ "Public-key document encryption is not supported in the CLI"
36
+ )
37
+ else: # pragma: nocover
38
+ raise click.ClickException(
39
+ "Input file appears to be encrypted, but appropriate "
40
+ "credentials are not available."
41
+ )
42
+ return writer
43
+
44
+ def __exit__(self, exc_type, exc_val, exc_tb):
45
+ if self.handle:
46
+ self.handle.close()
47
+
48
+
49
+ def open_for_signing(infile_path: str, lenient: bool):
50
+ return OpenForSigning(infile_path, lenient)
51
+
52
+
53
+ def get_text_params(ctx):
54
+ text_params = None
55
+ stamp_url = ctx.obj.stamp_url
56
+ if stamp_url is not None:
57
+ text_params = {'url': stamp_url}
58
+ return text_params
@@ -0,0 +1,100 @@
1
+ from typing import Optional
2
+
3
+ import click
4
+ from pyhanko.cli._root import cli_root
5
+ from pyhanko.cli.config import CLIConfig
6
+ from pyhanko.cli.runtime import DEFAULT_CONFIG_FILE, pyhanko_exception_manager
7
+ from pyhanko.cli.utils import _index_page, logger, readable_file
8
+ from pyhanko.config.errors import ConfigurationError
9
+ from pyhanko.stamp import QRStampStyle, qr_stamp_file, text_stamp_file
10
+
11
+ __all__ = ['stamp', 'select_style']
12
+
13
+ _CONFIG_REQUIRED_MSG = (
14
+ "Using stamp styles requires a configuration file "
15
+ f"({DEFAULT_CONFIG_FILE} by default)."
16
+ )
17
+
18
+
19
+ def select_style(ctx: click.Context, style_name: str, url: str):
20
+ cli_config: Optional[CLIConfig] = ctx.obj.config
21
+ if not cli_config:
22
+ if not style_name:
23
+ return None
24
+ raise click.ClickException(_CONFIG_REQUIRED_MSG)
25
+ try:
26
+ style = cli_config.get_stamp_style(style_name)
27
+ except ConfigurationError as e:
28
+ logger.error(e.msg, exc_info=e)
29
+ raise click.ClickException(e.msg)
30
+ if url and not isinstance(style, QRStampStyle):
31
+ raise click.ClickException(
32
+ "The --stamp-url parameter is only meaningful for QR stamp styles."
33
+ )
34
+ elif not url and isinstance(style, QRStampStyle):
35
+ raise click.ClickException(
36
+ "QR stamp styles require the --stamp-url option."
37
+ )
38
+
39
+ return style
40
+
41
+
42
+ # TODO: text_params support
43
+
44
+
45
+ @cli_root.command(help='stamp PDF files', name='stamp')
46
+ @click.argument('infile', type=readable_file)
47
+ @click.argument('outfile', type=click.Path(writable=True, dir_okay=False))
48
+ @click.argument('x', type=int)
49
+ @click.argument('y', type=int)
50
+ @click.option(
51
+ '--style-name',
52
+ help='stamp style name for stamp appearance',
53
+ required=False,
54
+ type=str,
55
+ )
56
+ @click.option(
57
+ '--page',
58
+ help='page on which the stamp should be applied',
59
+ required=False,
60
+ type=int,
61
+ default=1,
62
+ show_default=True,
63
+ )
64
+ @click.option(
65
+ '--stamp-url',
66
+ help='QR code URL to use in QR stamp style',
67
+ required=False,
68
+ type=str,
69
+ )
70
+ @click.pass_context
71
+ def stamp(
72
+ ctx,
73
+ infile: str,
74
+ outfile: str,
75
+ x: int,
76
+ y: int,
77
+ style_name: str,
78
+ page: int,
79
+ stamp_url: str,
80
+ ):
81
+ cli_config: Optional[CLIConfig] = ctx.obj.config
82
+ if cli_config is None:
83
+ raise click.ClickException(_CONFIG_REQUIRED_MSG)
84
+ with pyhanko_exception_manager():
85
+ stamp_style = select_style(ctx, style_name, stamp_url)
86
+ page_ix = _index_page(page)
87
+ if stamp_url:
88
+ qr_stamp_file(
89
+ infile,
90
+ outfile,
91
+ stamp_style,
92
+ dest_page=page_ix,
93
+ x=x,
94
+ y=y,
95
+ url=stamp_url,
96
+ )
97
+ else:
98
+ text_stamp_file(
99
+ infile, outfile, stamp_style, dest_page=page_ix, x=x, y=y
100
+ )
@@ -0,0 +1,2 @@
1
+ from .ltv import *
2
+ from .validate import *
@@ -0,0 +1,136 @@
1
+ import click
2
+ from pyhanko.cli._trust import build_vc_kwargs, trust_options
3
+ from pyhanko.cli.commands.signing import signing
4
+ from pyhanko.cli.runtime import pyhanko_exception_manager
5
+ from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter
6
+ from pyhanko.pdf_utils.reader import PdfFileReader
7
+ from pyhanko.sign import signers, validation
8
+ from pyhanko.sign.timestamps import HTTPTimeStamper
9
+
10
+ from pyhanko_certvalidator import ValidationContext
11
+
12
+ __all__ = ['ltv_fix', 'lta_update']
13
+
14
+
15
+ @trust_options
16
+ @signing.command(name='ltaupdate', help='update LTA timestamp')
17
+ @click.argument('infile', type=click.File('r+b'))
18
+ @click.option(
19
+ '--timestamp-url',
20
+ help='URL for timestamp server',
21
+ required=True,
22
+ type=str,
23
+ default=None,
24
+ )
25
+ @click.option(
26
+ '--retroactive-revinfo',
27
+ help='Treat revocation info as retroactively valid '
28
+ '(i.e. ignore thisUpdate timestamp)',
29
+ type=bool,
30
+ is_flag=True,
31
+ default=False,
32
+ show_default=True,
33
+ )
34
+ @click.pass_context
35
+ def lta_update(
36
+ ctx,
37
+ infile,
38
+ validation_context,
39
+ trust,
40
+ trust_replace,
41
+ other_certs,
42
+ timestamp_url,
43
+ retroactive_revinfo,
44
+ ):
45
+ with pyhanko_exception_manager():
46
+ vc_kwargs = build_vc_kwargs(
47
+ ctx.obj.config,
48
+ validation_context,
49
+ trust,
50
+ trust_replace,
51
+ other_certs,
52
+ retroactive_revinfo,
53
+ )
54
+ timestamper = HTTPTimeStamper(timestamp_url)
55
+ r = PdfFileReader(infile)
56
+ signers.PdfTimeStamper(timestamper).update_archival_timestamp_chain(
57
+ r, ValidationContext(**vc_kwargs)
58
+ )
59
+
60
+
61
+ # TODO perhaps add an option here to fix the lack of a timestamp and/or
62
+ # warn if none is present
63
+
64
+
65
+ @trust_options
66
+ @signing.command(
67
+ name='ltvfix', help='add revocation information for a signature to the DSS'
68
+ )
69
+ @click.argument('infile', type=click.File('r+b'))
70
+ @click.option('--field', help='name of the signature field', required=True)
71
+ @click.option(
72
+ '--timestamp-url',
73
+ help='URL for timestamp server',
74
+ required=False,
75
+ type=str,
76
+ default=None,
77
+ )
78
+ @click.option(
79
+ '--apply-lta-timestamp',
80
+ help='Apply a document timestamp after adding revocation info.',
81
+ required=False,
82
+ type=bool,
83
+ default=False,
84
+ is_flag=True,
85
+ show_default=True,
86
+ )
87
+ @click.pass_context
88
+ def ltv_fix(
89
+ ctx,
90
+ infile,
91
+ field,
92
+ timestamp_url,
93
+ apply_lta_timestamp,
94
+ validation_context,
95
+ trust_replace,
96
+ trust,
97
+ other_certs,
98
+ ):
99
+ if apply_lta_timestamp and not timestamp_url:
100
+ raise click.ClickException(
101
+ "Please specify a timestamp server using --timestamp-url."
102
+ )
103
+
104
+ vc_kwargs = build_vc_kwargs(
105
+ ctx.obj.config,
106
+ validation_context,
107
+ trust,
108
+ trust_replace,
109
+ other_certs,
110
+ retroactive_revinfo=False,
111
+ allow_fetching=True,
112
+ )
113
+ vc_kwargs['revocation_mode'] = 'hard-fail'
114
+ r = PdfFileReader(infile)
115
+
116
+ try:
117
+ emb_sig = next(
118
+ s for s in r.embedded_regular_signatures if s.field_name == field
119
+ )
120
+ except StopIteration:
121
+ raise click.ClickException(
122
+ f"Could not find a PDF signature labelled {field}."
123
+ )
124
+
125
+ output = validation.add_validation_info(
126
+ emb_sig, ValidationContext(**vc_kwargs), in_place=True
127
+ )
128
+
129
+ if apply_lta_timestamp:
130
+ timestamper = HTTPTimeStamper(timestamp_url)
131
+ signers.PdfTimeStamper(timestamper).timestamp_pdf(
132
+ IncrementalPdfFileWriter(output),
133
+ signers.DEFAULT_MD,
134
+ ValidationContext(**vc_kwargs),
135
+ in_place=True,
136
+ )