From 5a842792b69550cf441d4598feb1daff2fa8c83f Mon Sep 17 00:00:00 2001 From: Masamichi Hosoda Date: Fri, 18 Oct 2019 19:41:53 +0900 Subject: [PATCH] Parse Contents in signature dictionary without encryption Various PDF digital signing tools do not encrypt /Contents value in signature dictionary. Adobe Acrobat Reader DC can handle a PDF with the /Contents value not encrypted. Write Contents in signature dictionary without encryption Tests ensure that string /Contents are not handled specially when not found in sig dicts. --- include/qpdf/QPDFWriter.hh | 1 + libqpdf/QPDFObjectHandle.cc | 44 +++++++++++++++++++- libqpdf/QPDFWriter.cc | 3 +- qpdf/qtest/qpdf.test | 83 ++++++++++++++++++++++++++++++++++++- 4 files changed, 128 insertions(+), 3 deletions(-) diff --git a/include/qpdf/QPDFWriter.hh b/include/qpdf/QPDFWriter.hh index 06de6a56..f5cd2e06 100644 --- a/include/qpdf/QPDFWriter.hh +++ b/include/qpdf/QPDFWriter.hh @@ -481,6 +481,7 @@ class QPDFWriter static int const f_filtered = 1 << 1; static int const f_in_ostream = 1 << 2; static int const f_hex_string = 1 << 3; + static int const f_no_encryption = 1 << 4; enum trailer_e { t_normal, t_lin_first, t_lin_second }; diff --git a/libqpdf/QPDFObjectHandle.cc b/libqpdf/QPDFObjectHandle.cc index d49976b6..3dacfb8e 100644 --- a/libqpdf/QPDFObjectHandle.cc +++ b/libqpdf/QPDFObjectHandle.cc @@ -1779,12 +1779,19 @@ QPDFObjectHandle::parseInternal(PointerHolder input, bool done = false; int bad_count = 0; int good_count = 0; + bool b_contents = false; + std::vector contents_string_stack; + contents_string_stack.push_back(""); + std::vector contents_offset_stack; + contents_offset_stack.push_back(-1); while (! done) { bool bad = false; SparseOHArray& olist = olist_stack.back(); parser_state_e state = state_stack.back(); offset = offset_stack.back(); + std::string& contents_string = contents_string_stack.back(); + qpdf_offset_t& contents_offset = contents_offset_stack.back(); object = QPDFObjectHandle(); set_offset = false; @@ -1894,6 +1901,9 @@ QPDFObjectHandle::parseInternal(PointerHolder input, state_stack.push_back( (token.getType() == QPDFTokenizer::tt_array_open) ? st_array : st_dictionary); + b_contents = false; + contents_string_stack.push_back(""); + contents_offset_stack.push_back(-1); } break; @@ -1914,7 +1924,19 @@ QPDFObjectHandle::parseInternal(PointerHolder input, break; case QPDFTokenizer::tt_name: - object = newName(token.getValue()); + { + std::string name = token.getValue(); + object = newName(name); + + if (name == "/Contents") + { + b_contents = true; + } + else + { + b_contents = false; + } + } break; case QPDFTokenizer::tt_word: @@ -1975,6 +1997,12 @@ QPDFObjectHandle::parseInternal(PointerHolder input, std::string val = token.getValue(); if (decrypter) { + if (b_contents) + { + contents_string = val; + contents_offset = input->getLastOffset(); + b_contents = false; + } decrypter->decryptString(val); } object = QPDFObjectHandle::newString(val); @@ -2168,6 +2196,18 @@ QPDFObjectHandle::parseInternal(PointerHolder input, } dict[key] = val; } + if (!contents_string.empty() && + dict.count("/Type") && + dict["/Type"].isName() && + dict["/Type"].getName() == "/Sig" && + dict.count("/ByteRange") && + dict.count("/Contents") && + dict["/Contents"].isString()) + { + dict["/Contents"] + = QPDFObjectHandle::newString(contents_string); + dict["/Contents"].setParsedOffset(contents_offset); + } object = newDictionary(dict); setObjectDescriptionFromInput( object, context, object_description, input, offset); @@ -2190,6 +2230,8 @@ QPDFObjectHandle::parseInternal(PointerHolder input, { olist_stack.back().append(object); } + contents_string_stack.pop_back(); + contents_offset_stack.pop_back(); } } diff --git a/libqpdf/QPDFWriter.cc b/libqpdf/QPDFWriter.cc index e7eae5c6..a31f5da9 100644 --- a/libqpdf/QPDFWriter.cc +++ b/libqpdf/QPDFWriter.cc @@ -1695,7 +1695,7 @@ QPDFWriter::unparseObject(QPDFObjectHandle object, int level, { QTC::TC("qpdf", "QPDFWriter no encryption sig contents"); unparseChild(object.getKey(key), level + 1, - child_flags | f_hex_string); + child_flags | f_hex_string | f_no_encryption); } else { @@ -1866,6 +1866,7 @@ QPDFWriter::unparseObject(QPDFObjectHandle object, int level, std::string val; if (this->m->encrypted && (! (flags & f_in_ostream)) && + (! (flags & f_no_encryption)) && (! this->m->cur_data_key.empty())) { val = object.getStringValue(); diff --git a/qpdf/qtest/qpdf.test b/qpdf/qtest/qpdf.test index 2604ab43..1df8291e 100644 --- a/qpdf/qtest/qpdf.test +++ b/qpdf/qtest/qpdf.test @@ -3999,7 +3999,6 @@ show_ntests(); # ---------- $td->notify("--- Signature Dictionary ---"); $n_tests += 6; - foreach my $i (qw(preserve disable generate)) { $td->runtest("sig dict contents hex (object-streams=$i)", @@ -4017,6 +4016,88 @@ foreach my $i (qw(preserve disable generate)) $td->EXIT_STATUS => 0}); } +$n_tests += 4; +foreach my $i (qw(preserve disable)) +{ + $td->runtest("non sig dict contents text string (object-streams=$i)", + {$td->COMMAND => + "qpdf --object-streams=$i comment-annotation.pdf a.pdf"}, + {$td->STRING => "", + $td->EXIT_STATUS => 0}); + $td->runtest("find desired contents as non hex (object-streams=$i)", + {$td->COMMAND => + "grep \"/Contents (Salad)\" a.pdf"}, + {$td->REGEXP => ".*", + $td->EXIT_STATUS => 0}); +} + +$n_tests += 2; + $td->runtest("non sig dict contents text string (object-streams=generate)", + {$td->COMMAND => + "qpdf --object-streams=generate comment-annotation.pdf a.pdf"}, + {$td->STRING => "", + $td->EXIT_STATUS => 0}); + $td->runtest("plain text not found due to compression (object-streams=generate)", + {$td->COMMAND => + "grep \"/Contents (Salad)\" a.pdf"}, + {$td->REGEXP => ".*", + $td->EXIT_STATUS => 1}); + +$n_tests += 12; +foreach my $i (qw(40 128 256)) +{ + $td->runtest("encrypt $i", + {$td->COMMAND => + "qpdf --encrypt '' o $i -- digitally-signed.pdf a.pdf"}, + {$td->STRING => "", + $td->EXIT_STATUS => 0}); + $td->runtest("find desired contents (encrypt $i)", + {$td->COMMAND => + "grep -f digitally-signed-sig-dict-contents.out a.pdf"}, + {$td->REGEXP => ".*", + $td->EXIT_STATUS => 0}); + $td->runtest("decrypt", + {$td->COMMAND => + "qpdf --decrypt a.pdf b.pdf"}, + {$td->REGEXP => ".*", + $td->EXIT_STATUS => 0}); + $td->runtest("find desired contents (decrypt $i)", + {$td->COMMAND => + "grep -f digitally-signed-sig-dict-contents.out b.pdf"}, + {$td->REGEXP => ".*", + $td->EXIT_STATUS => 0}); +} + +$n_tests += 15; +foreach my $i (qw(40 128 256)) +{ + $td->runtest("non sig dict encrypt $i", + {$td->COMMAND => + "qpdf --encrypt '' o $i -- comment-annotation.pdf a.pdf"}, + {$td->STRING => "", + $td->EXIT_STATUS => 0}); + $td->runtest("plain text not found due to encryption (non sig dict encrypt $i)", + {$td->COMMAND => + "grep \"/Contents (Salad)\" a.pdf"}, + {$td->REGEXP => ".*", + $td->EXIT_STATUS => 1}); + $td->runtest("find encrypted contents (non sig dict encrypt $i)", + {$td->COMMAND => + "grep \"/Contents <.*>\" a.pdf"}, + {$td->REGEXP => ".*", + $td->EXIT_STATUS => 0}); + $td->runtest("non sig dict decrypt", + {$td->COMMAND => + "qpdf --decrypt a.pdf b.pdf"}, + {$td->REGEXP => ".*", + $td->EXIT_STATUS => 0}); + $td->runtest("find desired contents (non sig dict decrypt $i)", + {$td->COMMAND => + "grep \"/Contents (Salad)\" b.pdf"}, + {$td->REGEXP => ".*", + $td->EXIT_STATUS => 0}); +} + show_ntests(); # ---------- $td->notify("--- Get XRef Table ---");