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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -10
  3. data/examples/024-digital-signatures.rb +23 -0
  4. data/lib/hexapdf/configuration.rb +12 -12
  5. data/lib/hexapdf/dictionary_fields.rb +6 -2
  6. data/lib/hexapdf/digital_signature/cms_handler.rb +137 -0
  7. data/lib/hexapdf/digital_signature/handler.rb +138 -0
  8. data/lib/hexapdf/digital_signature/pkcs1_handler.rb +96 -0
  9. data/lib/hexapdf/{type → digital_signature}/signature.rb +3 -8
  10. data/lib/hexapdf/digital_signature/signatures.rb +210 -0
  11. data/lib/hexapdf/digital_signature/signing/default_handler.rb +317 -0
  12. data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +308 -0
  13. data/lib/hexapdf/digital_signature/signing/timestamp_handler.rb +148 -0
  14. data/lib/hexapdf/digital_signature/signing.rb +101 -0
  15. data/lib/hexapdf/{type/signature → digital_signature}/verification_result.rb +37 -41
  16. data/lib/hexapdf/digital_signature.rb +56 -0
  17. data/lib/hexapdf/document.rb +21 -14
  18. data/lib/hexapdf/encryption/standard_security_handler.rb +2 -1
  19. data/lib/hexapdf/type.rb +0 -1
  20. data/lib/hexapdf/version.rb +1 -1
  21. data/test/hexapdf/{type/signature → digital_signature}/common.rb +31 -3
  22. data/test/hexapdf/digital_signature/signing/test_default_handler.rb +162 -0
  23. data/test/hexapdf/digital_signature/signing/test_signed_data_creator.rb +225 -0
  24. data/test/hexapdf/digital_signature/signing/test_timestamp_handler.rb +88 -0
  25. data/test/hexapdf/{type/signature/test_adbe_pkcs7_detached.rb → digital_signature/test_cms_handler.rb} +7 -7
  26. data/test/hexapdf/{type/signature → digital_signature}/test_handler.rb +4 -4
  27. data/test/hexapdf/{type/signature/test_adbe_x509_rsa_sha1.rb → digital_signature/test_pkcs1_handler.rb} +3 -3
  28. data/test/hexapdf/{type → digital_signature}/test_signature.rb +7 -7
  29. data/test/hexapdf/digital_signature/test_signatures.rb +137 -0
  30. data/test/hexapdf/digital_signature/test_signing.rb +53 -0
  31. data/test/hexapdf/{type/signature → digital_signature}/test_verification_result.rb +7 -7
  32. data/test/hexapdf/test_dictionary_fields.rb +2 -1
  33. data/test/hexapdf/test_document.rb +1 -1
  34. data/test/hexapdf/test_writer.rb +3 -3
  35. metadata +25 -15
  36. data/lib/hexapdf/document/signatures.rb +0 -546
  37. data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +0 -135
  38. data/lib/hexapdf/type/signature/adbe_x509_rsa_sha1.rb +0 -95
  39. data/lib/hexapdf/type/signature/handler.rb +0 -140
  40. data/test/hexapdf/document/test_signatures.rb +0 -352
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 874e09b094ea4e793d1d123cfbaded6d1cc5ba93af3b57587e9faf402786a30f
4
- data.tar.gz: c1eed6a778936cd360b4f1878a18abc3e5727c1f36b5eab4838a9a72817dff7b
3
+ metadata.gz: 1fdace78c8d34d39c345e2ccd04edda4755e2fc8076cc7320793a4ef16f48520
4
+ data.tar.gz: a0ec03dc2d579eb8663512ec0c84cadbc877b9ec43cabb8ea0d6ab58df337585
5
5
  SHA512:
6
- metadata.gz: b66a7587a239acbeb9ebbb20f851b2fa7738c5a4c2f8a95ae7ae3d7419b84ae37b5d1fb48a6b7023bff0df3e1728c395b5351c9c42c05707fabd5a1722e2b88a
7
- data.tar.gz: 62ac7d070bb8ae3426685af497a047096dd32dc16a102baa961512652222b388198561776fc1227be69bdd0713183d91fa25e411262eec34a29227aae9723d5c
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
- [HexaPDF::Document::Signatures::TimestampHandler]
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
- * [HexaPDF::Document::Signatures::DefaultHandler#signature_size] to support
100
+ * `HexaPDF::Document::Signatures::DefaultHandler#signature_size` to support
70
101
  setting custom signature sizes
71
- * [HexaPDF::Document::Signatures::DefaultHandler#external_signing] to support
102
+ * `HexaPDF::Document::Signatures::DefaultHandler#external_signing` to support
72
103
  signing via custom mechanisms
73
- * [HexaPDF::Document::Signatures::embed_signature] to enable asynchronous
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
- * [HexaPDF::Document::Signatures::DefaultHandler] to allow setting the used
111
+ * `HexaPDF::Document::Signatures::DefaultHandler` to allow setting the used
81
112
  signature method
82
- * **Breaking change**: [HexaPDF::Document::Signatures::DefaultHandler#sign]
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
- [HexaPDF::Document::Signatures::DefaultHandler]
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
- * [HexaPDF::Document::Signatures#add] to raise an error if the reserved space
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
- * [HexaPDF::Document::Signatures#add] to work in case the signature object is
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
- * [HexaPDF::Document::Signatures#add] to use binary writing to avoid problems on
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::Type::Signature::Handler). If the value is a String, it should contain the name of a
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::Document::Signatures::DefaultHandler',
490
- timestamp: 'HexaPDF::Document::Signatures::TimestampHandler',
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::Type::Signature::AdbeX509RsaSha1',
494
- 'adbe.pkcs7.detached': 'HexaPDF::Type::Signature::AdbePkcs7Detached',
495
- 'ETSI.CAdES.detached': 'HexaPDF::Type::Signature::AdbePkcs7Detached',
496
- 'ETSI.RFC3161': 'HexaPDF::Type::Signature::AdbePkcs7Detached',
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::Type::Signature',
587
- DocTimeStamp: 'HexaPDF::Type::Signature',
588
- SigRef: 'HexaPDF::Type::Signature::SignatureReference',
589
- TransformParams: 'HexaPDF::Type::Signature::TransformParams',
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\d)(?:'|'([0-5]\d)'?|\z)?)?\z/n
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 = (m[7].nil? || m[7] == 'Z' ? 0 : "#{m[7]}#{m[8]}:#{m[9] || '00'}")
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 Type
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 s12.8.1, PDF2.0 s12.8.1, HexaPDF::Type::AcroForm::SignatureField
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 s12.8.1, PDF2.0 s12.8.1, HexaPDF::Type::Signature
120
+ # See: PDF1.7/2.0 s12.8.1, HexaPDF::DigitalSignature::Signature
126
121
  class SignatureReference < Dictionary
127
122
 
128
123
  define_type :SigRef