Example #1
0
    def parse_csc_auth_response(response_data: dict) -> CSCAuthorizationInfo:
        """
        Parse the response from a ``credentials/authorize`` call into
        a :class:`.CSCAuthorizationInfo` object.

        :param response_data:
            The decoded response JSON.
        :return:
            A :class:`.CSCAuthorizationInfo` object.
        """

        try:
            sad = str(response_data["SAD"])
        except KeyError:
            raise SigningError(
                "Could not extract SAD value from auth response")

        try:
            lifetime_seconds = int(response_data.get('expiresIn', 3600))
            now = datetime.now(tz=tzlocal.get_localzone())
            expires_at = now + timedelta(seconds=lifetime_seconds)
        except ValueError as e:
            raise SigningError(
                "Could not process expiresIn value in auth response") from e
        return CSCAuthorizationInfo(sad=sad, expires_at=expires_at)
Example #2
0
def _get_or_create_sigfield(field_name,
                            pdf_out: BasePdfFileWriter,
                            existing_fields_only,
                            new_field_spec: Optional[SigFieldSpec] = None):
    root = pdf_out.root
    if field_name is None:
        if not existing_fields_only:
            raise SigningError('Not specifying a field name is only allowed '
                               'when existing_fields_only=True')

        # most of the logic in prepare_sig_field has to do with preparing
        # for the potential addition of a new field. That is completely
        # irrelevant in this special case, so we might as well short circuit
        # things.
        field_created = False
        empty_fields = enumerate_sig_fields(pdf_out, filled_status=False)
        try:
            found_field_name, _, sig_field_ref = next(empty_fields)
        except StopIteration:
            raise SigningError('There are no empty signature fields.')

        others = ', '.join(fn for fn, _, _ in empty_fields if fn is not None)
        if others:
            raise SigningError(
                'There are several empty signature fields. Please specify '
                'a field name. The options are %s, %s.' %
                (found_field_name, others))
    else:
        # grab or create a sig field
        if new_field_spec is not None:
            sig_field_kwargs = {
                'box':
                new_field_spec.box,
                'include_on_page':
                pdf_out.find_page_for_modification(new_field_spec.on_page)[0],
                'combine_annotation':
                new_field_spec.combine_annotation
            }
        else:
            sig_field_kwargs = {}

        field_created, sig_field_ref = prepare_sig_field(
            field_name,
            root,
            update_writer=pdf_out,
            existing_fields_only=existing_fields_only,
            **sig_field_kwargs)

    ensure_sig_flags(writer=pdf_out, lock_sig_flags=True)

    return field_created, sig_field_ref
Example #3
0
    def __init__(self,
                 pkcs11_session: Session,
                 cert_label: Optional[str] = None,
                 signing_cert: x509.Certificate = None,
                 ca_chain=None,
                 key_label: Optional[str] = None,
                 prefer_pss=False,
                 embed_roots=True,
                 other_certs_to_pull=(),
                 bulk_fetch=True,
                 key_id: Optional[bytes] = None,
                 cert_id: Optional[bytes] = None,
                 use_raw_mechanism=False):
        """
        Initialise a PKCS11 signer.
        """
        if signing_cert is None and cert_id is None and cert_label is None:
            raise SigningError(
                "Please specify a signer's certificate through the "
                "'cert_id', 'signing_cert' and/or 'cert_label' options")

        self.cert_label = cert_label
        self.key_id = key_id
        self.cert_id = cert_id
        self._signing_cert = signing_cert
        if key_id is None and key_label is None:
            if cert_label is None:
                raise SigningError(
                    "If 'cert_label' is None, then 'key_label' or 'key_id' "
                    "must be provided.")
            key_label = cert_label
        self.key_label = key_label
        self.pkcs11_session = pkcs11_session
        cs = SimpleCertificateStore()
        self._cert_registry: CertificateStore = cs
        if ca_chain is not None:
            cs.register_multiple(ca_chain)
        if signing_cert is not None:
            cs.register(signing_cert)
        self.other_certs = other_certs_to_pull
        self._other_certs_loaded = False
        if other_certs_to_pull is not None and len(other_certs_to_pull) <= 1:
            self.bulk_fetch = False
        else:
            self.bulk_fetch = bulk_fetch
        self.use_raw_mechanism = use_raw_mechanism
        self._key_handle = None
        self._loaded = False
        self.__loading_event = None
        super().__init__(prefer_pss=prefer_pss, embed_roots=embed_roots)
Example #4
0
def _process_certificate_info_response(response_data) -> CSCCredentialInfo:

    try:
        b64_certs = response_data['cert']['certificates']
    except KeyError as e:
        raise SigningError(
            "Could not retrieve certificates from response") from e
    try:
        certs = [
            x509.Certificate.load(base64.b64decode(cert)) for cert in b64_certs
        ]
    except ValueError as e:
        raise SigningError("Could not decode certificates in response") from e
    try:
        algo_oids = response_data["key"]["algo"]
        if not isinstance(algo_oids, list):
            raise TypeError
        supported_algos = frozenset(
            algos.SignedDigestAlgorithmId(oid).native for oid in algo_oids)
    except (KeyError, ValueError, TypeError) as e:
        raise SigningError(
            "Could not retrieve supported signing mechanisms from response"
        ) from e

    try:
        max_batch_size = int(response_data['multisign'])
    except (KeyError, ValueError) as e:
        raise SigningError(
            "Could not retrieve max batch size from response") from e

    scal_value = response_data.get("SCAL", 1)
    try:
        scal_value = int(scal_value)
        if scal_value not in (1, 2):
            raise ValueError
    except ValueError:
        raise SigningError("SCAL value must be \"1\" or \"2\".")
    hash_pinning_required = scal_value == 2

    return CSCCredentialInfo(
        # The CSC spec requires the signer's certificate to be first
        # in the 'certs' array. The order for the others is unspecified,
        # but that doesn't matter.
        signing_cert=certs[0],
        chain=certs[1:],
        supported_mechanisms=supported_algos,
        max_batch_size=max_batch_size,
        hash_pinning_required=hash_pinning_required,
        response_data=response_data)
Example #5
0
async def async_sign_pdf(pdf_out: BasePdfFileWriter,
                         signature_meta: PdfSignatureMetadata,
                         signer: Signer,
                         timestamper: TimeStamper = None,
                         new_field_spec: Optional[SigFieldSpec] = None,
                         existing_fields_only=False,
                         bytes_reserved=None,
                         in_place=False,
                         output=None):
    """
    Thin convenience wrapper around :meth:`.PdfSigner.async_sign_pdf`.

    :param pdf_out:
        An :class:`.IncrementalPdfFileWriter`.
    :param bytes_reserved:
        Bytes to reserve for the CMS object in the PDF file.
        If not specified, make an estimate based on a dummy signature.
    :param signature_meta:
        The specification of the signature to add.
    :param signer:
        :class:`.Signer` object to use to produce the signature object.
    :param timestamper:
        :class:`.TimeStamper` object to use to produce any time stamp tokens
        that might be required.
    :param in_place:
        Sign the input in-place. If ``False``, write output to a
        :class:`.BytesIO` object.
    :param existing_fields_only:
        If ``True``, never create a new empty signature field to contain
        the signature.
        If ``False``, a new field may be created if no field matching
        :attr:`~.PdfSignatureMetadata.field_name` exists.
    :param new_field_spec:
        If a new field is to be created, this parameter allows the caller
        to specify the field's properties in the form of a
        :class:`.SigFieldSpec`. This parameter is only meaningful if
        ``existing_fields_only`` is ``False``.
    :param output:
        Write the output to the specified output stream.
        If ``None``, write to a new :class:`.BytesIO` object.
        Default is ``None``.
    :return:
        The output stream containing the signed output.
    """

    if new_field_spec is not None and existing_fields_only:
        raise SigningError(
            "Specifying a signature field spec is not meaningful when "
            "existing_fields_only=True.")

    signer = PdfSigner(signature_meta,
                       signer,
                       timestamper=timestamper,
                       new_field_spec=new_field_spec)
    return await signer.async_sign_pdf(
        pdf_out,
        existing_fields_only=existing_fields_only,
        bytes_reserved=bytes_reserved,
        in_place=in_place,
        output=output)
