Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add signature's dictionary entries on API (/ContactInfo, /Prop_AuthTime and /Prop_AuthType) #314

Merged
merged 1 commit into from
Sep 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(
MatthiasValvekens marked this conversation as resolved.
Show resolved Hide resolved
'--certify',
help='add certification signature',
Expand Down Expand Up @@ -146,6 +147,7 @@ def addsig(
field,
name,
reason,
contact_info,
MatthiasValvekens marked this conversation as resolved.
Show resolved Hide resolved
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'
Loading