Stefan Santesson created PDFBOX-1613:
----------------------------------------

             Summary: The ability to inject the time/random component into the 
COSWriter process to write a PDF document allows some advanced signature 
creation scenarios where the signature is generated on a separate server that 
does not hold the full PDF document.
                 Key: PDFBOX-1613
                 URL: https://issues.apache.org/jira/browse/PDFBOX-1613
             Project: PDFBox
          Issue Type: Improvement
          Components: Writing
    Affects Versions: 1.8.1
         Environment: Any
            Reporter: Stefan Santesson
            Priority: Minor
             Fix For: 1.8.1


I have developed a prototype server based signing service for the Swedish 
National eID infrastructure.
I'll skip the details, but I recently switched to PDFBox for the PDF signing 
process and it works great. However, I had to modify the COSWriter class to get 
this working.

I'm writing to check whether you would consider adding the functionality I need 
to future version of PDFBox.

The problem is the the signature service is just producing the signature, it is 
not trusted to handle the PDF document.
The government service having the PDF document signed is using PDFBox in a 2 
step process.

1) To produce the SignedAttributes DER Object of the CMS signature to be 
created. This is the part that is hashed and signed by the signature service.
2) After receiving the signature and signature certs from the signature 
service, completing the PDF signature by delivering the complete PKCS#7 object 
to PDFBox using the externally generated signature value and certs.

There are probably a more pure way to handle this, but Since PDFBox allows me 
to create a signature interface that produces the SignedData. I found it to be 
the easiest way to run the signature process 2 times.
1st pass using dummy key and dummy certs. This only to obtain the 
SignedAttributes.
2nd pass by delivering a SignedData object that include the Signature value and 
certs produced by the signature service.

Now in order to do this, I have to control the random seed added by the 
COSWriter, or else the signature created by the signature service will not 
match the hash in the SignedAttributes produced in the second pass.

My modification is provided below.
I simply provided an extra input parameter to the write function where I can 
provide the long seed

I then added a backwards compatible write function where the long seed is 
current time.

By providing the same seed to pass 1 and pass 2, I can get the externally 
created signature to match the SignedAttributes produced in the first pass.
The write function below is identical to the original COSWriter function except 
that it takes the idTime value from the function input parameter instead of 
getting it from System.currentTimeMillis().

Modified functions of COSWriter:

  /**
   * This will write the pdf document.
   *
   * @param doc The document to write.
   *
   * @throws COSVisitorException If an error occurs while generating the data.
   */
  public void write(PDDocument doc) throws COSVisitorException {
      write(doc, System.currentTimeMillis());
  }

  /**
   * This will write the pdf document.
   *
   * @param doc The document to write.
   * @param idTime The time seed used to generate the id
   *
   * @throws COSVisitorException If an error occurs while generating the data.
   */
  public void write(PDDocument doc, long idTime) throws COSVisitorException {
      document = doc;
      if (incrementalUpdate) {
          prepareIncrement(doc);
      }

      // if the document says we should remove encryption, then we shouldn't 
encrypt
      if (doc.isAllSecurityToBeRemoved()) {
          this.willEncrypt = false;
          // also need to get rid of the "Encrypt" in the trailer so readers
          // don't try to decrypt a document which is not encrypted
          COSDocument cosDoc = doc.getDocument();
          COSDictionary trailer = cosDoc.getTrailer();
          trailer.removeItem(COSName.ENCRYPT);
      } else {
          SecurityHandler securityHandler = document.getSecurityHandler();
          if (securityHandler != null) {
              try {
                  securityHandler.prepareDocumentForEncryption(document);
                  this.willEncrypt = true;
              } catch (IOException e) {
                  throw new COSVisitorException(e);
              } catch (CryptographyException e) {
                  throw new COSVisitorException(e);
              }
          } else {
              this.willEncrypt = false;
          }
      }

      COSDocument cosDoc = document.getDocument();
      COSDictionary trailer = cosDoc.getTrailer();
      COSArray idArray = (COSArray) trailer.getDictionaryObject(COSName.ID);
      if (idArray == null || incrementalUpdate) {
          try {

              //algorithm says to use time/path/size/values in doc to generate
              //the id.  We don't have path or size, so do the best we can
              MessageDigest md = MessageDigest.getInstance("MD5");

              md.update(Long.toString(idTime).getBytes("ISO-8859-1"));
              COSDictionary info = (COSDictionary) 
trailer.getDictionaryObject(COSName.INFO);
              if (info != null) {
                  Iterator<COSBase> values = info.getValues().iterator();
                  while (values.hasNext()) {
                      
md.update(values.next().toString().getBytes("ISO-8859-1"));
                  }
              }
              idArray = new COSArray();
              COSString id = new COSString(md.digest());
              idArray.add(id);
              idArray.add(id);
              trailer.setItem(COSName.ID, idArray);
          } catch (NoSuchAlgorithmException e) {
              throw new COSVisitorException(e);
          } catch (UnsupportedEncodingException e) {
              throw new COSVisitorException(e);
          }
      }
      cosDoc.accept(this);
  }




Finally. The way I use this in my signature process is by using this altered 
static function saveIncremental from the PDFDocument class.
Since this function is static, I just call this duplicated function instead of 
the one in the PDFDocument class.
Here I use my altered COSWriter  (CsCOSWriter).


    /**
     * Save the pdf as incremental. This method is a modification of the same
     * method of PDDcoument. This method use an altered COSWriter that allows
     * control over the time used to create the ID of the document. This way it
     * is possible to perform two consecutive signature generation passes that
     * produce the same document hash.
     *
     * @param doc The document being written with signature creation
     * @param input An input file stream of the document being written
     * @param output An output file stream for the result document
     * @param idTime The time in milliseconds from Jan 1st, 1970 GMT when the
     * signature is created. This time is also used to calculate the ID of the
     * document.
     * @throws IOException if something went wrong
     * @throws COSVisitorException if something went wrong
     */
    public static void saveIncremental(PDDocument doc, FileInputStream input, 
OutputStream output, long idTime) throws IOException, COSVisitorException {
        //update the count in case any pages have been added behind the scenes.
        doc.getDocumentCatalog().getPages().updateCount();
        CsCOSWriter writer = null;
        try {
            // Sometimes the original file will be missing a newline at the end
            // In order to avoid having %%EOF the first object on the same line
            // as the %%EOF, we put a newline here.  If there's already one at
            // the end of the file, an extra one won't hurt. PDFBOX-1051
            output.write("\r\n".getBytes());
            writer = new CsCOSWriter(output, input);
            writer.write(doc, idTime);
            writer.close();
        } finally {
            if (writer != null) {
                writer.close();
            }
        }
    }




--
This message is automatically generated by JIRA.
If you think it was sent incorrectly, please contact your JIRA administrators
For more information on JIRA, see: http://www.atlassian.com/software/jira

Reply via email to