hexapdf 0.19.0 → 0.20.0

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 (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