xmlsecurity PDF verify: support array ref annotations

Each pdf signature is mentioned in the Annots key of a page object.
Usually the key contains an array value. But it's valid for the key to
contain a reference to an object, where the object contains the actual
array, so support this case as well.

Also:

- stop parsing name tokens on the first seen '(' character (usually
  there is a whitespace between the two, but that's not required)
- handle \0 characters in the last 1024 bytes of the document by using
  std::search() instead of strstr().

Change-Id: I3a167b23340230f99f1ae4112473ed10e1c96b09
This commit is contained in:
Miklos Vajna
2016-10-27 13:00:27 +02:00
parent f82bd120bc
commit 3e12ef2cbb
3 changed files with 116 additions and 40 deletions

Binary file not shown.

View File

@@ -37,6 +37,12 @@ class PDFSigningTest : public test::BootstrapFixture
* had nOriginalSignatureCount signatures.
*/
void sign(const OUString& rInURL, const OUString& rOutURL, size_t nOriginalSignatureCount);
/**
* Read a pdf and make sure that it has the expected number of valid
* signatures.
*/
void verify(const OUString& rURL, size_t nCount);
public:
PDFSigningTest();
void setUp() override;
@@ -49,12 +55,15 @@ public:
void testPDFRemove();
/// Test removing all signatures from a previously multi-signed file.
void testPDFRemoveAll();
/// Test a PDF 1.4 document, signed by Adobe.
void testPDF14Adobe();
CPPUNIT_TEST_SUITE(PDFSigningTest);
CPPUNIT_TEST(testPDFAdd);
CPPUNIT_TEST(testPDFAdd2);
CPPUNIT_TEST(testPDFRemove);
CPPUNIT_TEST(testPDFRemoveAll);
CPPUNIT_TEST(testPDF14Adobe);
CPPUNIT_TEST_SUITE_END();
};
@@ -81,6 +90,21 @@ void PDFSigningTest::setUp()
#endif
}
void PDFSigningTest::verify(const OUString& rURL, size_t nCount)
{
SvFileStream aStream(rURL, StreamMode::READ);
xmlsecurity::pdfio::PDFDocument aVerifyDocument;
CPPUNIT_ASSERT(aVerifyDocument.Read(aStream));
std::vector<xmlsecurity::pdfio::PDFObjectElement*> aSignatures = aVerifyDocument.GetSignatureWidgets();
CPPUNIT_ASSERT_EQUAL(nCount, aSignatures.size());
for (size_t i = 0; i < aSignatures.size(); ++i)
{
SignatureInformation aInfo(i);
bool bLast = i == aSignatures.size() - 1;
CPPUNIT_ASSERT(xmlsecurity::pdfio::PDFDocument::ValidateSignature(aStream, aSignatures[i], aInfo, bLast));
}
}
void PDFSigningTest::sign(const OUString& rInURL, const OUString& rOutURL, size_t nOriginalSignatureCount)
{
// Make sure that input has nOriginalSignatureCount signatures.
@@ -108,21 +132,8 @@ void PDFSigningTest::sign(const OUString& rInURL, const OUString& rOutURL, size_
CPPUNIT_ASSERT(aDocument.Write(aOutStream));
}
// Read back the signed pdf and make sure that it has one valid signature.
{
SvFileStream aStream(rOutURL, StreamMode::READ);
xmlsecurity::pdfio::PDFDocument aVerifyDocument;
CPPUNIT_ASSERT(aVerifyDocument.Read(aStream));
std::vector<xmlsecurity::pdfio::PDFObjectElement*> aSignatures = aVerifyDocument.GetSignatureWidgets();
// This was nOriginalSignatureCount when PDFDocument::Sign() silently returned success, without doing anything.
CPPUNIT_ASSERT_EQUAL(nOriginalSignatureCount + 1, aSignatures.size());
for (size_t i = 0; i < aSignatures.size(); ++i)
{
SignatureInformation aInfo(i);
bool bLast = i == aSignatures.size() - 1;
CPPUNIT_ASSERT(xmlsecurity::pdfio::PDFDocument::ValidateSignature(aStream, aSignatures[i], aInfo, bLast));
}
}
// This was nOriginalSignatureCount when PDFDocument::Sign() silently returned success, without doing anything.
verify(rOutURL, nOriginalSignatureCount + 1);
}
void PDFSigningTest::testPDFAdd()
@@ -183,14 +194,9 @@ void PDFSigningTest::testPDFRemove()
}
// Read back the pdf and make sure that it no longer has signatures.
{
SvFileStream aStream(aOutURL, StreamMode::READ);
xmlsecurity::pdfio::PDFDocument aVerifyDocument;
CPPUNIT_ASSERT(aVerifyDocument.Read(aStream));
std::vector<xmlsecurity::pdfio::PDFObjectElement*> aSignatures = aVerifyDocument.GetSignatureWidgets();
// This failed when PDFDocument::RemoveSignature() silently returned success, without doing anything.
CPPUNIT_ASSERT(aSignatures.empty());
}
// This failed when PDFDocument::RemoveSignature() silently returned
// success, without doing anything.
verify(aOutURL, 0);
#endif
}
@@ -228,6 +234,17 @@ void PDFSigningTest::testPDFRemoveAll()
CPPUNIT_ASSERT_EQUAL(static_cast<std::size_t>(0), rInformations.size());
#endif
}
void PDFSigningTest::testPDF14Adobe()
{
#ifndef _WIN32
// Two signatures, first is SHA1, the second is SHA256.
// This was 0, as we failed to find the Annots key's value when it was a
// reference-to-array, not an array.
verify(m_directories.getURLFromSrc(DATA_DIRECTORY) + "pdf14adobe.pdf", 2);
#endif
}
CPPUNIT_TEST_SUITE_REGISTRATION(PDFSigningTest);
CPPUNIT_PLUGIN_IMPLEMENT();

View File

@@ -76,6 +76,7 @@ public:
class PDFReferenceElement;
class PDFDictionaryElement;
class PDFArrayElement;
/// Indirect object: something with a unique ID.
class PDFObjectElement : public PDFElement
@@ -89,6 +90,8 @@ class PDFObjectElement : public PDFElement
/// Length of the dictionary buffer till (before) the '<<' token.
sal_uInt64 m_nDictionaryLength;
PDFDictionaryElement* m_pDictionaryElement;
/// The contained direct array, if any.
PDFArrayElement* m_pArrayElement;
public:
PDFObjectElement(PDFDocument& rDoc, double fObjectValue, double fGenerationValue);
@@ -103,6 +106,8 @@ public:
sal_uInt64 GetDictionaryLength();
PDFDictionaryElement* GetDictionary() const;
void SetDictionary(PDFDictionaryElement* pDictionaryElement);
void SetArray(PDFArrayElement* pArrayElement);
PDFArrayElement* GetArray() const;
};
/// Dictionary object: a set key-value pairs.
@@ -628,6 +633,12 @@ bool PDFDocument::Tokenize(SvStream& rStream, bool bPartial)
bool bInXRef = false;
// The next number will be an xref offset.
bool bInStartXRef = false;
// Last seen object token.
PDFObjectElement* pObject = nullptr;
// Dictionary depth, so we know when we're outside any dictionaries.
int nDictionaryDepth = 0;
// Last seen array token that's outside any dictionaries.
PDFArrayElement* pArray = nullptr;
while (true)
{
char ch;
@@ -657,7 +668,10 @@ bool PDFDocument::Tokenize(SvStream& rStream, bool bPartial)
rStream.ReadChar(ch);
rStream.SeekRel(-2);
if (ch == '<')
{
m_aElements.push_back(std::unique_ptr<PDFElement>(new PDFDictionaryElement()));
++nDictionaryDepth;
}
else
m_aElements.push_back(std::unique_ptr<PDFElement>(new PDFHexStringElement()));
if (!m_aElements.back()->Read(rStream))
@@ -667,6 +681,7 @@ bool PDFDocument::Tokenize(SvStream& rStream, bool bPartial)
case '>':
{
m_aElements.push_back(std::unique_ptr<PDFElement>(new PDFEndDictionaryElement()));
--nDictionaryDepth;
rStream.SeekRel(-1);
if (!m_aElements.back()->Read(rStream))
return false;
@@ -674,7 +689,15 @@ bool PDFDocument::Tokenize(SvStream& rStream, bool bPartial)
}
case '[':
{
m_aElements.push_back(std::unique_ptr<PDFElement>(new PDFArrayElement()));
auto pArr = new PDFArrayElement();
m_aElements.push_back(std::unique_ptr<PDFElement>(pArr));
if (nDictionaryDepth == 0)
{
// The array is attached directly, inform the object.
pArray = pArr;
if (pObject)
pObject->SetArray(pArray);
}
rStream.SeekRel(-1);
if (!m_aElements.back()->Read(rStream))
return false;
@@ -683,6 +706,7 @@ bool PDFDocument::Tokenize(SvStream& rStream, bool bPartial)
case ']':
{
m_aElements.push_back(std::unique_ptr<PDFElement>(new PDFEndArrayElement()));
pArray = nullptr;
rStream.SeekRel(-1);
if (!m_aElements.back()->Read(rStream))
return false;
@@ -745,9 +769,17 @@ bool PDFDocument::Tokenize(SvStream& rStream, bool bPartial)
}
if (bObj)
m_aElements.push_back(std::unique_ptr<PDFElement>(new PDFObjectElement(*this, pObjectNumber->GetValue(), pGenerationNumber->GetValue())));
{
pObject = new PDFObjectElement(*this, pObjectNumber->GetValue(), pGenerationNumber->GetValue());
m_aElements.push_back(std::unique_ptr<PDFElement>(pObject));
}
else
{
m_aElements.push_back(std::unique_ptr<PDFElement>(new PDFReferenceElement(*this, pObjectNumber->GetValue(), pGenerationNumber->GetValue())));
if (pArray)
// Reference is part of a direct (non-dictionary) array, inform the array.
pArray->PushBack(m_aElements.back().get());
}
if (!m_aElements.back()->Read(rStream))
return false;
}
@@ -922,14 +954,14 @@ size_t PDFDocument::FindStartXRef(SvStream& rStream)
if (nSize != aBuf.size())
aBuf.resize(nSize);
OString aPrefix("startxref");
char* pOffset = strstr(aBuf.data(), aPrefix.getStr());
if (!pOffset)
auto it = std::search(aBuf.begin(), aBuf.end(), aPrefix.getStr(), aPrefix.getStr() + aPrefix.getLength());
if (it == aBuf.end())
{
SAL_WARN("xmlsecurity.pdfio", "PDFDocument::FindStartXRef: found no startxref");
return 0;
}
rStream.SeekRel(pOffset - aBuf.data() + aPrefix.getLength());
rStream.SeekRel(it - aBuf.begin() + aPrefix.getLength());
if (rStream.IsEof())
{
SAL_WARN("xmlsecurity.pdfio", "PDFDocument::FindStartXRef: unexpected end of stream after startxref");
@@ -1009,9 +1041,14 @@ void PDFDocument::ReadXRef(SvStream& rStream)
SAL_WARN("xmlsecurity.pdfio", "PDFDocument::ReadXRef: unexpected keyword");
return;
}
m_aXRef[nIndex] = aOffset.GetValue();
// Initially only the first entry is dirty.
m_aXRefDirty[nIndex] = nIndex == 0;
// xrefs are read in reverse order, so never update an existing
// offset with an older one.
if (m_aXRef.find(nIndex) == m_aXRef.end())
{
m_aXRef[nIndex] = aOffset.GetValue();
// Initially only the first entry is dirty.
m_aXRefDirty[nIndex] = nIndex == 0;
}
PDFDocument::SkipWhitespace(rStream);
}
}
@@ -1133,7 +1170,22 @@ std::vector<PDFObjectElement*> PDFDocument::GetSignatureWidgets()
for (const auto& pPage : aPages)
{
auto pAnnots = dynamic_cast<PDFArrayElement*>(pPage->Lookup("Annots"));
PDFElement* pAnnotsElement = pPage->Lookup("Annots");
auto pAnnots = dynamic_cast<PDFArrayElement*>(pAnnotsElement);
if (!pAnnots)
{
// Annots is not an array, see if it's a reference to an object
// with a direct array.
auto pAnnotsRef = dynamic_cast<PDFReferenceElement*>(pAnnotsElement);
if (pAnnotsRef)
{
if (PDFObjectElement* pAnnotsObject = pAnnotsRef->LookupObject())
{
pAnnots = pAnnotsObject->GetArray();
}
}
}
if (!pAnnots)
continue;
@@ -1438,12 +1490,8 @@ bool PDFDocument::ValidateSignature(SvStream& rStream, PDFObjectElement* pSignat
}
PRTime nSigningTime;
if (NSS_CMSSignerInfo_GetSigningTime(pCMSSignerInfo, &nSigningTime) != SECSuccess)
{
SAL_WARN("xmlsecurity.pdfio", "PDFDocument::ValidateSignature: NSS_CMSSignerInfo_GetSigningTime() failed");
return false;
}
else
// This may fail, in which case the date should be taken from the dictionary's "M" key.
if (NSS_CMSSignerInfo_GetSigningTime(pCMSSignerInfo, &nSigningTime) == SECSuccess)
{
// First convert the UNIX timestamp to an ISO8601 string.
OUStringBuffer aBuffer;
@@ -1679,7 +1727,8 @@ PDFObjectElement::PDFObjectElement(PDFDocument& rDoc, double fObjectValue, doubl
m_fGenerationValue(fGenerationValue),
m_nDictionaryOffset(0),
m_nDictionaryLength(0),
m_pDictionaryElement(nullptr)
m_pDictionaryElement(nullptr),
m_pArrayElement(nullptr)
{
}
@@ -2000,6 +2049,16 @@ void PDFObjectElement::SetDictionary(PDFDictionaryElement* pDictionaryElement)
m_pDictionaryElement = pDictionaryElement;
}
void PDFObjectElement::SetArray(PDFArrayElement* pArrayElement)
{
m_pArrayElement = pArrayElement;
}
PDFArrayElement* PDFObjectElement::GetArray() const
{
return m_pArrayElement;
}
PDFReferenceElement::PDFReferenceElement(PDFDocument& rDoc, int fObjectValue, int fGenerationValue)
: m_rDoc(rDoc),
m_fObjectValue(fObjectValue),
@@ -2203,7 +2262,7 @@ bool PDFNameElement::Read(SvStream& rStream)
rStream.ReadChar(ch);
while (!rStream.IsEof())
{
if (isspace(ch) || ch == '/' || ch == '[' || ch == '<' || ch == '(')
if (isspace(ch) || ch == '/' || ch == '[' || ch == '<' || ch == '>' || ch == '(')
{
rStream.SeekRel(-1);
m_aValue = aBuf.makeStringAndClear();