hexapdf 0.19.2 → 0.20.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +67 -0
  3. data/data/hexapdf/cert/demo_cert.rb +22 -0
  4. data/data/hexapdf/cert/root-ca.crt +119 -0
  5. data/data/hexapdf/cert/signing.crt +125 -0
  6. data/data/hexapdf/cert/signing.key +52 -0
  7. data/data/hexapdf/cert/sub-ca.crt +125 -0
  8. data/data/hexapdf/encoding/glyphlist.txt +4283 -4282
  9. data/data/hexapdf/encoding/zapfdingbats.txt +203 -202
  10. data/lib/hexapdf/cli/info.rb +21 -1
  11. data/lib/hexapdf/configuration.rb +26 -0
  12. data/lib/hexapdf/content/processor.rb +1 -1
  13. data/lib/hexapdf/document/signatures.rb +327 -0
  14. data/lib/hexapdf/document.rb +26 -0
  15. data/lib/hexapdf/font/encoding/glyph_list.rb +5 -6
  16. data/lib/hexapdf/importer.rb +1 -1
  17. data/lib/hexapdf/object.rb +5 -3
  18. data/lib/hexapdf/parser.rb +14 -9
  19. data/lib/hexapdf/rectangle.rb +0 -6
  20. data/lib/hexapdf/revision.rb +13 -6
  21. data/lib/hexapdf/task/dereference.rb +12 -4
  22. data/lib/hexapdf/task/optimize.rb +3 -3
  23. data/lib/hexapdf/type/acro_form/appearance_generator.rb +2 -4
  24. data/lib/hexapdf/type/acro_form/field.rb +2 -0
  25. data/lib/hexapdf/type/acro_form/form.rb +9 -1
  26. data/lib/hexapdf/type/annotation.rb +36 -3
  27. data/lib/hexapdf/type/font_simple.rb +1 -1
  28. data/lib/hexapdf/type/object_stream.rb +3 -1
  29. data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +121 -0
  30. data/lib/hexapdf/type/signature/adbe_x509_rsa_sha1.rb +95 -0
  31. data/lib/hexapdf/type/signature/handler.rb +140 -0
  32. data/lib/hexapdf/type/signature/verification_result.rb +92 -0
  33. data/lib/hexapdf/type/signature.rb +236 -0
  34. data/lib/hexapdf/type.rb +1 -0
  35. data/lib/hexapdf/version.rb +1 -1
  36. data/lib/hexapdf/writer.rb +16 -8
  37. data/test/hexapdf/content/test_processor.rb +1 -1
  38. data/test/hexapdf/document/test_signatures.rb +225 -0
  39. data/test/hexapdf/task/test_optimize.rb +4 -1
  40. data/test/hexapdf/test_document.rb +28 -0
  41. data/test/hexapdf/test_object.rb +7 -2
  42. data/test/hexapdf/test_parser.rb +12 -0
  43. data/test/hexapdf/test_rectangle.rb +0 -7
  44. data/test/hexapdf/test_revision.rb +44 -14
  45. data/test/hexapdf/test_writer.rb +4 -3
  46. data/test/hexapdf/type/acro_form/test_field.rb +11 -1
  47. data/test/hexapdf/type/acro_form/test_form.rb +5 -0
  48. data/test/hexapdf/type/signature/common.rb +71 -0
  49. data/test/hexapdf/type/signature/test_adbe_pkcs7_detached.rb +99 -0
  50. data/test/hexapdf/type/signature/test_adbe_x509_rsa_sha1.rb +66 -0
  51. data/test/hexapdf/type/signature/test_handler.rb +102 -0
  52. data/test/hexapdf/type/signature/test_verification_result.rb +47 -0
  53. data/test/hexapdf/type/test_annotation.rb +40 -2
  54. data/test/hexapdf/type/test_font_simple.rb +5 -5
  55. data/test/hexapdf/type/test_object_stream.rb +9 -0
  56. data/test/hexapdf/type/test_signature.rb +131 -0
  57. metadata +21 -3
@@ -0,0 +1,236 @@
1
+ # -*- encoding: utf-8; frozen_string_literal: true -*-
2
+ #
3
+ #--
4
+ # This file is part of HexaPDF.
5
+ #
6
+ # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
+ # Copyright (C) 2014-2021 Thomas Leitner
8
+ #
9
+ # HexaPDF is free software: you can redistribute it and/or modify it
10
+ # under the terms of the GNU Affero General Public License version 3 as
11
+ # published by the Free Software Foundation with the addition of the
12
+ # following permission added to Section 15 as permitted in Section 7(a):
13
+ # FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
14
+ # THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
15
+ # INFRINGEMENT OF THIRD PARTY RIGHTS.
16
+ #
17
+ # HexaPDF is distributed in the hope that it will be useful, but WITHOUT
18
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
19
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
20
+ # License for more details.
21
+ #
22
+ # You should have received a copy of the GNU Affero General Public License
23
+ # along with HexaPDF. If not, see <http://www.gnu.org/licenses/>.
24
+ #
25
+ # The interactive user interfaces in modified source and object code
26
+ # versions of HexaPDF must display Appropriate Legal Notices, as required
27
+ # under Section 5 of the GNU Affero General Public License version 3.
28
+ #
29
+ # In accordance with Section 7(b) of the GNU Affero General Public
30
+ # License, a covered work must retain the producer line in every PDF that
31
+ # is created or manipulated using HexaPDF.
32
+ #
33
+ # If the GNU Affero General Public License doesn't fit your need,
34
+ # commercial licenses are available at <https://gettalong.at/hexapdf/>.
35
+ #++
36
+
37
+ require 'openssl'
38
+ require 'hexapdf/dictionary'
39
+ require 'hexapdf/error'
40
+
41
+ module HexaPDF
42
+ module Type
43
+
44
+ # Represents a digital signature that is used to authenticate a user and the contents of the
45
+ # document.
46
+ #
47
+ # == Signature Verification
48
+ #
49
+ # Verification of signatures is a complex topic and what counts as completely verified may
50
+ # differ from use-case to use-case. Therefore HexaPDF provides as much diagnostic information as
51
+ # possible so that the user can decide whether a signature is valid.
52
+ #
53
+ # By defining a custom signature handler one is able to also customize the signature
54
+ # verification.
55
+ #
56
+ # See: PDF1.7 s12.8.1, PDF2.0 s12.8.1, HexaPDF::Type::AcroForm::SignatureField
57
+ class Signature < Dictionary
58
+
59
+ autoload :Handler, 'hexapdf/type/signature/handler'
60
+ autoload :AdbeX509RsaSha1, 'hexapdf/type/signature/adbe_x509_rsa_sha1'
61
+ autoload :AdbePkcs7Detached, 'hexapdf/type/signature/adbe_pkcs7_detached'
62
+ autoload :VerificationResult, 'hexapdf/type/signature/verification_result'
63
+
64
+ # Represents a transform parameters dictionary.
65
+ #
66
+ # The allowed fields depend on the transform method, so not all fields are available all the
67
+ # time.
68
+ #
69
+ # See: PDF1.7 s12.8.2.2, s12.8.2.3, s12.8.2.4
70
+ class TransformParams < Dictionary
71
+
72
+ define_type :TransformParams
73
+
74
+ define_field :Type, type: Symbol, default: type
75
+
76
+ # For DocMDP, also used by UR
77
+ define_field :P, type: [Integer, Boolean]
78
+ define_field :V, type: Symbol, allowed_values: [:"1.2", :"2.2"]
79
+
80
+ # For UR
81
+ define_field :Document, type: PDFArray
82
+ define_field :Msg, type: String
83
+ define_field :Annots, type: PDFArray, version: '1.5'
84
+ define_field :Form, type: PDFArray, version: '1.5'
85
+ define_field :Signature, type: PDFArray
86
+ define_field :EF, type: PDFArray, version: '1.6'
87
+
88
+ # For FieldMDP
89
+ define_field :Action, type: Symbol, allowed_values: [:All, :Include, :Exclude]
90
+ define_field :Fields, type: PDFArray
91
+
92
+ private
93
+
94
+ # All values allowed for the /Annots field
95
+ FIELD_ANNOTS_ALLOWED_VALUES = [:Create, :Delete, :Modify, :Copy, :Import, :Online, :SummaryView]
96
+
97
+ # All values allowed for the /Form field
98
+ FIELD_FORM_ALLOWED_VALUES = [:Add, :Delete, :Fillin, :Import, :Export, :SubmitStandalone,
99
+ :SpawnTemplate, :BarcodePlaintext, :Online]
100
+
101
+ # All values allowed for the /EF field
102
+ FIELD_EF_ALLOWED_VALUES = [:Create, :Delete, :Modify, :Import]
103
+
104
+ def perform_validation #:nodoc:
105
+ super
106
+ # We need to perform the checks here since the values are arrays and not single elements
107
+ if (annots = self[:Annots]) && !(annots = annots.value - FIELD_ANNOTS_ALLOWED_VALUES).empty?
108
+ yield("Field /Annots contains invalid entries: #{annots.join(', ')}", true)
109
+ value[:Annots].value -= annots
110
+ end
111
+ if (form = self[:Form]) && !(form = form.value - FIELD_FORM_ALLOWED_VALUES).empty?
112
+ yield("Field /Form contains invalid entries: #{form.join(', ')}", true)
113
+ value[:Form].value -= form
114
+ end
115
+ if (ef = self[:EF]) && !(ef = ef.value - FIELD_EF_ALLOWED_VALUES).empty?
116
+ yield("Field /EF contains invalid entries: #{ef.join(', ')}", true)
117
+ value[:EF].value -= ef
118
+ end
119
+ end
120
+
121
+ end
122
+
123
+ # Represents a signature reference dictionary.
124
+ #
125
+ # See: PDF1.7 s12.8.1, PDF2.0 s12.8.1, HexaPDF::Type::Signature
126
+ class SignatureReference < Dictionary
127
+
128
+ define_type :SigRef
129
+
130
+ define_field :Type, type: Symbol, default: type
131
+ define_field :TransformMethod, type: Symbol, required: true,
132
+ allowed_values: [:DocMDP, :UR, :FieldMDP]
133
+ define_field :TransformParams, type: Dictionary
134
+ define_field :Data, type: ::Object
135
+ define_field :DigestMethod, type: Symbol, version: '1.5',
136
+ allowed_values: [:MD5, :SHA1, :SHA256, :SHA384, :SHA512, :RIPEMD160]
137
+
138
+ private
139
+
140
+ def perform_validation #:nodoc:
141
+ super
142
+ if self[:TransformMethod] == :FieldMDP && !key?(:Data)
143
+ yield("Field /Data is required when /TransformMethod is /FieldMDP")
144
+ end
145
+ end
146
+
147
+ end
148
+
149
+ define_field :Type, type: Symbol, default: :Sig,
150
+ allowed_values: [:Sig, :DocTimeStamp]
151
+ define_field :Filter, type: Symbol
152
+ define_field :SubFilter, type: Symbol
153
+ define_field :Contents, type: PDFByteString
154
+ define_field :Cert, type: [PDFArray, PDFByteString]
155
+ define_field :ByteRange, type: PDFArray
156
+ define_field :Reference, type: PDFArray
157
+ define_field :Changes, type: PDFArray
158
+ define_field :Name, type: String
159
+ define_field :M, type: PDFDate
160
+ define_field :Location, type: String
161
+ define_field :Reason, type: String
162
+ define_field :ContactInfo, type: String
163
+ define_field :R, type: Integer
164
+ define_field :V, type: Integer, default: 0, version: '1.5'
165
+ define_field :Prop_Build, type: Dictionary, version: '1.5'
166
+ define_field :Prop_AuthTime, type: Integer, version: '1.5'
167
+ define_field :Prop_AuthType, type: Symbol, version: '1.5',
168
+ allowed_values: [:PIN, :Password, :Fingerprint]
169
+
170
+ # Returns the name of the person or authority that signed the document.
171
+ def signer_name
172
+ signature_handler.signer_name
173
+ end
174
+
175
+ # Returns the time of the signing.
176
+ def signing_time
177
+ signature_handler.signing_time
178
+ end
179
+
180
+ # Returns the reason for the signing.
181
+ def signing_reason
182
+ self[:Reason]
183
+ end
184
+
185
+ # Returns the location of the signing.
186
+ def signing_location
187
+ self[:Location]
188
+ end
189
+
190
+ # Returns the signature type based on the /SubFilter.
191
+ def signature_type
192
+ self[:SubFilter].to_s
193
+ end
194
+
195
+ # Returns the signature handler for this signature based on the /SubFilter entry.
196
+ def signature_handler
197
+ cache(:signature_handler) do
198
+ handler_class = document.config.constantize('signature.sub_filter_map', self[:SubFilter]) do
199
+ raise HexaPDF::Error, "No or unknown signature handler set: #{self[:SubFilter]}"
200
+ end
201
+ handler_class.new(self)
202
+ end
203
+ end
204
+
205
+ # Returns the raw signature value.
206
+ def contents
207
+ self[:Contents]
208
+ end
209
+
210
+ # Returns the signed data as indicated by the /ByteRange entry as byte string.
211
+ def signed_data
212
+ unless document.revisions.parser
213
+ raise HexaPDF::Error, "Can't load signed data without existing PDF file"
214
+ end
215
+ io = document.revisions.parser.io
216
+ data = ''.b
217
+ self[:ByteRange]&.each_slice(2) do |offset, length|
218
+ io.pos = offset
219
+ data << io.read(length)
220
+ end
221
+ data
222
+ end
223
+
224
+ # Returns a VerificationResult object with the verification information.
225
+ def verify(default_paths: true, trusted_certs: [], allow_self_signed: false)
226
+ store = OpenSSL::X509::Store.new
227
+ store.set_default_paths if default_paths
228
+ store.purpose = OpenSSL::X509::PURPOSE_SMIME_SIGN
229
+ trusted_certs.each {|cert| store.add_cert(cert) }
230
+ signature_handler.verify(store, allow_self_signed: allow_self_signed)
231
+ end
232
+
233
+ end
234
+
235
+ end
236
+ end
data/lib/hexapdf/type.rb CHANGED
@@ -72,6 +72,7 @@ module HexaPDF
72
72
  autoload(:FontType3, 'hexapdf/type/font_type3')
