hexapdf 0.19.2 → 0.20.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +67 -0
- data/data/hexapdf/cert/demo_cert.rb +22 -0
- data/data/hexapdf/cert/root-ca.crt +119 -0
- data/data/hexapdf/cert/signing.crt +125 -0
- data/data/hexapdf/cert/signing.key +52 -0
- data/data/hexapdf/cert/sub-ca.crt +125 -0
- data/data/hexapdf/encoding/glyphlist.txt +4283 -4282
- data/data/hexapdf/encoding/zapfdingbats.txt +203 -202
- data/lib/hexapdf/cli/info.rb +21 -1
- data/lib/hexapdf/configuration.rb +26 -0
- data/lib/hexapdf/content/processor.rb +1 -1
- data/lib/hexapdf/document/signatures.rb +327 -0
- data/lib/hexapdf/document.rb +26 -0
- data/lib/hexapdf/font/encoding/glyph_list.rb +5 -6
- data/lib/hexapdf/importer.rb +1 -1
- data/lib/hexapdf/object.rb +5 -3
- data/lib/hexapdf/parser.rb +14 -9
- data/lib/hexapdf/rectangle.rb +0 -6
- data/lib/hexapdf/revision.rb +13 -6
- data/lib/hexapdf/task/dereference.rb +12 -4
- data/lib/hexapdf/task/optimize.rb +3 -3
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +2 -4
- data/lib/hexapdf/type/acro_form/field.rb +2 -0
- data/lib/hexapdf/type/acro_form/form.rb +9 -1
- data/lib/hexapdf/type/annotation.rb +36 -3
- data/lib/hexapdf/type/font_simple.rb +1 -1
- data/lib/hexapdf/type/object_stream.rb +3 -1
- data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +121 -0
- data/lib/hexapdf/type/signature/adbe_x509_rsa_sha1.rb +95 -0
- data/lib/hexapdf/type/signature/handler.rb +140 -0
- data/lib/hexapdf/type/signature/verification_result.rb +92 -0
- data/lib/hexapdf/type/signature.rb +236 -0
- data/lib/hexapdf/type.rb +1 -0
- data/lib/hexapdf/version.rb +1 -1
- data/lib/hexapdf/writer.rb +16 -8
- data/test/hexapdf/content/test_processor.rb +1 -1
- data/test/hexapdf/document/test_signatures.rb +225 -0
- data/test/hexapdf/task/test_optimize.rb +4 -1
- data/test/hexapdf/test_document.rb +28 -0
- data/test/hexapdf/test_object.rb +7 -2
- data/test/hexapdf/test_parser.rb +12 -0
- data/test/hexapdf/test_rectangle.rb +0 -7
- data/test/hexapdf/test_revision.rb +44 -14
- data/test/hexapdf/test_writer.rb +4 -3
- data/test/hexapdf/type/acro_form/test_field.rb +11 -1
- data/test/hexapdf/type/acro_form/test_form.rb +5 -0
- data/test/hexapdf/type/signature/common.rb +71 -0
- data/test/hexapdf/type/signature/test_adbe_pkcs7_detached.rb +99 -0
- data/test/hexapdf/type/signature/test_adbe_x509_rsa_sha1.rb +66 -0
- data/test/hexapdf/type/signature/test_handler.rb +102 -0
- data/test/hexapdf/type/signature/test_verification_result.rb +47 -0
- data/test/hexapdf/type/test_annotation.rb +40 -2
- data/test/hexapdf/type/test_font_simple.rb +5 -5
- data/test/hexapdf/type/test_object_stream.rb +9 -0
- data/test/hexapdf/type/test_signature.rb +131 -0
- metadata +21 -3
|
@@ -78,6 +78,25 @@ module HexaPDF
|
|
|
78
78
|
self[:D] || self[:N]
|
|
79
79
|
end
|
|
80
80
|
|
|
81
|
+
APPEARANCE_TYPE_TO_KEY = {normal: :N, rollover: :R, down: :D}.freeze #:nodoc:
|
|
82
|
+
|
|
83
|
+
# Sets the appearance of the given appearance +type+, which can either be :normal, :rollover
|
|
84
|
+
# or :down, to +appearance+.
|
|
85
|
+
#
|
|
86
|
+
# If the +state_name+ argument is provided, the +appearance+ is stored under the
|
|
87
|
+
# +state_name+ key in a sub-dictionary of the appearance.
|
|
88
|
+
def set_appearance(appearance, type: :normal, state_name: nil)
|
|
89
|
+
key = APPEARANCE_TYPE_TO_KEY.fetch(type) do
|
|
90
|
+
raise ArgumentError, "Invalid value for type specified: #{type.inspect}"
|
|
91
|
+
end
|
|
92
|
+
if state_name
|
|
93
|
+
self[key] = {} unless value[key].kind_of?(Hash)
|
|
94
|
+
self[key][state_name] = appearance
|
|
95
|
+
else
|
|
96
|
+
self[key] = appearance
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
81
100
|
end
|
|
82
101
|
|
|
83
102
|
# Border style dictionary used by various annotation types.
|
|
@@ -132,11 +151,12 @@ module HexaPDF
|
|
|
132
151
|
# Returns the annotation's appearance stream of the given type (:normal, :rollover, or :down)
|
|
133
152
|
# or +nil+ if it doesn't exist.
|
|
134
153
|
#
|
|
135
|
-
# The appearance state is taken into account if
|
|
136
|
-
|
|
154
|
+
# The appearance state in /AS or the one provided via +state_name+ is taken into account if
|
|
155
|
+
# necessary.
|
|
156
|
+
def appearance(type: :normal, state_name: self[:AS])
|
|
137
157
|
entry = appearance_dict&.send("#{type}_appearance")
|
|
138
158
|
if entry.kind_of?(HexaPDF::Dictionary) && !entry.kind_of?(HexaPDF::Stream)
|
|
139
|
-
entry = entry[
|
|
159
|
+
entry = entry[state_name]
|
|
140
160
|
end
|
|
141
161
|
return unless entry.kind_of?(HexaPDF::Stream)
|
|
142
162
|
|
|
@@ -149,6 +169,19 @@ module HexaPDF
|
|
|
149
169
|
end
|
|
150
170
|
alias appearance? appearance
|
|
151
171
|
|
|
172
|
+
# Creates an empty appearance stream (a Form XObject) of the given type (:normal, :rollover,
|
|
173
|
+
# or :down) and returns it. If an appearance stream already exist, it is overwritten.
|
|
174
|
+
#
|
|
175
|
+
# If there can be multiple appearance streams for the annotation, use the +state_name+
|
|
176
|
+
# argument to provide the appearance state name.
|
|
177
|
+
def create_appearance(type: :normal, state_name: self[:AS])
|
|
178
|
+
xobject = document.add({Type: :XObject, Subtype: :Form,
|
|
179
|
+
BBox: [0, 0, self[:Rect].width, self[:Rect].height]})
|
|
180
|
+
self[:AP] ||= {}
|
|
181
|
+
appearance_dict.set_appearance(xobject, type: type, state_name: state_name)
|
|
182
|
+
xobject
|
|
183
|
+
end
|
|
184
|
+
|
|
152
185
|
private
|
|
153
186
|
|
|
154
187
|
# Helper method for bit field getter access.
|
|
@@ -133,7 +133,7 @@ module HexaPDF
|
|
|
133
133
|
#
|
|
134
134
|
# This method has to be implemented in subclasses.
|
|
135
135
|
def encoding_from_font
|
|
136
|
-
raise
|
|
136
|
+
raise "Needs to be implemented in subclass"
|
|
137
137
|
end
|
|
138
138
|
|
|
139
139
|
# Uses the given base encoding and the differences array to create a DifferenceEncoding
|
|
@@ -178,7 +178,9 @@ module HexaPDF
|
|
|
178
178
|
# Due to a bug in Adobe Acrobat, the Catalog may not be in an object stream if the
|
|
179
179
|
# document is encrypted
|
|
180
180
|
if obj.nil? || obj.null? || obj.gen != 0 || obj.kind_of?(Stream) || obj == encrypt_dict ||
|
|
181
|
-
(encrypt_dict && obj.type == :Catalog)
|
|
181
|
+
(encrypt_dict && obj.type == :Catalog) ||
|
|
182
|
+
obj.type == :Sig || obj.type == :DocTimeStamp ||
|
|
183
|
+
(obj.respond_to?(:key?) && obj.key?(:ByteRange) && obj.key?(:Contents))
|
|
182
184
|
delete_object(objects[index])
|
|
183
185
|
next
|
|
184
186
|
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# -*- encoding: utf-8; frozen_string_literal: true -*-
|
|
2
|
+
#
|
|
3
|
+
#--
|
|
4
|
+
# This file is part of HexaPDF.
|
|
5
|
+
#
|
|
6
|
+
# HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
|
|
7
|
+
# Copyright (C) 2014-2021 Thomas Leitner
|
|
8
|
+
#
|
|
9
|
+
# HexaPDF is free software: you can redistribute it and/or modify it
|
|
10
|
+
# under the terms of the GNU Affero General Public License version 3 as
|
|
11
|
+
# published by the Free Software Foundation with the addition of the
|
|
12
|
+
# following permission added to Section 15 as permitted in Section 7(a):
|
|
13
|
+
# FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
|
|
14
|
+
# THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
|
|
15
|
+
# INFRINGEMENT OF THIRD PARTY RIGHTS.
|
|
16
|
+
#
|
|
17
|
+
# HexaPDF is distributed in the hope that it will be useful, but WITHOUT
|
|
18
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
19
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
|
20
|
+
# License for more details.
|
|
21
|
+
#
|
|
22
|
+
# You should have received a copy of the GNU Affero General Public License
|
|
23
|
+
# along with HexaPDF. If not, see <http://www.gnu.org/licenses/>.
|
|
24
|
+
#
|
|
25
|
+
# The interactive user interfaces in modified source and object code
|
|
26
|
+
# versions of HexaPDF must display Appropriate Legal Notices, as required
|
|
27
|
+
# under Section 5 of the GNU Affero General Public License version 3.
|
|
28
|
+
#
|
|
29
|
+
# In accordance with Section 7(b) of the GNU Affero General Public
|
|
30
|
+
# License, a covered work must retain the producer line in every PDF that
|
|
31
|
+
# is created or manipulated using HexaPDF.
|
|
32
|
+
#
|
|
33
|
+
# If the GNU Affero General Public License doesn't fit your need,
|
|
34
|
+
# commercial licenses are available at <https://gettalong.at/hexapdf/>.
|
|
35
|
+
#++
|
|
36
|
+
|
|
37
|
+
require 'openssl'
|
|
38
|
+
require 'hexapdf/type/signature'
|
|
39
|
+
|
|
40
|
+
module HexaPDF
|
|
41
|
+
module Type
|
|
42
|
+
class Signature
|
|
43
|
+
|
|
44
|
+
# The signature handler for the adbe.pkcs7.detached sub-filter.
|
|
45
|
+
class AdbePkcs7Detached < Handler
|
|
46
|
+
|
|
47
|
+
# Creates a new signature handler for the given signature dictionary.
|
|
48
|
+
def initialize(signature_dict)
|
|
49
|
+
super
|
|
50
|
+
@pkcs7 = OpenSSL::PKCS7.new(signature_dict.contents)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Returns the common name of the signer.
|
|
54
|
+
def signer_name
|
|
55
|
+
signer_certificate.subject.to_a.assoc("CN")&.[](1) || super
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Returns the time of signing.
|
|
59
|
+
def signing_time
|
|
60
|
+
signer_info.signed_time rescue super
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Returns the certificate chain.
|
|
64
|
+
def certificate_chain
|
|
65
|
+
@pkcs7.certificates
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Returns the signer certificate (an instance of OpenSSL::X509::Certificate).
|
|
69
|
+
def signer_certificate
|
|
70
|
+
info = signer_info
|
|
71
|
+
certificate_chain.find {|cert| cert.issuer == info.issuer && cert.serial == info.serial }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Returns the signer information object (an instance of OpenSSL::PKCS7::SignerInfo).
|
|
75
|
+
def signer_info
|
|
76
|
+
@pkcs7.signers.first
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Verifies the signature using the provided OpenSSL::X509::Store object.
|
|
80
|
+
def verify(store, allow_self_signed: false)
|
|
81
|
+
result = super
|
|
82
|
+
|
|
83
|
+
signer_info = self.signer_info
|
|
84
|
+
signer_certificate = self.signer_certificate
|
|
85
|
+
certificate_chain = self.certificate_chain
|
|
86
|
+
|
|
87
|
+
if certificate_chain.empty?
|
|
88
|
+
result.log(:error, "No certificates found in signature")
|
|
89
|
+
return result
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
if @pkcs7.signers.size != 1
|
|
93
|
+
result.log(:error, "Exactly one signer needed, found #{@pkcs7.signers.size}")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
unless signer_certificate
|
|
97
|
+
result.log(:error, "Signer serial=#{signer_info.serial} issuer=#{signer_info.issuer} " \
|
|
98
|
+
"not found in certificates stored in PKCS7 object")
|
|
99
|
+
return result
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
key_usage = signer_certificate.extensions.find {|ext| ext.oid == 'keyUsage' }
|
|
103
|
+
unless key_usage.value.split(', ').include?("Digital Signature")
|
|
104
|
+
result.log(:error, "Certificate key usage is missing 'Digital Signature'")
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
if @pkcs7.verify(certificate_chain, store, signature_dict.signed_data,
|
|
108
|
+
OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY)
|
|
109
|
+
result.log(:info, "Signature valid")
|
|
110
|
+
else
|
|
111
|
+
result.log(:error, "Signature verification failed")
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
result
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# -*- encoding: utf-8; frozen_string_literal: true -*-
|
|
2
|
+
#
|
|
3
|
+
#--
|
|
4
|
+
# This file is part of HexaPDF.
|
|
5
|
+
#
|
|
6
|
+
# HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
|
|
7
|
+
# Copyright (C) 2014-2021 Thomas Leitner
|
|
8
|
+
#
|
|
9
|
+
# HexaPDF is free software: you can redistribute it and/or modify it
|
|
10
|
+
# under the terms of the GNU Affero General Public License version 3 as
|
|
11
|
+
# published by the Free Software Foundation with the addition of the
|
|
12
|
+
# following permission added to Section 15 as permitted in Section 7(a):
|
|
13
|
+
# FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
|
|
14
|
+
# THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
|
|
15
|
+
# INFRINGEMENT OF THIRD PARTY RIGHTS.
|
|
16
|
+
#
|
|
17
|
+
# HexaPDF is distributed in the hope that it will be useful, but WITHOUT
|
|
18
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
19
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
|
20
|
+
# License for more details.
|
|
21
|
+
#
|
|
22
|
+
# You should have received a copy of the GNU Affero General Public License
|
|
23
|
+
# along with HexaPDF. If not, see <http://www.gnu.org/licenses/>.
|
|
24
|
+
#
|
|
25
|
+
# The interactive user interfaces in modified source and object code
|
|
26
|
+
# versions of HexaPDF must display Appropriate Legal Notices, as required
|
|
27
|
+
# under Section 5 of the GNU Affero General Public License version 3.
|
|
28
|
+
#
|
|
29
|
+
# In accordance with Section 7(b) of the GNU Affero General Public
|
|
30
|
+
# License, a covered work must retain the producer line in every PDF that
|
|
31
|
+
# is created or manipulated using HexaPDF.
|
|
32
|
+
#
|
|
33
|
+
# If the GNU Affero General Public License doesn't fit your need,
|
|
34
|
+
# commercial licenses are available at <https://gettalong.at/hexapdf/>.
|
|
35
|
+
#++
|
|
36
|
+
|
|
37
|
+
require 'openssl'
|
|
38
|
+
require 'hexapdf/type/signature'
|
|
39
|
+
|
|
40
|
+
module HexaPDF
|
|
41
|
+
module Type
|
|
42
|
+
class Signature
|
|
43
|
+
|
|
44
|
+
# The signature handler for the adbe.x509.rsa_sha1 sub-filter.
|
|
45
|
+
#
|
|
46
|
+
# Since this handler is deprecated with PDF 2.0 it only provides the implementation for
|
|
47
|
+
# reading and verifying signatures.
|
|
48
|
+
class AdbeX509RsaSha1 < Handler
|
|
49
|
+
|
|
50
|
+
# Returns the certificate chain.
|
|
51
|
+
def certificate_chain
|
|
52
|
+
return [] unless signature_dict.key?(:Cert)
|
|
53
|
+
[signature_dict[:Cert]].flatten.map {|str| OpenSSL::X509::Certificate.new(str) }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Returns the signer certificate (an instance of OpenSSL::X509::Certificate).
|
|
57
|
+
def signer_certificate
|
|
58
|
+
certificate_chain.first
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Verifies the signature using the provided OpenSSL::X509::Store object.
|
|
62
|
+
def verify(store, allow_self_signed: false)
|
|
63
|
+
result = super
|
|
64
|
+
|
|
65
|
+
signer_certificate = self.signer_certificate
|
|
66
|
+
certificate_chain = self.certificate_chain
|
|
67
|
+
|
|
68
|
+
if certificate_chain.empty?
|
|
69
|
+
result.log(:error, "No certificates for verification found")
|
|
70
|
+
return result
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
signature = OpenSSL::ASN1.decode(signature_dict.contents)
|
|
74
|
+
if signature.tag != OpenSSL::ASN1::OCTET_STRING
|
|
75
|
+
result.log(:error, "PKCS1 signature object invalid, octet string expected")
|
|
76
|
+
return result
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
store.verify(signer_certificate, certificate_chain)
|
|
80
|
+
|
|
81
|
+
if signer_certificate.public_key.verify(OpenSSL::Digest.new('SHA1'),
|
|
82
|
+
signature.value, signature_dict.signed_data)
|
|
83
|
+
result.log(:info, "Signature valid")
|
|
84
|
+
else
|
|
85
|
+
result.log(:error, "Signature verification failed")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
result
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# -*- encoding: utf-8; frozen_string_literal: true -*-
|
|
2
|
+
#
|
|
3
|
+
#--
|
|
4
|
+
# This file is part of HexaPDF.
|
|
5
|
+
#
|
|
6
|
+
# HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
|
|
7
|
+
# Copyright (C) 2014-2021 Thomas Leitner
|
|
8
|
+
#
|
|
9
|
+
# HexaPDF is free software: you can redistribute it and/or modify it
|
|
10
|
+
# under the terms of the GNU Affero General Public License version 3 as
|
|
11
|
+
# published by the Free Software Foundation with the addition of the
|
|
12
|
+
# following permission added to Section 15 as permitted in Section 7(a):
|
|
13
|
+
# FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
|
|
14
|
+
# THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
|
|
15
|
+
# INFRINGEMENT OF THIRD PARTY RIGHTS.
|
|
16
|
+
#
|
|
17
|
+
# HexaPDF is distributed in the hope that it will be useful, but WITHOUT
|
|
18
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
19
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
|
20
|
+
# License for more details.
|
|
21
|
+
#
|
|
22
|
+
# You should have received a copy of the GNU Affero General Public License
|
|
23
|
+
# along with HexaPDF. If not, see <http://www.gnu.org/licenses/>.
|
|
24
|
+
#
|
|
25
|
+
# The interactive user interfaces in modified source and object code
|
|
26
|
+
# versions of HexaPDF must display Appropriate Legal Notices, as required
|
|
27
|
+
# under Section 5 of the GNU Affero General Public License version 3.
|
|
28
|
+
#
|
|
29
|
+
# In accordance with Section 7(b) of the GNU Affero General Public
|
|
30
|
+
# License, a covered work must retain the producer line in every PDF that
|
|
31
|
+
# is created or manipulated using HexaPDF.
|
|
32
|
+
#
|
|
33
|
+
# If the GNU Affero General Public License doesn't fit your need,
|
|
34
|
+
# commercial licenses are available at <https://gettalong.at/hexapdf/>.
|
|
35
|
+
#++
|
|
36
|
+
|
|
37
|
+
require 'hexapdf/type/signature'
|
|
38
|
+
|
|
39
|
+
module HexaPDF
|
|
40
|
+
module Type
|
|
41
|
+
class Signature
|
|
42
|
+
|
|
43
|
+
# The base signature handler providing common functionality.
|
|
44
|
+
#
|
|
45
|
+
# Specific signature handler need to override methods if necessary and implement the needed
|
|
46
|
+
# ones that don't have a default implementation.
|
|
47
|
+
class Handler
|
|
48
|
+
|
|
49
|
+
# The signature dictionary used by the handler.
|
|
50
|
+
attr_reader :signature_dict
|
|
51
|
+
|
|
52
|
+
# Creates a new signature handler for the given signature dictionary.
|
|
53
|
+
def initialize(signature_dict)
|
|
54
|
+
@signature_dict = signature_dict
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Returns the common name of the signer (/Name field of the signature dictionary).
|
|
58
|
+
def signer_name
|
|
59
|
+
@signature_dict[:Name]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Returns the time of signing (/M field of the signature dictionary).
|
|
63
|
+
def signing_time
|
|
64
|
+
@signature_dict[:M]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Returns the certificate chain.
|
|
68
|
+
#
|
|
69
|
+
# Needs to be implemented by specific handlers.
|
|
70
|
+
def certificate_chain
|
|
71
|
+
raise "Needs to be implemented by specific handlers"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Returns the certificate used for signing.
|
|
75
|
+
#
|
|
76
|
+
# Needs to be implemented by specific handlers.
|
|
77
|
+
def signer_certificate
|
|
78
|
+
raise "Needs to be implemented by specific handlers"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Verifies general signature properties and prepares the provided OpenSSL::X509::Store
|
|
82
|
+
# object for use by concrete implementations.
|
|
83
|
+
#
|
|
84
|
+
# Needs to be called by specific handlers.
|
|
85
|
+
def verify(store, allow_self_signed: false)
|
|
86
|
+
result = VerificationResult.new
|
|
87
|
+
check_certified_signature(result)
|
|
88
|
+
verify_signing_time(result)
|
|
89
|
+
store.verify_callback =
|
|
90
|
+
store_verification_callback(result, allow_self_signed: allow_self_signed)
|
|
91
|
+
result
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
protected
|
|
95
|
+
|
|
96
|
+
# Verifies that the signing time was within the validity period of the signer certificate.
|
|
97
|
+
def verify_signing_time(result)
|
|
98
|
+
time = signing_time
|
|
99
|
+
cert = signer_certificate
|
|
100
|
+
if time && cert && (time < cert.not_before || time > cert.not_after)
|
|
101
|
+
result.log(:error, "Signer certificate not valid at signing time")
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
DOCMDP_PERMS_MESSAGE_MAP = { # :nodoc:
|
|
106
|
+
1 => "No changes allowed",
|
|
107
|
+
2 => "Form filling and signing allowed",
|
|
108
|
+
3 => "Form filling, signing and annotation manipulation allowed",
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# Sets an informational message on +result+ whether the signature is a certified signature.
|
|
112
|
+
def check_certified_signature(result)
|
|
113
|
+
sigref = signature_dict[:Reference]&.find {|ref| ref[:TransformMethod] == :DocMDP }
|
|
114
|
+
if sigref && signature_dict.document.catalog[:Perms]&.[](:DocMDP) == signature_dict
|
|
115
|
+
perms = sigref[:TransformParams]&.[](:P) || 2
|
|
116
|
+
result.log(:info, "Certified signature (#{DOCMDP_PERMS_MESSAGE_MAP[perms]})")
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Returns the block that should be used as the OpenSSL::X509::Store verification callback.
|
|
121
|
+
#
|
|
122
|
+
# +result+:: The VerificationResult object that should be updated if problems are found.
|
|
123
|
+
#
|
|
124
|
+
# +allow_self_signed+:: Specifies whether self-signed certificates are allowed.
|
|
125
|
+
def store_verification_callback(result, allow_self_signed: false)
|
|
126
|
+
lambda do |_success, context|
|
|
127
|
+
if context.error == OpenSSL::X509::V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT ||
|
|
128
|
+
context.error == OpenSSL::X509::V_ERR_SELF_SIGNED_CERT_IN_CHAIN
|
|
129
|
+
result.log(allow_self_signed ? :info : :error, "Self-signed certificate found")
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
true
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# -*- encoding: utf-8; frozen_string_literal: true -*-
|
|
2
|
+
#
|
|
3
|
+
#--
|
|
4
|
+
# This file is part of HexaPDF.
|
|
5
|
+
#
|
|
6
|
+
# HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
|
|
7
|
+
# Copyright (C) 2014-2021 Thomas Leitner
|
|
8
|
+
#
|
|
9
|
+
# HexaPDF is free software: you can redistribute it and/or modify it
|
|
10
|
+
# under the terms of the GNU Affero General Public License version 3 as
|
|
11
|
+
# published by the Free Software Foundation with the addition of the
|
|
12
|
+
# following permission added to Section 15 as permitted in Section 7(a):
|
|
13
|
+
# FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
|
|
14
|
+
# THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
|
|
15
|
+
# INFRINGEMENT OF THIRD PARTY RIGHTS.
|
|
16
|
+
#
|
|
17
|
+
# HexaPDF is distributed in the hope that it will be useful, but WITHOUT
|
|
18
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
19
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
|
20
|
+
# License for more details.
|
|
21
|
+
#
|
|
22
|
+
# You should have received a copy of the GNU Affero General Public License
|
|
23
|
+
# along with HexaPDF. If not, see <http://www.gnu.org/licenses/>.
|
|
24
|
+
#
|
|
25
|
+
# The interactive user interfaces in modified source and object code
|
|
26
|
+
# versions of HexaPDF must display Appropriate Legal Notices, as required
|
|
27
|
+
# under Section 5 of the GNU Affero General Public License version 3.
|
|
28
|
+
#
|
|
29
|
+
# In accordance with Section 7(b) of the GNU Affero General Public
|
|
30
|
+
# License, a covered work must retain the producer line in every PDF that
|
|
31
|
+
# is created or manipulated using HexaPDF.
|
|
32
|
+
#
|
|
33
|
+
# If the GNU Affero General Public License doesn't fit your need,
|
|
34
|
+
# commercial licenses are available at <https://gettalong.at/hexapdf/>.
|
|
35
|
+
#++
|
|
36
|
+
|
|
37
|
+
require 'hexapdf/type/signature'
|
|
38
|
+
|
|
39
|
+
module HexaPDF
|
|
40
|
+
module Type
|
|
41
|
+
class Signature
|
|
42
|
+
|
|
43
|
+
# Holds the result information when verifying a signature.
|
|
44
|
+
class VerificationResult
|
|
45
|
+
|
|
46
|
+
# :nodoc:
|
|
47
|
+
MESSAGE_SORT_MAP = {
|
|
48
|
+
info: {warning: 1, error: 1, info: 0},
|
|
49
|
+
warning: {info: -1, error: 1, warning: 0},
|
|
50
|
+
error: {info: -1, warning: -1, error: 0},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# This structure represents a single status message, containing the type (:info, :warning,
|
|
54
|
+
# :error) and the content of the message.
|
|
55
|
+
Message = Struct.new(:type, :content) do
|
|
56
|
+
def <=>(other)
|
|
57
|
+
MESSAGE_SORT_MAP[type][other.type]
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# An array with all result messages.
|
|
62
|
+
attr_reader :messages
|
|
63
|
+
|
|
64
|
+
# Creates an empty result object.
|
|
65
|
+
def initialize
|
|
66
|
+
@messages = []
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Returns +true+ if there are no error messages.
|
|
70
|
+
def success?
|
|
71
|
+
@messages.none? {|message| message.type == :error }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Returns +true+ if there is at least one error message.
|
|
75
|
+
def failure?
|
|
76
|
+
!success?
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Adds a new message of the given type to this result object.
|
|
80
|
+
#
|
|
81
|
+
# +type+:: One of :info, :warning or :error.
|
|
82
|
+
#
|
|
83
|
+
# +content+:: The log message.
|
|
84
|
+
def log(type, content)
|
|
85
|
+
@messages << Message.new(type, content)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|