The use-case is as following: a local application must sign a PDF document using a full PKCS#7 detached signature generated on a remote server which requires a one-time-password sent by e-mail/SMS; the remote server generates the detached signature based on the associated document hash and the resulting signature is an “external signature”, as defined in the CMS RFC (RFC5652), section 5.2, such as the resulting signature file is a separate file.
I'm struggling for days now to implement a working solution for this use-case (using itextpdf 5.5.13.1), but the final signed file has the signature in error with the message "Certification by is invalid" -- see the "signed_with_error" file attached. The conceptual "two-steps signing" implemented process is:
1st step: from the original document file -- see the "original" attached file -- I create an intermediary file -- see the "intermediary" file attached -- in which I've inserted an empty signature based on which the associated document hash is created. In this step, the signature digest is saved to be used in the next step
2nd step: remote server is called, the detached signature file is received and I'm creating the signed file by inserting content in the signature from the pre-signed file using the signature digest from previous step and detached signature content received from the remote server.
Original, intermediary and signed with error sample files are:
original
intermediary
signed_with_error
Anyone has any idea of what could be wrong with my code beloow?
Relevant code parts are as following:
1st step:
ByteArrayOutputStream preSignedDocument = new ByteArrayOutputStream();
Path customerPathInDataStorage = storageService.resolveCustomerPathInDataStorage(customerExternalId);
PdfReader pdfReader = new PdfReader(originalDocumentContent);
PdfStamper stamper = PdfStamper.createSignature(pdfReader, preSignedDocument, '\0', customerPathInDataStorage.toFile(), true);
// create certificate chain using certificate received from remote server system
byte[] certificateContent = certificateInfo.getData(); // this is the customer certificate received one time from the remote server and used for every document signing initialization
X509Certificate certificate = SigningUtils.buildCertificateFromCertificateContent(certificateContent);
java.security.cert.Certificate[] certificatesChain = CertificateFactory.getInstance(CERTIFICATE_TYPE).generateCertPath(
Collections.singletonList(certificate)).getCertificates().toArray(new java.security.cert.Certificate[0]);
// create empty digital signature inside pre-signed document
PdfSignatureAppearance signatureAppearance = stamper.getSignatureAppearance();
signatureAppearance.setVisibleSignature(new Rectangle(72, 750, 400, 770), 1, "Signature_" + customerExternalId);
signatureAppearance.setCertificationLevel(PdfSignatureAppearance.CERTIFIED_NO_CHANGES_ALLOWED);
signatureAppearance.setCertificate(certificate);
CustomPreSignExternalSignature externalSignatureContainer =
new CustomPreSignExternalSignature(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);
MakeSignature.signExternalContainer(signatureAppearance, externalSignatureContainer, 8192);
ExternalDigest digest = new SignExternalDigest();
PdfPKCS7 pdfPKCS7 = new PdfPKCS7(null, certificatesChain, DOCUMENT_HASHING_ALGORITHM, null, digest, false);
byte[] signatureDigest = externalSignatureContainer.getSignatureDigest();
byte[] authAttributes = pdfPKCS7.getAuthenticatedAttributeBytes(signatureDigest, null, null,
MakeSignature.CryptoStandard.CMS);
pdfReader.close();
documentDetails.setPreSignedContent(preSignedDocument.toByteArray()); // this is the intermediary document content used in 2nd step in the line with the comment ***PRESIGNED_CONTENT****
documentDetails.setSignatureDigest(signatureDigest); // this is the signature digest used in 2nd step in the line with comment ****SIGNATURE_DIGEST****
byte[] hashForSigning = DigestAlgorithms.digest(new ByteArrayInputStream(authAttributes),
digest.getMessageDigest(DOCUMENT_HASHING_ALGORITHM));
documentDetails.setSigningHash(hashForSigning); // this is the hash sent to remote server for signing
2nd step:
// create certificate chain from detached signature
byte[] detachedSignatureContent = documentDetachedSignature.getSignature(); // this is the detached signature file content received from the remote server which contains also customer the certificate
X509Certificate certificate = SigningUtils.extractCertificateFromDetachedSignatureContent(detachedSignatureContent);
java.security.cert.Certificate[] certificatesChain = CertificateFactory.getInstance(CERTIFICATE_TYPE).generateCertPath(
Collections.singletonList(certificate)).getCertificates().toArray(new java.security.cert.Certificate[0]);
// create digital signature from detached signature
ExternalDigest digest = new SignExternalDigest();
PdfPKCS7 pdfPKCS7 = new PdfPKCS7(null, certificatesChain, DOCUMENT_HASHING_ALGORITHM, null, digest, false);
pdfPKCS7.setExternalDigest(detachedSignatureContent, null, SIGNATURE_ENCRYPTION_ALGORITHM);
byte[] signatureDigest = documentVersion.getSignatureDigest(); // this is the value from 1st step for ****SIGNATURE_DIGEST****
byte[] encodedSignature = pdfPKCS7.getEncodedPKCS7(signatureDigest, null, null, null, MakeSignature.CryptoStandard.CMS);
ExternalSignatureContainer externalSignatureContainer = new CustomExternalSignature(encodedSignature);
// add signature content to existing signature container of the intermediary PDF document
PdfReader pdfReader = new PdfReader(preSignedDocumentContent);// this is the value from 1st step for ***PRESIGNED_CONTENT****
ByteArrayOutputStream signedPdfOutput = new ByteArrayOutputStream();
MakeSignature.signDeferred(pdfReader, "Signature_" + customerExternalId, signedPdfOutput, externalSignatureContainer);
return signedPdfOutput.toByteArray();
dependencies:
public static final String DOCUMENT_HASHING_ALGORITHM = "SHA256";
public static final String CERTIFICATE_TYPE = "X.509";
public static final String SIGNATURE_ENCRYPTION_ALGORITHM = "RSA";
public class CustomPreSignExternalSignature implements ExternalSignatureContainer {
private static final Logger logger = LoggerFactory.getLogger(CustomPreSignExternalSignature.class);
private PdfDictionary dictionary;
private byte[] signatureDigest;
public CustomPreSignExternalSignature(PdfName filter, PdfName subFilter) {
dictionary = new PdfDictionary();
dictionary.put(PdfName.FILTER, filter);
dictionary.put(PdfName.SUBFILTER, subFilter);
}
@Override
public byte[] sign(InputStream data) throws GeneralSecurityException {
try {
ExternalDigest digest = new SignExternalDigest();
signatureDigest = DigestAlgorithms.digest(data, digest.getMessageDigest(DOCUMENT_HASHING_ALGORITHM));
} catch (IOException e) {
logger.error("CustomSignExternalSignature - can not create hash to be signed", e);
throw new GeneralSecurityException("CustomPreSignExternalSignature - can not create hash to be signed", e);
}
return new byte[0];
}
@Override
public void modifySigningDictionary(PdfDictionary pdfDictionary) {
pdfDictionary.putAll(dictionary);
}
public byte[] getSignatureDigest() {
return signatureDigest;
}
}
public class CustomExternalSignature implements ExternalSignatureContainer {
private byte[] signatureContent;
public CustomExternalSignature(byte[] signatureContent) {
this.signatureContent = signatureContent;
}
@Override
public byte[] sign(InputStream data) throws GeneralSecurityException {
return signatureContent;
}
@Override
public void modifySigningDictionary(PdfDictionary pdfDictionary) {
}
}
public class SignExternalDigest implements ExternalDigest {
@Override
public MessageDigest getMessageDigest(String hashAlgorithm) throws GeneralSecurityException {
return DigestAlgorithms.getMessageDigest(hashAlgorithm.toUpperCase(), null);
}
}
Later comment:
Even if it is not correct, I've observed that if, in 2nd step, instead of line:
byte[] signatureDigest = documentVersion.getSignatureDigest(); // this is the value from 1st step for ****SIGNATURE_DIGEST****
I'll use:
byte[] signatureDigest = new byte[0];
than the signed file will be generated with a visible certificate which can be validate but the PDF document is invalid with error "Document certification is INVALID". --- "The document has been altered or corrupted since Certification was applied." - see attached file signed_certification_invalid. It looks legit to be invalid, but for me is strange why in this case certification is displayed in document but it is broken when using - what I consider - "the right" signatureDigest value.