I am going to implement the external and late signing with PDFBox 3.0.x but the output signed PDF causes "Signature is invalid". The following is my code:
public class CreateSignature2 {
final DataSigner signer;
private Certificate cert;
private Certificate[] certificateChain;
public CreateSignature2(DataSigner signer) {
this.signer = signer;
try {
this.cert = signer.getSignerCert();
this.certificateChain = signer.getSignerCertChain().toArray(new Certificate[0]);
} catch (ApiException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public void signDocument(File inFile, Certificate cert, Certificate[] certChain) throws
Exception,
IOException,
CertificateEncodingException,
NoSuchAlgorithmException,
OperatorCreationException,
CMSException {
this.cert = cert;
setCertificateChain(certChain);
String name = inFile.getName();
String substring = name.substring(0, name.lastIndexOf('.'));
File outFile = new File(inFile.getParent(), substring + "_signed_pdfbox.pdf");
// Use late signing approach
LateSigningSession session = prepareSigning(inFile, outFile);
byte[] dataToSign = session.getDataToSign();
// Sign the data externally
List<byte[]> dataList = Arrays.asList(dataToSign);
List<byte[]> signatures = signer.sign(dataList);
byte[] signature = signatures.get(0);
// Complete the signing
completeSigning(session, signature);
}
private void setCertificateChain(final Certificate[] certificateChain) {
this.certificateChain = certificateChain;
}
/**
* Prepares the document for signing and returns the data that needs to be signed
*/
public LateSigningSession prepareSigning(File inFile, File outFile) throws
IOException,
NoSuchAlgorithmException,
CertificateEncodingException,
OperatorCreationException,
CMSException {
FileOutputStream output = new FileOutputStream(outFile);
PDDocument document = Loader.loadPDF(inFile);
try {
PDSignature signature = new PDSignature();
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
signature.setName("Test Name");
signature.setSignDate(Calendar.getInstance());
SignatureOptions signatureOptions = new SignatureOptions();
signatureOptions.setPage(0);
document.addSignature(signature, signatureOptions);
ExternalSigningSupport externalSigning = document.saveIncrementalForExternalSigning(output);
// Build CMS structure
ESSCertIDv2 certid = new ESSCertIDv2(
new AlgorithmIdentifier(NISTObjectIdentifiers.id_sha256),
MessageDigest.getInstance("SHA-256").digest(cert.getEncoded())
);
SigningCertificateV2 sigcert = new SigningCertificateV2(certid);
Attribute attr = new Attribute(PKCSObjectIdentifiers.id_aa_signingCertificateV2, new DERSet(sigcert));
ASN1EncodableVector v = new ASN1EncodableVector();
v.add(attr);
AttributeTable atttributeTable = new AttributeTable(v);
CMSAttributeTableGenerator attrGen = new DefaultSignedAttributeTableGenerator(atttributeTable);
org.bouncycastle.asn1.x509.Certificate cert2 = org.bouncycastle.asn1.x509.Certificate.getInstance(
ASN1Primitive.fromByteArray(cert.getEncoded())
);
JcaSignerInfoGeneratorBuilder sigb = new JcaSignerInfoGeneratorBuilder(
new JcaDigestCalculatorProviderBuilder().build()
);
sigb.setSignedAttributeGenerator(attrGen);
// Create a ContentSigner that captures the data
HashCapturingContentSigner contentSigner = new HashCapturingContentSigner();
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
gen.addCertificates(new JcaCertStore(Arrays.asList(certificateChain)));
gen.addSignerInfoGenerator(sigb.build(contentSigner, new X509CertificateHolder(cert2)));
// Process the document content to populate the ContentSigner's OutputStream
CMSTypedData msg = new CMSProcessableInputStream(externalSigning.getContent());
// This will write the data to be signed to the ContentSigner's OutputStream
// We catch the expected exception when getSignature() is called prematurely
try {
CMSSignedData signedData = gen.generate(msg, false);
} catch (RuntimeException e) {
if (e.getMessage() != null && e.getMessage().contains("Signature not set")) {
// Expected - we're capturing the data for late signing
} else {
throw e;
}
}
// Get the captured data from the ContentSigner
byte[] dataToSign = contentSigner.getCapturedData();
return new LateSigningSession(document, output, externalSigning, contentSigner,
gen, msg, dataToSign);
} catch (Exception e) {
// Clean up resources if anything fails
document.close();
output.close();
throw e;
}
}
/**
* Completes the signing process with the externally generated signature
*/
public void completeSigning(LateSigningSession session, byte[] externalSignature) throws
IOException,
CMSException {
try {
// Set the external signature on the ContentSigner
session.getContentSigner().setSignature(externalSignature);
// Now generate the final CMS signature
CMSSignedData signedData = session.getGenerator().generate(session.getMessage(), false);
byte[] cmsSignature = signedData.getEncoded();
// Set the signature on the document
session.getExternalSigningSupport().setSignature(cmsSignature);
} finally {
// Always close resources
session.close();
}
}
/**
* ContentSigner that captures data for late signing
*/
private static class HashCapturingContentSigner implements ContentSigner {
private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
private byte[] signature;
private boolean signatureSet = false;
@Override
public byte[] getSignature() {
if (!signatureSet) {
throw new RuntimeException("Signature not set. Use setSignature() for late signing.");
}
return signature;
}
public void setSignature(byte[] signature) {
this.signature = signature;
this.signatureSet = true;
}
@Override
public OutputStream getOutputStream() {
return outputStream;
}
@Override
public AlgorithmIdentifier getAlgorithmIdentifier() {
return new AlgorithmIdentifier(new ASN1ObjectIdentifier("1.2.840.113549.1.1.11"));
}
public byte[] getCapturedData() {
return outputStream.toByteArray();
}
}
/**
* Session class to hold signing state between preparation and completion
*/
public static class LateSigningSession implements AutoCloseable {
private final PDDocument document;
private final FileOutputStream output;
private final ExternalSigningSupport externalSigningSupport;
private final HashCapturingContentSigner contentSigner;
private final CMSSignedDataGenerator generator;
private final CMSTypedData message;
private final byte[] dataToSign;
public LateSigningSession(PDDocument document, FileOutputStream output,
ExternalSigningSupport externalSigningSupport,
HashCapturingContentSigner contentSigner,
CMSSignedDataGenerator generator,
CMSTypedData message,
byte[] dataToSign) {
this.document = document;
this.output = output;
this.externalSigningSupport = externalSigningSupport;
this.contentSigner = contentSigner;
this.generator = generator;
this.message = message;
this.dataToSign = dataToSign;
}
public byte[] getDataToSign() {
return dataToSign;
}
public ExternalSigningSupport getExternalSigningSupport() {
return externalSigningSupport;
}
public HashCapturingContentSigner getContentSigner() {
return contentSigner;
}
public CMSSignedDataGenerator getGenerator() {
return generator;
}
public CMSTypedData getMessage() {
return message;
}
@Override
public void close() throws IOException {
if (document != null) {
document.close();
}
if (output != null) {
output.close();
}
}
}
/**
* Alternative approach that recreates the CMS structure for completion
* This is more robust if the first approach has issues
*/
public LateSigningSession prepareSigningAlternative(File inFile, File outFile) throws
IOException,
NoSuchAlgorithmException,
CertificateEncodingException {
FileOutputStream output = new FileOutputStream(outFile);
PDDocument document = Loader.loadPDF(inFile);
try {
PDSignature signature = new PDSignature();
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
signature.setName("Test Name");
signature.setSignDate(Calendar.getInstance());
SignatureOptions signatureOptions = new SignatureOptions();
signatureOptions.setPage(0);
document.addSignature(signature, signatureOptions);
ExternalSigningSupport externalSigning = document.saveIncrementalForExternalSigning(output);
// Read the document content that will be processed
InputStream contentStream = externalSigning.getContent();
ByteArrayOutputStream contentBaos = new ByteArrayOutputStream();
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = contentStream.read(buffer)) != -1) {
contentBaos.write(buffer, 0, bytesRead);
}
byte[] documentContent = contentBaos.toByteArray();
return new LateSigningSession(document, output, externalSigning, null,
null, null, documentContent);
} catch (Exception e) {
document.close();
output.close();
throw e;
}
}
/**
* Alternative completion that recreates the CMS structure
*/
public void completeSigningAlternative(LateSigningSession session, byte[] externalSignature) throws
IOException,
NoSuchAlgorithmException,
CertificateEncodingException,
OperatorCreationException,
CMSException {
try {
// Recreate the CMS structure with the external signature
ESSCertIDv2 certid = new ESSCertIDv2(
new AlgorithmIdentifier(NISTObjectIdentifiers.id_sha256),
MessageDigest.getInstance("SHA-256").digest(cert.getEncoded())
);
SigningCertificateV2 sigcert = new SigningCertificateV2(certid);
Attribute attr = new Attribute(PKCSObjectIdentifiers.id_aa_signingCertificateV2, new DERSet(sigcert));
ASN1EncodableVector v = new ASN1EncodableVector();
v.add(attr);
AttributeTable atttributeTable = new AttributeTable(v);
CMSAttributeTableGenerator attrGen = new DefaultSignedAttributeTableGenerator(atttributeTable);
org.bouncycastle.asn1.x509.Certificate cert2 = org.bouncycastle.asn1.x509.Certificate.getInstance(
ASN1Primitive.fromByteArray(cert.getEncoded())
);
// Create ContentSigner with the pre-computed signature
ContentSigner contentSigner = new PrecomputedContentSigner(externalSignature);
JcaSignerInfoGeneratorBuilder sigb = new JcaSignerInfoGeneratorBuilder(
new JcaDigestCalculatorProviderBuilder().build()
);
sigb.setSignedAttributeGenerator(attrGen);
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
gen.addCertificates(new JcaCertStore(Arrays.asList(certificateChain)));
gen.addSignerInfoGenerator(sigb.build(contentSigner, new X509CertificateHolder(cert2)));
// Use the captured document content
CMSTypedData msg = new CMSProcessableByteArray(session.getDataToSign());
CMSSignedData signedData = gen.generate(msg, false);
byte[] cmsSignature = signedData.getEncoded();
session.getExternalSigningSupport().setSignature(cmsSignature);
} finally {
session.close();
}
}
/**
* ContentSigner that uses a pre-computed signature
*/
private static class PrecomputedContentSigner implements ContentSigner {
private final byte[] signature;
private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
public PrecomputedContentSigner(byte[] signature) {
this.signature = signature;
}
@Override
public byte[] getSignature() {
return signature;
}
@Override
public OutputStream getOutputStream() {
return outputStream;
}
@Override
public AlgorithmIdentifier getAlgorithmIdentifier() {
return new AlgorithmIdentifier(new ASN1ObjectIdentifier("1.2.840.113549.1.1.11"));
}
}
}
And the following is to call the above class:
File inputFile = new File(inPdfPath1);
File outputFile = new File(inPdfPath2);
CreateSignature2 sig2 = new CreateSignature2(externalSignService);
CreateSignature2.LateSigningSession session = sig2.prepareSigning(inputFile, outputFile);
byte[] dataToSign = session.getDataToSign();
System.out.println(">>>>SIZE: " + dataToSign.length);
// Send to external signing service
List<byte[]> dataList = Arrays.asList(dataToSign);
List<byte[]> signatures = externalSignService.sign(dataList);
byte[] signature = signatures.get(0);
// Complete the signing stage
sig2.completeSigning(session, signature);
Depends on the above CreateSignature2 class, please give me a hand and advice how can I implement the external and late sign using PDFBox? Thanks.
The CMSTypedData msg you create in prepareSigning is a CMSProcessableInputStream instance.
As the name indicates, it is based on an InputStream. This InputStream in the first use of msg (in prepareSigning) is read to its end and msg returns the data of the stream.
But if msg is re-used, the stream already is at the end and msg returns the data of an empty stream. This happens in your completeSigning where you use session.getMessage() which holds the previously used msg from prepareSigning.
Thus, you generate a signature value for the original signed attributes with the correct PDF hash. But in the resulting signature container you have signed attributes that contain the hash of an empty PDF.