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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +69 -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/lib/hexapdf/cli/info.rb +21 -1
- data/lib/hexapdf/configuration.rb +26 -0
- data/lib/hexapdf/content/graphics_state.rb +24 -5
- 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/encryption/standard_security_handler.rb +1 -2
- data/lib/hexapdf/importer.rb +1 -1
- data/lib/hexapdf/layout/style.rb +2 -1
- data/lib/hexapdf/object.rb +5 -3
- data/lib/hexapdf/parser.rb +21 -9
- data/lib/hexapdf/rectangle.rb +0 -6
- data/lib/hexapdf/revision.rb +13 -6
- 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.rb +5 -0
- data/lib/hexapdf/type/font_simple.rb +1 -1
- data/lib/hexapdf/type/font_type3.rb +20 -0
- data/lib/hexapdf/type/object_stream.rb +3 -1
- data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +125 -0
- data/lib/hexapdf/type/signature/adbe_x509_rsa_sha1.rb +99 -0
- data/lib/hexapdf/type/signature/handler.rb +112 -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 +24 -10
- data/test/hexapdf/content/test_graphics_state.rb +9 -1
- data/test/hexapdf/content/test_operator.rb +8 -3
- data/test/hexapdf/content/test_processor.rb +1 -1
- data/test/hexapdf/document/test_signatures.rb +225 -0
- data/test/hexapdf/encryption/test_standard_security_handler.rb +8 -6
- data/test/hexapdf/layout/test_style.rb +11 -0
- data/test/hexapdf/test_document.rb +28 -0
- data/test/hexapdf/test_object.rb +7 -2
- data/test/hexapdf/test_parser.rb +14 -0
- data/test/hexapdf/test_rectangle.rb +0 -7
- data/test/hexapdf/test_revision.rb +44 -14
- data/test/hexapdf/test_writer.rb +44 -14
- 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 +76 -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.rb +4 -0
- data/test/hexapdf/type/test_font_simple.rb +5 -5
- data/test/hexapdf/type/test_font_type3.rb +16 -1
- data/test/hexapdf/type/test_object_stream.rb +9 -0
- data/test/hexapdf/type/test_signature.rb +131 -0
- metadata +21 -33
- data/test/data/cert/create.sh +0 -171
- data/test/data/cert/root-ca/certs/84E66B6F4C359E741C0AFA014790DF39.pem +0 -119
- data/test/data/cert/root-ca/certs/84E66B6F4C359E741C0AFA014790DF3A.pem +0 -125
- data/test/data/cert/root-ca/db/crlnumber +0 -1
- data/test/data/cert/root-ca/db/index +0 -2
- data/test/data/cert/root-ca/db/index.attr +0 -1
- data/test/data/cert/root-ca/db/index.attr.old +0 -1
- data/test/data/cert/root-ca/db/index.old +0 -1
- data/test/data/cert/root-ca/db/serial +0 -1
- data/test/data/cert/root-ca/db/serial.old +0 -1
- data/test/data/cert/root-ca/private/root-ca.key +0 -52
- data/test/data/cert/root-ca/root-ca.conf +0 -65
- data/test/data/cert/root-ca/root-ca.crt +0 -119
- data/test/data/cert/root-ca/root-ca.csr +0 -28
- data/test/data/cert/signature-1-pkcs7-detached.pdf +0 -182
- data/test/data/cert/sub-ca/certs/453FF080E3EDCD6A388D5368DFC320D9.pem +0 -125
- data/test/data/cert/sub-ca/db/crlnumber +0 -1
- data/test/data/cert/sub-ca/db/index +0 -1
- data/test/data/cert/sub-ca/db/index.attr +0 -1
- data/test/data/cert/sub-ca/db/index.old +0 -0
- data/test/data/cert/sub-ca/db/serial +0 -1
- data/test/data/cert/sub-ca/db/serial.old +0 -1
- data/test/data/cert/sub-ca/private/signing.key +0 -52
- data/test/data/cert/sub-ca/private/sub-ca.key +0 -52
- data/test/data/cert/sub-ca/signing.crt +0 -125
- data/test/data/cert/sub-ca/signing.csr +0 -28
- data/test/data/cert/sub-ca/signing.p12 +0 -0
- data/test/data/cert/sub-ca/sub-ca.conf +0 -65
- data/test/data/cert/sub-ca/sub-ca.crt +0 -125
- 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
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
|
|
@@ -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
|
|
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
|
-
|
|
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: [
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
|
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:
|
|
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(/
|
|
237
|
+
assert_match(/Invalid password/i, exp.message)
|
|
237
238
|
end
|
|
238
239
|
|
|
239
|
-
it "
|
|
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:
|
|
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
|