1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /*
3  * This file is part of the LibreOffice project.
4  *
5  * This Source Code Form is subject to the terms of the Mozilla Public
6  * License, v. 2.0. If a copy of the MPL was not distributed with this
7  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8  */
9 
10 #include <sal/config.h>
11 
12 #include <config_crypto.h>
13 
14 #if USE_CRYPTO_NSS
15 #include <secoid.h>
16 #endif
17 
18 #include <string_view>
19 
20 #include <com/sun/star/xml/crypto/SEInitializer.hpp>
21 #include <com/sun/star/security/DocumentSignatureInformation.hpp>
22 
23 #include <osl/file.hxx>
24 #include <sal/log.hxx>
25 #include <test/bootstrapfixture.hxx>
26 #include <unotest/macros_test.hxx>
27 #include <tools/datetime.hxx>
28 #include <unotools/streamwrap.hxx>
29 #include <unotools/ucbstreamhelper.hxx>
30 #include <vcl/filter/pdfdocument.hxx>
31 #include <vcl/filter/PDFiumLibrary.hxx>
32 
33 #include <documentsignaturemanager.hxx>
34 #include <pdfsignaturehelper.hxx>
35 
36 #ifdef _WIN32
37 #define WIN32_LEAN_AND_MEAN
38 #include <windows.h>
39 #endif
40 
41 using namespace com::sun::star;
42 
43 namespace
44 {
45 constexpr OUStringLiteral DATA_DIRECTORY = u"/xmlsecurity/qa/unit/pdfsigning/data/";
46 }
47 
48 /// Testsuite for the PDF signing feature.
49 class PDFSigningTest : public test::BootstrapFixture, public unotest::MacrosTest
50 {
51 protected:
52     /**
53      * Sign rInURL once and save the result as rOutURL, asserting that rInURL
54      * had nOriginalSignatureCount signatures.
55      */
56     bool sign(const OUString& rInURL, const OUString& rOutURL, size_t nOriginalSignatureCount);
57     /**
58      * Read a pdf and make sure that it has the expected number of valid
59      * signatures.
60      */
61     std::vector<SignatureInformation> verify(const OUString& rURL, size_t nCount);
62 
63 public:
64     PDFSigningTest();
65     void setUp() override;
66     void tearDown() override;
67 };
68 
PDFSigningTest()69 PDFSigningTest::PDFSigningTest() {}
70 
setUp()71 void PDFSigningTest::setUp()
72 {
73     test::BootstrapFixture::setUp();
74     MacrosTest::setUpNssGpg(m_directories, "xmlsecurity_pdfsigning");
75 
76     uno::Reference<xml::crypto::XSEInitializer> xSEInitializer
77         = xml::crypto::SEInitializer::create(mxComponentContext);
78     uno::Reference<xml::crypto::XXMLSecurityContext> xSecurityContext
79         = xSEInitializer->createSecurityContext(OUString());
80 #if USE_CRYPTO_NSS
81 #ifdef NSS_USE_ALG_IN_ANY_SIGNATURE
82     // policy may disallow using SHA1 for signatures but unit test documents
83     // have such existing signatures (call this after createSecurityContext!)
84     NSS_SetAlgorithmPolicy(SEC_OID_SHA1, NSS_USE_ALG_IN_ANY_SIGNATURE, 0);
85 #endif
86 #endif
87 }
88 
tearDown()89 void PDFSigningTest::tearDown()
90 {
91     MacrosTest::tearDownNssGpg();
92     test::BootstrapFixture::tearDown();
93 }
94 
verify(const OUString & rURL,size_t nCount)95 std::vector<SignatureInformation> PDFSigningTest::verify(const OUString& rURL, size_t nCount)
96 {
97     uno::Reference<xml::crypto::XSEInitializer> xSEInitializer
98         = xml::crypto::SEInitializer::create(mxComponentContext);
99     uno::Reference<xml::crypto::XXMLSecurityContext> xSecurityContext
100         = xSEInitializer->createSecurityContext(OUString());
101     std::vector<SignatureInformation> aRet;
102 
103     SvFileStream aStream(rURL, StreamMode::READ);
104     PDFSignatureHelper aHelper;
105     CPPUNIT_ASSERT(aHelper.ReadAndVerifySignatureSvStream(aStream));
106 
107     std::shared_ptr<vcl::pdf::PDFium> pPDFium = vcl::pdf::PDFiumLibrary::get();
108     if (pPDFium)
109     {
110         CPPUNIT_ASSERT_EQUAL(nCount, aHelper.GetSignatureInformations().size());
111     }
112     for (size_t i = 0; i < aHelper.GetSignatureInformations().size(); ++i)
113     {
114         const SignatureInformation& rInfo = aHelper.GetSignatureInformations()[i];
115         aRet.push_back(rInfo);
116     }
117 
118     return aRet;
119 }
120 
sign(const OUString & rInURL,const OUString & rOutURL,size_t nOriginalSignatureCount)121 bool PDFSigningTest::sign(const OUString& rInURL, const OUString& rOutURL,
122                           size_t nOriginalSignatureCount)
123 {
124     // Make sure that input has nOriginalSignatureCount signatures.
125     uno::Reference<xml::crypto::XSEInitializer> xSEInitializer
126         = xml::crypto::SEInitializer::create(mxComponentContext);
127     uno::Reference<xml::crypto::XXMLSecurityContext> xSecurityContext
128         = xSEInitializer->createSecurityContext(OUString());
129     vcl::filter::PDFDocument aDocument;
130     {
131         SvFileStream aStream(rInURL, StreamMode::READ);
132         CPPUNIT_ASSERT(aDocument.Read(aStream));
133         std::vector<vcl::filter::PDFObjectElement*> aSignatures = aDocument.GetSignatureWidgets();
134         CPPUNIT_ASSERT_EQUAL(nOriginalSignatureCount, aSignatures.size());
135     }
136 
137     bool bSignSuccessful = false;
138     // Sign it and write out the result.
139     {
140         uno::Reference<xml::crypto::XSecurityEnvironment> xSecurityEnvironment
141             = xSecurityContext->getSecurityEnvironment();
142         uno::Sequence<uno::Reference<security::XCertificate>> aCertificates
143             = xSecurityEnvironment->getPersonalCertificates();
144         DateTime now(DateTime::SYSTEM);
145         for (auto& cert : aCertificates)
146         {
147             css::util::DateTime aNotValidAfter = cert->getNotValidAfter();
148             css::util::DateTime aNotValidBefore = cert->getNotValidBefore();
149 
150             // Only try certificates that are already active and not expired
151             if ((now > aNotValidAfter) || (now < aNotValidBefore))
152             {
153                 SAL_WARN("xmlsecurity.qa",
154                          "Skipping a certificate that is not yet valid or already not valid");
155             }
156             else
157             {
158                 bool bSignResult = aDocument.Sign(cert, "test", /*bAdES=*/true);
159 #ifdef _WIN32
160                 if (!bSignResult)
161                 {
162                     DWORD dwErr = GetLastError();
163                     if (HRESULT_FROM_WIN32(dwErr) == CRYPT_E_NO_KEY_PROPERTY)
164                     {
165                         SAL_WARN("xmlsecurity.qa", "Skipping a certificate without a private key");
166                         continue; // The certificate does not have a private key - not a valid certificate
167                     }
168                 }
169 #endif
170                 CPPUNIT_ASSERT(bSignResult);
171                 SvFileStream aOutStream(rOutURL, StreamMode::WRITE | StreamMode::TRUNC);
172                 CPPUNIT_ASSERT(aDocument.Write(aOutStream));
173                 bSignSuccessful = true;
174                 break;
175             }
176         }
177     }
178 
179     // This was nOriginalSignatureCount when PDFDocument::Sign() silently returned success, without doing anything.
180     if (bSignSuccessful)
181         verify(rOutURL, nOriginalSignatureCount + 1);
182 
183     // May return false if NSS failed to parse its own profile or Windows has no valid certificates installed.
184     return bSignSuccessful;
185 }
186 
187 /// Test adding a new signature to a previously unsigned file.
CPPUNIT_TEST_FIXTURE(PDFSigningTest,testPDFAdd)188 CPPUNIT_TEST_FIXTURE(PDFSigningTest, testPDFAdd)
189 {
190     std::shared_ptr<vcl::pdf::PDFium> pPDFium = vcl::pdf::PDFiumLibrary::get();
191     if (!pPDFium)
192     {
193         return;
194     }
195 
196     OUString aSourceDir = m_directories.getURLFromSrc(DATA_DIRECTORY);
197     OUString aInURL = aSourceDir + "no.pdf";
198     OUString aTargetDir
199         = m_directories.getURLFromWorkdir(u"/CppunitTest/xmlsecurity_pdfsigning.test.user/");
200     OUString aOutURL = aTargetDir + "add.pdf";
201     bool bHadCertificates = sign(aInURL, aOutURL, 0);
202 
203     if (bHadCertificates)
204     {
205         // Assert that the SubFilter is not adbe.pkcs7.detached in the bAdES case.
206         std::vector<SignatureInformation> aInfos = verify(aOutURL, 1);
207         CPPUNIT_ASSERT(aInfos[0].bHasSigningCertificate);
208         // Make sure the timestamp is correct.
209         DateTime aDateTime(DateTime::SYSTEM);
210         // This was 0 (on Windows), as neither the /M key nor the PKCS#7 blob contained a timestamp.
211         CPPUNIT_ASSERT_EQUAL(aDateTime.GetYear(), aInfos[0].stDateTime.Year);
212         // Assert that the digest algorithm is not SHA-1 in the bAdES case.
213         CPPUNIT_ASSERT_EQUAL(xml::crypto::DigestID::SHA256, aInfos[0].nDigestID);
214     }
215 }
216 
217 /// Test signing a previously unsigned file twice.
CPPUNIT_TEST_FIXTURE(PDFSigningTest,testPDFAdd2)218 CPPUNIT_TEST_FIXTURE(PDFSigningTest, testPDFAdd2)
219 {
220     // Sign.
221     OUString aSourceDir = m_directories.getURLFromSrc(DATA_DIRECTORY);
222     OUString aInURL = aSourceDir + "no.pdf";
223     OUString aTargetDir
224         = m_directories.getURLFromWorkdir(u"/CppunitTest/xmlsecurity_pdfsigning.test.user/");
225     OUString aOutURL = aTargetDir + "add.pdf";
226     bool bHadCertificates = sign(aInURL, aOutURL, 0);
227 
228     // Sign again.
229     aInURL = aTargetDir + "add.pdf";
230     aOutURL = aTargetDir + "add2.pdf";
231     // This failed with "second range end is not the end of the file" for the
232     // first signature.
233     if (bHadCertificates)
234         sign(aInURL, aOutURL, 1);
235 }
236 
237 /// Test removing a signature from a previously signed file.
CPPUNIT_TEST_FIXTURE(PDFSigningTest,testPDFRemove)238 CPPUNIT_TEST_FIXTURE(PDFSigningTest, testPDFRemove)
239 {
240     std::shared_ptr<vcl::pdf::PDFium> pPDFium = vcl::pdf::PDFiumLibrary::get();
241     if (!pPDFium)
242     {
243         return;
244     }
245 
246     // Make sure that good.pdf has 1 valid signature.
247     uno::Reference<xml::crypto::XSEInitializer> xSEInitializer
248         = xml::crypto::SEInitializer::create(mxComponentContext);
249     uno::Reference<xml::crypto::XXMLSecurityContext> xSecurityContext
250         = xSEInitializer->createSecurityContext(OUString());
251     vcl::filter::PDFDocument aDocument;
252     OUString aSourceDir = m_directories.getURLFromSrc(DATA_DIRECTORY);
253     OUString aInURL = aSourceDir + "good.pdf";
254     {
255         SvFileStream aStream(aInURL, StreamMode::READ);
256         PDFSignatureHelper aHelper;
257         aHelper.ReadAndVerifySignatureSvStream(aStream);
258         CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(1), aHelper.GetSignatureInformations().size());
259     }
260 
261     // Remove the signature and write out the result as remove.pdf.
262     OUString aTargetDir
263         = m_directories.getURLFromWorkdir(u"/CppunitTest/xmlsecurity_pdfsigning.test.user/");
264     OUString aOutURL = aTargetDir + "remove.pdf";
265     osl::File::copy(aInURL, aOutURL);
266     {
267         uno::Reference<io::XInputStream> xInputStream;
268         std::unique_ptr<SvStream> pStream
269             = utl::UcbStreamHelper::CreateStream(aOutURL, StreamMode::READWRITE);
270         uno::Reference<io::XStream> xStream(new utl::OStreamWrapper(std::move(pStream)));
271         xInputStream.set(xStream, uno::UNO_QUERY);
272         PDFSignatureHelper::RemoveSignature(xInputStream, 0);
273     }
274 
275     // Read back the pdf and make sure that it no longer has signatures.
276     // This failed when PDFDocument::RemoveSignature() silently returned
277     // success, without doing anything.
278     verify(aOutURL, 0);
279 }
280 
281 /// Test removing all signatures from a previously multi-signed file.
CPPUNIT_TEST_FIXTURE(PDFSigningTest,testPDFRemoveAll)282 CPPUNIT_TEST_FIXTURE(PDFSigningTest, testPDFRemoveAll)
283 {
284     std::shared_ptr<vcl::pdf::PDFium> pPDFium = vcl::pdf::PDFiumLibrary::get();
285     if (!pPDFium)
286     {
287         return;
288     }
289 
290     // Make sure that good2.pdf has 2 valid signatures.  Unlike in
291     // testPDFRemove(), here intentionally test DocumentSignatureManager and
292     // PDFSignatureHelper code as well.
293     uno::Reference<xml::crypto::XSEInitializer> xSEInitializer
294         = xml::crypto::SEInitializer::create(mxComponentContext);
295     uno::Reference<xml::crypto::XXMLSecurityContext> xSecurityContext
296         = xSEInitializer->createSecurityContext(OUString());
297 
298     // Copy the test document to a temporary file, as it'll be modified.
299     OUString aTargetDir
300         = m_directories.getURLFromWorkdir(u"/CppunitTest/xmlsecurity_pdfsigning.test.user/");
301     OUString aOutURL = aTargetDir + "remove-all.pdf";
302     CPPUNIT_ASSERT_EQUAL(
303         osl::File::RC::E_None,
304         osl::File::copy(m_directories.getURLFromSrc(DATA_DIRECTORY) + "2good.pdf", aOutURL));
305     // Load the test document as a storage and read its two signatures.
306     DocumentSignatureManager aManager(mxComponentContext, DocumentSignatureMode::Content);
307     std::unique_ptr<SvStream> pStream
308         = utl::UcbStreamHelper::CreateStream(aOutURL, StreamMode::READ | StreamMode::WRITE);
309     uno::Reference<io::XStream> xStream(new utl::OStreamWrapper(std::move(pStream)));
310     aManager.setSignatureStream(xStream);
311     aManager.read(/*bUseTempStream=*/false);
312     std::vector<SignatureInformation>& rInformations = aManager.getCurrentSignatureInformations();
313     // This was 1 when NSS_CMSSignerInfo_GetSigningCertificate() failed, which
314     // means that we only used the locally imported certificates for
315     // verification, not the ones provided in the PDF signature data.
316     CPPUNIT_ASSERT_EQUAL(static_cast<std::size_t>(2), rInformations.size());
317 
318     // Request removal of the first signature, should imply removal of the
319     // second chained signature as well.
320     aManager.remove(0);
321     // This was 2, Manager didn't write anything to disk when removal succeeded
322     // (instead of doing that when removal failed).
323     // Then this was 1, when the chained signature wasn't removed.
324     CPPUNIT_ASSERT_EQUAL(static_cast<std::size_t>(0), rInformations.size());
325 }
326 
CPPUNIT_TEST_FIXTURE(PDFSigningTest,testTdf107782)327 CPPUNIT_TEST_FIXTURE(PDFSigningTest, testTdf107782)
328 {
329     uno::Reference<xml::crypto::XSEInitializer> xSEInitializer
330         = xml::crypto::SEInitializer::create(mxComponentContext);
331     uno::Reference<xml::crypto::XXMLSecurityContext> xSecurityContext
332         = xSEInitializer->createSecurityContext(OUString());
333 
334     // Load the test document as a storage and read its signatures.
335     DocumentSignatureManager aManager(mxComponentContext, DocumentSignatureMode::Content);
336     OUString aURL = m_directories.getURLFromSrc(DATA_DIRECTORY) + "tdf107782.pdf";
337     std::unique_ptr<SvStream> pStream
338         = utl::UcbStreamHelper::CreateStream(aURL, StreamMode::READ | StreamMode::WRITE);
339     uno::Reference<io::XStream> xStream(new utl::OStreamWrapper(std::move(pStream)));
340     aManager.setSignatureStream(xStream);
341     aManager.read(/*bUseTempStream=*/false);
342     CPPUNIT_ASSERT(aManager.hasPDFSignatureHelper());
343 
344     // This failed with an std::bad_alloc exception on Windows.
345     aManager.getPDFSignatureHelper().GetDocumentSignatureInformations(
346         aManager.getSecurityEnvironment());
347 }
348 
349 /// Test a PDF 1.4 document, signed by Adobe.
CPPUNIT_TEST_FIXTURE(PDFSigningTest,testPDF14Adobe)350 CPPUNIT_TEST_FIXTURE(PDFSigningTest, testPDF14Adobe)
351 {
352     std::shared_ptr<vcl::pdf::PDFium> pPDFium = vcl::pdf::PDFiumLibrary::get();
353     if (!pPDFium)
354     {
355         return;
356     }
357 
358     // Two signatures, first is SHA1, the second is SHA256.
359     // This was 0, as we failed to find the Annots key's value when it was a
360     // reference-to-array, not an array.
361     std::vector<SignatureInformation> aInfos
362         = verify(m_directories.getURLFromSrc(DATA_DIRECTORY) + "pdf14adobe.pdf", 2);
363     // This was 0, out-of-PKCS#7 signature date wasn't read.
364     CPPUNIT_ASSERT_EQUAL(static_cast<sal_Int16>(2016), aInfos[1].stDateTime.Year);
365 }
366 
367 /// Test a PDF 1.6 document, signed by Adobe.
CPPUNIT_TEST_FIXTURE(PDFSigningTest,testPDF16Adobe)368 CPPUNIT_TEST_FIXTURE(PDFSigningTest, testPDF16Adobe)
369 {
370     // Contains a cross-reference stream, object streams and a compressed
371     // stream with a predictor. And a valid signature.
372     // Found signatures was 0, as parsing failed due to lack of support for
373     // these features.
374     verify(m_directories.getURLFromSrc(DATA_DIRECTORY) + "pdf16adobe.pdf", 1);
375 }
376 
377 /// Test adding a signature to a PDF 1.6 document.
CPPUNIT_TEST_FIXTURE(PDFSigningTest,testPDF16Add)378 CPPUNIT_TEST_FIXTURE(PDFSigningTest, testPDF16Add)
379 {
380     // Contains PDF 1.6 features, make sure we can add a signature using that
381     // markup correctly.
382     OUString aSourceDir = m_directories.getURLFromSrc(DATA_DIRECTORY);
383     OUString aInURL = aSourceDir + "pdf16adobe.pdf";
384     OUString aTargetDir
385         = m_directories.getURLFromWorkdir(u"/CppunitTest/xmlsecurity_pdfsigning.test.user/");
386     OUString aOutURL = aTargetDir + "add.pdf";
387     // This failed: verification broke as incorrect xref stream was written as
388     // part of the new signature.
389     bool bHadCertificates = sign(aInURL, aOutURL, 1);
390 
391     // Sign again.
392     aInURL = aTargetDir + "add.pdf";
393     aOutURL = aTargetDir + "add2.pdf";
394     // This failed as non-compressed AcroForm wasn't handled.
395     if (bHadCertificates)
396         sign(aInURL, aOutURL, 2);
397 }
398 
399 /// Test a PDF 1.4 document, signed by LO on Windows.
CPPUNIT_TEST_FIXTURE(PDFSigningTest,testPDF14LOWin)400 CPPUNIT_TEST_FIXTURE(PDFSigningTest, testPDF14LOWin)
401 {
402     // mscrypto used SEC_OID_PKCS1_SHA1_WITH_RSA_ENCRYPTION as a digest
403     // algorithm when it meant SEC_OID_SHA1, make sure we tolerate that on all
404     // platforms.
405     // This failed, as NSS HASH_Create() didn't handle the sign algorithm.
406     verify(m_directories.getURLFromSrc(DATA_DIRECTORY) + "pdf14lowin.pdf", 1);
407 }
408 
409 /// Test a PAdES document, signed by LO on Linux.
CPPUNIT_TEST_FIXTURE(PDFSigningTest,testPDFPAdESGood)410 CPPUNIT_TEST_FIXTURE(PDFSigningTest, testPDFPAdESGood)
411 {
412     std::shared_ptr<vcl::pdf::PDFium> pPDFium = vcl::pdf::PDFiumLibrary::get();
413     if (!pPDFium)
414     {
415         return;
416     }
417 
418     std::vector<SignatureInformation> aInfos
419         = verify(m_directories.getURLFromSrc(DATA_DIRECTORY) + "good-pades.pdf", 1);
420     CPPUNIT_ASSERT(aInfos[0].bHasSigningCertificate);
421 }
422 
423 /// Test a valid signature that does not cover the whole file.
CPPUNIT_TEST_FIXTURE(PDFSigningTest,testPartial)424 CPPUNIT_TEST_FIXTURE(PDFSigningTest, testPartial)
425 {
426     std::shared_ptr<vcl::pdf::PDFium> pPDFium = vcl::pdf::PDFiumLibrary::get();
427     if (!pPDFium)
428     {
429         return;
430     }
431 
432     std::vector<SignatureInformation> aInfos
433         = verify(m_directories.getURLFromSrc(DATA_DIRECTORY) + "partial.pdf", 1);
434     CPPUNIT_ASSERT(!aInfos.empty());
435     SignatureInformation& rInformation = aInfos[0];
436     CPPUNIT_ASSERT(rInformation.bPartialDocumentSignature);
437 }
438 
CPPUNIT_TEST_FIXTURE(PDFSigningTest,testPartialInBetween)439 CPPUNIT_TEST_FIXTURE(PDFSigningTest, testPartialInBetween)
440 {
441     std::shared_ptr<vcl::pdf::PDFium> pPDFium = vcl::pdf::PDFiumLibrary::get();
442     if (!pPDFium)
443     {
444         return;
445     }
446 
447     std::vector<SignatureInformation> aInfos
448         = verify(m_directories.getURLFromSrc(DATA_DIRECTORY) + "partial-in-between.pdf", 2);
449     CPPUNIT_ASSERT(!aInfos.empty());
450     SignatureInformation& rInformation = aInfos[0];
451     // Without the accompanying fix in place, this test would have failed, as unsigned incremental
452     // update between two signatures were not detected.
453     CPPUNIT_ASSERT(rInformation.bPartialDocumentSignature);
454 }
455 
CPPUNIT_TEST_FIXTURE(PDFSigningTest,testBadCertP1)456 CPPUNIT_TEST_FIXTURE(PDFSigningTest, testBadCertP1)
457 {
458     std::shared_ptr<vcl::pdf::PDFium> pPDFium = vcl::pdf::PDFiumLibrary::get();
459     if (!pPDFium)
460     {
461         return;
462     }
463 
464     std::vector<SignatureInformation> aInfos
465         = verify(m_directories.getURLFromSrc(DATA_DIRECTORY) + "bad-cert-p1.pdf", 1);
466     CPPUNIT_ASSERT(!aInfos.empty());
467     SignatureInformation& rInformation = aInfos[0];
468     // Without the accompanying fix in place, this test would have failed with:
469     // - Expected: 0 (SecurityOperationStatus_UNKNOWN)
470     // - Actual  : 1 (SecurityOperationStatus_OPERATION_SUCCEEDED)
471     // i.e. annotation after a P1 signature was not considered as a bad modification.
472     CPPUNIT_ASSERT_EQUAL(xml::crypto::SecurityOperationStatus::SecurityOperationStatus_UNKNOWN,
473                          rInformation.nStatus);
474 }
475 
CPPUNIT_TEST_FIXTURE(PDFSigningTest,testBadCertP3Stamp)476 CPPUNIT_TEST_FIXTURE(PDFSigningTest, testBadCertP3Stamp)
477 {
478     std::shared_ptr<vcl::pdf::PDFium> pPDFium = vcl::pdf::PDFiumLibrary::get();
479     if (!pPDFium)
480     {
481         return;
482     }
483 
484     std::vector<SignatureInformation> aInfos
485         = verify(m_directories.getURLFromSrc(DATA_DIRECTORY) + "bad-cert-p3-stamp.pdf", 1);
486     CPPUNIT_ASSERT(!aInfos.empty());
487     SignatureInformation& rInformation = aInfos[0];
488 
489     // Without the accompanying fix in place, this test would have failed with:
490     // - Expected: 0 (SecurityOperationStatus_UNKNOWN)
491     // - Actual  : 1 (SecurityOperationStatus_OPERATION_SUCCEEDED)
492     // i.e. adding a stamp annotation was not considered as a bad modification.
493     CPPUNIT_ASSERT_EQUAL(xml::crypto::SecurityOperationStatus::SecurityOperationStatus_UNKNOWN,
494                          rInformation.nStatus);
495 }
496 
497 /// Test writing a PAdES signature.
CPPUNIT_TEST_FIXTURE(PDFSigningTest,testSigningCertificateAttribute)498 CPPUNIT_TEST_FIXTURE(PDFSigningTest, testSigningCertificateAttribute)
499 {
500     std::shared_ptr<vcl::pdf::PDFium> pPDFium = vcl::pdf::PDFiumLibrary::get();
501     if (!pPDFium)
502     {
503         return;
504     }
505 
506     // Create a new signature.
507     OUString aSourceDir = m_directories.getURLFromSrc(DATA_DIRECTORY);
508     OUString aInURL = aSourceDir + "no.pdf";
509     OUString aTargetDir
510         = m_directories.getURLFromWorkdir(u"/CppunitTest/xmlsecurity_pdfsigning.test.user/");
511     OUString aOutURL = aTargetDir + "signing-certificate-attribute.pdf";
512     bool bHadCertificates = sign(aInURL, aOutURL, 0);
513     if (!bHadCertificates)
514         return;
515 
516     // Verify it.
517     std::vector<SignatureInformation> aInfos = verify(aOutURL, 1);
518     CPPUNIT_ASSERT(!aInfos.empty());
519     SignatureInformation& rInformation = aInfos[0];
520     // Assert that it has a signed signingCertificateV2 attribute.
521     CPPUNIT_ASSERT(rInformation.bHasSigningCertificate);
522 }
523 
524 /// Test that we accept files which are supposed to be good.
CPPUNIT_TEST_FIXTURE(PDFSigningTest,testGood)525 CPPUNIT_TEST_FIXTURE(PDFSigningTest, testGood)
526 {
527     std::shared_ptr<vcl::pdf::PDFium> pPDFium = vcl::pdf::PDFiumLibrary::get();
528     if (!pPDFium)
529     {
530         return;
531     }
532 
533     const std::initializer_list<std::u16string_view> aNames = {
534         // We failed to determine if this is good or bad.
535         u"good-non-detached.pdf",
536         // Boolean value for dictionary key caused read error.
537         u"dict-bool.pdf",
538     };
539 
540     for (const auto& rName : aNames)
541     {
542         std::vector<SignatureInformation> aInfos
543             = verify(m_directories.getURLFromSrc(DATA_DIRECTORY) + rName, 1);
544         CPPUNIT_ASSERT(!aInfos.empty());
545         SignatureInformation& rInformation = aInfos[0];
546         CPPUNIT_ASSERT_EQUAL(int(xml::crypto::SecurityOperationStatus_OPERATION_SUCCEEDED),
547                              static_cast<int>(rInformation.nStatus));
548     }
549 }
550 
551 /// Test that we don't crash / loop while tokenizing these files.
CPPUNIT_TEST_FIXTURE(PDFSigningTest,testTokenize)552 CPPUNIT_TEST_FIXTURE(PDFSigningTest, testTokenize)
553 {
554     const std::initializer_list<std::u16string_view> aNames = {
555         // We looped on this broken input.
556         u"no-eof.pdf",
557         // ']' in a name token was mishandled.
558         u"name-bracket.pdf",
559         // %%EOF at the end wasn't followed by a newline.
560         u"noeol.pdf",
561         // File that's intentionally smaller than 1024 bytes.
562         u"small.pdf",
563         u"tdf107149.pdf",
564         // Nested parentheses were not handled.
565         u"tdf114460.pdf",
566         // Valgrind was unhappy about this.
567         u"forcepoint16.pdf",
568     };
569 
570     for (const auto& rName : aNames)
571     {
572         SvFileStream aStream(m_directories.getURLFromSrc(DATA_DIRECTORY) + rName, StreamMode::READ);
573         vcl::filter::PDFDocument aDocument;
574         // Just make sure the tokenizer finishes without an error, don't look at the signature.
575         CPPUNIT_ASSERT(aDocument.Read(aStream));
576 
577         if (rName == u"tdf107149.pdf")
578             // This failed, page list was empty.
579             CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(1), aDocument.GetPages().size());
580     }
581 }
582 
583 /// Test handling of unknown SubFilter values.
CPPUNIT_TEST_FIXTURE(PDFSigningTest,testUnknownSubFilter)584 CPPUNIT_TEST_FIXTURE(PDFSigningTest, testUnknownSubFilter)
585 {
586     std::shared_ptr<vcl::pdf::PDFium> pPDFium = vcl::pdf::PDFiumLibrary::get();
587     if (!pPDFium)
588     {
589         return;
590     }
591 
592     // Tokenize the bugdoc.
593     uno::Reference<xml::crypto::XSEInitializer> xSEInitializer
594         = xml::crypto::SEInitializer::create(mxComponentContext);
595     uno::Reference<xml::crypto::XXMLSecurityContext> xSecurityContext
596         = xSEInitializer->createSecurityContext(OUString());
597     std::unique_ptr<SvStream> pStream = utl::UcbStreamHelper::CreateStream(
598         m_directories.getURLFromSrc(DATA_DIRECTORY) + "cr-comment.pdf", StreamMode::STD_READ);
599     uno::Reference<io::XStream> xStream(new utl::OStreamWrapper(std::move(pStream)));
600     DocumentSignatureManager aManager(mxComponentContext, DocumentSignatureMode::Content);
601     aManager.setSignatureStream(xStream);
602     aManager.read(/*bUseTempStream=*/false);
603 
604     // Make sure we find both signatures, even if the second has unknown SubFilter.
605     std::vector<SignatureInformation>& rInformations = aManager.getCurrentSignatureInformations();
606     CPPUNIT_ASSERT_EQUAL(static_cast<std::size_t>(2), rInformations.size());
607 }
608 
CPPUNIT_TEST_FIXTURE(PDFSigningTest,testGoodCustomMagic)609 CPPUNIT_TEST_FIXTURE(PDFSigningTest, testGoodCustomMagic)
610 {
611     std::shared_ptr<vcl::pdf::PDFium> pPDFium = vcl::pdf::PDFiumLibrary::get();
612     if (!pPDFium)
613     {
614         return;
615     }
616 
617     // Tokenize the bugdoc.
618     uno::Reference<xml::crypto::XSEInitializer> xSEInitializer
619         = xml::crypto::SEInitializer::create(mxComponentContext);
620     uno::Reference<xml::crypto::XXMLSecurityContext> xSecurityContext
621         = xSEInitializer->createSecurityContext(OUString());
622     std::unique_ptr<SvStream> pStream = utl::UcbStreamHelper::CreateStream(
623         m_directories.getURLFromSrc(DATA_DIRECTORY) + "good-custom-magic.pdf",
624         StreamMode::STD_READ);
625     uno::Reference<io::XStream> xStream(new utl::OStreamWrapper(std::move(pStream)));
626     DocumentSignatureManager aManager(mxComponentContext, DocumentSignatureMode::Content);
627     aManager.setSignatureStream(xStream);
628     aManager.read(/*bUseTempStream=*/false);
629 
630     // Without the accompanying fix in place, this test would have failed with:
631     // - Expected: 1 (SecurityOperationStatus_OPERATION_SUCCEEDED)
632     // - Actual  : 0 (SecurityOperationStatus_UNKNOWN)
633     // i.e. no signatures were found due to a custom non-comment magic after the header.
634     std::vector<SignatureInformation>& rInformations = aManager.getCurrentSignatureInformations();
635     CPPUNIT_ASSERT_EQUAL(static_cast<std::size_t>(1), rInformations.size());
636 }
637 
638 CPPUNIT_PLUGIN_IMPLEMENT();
639 
640 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
641