Example #6
0
 def read_mdp_dict(mdp):
     try:
         val = mdp['/P']
         return SeedSignatureType(None if val == 0 else MDPPerm(val))
     except (KeyError, TypeError, ValueError):
         raise SigningError(
             f"/MDP entry {mdp} in seed value dictionary is not "
             "correctly formatted.")
Example #7
0
 def get_signature_mechanism(self, digest_algorithm):
     if self.signature_mechanism is not None:
         return self.signature_mechanism
     result = super().get_signature_mechanism(digest_algorithm)
     result_algo = result['algorithm']
     supported = self.auth_manager.credential_info.supported_mechanisms
     if result_algo.native not in supported:
         raise SigningError(
             f"Signature mechanism {result_algo.native} is not supported, "
             f"must be one of {', '.join(alg for alg in supported)}.")
     return result
Example #8
0
    async def authorize_signature(
            self, hash_b64s: List[str]) -> CSCAuthorizationInfo:
        """
        Return the prefetched SAD, or raise an error if called twice.

        :param hash_b64s:
            List of hashes to be signed; ignored.
        :return:
            The prefetched authorisation data.
        """
        if self._used:
            raise SigningError("Prefetched SAD token is stale")
        self._used = True
        return self.csc_auth_info
Example #9
0
    async def _do_commit(self, batch: _CSCBatchInfo):
        """
        Internal commit routine that skips error handling and concurrency
        checks.
        """

        try:
            req_data = await self.format_csc_signing_req(
                batch.b64_hashes, batch.md_algorithm)
            session_info = self.auth_manager.csc_session_info
            url = session_info.endpoint_url("signatures/signHash")
            session = self.session
            async with session.post(url,
                                    headers=self.auth_manager.auth_headers,
                                    json=req_data,
                                    raise_for_status=True,
                                    timeout=self.sign_timeout) as response:
                response_data = await response.json()
            sig_b64s = response_data['signatures']
            actual_len = len(sig_b64s)
            expected_len = len(batch.b64_hashes)
            if actual_len != expected_len:
                raise SigningError(
                    f"Expected {expected_len} signatures, got {actual_len}")
            signatures = [base64.b64decode(sig) for sig in sig_b64s]
            batch.results = signatures
        except SigningError:
            raise
        except (ValueError, KeyError, TypeError) as e:
            raise SigningError(
                "Expected response with b64-encoded signature values") from e
        except aiohttp.ClientError as e:
            raise SigningError("Signature request failed") from e
        finally:
            self._current_batch = None
            batch.notifier.set()
