检测签名之间对已签名PDF所做的更改

2022-04-25 00:00:00 digital-signature java pdfbox itext

我正在开发一个应该验证pdf文件签名的应用程序。应用程序应在应用每个签名之前检测对文件内容进行的更新的完整历史记录。 例如:

  1. 签名者%1签署了纯pdf文件
  2. 签名者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)

相关文章