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,66 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from pyhanko.cli.commands.signing import signing
|
|
3
|
+
from pyhanko.cli.runtime import pyhanko_exception_manager
|
|
4
|
+
from pyhanko.cli.utils import parse_field_location_spec
|
|
5
|
+
from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter
|
|
6
|
+
from pyhanko.pdf_utils.reader import PdfFileReader
|
|
7
|
+
from pyhanko.pdf_utils.writer import copy_into_new_writer
|
|
8
|
+
from pyhanko.sign import fields
|
|
9
|
+
|
|
10
|
+
__all__ = ['list_sigfields', 'add_sig_field']
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@signing.command(name='list', help='list signature fields')
|
|
14
|
+
@click.argument('infile', type=click.File('rb'))
|
|
15
|
+
@click.option(
|
|
16
|
+
'--skip-status',
|
|
17
|
+
help='do not print status',
|
|
18
|
+
required=False,
|
|
19
|
+
type=bool,
|
|
20
|
+
is_flag=True,
|
|
21
|
+
default=False,
|
|
22
|
+
show_default=True,
|
|
23
|
+
)
|
|
24
|
+
def list_sigfields(infile, skip_status):
|
|
25
|
+
with pyhanko_exception_manager():
|
|
26
|
+
r = PdfFileReader(infile, strict=False)
|
|
27
|
+
field_info = fields.enumerate_sig_fields(r)
|
|
28
|
+
for ix, (name, value, field_ref) in enumerate(field_info):
|
|
29
|
+
if skip_status:
|
|
30
|
+
click.echo(name)
|
|
31
|
+
continue
|
|
32
|
+
click.echo(f"{name}:{'EMPTY' if value is None else 'FILLED'}")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@signing.command(
|
|
36
|
+
name='addfields', help='add empty signature fields to a PDF field'
|
|
37
|
+
)
|
|
38
|
+
@click.argument('infile', type=click.File('rb'))
|
|
39
|
+
@click.argument('outfile', type=click.File('wb'))
|
|
40
|
+
@click.option(
|
|
41
|
+
'--field',
|
|
42
|
+
metavar='PAGE/X1,Y1,X2,Y2/NAME',
|
|
43
|
+
multiple=True,
|
|
44
|
+
required=True,
|
|
45
|
+
help="Field specification (multiple allowed)",
|
|
46
|
+
)
|
|
47
|
+
@click.option(
|
|
48
|
+
'--resave',
|
|
49
|
+
is_flag=True,
|
|
50
|
+
help="Resave the PDF document instead of creating an incremental update",
|
|
51
|
+
)
|
|
52
|
+
def add_sig_field(infile, outfile, field, resave):
|
|
53
|
+
with pyhanko_exception_manager():
|
|
54
|
+
if resave:
|
|
55
|
+
writer = copy_into_new_writer(PdfFileReader(infile, strict=False))
|
|
56
|
+
else:
|
|
57
|
+
writer = IncrementalPdfFileWriter(infile, strict=False)
|
|
58
|
+
|
|
59
|
+
for s in field:
|
|
60
|
+
name, spec = parse_field_location_spec(s)
|
|
61
|
+
assert spec is not None
|
|
62
|
+
fields.append_signature_field(writer, spec)
|
|
63
|
+
|
|
64
|
+
writer.write(outfile)
|
|
65
|
+
infile.close()
|
|
66
|
+
outfile.close()
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from pyhanko.cli._root import cli_root
|
|
5
|
+
from pyhanko.cli._trust import (
|
|
6
|
+
_get_key_usage_settings,
|
|
7
|
+
build_vc_kwargs,
|
|
8
|
+
trust_options,
|
|
9
|
+
)
|
|
10
|
+
from pyhanko.cli.commands.signing.plugin import command_from_plugin
|
|
11
|
+
from pyhanko.cli.commands.stamp import select_style
|
|
12
|
+
from pyhanko.cli.utils import parse_field_location_spec
|
|
13
|
+
from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter
|
|
14
|
+
from pyhanko.sign import DEFAULT_SIGNER_KEY_USAGE, fields, signers
|
|
15
|
+
from pyhanko.sign.signers.pdf_byterange import BuildProps
|
|
16
|
+
from pyhanko.sign.timestamps import HTTPTimeStamper
|
|
17
|
+
from pyhanko.version import __version__
|
|
18
|
+
|
|
19
|
+
from pyhanko_certvalidator import ValidationContext
|
|
20
|
+
|
|
21
|
+
from ..._ctx import CLIContext
|
|
22
|
+
from ...plugin_api import SigningCommandPlugin
|
|
23
|
+
|
|
24
|
+
__all__ = ['signing', 'addsig', 'register']
|
|
25
|
+
|
|
26
|
+
from ...runtime import pyhanko_exception_manager
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@cli_root.group(help='sign PDFs and other files', name='sign')
|
|
30
|
+
def signing():
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@trust_options
|
|
35
|
+
@signing.group(name='addsig', help='add a signature')
|
|
36
|
+
@click.option(
|
|
37
|
+
'--field',
|
|
38
|
+
help=(
|
|
39
|
+
'signature field name, or field specification PAGE/X1,Y1,X2,Y2/NAME '
|
|
40
|
+
'(required unless the field contains exactly one signature field)'
|
|
41
|
+
),
|
|
42
|
+
required=False,
|
|
43
|
+
)
|
|
44
|
+
@click.option('--name', help='explicitly specify signer name', required=False)
|
|
45
|
+
@click.option('--reason', help='reason for signing', required=False)
|
|
46
|
+
@click.option('--location', help='location of signing', required=False)
|
|
47
|
+
@click.option('--contact-info', help='contact of the signer', required=False)
|
|
48
|
+
@click.option(
|
|
49
|
+
'--certify',
|
|
50
|
+
help='add certification signature',
|
|
51
|
+
required=False,
|
|
52
|
+
default=False,
|
|
53
|
+
is_flag=True,
|
|
54
|
+
type=bool,
|
|
55
|
+
show_default=True,
|
|
56
|
+
)
|
|
57
|
+
@click.option(
|
|
58
|
+
'--existing-only',
|
|
59
|
+
help='never create signature fields',
|
|
60
|
+
required=False,
|
|
61
|
+
default=False,
|
|
62
|
+
is_flag=True,
|
|
63
|
+
type=bool,
|
|
64
|
+
show_default=True,
|
|
65
|
+
)
|
|
66
|
+
@click.option(
|
|
67
|
+
'--timestamp-url',
|
|
68
|
+
help='URL for timestamp server',
|
|
69
|
+
required=False,
|
|
70
|
+
type=str,
|
|
71
|
+
default=None,
|
|
72
|
+
)
|
|
73
|
+
@click.option(
|
|
74
|
+
'--use-pades',
|
|
75
|
+
help='sign PAdES-style [level B/B-T/B-LT/B-LTA]',
|
|
76
|
+
required=False,
|
|
77
|
+
default=False,
|
|
78
|
+
is_flag=True,
|
|
79
|
+
type=bool,
|
|
80
|
+
show_default=True,
|
|
81
|
+
)
|
|
82
|
+
@click.option(
|
|
83
|
+
'--use-pades-lta',
|
|
84
|
+
help='produce PAdES-B-LTA signature',
|
|
85
|
+
required=False,
|
|
86
|
+
default=False,
|
|
87
|
+
is_flag=True,
|
|
88
|
+
type=bool,
|
|
89
|
+
show_default=True,
|
|
90
|
+
)
|
|
91
|
+
@click.option(
|
|
92
|
+
'--prefer-pss',
|
|
93
|
+
is_flag=True,
|
|
94
|
+
default=False,
|
|
95
|
+
type=bool,
|
|
96
|
+
help='prefer RSASSA-PSS to PKCS#1 v1.5 padding, if available',
|
|
97
|
+
)
|
|
98
|
+
@click.option(
|
|
99
|
+
'--with-validation-info',
|
|
100
|
+
help='embed revocation info',
|
|
101
|
+
required=False,
|
|
102
|
+
default=False,
|
|
103
|
+
is_flag=True,
|
|
104
|
+
type=bool,
|
|
105
|
+
show_default=True,
|
|
106
|
+
)
|
|
107
|
+
@click.option(
|
|
108
|
+
'--style-name',
|
|
109
|
+
help='stamp style name for signature appearance',
|
|
110
|
+
required=False,
|
|
111
|
+
type=str,
|
|
112
|
+
)
|
|
113
|
+
@click.option(
|
|
114
|
+
'--stamp-url',
|
|
115
|
+
help='QR code URL to use in QR stamp style',
|
|
116
|
+
required=False,
|
|
117
|
+
type=str,
|
|
118
|
+
)
|
|
119
|
+
@click.option(
|
|
120
|
+
'--detach',
|
|
121
|
+
type=bool,
|
|
122
|
+
is_flag=True,
|
|
123
|
+
default=False,
|
|
124
|
+
help=(
|
|
125
|
+
'write only the signature CMS object to the output file; '
|
|
126
|
+
'this can be used to sign non-PDF files'
|
|
127
|
+
),
|
|
128
|
+
)
|
|
129
|
+
@click.option(
|
|
130
|
+
'--detach-pem',
|
|
131
|
+
help='output PEM data instead of DER when using --detach',
|
|
132
|
+
type=bool,
|
|
133
|
+
is_flag=True,
|
|
134
|
+
default=False,
|
|
135
|
+
)
|
|
136
|
+
@click.option(
|
|
137
|
+
'--retroactive-revinfo',
|
|
138
|
+
help='Treat revocation info as retroactively valid '
|
|
139
|
+
'(i.e. ignore thisUpdate timestamp)',
|
|
140
|
+
type=bool,
|
|
141
|
+
is_flag=True,
|
|
142
|
+
default=False,
|
|
143
|
+
show_default=True,
|
|
144
|
+
)
|
|
145
|
+
@click.option(
|
|
146
|
+
'--no-strict-syntax',
|
|
147
|
+
help='Attempt to ignore syntactical problems in the input file '
|
|
148
|
+
'and enable signature creation in hybrid-reference files.'
|
|
149
|
+
'(warning: such documents may behave in unexpected ways)',
|
|
150
|
+
type=bool,
|
|
151
|
+
is_flag=True,
|
|
152
|
+
default=False,
|
|
153
|
+
show_default=True,
|
|
154
|
+
)
|
|
155
|
+
@click.pass_context
|
|
156
|
+
def addsig(
|
|
157
|
+
ctx: click.Context,
|
|
158
|
+
field,
|
|
159
|
+
name,
|
|
160
|
+
reason,
|
|
161
|
+
contact_info,
|
|
162
|
+
location,
|
|
163
|
+
certify,
|
|
164
|
+
existing_only,
|
|
165
|
+
timestamp_url,
|
|
166
|
+
use_pades,
|
|
167
|
+
use_pades_lta,
|
|
168
|
+
with_validation_info,
|
|
169
|
+
validation_context,
|
|
170
|
+
trust_replace,
|
|
171
|
+
trust,
|
|
172
|
+
other_certs,
|
|
173
|
+
style_name,
|
|
174
|
+
stamp_url,
|
|
175
|
+
prefer_pss,
|
|
176
|
+
retroactive_revinfo,
|
|
177
|
+
detach,
|
|
178
|
+
detach_pem,
|
|
179
|
+
no_strict_syntax,
|
|
180
|
+
):
|
|
181
|
+
ctx_obj: CLIContext = ctx.obj
|
|
182
|
+
ctx_obj.existing_fields_only = existing_only or field is None
|
|
183
|
+
ctx_obj.timestamp_url = timestamp_url
|
|
184
|
+
ctx_obj.prefer_pss = prefer_pss
|
|
185
|
+
|
|
186
|
+
if detach or detach_pem:
|
|
187
|
+
ctx_obj.detach_pem = detach_pem
|
|
188
|
+
ctx_obj.sig_settings = None
|
|
189
|
+
if field:
|
|
190
|
+
raise click.ClickException(
|
|
191
|
+
"--field is not compatible with --detach or --detach-pem"
|
|
192
|
+
)
|
|
193
|
+
return # everything else doesn't apply
|
|
194
|
+
|
|
195
|
+
if use_pades_lta:
|
|
196
|
+
use_pades = with_validation_info = True
|
|
197
|
+
if not timestamp_url:
|
|
198
|
+
raise click.ClickException(
|
|
199
|
+
"--timestamp-url is required for --use-pades-lta"
|
|
200
|
+
)
|
|
201
|
+
if use_pades:
|
|
202
|
+
subfilter = fields.SigSeedSubFilter.PADES
|
|
203
|
+
else:
|
|
204
|
+
subfilter = fields.SigSeedSubFilter.ADOBE_PKCS7_DETACHED
|
|
205
|
+
|
|
206
|
+
key_usage = DEFAULT_SIGNER_KEY_USAGE
|
|
207
|
+
if with_validation_info:
|
|
208
|
+
vc_kwargs = build_vc_kwargs(
|
|
209
|
+
ctx.obj.config,
|
|
210
|
+
validation_context,
|
|
211
|
+
trust,
|
|
212
|
+
trust_replace,
|
|
213
|
+
other_certs,
|
|
214
|
+
retroactive_revinfo,
|
|
215
|
+
allow_fetching=True,
|
|
216
|
+
)
|
|
217
|
+
vc = ValidationContext(**vc_kwargs)
|
|
218
|
+
key_usage_sett = _get_key_usage_settings(ctx, validation_context)
|
|
219
|
+
if key_usage_sett is not None and key_usage_sett.key_usage is not None:
|
|
220
|
+
key_usage = key_usage_sett.key_usage
|
|
221
|
+
else:
|
|
222
|
+
vc = None
|
|
223
|
+
field_name: Optional[str]
|
|
224
|
+
if field:
|
|
225
|
+
field_name, new_field_spec = parse_field_location_spec(
|
|
226
|
+
field, require_full_spec=False
|
|
227
|
+
)
|
|
228
|
+
else:
|
|
229
|
+
field_name = new_field_spec = None
|
|
230
|
+
if new_field_spec and existing_only:
|
|
231
|
+
raise click.ClickException(
|
|
232
|
+
"Specifying field coordinates is incompatible with --existing-only"
|
|
233
|
+
)
|
|
234
|
+
ctx_obj.sig_settings = signers.PdfSignatureMetadata(
|
|
235
|
+
field_name=field_name,
|
|
236
|
+
location=location,
|
|
237
|
+
reason=reason,
|
|
238
|
+
contact_info=contact_info,
|
|
239
|
+
name=name,
|
|
240
|
+
certify=certify,
|
|
241
|
+
subfilter=subfilter,
|
|
242
|
+
embed_validation_info=with_validation_info,
|
|
243
|
+
validation_context=vc,
|
|
244
|
+
signer_key_usage=key_usage,
|
|
245
|
+
use_pades_lta=use_pades_lta,
|
|
246
|
+
app_build_props=BuildProps(name='pyHanko CLI', revision=__version__),
|
|
247
|
+
)
|
|
248
|
+
ctx_obj.new_field_spec = new_field_spec
|
|
249
|
+
ctx_obj.stamp_style = select_style(ctx, style_name, stamp_url)
|
|
250
|
+
ctx_obj.stamp_url = stamp_url
|
|
251
|
+
ctx_obj.lenient = no_strict_syntax
|
|
252
|
+
ctx_obj.ux.visible_signature_desired = bool(style_name or new_field_spec)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _unavailable(signer_plugin: SigningCommandPlugin):
|
|
256
|
+
def _unavailable_callback():
|
|
257
|
+
raise click.ClickException(
|
|
258
|
+
signer_plugin.unavailable_message
|
|
259
|
+
or "This subcommand is not available"
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
return _unavailable_callback
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def register(plugins: List[SigningCommandPlugin]):
|
|
266
|
+
# we reset the command list before (re)populating it, in order to
|
|
267
|
+
# make the tests more consistent
|
|
268
|
+
addsig.commands = {}
|
|
269
|
+
for signer_plugin in plugins:
|
|
270
|
+
if signer_plugin.is_available():
|
|
271
|
+
addsig.add_command(command_from_plugin(signer_plugin))
|
|
272
|
+
else:
|
|
273
|
+
|
|
274
|
+
addsig.add_command(
|
|
275
|
+
click.Command(
|
|
276
|
+
name=signer_plugin.subcommand_name,
|
|
277
|
+
help=signer_plugin.help_summary + " [unavailable]",
|
|
278
|
+
callback=_unavailable(signer_plugin),
|
|
279
|
+
)
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
readable_file = click.Path(exists=True, readable=True, dir_okay=False)
|
|
284
|
+
writable_file = click.Path(writable=True, dir_okay=False)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
@trust_options
|
|
288
|
+
@signing.command(name='timestamp', help='add timestamp to PDF')
|
|
289
|
+
@click.argument('infile', type=readable_file)
|
|
290
|
+
@click.argument('outfile', type=writable_file)
|
|
291
|
+
@click.option(
|
|
292
|
+
'--timestamp-url',
|
|
293
|
+
help='URL for timestamp server',
|
|
294
|
+
required=True,
|
|
295
|
+
type=str,
|
|
296
|
+
default=None,
|
|
297
|
+
)
|
|
298
|
+
@click.pass_context
|
|
299
|
+
def timestamp(
|
|
300
|
+
ctx,
|
|
301
|
+
infile,
|
|
302
|
+
outfile,
|
|
303
|
+
validation_context,
|
|
304
|
+
trust,
|
|
305
|
+
trust_replace,
|
|
306
|
+
other_certs,
|
|
307
|
+
timestamp_url,
|
|
308
|
+
):
|
|
309
|
+
with pyhanko_exception_manager():
|
|
310
|
+
vc_kwargs = build_vc_kwargs(
|
|
311
|
+
ctx.obj.config,
|
|
312
|
+
validation_context,
|
|
313
|
+
trust,
|
|
314
|
+
trust_replace,
|
|
315
|
+
other_certs,
|
|
316
|
+
retroactive_revinfo=True,
|
|
317
|
+
)
|
|
318
|
+
timestamper = HTTPTimeStamper(timestamp_url)
|
|
319
|
+
with open(infile, 'rb') as inf:
|
|
320
|
+
w = IncrementalPdfFileWriter(inf)
|
|
321
|
+
pdf_timestamper = signers.PdfTimeStamper(timestamper)
|
|
322
|
+
with open(outfile, 'wb') as outf:
|
|
323
|
+
pdf_timestamper.timestamp_pdf(
|
|
324
|
+
w,
|
|
325
|
+
'sha256',
|
|
326
|
+
validation_context=ValidationContext(**vc_kwargs),
|
|
327
|
+
output=outf,
|
|
328
|
+
)
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import getpass
|
|
2
|
+
import os
|
|
3
|
+
from typing import ContextManager, List, Optional
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from pyhanko.cli._ctx import CLIContext
|
|
7
|
+
from pyhanko.cli.config import CLIConfig
|
|
8
|
+
from pyhanko.cli.plugin_api import SigningCommandPlugin
|
|
9
|
+
from pyhanko.cli.utils import logger, readable_file
|
|
10
|
+
from pyhanko.config.errors import ConfigurationError
|
|
11
|
+
from pyhanko.config.pkcs11 import (
|
|
12
|
+
PKCS11PinEntryMode,
|
|
13
|
+
PKCS11SignatureConfig,
|
|
14
|
+
TokenCriteria,
|
|
15
|
+
)
|
|
16
|
+
from pyhanko.sign import Signer
|
|
17
|
+
|
|
18
|
+
__all__ = ['PKCS11Plugin']
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
import pkcs11 # lgtm [py/unused-import]
|
|
23
|
+
|
|
24
|
+
pkcs11_available = True
|
|
25
|
+
except ImportError: # pragma: nocover
|
|
26
|
+
pkcs11 = None
|
|
27
|
+
pkcs11_available = False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
P11_PIN_ENV_VAR = "PYHANKO_PKCS11_PIN"
|
|
31
|
+
UNAVAIL_MSG = "This subcommand requires python-pkcs11 to be installed."
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PKCS11Plugin(SigningCommandPlugin):
|
|
35
|
+
subcommand_name = 'pkcs11'
|
|
36
|
+
help_summary = 'use generic PKCS#11 device to sign'
|
|
37
|
+
unavailable_message = UNAVAIL_MSG
|
|
38
|
+
|
|
39
|
+
def is_available(self) -> bool:
|
|
40
|
+
return pkcs11_available
|
|
41
|
+
|
|
42
|
+
def click_options(self) -> List[click.Option]:
|
|
43
|
+
return [
|
|
44
|
+
click.Option(
|
|
45
|
+
('--lib',),
|
|
46
|
+
help='path to PKCS#11 module',
|
|
47
|
+
type=readable_file,
|
|
48
|
+
required=False,
|
|
49
|
+
),
|
|
50
|
+
click.Option(
|
|
51
|
+
('--token-label',),
|
|
52
|
+
help='PKCS#11 token label',
|
|
53
|
+
type=str,
|
|
54
|
+
required=False,
|
|
55
|
+
),
|
|
56
|
+
click.Option(
|
|
57
|
+
('--cert-label',),
|
|
58
|
+
help='certificate label',
|
|
59
|
+
type=str,
|
|
60
|
+
required=False,
|
|
61
|
+
),
|
|
62
|
+
click.Option(
|
|
63
|
+
('--raw-mechanism',),
|
|
64
|
+
help='invoke raw PKCS#11 mechanism',
|
|
65
|
+
type=bool,
|
|
66
|
+
is_flag=True,
|
|
67
|
+
required=False,
|
|
68
|
+
),
|
|
69
|
+
click.Option(
|
|
70
|
+
('--key-label',), help='key label', type=str, required=False
|
|
71
|
+
),
|
|
72
|
+
click.Option(
|
|
73
|
+
('--slot-no',),
|
|
74
|
+
help='specify PKCS#11 slot to use',
|
|
75
|
+
required=False,
|
|
76
|
+
type=int,
|
|
77
|
+
default=None,
|
|
78
|
+
),
|
|
79
|
+
click.Option(
|
|
80
|
+
('--skip-user-pin',),
|
|
81
|
+
type=bool,
|
|
82
|
+
show_default=True,
|
|
83
|
+
default=False,
|
|
84
|
+
required=False,
|
|
85
|
+
is_flag=True,
|
|
86
|
+
help='do not prompt for PIN (e.g. if the token has a PIN pad)',
|
|
87
|
+
),
|
|
88
|
+
click.Option(
|
|
89
|
+
('--p11-setup',),
|
|
90
|
+
type=str,
|
|
91
|
+
required=False,
|
|
92
|
+
help='name of preconfigured PKCS#11 profile (overrides all '
|
|
93
|
+
'other options)',
|
|
94
|
+
),
|
|
95
|
+
click.Option(
|
|
96
|
+
('--other-cert',),
|
|
97
|
+
type=str,
|
|
98
|
+
required=False,
|
|
99
|
+
help='label of other cert to pull (multiple uses allowed)',
|
|
100
|
+
multiple=True,
|
|
101
|
+
),
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
def create_signer(
|
|
105
|
+
self, context: CLIContext, **kwargs
|
|
106
|
+
) -> ContextManager[Signer]:
|
|
107
|
+
return _pkcs11_signer_context(context, **kwargs)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _pkcs11_signer_context(
|
|
111
|
+
ctx: CLIContext,
|
|
112
|
+
lib,
|
|
113
|
+
token_label,
|
|
114
|
+
cert_label,
|
|
115
|
+
key_label,
|
|
116
|
+
slot_no,
|
|
117
|
+
skip_user_pin,
|
|
118
|
+
p11_setup,
|
|
119
|
+
raw_mechanism,
|
|
120
|
+
other_cert,
|
|
121
|
+
):
|
|
122
|
+
from pyhanko.sign import pkcs11
|
|
123
|
+
|
|
124
|
+
if p11_setup:
|
|
125
|
+
cli_config: Optional[CLIConfig] = ctx.config
|
|
126
|
+
if cli_config is None:
|
|
127
|
+
raise click.ClickException(
|
|
128
|
+
"The --p11-setup option requires a configuration file"
|
|
129
|
+
)
|
|
130
|
+
try:
|
|
131
|
+
pkcs11_config = ModuleConfigWrapper(cli_config).get_pkcs11_config(
|
|
132
|
+
p11_setup
|
|
133
|
+
)
|
|
134
|
+
except ConfigurationError as e:
|
|
135
|
+
msg = f"Error while reading PKCS#11 config {p11_setup}"
|
|
136
|
+
logger.error(msg, exc_info=e)
|
|
137
|
+
raise click.ClickException(msg)
|
|
138
|
+
else:
|
|
139
|
+
if not (lib and cert_label):
|
|
140
|
+
raise click.ClickException(
|
|
141
|
+
"The parameters --lib and --cert-label are required."
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
pinentry_mode = (
|
|
145
|
+
PKCS11PinEntryMode.SKIP
|
|
146
|
+
if skip_user_pin
|
|
147
|
+
else PKCS11PinEntryMode.PROMPT
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
pkcs11_config = PKCS11SignatureConfig(
|
|
151
|
+
module_path=lib,
|
|
152
|
+
cert_label=cert_label,
|
|
153
|
+
key_label=key_label,
|
|
154
|
+
slot_no=slot_no,
|
|
155
|
+
token_criteria=TokenCriteria(token_label),
|
|
156
|
+
# for now, DEFER requires a config file
|
|
157
|
+
prompt_pin=pinentry_mode,
|
|
158
|
+
raw_mechanism=raw_mechanism,
|
|
159
|
+
other_certs_to_pull=other_cert,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
pin = pkcs11_config.user_pin
|
|
163
|
+
|
|
164
|
+
# try to fetch the PIN from an env var
|
|
165
|
+
if pin is None:
|
|
166
|
+
pin_env = os.environ.get(P11_PIN_ENV_VAR, None)
|
|
167
|
+
if pin_env:
|
|
168
|
+
pin = pin_env.strip()
|
|
169
|
+
|
|
170
|
+
if pkcs11_config.prompt_pin == PKCS11PinEntryMode.PROMPT and pin is None:
|
|
171
|
+
pin = getpass.getpass(prompt='PKCS#11 user PIN: ')
|
|
172
|
+
return pkcs11.PKCS11SigningContext(pkcs11_config, user_pin=pin)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class ModuleConfigWrapper:
|
|
176
|
+
def __init__(self, config: CLIConfig):
|
|
177
|
+
config_dict = config.raw_config
|
|
178
|
+
self.pkcs11_setups = config_dict.get('pkcs11-setups', {})
|
|
179
|
+
|
|
180
|
+
def get_pkcs11_config(self, name):
|
|
181
|
+
try:
|
|
182
|
+
setup = self.pkcs11_setups[name]
|
|
183
|
+
except KeyError:
|
|
184
|
+
raise ConfigurationError(f"There's no PKCS#11 setup named '{name}'")
|
|
185
|
+
return PKCS11SignatureConfig.from_config(setup)
|