73
73
  autoload(:IconFit, 'hexapdf/type/icon_fit')
74
74
  autoload(:AcroForm, 'hexapdf/type/acro_form')
75
+ autoload(:Signature, 'hexapdf/type/signature')
75
76
 
76
77
  end
77
78
 
@@ -37,6 +37,6 @@
37
37
  module HexaPDF
38
38
 
39
39
  # The version of HexaPDF.
40
- VERSION = '0.19.2'
40
+ VERSION = '0.20.2'
41
41
 
42
42
  end
@@ -44,8 +44,10 @@ module HexaPDF
44
44
  # Writes the contents of a PDF document to an IO stream.
45
45
  class Writer
46
46
 
47
- # Writes the document to the IO object. If +incremental+ is +true+ and the document was created
48
- # from an existing PDF file, the changes are appended to a full copy of the source document.
47
+ # Writes the document to the IO object and returns the last XRefSection written.
48
+ #
49
+ # If +incremental+ is +true+ and the document was created from an existing PDF file, the changes
50
+ # are appended to a full copy of the source document.
49
51
  def self.write(document, io, incremental: false)
50
52
  if incremental && document.revisions.parser
51
53
  new(document, io).write_incremental
@@ -70,23 +72,27 @@ module HexaPDF
70
72
  @use_xref_streams = false
