hexapdf 0.28.0 → 0.30.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +59 -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 +13 -4
  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/pages.rb +35 -18
  18. data/lib/hexapdf/document.rb +21 -14
  19. data/lib/hexapdf/encryption/standard_security_handler.rb +4 -3
  20. data/lib/hexapdf/type/font_simple.rb +14 -2
  21. data/lib/hexapdf/type.rb +0 -1
  22. data/lib/hexapdf/version.rb +1 -1
  23. data/test/hexapdf/{type/signature → digital_signature}/common.rb +31 -3
  24. data/test/hexapdf/digital_signature/signing/test_default_handler.rb +162 -0
  25. data/test/hexapdf/digital_signature/signing/test_signed_data_creator.rb +225 -0
  26. data/test/hexapdf/digital_signature/signing/test_timestamp_handler.rb +88 -0
  27. data/test/hexapdf/{type/signature/test_adbe_pkcs7_detached.rb → digital_signature/test_cms_handler.rb} +7 -7
  28. data/test/hexapdf/{type/signature → digital_signature}/test_handler.rb +4 -4
  29. data/test/hexapdf/{type/signature/test_adbe_x509_rsa_sha1.rb → digital_signature/test_pkcs1_handler.rb} +3 -3
  30. data/test/hexapdf/{type → digital_signature}/test_signature.rb +7 -7
  31. data/test/hexapdf/digital_signature/test_signatures.rb +137 -0
  32. data/test/hexapdf/digital_signature/test_signing.rb +53 -0
  33. data/test/hexapdf/{type/signature → digital_signature}/test_verification_result.rb +7 -7
  34. data/test/hexapdf/document/test_pages.rb +25 -0
  35. data/test/hexapdf/encryption/test_standard_security_handler.rb +2 -2
  36. data/test/hexapdf/test_dictionary_fields.rb +9 -3
  37. data/test/hexapdf/test_document.rb +1 -1
  38. data/test/hexapdf/test_writer.rb +6 -6
  39. data/test/hexapdf/type/test_font_simple.rb +18 -6
  40. metadata +25 -15
  41. data/lib/hexapdf/document/signatures.rb +0 -546
  42. data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +0 -135
  43. data/lib/hexapdf/type/signature/adbe_x509_rsa_sha1.rb +0 -95
  44. data/lib/hexapdf/type/signature/handler.rb +0 -140
  45. 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: 139f1864e4decd05c57468dc3b326a9cc4e749f1d35d4b5dadb3e0549215afd6
4
+ data.tar.gz: e7a0d34979abe6738a084dec2334f449ebc1db3eb479dd5f2374a81eeabd83d7
5
5
  SHA512:
