/* -*- 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 <com/sun/star/xml/crypto/SEInitializer.hpp>
#include <com/sun/star/security/DocumentSignatureInformation.hpp>
#include <comphelper/processfactory.hxx>
#include <osl/file.hxx>
#include <sal/log.hxx>
#include <test/bootstrapfixture.hxx>
#include <tools/datetime.hxx>
#include <unotools/streamwrap.hxx>
#include <unotools/ucbstreamhelper.hxx>
#include <vcl/filter/pdfdocument.hxx>
#include <documentsignaturemanager.hxx>
#include <pdfio/pdfdocument.hxx>
#include <pdfsignaturehelper.hxx>
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#endif
using namespace com::sun::star;
namespace
{
char const DATA_DIRECTORY[] = "/xmlsecurity/qa/unit/pdfsigning/data/";
}
/// Testsuite for the PDF signing feature.
class PDFSigningTest : public test::BootstrapFixture
{
uno::Reference<uno::XComponentContext> mxComponentContext;
/**
* Sign rInURL once and save the result as rOutURL, asserting that rInURL
* had nOriginalSignatureCount signatures.
*/
bool sign(const OUString& rInURL, const OUString& rOutURL, size_t nOriginalSignatureCount);
/**
* Read a pdf and make sure that it has the expected number of valid
* signatures.
*/
std::vector<SignatureInformation> verify(const OUString& rURL, size_t nCount,
const OString& rExpectedSubFilter);
public:
PDFSigningTest();
void setUp() override;
/// Test adding a new signature to a previously unsigned file.
void testPDFAdd();
/// Test signing a previously unsigned file twice.
void testPDFAdd2();
/// Test removing a signature from a previously signed file.
void testPDFRemove();
/// Test removing all signatures from a previously multi-signed file.
void testPDFRemoveAll();
/// Test a PDF 1.4 document, signed by Adobe.
void testPDF14Adobe();
/// Test a PDF 1.6 document, signed by Adobe.
void testPDF16Adobe();
/// Test adding a signature to a PDF 1.6 document.
void testPDF16Add();
/// Test a PDF 1.4 document, signed by LO on Windows.
void testPDF14LOWin();
/// Test a PAdES document, signed by LO on Linux.
void testPDFPAdESGood();
/// Test a valid signature that does not cover the whole file.
void testPartial();
/// Test writing a PAdES signature.
void testSigningCertificateAttribute();
/// Test that we accept files which are supposed to be good.
void testGood();
/// Test that we don't crash / loop while tokenizing these files.
void testTokenize();
/// Test handling of unknown SubFilter values.
void testUnknownSubFilter();
void testTdf107782();
CPPUNIT_TEST_SUITE(PDFSigningTest);
CPPUNIT_TEST(testPDFAdd);
CPPUNIT_TEST(testPDFAdd2);
CPPUNIT_TEST(testPDFRemove);
CPPUNIT_TEST(testPDFRemoveAll);
CPPUNIT_TEST(testPDF14Adobe);
CPPUNIT_TEST(testPDF16Adobe);
CPPUNIT_TEST(testPDF16Add);
CPPUNIT_TEST(testPDF14LOWin);
CPPUNIT_TEST(testPDFPAdESGood);
CPPUNIT_TEST(testPartial);
CPPUNIT_TEST(testSigningCertificateAttribute);
CPPUNIT_TEST(testGood);
CPPUNIT_TEST(testTokenize);
CPPUNIT_TEST(testUnknownSubFilter);
CPPUNIT_TEST(testTdf107782);
CPPUNIT_TEST_SUITE_END();
};
PDFSigningTest::PDFSigningTest() {}
void PDFSigningTest::setUp()
{
test::BootstrapFixture::setUp();
mxComponentContext.set(comphelper::getComponentContext(getMultiServiceFactory()));
#ifndef _WIN32
// Set up cert8.db and key3.db in workdir/CppunitTest/
OUString aSourceDir = m_directories.getURLFromSrc(DATA_DIRECTORY);
OUString aTargetDir
= m_directories.getURLFromWorkdir("/CppunitTest/xmlsecurity_pdfsigning.test.user/");
osl::File::copy(aSourceDir + "cert8.db", aTargetDir + "cert8.db");
osl::File::copy(aSourceDir + "key3.db", aTargetDir + "key3.db");
OUString aTargetPath;
osl::FileBase::getSystemPathFromFileURL(aTargetDir, aTargetPath);
setenv("MOZILLA_CERTIFICATE_FOLDER", aTargetPath.toUtf8().getStr(), 1);
#endif
}
std::vector<SignatureInformation> PDFSigningTest::verify(const OUString& rURL, size_t nCount,
const OString& rExpectedSubFilter)
{
uno::Reference<xml::crypto::XSEInitializer> xSEInitializer
= xml::crypto::SEInitializer::create(mxComponentContext);
uno::Reference<xml::crypto::XXMLSecurityContext> xSecurityContext
= xSEInitializer->createSecurityContext(OUString());
std::vector<SignatureInformation> aRet;
SvFileStream aStream(rURL, StreamMode::READ);
vcl::filter::PDFDocument aVerifyDocument;
CPPUNIT_ASSERT(aVerifyDocument.Read(aStream));
std::vector<vcl::filter::PDFObjectElement*> aSignatures = aVerifyDocument.GetSignatureWidgets();
CPPUNIT_ASSERT_EQUAL(nCount, aSignatures.size());
for (size_t i = 0; i < aSignatures.size(); ++i)
{
SignatureInformation aInfo(i);
bool bLast = i == aSignatures.size() - 1;
CPPUNIT_ASSERT(
xmlsecurity::pdfio::ValidateSignature(aStream, aSignatures[i], aInfo, bLast));
aRet.push_back(aInfo);
if (!rExpectedSubFilter.isEmpty())
{
vcl::filter::PDFObjectElement* pValue = aSignatures[i]->LookupObject("V");
CPPUNIT_ASSERT(pValue);
auto pSubFilter
= dynamic_cast<vcl::filter::PDFNameElement*>(pValue->Lookup("SubFilter"));
CPPUNIT_ASSERT(pSubFilter);
CPPUNIT_ASSERT_EQUAL(rExpectedSubFilter, pSubFilter->GetValue());
}
}
return aRet;
}
bool PDFSigningTest::sign(const OUString& rInURL, const OUString& rOutURL,
size_t nOriginalSignatureCount)
{
// Make sure that input has nOriginalSignatureCount signatures.
uno::Reference<xml::crypto::XSEInitializer> xSEInitializer
= xml::crypto::SEInitializer::create(mxComponentContext);
uno::Reference<xml::crypto::XXMLSecurityContext> xSecurityContext
= xSEInitializer->createSecurityContext(OUString());
vcl::filter::PDFDocument aDocument;
{
SvFileStream aStream(rInURL, StreamMode::READ);
CPPUNIT_ASSERT(aDocument.Read(aStream));
std::vector<vcl::filter::PDFObjectElement*> aSignatures = aDocument.GetSignatureWidgets();
CPPUNIT_ASSERT_EQUAL(nOriginalSignatureCount, aSignatures.size());
}
bool bSignSuccessful = false;
// Sign it and write out the result.
{
uno::Reference<xml::crypto::XSecurityEnvironment> xSecurityEnvironment
= xSecurityContext->getSecurityEnvironment();
uno::Sequence<uno::Reference<security::XCertificate>> aCertificates
= xSecurityEnvironment->getPersonalCertificates();
DateTime now(DateTime::SYSTEM);
for (auto& cert : aCertificates)
{
css::util::DateTime aNotValidAfter = cert->getNotValidAfter();
css::util::DateTime aNotValidBefore = cert->getNotValidBefore();
// Only try certificates that are already active and not expired
if ((now > aNotValidAfter) || (now < aNotValidBefore))
{
SAL_WARN("xmlsecurity.pdfio.test",
"Skipping a certificate that is not yet valid or already not valid");
}
else
{
bool bSignResult = aDocument.Sign(cert, "test", /*bAdES=*/true);
#ifdef _WIN32
if (!bSignResult)
{
DWORD dwErr = GetLastError();
if (HRESULT_FROM_WIN32(dwErr) == CRYPT_E_NO_KEY_PROPERTY)
{
SAL_WARN("xmlsecurity.pdfio.test",
"Skipping a certificate without a private key");
continue; // The certificate does not have a private key - not a valid certificate
}
}
#endif
CPPUNIT_ASSERT(bSignResult);
SvFileStream aOutStream(rOutURL, StreamMode::WRITE | StreamMode::TRUNC);
CPPUNIT_ASSERT(aDocument.Write(aOutStream));
bSignSuccessful = true;
break;
}
}
}
// This was nOriginalSignatureCount when PDFDocument::Sign() silently returned success, without doing anything.
if (bSignSuccessful)
verify(rOutURL, nOriginalSignatureCount + 1, /*rExpectedSubFilter=*/OString());
// May return false if NSS failed to parse its own profile or Windows has no valid certificates installed.
return bSignSuccessful;
}
void PDFSigningTest::testPDFAdd()
{
OUString aSourceDir = m_directories.getURLFromSrc(DATA_DIRECTORY);
OUString aInURL = aSourceDir + "no.pdf";
OUString aTargetDir
= m_directories.getURLFromWorkdir("/CppunitTest/xmlsecurity_pdfsigning.test.user/");
OUString aOutURL = aTargetDir + "add.pdf";
bool bHadCertificates = sign(aInURL, aOutURL, 0);
if (bHadCertificates)
{
// Assert that the SubFilter is not adbe.pkcs7.detached in the bAdES case.
std::vector<SignatureInformation> aInfos = verify(aOutURL, 1, "ETSI.CAdES.detached");
// Make sure the timestamp is correct.
DateTime aDateTime(DateTime::SYSTEM);
// This was 0 (on Windows), as neither the /M key nor the PKCS#7 blob contained a timestamp.
CPPUNIT_ASSERT_EQUAL(aDateTime.GetYear(), aInfos[0].stDateTime.Year);
// Assert that the digest algorithm is not SHA-1 in the bAdES case.
CPPUNIT_ASSERT_EQUAL(xml::crypto::DigestID::SHA256, aInfos[0].nDigestID);
}
}
void PDFSigningTest::testPDFAdd2()
{
// Sign.
OUString aSourceDir = m_directories.getURLFromSrc(DATA_DIRECTORY);
OUString aInURL = aSourceDir + "no.pdf";
OUString aTargetDir
= m_directories.getURLFromWorkdir("/CppunitTest/xmlsecurity_pdfsigning.test.user/");
OUString aOutURL = aTargetDir + "add.pdf";
bool bHadCertificates = sign(aInURL, aOutURL, 0);
// Sign again.
aInURL = aTargetDir + "add.pdf";
aOutURL = aTargetDir + "add2.pdf";
// This failed with "second range end is not the end of the file" for the
// first signature.
if (bHadCertificates)
sign(aInURL, aOutURL, 1);
}
void PDFSigningTest::testPDFRemove()
{
// Make sure that good.pdf has 1 valid signature.
uno::Reference<xml::crypto::XSEInitializer> xSEInitializer
= xml::crypto::SEInitializer::create(mxComponentContext);
uno::Reference<xml::crypto::XXMLSecurityContext> xSecurityContext
= xSEInitializer->createSecurityContext(OUString());
vcl::filter::PDFDocument aDocument;
{
OUString aSourceDir = m_directories.getURLFromSrc(DATA_DIRECTORY);
OUString aInURL = aSourceDir + "good.pdf";
SvFileStream aStream(aInURL, StreamMode::READ);
CPPUNIT_ASSERT(aDocument.Read(aStream));
std::vector<vcl::filter::PDFObjectElement*> aSignatures = aDocument.GetSignatureWidgets();
CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(1), aSignatures.size());
SignatureInformation aInfo(0);
CPPUNIT_ASSERT(
xmlsecurity::pdfio::ValidateSignature(aStream, aSignatures[0], aInfo, /*bLast=*/true));
}
// Remove the signature and write out the result as remove.pdf.
OUString aTargetDir
= m_directories.getURLFromWorkdir("/CppunitTest/xmlsecurity_pdfsigning.test.user/");
OUString aOutURL = aTargetDir + "remove.pdf";
{
CPPUNIT_ASSERT(aDocument.RemoveSignature(0));
SvFileStream aOutStream(aOutURL, StreamMode::WRITE | StreamMode::TRUNC);
CPPUNIT_ASSERT(aDocument.Write(aOutStream));
}
// Read back the pdf and make sure that it no longer has signatures.
// This failed when PDFDocument::RemoveSignature() silently returned
// success, without doing anything.
verify(aOutURL, 0, /*rExpectedSubFilter=*/OString());
}
void PDFSigningTest::testPDFRemoveAll()
{
// Make sure that good2.pdf has 2 valid signatures. Unlike in
// testPDFRemove(), here intentionally test DocumentSignatureManager and
// PDFSignatureHelper code as well.
uno::Reference<xml::crypto::XSEInitializer> xSEInitializer
= xml::crypto::SEInitializer::create(mxComponentContext);
uno::Reference<xml::crypto::XXMLSecurityContext> xSecurityContext
= xSEInitializer->createSecurityContext(OUString());
// Copy the test document to a temporary file, as it'll be modified.
OUString aTargetDir
= m_directories.getURLFromWorkdir("/CppunitTest/xmlsecurity_pdfsigning.test.user/");
OUString aOutURL = aTargetDir + "remove-all.pdf";
CPPUNIT_ASSERT_EQUAL(
osl::File::RC::E_None,
osl::File::copy(m_directories.getURLFromSrc(DATA_DIRECTORY) + "2good.pdf", aOutURL));
// Load the test document as a storage and read its two signatures.
DocumentSignatureManager aManager(mxComponentContext, DocumentSignatureMode::Content);
std::unique_ptr<SvStream> pStream
= utl::UcbStreamHelper::CreateStream(aOutURL, StreamMode::READ | StreamMode::WRITE);
uno::Reference<io::XStream> xStream(new utl::OStreamWrapper(std::move(pStream)));
aManager.mxSignatureStream = xStream;
aManager.read(/*bUseTempStream=*/false);
std::vector<SignatureInformation>& rInformations = aManager.maCurrentSignatureInformations;
// This was 1 when NSS_CMSSignerInfo_GetSigningCertificate() failed, which
// means that we only used the locally imported certificates for
// verification, not the ones provided in the PDF signature data.
CPPUNIT_ASSERT_EQUAL(static_cast<std::size_t>(2), rInformations.size());
// Request removal of the first signature, should imply removal of the
// second chained signature as well.
aManager.remove(0);
// This was 2, Manager didn't write anything to disk when removal succeeded
// (instead of doing that when removal failed).
// Then this was 1, when the chained signature wasn't removed.
CPPUNIT_ASSERT_EQUAL(static_cast<std::size_t>(0), rInformations.size());
}
void PDFSigningTest::testTdf107782()
{
uno::Reference<xml::crypto::XSEInitializer> xSEInitializer
= xml::crypto::SEInitializer::create(mxComponentContext);
uno::Reference<xml::crypto::XXMLSecurityContext> xSecurityContext
= xSEInitializer->createSecurityContext(OUString());
// Load the test document as a storage and read its signatures.
DocumentSignatureManager aManager(mxComponentContext, DocumentSignatureMode::Content);
OUString aURL = m_directories.getURLFromSrc(DATA_DIRECTORY) + "tdf107782.pdf";
std::unique_ptr<SvStream> pStream
= utl::UcbStreamHelper::CreateStream(aURL, StreamMode::READ | StreamMode::WRITE);
uno::Reference<io::XStream> xStream(new utl::OStreamWrapper(std::move(pStream)));
aManager.mxSignatureStream = xStream;
aManager.read(/*bUseTempStream=*/false);
CPPUNIT_ASSERT(aManager.mpPDFSignatureHelper);
// This failed with an std::bad_alloc exception on Windows.
aManager.mpPDFSignatureHelper->GetDocumentSignatureInformations(
aManager.getSecurityEnvironment());
}
void PDFSigningTest::testPDF14Adobe()
{
// Two signatures, first is SHA1, the second is SHA256.
// This was 0, as we failed to find the Annots key's value when it was a
// reference-to-array, not an array.
std::vector<SignatureInformation> aInfos
= verify(m_directories.getURLFromSrc(DATA_DIRECTORY) + "pdf14adobe.pdf", 2,
/*rExpectedSubFilter=*/OString());
// This was 0, out-of-PKCS#7 signature date wasn't read.
CPPUNIT_ASSERT_EQUAL(static_cast<sal_Int16>(2016), aInfos[1].stDateTime.Year);
}
void PDFSigningTest::testPDF16Adobe()
{
// Contains a cross-reference stream, object streams and a compressed
// stream with a predictor. And a valid signature.
// Found signatures was 0, as parsing failed due to lack of support for
// these features.
verify(m_directories.getURLFromSrc(DATA_DIRECTORY) + "pdf16adobe.pdf", 1,
/*rExpectedSubFilter=*/OString());
}
void PDFSigningTest::testPDF16Add()
{
// Contains PDF 1.6 features, make sure we can add a signature using that
// markup correctly.
OUString aSourceDir = m_directories.getURLFromSrc(DATA_DIRECTORY);
OUString aInURL = aSourceDir + "pdf16adobe.pdf";
OUString aTargetDir
= m_directories.getURLFromWorkdir("/CppunitTest/xmlsecurity_pdfsigning.test.user/");
OUString aOutURL = aTargetDir + "add.pdf";
// This failed: verification broke as incorrect xref stream was written as
// part of the new signature.
bool bHadCertificates = sign(aInURL, aOutURL, 1);
// Sign again.
aInURL = aTargetDir + "add.pdf";
aOutURL = aTargetDir + "add2.pdf";
// This failed as non-compressed AcroForm wasn't handled.
if (bHadCertificates)
sign(aInURL, aOutURL, 2);
}
void PDFSigningTest::testPDF14LOWin()
{
// mscrypto used SEC_OID_PKCS1_SHA1_WITH_RSA_ENCRYPTION as a digest
// algorithm when it meant SEC_OID_SHA1, make sure we tolerate that on all
// platforms.
// This failed, as NSS HASH_Create() didn't handle the sign algorithm.
verify(m_directories.getURLFromSrc(DATA_DIRECTORY) + "pdf14lowin.pdf", 1,
/*rExpectedSubFilter=*/OString());
}
void PDFSigningTest::testPDFPAdESGood()
{
verify(m_directories.getURLFromSrc(DATA_DIRECTORY) + "good-pades.pdf", 1,
"ETSI.CAdES.detached");
}
void PDFSigningTest::testPartial()
{
std::vector<SignatureInformation> aInfos
= verify(m_directories.getURLFromSrc(DATA_DIRECTORY) + "partial.pdf", 1,
/*rExpectedSubFilter=*/OString());
CPPUNIT_ASSERT(!aInfos.empty());
SignatureInformation& rInformation = aInfos[0];
CPPUNIT_ASSERT(rInformation.bPartialDocumentSignature);
}
void PDFSigningTest::testSigningCertificateAttribute()
{
// Create a new signature.
OUString aSourceDir = m_directories.getURLFromSrc(DATA_DIRECTORY);
OUString aInURL = aSourceDir + "no.pdf";
OUString aTargetDir
= m_directories.getURLFromWorkdir("/CppunitTest/xmlsecurity_pdfsigning.test.user/");
OUString aOutURL = aTargetDir + "signing-certificate-attribute.pdf";
bool bHadCertificates = sign(aInURL, aOutURL, 0);
if (!bHadCertificates)
return;
// Verify it.
std::vector<SignatureInformation> aInfos = verify(aOutURL, 1, "ETSI.CAdES.detached");
CPPUNIT_ASSERT(!aInfos.empty());
SignatureInformation& rInformation = aInfos[0];
// Assert that it has a signed signingCertificateV2 attribute.
CPPUNIT_ASSERT(rInformation.bHasSigningCertificate);
}
void PDFSigningTest::testGood()
{
const std::initializer_list<OUStringLiteral> aNames = {
// We failed to determine if this is good or bad.
"good-non-detached.pdf",
// Boolean value for dictionary key caused read error.
"dict-bool.pdf",
};
for (const auto& rName : aNames)
{
std::vector<SignatureInformation> aInfos
= verify(m_directories.getURLFromSrc(DATA_DIRECTORY) + rName, 1,
/*rExpectedSubFilter=*/OString());
CPPUNIT_ASSERT(!aInfos.empty());
SignatureInformation& rInformation = aInfos[0];
CPPUNIT_ASSERT_EQUAL(int(xml::crypto::SecurityOperationStatus_OPERATION_SUCCEEDED),
static_cast<int>(rInformation.nStatus));
}
}
void PDFSigningTest::testTokenize()
{
const std::initializer_list<OUStringLiteral> aNames = {
// We looped on this broken input.
"no-eof.pdf",
// ']' in a name token was mishandled.
"name-bracket.pdf",
// %%EOF at the end wasn't followed by a newline.
"noeol.pdf",
// File that's intentionally smaller than 1024 bytes.
"small.pdf",
"tdf107149.pdf",
// Nested parentheses were not handled.
"tdf114460.pdf",
// Valgrind was unhappy about this.
"forcepoint16.pdf",
};
for (const auto& rName : aNames)
{
SvFileStream aStream(m_directories.getURLFromSrc(DATA_DIRECTORY) + rName, StreamMode::READ);
vcl::filter::PDFDocument aDocument;
// Just make sure the tokenizer finishes without an error, don't look at the signature.
CPPUNIT_ASSERT(aDocument.Read(aStream));
if (OUString(rName) == "tdf107149.pdf")
// This failed, page list was empty.
CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(1), aDocument.GetPages().size());
}
}
void PDFSigningTest::testUnknownSubFilter()
{
// 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) + "cr-comment.pdf", StreamMode::STD_READ);
uno::Reference<io::XStream> xStream(new utl::OStreamWrapper(std::move(pStream)));
DocumentSignatureManager aManager(mxComponentContext, DocumentSignatureMode::Content);
aManager.mxSignatureStream = xStream;
aManager.read(/*bUseTempStream=*/false);
// Make sure we find both signatures, even if the second has unknown SubFilter.
std::vector<SignatureInformation>& rInformations = aManager.maCurrentSignatureInformations;
CPPUNIT_ASSERT_EQUAL(static_cast<std::size_t>(2), rInformations.size());
}
CPPUNIT_TEST_SUITE_REGISTRATION(PDFSigningTest);
CPPUNIT_PLUGIN_IMPLEMENT();
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
↑ V522 There might be dereferencing of a potential null pointer 'pSubFilter'.