71
73
  end
72
74
 
73
- # Writes the document to the IO object.
75
+ # Writes the document to the IO object and returns the last XRefSection written.
74
76
  def write
75
77
  write_file_header
76
78
 
77
- pos = nil
79
+ pos = xref_section = nil
78
80
  @document.trailer.info[:Producer] = "HexaPDF version #{HexaPDF::VERSION}"
79
81
  @document.revisions.each do |rev|
80
- pos = write_revision(rev, pos)
82
+ pos, xref_section = write_revision(rev, pos)
81
83
  end
84
+
85
+ xref_section
82
86
  end
83
87
 
84
- # Writes the complete source document and one revision containing all changes to the IO.
88
+ # Writes the complete source document unmodified to the IO and then one revision containing all
89
+ # changes. Returns the XRefSection of that one revision.
85
90
  #
86
91
  # For this method to work the document must have been created from an existing file.
87
92
  def write_incremental
88
93
  @document.revisions.parser.io.seek(0, IO::SEEK_SET)
89
94
  IO.copy_stream(@document.revisions.parser.io, @io)
95
+ @io << "\n"
90
96
 
91
97
  @rev_size = @document.revisions.current.next_free_oid
92
98
  @use_xref_streams = @document.revisions.parser.contains_xref_streams?
@@ -95,7 +101,9 @@ module HexaPDF
95
101
  @document.revisions.each do |rev|
96
102
  rev.each_modified_object {|obj| revision.send(:add_without_check, obj) }
97
103
  end
98
- write_revision(revision, @document.revisions.parser.startxref_offset)
104
+ _pos, xref_section = write_revision(revision, @document.revisions.parser.startxref_offset)
105
+
106
+ xref_section
99
107
  end
