hexapdf 0.28.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +41 -10
- data/examples/024-digital-signatures.rb +23 -0
- data/lib/hexapdf/configuration.rb +12 -12
- data/lib/hexapdf/dictionary_fields.rb +6 -2
- 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.rb +21 -14
- data/lib/hexapdf/encryption/standard_security_handler.rb +2 -1
- data/lib/hexapdf/type.rb +0 -1
- data/lib/hexapdf/version.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/test_dictionary_fields.rb +2 -1
- data/test/hexapdf/test_document.rb +1 -1
- data/test/hexapdf/test_writer.rb +3 -3
- metadata +25 -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,56 @@
|
|
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
|
+
module HexaPDF
|
38
|
+
|
39
|
+
# PDF documents can be signed using digital signatures. Such a signature can be used to
|
40
|
+
# authenticate the identity of the signer and the contents of the documents.
|
41
|
+
#
|
42
|
+
# This module contains all code related to digital signatures in PDF.
|
43
|
+
#
|
44
|
+
# See: PDF1.7/2.0 s12.8
|
45
|
+
module DigitalSignature
|
46
|
+
|
47
|
+
autoload(:Signatures, 'hexapdf/digital_signature/signatures')
|
48
|
+
autoload(:Signature, "hexapdf/digital_signature/signature")
|
49
|
+
autoload(:Handler, 'hexapdf/digital_signature/handler')
|
50
|
+
autoload(:CMSHandler, "hexapdf/digital_signature/cms_handler")
|
51
|
+
autoload(:PKCS1Handler, "hexapdf/digital_signature/pkcs1_handler")
|
52
|
+
autoload(:VerificationResult, 'hexapdf/digital_signature/verification_result')
|
53
|
+
autoload(:Signing, 'hexapdf/digital_signature/signing')
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
data/lib/hexapdf/document.rb
CHANGED
@@ -52,6 +52,7 @@ require 'hexapdf/importer'
|
|
52
52
|
require 'hexapdf/image_loader'
|
53
53
|
require 'hexapdf/font_loader'
|
54
54
|
require 'hexapdf/layout'
|
55
|
+
require 'hexapdf/digital_signature'
|
55
56
|
|
56
57
|
begin
|
57
58
|
require 'hexapdf/cext'
|
@@ -105,7 +106,6 @@ module HexaPDF
|
|
105
106
|
autoload(:Fonts, 'hexapdf/document/fonts')
|
106
107
|
autoload(:Images, 'hexapdf/document/images')
|
107
108
|
autoload(:Files, 'hexapdf/document/files')
|
108
|
-
autoload(:Signatures, 'hexapdf/document/signatures')
|
109
109
|
autoload(:Destinations, 'hexapdf/document/destinations')
|
110
110
|
autoload(:Layout, 'hexapdf/document/layout')
|
111
111
|
|
@@ -152,15 +152,19 @@ module HexaPDF
|
|
152
152
|
#
|
153
153
|
# Options:
|
154
154
|
#
|
155
|
-
# io::
|
156
|
-
#
|
155
|
+
# io::
|
156
|
+
# If an IO object is provided, then this document can read PDF objects from this IO object,
|
157
|
+
# otherwise it can only contain created PDF objects.
|
157
158
|
#
|
158
|
-
# decryption_opts::
|
159
|
+
# decryption_opts::
|
160
|
+
# A hash with options for decrypting the PDF objects loaded from the IO. The PDF standard
|
161
|
+
# security handler expects a :password key to be set to either the user or owner password of
|
162
|
+
# the PDF file.
|
159
163
|
#
|
160
|
-
# config::
|
161
|
-
#
|
162
|
-
#
|
163
|
-
#
|
164
|
+
# config::
|
165
|
+
# A hash with configuration options that is deep-merged into the default configuration (see
|
166
|
+
# HexaPDF::DefaultDocumentConfiguration[../index.html#DefaultDocumentConfiguration], meaning
|
167
|
+
# that direct sub-hashes are merged instead of overwritten.
|
164
168
|
def initialize(io: nil, decryption_opts: {}, config: {})
|
165
169
|
@config = Configuration.with_defaults(config)
|
166
170
|
@version = '1.2'
|
@@ -585,25 +589,28 @@ module HexaPDF
|
|
585
589
|
acro_form&.signature_flag?(:signatures_exist)
|
586
590
|
end
|
587
591
|
|
588
|
-
# Returns
|
592
|
+
# Returns a DigitalSignature::Signatures object that allows working with the digital signatures
|
593
|
+
# of this document.
|
589
594
|
def signatures
|
590
|
-
@signatures ||= Signatures.new(self)
|
595
|
+
@signatures ||= DigitalSignature::Signatures.new(self)
|
591
596
|
end
|
592
597
|
|
593
598
|
# Signs the document and writes it to the given file or IO object.
|
594
599
|
#
|
595
600
|
# For details on the arguments +file_or_io+, +signature+ and +write_options+ see
|
596
|
-
# HexaPDF::
|
601
|
+
# HexaPDF::DigitalSignature::Signatures#add.
|
597
602
|
#
|
598
603
|
# The signing handler to be used is determined by the +handler+ argument together with the rest
|
599
|
-
# of the keyword arguments (see HexaPDF::
|
604
|
+
# of the keyword arguments (see HexaPDF::DigitalSignature::Signatures#signing_handler for
|
605
|
+
# details).
|
600
606
|
#
|
601
|
-
# If not changed, the default signing handler is
|
607
|
+
# If not changed, the default signing handler is
|
608
|
+
# HexaPDF::DigitalSignature::Signing::DefaultHandler.
|
602
609
|
#
|
603
610
|
# *Note*: Once signing is done the document cannot be changed anymore since it was written. If a
|
604
611
|
# document needs to be signed multiple times, it needs to be loaded again after writing.
|
605
612
|
def sign(file_or_io, handler: :default, signature: nil, write_options: {}, **handler_options)
|
606
|
-
handler = signatures.
|
613
|
+
handler = signatures.signing_handler(name: handler, **handler_options)
|
607
614
|
signatures.add(file_or_io, handler, signature: signature, write_options: write_options)
|
608
615
|
end
|
609
616
|
|
@@ -97,7 +97,8 @@ module HexaPDF
|
|
97
97
|
# a user is allowed to do with a PDF file.
|
98
98
|
#
|
99
99
|
# When a user or owner password is specified, a PDF file can only be opened when the correct
|
100
|
-
# password is supplied.
|
100
|
+
# password is supplied. To open such an encrypted PDF file, the +decryption_opts+ provided to
|
101
|
+
# HexaPDF::Document.new needs to contain a :password key with the password.
|
101
102
|
#
|
102
103
|
# See: PDF1.7 s7.6.3, PDF2.0 s7.6.3
|
103
104
|
class StandardSecurityHandler < SecurityHandler
|
data/lib/hexapdf/type.rb
CHANGED
@@ -72,7 +72,6 @@ module HexaPDF
|
|
72
72
|
autoload(:FontType3, 'hexapdf/type/font_type3')
|
73
73
|
autoload(:IconFit, 'hexapdf/type/icon_fit')
|
74
74
|
autoload(:AcroForm, 'hexapdf/type/acro_form')
|
75
|
-
autoload(:Signature, 'hexapdf/type/signature')
|
76
75
|
autoload(:Outline, 'hexapdf/type/outline')
|
77
76
|
autoload(:OutlineItem, 'hexapdf/type/outline_item')
|
78
77
|
autoload(:PageLabel, 'hexapdf/type/page_label')
|
data/lib/hexapdf/version.rb
CHANGED
@@ -6,7 +6,7 @@ module HexaPDF
|
|
6
6
|
class Certificates
|
7
7
|
|
8
8
|
def ca_key
|
9
|
-
@ca_key ||= OpenSSL::PKey::RSA.new(
|
9
|
+
@ca_key ||= OpenSSL::PKey::RSA.new(2048)
|
10
10
|
end
|
11
11
|
|
12
12
|
def ca_certificate
|
@@ -36,13 +36,17 @@ module HexaPDF
|
|
36
36
|
end
|
37
37
|
|
38
38
|
def signer_key
|
39
|
-
@signer_key ||= OpenSSL::PKey::RSA.new(
|
39
|
+
@signer_key ||= OpenSSL::PKey::RSA.new(2048)
|
40
|
+
end
|
41
|
+
|
42
|
+
def dsa_signer_key
|
43
|
+
@dsa_signer_key ||= OpenSSL::PKey::DSA.new(2048)
|
40
44
|
end
|
41
45
|
|
42
46
|
def signer_certificate
|
43
47
|
@signer_certificate ||=
|
44
48
|
begin
|
45
|
-
name = OpenSSL::X509::Name.parse('/CN=signer/DC=gettalong')
|
49
|
+
name = OpenSSL::X509::Name.parse('/CN=RSA signer/DC=gettalong')
|
46
50
|
|
47
51
|
signer_cert = OpenSSL::X509::Certificate.new
|
48
52
|
signer_cert.serial = 2
|
@@ -65,6 +69,30 @@ module HexaPDF
|
|
65
69
|
end
|
66
70
|
end
|
67
71
|
|
72
|
+
def dsa_signer_certificate
|
73
|
+
@dsa_signer_certificate ||=
|
74
|
+
begin
|
75
|
+
signer_cert = OpenSSL::X509::Certificate.new
|
76
|
+
signer_cert.serial = 3
|
77
|
+
signer_cert.version = 2
|
78
|
+
signer_cert.not_before = Time.now - 86400
|
79
|
+
signer_cert.not_after = Time.now + 86400
|
80
|
+
signer_cert.public_key = dsa_signer_key.public_key
|
81
|
+
signer_cert.subject = OpenSSL::X509::Name.parse('/CN=DSA signer/DC=gettalong')
|
82
|
+
signer_cert.issuer = ca_certificate.subject
|
83
|
+
|
84
|
+
extension_factory = OpenSSL::X509::ExtensionFactory.new
|
85
|
+
extension_factory.subject_certificate = signer_cert
|
86
|
+
extension_factory.issuer_certificate = ca_certificate
|
87
|
+
signer_cert.add_extension(extension_factory.create_extension('subjectKeyIdentifier', 'hash'))
|
88
|
+
signer_cert.add_extension(extension_factory.create_extension('basicConstraints', 'CA:FALSE'))
|
89
|
+
signer_cert.add_extension(extension_factory.create_extension('keyUsage', 'digitalSignature'))
|
90
|
+
signer_cert.sign(ca_key, OpenSSL::Digest.new('SHA1'))
|
91
|
+
|
92
|
+
signer_cert
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
68
96
|
def timestamp_certificate
|
69
97
|
@timestamp_certificate ||=
|
70
98
|
begin
|
@@ -0,0 +1,162 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
require 'hexapdf/document'
|
5
|
+
require_relative '../common'
|
6
|
+
|
7
|
+
describe HexaPDF::DigitalSignature::Signing::DefaultHandler do
|
8
|
+
before do
|
9
|
+
@doc = HexaPDF::Document.new
|
10
|
+
@handler = HexaPDF::DigitalSignature::Signing::DefaultHandler.new(
|
11
|
+
certificate: CERTIFICATES.signer_certificate,
|
12
|
+
key: CERTIFICATES.signer_key,
|
13
|
+
certificate_chain: [CERTIFICATES.ca_certificate]
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "defaults to standard CMS signatures" do
|
18
|
+
assert_equal(:cms, @handler.signature_type)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "returns the size of serialized signature" do
|
22
|
+
assert(@handler.signature_size > 1000)
|
23
|
+
@handler.signature_size = 100
|
24
|
+
assert_equal(100, @handler.signature_size)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "allows setting the DocMDP permissions" do
|
28
|
+
assert_nil(@handler.doc_mdp_permissions)
|
29
|
+
|
30
|
+
@handler.doc_mdp_permissions = :no_changes
|
31
|
+
assert_equal(1, @handler.doc_mdp_permissions)
|
32
|
+
@handler.doc_mdp_permissions = 1
|
33
|
+
assert_equal(1, @handler.doc_mdp_permissions)
|
34
|
+
|
35
|
+
@handler.doc_mdp_permissions = :form_filling
|
36
|
+
assert_equal(2, @handler.doc_mdp_permissions)
|
37
|
+
@handler.doc_mdp_permissions = 2
|
38
|
+
assert_equal(2, @handler.doc_mdp_permissions)
|
39
|
+
|
40
|
+
@handler.doc_mdp_permissions = :form_filling_and_annotations
|
41
|
+
assert_equal(3, @handler.doc_mdp_permissions)
|
42
|
+
@handler.doc_mdp_permissions = 3
|
43
|
+
assert_equal(3, @handler.doc_mdp_permissions)
|
44
|
+
|
45
|
+
@handler.doc_mdp_permissions = nil
|
46
|
+
assert_nil(@handler.doc_mdp_permissions)
|
47
|
+
|
48
|
+
assert_raises(ArgumentError) { @handler.doc_mdp_permissions = :other }
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "sign" do
|
52
|
+
it "can sign the data using the provided certificate and key" do
|
53
|
+
data = StringIO.new("data")
|
54
|
+
signed_data = @handler.sign(data, [0, data.string.size, 0, 0])
|
55
|
+
|
56
|
+
pkcs7 = OpenSSL::PKCS7.new(signed_data)
|
57
|
+
assert(pkcs7.detached?)
|
58
|
+
assert_equal([CERTIFICATES.signer_certificate, CERTIFICATES.ca_certificate],
|
59
|
+
pkcs7.certificates)
|
60
|
+
store = OpenSSL::X509::Store.new
|
61
|
+
store.add_cert(CERTIFICATES.ca_certificate)
|
62
|
+
assert(pkcs7.verify([], store, data.string, OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY))
|
63
|
+
end
|
64
|
+
|
65
|
+
it "can change the used digest algorithm" do
|
66
|
+
@handler.digest_algorithm = 'sha384'
|
67
|
+
asn1 = OpenSSL::ASN1.decode(@handler.sign(StringIO.new('data'), [0, 4, 0, 0]))
|
68
|
+
assert_equal('SHA384', asn1.value[1].value[0].value[1].value[0].value[0].value)
|
69
|
+
end
|
70
|
+
|
71
|
+
it "can embed a timestamp token" do
|
72
|
+
@handler.timestamp_handler = tsh = Object.new
|
73
|
+
tsh.define_singleton_method(:sign) {|_, _| OpenSSL::ASN1::OctetString.new("signed-tsh") }
|
74
|
+
signed = @handler.sign(StringIO.new('data'), [0, 4, 0, 0])
|
75
|
+
asn1 = OpenSSL::ASN1.decode(signed)
|
76
|
+
assert_equal('signed-tsh', asn1.value[1].value[0].value[4].value[0].
|
77
|
+
value[6].value[0].value[1].value[0].value)
|
78
|
+
end
|
79
|
+
|
80
|
+
it "creates PAdES compatible signatures" do
|
81
|
+
@handler.signature_type = :pades
|
82
|
+
signed = @handler.sign(StringIO.new('data'), [0, 4, 0, 0])
|
83
|
+
asn1 = OpenSSL::ASN1.decode(signed)
|
84
|
+
# check by absence of signing-time signed attribute
|
85
|
+
refute(asn1.value[1].value[0].value[4].value[0].value[3].value.
|
86
|
+
find {|obj| obj.value[0].value == 'signingTime' })
|
87
|
+
end
|
88
|
+
|
89
|
+
it "can use external signing without certificate set" do
|
90
|
+
@handler.certificate = nil
|
91
|
+
@handler.external_signing = proc { "hallo" }
|
92
|
+
assert_equal("hallo", @handler.sign(StringIO.new, [0, 0, 0, 0]))
|
93
|
+
end
|
94
|
+
|
95
|
+
it "can use external signing with certificate set but not the key" do
|
96
|
+
@handler.key = nil
|
97
|
+
@handler.external_signing = proc do |algorithm, _hash|
|
98
|
+
assert_equal('sha256', algorithm)
|
99
|
+
"hallo"
|
100
|
+
end
|
101
|
+
result = @handler.sign(StringIO.new, [0, 0, 0, 0])
|
102
|
+
asn1 = OpenSSL::ASN1.decode(result)
|
103
|
+
assert_equal("hallo", asn1.value[1].value[0].value[4].value[0].value[5].value)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
describe "finalize_objects" do
|
108
|
+
before do
|
109
|
+
@field = @doc.wrap({})
|
110
|
+
@obj = @doc.wrap({})
|
111
|
+
end
|
112
|
+
|
113
|
+
it "only sets the mandatory values if no concrete finalization tasks need to be done" do
|
114
|
+
@handler.finalize_objects(@field, @obj)
|
115
|
+
assert(@field.empty?)
|
116
|
+
assert_equal(:'Adobe.PPKLite', @obj[:Filter])
|
117
|
+
assert_equal(:'adbe.pkcs7.detached', @obj[:SubFilter])
|
118
|
+
assert_kind_of(Time, @obj[:M])
|
119
|
+
end
|
120
|
+
|
121
|
+
it "adjust the /SubFilter if signature type is pades" do
|
122
|
+
@handler.signature_type = :pades
|
123
|
+
@handler.finalize_objects(@field, @obj)
|
124
|
+
assert_equal(:'ETSI.CAdES.detached', @obj[:SubFilter])
|
125
|
+
end
|
126
|
+
|
127
|
+
it "sets the reason, location and contact info fields" do
|
128
|
+
@handler.reason = 'Reason'
|
129
|
+
@handler.location = 'Location'
|
130
|
+
@handler.contact_info = 'Contact'
|
131
|
+
@handler.finalize_objects(@field, @obj)
|
132
|
+
assert(@field.empty?)
|
133
|
+
assert_equal(['Reason', 'Location', 'Contact'], @obj.value.values_at(:Reason, :Location, :ContactInfo))
|
134
|
+
end
|
135
|
+
|
136
|
+
it "fills the build properties dictionary with appropriate application information" do
|
137
|
+
@handler.finalize_objects(@field, @obj)
|
138
|
+
assert_equal(:HexaPDF, @obj[:Prop_Build][:App][:Name])
|
139
|
+
assert_equal(HexaPDF::VERSION, @obj[:Prop_Build][:App][:REx])
|
140
|
+
end
|
141
|
+
|
142
|
+
it "applies the specified DocMDP permissions" do
|
143
|
+
@handler.doc_mdp_permissions = :no_changes
|
144
|
+
@handler.finalize_objects(@field, @obj)
|
145
|
+
ref = @obj[:Reference][0]
|
146
|
+
assert_equal(:DocMDP, ref[:TransformMethod])
|
147
|
+
assert_equal(:SHA256, ref[:DigestMethod])
|
148
|
+
assert_equal(1, ref[:TransformParams][:P])
|
149
|
+
assert_equal(:'1.2', ref[:TransformParams][:V])
|
150
|
+
assert_same(@obj, @doc.catalog[:Perms][:DocMDP])
|
151
|
+
end
|
152
|
+
|
153
|
+
it "fails if DocMDP should be set but there is already a signature" do
|
154
|
+
@handler.doc_mdp_permissions = :no_changes
|
155
|
+
2.times do
|
156
|
+
field = @doc.acro_form(create: true).create_signature_field('test')
|
157
|
+
field.field_value = :something
|
158
|
+
end
|
159
|
+
assert_raises(HexaPDF::Error) { @handler.finalize_objects(@field, @obj) }
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,225 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
require 'hexapdf/document'
|
5
|
+
require_relative '../common'
|
6
|
+
|
7
|
+
describe HexaPDF::DigitalSignature::Signing::SignedDataCreator do
|
8
|
+
before do
|
9
|
+
@klass = HexaPDF::DigitalSignature::Signing::SignedDataCreator
|
10
|
+
@signed_data = @klass.new
|
11
|
+
@signed_data.certificate = CERTIFICATES.signer_certificate
|
12
|
+
@signed_data.key = CERTIFICATES.signer_key
|
13
|
+
@signed_data.certificates = [CERTIFICATES.ca_certificate]
|
14
|
+
end
|
15
|
+
|
16
|
+
it "allows setting the attributes" do
|
17
|
+
obj = @klass.new
|
18
|
+
obj.certificate = :cert
|
19
|
+
obj.key = :key
|
20
|
+
obj.certificates = :certs
|
21
|
+
obj.digest_algorithm = 'sha512'
|
22
|
+
obj.timestamp_handler = :tsh
|
23
|
+
assert_equal(:cert, obj.certificate)
|
24
|
+
assert_equal(:key, obj.key)
|
25
|
+
assert_equal(:certs, obj.certificates)
|
26
|
+
assert_equal('sha512', obj.digest_algorithm)
|
27
|
+
assert_equal(:tsh, obj.timestamp_handler)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "doesn't allow setting attributes to nil using ::create" do
|
31
|
+
asn1 = @klass.create("data",
|
32
|
+
certificate: CERTIFICATES.signer_certificate,
|
33
|
+
key: CERTIFICATES.signer_key,
|
34
|
+
digest_algorithm: nil)
|
35
|
+
assert_equal('2.16.840.1.101.3.4.2.1', asn1.value[1].value[1].value[0].value[0].value)
|
36
|
+
end
|
37
|
+
|
38
|
+
describe "content info structure" do
|
39
|
+
it "sets the correct content type value for the outer container" do
|
40
|
+
asn1 = @signed_data.create("data")
|
41
|
+
assert_equal('1.2.840.113549.1.7.2', asn1.value[0].value)
|
42
|
+
end
|
43
|
+
|
44
|
+
it "has the signed data structure marked as explicit" do
|
45
|
+
asn1 = @signed_data.create("data")
|
46
|
+
signed_data = asn1.value[1]
|
47
|
+
assert_equal(0, signed_data.tag)
|
48
|
+
assert_equal(:EXPLICIT, signed_data.tagging)
|
49
|
+
assert_equal(:CONTEXT_SPECIFIC, signed_data.tag_class)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe "signed data structure" do
|
54
|
+
before do
|
55
|
+
@structure = @signed_data.create("data").value[1]
|
56
|
+
end
|
57
|
+
|
58
|
+
it "sets the correct version" do
|
59
|
+
assert_equal(1, @structure.value[0].value)
|
60
|
+
end
|
61
|
+
|
62
|
+
it "contains a reference to the used digest algorithm" do
|
63
|
+
assert_equal('2.16.840.1.101.3.4.2.1', @structure.value[1].value[0].value[0].value)
|
64
|
+
assert_nil(@structure.value[1].value[0].value[1].value)
|
65
|
+
end
|
66
|
+
|
67
|
+
it "contains an empty encapsulated content structure" do
|
68
|
+
assert_equal(1, @structure.value[2].value.size)
|
69
|
+
assert_equal('1.2.840.113549.1.7.1', @structure.value[2].value[0].value)
|
70
|
+
end
|
71
|
+
|
72
|
+
it "contains the assigned certificates" do
|
73
|
+
assert_equal(2, @structure.value[3].value.size)
|
74
|
+
assert_equal(0, @structure.value[3].tag)
|
75
|
+
assert_equal(:IMPLICIT, @structure.value[3].tagging)
|
76
|
+
assert_equal(:CONTEXT_SPECIFIC, @structure.value[3].tag_class)
|
77
|
+
assert_equal([CERTIFICATES.signer_certificate, CERTIFICATES.ca_certificate],
|
78
|
+
@structure.value[3].value)
|
79
|
+
end
|
80
|
+
|
81
|
+
it "contains a single signer info structure" do
|
82
|
+
assert_equal(1, @structure.value[4].value.size)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
describe "signer info" do
|
87
|
+
before do
|
88
|
+
@structure = @signed_data.create("data").value[1].value[4].value[0]
|
89
|
+
end
|
90
|
+
|
91
|
+
it "has the expected number of entries" do
|
92
|
+
assert_equal(6, @structure.value.size)
|
93
|
+
end
|
94
|
+
|
95
|
+
it "sets the correct version" do
|
96
|
+
assert_equal(1, @structure.value[0].value)
|
97
|
+
end
|
98
|
+
|
99
|
+
it "uses issuer and serial for the signer identifer" do
|
100
|
+
assert_equal(CERTIFICATES.signer_certificate.issuer, @structure.value[1].value[0])
|
101
|
+
assert_equal(CERTIFICATES.signer_certificate.serial, @structure.value[1].value[1].value)
|
102
|
+
end
|
103
|
+
|
104
|
+
it "contains a reference to the used digest algorithm" do
|
105
|
+
assert_equal('2.16.840.1.101.3.4.2.1', @structure.value[2].value[0].value)
|
106
|
+
assert_nil(@structure.value[2].value[1].value)
|
107
|
+
end
|
108
|
+
|
109
|
+
describe "signed attributes" do
|
110
|
+
it "uses the correct tagging for the attributes" do
|
111
|
+
assert_equal(0, @structure.value[3].tag)
|
112
|
+
assert_equal(:IMPLICIT, @structure.value[3].tagging)
|
113
|
+
assert_equal(:CONTEXT_SPECIFIC, @structure.value[3].tag_class)
|
114
|
+
end
|
115
|
+
|
116
|
+
it "contains the content type identifier" do
|
117
|
+
attr = @structure.value[3].value.find {|obj| obj.value[0].value == '1.2.840.113549.1.9.3' }
|
118
|
+
assert_equal('1.2.840.113549.1.7.1', attr.value[1].value[0].value)
|
119
|
+
end
|
120
|
+
|
121
|
+
it "contains the message digest attribute" do
|
122
|
+
attr = @structure.value[3].value.find {|obj| obj.value[0].value == '1.2.840.113549.1.9.4' }
|
123
|
+
assert_equal(OpenSSL::Digest.digest('SHA256', 'data'), attr.value[1].value[0].value)
|
124
|
+
end
|
125
|
+
|
126
|
+
it "contains the signing certificate attribute" do
|
127
|
+
attr = @structure.value[3].value.find {|obj| obj.value[0].value == '1.2.840.113549.1.9.16.2.47' }
|
128
|
+
signing_cert = attr.value[1].value[0]
|
129
|
+
assert_equal(1, signing_cert.value.size)
|
130
|
+
assert_equal(1, signing_cert.value[0].value.size)
|
131
|
+
assert_equal(2, signing_cert.value[0].value[0].value.size)
|
132
|
+
assert_equal(OpenSSL::Digest.digest('sha256', CERTIFICATES.signer_certificate.to_der),
|
133
|
+
signing_cert.value[0].value[0].value[0].value)
|
134
|
+
assert_equal(2, signing_cert.value[0].value[0].value[1].value.size)
|
135
|
+
assert_equal(1, signing_cert.value[0].value[0].value[1].value[0].value.size)
|
136
|
+
assert_equal(1, signing_cert.value[0].value[0].value[1].value[0].value[0].value.size)
|
137
|
+
assert_equal(4, signing_cert.value[0].value[0].value[1].value[0].value[0].tag)
|
138
|
+
assert_equal(:IMPLICIT, signing_cert.value[0].value[0].value[1].value[0].value[0].tagging)
|
139
|
+
assert_equal(:CONTEXT_SPECIFIC, signing_cert.value[0].value[0].value[1].value[0].value[0].tag_class)
|
140
|
+
assert_equal(CERTIFICATES.signer_certificate.issuer,
|
141
|
+
signing_cert.value[0].value[0].value[1].value[0].value[0].value[0])
|
142
|
+
assert_equal(CERTIFICATES.signer_certificate.serial,
|
143
|
+
signing_cert.value[0].value[0].value[1].value[1].value)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
it "contains the signature algorithm reference" do
|
148
|
+
assert_equal('1.2.840.113549.1.1.1', @structure.value[4].value[0].value)
|
149
|
+
assert_nil(@structure.value[4].value[1].value)
|
150
|
+
end
|
151
|
+
|
152
|
+
it "contains the signature itself" do
|
153
|
+
to_sign = OpenSSL::ASN1::Set.new(@structure.value[3].value).to_der
|
154
|
+
assert_equal(CERTIFICATES.signer_key.sign('SHA256', to_sign), @structure.value[5].value)
|
155
|
+
end
|
156
|
+
|
157
|
+
it "fails if the signature algorithm is not supported" do
|
158
|
+
@signed_data.certificate = CERTIFICATES.dsa_signer_certificate
|
159
|
+
@signed_data.key = CERTIFICATES.dsa_signer_key
|
160
|
+
assert_raises(HexaPDF::Error) { @signed_data.create("data") }
|
161
|
+
end
|
162
|
+
|
163
|
+
it "can use a different digest algorithm" do
|
164
|
+
@signed_data.digest_algorithm = 'sha384'
|
165
|
+
structure = @signed_data.create("data").value[1].value[4].value[0]
|
166
|
+
to_sign = OpenSSL::ASN1::Set.new(structure.value[3].value).to_der
|
167
|
+
assert_equal('2.16.840.1.101.3.4.2.2', structure.value[2].value[0].value)
|
168
|
+
assert_equal(CERTIFICATES.signer_key.sign('SHA384', to_sign), structure.value[5].value)
|
169
|
+
end
|
170
|
+
|
171
|
+
it "allows delegating the signature to a provided signing block" do
|
172
|
+
@signed_data.key = nil
|
173
|
+
digest_algorithm = nil
|
174
|
+
calculated_hash = nil
|
175
|
+
structure = @signed_data.create("data") do |algorithm, hash|
|
176
|
+
digest_algorithm = algorithm
|
177
|
+
calculated_hash = hash
|
178
|
+
"signed"
|
179
|
+
end.value[1].value[4].value[0]
|
180
|
+
to_sign = OpenSSL::Digest.digest('SHA256', OpenSSL::ASN1::Set.new(structure.value[3].value).to_der)
|
181
|
+
assert_equal('sha256', digest_algorithm)
|
182
|
+
assert_equal(calculated_hash, to_sign)
|
183
|
+
assert_equal('signed', structure.value[5].value)
|
184
|
+
end
|
185
|
+
|
186
|
+
describe "unsigned attributes" do
|
187
|
+
it "allows adding a timestamp token" do
|
188
|
+
tsh = Object.new
|
189
|
+
io = nil
|
190
|
+
byte_range = nil
|
191
|
+
tsh.define_singleton_method(:sign) do |i_io, i_byte_range|
|
192
|
+
io = i_io
|
193
|
+
byte_range = i_byte_range
|
194
|
+
"timestamp"
|
195
|
+
end
|
196
|
+
@signed_data.timestamp_handler = tsh
|
197
|
+
|
198
|
+
structure = @signed_data.create("data").value[1].value[4].value[0]
|
199
|
+
assert_equal(structure.value[5].value, io.string)
|
200
|
+
assert_equal([0, io.string.size, 0, 0], byte_range)
|
201
|
+
|
202
|
+
attr = structure.value[6].value.find {|obj| obj.value[0].value == '1.2.840.113549.1.9.16.2.14' }
|
203
|
+
assert_equal("timestamp", attr.value[1].value[0])
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
describe "cms signature" do
|
209
|
+
it "includes the current time as signing time" do
|
210
|
+
Time.stub(:now, Time.at(0)) do
|
211
|
+
asn1 = OpenSSL::ASN1.decode(@signed_data.create("data"))
|
212
|
+
attr = asn1.value[1].value[0].value[4].value[0].value[3].value.
|
213
|
+
find {|obj| obj.value[0].value == 'signingTime' }
|
214
|
+
assert_equal(Time.now.utc, attr.value[1].value[0].value)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
describe "pades signature" do
|
220
|
+
it "doesn't include the signing-time attribute" do
|
221
|
+
signer_info = @signed_data.create("data", type: :pades).value[1].value[4].value[0]
|
222
|
+
refute(signer_info.value[3].value.find {|obj| obj.value[0].value == 'signingTime' })
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|