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