0

I have loaded a PDDocument.

I retrieved the PDSignature object named sig.

The byte range of the signature is provided by sig.getByteRange(). In my case it is:

0-18373 43144-46015

I want to verify that the byte range of the signature is valid. Because the signature has to verify the whole file expect itself. Also the byte range is provided by the signature so I cannot rely on it.

I can check the first value to be 0 and the last value has to be the size of the file -1.

But I also need to verify the second and the third value (18373 and 43144). Therefore I need to know the position of the PDSignature in the document and its length.

How do I get these?

zomega
  • 1,538
  • 8
  • 26

1 Answers1

4

Have a look at the PDFBox example ShowSignature. It does this indirectly: It checks whether the bytes in the gap of the byte ranges coincide exactly with the signature value determined by document parsing.

In the method 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);
}

The helper method 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);
        }
    }
}

(Strictly speaking a binary string in normal brackets would also be ok as long as it fills the whole gap, it needn't be a hex string.)

mkl
  • 90,588
  • 15
  • 125
  • 265
  • This is what I want but there's one thing missing. In the first snippet the value of byteRange[1] is not verified. It has to be the beginning of the signature. Can you explain that? – zomega Sep 26 '19 at 09:50
  • As said in the answer, *it does this indirectly: It checks whether the bytes in the gap of the byte ranges coincide exactly with the signature value determined by document parsing.* The first snippet calls the method `checkContentValueWithFile`, and that method checks whether the PDF file content in the gap from `byteRange[1]` (which should be `byteRange[0] + byteRange[1]` but `byteRange[0] == 0` is checked before) to `byteRange[2]` is _exactly_ the read signature value content. As long as the signature is mathematically correct, it is practically impossible to have it in the signed data, too. – mkl Sep 26 '19 at 10:09
  • In the code snippet only the length of the signature is verified (byteRange[1] + contentLen != byteRange[2]). Also the content is verified in checkContentValueWithFile. But not the start position of the signature byteRange[1] :-) – zomega Sep 26 '19 at 10:16
  • *"But not the start position of the signature byteRange[1] :-)"* - Not explicitly but implicitly: If the signature is mathematically correct, it must (practically) be in the gap, and if the gap length is as long as the parsed signature string (checked in the snippet), the signature string must exactly fill the gap, so the signature must start at `byteRange[1]`. – mkl Sep 26 '19 at 10:25