hexapdf 0.28.0 → 0.29.0

Sign up to get free protection for your applications and to get access to all the features.
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