gettalong / hexapdf

Versatile PDF creation and manipulation for Ruby
https://hexapdf.gettalong.org
Other
1.21k stars 69 forks source link

Extract which bytes should be included in the hash calculation (Signature) #313

Closed TiagoPaza closed 1 month ago

TiagoPaza commented 1 month ago

Hello guys,

I'm looking to implement the enveloped signature in a project and I'm creating a proof of concept.

The integration documentation requests that:

  1. Prepare the signature document
  2. Calculate which bytes (byte-ranges) of the file prepared in step 1 should be included in the hash calculation. Unlike detached signatures, the hash calculation for enveloped PDF signatures is not the SHA256 hash of the original (full) document. It is a part of the document prepared in step 1.

I've already covered the other steps, but I'm confused about how to perform this calculation using HexaPDF. I found some examples in JAVA using the iText library

Below is my code following the examples in the documentation:

def prepare_file
    pdf_doc = HexaPDF::Document.new

    page = pdf_doc.pages.add
    page_box = page.box

    frame = Frame.new(page_box.left + 20, page_box.bottom + 20,
                      page_box.width - 40, page_box.height - 40)

    boxes = []

    boxes << Box.create(width: 50, height: 50, margin: 20,
                        position: :float, align: :right,
                        background_color: "hp-blue-light2",
                        border: {width: 1, color: "hp-blue-dark"})
    boxes << pdf_doc.layout.lorem_ipsum_box(count: 3, position: :flow, text_align: :justify)

    i = 0
    frame_filled = false
    until frame_filled
      box = boxes[i]
      drawn = false
      until drawn || frame_filled
        result = frame.fit(box)
        if result.success?
          frame.draw(page.canvas, result)
          drawn = true
        else
          frame_filled = !frame.find_next_region
        end
      end
      i = (i + 1) % boxes.length
    end

    data = nil # Used for storing the to-be-signed data
    signing_mechanism = lambda do |io, byte_range|
      # Store the to-be-signed data in the local variable data
      io.pos = byte_range[0]
      data = io.read(byte_range[1])
      io.pos = byte_range[2]
      data << io.read(byte_range[3])
      ""
    end

    sig_field = pdf_doc.acro_form(create: true).create_signature_field('signature')

    widget = sig_field.create_widget(pdf_doc.pages[0], Rect: [20, 20, 120, 120])
    widget.create_appearance.canvas.
      stroke_color("red").rectangle(1, 1, 99, 99).stroke.
      font("Helvetica", size: 10).
      text("Certified by signer", at: [10, 10])

    pdf_doc.sign("signed.pdf", external_signing: signing_mechanism, doc_mdp_permissions: :form_filling, signature_size: 8_192 )

    render json: { message: "PDF prepared successfully"}, status: :ok
  end

  def sign_file
    p7s_file = params[:p7s]

    if p7s_file.respond_to?(:tempfile)
      pkcs = OpenSSL::PKCS7.new(File.read(p7s_file.tempfile))

      # Embed the signature
      HexaPDF::DigitalSignature::Signing.embed_signature(File.open("signed.pdf", 'rb+'), pkcs.to_der)

    end

    render json: { message: "PDF signed successfully" }, status: :ok
  end
gettalong commented 1 month ago

You already have the code in there:

    data = nil # Used for storing the to-be-signed data
    signing_mechanism = lambda do |io, byte_range|
      # Store the to-be-signed data in the local variable data
      io.pos = byte_range[0]
      data = io.read(byte_range[1])
      io.pos = byte_range[2]
      data << io.read(byte_range[3])
      ""
    end

This part collects the bytes to be signed in the data variable. So either calculate the hash over data yourself or send it to the other part of your application that creates the PKCS#7 file.