hexapdf 0.28.0 → 0.31.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|