Skip to content

Instantly share code, notes, and snippets.

@wgnrd
Last active September 9, 2024 11:45
Show Gist options
  • Save wgnrd/e6fcb6155795014db10fd482bdb40133 to your computer and use it in GitHub Desktop.
Save wgnrd/e6fcb6155795014db10fd482bdb40133 to your computer and use it in GitHub Desktop.
signPDF
public void signPdf(String filePath, String outfilePath, Certificate[] chain, SignatureAppearance signatureAppearance) throws Exception {
try (
OutputStream output = new FileOutputStream(outfilePath);
PDDocument document = PDDocument.load(new File(filePath))
) {
// Create Metadata and visual options for the signature
PDSignature signature = createSignature(signatureAppearance.getSignerName());
SignatureOptions options = createSignatureOptions(document, signature, signatureAppearance);
document.addSignature(signature, null, options);
// Create a ContentSigner object which will be used to sign the PDF file.
ContentSigner contentSigner = createContentSigner();
// Create a generator for the CMS signature.
CMSSignedDataGenerator cmsSignedDataGenerator = createCMSSignedDataGenerator(chain, contentSigner);
// Prepare a document for signing it externally.
ExternalSigningSupport preSignDocument = document.saveIncrementalForExternalSigning(output);
// Create an object containing all the bytes which need to be signed.
CMSTypedData msg = new CMSProcessableByteArray(preSignDocument.getContent().readAllBytes());
// The CMSSignedDataGenerator writes the data to be signed to the ContentSigner's OutputStream.
// After that the getSignature method is called to retrieve the of the signed data.
CMSSignedData signedData = cmsSignedDataGenerator.generate(msg, false);
// Get the byte array that represents the CMS structure with the signed data.
byte[] cmsSignature = signedData.getEncoded();
// Insert the byte array into the PDF file at the location prepared by the saveIncrementalForExternalSigning method.
preSignDocument.setSignature(cmsSignature);
}
}
private PDSignature createSignature(String signerName) {
PDSignature signature = new PDSignature();
// The Filter specifies that the signature was created with Adobe software.
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
// The Subfilter specifies that the signature uses a detached CMS (PKCS #7) encoding scheme.
signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
signature.setName(signerName);
signature.setReason("Testing purposes");
signature.setLocation("Test Location");
signature.setSignDate(Calendar.getInstance());
return signature;
}
private SignatureOptions createSignatureOptions(PDDocument document, PDSignature signature, SignatureAppearance signatureAppearance) throws IOException {
Rectangle2D humanRect = new Rectangle2D.Float(signatureAppearance.getSignatureXLocation(), signatureAppearance.getSignatureYLocation(), 200, 50);
PDRectangle rect = createSignatureRectangle(document, humanRect);
SignatureOptions options = new SignatureOptions();
options.setPreferredSignatureSize(12288);
options.setPage(0);
options.setVisualSignature(createVisualSignatureTemplate(document, 0, rect, signature, signatureAppearance));
return options;
}
private CMSSignedDataGenerator createCMSSignedDataGenerator(Certificate[] chain, ContentSigner contentSigner) throws Exception {
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
Store certStore = new JcaCertStore(Arrays.stream(chain).toList());
X509Certificate signerCert = (X509Certificate) chain[0];
DigestCalculatorProvider digestCalculatorProvider = new JcaDigestCalculatorProviderBuilder().setProvider(new BouncyCastleProvider()).build();
gen.addCertificates(certStore);
gen.addSignerInfoGenerator(
new JcaSignerInfoGeneratorBuilder(digestCalculatorProvider)
.setSignedAttributeGenerator(new PadesSignedAttributeGenerator(new X509CertificateHolder(signerCert.getEncoded()), new JcaDigestCalculatorProviderBuilder().setProvider(new BouncyCastleProvider()).build()))
.setUnsignedAttributeGenerator(new PadesUnsignedAttributeGenerator())
.build(contentSigner, signerCert));
return gen;
}
private ContentSigner createContentSigner() {
return new ContentSigner() {
// This stream will be filled by the saveIncrementalForExternalSigning method. It holds the bytes of the PDF file.
private final ByteArrayOutputStream stream = new ByteArrayOutputStream();
@Override
public byte[] getSignature() {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
// Hash the bytes of the PDF file.
byte[] hashBytes = digest.digest(stream.toByteArray());
// The hash is encoded in Base64 as this is the format expected by the server.
String hash = Base64.getEncoder().encodeToString(hashBytes);
// Send the hash to the server and retrieve the signed hash.
// The response is Base64 encoded, so it needs to be decoded for the CMS signature.
return java.util.Base64.getDecoder().decode(webAPIService.getSignedHash(hash));
} catch (Exception e) {
throw new RuntimeException("Exception while signing", e);
}
}
@Override
public OutputStream getOutputStream() {
return stream;
}
@Override
public AlgorithmIdentifier getAlgorithmIdentifier() {
// The algorithm identifier for SHA-256.
return new AlgorithmIdentifier(new ASN1ObjectIdentifier("1.2.840.113549.1.1.11"));
}
};
}
record PadesSignedAttributeGenerator(X509CertificateHolder x509CertificateHolder, DigestCalculatorProvider digestCalculatorProvider) implements CMSAttributeTableGenerator {
@Override
public AttributeTable getAttributes(@SuppressWarnings("rawtypes") Map params) throws CMSAttributeTableGenerationException {
String currentAttribute = null;
try {
ASN1EncodableVector signedAttributes = new ASN1EncodableVector();
currentAttribute = "SigningCertificateAttribute";
AlgorithmIdentifier digAlgId = (AlgorithmIdentifier) params.get(CMSAttributeTableGenerator.DIGEST_ALGORITHM_IDENTIFIER);
signedAttributes.add(createSigningCertificateAttribute(digAlgId));
currentAttribute = "ContentType";
ASN1ObjectIdentifier contentType = ASN1ObjectIdentifier.getInstance(params.get(CMSAttributeTableGenerator.CONTENT_TYPE));
signedAttributes.add(new Attribute(CMSAttributes.contentType, new DERSet(contentType)));
currentAttribute = "MessageDigest";
byte[] messageDigest = (byte[]) params.get(CMSAttributeTableGenerator.DIGEST);
signedAttributes.add(new Attribute(CMSAttributes.messageDigest, new DERSet(new DEROctetString(messageDigest))));
return new AttributeTable(signedAttributes);
} catch (Exception e) {
throw new CMSAttributeTableGenerationException(currentAttribute, e);
}
}
Attribute createSigningCertificateAttribute(AlgorithmIdentifier digAlg) throws IOException, OperatorCreationException {
final IssuerSerial issuerSerial = getIssuerSerial();
DigestCalculator digestCalculator = digestCalculatorProvider.get(digAlg);
digestCalculator.getOutputStream().write(x509CertificateHolder.getEncoded());
final byte[] certHash = digestCalculator.getDigest();
if (OIWObjectIdentifiers.idSHA1.equals(digAlg.getAlgorithm())) {
final ESSCertID essCertID = new ESSCertID(certHash, issuerSerial);
SigningCertificate signingCertificate = new SigningCertificate(essCertID);
return new Attribute(id_aa_signingCertificate, new DERSet(signingCertificate));
} else {
ESSCertIDv2 essCertIdv2;
if (NISTObjectIdentifiers.id_sha256.equals(digAlg.getAlgorithm())) {
// SHA-256 is default
essCertIdv2 = new ESSCertIDv2(null, certHash, issuerSerial);
} else {
essCertIdv2 = new ESSCertIDv2(digAlg, certHash, issuerSerial);
}
SigningCertificateV2 signingCertificateV2 = new SigningCertificateV2(essCertIdv2);
return new Attribute(id_aa_signingCertificateV2, new DERSet(signingCertificateV2));
}
}
public IssuerSerial getIssuerSerial() {
final X500Name issuerX500Name = x509CertificateHolder.getIssuer();
final GeneralName generalName = new GeneralName(issuerX500Name);
final GeneralNames generalNames = new GeneralNames(generalName);
final BigInteger serialNumber = x509CertificateHolder.getSerialNumber();
return new IssuerSerial(generalNames, serialNumber);
}
}
static class PadesUnsignedAttributeGenerator implements CMSAttributeTableGenerator {
PadesUnsignedAttributeGenerator() {
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment