Example #1
0
    def render(self):
        command_stream = [b'q']

        # text rendering
        self._init_text_box()
        _text_params = self.get_default_text_params()
        if self.text_params is not None:
            _text_params.update(self.text_params)
        text = self.style.stamp_text % _text_params
        self.text_box.content = text

        stamp_height = self.get_stamp_height()
        stamp_width = self.get_stamp_width()

        bg = self.style.background
        if bg is not None:
            # TODO this is one of the places where some more clever layout
            #  engine would really help, since all of this is pretty ad hoc and
            #  makes a number of non-obvious choices that would be better off
            #  delegated to somewhere else.
            bg.set_writer(self.writer)

            # scale the background
            bg_height = 0.9 * stamp_height
            if bg.box.height_defined:
                sf = bg_height / bg.box.height
            else:
                bg.box.height = bg_height
                sf = 1
            bg_y = 0.05 * stamp_height
            bg_width = bg.box.width * sf
            bg_x = 0
            if bg_width <= stamp_width:
                bg_x = (stamp_width - bg_width) // 2

            # set opacity in graphics state
            opacity = generic.FloatObject(self.style.background_opacity)
            self.set_resource(category=ResourceType.EXT_G_STATE,
                              name=pdf_name('/BackgroundGS'),
                              value=generic.DictionaryObject({
                                  pdf_name('/CA'):
                                  opacity,
                                  pdf_name('/ca'):
                                  opacity
                              }))
            command_stream.append(
                b'q /BackgroundGS gs %g 0 0 %g %g %g cm %s Q' %
                (sf, sf, bg_x, bg_y, bg.render()))
            self.import_resources(bg.resources)

        tb = self.text_box
        text_commands = tb.render()

        text_scale = 1
        if self.expected_text_width is not None and tb.box.width_defined:
            text_scale = self.expected_text_width / tb.box.width

        command_stream.append(
            b'q %g 0 0 %g %g %g cm' %
            (text_scale, text_scale, self.text_box_x(), self.text_box_y()))
        command_stream.append(text_commands)
        command_stream.append(b'Q')

        # append additional drawing commands
        command_stream.extend(self.extra_commands())

        # draw border around stamp
        command_stream.append(
            b'%g w 0 0 %g %g re S' %
            (self.style.border_width, stamp_width, stamp_height))
        command_stream.append(b'Q')
        return b' '.join(command_stream)
Example #2
0
    def __init__(self,
                 field_name,
                 include_on_page,
                 *,
                 writer,
                 sig_object_ref=None,
                 box=None,
                 appearances: Optional[AnnotAppearances] = None):

        if box is not None:
            visible = True
            rect = list(map(generic.FloatObject, box))
            if appearances is not None:
                ap = appearances.as_pdf_object()
            else:
                ap = None
        else:
            rect = [generic.FloatObject(0)] * 4
            ap = None
            visible = False

        # this sets the "Print" bit, and activates "Locked" if the
        # signature field is ready to be filled
        flags = 0b100 if sig_object_ref is None else 0b10000100
        super().__init__({
            # Signature field properties
            pdf_name('/FT'):
            pdf_name('/Sig'),
            pdf_name('/T'):
            pdf_string(field_name),
            # Annotation properties: bare minimum
            pdf_name('/Type'):
            pdf_name('/Annot'),
            pdf_name('/Subtype'):
            pdf_name('/Widget'),
            pdf_name('/F'):
            generic.NumberObject(flags),
            pdf_name('/P'):
            include_on_page,
            pdf_name('/Rect'):
            generic.ArrayObject(rect)
        })
        if sig_object_ref is not None:
            self[pdf_name('/V')] = sig_object_ref
        if ap is not None:
            self[pdf_name('/AP')] = ap

        # register ourselves
        self.reference = self_reference = writer.add_object(self)
        # if we're building an invisible form field, this is all there is to it
        if visible:
            writer.register_annotation(include_on_page, self_reference)
