Signing Pdfs on a Server Document Using a Signature from the User

Digitally sign a PDF on the server

Since it is not possible or anyway safe to extract and send the client's private key, to sign pdfs on the server you need to establish a "session" with the client and let them calculate the signature.


The steps should be something like:

  1. the client sends his public certificate to be embedded in the signed pdf

  2. the server generates the pdf, embeds the certificate and calculates the hash (eg: sha1)

  3. the server sends the hash to the client applet

  4. the applet calculates the digital signature with her private key

  5. the applet sends the signature to the server

  6. the server embeds the digital signature and closes the pdf.


To do this with itext you will have to use the preclose method after ambedding the certificate, so to be able to alculate the sha1 hash on the final document.
Then after pre-closing the pdf you will have to calculate the hash of the pdf and send it to the client.
Be careful: while preclosed you will have to keep the document in memory, for example in a server session.

To generate the pdf, embed certificates and prepaare the document you can use itextsharp, the c# port of the itext library. To calculate the hash and create the pkcs7 envelopes you can use the .net crypto api.

Hope this helps.

How does a e-signature over PDF software works?

There is an abundance of technologies & tools available to develop such e-signature software. Let's take one example which is somewhat similar to the software you mentioned. The use-case to build such software can be realised using the front-end and back-end (server-side) technologies.

On the Client-side, you can use front end frameworks like Angular and React to develop an interface for the end-users using which they can upload the documents to the server-side. You can create signature block layouts and display them alongside the document.
Users can drop those blocks on specific locations and send the request for signing to the recipients. During the signing process, user details will be populated into the signature block and sent to the server for performing the signing process.

On the server side, the responsibility would be to take the appropriate information from the request and print/add those signature blocks to the document at the appropriate positions. On the server side, you need some library to process the documents for adding the signature blocks and signing the documents. If you're using java alongside the spring framework you can use pdfbox for doing such operations.

On server side the responisbility would be to take the appropriate information from the request and print/add those signature blocks to the document at the appropriate positions. On server side you needs some library to process the documents for adding the signature blocks and signing the documents. If you're using java alongside spring framework you can use pdfbox for doing such operations.

How does it render the boxes over the PDF, how does it place the
signature over the PDF when I am done writing, how does that JS
signature create a PDF on the server side?

The contract between client and server for the signature blocks could be a JSON object having the information about the coordinates where the request creator drops those blocks for recipients for signing. On the server side, you can manipulate the document to add those signature blocks at the mentioned coordinates in the request.

Signing a hash with DSS (Digital Signature Service)

In general

This was written in response to the original revision of your question.

Your code mentions PAdES. Thus, I assume you mean integrated PAdES (not detached CAdES or XAdES) signatures when you say you're trying to sign a PDF document with DSS.

Creating integrated PDF signatures (like PAdES) requires first preparing the PDF to be able to carry an embedded signature, i.e. adding a signature dictionary to an existing or new signature field. This signature dictionary contains multiple information, signing time, signing reason, etc., and also a placeholder for embedding a CMS signature container later. Then this prepared PDF (except the placeholder) is hashed.

Furthermore, your code mentions that you choose the level of the signature (-B, -T, -LT, -LTA).

Creating PAdES Baseline LT and PAdES Baseline LTA signatures requires preparing a PDF with a PAdES Baseline T signature and adding a collection of additional objects, depending on the nature of the T signature.

eSig DSS can do all this preparing for you if it has the PDF to prepare.

So if you only want to send a hash value from server A to B, you have to use eSig DSS on your server A to do most of the work, and server B only serves as a dumb signing service returning a signed hash value or at most a CMS container usable for PAdES.

Whether you can do this without server A knowing about the certificate, depends on whether you want certificate details to appear in a signature widget for the new signature or not. Creating the widget appearance is part of the PDF preparation step, so if you want such a widget with certificate information, server A needs to know the certificate, usually even before requesting the signature!

