-
-
Save wgnrd/e6fcb6155795014db10fd482bdb40133 to your computer and use it in GitHub Desktop.
signPDF
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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