1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
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/.
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>
27 #define WIN32_LEAN_AND_MEAN
31 using namespace com::sun::star
;
35 char const DATA_DIRECTORY
[] = "/xmlsecurity/qa/unit/pdfsigning/data/";
38 /// Testsuite for the PDF signing feature.
39 class PDFSigningTest
: public test::BootstrapFixture
42 uno::Reference
<uno::XComponentContext
> mxComponentContext
;
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
);
50 * Read a pdf and make sure that it has the expected number of valid
53 std::vector
<SignatureInformation
> verify(const OUString
& rURL
, size_t nCount
,
54 const OString
& rExpectedSubFilter
);
58 void setUp() override
;
61 PDFSigningTest::PDFSigningTest() {}
63 void PDFSigningTest::setUp()
65 test::BootstrapFixture::setUp();
67 mxComponentContext
.set(comphelper::getComponentContext(getMultiServiceFactory()));
70 // Set up cert8.db and key3.db in workdir/CppunitTest/
71 OUString aSourceDir
= m_directories
.getURLFromSrc(DATA_DIRECTORY
);
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");
77 osl::FileBase::getSystemPathFromFileURL(aTargetDir
, aTargetPath
);
78 setenv("MOZILLA_CERTIFICATE_FOLDER", aTargetPath
.toUtf8().getStr(), 1);
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;
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
);
109 = dynamic_cast<vcl::filter::PDFNameElement
*>(pValue
->Lookup("SubFilter"));
110 CPPUNIT_ASSERT(pSubFilter
);
111 CPPUNIT_ASSERT_EQUAL(rExpectedSubFilter
, pSubFilter
->GetValue());
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");
155 bool bSignResult
= aDocument
.Sign(cert
, "test", /*bAdES=*/true);
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
168 CPPUNIT_ASSERT(bSignResult
);
169 SvFileStream
aOutStream(rOutURL
, StreamMode::WRITE
| StreamMode::TRUNC
);
170 CPPUNIT_ASSERT(aDocument
.Write(aOutStream
));
171 bSignSuccessful
= true;
177 // This was nOriginalSignatureCount when PDFDocument::Sign() silently returned success, without doing anything.
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";
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
)
212 OUString aSourceDir
= m_directories
.getURLFromSrc(DATA_DIRECTORY
);
213 OUString aInURL
= aSourceDir
+ "no.pdf";
215 = m_directories
.getURLFromWorkdir("/CppunitTest/xmlsecurity_pdfsigning.test.user/");
216 OUString aOutURL
= aTargetDir
+ "add.pdf";
217 bool bHadCertificates
= sign(aInURL
, aOutURL
, 0);
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
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);
246 xmlsecurity::pdfio::ValidateSignature(aStream
, aSignatures
[0], aInfo
, /*bLast=*/true));
249 // Remove the signature and write out the result as remove.pdf.
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.
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.
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
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
356 OUString aSourceDir
= m_directories
.getURLFromSrc(DATA_DIRECTORY
);
357 OUString aInURL
= aSourceDir
+ "pdf16adobe.pdf";
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);
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
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";
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
)
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.
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.
451 // ']' in a name token was mishandled.
453 // %%EOF at the end wasn't followed by a newline.
455 // File that's intentionally smaller than 1024 bytes.
458 // Nested parentheses were not handled.
460 // Valgrind was unhappy about this.
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: */