100
108
 
101
109
  private
@@ -150,7 +158,7 @@ module HexaPDF
150
158
 
151
159
  write_startxref(startxref)
152
160
 
153
- startxref
161
+ [startxref, xref_section]
154
162
  end
155
163
 
156
164
  # :call-seq:
@@ -156,7 +156,7 @@ describe HexaPDF::Content::Processor do
156
156
 
157
157
  it "fails if the current font is a vertical font" do
158
158
  @processor.graphics_state.font.define_singleton_method(:writing_mode) { :vertical }
159
- assert_raises(NotImplementedError) { @processor.send(:decode_text_with_positioning, "a") }
159
+ assert_raises(RuntimeError) { @processor.send(:decode_text_with_positioning, "a") }
160
160
  end
161
161
  end
162
162
  end
@@ -0,0 +1,225 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require 'stringio'
5
+ require 'tempfile'
6
+ require 'hexapdf/document'
7
+ require_relative '../type/signature/common'
8
+
9
+ describe HexaPDF::Document::Signatures do
10
+ before do
11
+ @doc = HexaPDF::Document.new
12
+ @form = @doc.acro_form(create: true)
13
+ @sig1 = @form.create_signature_field("test1")
14
+ @sig2 = @form.create_signature_field("test2")
15
+ @handler = HexaPDF::Document::Signatures::DefaultHandler.new(
16
+ certificate: CERTIFICATES.signer_certificate,
17
+ key: CERTIFICATES.signer_key,
18
+ certificate_chain: [CERTIFICATES.ca_certificate]
19
+ )
20
+ end
21
+
22
+ describe "DefaultHandler" do
23
+ it "returns the filter name" do
24
+ assert_equal(:'Adobe.PPKLite', @handler.filter_name)
25
+ end
26
+
27
+ it "returns the sub filter algorithm name" do
28
+ assert_equal(:'adbe.pkcs7.detached', @handler.sub_filter_name)
29
+ end
30
+
31
+ it "returns the size of serialized signature" do
32
+ assert_equal(1310, @handler.signature_size)
33
+ end
34
+
35
+ it "allows setting the DocMDP permissions" do
36
+ assert_nil(@handler.doc_mdp_permissions)
37
+
38
+ @handler.doc_mdp_permissions = :no_changes
39
+ assert_equal(1, @handler.doc_mdp_permissions)
40
+ @handler.doc_mdp_permissions = 1
41
+ assert_equal(1, @handler.doc_mdp_permissions)
42
+
43
+ @handler.doc_mdp_permissions = :form_filling
44
+ assert_equal(2, @handler.doc_mdp_permissions)
45
+ @handler.doc_mdp_permissions = 2
46
+ assert_equal(2, @handler.doc_mdp_permissions)
47
+
48
+ @handler.doc_mdp_permissions = :form_filling_and_annotations
49
+ assert_equal(3, @handler.doc_mdp_permissions)
50
+ @handler.doc_mdp_permissions = 3
51
+ assert_equal(3, @handler.doc_mdp_permissions)
52
+
53
+ @handler.doc_mdp_permissions = nil
54
+ assert_nil(@handler.doc_mdp_permissions)
55
+
56
+ assert_raises(ArgumentError) { @handler.doc_mdp_permissions = :other }
57
+ end
58
+
59
+ it "can sign the data using PKCS7" do
60
+ data = "data"
61
+ store = OpenSSL::X509::Store.new
62
+ store.add_cert(CERTIFICATES.ca_certificate)
63
+
64
+ pkcs7 = OpenSSL::PKCS7.new(@handler.sign(data))
65
+ assert(pkcs7.detached?)
66
+ assert_equal([CERTIFICATES.signer_certificate, CERTIFICATES.ca_certificate],
67
+ pkcs7.certificates)
68
+ assert(pkcs7.verify([], store, data, OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY))
69
+ end
70
+
71
+ describe "finalize_objects" do
72
+ before do
73
+ @field = @doc.wrap({})
74
+ @obj = @doc.wrap({})
75
+ end
76
+
77
+ it "does nothing if no finalization tasks need to be done" do
78
+ @handler.finalize_objects(@field, @obj)
79
+ assert(@field.empty?)
80
+ assert(@obj.empty?)
81
+ end
82
+
83
+ it "sets the reason, location and contact info fields" do
84
+ @handler.reason = 'Reason'
85
+ @handler.location = 'Location'
86
+ @handler.contact_info = 'Contact'
87
+ @handler.finalize_objects(@field, @obj)
88
+ assert(@field.empty?)
89
+ assert_equal({Reason: 'Reason', Location: 'Location', ContactInfo: 'Contact'}, @obj.value)
90
+ end
91
+
92
+ it "applies the specified DocMDP permissions" do
93
+ @handler.doc_mdp_permissions = :no_changes
94
+ @handler.finalize_objects(@field, @obj)
95
+ ref = @obj[:Reference][0]
96
+ assert_equal(:DocMDP, ref[:TransformMethod])
97
+ assert_equal(:SHA1, ref[:DigestMethod])
98
+ assert_equal(1, ref[:TransformParams][:P])
99
+ assert_equal(:'1.2', ref[:TransformParams][:V])
100
+ assert_same(@obj, @doc.catalog[:Perms][:DocMDP])
101
+ end
102
+
103
+ it "fails if DocMDP should be set but there is already a signature" do
104
+ @handler.doc_mdp_permissions = :no_changes
105
+ 2.times do
106
+ field = @doc.acro_form(create: true).create_signature_field('test')
107
+ field.field_value = :something
108
+ end
109
+ assert_raises(HexaPDF::Error) { @handler.finalize_objects(@field, @obj) }
110
+ end
111
+ end
112
+ end
113
+
114
+ it "iterates over all signature dictionaries" do
115
+ assert_equal([], @doc.signatures.to_a)
116
+ @sig1.field_value = :sig1
117
+ @sig2.field_value = :sig2
118
+ assert_equal([:sig1, :sig2], @doc.signatures.to_a)
119
+ end
120
+
121
+ it "returns the number of signature dictionaries" do
122
+ @sig1.field_value = :sig1
123
+ assert_equal(1, @doc.signatures.count)
124
+ end
125
+
126
+ describe "handler" do
127
+ it "return the initialized handler" do
128
+ handler = @doc.signatures.handler(certificate: 'cert', reason: 'reason')
129
+ assert_equal('cert', handler.certificate)
130
+ assert_equal('reason', handler.reason)
131
+ end
132
+
133
+ it "fails if the given task is not available" do
134
+ assert_raises(HexaPDF::Error) { @doc.signatures.handler(name: :unknown) }
135
+ end
136
+ end
137
+
138
+ describe "add" do
139
+ before do
140
+ @doc = HexaPDF::Document.new(io: StringIO.new(MINIMAL_PDF))
141
+ @io = StringIO.new
142
+ end
143
+
144
+ it "uses the provided signature dictionary" do
145
+ sig = @doc.add({Type: :Sig, Key: :value})
146
+ @doc.signatures.add(@io, @handler, signature: sig)
147
+ assert_equal(1, @doc.signatures.to_a.compact.size)
148
+ assert_equal(:value, @doc.signatures.to_a[0][:Key])
149
+ refute_equal(:value, @doc.acro_form.each_field.first[:Key])
150
+ end
151
+
152
+ it "creates the signature dictionary if none is provided" do
153
+ @doc.signatures.add(@io, @handler)
154
+ assert_equal(1, @doc.signatures.to_a.compact.size)
155
+ refute(@doc.acro_form.each_field.first.key?(:Contents))
156
+ end
157
+
158
+ it "sets the needed information on the signature dictionary" do
159
+ def @handler.finalize_objects(sigfield, sig)
160
+ sig[:key] = :sig
161
+ sigfield[:key] = :sig_field
162
+ end
163
+ @doc.signatures.add(@io, @handler, write_options: {update_fields: false})
164
+ sig = @doc.signatures.first
165
+ assert_equal(:'Adobe.PPKLite', sig[:Filter])
166
+ assert_equal(:'adbe.pkcs7.detached', sig[:SubFilter])
167
+ assert_equal([0, 968, 3590, 2425], sig[:ByteRange].value)
168
+ assert_equal(:sig, sig[:key])
169
+ assert_equal(:sig_field, @doc.acro_form.each_field.first[:key])
170
+ assert(sig.key?(:Contents))
171
+ assert(sig.key?(:M))
172
+ end
173
+
174
+ it "creates the main form dictionary if necessary" do
175
+ @doc.signatures.add(@io, @handler)
176
+ assert(@doc.acro_form)
177
+ assert_equal([:signatures_exist, :append_only], @doc.acro_form.signature_flags)
178
+ end
179
+
180
+ it "uses the provided signature field" do
181
+ field = @doc.acro_form(create: true).create_signature_field('Signature2')
182
+ @doc.signatures.add(@io, @handler, signature: field)
183
+ assert_nil(@doc.acro_form.field_by_name("Signature3"))
184
+ refute_nil(field.field_value)
185
+ assert_nil(@doc.signatures.first[:T])
186
+ end
187
+
188
+ it "uses an existing signature field if possible" do
189
+ field = @doc.acro_form(create: true).create_signature_field('Signature2')
190
+ field.field_value = sig = @doc.add({Type: :Sig, key: :value})
191
+ @doc.signatures.add(@io, @handler, signature: sig)
192
+ assert_nil(@doc.acro_form.field_by_name("Signature3"))
193
+ assert_same(sig, @doc.signatures.first)
194
+ end
195
+
196
+ it "creates the signature field if necessary" do
197
+ @doc.acro_form(create: true).create_text_field('Signature2')
198
+ @doc.signatures.add(@io, @handler)
199
+ field = @doc.acro_form.field_by_name("Signature3")
200
+ assert_equal(:Sig, field.field_type)
201
+ refute_nil(field.field_value)
202
+ assert_equal(1, field.each_widget.count)
203
+ end
204
+
205
+ it "handles different xref section types correctly when determing the offsets" do
206
+ @doc.delete(7)
207
+ sig = @doc.signatures.add(@io, @handler, write_options: {update_fields: false})
208
+ assert_equal([0, 968, 3590, 2412], sig[:ByteRange].value)
209
+ end
210
+
211
+ it "allows writing to a file in addition to writing to an IO" do
212
+ tempfile = Tempfile.new('hexapdf-signature')
213
+ tempfile.close
214
+ @doc.signatures.add(tempfile.path, @handler)
215
+ doc = HexaPDF::Document.open(tempfile.path)
216
+ assert(doc.signatures.first.verify(allow_self_signed: true).success?)
217
+ end
218
+
219
+ it "adds a new revision with the signature" do
220
+ @doc.signatures.add(@io, @handler)
221
+ signed_doc = HexaPDF::Document.new(io: @io)
222
+ assert(signed_doc.signatures.first.verify)
223
+ end
224
+ end
225
+ end
@@ -168,12 +168,14 @@ describe HexaPDF::Task::Optimize do
168
168
  page1.resources[:XObject][:test] = @doc.add({})
