1
votes

I'm implementing an application to sign PDF files in the server, with the follow scenario (to make long history, short):

  1. Client start signature sending to server, date/time and watermark
  2. Server add signature dictionaries into file and send data to be signed
  3. Client sign content
  4. Server finish the signature

I'm using PDFBox 2.0.15, and making use of new feature saveIncrementalForExternalSigning as shown in the code below:

try {
        String name = document.getID();
        File signedFile = new File(workingDir.getAbsolutePath() + sep + name + "_Signed.pdf");
        this.log("[SIGNATURE] Creating signed version of the document");
        if (signedFile.exists()) {
            signedFile.delete();
        }
        FileOutputStream tbsFos = new FileOutputStream(signedFile);
        ExternalSigningSupport externalSigning = pdfdoc.saveIncrementalForExternalSigning(tbsFos);

        byte[] content = readExternalSignatureContent(externalSigning);
        if (postparams.get("action").equalsIgnoreCase("calc_hash")) {
            this.log("[SIGNATURE] Calculating hash of the document");
            String strBase64 = ParametersHandle.compressParamBase64(content);

            // this saves the file with a 0 signature
            externalSigning.setSignature(new byte[0]);

            // remember the offset (add 1 because of "<")
            int offset = signature.getByteRange()[1] + 1;

            this.log("[SIGNATURE] Sending calculated hash to APP");
            return new String[] { strBase64, processID, String.valueOf(offset) };
        } else {
            this.log("[SIGNATURE] Signature received from APP");
            String signature64 = postparams.get("sign_disgest");
            byte[] cmsSignature = ParametersHandle.decompressParamFromBase64(signature64);

            this.log("[SIGNATURE] Setting signature to document");
            externalSigning.setSignature(cmsSignature);

            pdfdoc.close();

            IOUtils.closeQuietly(signatureOptions);

            this.log("[DOXIS] Creating new version of document on Doxis");
            createNewVersionOfDocument(doxisServer, documentServer, doxisSession, document, signedFile);

            return new String[] { "SIGNOK" };
        }
    } catch (IOException ex) {
        this.log("[SAVE FOR SIGN] " + ex);
        return null;
    }

In the "IF" statement I'm generating data to be signed. In the "ELSE" statement adding the signature, that comes via post request (that is what ParametersHandle.decompressParamFromBase64 does), into document. So I have two post requests for this method in this try.

A second approach was doing each post request in one method, so I have this second code block:

// remember the offset (add 1 because of "<") 
        int offset = Integer.valueOf(postparams.get("offset"));
        this.log("[PDF BOX] Retrieving offset of bytes range for this signature. The value is: "
                + String.valueOf(offset));

        File signedPDF = new File(workingDir.getAbsolutePath() + sep + name + "_Signed.pdf");
        this.log("[SIGNATURE] Reloading document for apply signature: " + signedPDF.getAbsolutePath());

        // invoke external signature service
        String signature64 = postparams.get("sign_disgest");
        byte[] cmsSignature = ParametersHandle.decompressParamFromBase64(signature64);

        this.log("[SIGNATURE] Got signature byte array from APP.");
        // set signature bytes received from the service

        // now write the signature at the correct offset without any PDFBox methods
        this.log("[SIGNATURE] Writing signed document...");
        RandomAccessFile raf = new RandomAccessFile(signedPDF, "rw");
        raf.seek(offset);
        raf.write(Hex.getBytes(cmsSignature));
        raf.close();
        this.log("[SIGNATURE] New signed document has been saved!");

The problem is: I'm getting the error "The document has been altered or corrupted since the Signature was applied" when validating it on Adobe Reader. On my understanding it should not happen since the offset of the signature byte range is being remembered on the second post call.

Any help or idea is appreciated,

Thank you in advance.

[EDIT]

For a complete list of used files: https://drive.google.com/drive/folders/1S9a88lCGaQYujlEyCrhyzqvmWB-68LR3

[EDIT 2]

Based on @mkl comment, here is the method where the signature is made:

public byte[] sign(byte[] hash)
        throws IOException, NoSuchAlgorithmException, InvalidKeyException, SignatureException {
    PrivateKey privKey = (PrivateKey) windowsCertRep.getPrivateKey(this.selected_alias, "");
    X509Certificate[] certificateChain = windowsCertRep.getCertificateChain(this.selected_alias);

    try
    {
        CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
        X509Certificate cert = (X509Certificate) certificateChain[0];
        ContentSigner sha1Signer = new JcaContentSignerBuilder("SHA256WithRSA").build(privKey);
        gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().build()).build(sha1Signer, cert));
        gen.addCertificates(new JcaCertStore(Arrays.asList(certificateChain)));
        CMSProcessableInputStream msg = new CMSProcessableInputStream(new ByteArrayInputStream(hash));
        CMSSignedData signedData = gen.generate(msg, false);
        return signedData.getEncoded();
    }
    catch (GeneralSecurityException e)
    {
        throw new IOException(e);
    }
    catch (CMSException e)
    {
        throw new IOException(e);
    }
    catch (OperatorCreationException e)
    {
        throw new IOException(e);
    }

}