What kind of protocol you run between server A and B then, is up to you. You merely have to implement SignatureTokenConnection accordingly to use on server A in eSig DSS to communicate with server B.

In case of your approach

After you now showed code from both servers, one can discuss your specific approach.

On server A you use eSig DSS to prepare a PAdES signature and embed a CMS signature container with the SignatureValue your SignHashDocument.signHash call returns:

ToBeSigned dataToSign = service.getDataToSign(toSignDocument, parameters);

SignatureValue signatureValue = SignHashDocument.signHash(dataToSign);

DSSDocument signedDocument = service.signDocument(toSignDocument, parameters, signatureValue);

I.e. server A creates the CMS signature container and server B only supplies the signed hash.

This cannot work unless you know the certificate used for signing and set it in the parameters before the service.getDataToSign call.

The reason is that the CMS container contains references to that certificate in both the unsigned bytes and (for PAdES) the signed bytes of the container. For the reference in the unsigned bytes it theoretically would suffice to retrieve the certificate together with the signature bytes, but for the reference in the signed bytes it has to be known beforehand.

Alternatively you can try to implement the full generation of the CMS container on server B.

This requires considerable changes, though, and you have to dive quite a bit deeper into the PAdESService sources. Instead of the three lines quoted above on server A you have to:

  • first retrieve a PDF object library and PDF signature service

    IPdfObjFactory pdfObjFactory = new ServiceLoaderPdfObjFactory();
    PDFSignatureService pdfSignatureService = pdfObjFactory.newPAdESSignatureService();
  • prepare the PDF a first time for signing and calculate the document digest,

    byte[] hash = pdfSignatureService.digest(toSignDocument, parameters);
  • send this document digest to the backend (server B) which must create and return a special CAdES signature container, not merely naked signature bytes,

  • and prepare the PDF a second time for signing and inject this signature container:

    DSSDocument signature = pdfSignatureService.sign(toSignDocument, encodedData, parameters);

A proof of concept

Here a proof of concept using eSig DSS 5.8:

On your server A we can essentially use your existing code:

DSSDocument toSignDocument = PDF_DOCUMENT_TO_SIGN;
DSSDocument image = IMAGE_DOCUMENT;


PAdESSignatureParameters parameters = new PAdESSignatureParameters();
parameters.setDigestAlgorithm(DigestAlgorithm.SHA512);
parameters.setReason("Preuve de signature");
parameters.setLocation("MAROC");
parameters.setGenerateTBSWithoutCertificate(true);

SignatureImageParameters imageParameters = new SignatureImageParameters();
imageParameters.setPage(1);
imageParameters.setImage(image);
imageParameters.setxAxis(350);
imageParameters.setyAxis(400);
imageParameters.setWidth(200);
imageParameters.setHeight(100);
parameters.setImageParameters(imageParameters);

SignatureImageTextParameters textParameters = new SignatureImageTextParameters();
DSSFont font = new DSSJavaFont(Font.SERIF);
font.setSize(16);
textParameters.setFont(font);
textParameters.setTextColor(Color.BLUE);
textParameters.setSignerTextPosition(SignerTextPosition.RIGHT);
textParameters.setSignerTextHorizontalAlignment(SignerTextHorizontalAlignment.LEFT);
textParameters.setSignerTextVerticalAlignment(SignerTextVerticalAlignment.TOP);
textParameters.setText("TESTING");
imageParameters.setTextParameters(textParameters);


IPdfObjFactory pdfObjFactory = new ServiceLoaderPdfObjFactory();
PDFSignatureService pdfSignatureService = pdfObjFactory.newPAdESSignatureService();

byte[] hash = pdfSignatureService.digest(toSignDocument, parameters);

byte[] signatureValue = signHash(hash);

DSSDocument signedDocument = pdfSignatureService.sign(toSignDocument, signatureValue, parameters);


signedDocument.save(PATH_TO_SAVE_THE_SIGNED_DOCUMENT_TO);

