hexapdf 0.19.2 → 0.20.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +67 -0
- data/data/hexapdf/cert/demo_cert.rb +22 -0
- data/data/hexapdf/cert/root-ca.crt +119 -0
- data/data/hexapdf/cert/signing.crt +125 -0
- data/data/hexapdf/cert/signing.key +52 -0
- data/data/hexapdf/cert/sub-ca.crt +125 -0
- data/data/hexapdf/encoding/glyphlist.txt +4283 -4282
- data/data/hexapdf/encoding/zapfdingbats.txt +203 -202
- data/lib/hexapdf/cli/info.rb +21 -1
- data/lib/hexapdf/configuration.rb +26 -0
- data/lib/hexapdf/content/processor.rb +1 -1
- data/lib/hexapdf/document/signatures.rb +327 -0
- data/lib/hexapdf/document.rb +26 -0
- data/lib/hexapdf/font/encoding/glyph_list.rb +5 -6
- data/lib/hexapdf/importer.rb +1 -1
- data/lib/hexapdf/object.rb +5 -3
- data/lib/hexapdf/parser.rb +14 -9
- data/lib/hexapdf/rectangle.rb +0 -6
- data/lib/hexapdf/revision.rb +13 -6
- data/lib/hexapdf/task/dereference.rb +12 -4
- data/lib/hexapdf/task/optimize.rb +3 -3
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +2 -4
- data/lib/hexapdf/type/acro_form/field.rb +2 -0
- data/lib/hexapdf/type/acro_form/form.rb +9 -1
- data/lib/hexapdf/type/annotation.rb +36 -3
- data/lib/hexapdf/type/font_simple.rb +1 -1
- data/lib/hexapdf/type/object_stream.rb +3 -1
- data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +121 -0
- data/lib/hexapdf/type/signature/adbe_x509_rsa_sha1.rb +95 -0
- data/lib/hexapdf/type/signature/handler.rb +140 -0
- data/lib/hexapdf/type/signature/verification_result.rb +92 -0
- data/lib/hexapdf/type/signature.rb +236 -0
- data/lib/hexapdf/type.rb +1 -0
- data/lib/hexapdf/version.rb +1 -1
- data/lib/hexapdf/writer.rb +16 -8
- data/test/hexapdf/content/test_processor.rb +1 -1
- data/test/hexapdf/document/test_signatures.rb +225 -0
- data/test/hexapdf/task/test_optimize.rb +4 -1
- data/test/hexapdf/test_document.rb +28 -0
- data/test/hexapdf/test_object.rb +7 -2
- data/test/hexapdf/test_parser.rb +12 -0
- data/test/hexapdf/test_rectangle.rb +0 -7
- data/test/hexapdf/test_revision.rb +44 -14
- data/test/hexapdf/test_writer.rb +4 -3
- data/test/hexapdf/type/acro_form/test_field.rb +11 -1
- data/test/hexapdf/type/acro_form/test_form.rb +5 -0
- data/test/hexapdf/type/signature/common.rb +71 -0
- data/test/hexapdf/type/signature/test_adbe_pkcs7_detached.rb +99 -0
- data/test/hexapdf/type/signature/test_adbe_x509_rsa_sha1.rb +66 -0
- data/test/hexapdf/type/signature/test_handler.rb +102 -0
- data/test/hexapdf/type/signature/test_verification_result.rb +47 -0
- data/test/hexapdf/type/test_annotation.rb +40 -2
- data/test/hexapdf/type/test_font_simple.rb +5 -5
- data/test/hexapdf/type/test_object_stream.rb +9 -0
- data/test/hexapdf/type/test_signature.rb +131 -0
- 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
data/lib/hexapdf/version.rb
CHANGED
data/lib/hexapdf/writer.rb
CHANGED
|
@@ -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
|
|
48
|
-
#
|
|
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
|
|
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(
|
|
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 = []
|
data/test/hexapdf/test_object.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
data/test/hexapdf/test_parser.rb
CHANGED
|
@@ -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
|