I've tested the CreateVisibleSignature2 examaple, replacing the sign method for one calling this service that returns me the signature, e it works.

1
Please add the log output and the signed PDF file. All this looks a bit confusing at first but after I looked at the example it seems you DID read and apply it :-)Tilman Hausherr
Please share not only the eventually signed file but also the file result from the first step. Furthermore, have you tested the original PDFBox signing code with merely the signature container creation replaced by a call to your service? Probably there is an issue with that signature container, not with the code you shared above.mkl
More suspicious things: The length of the "hash" in the log is 4998 but it should be about 55000. The non zero part in the signature in the PDF has a length of 11962 according to NOTEPAD++. Divided by 2 this would be about 6000. But in the log the length is 5140.Tilman Hausherr
@Tilmanhausherr thanks for all your help yesterday, it helped me to filter the problem and now I think I've found it. As I told you, I'm using websocket to transfer data between server and client. And here is the hint: When I start the signature process I have 60662 bytes. After compress it and and encode to base64 I have 72548 bytes (I know that is bigger, but if I encode 60k to base 64 it'll get bigger than that). The problem is: my websocket class is ready to deal with 65k bytes haha. So now I'm working on it and can post the results later.mTheSame
If transfer of large data is a problem, then transfer the hash only, and use this code: stackoverflow.com/questions/41767351Tilman Hausherr

1 Answers

1
votes

Thanks to Tilman Hausherr I could figure out what was going on:

1 - I have a Desktop APP that communicates with SmatCards and so on, and it is the signer. To communicate with the server (through a webpage) we use WebSocket. I've written my own websocket server class, and that is why it's only prepared to work with 65k bytes. Than when I tried to send the data here:

ExternalSigningSupport externalSigning = doc.saveIncrementalForExternalSigning(fos);           
byte[] cmsSignature = sign(externalSigning.getContent());                           

I got errors in the APP.

2 - Tilman, suggested me to take a look on this @mkl answer where he does the same thing: create a SHA256 hash of the externalSigning.getContent() and send to be signed in another place. I don't know why, but the only thing that didn't work for me was:

gen.addSignerInfoGenerator(builder.build(
                new BcRSAContentSignerBuilder(sha256withRSA,
                        new DefaultDigestAlgorithmIdentifierFinder().find(sha256withRSA))
                                .build(PrivateKeyFactory.createKey(pk.getEncoded())),
                new JcaX509CertificateHolder(cert)));

So, I've replaced this block to:

ContentSigner sha256Signer = new JcaContentSignerBuilder("SHA256WithRSA").build(privKey);

Than, my complete signature method is like:

        PrivateKey privKey = (PrivateKey) windowsCertRep.getPrivateKey(this.selected_alias, "changeit");
    X509Certificate[] certificateChain = windowsCertRep.getCertificateChain(this.selected_alias);

        List<X509Certificate> certList = Arrays.asList(certificateChain);
        JcaCertStore certs = new JcaCertStore(certList);

        CMSSignedDataGenerator gen = new CMSSignedDataGenerator();

        Attribute attr = new Attribute(CMSAttributes.messageDigest,
                new DERSet(new DEROctetString(hash)));

        ASN1EncodableVector v = new ASN1EncodableVector();
        v.add(attr);

        SignerInfoGeneratorBuilder builder = new SignerInfoGeneratorBuilder(new BcDigestCalculatorProvider())
                .setSignedAttributeGenerator(new DefaultSignedAttributeTableGenerator(new AttributeTable(v)));

        AlgorithmIdentifier sha256withRSA = new DefaultSignatureAlgorithmIdentifierFinder().find("SHA256withRSA");

        CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
        InputStream in = new ByteArrayInputStream(certificateChain[0].getEncoded());
        X509Certificate cert = (X509Certificate) certFactory.generateCertificate(in);

        ContentSigner sha256Signer = new JcaContentSignerBuilder("SHA256WithRSA").build(privKey);
        gen.addSignerInfoGenerator(builder.build(sha256Signer, new JcaX509CertificateHolder(cert)));

        gen.addCertificates(certs);

        CMSSignedData s = gen.generate(new CMSAbsentContent(), false);
        return s.getEncoded();

So, thank you community once more!!!