hexapdf 0.19.2 → 0.20.2

Sign up to get free protection for your applications and to get access to all the features.
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