hexapdf 0.28.0 → 0.31.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 +86 -10
- data/examples/024-digital-signatures.rb +23 -0
- data/lib/hexapdf/cli/command.rb +16 -1
- data/lib/hexapdf/cli/info.rb +9 -1
- data/lib/hexapdf/cli/inspect.rb +2 -2
- data/lib/hexapdf/composer.rb +76 -28
- data/lib/hexapdf/configuration.rb +29 -16
- data/lib/hexapdf/dictionary_fields.rb +13 -4
- data/lib/hexapdf/digital_signature/cms_handler.rb +137 -0
- data/lib/hexapdf/digital_signature/handler.rb +138 -0
- data/lib/hexapdf/digital_signature/pkcs1_handler.rb +96 -0
- data/lib/hexapdf/{type → digital_signature}/signature.rb +3 -8
- data/lib/hexapdf/digital_signature/signatures.rb +210 -0
- data/lib/hexapdf/digital_signature/signing/default_handler.rb +317 -0
- data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +308 -0
- data/lib/hexapdf/digital_signature/signing/timestamp_handler.rb +148 -0
- data/lib/hexapdf/digital_signature/signing.rb +101 -0
- data/lib/hexapdf/{type/signature → digital_signature}/verification_result.rb +37 -41
- data/lib/hexapdf/digital_signature.rb +56 -0
- data/lib/hexapdf/document/pages.rb +31 -18
- data/lib/hexapdf/document.rb +29 -15
- data/lib/hexapdf/encryption/standard_security_handler.rb +4 -3
- data/lib/hexapdf/filter/flate_decode.rb +20 -8
- data/lib/hexapdf/layout/page_style.rb +144 -0
- data/lib/hexapdf/layout.rb +1 -0
- data/lib/hexapdf/task/optimize.rb +8 -6
- data/lib/hexapdf/type/font_simple.rb +14 -2
- data/lib/hexapdf/type/object_stream.rb +7 -2
- data/lib/hexapdf/type/outline.rb +1 -1
- data/lib/hexapdf/type/outline_item.rb +1 -1
- data/lib/hexapdf/type/page.rb +29 -8
- data/lib/hexapdf/type/xref_stream.rb +11 -4
- data/lib/hexapdf/type.rb +0 -1
- data/lib/hexapdf/version.rb +1 -1
- data/lib/hexapdf/writer.rb +1 -1
- data/test/hexapdf/{type/signature → digital_signature}/common.rb +31 -3
- data/test/hexapdf/digital_signature/signing/test_default_handler.rb +162 -0
- data/test/hexapdf/digital_signature/signing/test_signed_data_creator.rb +225 -0
- data/test/hexapdf/digital_signature/signing/test_timestamp_handler.rb +88 -0
- data/test/hexapdf/{type/signature/test_adbe_pkcs7_detached.rb → digital_signature/test_cms_handler.rb} +7 -7
- data/test/hexapdf/{type/signature → digital_signature}/test_handler.rb +4 -4
- data/test/hexapdf/{type/signature/test_adbe_x509_rsa_sha1.rb → digital_signature/test_pkcs1_handler.rb} +3 -3
- data/test/hexapdf/{type → digital_signature}/test_signature.rb +7 -7
- data/test/hexapdf/digital_signature/test_signatures.rb +137 -0
- data/test/hexapdf/digital_signature/test_signing.rb +53 -0
- data/test/hexapdf/{type/signature → digital_signature}/test_verification_result.rb +7 -7
- data/test/hexapdf/document/test_pages.rb +25 -0
- data/test/hexapdf/encryption/test_standard_security_handler.rb +2 -2
- data/test/hexapdf/filter/test_flate_decode.rb +19 -5
- data/test/hexapdf/layout/test_page_style.rb +70 -0
- data/test/hexapdf/task/test_optimize.rb +11 -9
- data/test/hexapdf/test_composer.rb +35 -10
- data/test/hexapdf/test_dictionary_fields.rb +9 -3
- data/test/hexapdf/test_document.rb +1 -1
- data/test/hexapdf/test_writer.rb +8 -8
- data/test/hexapdf/type/test_font_simple.rb +18 -6
- data/test/hexapdf/type/test_object_stream.rb +16 -7
- data/test/hexapdf/type/test_outline.rb +3 -1
- data/test/hexapdf/type/test_outline_item.rb +3 -1
- data/test/hexapdf/type/test_page.rb +42 -11
- data/test/hexapdf/type/test_xref_stream.rb +6 -1
- metadata +27 -15
- data/lib/hexapdf/document/signatures.rb +0 -546
- data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +0 -135
- data/lib/hexapdf/type/signature/adbe_x509_rsa_sha1.rb +0 -95
- data/lib/hexapdf/type/signature/handler.rb +0 -140
- data/test/hexapdf/document/test_signatures.rb +0 -352
|
@@ -0,0 +1,144 @@
|
|
|
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-2023 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 'hexapdf/error'
|
|
38
|
+
require 'hexapdf/layout/style'
|
|
39
|
+
require 'hexapdf/layout/frame'
|
|
40
|
+
|
|
41
|
+
module HexaPDF
|
|
42
|
+
module Layout
|
|
43
|
+
|
|
44
|
+
# A PageStyle defines the initial look of a page and the placement of one or more frames.
|
|
45
|
+
class PageStyle
|
|
46
|
+
|
|
47
|
+
# The page size.
|
|
48
|
+
#
|
|
49
|
+
# Can be any valid predefined page size (see HexaPDF::Type::Page::PAPER_SIZE) or an array
|
|
50
|
+
# [llx, lly, urx, ury] specifying a custom page size.
|
|
51
|
+
#
|
|
52
|
+
# Example:
|
|
53
|
+
#
|
|
54
|
+
# style.page_size = :A4
|
|
55
|
+
# style.page_size = [0, 0, 200, 200]
|
|
56
|
+
attr_accessor :page_size
|
|
57
|
+
|
|
58
|
+
# The page orientation, either +:portrait+ or +:landscape+.
|
|
59
|
+
#
|
|
60
|
+
# Only used if #page_size is one of the predefined page sizes and not an array.
|
|
61
|
+
attr_accessor :orientation
|
|
62
|
+
|
|
63
|
+
# A callable object that defines the initial content of a page created with #create_page.
|
|
64
|
+
#
|
|
65
|
+
# The callable object is given a canvas and the page style as arguments. It needs to draw the
|
|
66
|
+
# initial content of the page. Note that the graphics state of the canvas is *not* saved
|
|
67
|
+
# before executing the template code and restored afterwards. If this is needed, the object
|
|
68
|
+
# needs to do it itself.
|
|
69
|
+
#
|
|
70
|
+
# Furthermore it should set the #frame and #next_style attributes appropriately, if not done
|
|
71
|
+
# beforehand. The #create_frame method can be used for easily creating a rectangular frame.
|
|
72
|
+
#
|
|
73
|
+
# Example:
|
|
74
|
+
#
|
|
75
|
+
# page_style.template = lambda do |canvas, style
|
|
76
|
+
# box = canvas.context.box
|
|
77
|
+
# canvas.fill_color("fd0") do
|
|
78
|
+
# canvas.rectangle(0, 0, box.width, box.height).fill
|
|
79
|
+
# end
|
|
80
|
+
# style.frame = style.create_frame(canvas.context, 72)
|
|
81
|
+
# end
|
|
82
|
+
attr_accessor :template
|
|
83
|
+
|
|
84
|
+
# The HexaPDF::Layout::Frame object that defines the area on the page where content should be
|
|
85
|
+
# placed.
|
|
86
|
+
#
|
|
87
|
+
# This can either be set beforehand or during execution of the #template.
|
|
88
|
+
#
|
|
89
|
+
# If no frame has been set, a frame covering the page except for a default margin on all sides
|
|
90
|
+
# is set during #create_page.
|
|
91
|
+
attr_accessor :frame
|
|
92
|
+
|
|
93
|
+
# Defines the name of the page style that should be used for the next page.
|
|
94
|
+
#
|
|
95
|
+
# If this attribute is +nil+ (the default), it means that this style should be used again.
|
|
96
|
+
attr_accessor :next_style
|
|
97
|
+
|
|
98
|
+
# Creates a new page style instance for the given page size and orientation. If a block is
|
|
99
|
+
# given, it is used as template for defining the initial content.
|
|
100
|
+
#
|
|
101
|
+
# Example:
|
|
102
|
+
#
|
|
103
|
+
# PageStyle.new(page_size: :Letter) do |canvas, style|
|
|
104
|
+
# style.frame = style.create_frame(canvas.context, 72)
|
|
105
|
+
# style.next_style = :other
|
|
106
|
+
# canvas.fill_color("fd0") { canvas.circle(100, 100, 50).fill }
|
|
107
|
+
# end
|
|
108
|
+
def initialize(page_size: :A4, orientation: :portrait, &block)
|
|
109
|
+
@page_size = page_size
|
|
110
|
+
@orientation = orientation
|
|
111
|
+
@template = block
|
|
112
|
+
@frame = nil
|
|
113
|
+
@next_style = nil
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Creates a new page in the given document with this page style and returns it.
|
|
117
|
+
#
|
|
118
|
+
# If #frame has not been set beforehand or during execution of the #template, a default frame
|
|
119
|
+
# covering the whole page except a margin of 36 is created.
|
|
120
|
+
def create_page(document)
|
|
121
|
+
page = document.pages.create(media_box: page_size, orientation: orientation)
|
|
122
|
+
template&.call(page.canvas, self)
|
|
123
|
+
self.frame ||= create_frame(page, 36)
|
|
124
|
+
page
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Creates a frame based on the given page's box and margin.
|
|
128
|
+
#
|
|
129
|
+
# The +margin+ can be any value allowed by HexaPDF::Layout::Style::Quad#set.
|
|
130
|
+
#
|
|
131
|
+
# *Note*: This is a helper method for use inside the #template callable.
|
|
132
|
+
def create_frame(page, margin = 36)
|
|
133
|
+
box = page.box
|
|
134
|
+
margin = Layout::Style::Quad.new(margin)
|
|
135
|
+
Layout::Frame.new(box.left + margin.left,
|
|
136
|
+
box.bottom + margin.bottom,
|
|
137
|
+
box.width - margin.left - margin.right,
|
|
138
|
+
box.height - margin.bottom - margin.top)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
end
|
|
144
|
+
end
|
data/lib/hexapdf/layout.rb
CHANGED
|
@@ -38,6 +38,8 @@ require 'set'
|
|
|
38
38
|
require 'hexapdf/serializer'
|
|
39
39
|
require 'hexapdf/content/parser'
|
|
40
40
|
require 'hexapdf/content/operator'
|
|
41
|
+
require 'hexapdf/type/xref_stream'
|
|
42
|
+
require 'hexapdf/type/object_stream'
|
|
41
43
|
|
|
42
44
|
module HexaPDF
|
|
43
45
|
module Task
|
|
@@ -124,7 +126,7 @@ module HexaPDF
|
|
|
124
126
|
if object_streams == :generate
|
|
125
127
|
process_object_streams(doc, :generate, xref_streams)
|
|
126
128
|
elsif xref_streams == :generate
|
|
127
|
-
doc.add({
|
|
129
|
+
doc.add({}, type: Type::XRefStream)
|
|
128
130
|
end
|
|
129
131
|
end
|
|
130
132
|
|
|
@@ -150,14 +152,14 @@ module HexaPDF
|
|
|
150
152
|
end
|
|
151
153
|
objects_to_delete.each {|obj| rev.delete(obj) }
|
|
152
154
|
if xref_streams == :generate && !xref_stream
|
|
153
|
-
rev.add(doc.wrap({
|
|
155
|
+
rev.add(doc.wrap({}, type: Type::XRefStream, oid: doc.revisions.next_oid))
|
|
154
156
|
end
|
|
155
157
|
end
|
|
156
158
|
when :generate
|
|
157
159
|
doc.revisions.each do |rev|
|
|
158
160
|
xref_stream = false
|
|
159
161
|
count = 0
|
|
160
|
-
objstms = [doc.wrap({
|
|
162
|
+
objstms = [doc.wrap({}, type: Type::ObjectStream)]
|
|
161
163
|
old_objstms = []
|
|
162
164
|
rev.each do |obj|
|
|
163
165
|
case obj.type
|
|
@@ -173,7 +175,7 @@ module HexaPDF
|
|
|
173
175
|
objstms[-1].add_object(obj)
|
|
174
176
|
count += 1
|
|
175
177
|
if count == 200
|
|
176
|
-
objstms << doc.wrap({
|
|
178
|
+
objstms << doc.wrap({}, type: Type::ObjectStream)
|
|
177
179
|
count = 0
|
|
178
180
|
end
|
|
179
181
|
end
|
|
@@ -182,7 +184,7 @@ module HexaPDF
|
|
|
182
184
|
objstm.data.oid = doc.revisions.next_oid
|
|
183
185
|
rev.add(objstm)
|
|
184
186
|
end
|
|
185
|
-
rev.add(doc.wrap({
|
|
187
|
+
rev.add(doc.wrap({}, type: Type::XRefStream, oid: doc.revisions.next_oid)) unless xref_stream
|
|
186
188
|
end
|
|
187
189
|
end
|
|
188
190
|
end
|
|
@@ -207,7 +209,7 @@ module HexaPDF
|
|
|
207
209
|
xref_stream = true if obj.type == :XRef
|
|
208
210
|
delete_fields_with_defaults(obj)
|
|
209
211
|
end
|
|
210
|
-
rev.add(doc.wrap({
|
|
212
|
+
rev.add(doc.wrap({}, type: Type::XRefStream, oid: doc.revisions.next_oid)) unless xref_stream
|
|
211
213
|
end
|
|
212
214
|
end
|
|
213
215
|
end
|
|
@@ -171,9 +171,21 @@ module HexaPDF
|
|
|
171
171
|
yield("Required field #{field} is not set", false) if self[field].nil?
|
|
172
172
|
end
|
|
173
173
|
|
|
174
|
+
widths = self[:Widths]
|
|
174
175
|
if key?(:Widths) && key?(:LastChar) && key?(:FirstChar) &&
|
|
175
|
-
|
|
176
|
-
yield("Invalid number of entries in field Widths",
|
|
176
|
+
widths.length != (self[:LastChar] - self[:FirstChar] + 1)
|
|
177
|
+
yield("Invalid number of entries in field Widths", true)
|
|
178
|
+
difference = self[:LastChar] - self[:FirstChar] + 1 - widths.length
|
|
179
|
+
if difference > 0
|
|
180
|
+
missing_value = if widths.count(widths[0]) == widths.length
|
|
181
|
+
widths[0]
|
|
182
|
+
else
|
|
183
|
+
self[:FontDescriptor]&.[](:MissingWidth) || 0
|
|
184
|
+
end
|
|
185
|
+
difference.times { widths << missing_value }
|
|
186
|
+
else
|
|
187
|
+
widths.slice!(difference, -difference)
|
|
188
|
+
end
|
|
177
189
|
end
|
|
178
190
|
end
|
|
179
191
|
|
|
@@ -101,8 +101,8 @@ module HexaPDF
|
|
|
101
101
|
define_type :ObjStm
|
|
102
102
|
|
|
103
103
|
define_field :Type, type: Symbol, required: true, default: type, version: '1.5'
|
|
104
|
-
define_field :N, type: Integer
|
|
105
|
-
define_field :First, type: Integer
|
|
104
|
+
define_field :N, type: Integer, required: true
|
|
105
|
+
define_field :First, type: Integer, required: true
|
|
106
106
|
define_field :Extends, type: Stream
|
|
107
107
|
|
|
108
108
|
# Parses the stream and returns an ObjectStream::Data object that can be used for retrieving
|
|
@@ -230,6 +230,11 @@ module HexaPDF
|
|
|
230
230
|
|
|
231
231
|
# Validates that the generation number of the object stream is zero.
|
|
232
232
|
def perform_validation
|
|
233
|
+
# Assign dummy values so that the validation for required values works since those values
|
|
234
|
+
# are only set on #write_objects
|
|
235
|
+
self[:N] ||= 0
|
|
236
|
+
self[:First] ||= 0
|
|
237
|
+
|
|
233
238
|
super
|
|
234
239
|
yield("Object stream has invalid generation number > 0", false) if gen != 0
|
|
235
240
|
end
|
data/lib/hexapdf/type/outline.rb
CHANGED
|
@@ -126,7 +126,7 @@ module HexaPDF
|
|
|
126
126
|
if (first && !last) || (!first && last)
|
|
127
127
|
yield('Outline dictionary is missing an endpoint reference', true)
|
|
128
128
|
node, dir = first ? [first, :Next] : [last, :Prev]
|
|
129
|
-
node = node[dir] while node
|
|
129
|
+
node = node[dir] while node[dir]
|
|
130
130
|
self[dir == :Next ? :Last : :First] = node
|
|
131
131
|
elsif !first && !last && self[:Count] && self[:Count] != 0
|
|
132
132
|
yield('Outline dictionary key /Count set but no items exist', true)
|
|
@@ -397,7 +397,7 @@ module HexaPDF
|
|
|
397
397
|
if (first && !last) || (!first && last)
|
|
398
398
|
yield('Outline item dictionary is missing an endpoint reference', true)
|
|
399
399
|
node, dir = first ? [first, :Next] : [last, :Prev]
|
|
400
|
-
node = node[dir] while node
|
|
400
|
+
node = node[dir] while node[dir]
|
|
401
401
|
self[dir == :Next ? :Last : :First] = node
|
|
402
402
|
elsif !first && !last && self[:Count] && self[:Count] != 0
|
|
403
403
|
yield('Outline item dictionary key /Count set but no descendants exist', true)
|
data/lib/hexapdf/type/page.rb
CHANGED
|
@@ -104,8 +104,16 @@ module HexaPDF
|
|
|
104
104
|
Executive: [0, 0, 522, 756].freeze,
|
|
105
105
|
}.freeze
|
|
106
106
|
|
|
107
|
-
# Returns the media box for the given paper size
|
|
107
|
+
# Returns the media box for the given paper size or array.
|
|
108
|
+
#
|
|
109
|
+
# If an array is specified, it needs to contain exactly four numbers. The +orientation+
|
|
110
|
+
# argument is not used in this case.
|
|
111
|
+
#
|
|
112
|
+
# See PAPER_SIZE for the defined paper sizes.
|
|
108
113
|
def self.media_box(paper_size, orientation: :portrait)
|
|
114
|
+
return paper_size if paper_size.kind_of?(Array) && paper_size.size == 4 &&
|
|
115
|
+
paper_size.all?(Numeric)
|
|
116
|
+
|
|
109
117
|
unless PAPER_SIZE.key?(paper_size)
|
|
110
118
|
raise HexaPDF::Error, "Invalid paper size specified: #{paper_size}"
|
|
111
119
|
end
|
|
@@ -118,9 +126,6 @@ module HexaPDF
|
|
|
118
126
|
# The inheritable fields.
|
|
119
127
|
INHERITABLE_FIELDS = [:Resources, :MediaBox, :CropBox, :Rotate].freeze
|
|
120
128
|
|
|
121
|
-
# The required inheritable fields.
|
|
122
|
-
REQUIRED_INHERITABLE_FIELDS = [:Resources, :MediaBox].freeze
|
|
123
|
-
|
|
124
129
|
define_type :Page
|
|
125
130
|
|
|
126
131
|
define_field :Type, type: Symbol, required: true, default: type
|
|
@@ -609,10 +614,26 @@ module HexaPDF
|
|
|
609
614
|
return unless parent_node
|
|
610
615
|
|
|
611
616
|
super
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
yield("
|
|
615
|
-
resources.validate(&block)
|
|
617
|
+
|
|
618
|
+
unless self[:Resources]
|
|
619
|
+
yield("Required inheritable page field Resources not set", true)
|
|
620
|
+
resources.validate(&block)
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
unless self[:MediaBox]
|
|
624
|
+
yield("Required inheritable page field MediaBox not set", true)
|
|
625
|
+
index = self.index
|
|
626
|
+
box_before = index == 0 ? nil : document.pages[index - 1][:MediaBox]
|
|
627
|
+
box_after = index == document.pages.count - 1 ? nil : document.pages[index + 1]&.[](:MediaBox)
|
|
628
|
+
self[:MediaBox] =
|
|
629
|
+
if box_before && (box_before&.value == box_after&.value || box_after.nil?)
|
|
630
|
+
box_before.dup
|
|
631
|
+
elsif box_after && box_before.nil?
|
|
632
|
+
box_after
|
|
633
|
+
else
|
|
634
|
+
self.class.media_box(document.config['page.default_media_box'],
|
|
635
|
+
orientation: document.config['page.default_media_orientation'])
|
|
636
|
+
end
|
|
616
637
|
end
|
|
617
638
|
end
|
|
618
639
|
|
|
@@ -72,12 +72,10 @@ module HexaPDF
|
|
|
72
72
|
|
|
73
73
|
define_field :Type, type: Symbol, default: type, required: true, indirect: false,
|
|
74
74
|
version: '1.5'
|
|
75
|
-
|
|
76
|
-
define_field :Size, type: Integer, indirect: false
|
|
75
|
+
define_field :Size, type: Integer, indirect: false, required: true
|
|
77
76
|
define_field :Index, type: PDFArray, indirect: false
|
|
78
77
|
define_field :Prev, type: Integer, indirect: false
|
|
79
|
-
|
|
80
|
-
define_field :W, type: PDFArray, indirect: false
|
|
78
|
+
define_field :W, type: PDFArray, indirect: false, required: true
|
|
81
79
|
|
|
82
80
|
# Returns an XRefSection that represents the content of this cross-reference stream.
|
|
83
81
|
#
|
|
@@ -219,6 +217,15 @@ module HexaPDF
|
|
|
219
217
|
[[1, middle, 2], pack_string]
|
|
220
218
|
end
|
|
221
219
|
|
|
220
|
+
def perform_validation #:nodoc
|
|
221
|
+
# Size is not required because it will be auto-filled before the object is written
|
|
222
|
+
# W is not required because it will be auto-filled on #update_with_xref_section_and_trailer
|
|
223
|
+
# Set both here to dummy values to make validation work for the required values
|
|
224
|
+
self[:Size] ||= 1
|
|
225
|
+
self[:W] ||= [1, 1, 1]
|
|
226
|
+
super
|
|
227
|
+
end
|
|
228
|
+
|
|
222
229
|
end
|
|
223
230
|
|
|
224
231
|
end
|
data/lib/hexapdf/type.rb
CHANGED
|
@@ -72,7 +72,6 @@ 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')
|
|
76
75
|
autoload(:Outline, 'hexapdf/type/outline')
|
|
77
76
|
autoload(:OutlineItem, 'hexapdf/type/outline_item')
|
|
78
77
|
autoload(:PageLabel, 'hexapdf/type/page_label')
|
data/lib/hexapdf/version.rb
CHANGED
data/lib/hexapdf/writer.rb
CHANGED
|
@@ -207,7 +207,7 @@ module HexaPDF
|
|
|
207
207
|
end
|
|
208
208
|
|
|
209
209
|
if (!object_streams.empty? || @use_xref_streams) && xref_stream.nil?
|
|
210
|
-
xref_stream = @document.wrap({
|
|
210
|
+
xref_stream = @document.wrap({}, type: Type::XRefStream, oid: @document.revisions.next_oid)
|
|
211
211
|
rev.add(xref_stream)
|
|
212
212
|
end
|
|
213
213
|
|
|
@@ -6,7 +6,7 @@ module HexaPDF
|
|
|
6
6
|
class Certificates
|
|
7
7
|
|
|
8
8
|
def ca_key
|
|
9
|
-
@ca_key ||= OpenSSL::PKey::RSA.new(
|
|
9
|
+
@ca_key ||= OpenSSL::PKey::RSA.new(2048)
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def ca_certificate
|
|
@@ -36,13 +36,17 @@ module HexaPDF
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def signer_key
|
|
39
|
-
@signer_key ||= OpenSSL::PKey::RSA.new(
|
|
39
|
+
@signer_key ||= OpenSSL::PKey::RSA.new(2048)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def dsa_signer_key
|
|
43
|
+
@dsa_signer_key ||= OpenSSL::PKey::DSA.new(2048)
|
|
40
44
|
end
|
|
41
45
|
|
|
42
46
|
def signer_certificate
|
|
43
47
|
@signer_certificate ||=
|
|
44
48
|
begin
|
|
45
|
-
name = OpenSSL::X509::Name.parse('/CN=signer/DC=gettalong')
|
|
49
|
+
name = OpenSSL::X509::Name.parse('/CN=RSA signer/DC=gettalong')
|
|
46
50
|
|
|
47
51
|
signer_cert = OpenSSL::X509::Certificate.new
|
|
48
52
|
signer_cert.serial = 2
|
|
@@ -65,6 +69,30 @@ module HexaPDF
|
|
|
65
69
|
end
|
|
66
70
|
end
|
|
67
71
|
|
|
72
|
+
def dsa_signer_certificate
|
|
73
|
+
@dsa_signer_certificate ||=
|
|
74
|
+
begin
|
|
75
|
+
signer_cert = OpenSSL::X509::Certificate.new
|
|
76
|
+
signer_cert.serial = 3
|
|
77
|
+
signer_cert.version = 2
|
|
78
|
+
signer_cert.not_before = Time.now - 86400
|
|
79
|
+
signer_cert.not_after = Time.now + 86400
|
|
80
|
+
signer_cert.public_key = dsa_signer_key.public_key
|
|
81
|
+
signer_cert.subject = OpenSSL::X509::Name.parse('/CN=DSA signer/DC=gettalong')
|
|
82
|
+
signer_cert.issuer = ca_certificate.subject
|
|
83
|
+
|
|
84
|
+
extension_factory = OpenSSL::X509::ExtensionFactory.new
|
|
85
|
+
extension_factory.subject_certificate = signer_cert
|
|
86
|
+
extension_factory.issuer_certificate = ca_certificate
|
|
87
|
+
signer_cert.add_extension(extension_factory.create_extension('subjectKeyIdentifier', 'hash'))
|
|
88
|
+
signer_cert.add_extension(extension_factory.create_extension('basicConstraints', 'CA:FALSE'))
|
|
89
|
+
signer_cert.add_extension(extension_factory.create_extension('keyUsage', 'digitalSignature'))
|
|
90
|
+
signer_cert.sign(ca_key, OpenSSL::Digest.new('SHA1'))
|
|
91
|
+
|
|
92
|
+
signer_cert
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
68
96
|
def timestamp_certificate
|
|
69
97
|
@timestamp_certificate ||=
|
|
70
98
|
begin
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
require 'test_helper'
|
|
4
|
+
require 'hexapdf/document'
|
|
5
|
+
require_relative '../common'
|
|
6
|
+
|
|
7
|
+
describe HexaPDF::DigitalSignature::Signing::DefaultHandler do
|
|
8
|
+
before do
|
|
9
|
+
@doc = HexaPDF::Document.new
|
|
10
|
+
@handler = HexaPDF::DigitalSignature::Signing::DefaultHandler.new(
|
|
11
|
+
certificate: CERTIFICATES.signer_certificate,
|
|
12
|
+
key: CERTIFICATES.signer_key,
|
|
13
|
+
certificate_chain: [CERTIFICATES.ca_certificate]
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "defaults to standard CMS signatures" do
|
|
18
|
+
assert_equal(:cms, @handler.signature_type)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "returns the size of serialized signature" do
|
|
22
|
+
assert(@handler.signature_size > 1000)
|
|
23
|
+
@handler.signature_size = 100
|
|
24
|
+
assert_equal(100, @handler.signature_size)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "allows setting the DocMDP permissions" do
|
|
28
|
+
assert_nil(@handler.doc_mdp_permissions)
|
|
29
|
+
|
|
30
|
+
@handler.doc_mdp_permissions = :no_changes
|
|
31
|
+
assert_equal(1, @handler.doc_mdp_permissions)
|
|
32
|
+
@handler.doc_mdp_permissions = 1
|
|
33
|
+
assert_equal(1, @handler.doc_mdp_permissions)
|
|
34
|
+
|
|
35
|
+
@handler.doc_mdp_permissions = :form_filling
|
|
36
|
+
assert_equal(2, @handler.doc_mdp_permissions)
|
|
37
|
+
@handler.doc_mdp_permissions = 2
|
|
38
|
+
assert_equal(2, @handler.doc_mdp_permissions)
|
|
39
|
+
|
|
40
|
+
@handler.doc_mdp_permissions = :form_filling_and_annotations
|
|
41
|
+
assert_equal(3, @handler.doc_mdp_permissions)
|
|
42
|
+
@handler.doc_mdp_permissions = 3
|
|
43
|
+
assert_equal(3, @handler.doc_mdp_permissions)
|
|
44
|
+
|
|
45
|
+
@handler.doc_mdp_permissions = nil
|
|
46
|
+
assert_nil(@handler.doc_mdp_permissions)
|
|
47
|
+
|
|
48
|
+
assert_raises(ArgumentError) { @handler.doc_mdp_permissions = :other }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
describe "sign" do
|
|
52
|
+
it "can sign the data using the provided certificate and key" do
|
|
53
|
+
data = StringIO.new("data")
|
|
54
|
+
signed_data = @handler.sign(data, [0, data.string.size, 0, 0])
|
|
55
|
+
|
|
56
|
+
pkcs7 = OpenSSL::PKCS7.new(signed_data)
|
|
57
|
+
assert(pkcs7.detached?)
|
|
58
|
+
assert_equal([CERTIFICATES.signer_certificate, CERTIFICATES.ca_certificate],
|
|
59
|
+
pkcs7.certificates)
|
|
60
|
+
store = OpenSSL::X509::Store.new
|
|
61
|
+
store.add_cert(CERTIFICATES.ca_certificate)
|
|
62
|
+
assert(pkcs7.verify([], store, data.string, OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it "can change the used digest algorithm" do
|
|
66
|
+
@handler.digest_algorithm = 'sha384'
|
|
67
|
+
asn1 = OpenSSL::ASN1.decode(@handler.sign(StringIO.new('data'), [0, 4, 0, 0]))
|
|
68
|
+
assert_equal('SHA384', asn1.value[1].value[0].value[1].value[0].value[0].value)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it "can embed a timestamp token" do
|
|
72
|
+
@handler.timestamp_handler = tsh = Object.new
|
|
73
|
+
tsh.define_singleton_method(:sign) {|_, _| OpenSSL::ASN1::OctetString.new("signed-tsh") }
|
|
74
|
+
signed = @handler.sign(StringIO.new('data'), [0, 4, 0, 0])
|
|
75
|
+
asn1 = OpenSSL::ASN1.decode(signed)
|
|
76
|
+
assert_equal('signed-tsh', asn1.value[1].value[0].value[4].value[0].
|
|
77
|
+
value[6].value[0].value[1].value[0].value)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it "creates PAdES compatible signatures" do
|
|
81
|
+
@handler.signature_type = :pades
|
|
82
|
+
signed = @handler.sign(StringIO.new('data'), [0, 4, 0, 0])
|
|
83
|
+
asn1 = OpenSSL::ASN1.decode(signed)
|
|
84
|
+
# check by absence of signing-time signed attribute
|
|
85
|
+
refute(asn1.value[1].value[0].value[4].value[0].value[3].value.
|
|
86
|
+
find {|obj| obj.value[0].value == 'signingTime' })
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it "can use external signing without certificate set" do
|
|
90
|
+
@handler.certificate = nil
|
|
91
|
+
@handler.external_signing = proc { "hallo" }
|
|
92
|
+
assert_equal("hallo", @handler.sign(StringIO.new, [0, 0, 0, 0]))
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "can use external signing with certificate set but not the key" do
|
|
96
|
+
@handler.key = nil
|
|
97
|
+
@handler.external_signing = proc do |algorithm, _hash|
|
|
98
|
+
assert_equal('sha256', algorithm)
|
|
99
|
+
"hallo"
|
|
100
|
+
end
|
|
101
|
+
result = @handler.sign(StringIO.new, [0, 0, 0, 0])
|
|
102
|
+
asn1 = OpenSSL::ASN1.decode(result)
|
|
103
|
+
assert_equal("hallo", asn1.value[1].value[0].value[4].value[0].value[5].value)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
describe "finalize_objects" do
|
|
108
|
+
before do
|
|
109
|
+
@field = @doc.wrap({})
|
|
110
|
+
@obj = @doc.wrap({})
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it "only sets the mandatory values if no concrete finalization tasks need to be done" do
|
|
114
|
+
@handler.finalize_objects(@field, @obj)
|
|
115
|
+
assert(@field.empty?)
|
|
116
|
+
assert_equal(:'Adobe.PPKLite', @obj[:Filter])
|
|
117
|
+
assert_equal(:'adbe.pkcs7.detached', @obj[:SubFilter])
|
|
118
|
+
assert_kind_of(Time, @obj[:M])
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it "adjust the /SubFilter if signature type is pades" do
|
|
122
|
+
@handler.signature_type = :pades
|
|
123
|
+
@handler.finalize_objects(@field, @obj)
|
|
124
|
+
assert_equal(:'ETSI.CAdES.detached', @obj[:SubFilter])
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it "sets the reason, location and contact info fields" do
|
|
128
|
+
@handler.reason = 'Reason'
|
|
129
|
+
@handler.location = 'Location'
|
|
130
|
+
@handler.contact_info = 'Contact'
|
|
131
|
+
@handler.finalize_objects(@field, @obj)
|
|
132
|
+
assert(@field.empty?)
|
|
133
|
+
assert_equal(['Reason', 'Location', 'Contact'], @obj.value.values_at(:Reason, :Location, :ContactInfo))
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it "fills the build properties dictionary with appropriate application information" do
|
|
137
|
+
@handler.finalize_objects(@field, @obj)
|
|
138
|
+
assert_equal(:HexaPDF, @obj[:Prop_Build][:App][:Name])
|
|
139
|
+
assert_equal(HexaPDF::VERSION, @obj[:Prop_Build][:App][:REx])
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
it "applies the specified DocMDP permissions" do
|
|
143
|
+
@handler.doc_mdp_permissions = :no_changes
|
|
144
|
+
@handler.finalize_objects(@field, @obj)
|
|
145
|
+
ref = @obj[:Reference][0]
|
|
146
|
+
assert_equal(:DocMDP, ref[:TransformMethod])
|
|
147
|
+
assert_equal(:SHA256, ref[:DigestMethod])
|
|
148
|
+
assert_equal(1, ref[:TransformParams][:P])
|
|
149
|
+
assert_equal(:'1.2', ref[:TransformParams][:V])
|
|
150
|
+
assert_same(@obj, @doc.catalog[:Perms][:DocMDP])
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
it "fails if DocMDP should be set but there is already a signature" do
|
|
154
|
+
@handler.doc_mdp_permissions = :no_changes
|
|
155
|
+
2.times do
|
|
156
|
+
field = @doc.acro_form(create: true).create_signature_field('test')
|
|
157
|
+
field.field_value = :something
|
|
158
|
+
end
|
|
159
|
+
assert_raises(HexaPDF::Error) { @handler.finalize_objects(@field, @obj) }
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|