File: sign.py

package info (click to toggle)
fpdf2 2.8.7-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 114,352 kB
  • sloc: python: 50,410; sh: 133; makefile: 12
file content (128 lines) | stat: -rw-r--r-- 4,645 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
"""
Module dedicated to document signature generation.

The contents of this module are internal to fpdf2, and not part of the public API.
They may change at any time without prior warning or any deprecation period,
in non-backward-compatible ways.

Usage documentation at: <https://py-pdf.github.io/fpdf2/Signing.html>
"""

# pyright: reportUnknownArgumentType=false, reportUnknownMemberType=false, reportUnknownVariableType=false

import hashlib
from datetime import timezone
from typing import TYPE_CHECKING, Any, Optional
from unittest.mock import patch

from .syntax import Name, PDFDate, build_obj_dict, create_dictionary_string as pdf_dict
from .util import buffer_subst

if TYPE_CHECKING:
    from datetime import datetime

    from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes
    from cryptography.x509 import Certificate
    from endesive import signer

    from .encryption import StandardSecurityHandler


class Signature:
    def __init__(
        self,
        contact_info: Optional[str] = None,
        location: Optional[str] = None,
        m: Optional[PDFDate] = None,
        reason: Optional[str] = None,
    ) -> None:
        self.type = Name("Sig")
        self.filter = Name("Adobe.PPKLite")
        self.sub_filter = Name("adbe.pkcs7.detached")
        self.contact_info = contact_info
        "Information provided by the signer to enable a recipient to contact the signer to verify the signature"
        self.location = location
        "The CPU host name or physical location of the signing"
        self.m = m
        "The time of signing"
        self.reason = reason
        "The reason for the signing"
        self.byte_range = _SIGNATURE_BYTERANGE_PLACEHOLDER
        self.contents = "<" + _SIGNATURE_CONTENTS_PLACEHOLDER + ">"

    def serialize(
        self,
        _security_handler: Optional["StandardSecurityHandler"] = None,
        _obj_id: Optional[int] = None,
    ) -> str:
        obj_dict = build_obj_dict(
            {key: getattr(self, key) for key in dir(self)},
            _security_handler=_security_handler,
            _obj_id=_obj_id,
        )
        return pdf_dict(obj_dict)


def sign_content(
    signer: "signer",  # pyright: ignore[reportGeneralTypeIssues]
    buffer: bytearray,
    key: Optional["PrivateKeyTypes"],
    cert: "Certificate",
    extra_certs: list["Certificate"],
    hashalgo: str,
    sign_time: "datetime",
) -> bytearray:
    """
    Perform PDF signing based on the content of the buffer, performing substitutions on it.
    The signing operation does not alter the buffer size
    """
    # We start by substituting the ByteRange,
    # that defines which part of the document content the signature is based on.
    # This is basically ALL the content EXCEPT the signature content itself.
    sig_placeholder = _SIGNATURE_CONTENTS_PLACEHOLDER.encode("latin1")
    start_index = buffer.find(sig_placeholder)
    end_index = start_index + len(sig_placeholder)
    content_range = (0, start_index - 1, end_index + 1, len(buffer) - end_index - 1)
    # pylint: disable=consider-using-f-string
    buffer = buffer_subst(
        buffer,
        _SIGNATURE_BYTERANGE_PLACEHOLDER,
        "[%010d %010d %010d %010d]" % content_range,
    )

    # We compute the ByteRange hash, of everything before & after the placeholder:
    content_hash = hashlib.new(hashalgo)
    content_hash.update(buffer[: content_range[1]])  # before
    content_hash.update(buffer[content_range[2] :])  # after

    # This monkey-patching is needed, at the time of endesive v2.0.9,
    # to get control over signed_time, initialized by endesive.signer.sign() to be datetime.now():
    class mock_datetime:
        @staticmethod
        def now(tz: Any) -> "datetime":  # pylint: disable=unused-argument
            return sign_time.astimezone(timezone.utc)

    sign = patch("endesive.signer.datetime", mock_datetime)(signer.sign)

    contents = sign(
        datau=None,
        key=key,
        cert=cert,
        othercerts=extra_certs,
        hashalgo=hashalgo,
        attrs=True,
        signed_value=content_hash.digest(),
    )
    contents = _pkcs11_aligned(contents).encode("latin1")
    # Sanity check, otherwise we will break the xref table:
    assert len(sig_placeholder) == len(contents)
    return buffer.replace(sig_placeholder, contents, 1)


def _pkcs11_aligned(data: tuple[int, ...]) -> str:
    data_str = "".join(f"{i:02x}" for i in data)
    return data_str + "0" * (0x4000 - len(data_str))


_SIGNATURE_BYTERANGE_PLACEHOLDER = "[0000000000 0000000000 0000000000 0000000000]"
_SIGNATURE_CONTENTS_PLACEHOLDER = _pkcs11_aligned((0,))