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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -10
  3. data/examples/024-digital-signatures.rb +23 -0
  4. data/lib/hexapdf/configuration.rb +12 -12
  5. data/lib/hexapdf/dictionary_fields.rb +6 -2
  6. data/lib/hexapdf/digital_signature/cms_handler.rb +137 -0
  7. data/lib/hexapdf/digital_signature/handler.rb +138 -0
  8. data/lib/hexapdf/digital_signature/pkcs1_handler.rb +96 -0
  9. data/lib/hexapdf/{type → digital_signature}/signature.rb +3 -8
  10. data/lib/hexapdf/digital_signature/signatures.rb +210 -0
  11. data/lib/hexapdf/digital_signature/signing/default_handler.rb +317 -0
  12. data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +308 -0
  13. data/lib/hexapdf/digital_signature/signing/timestamp_handler.rb +148 -0
  14. data/lib/hexapdf/digital_signature/signing.rb +101 -0
  15. data/lib/hexapdf/{type/signature → digital_signature}/verification_result.rb +37 -41
  16. data/lib/hexapdf/digital_signature.rb +56 -0
  17. data/lib/hexapdf/document.rb +21 -14
  18. data/lib/hexapdf/encryption/standard_security_handler.rb +2 -1
  19. data/lib/hexapdf/type.rb +0 -1
  20. data/lib/hexapdf/version.rb +1 -1
  21. data/test/hexapdf/{type/signature → digital_signature}/common.rb +31 -3
  22. data/test/hexapdf/digital_signature/signing/test_default_handler.rb +162 -0
  23. data/test/hexapdf/digital_signature/signing/test_signed_data_creator.rb +225 -0
  24. data/test/hexapdf/digital_signature/signing/test_timestamp_handler.rb +88 -0
  25. data/test/hexapdf/{type/signature/test_adbe_pkcs7_detached.rb → digital_signature/test_cms_handler.rb} +7 -7
  26. data/test/hexapdf/{type/signature → digital_signature}/test_handler.rb +4 -4
  27. data/test/hexapdf/{type/signature/test_adbe_x509_rsa_sha1.rb → digital_signature/test_pkcs1_handler.rb} +3 -3
  28. data/test/hexapdf/{type → digital_signature}/test_signature.rb +7 -7
  29. data/test/hexapdf/digital_signature/test_signatures.rb +137 -0
  30. data/test/hexapdf/digital_signature/test_signing.rb +53 -0
  31. data/test/hexapdf/{type/signature → digital_signature}/test_verification_result.rb +7 -7
  32. data/test/hexapdf/test_dictionary_fields.rb +2 -1
  33. data/test/hexapdf/test_document.rb +1 -1
  34. data/test/hexapdf/test_writer.rb +3 -3
  35. metadata +25 -15
  36. data/lib/hexapdf/document/signatures.rb +0 -546
  37. data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +0 -135
  38. data/lib/hexapdf/type/signature/adbe_x509_rsa_sha1.rb +0 -95
  39. data/lib/hexapdf/type/signature/handler.rb +0 -140
  40. data/test/hexapdf/document/test_signatures.rb +0 -352
@@ -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
@@ -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:: If an IO object is provided, then this document can read PDF objects from this IO
156
- # object, otherwise it can only contain created PDF objects.
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:: A hash with options for decrypting the PDF objects loaded from the IO.
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:: A hash with configuration options that is deep-merged into the default configuration
161
- # (see
162
- # HexaPDF::DefaultDocumentConfiguration[../index.html#DefaultDocumentConfiguration],
163
- # meaning that direct sub-hashes are merged instead of overwritten.
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 an array with the digital signatures of this document.
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::Document::Signatures#add.
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::Document::Signatures#handler for details).
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 HexaPDF::Document::Signatures::DefaultHandler.
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.handler(name: handler, **handler_options)
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')
@@ -37,6 +37,6 @@
37
37
  module HexaPDF
38
38
 
39
39
  # The version of HexaPDF.
40
- VERSION = '0.28.0'
40
+ VERSION = '0.29.0'
41
41
 
42
42
  end
@@ -6,7 +6,7 @@ module HexaPDF
6
6
  class Certificates
7
7
 
8
8
  def ca_key
9
- @ca_key ||= OpenSSL::PKey::RSA.new(512)
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(512)
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