hexapdf 0.20.0 → 0.20.4

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.
@@ -78,6 +78,45 @@ module HexaPDF
78
78
  raise "Needs to be implemented by specific handlers"
79
79
  end
80
80
 
81
+ # Verifies general signature properties and prepares the provided OpenSSL::X509::Store
82
+ # object for use by concrete implementations.
83
+ #
84
+ # Needs to be called by specific handlers.
85
+ def verify(store, allow_self_signed: false)
86
+ result = VerificationResult.new
87
+ check_certified_signature(result)
88
+ verify_signing_time(result)
89
+ store.verify_callback =
90
+ store_verification_callback(result, allow_self_signed: allow_self_signed)
91
+ result
92
+ end
93
+
94
+ protected
95
+
96
+ # Verifies that the signing time was within the validity period of the signer certificate.
97
+ def verify_signing_time(result)
98
+ time = signing_time
99
+ cert = signer_certificate
100
+ if time && cert && (time < cert.not_before || time > cert.not_after)
101
+ result.log(:error, "Signer certificate not valid at signing time")
102
+ end
103
+ end
104
+
105
+ DOCMDP_PERMS_MESSAGE_MAP = { # :nodoc:
106
+ 1 => "No changes allowed",
107
+ 2 => "Form filling and signing allowed",
108
+ 3 => "Form filling, signing and annotation manipulation allowed",
109
+ }
110
+
111
+ # Sets an informational message on +result+ whether the signature is a certified signature.
112
+ def check_certified_signature(result)
113
+ sigref = signature_dict[:Reference]&.find {|ref| ref[:TransformMethod] == :DocMDP }
114
+ if sigref && signature_dict.document.catalog[:Perms]&.[](:DocMDP) == signature_dict
115
+ perms = sigref[:TransformParams]&.[](:P) || 2
116
+ result.log(:info, "Certified signature (#{DOCMDP_PERMS_MESSAGE_MAP[perms]})")
117
+ end
118
+ end
119
+
81
120
  # Returns the block that should be used as the OpenSSL::X509::Store verification callback.
82
121
  #
83
122
  # +result+:: The VerificationResult object that should be updated if problems are found.
@@ -94,17 +133,6 @@ module HexaPDF
94
133
  end
95
134
  end
96
135
 
97
- protected
98
-
99
- # Verifies that the signing time was within the validity period of the signer certificate.
100
- def verify_signing_time(result)
101
- time = signing_time
102
- cert = signer_certificate
103
- if time && (time < cert.not_before || time > cert.not_after)
104
- result.log(:error, "Signer certificate not valid at signing time")
105
- end
106
- end
107
-
108
136
  end
109
137
 
110
138
  end
@@ -37,6 +37,6 @@
37
37
  module HexaPDF
38
38
 
39
39
  # The version of HexaPDF.
40
- VERSION = '0.20.0'
40
+ VERSION = '0.20.4'
41
41
 
42
42
  end
@@ -90,12 +90,20 @@ module HexaPDF
90
90
  #
91
91
  # For this method to work the document must have been created from an existing file.
92
92
  def write_incremental
93
- @document.revisions.parser.io.seek(0, IO::SEEK_SET)
94
- IO.copy_stream(@document.revisions.parser.io, @io)
93
+ parser = @document.revisions.parser
94
+
95
+ _, orig_trailer = parser.load_revision(parser.startxref_offset)
96
+ orig_trailer = @document.wrap(orig_trailer, type: :XXTrailer)
97
+ if @document.revisions.current.trailer[:Encrypt]&.value != orig_trailer[:Encrypt]&.value
98
+ raise HexaPDF::Error, "Used encryption cannot be modified when doing incremental writing"
99
+ end
100
+
101
+ parser.io.seek(0, IO::SEEK_SET)
102
+ IO.copy_stream(parser.io, @io)
95
103
  @io << "\n"
96
104
 
97
105
  @rev_size = @document.revisions.current.next_free_oid
98
- @use_xref_streams = @document.revisions.parser.contains_xref_streams?
106
+ @use_xref_streams = parser.contains_xref_streams?
99
107
 
100
108
  revision = Revision.new(@document.revisions.current.trailer)
101
109
  @document.revisions.each do |rev|
@@ -294,9 +294,17 @@ describe HexaPDF::Encryption::SecurityHandler do
294
294
  assert_equal('string', obj.stream)
295
295
  end
296
296
 
297
- it "doesn't decrypt a document's Encrypt dictionary" do
298
- @document.trailer[:Encrypt] = @obj
299
- assert_equal(@encrypted, @handler.decrypt(@obj)[:Key])
297
+ it "doesn't decrypt a document's Encrypt dictionaries" do
298
+ @document = HexaPDF::Document.new
299
+ @document.trailer[:Encrypt] = @document.add({Key: "Something"})
300
+ @document.revisions.add
301
+ @document.trailer[:Encrypt] = @document.add({Key: "Otherthing"})
302
+ @handler = TestHandler.new(@document)
303
+
304
+ assert_equal("Something",
305
+ @handler.decrypt(@document.revisions[0].trailer[:Encrypt])[:Key])
306
+ assert_equal("Otherthing",
307
+ @handler.decrypt(@document.revisions[1].trailer[:Encrypt])[:Key])
300
308
  end
301
309
 
302
310
  it "defers handling encryption to a Crypt filter is specified" do
@@ -103,5 +103,9 @@ describe HexaPDF::Font::Type1Wrapper do
103
103
  it "sets the circular reference" do
104
104
  assert_same(@times_wrapper, @times_wrapper.pdf_object.font_wrapper)
105
105
  end
106
+
107
+ it "makes sure that the PDF dictionaries are indirect" do
108
+ assert(@times_wrapper.pdf_object.indirect?)
109
+ end
106
110
  end
107
111
  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
@@ -40,7 +40,7 @@ describe HexaPDF::Writer do
40
40
  219
41
41
  %%EOF
42
42
  3 0 obj
43
- <</Producer(HexaPDF version 0.20.0)>>
43
+ <</Producer(HexaPDF version 0.20.4)>>
44
44
  endobj
45
45
  xref
46
46
  3 1
@@ -72,7 +72,7 @@ describe HexaPDF::Writer do
72
72
  141
73
73
  %%EOF
74
74
  6 0 obj
75
- <</Producer(HexaPDF version 0.20.0)>>
75
+ <</Producer(HexaPDF version 0.20.4)>>
76
76
  endobj
77
77
  2 0 obj
78
78
  <</Length 10>>stream
@@ -123,6 +123,17 @@ describe HexaPDF::Writer do
123
123
  HexaPDF::Writer.write(doc, output_io, incremental: true)
124
124
  refute_match(/^trailer/, output_io.string)
125
125
  end
126
+
127
+ it "raises an error if the used encryption was changed" do
128
+ io = StringIO.new
129
+ doc = HexaPDF::Document.new
130
+ doc.encrypt
131
+ doc.write(io)
132
+
133
+ doc = HexaPDF::Document.new(io: io)
134
+ doc.encrypt(owner_password: 'test')
135
+ assert_raises(HexaPDF::Error) { doc.write('notused', incremental: true) }
136
+ end
126
137
  end
127
138
 
128
139
  it "creates an xref stream if no xref stream is in a revision but object streams are" do
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'test_helper'
4
4
  require 'hexapdf/type/signature'
5
+ require 'hexapdf/document'
5
6
  require 'time'
6
7
  require 'ostruct'
7
8
 
@@ -10,6 +11,7 @@ describe HexaPDF::Type::Signature::Handler do
10
11
  @time = Time.parse("2021-11-14 7:00")
11
12
  @dict = {Name: "handler", M: @time}
12
13
  @handler = HexaPDF::Type::Signature::Handler.new(@dict)
14
+ @result = HexaPDF::Type::Signature::VerificationResult.new
13
15
  end
14
16
 
15
17
  it "returns the signer name" do
@@ -30,7 +32,6 @@ describe HexaPDF::Type::Signature::Handler do
30
32
 
31
33
  describe "store_verification_callback" do
32
34
  before do
33
- @result = HexaPDF::Type::Signature::VerificationResult.new
34
35
  @context = OpenStruct.new
35
36
  end
36
37
 
@@ -40,7 +41,7 @@ describe HexaPDF::Type::Signature::Handler do
40
41
  [true, false].each do |allow_self_signed|
41
42
  @result.messages.clear
42
43
  @context.error = error
43
- @handler.store_verification_callback(@result, allow_self_signed: allow_self_signed).
44
+ @handler.send(:store_verification_callback, @result, allow_self_signed: allow_self_signed).
44
45
  call(false, @context)
45
46
  assert_equal(1, @result.messages.size)
46
47
  assert_match(/self-signed certificate/i, @result.messages[0].content)
@@ -51,26 +52,51 @@ describe HexaPDF::Type::Signature::Handler do
51
52
  end
52
53
 
53
54
  it "verifies the signing time" do
54
- result = HexaPDF::Type::Signature::VerificationResult.new
55
55
  [
56
56
  [true, '6:00', '8:00'],
57
57
  [false, '7:30', '8:00'],
58
58
  [false, '5:00', '6:00'],
59
59
  ].each do |success, not_before, not_after|
60
- result.messages.clear
60
+ @result.messages.clear
61
61
  @handler.define_singleton_method(:signer_certificate) do
62
62
  OpenStruct.new.tap do |struct|
63
63
  struct.not_before = Time.parse("2021-11-14 #{not_before}")
64
64
  struct.not_after = Time.parse("2021-11-14 #{not_after}")
65
65
  end
66
66
  end
67
- @handler.send(:verify_signing_time, result)
67
+ @handler.send(:verify_signing_time, @result)
68
68
  if success
69
- assert(result.messages.empty?)
69
+ assert(@result.messages.empty?)
70
70
  else
71
- assert_equal(1, result.messages.size)
71
+ assert_equal(1, @result.messages.size)
72
72
  end
73
73
  @handler.singleton_class.remove_method(:signer_certificate)
74
74
  end
75
75
  end
76
+
77
+ describe "check_certified_signature" do
78
+ before do
79
+ @dict = HexaPDF::Document.new.wrap({Type: :Sig})
80
+ @handler.instance_variable_set(:@signature_dict, @dict)
81
+ end
82
+
83
+ it "logs nothing if there is no signature reference dictionary" do
84
+ @handler.send(:check_certified_signature, @result)
85
+ assert(@result.messages.empty?)
86
+ end
87
+
88
+ it "logs nothing if the global DocMDP permissions entry doesn't point to the signature" do
89
+ @dict[:Reference] = [{TransformMethod: :DocMDP}]
90
+ @handler.send(:check_certified_signature, @result)
91
+ assert(@result.messages.empty?)
92
+ end
93
+
94
+ it "logs a message if the signature is a certified one" do
95
+ @dict[:Reference] = [{TransformMethod: :DocMDP}]
96
+ @dict.document.catalog[:Perms] = {DocMDP: @dict}
97
+ @handler.send(:check_certified_signature, @result)
98
+ assert_equal(1, @result.messages.size)
99
+ assert_match(/certified signature/i, @result.messages[0].content)
100
+ end
101
+ end
76
102
  end
@@ -77,7 +77,13 @@ describe HexaPDF::Type::Annotation do
77
77
  stream[:BBox] = [1, 2, 3, 4]
78
78
  appearance = @annot.appearance
79
79
  assert_same(stream.data, appearance.data)
80
- assert_equal(:Form, appearance[:Subtype])
80
+ assert_kind_of(HexaPDF::Type::Form, appearance)
81
+
82
+ stream[:Type] = :XObject
83
+ stream[:Subtype] = :Form
84
+ appearance = @annot.appearance
85
+ assert_same(stream.data, appearance.data)
86
+ assert_kind_of(HexaPDF::Type::Form, appearance)
81
87
 
82
88
  @annot[:AP][:N] = {X: {}}
83
89
  assert_nil(@annot.appearance)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hexapdf
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.20.0
4
+ version: 0.20.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas Leitner
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-12-30 00:00:00.000000000 Z
11
+ date: 2022-01-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cmdparse
@@ -671,7 +671,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
671
671
  - !ruby/object:Gem::Version
672
672
  version: '0'
673
673
  requirements: []
674
- rubygems_version: 3.2.32
674
+ rubygems_version: 3.3.3
675
675
  signing_key:
676
676
  specification_version: 4
677
677
  summary: HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby