hexapdf 0.28.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +86 -10
- data/examples/024-digital-signatures.rb +23 -0
- data/lib/hexapdf/cli/command.rb +16 -1
- data/lib/hexapdf/cli/info.rb +9 -1
- data/lib/hexapdf/cli/inspect.rb +2 -2
- data/lib/hexapdf/composer.rb +76 -28
- data/lib/hexapdf/configuration.rb +29 -16
- data/lib/hexapdf/dictionary_fields.rb +13 -4
- data/lib/hexapdf/digital_signature/cms_handler.rb +137 -0
- data/lib/hexapdf/digital_signature/handler.rb +138 -0
- data/lib/hexapdf/digital_signature/pkcs1_handler.rb +96 -0
- data/lib/hexapdf/{type → digital_signature}/signature.rb +3 -8
- data/lib/hexapdf/digital_signature/signatures.rb +210 -0
- data/lib/hexapdf/digital_signature/signing/default_handler.rb +317 -0
- data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +308 -0
- data/lib/hexapdf/digital_signature/signing/timestamp_handler.rb +148 -0
- data/lib/hexapdf/digital_signature/signing.rb +101 -0
- data/lib/hexapdf/{type/signature → digital_signature}/verification_result.rb +37 -41
- data/lib/hexapdf/digital_signature.rb +56 -0
- data/lib/hexapdf/document/pages.rb +31 -18
- data/lib/hexapdf/document.rb +29 -15
- data/lib/hexapdf/encryption/standard_security_handler.rb +4 -3
- data/lib/hexapdf/filter/flate_decode.rb +20 -8
- data/lib/hexapdf/layout/page_style.rb +144 -0
- data/lib/hexapdf/layout.rb +1 -0
- data/lib/hexapdf/task/optimize.rb +8 -6
- data/lib/hexapdf/type/font_simple.rb +14 -2
- data/lib/hexapdf/type/object_stream.rb +7 -2
- data/lib/hexapdf/type/outline.rb +1 -1
- data/lib/hexapdf/type/outline_item.rb +1 -1
- data/lib/hexapdf/type/page.rb +29 -8
- data/lib/hexapdf/type/xref_stream.rb +11 -4
- data/lib/hexapdf/type.rb +0 -1
- data/lib/hexapdf/version.rb +1 -1
- data/lib/hexapdf/writer.rb +1 -1
- data/test/hexapdf/{type/signature → digital_signature}/common.rb +31 -3
- data/test/hexapdf/digital_signature/signing/test_default_handler.rb +162 -0
- data/test/hexapdf/digital_signature/signing/test_signed_data_creator.rb +225 -0
- data/test/hexapdf/digital_signature/signing/test_timestamp_handler.rb +88 -0
- data/test/hexapdf/{type/signature/test_adbe_pkcs7_detached.rb → digital_signature/test_cms_handler.rb} +7 -7
- data/test/hexapdf/{type/signature → digital_signature}/test_handler.rb +4 -4
- data/test/hexapdf/{type/signature/test_adbe_x509_rsa_sha1.rb → digital_signature/test_pkcs1_handler.rb} +3 -3
- data/test/hexapdf/{type → digital_signature}/test_signature.rb +7 -7
- data/test/hexapdf/digital_signature/test_signatures.rb +137 -0
- data/test/hexapdf/digital_signature/test_signing.rb +53 -0
- data/test/hexapdf/{type/signature → digital_signature}/test_verification_result.rb +7 -7
- data/test/hexapdf/document/test_pages.rb +25 -0
- data/test/hexapdf/encryption/test_standard_security_handler.rb +2 -2
- data/test/hexapdf/filter/test_flate_decode.rb +19 -5
- data/test/hexapdf/layout/test_page_style.rb +70 -0
- data/test/hexapdf/task/test_optimize.rb +11 -9
- data/test/hexapdf/test_composer.rb +35 -10
- data/test/hexapdf/test_dictionary_fields.rb +9 -3
- data/test/hexapdf/test_document.rb +1 -1
- data/test/hexapdf/test_writer.rb +8 -8
- data/test/hexapdf/type/test_font_simple.rb +18 -6
- data/test/hexapdf/type/test_object_stream.rb +16 -7
- data/test/hexapdf/type/test_outline.rb +3 -1
- data/test/hexapdf/type/test_outline_item.rb +3 -1
- data/test/hexapdf/type/test_page.rb +42 -11
- data/test/hexapdf/type/test_xref_stream.rb +6 -1
- metadata +27 -15
- data/lib/hexapdf/document/signatures.rb +0 -546
- data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +0 -135
- data/lib/hexapdf/type/signature/adbe_x509_rsa_sha1.rb +0 -95
- data/lib/hexapdf/type/signature/handler.rb +0 -140
- data/test/hexapdf/document/test_signatures.rb +0 -352
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# -*- encoding: utf-8; frozen_string_literal: true -*-
|
|
2
|
+
#
|
|
3
|
+
#--
|
|
4
|
+
# This file is part of HexaPDF.
|
|
5
|
+
#
|
|
6
|
+
# HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
|
|
7
|
+
# Copyright (C) 2014-2022 Thomas Leitner
|
|
8
|
+
#
|
|
9
|
+
# HexaPDF is free software: you can redistribute it and/or modify it
|
|
10
|
+
# under the terms of the GNU Affero General Public License version 3 as
|
|
11
|
+
# published by the Free Software Foundation with the addition of the
|
|
12
|
+
# following permission added to Section 15 as permitted in Section 7(a):
|
|
13
|
+
# FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
|
|
14
|
+
# THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
|
|
15
|
+
# INFRINGEMENT OF THIRD PARTY RIGHTS.
|
|
16
|
+
#
|
|
17
|
+
# HexaPDF is distributed in the hope that it will be useful, but WITHOUT
|
|
18
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
19
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
|
20
|
+
# License for more details.
|
|
21
|
+
#
|
|
22
|
+
# You should have received a copy of the GNU Affero General Public License
|
|
23
|
+
# along with HexaPDF. If not, see <http://www.gnu.org/licenses/>.
|
|
24
|
+
#
|
|
25
|
+
# The interactive user interfaces in modified source and object code
|
|
26
|
+
# versions of HexaPDF must display Appropriate Legal Notices, as required
|
|
27
|
+
# under Section 5 of the GNU Affero General Public License version 3.
|
|
28
|
+
#
|
|
29
|
+
# In accordance with Section 7(b) of the GNU Affero General Public
|
|
30
|
+
# License, a covered work must retain the producer line in every PDF that
|
|
31
|
+
# is created or manipulated using HexaPDF.
|
|
32
|
+
#
|
|
33
|
+
# If the GNU Affero General Public License doesn't fit your need,
|
|
34
|
+
# commercial licenses are available at <https://gettalong.at/hexapdf/>.
|
|
35
|
+
#++
|
|
36
|
+
|
|
37
|
+
require 'hexapdf/digital_signature/verification_result'
|
|
38
|
+
|
|
39
|
+
module HexaPDF
|
|
40
|
+
module DigitalSignature
|
|
41
|
+
|
|
42
|
+
# The base signature handler providing common functionality.
|
|
43
|
+
#
|
|
44
|
+
# Specific signature handler need to override methods if necessary and implement the needed
|
|
45
|
+
# ones that don't have a default implementation.
|
|
46
|
+
class Handler
|
|
47
|
+
|
|
48
|
+
# The signature dictionary used by the handler.
|
|
49
|
+
attr_reader :signature_dict
|
|
50
|
+
|
|
51
|
+
# Creates a new signature handler for the given signature dictionary.
|
|
52
|
+
def initialize(signature_dict)
|
|
53
|
+
@signature_dict = signature_dict
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Returns the common name of the signer (/Name field of the signature dictionary).
|
|
57
|
+
def signer_name
|
|
58
|
+
@signature_dict[:Name]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Returns the time of signing (/M field of the signature dictionary).
|
|
62
|
+
def signing_time
|
|
63
|
+
@signature_dict[:M]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Returns the certificate chain.
|
|
67
|
+
#
|
|
68
|
+
# Needs to be implemented by specific handlers.
|
|
69
|
+
def certificate_chain
|
|
70
|
+
raise "Needs to be implemented by specific handlers"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Returns the certificate used for signing.
|
|
74
|
+
#
|
|
75
|
+
# Needs to be implemented by specific handlers.
|
|
76
|
+
def signer_certificate
|
|
77
|
+
raise "Needs to be implemented by specific handlers"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Verifies general signature properties and prepares the provided OpenSSL::X509::Store
|
|
81
|
+
# object for use by concrete implementations.
|
|
82
|
+
#
|
|
83
|
+
# Needs to be called by specific handlers.
|
|
84
|
+
def verify(store, allow_self_signed: false)
|
|
85
|
+
result = VerificationResult.new
|
|
86
|
+
check_certified_signature(result)
|
|
87
|
+
verify_signing_time(result)
|
|
88
|
+
store.verify_callback =
|
|
89
|
+
store_verification_callback(result, allow_self_signed: allow_self_signed)
|
|
90
|
+
result
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
protected
|
|
94
|
+
|
|
95
|
+
# Verifies that the signing time was within the validity period of the signer certificate.
|
|
96
|
+
def verify_signing_time(result)
|
|
97
|
+
time = signing_time
|
|
98
|
+
cert = signer_certificate
|
|
99
|
+
if time && cert && (time < cert.not_before || time > cert.not_after)
|
|
100
|
+
result.log(:error, "Signer certificate not valid at signing time")
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
DOCMDP_PERMS_MESSAGE_MAP = { # :nodoc:
|
|
105
|
+
1 => "No changes allowed",
|
|
106
|
+
2 => "Form filling and signing allowed",
|
|
107
|
+
3 => "Form filling, signing and annotation manipulation allowed",
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# Sets an informational message on +result+ whether the signature is a certified signature.
|
|
111
|
+
def check_certified_signature(result)
|
|
112
|
+
sigref = signature_dict[:Reference]&.find {|ref| ref[:TransformMethod] == :DocMDP }
|
|
113
|
+
if sigref && signature_dict.document.catalog[:Perms]&.[](:DocMDP) == signature_dict
|
|
114
|
+
perms = sigref[:TransformParams]&.[](:P) || 2
|
|
115
|
+
result.log(:info, "Certified signature (#{DOCMDP_PERMS_MESSAGE_MAP[perms]})")
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Returns the block that should be used as the OpenSSL::X509::Store verification callback.
|
|
120
|
+
#
|
|
121
|
+
# +result+:: The VerificationResult object that should be updated if problems are found.
|
|
122
|
+
#
|
|
123
|
+
# +allow_self_signed+:: Specifies whether self-signed certificates are allowed.
|
|
124
|
+
def store_verification_callback(result, allow_self_signed: false)
|
|
125
|
+
lambda do |_success, context|
|
|
126
|
+
if context.error == OpenSSL::X509::V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT ||
|
|
127
|
+
context.error == OpenSSL::X509::V_ERR_SELF_SIGNED_CERT_IN_CHAIN
|
|
128
|
+
result.log(allow_self_signed ? :info : :error, "Self-signed certificate found")
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
true
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# -*- encoding: utf-8; frozen_string_literal: true -*-
|
|
2
|
+
#
|
|
3
|
+
#--
|
|
4
|
+
# This file is part of HexaPDF.
|
|
5
|
+
#
|
|
6
|
+
# HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
|
|
7
|
+
# Copyright (C) 2014-2022 Thomas Leitner
|
|
8
|
+
#
|
|
9
|
+
# HexaPDF is free software: you can redistribute it and/or modify it
|
|
10
|
+
# under the terms of the GNU Affero General Public License version 3 as
|
|
11
|
+
# published by the Free Software Foundation with the addition of the
|
|
12
|
+
# following permission added to Section 15 as permitted in Section 7(a):
|
|
13
|
+
# FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
|
|
14
|
+
# THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
|
|
15
|
+
# INFRINGEMENT OF THIRD PARTY RIGHTS.
|
|
16
|
+
#
|
|
17
|
+
# HexaPDF is distributed in the hope that it will be useful, but WITHOUT
|
|
18
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
19
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
|
20
|
+
# License for more details.
|
|
21
|
+
#
|
|
22
|
+
# You should have received a copy of the GNU Affero General Public License
|
|
23
|
+
# along with HexaPDF. If not, see <http://www.gnu.org/licenses/>.
|
|
24
|
+
#
|
|
25
|
+
# The interactive user interfaces in modified source and object code
|
|
26
|
+
# versions of HexaPDF must display Appropriate Legal Notices, as required
|
|
27
|
+
# under Section 5 of the GNU Affero General Public License version 3.
|
|
28
|
+
#
|
|
29
|
+
# In accordance with Section 7(b) of the GNU Affero General Public
|
|
30
|
+
# License, a covered work must retain the producer line in every PDF that
|
|
31
|
+
# is created or manipulated using HexaPDF.
|
|
32
|
+
#
|
|
33
|
+
# If the GNU Affero General Public License doesn't fit your need,
|
|
34
|
+
# commercial licenses are available at <https://gettalong.at/hexapdf/>.
|
|
35
|
+
#++
|
|
36
|
+
|
|
37
|
+
require 'openssl'
|
|
38
|
+
require 'hexapdf/digital_signature/handler'
|
|
39
|
+
|
|
40
|
+
module HexaPDF
|
|
41
|
+
module DigitalSignature
|
|
42
|
+
|
|
43
|
+
# The signature handler for PKCS#1 based sub-filters, the only being the adbe.x509.rsa_sha1
|
|
44
|
+
# sub-filter.
|
|
45
|
+
#
|
|
46
|
+
# Since PKCS#1 signatures are deprecated with PDF 2.0, the handler only provides the
|
|
47
|
+
# implementation for reading and verifying signatures.
|
|
48
|
+
#
|
|
49
|
+
# See: PDF1.7/2.0 s12.8.3.2
|
|
50
|
+
class PKCS1Handler < Handler
|
|
51
|
+
|
|
52
|
+
# Returns the certificate chain.
|
|
53
|
+
def certificate_chain
|
|
54
|
+
return [] unless signature_dict.key?(:Cert)
|
|
55
|
+
[signature_dict[:Cert]].flatten.map {|str| OpenSSL::X509::Certificate.new(str) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Returns the signer certificate (an instance of OpenSSL::X509::Certificate).
|
|
59
|
+
def signer_certificate
|
|
60
|
+
certificate_chain.first
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Verifies the signature using the provided OpenSSL::X509::Store object.
|
|
64
|
+
def verify(store, allow_self_signed: false)
|
|
65
|
+
result = super
|
|
66
|
+
|
|
67
|
+
signer_certificate = self.signer_certificate
|
|
68
|
+
certificate_chain = self.certificate_chain
|
|
69
|
+
|
|
70
|
+
if certificate_chain.empty?
|
|
71
|
+
result.log(:error, "No certificates for verification found")
|
|
72
|
+
return result
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
signature = OpenSSL::ASN1.decode(signature_dict.contents)
|
|
76
|
+
if signature.tag != OpenSSL::ASN1::OCTET_STRING
|
|
77
|
+
result.log(:error, "PKCS1 signature object invalid, octet string expected")
|
|
78
|
+
return result
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
store.verify(signer_certificate, certificate_chain)
|
|
82
|
+
|
|
83
|
+
if signer_certificate.public_key.verify(OpenSSL::Digest.new('SHA1'),
|
|
84
|
+
signature.value, signature_dict.signed_data)
|
|
85
|
+
result.log(:info, "Signature valid")
|
|
86
|
+
else
|
|
87
|
+
result.log(:error, "Signature verification failed")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
result
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -39,7 +39,7 @@ require 'hexapdf/dictionary'
|
|
|
39
39
|
require 'hexapdf/error'
|
|
40
40
|
|
|
41
41
|
module HexaPDF
|
|
42
|
-
module
|
|
42
|
+
module DigitalSignature
|
|
43
43
|
|
|
44
44
|
# Represents a digital signature that is used to authenticate a user and the contents of the
|
|
45
45
|
# document.
|
|
@@ -53,14 +53,9 @@ module HexaPDF
|
|
|
53
53
|
# By defining a custom signature handler one is able to also customize the signature
|
|
54
54
|
# verification.
|
|
55
55
|
#
|
|
56
|
-
# See: PDF1.7
|
|
56
|
+
# See: PDF1.7/2.0 s12.8.1, HexaPDF::Type::AcroForm::SignatureField
|
|
57
57
|
class Signature < Dictionary
|
|
58
58
|
|
|
59
|
-
autoload :Handler, 'hexapdf/type/signature/handler'
|
|
60
|
-
autoload :AdbeX509RsaSha1, 'hexapdf/type/signature/adbe_x509_rsa_sha1'
|
|
61
|
-
autoload :AdbePkcs7Detached, 'hexapdf/type/signature/adbe_pkcs7_detached'
|
|
62
|
-
autoload :VerificationResult, 'hexapdf/type/signature/verification_result'
|
|
63
|
-
|
|
64
59
|
# Represents a transform parameters dictionary.
|
|
65
60
|
#
|
|
66
61
|
# The allowed fields depend on the transform method, so not all fields are available all the
|
|
@@ -122,7 +117,7 @@ module HexaPDF
|
|
|
122
117
|
|
|
123
118
|
# Represents a signature reference dictionary.
|
|
124
119
|
#
|
|
125
|
-
# See: PDF1.7
|
|
120
|
+
# See: PDF1.7/2.0 s12.8.1, HexaPDF::DigitalSignature::Signature
|
|
126
121
|
class SignatureReference < Dictionary
|
|
127
122
|
|
|
128
123
|
define_type :SigRef
|
|
@@ -0,0 +1,210 @@
|
|
|
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 'stringio'
|
|
39
|
+
require 'hexapdf/digital_signature'
|
|
40
|
+
require 'hexapdf/error'
|
|
41
|
+
|
|
42
|
+
module HexaPDF
|
|
43
|
+
module DigitalSignature
|
|
44
|
+
|
|
45
|
+
# This class provides methods for interacting with digital signatures of a PDF file. Use it
|
|
46
|
+
# through HexaPDF::Document#signatures.
|
|
47
|
+
class Signatures
|
|
48
|
+
|
|
49
|
+
include Enumerable
|
|
50
|
+
|
|
51
|
+
# Creates a new Signatures object for the given PDF document.
|
|
52
|
+
def initialize(document)
|
|
53
|
+
@document = document
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Creates a signing handler with the given attributes and returns it.
|
|
57
|
+
#
|
|
58
|
+
# A signing handler name is mapped to a class via the 'signature.signing_handler'
|
|
59
|
+
# configuration option. The default signing handler is DefaultHandler.
|
|
60
|
+
def signing_handler(name: :default, **attributes)
|
|
61
|
+
handler = @document.config.constantize('signature.signing_handler', name) do
|
|
62
|
+
raise HexaPDF::Error, "No signing handler named '#{name}' is available"
|
|
63
|
+
end
|
|
64
|
+
handler.new(**attributes)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Adds a signature to the document and returns the corresponding signature object.
|
|
68
|
+
#
|
|
69
|
+
# This method will add a new signature to the document and write the updated document to the
|
|
70
|
+
# given file or IO stream. Afterwards the document can't be modified anymore and still retain
|
|
71
|
+
# a correct digital signature. To modify the signed document (e.g. for adding another
|
|
72
|
+
# signature) create a new document based on the given file or IO stream instead.
|
|
73
|
+
#
|
|
74
|
+
# +signature+::
|
|
75
|
+
# Can either be a signature object (determined via the /Type key), a signature field or
|
|
76
|
+
# +nil+. Providing a signature object or signature field provides for more control, e.g.:
|
|
77
|
+
#
|
|
78
|
+
# * Setting values for optional signature object fields like /Reason and /Location.
|
|
79
|
+
# * (In)directly specifying which signature field should be used.
|
|
80
|
+
#
|
|
81
|
+
# If a signature object is provided and it is not associated with an AcroForm signature
|
|
82
|
+
# field, a new signature field is created and added to the main AcroForm object, creating
|
|
83
|
+
# that if necessary.
|
|
84
|
+
#
|
|
85
|
+
# If a signature field is provided and it already has a signature object as field value,
|
|
86
|
+
# that signature object is discarded.
|
|
87
|
+
#
|
|
88
|
+
# If the signature field doesn't have a widget, a non-visible one is created on the first
|
|
89
|
+
# page.
|
|
90
|
+
#
|
|
91
|
+
# +handler+::
|
|
92
|
+
# The signing handler that provides the necessary methods for signing and adjusting the
|
|
93
|
+
# signature and signature field objects to one's liking, see #handler and DefaultHandler.
|
|
94
|
+
#
|
|
95
|
+
# +write_options+::
|
|
96
|
+
# The key-value pairs of this hash will be passed on to the HexaPDF::Document#write
|
|
97
|
+
# method. Note that +incremental+ will be automatically set to ensure proper behaviour.
|
|
98
|
+
#
|
|
99
|
+
# The used signature object will have the following default values set:
|
|
100
|
+
#
|
|
101
|
+
# /Filter:: /Adobe.PPKLite
|
|
102
|
+
# /SubFilter:: /adbe.pkcs7.detached
|
|
103
|
+
# /M:: The current time.
|
|
104
|
+
#
|
|
105
|
+
# These values can be overridden in the #finalize_objects method of the signature handler.
|
|
106
|
+
def add(file_or_io, handler, signature: nil, write_options: {})
|
|
107
|
+
if signature && signature.type != :Sig
|
|
108
|
+
signature_field = signature
|
|
109
|
+
signature = signature_field.field_value
|
|
110
|
+
end
|
|
111
|
+
signature ||= @document.add({Type: :Sig})
|
|
112
|
+
|
|
113
|
+
# Prepare AcroForm
|
|
114
|
+
form = @document.acro_form(create: true)
|
|
115
|
+
form.signature_flag(:signatures_exist, :append_only)
|
|
116
|
+
|
|
117
|
+
# Prepare signature field
|
|
118
|
+
signature_field ||= form.each_field.find {|field| field.field_value == signature } ||
|
|
119
|
+
form.create_signature_field(generate_field_name)
|
|
120
|
+
signature_field.field_value = signature
|
|
121
|
+
|
|
122
|
+
if signature_field.each_widget.to_a.empty?
|
|
123
|
+
signature_field.create_widget(@document.pages[0], Rect: [0, 0, 0, 0])
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Prepare signature object
|
|
127
|
+
handler.finalize_objects(signature_field, signature)
|
|
128
|
+
signature[:ByteRange] = [0, 1_000_000_000_000, 1_000_000_000_000, 1_000_000_000_000]
|
|
129
|
+
signature[:Contents] = '00' * handler.signature_size # twice the size due to hex encoding
|
|
130
|
+
|
|
131
|
+
io = if file_or_io.kind_of?(String)
|
|
132
|
+
File.open(file_or_io, 'wb+')
|
|
133
|
+
else
|
|
134
|
+
file_or_io
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Save the current state so that we can determine the correct /ByteRange value and set the
|
|
138
|
+
# values
|
|
139
|
+
start_xref, section = @document.write(io, incremental: true, **write_options)
|
|
140
|
+
signature_offset, signature_length = Signing.locate_signature_dict(section, start_xref,
|
|
141
|
+
signature.oid)
|
|
142
|
+
io.pos = signature_offset
|
|
143
|
+
signature_data = io.read(signature_length)
|
|
144
|
+
|
|
145
|
+
io.seek(0, IO::SEEK_END)
|
|
146
|
+
file_size = io.pos
|
|
147
|
+
|
|
148
|
+
# Calculate the offsets for the /ByteRange
|
|
149
|
+
contents_offset = signature_offset + signature_data.index('Contents(') + 8
|
|
150
|
+
offset2 = contents_offset + signature[:Contents].size + 2 # +2 because of the needed < and >
|
|
151
|
+
length2 = file_size - offset2
|
|
152
|
+
signature[:ByteRange] = [0, contents_offset, offset2, length2]
|
|
153
|
+
|
|
154
|
+
# Set the correct /ByteRange value
|
|
155
|
+
signature_data.sub!(/ByteRange\[0 1000000000000 1000000000000 1000000000000\]/) do |match|
|
|
156
|
+
length = match.size
|
|
157
|
+
result = "ByteRange[0 #{contents_offset} #{offset2} #{length2}]"
|
|
158
|
+
result.ljust(length)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Now everything besides the /Contents value is correct, so we can read the contents for
|
|
162
|
+
# signing
|
|
163
|
+
io.pos = signature_offset
|
|
164
|
+
io.write(signature_data)
|
|
165
|
+
signature[:Contents] = handler.sign(io, signature[:ByteRange].value)
|
|
166
|
+
|
|
167
|
+
# And now replace the /Contents value
|
|
168
|
+
Signing.replace_signature_contents(signature_data, signature[:Contents])
|
|
169
|
+
io.pos = signature_offset
|
|
170
|
+
io.write(signature_data)
|
|
171
|
+
|
|
172
|
+
signature
|
|
173
|
+
ensure
|
|
174
|
+
io.close if io && io != file_or_io
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# :call-seq:
|
|
178
|
+
# signatures.each {|signature| block } -> signatures
|
|
179
|
+
# signatures.each -> Enumerator
|
|
180
|
+
#
|
|
181
|
+
# Iterates over all signatures in the order they are found.
|
|
182
|
+
def each
|
|
183
|
+
return to_enum(__method__) unless block_given?
|
|
184
|
+
|
|
185
|
+
return [] unless (form = @document.acro_form)
|
|
186
|
+
form.each_field do |field|
|
|
187
|
+
yield(field.field_value) if field.field_type == :Sig && field.field_value
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Returns the number of signatures in the PDF document. May be zero if the document has no
|
|
192
|
+
# signatures.
|
|
193
|
+
def count
|
|
194
|
+
each.to_a.size
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
private
|
|
198
|
+
|
|
199
|
+
# Generates a field name for a signature field.
|
|
200
|
+
def generate_field_name
|
|
201
|
+
index = (@document.acro_form.each_field.
|
|
202
|
+
map {|field| field.full_field_name.scan(/\ASignature(\d+)/).first&.first.to_i }.
|
|
203
|
+
max || 0) + 1
|
|
204
|
+
"Signature#{index}"
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
end
|
|
210
|
+
end
|