summaryrefslogtreecommitdiff
path: root/xmlsecurity
diff options
context:
space:
mode:
authorMiklos Vajna <vmiklos@collabora.com>2020-11-12 21:09:03 +0100
committerMiklos Vajna <vmiklos@collabora.com>2020-11-13 15:29:09 +0100
commit2f89aa232302368201383b1a168f31a02f80077b (patch)
tree4402c6473dfff567ecbfa4586c62aa353f667fdc /xmlsecurity
parent9c9a8229201bd134df923420c97db84688a7813c (diff)
xmlsecurity: verify signatures using pdfium
And add a testcase with an odd PDF which were not handled by the old tokenizer but is handled by pdfium. Signature verification is happening implicitly while a document is opened, so it makes sense to use the more battle-tested pdfium to do this verification, instead of own code. (The APIs are somewhat low-level, so we can easily keep using our crypto stack for digest verification and our own certificate validation.) Signature creation still happens with the same own code, though. Change-Id: Ia64e84ab497422245e4ffd8a80a6a728cea84ff7 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/105766 Reviewed-by: Miklos Vajna <vmiklos@collabora.com> Tested-by: Jenkins
Diffstat (limited to 'xmlsecurity')
-rw-r--r--xmlsecurity/qa/unit/pdfsigning/data/good-custom-magic.pdfbin0 -> 57404 bytes
-rw-r--r--xmlsecurity/qa/unit/pdfsigning/pdfsigning.cxx23
-rw-r--r--xmlsecurity/source/helper/pdfsignaturehelper.cxx293
3 files changed, 175 insertions, 141 deletions
diff --git a/xmlsecurity/qa/unit/pdfsigning/data/good-custom-magic.pdf b/xmlsecurity/qa/unit/pdfsigning/data/good-custom-magic.pdf
new file mode 100644
index 000000000000..2c532cbf6a44
--- /dev/null
+++ b/xmlsecurity/qa/unit/pdfsigning/data/good-custom-magic.pdf
Binary files differ
diff --git a/xmlsecurity/qa/unit/pdfsigning/pdfsigning.cxx b/xmlsecurity/qa/unit/pdfsigning/pdfsigning.cxx
index fb47b9887f15..283223b9c409 100644
--- a/xmlsecurity/qa/unit/pdfsigning/pdfsigning.cxx
+++ b/xmlsecurity/qa/unit/pdfsigning/pdfsigning.cxx
@@ -511,6 +511,29 @@ CPPUNIT_TEST_FIXTURE(PDFSigningTest, testUnknownSubFilter)
CPPUNIT_ASSERT_EQUAL(static_cast<std::size_t>(2), rInformations.size());
}
+CPPUNIT_TEST_FIXTURE(PDFSigningTest, testGoodCustomMagic)
+{
+ // Tokenize the bugdoc.
+ uno::Reference<xml::crypto::XSEInitializer> xSEInitializer
+ = xml::crypto::SEInitializer::create(mxComponentContext);
+ uno::Reference<xml::crypto::XXMLSecurityContext> xSecurityContext
+ = xSEInitializer->createSecurityContext(OUString());
+ std::unique_ptr<SvStream> pStream = utl::UcbStreamHelper::CreateStream(
+ m_directories.getURLFromSrc(DATA_DIRECTORY) + "good-custom-magic.pdf",
+ StreamMode::STD_READ);
+ uno::Reference<io::XStream> xStream(new utl::OStreamWrapper(std::move(pStream)));
+ DocumentSignatureManager aManager(mxComponentContext, DocumentSignatureMode::Content);
+ aManager.setSignatureStream(xStream);
+ aManager.read(/*bUseTempStream=*/false);
+
+ // Without the accompanying fix in place, this test would have failed with:
+ // - Expected: 1 (SecurityOperationStatus_OPERATION_SUCCEEDED)
+ // - Actual : 0 (SecurityOperationStatus_UNKNOWN)
+ // i.e. no signatures were found due to a custom non-comment magic after the header.
+ std::vector<SignatureInformation>& rInformations = aManager.getCurrentSignatureInformations();
+ CPPUNIT_ASSERT_EQUAL(static_cast<std::size_t>(1), rInformations.size());
+}
+
CPPUNIT_PLUGIN_IMPLEMENT();
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/xmlsecurity/source/helper/pdfsignaturehelper.cxx b/xmlsecurity/source/helper/pdfsignaturehelper.cxx
index 72f7618a2253..6b8eefef7099 100644
--- a/xmlsecurity/source/helper/pdfsignaturehelper.cxx
+++ b/xmlsecurity/source/helper/pdfsignaturehelper.cxx
@@ -35,6 +35,9 @@
#include <svl/cryptosign.hxx>
#include <config_features.h>
#include <vcl/filter/PDFiumLibrary.hxx>
+#if HAVE_FEATURE_PDFIUM
+#include <fpdf_signature.h>
+#endif
using namespace ::com::sun::star;
@@ -116,82 +119,91 @@ void GetSignatureLineShape(const uno::Reference<frame::XModel>& xModel, sal_Int3
aStream.ReadBytes(rSignatureLineShape.data(), rSignatureLineShape.size());
}
+#if HAVE_FEATURE_PDFIUM
+/// Represents a parsed signature.
+struct Signature
+{
+ FPDF_SIGNATURE m_pSignature;
+ /// Offset+length pairs.
+ std::vector<std::pair<size_t, size_t>> m_aByteRanges;
+};
+
/// Turns an array of floats into offset + length pairs.
-bool GetByteRangesFromPDF(const vcl::filter::PDFArrayElement& rArray,
+void GetByteRangesFromPDF(FPDF_SIGNATURE pSignature,
std::vector<std::pair<size_t, size_t>>& rByteRanges)
{
- size_t nByteRangeOffset = 0;
- const std::vector<vcl::filter::PDFElement*>& rByteRangeElements = rArray.GetElements();
- for (size_t i = 0; i < rByteRangeElements.size(); ++i)
+ int nByteRangeLen = FPDFSignatureObj_GetByteRange(pSignature, nullptr, 0);
+ if (nByteRangeLen <= 0)
{
- auto pNumber = dynamic_cast<vcl::filter::PDFNumberElement*>(rByteRangeElements[i]);
- if (!pNumber)
- {
- SAL_WARN("xmlsecurity.helper",
- "ValidateSignature: signature offset and length has to be a number");
- return false;
- }
+ SAL_WARN("xmlsecurity.helper", "GetByteRangesFromPDF: no byte ranges");
+ return;
+ }
+
+ std::vector<int> aByteRange(nByteRangeLen);
+ FPDFSignatureObj_GetByteRange(pSignature, aByteRange.data(), aByteRange.size());
+ size_t nByteRangeOffset = 0;
+ for (size_t i = 0; i < aByteRange.size(); ++i)
+ {
if (i % 2 == 0)
{
- nByteRangeOffset = pNumber->GetValue();
+ nByteRangeOffset = aByteRange[i];
continue;
}
- size_t nByteRangeLength = pNumber->GetValue();
- rByteRanges.emplace_back(nByteRangeOffset, nByteRangeLength);
- }
- return true;
+ size_t nLength = aByteRange[i];
+ rByteRanges.emplace_back(nByteRangeOffset, nLength);
+ }
}
/// Determines the last position that is covered by a signature.
-bool GetEOFOfSignature(vcl::filter::PDFObjectElement* pSignature, size_t& rEOF)
+bool GetEOFOfSignature(const Signature& rSignature, size_t& rEOF)
{
- vcl::filter::PDFObjectElement* pValue = pSignature->LookupObject("V");
- if (!pValue)
+ if (rSignature.m_aByteRanges.size() < 2)
{
return false;
}
- auto pByteRange = dynamic_cast<vcl::filter::PDFArrayElement*>(pValue->Lookup("ByteRange"));
- if (!pByteRange || pByteRange->GetElements().size() < 2)
+ rEOF = rSignature.m_aByteRanges[1].first + rSignature.m_aByteRanges[1].second;
+ return true;
+}
+
+/**
+ * Get the value of the "modification detection and prevention" permission:
+ * Valid values are 1, 2 and 3: only 3 allows annotations after signing.
+ */
+int GetMDPPerm(const std::vector<Signature>& rSignatures)
+{
+ int nRet = 3;
+
+ if (rSignatures.empty())
{
- return false;
+ return nRet;
}
- std::vector<std::pair<size_t, size_t>> aByteRanges;
- if (!GetByteRangesFromPDF(*pByteRange, aByteRanges))
+ for (const auto& rSignature : rSignatures)
{
- return false;
+ int nPerm = FPDFSignatureObj_GetDocMDPPermission(rSignature.m_pSignature);
+ if (nPerm != 0)
+ {
+ return nPerm;
+ }
}
- rEOF = aByteRanges[1].first + aByteRanges[1].second;
- return true;
+ return nRet;
}
/// Checks if there are unsigned incremental updates between the signatures or after the last one.
-bool IsCompleteSignature(SvStream& rStream, vcl::filter::PDFDocument& rDocument,
- vcl::filter::PDFObjectElement* pSignature)
+bool IsCompleteSignature(SvStream& rStream, const Signature& rSignature,
+ const std::set<unsigned int>& rSignedEOFs,
+ const std::vector<unsigned int>& rAllEOFs)
{
- std::set<size_t> aSignedEOFs;
- for (const auto& i : rDocument.GetSignatureWidgets())
- {
- size_t nEOF = 0;
- if (!GetEOFOfSignature(i, nEOF))
- {
- return false;
- }
-
- aSignedEOFs.insert(nEOF);
- }
-
size_t nSignatureEOF = 0;
- if (!GetEOFOfSignature(pSignature, nSignatureEOF))
+ if (!GetEOFOfSignature(rSignature, nSignatureEOF))
{
return false;
}
- const std::vector<size_t>& rAllEOFs = rDocument.GetEOFs();
bool bFoundOwn = false;
for (const auto& rEOF : rAllEOFs)
{
@@ -206,7 +218,7 @@ bool IsCompleteSignature(SvStream& rStream, vcl::filter::PDFDocument& rDocument,
continue;
}
- if (aSignedEOFs.find(rEOF) == aSignedEOFs.end())
+ if (rSignedEOFs.find(rEOF) == rSignedEOFs.end())
{
// Unsigned incremental update found.
return false;
@@ -225,8 +237,6 @@ bool IsCompleteSignature(SvStream& rStream, vcl::filter::PDFDocument& rDocument,
return std::find(rAllEOFs.begin(), rAllEOFs.end(), nFileEnd) != rAllEOFs.end();
}
-#if HAVE_FEATURE_PDFIUM
-
/**
* Contains checksums of a PDF page, which is rendered without annotations. It also contains
* the geometry of a few dangerous annotation types.
@@ -290,21 +300,19 @@ void AnalyizeSignatureStream(SvMemoryStream& rStream, std::vector<PageChecksum>&
rPageChecksums.push_back(aPageChecksum);
}
}
-#endif
/**
* Checks if incremental updates after singing performed valid modifications only.
* nMDPPerm decides if annotations/commenting is OK, other changes are always not.
*/
-bool IsValidSignature(SvStream& rStream, vcl::filter::PDFObjectElement* pSignature, int nMDPPerm)
+bool IsValidSignature(SvStream& rStream, const Signature& rSignature, int nMDPPerm)
{
size_t nSignatureEOF = 0;
- if (!GetEOFOfSignature(pSignature, nSignatureEOF))
+ if (!GetEOFOfSignature(rSignature, nSignatureEOF))
{
return false;
}
-#if HAVE_FEATURE_PDFIUM
SvMemoryStream aSignatureStream;
sal_uInt64 nPos = rStream.Tell();
rStream.Seek(0);
@@ -326,11 +334,6 @@ bool IsValidSignature(SvStream& rStream, vcl::filter::PDFObjectElement* pSignatu
// Fail if any page looks different after signing and at the end. Annotations/commenting doesn't
// count, though.
return aSignedPages == aAllPages;
-#else
- (void)rStream;
- (void)nMDPPerm;
- return true;
-#endif
}
/**
@@ -338,116 +341,92 @@ bool IsValidSignature(SvStream& rStream, vcl::filter::PDFObjectElement* pSignatu
* @param rDocument the parsed document to see if the signature is partial.
* @return If we can determinate a result.
*/
-bool ValidateSignature(SvStream& rStream, vcl::filter::PDFObjectElement* pSignature,
- SignatureInformation& rInformation, vcl::filter::PDFDocument& rDocument,
- int nMDPPerm)
+bool ValidateSignature(SvStream& rStream, const Signature& rSignature,
+ SignatureInformation& rInformation, int nMDPPerm,
+ const std::set<unsigned int>& rSignatureEOFs,
+ const std::vector<unsigned int>& rTrailerEnds)
{
- vcl::filter::PDFObjectElement* pValue = pSignature->LookupObject("V");
- if (!pValue)
- {
- SAL_WARN("xmlsecurity.helper", "ValidateSignature: no value");
- return false;
- }
-
- auto pContents = dynamic_cast<vcl::filter::PDFHexStringElement*>(pValue->Lookup("Contents"));
- if (!pContents)
+ int nContentsLen = FPDFSignatureObj_GetContents(rSignature.m_pSignature, nullptr, 0);
+ if (nContentsLen <= 0)
{
SAL_WARN("xmlsecurity.helper", "ValidateSignature: no contents");
return false;
}
+ std::vector<unsigned char> aContents(nContentsLen);
+ FPDFSignatureObj_GetContents(rSignature.m_pSignature, aContents.data(), aContents.size());
- auto pByteRange = dynamic_cast<vcl::filter::PDFArrayElement*>(pValue->Lookup("ByteRange"));
- if (!pByteRange || pByteRange->GetElements().size() < 2)
- {
- SAL_WARN("xmlsecurity.helper", "ValidateSignature: no byte range or too few elements");
- return false;
- }
+ int nSubFilterLen = FPDFSignatureObj_GetSubFilter(rSignature.m_pSignature, nullptr, 0);
+ std::vector<char> aSubFilterBuf(nSubFilterLen);
+ FPDFSignatureObj_GetSubFilter(rSignature.m_pSignature, aSubFilterBuf.data(),
+ aSubFilterBuf.size());
+ // Buffer is NUL-terminated.
+ OString aSubFilter(aSubFilterBuf.data(), aSubFilterBuf.size() - 1);
- auto pSubFilter = dynamic_cast<vcl::filter::PDFNameElement*>(pValue->Lookup("SubFilter"));
- const bool bNonDetached = pSubFilter && pSubFilter->GetValue() == "adbe.pkcs7.sha1";
- if (!pSubFilter
- || (pSubFilter->GetValue() != "adbe.pkcs7.detached" && !bNonDetached
- && pSubFilter->GetValue() != "ETSI.CAdES.detached"))
+ const bool bNonDetached = aSubFilter == "adbe.pkcs7.sha1";
+ if (aSubFilter.isEmpty()
+ || (aSubFilter != "adbe.pkcs7.detached" && !bNonDetached
+ && aSubFilter != "ETSI.CAdES.detached"))
{
- if (!pSubFilter)
+ if (aSubFilter.isEmpty())
SAL_WARN("xmlsecurity.helper", "ValidateSignature: missing sub-filter");
else
- SAL_WARN("xmlsecurity.helper", "ValidateSignature: unsupported sub-filter: '"
- << pSubFilter->GetValue() << "'");
+ SAL_WARN("xmlsecurity.helper",
+ "ValidateSignature: unsupported sub-filter: '" << aSubFilter << "'");
return false;
}
// Reason / comment / description is optional.
- auto pReason = dynamic_cast<vcl::filter::PDFHexStringElement*>(pValue->Lookup("Reason"));
- if (pReason)
- {
- // See appendUnicodeTextString() for the export equivalent of this.
- std::vector<unsigned char> aReason = vcl::filter::PDFDocument::DecodeHexString(pReason);
- OUStringBuffer aBuffer;
- sal_uInt16 nByte = 0;
- for (size_t i = 0; i < aReason.size(); ++i)
- {
- if (i % 2 == 0)
- nByte = aReason[i];
- else
- {
- sal_Unicode nUnicode;
- nUnicode = (nByte << 8);
- nUnicode |= aReason[i];
- aBuffer.append(nUnicode);
- }
- }
-
- if (!aBuffer.isEmpty())
- rInformation.ouDescription = aBuffer.makeStringAndClear();
+ int nReasonLen = FPDFSignatureObj_GetReason(rSignature.m_pSignature, nullptr, 0);
+ if (nReasonLen > 0)
+ {
+ std::vector<char16_t> aReasonBuf(nReasonLen);
+ FPDFSignatureObj_GetReason(rSignature.m_pSignature, aReasonBuf.data(), aReasonBuf.size());
+ rInformation.ouDescription = OUString(aReasonBuf.data(), aReasonBuf.size() - 1);
}
// Date: used only when the time of signing is not available in the
// signature.
- auto pM = dynamic_cast<vcl::filter::PDFLiteralStringElement*>(pValue->Lookup("M"));
- if (pM)
+ int nTimeLen = FPDFSignatureObj_GetTime(rSignature.m_pSignature, nullptr, 0);
+ if (nTimeLen > 0)
{
// Example: "D:20161027100104".
- const OString& rM = pM->GetValue();
- if (rM.startsWith("D:") && rM.getLength() >= 16)
+ std::vector<char> aTimeBuf(nTimeLen);
+ FPDFSignatureObj_GetTime(rSignature.m_pSignature, aTimeBuf.data(), aTimeBuf.size());
+ OString aM(aTimeBuf.data(), aTimeBuf.size() - 1);
+ if (aM.startsWith("D:") && aM.getLength() >= 16)
{
- rInformation.stDateTime.Year = rM.copy(2, 4).toInt32();
- rInformation.stDateTime.Month = rM.copy(6, 2).toInt32();
- rInformation.stDateTime.Day = rM.copy(8, 2).toInt32();
- rInformation.stDateTime.Hours = rM.copy(10, 2).toInt32();
- rInformation.stDateTime.Minutes = rM.copy(12, 2).toInt32();
- rInformation.stDateTime.Seconds = rM.copy(14, 2).toInt32();
+ rInformation.stDateTime.Year = aM.copy(2, 4).toInt32();
+ rInformation.stDateTime.Month = aM.copy(6, 2).toInt32();
+ rInformation.stDateTime.Day = aM.copy(8, 2).toInt32();
+ rInformation.stDateTime.Hours = aM.copy(10, 2).toInt32();
+ rInformation.stDateTime.Minutes = aM.copy(12, 2).toInt32();
+ rInformation.stDateTime.Seconds = aM.copy(14, 2).toInt32();
}
}
- // Build a list of offset-length pairs, representing the signed bytes.
- std::vector<std::pair<size_t, size_t>> aByteRanges;
- if (!GetByteRangesFromPDF(*pByteRange, aByteRanges))
- {
- return false;
- }
-
// Detect if the byte ranges don't cover everything, but the signature itself.
- if (aByteRanges.size() < 2)
+ if (rSignature.m_aByteRanges.size() < 2)
{
SAL_WARN("xmlsecurity.helper", "ValidateSignature: expected 2 byte ranges");
return false;
}
- if (aByteRanges[0].first != 0)
+ if (rSignature.m_aByteRanges[0].first != 0)
{
SAL_WARN("xmlsecurity.helper", "ValidateSignature: first range start is not 0");
return false;
}
- // 2 is the leading "<" and the trailing ">" around the hex string.
- size_t nSignatureLength = static_cast<size_t>(pContents->GetValue().getLength()) + 2;
- if (aByteRanges[1].first != (aByteRanges[0].second + nSignatureLength))
+ // Binary vs hex dump and 2 is the leading "<" and the trailing ">" around the hex string.
+ size_t nSignatureLength = aContents.size() * 2 + 2;
+ if (rSignature.m_aByteRanges[1].first
+ != (rSignature.m_aByteRanges[0].second + nSignatureLength))
{
SAL_WARN("xmlsecurity.helper",
"ValidateSignature: second range start is not the end of the signature");
return false;
}
- rInformation.bPartialDocumentSignature = !IsCompleteSignature(rStream, rDocument, pSignature);
- if (!IsValidSignature(rStream, pSignature, nMDPPerm))
+ rInformation.bPartialDocumentSignature
+ = !IsCompleteSignature(rStream, rSignature, rSignatureEOFs, rTrailerEnds);
+ if (!IsValidSignature(rStream, rSignature, nMDPPerm))
{
SAL_WARN("xmlsecurity.helper", "ValidateSignature: invalid incremental update detected");
return false;
@@ -455,16 +434,10 @@ bool ValidateSignature(SvStream& rStream, vcl::filter::PDFObjectElement* pSignat
// At this point there is no obviously missing info to validate the
// signature.
- std::vector<unsigned char> aSignature = vcl::filter::PDFDocument::DecodeHexString(pContents);
- if (aSignature.empty())
- {
- SAL_WARN("xmlsecurity.helper", "ValidateSignature: empty contents");
- return false;
- }
-
- return svl::crypto::Signing::Verify(rStream, aByteRanges, bNonDetached, aSignature,
+ return svl::crypto::Signing::Verify(rStream, rSignature.m_aByteRanges, bNonDetached, aContents,
rInformation);
}
+#endif
}
PDFSignatureHelper::PDFSignatureHelper() = default;
@@ -484,30 +457,68 @@ bool PDFSignatureHelper::ReadAndVerifySignature(
bool PDFSignatureHelper::ReadAndVerifySignatureSvStream(SvStream& rStream)
{
- vcl::filter::PDFDocument aDocument;
- if (!aDocument.Read(rStream))
+#if HAVE_FEATURE_PDFIUM
+ auto pPdfium = vcl::pdf::PDFiumLibrary::get();
+ SvMemoryStream aStream;
+ sal_uInt64 nPos = rStream.Tell();
+ rStream.Seek(0);
+ aStream.WriteStream(rStream);
+ rStream.Seek(nPos);
+ std::unique_ptr<vcl::pdf::PDFiumDocument> pPdfDocument
+ = pPdfium->openDocument(aStream.GetData(), aStream.GetSize());
+ if (!pPdfDocument)
{
SAL_WARN("xmlsecurity.helper", "failed to read the document");
return false;
}
- std::vector<vcl::filter::PDFObjectElement*> aSignatures = aDocument.GetSignatureWidgets();
- if (aSignatures.empty())
+ int nSignatureCount = FPDF_GetSignatureCount(pPdfDocument->getPointer());
+ if (nSignatureCount <= 0)
+ {
return true;
+ }
+ std::vector<Signature> aSignatures(nSignatureCount);
+ for (int i = 0; i < nSignatureCount; ++i)
+ {
+ FPDF_SIGNATURE pSignature = FPDF_GetSignatureObject(pPdfDocument->getPointer(), i);
+ std::vector<std::pair<size_t, size_t>> aByteRanges;
+ GetByteRangesFromPDF(pSignature, aByteRanges);
+ aSignatures[i] = Signature{ pSignature, aByteRanges };
+ }
+
+ std::set<unsigned int> aSignatureEOFs;
+ for (const auto& rSignature : aSignatures)
+ {
+ size_t nEOF = 0;
+ if (GetEOFOfSignature(rSignature, nEOF))
+ {
+ aSignatureEOFs.insert(nEOF);
+ }
+ }
+
+ int nNumTrailers = FPDF_GetTrailerEnds(pPdfDocument->getPointer(), nullptr, 0);
+ std::vector<unsigned int> aTrailerEnds(nNumTrailers);
+ FPDF_GetTrailerEnds(pPdfDocument->getPointer(), aTrailerEnds.data(), aTrailerEnds.size());
m_aSignatureInfos.clear();
- int nMDPPerm = aDocument.GetMDPPerm();
+ int nMDPPerm = GetMDPPerm(aSignatures);
for (size_t i = 0; i < aSignatures.size(); ++i)
{
SignatureInformation aInfo(i);
- if (!ValidateSignature(rStream, aSignatures[i], aInfo, aDocument, nMDPPerm))
+ if (!ValidateSignature(rStream, aSignatures[i], aInfo, nMDPPerm, aSignatureEOFs,
+ aTrailerEnds))
+ {
SAL_WARN("xmlsecurity.helper", "failed to determine digest match");
+ }
m_aSignatureInfos.push_back(aInfo);
}
+#else
+ (void)rStream;
+#endif
return true;
}