diff --git a/pyhanko/cli/_trust.py b/pyhanko/cli/_trust.py index eece8931..7760d6eb 100644 --- a/pyhanko/cli/_trust.py +++ b/pyhanko/cli/_trust.py @@ -1,7 +1,6 @@ from typing import Iterable, Optional, TypeVar, Union import click -from pyhanko_certvalidator import ValidationContext from pyhanko.cli.config import CLIConfig from pyhanko.cli.utils import logger, readable_file diff --git a/pyhanko/cli/commands/signing/__init__.py b/pyhanko/cli/commands/signing/__init__.py index 3395895d..067770df 100644 --- a/pyhanko/cli/commands/signing/__init__.py +++ b/pyhanko/cli/commands/signing/__init__.py @@ -13,14 +13,18 @@ from pyhanko.cli.commands.signing.plugin import command_from_plugin from pyhanko.cli.commands.stamp import select_style from pyhanko.cli.utils import parse_field_location_spec +from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter from pyhanko.sign import DEFAULT_SIGNER_KEY_USAGE, fields, signers from pyhanko.sign.signers.pdf_byterange import BuildProps +from pyhanko.sign.timestamps import HTTPTimeStamper from ..._ctx import CLIContext from ...plugin_api import SigningCommandPlugin __all__ = ['signing', 'addsig', 'register'] +from ...runtime import pyhanko_exception_manager + @cli_root.group(help='sign PDFs and other files', name='sign') def signing(): @@ -270,3 +274,51 @@ def _unavailable(): callback=_unavailable, ) ) + + +readable_file = click.Path(exists=True, readable=True, dir_okay=False) +writable_file = click.Path(writable=True, dir_okay=False) + + +@trust_options +@signing.command(name='timestamp', help='add timestamp to PDF') +@click.argument('infile', type=readable_file) +@click.argument('outfile', type=writable_file) +@click.option( + '--timestamp-url', + help='URL for timestamp server', + required=True, + type=str, + default=None, +) +@click.pass_context +def timestamp( + ctx, + infile, + outfile, + validation_context, + trust, + trust_replace, + other_certs, + timestamp_url, +): + with pyhanko_exception_manager(): + vc_kwargs = build_vc_kwargs( + ctx.obj.config, + validation_context, + trust, + trust_replace, + other_certs, + retroactive_revinfo=True, + ) + timestamper = HTTPTimeStamper(timestamp_url) + with open(infile, 'rb') as inf: + w = IncrementalPdfFileWriter(inf) + pdf_timestamper = signers.PdfTimeStamper(timestamper) + with open(outfile, 'wb') as outf: + pdf_timestamper.timestamp_pdf( + w, + 'sha256', + validation_context=ValidationContext(**vc_kwargs), + output=outf, + ) diff --git a/pyhanko_tests/cli_tests/test_cli_signing.py b/pyhanko_tests/cli_tests/test_cli_signing.py index 4498c234..84b3e0f1 100644 --- a/pyhanko_tests/cli_tests/test_cli_signing.py +++ b/pyhanko_tests/cli_tests/test_cli_signing.py @@ -1205,3 +1205,32 @@ def test_cli_with_signature_dictionary_entries(cli_runner): assert last_sign['/ContactInfo'] == 'www.pyhanko.com/verify' assert last_sign['/Location'] == 'THIS-COMPUTER' + + +def test_cli_timestamp(pki_arch_name, timestamp_url, cli_runner, root_cert): + if pki_arch_name == 'ed448': + # FIXME deal with this bug on the Certomancer end + pytest.skip("ed448 timestamping in Certomancer doesn't work") + cfg = { + 'validation-contexts': { + 'test': { + 'trust': root_cert, + } + }, + } + + _write_config(cfg) + result = cli_runner.invoke( + cli_root, + [ + 'sign', + 'timestamp', + '--validation-context', + 'test', + '--timestamp-url', + timestamp_url, + INPUT_PATH, + SIGNED_OUTPUT_PATH, + ], + ) + assert not result.exception, result.output