hexapdf 0.19.0 → 0.20.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +69 -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/lib/hexapdf/cli/info.rb +21 -1
  9. data/lib/hexapdf/configuration.rb +26 -0
  10. data/lib/hexapdf/content/graphics_state.rb +24 -5
  11. data/lib/hexapdf/content/processor.rb +1 -1
  12. data/lib/hexapdf/document/signatures.rb +327 -0
  13. data/lib/hexapdf/document.rb +26 -0
  14. data/lib/hexapdf/encryption/standard_security_handler.rb +1 -2
  15. data/lib/hexapdf/importer.rb +1 -1
  16. data/lib/hexapdf/layout/style.rb +2 -1
  17. data/lib/hexapdf/object.rb +5 -3
  18. data/lib/hexapdf/parser.rb +21 -9
  19. data/lib/hexapdf/rectangle.rb +0 -6
  20. data/lib/hexapdf/revision.rb +13 -6
  21. data/lib/hexapdf/type/acro_form/appearance_generator.rb +2 -4
  22. data/lib/hexapdf/type/acro_form/field.rb +2 -0
  23. data/lib/hexapdf/type/acro_form/form.rb +9 -1
  24. data/lib/hexapdf/type/annotation.rb +36 -3
  25. data/lib/hexapdf/type/font.rb +5 -0
  26. data/lib/hexapdf/type/font_simple.rb +1 -1
  27. data/lib/hexapdf/type/font_type3.rb +20 -0
  28. data/lib/hexapdf/type/object_stream.rb +3 -1
  29. data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +125 -0
  30. data/lib/hexapdf/type/signature/adbe_x509_rsa_sha1.rb +99 -0
  31. data/lib/hexapdf/type/signature/handler.rb +112 -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 +24 -10
  37. data/test/hexapdf/content/test_graphics_state.rb +9 -1
  38. data/test/hexapdf/content/test_operator.rb +8 -3
  39. data/test/hexapdf/content/test_processor.rb +1 -1
  40. data/test/hexapdf/document/test_signatures.rb +225 -0
  41. data/test/hexapdf/encryption/test_standard_security_handler.rb +8 -6
  42. data/test/hexapdf/layout/test_style.rb +11 -0
  43. data/test/hexapdf/test_document.rb +28 -0
  44. data/test/hexapdf/test_object.rb +7 -2
  45. data/test/hexapdf/test_parser.rb +14 -0
  46. data/test/hexapdf/test_rectangle.rb +0 -7
  47. data/test/hexapdf/test_revision.rb +44 -14
  48. data/test/hexapdf/test_writer.rb +44 -14
  49. data/test/hexapdf/type/acro_form/test_field.rb +11 -1
  50. data/test/hexapdf/type/acro_form/test_form.rb +5 -0
  51. data/test/hexapdf/type/signature/common.rb +71 -0
  52. data/test/hexapdf/type/signature/test_adbe_pkcs7_detached.rb +99 -0
  53. data/test/hexapdf/type/signature/test_adbe_x509_rsa_sha1.rb +66 -0
  54. data/test/hexapdf/type/signature/test_handler.rb +76 -0
  55. data/test/hexapdf/type/signature/test_verification_result.rb +47 -0
  56. data/test/hexapdf/type/test_annotation.rb +40 -2
  57. data/test/hexapdf/type/test_font.rb +4 -0
  58. data/test/hexapdf/type/test_font_simple.rb +5 -5
  59. data/test/hexapdf/type/test_font_type3.rb +16 -1
  60. data/test/hexapdf/type/test_object_stream.rb +9 -0
  61. data/test/hexapdf/type/test_signature.rb +131 -0
  62. metadata +21 -33
  63. data/test/data/cert/create.sh +0 -171
  64. data/test/data/cert/root-ca/certs/84E66B6F4C359E741C0AFA014790DF39.pem +0 -119
  65. data/test/data/cert/root-ca/certs/84E66B6F4C359E741C0AFA014790DF3A.pem +0 -125
  66. data/test/data/cert/root-ca/db/crlnumber +0 -1
  67. data/test/data/cert/root-ca/db/index +0 -2
  68. data/test/data/cert/root-ca/db/index.attr +0 -1
  69. data/test/data/cert/root-ca/db/index.attr.old +0 -1
  70. data/test/data/cert/root-ca/db/index.old +0 -1
  71. data/test/data/cert/root-ca/db/serial +0 -1
  72. data/test/data/cert/root-ca/db/serial.old +0 -1
  73. data/test/data/cert/root-ca/private/root-ca.key +0 -52
  74. data/test/data/cert/root-ca/root-ca.conf +0 -65
  75. data/test/data/cert/root-ca/root-ca.crt +0 -119
  76. data/test/data/cert/root-ca/root-ca.csr +0 -28
  77. data/test/data/cert/signature-1-pkcs7-detached.pdf +0 -182
  78. data/test/data/cert/sub-ca/certs/453FF080E3EDCD6A388D5368DFC320D9.pem +0 -125
  79. data/test/data/cert/sub-ca/db/crlnumber +0 -1
  80. data/test/data/cert/sub-ca/db/index +0 -1
  81. data/test/data/cert/sub-ca/db/index.attr +0 -1
  82. data/test/data/cert/sub-ca/db/index.old +0 -0
  83. data/test/data/cert/sub-ca/db/serial +0 -1
  84. data/test/data/cert/sub-ca/db/serial.old +0 -1
  85. data/test/data/cert/sub-ca/private/signing.key +0 -52
  86. data/test/data/cert/sub-ca/private/sub-ca.key +0 -52
  87. data/test/data/cert/sub-ca/signing.crt +0 -125
  88. data/test/data/cert/sub-ca/signing.csr +0 -28
  89. data/test/data/cert/sub-ca/signing.p12 +0 -0
  90. data/test/data/cert/sub-ca/sub-ca.conf +0 -65
  91. data/test/data/cert/sub-ca/sub-ca.crt +0 -125
  92. data/test/data/cert/sub-ca/sub-ca.csr +0 -28