169
169
  page1.resources[:XObject][:used_on_page2] = @doc.add({})
170
170
  page1.resources[:XObject][:unused] = @doc.add({})
171
- page1.contents = "/test Do"
171
+ page1.contents = "/test Do /InvalidRef Do"
172
172
  page2 = @doc.pages.add
173
173
  page2.resources[:XObject] = {}
174
174
  page2.resources[:XObject][:used_on2] = page1.resources[:XObject][:used_on_page2]
175
175
  page2.resources[:XObject][:also_unused] = page1.resources[:XObject][:unused]
176
176
  page2.contents = "/used_on2 Do"
177
+ page3 = @doc.pages.add
178
+ page3.contents = "/unused Do "
177
179
 
178
180
  @doc.task(:optimize, prune_page_resources: true, compress_pages: compress_pages)
179
181
 
@@ -182,6 +184,7 @@ describe HexaPDF::Task::Optimize do
182
184
  refute(page1.resources[:XObject].key?(:unused))
183
185
  assert(page2.resources[:XObject].key?(:used_on2))
184
186
  refute(page2.resources[:XObject].key?(:also_unused))
187
+ assert_equal("/unused Do#{compress_pages ? "\n" : ' '}", page3.contents)
185
188
  end
186
189
  end
187
190
  end
