本文介紹了檢測簽名之間對已簽名PDF所做的更改的處理方法,對大家解決問題具有一定的參考價值,需要的朋友們下面隨著小編來一起學習吧!
問題描述
我正在開發一個應該驗證pdf文件簽名的應用程序。應用程序應在應用每個簽名之前檢測對文件內容進行的更新的完整歷史記錄。
例如:
-
簽名者%1簽署了純pdf文件
簽名者2向簽名文件添加了注釋,然后對其簽名
應用程序如何檢測到簽名者2在其簽名之前添加了注釋。
我已經嘗試使用itext和pdfbox
推薦答案
已經在comment中解釋過,iText和PDFBox都沒有提供高級接口,告訴您在增量更新過程中在UI對象(注釋、文本內容等)方面發生了哪些變化。
您可以使用它們將PDF的不同修訂呈現為位圖并比較這些圖像。
或者您可以使用它們告訴您低級COS對象(字典、數組、數字、字符串等)的變化。
但分析這些圖像或低級別對象中的更改并確定它們在用戶界面對象方面的含義,例如添加了評論和僅添加了評論,這是非常重要的。
In response您要求
能否解釋一下,如何檢測低級COS對象的變化。
要比較的內容和要考慮的更改
首先,您必須清楚可以比較哪些文檔狀態來檢測更改。
PDF格式允許在所謂的增量更新中將更改追加到PDF。這允許對簽名文檔進行更改,而無需加密破壞這些簽名,因為原始簽名字節保持原樣:
但是,中間可能有更多未簽名的增量更新;例如,版本2的更改可能包括多個增量更新。
可以考慮比較由任意增量更新創建的修訂。然而,這里的問題是,如果沒有簽名,您無法識別應用增量更新的人。
因此,通常更有意義的做法是只比較簽名的修訂版,并讓每個簽名者負責自上一個簽名修訂版以來的所有更改。此處唯一的例外是整個文件,因為它是PDF的當前版本,即使沒有覆蓋所有文件的簽名也是特別重要的。
接下來,您必須決定您認為什么是更改。特別是:
增量更新中的每個對象覆蓋都是更改嗎?即使是用完全相同的副本覆蓋原始對象的對象?
使直接對象成為間接對象(反之亦然)但保持所有內容和引用不變的更改怎么辦?
添加未從標準結構中的任何位置引用的新對象怎么辦?
添加未從交叉引用流或表引用的對象怎么辦?
添加根本不遵循PDF語法的數據怎么辦?
如果您也對此類更改感興趣,現有的現成PDF庫通常不會為您提供確定更改的方法;您很可能至少必須更改它們的代碼以遍歷交叉引用表/流的鏈,甚至直接分析更新中的文件字節。
如果您對此類更改不感興趣,則通常不需要更改或替換庫例程。
由于由符合規范的PDF處理器處理PDF時,列舉的和類似的更改沒有區別,因此通常可以忽略此類更改。
如果這也是您的立場,下面的示例工具可能會為您提供一個起點。
基于iText 7的示例工具
使用上述限制,您可以在不更改庫的情況下使用iText 7比較PDF的簽名修訂版,方法是將要比較的修訂版加載到單獨的PdfDocument
實例中,并從尾部開始遞歸比較PDF對象。
我曾經將其作為個人使用的小型輔助工具來實現(因此它還沒有完全完成,更多的工作正在進行中)。首先是允許比較兩個任意文檔的基類:
public class PdfCompare {
public static void main(String[] args) throws IOException {
System.out.printf("Comparing:
* %s
* %s
", args[0], args[1]);
try ( PdfDocument pdfDocument1 = new PdfDocument(new PdfReader(args[0]));
PdfDocument pdfDocument2 = new PdfDocument(new PdfReader(args[1])) ) {
PdfCompare pdfCompare = new PdfCompare(pdfDocument1, pdfDocument2);
pdfCompare.compare();
List<Difference> differences = pdfCompare.getDifferences();
if (differences == null || differences.isEmpty()) {
System.out.println("No differences found.");
} else {
System.out.printf("%d differences found:
", differences.size());
for (Difference difference : pdfCompare.getDifferences()) {
for (String element : difference.getPath()) {
System.out.print(element);
}
System.out.printf(" - %s
", difference.getDescription());
}
}
}
}
public interface Difference {
List<String> getPath();
String getDescription();
}
public PdfCompare(PdfDocument pdfDocument1, PdfDocument pdfDocument2) {
trailer1 = pdfDocument1.getTrailer();
trailer2 = pdfDocument2.getTrailer();
}
public void compare() {
LOGGER.info("Starting comparison");
try {
compared.clear();
differences.clear();
LOGGER.info("START COMPARE");
compare(trailer1, trailer2, Collections.singletonList("trailer"));
LOGGER.info("START SHORTEN PATHS");
shortenPaths();
} finally {
LOGGER.info("Finished comparison and shortening");
}
}
public List<Difference> getDifferences() {
return differences;
}
class DifferenceImplSimple implements Difference {
DifferenceImplSimple(PdfObject object1, PdfObject object2, List<String> path, String description) {
this.pair = Pair.of(object1, object2);
this.path = path;
this.description = description;
}
@Override
public List<String> getPath() {
List<String> byPair = getShortestPath(pair);
return byPair != null ? byPair : shorten(path);
}
@Override public String getDescription() { return description; }
final Pair<PdfObject, PdfObject> pair;
final List<String> path;
final String description;
}
void compare(PdfObject object1, PdfObject object2, List<String> path) {
LOGGER.debug("Comparing objects at {}.", path);
if (object1 == null && object2 == null)
{
LOGGER.debug("Both objects are null at {}.", path);
return;
}
if (object1 == null) {
differences.add(new DifferenceImplSimple(object1, object2, path, "Missing in document 1"));
LOGGER.info("Object in document 1 is missing at {}.", path);
return;
}
if (object2 == null) {
differences.add(new DifferenceImplSimple(object1, object2, path, "Missing in document 2"));
LOGGER.info("Object in document 2 is missing at {}.", path);
return;
}
if (object1.getType() != object2.getType()) {
differences.add(new DifferenceImplSimple(object1, object2, path,
String.format("Type difference, %s in document 1 and %s in document 2",
getTypeName(object1.getType()), getTypeName(object2.getType()))));
LOGGER.info("Objects have different types at {}, {} and {}.", path, getTypeName(object1.getType()), getTypeName(object2.getType()));
return;
}
switch (object1.getType()) {
case PdfObject.ARRAY:
compareContents((PdfArray) object1, (PdfArray) object2, path);
break;
case PdfObject.DICTIONARY:
compareContents((PdfDictionary) object1, (PdfDictionary) object2, path);
break;
case PdfObject.STREAM:
compareContents((PdfStream)object1, (PdfStream)object2, path);
break;
case PdfObject.BOOLEAN:
case PdfObject.INDIRECT_REFERENCE:
case PdfObject.LITERAL:
case PdfObject.NAME:
case PdfObject.NULL:
case PdfObject.NUMBER:
case PdfObject.STRING:
compareContentsSimple(object1, object2, path);
break;
default:
differences.add(new DifferenceImplSimple(object1, object2, path, "Unknown object type " + object1.getType() + "; cannot compare"));
LOGGER.warn("Unknown object type at {}, {}.", path, object1.getType());
break;
}
}
void compareContents(PdfArray array1, PdfArray array2, List<String> path) {
int count1 = array1.size();
int count2 = array2.size();
if (count1 < count2) {
differences.add(new DifferenceImplSimple(array1, array2, path, "Document 1 misses " + (count2-count1) + " array entries"));
LOGGER.info("Array in document 1 is missing {} entries at {} for {}.", (count2-count1), path);
}
if (count1 > count2) {
differences.add(new DifferenceImplSimple(array1, array2, path, "Document 2 misses " + (count1-count2) + " array entries"));
LOGGER.info("Array in document 2 is missing {} entries at {} for {}.", (count1-count2), path);
}
if (alreadyCompared(array1, array2, path)) {
return;
}
int count = Math.min(count1, count2);
for (int i = 0; i < count; i++) {
compare(array1.get(i), array2.get(i), join(path, String.format("[%d]", i)));
}
}
void compareContents(PdfDictionary dictionary1, PdfDictionary dictionary2, List<String> path) {
List<PdfName> missing1 = new ArrayList<PdfName>(dictionary2.keySet());
missing1.removeAll(dictionary1.keySet());
if (!missing1.isEmpty()) {
differences.add(new DifferenceImplSimple(dictionary1, dictionary2, path, "Document 1 misses dictionary entries for " + missing1));
LOGGER.info("Dictionary in document 1 is missing entries at {} for {}.", path, missing1);
}
List<PdfName> missing2 = new ArrayList<PdfName>(dictionary1.keySet());
missing2.removeAll(dictionary2.keySet());
if (!missing2.isEmpty()) {
differences.add(new DifferenceImplSimple(dictionary1, dictionary2, path, "Document 2 misses dictionary entries for " + missing2));
LOGGER.info("Dictionary in document 2 is missing entries at {} for {}.", path, missing2);
}
if (alreadyCompared(dictionary1, dictionary2, path)) {
return;
}
List<PdfName> common = new ArrayList<PdfName>(dictionary1.keySet());
common.retainAll(dictionary2.keySet());
for (PdfName name : common) {
compare(dictionary1.get(name), dictionary2.get(name), join(path, name.toString()));
}
}
void compareContents(PdfStream stream1, PdfStream stream2, List<String> path) {
compareContents((PdfDictionary)stream1, (PdfDictionary)stream2, path);
byte[] bytes1 = stream1.getBytes();
byte[] bytes2 = stream2.getBytes();
if (!Arrays.equals(bytes1, bytes2)) {
differences.add(new DifferenceImplSimple(stream1, stream2, path, "Stream contents differ"));
LOGGER.info("Stream contents differ at {}.", path);
}
}
void compareContentsSimple(PdfObject object1, PdfObject object2, List<String> path) {
// vvv--- work-around for DEVSIX-4931, likely to be fixed in 7.1.15
if (object1 instanceof PdfNumber)
((PdfNumber)object1).getValue();
if (object2 instanceof PdfNumber)
((PdfNumber)object2).getValue();
// ^^^--- work-around for DEVSIX-4931, likely to be fixed in 7.1.15
if (!object1.equals(object2)) {
if (object1 instanceof PdfString) {
String string1 = object1.toString();
if (string1.length() > 40)
string1 = string1.substring(0, 40) + 'u22EF';
string1 = sanitize(string1);
String string2 = object2.toString();
if (string2.length() > 40)
string2 = string2.substring(0, 40) + 'u22EF';
string2 = sanitize(string2);
differences.add(new DifferenceImplSimple(object1, object2, path, String.format("String values differ, '%s' and '%s'", string1, string2)));
LOGGER.info("String values differ at {}, '{}' and '{}'.", path, string1, string2);
} else {
differences.add(new DifferenceImplSimple(object1, object2, path, String.format("Object values differ, '%s' and '%s'", object1, object2)));
LOGGER.info("Object values differ at {}, '{}' and '{}'.", path, object1, object2);
}
}
}
String sanitize(CharSequence string) {
char[] sanitized = new char[string.length()];
for (int i = 0; i < sanitized.length; i++) {
char c = string.charAt(i);
if (c >= 0 && c < ' ')
c = 'uFFFD';
sanitized[i] = c;
}
return new String(sanitized);
}
String getTypeName(byte type) {
switch (type) {
case PdfObject.ARRAY: return "ARRAY";
case PdfObject.BOOLEAN: return "BOOLEAN";
case PdfObject.DICTIONARY: return "DICTIONARY";
case PdfObject.LITERAL: return "LITERAL";
case PdfObject.INDIRECT_REFERENCE: return "REFERENCE";
case PdfObject.NAME: return "NAME";
case PdfObject.NULL: return "NULL";
case PdfObject.NUMBER: return "NUMBER";
case PdfObject.STREAM: return "STREAM";
case PdfObject.STRING: return "STRING";
default:
return "UNKNOWN";
}
}
List<String> join(List<String> path, String element) {
String[] array = path.toArray(new String[path.size() + 1]);
array[array.length-1] = element;
return Arrays.asList(array);
}
boolean alreadyCompared(PdfObject object1, PdfObject object2, List<String> path) {
Pair<PdfObject, PdfObject> pair = Pair.of(object1, object2);
if (compared.containsKey(pair)) {
//LOGGER.debug("Objects already compared at {}, previously at {}.", path, compared.get(pair));
Set<List<String>> paths = compared.get(pair);
boolean alreadyPresent = false;
// List<List<String>> toRemove = new ArrayList<>();
// for (List<String> formerPath : paths) {
// for (int i = 0; ; i++) {
// if (i == path.size()) {
// toRemove.add(formerPath);
// System.out.print('.');
// break;
// }
// if (i == formerPath.size()) {
// alreadyPresent = true;
// System.out.print(':');
// break;
// }
// if (!path.get(i).equals(formerPath.get(i)))
// break;
// }
// }
// paths.removeAll(toRemove);
if (!alreadyPresent)
paths.add(path);
return true;
}
compared.put(pair, new HashSet<>(Collections.singleton(path)));
return false;
}
List<String> getShortestPath(Pair<PdfObject, PdfObject> pair) {
Set<List<String>> paths = compared.get(pair);
//return (paths == null) ? null : Collections.min(paths, pathComparator);
return (paths == null || paths.isEmpty()) ? null : shortened.get(paths.stream().findFirst().get());
}
void shortenPaths() {
List<Map<List<String>, SortedSet<List<String>>>> data = new ArrayList<>();
for (Set<List<String>> set : compared.values()) {
SortedSet<List<String>> sortedSet = new TreeSet<List<String>>(pathComparator);
sortedSet.addAll(set);
for (List<String> path : sortedSet) {
while (path.size() >= data.size()) {
data.add(new HashMap<>());
}
SortedSet<List<String>> former = data.get(path.size()).put(path, sortedSet);
if (former != null) {
LOGGER.error("Path not well-defined for {}", path);
}
}
}
for (int pathSize = 3; pathSize < data.size(); pathSize++) {
for (Map.Entry<List<String>, SortedSet<List<String>>> pathEntry : data.get(pathSize).entrySet()) {
List<String> path = pathEntry.getKey();
SortedSet<List<String>> equivalents = pathEntry.getValue();
for (int subpathSize = 2; subpathSize < pathSize; subpathSize++) {
List<String> subpath = path.subList(0, subpathSize);
List<String> remainder = path.subList(subpathSize, pathSize);
SortedSet<List<String>> subequivalents = data.get(subpathSize).get(subpath);
if (subequivalents != null && subequivalents.size() > 1) {
List<String> subequivalent = subequivalents.first();
if (subequivalent.size() < subpathSize) {
List<String> replacement = join(subequivalent, remainder);
if (equivalents.add(replacement)) {
data.get(replacement.size()).put(replacement, equivalents);
}
}
}
}
}
}
shortened.clear();
for (Map<List<String>, SortedSet<List<String>>> singleLengthData : data) {
for (Map.Entry<List<String>, SortedSet<List<String>>> entry : singleLengthData.entrySet()) {
List<String> path = entry.getKey();
List<String> shortenedPath = entry.getValue().first();
shortened.put(path, shortenedPath);
}
}
}
List<String> join(List<String> path, List<String> elements) {
String[] array = path.toArray(new String[path.size() + elements.size()]);
for (int i = 0; i < elements.size(); i++) {
array[path.size() + i] = elements.get(i);
}
return Arrays.asList(array);
}
List<String> shorten(List<String> path) {
List<String> shortPath = path;
for (int subpathSize = path.size(); subpathSize > 2; subpathSize--) {
List<String> subpath = path.subList(0, subpathSize);
List<String> shortSubpath = shortened.get(subpath);
if (shortSubpath != null && shortSubpath.size() < subpathSize) {
List<String> remainder = path.subList(subpathSize, path.size());
List<String> replacement = join(shortSubpath, remainder);
if (replacement.size() < shortPath.size())
shortPath = replacement;
}
}
return shortPath;
}
final static Logger LOGGER = LoggerFactory.getLogger(PdfCompare.class);
final PdfDictionary trailer1;
final PdfDictionary trailer2;
final Map<Pair<PdfObject, PdfObject>, Set<List<String>>> compared = new HashMap<>();
final List<Difference> differences = new ArrayList<>();
final Map<List<String>, List<String>> shortened = new HashMap<>();
final static Comparator<List<String>> pathComparator = new Comparator<List<String>>() {
@Override
public int compare(List<String> o1, List<String> o2) {
int compare = Integer.compare(o1.size(), o2.size());
if (compare != 0)
return compare;
for (int i = 0; i < o1.size(); i++) {
compare = o1.get(i).compareTo(o2.get(i));
if (compare != 0)
return compare;
}
return 0;
}
};
}
(PdfCompare.java)
使用此代碼進行修訂比較的工具是其子類:
public class PdfRevisionCompare extends PdfCompare {
public static void main(String[] args) throws IOException {
for (String arg : args) {
System.out.printf("
Comparing revisions of: %s
***********************
", args[0]);
try (PdfDocument pdfDocument = new PdfDocument(new PdfReader(arg))) {
SignatureUtil signatureUtil = new SignatureUtil(pdfDocument);
List<String> signatureNames = signatureUtil.getSignatureNames();
if (signatureNames.isEmpty()) {
System.out.println("No signed revisions detected. (no AcroForm)");
continue;
}
String previousRevision = signatureNames.get(0);
PdfDocument previousDocument = new PdfDocument(new PdfReader(signatureUtil.extractRevision(previousRevision)));
System.out.printf("* Initial signed revision: %s
", previousRevision);
for (int i = 1; i < signatureNames.size(); i++) {
String currentRevision = signatureNames.get(i);
PdfDocument currentDocument = new PdfDocument(new PdfReader(signatureUtil.extractRevision(currentRevision)));
showDifferences(previousDocument, currentDocument);
System.out.printf("* Next signed revision (%d): %s
", i+1, currentRevision);
previousDocument.close();
previousDocument = currentDocument;
previousRevision = currentRevision;
}
if (signatureUtil.signatureCoversWholeDocument(previousRevision)) {
System.out.println("No unsigned updates.");
} else {
showDifferences(previousDocument, pdfDocument);
System.out.println("* Final unsigned revision");
}
previousDocument.close();
}
}
}
static void showDifferences(PdfDocument previousDocument, PdfDocument currentDocument) {
PdfRevisionCompare pdfRevisionCompare = new PdfRevisionCompare(previousDocument, currentDocument);
pdfRevisionCompare.compare();
List<Difference> differences = pdfRevisionCompare.getDifferences();
if (differences == null || differences.isEmpty()) {
System.out.println("No differences found.");
} else {
System.out.printf("%d differences found:
", differences.size());
for (Difference difference : differences) {
for (String element : difference.getPath()) {
System.out.print(element);
}
System.out.printf(" - %s
", difference.getDescription());
}
}
}
public PdfRevisionCompare(PdfDocument pdfDocument1, PdfDocument pdfDocument2) {
super(pdfDocument1, pdfDocument2);
}
}
(PdfRevisionCompare.java)
這篇關于檢測簽名之間對已簽名PDF所做的更改的文章就介紹到這了,希望我們推薦的答案對大家有所幫助,