diff options
author | Miklos Vajna <vmiklos@collabora.com> | 2020-09-24 21:06:46 +0200 |
---|---|---|
committer | Miklos Vajna <vmiklos@collabora.com> | 2020-09-25 09:02:57 +0200 |
commit | 2dd3d03b5350e18cb5f39978022620b55d3d8c5b (patch) | |
tree | 5c82e20fef4188cad18ed88be6b6a1a98613750c /xmlsecurity/source/helper/pdfsignaturehelper.cxx | |
parent | 6505f43b09fda83f26159746083b971bfab8054a (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.cxx | 313 |
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); |