hexapdf 0.28.0 → 0.29.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 +41 -10
- data/examples/024-digital-signatures.rb +23 -0
- data/lib/hexapdf/configuration.rb +12 -12
- data/lib/hexapdf/dictionary_fields.rb +6 -2
- 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.rb +21 -14
- data/lib/hexapdf/encryption/standard_security_handler.rb +2 -1
- data/lib/hexapdf/type.rb +0 -1
- data/lib/hexapdf/version.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/test_dictionary_fields.rb +2 -1
- data/test/hexapdf/test_document.rb +1 -1
- data/test/hexapdf/test_writer.rb +3 -3
- metadata +25 -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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1fdace78c8d34d39c345e2ccd04edda4755e2fc8076cc7320793a4ef16f48520
|
4
|
+
data.tar.gz: a0ec03dc2d579eb8663512ec0c84cadbc877b9ec43cabb8ea0d6ab58df337585
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a8b18782359af03f0eda710658d0839554f0e95cd8068683dcaea56e8493c3b8a0b4cc30c9e39a3fdbe743df4d5e34b84ce4ab27125c8c14300b861ecbff16e9
|
7
|
+
data.tar.gz: 8d5c60a368d85fa9c2b118d955933943f5d0cf79e91d744348cf1e3f1c9435d44880033c844b58ab098c8ab8d1e2e7cd4c5e04710133b25543f31457277d0ba2
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,34 @@
|
|
1
|
+
## 0.29.0 - 2023-01-30
|
2
|
+
|
3
|
+
### Added
|
4
|
+
|
5
|
+
* [HexaPDF::DigitalSignature::Signing::SignedDataCreator] for creating custom
|
6
|
+
CMS signed data objects
|
7
|
+
|
8
|
+
### Changed
|
9
|
+
|
10
|
+
* **Breaking change**: Refactored digital signature support and moved all
|
11
|
+
related code under the [HexaPDF::DigitalSignature] module
|
12
|
+
* **Breaking change**: New external signing mode without the need for creating
|
13
|
+
the PKCS#7/CMS signed data object for
|
14
|
+
[HexaPDF::DigitalSignature::Signing::DefaultHandler]
|
15
|
+
* **Breaking change**: Use value :pades instead of :etsi for
|
16
|
+
[HexaPDF::DigitalSignature::Signing::DefaultHandler#signature_type]
|
17
|
+
* [HexaPDF::DigitalSignature::Signing::DefaultHandler] to allow creating PAdES
|
18
|
+
level B-B and B-T signatures
|
19
|
+
* [HexaPDF::DigitalSignature::Signing::DefaultHandler] to allow specifying the
|
20
|
+
used digest algorithm
|
21
|
+
* [HexaPDF::DigitalSignature::Signing::DefaultHandler] to allow specifying a
|
22
|
+
timestamp handler for including a timestamp token in the signature
|
23
|
+
* Moved setting of signature entries /Filter, /SubFilter and /M fields to the
|
24
|
+
signing handlers
|
25
|
+
|
26
|
+
### Fixed
|
27
|
+
|
28
|
+
* [HexaPDF::DictionaryFields::DateConverter] to handle invalid timezone hour and
|
29
|
+
minute values
|
30
|
+
|
31
|
+
|
1
32
|
## 0.28.0 - 2022-12-30
|
2
33
|
|
3
34
|
### Added
|
@@ -61,30 +92,30 @@
|
|
61
92
|
### Added
|
62
93
|
|
63
94
|
* Support for timestamp signatures through the
|
64
|
-
|
95
|
+
`HexaPDF::Document::Signatures::TimestampHandler`
|
65
96
|
* [HexaPDF::Document::Destinations#resolve] for resolving destination values
|
66
97
|
* [HexaPDF::Document::Destinations::Destination#value] to return the destination
|
67
98
|
array
|
68
99
|
* Support for verifying document timestamp signatures
|
69
|
-
*
|
100
|
+
* `HexaPDF::Document::Signatures::DefaultHandler#signature_size` to support
|
70
101
|
setting custom signature sizes
|
71
|
-
*
|
102
|
+
* `HexaPDF::Document::Signatures::DefaultHandler#external_signing` to support
|
72
103
|
signing via custom mechanisms
|
73
|
-
*
|
104
|
+
* `HexaPDF::Document::Signatures::embed_signature` to enable asynchronous
|
74
105
|
external signing
|
75
106
|
|
76
107
|
### Changed
|
77
108
|
|
78
109
|
* **Breaking change**: The crop box is now used instead of the media box in most
|
79
110
|
cases to be in line with the specification
|
80
|
-
*
|
111
|
+
* `HexaPDF::Document::Signatures::DefaultHandler` to allow setting the used
|
81
112
|
signature method
|
82
|
-
* **Breaking change**:
|
113
|
+
* **Breaking change**: `HexaPDF::Document::Signatures::DefaultHandler#sign`
|
83
114
|
needs to accept the IO object and the byte range instead of just the data
|
84
115
|
* **Breaking change**: Enhanced support for outline items with new methods
|
85
116
|
`#level` and `#destination_page` as well as changes to `#add` and `#each_item`
|
86
117
|
* **Breaking change**: Removed `#filter_name` and `#sub_filter_name` from
|
87
|
-
|
118
|
+
`HexaPDF::Document::Signatures::DefaultHandler`
|
88
119
|
* `HexaPDF::Type::Resources#perform_validation` to not add a default procedure
|
89
120
|
set since this feature is deprecated
|
90
121
|
|
@@ -101,7 +132,7 @@
|
|
101
132
|
* [HexaPDF::Type::OutlineItem] to always be an indirect object
|
102
133
|
* `HexaPDF::Tokenizer#parse_number` to handle references correctly in all cases
|
103
134
|
* [HexaPDF::Type::Page#rotate] to correctly flatten all page boxes
|
104
|
-
*
|
135
|
+
* `HexaPDF::Document::Signatures#add` to raise an error if the reserved space
|
105
136
|
for the signature is not enough
|
106
137
|
* `HexaPDF::Type::AcroForm::Form#perform_validation` to fix broken /Parent
|
107
138
|
entries and to remove invalid objects from the field hierarchy
|
@@ -276,7 +307,7 @@
|
|
276
307
|
moved node doesn't change
|
277
308
|
* [HexaPDF::Type::PageTreeNode#move_page] to use the correct target position
|
278
309
|
when the moved node is before the target position
|
279
|
-
*
|
310
|
+
* `HexaPDF::Document::Signatures#add` to work in case the signature object is
|
280
311
|
the last object written
|
281
312
|
* CLI command `hexapdf inspect` to show correct byte range of the last revision
|
282
313
|
* [HexaPDF::Writer#write_incremental] to only use a cross-reference stream if a
|
@@ -285,7 +316,7 @@
|
|
285
316
|
disabled
|
286
317
|
* [HexaPDF::Font::Encoding::GlyphList] to use binary reading to avoid problems
|
287
318
|
on Windows
|
288
|
-
*
|
319
|
+
* `HexaPDF::Document::Signatures#add` to use binary writing to avoid problems on
|
289
320
|
Windows
|
290
321
|
|
291
322
|
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# # Images
|
2
|
+
#
|
3
|
+
# This example shows how to embed images into a PDF document, directly on a
|
4
|
+
# page's canvas and through the high-level [HexaPDF::Composer].
|
5
|
+
#
|
6
|
+
# Usage:
|
7
|
+
# : `ruby digital-signatures.rb`
|
8
|
+
#
|
9
|
+
|
10
|
+
require 'hexapdf'
|
11
|
+
require HexaPDF.data_dir + '/cert/demo_cert.rb'
|
12
|
+
|
13
|
+
doc = if ARGV[0]
|
14
|
+
HexaPDF::Document.open(ARGV[0])
|
15
|
+
else
|
16
|
+
HexaPDF::Document.new.pages.add.document
|
17
|
+
end
|
18
|
+
doc.sign("digital-signatures.pdf",
|
19
|
+
reason: 'Some reason',
|
20
|
+
certificate: HexaPDF.demo_cert.cert,
|
21
|
+
key: HexaPDF.demo_cert.key,
|
22
|
+
certificate_chain: [HexaPDF.demo_cert.sub_ca,
|
23
|
+
HexaPDF.demo_cert.root_ca])
|
@@ -393,8 +393,8 @@ module HexaPDF
|
|
393
393
|
#
|
394
394
|
# signature.sub_filter_map::
|
395
395
|
# A mapping from a PDF name (a Symbol) to a signature handler class (see
|
396
|
-
# HexaPDF::
|
397
|
-
# constant to such a class.
|
396
|
+
# HexaPDF::DigitalSignature::Handler). If the value is a String, it should contain the name of
|
397
|
+
# a constant to such a class.
|
398
398
|
#
|
399
399
|
# The sub filter map is used for mapping specific signature algorithms to handler classes. The
|
400
400
|
# filter value of a signature dictionary is ignored since we only support the standard
|
@@ -486,14 +486,14 @@ module HexaPDF
|
|
486
486
|
link: 'HexaPDF::Layout::Style::LinkLayer',
|
487
487
|
},
|
488
488
|
'signature.signing_handler' => {
|
489
|
-
default: 'HexaPDF::
|
490
|
-
timestamp: 'HexaPDF::
|
489
|
+
default: 'HexaPDF::DigitalSignature::Signing::DefaultHandler',
|
490
|
+
timestamp: 'HexaPDF::DigitalSignature::Signing::TimestampHandler',
|
491
491
|
},
|
492
492
|
'signature.sub_filter_map' => {
|
493
|
-
'adbe.x509.rsa_sha1': 'HexaPDF::
|
494
|
-
'adbe.pkcs7.detached': 'HexaPDF::
|
495
|
-
'ETSI.CAdES.detached': 'HexaPDF::
|
496
|
-
'ETSI.RFC3161': 'HexaPDF::
|
493
|
+
'adbe.x509.rsa_sha1': 'HexaPDF::DigitalSignature::PKCS1Handler',
|
494
|
+
'adbe.pkcs7.detached': 'HexaPDF::DigitalSignature::CMSHandler',
|
495
|
+
'ETSI.CAdES.detached': 'HexaPDF::DigitalSignature::CMSHandler',
|
496
|
+
'ETSI.RFC3161': 'HexaPDF::DigitalSignature::CMSHandler',
|
497
497
|
},
|
498
498
|
'task.map' => {
|
499
499
|
optimize: 'HexaPDF::Task::Optimize',
|
@@ -583,10 +583,10 @@ module HexaPDF
|
|
583
583
|
SigFieldLock: 'HexaPDF::Type::AcroForm::SignatureField::LockDictionary',
|
584
584
|
SV: 'HexaPDF::Type::AcroForm::SignatureField::SeedValueDictionary',
|
585
585
|
SVCert: 'HexaPDF::Type::AcroForm::SignatureField::CertificateSeedValueDictionary',
|
586
|
-
Sig: 'HexaPDF::
|
587
|
-
DocTimeStamp: 'HexaPDF::
|
588
|
-
SigRef: 'HexaPDF::
|
589
|
-
TransformParams: 'HexaPDF::
|
586
|
+
Sig: 'HexaPDF::DigitalSignature::Signature',
|
587
|
+
DocTimeStamp: 'HexaPDF::DigitalSignature::Signature',
|
588
|
+
SigRef: 'HexaPDF::DigitalSignature::Signature::SignatureReference',
|
589
|
+
TransformParams: 'HexaPDF::DigitalSignature::Signature::TransformParams',
|
590
590
|
Outlines: 'HexaPDF::Type::Outline',
|
591
591
|
XXOutlineItem: 'HexaPDF::Type::OutlineItem',
|
592
592
|
PageLabel: 'HexaPDF::Type::PageLabel',
|
@@ -293,14 +293,18 @@ module HexaPDF
|
|
293
293
|
end
|
294
294
|
|
295
295
|
# :nodoc:
|
296
|
-
DATE_RE = /\AD:(\d{4})(\d\d)?(\d\d)?(\d\d)?(\d\d)?(\d\d)?([Z+-])?(?:(\d
|
296
|
+
DATE_RE = /\AD:(\d{4})(\d\d)?(\d\d)?(\d\d)?(\d\d)?(\d\d)?([Z+-])?(?:(\d+)(?:'|'(\d+)'?|\z)?)?\z/n
|
297
297
|
|
298
298
|
# Checks if the given object is a string and converts into a Time object if possible.
|
299
299
|
# Otherwise returns +nil+.
|
300
300
|
def self.convert(str, _type, _document)
|
301
301
|
return unless str.kind_of?(String) && (m = str.match(DATE_RE))
|
302
302
|
|
303
|
-
utc_offset =
|
303
|
+
utc_offset = if m[7].nil? || m[7] == 'Z'
|
304
|
+
0
|
305
|
+
else
|
306
|
+
(m[7] == '-' ? -1 : 1) * (m[8].to_i * 3600 + m[9].to_i * 60).clamp(0, 86399)
|
307
|
+
end
|
304
308
|
Time.new(m[1].to_i, (m[2] ? m[2].to_i : 1), (m[3] ? m[3].to_i : 1),
|
305
309
|
m[4].to_i, m[5].to_i, m[6].to_i, utc_offset)
|
306
310
|
end
|
@@ -0,0 +1,137 @@
|
|
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 'hexapdf/digital_signature/handler'
|
39
|
+
|
40
|
+
module HexaPDF
|
41
|
+
module DigitalSignature
|
42
|
+
|
43
|
+
# The signature handler for PKCS#7 a.k.a. CMS signatures. Those include, for example, the
|
44
|
+
# adbe.pkcs7.detached sub-filter.
|
45
|
+
#
|
46
|
+
# See: PDF1.7/2.0 s12.8.3.3
|
47
|
+
class CMSHandler < Handler
|
48
|
+
|
49
|
+
# Creates a new signature handler for the given signature dictionary.
|
50
|
+
def initialize(signature_dict)
|
51
|
+
super
|
52
|
+
@pkcs7 = OpenSSL::PKCS7.new(signature_dict.contents)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Returns the common name of the signer.
|
56
|
+
def signer_name
|
57
|
+
signer_certificate.subject.to_a.assoc("CN")&.[](1) || super
|
58
|
+
end
|
59
|
+
|
60
|
+
# Returns the time of signing.
|
61
|
+
def signing_time
|
62
|
+
signer_info.signed_time rescue super
|
63
|
+
end
|
64
|
+
|
65
|
+
# Returns the certificate chain.
|
66
|
+
def certificate_chain
|
67
|
+
@pkcs7.certificates
|
68
|
+
end
|
69
|
+
|
70
|
+
# Returns the signer certificate (an instance of OpenSSL::X509::Certificate).
|
71
|
+
def signer_certificate
|
72
|
+
info = signer_info
|
73
|
+
certificate_chain.find {|cert| cert.issuer == info.issuer && cert.serial == info.serial }
|
74
|
+
end
|
75
|
+
|
76
|
+
# Returns the signer information object (an instance of OpenSSL::PKCS7::SignerInfo).
|
77
|
+
def signer_info
|
78
|
+
@pkcs7.signers.first
|
79
|
+
end
|
80
|
+
|
81
|
+
# Verifies the signature using the provided OpenSSL::X509::Store object.
|
82
|
+
def verify(store, allow_self_signed: false)
|
83
|
+
result = super
|
84
|
+
|
85
|
+
signer_info = self.signer_info
|
86
|
+
signer_certificate = self.signer_certificate
|
87
|
+
certificate_chain = self.certificate_chain
|
88
|
+
|
89
|
+
if certificate_chain.empty?
|
90
|
+
result.log(:error, "No certificates found in signature")
|
91
|
+
return result
|
92
|
+
end
|
93
|
+
|
94
|
+
if @pkcs7.signers.size != 1
|
95
|
+
result.log(:error, "Exactly one signer needed, found #{@pkcs7.signers.size}")
|
96
|
+
end
|
97
|
+
|
98
|
+
unless signer_certificate
|
99
|
+
result.log(:error, "Signer serial=#{signer_info.serial} issuer=#{signer_info.issuer} " \
|
100
|
+
"not found in certificates stored in PKCS7 object")
|
101
|
+
return result
|
102
|
+
end
|
103
|
+
|
104
|
+
key_usage = signer_certificate.extensions.find {|ext| ext.oid == 'keyUsage' }
|
105
|
+
unless key_usage && key_usage.value.split(', ').include?("Digital Signature")
|
106
|
+
result.log(:error, "Certificate key usage is missing 'Digital Signature'")
|
107
|
+
end
|
108
|
+
|
109
|
+
if signature_dict.signature_type == 'ETSI.RFC3161'
|
110
|
+
# Getting the needed values is not directly supported by Ruby OpenSSL
|
111
|
+
p7 = OpenSSL::ASN1.decode(signature_dict.contents.sub(/\x00*\z/, ''))
|
112
|
+
signed_data = p7.value[1].value[0]
|
113
|
+
content_info = signed_data.value[2]
|
114
|
+
content = OpenSSL::ASN1.decode(content_info.value[1].value[0].value)
|
115
|
+
digest_algorithm = content.value[2].value[0].value[0].value
|
116
|
+
original_hash = content.value[2].value[1].value
|
117
|
+
recomputed_hash = OpenSSL::Digest.digest(digest_algorithm, signature_dict.signed_data)
|
118
|
+
hash_valid = (original_hash == recomputed_hash)
|
119
|
+
else
|
120
|
+
data = signature_dict.signed_data
|
121
|
+
hash_valid = true # hash will be checked by @pkcs7.verify
|
122
|
+
end
|
123
|
+
if hash_valid && @pkcs7.verify(certificate_chain, store, data,
|
124
|
+
OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY)
|
125
|
+
result.log(:info, "Signature valid")
|
126
|
+
else
|
127
|
+
result.log(:error, "Signature verification failed")
|
128
|
+
end
|
129
|
+
|
130
|
+
result
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
@@ -0,0 +1,138 @@
|
|
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/digital_signature/verification_result'
|
38
|
+
|
39
|
+
module HexaPDF
|
40
|
+
module DigitalSignature
|
41
|
+
|
42
|
+
# The base signature handler providing common functionality.
|
43
|
+
#
|
44
|
+
# Specific signature handler need to override methods if necessary and implement the needed
|
45
|
+
# ones that don't have a default implementation.
|
46
|
+
class Handler
|
47
|
+
|
48
|
+
# The signature dictionary used by the handler.
|
49
|
+
attr_reader :signature_dict
|
50
|
+
|
51
|
+
# Creates a new signature handler for the given signature dictionary.
|
52
|
+
def initialize(signature_dict)
|
53
|
+
@signature_dict = signature_dict
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns the common name of the signer (/Name field of the signature dictionary).
|
57
|
+
def signer_name
|
58
|
+
@signature_dict[:Name]
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns the time of signing (/M field of the signature dictionary).
|
62
|
+
def signing_time
|
63
|
+
@signature_dict[:M]
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns the certificate chain.
|
67
|
+
#
|
68
|
+
# Needs to be implemented by specific handlers.
|
69
|
+
def certificate_chain
|
70
|
+
raise "Needs to be implemented by specific handlers"
|
71
|
+
end
|
72
|
+
|
73
|
+
# Returns the certificate used for signing.
|
74
|
+
#
|
75
|
+
# Needs to be implemented by specific handlers.
|
76
|
+
def signer_certificate
|
77
|
+
raise "Needs to be implemented by specific handlers"
|
78
|
+
end
|
79
|
+
|
80
|
+
# Verifies general signature properties and prepares the provided OpenSSL::X509::Store
|
81
|
+
# object for use by concrete implementations.
|
82
|
+
#
|
83
|
+
# Needs to be called by specific handlers.
|
84
|
+
def verify(store, allow_self_signed: false)
|
85
|
+
result = VerificationResult.new
|
86
|
+
check_certified_signature(result)
|
87
|
+
verify_signing_time(result)
|
88
|
+
store.verify_callback =
|
89
|
+
store_verification_callback(result, allow_self_signed: allow_self_signed)
|
90
|
+
result
|
91
|
+
end
|
92
|
+
|
93
|
+
protected
|
94
|
+
|
95
|
+
# Verifies that the signing time was within the validity period of the signer certificate.
|
96
|
+
def verify_signing_time(result)
|
97
|
+
time = signing_time
|
98
|
+
cert = signer_certificate
|
99
|
+
if time && cert && (time < cert.not_before || time > cert.not_after)
|
100
|
+
result.log(:error, "Signer certificate not valid at signing time")
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
DOCMDP_PERMS_MESSAGE_MAP = { # :nodoc:
|
105
|
+
1 => "No changes allowed",
|
106
|
+
2 => "Form filling and signing allowed",
|
107
|
+
3 => "Form filling, signing and annotation manipulation allowed",
|
108
|
+
}
|
109
|
+
|
110
|
+
# Sets an informational message on +result+ whether the signature is a certified signature.
|
111
|
+
def check_certified_signature(result)
|
112
|
+
sigref = signature_dict[:Reference]&.find {|ref| ref[:TransformMethod] == :DocMDP }
|
113
|
+
if sigref && signature_dict.document.catalog[:Perms]&.[](:DocMDP) == signature_dict
|
114
|
+
perms = sigref[:TransformParams]&.[](:P) || 2
|
115
|
+
result.log(:info, "Certified signature (#{DOCMDP_PERMS_MESSAGE_MAP[perms]})")
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Returns the block that should be used as the OpenSSL::X509::Store verification callback.
|
120
|
+
#
|
121
|
+
# +result+:: The VerificationResult object that should be updated if problems are found.
|
122
|
+
#
|
123
|
+
# +allow_self_signed+:: Specifies whether self-signed certificates are allowed.
|
124
|
+
def store_verification_callback(result, allow_self_signed: false)
|
125
|
+
lambda do |_success, context|
|
126
|
+
if context.error == OpenSSL::X509::V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT ||
|
127
|
+
context.error == OpenSSL::X509::V_ERR_SELF_SIGNED_CERT_IN_CHAIN
|
128
|
+
result.log(allow_self_signed ? :info : :error, "Self-signed certificate found")
|
129
|
+
end
|
130
|
+
|
131
|
+
true
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,96 @@
|
|
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 'hexapdf/digital_signature/handler'
|
39
|
+
|
40
|
+
module HexaPDF
|
41
|
+
module DigitalSignature
|
42
|
+
|
43
|
+
# The signature handler for PKCS#1 based sub-filters, the only being the adbe.x509.rsa_sha1
|
44
|
+
# sub-filter.
|
45
|
+
#
|
46
|
+
# Since PKCS#1 signatures are deprecated with PDF 2.0, the handler only provides the
|
47
|
+
# implementation for reading and verifying signatures.
|
48
|
+
#
|
49
|
+
# See: PDF1.7/2.0 s12.8.3.2
|
50
|
+
class PKCS1Handler < Handler
|
51
|
+
|
52
|
+
# Returns the certificate chain.
|
53
|
+
def certificate_chain
|
54
|
+
return [] unless signature_dict.key?(:Cert)
|
55
|
+
[signature_dict[:Cert]].flatten.map {|str| OpenSSL::X509::Certificate.new(str) }
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns the signer certificate (an instance of OpenSSL::X509::Certificate).
|
59
|
+
def signer_certificate
|
60
|
+
certificate_chain.first
|
61
|
+
end
|
62
|
+
|
63
|
+
# Verifies the signature using the provided OpenSSL::X509::Store object.
|
64
|
+
def verify(store, allow_self_signed: false)
|
65
|
+
result = super
|
66
|
+
|
67
|
+
signer_certificate = self.signer_certificate
|
68
|
+
certificate_chain = self.certificate_chain
|
69
|
+
|
70
|
+
if certificate_chain.empty?
|
71
|
+
result.log(:error, "No certificates for verification found")
|
72
|
+
return result
|
73
|
+
end
|
74
|
+
|
75
|
+
signature = OpenSSL::ASN1.decode(signature_dict.contents)
|
76
|
+
if signature.tag != OpenSSL::ASN1::OCTET_STRING
|
77
|
+
result.log(:error, "PKCS1 signature object invalid, octet string expected")
|
78
|
+
return result
|
79
|
+
end
|
80
|
+
|
81
|
+
store.verify(signer_certificate, certificate_chain)
|
82
|
+
|
83
|
+
if signer_certificate.public_key.verify(OpenSSL::Digest.new('SHA1'),
|
84
|
+
signature.value, signature_dict.signed_data)
|
85
|
+
result.log(:info, "Signature valid")
|
86
|
+
else
|
87
|
+
result.log(:error, "Signature verification failed")
|
88
|
+
end
|
89
|
+
|
90
|
+
result
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
end
|
@@ -39,7 +39,7 @@ require 'hexapdf/dictionary'
|
|
39
39
|
require 'hexapdf/error'
|
40
40
|
|
41
41
|
module HexaPDF
|
42
|
-
module
|
42
|
+
module DigitalSignature
|
43
43
|
|
44
44
|
# Represents a digital signature that is used to authenticate a user and the contents of the
|
45
45
|
# document.
|
@@ -53,14 +53,9 @@ module HexaPDF
|
|
53
53
|
# By defining a custom signature handler one is able to also customize the signature
|
54
54
|
# verification.
|
55
55
|
#
|
56
|
-
# See: PDF1.7
|
56
|
+
# See: PDF1.7/2.0 s12.8.1, HexaPDF::Type::AcroForm::SignatureField
|
57
57
|
class Signature < Dictionary
|
58
58
|
|
59
|
-
autoload :Handler, 'hexapdf/type/signature/handler'
|
60
|
-
autoload :AdbeX509RsaSha1, 'hexapdf/type/signature/adbe_x509_rsa_sha1'
|
61
|
-
autoload :AdbePkcs7Detached, 'hexapdf/type/signature/adbe_pkcs7_detached'
|
62
|
-
autoload :VerificationResult, 'hexapdf/type/signature/verification_result'
|
63
|
-
|
64
59
|
# Represents a transform parameters dictionary.
|
65
60
|
#
|
66
61
|
# The allowed fields depend on the transform method, so not all fields are available all the
|
@@ -122,7 +117,7 @@ module HexaPDF
|
|
122
117
|
|
123
118
|
# Represents a signature reference dictionary.
|
124
119
|
#
|
125
|
-
# See: PDF1.7
|
120
|
+
# See: PDF1.7/2.0 s12.8.1, HexaPDF::DigitalSignature::Signature
|
126
121
|
class SignatureReference < Dictionary
|
127
122
|
|
128
123
|
define_type :SigRef
|