How-to guide

PAdES B-B with DSS

DSS (Digital Signature Services, the EU's open-source library) has an explicit two-service path for exactly our situation: the PDF side and the CMS side are separate, and the signature value comes from outside. The classes are PAdESWithExternalCMSService and ExternalCMSService. The code below is that path on DSS 6.3, reduced to the essentials.

Maven: dss-pades-pdfbox (or dss-pades-openpdf), dss-cms and dss-service for the online sources you will want later. Java 17.

Prepare (step 4 of the flow)

Inputs: the PDF, the certificate chain from credentials/info (signer first), and a signing time you will keep.

import eu.europa.esig.dss.enumerations.DigestAlgorithm;
import eu.europa.esig.dss.enumerations.SignatureLevel;
import eu.europa.esig.dss.enumerations.SignaturePackaging;
import eu.europa.esig.dss.model.*;
import eu.europa.esig.dss.model.x509.CertificateToken;
import eu.europa.esig.dss.pades.PAdESSignatureParameters;
import eu.europa.esig.dss.pades.signature.PAdESWithExternalCMSService;
import eu.europa.esig.dss.cms.signature.ExternalCMSService;   // package name differs per DSS version; see note
import eu.europa.esig.dss.spi.validation.CommonCertificateVerifier;

CommonCertificateVerifier verifier = new CommonCertificateVerifier();

PAdESSignatureParameters params = new PAdESSignatureParameters();
params.setSignatureLevel(SignatureLevel.PAdES_BASELINE_B);
params.setSignaturePackaging(SignaturePackaging.ENVELOPED);
params.setDigestAlgorithm(DigestAlgorithm.SHA256);
params.setSigningCertificate(signingCert);          // CertificateToken of certificates[0]
params.setCertificateChain(chain);                  // signer first, then issuers
params.bLevel().setSigningDate(signingDate);        // fix it now, reuse it at complete
params.setContentSize(32768);                       // reserved /Contents size, see below

DSSDocument pdf = new FileDocument("input.pdf");

// 1. Digest of the PDF byte ranges, with the signature field and /M already in place.
PAdESWithExternalCMSService padesService = new PAdESWithExternalCMSService();
padesService.setCertificateVerifier(verifier);
DSSMessageDigest messageDigest = padesService.getMessageDigest(pdf, params);

// 2. The CMS signed attributes around that digest: this is what gets signed.
ExternalCMSService cmsService = new ExternalCMSService(verifier);
ToBeSigned toBeSigned = cmsService.getDataToSign(messageDigest, params);

// 3. The hash for Cleverbase: SHA-256 over the DER of the signed attributes.
byte[] hash = MessageDigest.getInstance("SHA-256").digest(toBeSigned.getBytes());
String hashB64 = Base64.getEncoder().encodeToString(hash);          // for signHash
String hashB64Url = Base64.getUrlEncoder().withoutPadding().encodeToString(hash); // for oauth2/authorize

Persist messageDigest, params (or the inputs to rebuild it identically, above all signingDate) and the PDF as it is now. The authorize leg, the signer's confirmation and signHash happen between this and the next block, possibly minutes later and in another process.

Note on getMessageDigest: when the PDF has no signature field yet, DSS creates one. If you want the field at a particular place or with a visible appearance, set params.getImageParameters() and a SignatureFieldParameters before this call, and keep them identical at complete. Our implementation prepares the field in a separate pass and hands the prepared PDF to both steps, which is the safest way to guarantee the byte ranges are the same.

Complete (step 7 of the flow)

Inputs: everything persisted above, and signatures[0] from signHash.

import eu.europa.esig.dss.enumerations.SignatureAlgorithm;

byte[] signatureBytes = Base64.getDecoder().decode(signaturesFromCleverbase.get(0));

// The CMS names the COMBINED algorithm, even though the API took the key
// algorithm: signAlgo 1.2.840.113549.1.1.1 + hashAlgo SHA-256 is RSA_SHA256 here.
SignatureValue signatureValue = new SignatureValue(SignatureAlgorithm.RSA_SHA256, signatureBytes);

// 4. The CMS: signed attributes + your signature value + the certificate chain.
DSSDocument cms = cmsService.signMessageDigest(messageDigest, params, signatureValue);

// 5. The CMS into the reserved /Contents of the prepared PDF.
DSSDocument signed = padesService.signDocument(pdf, params, cms);
signed.save("signed.pdf");

signDocument writes an incremental update; the original bytes are untouched and the signature covers them. If params differ from the prepare call in anything that changes the signed byte ranges (signing date, field, content size), DSS throws or, worse, produces a signature over different bytes than were authorized. That is why the persisted state matters more than the code.

Pinning the algorithm

The two sides use different names for the same signature, so keep them next to each other in one place:

// What the API takes: the key algorithm, with SHA-256 named separately.
static final String CSC_SIGN_ALGO = "1.2.840.113549.1.1.1";        // rsaEncryption
static final String CSC_HASH_ALGO = "2.16.840.1.101.3.4.2.1";      // SHA-256

// What the CMS must say: the combined algorithm.
static final SignatureAlgorithm DSS_ALGO = SignatureAlgorithm.RSA_SHA256;

key.algo in credentials/info advertises 1.2.840.113549.1.1.1 for an RSA key, which is what you send as signAlgo; DSS then writes sha256WithRSAEncryption into the CMS, which is correct. Naming rsaEncryption in the CMS instead, because that is what you sent, is the most common cause of "the signature came back but does not validate".

The reserved size

setContentSize is the number of bytes reserved for /Contents. The CMS has to fit, including the certificate chain; a B-B CMS with three RSA-2048 certificates is around 4 kB, and 8 to 10 kB once a signature timestamp is in it. Our own signing service reserves 32768 bytes, deliberately generous: too small fails at signDocument, after the signer has authorized, and too large only adds file size.

The same number rides along to extendDocument when you go to B-T, B-LT or B-LTA, which is what sizes the /Contents of the document timestamp DSS adds for LTA. That timestamp is a new field with its own reservation; it holds a TimeStampToken and the TSA's chain, so 4 to 8 kB is the real need there. One generous number covers both cases and saves you two constants.

Where the DSS classes live

DSS moved the CMS classes between minor versions (eu.europa.esig.dss.cades.signature.ExternalCMSService in older 6.x, eu.europa.esig.dss.cms... in newer ones). Check the Javadoc of the version you pin; the method names getMessageDigest, getDataToSign, signMessageDigest and signDocument have been stable.

Going further

  • B-T: add an OnlineTSPSource to both services (setTspSource) and set the level to PAdES_BASELINE_T; the timestamp is added at complete, after the signature. The signed hash does not change.
  • B-LT and B-LTA: a separate extension step with PAdESService.extendDocument, needing revocation sources on the verifier. "The PAdES levels" covers when and how.