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/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 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 0000000..a4deb76 Binary files /dev/null and b/test/support/indirect_annots.expected.pdf differ diff --git a/test/support/indirect_annots.pdf b/test/support/indirect_annots.pdf new file mode 100644 index 0000000..924186e Binary files /dev/null and b/test/support/indirect_annots.pdf differ