Source code for AIS.pdf
# -*- coding: utf-8 -*-
"""
AIS.py - A Python interface for the Swisscom All-in Signing Service.
:copyright: (c) 2016 by Camptocamp
:license: AGPLv3, see README and LICENSE for more details
"""
import base64
from datetime import datetime
import io
from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter
from pyhanko.sign import fields
from pyhanko.sign import signers
from pyhanko.sign.signers import cms_embedder
from .exceptions import SignatureTooLarge
from typing import overload
from typing import IO
from typing import Optional
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .types import FileLike
from .types import SupportsBinaryRead
def is_seekable(fp: 'SupportsBinaryRead') -> bool:
return getattr(fp, 'seekable', lambda: False)()
[docs]class PDF:
"""A container for a PDF file to be signed and the signed version."""
@overload
def __init__(
self,
input_file: 'FileLike',
*,
out_stream: Optional[IO[bytes]] = ...,
sig_name: str = ...,
sig_size: int = ...
): ...
@overload
def __init__(
self,
*,
inout_stream: IO[bytes],
sig_name: str = ...,
sig_size: int = ...
): ...
def __init__(
self,
input_file: Optional['FileLike'] = None,
*,
inout_stream: Optional[IO[bytes]] = None,
out_stream: Optional[IO[bytes]] = None,
sig_name: str = 'Signature',
sig_size: int = 64*1024, # 64 KiB
):
"""Accepts either a filename or a file-like object.
It is the callers responsibility to ensure that buffers
passed as `input_file` are pointing to the start of the file.
We make no attempt to seek to the start of the file.
:param inout_stream: Optional stream that will directly
be used as both the input and output stream for in-place
signing.
:param out_stream: Optional stream that will be used to
store the signed PDF. By default a BytesIO stream will
be created to store the signed PDF.
:param sig_name: Name of the Signature field to use. If
no Signature with that name exists in the PDF, a new one
will be created.
:param sig_size: Size of the signature in DER encoding
in bytes. By default 64KiB will be reserved, which should
be enough for most cases right now.
"""
in_place = out_stream is None
writer_stream: 'SupportsBinaryRead'
if isinstance(inout_stream, io.BytesIO):
# in this case we create the signed version in-place
# so the out_stream will be assigned the in_stream
writer_stream = inout_stream
assert out_stream is None
elif input_file is None:
raise ValueError('Either input_file or in_stream needs to be set')
elif isinstance(input_file, str):
# in this case we just read the entire file into a buffer
# and create the signed version in-place
with open(input_file, 'rb') as fp:
writer_stream = io.BytesIO(fp.read())
elif is_seekable(input_file):
# in this case we can't assume that we're allowed to
# create the signed version in-place, but we can allow
# the IncrementalPdfFileWriter to operate on the input
# file directly.
writer_stream = input_file
out_stream = out_stream or io.BytesIO()
in_place = False
else:
# in this case we can't seek the input file so we need
# to read the entire file into a buffer as well
writer_stream = io.BytesIO(input_file.read())
writer = IncrementalPdfFileWriter(writer_stream)
self.cms_writer = cms_embedder.PdfCMSEmbedder().write_cms(
field_name=sig_name,
writer=writer
)
"""CMS Writer used for embedding the signature"""
next(self.cms_writer)
self.sig_size = sig_size
"""Number of bytes reserved for the signature.
It is the caller's responsibility to ensure that this is
large enough to store the entire signature.
Currently 64KiB (the default) appear to be enough. But
the signature has been known to grow in size over the
years.
"""
if in_place:
assert out_stream is None
assert hasattr(writer_stream, 'write')
out_stream = writer_stream # type: ignore[assignment]
self.sig_io_setup = cms_embedder.SigIOSetup(
md_algorithm='sha256',
in_place=in_place,
output=out_stream
)
"""Signing I/O setup to be passed to pyHanko"""
@property
def out_stream(self) -> IO[bytes]:
"""Output stream for the signed PDF."""
assert self.sig_io_setup.output is not None
return self.sig_io_setup.output
[docs] def digest(self) -> str:
"""Computes the PDF digest."""
sig_obj = signers.SignatureObject(
timestamp=datetime.now(),
bytes_reserved=self.sig_size,
)
self.cms_writer.send(
cms_embedder.SigObjSetup(
sig_placeholder=sig_obj,
mdp_setup=cms_embedder.SigMDPSetup(
md_algorithm='sha256',
certify=True,
docmdp_perms=fields.MDPPerm.NO_CHANGES,
)
)
)
digest, out_stream = self.cms_writer.send(self.sig_io_setup)
assert out_stream is self.out_stream
result = base64.b64encode(digest.document_digest)
return result.decode('ascii')
[docs] def write_signature(self, signature: bytes) -> None:
""" Writes the signature into the pdf file.
`digest` needs to be called first.
:raises: :class:`SignatureTooLarge`: If sig_size is
too small to store the entire signature.
"""
signature_size = len(signature)*2 # account for hex encoding
if signature_size > self.sig_size:
raise SignatureTooLarge(signature_size)
self.cms_writer.send(signature)