@@ -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.0'
40
+ VERSION = '0.20.0'
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
@@ -66,33 +68,42 @@ module HexaPDF
66
68
  @serializer = Serializer.new
67
69
  @serializer.encrypter = @document.encrypted? ? @document.security_handler : nil
68
70
  @rev_size = 0
71
+
72
+ @use_xref_streams = false
69
73
  end
70
74
 
71
- # Writes the document to the IO object.
75
+ # Writes the document to the IO object and returns the last XRefSection written.
72
76
  def write
73
77
  write_file_header
74
78
 
75
- pos = nil
79
+ pos = xref_section = nil
76
80
  @document.trailer.info[:Producer] = "HexaPDF version #{HexaPDF::VERSION}"
77
81
  @document.revisions.each do |rev|
78
- pos = write_revision(rev, pos)
82
+ pos, xref_section = write_revision(rev, pos)
79
83
  end
84
+
85
+ xref_section
80
86
  end
81
87
 
82
- # 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.
83
90
  #
84
91
  # For this method to work the document must have been created from an existing file.
85
92
  def write_incremental
86
93
  @document.revisions.parser.io.seek(0, IO::SEEK_SET)
87
94
  IO.copy_stream(@document.revisions.parser.io, @io)
95
+ @io << "\n"
88
96
 
89
97
  @rev_size = @document.revisions.current.next_free_oid
98
+ @use_xref_streams = @document.revisions.parser.contains_xref_streams?
90
99
 
91
100
  revision = Revision.new(@document.revisions.current.trailer)
92
101
  @document.revisions.each do |rev|
93
102
  rev.each_modified_object {|obj| revision.send(:add_without_check, obj) }
94
103
  end
95
- write_revision(revision, @document.revisions.parser.startxref_offset)
104
+ _pos, xref_section = write_revision(revision, @document.revisions.parser.startxref_offset)
105
+
106
+ xref_section
96
107
  end
97
108
 
98
109
  private
@@ -147,7 +158,7 @@ module HexaPDF
147
158
 
148
159
  write_startxref(startxref)
149
160
 
150
- startxref
161
+ [startxref, xref_section]
151
162
  end
152
163
 
153
164
  # :call-seq:
@@ -170,10 +181,13 @@ module HexaPDF
170
181
  end
171
182
  end
172
183
 
173
- if !object_streams.empty? && xref_stream.nil?
174
- raise HexaPDF::Error, "Cannot use object streams when there is no xref stream"
184
+ if (!object_streams.empty? || @use_xref_streams) && xref_stream.nil?
185
+ xref_stream = @document.wrap({Type: :XRef}, oid: rev.next_free_oid)
186
+ rev.add(xref_stream)
175
187
  end
176
188
 
189
+ @use_xref_streams = true if xref_stream
190
+
177
191
  [xref_stream, object_streams]
178
192
  end
179
193
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'test_helper'
4
4
  require 'hexapdf/content/graphics_state'
5
+ require 'ostruct'
5
6
 
6
7
  # Dummy class used as wrapper so that constant lookup works correctly
7
8
  class GraphicsStateWrapper < Minitest::Spec