(SplitPAdESSigning test testSplitPAdESGenerationForMehdi)

The method signHash now shall independently create a CMS signature container for the given document hash, and this container shall conform to PAdES requirements. eSig DSS contains methods and classes providing this functionality but they protected or even less visible. Thus, for our POC we simply copy them into our code.

For simplicity I use hard coded SHA512withRSA as signing algorithm.

Thus:

byte[] signHash(byte[] hash) throws IOException {
Pkcs12SignatureToken signingToken = new Pkcs12SignatureToken(YOUR_P12_DATA);
DSSPrivateKeyEntry privateKey = signingToken.getKey(YOUR_ALIAS);

CommonCertificateVerifier commonCertificateVerifier = new CommonCertificateVerifier();
padesCMSSignedDataBuilder = new PadesCMSSignedDataBuilder(commonCertificateVerifier);

PAdESSignatureParameters parameters = new PAdESSignatureParameters();
parameters.setDigestAlgorithm(DigestAlgorithm.SHA512);
parameters.setEncryptionAlgorithm(EncryptionAlgorithm.RSA);
parameters.setSignatureLevel(SignatureLevel.PAdES_BASELINE_B);
parameters.setSigningCertificate(privateKey.getCertificate());

ToBeSigned dataToSign = getDataToSign(hash, parameters);
SignatureValue signatureValue = signingToken.sign(dataToSign, DigestAlgorithm.SHA512, privateKey);
return generateCMSSignedData(hash, parameters, signatureValue);
}

PadesCMSSignedDataBuilder padesCMSSignedDataBuilder;

(SplitPAdESSigning method)

The helper methods getDataToSign and generateCMSSignedData are essentially copied from PAdESService; they use the padesCMSSignedDataBuilder provided by signHash (instead of a member variable you can also make it another argument of these two methods):

/** @see eu.europa.esig.dss.pades.signature.PAdESService#getDataToSign(DSSDocument, PAdESSignatureParameters) */
public ToBeSigned getDataToSign(byte[] messageDigest, final PAdESSignatureParameters parameters) throws DSSException {
final SignatureAlgorithm signatureAlgorithm = parameters.getSignatureAlgorithm();
final CustomContentSigner customContentSigner = new CustomContentSigner(signatureAlgorithm.getJCEId());

SignerInfoGeneratorBuilder signerInfoGeneratorBuilder = padesCMSSignedDataBuilder.getSignerInfoGeneratorBuilder(parameters, messageDigest);

final CMSSignedDataGenerator generator = padesCMSSignedDataBuilder.createCMSSignedDataGenerator(parameters, customContentSigner,
signerInfoGeneratorBuilder, null);

final CMSProcessableByteArray content = new CMSProcessableByteArray(messageDigest);

CMSUtils.generateDetachedCMSSignedData(generator, content);

final byte[] dataToSign = customContentSigner.getOutputStream().toByteArray();
return new ToBeSigned(dataToSign);
}

/** @see eu.europa.esig.dss.pades.signature.PAdESService#generateCMSSignedData(DSSDocument, PAdESSignatureParameters, SignatureValue) */
protected byte[] generateCMSSignedData(byte[] messageDigest, final PAdESSignatureParameters parameters,
final SignatureValue signatureValue) {
final SignatureAlgorithm signatureAlgorithm = parameters.getSignatureAlgorithm();
final SignatureLevel signatureLevel = parameters.getSignatureLevel();
Objects.requireNonNull(signatureAlgorithm, "SignatureAlgorithm cannot be null!");
Objects.requireNonNull(signatureLevel, "SignatureLevel must be defined!");

final CustomContentSigner customContentSigner = new CustomContentSigner(signatureAlgorithm.getJCEId(), signatureValue.getValue());

final SignerInfoGeneratorBuilder signerInfoGeneratorBuilder = padesCMSSignedDataBuilder.getSignerInfoGeneratorBuilder(parameters, messageDigest);

final CMSSignedDataGenerator generator = padesCMSSignedDataBuilder.createCMSSignedDataGenerator(parameters, customContentSigner,
signerInfoGeneratorBuilder, null);

final CMSProcessableByteArray content = new CMSProcessableByteArray(messageDigest);
CMSSignedData data = CMSUtils.generateDetachedCMSSignedData(generator, content);

return DSSASN1Utils.getDEREncoded(data);
}

