From f2588290aea59167ccc0b7292e8e1c398cc13d1a Mon Sep 17 00:00:00 2001 From: Eduardo Perottoni Date: Wed, 23 Aug 2023 23:39:43 -0300 Subject: [PATCH] Adding prop_auth_time and contact_info entries See #307 --- pyhanko/cli/commands/signing/__init__.py | 3 ++ pyhanko/sign/fields.py | 11 ++++++ pyhanko/sign/signers/pdf_byterange.py | 24 +++++++++++- pyhanko/sign/signers/pdf_signer.py | 27 +++++++++++++ pyhanko_tests/cli_tests/test_cli_signing.py | 33 ++++++++++++++++ pyhanko_tests/test_signing.py | 43 +++++++++++++++++++++ 6 files changed, 140 insertions(+), 1 deletion(-) diff --git a/pyhanko/cli/commands/signing/__init__.py b/pyhanko/cli/commands/signing/__init__.py index 4a482438..51efb5e5 100644 --- a/pyhanko/cli/commands/signing/__init__.py +++ b/pyhanko/cli/commands/signing/__init__.py @@ -33,6 +33,7 @@ def signing(): @click.option('--name', help='explicitly specify signer name', required=False) @click.option('--reason', help='reason for signing', required=False) @click.option('--location', help='location of signing', required=False) +@click.option('--contact-info', help='contact of the signer', required=False) @click.option( '--certify', help='add certification signature', @@ -146,6 +147,7 @@ def addsig( field, name, reason, + contact_info, location, certify, existing_only, @@ -222,6 +224,7 @@ def addsig( field_name=field_name, location=location, reason=reason, + contact_info=contact_info, name=name, certify=certify, subfilter=subfilter, diff --git a/pyhanko/sign/fields.py b/pyhanko/sign/fields.py index 30f3e740..f8490aaa 100644 --- a/pyhanko/sign/fields.py +++ b/pyhanko/sign/fields.py @@ -678,6 +678,17 @@ class SigSeedSubFilter(Enum): ETSI_RFC3161 = pdf_name("/ETSI.RFC3161") +@unique +class SigAuthType(Enum): + """ + Enum declaring all supported ``/Prop_AuthType`` values. + """ + + PIN = pdf_string("PIN") + PASSWORD = pdf_string("Password") + FINGERPRINT = pdf_string("Fingerprint") + + @unique class SeedValueDictVersion(OrderedEnum): """ diff --git a/pyhanko/sign/signers/pdf_byterange.py b/pyhanko/sign/signers/pdf_byterange.py index eb362037..38650147 100644 --- a/pyhanko/sign/signers/pdf_byterange.py +++ b/pyhanko/sign/signers/pdf_byterange.py @@ -18,7 +18,7 @@ from pyhanko.pdf_utils.writer import BasePdfFileWriter from pyhanko.sign.general import SigningError, get_pyca_cryptography_hash -from ..fields import SigSeedSubFilter +from ..fields import SigAuthType, SigSeedSubFilter from . import constants __all__ = [ @@ -414,6 +414,17 @@ class SignatureObject(PdfSignedData): Optional signing location. :param reason: Optional signing reason. May be restricted by seed values. + :params contact_info: + Optional information from the signer to enable the receiver to contact + the signer and verify the signature. + :param app_build_props: + Optional dictionary containing informations about the computer environment used for signing. + See :class:`.BuildProps`. + :param prop_auth_time: + Optional information representing the number of seconds since signer was last authenticated. + :param prop_auth_type: + Optional information about the method of user's authentication + See :class:`.SigAuthType`. """ def __init__( @@ -423,7 +434,10 @@ def __init__( name=None, location=None, reason=None, + contact_info=None, app_build_props: Optional[BuildProps] = None, + prop_auth_time: Optional[int] = None, + prop_auth_type: Optional[SigAuthType] = None, bytes_reserved=None, ): super().__init__( @@ -439,10 +453,18 @@ def __init__( self[pdf_name('/Location')] = pdf_string(location) if reason: self[pdf_name('/Reason')] = pdf_string(reason) + if contact_info: + self[pdf_name('/ContactInfo')] = pdf_string(contact_info) if app_build_props: self[pdf_name('/Prop_Build')] = generic.DictionaryObject( {pdf_name("/App"): app_build_props.as_pdf_object()} ) + if prop_auth_time: + self[pdf_name('/Prop_AuthTime')] = generic.NumberObject( + prop_auth_time + ) + if prop_auth_type: + self[pdf_name('/Prop_AuthType')] = prop_auth_type.value class DocumentTimestamp(PdfSignedData): diff --git a/pyhanko/sign/signers/pdf_signer.py b/pyhanko/sign/signers/pdf_signer.py index 69a4e0ba..439c7954 100644 --- a/pyhanko/sign/signers/pdf_signer.py +++ b/pyhanko/sign/signers/pdf_signer.py @@ -30,6 +30,7 @@ InvisSigSettings, MDPPerm, SeedLockDocument, + SigAuthType, SigFieldSpec, SigSeedSubFilter, SigSeedValFlags, @@ -293,6 +294,12 @@ class PdfSignatureMetadata: Reason for signing (textual). """ + contact_info: Optional[str] = None + """ + Information provided by the signer to enable the receiver to contact the + signer to verify the signature. + """ + name: Optional[str] = None """ Name of the signer. This value is usually not necessary to set, since @@ -309,6 +316,22 @@ class PdfSignatureMetadata: dictionary of the signature. """ + prop_auth_time: Optional[int] = None + """ + Number of seconds since signer was last authenticated. + """ + + prop_auth_type: Optional[SigAuthType] = None + """ + Signature /Prop_AuthType to use. + + This should be one of + :attr:`~.fields.SigAuthType.PIN` or + :attr:`~.fields.SigAuthType.PASSWORD` or + :attr:`~.fields.SigAuthType.FINGERPRINT` + If not specified, this property won't be set on the signature dictionary. + """ + certify: bool = False """ Sign with an author (certification) signature, as opposed to an approval @@ -2141,6 +2164,7 @@ def prepare_tbs_document( timestamp=system_time, text_params=appearance_text_params, ) + sig_obj = SignatureObject( bytes_reserved=bytes_reserved, subfilter=self.subfilter, @@ -2148,6 +2172,9 @@ def prepare_tbs_document( name=name_specified if name_specified else None, location=signature_meta.location, reason=signature_meta.reason, + contact_info=signature_meta.contact_info, + prop_auth_time=signature_meta.prop_auth_time, + prop_auth_type=signature_meta.prop_auth_type, app_build_props=signature_meta.app_build_props, ) diff --git a/pyhanko_tests/cli_tests/test_cli_signing.py b/pyhanko_tests/cli_tests/test_cli_signing.py index 27ab3170..4498c234 100644 --- a/pyhanko_tests/cli_tests/test_cli_signing.py +++ b/pyhanko_tests/cli_tests/test_cli_signing.py @@ -1172,3 +1172,36 @@ def test_succeed_non_strict_hybrid(cli_runner): ], ) assert result.exit_code == 0 + + +def test_cli_with_signature_dictionary_entries(cli_runner): + result = cli_runner.invoke( + cli_root, + [ + 'sign', + 'addsig', + '--field', + 'Sig1', + '--reason', + 'I agree with this document', + '--location', + 'THIS-COMPUTER', + '--contact-info', + 'www.pyhanko.com/verify', + 'pemder', + '--no-pass', + '--cert', + _write_cert(TESTING_CA, CertLabel('signer1'), "cert.pem"), + '--key', + _write_user_key(TESTING_CA), + INPUT_PATH, + SIGNED_OUTPUT_PATH, + ], + ) + assert not result.exception, result.output + with open(SIGNED_OUTPUT_PATH, 'rb') as outf: + r = PdfFileReader(outf) + last_sign = r.embedded_signatures[-1].sig_object + + assert last_sign['/ContactInfo'] == 'www.pyhanko.com/verify' + assert last_sign['/Location'] == 'THIS-COMPUTER' diff --git a/pyhanko_tests/test_signing.py b/pyhanko_tests/test_signing.py index dfc4d5da..89e1e659 100644 --- a/pyhanko_tests/test_signing.py +++ b/pyhanko_tests/test_signing.py @@ -1628,3 +1628,46 @@ def test_sign_with_build_props_versioned_app_name(): build_prop_dict = s.sig_object['/Prop_Build']['/App'] assert build_prop_dict['/Name'] == '/Test Application' assert build_prop_dict['/REx'] == '1.2.3' + + +@freeze_time('2020-11-01') +def test_signature_dict_with_prop_auth_time(): + w = IncrementalPdfFileWriter(BytesIO(MINIMAL)) + meta = signers.PdfSignatureMetadata(field_name='Sig1', prop_auth_time=512) + + out = signers.sign_pdf(w, meta, signer=FROM_CA) + r = PdfFileReader(out) + s = r.embedded_signatures[0] + val_trusted(s) + assert s.sig_object['/Prop_AuthTime'] == 512 + + +@freeze_time('2020-11-01') +def test_signature_dict_with_contact_info(): + w = IncrementalPdfFileWriter(BytesIO(MINIMAL)) + contact_info = '+55 99 99999-9999' + meta = signers.PdfSignatureMetadata( + field_name='Sig1', contact_info=contact_info + ) + + out = signers.sign_pdf(w, meta, signer=FROM_CA) + r = PdfFileReader(out) + s = r.embedded_signatures[0] + val_trusted(s) + assert s.sig_object['/ContactInfo'] == contact_info + + +@freeze_time('2020-11-01') +def test_signature_dict_with_prop_auth_type(): + w = IncrementalPdfFileWriter(BytesIO(MINIMAL)) + auth_type = fields.SigAuthType.PASSWORD + meta = signers.PdfSignatureMetadata( + field_name='Sig1', + prop_auth_type=auth_type, + ) + + out = signers.sign_pdf(w, meta, signer=FROM_CA) + r = PdfFileReader(out) + s = r.embedded_signatures[0] + val_trusted(s) + assert s.sig_object['/Prop_AuthType'] == 'Password'