# -*- encoding: utf-8; frozen_string_literal: true -*- # #-- # This file is part of HexaPDF. # # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby # Copyright (C) 2014-2024 Thomas Leitner # # HexaPDF is free software: you can redistribute it and/or modify it # under the terms of the GNU Affero General Public License version 3 as # published by the Free Software Foundation with the addition of the # following permission added to Section 15 as permitted in Section 7(a): # FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY # THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON # INFRINGEMENT OF THIRD PARTY RIGHTS. # # HexaPDF is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with HexaPDF. If not, see . # # The interactive user interfaces in modified source and object code # versions of HexaPDF must display Appropriate Legal Notices, as required # under Section 5 of the GNU Affero General Public License version 3. # # In accordance with Section 7(b) of the GNU Affero General Public # License, a covered work must retain the producer line in every PDF that # is created or manipulated using HexaPDF. # # If the GNU Affero General Public License doesn't fit your need, # commercial licenses are available at . #++ require 'openssl' require 'hexapdf/error' require 'hexapdf/version' require 'stringio' module HexaPDF module DigitalSignature module Signing # This is the default signing handler which provides the ability to sign a document with the # adbe.pkcs7.detached or ETSI.CAdES.detached algorithms. It is registered under the :default # name. # # == Usage # # The signing handler is used by default by all methods that need a signing handler. Therefore # it is usually only necessary to provide the actual attribute values. # # *Note*: Currently only RSA is supported, DSA and ECDSA are not. See the examples below for # how to handle them using external signing. # # # == CMS and PAdES Signatures # # The handler supports the older standard of CMS signatures as well as the newer PAdES # signatures specified in PDF 2.0. By default, CMS signatures are created but this can be # changed by setting #signature_type to :pades. # # When creating PAdES signatures the following two PAdES baseline signatures are supported: # B-B and B-T. The difference between those two is that a timestamp handler was defined for # B-T compatibility. # # # == Signing Modes - Internal, External, External/Asynchronous # # This handler provides two ways to create the CMS signed-data structure required by # Signatures#add: # # * By providing the signing certificate together with the signing key and the certificate # chain, HexaPDF itself does the signing *internally*. It is the preferred way if all the # needed information is available. # # Assign the respective data to the #certificate, #key and #certificate_chain attributes. # # * By using an *external signing mechanism*, a callable object assigned to #external_signing. # Here the actual signing happens "outside" of HexaPDF, for example, in custom code or even # asynchronously. This is needed in case the signing key is not directly available but only # an interface to it (e.g. when dealing with a HSM). # # Depending on whether #certificate is set the signing happens differently: # # * If #certificate is not set, the callable object is used instead of #sign, so it needs to # accept the same arguments as #sign and needs to return a complete, DER-serialized CMS # signed data object. # # * If #certificate is set, the CMS signed data object is created by HexaPDF. The # callable #external_signing object is called with the used digest algorithm and the # already digested data which needs to be signed (but *not* digested) and the signature # returned. # # If the signing process needs to be *asynchronous*, make sure to set the #signature_size # appropriately, return an empty string during signing and later use # Signatures.embed_signature to embed the actual signature. # # # == Optional Data # # Besides the required data, some optional attributes can also be specified: # # * Reason, location and contact information # * Making the signature a certification signature by applying the DocMDP transform method and # a DoCMDP permission # # # == Examples # # # Signing using certificate + key # document.sign("output.pdf", certificate: my_cert, key: my_key, # certificate_chain: my_chain) # # # Signing using an external mechanism without certificate set # signing_proc = lambda do |io, byte_range| # io.pos = byte_range[0] # data = io.read(byte_range[1]) # io.pos = byte_range[2] # data << io.read(byte_range[3]) # signing_service.pkcs7_sign(data).to_der # end # document.sign("output.pdf", signature_size: 10_000, external_signing: signing_proc) # # # Signing using external mechanism with certificate set # signing_proc = lambda do |digest_method, hash| # signing_service.sign_raw(digest_method, hash) # end # document.sign("output.pdf", certificate: my_cert, certificate_chain: my_chain, # external_signing: signing_proc) # # # Signing with DSA or ECDSA certificate/keys # signing_proc = lambda do |io, byte_range| # io.pos = byte_range[0] # data = io.read(byte_range[1]) # io.pos = byte_range[2] # data << io.read(byte_range[3]) # OpenSSL::PKCS7.sign(certificate, key, data, certificate_chain, # OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY).to_der # end # document.sign("output.pdf", signature_size: 10_000, external_signing: signing_proc) # # # == Implementing a Signing Handler # # This class also serves as an example on how to create a custom handler: The public methods # #signature_size, #finalize_objects and #sign are used by the digital signature algorithm. # See their descriptions for details. # # Once a custom signing handler has been created, it can be registered under the # 'signature.signing_handler' configuration option for easy use. It has to take keyword # arguments in its initialize method to be compatible with the Signatures#handler method. class DefaultHandler # The certificate with which to sign the PDF. # # If the certificate is provided, HexaPDF creates the signature object. Otherwise the # #external_signing callable object has to create it. # # See the class documentation section "Signing Modes" on how #certificate, #key and # #external_signing play together. attr_accessor :certificate # The private key for the #certificate. # # If the key is provided, HexaPDF does the signing. Otherwise the #external_signing callable # object has to sign the data. # # See the class documentation section "Signing Modes" on how #certificate, #key and # #external_signing play together. attr_accessor :key # The certificate chain that should be embedded in the PDF; usually contains all # certificates up to the root certificate. attr_accessor :certificate_chain # The digest algorithm that should be used when creating the signature. # # See SignedDataCreator#digest_algorithm for the default value (if nothing is set) and for # the allowed values. attr_accessor :digest_algorithm # The timestamp handler that should be used for timestamping the signature. # # If this attribute is set, a timestamp token is embedded into the CMS object. attr_accessor :timestamp_handler # A callable object for custom signing mechanisms. # # The callable object has two different uses depending on whether #certificate is set: # # * If #certificate is not set, it fulfills the same role as the #sign method and needs to # conform to that interface. # # * If #certificate is set and #key is not, it is just used for signing. Here it needs to # accept the used digest algorithm and the already digested data as arguments and return # the signature. # # Also dee the class documentation section "Signing Modes" on how #certificate, #key and # #external_signing play together. attr_accessor :external_signing # The reason for signing. If used, will be set on the signature dictionary. attr_accessor :reason # The signing location. If used, will be set on the signature dictionary. attr_accessor :location # The contact information. If used, will be set on the signature dictionary. attr_accessor :contact_info # The size of the serialized signature that should be reserved. # # If this attribute is not set, an empty string will be signed using #sign to determine the # signature size. # # The size needs to be at least as big as the final signature, otherwise signing results in # an error. attr_writer :signature_size # The type of signature to be written (i.e. the value of the /SubFilter key). # # The value can either be :cms (the default; uses a detached CMS signature) or :pades # (uses an ETSI CAdES compatible signature). attr_accessor :signature_type # The DocMDP permissions that should be set on the document. # # See #doc_mdp_permissions= attr_reader :doc_mdp_permissions # Creates a new DefaultHandler instance with the given attributes. def initialize(**arguments) @signature_size = nil @signature_type = :cms arguments.each {|name, value| send("#{name}=", value) } end # Sets the DocMDP permissions that should be applied to the document. # # Valid values for +permissions+ are: # # +nil+:: # Don't set any DocMDP permissions (default). # # +:no_changes+ or 1:: # No changes whatsoever are allowed. # # +:form_filling+ or 2:: # Only filling in forms and signing are allowed. # # +:form_filling_and_annotations+ or 3:: # Only filling in forms, signing and annotation creation/deletion/modification are # allowed. def doc_mdp_permissions=(permissions) case permissions when :no_changes, 1 then @doc_mdp_permissions = 1 when :form_filling, 2 then @doc_mdp_permissions = 2 when :form_filling_and_annotations, 3 then @doc_mdp_permissions = 3 when nil then @doc_mdp_permissions = nil else raise ArgumentError, "Invalid permissions value '#{permissions.inspect}'" end end # Returns the size of the serialized signature that should be reserved. # # If a custom size is set using #signature_size=, it used. Otherwise the size is determined # by using #sign to sign an empty string. def signature_size @signature_size || sign(StringIO.new, [0, 0, 0, 0]).size end # Finalizes the signature field as well as the signature dictionary before writing. def finalize_objects(_signature_field, signature) signature[:Filter] = :'Adobe.PPKLite' signature[:SubFilter] = (signature_type == :pades ? :'ETSI.CAdES.detached' : :'adbe.pkcs7.detached') signature[:M] = Time.now signature[:Reason] = reason if reason signature[:Location] = location if location signature[:ContactInfo] = contact_info if contact_info signature[:Prop_Build] = {App: {Name: :HexaPDF, REx: HexaPDF::VERSION}} if doc_mdp_permissions doc = signature.document if doc.signatures.count > 1 raise HexaPDF::Error, "Can set DocMDP access permissions only on first signature" end params = doc.add({Type: :TransformParams, V: :'1.2', P: doc_mdp_permissions}) sigref = doc.add({Type: :SigRef, TransformMethod: :DocMDP, DigestMethod: :SHA256, TransformParams: params}) signature[:Reference] = [sigref] (doc.catalog[:Perms] ||= {})[:DocMDP] = signature end end # Returns the DER serialized CMS signed data object containing the signature for the given # IO byte ranges. # # The +byte_range+ argument is an array containing four numbers [offset1, length1, offset2, # length2]. The offset numbers are byte positions in the +io+ argument and the to-be-signed # data can be determined by reading length bytes at the offsets. def sign(io, byte_range) if certificate io.pos = byte_range[0] data = io.read(byte_range[1]) io.pos = byte_range[2] data << io.read(byte_range[3]) SignedDataCreator.create(data, type: signature_type, certificate: certificate, key: key, digest_algorithm: digest_algorithm, timestamp_handler: timestamp_handler, certificates: certificate_chain, &external_signing).to_der else external_signing.call(io, byte_range) end end end end end end