def compare_output(writer: BasePdfFileWriter, expected_output_path): with tempfile.TemporaryDirectory() as working_dir: output_path = os.path.join(working_dir, 'output.pdf') with open(output_path, 'wb') as outf: writer.write(outf) expected_png = _render_pdf( expected_output_path, os.path.join(working_dir, 'expected') ) actual_png = _render_pdf( output_path, os.path.join(working_dir, 'actual') ) result = subprocess.run( # use the Absolute Error metric, since it's a single number # and hence very easy to process [ compare_path, '-metric', 'ae', expected_png, actual_png, os.path.join(working_dir, 'diff.png') ], capture_output=True ) # TODO maintain a directory of failed test outputs? if result.stderr != b'0': raise RuntimeError( f"Output compare test failed --- absolute error: " f"{result.stderr.decode('utf8')}" )
def register_widget_annotation(self, writer: BasePdfFileWriter, sig_field_ref): annot_dict = self.annot_dict if annot_dict is not self: annot_ref = writer.add_object(annot_dict) self['/Kids'] = generic.ArrayObject([annot_ref]) else: annot_ref = sig_field_ref writer.register_annotation(self.page_ref, annot_ref)
def embed(self, writer: BasePdfFileWriter, obj_stream=None): fd = self._font_descriptor self[pdf_name('/FontDescriptor')] = fd_ref = writer.add_object( fd, obj_stream=obj_stream ) font_stream_ref = self.set_font_file(writer) return fd_ref, font_stream_ref
def _insert_or_get_field_at(writer: BasePdfFileWriter, fields, path, parent_ref=None, modified=False, field_obj=None): current_partial, tail = path[0], path[1:] for field_ref in fields: assert isinstance(field_ref, generic.IndirectObject) field = field_ref.get_object() if field.get('/T', None) == current_partial: break else: # have to insert a new element into the fields array if field_obj is not None and not tail: field = field_obj else: # create a generic field field = generic.DictionaryObject() field['/T'] = pdf_string(current_partial) if parent_ref is not None: field['/Parent'] = parent_ref field_ref = writer.add_object(field) fields.append(field_ref) writer.update_container(fields) modified = True if not tail: return modified, field_ref # check for /Kids, and create it if necessary try: kids = field['/Kids'] except KeyError: kids = field['/Kids'] = generic.ArrayObject() writer.update_container(field) modified = True # recurse in to /Kids array return _insert_or_get_field_at(writer, kids, tail, parent_ref=field_ref, modified=modified, field_obj=field_obj)
def __init__(self, writer: BasePdfFileWriter, base_postscript_name: str, embedded_subset: bool, obj_stream=None): fsc = writer.get_subset_collection(base_postscript_name) if embedded_subset: self.subset_prefix = prefix = fsc.add_subset() else: self.subset_prefix = prefix = None fsc.subsets[prefix] = self self.writer = writer self.obj_stream = obj_stream
def set_font_file(self, writer: BasePdfFileWriter): stream_buf = BytesIO() self.tt_font.save(stream_buf) stream_buf.seek(0) font_stream = generic.StreamObject(stream_data=stream_buf.read()) font_stream.compress() font_stream_ref = writer.add_object(font_stream) self._font_descriptor[pdf_name('/FontFile2')] = font_stream_ref return font_stream_ref
def __init__(self, writer: BasePdfFileWriter, font_handle, font_size, features=None, ot_language_tag=None, ot_script_tag=None, writing_direction=None, bcp47_lang_code=None, obj_stream=None): # harfbuzz expects bytes font_handle.seek(0) font_bytes = font_handle.read() font_handle.seek(0) face = hb.Face(font_bytes) self.font_size = font_size self.hb_font = hb.Font(face) self.tt = tt = ttLib.TTFont(font_handle) base_ps_name = _read_ps_name(tt) super().__init__( writer, base_ps_name, embedded_subset=True, obj_stream=obj_stream ) self._font_ref = writer.allocate_placeholder() try: cff = self.tt['CFF '] self.cff_charset = cff.cff[0].charset # CFF font programs are embedded differently # (in a more Adobe-native way) self.cidfont_obj = cidfont_obj = CIDFontType0( tt, base_ps_name, self.subset_prefix ) self.use_raw_gids = cidfont_obj.use_raw_gids except KeyError: self.cff_charset = None self.use_raw_gids = True self.cidfont_obj = CIDFontType2( tt, base_ps_name, self.subset_prefix ) self.features = features # the 'head' table is mandatory self.units_per_em = tt['head'].unitsPerEm self._glyphs = {} self._glyph_set = tt.getGlyphSet(preferCFF=True) self._cid_to_unicode = {} self.__reverse_cmap = None self.ot_language_tag = _check_ot_tag(ot_language_tag) self.ot_script_tag = _check_ot_tag(ot_script_tag) if writing_direction is not None and \ writing_direction not in ('ltr', 'rtl', 'ttb', 'btt'): raise ValueError( "Writing direction must be one of 'ltr', 'rtl', 'ttb' or 'btt'." ) self.writing_direction = writing_direction self.bcp47_lang_code = bcp47_lang_code self._subset_created = self._write_prepared = False
def set_font_file(self, writer: BasePdfFileWriter): stream_buf = BytesIO() # write the CFF table to the stream self.cff.compile(stream_buf, self.tt_font) stream_buf.seek(0) font_stream = generic.StreamObject({ # this is a Type0 CFF font program (see Table 126 in ISO 32000) pdf_name('/Subtype'): pdf_name('/CIDFontType0C'), }, stream_data=stream_buf.read()) font_stream.compress() font_stream_ref = writer.add_object(font_stream) self._font_descriptor[pdf_name('/FontFile3')] = font_stream_ref return font_stream_ref
def set_font_file(self, writer: BasePdfFileWriter): stream_buf = BytesIO() self.tt_font.save(stream_buf) stream_buf.seek(0) font_stream = generic.StreamObject({ # this is a Type2 TTF font program pdf_name('/Subtype'): pdf_name('/CIDFontType2'), }, stream_data=stream_buf.read()) font_stream.compress() font_stream_ref = writer.add_object(font_stream) self._font_descriptor[pdf_name('/FontFile2')] = font_stream_ref return font_stream_ref
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
def append_signature_field(pdf_out: BasePdfFileWriter, sig_field_spec: SigFieldSpec): """ Append signature fields to a PDF file. :param pdf_out: Incremental writer to house the objects. :param sig_field_spec: A :class:`.SigFieldSpec` object describing the signature field to add. """ root = pdf_out.root page_ref = pdf_out.find_page_for_modification(sig_field_spec.on_page)[0] # use default appearance field_created, sig_field_ref = _prepare_sig_field( sig_field_spec.sig_field_name, root, update_writer=pdf_out, existing_fields_only=False, lock_sig_flags=False, box=sig_field_spec.box, include_on_page=page_ref, combine_annotation=sig_field_spec.combine_annotation ) if not field_created: raise PdfWriteError( 'Signature field with name %s already exists.' % sig_field_spec.sig_field_name ) sig_field = sig_field_ref.get_object() if sig_field_spec.seed_value_dict is not None: # /SV must be an indirect reference as per the spec sv_ref = pdf_out.add_object( sig_field_spec.seed_value_dict.as_pdf_object() ) sig_field[pdf_name('/SV')] = sv_ref lock = sig_field_spec.format_lock_dictionary() if lock is not None: sig_field[pdf_name('/Lock')] = pdf_out.add_object(lock)
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
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)
def supply_dss_in_writer(cls, pdf_out: BasePdfFileWriter, sig_contents, *, certs=None, ocsps=None, crls=None, paths=None, validation_context=None, embed_roots: bool = True) \ -> 'DocumentSecurityStore': """ Add or update a DSS, and optionally associate the new information with a VRI entry tied to a signature object. You can either specify the CMS objects to include directly, or pass them in as output from `pyhanko_certvalidator`. :param pdf_out: PDF writer to write to. :param sig_contents: Contents of the new signature (used to compute the VRI hash), as as a hexadecimal string, including any padding. If ``None``, the information will not be added to any VRI dictionary. :param certs: Certificates to include in the VRI entry. :param ocsps: OCSP responses to include in the VRI entry. :param crls: CRLs to include in the VRI entry. :param paths: Validation paths that have been established, and need to be added to the DSS. :param validation_context: Validation context from which to draw OCSP responses and CRLs. :param embed_roots: .. versionadded:: 0.9.0 Option that controls whether the root certificate of each validation path should be embedded into the DSS. The default is ``True``. .. note:: Trust roots are configured by the validator, so embedding them typically does nothing in a typical validation process. Therefore they can be safely omitted in most cases. Nonetheless, embedding the roots can be useful for documentation purposes. .. warning:: This only applies to paths, not the ``certs`` parameter. :return: a :class:`DocumentSecurityStore` object containing both the new and existing contents of the DSS (if any). """ try: dss = cls.read_dss(pdf_out) created = False except ValidationInfoReadingError: created = True dss = cls(writer=pdf_out) if sig_contents is not None: identifier = \ DocumentSecurityStore.sig_content_identifier(sig_contents) else: identifier = None def _certs(): yield from certs or () path: ValidationPath for path in (paths or ()): path_parts = iter(path) if not embed_roots: # skip the first cert (i.e. the root) next(path_parts) yield from path_parts def _ocsps(): yield from ocsps or () if validation_context is not None: yield from validation_context.ocsps def _crls(): yield from crls or () if validation_context is not None: yield from validation_context.crls dss.register_vri( identifier, certs=_certs(), ocsps=_ocsps(), crls=_crls() ) dss_dict = dss.as_pdf_object() # if we're updating the DSS, this is all we need to do. # if we're adding a fresh DSS, we need to register it. if created: dss_ref = pdf_out.add_object(dss_dict) pdf_out.root[pdf_name('/DSS')] = dss_ref pdf_out.update_root() return dss
def fill(self, writer: BasePdfFileWriter, md_algorithm, in_place=False, output=None, chunk_size=misc.DEFAULT_CHUNK_SIZE): """ Generator coroutine that handles the document hash computation and the actual filling of the placeholder data. .. danger:: This is internal API; you should use use :class:`.PdfSigner` wherever possible. If you *really* need fine-grained control, use :class:`~pyhanko.sign.signers.cms_embedder.PdfCMSEmbedder` instead. """ if in_place: if not isinstance(writer, IncrementalPdfFileWriter): raise TypeError( "in_place is only meaningful for incremental writers." ) # pragma: nocover output = writer.prev.stream writer.write_in_place() else: output = misc.prepare_rw_output_stream(output) writer.write(output) # retcon time: write the proper values of the /ByteRange entry # in the signature object eof = output.tell() sig_start, sig_end = self.contents.offsets self.byte_range.fill_offsets(output, sig_start, sig_end, eof) # compute the digests md_spec = get_pyca_cryptography_hash(md_algorithm) md = hashes.Hash(md_spec) # attempt to get a memoryview for automatic buffering output_buffer = None if isinstance(output, BytesIO): output_buffer = output.getbuffer() else: try: output_buffer = memoryview(output) except (TypeError, IOError): pass if output_buffer is not None: # these are memoryviews, so slices should not copy stuff around # (also, the interface files for pyca/cryptography don't specify # that memoryviews are allowed, but they are) # noinspection PyTypeChecker md.update(output_buffer[:sig_start]) # noinspection PyTypeChecker md.update(output_buffer[sig_end:eof]) output_buffer.release() else: temp_buffer = bytearray(chunk_size) output.seek(0) misc.chunked_digest(temp_buffer, output, md, max_read=sig_start) output.seek(sig_end) misc.chunked_digest(temp_buffer, output, md, max_read=eof - sig_end) digest_value = md.finalize() prepared_br_digest = PreparedByteRangeDigest( document_digest=digest_value, md_algorithm=md_algorithm, reserved_region_start=sig_start, reserved_region_end=sig_end) cms_data = yield prepared_br_digest, output yield prepared_br_digest.fill_with_cms(output, cms_data)
def embed_subset(self, writer: BasePdfFileWriter, obj_stream=None): """ Embed a subset of this glyph accumulator's font into the provided PDF writer. Said subset will include all glyphs necessary to render the strings provided to the accumulator via :meth:`feed_string`. .. danger:: Due to the way ``fontTools`` handles subsetting, this is a destructive operation. The in-memory representation of the original font will be overwritten by the generated subset. :param writer: A PDF writer. :param obj_stream: If provided, write all relevant objects to the provided `obj_stream`. If ``None`` (the default), they will simply be written to the file as top-level objects. :return: A reference to the embedded ``/Font`` object. """ if self._font_ref is not None: return self._font_ref self._extract_subset() cidfont_obj = CIDFontType0(self.tt) # TODO keep track of used subset prefixes in the writer! cff_topdict = self.tt['CFF '].cff[0] name = cidfont_obj.name cff_topdict.rawDict['FullName'] = '%s+%s' % (generate_subset_prefix(), name) cidfont_obj.embed(writer, obj_stream=obj_stream) cidfont_ref = writer.add_object(cidfont_obj) to_unicode = self._format_tounicode_cmap(*cidfont_obj.ros) type0 = generic.DictionaryObject({ pdf_name('/Type'): pdf_name('/Font'), pdf_name('/Subtype'): pdf_name('/Type0'), pdf_name('/DescendantFonts'): generic.ArrayObject([cidfont_ref]), # take the Identity-H encoding to inherit from the /Encoding # entry specified in our CIDSystemInfo dict pdf_name('/Encoding'): pdf_name('/Identity-H'), pdf_name('/BaseFont'): pdf_name('/%s-Identity-H' % cidfont_obj.name), pdf_name('/ToUnicode'): writer.add_object(to_unicode) }) to_unicode.compress() # compute widths entry # (easiest to do here, since it seems we need the original CIDs) by_cid = iter(sorted(self._glyphs.values(), key=lambda t: t[0])) def _widths(): current_chunk = [] prev_cid = None (first_cid, _, _), itr = peek(by_cid) for cid, _, g in itr: if current_chunk and cid != prev_cid + 1: yield generic.NumberObject(first_cid) yield generic.ArrayObject(current_chunk) current_chunk = [] first_cid = cid current_chunk.append(generic.NumberObject(g.width)) prev_cid = cid if current_chunk: yield generic.NumberObject(first_cid) yield generic.ArrayObject(current_chunk) cidfont_obj[pdf_name('/W')] = generic.ArrayObject(list(_widths())) self._font_ref = ref = writer.add_object(type0, obj_stream=obj_stream) return ref