Example #3
0
    w = IncrementalPdfFileWriter(BytesIO(MINIMAL))
    meta = signers.PdfSignatureMetadata(field_name='Sig1')
    out = signers.sign_pdf(w, meta, signer=SELF_SIGN)

    r = PdfFileReader(out)
    emb = r.embedded_signatures[0]
    # Again:
    # (yes, obviously this also isn't a valid timestamp token, hence the
    #  match=... rule here)
    with pytest.raises(SignatureValidationError,
                       match='.*must be /DocTimeStamp.*'):
        validate_pdf_timestamp(emb, validation_context=SIMPLE_V_CONTEXT())


@pytest.mark.parametrize('wrong_subfilter', [
    pdf_name('/abcde'), pdf_name("/ETSI.RFC3161"), None,
    generic.NullObject()
])
@freeze_time('2020-11-01')
def test_sig_wrong_subfilter(wrong_subfilter):
    def tamper(writer, sig_obj):
        if wrong_subfilter:
            sig_obj['/SubFilter'] = wrong_subfilter
        else:
            del sig_obj['/SubFilter']
    out = _tamper_with_sig_obj(tamper)

    r = PdfFileReader(out)
    emb = r.embedded_signatures[0]
    with pytest.raises(SignatureValidationError):
        val_trusted(emb)
