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.
- pyhanko/__main__.py +9 -0
- pyhanko/cli/__init__.py +12 -0
- pyhanko/cli/_ctx.py +92 -0
- pyhanko/cli/_root.py +171 -0
- pyhanko/cli/_trust.py +159 -0
- pyhanko/cli/commands/__init__.py +0 -0
- pyhanko/cli/commands/crypt.py +219 -0
- pyhanko/cli/commands/fields.py +66 -0
- pyhanko/cli/commands/signing/__init__.py +328 -0
- pyhanko/cli/commands/signing/pkcs11_cli.py +185 -0
- pyhanko/cli/commands/signing/plugin.py +144 -0
- pyhanko/cli/commands/signing/simple.py +260 -0
- pyhanko/cli/commands/signing/utils.py +58 -0
- pyhanko/cli/commands/stamp.py +100 -0
- pyhanko/cli/commands/validation/__init__.py +2 -0
- pyhanko/cli/commands/validation/ltv.py +136 -0
- pyhanko/cli/commands/validation/validate.py +355 -0
- pyhanko/cli/config.py +306 -0
- pyhanko/cli/plugin_api.py +118 -0
- pyhanko/cli/py.typed +0 -0
- pyhanko/cli/runtime.py +80 -0
- pyhanko/cli/utils.py +68 -0
- pyhanko/cli/version.py +2 -0
- pyhanko_cli-0.1.0.dist-info/METADATA +68 -0
- pyhanko_cli-0.1.0.dist-info/RECORD +29 -0
- pyhanko_cli-0.1.0.dist-info/WHEEL +5 -0
- pyhanko_cli-0.1.0.dist-info/entry_points.txt +2 -0
- pyhanko_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- pyhanko_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import getpass
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
import pyhanko.sign
|
|
7
|
+
from asn1crypto import cms, pem
|
|
8
|
+
from pyhanko.cli._trust import (
|
|
9
|
+
_get_key_usage_settings,
|
|
10
|
+
_prepare_vc,
|
|
11
|
+
build_vc_kwargs,
|
|
12
|
+
trust_options,
|
|
13
|
+
)
|
|
14
|
+
from pyhanko.cli.commands.signing import signing
|
|
15
|
+
from pyhanko.cli.runtime import pyhanko_exception_manager
|
|
16
|
+
from pyhanko.cli.utils import logger
|
|
17
|
+
from pyhanko.pdf_utils import crypt
|
|
18
|
+
from pyhanko.pdf_utils.misc import isoparse
|
|
19
|
+
from pyhanko.pdf_utils.reader import PdfFileReader
|
|
20
|
+
from pyhanko.sign import validation
|
|
21
|
+
from pyhanko.sign.validation import RevocationInfoValidationType
|
|
22
|
+
from pyhanko.sign.validation.errors import SignatureValidationError
|
|
23
|
+
|
|
24
|
+
from pyhanko_certvalidator import ValidationContext
|
|
25
|
+
|
|
26
|
+
__all__ = ['validate_signatures']
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _signature_status(
|
|
30
|
+
ltv_profile,
|
|
31
|
+
vc_kwargs,
|
|
32
|
+
force_revinfo,
|
|
33
|
+
key_usage_settings,
|
|
34
|
+
embedded_sig,
|
|
35
|
+
skip_diff=False,
|
|
36
|
+
):
|
|
37
|
+
if ltv_profile is None:
|
|
38
|
+
vc = ValidationContext(**vc_kwargs)
|
|
39
|
+
status = pyhanko.sign.validation.validate_pdf_signature(
|
|
40
|
+
embedded_sig,
|
|
41
|
+
key_usage_settings=key_usage_settings,
|
|
42
|
+
signer_validation_context=vc,
|
|
43
|
+
skip_diff=skip_diff,
|
|
44
|
+
)
|
|
45
|
+
else:
|
|
46
|
+
status = validation.validate_pdf_ltv_signature(
|
|
47
|
+
embedded_sig,
|
|
48
|
+
ltv_profile,
|
|
49
|
+
key_usage_settings=key_usage_settings,
|
|
50
|
+
force_revinfo=force_revinfo,
|
|
51
|
+
validation_context_kwargs=vc_kwargs,
|
|
52
|
+
skip_diff=skip_diff,
|
|
53
|
+
)
|
|
54
|
+
return status
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _validate_detached(
|
|
58
|
+
infile, sig_infile, validation_context, key_usage_settings
|
|
59
|
+
):
|
|
60
|
+
sig_bytes = sig_infile.read()
|
|
61
|
+
try:
|
|
62
|
+
if pem.detect(sig_bytes):
|
|
63
|
+
_, _, sig_bytes = pem.unarmor(sig_bytes)
|
|
64
|
+
content_info = cms.ContentInfo.load(sig_bytes)
|
|
65
|
+
if content_info['content_type'].native != 'signed_data':
|
|
66
|
+
raise click.ClickException("CMS content type is not signedData")
|
|
67
|
+
except ValueError as e:
|
|
68
|
+
raise click.ClickException("Could not parse CMS object") from e
|
|
69
|
+
|
|
70
|
+
validation_coro = validation.async_validate_detached_cms(
|
|
71
|
+
infile,
|
|
72
|
+
signed_data=content_info['content'],
|
|
73
|
+
signer_validation_context=validation_context,
|
|
74
|
+
key_usage_settings=key_usage_settings,
|
|
75
|
+
)
|
|
76
|
+
return asyncio.run(validation_coro)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _signature_status_str(status_callback, pretty_print, executive_summary):
|
|
80
|
+
try:
|
|
81
|
+
status = status_callback()
|
|
82
|
+
if executive_summary and not pretty_print:
|
|
83
|
+
return (
|
|
84
|
+
'VALID' if status.bottom_line else 'INVALID',
|
|
85
|
+
status.bottom_line,
|
|
86
|
+
)
|
|
87
|
+
elif pretty_print:
|
|
88
|
+
return status.pretty_print_details(), status.bottom_line
|
|
89
|
+
else:
|
|
90
|
+
return status.summary(), status.bottom_line
|
|
91
|
+
except validation.ValidationInfoReadingError as e:
|
|
92
|
+
msg = (
|
|
93
|
+
'An error occurred while parsing the revocation information '
|
|
94
|
+
'for this signature: ' + str(e)
|
|
95
|
+
)
|
|
96
|
+
logger.error(msg)
|
|
97
|
+
if pretty_print:
|
|
98
|
+
return msg, False
|
|
99
|
+
else:
|
|
100
|
+
return 'REVINFO_FAILURE', False
|
|
101
|
+
except SignatureValidationError as e:
|
|
102
|
+
msg = 'An error occurred while validating this signature: ' + str(e)
|
|
103
|
+
logger.error(msg, exc_info=e)
|
|
104
|
+
if pretty_print:
|
|
105
|
+
return msg, False
|
|
106
|
+
else:
|
|
107
|
+
return 'INVALID', False
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _attempt_iso_dt_parse(dt_str) -> datetime:
|
|
111
|
+
try:
|
|
112
|
+
dt = isoparse(dt_str)
|
|
113
|
+
except ValueError:
|
|
114
|
+
raise click.ClickException(f"datetime {dt_str!r} could not be parsed")
|
|
115
|
+
return dt
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# TODO add an option to do LTV, but guess the profile
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@trust_options
|
|
122
|
+
@signing.command(name='validate', help='validate signatures')
|
|
123
|
+
@click.argument('infile', type=click.File('rb'))
|
|
124
|
+
@click.option(
|
|
125
|
+
'--executive-summary',
|
|
126
|
+
help='only print final judgment on signature validity',
|
|
127
|
+
type=bool,
|
|
128
|
+
is_flag=True,
|
|
129
|
+
default=False,
|
|
130
|
+
show_default=True,
|
|
131
|
+
)
|
|
132
|
+
@click.option(
|
|
133
|
+
'--pretty-print',
|
|
134
|
+
help='render a prettier summary for the signatures in the file',
|
|
135
|
+
type=bool,
|
|
136
|
+
is_flag=True,
|
|
137
|
+
default=False,
|
|
138
|
+
show_default=True,
|
|
139
|
+
)
|
|
140
|
+
@click.option(
|
|
141
|
+
'--ltv-profile',
|
|
142
|
+
help='LTV signature validation profile',
|
|
143
|
+
type=click.Choice(RevocationInfoValidationType.as_tuple()),
|
|
144
|
+
required=False,
|
|
145
|
+
)
|
|
146
|
+
@click.option(
|
|
147
|
+
'--force-revinfo',
|
|
148
|
+
help='Fail trust validation if a certificate has no known CRL '
|
|
149
|
+
'or OCSP endpoints.',
|
|
150
|
+
type=bool,
|
|
151
|
+
is_flag=True,
|
|
152
|
+
default=False,
|
|
153
|
+
show_default=True,
|
|
154
|
+
)
|
|
155
|
+
@click.option(
|
|
156
|
+
'--soft-revocation-check',
|
|
157
|
+
help='Do not fail validation on revocation checking failures '
|
|
158
|
+
'(only applied to on-line revocation checks)',
|
|
159
|
+
type=bool,
|
|
160
|
+
is_flag=True,
|
|
161
|
+
default=False,
|
|
162
|
+
show_default=True,
|
|
163
|
+
)
|
|
164
|
+
@click.option(
|
|
165
|
+
'--no-revocation-check',
|
|
166
|
+
help='Do not attempt to check revocation status '
|
|
167
|
+
'(meaningless for LTV validation)',
|
|
168
|
+
type=bool,
|
|
169
|
+
is_flag=True,
|
|
170
|
+
default=False,
|
|
171
|
+
show_default=True,
|
|
172
|
+
)
|
|
173
|
+
@click.option(
|
|
174
|
+
'--retroactive-revinfo',
|
|
175
|
+
help='Treat revocation info as retroactively valid '
|
|
176
|
+
'(i.e. ignore thisUpdate timestamp)',
|
|
177
|
+
type=bool,
|
|
178
|
+
is_flag=True,
|
|
179
|
+
default=False,
|
|
180
|
+
show_default=True,
|
|
181
|
+
)
|
|
182
|
+
@click.option(
|
|
183
|
+
'--validation-time',
|
|
184
|
+
help=(
|
|
185
|
+
'Override the validation time (ISO 8601 date). '
|
|
186
|
+
'The special value \'claimed\' causes the validation time '
|
|
187
|
+
'claimed by the signer to be used. Revocation checking '
|
|
188
|
+
'will be disabled. Option ignored in LTV mode.'
|
|
189
|
+
),
|
|
190
|
+
type=str,
|
|
191
|
+
required=False,
|
|
192
|
+
)
|
|
193
|
+
@click.option(
|
|
194
|
+
'--password',
|
|
195
|
+
required=False,
|
|
196
|
+
type=str,
|
|
197
|
+
help='password to access the file (can also be read from stdin)',
|
|
198
|
+
)
|
|
199
|
+
@click.option(
|
|
200
|
+
'--no-diff-analysis',
|
|
201
|
+
default=False,
|
|
202
|
+
type=bool,
|
|
203
|
+
is_flag=True,
|
|
204
|
+
help='disable incremental update analysis',
|
|
205
|
+
)
|
|
206
|
+
@click.option(
|
|
207
|
+
'--detached',
|
|
208
|
+
type=click.File('rb'),
|
|
209
|
+
help=(
|
|
210
|
+
'Read signature CMS object from the indicated file; '
|
|
211
|
+
'this can be used to verify signatures on non-PDF files'
|
|
212
|
+
),
|
|
213
|
+
)
|
|
214
|
+
@click.option(
|
|
215
|
+
'--no-strict-syntax',
|
|
216
|
+
help='Attempt to ignore syntactical problems in the input file '
|
|
217
|
+
'and enable signature validation in hybrid-reference files.'
|
|
218
|
+
'(warning: this may affect validation results in unexpected '
|
|
219
|
+
'ways.)',
|
|
220
|
+
type=bool,
|
|
221
|
+
is_flag=True,
|
|
222
|
+
default=False,
|
|
223
|
+
show_default=True,
|
|
224
|
+
)
|
|
225
|
+
@click.pass_context
|
|
226
|
+
def validate_signatures(
|
|
227
|
+
ctx: click.Context,
|
|
228
|
+
infile,
|
|
229
|
+
executive_summary,
|
|
230
|
+
pretty_print,
|
|
231
|
+
validation_context,
|
|
232
|
+
trust,
|
|
233
|
+
trust_replace,
|
|
234
|
+
other_certs,
|
|
235
|
+
ltv_profile,
|
|
236
|
+
force_revinfo,
|
|
237
|
+
soft_revocation_check,
|
|
238
|
+
no_revocation_check,
|
|
239
|
+
password,
|
|
240
|
+
retroactive_revinfo,
|
|
241
|
+
detached,
|
|
242
|
+
no_diff_analysis,
|
|
243
|
+
validation_time,
|
|
244
|
+
no_strict_syntax,
|
|
245
|
+
):
|
|
246
|
+
no_revocation_check |= validation_time is not None
|
|
247
|
+
|
|
248
|
+
if no_revocation_check:
|
|
249
|
+
soft_revocation_check = True
|
|
250
|
+
|
|
251
|
+
if pretty_print and executive_summary:
|
|
252
|
+
raise click.ClickException(
|
|
253
|
+
"--pretty-print is incompatible with --executive-summary."
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
if ltv_profile is not None:
|
|
257
|
+
if validation_time is not None:
|
|
258
|
+
raise click.ClickException(
|
|
259
|
+
"--validation-time is not compatible with --ltv-profile"
|
|
260
|
+
)
|
|
261
|
+
ltv_profile = RevocationInfoValidationType(ltv_profile)
|
|
262
|
+
|
|
263
|
+
vc_kwargs = build_vc_kwargs(
|
|
264
|
+
ctx.obj.config,
|
|
265
|
+
validation_context,
|
|
266
|
+
trust,
|
|
267
|
+
trust_replace,
|
|
268
|
+
other_certs,
|
|
269
|
+
retroactive_revinfo,
|
|
270
|
+
allow_fetching=False if no_revocation_check else None,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
use_claimed_validation_time = False
|
|
274
|
+
if validation_time == 'claimed':
|
|
275
|
+
use_claimed_validation_time = True
|
|
276
|
+
elif validation_time is not None:
|
|
277
|
+
vc_kwargs['moment'] = _attempt_iso_dt_parse(validation_time)
|
|
278
|
+
|
|
279
|
+
key_usage_settings = _get_key_usage_settings(ctx, validation_context)
|
|
280
|
+
vc_kwargs = _prepare_vc(
|
|
281
|
+
vc_kwargs,
|
|
282
|
+
soft_revocation_check=soft_revocation_check,
|
|
283
|
+
force_revinfo=force_revinfo,
|
|
284
|
+
)
|
|
285
|
+
with pyhanko_exception_manager():
|
|
286
|
+
if detached is not None:
|
|
287
|
+
(status_str, signature_ok) = _signature_status_str(
|
|
288
|
+
status_callback=lambda: _validate_detached(
|
|
289
|
+
infile,
|
|
290
|
+
detached,
|
|
291
|
+
ValidationContext(**vc_kwargs),
|
|
292
|
+
key_usage_settings,
|
|
293
|
+
),
|
|
294
|
+
pretty_print=pretty_print,
|
|
295
|
+
executive_summary=executive_summary,
|
|
296
|
+
)
|
|
297
|
+
if signature_ok:
|
|
298
|
+
click.echo(status_str)
|
|
299
|
+
else:
|
|
300
|
+
raise click.ClickException(status_str)
|
|
301
|
+
return
|
|
302
|
+
|
|
303
|
+
if no_strict_syntax:
|
|
304
|
+
logger.info(
|
|
305
|
+
"Strict PDF syntax is disabled; this could impact validation "
|
|
306
|
+
"results. Use caution."
|
|
307
|
+
)
|
|
308
|
+
r = PdfFileReader(infile, strict=False)
|
|
309
|
+
else:
|
|
310
|
+
r = PdfFileReader(infile)
|
|
311
|
+
sh = r.security_handler
|
|
312
|
+
if isinstance(sh, crypt.StandardSecurityHandler):
|
|
313
|
+
if password is None:
|
|
314
|
+
password = getpass.getpass(prompt='File password: ')
|
|
315
|
+
auth_result = r.decrypt(password)
|
|
316
|
+
if auth_result.status == crypt.AuthStatus.FAILED:
|
|
317
|
+
raise click.ClickException("Password didn't match.")
|
|
318
|
+
elif sh is not None:
|
|
319
|
+
raise click.ClickException(
|
|
320
|
+
"The CLI supports only password-based encryption when "
|
|
321
|
+
"validating (for now)"
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
all_signatures_ok = True
|
|
325
|
+
for ix, embedded_sig in enumerate(r.embedded_regular_signatures):
|
|
326
|
+
fingerprint: str = embedded_sig.signer_cert.sha256.hex()
|
|
327
|
+
if use_claimed_validation_time:
|
|
328
|
+
vc_kwargs['moment'] = embedded_sig.self_reported_timestamp
|
|
329
|
+
(status_str, signature_ok) = _signature_status_str(
|
|
330
|
+
status_callback=lambda: _signature_status(
|
|
331
|
+
ltv_profile=ltv_profile,
|
|
332
|
+
force_revinfo=force_revinfo,
|
|
333
|
+
vc_kwargs=vc_kwargs,
|
|
334
|
+
key_usage_settings=key_usage_settings,
|
|
335
|
+
embedded_sig=embedded_sig,
|
|
336
|
+
skip_diff=no_diff_analysis,
|
|
337
|
+
),
|
|
338
|
+
pretty_print=pretty_print,
|
|
339
|
+
executive_summary=executive_summary,
|
|
340
|
+
)
|
|
341
|
+
name = embedded_sig.field_name
|
|
342
|
+
|
|
343
|
+
if pretty_print:
|
|
344
|
+
header = f'Field {ix + 1}: {name}'
|
|
345
|
+
line = '=' * len(header)
|
|
346
|
+
click.echo(line)
|
|
347
|
+
click.echo(header)
|
|
348
|
+
click.echo(line)
|
|
349
|
+
click.echo('\n\n' + status_str)
|
|
350
|
+
else:
|
|
351
|
+
click.echo('%s:%s:%s' % (name, fingerprint, status_str))
|
|
352
|
+
all_signatures_ok &= signature_ok
|
|
353
|
+
|
|
354
|
+
if not all_signatures_ok:
|
|
355
|
+
raise click.ClickException("Validation failed")
|
pyhanko/cli/config.py
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from datetime import timedelta
|
|
3
|
+
from typing import Dict, List, Optional, Type, Union
|
|
4
|
+
|
|
5
|
+
import yaml
|
|
6
|
+
from pyhanko.config.errors import ConfigurationError
|
|
7
|
+
from pyhanko.config.logging import LogConfig, parse_logging_config
|
|
8
|
+
from pyhanko.config.trust import DEFAULT_TIME_TOLERANCE, parse_trust_config
|
|
9
|
+
from pyhanko.sign.signers import DEFAULT_SIGNING_STAMP_STYLE
|
|
10
|
+
from pyhanko.sign.validation.settings import KeyUsageConstraints
|
|
11
|
+
from pyhanko.stamp import BaseStampStyle, QRStampStyle, TextStampStyle
|
|
12
|
+
|
|
13
|
+
from pyhanko_certvalidator import ValidationContext
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class CLIConfig:
|
|
18
|
+
"""
|
|
19
|
+
CLI configuration settings.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
validation_contexts: Dict[str, dict]
|
|
23
|
+
"""
|
|
24
|
+
Named validation contexts. The values in this dictionary
|
|
25
|
+
are themselves dictionaries that support the following keys:
|
|
26
|
+
|
|
27
|
+
* ``trust``: path to a root certificate or list of such paths
|
|
28
|
+
* ``trust-replace``: whether the value of the ``trust`` setting should
|
|
29
|
+
replace the system trust, or add to it
|
|
30
|
+
* ``other-certs``: paths to other relevant certificates that are not
|
|
31
|
+
trusted by fiat.
|
|
32
|
+
* ``time-tolerance``: a time drift tolerance setting in seconds
|
|
33
|
+
* ``retroactive-revinfo``: whether to consider revocation information
|
|
34
|
+
retroactively valid
|
|
35
|
+
* ``signer-key-usage-policy``: Signer key usage requirements. See
|
|
36
|
+
:class:`.KeyUsageConstraints`.
|
|
37
|
+
|
|
38
|
+
There are two settings that are deprecated but still supported for backwards
|
|
39
|
+
compatibility:
|
|
40
|
+
|
|
41
|
+
* ``signer-key-usage``: Supplanted by ``signer-key-usage-policy``
|
|
42
|
+
* ``signer-extd-key-usage``: Supplanted by ``signer-key-usage-policy``
|
|
43
|
+
|
|
44
|
+
These may eventually be removed.
|
|
45
|
+
|
|
46
|
+
Callers should not process this information directly, but rely on
|
|
47
|
+
:meth:`get_validation_context` instead.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
stamp_styles: Dict[str, dict]
|
|
51
|
+
"""
|
|
52
|
+
Named stamp styles. The type of style is selected by the ``type`` key, which
|
|
53
|
+
can be either ``qr`` or ``text`` (the default is ``text``).
|
|
54
|
+
For other settings values, see :class:.`QRStampStyle` and
|
|
55
|
+
:class:`.TextStampStyle`.
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
Callers should not process this information directly, but rely on
|
|
59
|
+
:meth:`get_stamp_style` instead.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
default_validation_context: str
|
|
63
|
+
"""
|
|
64
|
+
The name of the default validation context.
|
|
65
|
+
The default value for this setting is ``default``.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
default_stamp_style: str
|
|
69
|
+
"""
|
|
70
|
+
The name of the default stamp style.
|
|
71
|
+
The default value for this setting is ``default``.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
time_tolerance: timedelta
|
|
75
|
+
"""
|
|
76
|
+
Time drift tolerance (global default).
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
retroactive_revinfo: bool
|
|
80
|
+
"""
|
|
81
|
+
Whether to consider revocation information retroactively valid
|
|
82
|
+
(global default).
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
raw_config: dict
|
|
86
|
+
"""
|
|
87
|
+
The raw config data parsed into a Python dictionary.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
# TODO graceful error handling for syntax & type issues?
|
|
91
|
+
|
|
92
|
+
def _get_validation_settings_raw(self, name=None):
|
|
93
|
+
name = name or self.default_validation_context
|
|
94
|
+
try:
|
|
95
|
+
return self.validation_contexts[name]
|
|
96
|
+
except KeyError:
|
|
97
|
+
raise ConfigurationError(
|
|
98
|
+
f"There is no validation context named '{name}'."
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def get_validation_context(
|
|
102
|
+
self, name: Optional[str] = None, as_dict: bool = False
|
|
103
|
+
):
|
|
104
|
+
"""
|
|
105
|
+
Retrieve a validation context by name.
|
|
106
|
+
|
|
107
|
+
:param name:
|
|
108
|
+
The name of the validation context. If not supplied, the value
|
|
109
|
+
of :attr:`default_validation_context` will be used.
|
|
110
|
+
:param as_dict:
|
|
111
|
+
If ``True`` return the settings as a keyword argument dictionary.
|
|
112
|
+
If ``False`` (the default), return a
|
|
113
|
+
:class:`~pyhanko_certvalidator.context.ValidationContext`
|
|
114
|
+
object.
|
|
115
|
+
"""
|
|
116
|
+
vc_config = self._get_validation_settings_raw(name)
|
|
117
|
+
vc_kwargs = parse_trust_config(
|
|
118
|
+
vc_config, self.time_tolerance, self.retroactive_revinfo
|
|
119
|
+
)
|
|
120
|
+
return vc_kwargs if as_dict else ValidationContext(**vc_kwargs)
|
|
121
|
+
|
|
122
|
+
def get_signer_key_usages(
|
|
123
|
+
self, name: Optional[str] = None
|
|
124
|
+
) -> KeyUsageConstraints:
|
|
125
|
+
"""
|
|
126
|
+
Get a set of key usage constraints for a given validation context.
|
|
127
|
+
|
|
128
|
+
:param name:
|
|
129
|
+
The name of the validation context. If not supplied, the value
|
|
130
|
+
of :attr:`default_validation_context` will be used.
|
|
131
|
+
:return:
|
|
132
|
+
A :class:`.KeyUsageConstraints` object.
|
|
133
|
+
"""
|
|
134
|
+
vc_config = self._get_validation_settings_raw(name)
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
policy_settings = dict(vc_config['signer-key-usage-policy'])
|
|
138
|
+
except KeyError:
|
|
139
|
+
policy_settings = {}
|
|
140
|
+
|
|
141
|
+
# fallbacks to stay compatible with the simpler 0.5.0 signer-key-usage
|
|
142
|
+
# and signer-extd-key-usage settings: copy old settings keys to
|
|
143
|
+
# their corresponding values in the new one
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
key_usage_strings = vc_config['signer-key-usage']
|
|
147
|
+
policy_settings.setdefault('key-usage', key_usage_strings)
|
|
148
|
+
except KeyError:
|
|
149
|
+
pass
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
key_usage_strings = vc_config['signer-extd-key-usage']
|
|
153
|
+
policy_settings.setdefault('extd-key-usage', key_usage_strings)
|
|
154
|
+
except KeyError:
|
|
155
|
+
pass
|
|
156
|
+
|
|
157
|
+
return KeyUsageConstraints.from_config(policy_settings)
|
|
158
|
+
|
|
159
|
+
def get_stamp_style(
|
|
160
|
+
self, name: Optional[str] = None
|
|
161
|
+
) -> Union[TextStampStyle, QRStampStyle]:
|
|
162
|
+
"""
|
|
163
|
+
Retrieve a stamp style by name.
|
|
164
|
+
|
|
165
|
+
:param name:
|
|
166
|
+
The name of the style. If not supplied, the value
|
|
167
|
+
of :attr:`default_stamp_style` will be used.
|
|
168
|
+
:return:
|
|
169
|
+
A :class:`TextStampStyle` or `QRStampStyle` object.
|
|
170
|
+
"""
|
|
171
|
+
name = name or self.default_stamp_style
|
|
172
|
+
config = self.stamp_styles.get(name, None)
|
|
173
|
+
if config is None:
|
|
174
|
+
raise ConfigurationError(f"There is no stamp style named '{name}'.")
|
|
175
|
+
try:
|
|
176
|
+
style_config = dict(config)
|
|
177
|
+
except (TypeError, ValueError) as e:
|
|
178
|
+
raise ConfigurationError(
|
|
179
|
+
f"Could not process stamp style named '{name}'"
|
|
180
|
+
) from e
|
|
181
|
+
cls = STAMP_STYLE_TYPES[style_config.pop('type', 'text')]
|
|
182
|
+
return cls.from_config(style_config)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@dataclass(frozen=True)
|
|
186
|
+
class CLIRootConfig:
|
|
187
|
+
"""
|
|
188
|
+
Config settings that are only relevant tothe CLI root and are not exposed
|
|
189
|
+
to subcommands and plugins.
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
config: CLIConfig
|
|
193
|
+
"""
|
|
194
|
+
General CLI config.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
log_config: Dict[Optional[str], LogConfig]
|
|
198
|
+
"""
|
|
199
|
+
Per-module logging configuration. The keys in this dictionary are
|
|
200
|
+
module names, the :class:`.LogConfig` values define the logging settings.
|
|
201
|
+
|
|
202
|
+
The ``None`` key houses the configuration for the root logger, if any.
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
plugin_endpoints: List[str]
|
|
206
|
+
"""
|
|
207
|
+
List of plugin endpoints to load, of the form
|
|
208
|
+
``package.module:PluginClass``.
|
|
209
|
+
See :class:`~pyhanko.cli.plugin_api.SigningCommandPlugin`.
|
|
210
|
+
|
|
211
|
+
The value of this setting is ignored if ``--no-plugins`` is passed.
|
|
212
|
+
|
|
213
|
+
.. note::
|
|
214
|
+
This is convenient for importing plugin classes that don't live in
|
|
215
|
+
installed packages for some reason or another.
|
|
216
|
+
|
|
217
|
+
Plugins that are part of packages should define their endpoints
|
|
218
|
+
in the package metadata, which will allow them to be discovered
|
|
219
|
+
automatically. See the docs for
|
|
220
|
+
:class:`~pyhanko.cli.plugin_api.SigningCommandPlugin` for more
|
|
221
|
+
information.
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# TODO allow CRL/OCSP loading here as well (esp. CRL loading might be useful
|
|
226
|
+
# in some cases)
|
|
227
|
+
# Time-related settings are probably better off in the CLI.
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
DEFAULT_VALIDATION_CONTEXT = DEFAULT_STAMP_STYLE = 'default'
|
|
231
|
+
STAMP_STYLE_TYPES: Dict[str, Type[BaseStampStyle]] = {
|
|
232
|
+
'qr': QRStampStyle,
|
|
233
|
+
'text': TextStampStyle,
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def parse_cli_config(yaml_str) -> CLIRootConfig:
|
|
238
|
+
config_dict = yaml.safe_load(yaml_str) or {}
|
|
239
|
+
return CLIRootConfig(
|
|
240
|
+
**process_root_config_settings(config_dict),
|
|
241
|
+
config=CLIConfig(
|
|
242
|
+
**process_config_dict(config_dict), raw_config=config_dict
|
|
243
|
+
),
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def process_root_config_settings(config_dict: dict) -> dict:
|
|
248
|
+
plugins = config_dict.get('plugins', [])
|
|
249
|
+
# logging config
|
|
250
|
+
log_config_spec = config_dict.get('logging', {})
|
|
251
|
+
log_config = parse_logging_config(log_config_spec)
|
|
252
|
+
return dict(
|
|
253
|
+
log_config=log_config,
|
|
254
|
+
plugin_endpoints=plugins,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def process_config_dict(config_dict: dict) -> dict:
|
|
259
|
+
# validation context config
|
|
260
|
+
vcs: Dict[str, dict] = {DEFAULT_VALIDATION_CONTEXT: {}}
|
|
261
|
+
try:
|
|
262
|
+
vc_specs = config_dict['validation-contexts']
|
|
263
|
+
vcs.update(vc_specs)
|
|
264
|
+
except KeyError:
|
|
265
|
+
pass
|
|
266
|
+
|
|
267
|
+
# stamp style config
|
|
268
|
+
# TODO this style is obviously not suited for non-signing scenarios
|
|
269
|
+
# (but it'll do for now)
|
|
270
|
+
stamp_configs = {
|
|
271
|
+
DEFAULT_STAMP_STYLE: {
|
|
272
|
+
'stamp-text': DEFAULT_SIGNING_STAMP_STYLE.stamp_text,
|
|
273
|
+
'background': '__stamp__',
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
try:
|
|
277
|
+
stamp_specs = config_dict['stamp-styles']
|
|
278
|
+
stamp_configs.update(stamp_specs)
|
|
279
|
+
except KeyError:
|
|
280
|
+
pass
|
|
281
|
+
|
|
282
|
+
# some misc settings
|
|
283
|
+
default_vc = config_dict.get(
|
|
284
|
+
'default-validation-context', DEFAULT_VALIDATION_CONTEXT
|
|
285
|
+
)
|
|
286
|
+
default_stamp_style = config_dict.get(
|
|
287
|
+
'default-stamp-style', DEFAULT_STAMP_STYLE
|
|
288
|
+
)
|
|
289
|
+
time_tolerance_seconds = config_dict.get(
|
|
290
|
+
'time-tolerance', DEFAULT_TIME_TOLERANCE.seconds
|
|
291
|
+
)
|
|
292
|
+
if not isinstance(time_tolerance_seconds, int):
|
|
293
|
+
raise ConfigurationError(
|
|
294
|
+
"time-tolerance parameter must be specified in seconds"
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
time_tolerance = timedelta(seconds=time_tolerance_seconds)
|
|
298
|
+
retroactive_revinfo = bool(config_dict.get('retroactive-revinfo', False))
|
|
299
|
+
return dict(
|
|
300
|
+
validation_contexts=vcs,
|
|
301
|
+
default_validation_context=default_vc,
|
|
302
|
+
time_tolerance=time_tolerance,
|
|
303
|
+
retroactive_revinfo=retroactive_revinfo,
|
|
304
|
+
stamp_styles=stamp_configs,
|
|
305
|
+
default_stamp_style=default_stamp_style,
|
|
306
|
+
)
|