Example #10
0
    def fill_reserved_region(self, output: IO, content_bytes: bytes):
        """
        Write hex-encoded contents to the reserved region indicated
        by :attr:`reserved_region_start` and :attr:`reserved_region_end` in the
        output stream.

        :param output:
            Output stream to use. Must be writable and seekable.
        :param content_bytes:
            Content bytes. These will be padded, hexadecimally encoded and
            written to the appropriate location in output stream.
        :return:
            A :class:`bytes` object containing the contents that were written,
            plus any additional padding.
        """
        content_hex = binascii.hexlify(content_bytes).upper()

        start = self.reserved_region_start
        end = self.reserved_region_end
        # might as well compute this
        bytes_reserved = end - start - 2
        length = len(content_hex)
        if length > bytes_reserved:
            raise SigningError(
                f"Final ByteRange payload larger than expected: "
                f"allocated {bytes_reserved} bytes, but contents "
                f"required {length} bytes.")  # pragma: nocover

        # +1 to skip the '<'
        output.seek(start + 1)
        # NOTE: the PDF spec is not completely clear on this, but
        # signature contents are NOT supposed to be encrypted.
        # Perhaps this falls under the "strings in encrypted containers"
        # denominator in § 7.6.1?
        # Addition: the PDF 2.0 spec *does* spell out that this content
        # is not to be encrypted.
        output.write(content_hex)

        output.seek(0)
        padding = bytes(bytes_reserved // 2 - len(content_bytes))
        return content_bytes + padding
    async def authorize_signature(
            self, hash_b64s: List[str]) -> CSCAuthorizationInfo:
        self.authorizations_requested += 1
        session_info = self.csc_session_info
        req_data = self.format_csc_auth_request(hash_b64s=hash_b64s)
        session = self.session

        url = session_info.endpoint_url("credentials/authorize")
        async with session.post(url,
                                headers=self.auth_headers,
                                json=req_data,
                                raise_for_status=True,
                                timeout=30) as response:
            try:
                response_data = await response.json()
            except aiohttp.ClientError as e:
                raise SigningError("Credential auth request failed") from e

        if self.waste_time:
            await asyncio.sleep(self.waste_time)
        return self.parse_csc_auth_response(response_data)
Example #12
0
    async def commit(self):
        """
        Commit the current batch by calling the ``signatures/signHash`` endpoint
        on the CSC service.

        This coroutine does not return anything; instead, it notifies all
        waiting signing coroutines that their signature has been fetched.
        """

        batch = self._current_batch
        if batch is None or batch.results is not None:
            return
        elif batch.initiated:
            # just wait for the commit to finish together with
            # all the signers in the queue
            await batch.notifier.wait()
            if not batch.results:
                raise SigningError("Commit failed")
        else:
            batch.initiated = True
            await self._do_commit(batch)
Example #13
0
 async def _ensure_batch(self, digest_algorithm) -> _CSCBatchInfo:
     while self._current_batch is not None and self._current_batch.initiated:
         logger.debug("Commit ongoing... Waiting for it to finish")
         # There's a commit going on, wait for it to finish
         await self._current_batch.notifier.wait()
         logger.debug(f"Done waiting for commit: "
                      f"new batch: {repr(self._current_batch)})")
         # ...and start a new batch right after (unless someone else
         # already did, or the new batch is somehow already full/already
         # committing, in which case we have to keep queueing)
     if self._current_batch is not None:
         batch = self._current_batch
         if batch.md_algorithm != digest_algorithm:
             raise SigningError(
                 f"All signatures in the same batch must use the same "
                 f"digest function; encountered both {batch.md_algorithm} "
                 f"and {digest_algorithm}.")
         return batch
     self._current_batch = batch = _CSCBatchInfo(
         notifier=asyncio.Event(),
         md_algorithm=digest_algorithm,
     )
     return batch
Example #14
0
    async def async_sign_raw(self,
                             data: bytes,
                             digest_algorithm: str,
                             dry_run=False) -> bytes:
        if dry_run:
            return bytes(self.est_raw_signature_size)

        tbs_hash = base64_digest(data, digest_algorithm)
        # ensure that there's a batch that we can hitch a ride on
        batch = await self._ensure_batch(digest_algorithm)
        ix = batch.add(tbs_hash)
        # autocommit if the batch is full
        if self.batch_autocommit and ix == self.batch_size - 1:
            try:
                await self.commit()
            except SigningError as e:
                # log and move on, we'll throw a regular exception later
                logger.error("Failed to commit signatures", exc_info=e)

        # Sleep until a commit goes through
        await batch.notifier.wait()
        if not batch.results:
            raise SigningError("No signing results available")
        return batch.results[ix]
Example #15
0
async def fetch_certs_in_csc_credential(
        session: aiohttp.ClientSession,
        csc_session_info: CSCServiceSessionInfo,
        timeout: int = 30) -> CSCCredentialInfo:
    """
    Call the ``credentials/info`` endpoint of the CSC service for a specific
    credential, and encode the result into a :class:`.CSCCredentialInfo`
    object.

    :param session:
        The ``aiohttp`` session to use when performing queries.
    :param csc_session_info:
        General information about the CSC service and the credential.
    :param timeout:
        How many seconds to allow before time-out.
    :return:
        A :class:`.CSCCredentialInfo` object with the processed response.
    """
    url = csc_session_info.endpoint_url("credentials/info")
    req_data = {
        "credentialID": csc_session_info.credential_id,
        "certificates": "chain",
        "certInfo": False
    }

    try:
        async with session.post(url,
                                headers=csc_session_info.auth_headers,
                                json=req_data,
                                raise_for_status=True,
                                timeout=timeout) as response:
            response_data = await response.json()
    except aiohttp.ClientError as e:
        raise SigningError("Credential info request failed") from e

    return _process_certificate_info_response(response_data)
Example #16
0
    def from_pdf_object(cls, pdf_dict):
        """
        Read from a seed value dictionary.

        :param pdf_dict:
            A :class:`~.generic.DictionaryObject`.
        :return:
            A :class:`.SigSeedValueSpec` object.
        """
        if isinstance(pdf_dict, generic.IndirectObject):
            pdf_dict = pdf_dict.get_object()
        try:
            if pdf_dict['/Type'] != '/SV':  # pragma: nocover
                raise ValueError('Object /Type entry is not /SV')
        except KeyError:  # pragma: nocover
            pass

        flags = SigSeedValFlags(pdf_dict.get('/Ff', 0))
        try:
            sig_filter = pdf_dict['/Filter']
            if (flags & SigSeedValFlags.FILTER) and \
                    (sig_filter != '/Adobe.PPKLite'):
                raise SigningError(
                    "Signature handler '%s' is not available, only the "
                    "default /Adobe.PPKLite is supported." % sig_filter)
        except KeyError:
            pass

        # TODO support all PDF 2.0 values
        min_version = pdf_dict.get('/V', 1)
        if flags & SigSeedValFlags.V and min_version > 1:
            raise SigningError(
                "Seed value dictionary version %s not supported." %
                min_version)

        try:
            add_rev_info = bool(pdf_dict['/AddRevInfo'])
        except KeyError:
            add_rev_info = None

        subfilter_reqs = pdf_dict.get('/SubFilter', None)
        subfilters = None
        if subfilter_reqs is not None:

            def _subfilters():
                for s in subfilter_reqs:
                    try:
                        yield SigSeedSubFilter(s)
                    except ValueError:
                        pass

            subfilters = list(_subfilters())

        try:
            digest_methods = [s.lower() for s in pdf_dict['/DigestMethod']]
        except KeyError:
            digest_methods = None

        reasons = get_and_apply(pdf_dict, '/Reasons', list)
        timestamp_dict = pdf_dict.get('/TimeStamp', {})
        timestamp_server_url = timestamp_dict.get('/URL', None)
        timestamp_required = bool(timestamp_dict.get('/Ff', 0))
        cert_constraints = pdf_dict.get('/Cert', None)
        if cert_constraints is not None:
            cert_constraints = SigCertConstraints.from_pdf_object(
                cert_constraints)
        return cls(flags=flags,
                   reasons=reasons,
                   timestamp_server_url=timestamp_server_url,
                   cert=cert_constraints,
                   subfilters=subfilters,
                   digest_methods=digest_methods,
                   add_rev_info=add_rev_info,
                   timestamp_required=timestamp_required)
Example #17
0
    def write_cms(self,
                  field_name: str,
                  writer: BasePdfFileWriter,
                  existing_fields_only=False):
        """
        .. versionadded:: 0.3.0

        .. versionchanged:: 0.7.0
            Digest wrapped in
            :class:`~pyhanko.sign.signers.pdf_byterange.PreparedByteRangeDigest`
            in step 3; ``output`` returned in step 3 instead of step 4.

        This method returns a generator coroutine that controls the process
        of embedding CMS data into a PDF signature field.
        Can be used for both timestamps and regular signatures.

        .. danger::
            This is a very low-level interface that performs virtually no
            error checking, and is intended to be used in situations
            where the construction of the CMS object to be embedded
            is not under the caller's control (e.g. a remote signer
            that produces full-fledged CMS objects).

            In almost every other case, you're better of using
            :class:`.PdfSigner` instead, with a custom :class:`.Signer`
            implementation to handle the cryptographic operations if necessary.

        The coroutine follows the following specific protocol.

        1. First, it retrieves or creates the signature field to embed the
           CMS object in, and yields a reference to said field.
        2. The caller should then send in a :class:`.SigObjSetup` object, which
           is subsequently processed by the coroutine. For convenience, the
           coroutine will then yield a reference to the signature dictionary
           (as embedded in the PDF writer).
        3. Next, the caller should send a :class:`.SigIOSetup` object,
           describing how the resulting document should be hashed and written
           to the output. The coroutine will write the entire document with a
           placeholder region reserved for the signature and compute the
           document's hash and yield it to the caller.
           It will then yield a ``prepared_digest, output`` tuple, where
           ``prepared_digest`` is a :class:`.PreparedByteRangeDigest` object
           containing the document digest and the relevant offsets, and
           ``output`` is the output stream to which the document to be
           signed was written.

           From this point onwards, **no objects may be changed or added** to
           the :class:`.IncrementalPdfFileWriter` currently in use.
        4. Finally, the caller should pass in a CMS object to place inside
           the signature dictionary. The CMS object can be supplied as a raw
           :class:`bytes` object, or an :mod:`asn1crypto`-style object.
           The coroutine's final yield is the value of the signature
           dictionary's ``/Contents`` entry, given as a hexadecimal string.

        .. caution::
            It is the caller's own responsibility to ensure that enough room
            is available in the placeholder signature object to contain
            the final CMS object.

        :param field_name:
            The name of the field to fill in. This should be a field of type
            ``/Sig``.
        :param writer:
            An :class:`.IncrementalPdfFileWriter` containing the
            document to sign.
        :param existing_fields_only:
            If ``True``, never create a new empty signature field to contain
            the signature.
            If ``False``, a new field may be created if no field matching
            ``field_name`` exists.
        :return:
            A generator coroutine implementing the protocol described above.
        """

        new_field_spec = self.new_field_spec \
            if not existing_fields_only else None
        # start by creating or fetching the appropriate signature field
        field_created, sig_field_ref = _get_or_create_sigfield(
            field_name,
            writer,
            existing_fields_only,
            new_field_spec=new_field_spec)

        # yield control to caller to further process the field dictionary
        # if necessary, request setup specs for sig object
        sig_obj_setup = yield sig_field_ref
        assert isinstance(sig_obj_setup, SigObjSetup)

        sig_field = sig_field_ref.get_object()

        # take care of the field's visual appearance (if applicable)
        appearance_setup = sig_obj_setup.appearance_setup
        if appearance_setup is not None:
            try:
                sig_annot, = sig_field['/Kids']
                sig_annot = sig_annot.get_object()
            except (ValueError, TypeError):
                raise SigningError(
                    "Failed to access signature field's annotation. "
                    "Signature field must have exactly one child annotation, "
                    "or it must be combined with its annotation.")
            except KeyError:
                sig_annot = sig_field

            appearance_setup.apply(sig_annot, writer)

        sig_obj = sig_obj_setup.sig_placeholder
        sig_obj_ref = writer.add_object(sig_obj)

        # fill in a reference to the (empty) signature object
        sig_field[pdf_name('/V')] = sig_obj_ref

        if not field_created:
            # still need to mark it for updating
            writer.mark_update(sig_field_ref)

        mdp_setup = sig_obj_setup.mdp_setup
        if mdp_setup is not None:
            mdp_setup.apply(sig_obj_ref, writer)

        # again, pass control to the caller
        # and request I/O parameters for putting the cryptographic signature
        # into the output.
        # We pass a reference to the embedded signature object as a convenience.

        sig_io = yield sig_obj_ref
        assert isinstance(sig_io, SigIOSetup)

        # pass control to the sig object's write_signature coroutine
        yield from sig_obj.fill(writer,
                                sig_io.md_algorithm,
                                in_place=sig_io.in_place,
                                output=sig_io.output,
                                chunk_size=sig_io.chunk_size)
Example #18
0
def _prepare_sig_field(sig_field_name,
                       root,
                       update_writer: BasePdfFileWriter,
                       existing_fields_only=False,
                       lock_sig_flags=True,
                       **kwargs):
    """
    Returns a tuple of a boolean and a reference to a signature field.
    The boolean is ``True`` if the field was created, and ``False`` otherwise.
    """
    if sig_field_name is None:  # pragma: nocover
        raise ValueError

    try:
        form = root['/AcroForm']

        try:
            fields = form['/Fields']
        except KeyError:
            raise ValueError('/AcroForm has no /Fields')

        candidates = enumerate_sig_fields_in(fields, with_name=sig_field_name)
        sig_field_ref = None
        try:
            field_name, value, sig_field_ref = next(candidates)
            if value is not None:
                raise SigningError(
                    'Signature field with name %s appears to be filled already.'
                    % sig_field_name)
        except StopIteration:
            if existing_fields_only:
                raise SigningError(
                    'No empty signature field with name %s found.' %
                    sig_field_name)
        form_created = False
    except KeyError:
        # we have to create the form
        if existing_fields_only:
            raise SigningError('This file does not contain a form.')
        # no AcroForm present, so create one
        form = generic.DictionaryObject()
        root[pdf_name('/AcroForm')] = update_writer.add_object(form)
        fields = generic.ArrayObject()
        form[pdf_name('/Fields')] = fields
        # now we need to mark the root as updated
        update_writer.update_root()
        form_created = True
        sig_field_ref = None

    if sig_field_ref is not None:
        return False, sig_field_ref

    # no signature field exists, so create one
    # default: grab a reference to the first page
    page_ref = update_writer.find_page_for_modification(0)[0]
    sig_form_kwargs = {'include_on_page': page_ref}
    sig_form_kwargs.update(**kwargs)
    sig_field = SignatureFormField(sig_field_name, **sig_form_kwargs)
    created, sig_field_ref = _insert_or_get_field_at(
        update_writer,
        fields,
        path=sig_field_name.split('.'),
        field_obj=sig_field)
    update_writer.register_annotation(page_ref, sig_field_ref)

    # make sure /SigFlags is present. If not, create it
    sig_flags = 3 if lock_sig_flags else 1
    form.setdefault(pdf_name('/SigFlags'), generic.NumberObject(sig_flags))
    # if a field was added to an existing form, register an extra update
    if not form_created:
        update_writer.update_container(fields)
    return True, sig_field_ref
Example #19
0
def enumerate_sig_fields_in(field_list,
                            filled_status=None,
                            with_name=None,
                            parent_name="",
                            parents=None):
    if not isinstance(field_list, generic.ArrayObject):
        logger.warning(
            f"Values of type {type(field_list)} are not valid as field "
            f"lists, must be array objects -- skipping.")
        return

    parents = parents or ()
    for field_ref in field_list:
        if not isinstance(field_ref, generic.IndirectObject):
            logger.warning(
                "Entries in field list must be indirect references -- skipping."
            )
            continue

        field = field_ref.get_object()
        if not isinstance(field, generic.DictionaryObject):
            logger.warning(
                "Entries in field list must be dictionary objects, not "
                f"{type(field)} -- skipping.")
            continue
        # /T is the field name. If not specified, we're dealing with a bare
        # widget, so skip it. (these should never occur in /Fields, but hey)
        try:
            field_name = field['/T']
        except KeyError:
            continue
        fq_name = field_name if not parent_name else (
            "%s.%s" % (parent_name, field_name))
        explicitly_requested = with_name is not None and fq_name == with_name
        child_requested = explicitly_requested or (
            with_name is not None and with_name.startswith(fq_name))
        # /FT is inheritable, so go up the chain
        current_path = (field, ) + parents
        for parent_field in current_path:
            try:
                field_type = parent_field['/FT']
                break
            except KeyError:
                continue
        else:
            field_type = None

        if field_type == '/Sig':
            field_value = field.get('/V')
            # "cast" to a regular string object
            filled = field_value is not None
            status_check = filled_status is None or filled == filled_status
            name_check = with_name is None or explicitly_requested
            if status_check and name_check:
                yield fq_name, field_value, field_ref
        elif explicitly_requested:
            raise SigningError(
                'Field with name %s exists but is not a signature field' %
                fq_name)

        # if necessary, descend into the field hierarchy
        if with_name is None or (child_requested and not explicitly_requested):
            try:
                yield from enumerate_sig_fields_in(field['/Kids'],
                                                   parent_name=fq_name,
                                                   parents=current_path,
                                                   with_name=with_name,
                                                   filled_status=filled_status)
            except KeyError:
                continue
Example #20
0
    def from_pdf_object(cls, pdf_dict):
        """
        Read from a seed value dictionary.

        :param pdf_dict:
            A :class:`~.generic.DictionaryObject`.
        :return:
            A :class:`.SigSeedValueSpec` object.
        """
        if isinstance(pdf_dict, generic.IndirectObject):
            pdf_dict = pdf_dict.get_object()
        try:
            if pdf_dict['/Type'] != '/SV':  # pragma: nocover
                raise ValueError('Object /Type entry is not /SV')
        except KeyError:  # pragma: nocover
            pass

        flags = SigSeedValFlags(pdf_dict.get('/Ff', 0))
        try:
            sig_filter = pdf_dict['/Filter']
            if (flags & SigSeedValFlags.FILTER) and \
                    (sig_filter != '/Adobe.PPKLite'):
                raise SigningError(
                    "Signature handler '%s' is not available, only the "
                    "default /Adobe.PPKLite is supported." % sig_filter)
        except KeyError:
            pass

        try:
            min_version = pdf_dict['/V']
            supported = SeedValueDictVersion.PDF_2_0.value
            if flags & SigSeedValFlags.V and min_version > supported:
                raise SigningError(
                    "Seed value dictionary version %s not supported." %
                    min_version)
            min_version = SeedValueDictVersion(min_version)
        except KeyError:
            min_version = None

        try:
            add_rev_info = bool(pdf_dict['/AddRevInfo'])
        except KeyError:
            add_rev_info = None

        subfilter_reqs = pdf_dict.get('/SubFilter', None)
        subfilters = None
        if subfilter_reqs is not None:

            def _subfilters():
                for s in subfilter_reqs:
                    try:
                        yield SigSeedSubFilter(s)
                    except ValueError:
                        pass

            subfilters = list(_subfilters())

        try:
            digest_methods = [s.lower() for s in pdf_dict['/DigestMethod']]
        except KeyError:
            digest_methods = None

        reasons = get_and_apply(pdf_dict, '/Reasons', list)
        legal_attestations = get_and_apply(pdf_dict, '/LegalAttestation', list)

        def read_mdp_dict(mdp):
            try:
                val = mdp['/P']
                return SeedSignatureType(None if val == 0 else MDPPerm(val))
            except (KeyError, TypeError, ValueError):
                raise SigningError(
                    f"/MDP entry {mdp} in seed value dictionary is not "
                    "correctly formatted.")

        signature_type = get_and_apply(pdf_dict, '/MDP', read_mdp_dict)

        def read_lock_document(val):
            try:
                return SeedLockDocument(val)
            except ValueError:
                raise SigningError(f"/LockDocument entry '{val}' is invalid.")

        lock_document = get_and_apply(pdf_dict, '/LockDocument',
                                      read_lock_document)
        appearance_filter = pdf_dict.get('/AppearanceFilter', None)
        timestamp_dict = pdf_dict.get('/TimeStamp', {})
        timestamp_server_url = timestamp_dict.get('/URL', None)
        timestamp_required = bool(timestamp_dict.get('/Ff', 0))
        cert_constraints = pdf_dict.get('/Cert', None)
        if cert_constraints is not None:
            cert_constraints = SigCertConstraints.from_pdf_object(
                cert_constraints)
        return cls(flags=flags,
                   reasons=reasons,
                   timestamp_server_url=timestamp_server_url,
                   cert=cert_constraints,
                   subfilters=subfilters,
                   digest_methods=digest_methods,
                   add_rev_info=add_rev_info,
                   timestamp_required=timestamp_required,
                   legal_attestations=legal_attestations,
                   seed_signature_type=signature_type,
                   sv_dict_version=min_version,
                   lock_document=lock_document,
                   appearance=appearance_filter)
Example #21
0
 def read_lock_document(val):
     try:
         return SeedLockDocument(val)
     except ValueError:
         raise SigningError(f"/LockDocument entry '{val}' is invalid.")