summaryrefslogtreecommitdiff
path: root/xmlsecurity/source/helper/pdfsignaturehelper.cxx
diff options
context:
space:
mode:
authorMiklos Vajna <vmiklos@collabora.com>2020-09-24 21:06:46 +0200
committerMiklos Vajna <vmiklos@collabora.com>2020-09-25 09:02:57 +0200
commit2dd3d03b5350e18cb5f39978022620b55d3d8c5b (patch)
tree5c82e20fef4188cad18ed88be6b6a1a98613750c /xmlsecurity/source/helper/pdfsignaturehelper.cxx
parent6505f43b09fda83f26159746083b971bfab8054a (diff)
xmlsecurity: fold pdfio into pdfsignaturehelper
Most of the initial pdfio was moved to vcl as vcl::filter::PDFDocument. A small part was left here, because it depended on NSS. Then later the NSS bits were moved to svl::crypto::Signing. The rest is just a small amount of code, keeping that separate from PDFSignatureHelper, which is its only user makes little sense. With this, vcl::filter::PDFDocument is an implementation detail of PDFSignatureHelper during signature verification. Change-Id: I6230f9e46deeff7159970f88dbb3bd2de0e9ce7d Reviewed-on: https://gerrit.libreoffice.org/c/core/+/103350 Tested-by: Jenkins Reviewed-by: Miklos Vajna <vmiklos@collabora.com>
Diffstat (limited to 'xmlsecurity/source/helper/pdfsignaturehelper.cxx')
-rw-r--r--xmlsecurity/source/helper/pdfsignaturehelper.cxx313
1 files changed, 310 insertions, 3 deletions
diff --git a/xmlsecurity/source/helper/pdfsignaturehelper.cxx b/xmlsecurity/source/helper/pdfsignaturehelper.cxx
index b49cdd3e449f..f9bb5342c626 100644
--- a/xmlsecurity/source/helper/pdfsignaturehelper.cxx
+++ b/xmlsecurity/source/helper/pdfsignaturehelper.cxx
@@ -30,8 +30,11 @@
#include <unotools/streamwrap.hxx>
#include <unotools/ucbstreamhelper.hxx>
#include <vcl/filter/pdfdocument.hxx>
-
-#include <pdfio/pdfdocument.hxx>
+#include <vcl/checksum.hxx>
+#include <rtl/ustrbuf.hxx>
+#include <svl/cryptosign.hxx>
+#include <config_features.h>
+#include <vcl/filter/PDFiumLibrary.hxx>
using namespace ::com::sun::star;
@@ -112,6 +115,310 @@ void GetSignatureLineShape(const uno::Reference<frame::XModel>& xModel, sal_Int3
rSignatureLineShape = std::vector<sal_Int8>(aStream.GetSize());
aStream.ReadBytes(rSignatureLineShape.data(), rSignatureLineShape.size());
}
+
+/// Turns an array of floats into offset + length pairs.
+bool GetByteRangesFromPDF(vcl::filter::PDFArrayElement& rArray,
+ 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)
+ {
+ 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;
+ }
+
+ if (i % 2 == 0)
+ {
+ nByteRangeOffset = pNumber->GetValue();
+ continue;
+ }
+ size_t nByteRangeLength = pNumber->GetValue();
+ rByteRanges.emplace_back(nByteRangeOffset, nByteRangeLength);
+ }
+
+ return true;
+}
+
+/// Determines the last position that is covered by a signature.
+bool GetEOFOfSignature(vcl::filter::PDFObjectElement* pSignature, size_t& rEOF)
+{
+ vcl::filter::PDFObjectElement* pValue = pSignature->LookupObject("V");
+ if (!pValue)
+ {
+ return false;
+ }
+
+ auto pByteRange = dynamic_cast<vcl::filter::PDFArrayElement*>(pValue->Lookup("ByteRange"));
+ if (!pByteRange || pByteRange->GetElements().size() < 2)
+ {
+ return false;
+ }
+
+ std::vector<std::pair<size_t, size_t>> aByteRanges;
+ if (!GetByteRangesFromPDF(*pByteRange, aByteRanges))
+ {
+ return false;
+ }
+
+ rEOF = aByteRanges[1].first + aByteRanges[1].second;
+ return true;
+}
+
+/// 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)
+{
+ 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))
+ {
+ return false;
+ }
+
+ const std::vector<size_t>& rAllEOFs = rDocument.GetEOFs();
+ bool bFoundOwn = false;
+ for (const auto& rEOF : rAllEOFs)
+ {
+ if (rEOF == nSignatureEOF)
+ {
+ bFoundOwn = true;
+ continue;
+ }
+
+ if (!bFoundOwn)
+ {
+ continue;
+ }
+
+ if (aSignedEOFs.find(rEOF) == aSignedEOFs.end())
+ {
+ // Unsigned incremental update found.
+ return false;
+ }
+ }
+
+ // Make sure we find the incremental update of the signature itself.
+ if (!bFoundOwn)
+ {
+ return false;
+ }
+
+ // No additional content after the last incremental update.
+ rStream.Seek(STREAM_SEEK_TO_END);
+ size_t nFileEnd = rStream.Tell();
+ return std::find(rAllEOFs.begin(), rAllEOFs.end(), nFileEnd) != rAllEOFs.end();
+}
+
+#if HAVE_FEATURE_PDFIUM
+/// Collects the checksum of each page of one version of the PDF.
+void AnalyizeSignatureStream(SvMemoryStream& rStream, std::vector<BitmapChecksum>& rPageChecksums)
+{
+ auto pPdfium = vcl::pdf::PDFiumLibrary::get();
+ vcl::pdf::PDFiumDocument aPdfDocument(
+ FPDF_LoadMemDocument(rStream.GetData(), rStream.GetSize(), /*password=*/nullptr));
+
+ int nPageCount = aPdfDocument.getPageCount();
+ for (int nPage = 0; nPage < nPageCount; ++nPage)
+ {
+ std::unique_ptr<vcl::pdf::PDFiumPage> pPdfPage(aPdfDocument.openPage(nPage));
+ if (!pPdfPage)
+ {
+ return;
+ }
+
+ BitmapChecksum nPageChecksum = pPdfPage->getChecksum();
+ rPageChecksums.push_back(nPageChecksum);
+ }
+}
+#endif
+
+/**
+ * Checks if incremental updates after singing performed valid modifications only.
+ * Annotations/commenting is OK, other changes are not.
+ */
+bool IsValidSignature(SvStream& rStream, vcl::filter::PDFObjectElement* pSignature)
+{
+ size_t nSignatureEOF = 0;
+ if (!GetEOFOfSignature(pSignature, nSignatureEOF))
+ {
+ return false;
+ }
+
+#if HAVE_FEATURE_PDFIUM
+ SvMemoryStream aSignatureStream;
+ sal_uInt64 nPos = rStream.Tell();
+ rStream.Seek(0);
+ aSignatureStream.WriteStream(rStream, nSignatureEOF);
+ rStream.Seek(nPos);
+ aSignatureStream.Seek(0);
+ std::vector<BitmapChecksum> aSignedPages;
+ AnalyizeSignatureStream(aSignatureStream, aSignedPages);
+
+ SvMemoryStream aFullStream;
+ nPos = rStream.Tell();
+ rStream.Seek(0);
+ aFullStream.WriteStream(rStream);
+ rStream.Seek(nPos);
+ aFullStream.Seek(0);
+ std::vector<BitmapChecksum> aAllPages;
+ AnalyizeSignatureStream(aFullStream, aAllPages);
+
+ // Fail if any page looks different after signing and at the end. Annotations/commenting doesn't
+ // count, though.
+ return aSignedPages == aAllPages;
+#else
+ (void)rStream;
+ return true;
+#endif
+}
+
+/**
+ * @param rInformation The actual result.
+ * @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)
+{
+ 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)
+ {
+ SAL_WARN("xmlsecurity.helper", "ValidateSignature: no contents");
+ return false;
+ }
+
+ 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;
+ }
+
+ 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"))
+ {
+ if (!pSubFilter)
+ SAL_WARN("xmlsecurity.helper", "ValidateSignature: missing sub-filter");
+ else
+ SAL_WARN("xmlsecurity.helper", "ValidateSignature: unsupported sub-filter: '"
+ << pSubFilter->GetValue() << "'");
+ 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();
+ }
+
+ // 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)
+ {
+ // Example: "D:20161027100104".
+ const OString& rM = pM->GetValue();
+ if (rM.startsWith("D:") && rM.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();
+ }
+ }
+
+ // 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)
+ {
+ SAL_WARN("xmlsecurity.helper", "ValidateSignature: expected 2 byte ranges");
+ return false;
+ }
+ if (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))
+ {
+ 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))
+ {
+ SAL_WARN("xmlsecurity.helper", "ValidateSignature: invalid incremental update detected");
+ return false;
+ }
+
+ // 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,
+ rInformation);
+}
}
PDFSignatureHelper::PDFSignatureHelper() = default;
@@ -148,7 +455,7 @@ bool PDFSignatureHelper::ReadAndVerifySignatureSvStream(SvStream& rStream)
{
SignatureInformation aInfo(i);
- if (!xmlsecurity::pdfio::ValidateSignature(rStream, aSignatures[i], aInfo, aDocument))
+ if (!ValidateSignature(rStream, aSignatures[i], aInfo, aDocument))
SAL_WARN("xmlsecurity.helper", "failed to determine digest match");
m_aSignatureInfos.push_back(aInfo);