hexapdf 0.19.3 → 0.20.3

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