From 86a35476bb7a7903ee5306953e3d5e34495d7228 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Co=C3=AAlho?= Date: Wed, 29 Nov 2023 17:57:47 +0000 Subject: [PATCH 1/2] Move test to the correct folder --- test/{ => rubrik}/pkcs7_signature_test.rb | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/{ => rubrik}/pkcs7_signature_test.rb (100%) diff --git a/test/pkcs7_signature_test.rb b/test/rubrik/pkcs7_signature_test.rb similarity index 100% rename from test/pkcs7_signature_test.rb rename to test/rubrik/pkcs7_signature_test.rb From 9f1733885ffb5bdd19e915f03eac522fc45bfac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Co=C3=AAlho?= Date: Wed, 29 Nov 2023 18:17:02 +0000 Subject: [PATCH 2/2] Handle the case that the Annots Array is indirect --- cspell.json | 13 ++--- lib/rubrik/document.rb | 16 ++++-- test/rubrik/document_test.rb | 62 ++++++++++++++++++++-- test/rubrik/sign_test.rb | 39 ++++++++++++++ test/support/indirect_annots.expected.pdf | Bin 0 -> 9347 bytes test/support/indirect_annots.pdf | Bin 0 -> 619 bytes 6 files changed, 117 insertions(+), 13 deletions(-) create mode 100644 test/support/indirect_annots.expected.pdf create mode 100644 test/support/indirect_annots.pdf diff --git a/cspell.json b/cspell.json index 2b900a1..9342020 100644 --- a/cspell.json +++ b/cspell.json @@ -4,14 +4,15 @@ "dictionaryDefinitions": [], "dictionaries": [], "words": [ - "Rubrik", - "PKCS", - "Tempfile", - "rubygems", - "ETSI", + "Annots", "codebases", "devcontainer", - "simplecov" + "ETSI", + "PKCS", + "Rubrik", + "rubygems", + "simplecov", + "Tempfile" ], "ignoreWords": [], "import": [] diff --git a/lib/rubrik/document.rb b/lib/rubrik/document.rb index e629bd1..c7ca32c 100644 --- a/lib/rubrik/document.rb +++ b/lib/rubrik/document.rb @@ -76,10 +76,20 @@ def add_signature_field } } - modified_page = objects.fetch(first_page_reference).dup - (modified_page[:Annots] ||= []) << signature_field_id + first_page = objects.fetch(first_page_reference) + annots = first_page[:Annots] - modified_objects << {id: first_page_reference, value: modified_page} + if annots.is_a?(PDF::Reader::Reference) + new_annots = objects.fetch(annots).dup + new_annots << signature_field_id + + modified_objects << {id: annots, value: new_annots} + else + new_first_page = first_page.dup + (new_first_page[:Annots] ||= []) << signature_field_id + + modified_objects << {id: first_page_reference, value: new_first_page} + end (interactive_form[:Fields] ||= []) << signature_field_id diff --git a/test/rubrik/document_test.rb b/test/rubrik/document_test.rb index 032886d..6bdcb4b 100644 --- a/test/rubrik/document_test.rb +++ b/test/rubrik/document_test.rb @@ -18,7 +18,7 @@ def test_initialize_document_without_interactive_form assert_equal(6, document.last_object_id) assert_kind_of(PDF::Reader::ObjectHash, document.objects) - acro_form = document.modified_objects.find { |obj| obj.dig(:value, :Type) == :Catalog } + acro_form = document.modified_objects.find { |obj| obj.is_a?(Hash) && obj.dig(:value, :Type) == :Catalog } acro_form_reference = T.must(acro_form).dig(:value, :AcroForm) assert_pattern do document.modified_objects => [*, {id: ^acro_form_reference, value: {Fields: [], SigFlags: 3}}, *] @@ -62,7 +62,7 @@ def test_add_signature_field assert_equal(3, number_of_added_objects) assert_pattern do - signature_value = document.modified_objects.find { _1[:id] == result } + signature_value = document.modified_objects.find { _1.is_a?(Hash) && _1[:id] == result } signature_value => {id: ^result, value: { Type: :Sig, @@ -74,7 +74,7 @@ def test_add_signature_field } end - signature_field = document.modified_objects.find { _1.dig(:value, :FT) == :Sig } + signature_field = document.modified_objects.find { _1.is_a?(Hash) && _1.dig(:value, :FT) == :Sig } assert_pattern do first_page_reference = document.objects.page_references[0] signature_field => { @@ -93,12 +93,66 @@ def test_add_signature_field signature_field_id = T.must(signature_field)[:id] - first_page = document.modified_objects.find { _1.dig(:value, :Type) == :Page } + first_page = document.modified_objects.find { _1.is_a?(Hash) && _1.dig(:value, :Type) == :Page } assert_pattern { first_page => {value: {Annots: [*, ^signature_field_id, *]}}} assert_pattern { document.send(:interactive_form) => {Fields: [*, ^signature_field_id, *]} } ensure input&.close end + + def test_add_signature_field_with_indirect_annots + # Arrange + input = File.open(SupportPDF["indirect_annots"], "rb") + document = Document.new(input) + initial_number_of_objects = document.modified_objects.size + + # Act + result = document.add_signature_field + + # Assert + number_of_added_objects = document.modified_objects.size - initial_number_of_objects + assert_equal(3, number_of_added_objects) + + assert_pattern do + signature_value = document.modified_objects.find { _1.is_a?(Hash) && _1[:id] == result } + signature_value => {id: ^result, + value: { + Type: :Sig, + Filter: :"Adobe.PPKLite", + SubFilter: :"adbe.pkcs7.detached", + Contents: Document::CONTENTS_PLACEHOLDER, + ByteRange: Document::BYTE_RANGE_PLACEHOLDER + } + } + end + + signature_field = document.modified_objects.find { _1.is_a?(Hash) && _1.dig(:value, :FT) == :Sig } + assert_pattern do + first_page_reference = document.objects.page_references[0] + signature_field => { + id: PDF::Reader::Reference, + value: { + T: /Signature-\w{4}/, + V: ^result, + Type: :Annot, + Subtype: :Widget, + Rect: [0, 0, 0, 0], + F: 4, + P: ^first_page_reference + } + } + end + + signature_field_id = T.must(signature_field)[:id] + + assert_pattern do + document.modified_objects => [*, {id: PDF::Reader::Reference, value: [signature_field_id]} , *] + end + + assert_pattern { document.send(:interactive_form) => {Fields: [*, ^signature_field_id, *]} } + ensure + input&.close + end end end diff --git a/test/rubrik/sign_test.rb b/test/rubrik/sign_test.rb index 8eff1bb..c6217be 100644 --- a/test/rubrik/sign_test.rb +++ b/test/rubrik/sign_test.rb @@ -76,5 +76,44 @@ def test_document_without_interactive_form input_pdf&.close expected_output&.close end + + def test_document_with_indirect_annots + # Arrange + input_pdf = File.open(SupportPDF["indirect_annots"], "rb") + # FACT: the output file is opened with only write permission and should be later reopened to rw. + output_pdf = File.open(SupportPDF["indirect_annots_temp"], "wb") + certificate_file = File.open("test/support/demo_cert.pem", "rb") + + private_key = OpenSSL::PKey::RSA.new(certificate_file, "") + certificate_file.rewind + certificate = OpenSSL::X509::Certificate.new(certificate_file) + + # Act + Sign.call(input_pdf, output_pdf, private_key:, certificate:) + + # Assert + expected_output = File.open(SupportPDF["indirect_annots.expected"], "rb") + + expected_output.readlines.zip(output_pdf.readlines).each do |(expected_line, actual_line)| + # We can't verify the signature because it changes on every run + if actual_line&.match?("/Type /Sig") + actual_line.sub!(/<[a-f0-9]+>/, "") + expected_line.sub!(/<[a-f0-9]+>/, "") + # The signature field name is also random + elsif actual_line&.match?(/Signature-[a-f0-9]{4}/) + actual_line.sub!(/Signature-[a-f0-9]{4}/, "") + expected_line.sub!(/Signature-[a-f0-9]{4}/, "") + end + + assert_equal(expected_line, actual_line) + end + ensure + certificate_file&.close + input_pdf&.close + expected_output&.close + + output_pdf&.close + File.delete(output_pdf.path) if output_pdf + end end end diff --git a/test/support/indirect_annots.expected.pdf b/test/support/indirect_annots.expected.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a4deb76536e81b266803fc8a65d00bef461708f2 GIT binary patch literal 9347 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 literal 0 HcmV?d00001 diff --git a/test/support/indirect_annots.pdf b/test/support/indirect_annots.pdf new file mode 100644 index 0000000000000000000000000000000000000000..924186e66778161b905542a0fdba30100b8e0fb9 GIT binary patch literal 619 zcmZWn%WlFj5WM><_QJ7_^KcR*gamEVLthAbLmXVPq^W|993#pPoFf|rj2LU65|YD@*eLRHv8>&eJ