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,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)
|