I have created PDF/A document with attachment. I have signed it twice (first with PdfSigner.CERTIFIED_FORM_FILLING and second time with PdfSigner.NOT_CERTIFIED). Then I verified the signatures. All the Code I used came from iText7 provided examples. Verification went as expected, but when I opened the document in Adobe, it gave me an error on the first signature, saying that the document have been changed ('Error during signature verification. Unexpected Byte range values defining scope of signed data').
The Code I have used to create the PDF:
public class PdfA3WitAttachment {
public static final String DATA = "resources/data/united_states.csv";
public static final String FONT = "resources/fonts/FreeSans.ttf";
public static final String BOLD_FONT = "resources/fonts/FreeSansBold.ttf";
public static final String INTENT = "resources/color/sRGB_CS_profile.icm";
/** An image resource. */
public static final String FOX = "resources/images/fox.bmp";
/** An image resource. */
public static final String DOG = "resources/images/dog.bmp";
public static final String DEST = "results/withAttachment.pdf";
public static void main(String args[]) throws IOException {
File file = new File(DEST);
file.getParentFile().mkdirs();
new PdfA3WitAttachment().createPdf(DEST);
}
public void createPdf(String dest) throws IOException {
PdfADocument pdf = new PdfADocument(new PdfWriter(dest),
PdfAConformanceLevel.PDF_A_3A,
new PdfOutputIntent("Custom", "", "http://www.color.org",
"sRGB IEC61966-2.1", new FileInputStream(INTENT)));
Document document = new Document(pdf, PageSize.A4.rotate());
document.setMargins(20, 20, 20, 20);
//Setting some required parameters
pdf.setTagged();
pdf.getCatalog().setLang(new PdfString("en-US"));
pdf.getCatalog().setViewerPreferences(
new PdfViewerPreferences().setDisplayDocTitle(true));
PdfDocumentInfo info = pdf.getDocumentInfo();
info.setTitle("iText7 PDF/A-3 example");
//Add attachment
PdfDictionary parameters = new PdfDictionary();
parameters.put(PdfName.ModDate, new PdfDate().getPdfObject());
PdfFileSpec fileSpec = PdfFileSpec.createEmbeddedFileSpec(
pdf, Files.readAllBytes(Paths.get(DATA)), "united_states.csv",
"united_states.csv", new PdfName("text/csv"), parameters,
PdfName.Data, false);
fileSpec.put(new PdfName("AFRelationship"), new PdfName("Data"));
pdf.addFileAttachment("united_states.csv", fileSpec);
PdfArray array = new PdfArray();
array.add(fileSpec.getPdfObject().getIndirectReference());
pdf.getCatalog().put(new PdfName("AF"), array);
//Embed fonts
PdfFont font = PdfFontFactory.createFont(FONT, true);
// Create content
Image fox = new Image(ImageDataFactory.create(FOX));
fox.getAccessibilityProperties().setAlternateDescription("fox");
Image dog = new Image(ImageDataFactory.create(DOG));
dog.getAccessibilityProperties().setAlternateDescription("dog");
document.add(
new Paragraph()
.setFont(font)
.setFontSize(20)
.add(new Text("The quick brown "))
.add(fox)
.add(new Text(" jumps over the lazy "))
.add(dog));
// step 4
document.close();
//Close document
document.close();
}
public void process(Table table, String line, PdfFont font, boolean isHeader) {
StringTokenizer tokenizer = new StringTokenizer(line, ";");
while (tokenizer.hasMoreTokens()) {
if (isHeader) {
table.addHeaderCell(new Cell().setHorizontalAlignment(HorizontalAlignment.CENTER).add(new Paragraph(tokenizer.nextToken()).setHorizontalAlignment(HorizontalAlignment.CENTER).setFont(font)));
} else {
table.addCell(new Cell().setHorizontalAlignment(HorizontalAlignment.CENTER).add(new Paragraph(tokenizer.nextToken()).setHorizontalAlignment(HorizontalAlignment.CENTER).setFont(font)));
}
}
}
}
The Code used for signing and verification:
public class SignWithCertificate {
/*public static final String SRC = "./results/zugferd/pdfa/quickbrownfox4.pdf";
public static final String DEST = "./results/zugferd/pdfa/quickbrownfox4_cacert.pdf";
public static final String DEST1 = "./results/zugferd/pdfa/quickbrownfox4_cacert_sign.pdf";*/
public static final String SRC = "./results/withAttachment.pdf";
public static final String DEST = "./results/withAttachment_cacert.pdf";
public static final String DEST1 = "./results/withAttachment_cacert_sign.pdf";
public void sign(String src, String dest, Certificate[] chain, PrivateKey pk, String digestAlgorithm, String provider, PdfSigner.CryptoStandard subfilter,
String reason, String location, IOcspClient ocspClient, ITSAClient tsaClient, int estimatedSize)
throws GeneralSecurityException, IOException {
boolean appendMode = false;
System.out.println("_____________CERTIFICATE SIGN________________");
System.out.println(appendMode);
System.out.println("_____________________________________________");
// Creating the reader and the signer
PdfReader reader = new PdfReader(src);
PdfSigner signer = new PdfSigner(reader, new FileOutputStream(dest), appendMode);
signer.setCertificationLevel(PdfSigner.CERTIFIED_FORM_FILLING);
// Creating the appearance
PdfSignatureAppearance appearance = signer.getSignatureAppearance().setReason(reason).setLocation(location).setReuseAppearance(false);
Rectangle rect = new Rectangle(36, 648, 200, 100);
appearance.setPageRect(rect).setPageNumber(1);
signer.setFieldName("cert");
// Creating the signature
IExternalSignature pks = new PrivateKeySignature(pk, digestAlgorithm, provider);
IExternalDigest digest = new BouncyCastleDigest();
Collection<ICrlClient> crlList = new ArrayList<>();
crlList.add(new CrlClientOnline(chain));
signer.signDetached(digest, pks, chain, crlList, ocspClient, tsaClient, estimatedSize, subfilter);
}
public void signAgain(String src, String dest, Certificate[] chain, PrivateKey pk, String digestAlgorithm, String provider,
PdfSigner.CryptoStandard subfilter, String reason, String location, IOcspClient ocspClient, ITSAClient tsaClient,
int estimatedSize) throws GeneralSecurityException, IOException {
boolean appendMode = true;
System.out.println("_______________________ SIGN________________");
System.out.println(appendMode);
System.out.println("_____________________________________________");
// Creating the reader and the signer
PdfReader reader = new PdfReader(src);
PdfSigner signer = new PdfSigner(reader, new FileOutputStream(dest), appendMode);
signer.setCertificationLevel(PdfSigner.NOT_CERTIFIED);
// Creating the appearance
PdfSignatureAppearance appearance = signer.getSignatureAppearance().setReason(reason).setLocation(location).setReuseAppearance(false);
Rectangle rect = new Rectangle(246, 648, 200, 100);
appearance.setPageRect(rect).setPageNumber(1);
signer.setFieldName("sig");
// Creating the signature
IExternalSignature pks = new PrivateKeySignature(pk, digestAlgorithm, provider);
IExternalDigest digest = new BouncyCastleDigest();
Collection<ICrlClient> crlList = new ArrayList<>();
crlList.add(new CrlClientOnline(chain));
signer.signDetached(digest, pks, chain, crlList, ocspClient, tsaClient, estimatedSize, subfilter);
}
public void verifySignatures(String path) throws IOException, GeneralSecurityException {
System.out.println(path);
PdfReader reader = new PdfReader(path);
PdfDocument pdfDoc = new PdfDocument(reader);
SignatureUtil util = new SignatureUtil(pdfDoc);
List<String> names = util.getSignatureNames();
for (String name : names) {
System.out.println("===== " + name + " =====");
verifySignature(util, name);
}
System.out.println();
}
public PdfPKCS7 verifySignature(SignatureUtil util, String name) throws GeneralSecurityException, IOException {
System.out.println("Signature covers whole document: " + util.signatureCoversWholeDocument(name));
System.out.println("Document revision: " + util.getRevision(name) + " of " + util.getTotalRevisions());
PdfPKCS7 pkcs7 = util.verifySignature(name);
System.out.println("Integrity check OK? " + pkcs7.verify());
return pkcs7;
}
public void inspectSignatures(String path) throws IOException, GeneralSecurityException {
System.out.println(path);
PdfReader reader = new PdfReader(path);
PdfDocument pdfDoc = new PdfDocument(reader);
PdfAcroForm form = PdfAcroForm.getAcroForm(pdfDoc, true);
Map<String, PdfFormField> fields = form.getFormFields();
SignatureUtil util = new SignatureUtil(pdfDoc);
List<String> names = util.getSignatureNames();
SignaturePermissions perms = null;
for (String name : names) {
System.out.println("===== " + name + " =====");
perms = inspectSignature(fields.get(name), name, perms, util);
}
System.out.println();
}
public SignaturePermissions inspectSignature(PdfFormField field, String name, SignaturePermissions perms, SignatureUtil util) throws GeneralSecurityException, IOException {
PdfArray position = field.getWidgets().get(0).getRectangle();
if (position != null && position.size() > 0) {
float width = (float) (position.getAsNumber(2).getValue() - position.getAsNumber(0).getValue());
float height = (float) (position.getAsNumber(3).getValue() - position.getAsNumber(1).getValue());
if (width == 0 || height == 0) {
System.out.println("Invisible signature");
} else {
PdfPage page = field.getWidgets().get(0).getPage();
System.out.println(String.format("Field on page %s; llx: %s, lly: %s, urx: %s; ury: %s", page, position.getAsNumber(0).getValue(), position.getAsNumber(1).getValue(),
position.getAsNumber(2).getValue(), position.getAsNumber(3).getValue()));
}
}
PdfPKCS7 pkcs7 = verifySignature(util, name);
System.out.println("Digest algorithm: " + pkcs7.getHashAlgorithm());
System.out.println("Encryption algorithm: " + pkcs7.getEncryptionAlgorithm());
System.out.println("Filter subtype: " + pkcs7.getFilterSubtype());
X509Certificate cert = (X509Certificate) pkcs7.getSigningCertificate();
System.out.println("Name of the signer: " + CertificateInfo.getSubjectFields(cert).getField("CN"));
if (pkcs7.getSignName() != null)
System.out.println("Alternative name of the signer: " + pkcs7.getSignName());
SimpleDateFormat date_format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SS");
System.out.println("Signed on: " + date_format.format(pkcs7.getSignDate().getTime()));
if (pkcs7.getTimeStampDate() != null) {
System.out.println("TimeStamp: " + date_format.format(pkcs7.getTimeStampDate().getTime()));
TimeStampToken ts = pkcs7.getTimeStampToken();
System.out.println("TimeStamp service: " + ts.getTimeStampInfo().getTsa());
System.out.println("TimeStamp verified? " + pkcs7.verifyTimestampImprint());
}
System.out.println("Location: " + pkcs7.getLocation());
System.out.println("Reason: " + pkcs7.getReason());
PdfDictionary sigDict = util.getSignatureDictionary(name);
PdfString contact = sigDict.getAsString(PdfName.ContactInfo);
if (contact != null)
System.out.println("Contact info: " + contact);
perms = new SignaturePermissions(sigDict, perms);
System.out.println("Signature type: " + (perms.isCertification() ? "certification" : "approval"));
System.out.println("Filling out fields allowed: " + perms.isFillInAllowed());
System.out.println("Adding annotations allowed: " + perms.isAnnotationsAllowed());
for (FieldLock lock : perms.getFieldLocks()) {
System.out.println("Lock: " + lock.toString());
}
return perms;
}
public static void main(String[] args) throws IOException, GeneralSecurityException {
String path = "./src/test/SupplierX.p12";
String astrasKSpath = "./src/test/ASTRAS_APP.p12";
char[] pass = "password".toCharArray();
BouncyCastleProvider provider = new BouncyCastleProvider();
Security.addProvider(provider);
KeyStore ks = KeyStore.getInstance("pkcs12", provider.getName());
ks.load(new FileInputStream(path), pass);
KeyStore astrasKs = KeyStore.getInstance("pkcs12", provider.getName());
astrasKs.load(new FileInputStream(astrasKSpath), pass);
SignWithCertificate app = new SignWithCertificate();
String alias = "ASTRAS_APP";
PrivateKey pk = (PrivateKey) astrasKs.getKey(alias, pass);
Certificate[] chain = astrasKs.getCertificateChain(alias);
app.sign(SRC, DEST, chain, pk, DigestAlgorithms.SHA256, provider.getName(), PdfSigner.CryptoStandard.CMS, "Test Sign Certificate", "München", null, null, 0);
alias = "SupplierX";
pk = (PrivateKey) ks.getKey(alias, pass);
chain = ks.getCertificateChain(alias);
app.signAgain(DEST, DEST1, chain, pk, DigestAlgorithms.SHA256, provider.getName(), PdfSigner.CryptoStandard.CMS, "Test Sign", "München", null, null, 0);
app.verifySignatures(DEST);
app.verifySignatures(DEST1);
}
}
I am wondering how come that iText verifyies and Adobe does not. Any help much appreciated (the produced files are here: https://drive.google.com/drive/folders/1e8gILlclqftqQRcRb7ZurRzqU-6h1oN-).