Example #4
0
def _prepare_sig_field(sig_field_name,
                       root,
                       update_writer: IncrementalPdfFileWriter,
                       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

    if '.' in sig_field_name:
        raise NotImplementedError(
            "Creating fields deep in the form hierarchy is not supported"
            "right now.")

    # 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,
                                   writer=update_writer,
                                   **sig_form_kwargs)
    sig_field_ref = sig_field.reference
    fields.append(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 #5
0
    def as_pdf_object(self):
        """
        Render this :class:`.SigSeedValueSpec` object to a PDF dictionary.

        :return:
            A :class:`~.generic.DictionaryObject`.
        """
        result = generic.DictionaryObject({
            pdf_name('/Type'):
            pdf_name('/SV'),
            pdf_name('/Ff'):
            generic.NumberObject(self.flags.value),
        })

        if self.subfilters is not None:
            result[pdf_name('/SubFilter')] = generic.ArrayObject(
                sf.value for sf in self.subfilters)
        if self.add_rev_info is not None:
            result[pdf_name('/AddRevInfo')] = generic.BooleanObject(
                self.add_rev_info)
        if self.digest_methods is not None:
            result[pdf_name('/DigestMethod')] = generic.ArrayObject(
                map(pdf_string, self.digest_methods))
        if self.reasons is not None:
            result[pdf_name('/Reasons')] = generic.ArrayObject(
                pdf_string(reason) for reason in self.reasons)
        if self.timestamp_server_url is not None:
            result[pdf_name('/TimeStamp')] = generic.DictionaryObject({
                pdf_name('/URL'):
                pdf_string(self.timestamp_server_url),
                pdf_name('/Ff'):
                generic.NumberObject(1 if self.timestamp_required else 0)
            })
        if self.cert is not None:
            result[pdf_name('/Cert')] = self.cert.as_pdf_object()
        return result
Example #6
0
class SigCertConstraints:
    """
    This part of the seed value dictionary allows the document author
    to set constraints on the signer's certificate.

    See Table 235 in ISO 32000-1.
    """
    flags: SigCertConstraintFlags = SigCertConstraintFlags(0)
    """
    Enforcement flags. By default, all entries are optional.
    """

    subjects: List[x509.Certificate] = None
    """
    Explicit list of certificates that can be used to sign a signature field.
    """

    subject_dn: x509.Name = None
    """
    Certificate subject names that can be used to sign a signature field.
    Subject DN entries that are not mentioned are unconstrained.
    """

    issuers: List[x509.Certificate] = None
    """
    List of issuer certificates that the signer certificate can be issued by.
    Note that these issuers do not need to be the *direct* issuer of the
    signer's certificate; any descendant relationship will do.
    """

    info_url: str = None
    """
    Informational URL that should be opened when an appropriate signature
    cannot be found.
    """

    url_type: generic.NameObject = pdf_name('/Browser')
    """
    Handler that should be used to open :attr:`info_url`.
    ``/Browser`` is the only implementation-independent value.
    """

    # TODO support key usage and OID constraints

    @classmethod
    def from_pdf_object(cls, pdf_dict):
        """
        Read a PDF dictionary into a :class:`.SigCertConstraints` object.

        :param pdf_dict:
            A :class:`~.generic.DictionaryObject`.
        :return:
            A :class:`.SigCertConstraints` object.
        """

        if isinstance(pdf_dict, generic.IndirectObject):
            pdf_dict = pdf_dict.get_object()
        try:
            if pdf_dict['/Type'] != '/SVCert':  # pragma: nocover
                raise ValueError('Object /Type entry is not /SVCert')
        except KeyError:  # pragma: nocover
            pass
        flags = SigCertConstraintFlags(pdf_dict.get('/Ff', 0))
        subjects = [
            oskeys.parse_certificate(cert.original_bytes)
            for cert in pdf_dict.get('/Subject', ())
        ]
        issuers = [
            oskeys.parse_certificate(cert.original_bytes)
            for cert in pdf_dict.get('/Issuer', ())
        ]

        def format_attr(attr):
            # strip initial /
            attr = attr[1:]
            # attempt to convert abbreviated attrs to OIDs, since build()
            # takes OIDs
            return name_type_abbrevs_rev.get(attr.upper(), attr)

        subject_dns = x509.Name.build({
            format_attr(attr): value
            for dn_dir in pdf_dict.get('/SubjectDN', ())
            for attr, value in dn_dir.items()
        })

        url = pdf_dict.get('/URL')
        url_type = pdf_dict.get('/URLType')
        kwargs = {
            'flags': flags,
            'subjects': subjects or None,
            'subject_dn': subject_dns or None,
            'issuers': issuers or None,
            'info_url': url
        }
        if url is not None and url_type is not None:
            kwargs['url_type'] = url_type
        return cls(**kwargs)

    def as_pdf_object(self):
        """
        Render this :class:`.SigCertConstraints` object to a PDF dictionary.

        :return:
            A :class:`~.generic.DictionaryObject`.
        """

        result = generic.DictionaryObject({
            pdf_name('/Type'):
            pdf_name('/SVCert'),
            pdf_name('/Ff'):
            generic.NumberObject(self.flags.value),
        })
        if self.subjects is not None:
            result[pdf_name('/Subject')] = generic.ArrayObject(
                generic.ByteStringObject(cert.dump())
                for cert in self.subjects)
        if self.subject_dn:
            # FIXME Adobe Reader seems to ignore this for some reason.
            #  Should try to figure out what I'm doing wrong
            result[pdf_name('/SubjectDN')] = generic.ArrayObject([
                generic.DictionaryObject({
                    pdf_name('/' + key): pdf_string(value)
                    for key, value in x509_name_keyval_pairs(
                        self.subject_dn, abbreviate_oids=True)
                })
            ])
        if self.issuers is not None:
            result[pdf_name('/Issuer')] = generic.ArrayObject(
                generic.ByteStringObject(cert.dump()) for cert in self.issuers)
        if self.info_url is not None:
            result[pdf_name('/URL')] = pdf_string(self.info_url)
            result[pdf_name('/URLType')] = self.url_type

        return result

    def satisfied_by(self, signer: x509.Certificate,
                     validation_path: Optional[ValidationPath]):
        """
        Evaluate whether a signing certificate satisfies the required
        constraints of this :class:`.SigCertConstraints` object.

        :param signer:
            The candidate signer's certificate.
        :param validation_path:
            Validation path of the signer's certificate.
        :raises UnacceptableSignerError:
            Raised if the conditions are not met.
        """
        # this function assumes that key usage & trust checks have
        #  passed already.
        flags = self.flags
        if (flags & SigCertConstraintFlags.SUBJECT) \
                and self.subjects is not None:
            # Explicit whitelist of approved signer certificates
            # compare using issuer_serial
            acceptable = (s.issuer_serial for s in self.subjects)
            if signer.issuer_serial not in acceptable:
                raise UnacceptableSignerError(
                    "Signer certificate not on SVCert whitelist.")
        if (flags & SigCertConstraintFlags.ISSUER) \
                and self.issuers is not None:
            if validation_path is None:
                raise UnacceptableSignerError("Validation path not provided.")
            # Here, we need to match any issuer in the chain of trust to
            #  any of the issuers on the approved list.

            # To do so, we collect all issuer_serial identifiers in the chain
            # for all certificates except the last one (i.e. the current signer)
            path_iss_serials = {
                entry.issuer_serial
                for entry in validation_path.copy().pop()
            }
            for issuer in self.issuers:
                if issuer.issuer_serial in path_iss_serials:
                    break
            else:
                # raise error if the loop runs to completion
                raise UnacceptableSignerError(
                    "Signer certificate cannot be traced back to approved "
                    "issuer.")
        if (flags & SigCertConstraintFlags.SUBJECT_DN) and self.subject_dn:
            # I'm not entirely sure whether my reading of the standard is
            #  is correct, but I believe that this is the intention:
            # A DistinguishedName object is a sequence of
            #  relative distinguished names (RDNs). The contents of the
            #  /SubjectDN specify a list of constraints that might apply to each
            #  of these RDNs. I believe the requirement is that each of the
            #  SubjectDN entries must match one of these RDNs.

            requirement_list = list(x509_name_keyval_pairs(self.subject_dn))
            subject_name = list(x509_name_keyval_pairs(signer.subject))
            if not all(attr in subject_name for attr in requirement_list):
                raise UnacceptableSignerError(
                    "Subject does not have some of the following required "
                    "attributes: " + self.subject_dn.human_friendly)
Example #7
0
    def _inner_layout_natural_size(self):

        text_commands, (text_width, text_height) \
            = super()._inner_layout_natural_size()

        qr_ref, natural_qr_size = self._qr_xobject()
        self.set_resource(
            category=content.ResourceType.XOBJECT, name=pdf_name('/QR'),
            value=qr_ref
        )

        style = self.style
        stamp_box = self.box

        # To size the QR code, we proceed as follows:
        #  - If qr_inner_size is not None, use it
        #  - If the stamp has a fully defined bbox already,
        #    make sure it fits within the innseps, and it's not too much smaller
        #    than the text box
        #  - Else, scale down by DEFAULT_QR_SCALE and use that value
        #
        # Note: if qr_inner_size is defined AND the stamp bbox is available
        # already, scaling might still take effect depending on the inner layout
        # rule.
        innsep = style.innsep
        if style.qr_inner_size is not None:
            qr_size = style.qr_inner_size
        elif stamp_box.width_defined and stamp_box.height_defined:
            # ensure that the QR code doesn't shrink too much if the text
            # box is too tall.
            min_dim = min(
                max(stamp_box.height, text_height),
                max(stamp_box.width, text_width)
            )
            qr_size = min_dim - 2 * innsep
        else:
            qr_size = int(round(DEFAULT_QR_SCALE * natural_qr_size))

        qr_innunits_scale = qr_size / natural_qr_size
        qr_padded = qr_size + 2 * innsep
        # Next up: put the QR code and the text box together to get the
        # inner layout bounding box
        if style.qr_position.horizontal_flow:
            inn_width = qr_padded + text_width
            inn_height = max(qr_padded, text_height)
        else:
            inn_width = max(qr_padded, text_width)
            inn_height = qr_padded + text_height
        # grab the base layout rule from the QR position setting
        default_layout: layout.SimpleBoxLayoutRule = style.qr_position.value

        # Fill in the margins
        qr_layout_rule = layout.SimpleBoxLayoutRule(
            x_align=default_layout.x_align, y_align=default_layout.y_align,
            margins=layout.Margins.uniform(innsep),
            # There's no point in scaling here, the inner content canvas
            # is always big enough
            inner_content_scaling=layout.InnerScaling.NO_SCALING
        )

        inner_box = layout.BoxConstraints(inn_width, inn_height)
        qr_inn_pos = qr_layout_rule.fit(inner_box, qr_size, qr_size)

        # we still need to take the axis reversal into account
        # (which also implies an adjustment in the y displacement)
        draw_qr_command = b'q %g 0 0 %g %g %g cm /QR Do Q' % (
            qr_inn_pos.x_scale * qr_innunits_scale,
            -qr_inn_pos.y_scale * qr_innunits_scale,
            qr_inn_pos.x_pos, qr_inn_pos.y_pos + qr_size,
        )

        # Time to put in the text box now
        if style.qr_position == QRPosition.LEFT_OF_TEXT:
            tb_margins = layout.Margins(
                left=qr_padded, right=0, top=0, bottom=0
            )
        elif style.qr_position == QRPosition.RIGHT_OF_TEXT:
            tb_margins = layout.Margins(
                right=qr_padded, left=0, top=0, bottom=0
            )
        elif style.qr_position == QRPosition.BELOW_TEXT:
            tb_margins = layout.Margins(
                bottom=qr_padded, right=0, left=0, top=0
            )
        else:
            tb_margins = layout.Margins(
                top=qr_padded, right=0, left=0, bottom=0
            )

        tb_layout_rule = layout.SimpleBoxLayoutRule(
            # flip around the alignment conventions of the default layout
            # to position the text box on the other side
            x_align=default_layout.x_align.flipped,
            y_align=default_layout.y_align.flipped,
            margins=tb_margins,
            inner_content_scaling=layout.InnerScaling.NO_SCALING
        )

        # position the text box
        text_inn_pos = tb_layout_rule.fit(inner_box, text_width, text_height)

        commands = [draw_qr_command, b'q', text_inn_pos.as_cm()]
        commands.extend(text_commands)
        commands.append(b'Q')
        return commands, (inn_width, inn_height)
Example #8
0
    def import_page_as_xobject(self,
                               other: PdfHandler,
                               page_ix=0,
                               content_stream=0,
                               inherit_filters=True):
        """
        Import a page content stream from some other
        :class:`~.rw_common.PdfHandler` into the current one as a form XObject.

        :param other:
            A :class:`~.rw_common.PdfHandler`
        :param page_ix:
            Index of the page to copy (default: 0)
        :param content_stream:
            Index of the page's content stream to copy, if multiple are present
            (default: 0)
        :param inherit_filters:
            Inherit the content stream's filters, if present.
        :return:
            An :class:`~.generic.IndirectObject` referring to the page object
            as added to the current reader.
        """
        page_ref, resources = other.find_page_for_modification(page_ix)
        page_obj = page_ref.get_object()

        # find the page's /MediaBox by going up the tree until we encounter it
        pagetree_obj = page_obj
        while True:
            try:
                mb = pagetree_obj['/MediaBox']
                break
            except KeyError:
                try:
                    pagetree_obj = pagetree_obj['/Parent']
                except KeyError:  # pragma: nocover
                    raise PdfReadError(
                        f'Page {page_ix} does not have a /MediaBox')

        stream_dict = {
            pdf_name('/BBox'): mb,
            pdf_name('/Resources'): self.import_object(resources),
            pdf_name('/Type'): pdf_name('/XObject'),
            pdf_name('/Subtype'): pdf_name('/Form')
        }
        command_stream = page_obj['/Contents']
        # if the page /Contents is an array, retrieve the content stream
        # with the appropriate index
        if isinstance(command_stream, generic.ArrayObject):
            command_stream = command_stream[content_stream].get_object()
        assert isinstance(command_stream, generic.StreamObject)
        filters = None
        if inherit_filters:
            try:
                # try to inherit filters from the original command stream
                filters = command_stream['/Filter']
            except KeyError:
                pass

        if filters is not None:
            stream_dict[pdf_name('/Filter')] = self.import_object(filters)
            result = generic.StreamObject(
                stream_dict, encoded_data=command_stream.encoded_data)
        else:
            result = generic.StreamObject(stream_dict,
                                          stream_data=command_stream.data)

        return self.add_object(result)