Reputation: 2316
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?
Upvotes: 0
Views: 1764
Reputation: 95918
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.)
Upvotes: 4