(SplitPAdESSigning methods)

The classes PadesCMSSignedDataBuilder and PAdESLevelBaselineB due to restricted visibility are copied along:

/** @see eu.europa.esig.dss.cades.signature.CMSSignedDataBuilder */
class PadesCMSSignedDataBuilder extends CMSSignedDataBuilder {
public PadesCMSSignedDataBuilder(CertificateVerifier certificateVerifier) {
super(certificateVerifier);
}

@Override
protected CMSSignedDataGenerator createCMSSignedDataGenerator(CAdESSignatureParameters parameters, ContentSigner contentSigner, SignerInfoGeneratorBuilder signerInfoGeneratorBuilder,
CMSSignedData originalSignedData) throws DSSException {

return super.createCMSSignedDataGenerator(parameters, contentSigner, signerInfoGeneratorBuilder, originalSignedData);
}

protected SignerInfoGeneratorBuilder getSignerInfoGeneratorBuilder(final PAdESSignatureParameters parameters, final byte[] messageDigest) {
final CAdESLevelBaselineB cadesLevelBaselineB = new CAdESLevelBaselineB(true);
final PAdESLevelBaselineB padesProfileB = new PAdESLevelBaselineB();

final DigestCalculatorProvider digestCalculatorProvider = new BcDigestCalculatorProvider();

SignerInfoGeneratorBuilder signerInfoGeneratorBuilder = new SignerInfoGeneratorBuilder(digestCalculatorProvider);

signerInfoGeneratorBuilder = signerInfoGeneratorBuilder.setSignedAttributeGenerator(new CMSAttributeTableGenerator() {
@Override
public AttributeTable getAttributes(@SuppressWarnings("rawtypes") Map params) throws CMSAttributeTableGenerationException {
return padesProfileB.getSignedAttributes(params, cadesLevelBaselineB, parameters, messageDigest);
}
});

signerInfoGeneratorBuilder = signerInfoGeneratorBuilder.setUnsignedAttributeGenerator(new CMSAttributeTableGenerator() {
@Override
public AttributeTable getAttributes(@SuppressWarnings("rawtypes") Map params) throws CMSAttributeTableGenerationException {
return padesProfileB.getUnsignedAttributes();
}
});

return signerInfoGeneratorBuilder;
}
}

/** @see eu.europa.esig.dss.pades.signature.PAdESLevelBaselineB */
class PAdESLevelBaselineB {
AttributeTable getSignedAttributes(@SuppressWarnings("rawtypes") Map params,
CAdESLevelBaselineB cadesProfile, PAdESSignatureParameters parameters, byte[] messageDigest) {
AttributeTable signedAttributes = cadesProfile.getSignedAttributes(parameters);

if (signedAttributes.get(CMSAttributes.contentType) == null) {
ASN1ObjectIdentifier contentType = (ASN1ObjectIdentifier) params.get(CMSAttributeTableGenerator.CONTENT_TYPE);
if (contentType != null) {
signedAttributes = signedAttributes.add(CMSAttributes.contentType, contentType);
}
}

if (signedAttributes.get(CMSAttributes.messageDigest) == null) {
signedAttributes = signedAttributes.add(CMSAttributes.messageDigest, new DEROctetString(messageDigest));
}

return signedAttributes;
}

AttributeTable getUnsignedAttributes() {
return null;
}
}

(SplitPAdESSigning helper classes)

signHash and its helpers do not depend on the server A code and, therefore, also can be located on server B.



Related Topics



Leave a reply



Submit