diff options
author | Miklos Vajna <vmiklos@collabora.com> | 2020-11-12 21:09:03 +0100 |
---|---|---|
committer | Miklos Vajna <vmiklos@collabora.com> | 2020-11-13 15:29:09 +0100 |
commit | 2f89aa232302368201383b1a168f31a02f80077b (patch) | |
tree | 4402c6473dfff567ecbfa4586c62aa353f667fdc /xmlsecurity | |
parent | 9c9a8229201bd134df923420c97db84688a7813c (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.pdf | bin | 0 -> 57404 bytes | |||
-rw-r--r-- | xmlsecurity/qa/unit/pdfsigning/pdfsigning.cxx | 23 | ||||
-rw-r--r-- | xmlsecurity/source/helper/pdfsignaturehelper.cxx | 293 |
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 Binary files differnew file mode 100644 index 000000000000..2c532cbf6a44 --- /dev/null +++ b/xmlsecurity/qa/unit/pdfsigning/data/good-custom-magic.pdf 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; } |