/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ /* * This file is part of the LibreOffice project. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace ::com::sun::star; namespace { /// Gets the current page of the current view from xModel and puts it to the 1-based rPage. bool GetSignatureLinePage(const uno::Reference& xModel, sal_Int32& rPage) { uno::Reference xController(xModel->getCurrentController(), uno::UNO_QUERY); if (!xController.is()) { return false; } uno::Reference xPage(xController->getCurrentPage(), uno::UNO_QUERY); if (!xPage.is()) { return false; } return xPage->getPropertyValue("Number") >>= rPage; } /// If the currently selected shape is a Draw signature line, export that to PDF. void GetSignatureLineShape(const uno::Reference& xModel, sal_Int32& rPage, std::vector& rSignatureLineShape) { if (!xModel.is()) { return; } if (!GetSignatureLinePage(xModel, rPage)) { return; } uno::Reference xShapes(xModel->getCurrentSelection(), uno::UNO_QUERY); if (!xShapes.is() || xShapes->getCount() < 1) { return; } uno::Reference xShapeProps(xShapes->getByIndex(0), uno::UNO_QUERY); if (!xShapeProps.is()) { return; } comphelper::SequenceAsHashMap aMap(xShapeProps->getPropertyValue("InteropGrabBag")); auto it = aMap.find("SignatureCertificate"); if (it == aMap.end()) { return; } // We know that we add a signature line shape to an existing PDF at this point. uno::Reference xStorable(xModel, uno::UNO_QUERY); if (!xStorable.is()) { return; } // Export just the signature line. utl::MediaDescriptor aMediaDescriptor; aMediaDescriptor["FilterName"] <<= OUString("draw_pdf_Export"); SvMemoryStream aStream; uno::Reference xStream(new utl::OStreamWrapper(aStream)); aMediaDescriptor["OutputStream"] <<= xStream; uno::Sequence aFilterData( comphelper::InitPropertySequence({ { "Selection", uno::Any(xShapes) } })); aMediaDescriptor["FilterData"] <<= aFilterData; xStorable->storeToURL("private:stream", aMediaDescriptor.getAsConstPropertyValueList()); xStream->flush(); aStream.Seek(0); rSignatureLineShape = std::vector(aStream.GetSize()); aStream.ReadBytes(rSignatureLineShape.data(), rSignatureLineShape.size()); } /// Represents a parsed signature. struct Signature { std::unique_ptr m_pSignature; /// Offset+length pairs. std::vector> m_aByteRanges; }; /// Turns an array of floats into offset + length pairs. void GetByteRangesFromPDF(const std::unique_ptr& pSignature, std::vector>& rByteRanges) { std::vector aByteRange = pSignature->getByteRange(); if (aByteRange.empty()) { SAL_WARN("xmlsecurity.helper", "GetByteRangesFromPDF: no byte ranges"); return; } size_t nByteRangeOffset = 0; for (size_t i = 0; i < aByteRange.size(); ++i) { if (i % 2 == 0) { nByteRangeOffset = aByteRange[i]; continue; } size_t nLength = aByteRange[i]; rByteRanges.emplace_back(nByteRangeOffset, nLength); } } /// Determines the last position that is covered by a signature. bool GetEOFOfSignature(const Signature& rSignature, size_t& rEOF) { if (rSignature.m_aByteRanges.size() < 2) { return false; } 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& rSignatures) { int nRet = 3; if (rSignatures.empty()) { return nRet; } for (const auto& rSignature : rSignatures) { int nPerm = rSignature.m_pSignature->getDocMDPPermission(); if (nPerm != 0) { return nPerm; } } return nRet; } /// Checks if there are unsigned incremental updates between the signatures or after the last one. bool IsCompleteSignature(SvStream& rStream, const Signature& rSignature, const std::set& rSignedEOFs, const std::vector& rAllEOFs) { size_t nSignatureEOF = 0; if (!GetEOFOfSignature(rSignature, nSignatureEOF)) { return false; } bool bFoundOwn = false; for (const auto& rEOF : rAllEOFs) { if (rEOF == nSignatureEOF) { bFoundOwn = true; continue; } if (!bFoundOwn) { continue; } if (rSignedEOFs.find(rEOF) == rSignedEOFs.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(); } /** * Contains checksums of a PDF page, which is rendered without annotations. It also contains * the geometry of a few dangerous annotation types. */ struct PageChecksum { BitmapChecksum m_nPageContent; std::vector m_aAnnotations; bool operator==(const PageChecksum& rChecksum) const; }; bool PageChecksum::operator==(const PageChecksum& rChecksum) const { if (m_nPageContent != rChecksum.m_nPageContent) { return false; } return m_aAnnotations == rChecksum.m_aAnnotations; } /// Collects the checksum of each page of one version of the PDF. void AnalyizeSignatureStream(SvMemoryStream& rStream, std::vector& rPageChecksums, int nMDPPerm) { auto pPdfium = vcl::pdf::PDFiumLibrary::get(); std::unique_ptr pPdfDocument = pPdfium->openDocument(rStream.GetData(), rStream.GetSize(), OString()); if (!pPdfDocument) { return; } int nPageCount = pPdfDocument->getPageCount(); for (int nPage = 0; nPage < nPageCount; ++nPage) { std::unique_ptr pPdfPage = pPdfDocument->openPage(nPage); if (!pPdfPage) { return; } PageChecksum aPageChecksum; aPageChecksum.m_nPageContent = pPdfPage->getChecksum(nMDPPerm); for (int i = 0; i < pPdfPage->getAnnotationCount(); ++i) { std::unique_ptr pPdfAnnotation = pPdfPage->getAnnotation(i); if (!pPdfAnnotation) { SAL_WARN("xmlsecurity.helper", "Cannot get PDFiumAnnotation"); continue; } vcl::pdf::PDFAnnotationSubType eType = pPdfAnnotation->getSubType(); switch (eType) { case vcl::pdf::PDFAnnotationSubType::Unknown: case vcl::pdf::PDFAnnotationSubType::FreeText: case vcl::pdf::PDFAnnotationSubType::Stamp: case vcl::pdf::PDFAnnotationSubType::Redact: aPageChecksum.m_aAnnotations.push_back(pPdfAnnotation->getRectangle()); break; default: break; } } rPageChecksums.push_back(aPageChecksum); } } /** * 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, const Signature& rSignature, int nMDPPerm) { size_t nSignatureEOF = 0; if (!GetEOFOfSignature(rSignature, nSignatureEOF)) { return false; } SvMemoryStream aSignatureStream; sal_uInt64 nPos = rStream.Tell(); rStream.Seek(0); aSignatureStream.WriteStream(rStream, nSignatureEOF); rStream.Seek(nPos); aSignatureStream.Seek(0); std::vector aSignedPages; AnalyizeSignatureStream(aSignatureStream, aSignedPages, nMDPPerm); SvMemoryStream aFullStream; nPos = rStream.Tell(); rStream.Seek(0); aFullStream.WriteStream(rStream); rStream.Seek(nPos); aFullStream.Seek(0); std::vector aAllPages; AnalyizeSignatureStream(aFullStream, aAllPages, nMDPPerm); // Fail if any page looks different after signing and at the end. Annotations/commenting doesn't // count, though. return aSignedPages == aAllPages; } /** * @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, const Signature& rSignature, SignatureInformation& rInformation, int nMDPPerm, const std::set& rSignatureEOFs, const std::vector& rTrailerEnds) { std::vector aContents = rSignature.m_pSignature->getContents(); if (aContents.empty()) { SAL_WARN("xmlsecurity.helper", "ValidateSignature: no contents"); return false; } OString aSubFilter = rSignature.m_pSignature->getSubFilter(); const bool bNonDetached = aSubFilter == "adbe.pkcs7.sha1"; if (aSubFilter.isEmpty() || (aSubFilter != "adbe.pkcs7.detached" && !bNonDetached && aSubFilter != "ETSI.CAdES.detached")) { if (aSubFilter.isEmpty()) SAL_WARN("xmlsecurity.helper", "ValidateSignature: missing sub-filter"); else SAL_WARN("xmlsecurity.helper", "ValidateSignature: unsupported sub-filter: '" << aSubFilter << "'"); return false; } // Reason / comment / description is optional. rInformation.ouDescription = rSignature.m_pSignature->getReason(); // Date: used only when the time of signing is not available in the // signature. rInformation.stDateTime = rSignature.m_pSignature->getTime(); // Detect if the byte ranges don't cover everything, but the signature itself. if (rSignature.m_aByteRanges.size() < 2) { SAL_WARN("xmlsecurity.helper", "ValidateSignature: expected 2 byte ranges"); return false; } if (rSignature.m_aByteRanges[0].first != 0) { SAL_WARN("xmlsecurity.helper", "ValidateSignature: first range start is not 0"); return false; } // 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, rSignature, rSignatureEOFs, rTrailerEnds); if (!IsValidSignature(rStream, rSignature, nMDPPerm)) { SAL_WARN("xmlsecurity.helper", "ValidateSignature: invalid incremental update detected"); return false; } // At this point there is no obviously missing info to validate the // signature. return svl::crypto::Signing::Verify(rStream, rSignature.m_aByteRanges, bNonDetached, aContents, rInformation); } } PDFSignatureHelper::PDFSignatureHelper() = default; bool PDFSignatureHelper::ReadAndVerifySignature( const uno::Reference& xInputStream) { if (!xInputStream.is()) { SAL_WARN("xmlsecurity.helper", "input stream missing"); return false; } std::unique_ptr pStream(utl::UcbStreamHelper::CreateStream(xInputStream, true)); return ReadAndVerifySignatureSvStream(*pStream); } bool PDFSignatureHelper::ReadAndVerifySignatureSvStream(SvStream& rStream) { auto pPdfium = vcl::pdf::PDFiumLibrary::get(); if (!pPdfium) { return true; } SvMemoryStream aStream; sal_uInt64 nPos = rStream.Tell(); rStream.Seek(0); aStream.WriteStream(rStream); rStream.Seek(nPos); std::unique_ptr pPdfDocument = pPdfium->openDocument(aStream.GetData(), aStream.GetSize(), OString()); if (!pPdfDocument) { SAL_WARN("xmlsecurity.helper", "failed to read the document"); return false; } int nSignatureCount = pPdfDocument->getSignatureCount(); if (nSignatureCount <= 0) { return true; } std::vector aSignatures(nSignatureCount); for (int i = 0; i < nSignatureCount; ++i) { std::unique_ptr pSignature = pPdfDocument->getSignature(i); std::vector> aByteRanges; GetByteRangesFromPDF(pSignature, aByteRanges); aSignatures[i] = Signature{ std::move(pSignature), aByteRanges }; } std::set aSignatureEOFs; for (const auto& rSignature : aSignatures) { size_t nEOF = 0; if (GetEOFOfSignature(rSignature, nEOF)) { aSignatureEOFs.insert(nEOF); } } std::vector aTrailerEnds = pPdfDocument->getTrailerEnds(); m_aSignatureInfos.clear(); int nMDPPerm = GetMDPPerm(aSignatures); for (size_t i = 0; i < aSignatures.size(); ++i) { SignatureInformation aInfo(i); if (!ValidateSignature(rStream, aSignatures[i], aInfo, nMDPPerm, aSignatureEOFs, aTrailerEnds)) { SAL_WARN("xmlsecurity.helper", "failed to determine digest match"); } m_aSignatureInfos.push_back(aInfo); } return true; } SignatureInformations const& PDFSignatureHelper::GetSignatureInformations() const { return m_aSignatureInfos; } uno::Sequence PDFSignatureHelper::GetDocumentSignatureInformations( const uno::Reference& xSecEnv) const { uno::Sequence aRet(m_aSignatureInfos.size()); auto aRetRange = asNonConstRange(aRet); for (size_t i = 0; i < m_aSignatureInfos.size(); ++i) { const SignatureInformation& rInternal = m_aSignatureInfos[i]; security::DocumentSignatureInformation& rExternal = aRetRange[i]; rExternal.SignatureIsValid = rInternal.nStatus == xml::crypto::SecurityOperationStatus_OPERATION_SUCCEEDED; if (rInternal.GetSigningCertificate() && !rInternal.GetSigningCertificate()->X509Certificate.isEmpty()) { rExternal.Signer = xSecEnv->createCertificateFromAscii( rInternal.GetSigningCertificate()->X509Certificate); } rExternal.PartialDocumentSignature = rInternal.bPartialDocumentSignature; // Verify certificate. if (rExternal.Signer.is()) { try { rExternal.CertificateStatus = xSecEnv->verifyCertificate(rExternal.Signer, {}); } catch (const uno::SecurityException&) { DBG_UNHANDLED_EXCEPTION("xmlsecurity.helper", "failed to verify certificate"); rExternal.CertificateStatus = security::CertificateValidity::INVALID; } } else rExternal.CertificateStatus = security::CertificateValidity::INVALID; } return aRet; } sal_Int32 PDFSignatureHelper::GetNewSecurityId() const { return m_aSignatureInfos.size(); } void PDFSignatureHelper::SetX509Certificate( const uno::Reference& xCertificate) { m_xCertificate = xCertificate; } void PDFSignatureHelper::SetDescription(const OUString& rDescription) { m_aDescription = rDescription; } bool PDFSignatureHelper::Sign(const uno::Reference& xModel, const uno::Reference& xInputStream, bool bAdES) { std::unique_ptr pStream(utl::UcbStreamHelper::CreateStream(xInputStream, true)); vcl::filter::PDFDocument aDocument; if (!aDocument.Read(*pStream)) { SAL_WARN("xmlsecurity.helper", "failed to read the document"); return false; } sal_Int32 nPage = 0; std::vector aSignatureLineShape; GetSignatureLineShape(xModel, nPage, aSignatureLineShape); if (nPage > 0) { // UNO page number is 1-based. aDocument.SetSignaturePage(nPage - 1); } if (!aSignatureLineShape.empty()) { aDocument.SetSignatureLine(std::move(aSignatureLineShape)); } if (!aDocument.Sign(m_xCertificate, m_aDescription, bAdES)) { SAL_WARN("xmlsecurity.helper", "failed to sign"); return false; } uno::Reference xStream(xInputStream, uno::UNO_QUERY); std::unique_ptr pOutStream(utl::UcbStreamHelper::CreateStream(xStream, true)); if (!aDocument.Write(*pOutStream)) { SAL_WARN("xmlsecurity.helper", "failed to write signed data"); return false; } return true; } bool PDFSignatureHelper::RemoveSignature(const uno::Reference& xInputStream, sal_uInt16 nPosition) { std::unique_ptr pStream(utl::UcbStreamHelper::CreateStream(xInputStream, true)); vcl::filter::PDFDocument aDocument; if (!aDocument.Read(*pStream)) { SAL_WARN("xmlsecurity.helper", "failed to read the document"); return false; } if (!aDocument.RemoveSignature(nPosition)) { SAL_WARN("xmlsecurity.helper", "failed to remove signature"); return false; } uno::Reference xStream(xInputStream, uno::UNO_QUERY); uno::Reference xTruncate(xStream, uno::UNO_QUERY); if (!xTruncate.is()) { SAL_WARN("xmlsecurity.helper", "failed to truncate"); return false; } xTruncate->truncate(); std::unique_ptr pOutStream(utl::UcbStreamHelper::CreateStream(xStream, true)); if (!aDocument.Write(*pOutStream)) { SAL_WARN("xmlsecurity.helper", "failed to write without signature"); return false; } return true; } /* vim:set shiftwidth=4 softtabstop=4 expandtab: */