【问题标题】:Verify the byte range of a PDSignature in a pdf file using PDFBox使用 PDFBox 验证 pdf 文件中 PDSignature 的字节范围
【发布时间】:2020-01-25 21:00:00
【问题描述】:

我已经加载了一个 PDDocument。

我检索到名为 sig 的 PDSignature 对象。

签名的字节范围由sig.getByteRange()提供。在我的情况下是:

0-18373 43144-46015

我想验证签名的字节范围是否有效。 因为签名必须验证整个文件期望本身。 字节范围也是由签名提供的,所以我不能依赖它。

我可以检查第一个值是 0,最后一个值必须是文件的大小 -1。

但我还需要验证第二个和第三个值(18373 和 43144)。因此,我需要知道 PDSignature 在文档中的位置及其长度。

我如何获得这些?

【问题讨论】:

    标签: java pdf pdfbox


    【解决方案1】:

    查看 PDFBox 示例ShowSignature。它间接地做到了这一点:它检查字节范围间隙中的字节是否与文档解析确定的签名值完全一致。

    showSignature方法中:

    int[] byteRange = sig.getByteRange();
    if (byteRange.length != 4)
    {
        System.err.println("Signature byteRange must have 4 items");
    }
    else
    {
        long fileLen = infile.length();
        long rangeMax = byteRange[2] + (long) byteRange[3];
        // multiply content length with 2 (because it is in hex in the PDF) and add 2 for < and >
        int contentLen = contents.getString().length() * 2 + 2;
        if (fileLen != rangeMax || byteRange[0] != 0 || byteRange[1] + contentLen != byteRange[2])
        {
            // a false result doesn't necessarily mean that the PDF is a fake
            // see this answer why:
            // https://stackoverflow.com/a/48185913/535646
            System.out.println("Signature does not cover whole document");
        }
        else
        {
            System.out.println("Signature covers whole document");
        }
        checkContentValueWithFile(infile, byteRange, contents);
    }
    

    辅助方法checkContentValueWithFile:

    private void checkContentValueWithFile(File file, int[] byteRange, COSString contents) throws IOException
    {
        // https://stackoverflow.com/questions/55049270
        // comment by mkl: check whether gap contains a hex value equal
        // byte-by-byte to the Content value, to prevent attacker from using a literal string
        // to allow extra space
        try (RandomAccessBufferedFileInputStream raf = new RandomAccessBufferedFileInputStream(file))
        {
            raf.seek(byteRange[1]);
            int c = raf.read();
            if (c != '<')
            {
                System.err.println("'<' expected at offset " + byteRange[1] + ", but got " + (char) c);
            }
            byte[] contentFromFile = raf.readFully(byteRange[2] - byteRange[1] - 2);
            byte[] contentAsHex = Hex.getString(contents.getBytes()).getBytes(Charsets.US_ASCII);
            if (contentFromFile.length != contentAsHex.length)
            {
                System.err.println("Raw content length from file is " +
                        contentFromFile.length +
                        ", but internal content string in hex has length " +
                        contentAsHex.length);
            }
            // Compare the two, we can't do byte comparison because of upper/lower case
            // also check that it is really hex
            for (int i = 0; i < contentFromFile.length; ++i)
            {
                try
                {
                    if (Integer.parseInt(String.valueOf((char) contentFromFile[i]), 16) !=
                        Integer.parseInt(String.valueOf((char) contentAsHex[i]), 16))
                    {
                        System.err.println("Possible manipulation at file offset " +
                                (byteRange[1] + i + 1) + " in signature content");
                        break;
                    }
                }
                catch (NumberFormatException ex)
                {
                    System.err.println("Incorrect hex value");
                    System.err.println("Possible manipulation at file offset " +
                            (byteRange[1] + i + 1) + " in signature content");
                    break;
                }
            }
            c = raf.read();
            if (c != '>')
            {
                System.err.println("'>' expected at offset " + byteRange[2] + ", but got " + (char) c);
            }
        }
    }
    

    (严格来说,普通括号中的二进制字符串也可以只要填满整个间隙,它不必是十六进制字符串。)

    【讨论】:

    • 这就是我想要的,但缺少一件事。在第一个 sn-p 中,未验证 byteRange[1] 的值。它必须是签名的开头。你能解释一下吗?
    • 如答案中所说,它是间接这样做的:它检查字节范围间隙中的字节是否与文档解析确定的签名值完全一致。首先sn-p调用方法checkContentValueWithFile,该方法检查从byteRange[1](应该是byteRange[0] + byteRange[1]但之前检查byteRange[0] == 0)到byteRange[2]之间的PDF文件内容是否为正是读取的签名值内容。只要签名在数学上是正确的,实际上也不可能将其包含在签名数据中。
    • 在代码 sn-p 中,仅验证签名的长度 (byteRange[1] + contentLen != byteRange[2])。内容也在 checkContentValueWithFile 中得到验证。但不是签名 byteRange[1] 的起始位置 :-)
    • "但不是签名 byteRange[1] 的起始位置 :-)" - 不是显式而是隐式:如果签名在数学上是正确的,它必须(实际上)在间隙中,并且如果间隙长度与解析的签名字符串一样长(在sn-p中检查),则签名字符串必须完全填充间隙,因此签名必须从byteRange[1]开始。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-06-15
    • 2018-03-20
    • 1970-01-01
    • 2015-11-19
    • 2017-12-04
    • 2016-10-02
    • 1970-01-01
    相关资源
    最近更新 更多