Skip to content

Commit

Permalink
Adding prop_auth_time and contact_info entries
Browse files Browse the repository at this point in the history
See #307
  • Loading branch information
eduperottoni authored and MatthiasValvekens committed Sep 25, 2023
1 parent fc3d7d2 commit f258829
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 1 deletion.
3 changes: 3 additions & 0 deletions pyhanko/cli/commands/signing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -146,6 +147,7 @@ def addsig(
field,
name,
reason,
contact_info,
location,
certify,
existing_only,
Expand Down Expand Up @@ -222,6 +224,7 @@ def addsig(
field_name=field_name,
location=location,
reason=reason,
contact_info=contact_info,
name=name,
certify=certify,
subfilter=subfilter,
Expand Down
11 changes: 11 additions & 0 deletions pyhanko/sign/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down
24 changes: 23 additions & 1 deletion pyhanko/sign/signers/pdf_byterange.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__ = [
Expand Down Expand Up @@ -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__(
Expand All @@ -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__(
Expand All @@ -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):
Expand Down
27 changes: 27 additions & 0 deletions pyhanko/sign/signers/pdf_signer.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
InvisSigSettings,
MDPPerm,
SeedLockDocument,
SigAuthType,
SigFieldSpec,
SigSeedSubFilter,
SigSeedValFlags,
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -2141,13 +2164,17 @@ def prepare_tbs_document(
timestamp=system_time,
text_params=appearance_text_params,
)

sig_obj = SignatureObject(
bytes_reserved=bytes_reserved,
subfilter=self.subfilter,
timestamp=system_time,
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,
)

Expand Down
33 changes: 33 additions & 0 deletions pyhanko_tests/cli_tests/test_cli_signing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
43 changes: 43 additions & 0 deletions pyhanko_tests/test_signing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'

0 comments on commit f258829

Please sign in to comment.