dealfonso / sapp

Simple and Agnostic PDF Document Parser in PHP - sign PDF docs using PHP
GNU Lesser General Public License v3.0
110 stars 29 forks source link

Client signing [enhancement] #35

Closed malamalca closed 1 year ago

malamalca commented 1 year ago

Hi,

it would be great to have an ability to implement signing of pdfs on a client side.

  1. sapp should provide a hash of the pdf document to be signed [SAPP -> getPDFHash()]
  2. user signs the hash in browser via external code (javascript/php)
  3. the developer has a way to pass signature and algorithm to SAPP which generates ASN1/pkcs7 signature block manually and incorporates it in a pdf file

Would it be possible with SAPP?

angeljqv commented 1 year ago

Why dont just use Vanilla JS for all the process avoiding SAPP?

dealfonso commented 1 year ago

Hi,

@malamalca do you have a library to get an arbitrary pkcs7 signature using javascript in a browser? I find that ASN1/pkcs7 is also PHP in the server.

@angeljqv do you have an example of how to sign PDF documents using javascript in a browser? I'd be very grateful if you could show us an example (I have been searching for it for a long time, and I reached to the conclusion that I need an external application; e.g. a java applet).

angeljqv commented 1 year ago

With forge

Look at this signing example: xades signing

Also: signpdf

PAdES compliant signatures To produce PAdES compliant signatures, the ETSI Signature Dictionary SubFilter value must be ETSI.CAdES.detached instead of the standard Adobe value. That's where the Signer kicks in. Given a PDF and a P12 certificate a signature is generated in detached mode and is replaced in the placeholder. This is best demonstrated in the tests.

Dependencies node-forge is used for working with signatures. PDFKit is used in the tests for generating a PDF with a signature placeholder.

pdfsign.js

malamalca commented 1 year ago

@dealfonso I intended to generate ASN1/pkcs7 packet with php, not with javascript. Javascript only generates signature of a hash and sends it back to server. Began the implementation on library mentioned above, did not finish it yet.

@angeljqv The signature is not a problem. Signing pdf with that signature on client side is.

dealfonso commented 1 year ago

my problem is signing anything in the browser because I could not find a way to access to the certificate store, except from using a java applet or an external application.

angeljqv commented 1 year ago

With forge

@dealfonso comments? thoughts?

malamalca commented 1 year ago

I am signing XML hashes on client side with hwcrypto.js

parallels999 commented 1 year ago

@malamalca just extend SAPP and override the functions to do what you want Example https://github.com/dealfonso/sapp/issues/36#issuecomment-1296953323

dealfonso commented 1 year ago

With forge

@dealfonso comments? thoughts?

Hi,

I read about forge some time ago, but the problem is reading the private key from the browser.

malamalca commented 1 year ago

You do not read or extract a private key. That is the point. The whole signing process is done on client (can happen even outside the operating system). You provide the hash, client answers with signature.

dealfonso commented 1 year ago

Sorry, but I cannot 100% understand what you are trying to do :(

malamalca commented 1 year ago

We need two things:

  1. get raw pdf with signature that has empty contents
  2. pass signature contents directly into signature object

The client signing process is following (parameters for to_pdf_file_b() are shown in code below):

  1. get pdf with empty signature contents by calling $data = $pdf->to_pdf_file_b(false, true);
  2. calculate digest/hash of pdf $hash = hash('sha256', $data);
  3. sign hash on client's browser; browser returns signed hash and public cert
  4. use signed digest and public cert and generate pkcs7 signature
  5. pass signature into $signedPdf = to_pdf_file_b(false, false, $pkcsSignature)

Below is a simplified version of what should be implemented. Of course not in this form, it is just meta code to explain what I am trying to do. There are some certificate checks that should be changed (actually no pfx/p12 certs are needed in this case).

public function to_pdf_file_b($rebuild = false, $return_raw = false, $signature_contents= null) : Buffer {
....
if ($_signature !== null) {
            // In case that the document is signed, calculate the signature

            $_signature->set_sizes($_doc_to_xref->size(), $_doc_from_xref->size());
            $_signature["Contents"] = new PDFValueSimple("");
            $_signable_document = new Buffer($_doc_to_xref->get_raw() . $_signature->to_pdf_entry() . $_doc_from_xref->get_raw());

            // new
            if ($return_raw) {
                        return $_signable_document->get_raw();
            }

            // new
            if (empty($signature_contents)) {
                        // We need to write the content to a temporary folder to use the pkcs7 signature mechanism
                        $temp_filename = tempnam(__TMP_FOLDER, 'pdfsign');
                        $temp_file = fopen($temp_filename, 'wb');
                        fwrite($temp_file, $_signable_document->get_raw());
                        fclose($temp_file);

                        // Calculate the signature and remove the temporary file
                        $certificate = $_signature->get_certificate();
                        $signature_contents = PDFUtilFnc::calculate_pkcs7_signature($temp_filename, $certificate['cert'], $certificate['pkey'], __TMP_FOLDER);
                        unlink($temp_filename);
            }

            // Then restore the contents field
            $_signature["Contents"] = new PDFValueHexString($signature_contents);

            // Add this object to the content previous to this document xref
            $_doc_to_xref->data($_signature->to_pdf_entry());
        }

        // Reset the state to make signature objects not to mess with the user's objects
        $this->pop_state();
        return new Buffer($_doc_to_xref->get_raw() . $_doc_from_xref->get_raw());
    }
...
dealfonso commented 1 year ago

Hi @malamalca

I have created a new branch signature_placeholder. In that version, functions to get the pdf file (i.e. to_pdf_file_s, to_pdf_file_s, and to_pdf_file_b) accept a parameter calculate_signature_hash that enables to calculate or not the hash using the certificates.

If setting calculate_signature_hash to false, the signature hash will contain a placeholder (i.e. 0000000...000). Then you can calculate the hash using an external application and substitute it with your calculated value.

The reasoning is that the dates would change if you call the SAPP functions at different times, and therefore the document would change (thus changing the hash). I think that doing this differently will not add value but will add complexity by having to deal with the modification date and so on.

Could you please check if it is good for you?

malamalca commented 1 year ago

The start is ok, it could be implemented as such. Manually replacing signature contents with PHP would work too.

But there is a major problem - the $_signature will not be generated because there are no certificates passed to SAPP when signing on client.

https://github.com/dealfonso/sapp/blob/1a2d70d6648c2ed5ca595011493d8e7ca116aa6a/src/PDFDoc.php#L692 https://github.com/dealfonso/sapp/blob/1a2d70d6648c2ed5ca595011493d8e7ca116aa6a/src/PDFDoc.php#L398

dealfonso commented 1 year ago

Hi,

you are right. I have updated the branch to reorganize the code so that now it is a little more didactic and it is easier to understand what is the structure of a PDF document.

And now, I have also included a new function to_pdf_with_signature_placeholder that prepares the structure with a placeholder, to store the certificate. I have also included two new examples: pdfpreparesign.php and pdfpreparesigni.php to demonstrate how to use that function.

Could you please verify that the branch is working as expected?

dealfonso commented 1 year ago

hi @malamalca, did this worked for you?