6
- metadata.gz: b66a7587a239acbeb9ebbb20f851b2fa7738c5a4c2f8a95ae7ae3d7419b84ae37b5d1fb48a6b7023bff0df3e1728c395b5351c9c42c05707fabd5a1722e2b88a
7
- data.tar.gz: 62ac7d070bb8ae3426685af497a047096dd32dc16a102baa961512652222b388198561776fc1227be69bdd0713183d91fa25e411262eec34a29227aae9723d5c
6
+ metadata.gz: 339c38737585eafdf447f7516a52ba7ce40b7e2e476336a2404e97a97645d5043b2956835d874296841b8b68d5935c53d97244ac90a2fd621d0471ce85ae8809
7
+ data.tar.gz: 4a4c843a32859c639fdf724ca1c6492fa3d387a1c8a92335a2341e82de42a5b9bc30663a3226c1cc0f2ca116dd638071e24b595b323e64fa1e0342d8681199bd
data/CHANGELOG.md CHANGED
@@ -1,3 +1,52 @@
1
+ ## 0.30.0 - 2023-01-13
2
+
3
+ ### Added
4
+
5
+ * [HexaPDF::Document::Pages#create] for creating a page object without adding it
6
+ to the page tree
7
+
8
+ ### Changed
9
+
10
+ * `HexaPDF::Type::FontSimple#perform_validation` to correct /Widths fields in
11
+ case it has an invalid number of entries
12
+
13
+ ### Fixed
14
+
15
+ * [HexaPDF::DictionaryFields::DateConverter] to handle invalid months, day,
16
+ hour, minute and second values
17
+
18
+
19
+ ## 0.29.0 - 2023-01-30
20
+
21
+ ### Added
22
+
23
+ * [HexaPDF::DigitalSignature::Signing::SignedDataCreator] for creating custom
24
+ CMS signed data objects
25
+
26
+ ### Changed
27
+
28
+ * **Breaking change**: Refactored digital signature support and moved all
29
+ related code under the [HexaPDF::DigitalSignature] module
30
+ * **Breaking change**: New external signing mode without the need for creating
31
+ the PKCS#7/CMS signed data object for
32
+ [HexaPDF::DigitalSignature::Signing::DefaultHandler]
33
+ * **Breaking change**: Use value :pades instead of :etsi for
34
+ [HexaPDF::DigitalSignature::Signing::DefaultHandler#signature_type]
35
+ * [HexaPDF::DigitalSignature::Signing::DefaultHandler] to allow creating PAdES
36
+ level B-B and B-T signatures
37
+ * [HexaPDF::DigitalSignature::Signing::DefaultHandler] to allow specifying the
38
+ used digest algorithm
39
+ * [HexaPDF::DigitalSignature::Signing::DefaultHandler] to allow specifying a
40
+ timestamp handler for including a timestamp token in the signature
41
+ * Moved setting of signature entries /Filter, /SubFilter and /M fields to the
42
+ signing handlers
43
+
44
+ ### Fixed
45
+
46
+ * [HexaPDF::DictionaryFields::DateConverter] to handle invalid timezone hour and
47
+ minute values
48
+
49
+
1
50
  ## 0.28.0 - 2022-12-30
2
51
 
3
52
  ### Added
@@ -61,30 +110,30 @@
61
110
  ### Added
62
111
 
63
112
  * Support for timestamp signatures through the
64
- [HexaPDF::Document::Signatures::TimestampHandler]
113
+ `HexaPDF::Document::Signatures::TimestampHandler`
65
114
  * [HexaPDF::Document::Destinations#resolve] for resolving destination values
66
115
  * [HexaPDF::Document::Destinations::Destination#value] to return the destination
67
116
  array
68
117
  * Support for verifying document timestamp signatures
69
- * [HexaPDF::Document::Signatures::DefaultHandler#signature_size] to support
118
+ * `HexaPDF::Document::Signatures::DefaultHandler#signature_size` to support
70
119
  setting custom signature sizes
71
- * [HexaPDF::Document::Signatures::DefaultHandler#external_signing] to support
120
+ * `HexaPDF::Document::Signatures::DefaultHandler#external_signing` to support
72
121
  signing via custom mechanisms
73
- * [HexaPDF::Document::Signatures::embed_signature] to enable asynchronous
122
+ * `HexaPDF::Document::Signatures::embed_signature` to enable asynchronous
74
123
  external signing
75
124
 
76
125
  ### Changed
77
126
 
78
127
  * **Breaking change**: The crop box is now used instead of the media box in most
79
128
  cases to be in line with the specification
80
- * [HexaPDF::Document::Signatures::DefaultHandler] to allow setting the used
129
+ * `HexaPDF::Document::Signatures::DefaultHandler` to allow setting the used
81
130
  signature method
82
- * **Breaking change**: [HexaPDF::Document::Signatures::DefaultHandler#sign]
131
+ * **Breaking change**: `HexaPDF::Document::Signatures::DefaultHandler#sign`
83
132
  needs to accept the IO object and the byte range instead of just the data
84
133
  * **Breaking change**: Enhanced support for outline items with new methods
85
134
  `#level` and `#destination_page` as well as changes to `#add` and `#each_item`
86
135
  * **Breaking change**: Removed `#filter_name` and `#sub_filter_name` from
87
- [HexaPDF::Document::Signatures::DefaultHandler]
136
+ `HexaPDF::Document::Signatures::DefaultHandler`
88
137
  * `HexaPDF::Type::Resources#perform_validation` to not add a default procedure
89
138
  set since this feature is deprecated
90
139
 
@@ -101,7 +150,7 @@
101
150
  * [HexaPDF::Type::OutlineItem] to always be an indirect object
102
151
  * `HexaPDF::Tokenizer#parse_number` to handle references correctly in all cases
103
152
  * [HexaPDF::Type::Page#rotate] to correctly flatten all page boxes
104
- * [HexaPDF::Document::Signatures#add] to raise an error if the reserved space
153
+ * `HexaPDF::Document::Signatures#add` to raise an error if the reserved space
105
154
  for the signature is not enough
106
155
  * `HexaPDF::Type::AcroForm::Form#perform_validation` to fix broken /Parent
107
156
  entries and to remove invalid objects from the field hierarchy
@@ -276,7 +325,7 @@
276
325
  moved node doesn't change
277
326
  * [HexaPDF::Type::PageTreeNode#move_page] to use the correct target position
278
327
  when the moved node is before the target position
279
- * [HexaPDF::Document::Signatures#add] to work in case the signature object is
328
+ * `HexaPDF::Document::Signatures#add` to work in case the signature object is
280
329
  the last object written
281
330
  * CLI command `hexapdf inspect` to show correct byte range of the last revision
282
331
  * [HexaPDF::Writer#write_incremental] to only use a cross-reference stream if a
@@ -285,7 +334,7 @@
285
334
  disabled
286
335
  * [HexaPDF::Font::Encoding::GlyphList] to use binary reading to avoid problems
287
336
  on Windows
288
- * [HexaPDF::Document::Signatures#add] to use binary writing to avoid problems on
337
+ * `HexaPDF::Document::Signatures#add` to use binary writing to avoid problems on
289
338
  Windows
290
339
 
291
340
 
@@ -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,16 +293,25 @@ 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'}")
304
- Time.new(m[1].to_i, (m[2] ? m[2].to_i : 1), (m[3] ? m[3].to_i : 1),
305
- m[4].to_i, m[5].to_i, m[6].to_i, 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
308
+ begin
309
+ Time.new(m[1].to_i, (m[2] ? m[2].to_i : 1), (m[3] ? m[3].to_i : 1),
310
+ m[4].to_i, m[5].to_i, m[6].to_i, utc_offset)
311
+ rescue ArgumentError
312
+ Time.new(m[1].to_i, m[2].to_i.clamp(1, 12), m[3].to_i.clamp(1, 31),
313
+ m[4].to_i.clamp(0, 23), m[5].to_i.clamp(0, 59), m[6].to_i.clamp(0, 59), utc_offset)
314
+ end
306
315
  end
307
316
 
308
317
  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