office-gobmx/xmlsecurity/source/helper/pdfsignaturehelper.cxx
Miklos Vajna 0de900cec7 cool#9992 lok doc sign, hash extract: time for getCommandValues('Signature')
The final goal of this API is to give time & hash information about the
PDF signature, so once a 3rd-party produces the PKCS#7 signature, that
can be added to the document and the actual PDF sign can be re-run with
the same parameters.

This commit continues the replacement of XCertificate with
svl::crypto::SigningContext up to the point that the timestamp used in
svl/ can be exposed on the LOK API.

This is done by updating DocumentSignatureManager::add(),
PDFSignatureHelper::SetX509Certificate(),
vcl::filter::PDFDocument::Sign() and finally the svl::crypto::Signing
ctor to work with the signing context instead of an XCertificate
directly.

Time reporting works now, so add a test for that. The digest part still
needs doing.

Change-Id: I83f1274cd420b67194b7caf12b1027e623d4f7fe
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/176404
Reviewed-by: Miklos Vajna <vmiklos@collabora.com>
Tested-by: Jenkins
2024-11-11 21:54:23 +01:00

628 lines
20 KiB
C++

/* -*- 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 <pdfsignaturehelper.hxx>
#include <memory>
#include <com/sun/star/io/XTruncate.hpp>
#include <com/sun/star/io/XStream.hpp>
#include <com/sun/star/security/CertificateValidity.hpp>
#include <com/sun/star/uno/SecurityException.hpp>
#include <com/sun/star/security/DocumentSignatureInformation.hpp>
#include <com/sun/star/xml/crypto/XSecurityEnvironment.hpp>
#include <com/sun/star/drawing/XShapes.hpp>
#include <com/sun/star/frame/XModel.hpp>
#include <com/sun/star/frame/XStorable.hpp>
#include <com/sun/star/beans/XPropertySet.hpp>
#include <com/sun/star/drawing/XDrawView.hpp>
#include <comphelper/propertysequence.hxx>
#include <sal/log.hxx>
#include <comphelper/diagnose_ex.hxx>
#include <unotools/mediadescriptor.hxx>
#include <unotools/streamwrap.hxx>
#include <unotools/ucbstreamhelper.hxx>
#include <vcl/filter/pdfdocument.hxx>
#include <vcl/checksum.hxx>
#include <svl/cryptosign.hxx>
#include <vcl/filter/PDFiumLibrary.hxx>
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<frame::XModel>& xModel, sal_Int32& rPage)
{
uno::Reference<drawing::XDrawView> xController(xModel->getCurrentController(), uno::UNO_QUERY);
if (!xController.is())
{
return false;
}
uno::Reference<beans::XPropertySet> xPage(xController->getCurrentPage(), uno::UNO_QUERY);
if (!xPage.is())
{
return false;
}
return xPage->getPropertyValue(u"Number"_ustr) >>= rPage;
}
/// If the currently selected shape is a Draw signature line, export that to PDF.
void GetSignatureLineShape(const uno::Reference<frame::XModel>& xModel, sal_Int32& rPage,
std::vector<sal_Int8>& rSignatureLineShape)
{
if (!xModel.is())
{
return;
}
if (!GetSignatureLinePage(xModel, rPage))
{
return;
}
uno::Reference<drawing::XShapes> xShapes(xModel->getCurrentSelection(), uno::UNO_QUERY);
if (!xShapes.is() || xShapes->getCount() < 1)
{
return;
}
uno::Reference<beans::XPropertySet> xShapeProps(xShapes->getByIndex(0), uno::UNO_QUERY);
if (!xShapeProps.is())
{
return;
}
comphelper::SequenceAsHashMap aMap(xShapeProps->getPropertyValue(u"InteropGrabBag"_ustr));
auto it = aMap.find(u"SignatureCertificate"_ustr);
if (it == aMap.end())
{
return;
}
// We know that we add a signature line shape to an existing PDF at this point.
uno::Reference<frame::XStorable> xStorable(xModel, uno::UNO_QUERY);
if (!xStorable.is())
{
return;
}
// Export just the signature line.
utl::MediaDescriptor aMediaDescriptor;
aMediaDescriptor[u"FilterName"_ustr] <<= u"draw_pdf_Export"_ustr;
SvMemoryStream aStream;
uno::Reference<io::XOutputStream> xStream(new utl::OStreamWrapper(aStream));
aMediaDescriptor[u"OutputStream"_ustr] <<= xStream;
uno::Sequence<beans::PropertyValue> aFilterData(
comphelper::InitPropertySequence({ { "Selection", uno::Any(xShapes) } }));
aMediaDescriptor[u"FilterData"_ustr] <<= aFilterData;
xStorable->storeToURL(u"private:stream"_ustr, aMediaDescriptor.getAsConstPropertyValueList());
xStream->flush();
aStream.Seek(0);
rSignatureLineShape = std::vector<sal_Int8>(aStream.GetSize());
aStream.ReadBytes(rSignatureLineShape.data(), rSignatureLineShape.size());
}
/// Represents a parsed signature.
struct Signature
{
std::unique_ptr<vcl::pdf::PDFiumSignature> m_pSignature;
/// Offset+length pairs.
std::vector<std::pair<size_t, size_t>> m_aByteRanges;
};
/// Turns an array of floats into offset + length pairs.
void GetByteRangesFromPDF(const std::unique_ptr<vcl::pdf::PDFiumSignature>& pSignature,
std::vector<std::pair<size_t, size_t>>& rByteRanges)
{
std::vector<int> 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<Signature>& 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<unsigned int>& rSignedEOFs,
const std::vector<unsigned int>& 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<basegfx::B2DRectangle> 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<PageChecksum>& rPageChecksums,
int nMDPPerm)
{
auto pPdfium = vcl::pdf::PDFiumLibrary::get();
std::unique_ptr<vcl::pdf::PDFiumDocument> 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<vcl::pdf::PDFiumPage> 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<vcl::pdf::PDFiumAnnotation> 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<PageChecksum> aSignedPages;
AnalyizeSignatureStream(aSignatureStream, aSignedPages, nMDPPerm);
SvMemoryStream aFullStream;
nPos = rStream.Tell();
rStream.Seek(0);
aFullStream.WriteStream(rStream);
rStream.Seek(nPos);
aFullStream.Seek(0);
std::vector<PageChecksum> 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<unsigned int>& rSignatureEOFs,
const std::vector<unsigned int>& rTrailerEnds)
{
std::vector<unsigned char> 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<io::XInputStream>& xInputStream)
{
if (!xInputStream.is())
{
SAL_WARN("xmlsecurity.helper", "input stream missing");
return false;
}
std::unique_ptr<SvStream> 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<vcl::pdf::PDFiumDocument> 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<Signature> aSignatures(nSignatureCount);
for (int i = 0; i < nSignatureCount; ++i)
{
std::unique_ptr<vcl::pdf::PDFiumSignature> pSignature = pPdfDocument->getSignature(i);
std::vector<std::pair<size_t, size_t>> aByteRanges;
GetByteRangesFromPDF(pSignature, aByteRanges);
aSignatures[i] = Signature{ std::move(pSignature), std::move(aByteRanges) };
}
std::set<unsigned int> aSignatureEOFs;
for (const auto& rSignature : aSignatures)
{
size_t nEOF = 0;
if (GetEOFOfSignature(rSignature, nEOF))
{
aSignatureEOFs.insert(nEOF);
}
}
std::vector<unsigned int> 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<security::DocumentSignatureInformation>
PDFSignatureHelper::GetDocumentSignatureInformations(
const uno::Reference<xml::crypto::XSecurityEnvironment>& xSecEnv) const
{
uno::Sequence<security::DocumentSignatureInformation> 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(svl::crypto::SigningContext& rSigningContext)
{
m_pSigningContext = &rSigningContext;
}
void PDFSignatureHelper::SetDescription(const OUString& rDescription)
{
m_aDescription = rDescription;
}
bool PDFSignatureHelper::Sign(const uno::Reference<frame::XModel>& xModel,
const uno::Reference<io::XInputStream>& xInputStream, bool bAdES)
{
std::unique_ptr<SvStream> 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<sal_Int8> 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 (!m_pSigningContext || !aDocument.Sign(*m_pSigningContext, m_aDescription, bAdES))
{
if (m_pSigningContext && m_pSigningContext->m_xCertificate.is())
{
SAL_WARN("xmlsecurity.helper", "failed to sign");
}
return false;
}
uno::Reference<io::XStream> xStream(xInputStream, uno::UNO_QUERY);
std::unique_ptr<SvStream> 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<io::XInputStream>& xInputStream,
sal_uInt16 nPosition)
{
std::unique_ptr<SvStream> 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<io::XStream> xStream(xInputStream, uno::UNO_QUERY);
uno::Reference<io::XTruncate> xTruncate(xStream, uno::UNO_QUERY);
if (!xTruncate.is())
{
SAL_WARN("xmlsecurity.helper", "failed to truncate");
return false;
}
xTruncate->truncate();
std::unique_ptr<SvStream> 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: */