pyhanko-cli 0.1.0__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.
- pyhanko_cli-0.1.0/LICENSE +21 -0
- pyhanko_cli-0.1.0/MANIFEST.in +5 -0
- pyhanko_cli-0.1.0/PKG-INFO +68 -0
- pyhanko_cli-0.1.0/README.md +34 -0
- pyhanko_cli-0.1.0/pyproject.toml +101 -0
- pyhanko_cli-0.1.0/setup.cfg +4 -0
- pyhanko_cli-0.1.0/src/pyhanko/__main__.py +9 -0
- pyhanko_cli-0.1.0/src/pyhanko/cli/__init__.py +12 -0
- pyhanko_cli-0.1.0/src/pyhanko/cli/_ctx.py +92 -0
- pyhanko_cli-0.1.0/src/pyhanko/cli/_root.py +171 -0
- pyhanko_cli-0.1.0/src/pyhanko/cli/_trust.py +159 -0
- pyhanko_cli-0.1.0/src/pyhanko/cli/commands/__init__.py +0 -0
- pyhanko_cli-0.1.0/src/pyhanko/cli/commands/crypt.py +219 -0
- pyhanko_cli-0.1.0/src/pyhanko/cli/commands/fields.py +66 -0
- pyhanko_cli-0.1.0/src/pyhanko/cli/commands/signing/__init__.py +328 -0
- pyhanko_cli-0.1.0/src/pyhanko/cli/commands/signing/pkcs11_cli.py +185 -0
- pyhanko_cli-0.1.0/src/pyhanko/cli/commands/signing/plugin.py +144 -0
- pyhanko_cli-0.1.0/src/pyhanko/cli/commands/signing/simple.py +260 -0
- pyhanko_cli-0.1.0/src/pyhanko/cli/commands/signing/utils.py +58 -0
- pyhanko_cli-0.1.0/src/pyhanko/cli/commands/stamp.py +100 -0
- pyhanko_cli-0.1.0/src/pyhanko/cli/commands/validation/__init__.py +2 -0
- pyhanko_cli-0.1.0/src/pyhanko/cli/commands/validation/ltv.py +136 -0
- pyhanko_cli-0.1.0/src/pyhanko/cli/commands/validation/validate.py +355 -0
- pyhanko_cli-0.1.0/src/pyhanko/cli/config.py +306 -0
- pyhanko_cli-0.1.0/src/pyhanko/cli/plugin_api.py +118 -0
- pyhanko_cli-0.1.0/src/pyhanko/cli/py.typed +0 -0
- pyhanko_cli-0.1.0/src/pyhanko/cli/runtime.py +80 -0
- pyhanko_cli-0.1.0/src/pyhanko/cli/utils.py +68 -0
- pyhanko_cli-0.1.0/src/pyhanko/cli/version.py +2 -0
- pyhanko_cli-0.1.0/src/pyhanko_cli.egg-info/PKG-INFO +68 -0
- pyhanko_cli-0.1.0/src/pyhanko_cli.egg-info/SOURCES.txt +33 -0
- pyhanko_cli-0.1.0/src/pyhanko_cli.egg-info/dependency_links.txt +1 -0
- pyhanko_cli-0.1.0/src/pyhanko_cli.egg-info/entry_points.txt +2 -0
- pyhanko_cli-0.1.0/src/pyhanko_cli.egg-info/requires.txt +5 -0
- pyhanko_cli-0.1.0/src/pyhanko_cli.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020-2023 Matthias Valvekens
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Exclude tests from sdist until we have clear requirements
|
|
2
|
+
# from downstreams on how to package test data / build setup stuff
|
|
3
|
+
# in a monorepo context. All things considered, building from the whole
|
|
4
|
+
# source tarball seems like the only reasonable option in a context like that
|
|
5
|
+
prune tests
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyhanko-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI tools for stamping and signing PDF files
|
|
5
|
+
Author-email: Matthias Valvekens <dev@mvalvekens.be>
|
|
6
|
+
Maintainer-email: Matthias Valvekens <dev@mvalvekens.be>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/MatthiasValvekens/pyHanko
|
|
9
|
+
Project-URL: Documentation, https://pyhanko.readthedocs.io/
|
|
10
|
+
Project-URL: Changes, https://pyhanko.readthedocs.io/en/latest/changelog.html
|
|
11
|
+
Project-URL: Source Code, https://github.com/MatthiasValvekens/pyHanko
|
|
12
|
+
Project-URL: Issue Tracker, https://github.com/MatthiasValvekens/pyHanko/issues
|
|
13
|
+
Keywords: signature,pdf,pades,digital-signature,pkcs11
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Security :: Cryptography
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
|
+
Requires-Python: >=3.9
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: asn1crypto>=1.5.1
|
|
29
|
+
Requires-Dist: tzlocal>=4.3
|
|
30
|
+
Requires-Dist: pyhanko<0.30,>=0.29.0
|
|
31
|
+
Requires-Dist: pyhanko-certvalidator<0.28,>=0.27.0
|
|
32
|
+
Requires-Dist: click<8.2.0,>=8.1.3
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
The lack of open-source CLI tooling to handle digitally signing and stamping PDF files was bothering me, so I went ahead and rolled my own.
|
|
36
|
+
|
|
37
|
+
### Installing
|
|
38
|
+
|
|
39
|
+
PyHanko is hosted on [PyPI](https://pypi.org/project/pyHanko/),
|
|
40
|
+
and can be installed using `pip`:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install pyhanko-cli
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Documentation
|
|
47
|
+
|
|
48
|
+
The [documentation for pyHanko is hosted on ReadTheDocs](https://pyhanko.readthedocs.io/en/latest/)
|
|
49
|
+
and includes information on CLI usage, library usage, and API reference documentation derived from
|
|
50
|
+
inline docstrings.
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
### Optional features
|
|
54
|
+
|
|
55
|
+
Optional dependencies are managed at the level of the ``pyhanko`` package.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install 'pyHanko[pkcs11,image-support,opentype,qr]' pyhanko-cli
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Depending on your shell, you might have to leave off the quotes:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install pyHanko[pkcs11,image-support,opentype,qr] pyhanko-cli
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
This `pip` invocation includes the optional dependencies required for PKCS#11, image handling,
|
|
68
|
+
OpenType/TrueType support and QR code generation.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
The lack of open-source CLI tooling to handle digitally signing and stamping PDF files was bothering me, so I went ahead and rolled my own.
|
|
2
|
+
|
|
3
|
+
### Installing
|
|
4
|
+
|
|
5
|
+
PyHanko is hosted on [PyPI](https://pypi.org/project/pyHanko/),
|
|
6
|
+
and can be installed using `pip`:
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install pyhanko-cli
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
### Documentation
|
|
13
|
+
|
|
14
|
+
The [documentation for pyHanko is hosted on ReadTheDocs](https://pyhanko.readthedocs.io/en/latest/)
|
|
15
|
+
and includes information on CLI usage, library usage, and API reference documentation derived from
|
|
16
|
+
inline docstrings.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Optional features
|
|
20
|
+
|
|
21
|
+
Optional dependencies are managed at the level of the ``pyhanko`` package.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install 'pyHanko[pkcs11,image-support,opentype,qr]' pyhanko-cli
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Depending on your shell, you might have to leave off the quotes:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install pyHanko[pkcs11,image-support,opentype,qr] pyhanko-cli
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
This `pip` invocation includes the optional dependencies required for PKCS#11, image handling,
|
|
34
|
+
OpenType/TrueType support and QR code generation.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=80.8.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pyhanko-cli"
|
|
7
|
+
description = "CLI tools for stamping and signing PDF files"
|
|
8
|
+
authors = [{name = "Matthias Valvekens", email = "dev@mvalvekens.be"}]
|
|
9
|
+
maintainers = [{name = "Matthias Valvekens", email = "dev@mvalvekens.be"}]
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
keywords = [
|
|
13
|
+
"signature",
|
|
14
|
+
"pdf",
|
|
15
|
+
"pades",
|
|
16
|
+
"digital-signature",
|
|
17
|
+
"pkcs11",
|
|
18
|
+
]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 4 - Beta",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"Operating System :: OS Independent",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.9",
|
|
25
|
+
"Programming Language :: Python :: 3.10",
|
|
26
|
+
"Programming Language :: Python :: 3.11",
|
|
27
|
+
"Programming Language :: Python :: 3.12",
|
|
28
|
+
"Programming Language :: Python :: 3.13",
|
|
29
|
+
"Topic :: Security :: Cryptography",
|
|
30
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
31
|
+
]
|
|
32
|
+
requires-python = ">=3.9"
|
|
33
|
+
dependencies = [
|
|
34
|
+
"asn1crypto>=1.5.1",
|
|
35
|
+
"tzlocal>=4.3",
|
|
36
|
+
"pyhanko>=0.29.0,<0.30",
|
|
37
|
+
"pyhanko-certvalidator>=0.27.0,<0.28",
|
|
38
|
+
"click>=8.1.3,<8.2.0",
|
|
39
|
+
]
|
|
40
|
+
version = "0.1.0"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
[project.readme]
|
|
44
|
+
file = "README.md"
|
|
45
|
+
content-type = "text/markdown"
|
|
46
|
+
|
|
47
|
+
[project.urls]
|
|
48
|
+
Homepage = "https://github.com/MatthiasValvekens/pyHanko"
|
|
49
|
+
Documentation = "https://pyhanko.readthedocs.io/"
|
|
50
|
+
Changes = "https://pyhanko.readthedocs.io/en/latest/changelog.html"
|
|
51
|
+
"Source Code" = "https://github.com/MatthiasValvekens/pyHanko"
|
|
52
|
+
"Issue Tracker" = "https://github.com/MatthiasValvekens/pyHanko/issues"
|
|
53
|
+
|
|
54
|
+
[dependency-groups]
|
|
55
|
+
testing-base = [
|
|
56
|
+
"common-test-utils",
|
|
57
|
+
"pytest>=6.1.1",
|
|
58
|
+
"requests-mock>=1.8.0",
|
|
59
|
+
"freezegun>=1.1.0",
|
|
60
|
+
"certomancer>=0.12.3,<0.13",
|
|
61
|
+
"pytest-cov>=4.0,<6.2",
|
|
62
|
+
"pytest-asyncio==0.24.0;python_version<\"3.9\"",
|
|
63
|
+
"pytest-asyncio==0.26.0;python_version>=\"3.9\"",
|
|
64
|
+
"backports.zoneinfo[tzdata];python_version<\"3.9\"",
|
|
65
|
+
]
|
|
66
|
+
testing = [
|
|
67
|
+
{include-group = "testing-base"},
|
|
68
|
+
"aiohttp>=3.9,<3.12",
|
|
69
|
+
"pytest-aiohttp>=1.0.4,<1.2.0",
|
|
70
|
+
"pyhanko[pkcs11,opentype,async_http,image-support,qr]"
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
[project.scripts]
|
|
74
|
+
pyhanko = "pyhanko.__main__:launch"
|
|
75
|
+
|
|
76
|
+
[tool.setuptools]
|
|
77
|
+
include-package-data = false
|
|
78
|
+
package-dir = {"" = "src"}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
[tool.setuptools.package-data]
|
|
82
|
+
pyhanko = ["py.typed"]
|
|
83
|
+
|
|
84
|
+
[tool.pytest.ini_options]
|
|
85
|
+
pythonpath = ["src"]
|
|
86
|
+
log_format = "%(asctime)s %(levelname)s %(message)s"
|
|
87
|
+
log_date_format = "%Y-%m-%d %H:%M:%S"
|
|
88
|
+
log_cli = true
|
|
89
|
+
log_cli_level = "INFO"
|
|
90
|
+
asyncio_mode = "strict"
|
|
91
|
+
asyncio_default_fixture_loop_scope="function"
|
|
92
|
+
markers = [
|
|
93
|
+
"nosmoke: exclude from smoke test runs"
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
[tool.coverage.run]
|
|
97
|
+
omit = ["*docs*", "*test*", "*venv*", "setup.py", "_saslprep.py"]
|
|
98
|
+
|
|
99
|
+
[tool.coverage.report]
|
|
100
|
+
exclude_lines = ["pragma: no cover", "pragma: nocover", "raise AssertionError", "raise NotImplementedError", "TYPE_CHECKING", "^\\s*\\.\\.\\."]
|
|
101
|
+
precision = 2
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from pyhanko.cli._root import cli_root
|
|
2
|
+
from pyhanko.cli.commands.crypt import *
|
|
3
|
+
from pyhanko.cli.commands.fields import *
|
|
4
|
+
from pyhanko.cli.commands.signing import *
|
|
5
|
+
from pyhanko.cli.commands.stamp import *
|
|
6
|
+
from pyhanko.cli.commands.validation import *
|
|
7
|
+
|
|
8
|
+
__all__ = ['launch', 'cli_root']
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def launch():
|
|
12
|
+
cli_root(prog_name='pyhanko')
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from pyhanko.cli.config import CLIConfig
|
|
5
|
+
from pyhanko.sign import PdfSignatureMetadata
|
|
6
|
+
from pyhanko.sign.fields import SigFieldSpec
|
|
7
|
+
from pyhanko.stamp import BaseStampStyle
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class UXContext:
|
|
12
|
+
"""
|
|
13
|
+
Context object to track information that affects the UX, e.g. user intent
|
|
14
|
+
as inferred from certain argument combinations that are otherwise difficult
|
|
15
|
+
to wire throughout the UI code.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
visible_signature_desired: bool = False
|
|
19
|
+
"""
|
|
20
|
+
Set to `True` if the user explicitly specifies `--field` with a bounding box
|
|
21
|
+
or passes `--style-name` explicitly.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class CLIContext:
|
|
27
|
+
"""
|
|
28
|
+
Context object that cobbles together various CLI settings values that were
|
|
29
|
+
gathered by various subcommands during the lifetime of a CLI invocation,
|
|
30
|
+
either from configuration or from command line arguments.
|
|
31
|
+
This object is passed around as a ``click`` context object.
|
|
32
|
+
|
|
33
|
+
Not all settings are applicable to all subcommands, so all values are
|
|
34
|
+
optional.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
sig_settings: Optional[PdfSignatureMetadata] = None
|
|
38
|
+
"""
|
|
39
|
+
The settings that will be used to produce a new signature.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
config: Optional[CLIConfig] = None
|
|
43
|
+
"""
|
|
44
|
+
Values for CLI configuration settings.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
existing_fields_only: bool = False
|
|
48
|
+
"""
|
|
49
|
+
Whether signing operations should use existing fields only.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
timestamp_url: Optional[str] = None
|
|
53
|
+
"""
|
|
54
|
+
Endpoint URL for the timestamping service to use.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
stamp_style: Optional[BaseStampStyle] = None
|
|
58
|
+
"""
|
|
59
|
+
Stamp style to use for generating visual signature appearances, if
|
|
60
|
+
applicable.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
stamp_url: Optional[str] = None
|
|
64
|
+
"""
|
|
65
|
+
For QR stamp styles, defines the URL used to generate the QR code.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
new_field_spec: Optional[SigFieldSpec] = None
|
|
69
|
+
"""
|
|
70
|
+
Field spec used to generate new signature fields, if applicable.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
prefer_pss: bool = False
|
|
74
|
+
"""
|
|
75
|
+
When working with RSA keys, prefer RSASSA-PSS signing if available.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
detach_pem: bool = False
|
|
79
|
+
"""
|
|
80
|
+
When producing detached signature payloads (i.e. non-PDF CMS), save the
|
|
81
|
+
result in a PEM file instead of in a DER file.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
lenient: bool = False
|
|
85
|
+
"""
|
|
86
|
+
Process PDF files in nonstrict mode.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
ux: UXContext = field(default_factory=UXContext)
|
|
90
|
+
"""
|
|
91
|
+
UX information.
|
|
92
|
+
"""
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Iterable, Optional
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from pyhanko.cli._ctx import CLIContext
|
|
7
|
+
from pyhanko.cli.config import CLIRootConfig, parse_cli_config
|
|
8
|
+
from pyhanko.cli.plugin_api import (
|
|
9
|
+
SIGNING_PLUGIN_ENTRY_POINT_GROUP,
|
|
10
|
+
SIGNING_PLUGIN_REGISTRY,
|
|
11
|
+
)
|
|
12
|
+
from pyhanko.cli.runtime import DEFAULT_CONFIG_FILE, logging_setup
|
|
13
|
+
from pyhanko.cli.version import __version__ as cli_version
|
|
14
|
+
from pyhanko.config.logging import LogConfig, parse_logging_config
|
|
15
|
+
from pyhanko.version import __version__ as lib_version
|
|
16
|
+
|
|
17
|
+
__all__ = ['cli_root']
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
full_version = f"{lib_version} (CLI {cli_version})"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@click.group()
|
|
24
|
+
@click.version_option(prog_name='pyHanko', version=full_version)
|
|
25
|
+
@click.option(
|
|
26
|
+
'--config',
|
|
27
|
+
help=(
|
|
28
|
+
'YAML file to load configuration from'
|
|
29
|
+
f'[default: {DEFAULT_CONFIG_FILE}]'
|
|
30
|
+
),
|
|
31
|
+
required=False,
|
|
32
|
+
type=click.File('r'),
|
|
33
|
+
)
|
|
34
|
+
@click.option(
|
|
35
|
+
'--verbose',
|
|
36
|
+
help='Run in verbose mode',
|
|
37
|
+
required=False,
|
|
38
|
+
default=False,
|
|
39
|
+
type=bool,
|
|
40
|
+
is_flag=True,
|
|
41
|
+
)
|
|
42
|
+
@click.option(
|
|
43
|
+
'--no-plugins',
|
|
44
|
+
help='Disable non-builtin plugin loading',
|
|
45
|
+
type=bool,
|
|
46
|
+
is_flag=True,
|
|
47
|
+
)
|
|
48
|
+
@click.pass_context
|
|
49
|
+
def _root(ctx: click.Context, config, verbose, no_plugins):
|
|
50
|
+
config_text = None
|
|
51
|
+
if config is None:
|
|
52
|
+
try:
|
|
53
|
+
with open(DEFAULT_CONFIG_FILE, 'r') as f:
|
|
54
|
+
config_text = f.read()
|
|
55
|
+
config = DEFAULT_CONFIG_FILE
|
|
56
|
+
except FileNotFoundError:
|
|
57
|
+
pass
|
|
58
|
+
except IOError as e:
|
|
59
|
+
raise click.ClickException(
|
|
60
|
+
f"Failed to read {DEFAULT_CONFIG_FILE}: {str(e)}"
|
|
61
|
+
)
|
|
62
|
+
else:
|
|
63
|
+
try:
|
|
64
|
+
config_text = config.read()
|
|
65
|
+
except IOError as e:
|
|
66
|
+
raise click.ClickException(
|
|
67
|
+
f"Failed to read configuration: {str(e)}",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
ctx.ensure_object(CLIContext)
|
|
71
|
+
ctx_obj: CLIContext = ctx.obj
|
|
72
|
+
cfg: Optional[CLIRootConfig] = None
|
|
73
|
+
if config_text is not None:
|
|
74
|
+
cfg = parse_cli_config(config_text)
|
|
75
|
+
ctx_obj.config = cfg.config
|
|
76
|
+
log_config = cfg.log_config
|
|
77
|
+
else:
|
|
78
|
+
# grab the default
|
|
79
|
+
log_config = parse_logging_config({})
|
|
80
|
+
|
|
81
|
+
from .commands.signing import register
|
|
82
|
+
|
|
83
|
+
plugins_to_register = _load_plugins(cfg, plugins_enabled=not no_plugins)
|
|
84
|
+
register(plugins_to_register)
|
|
85
|
+
|
|
86
|
+
if verbose:
|
|
87
|
+
# override the root logger's logging level, but preserve the output
|
|
88
|
+
root_logger_config = log_config[None]
|
|
89
|
+
log_config[None] = LogConfig(
|
|
90
|
+
level=logging.DEBUG, output=root_logger_config.output
|
|
91
|
+
)
|
|
92
|
+
else:
|
|
93
|
+
# use the root logger's output settings to populate the default
|
|
94
|
+
log_output = log_config[None].output
|
|
95
|
+
# Revinfo fetch logs -> filter by default
|
|
96
|
+
log_config['pyhanko_certvalidator.fetchers'] = LogConfig(
|
|
97
|
+
level=logging.WARNING, output=log_output
|
|
98
|
+
)
|
|
99
|
+
if 'fontTools.subset' not in log_config:
|
|
100
|
+
# the fontTools subsetter has a very noisy INFO log, so
|
|
101
|
+
# set that one to WARNING by default
|
|
102
|
+
log_config['fontTools.subset'] = LogConfig(
|
|
103
|
+
level=logging.WARNING, output=log_output
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
logging_setup(log_config, verbose)
|
|
107
|
+
|
|
108
|
+
if verbose:
|
|
109
|
+
logging.debug("Running with --verbose")
|
|
110
|
+
if config_text is not None:
|
|
111
|
+
logging.debug(f'Finished reading configuration from {config}.')
|
|
112
|
+
else:
|
|
113
|
+
logging.debug('There was no configuration to parse.')
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _load_plugins(root_config: Optional[CLIRootConfig], plugins_enabled: bool):
|
|
117
|
+
import sys
|
|
118
|
+
from importlib import metadata
|
|
119
|
+
|
|
120
|
+
# we always load the default ones
|
|
121
|
+
to_load = [
|
|
122
|
+
'pyhanko.cli.commands.signing.pkcs11_cli:PKCS11Plugin',
|
|
123
|
+
'pyhanko.cli.commands.signing.simple:PKCS12Plugin',
|
|
124
|
+
'pyhanko.cli.commands.signing.simple:PemderPlugin',
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
eps_from_metadata: Iterable[metadata.EntryPoint] = []
|
|
128
|
+
if plugins_enabled:
|
|
129
|
+
if root_config is not None:
|
|
130
|
+
to_load += [str(mod) for mod in root_config.plugin_endpoints]
|
|
131
|
+
|
|
132
|
+
# need to use dict interface for 3.8 interop
|
|
133
|
+
if sys.version_info < (3, 10):
|
|
134
|
+
eps_from_metadata = metadata.entry_points().get(
|
|
135
|
+
SIGNING_PLUGIN_ENTRY_POINT_GROUP, []
|
|
136
|
+
)
|
|
137
|
+
else:
|
|
138
|
+
eps_from_metadata = metadata.entry_points(
|
|
139
|
+
group=SIGNING_PLUGIN_ENTRY_POINT_GROUP
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# noinspection PyArgumentList
|
|
143
|
+
to_load_as_endpoints: Iterable[metadata.EntryPoint] = [
|
|
144
|
+
metadata.EntryPoint(
|
|
145
|
+
name='',
|
|
146
|
+
value=v,
|
|
147
|
+
group=SIGNING_PLUGIN_ENTRY_POINT_GROUP,
|
|
148
|
+
)
|
|
149
|
+
for v in to_load
|
|
150
|
+
]
|
|
151
|
+
resulting_plugins = list(SIGNING_PLUGIN_REGISTRY)
|
|
152
|
+
seen = set(type(x) for x in SIGNING_PLUGIN_REGISTRY)
|
|
153
|
+
for ep in itertools.chain(to_load_as_endpoints, eps_from_metadata):
|
|
154
|
+
plugin_cls = ep.load()
|
|
155
|
+
if not isinstance(plugin_cls, type):
|
|
156
|
+
click.echo(
|
|
157
|
+
click.style(
|
|
158
|
+
f"Plugins must be defined as references to classes with a "
|
|
159
|
+
f"nullary init function, but '{ep.value}' is "
|
|
160
|
+
f"a {type(plugin_cls)}. Disregarding...",
|
|
161
|
+
bold=True,
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
continue
|
|
165
|
+
if plugin_cls not in seen:
|
|
166
|
+
seen.add(plugin_cls)
|
|
167
|
+
resulting_plugins.append(plugin_cls())
|
|
168
|
+
return resulting_plugins
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
cli_root: click.Group = _root
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
from typing import Iterable, Optional, TypeVar, Union
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from pyhanko.cli.config import CLIConfig
|
|
5
|
+
from pyhanko.cli.utils import logger, readable_file
|
|
6
|
+
from pyhanko.config.errors import ConfigurationError
|
|
7
|
+
from pyhanko.config.trust import init_validation_context_kwargs
|
|
8
|
+
from pyhanko.keys import load_certs_from_pemder
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def build_vc_kwargs(
|
|
12
|
+
cli_config: Optional[CLIConfig],
|
|
13
|
+
validation_context: Optional[str],
|
|
14
|
+
trust: Union[Iterable[str], str],
|
|
15
|
+
trust_replace: bool,
|
|
16
|
+
other_certs: Union[Iterable[str], str],
|
|
17
|
+
retroactive_revinfo: bool,
|
|
18
|
+
allow_fetching: Optional[bool] = None,
|
|
19
|
+
):
|
|
20
|
+
try:
|
|
21
|
+
if validation_context is not None:
|
|
22
|
+
if any((trust, other_certs)):
|
|
23
|
+
raise click.ClickException(
|
|
24
|
+
"--validation-context is incompatible with --trust "
|
|
25
|
+
"and --other-certs"
|
|
26
|
+
)
|
|
27
|
+
# load the desired context from config
|
|
28
|
+
if cli_config is None:
|
|
29
|
+
raise click.ClickException("No config file specified.")
|
|
30
|
+
try:
|
|
31
|
+
result = cli_config.get_validation_context(
|
|
32
|
+
validation_context, as_dict=True
|
|
33
|
+
)
|
|
34
|
+
except ConfigurationError as e:
|
|
35
|
+
msg = (
|
|
36
|
+
"Configuration problem. Are you sure that the validation "
|
|
37
|
+
f"context '{validation_context}' is properly defined in the"
|
|
38
|
+
" configuration file?"
|
|
39
|
+
)
|
|
40
|
+
logger.error(msg, exc_info=e)
|
|
41
|
+
raise click.ClickException(msg)
|
|
42
|
+
elif trust or other_certs:
|
|
43
|
+
# load a validation profile using command line kwargs
|
|
44
|
+
result = init_validation_context_kwargs(
|
|
45
|
+
trust=trust,
|
|
46
|
+
trust_replace=trust_replace,
|
|
47
|
+
other_certs=other_certs,
|
|
48
|
+
retroactive_revinfo=retroactive_revinfo,
|
|
49
|
+
)
|
|
50
|
+
elif cli_config is not None:
|
|
51
|
+
# load the default settings from the CLI config
|
|
52
|
+
try:
|
|
53
|
+
result = cli_config.get_validation_context(as_dict=True)
|
|
54
|
+
except ConfigurationError as e:
|
|
55
|
+
msg = "Failed to load default validation context."
|
|
56
|
+
logger.error(msg, exc_info=e)
|
|
57
|
+
raise click.ClickException(msg)
|
|
58
|
+
else:
|
|
59
|
+
result = {}
|
|
60
|
+
|
|
61
|
+
if allow_fetching is not None:
|
|
62
|
+
result['allow_fetching'] = allow_fetching
|
|
63
|
+
else:
|
|
64
|
+
result.setdefault('allow_fetching', True)
|
|
65
|
+
|
|
66
|
+
# allow CLI --retroactive-revinfo flag to override settings
|
|
67
|
+
# if necessary
|
|
68
|
+
if retroactive_revinfo:
|
|
69
|
+
result['retroactive_revinfo'] = True
|
|
70
|
+
return result
|
|
71
|
+
except click.ClickException:
|
|
72
|
+
raise
|
|
73
|
+
except IOError as e:
|
|
74
|
+
msg = "I/O problem while reading validation config"
|
|
75
|
+
logger.error(msg, exc_info=e)
|
|
76
|
+
raise click.ClickException(msg)
|
|
77
|
+
except Exception as e:
|
|
78
|
+
msg = "Generic processing problem while reading validation config"
|
|
79
|
+
logger.error(msg, exc_info=e)
|
|
80
|
+
raise click.ClickException(msg)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _get_key_usage_settings(ctx: click.Context, validation_context: str):
|
|
84
|
+
cli_config: Optional[CLIConfig] = ctx.obj.config
|
|
85
|
+
if cli_config is None:
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
# note: validation_context can be None, this triggers fallback to the
|
|
89
|
+
# default validation context specified in the configuration file
|
|
90
|
+
# If we add support for specifying key usage settings as CLI arguments,
|
|
91
|
+
# using the same fallbacks as _build_cli_kwargs would probably be cleaner
|
|
92
|
+
return cli_config.get_signer_key_usages(name=validation_context)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
TRUST_OPTIONS = [
|
|
96
|
+
click.Option(
|
|
97
|
+
('--validation-context',),
|
|
98
|
+
help='use validation context from config',
|
|
99
|
+
required=False,
|
|
100
|
+
type=str,
|
|
101
|
+
),
|
|
102
|
+
click.Option(
|
|
103
|
+
('--trust',),
|
|
104
|
+
help='list trust roots (multiple allowed)',
|
|
105
|
+
required=False,
|
|
106
|
+
multiple=True,
|
|
107
|
+
type=readable_file,
|
|
108
|
+
),
|
|
109
|
+
click.Option(
|
|
110
|
+
('--trust-replace',),
|
|
111
|
+
help='listed trust roots supersede OS-provided trust store',
|
|
112
|
+
required=False,
|
|
113
|
+
type=bool,
|
|
114
|
+
is_flag=True,
|
|
115
|
+
default=False,
|
|
116
|
+
show_default=True,
|
|
117
|
+
),
|
|
118
|
+
click.Option(
|
|
119
|
+
('--other-certs',),
|
|
120
|
+
help='other certs relevant for validation',
|
|
121
|
+
required=False,
|
|
122
|
+
multiple=True,
|
|
123
|
+
type=readable_file,
|
|
124
|
+
),
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
FC = TypeVar('FC', bound=click.Command)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def trust_options(f: FC) -> FC:
|
|
132
|
+
f.params.extend(TRUST_OPTIONS)
|
|
133
|
+
return f
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _prepare_vc(vc_kwargs, soft_revocation_check, force_revinfo):
|
|
137
|
+
if soft_revocation_check and force_revinfo:
|
|
138
|
+
raise click.ClickException(
|
|
139
|
+
"--soft-revocation-check is incompatible with " "--force-revinfo"
|
|
140
|
+
)
|
|
141
|
+
if force_revinfo:
|
|
142
|
+
rev_mode = 'require'
|
|
143
|
+
elif soft_revocation_check:
|
|
144
|
+
rev_mode = 'soft-fail'
|
|
145
|
+
else:
|
|
146
|
+
rev_mode = 'hard-fail'
|
|
147
|
+
vc_kwargs['revocation_mode'] = rev_mode
|
|
148
|
+
return vc_kwargs
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def grab_certs(files):
|
|
152
|
+
if not files:
|
|
153
|
+
return None
|
|
154
|
+
try:
|
|
155
|
+
return list(load_certs_from_pemder(files))
|
|
156
|
+
except (IOError, ValueError) as e:
|
|
157
|
+
raise click.ClickException(
|
|
158
|
+
f'Could not load certificates from {files}'
|
|
159
|
+
) from e
|
|
File without changes
|