hexapdf 0.19.1 → 0.19.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 +10 -0
- data/lib/hexapdf/encryption/standard_security_handler.rb +1 -2
- data/lib/hexapdf/parser.rb +7 -0
- data/lib/hexapdf/version.rb +1 -1
- data/lib/hexapdf/writer.rb +8 -2
- data/test/hexapdf/encryption/test_standard_security_handler.rb +8 -6
- data/test/hexapdf/test_parser.rb +2 -0
- data/test/hexapdf/test_writer.rb +42 -13
- metadata +2 -4
- data/lib/hexapdf/document/signatures.rb +0 -221
- data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +0 -125
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cb1a2ebe9b89b1c97bcfe6a9227bbdc9b8627458900347ac0965784f4d95f83f
|
4
|
+
data.tar.gz: 294eeaa8e9d9926debf2d28d83c8cd419f5685a88b9d23c378fa783fce2d62cb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e2284d3397a856d895b8ec2c83b3ead95ba11a571df90f9fdaed7cd83e4880004ea04150df9205a534d203ae5a1958977a7755ad9f06d622c4f38ae43c0823ab
|
7
|
+
data.tar.gz: 2d6b454f78f7b319240fc062429c32f87ed350ab64a596e7b5050abe65b4e8b4bd9c3551cb87a699219fbd484b5c7670c9662674d9aa5dc8ef800dbe70a273c2
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,13 @@
|
|
1
|
+
## 0.19.2 - 2021-12-14
|
2
|
+
|
3
|
+
### Fixed
|
4
|
+
|
5
|
+
* Set the trailer's ID field to an array of two empty strings when decrypting in
|
6
|
+
case it is missing
|
7
|
+
* Incremental writing when one of the existing revisions contains a
|
8
|
+
cross-reference stream
|
9
|
+
|
10
|
+
|
1
11
|
## 0.19.1 - 2021-12-12
|
2
12
|
|
3
13
|
### Added
|
@@ -328,8 +328,7 @@ module HexaPDF
|
|
328
328
|
raise(HexaPDF::UnsupportedEncryptionError,
|
329
329
|
"Invalid /R value for standard security handler")
|
330
330
|
elsif dict[:R] <= 4 && !document.trailer[:ID].kind_of?(PDFArray)
|
331
|
-
|
332
|
-
"Document ID for needed for decryption")
|
331
|
+
document.trailer[:ID] = ['', '']
|
333
332
|
end
|
334
333
|
@trailer_id_hash = trailer_id_hash
|
335
334
|
|
data/lib/hexapdf/parser.rb
CHANGED
@@ -62,9 +62,15 @@ module HexaPDF
|
|
62
62
|
@object_stream_data = {}
|
63
63
|
@reconstructed_revision = nil
|
64
64
|
@in_reconstruct_revision = false
|
65
|
+
@contains_xref_streams = false
|
65
66
|
retrieve_pdf_header_offset_and_version
|
66
67
|
end
|
67
68
|
|
69
|
+
# Returns +true+ if the PDF file contains cross-reference streams.
|
70
|
+
def contains_xref_streams?
|
71
|
+
@contains_xref_streams
|
72
|
+
end
|
73
|
+
|
68
74
|
# Loads the indirect (potentially compressed) object specified by the given cross-reference
|
69
75
|
# entry.
|
70
76
|
#
|
@@ -230,6 +236,7 @@ module HexaPDF
|
|
230
236
|
maybe_raise("Cross-reference stream doesn't contain entry for itself", pos: pos)
|
231
237
|
xref_section.add_in_use_entry(obj.oid, obj.gen, pos)
|
232
238
|
end
|
239
|
+
@contains_xref_streams = true
|
233
240
|
end
|
234
241
|
xref_section.delete(0)
|
235
242
|
[xref_section, trailer]
|
data/lib/hexapdf/version.rb
CHANGED
data/lib/hexapdf/writer.rb
CHANGED
@@ -66,6 +66,8 @@ module HexaPDF
|
|
66
66
|
@serializer = Serializer.new
|
67
67
|
@serializer.encrypter = @document.encrypted? ? @document.security_handler : nil
|
68
68
|
@rev_size = 0
|
69
|
+
|
70
|
+
@use_xref_streams = false
|
69
71
|
end
|
70
72
|
|
71
73
|
# Writes the document to the IO object.
|
@@ -87,6 +89,7 @@ module HexaPDF
|
|
87
89
|
IO.copy_stream(@document.revisions.parser.io, @io)
|
88
90
|
|
89
91
|
@rev_size = @document.revisions.current.next_free_oid
|
92
|
+
@use_xref_streams = @document.revisions.parser.contains_xref_streams?
|
90
93
|
|
91
94
|
revision = Revision.new(@document.revisions.current.trailer)
|
92
95
|
@document.revisions.each do |rev|
|
@@ -170,10 +173,13 @@ module HexaPDF
|
|
170
173
|
end
|
171
174
|
end
|
172
175
|
|
173
|
-
if !object_streams.empty? && xref_stream.nil?
|
174
|
-
|
176
|
+
if (!object_streams.empty? || @use_xref_streams) && xref_stream.nil?
|
177
|
+
xref_stream = @document.wrap({Type: :XRef}, oid: rev.next_free_oid)
|
178
|
+
rev.add(xref_stream)
|
175
179
|
end
|
176
180
|
|
181
|
+
@use_xref_streams = true if xref_stream
|
182
|
+
|
177
183
|
[xref_stream, object_streams]
|
178
184
|
end
|
179
185
|
|
@@ -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
|
data/test/hexapdf/test_parser.rb
CHANGED
@@ -531,12 +531,14 @@ describe HexaPDF::Parser do
|
|
531
531
|
xref_section, trailer = @parser.load_revision(@parser.startxref_offset)
|
532
532
|
assert_equal({Test: 'now'}, trailer)
|
533
533
|
assert(xref_section[1].in_use?)
|
534
|
+
refute(@parser.contains_xref_streams?)
|
534
535
|
end
|
535
536
|
|
536
537
|
it "works for a cross-reference stream" do
|
537
538
|
xref_section, trailer = @parser.load_revision(212)
|
538
539
|
assert_equal({Size: 2}, trailer)
|
539
540
|
assert(xref_section[1].in_use?)
|
541
|
+
assert(@parser.contains_xref_streams?)
|
540
542
|
end
|
541
543
|
|
542
544
|
it "fails if another object is found instead of a cross-reference stream" do
|
data/test/hexapdf/test_writer.rb
CHANGED
@@ -40,7 +40,7 @@ describe HexaPDF::Writer do
|
|
40
40
|
219
|
41
41
|
%%EOF
|
42
42
|
3 0 obj
|
43
|
-
<</Producer(HexaPDF version 0.19.
|
43
|
+
<</Producer(HexaPDF version 0.19.2)>>
|
44
44
|
endobj
|
45
45
|
xref
|
46
46
|
3 1
|
@@ -72,7 +72,7 @@ describe HexaPDF::Writer do
|
|
72
72
|
141
|
73
73
|
%%EOF
|
74
74
|
6 0 obj
|
75
|
-
<</Producer(HexaPDF version 0.19.
|
75
|
+
<</Producer(HexaPDF version 0.19.2)>>
|
76
76
|
endobj
|
77
77
|
2 0 obj
|
78
78
|
<</Length 10>>stream
|
@@ -103,21 +103,50 @@ describe HexaPDF::Writer do
|
|
103
103
|
assert_document_conversion(@compressed_input_io)
|
104
104
|
end
|
105
105
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
106
|
+
describe "write_incremental" do
|
107
|
+
it "writes a document in incremental mode" do
|
108
|
+
doc = HexaPDF::Document.new(io: @std_input_io)
|
109
|
+
doc.pages.add
|
110
|
+
output_io = StringIO.new
|
111
|
+
HexaPDF::Writer.write(doc, output_io, incremental: true)
|
112
|
+
assert_equal(output_io.string[0, @std_input_io.string.length], @std_input_io.string)
|
113
|
+
doc = HexaPDF::Document.new(io: output_io)
|
114
|
+
assert_equal(4, doc.revisions.size)
|
115
|
+
assert_equal(2, doc.revisions.current.each.to_a.size)
|
116
|
+
end
|
117
|
+
|
118
|
+
it "uses an xref stream if the document already contains at least one" do
|
119
|
+
doc = HexaPDF::Document.new(io: @compressed_input_io)
|
120
|
+
doc.pages.add
|
121
|
+
output_io = StringIO.new
|
122
|
+
HexaPDF::Writer.write(doc, output_io, incremental: true)
|
123
|
+
refute_match(/^trailer/, output_io.string)
|
124
|
+
end
|
115
125
|
end
|
116
126
|
|
117
|
-
it "
|
127
|
+
it "creates an xref stream if no xref stream is in a revision but object streams are" do
|
118
128
|
document = HexaPDF::Document.new
|
119
129
|
document.add({Type: :ObjStm})
|
120
|
-
|
130
|
+
HexaPDF::Writer.new(document, StringIO.new).write
|
131
|
+
assert(:XRef, document.object(2).type)
|
132
|
+
end
|
133
|
+
|
134
|
+
it "creates an xref stream if a previous revision had one" do
|
135
|
+
document = HexaPDF::Document.new
|
136
|
+
document.pages.add
|
137
|
+
document.revisions.add
|
138
|
+
document.pages.add
|
139
|
+
document.add({Type: :ObjStm})
|
140
|
+
document.revisions.add
|
141
|
+
document.pages.add
|
142
|
+
io = StringIO.new
|
143
|
+
HexaPDF::Writer.new(document, io).write
|
144
|
+
|
145
|
+
document = HexaPDF::Document.new(io: io)
|
146
|
+
assert_equal(3, document.revisions.count)
|
147
|
+
assert(document.revisions[0].none? {|obj| obj.type == :XRef })
|
148
|
+
assert(document.revisions[1].one? {|obj| obj.type == :XRef })
|
149
|
+
assert(document.revisions[2].one? {|obj| obj.type == :XRef })
|
121
150
|
end
|
122
151
|
|
123
152
|
it "raises an error if the class is misused and an xref section contains invalid entries" do
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hexapdf
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.19.
|
4
|
+
version: 0.19.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Thomas Leitner
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-12-
|
11
|
+
date: 2021-12-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: cmdparse
|
@@ -254,7 +254,6 @@ files:
|
|
254
254
|
- lib/hexapdf/document/fonts.rb
|
255
255
|
- lib/hexapdf/document/images.rb
|
256
256
|
- lib/hexapdf/document/pages.rb
|
257
|
-
- lib/hexapdf/document/signatures.rb
|
258
257
|
- lib/hexapdf/encryption.rb
|
259
258
|
- lib/hexapdf/encryption/aes.rb
|
260
259
|
- lib/hexapdf/encryption/arc4.rb
|
@@ -397,7 +396,6 @@ files:
|
|
397
396
|
- lib/hexapdf/type/page.rb
|
398
397
|
- lib/hexapdf/type/page_tree_node.rb
|
399
398
|
- lib/hexapdf/type/resources.rb
|
400
|
-
- lib/hexapdf/type/signature/adbe_pkcs7_detached.rb
|
401
399
|
- lib/hexapdf/type/trailer.rb
|
402
400
|
- lib/hexapdf/type/viewer_preferences.rb
|
403
401
|
- lib/hexapdf/type/xref_stream.rb
|
@@ -1,221 +0,0 @@
|
|
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
|
-
|
39
|
-
module HexaPDF
|
40
|
-
class Document
|
41
|
-
|
42
|
-
# This class provides methods for interacting with digital signatures of a PDF file.
|
43
|
-
class Signatures
|
44
|
-
|
45
|
-
# This is the default signing handler which provides the ability to sign a document with a
|
46
|
-
# provided certificate using the adb.pkcs7.detached algorithm.
|
47
|
-
class DefaultHandler
|
48
|
-
|
49
|
-
# Creates a new DefaultHandler with the given signing certificate, the associated signing
|
50
|
-
# key and an optional array of certificates that should also be present in the signature.
|
51
|
-
def initialize(certificate, key, certificate_chain = [])
|
52
|
-
@certificate = certificate
|
53
|
-
@key = key
|
54
|
-
@certificate_chain = certificate_chain
|
55
|
-
end
|
56
|
-
|
57
|
-
# Returns the name to be set on the /Filter key when using this signing handler.
|
58
|
-
def filter_name
|
59
|
-
:"Adobe.PPKLite"
|
60
|
-
end
|
61
|
-
|
62
|
-
# Returns the name to be set on the /SubFilter key when using this signing handler.
|
63
|
-
def sub_filter_name
|
64
|
-
:"adbe.pkcs7.detached"
|
65
|
-
end
|
66
|
-
|
67
|
-
# Returns the size of the signature that would be created.
|
68
|
-
def signature_size
|
69
|
-
sign("").size
|
70
|
-
end
|
71
|
-
|
72
|
-
# Returns the DER serialized OpenSSL::PKCS7 structure containing the signature for the given
|
73
|
-
# data.
|
74
|
-
def sign(data)
|
75
|
-
OpenSSL::PKCS7.sign(@certificate, @key, data, @certificate_chain,
|
76
|
-
OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY).to_der
|
77
|
-
end
|
78
|
-
|
79
|
-
end
|
80
|
-
|
81
|
-
include Enumerable
|
82
|
-
|
83
|
-
# Creates a new Signatures object for the given PDF document.
|
84
|
-
def initialize(document)
|
85
|
-
@document = document
|
86
|
-
end
|
87
|
-
|
88
|
-
# Adds a signature to the document and returns the corresponding signature object.
|
89
|
-
#
|
90
|
-
# This method will add a new signature to the document and write the updated document to the
|
91
|
-
# given file or IO stream. Afterwards the document can't be modified anymore and still retain
|
92
|
-
# a correct digital signature; create a new document based on the file or IO stream instead.
|
93
|
-
#
|
94
|
-
# +signature+::
|
95
|
-
# Can either be a signature object or +nil+. Providing a signature object provides for
|
96
|
-
# more control, e.g.:
|
97
|
-
#
|
98
|
-
# * Setting values for optional fields like /Reason and /Location.
|
99
|
-
# * Indirectly specifying which signature field should be used.
|
100
|
-
#
|
101
|
-
# If the +signature+ is not associated with an AcroForm signature field, a new signature
|
102
|
-
# field is created and added to the main AcroForm object, creating that if necessary.
|
103
|
-
#
|
104
|
-
# If the associated signature field doesn't have a widget, a non-visible one is created on
|
105
|
-
# the first page.
|
106
|
-
#
|
107
|
-
# +handler+::
|
108
|
-
# The signature handler that provides the necessary methods for signing, see
|
109
|
-
# DefaultHandler.
|
110
|
-
#
|
111
|
-
# +write_options+::
|
112
|
-
# These options will be passed on to the HexaPDF::Document#write command. Note that
|
113
|
-
# +incremental+ will be automatically set if signing an already existing file.
|
114
|
-
def add(file_or_io, handler, signature: nil, **write_options)
|
115
|
-
signature ||= @document.add({Type: :Sig})
|
116
|
-
signature[:Filter] = handler.filter_name
|
117
|
-
signature[:SubFilter] = handler.sub_filter_name
|
118
|
-
signature[:ByteRange] = [0, 1_000_000_000_000, 1_000_000_000_000, 1_000_000_000_000]
|
119
|
-
signature[:Contents] = '00' * handler.signature_size # twice the size due to hex encoding
|
120
|
-
|
121
|
-
# Prepare signature field
|
122
|
-
form = @document.acro_form(create: true)
|
123
|
-
form.signature_flag(:signatures_exist)
|
124
|
-
|
125
|
-
signature_field = each.find {|value| value == signature }
|
126
|
-
unless signature_field
|
127
|
-
signature_field = form.create_signature_field(generate_field_name)
|
128
|
-
signature_field.field_value = signature
|
129
|
-
end
|
130
|
-
|
131
|
-
if signature_field.each_widget.to_a.empty?
|
132
|
-
signature_field.create_widget(@document.pages[0], Rect: [0, 0, 0, 0])
|
133
|
-
end
|
134
|
-
|
135
|
-
io = if file_or_io.kind_of?(String)
|
136
|
-
File.open(file_or_io, 'w+')
|
137
|
-
else
|
138
|
-
file_or_io
|
139
|
-
end
|
140
|
-
|
141
|
-
# Save the current state so that we can determine the correct /ByteRange value and set the
|
142
|
-
# values
|
143
|
-
section = @document.write(io, incremental: true, **write_options)
|
144
|
-
data = section.map {|oid, _gen, entry| [entry.pos.to_i, oid] }.sort
|
145
|
-
index = data.index {|_pos, oid| oid == signature.oid }
|
146
|
-
signature_offset = data[index][0]
|
147
|
-
signature_length = data[index + 1][0] - data[index][0]
|
148
|
-
io.pos = signature_offset
|
149
|
-
signature_data = io.read(signature_length)
|
150
|
-
|
151
|
-
io.rewind
|
152
|
-
file_data = io.read
|
153
|
-
|
154
|
-
# Calculate the offsets for the /ByteRange
|
155
|
-
contents_offset = signature_offset + signature_data.index('Contents(') + 8
|
156
|
-
offset2 = contents_offset + signature[:Contents].size + 2 # +2 because of the needed < and >
|
157
|
-
length2 = file_data.size - offset2
|
158
|
-
signature[:ByteRange] = [0, contents_offset, offset2, length2]
|
159
|
-
|
160
|
-
# Set the correct /ByteRange value
|
161
|
-
signature_data.sub!(/ByteRange\[0 1000000000000 1000000000000 1000000000000\]/) do |match|
|
162
|
-
length = match.size
|
163
|
-
result = "ByteRange[0 #{contents_offset} #{offset2} #{length2}]"
|
164
|
-
result.ljust(length)
|
165
|
-
end
|
166
|
-
|
167
|
-
# Now everything besides the /Contents value is correct, so we can read the contents for
|
168
|
-
# signing
|
169
|
-
file_data[signature_offset, signature_length] = signature_data
|
170
|
-
signed_contents = file_data[0, contents_offset] << file_data[offset2, length2]
|
171
|
-
signature[:Contents] = handler.sign(signed_contents)
|
172
|
-
|
173
|
-
# Set the correct /Contents value as hexstring
|
174
|
-
signature_data.sub!(/Contents\(0+\)/) do |match|
|
175
|
-
length = match.size
|
176
|
-
result = "Contents<#{signature[:Contents].unpack1('H*')}"
|
177
|
-
"#{result.ljust(length - 1, '0')}>"
|
178
|
-
end
|
179
|
-
|
180
|
-
io.pos = signature_offset
|
181
|
-
io.write(signature_data)
|
182
|
-
|
183
|
-
signature
|
184
|
-
ensure
|
185
|
-
io.close if io && io != file_or_io
|
186
|
-
end
|
187
|
-
|
188
|
-
# :call-seq:
|
189
|
-
# signatures.each {|signature| block } -> signatures
|
190
|
-
# signatures.each -> Enumerator
|
191
|
-
#
|
192
|
-
# Iterates over all signatures in the order they are found.
|
193
|
-
def each
|
194
|
-
return to_enum(__method__) unless block_given?
|
195
|
-
|
196
|
-
return [] unless (form = @document.acro_form)
|
197
|
-
form.each_field do |field|
|
198
|
-
yield(field.field_value) if field.field_type == :Sig && field.field_value
|
199
|
-
end
|
200
|
-
end
|
201
|
-
|
202
|
-
# Returns the number of signatures in the PDF document. May be zero if the document has no
|
203
|
-
# signatures.
|
204
|
-
def count
|
205
|
-
each.to_a.size
|
206
|
-
end
|
207
|
-
|
208
|
-
private
|
209
|
-
|
210
|
-
# Generates a field name for a signature field.
|
211
|
-
def generate_field_name
|
212
|
-
index = (@document.acro_form.each_field.
|
213
|
-
map {|field| field.full_field_name.scan(/\ASignature(\d+)/).first&.first.to_i }.
|
214
|
-
max || 0) + 1
|
215
|
-
"Signature#{index}"
|
216
|
-
end
|
217
|
-
|
218
|
-
end
|
219
|
-
|
220
|
-
end
|
221
|
-
end
|
@@ -1,125 +0,0 @@
|
|
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/type/signature'
|
39
|
-
|
40
|
-
module HexaPDF
|
41
|
-
module Type
|
42
|
-
class Signature
|
43
|
-
|
44
|
-
# The signature handler for the adbe.pkcs7.detached sub-filter.
|
45
|
-
class AdbePkcs7Detached < Handler
|
46
|
-
|
47
|
-
# Creates a new signature handler for the given signature dictionary.
|
48
|
-
def initialize(signature_dict)
|
49
|
-
super
|
50
|
-
@pkcs7 = OpenSSL::PKCS7.new(signature_dict.contents)
|
51
|
-
end
|
52
|
-
|
53
|
-
# Returns the common name of the signer.
|
54
|
-
def signer_name
|
55
|
-
signer_certificate.subject.to_a.assoc("CN")&.[](1) || super
|
56
|
-
end
|
57
|
-
|
58
|
-
# Returns the time of signing.
|
59
|
-
def signing_time
|
60
|
-
signer_info.signed_time rescue super
|
61
|
-
end
|
62
|
-
|
63
|
-
# Returns the certificate chain.
|
64
|
-
def certificate_chain
|
65
|
-
@pkcs7.certificates
|
66
|
-
end
|
67
|
-
|
68
|
-
# Returns the signer certificate (an instance of OpenSSL::X509::Certificate).
|
69
|
-
def signer_certificate
|
70
|
-
info = signer_info
|
71
|
-
certificate_chain.find {|cert| cert.issuer == info.issuer && cert.serial == info.serial }
|
72
|
-
end
|
73
|
-
|
74
|
-
# Returns the signer information object (an instance of OpenSSL::PKCS7::SignerInfo).
|
75
|
-
def signer_info
|
76
|
-
@pkcs7.signers.first
|
77
|
-
end
|
78
|
-
|
79
|
-
# Verifies the signature using the provided OpenSSL::X509::Store object.
|
80
|
-
def verify(store, allow_self_signed: false)
|
81
|
-
result = VerificationResult.new
|
82
|
-
|
83
|
-
signer_info = self.signer_info
|
84
|
-
signer_certificate = self.signer_certificate
|
85
|
-
certificate_chain = self.certificate_chain
|
86
|
-
|
87
|
-
if certificate_chain.empty?
|
88
|
-
result.log(:error, "No certificates found in signature")
|
89
|
-
return result
|
90
|
-
end
|
91
|
-
|
92
|
-
if @pkcs7.signers.size != 1
|
93
|
-
result.log(:error, "Exactly one signer needed, found #{@pkcs7.signers.size}")
|
94
|
-
end
|
95
|
-
|
96
|
-
unless signer_certificate
|
97
|
-
result.log(:error, "Signer serial=#{signer_info.serial} issuer=#{signer_info.issuer} " \
|
98
|
-
"not found in certificates stored in PKCS7 object")
|
99
|
-
return result
|
100
|
-
end
|
101
|
-
|
102
|
-
key_usage = signer_certificate.extensions.find {|ext| ext.oid == 'keyUsage' }
|
103
|
-
unless key_usage && key_usage.value.split(', ').include?("Digital Signature")
|
104
|
-
result.log(:error, "Certificate key usage is missing 'Digital Signature'")
|
105
|
-
end
|
106
|
-
|
107
|
-
verify_signing_time(result)
|
108
|
-
|
109
|
-
store.verify_callback = store_verification_callback(result,
|
110
|
-
allow_self_signed: allow_self_signed)
|
111
|
-
if @pkcs7.verify(certificate_chain, store, signature_dict.signed_data,
|
112
|
-
OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY)
|
113
|
-
result.log(:info, "Signature valid")
|
114
|
-
else
|
115
|
-
result.log(:error, "Signature verification failed")
|
116
|
-
end
|
117
|
-
|
118
|
-
result
|
119
|
-
end
|
120
|
-
|
121
|
-
end
|
122
|
-
|
123
|
-
end
|
124
|
-
end
|
125
|
-
end
|