bump product version to 6.4.0.3
[LibreOffice.git] / xmlsecurity / qa / unit / pdfsigning / pdfsigning.cxx
blob97e0b7d28f971563e17c79916bd0a08818e02dca
1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /*
3 * This file is part of the LibreOffice project.
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 */
10 #include <com/sun/star/xml/crypto/SEInitializer.hpp>
11 #include <com/sun/star/security/DocumentSignatureInformation.hpp>
13 #include <comphelper/processfactory.hxx>
14 #include <osl/file.hxx>
15 #include <sal/log.hxx>
16 #include <test/bootstrapfixture.hxx>
17 #include <tools/datetime.hxx>
18 #include <unotools/streamwrap.hxx>
19 #include <unotools/ucbstreamhelper.hxx>
20 #include <vcl/filter/pdfdocument.hxx>
22 #include <documentsignaturemanager.hxx>
23 #include <pdfio/pdfdocument.hxx>
24 #include <pdfsignaturehelper.hxx>
26 #ifdef _WIN32
27 #define WIN32_LEAN_AND_MEAN
28 #include <windows.h>
29 #endif
31 using namespace com::sun::star;
33 namespace
35 char const DATA_DIRECTORY[] = "/xmlsecurity/qa/unit/pdfsigning/data/";
38 /// Testsuite for the PDF signing feature.
39 class PDFSigningTest : public test::BootstrapFixture
41 protected:
42 uno::Reference<uno::XComponentContext> mxComponentContext;
44 /**
45 * Sign rInURL once and save the result as rOutURL, asserting that rInURL
46 * had nOriginalSignatureCount signatures.
48 bool sign(const OUString& rInURL, const OUString& rOutURL, size_t nOriginalSignatureCount);
49 /**
50 * Read a pdf and make sure that it has the expected number of valid
51 * signatures.
53 std::vector<SignatureInformation> verify(const OUString& rURL, size_t nCount,
54 const OString& rExpectedSubFilter);
56 public:
57 PDFSigningTest();
58 void setUp() override;
61 PDFSigningTest::PDFSigningTest() {}
63 void PDFSigningTest::setUp()
65 test::BootstrapFixture::setUp();
67 mxComponentContext.set(comphelper::getComponentContext(getMultiServiceFactory()));
69 #ifndef _WIN32
70 // Set up cert8.db and key3.db in workdir/CppunitTest/
71 OUString aSourceDir = m_directories.getURLFromSrc(DATA_DIRECTORY);
72 OUString aTargetDir
73 = m_directories.getURLFromWorkdir("/CppunitTest/xmlsecurity_pdfsigning.test.user/");
74 osl::File::copy(aSourceDir + "cert8.db", aTargetDir + "cert8.db");
75 osl::File::copy(aSourceDir + "key3.db", aTargetDir + "key3.db");
76 OUString aTargetPath;
77 osl::FileBase::getSystemPathFromFileURL(aTargetDir, aTargetPath);
78 setenv("MOZILLA_CERTIFICATE_FOLDER", aTargetPath.toUtf8().getStr(), 1);
79 #endif
82 std::vector<SignatureInformation> PDFSigningTest::verify(const OUString& rURL, size_t nCount,
83 const OString& rExpectedSubFilter)
85 uno::Reference<xml::crypto::XSEInitializer> xSEInitializer
86 = xml::crypto::SEInitializer::create(mxComponentContext);
87 uno::Reference<xml::crypto::XXMLSecurityContext> xSecurityContext
88 = xSEInitializer->createSecurityContext(OUString());
89 std::vector<SignatureInformation> aRet;
91 SvFileStream aStream(rURL, StreamMode::READ);
92 vcl::filter::PDFDocument aVerifyDocument;
93 CPPUNIT_ASSERT(aVerifyDocument.Read(aStream));
94 std::vector<vcl::filter::PDFObjectElement*> aSignatures = aVerifyDocument.GetSignatureWidgets();
95 CPPUNIT_ASSERT_EQUAL(nCount, aSignatures.size());
96 for (size_t i = 0; i < aSignatures.size(); ++i)
98 SignatureInformation aInfo(i);
99 bool bLast = i == aSignatures.size() - 1;
100 CPPUNIT_ASSERT(
101 xmlsecurity::pdfio::ValidateSignature(aStream, aSignatures[i], aInfo, bLast));
102 aRet.push_back(aInfo);
104 if (!rExpectedSubFilter.isEmpty())
106 vcl::filter::PDFObjectElement* pValue = aSignatures[i]->LookupObject("V");
107 CPPUNIT_ASSERT(pValue);
108 auto pSubFilter
109 = dynamic_cast<vcl::filter::PDFNameElement*>(pValue->Lookup("SubFilter"));
110 CPPUNIT_ASSERT(pSubFilter);
111 CPPUNIT_ASSERT_EQUAL(rExpectedSubFilter, pSubFilter->GetValue());
115 return aRet;
118 bool PDFSigningTest::sign(const OUString& rInURL, const OUString& rOutURL,
119 size_t nOriginalSignatureCount)
121 // Make sure that input has nOriginalSignatureCount signatures.
122 uno::Reference<xml::crypto::XSEInitializer> xSEInitializer
123 = xml::crypto::SEInitializer::create(mxComponentContext);
124 uno::Reference<xml::crypto::XXMLSecurityContext> xSecurityContext
125 = xSEInitializer->createSecurityContext(OUString());
126 vcl::filter::PDFDocument aDocument;
128 SvFileStream aStream(rInURL, StreamMode::READ);
129 CPPUNIT_ASSERT(aDocument.Read(aStream));
130 std::vector<vcl::filter::PDFObjectElement*> aSignatures = aDocument.GetSignatureWidgets();
131 CPPUNIT_ASSERT_EQUAL(nOriginalSignatureCount, aSignatures.size());
134 bool bSignSuccessful = false;
135 // Sign it and write out the result.
137 uno::Reference<xml::crypto::XSecurityEnvironment> xSecurityEnvironment
138 = xSecurityContext->getSecurityEnvironment();
139 uno::Sequence<uno::Reference<security::XCertificate>> aCertificates
140 = xSecurityEnvironment->getPersonalCertificates();
141 DateTime now(DateTime::SYSTEM);
142 for (auto& cert : aCertificates)
144 css::util::DateTime aNotValidAfter = cert->getNotValidAfter();
145 css::util::DateTime aNotValidBefore = cert->getNotValidBefore();
147 // Only try certificates that are already active and not expired
148 if ((now > aNotValidAfter) || (now < aNotValidBefore))
150 SAL_WARN("xmlsecurity.pdfio.test",
151 "Skipping a certificate that is not yet valid or already not valid");
153 else
155 bool bSignResult = aDocument.Sign(cert, "test", /*bAdES=*/true);
156 #ifdef _WIN32
157 if (!bSignResult)
159 DWORD dwErr = GetLastError();
160 if (HRESULT_FROM_WIN32(dwErr) == CRYPT_E_NO_KEY_PROPERTY)
162 SAL_WARN("xmlsecurity.pdfio.test",
163 "Skipping a certificate without a private key");
164 continue; // The certificate does not have a private key - not a valid certificate
167 #endif
168 CPPUNIT_ASSERT(bSignResult);
169 SvFileStream aOutStream(rOutURL, StreamMode::WRITE | StreamMode::TRUNC);
170 CPPUNIT_ASSERT(aDocument.Write(aOutStream));
171 bSignSuccessful = true;
172 break;
177 // This was nOriginalSignatureCount when PDFDocument::Sign() silently returned success, without doing anything.
178 if (bSignSuccessful)
179 verify(rOutURL, nOriginalSignatureCount + 1, /*rExpectedSubFilter=*/OString());
181 // May return false if NSS failed to parse its own profile or Windows has no valid certificates installed.
182 return bSignSuccessful;
185 /// Test adding a new signature to a previously unsigned file.
186 CPPUNIT_TEST_FIXTURE(PDFSigningTest, testPDFAdd)
188 OUString aSourceDir = m_directories.getURLFromSrc(DATA_DIRECTORY);
189 OUString aInURL = aSourceDir + "no.pdf";
190 OUString aTargetDir
191 = m_directories.getURLFromWorkdir("/CppunitTest/xmlsecurity_pdfsigning.test.user/");
192 OUString aOutURL = aTargetDir + "add.pdf";
193 bool bHadCertificates = sign(aInURL, aOutURL, 0);
195 if (bHadCertificates)
197 // Assert that the SubFilter is not adbe.pkcs7.detached in the bAdES case.
198 std::vector<SignatureInformation> aInfos = verify(aOutURL, 1, "ETSI.CAdES.detached");
199 // Make sure the timestamp is correct.
200 DateTime aDateTime(DateTime::SYSTEM);
201 // This was 0 (on Windows), as neither the /M key nor the PKCS#7 blob contained a timestamp.
202 CPPUNIT_ASSERT_EQUAL(aDateTime.GetYear(), aInfos[0].stDateTime.Year);
203 // Assert that the digest algorithm is not SHA-1 in the bAdES case.
204 CPPUNIT_ASSERT_EQUAL(xml::crypto::DigestID::SHA256, aInfos[0].nDigestID);
208 /// Test signing a previously unsigned file twice.
209 CPPUNIT_TEST_FIXTURE(PDFSigningTest, testPDFAdd2)
211 // Sign.
212 OUString aSourceDir = m_directories.getURLFromSrc(DATA_DIRECTORY);
213 OUString aInURL = aSourceDir + "no.pdf";
214 OUString aTargetDir
215 = m_directories.getURLFromWorkdir("/CppunitTest/xmlsecurity_pdfsigning.test.user/");
216 OUString aOutURL = aTargetDir + "add.pdf";
217 bool bHadCertificates = sign(aInURL, aOutURL, 0);
219 // Sign again.
220 aInURL = aTargetDir + "add.pdf";
221 aOutURL = aTargetDir + "add2.pdf";
222 // This failed with "second range end is not the end of the file" for the
223 // first signature.
224 if (bHadCertificates)
225 sign(aInURL, aOutURL, 1);
228 /// Test removing a signature from a previously signed file.
229 CPPUNIT_TEST_FIXTURE(PDFSigningTest, testPDFRemove)
231 // Make sure that good.pdf has 1 valid signature.
232 uno::Reference<xml::crypto::XSEInitializer> xSEInitializer
233 = xml::crypto::SEInitializer::create(mxComponentContext);
234 uno::Reference<xml::crypto::XXMLSecurityContext> xSecurityContext
235 = xSEInitializer->createSecurityContext(OUString());
236 vcl::filter::PDFDocument aDocument;
238 OUString aSourceDir = m_directories.getURLFromSrc(DATA_DIRECTORY);
239 OUString aInURL = aSourceDir + "good.pdf";
240 SvFileStream aStream(aInURL, StreamMode::READ);
241 CPPUNIT_ASSERT(aDocument.Read(aStream));
242 std::vector<vcl::filter::PDFObjectElement*> aSignatures = aDocument.GetSignatureWidgets();
243 CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(1), aSignatures.size());
244 SignatureInformation aInfo(0);
245 CPPUNIT_ASSERT(
246 xmlsecurity::pdfio::ValidateSignature(aStream, aSignatures[0], aInfo, /*bLast=*/true));
249 // Remove the signature and write out the result as remove.pdf.
250 OUString aTargetDir
251 = m_directories.getURLFromWorkdir("/CppunitTest/xmlsecurity_pdfsigning.test.user/");
252 OUString aOutURL = aTargetDir + "remove.pdf";
254 CPPUNIT_ASSERT(aDocument.RemoveSignature(0));
255 SvFileStream aOutStream(aOutURL, StreamMode::WRITE | StreamMode::TRUNC);
256 CPPUNIT_ASSERT(aDocument.Write(aOutStream));
259 // Read back the pdf and make sure that it no longer has signatures.
260 // This failed when PDFDocument::RemoveSignature() silently returned
261 // success, without doing anything.
262 verify(aOutURL, 0, /*rExpectedSubFilter=*/OString());
265 /// Test removing all signatures from a previously multi-signed file.
266 CPPUNIT_TEST_FIXTURE(PDFSigningTest, testPDFRemoveAll)
268 // Make sure that good2.pdf has 2 valid signatures. Unlike in
269 // testPDFRemove(), here intentionally test DocumentSignatureManager and
270 // PDFSignatureHelper code as well.
271 uno::Reference<xml::crypto::XSEInitializer> xSEInitializer
272 = xml::crypto::SEInitializer::create(mxComponentContext);
273 uno::Reference<xml::crypto::XXMLSecurityContext> xSecurityContext
274 = xSEInitializer->createSecurityContext(OUString());
276 // Copy the test document to a temporary file, as it'll be modified.
277 OUString aTargetDir
278 = m_directories.getURLFromWorkdir("/CppunitTest/xmlsecurity_pdfsigning.test.user/");
279 OUString aOutURL = aTargetDir + "remove-all.pdf";
280 CPPUNIT_ASSERT_EQUAL(
281 osl::File::RC::E_None,
282 osl::File::copy(m_directories.getURLFromSrc(DATA_DIRECTORY) + "2good.pdf", aOutURL));
283 // Load the test document as a storage and read its two signatures.
284 DocumentSignatureManager aManager(mxComponentContext, DocumentSignatureMode::Content);
285 std::unique_ptr<SvStream> pStream
286 = utl::UcbStreamHelper::CreateStream(aOutURL, StreamMode::READ | StreamMode::WRITE);
287 uno::Reference<io::XStream> xStream(new utl::OStreamWrapper(std::move(pStream)));
288 aManager.setSignatureStream(xStream);
289 aManager.read(/*bUseTempStream=*/false);
290 std::vector<SignatureInformation>& rInformations = aManager.getCurrentSignatureInformations();
291 // This was 1 when NSS_CMSSignerInfo_GetSigningCertificate() failed, which
292 // means that we only used the locally imported certificates for
293 // verification, not the ones provided in the PDF signature data.
294 CPPUNIT_ASSERT_EQUAL(static_cast<std::size_t>(2), rInformations.size());
296 // Request removal of the first signature, should imply removal of the
297 // second chained signature as well.
298 aManager.remove(0);
299 // This was 2, Manager didn't write anything to disk when removal succeeded
300 // (instead of doing that when removal failed).
301 // Then this was 1, when the chained signature wasn't removed.
302 CPPUNIT_ASSERT_EQUAL(static_cast<std::size_t>(0), rInformations.size());
305 CPPUNIT_TEST_FIXTURE(PDFSigningTest, testTdf107782)
307 uno::Reference<xml::crypto::XSEInitializer> xSEInitializer
308 = xml::crypto::SEInitializer::create(mxComponentContext);
309 uno::Reference<xml::crypto::XXMLSecurityContext> xSecurityContext
310 = xSEInitializer->createSecurityContext(OUString());
312 // Load the test document as a storage and read its signatures.
313 DocumentSignatureManager aManager(mxComponentContext, DocumentSignatureMode::Content);
314 OUString aURL = m_directories.getURLFromSrc(DATA_DIRECTORY) + "tdf107782.pdf";
315 std::unique_ptr<SvStream> pStream
316 = utl::UcbStreamHelper::CreateStream(aURL, StreamMode::READ | StreamMode::WRITE);
317 uno::Reference<io::XStream> xStream(new utl::OStreamWrapper(std::move(pStream)));
318 aManager.setSignatureStream(xStream);
319 aManager.read(/*bUseTempStream=*/false);
320 CPPUNIT_ASSERT(aManager.hasPDFSignatureHelper());
322 // This failed with an std::bad_alloc exception on Windows.
323 aManager.getPDFSignatureHelper().GetDocumentSignatureInformations(
324 aManager.getSecurityEnvironment());
327 /// Test a PDF 1.4 document, signed by Adobe.
328 CPPUNIT_TEST_FIXTURE(PDFSigningTest, testPDF14Adobe)
330 // Two signatures, first is SHA1, the second is SHA256.
331 // This was 0, as we failed to find the Annots key's value when it was a
332 // reference-to-array, not an array.
333 std::vector<SignatureInformation> aInfos
334 = verify(m_directories.getURLFromSrc(DATA_DIRECTORY) + "pdf14adobe.pdf", 2,
335 /*rExpectedSubFilter=*/OString());
336 // This was 0, out-of-PKCS#7 signature date wasn't read.
337 CPPUNIT_ASSERT_EQUAL(static_cast<sal_Int16>(2016), aInfos[1].stDateTime.Year);
340 /// Test a PDF 1.6 document, signed by Adobe.
341 CPPUNIT_TEST_FIXTURE(PDFSigningTest, testPDF16Adobe)
343 // Contains a cross-reference stream, object streams and a compressed
344 // stream with a predictor. And a valid signature.
345 // Found signatures was 0, as parsing failed due to lack of support for
346 // these features.
347 verify(m_directories.getURLFromSrc(DATA_DIRECTORY) + "pdf16adobe.pdf", 1,
348 /*rExpectedSubFilter=*/OString());
351 /// Test adding a signature to a PDF 1.6 document.
352 CPPUNIT_TEST_FIXTURE(PDFSigningTest, testPDF16Add)
354 // Contains PDF 1.6 features, make sure we can add a signature using that
355 // markup correctly.
356 OUString aSourceDir = m_directories.getURLFromSrc(DATA_DIRECTORY);
357 OUString aInURL = aSourceDir + "pdf16adobe.pdf";
358 OUString aTargetDir
359 = m_directories.getURLFromWorkdir("/CppunitTest/xmlsecurity_pdfsigning.test.user/");
360 OUString aOutURL = aTargetDir + "add.pdf";
361 // This failed: verification broke as incorrect xref stream was written as
362 // part of the new signature.
363 bool bHadCertificates = sign(aInURL, aOutURL, 1);
365 // Sign again.
366 aInURL = aTargetDir + "add.pdf";
367 aOutURL = aTargetDir + "add2.pdf";
368 // This failed as non-compressed AcroForm wasn't handled.
369 if (bHadCertificates)
370 sign(aInURL, aOutURL, 2);
373 /// Test a PDF 1.4 document, signed by LO on Windows.
374 CPPUNIT_TEST_FIXTURE(PDFSigningTest, testPDF14LOWin)
376 // mscrypto used SEC_OID_PKCS1_SHA1_WITH_RSA_ENCRYPTION as a digest
377 // algorithm when it meant SEC_OID_SHA1, make sure we tolerate that on all
378 // platforms.
379 // This failed, as NSS HASH_Create() didn't handle the sign algorithm.
380 verify(m_directories.getURLFromSrc(DATA_DIRECTORY) + "pdf14lowin.pdf", 1,
381 /*rExpectedSubFilter=*/OString());
384 /// Test a PAdES document, signed by LO on Linux.
385 CPPUNIT_TEST_FIXTURE(PDFSigningTest, testPDFPAdESGood)
387 verify(m_directories.getURLFromSrc(DATA_DIRECTORY) + "good-pades.pdf", 1,
388 "ETSI.CAdES.detached");
391 /// Test a valid signature that does not cover the whole file.
392 CPPUNIT_TEST_FIXTURE(PDFSigningTest, testPartial)
394 std::vector<SignatureInformation> aInfos
395 = verify(m_directories.getURLFromSrc(DATA_DIRECTORY) + "partial.pdf", 1,
396 /*rExpectedSubFilter=*/OString());
397 CPPUNIT_ASSERT(!aInfos.empty());
398 SignatureInformation& rInformation = aInfos[0];
399 CPPUNIT_ASSERT(rInformation.bPartialDocumentSignature);
402 /// Test writing a PAdES signature.
403 CPPUNIT_TEST_FIXTURE(PDFSigningTest, testSigningCertificateAttribute)
405 // Create a new signature.
406 OUString aSourceDir = m_directories.getURLFromSrc(DATA_DIRECTORY);
407 OUString aInURL = aSourceDir + "no.pdf";
408 OUString aTargetDir
409 = m_directories.getURLFromWorkdir("/CppunitTest/xmlsecurity_pdfsigning.test.user/");
410 OUString aOutURL = aTargetDir + "signing-certificate-attribute.pdf";
411 bool bHadCertificates = sign(aInURL, aOutURL, 0);
412 if (!bHadCertificates)
413 return;
415 // Verify it.
416 std::vector<SignatureInformation> aInfos = verify(aOutURL, 1, "ETSI.CAdES.detached");
417 CPPUNIT_ASSERT(!aInfos.empty());
418 SignatureInformation& rInformation = aInfos[0];
419 // Assert that it has a signed signingCertificateV2 attribute.
420 CPPUNIT_ASSERT(rInformation.bHasSigningCertificate);
423 /// Test that we accept files which are supposed to be good.
424 CPPUNIT_TEST_FIXTURE(PDFSigningTest, testGood)
426 const std::initializer_list<OUStringLiteral> aNames = {
427 // We failed to determine if this is good or bad.
428 "good-non-detached.pdf",
429 // Boolean value for dictionary key caused read error.
430 "dict-bool.pdf",
433 for (const auto& rName : aNames)
435 std::vector<SignatureInformation> aInfos
436 = verify(m_directories.getURLFromSrc(DATA_DIRECTORY) + rName, 1,
437 /*rExpectedSubFilter=*/OString());
438 CPPUNIT_ASSERT(!aInfos.empty());
439 SignatureInformation& rInformation = aInfos[0];
440 CPPUNIT_ASSERT_EQUAL(int(xml::crypto::SecurityOperationStatus_OPERATION_SUCCEEDED),
441 static_cast<int>(rInformation.nStatus));
445 /// Test that we don't crash / loop while tokenizing these files.
446 CPPUNIT_TEST_FIXTURE(PDFSigningTest, testTokenize)
448 const std::initializer_list<OUStringLiteral> aNames = {
449 // We looped on this broken input.
450 "no-eof.pdf",
451 // ']' in a name token was mishandled.
452 "name-bracket.pdf",
453 // %%EOF at the end wasn't followed by a newline.
454 "noeol.pdf",
455 // File that's intentionally smaller than 1024 bytes.
456 "small.pdf",
457 "tdf107149.pdf",
458 // Nested parentheses were not handled.
459 "tdf114460.pdf",
460 // Valgrind was unhappy about this.
461 "forcepoint16.pdf",
464 for (const auto& rName : aNames)
466 SvFileStream aStream(m_directories.getURLFromSrc(DATA_DIRECTORY) + rName, StreamMode::READ);
467 vcl::filter::PDFDocument aDocument;
468 // Just make sure the tokenizer finishes without an error, don't look at the signature.
469 CPPUNIT_ASSERT(aDocument.Read(aStream));
471 if (OUString(rName) == "tdf107149.pdf")
472 // This failed, page list was empty.
473 CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(1), aDocument.GetPages().size());
477 /// Test handling of unknown SubFilter values.
478 CPPUNIT_TEST_FIXTURE(PDFSigningTest, testUnknownSubFilter)
480 // Tokenize the bugdoc.
481 uno::Reference<xml::crypto::XSEInitializer> xSEInitializer
482 = xml::crypto::SEInitializer::create(mxComponentContext);
483 uno::Reference<xml::crypto::XXMLSecurityContext> xSecurityContext
484 = xSEInitializer->createSecurityContext(OUString());
485 std::unique_ptr<SvStream> pStream = utl::UcbStreamHelper::CreateStream(
486 m_directories.getURLFromSrc(DATA_DIRECTORY) + "cr-comment.pdf", StreamMode::STD_READ);
487 uno::Reference<io::XStream> xStream(new utl::OStreamWrapper(std::move(pStream)));
488 DocumentSignatureManager aManager(mxComponentContext, DocumentSignatureMode::Content);
489 aManager.setSignatureStream(xStream);
490 aManager.read(/*bUseTempStream=*/false);
492 // Make sure we find both signatures, even if the second has unknown SubFilter.
493 std::vector<SignatureInformation>& rInformations = aManager.getCurrentSignatureInformations();
494 CPPUNIT_ASSERT_EQUAL(static_cast<std::size_t>(2), rInformations.size());
497 CPPUNIT_PLUGIN_IMPLEMENT();
499 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */