Class: HexaPDF::DigitalSignature::CMSHandler

Inherits:
Handler
  • Object
show all
Defined in:
lib/hexapdf/digital_signature/cms_handler.rb

Overview

The signature handler for PKCS#7 a.k.a. CMS signatures. Those include, for example, the adbe.pkcs7.detached and ETSI.CAdES.detached sub-filters.

See: PDF2.0 s12.8.3.3

Instance Attribute Summary

Attributes inherited from Handler

#signature_dict

Instance Method Summary collapse

Constructor Details

#initialize(signature_dict) ⇒ CMSHandler

Creates a new signature handler for the given signature dictionary.



50
51
52
53
# File 'lib/hexapdf/digital_signature/cms_handler.rb', line 50

def initialize(signature_dict)
  super
  @pkcs7 = OpenSSL::PKCS7.new(signature_dict.contents)
end

Instance Method Details

#certificate_chainObject

Returns the certificate chain.



70
71
72
# File 'lib/hexapdf/digital_signature/cms_handler.rb', line 70

def certificate_chain
  @pkcs7.certificates
end

#embedded_tsa_signatureObject

Returns the OpenSSL::PKCS7 object for the embedded TSA signature if there is one or nil otherwise.



87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/hexapdf/digital_signature/cms_handler.rb', line 87

def embedded_tsa_signature
  return @embedded_tsa_signature if defined?(@embedded_tsa_signature)

  @embedded_tsa_signature = nil
  p7 = OpenSSL::ASN1.decode(signature_dict.contents.sub(/\x00*\z/, ''))
  signed_data = p7.value[1].value[0]
  signer_info = signed_data.value[-1].value[0] # first (and only) signer info
  return unless signer_info.value[-1].tag == 1 # check for unsigned attributes
  timestamp_token = signer_info.value[-1].value.find do |unsigned_attr|
    unsigned_attr.value[0].value == "id-smime-aa-timeStampToken"
  end
  return unless timestamp_token
  @embedded_tsa_signature = OpenSSL::PKCS7.new(timestamp_token.value[1].value[0])
end

#signer_certificateObject

Returns the signer certificate (an instance of OpenSSL::X509::Certificate).



75
76
77
78
# File 'lib/hexapdf/digital_signature/cms_handler.rb', line 75

def signer_certificate
  info = signer_info
  certificate_chain.find {|cert| cert.issuer == info.issuer && cert.serial == info.serial }
end

#signer_infoObject

Returns the signer information object (an instance of OpenSSL::PKCS7::SignerInfo).



81
82
83
# File 'lib/hexapdf/digital_signature/cms_handler.rb', line 81

def signer_info
  @pkcs7.signers.first
end

#signer_nameObject

Returns the common name of the signer.



56
57
58
# File 'lib/hexapdf/digital_signature/cms_handler.rb', line 56

def signer_name
  signer_certificate.subject.to_a.assoc("CN")&.[](1) || super
end

#signing_timeObject

Returns the time of signing.



61
62
63
64
65
66
67
# File 'lib/hexapdf/digital_signature/cms_handler.rb', line 61

def signing_time
  if embedded_tsa_signature
    embedded_tsa_signature.signers.first.signed_time
  else
    signer_info.signed_time rescue super
  end
end

#verify(store, allow_self_signed: false) ⇒ Object

Verifies the signature using the provided OpenSSL::X509::Store object.



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/hexapdf/digital_signature/cms_handler.rb', line 103

def verify(store, allow_self_signed: false)
  result = super

  signer_info = self.signer_info
  signer_certificate = self.signer_certificate
  certificate_chain = self.certificate_chain

  if certificate_chain.empty?
    result.log(:error, "No certificates found in signature")
    return result
  end

  if @pkcs7.signers.size != 1
    result.log(:error, "Exactly one signer needed, found #{@pkcs7.signers.size}")
  end

  unless signer_certificate
    result.log(:error, "Signer serial=#{signer_info.serial} issuer=#{signer_info.issuer} " \
               "not found in certificates stored in PKCS7 object")
    return result
  end

  if embedded_tsa_signature
    result.log(:info, 'Signing time comes from timestamp authority')
  end

  key_usage = signer_certificate.extensions.find {|ext| ext.oid == 'keyUsage' }
  key_usage = key_usage&.value&.split(', ')
  if key_usage&.include?("Non Repudiation") && !key_usage.include?("Digital Signature")
    result.log(:info, 'Certificate used for non-repudiation')
  elsif !key_usage || !key_usage.include?("Digital Signature")
    result.log(:error, "Certificate key usage is missing 'Digital Signature' or 'Non Repudiation'")
  end

  if signature_dict.signature_type == 'ETSI.RFC3161'
    # Getting the needed values is not directly supported by Ruby OpenSSL
    p7 = OpenSSL::ASN1.decode(signature_dict.contents.sub(/\x00*\z/, ''))
    signed_data = p7.value[1].value[0]
     = signed_data.value[2]
    content = OpenSSL::ASN1.decode(.value[1].value[0].value)
    digest_algorithm = content.value[2].value[0].value[0].value
    original_hash = content.value[2].value[1].value
    recomputed_hash = OpenSSL::Digest.digest(digest_algorithm, signature_dict.signed_data)
    hash_valid = (original_hash == recomputed_hash)
  else
    data = signature_dict.signed_data
    hash_valid = true # hash will be checked by @pkcs7.verify
  end
  if hash_valid && @pkcs7.verify(certificate_chain, store, data,
                                 OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY)
    result.log(:info, "Signature valid")
  else
    result.log(:error, "Signature verification failed")
  end

  certs = [signer_certificate]
  cur_cert = certs.first
  while true
    cur_cert = certificate_chain.find {|cert| cert.subject == cur_cert.issuer }
    if cur_cert && !certs.include?(cur_cert)
      certs << cur_cert
    else
      break
    end
  end
  cert_subjects = certs.map {|cert| cert.subject.to_a.assoc("CN")&.[](1) }
  result.log(:info, "Certificate chain: #{cert_subjects.join(" -> ")}")

  result
end