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,148 @@
|
|
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-2022 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 'net/http'
|
39
|
+
require 'hexapdf/error'
|
40
|
+
require 'stringio'
|
41
|
+
|
42
|
+
module HexaPDF
|
43
|
+
module DigitalSignature
|
44
|
+
module Signing
|
45
|
+
|
46
|
+
# This is a signing handler for adding a timestamp signature (a PDF2.0 feature) to a PDF
|
47
|
+
# document. It is registered under the :timestamp name.
|
48
|
+
#
|
49
|
+
# The timestamp is provided by a timestamp authority and establishes the document contents at
|
50
|
+
# the time indicated in the timestamp. Timestamping a PDF document is usually done in context
|
51
|
+
# of long term validation but can also be done standalone.
|
52
|
+
#
|
53
|
+
# == Usage
|
54
|
+
#
|
55
|
+
# It is necessary to provide at least the URL of the timestamp authority server (TSA) via
|
56
|
+
# #tsa_url, everything else is optional and uses default values. The TSA server must not use
|
57
|
+
# authentication to be usable.
|
58
|
+
#
|
59
|
+
# Example:
|
60
|
+
#
|
61
|
+
# document.sign("output.pdf", handler: :timestamp, tsa_url: 'https://freetsa.org/tsr')
|
62
|
+
class TimestampHandler
|
63
|
+
|
64
|
+
# The URL of the timestamp authority server.
|
65
|
+
#
|
66
|
+
# This value is required.
|
67
|
+
attr_accessor :tsa_url
|
68
|
+
|
69
|
+
# The hash algorithm to use for timestamping. Defaults to SHA512.
|
70
|
+
attr_accessor :tsa_hash_algorithm
|
71
|
+
|
72
|
+
# The policy OID to use for timestamping. Defaults to +nil+.
|
73
|
+
attr_accessor :tsa_policy_id
|
74
|
+
|
75
|
+
# The size of the serialized signature that should be reserved.
|
76
|
+
#
|
77
|
+
# If this attribute has not been set, an empty string will be signed using #sign to
|
78
|
+
# determine the signature size which will contact the TSA server
|
79
|
+
#
|
80
|
+
# The size needs to be at least as big as the final signature, otherwise signing results in
|
81
|
+
# an error.
|
82
|
+
attr_writer :signature_size
|
83
|
+
|
84
|
+
# The reason for timestamping. If used, will be set on the signature object.
|
85
|
+
attr_accessor :reason
|
86
|
+
|
87
|
+
# The timestamping location. If used, will be set on the signature object.
|
88
|
+
attr_accessor :location
|
89
|
+
|
90
|
+
# The contact information. If used, will be set on the signature object.
|
91
|
+
attr_accessor :contact_info
|
92
|
+
|
93
|
+
# Creates a new TimestampHandler with the given attributes.
|
94
|
+
def initialize(**arguments)
|
95
|
+
@signature_size = nil
|
96
|
+
arguments.each {|name, value| send("#{name}=", value) }
|
97
|
+
end
|
98
|
+
|
99
|
+
# Returns the size of the serialized signature that should be reserved.
|
100
|
+
def signature_size
|
101
|
+
@signature_size || (sign(StringIO.new, [0, 0, 0, 0]).size * 1.5).to_i
|
102
|
+
end
|
103
|
+
|
104
|
+
# Finalizes the signature field as well as the signature dictionary before writing.
|
105
|
+
def finalize_objects(_signature_field, signature)
|
106
|
+
signature.document.version = '2.0'
|
107
|
+
signature[:Type] = :DocTimeStamp
|
108
|
+
signature[:Filter] = :'Adobe.PPKLite'
|
109
|
+
signature[:SubFilter] = :'ETSI.RFC3161'
|
110
|
+
signature[:Reason] = reason if reason
|
111
|
+
signature[:Location] = location if location
|
112
|
+
signature[:ContactInfo] = contact_info if contact_info
|
113
|
+
end
|
114
|
+
|
115
|
+
# Returns the DER serialized OpenSSL::PKCS7 structure containing the timestamp token for the
|
116
|
+
# given IO byte ranges.
|
117
|
+
def sign(io, byte_range)
|
118
|
+
hash_algorithm = tsa_hash_algorithm || 'SHA512'
|
119
|
+
digest = OpenSSL::Digest.new(hash_algorithm)
|
120
|
+
io.pos = byte_range[0]
|
121
|
+
digest << io.read(byte_range[1])
|
122
|
+
io.pos = byte_range[2]
|
123
|
+
digest << io.read(byte_range[3])
|
124
|
+
|
125
|
+
req = OpenSSL::Timestamp::Request.new
|
126
|
+
req.algorithm = hash_algorithm
|
127
|
+
req.message_imprint = digest.digest
|
128
|
+
req.policy_id = tsa_policy_id if tsa_policy_id
|
129
|
+
|
130
|
+
http_response = Net::HTTP.post(URI(tsa_url), req.to_der,
|
131
|
+
'content-type' => 'application/timestamp-query')
|
132
|
+
if http_response.kind_of?(Net::HTTPOK)
|
133
|
+
response = OpenSSL::Timestamp::Response.new(http_response.body)
|
134
|
+
if response.status == 0
|
135
|
+
response.token.to_der
|
136
|
+
else
|
137
|
+
raise HexaPDF::Error, "Timestamp token could not be created: #{response.failure_info}"
|
138
|
+
end
|
139
|
+
else
|
140
|
+
raise HexaPDF::Error, "Invalid TSA server response: #{http_response.body}"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,101 @@
|
|
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-2022 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/document'
|
38
|
+
|
39
|
+
module HexaPDF
|
40
|
+
module DigitalSignature
|
41
|
+
|
42
|
+
# This module contains everything related to the signing of a PDF document, i.e. signing
|
43
|
+
# handlers and the actual code for signing.
|
44
|
+
module Signing
|
45
|
+
|
46
|
+
autoload(:DefaultHandler, 'hexapdf/digital_signature/signing/default_handler')
|
47
|
+
autoload(:TimestampHandler, 'hexapdf/digital_signature/signing/timestamp_handler')
|
48
|
+
autoload(:SignedDataCreator, 'hexapdf/digital_signature/signing/signed_data_creator')
|
49
|
+
|
50
|
+
# Embeds the given +signature+ into the /Contents value of the newest signature dictionary of
|
51
|
+
# the PDF document given by the +io+ argument.
|
52
|
+
#
|
53
|
+
# This functionality can be used together with the support for external signing (see
|
54
|
+
# DefaultHandler and DefaultHandler#external_signing) to implement asynchronous signing.
|
55
|
+
#
|
56
|
+
# Note: This will, most probably, only work on documents prepared for external signing by
|
57
|
+
# HexaPDF and not by other libraries.
|
58
|
+
def self.embed_signature(io, signature)
|
59
|
+
doc = HexaPDF::Document.new(io: io)
|
60
|
+
signature_dict = doc.signatures.find {|sig| doc.revisions.current.object(sig) == sig }
|
61
|
+
signature_dict_offset, signature_dict_length = locate_signature_dict(
|
62
|
+
doc.revisions.current.xref_section,
|
63
|
+
doc.revisions.parser.startxref_offset,
|
64
|
+
signature_dict.oid
|
65
|
+
)
|
66
|
+
io.pos = signature_dict_offset
|
67
|
+
signature_data = io.read(signature_dict_length)
|
68
|
+
replace_signature_contents(signature_data, signature)
|
69
|
+
io.pos = signature_dict_offset
|
70
|
+
io.write(signature_data)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Uses the information in the given cross-reference section as well as the byte offset of the
|
74
|
+
# cross-reference section to calculate the offset and length of the signature dictionary with
|
75
|
+
# the given object id.
|
76
|
+
def self.locate_signature_dict(xref_section, start_xref_position, signature_oid)
|
77
|
+
data = xref_section.map {|oid, _gen, entry| [entry.pos, oid] if entry.in_use? }.compact.sort <<
|
78
|
+
[start_xref_position, nil]
|
79
|
+
index = data.index {|_pos, oid| oid == signature_oid }
|
80
|
+
[data[index][0], data[index + 1][0] - data[index][0]]
|
81
|
+
end
|
82
|
+
|
83
|
+
# Replaces the value of the /Contents key in the serialized +signature_data+ with the value of
|
84
|
+
# +contents+.
|
85
|
+
def self.replace_signature_contents(signature_data, contents)
|
86
|
+
signature_data.sub!(/Contents(?:\(.*?\)|<.*?>)/) do |match|
|
87
|
+
length = match.size
|
88
|
+
result = "Contents<#{contents.unpack1('H*')}"
|
89
|
+
if length < result.size
|
90
|
+
raise HexaPDF::Error, "The reserved space for the signature was too small " \
|
91
|
+
"(#{(length - 10) / 2} vs #{(result.size - 10) / 2}) - use the handlers " \
|
92
|
+
"#signature_size method to increase the reserved space"
|
93
|
+
end
|
94
|
+
"#{result.ljust(length - 1, '0')}>"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
@@ -34,59 +34,55 @@
|
|
34
34
|
# commercial licenses are available at <https://gettalong.at/hexapdf/>.
|
35
35
|
#++
|
36
36
|
|
37
|
-
require 'hexapdf/type/signature'
|
38
|
-
|
39
37
|
module HexaPDF
|
40
|
-
module
|
41
|
-
class Signature
|
38
|
+
module DigitalSignature
|
42
39
|
|
43
|
-
|
44
|
-
|
40
|
+
# Holds the result information when verifying a signature.
|
41
|
+
class VerificationResult
|
45
42
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
43
|
+
# :nodoc:
|
44
|
+
MESSAGE_SORT_MAP = {
|
45
|
+
info: {warning: 1, error: 1, info: 0},
|
46
|
+
warning: {info: -1, error: 1, warning: 0},
|
47
|
+
error: {info: -1, warning: -1, error: 0},
|
48
|
+
}
|
52
49
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
end
|
50
|
+
# This structure represents a single status message, containing the type (:info, :warning,
|
51
|
+
# :error) and the content of the message.
|
52
|
+
Message = Struct.new(:type, :content) do
|
53
|
+
def <=>(other)
|
54
|
+
MESSAGE_SORT_MAP[type][other.type]
|
59
55
|
end
|
56
|
+
end
|
60
57
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
# Creates an empty result object.
|
65
|
-
def initialize
|
66
|
-
@messages = []
|
67
|
-
end
|
58
|
+
# An array with all result messages.
|
59
|
+
attr_reader :messages
|
68
60
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
61
|
+
# Creates an empty result object.
|
62
|
+
def initialize
|
63
|
+
@messages = []
|
64
|
+
end
|
73
65
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
66
|
+
# Returns +true+ if there are no error messages.
|
67
|
+
def success?
|
68
|
+
@messages.none? {|message| message.type == :error }
|
69
|
+
end
|
78
70
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
# +content+:: The log message.
|
84
|
-
def log(type, content)
|
85
|
-
@messages << Message.new(type, content)
|
86
|
-
end
|
71
|
+
# Returns +true+ if there is at least one error message.
|
72
|
+
def failure?
|
73
|
+
!success?
|
74
|
+
end
|
87
75
|
|
76
|
+
# Adds a new message of the given type to this result object.
|
77
|
+
#
|
78
|
+
# +type+:: One of :info, :warning or :error.
|
79
|
+
#
|
80
|
+
# +content+:: The log message.
|
81
|
+
def log(type, content)
|
82
|
+
@messages << Message.new(type, content)
|
88
83
|
end
|
89
84
|
|
90
85
|
end
|
86
|
+
|
91
87
|
end
|
92
88
|
end
|
@@ -0,0 +1,56 @@
|
|
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-2022 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
|
+
module HexaPDF
|
38
|
+
|
39
|
+
# PDF documents can be signed using digital signatures. Such a signature can be used to
|
40
|
+
# authenticate the identity of the signer and the contents of the documents.
|
41
|
+
#
|
42
|
+
# This module contains all code related to digital signatures in PDF.
|
43
|
+
#
|
44
|
+
# See: PDF1.7/2.0 s12.8
|
45
|
+
module DigitalSignature
|
46
|
+
|
47
|
+
autoload(:Signatures, 'hexapdf/digital_signature/signatures')
|
48
|
+
autoload(:Signature, "hexapdf/digital_signature/signature")
|
49
|
+
autoload(:Handler, 'hexapdf/digital_signature/handler')
|
50
|
+
autoload(:CMSHandler, "hexapdf/digital_signature/cms_handler")
|
51
|
+
autoload(:PKCS1Handler, "hexapdf/digital_signature/pkcs1_handler")
|
52
|
+
autoload(:VerificationResult, 'hexapdf/digital_signature/verification_result')
|
53
|
+
autoload(:Signing, 'hexapdf/digital_signature/signing')
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
@@ -75,28 +75,41 @@ module HexaPDF
|
|
75
75
|
@document.catalog.pages
|
76
76
|
end
|
77
77
|
|
78
|
-
#
|
79
|
-
#
|
80
|
-
#
|
81
|
-
#
|
78
|
+
# Creates a page object and returns it *without* adding it to the page tree.
|
79
|
+
#
|
80
|
+
# +media_box+::
|
81
|
+
# If this argument is +nil+/not specified, the value is taken from the configuration
|
82
|
+
# option 'page.default_media_box'.
|
83
|
+
#
|
84
|
+
# If the resulting value is an array with four numbers (specifying the media box), the new
|
85
|
+
# page will have these exact dimensions.
|
82
86
|
#
|
83
|
-
#
|
87
|
+
# If the value is a symbol, it is taken as a reference to a pre-defined media box in
|
88
|
+
# HexaPDF::Type::Page::PAPER_SIZE. The +orientation+ can then be used to specify the page
|
89
|
+
# orientation.
|
84
90
|
#
|
85
|
-
#
|
86
|
-
# 'page.
|
91
|
+
# +orientation+::
|
92
|
+
# If this argument is not specified, it is taken from 'page.default_media_orientation'. It
|
93
|
+
# is only used if +media_box+ is a symbol and not an array.
|
94
|
+
def create(media_box: nil, orientation: nil)
|
95
|
+
media_box ||= @document.config['page.default_media_box']
|
96
|
+
orientation ||= @document.config['page.default_media_orientation']
|
97
|
+
box = Type::Page.media_box(media_box, orientation: orientation)
|
98
|
+
@document.add({Type: :Page, MediaBox: box})
|
99
|
+
end
|
100
|
+
|
101
|
+
# :call-seq:
|
102
|
+
# pages.add -> new_page
|
103
|
+
# pages.add(page) -> page
|
104
|
+
# pages.add(media_box, orientation: nil) -> new_page
|
87
105
|
#
|
88
|
-
#
|
89
|
-
# page will have these dimensions.
|
106
|
+
# Adds the given page or a new empty page at the end and returns it.
|
90
107
|
#
|
91
|
-
# If
|
92
|
-
#
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
page = @document.add({Type: :Page, MediaBox: page})
|
97
|
-
elsif page.kind_of?(Symbol)
|
98
|
-
box = Type::Page.media_box(page, orientation: orientation)
|
99
|
-
page = @document.add({Type: :Page, MediaBox: box})
|
108
|
+
# If called with a page object as argument, that page object is used. Otherwise #create is
|
109
|
+
# called with the arguments +media_box+ and +orientation+ to create a new page.
|
110
|
+
def add(page = nil, orientation: nil)
|
111
|
+
unless page.kind_of?(HexaPDF::Type::Page)
|
112
|
+
page = create(media_box: page, orientation: orientation)
|
100
113
|
end
|
101
114
|
@document.catalog.pages.add_page(page)
|
102
115
|
end
|
data/lib/hexapdf/document.rb
CHANGED
@@ -52,6 +52,7 @@ require 'hexapdf/importer'
|
|
52
52
|
require 'hexapdf/image_loader'
|
53
53
|
require 'hexapdf/font_loader'
|
54
54
|
require 'hexapdf/layout'
|
55
|
+
require 'hexapdf/digital_signature'
|
55
56
|
|
56
57
|
begin
|
57
58
|
require 'hexapdf/cext'
|
@@ -105,7 +106,6 @@ module HexaPDF
|
|
105
106
|
autoload(:Fonts, 'hexapdf/document/fonts')
|
106
107
|
autoload(:Images, 'hexapdf/document/images')
|
107
108
|
autoload(:Files, 'hexapdf/document/files')
|
108
|
-
autoload(:Signatures, 'hexapdf/document/signatures')
|
109
109
|
autoload(:Destinations, 'hexapdf/document/destinations')
|
110
110
|
autoload(:Layout, 'hexapdf/document/layout')
|
111
111
|
|
@@ -152,15 +152,19 @@ module HexaPDF
|
|
152
152
|
#
|
153
153
|
# Options:
|
154
154
|
#
|
155
|
-
# io::
|
156
|
-
#
|
155
|
+
# io::
|
156
|
+
# If an IO object is provided, then this document can read PDF objects from this IO object,
|
157
|
+
# otherwise it can only contain created PDF objects.
|
157
158
|
#
|
158
|
-
# decryption_opts::
|
159
|
+
# decryption_opts::
|
160
|
+
# A hash with options for decrypting the PDF objects loaded from the IO. The PDF standard
|
161
|
+
# security handler expects a :password key to be set to either the user or owner password of
|
162
|
+
# the PDF file.
|
159
163
|
#
|
160
|
-
# config::
|
161
|
-
#
|
162
|
-
#
|
163
|
-
#
|
164
|
+
# config::
|
165
|
+
# A hash with configuration options that is deep-merged into the default configuration (see
|
166
|
+
# HexaPDF::DefaultDocumentConfiguration[../index.html#DefaultDocumentConfiguration], meaning
|
167
|
+
# that direct sub-hashes are merged instead of overwritten.
|
164
168
|
def initialize(io: nil, decryption_opts: {}, config: {})
|
165
169
|
@config = Configuration.with_defaults(config)
|
166
170
|
@version = '1.2'
|
@@ -321,7 +325,14 @@ module HexaPDF
|
|
321
325
|
type = (klass <= HexaPDF::Dictionary ? klass.type : nil)
|
322
326
|
else
|
323
327
|
type ||= deref(data.value[:Type]) if data.value.kind_of?(Hash)
|
324
|
-
|
328
|
+
if type
|
329
|
+
klass = GlobalConfiguration.constantize('object.type_map', type) { nil }
|
330
|
+
if (type == :ObjStm || type == :XRef) &&
|
331
|
+
klass.each_field.any? {|name, field| field.required? && !data.value.key?(name) }
|
332
|
+
data.value.delete(:Type)
|
333
|
+
klass = nil
|
334
|
+
end
|
335
|
+
end
|
325
336
|
end
|
326
337
|
|
327
338
|
if data.value.kind_of?(Hash)
|
@@ -585,25 +596,28 @@ module HexaPDF
|
|
585
596
|
acro_form&.signature_flag?(:signatures_exist)
|
586
597
|
end
|
587
598
|
|
588
|
-
# Returns
|
599
|
+
# Returns a DigitalSignature::Signatures object that allows working with the digital signatures
|
600
|
+
# of this document.
|
589
601
|
def signatures
|
590
|
-
@signatures ||= Signatures.new(self)
|
602
|
+
@signatures ||= DigitalSignature::Signatures.new(self)
|
591
603
|
end
|
592
604
|
|
593
605
|
# Signs the document and writes it to the given file or IO object.
|
594
606
|
#
|
595
607
|
# For details on the arguments +file_or_io+, +signature+ and +write_options+ see
|
596
|
-
# HexaPDF::
|
608
|
+
# HexaPDF::DigitalSignature::Signatures#add.
|
597
609
|
#
|
598
610
|
# The signing handler to be used is determined by the +handler+ argument together with the rest
|
599
|
-
# of the keyword arguments (see HexaPDF::
|
611
|
+
# of the keyword arguments (see HexaPDF::DigitalSignature::Signatures#signing_handler for
|
612
|
+
# details).
|
600
613
|
#
|
601
|
-
# If not changed, the default signing handler is
|
614
|
+
# If not changed, the default signing handler is
|
615
|
+
# HexaPDF::DigitalSignature::Signing::DefaultHandler.
|
602
616
|
#
|
603
617
|
# *Note*: Once signing is done the document cannot be changed anymore since it was written. If a
|
604
618
|
# document needs to be signed multiple times, it needs to be loaded again after writing.
|
605
619
|
def sign(file_or_io, handler: :default, signature: nil, write_options: {}, **handler_options)
|
606
|
-
handler = signatures.
|
620
|
+
handler = signatures.signing_handler(name: handler, **handler_options)
|
607
621
|
signatures.add(file_or_io, handler, signature: signature, write_options: write_options)
|
608
622
|
end
|
609
623
|
|
@@ -97,7 +97,8 @@ module HexaPDF
|
|
97
97
|
# a user is allowed to do with a PDF file.
|
98
98
|
#
|
99
99
|
# When a user or owner password is specified, a PDF file can only be opened when the correct
|
100
|
-
# password is supplied.
|
100
|
+
# password is supplied. To open such an encrypted PDF file, the +decryption_opts+ provided to
|
101
|
+
# HexaPDF::Document.new needs to contain a :password key with the password.
|
101
102
|
#
|
102
103
|
# See: PDF1.7 s7.6.3, PDF2.0 s7.6.3
|
103
104
|
class StandardSecurityHandler < SecurityHandler
|
@@ -323,10 +324,10 @@ module HexaPDF
|
|
323
324
|
def prepare_decryption(password: '', check_permissions: true)
|
324
325
|
if dict[:Filter] != :Standard
|
325
326
|
raise(HexaPDF::UnsupportedEncryptionError,
|
326
|
-
"Invalid /Filter value for standard security handler")
|
327
|
+
"Invalid /Filter value #{dict[:Filter]} for standard security handler")
|
327
328
|
elsif ![2, 3, 4, 6].include?(dict[:R])
|
328
329
|
raise(HexaPDF::UnsupportedEncryptionError,
|
329
|
-
"Invalid /R value for standard security handler")
|
330
|
+
"Invalid /R value #{dict[:R]} for standard security handler")
|
330
331
|
elsif dict[:R] <= 4 && !document.trailer[:ID].kind_of?(PDFArray)
|
331
332
|
document.trailer[:ID] = ['', '']
|
332
333
|
end
|
@@ -55,21 +55,33 @@ module HexaPDF
|
|
55
55
|
def self.decoder(source, options = nil)
|
56
56
|
fib = Fiber.new do
|
57
57
|
inflater = Zlib::Inflate.new
|
58
|
+
error_raised = nil
|
59
|
+
|
58
60
|
while source.alive? && (data = source.resume)
|
59
61
|
next if data.empty?
|
60
62
|
begin
|
61
|
-
|
62
|
-
rescue
|
63
|
-
|
63
|
+
Fiber.yield(inflater.inflate(data))
|
64
|
+
rescue Zlib::DataError, Zlib::BufError => e
|
65
|
+
# Only swallow the error if it appears at the end of the stream
|
66
|
+
if error_raised || HexaPDF::GlobalConfiguration['filter.flate.on_error'].call(inflater, e)
|
67
|
+
raise FilterError, "Problem while decoding Flate encoded stream: #{e}"
|
68
|
+
else
|
69
|
+
Fiber.yield(inflater.flush_next_out)
|
70
|
+
error_raised = e
|
71
|
+
end
|
64
72
|
end
|
65
|
-
Fiber.yield(data)
|
66
73
|
end
|
74
|
+
|
67
75
|
begin
|
68
76
|
data = inflater.total_in == 0 || (data = inflater.finish).empty? ? nil : data
|
69
77
|
inflater.close
|
70
78
|
data
|
71
|
-
rescue
|
72
|
-
|
79
|
+
rescue Zlib::DataError, Zlib::BufError => e
|
80
|
+
if HexaPDF::GlobalConfiguration['filter.flate.on_error'].call(inflater, e)
|
81
|
+
raise FilterError, "Problem while decoding Flate encoded stream: #{e}"
|
82
|
+
else
|
83
|
+
Fiber.yield(inflater.flush_next_out)
|
84
|
+
end
|
73
85
|
end
|
74
86
|
end
|
75
87
|
|
@@ -87,9 +99,9 @@ module HexaPDF
|
|
87
99
|
end
|
88
100
|
|
89
101
|
Fiber.new do
|
90
|
-
deflater = Zlib::Deflate.new(HexaPDF::GlobalConfiguration['filter.
|
102
|
+
deflater = Zlib::Deflate.new(HexaPDF::GlobalConfiguration['filter.flate.compression'],
|
91
103
|
Zlib::MAX_WBITS,
|
92
|
-
HexaPDF::GlobalConfiguration['filter.
|
104
|
+
HexaPDF::GlobalConfiguration['filter.flate.memory'])
|
93
105
|
while source.alive? && (data = source.resume)
|
94
106
|
data = deflater.deflate(data)
|
95
107
|
Fiber.yield(data)
|