@@ -146,6 +147,13 @@ class GraphicsStateWrapper < Minitest::Spec
146
147
  it "fails when restoring the graphics state if the stack is empty" do
147
148
  assert_raises(HexaPDF::Error) { @gs.restore }
148
149
  end
149
- end
150
150
 
151
+ it "uses the correct glyph to text space scaling" do
152
+ font = OpenStruct.new
153
+ font.glyph_scaling_factor = 0.002
154
+ @gs.font = font
155
+ @gs.font_size = 10
156
+ assert_equal(0.02, @gs.scaled_font_size)
157
+ end
158
+ end
151
159
  end
@@ -4,6 +4,7 @@ require 'test_helper'
4
4
  require 'hexapdf/content/operator'
5
5
  require 'hexapdf/content/processor'
6
6
  require 'hexapdf/serializer'
7
+ require 'ostruct'
7
8
 
8
9
  describe HexaPDF::Content::Operator::BaseOperator do
9
10
  before do
@@ -190,9 +191,11 @@ end
190
191
 
191
192
  describe_operator :SetGraphicsStateParameters, :gs do
192
193
  it "applies parameters from an ExtGState dictionary" do
194
+ font = OpenStruct.new
195
+ font.glyph_scaling_factor = 0.01
193
196
  @processor.resources[:ExtGState] = {Name: {LW: 10, LC: 2, LJ: 2, ML: 2, D: [[3, 5], 2],
194
197
  RI: 2, SA: true, BM: :Multiply, CA: 0.5, ca: 0.5,
195
- AIS: true, TK: false, Font: [:Test, 10]}}
198
+ AIS: true, TK: false, Font: [font, 10]}}
196
199
  @processor.resources.define_singleton_method(:document) do
197
200
  Object.new.tap {|obj| obj.define_singleton_method(:deref) {|o| o } }
198
201
  end
@@ -210,7 +213,7 @@ describe_operator :SetGraphicsStateParameters, :gs do
210
213
  assert_equal(0.5, gs.stroke_alpha)
211
214
  assert_equal(0.5, gs.fill_alpha)
212
215
  assert(gs.alpha_source)
213
- assert_equal(:Test, gs.font)
216
+ assert_equal(font, gs.font)
214
217
  assert_equal(10, gs.font_size)
215
218
  refute(gs.text_knockout)
216
219
  end
@@ -448,7 +451,9 @@ describe_operator :SetFontAndSize, :Tf do
448
451
  self[:Font] && self[:Font][name]
449
452
  end
450
453
 
451
- @processor.resources[:Font] = {F1: :test}
454
+ font = OpenStruct.new
455
+ font.glyph_scaling_factor = 0.01
456
+ @processor.resources[:Font] = {F1: font}
452
457
  invoke(:F1, 10)
453
458
  assert_equal(@processor.resources.font(:F1), @processor.graphics_state.font)
454
459
  assert_equal(10, @processor.graphics_state.font_size)
@@ -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
@@ -229,19 +229,21 @@ describe HexaPDF::Encryption::StandardSecurityHandler do
229
229
  assert_match(/Invalid \/R/i, exp.message)
230
230
  end
231
231
 
232
- it "fails if the ID in the document's trailer is missing although it is needed" do
232
+ it "fails if the supplied password is invalid" do
233
233
  exp = assert_raises(HexaPDF::EncryptionError) do
234
- @handler.set_up_decryption({Filter: :Standard, V: 2, R: 2})
234
+ @handler.set_up_decryption({Filter: :Standard, V: 2, R: 6, U: 'a' * 48, O: 'a' * 48,
235
+ UE: 'a' * 32, OE: 'a' * 32})
235
236
  end
236
- assert_match(/Document ID/i, exp.message)
237
+ assert_match(/Invalid password/i, exp.message)
237
238
  end
238
239
 
239
- it "fails if the supplied password is invalid" do
240
+ it "assigns empty strings to the trailer's ID field if it is missing" do
241
+ refute(@document.trailer.key?(:ID))
240
242
  exp = assert_raises(HexaPDF::EncryptionError) do
241
- @handler.set_up_decryption({Filter: :Standard, V: 2, R: 6, U: 'a' * 48, O: 'a' * 48,
242
- UE: 'a' * 32, OE: 'a' * 32})
243
+ @handler.set_up_decryption({Filter: :Standard, V: 1, R: 2, U: 'a' * 48, O: 'a' * 48, P: 15})
243
244
  end
244
245
  assert_match(/Invalid password/i, exp.message)
246
+ assert_equal(['', ''], @document.trailer[:ID].value)
245
247
  end
246
248
 
247
249
  describe "/Perms field checking" do