@@ -596,6 +596,34 @@ describe HexaPDF::Document do
596
596
  end
597
597
  end
598
598
 
599
+ describe "signature interface" do
600
+ it "returns whether the document is signed or not" do
601
+ refute(@doc.signed?)
602
+
603
+ form = @doc.acro_form(create: true)
604
+ form.signature_flag(:signatures_exist)
605
+ assert(@doc.signed?)
606
+ end
607
+
608
+ it "returns all signature fields of the document" do
609
+ form = @doc.acro_form(create: true)
610
+ sig1 = @doc.add({FT: :Sig, T: 'sig1', V: :sig1})
611
+ sig2 = @doc.add({FT: :Sig, T: 'sig2', V: :sig2})
612
+ form.root_fields << sig1 << sig2
613
+ assert_equal([:sig1, :sig2], @doc.signatures.to_a)
614
+ end
615
+
616
+ it "allows to conveniently sign a document" do
617
+ mock = Minitest::Mock.new
618
+ mock.expect(:handler, :handler, [{name: :handler, opt: :key}])
619
+ mock.expect(:add, :added, [:io, :handler, {signature: :sig, write_options: :write_options}])
620
+ @doc.instance_variable_set(:@signatures, mock)
621
+ result = @doc.sign(:io, handler: :handler, write_options: :write_options, signature: :sig, opt: :key)
622
+ assert_equal(:added, result)
623
+ mock.verify
624
+ end
625
+ end
626
+
599
627
  describe "listener interface" do
