Handleiding

PAdES B-B met DSS

DSS (Digital Signature Services, de open-source bibliotheek van de EU) heeft een expliciet pad met twee services voor precies onze situatie: de PDF-kant en de CMS-kant zijn gescheiden, en de handtekeningwaarde komt van buiten. De klassen zijn PAdESWithExternalCMSService en ExternalCMSService. De code hieronder is dat pad op DSS 6.3, teruggebracht tot de essentie.

Maven: dss-pades-pdfbox (of dss-pades-openpdf), dss-cms en dss-service voor de online bronnen die je later wilt. Java 17.

Voorbereiden (stap 4 van de flow)

Invoer: de PDF, de certificaatketen uit credentials/info (ondertekenaar eerst), en een ondertekentijd die je vasthoudt.

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

Bewaar messageDigest, params (of de invoer om hem identiek opnieuw te maken, vooral signingDate) en de PDF zoals hij nu is. De autorisatierondgang, de bevestiging van de ondertekenaar en signHash gebeuren tussen dit en het volgende blok, mogelijk minuten later en in een ander proces.

Over getMessageDigest: heeft de PDF nog geen handtekeningveld, dan maakt DSS er een. Wil je het veld op een bepaalde plek of met een zichtbare weergave, zet dan params.getImageParameters() en een SignatureFieldParameters voor deze aanroep, en houd ze identiek bij het afmaken. Onze implementatie maakt het veld in een aparte pas en geeft de voorbereide PDF aan beide stappen, en dat is de veiligste manier om te garanderen dat de byte ranges hetzelfde zijn.

Afmaken (stap 7 van de flow)

Invoer: alles wat je hierboven bewaarde, en signatures[0] uit 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 schrijft een incrementele update; de oorspronkelijke bytes blijven ongemoeid en de handtekening dekt ze. Wijken params af van de voorbereidingsaanroep in iets dat de ondertekende byte ranges verandert (ondertekendatum, veld, contentgrootte), dan gooit DSS een fout of, erger, maakt hij een handtekening over andere bytes dan er geautoriseerd waren. Daarom is de bewaarde stand belangrijker dan de code.

Het algoritme vastzetten

De twee kanten gebruiken verschillende namen voor dezelfde handtekening, dus houd ze op één plek bij elkaar:

// 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 adverteert 1.2.840.113549.1.1.1 voor een RSA-sleutel, en dat is wat je als signAlgo verstuurt; DSS schrijft daarna sha256WithRSAEncryption in de CMS, en dat is juist. In plaats daarvan rsaEncryption in de CMS noemen, omdat je dat verstuurde, is de meest voorkomende oorzaak van "de handtekening kwam terug maar valideert niet".

De gereserveerde grootte

setContentSize is het aantal bytes dat voor /Contents wordt vrijgehouden. De CMS moet erin passen, inclusief de certificaatketen; een B-B-CMS met drie RSA-2048-certificaten is ongeveer 4 kB, en 8 tot 10 kB zodra er een handtekeningtijdstempel in zit. Onze eigen ondertekenservice reserveert 32768 bytes, met opzet ruim: te klein mislukt bij signDocument, nadat de ondertekenaar heeft geautoriseerd, en te groot kost alleen bestandsgrootte.

Datzelfde getal gaat mee naar extendDocument als je naar B-T, B-LT of B-LTA gaat, en dat bepaalt de /Contents van de documenttijdstempel die DSS voor LTA toevoegt. Die tijdstempel is een nieuw veld met zijn eigen reservering; er zit een TimeStampToken en de keten van de TSA in, dus 4 tot 8 kB is daar de echte behoefte. Eén ruim getal dekt beide gevallen en scheelt je twee constanten.

Waar de DSS-klassen staan

DSS heeft de CMS-klassen tussen minor versies verplaatst (eu.europa.esig.dss.cades.signature.ExternalCMSService in oudere 6.x, eu.europa.esig.dss.cms... in nieuwere). Kijk in de Javadoc van de versie die je vastzet; de methodenamen getMessageDigest, getDataToSign, signMessageDigest en signDocument zijn stabiel gebleven.

Verder gaan

  • B-T: voeg een OnlineTSPSource toe aan beide services (setTspSource) en zet het niveau op PAdES_BASELINE_T; de tijdstempel komt erbij bij het afmaken, na de handtekening. De ondertekende hash verandert niet.
  • B-LT en B-LTA: een aparte uitbreidingsstap met PAdESService.extendDocument, met revocatiebronnen op de verifier. "De PAdES-niveaus" behandelt wanneer en hoe.