600
628
  it "allows registering and dispatching messages" do
601
629
  args = []
@@ -182,7 +182,7 @@ describe HexaPDF::Object do
182
182
  assert_match(/\[5, 0\].*value=5/, obj.inspect)
183
183
  end
184
184
 
185
- it "can be compared to another object or reference" do
185
+ it "can be compared to another object, reference or, if not indirect, a simple value" do
186
186
  obj = HexaPDF::Object.new(5, oid: 5)
187
187
 
188
188
  assert_equal(obj, HexaPDF::Object.new(obj))
@@ -192,6 +192,11 @@ describe HexaPDF::Object do
192
192
  refute_equal(obj, HexaPDF::Object.new(5, oid: 5, gen: 1))
193
193
 
194
194
  assert_equal(obj, HexaPDF::Reference.new(5, 0))
195
+
196
+ refute_equal(obj, 5)
197
+ obj.data.oid = 0
198
+ assert_equal(obj, 5)
199
+ assert_equal(obj, HexaPDF::Object.new(5))
195
200
  end
196
201
 
197
202
  it "works correctly as hash key, is interchangable in this regard with Reference objects" do
@@ -216,7 +221,7 @@ describe HexaPDF::Object do
216
221
  it "creates an independent object" do
217
222
  obj = HexaPDF::Object.new({a: "mystring", b: HexaPDF::Reference.new(1, 0), c: 5})
218
223
  copy = obj.deep_copy
219
- refute_equal(copy, obj)
224
+ refute_same(copy, obj)
220
225
  assert_equal(copy.value, obj.value)
221
226
  refute_same(copy.value[:a], obj.value[:a])
222
227
  end
@@ -338,6 +338,11 @@ describe HexaPDF::Parser do
338
338
  assert_equal(5, @parser.startxref_offset)
339
339
  end
340
340
 
341
+ it "handles the case of startxref and its value being on the same line" do
342
+ create_parser("startxref 5\n%%EOF")
343
+ assert_equal(5, @parser.startxref_offset)
344
+ end
345
+
341
346
  it "fails even in big files when nothing is found" do
342
347
  create_parser("\nhallo" * 5000)
343
348
  exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.startxref_offset }
@@ -366,6 +371,13 @@ describe HexaPDF::Parser do
366
371
  exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.startxref_offset }
367
372
  assert_match(/end-of-file marker not found/, exp.message)
368
373
  end
374
+
375
+ it "fails on strict parsing if the startxref is on the same line as its value" do
376
+ @document.config['parser.on_correctable_error'] = proc { true }
377
+ create_parser("startxref 5\n%%EOF")
378
+ exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.startxref_offset }
379
+ assert_match(/startxref on same line/, exp.message)
380
+ end
369
381
  end
370
382
 
371
383
  describe "file_header_version" do
@@ -50,13 +50,6 @@ describe HexaPDF::Rectangle do
50
50
  assert_equal(12, rect.top)
51
51
  end
52
52
 
53
- it "allows comparison to arrays" do
54
- rect = HexaPDF::Rectangle.new([0, 1, 2, 5])
55
- assert(rect == [0, 1, 2, 5])
56
- rect.oid = 5
57
- refute(rect == [0, 1, 2, 5])
58
- end
59
-
60
53
  describe "validation" do
61
54
  it "ensures that it is a correct PDF rectangle" do
62
55
  doc = HexaPDF::Document.new