123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225 |
- # Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
- class SecureMailing::SMIME::Incoming < SecureMailing::Backend::Handler
- attr_accessor :mail, :content_type
- EXPRESSION_MIME = %r{application/(x-pkcs7|pkcs7)-mime}i.freeze
- EXPRESSION_SIGNATURE = %r{(application/(x-pkcs7|pkcs7)-signature|signed-data)}i.freeze
- OPENSSL_PKCS7_VERIFY_FLAGS = OpenSSL::PKCS7::NOVERIFY | OpenSSL::PKCS7::NOINTERN
- def initialize(mail)
- super()
- @mail = mail
- @content_type = mail[:mail_instance].content_type
- end
- def process
- return if !process?
- initialize_article_preferences
- decrypt
- verify_signature
- log
- end
- def initialize_article_preferences
- article_preferences[:security] = {
- type: 'S/MIME',
- sign: {
- success: false,
- comment: nil,
- },
- encryption: {
- success: false,
- comment: nil,
- }
- }
- end
- def article_preferences
- @article_preferences ||= begin
- key = :'x-zammad-article-preferences'
- mail[ key ] ||= {}
- mail[ key ]
- end
- end
- def process?
- signed? || smime?
- end
- def signed?(check_content_type = content_type)
- EXPRESSION_SIGNATURE.match?(check_content_type)
- end
- def signed_type
- @signed_type ||= begin
- # Special wrapped mime-type S/MIME signature check (e.g. for Microsoft Outlook).
- if content_type.include?('signed-data') && EXPRESSION_MIME.match?(content_type)
- 'wrapped'
- else
- 'inline'
- end
- end
- end
- def smime?(check_content_type = content_type)
- EXPRESSION_MIME.match?(check_content_type)
- end
- def decrypt
- return if !smime?
- success = false
- comment = __('Private key for decryption could not be found.')
- ::SMIMECertificate.where.not(private_key: [nil, '']).find_each do |cert|
- key = OpenSSL::PKey::RSA.new(cert.private_key, cert.private_key_secret)
- begin
- decrypted_data = decrypt_p7enc.decrypt(key, cert.parsed)
- rescue
- next
- end
- parse_new_mail(decrypted_data)
- success = true
- comment = cert.subject
- if cert.expired?
- comment += " (Certificate #{cert.fingerprint} with start date #{cert.not_before_at} and end date #{cert.not_after_at} expired!)"
- end
- # overwrite content_type for signature checking
- @content_type = mail[:mail_instance].content_type
- break
- end
- article_preferences[:security][:encryption] = {
- success: success,
- comment: comment,
- }
- end
- def verify_signature
- return if !signed?
- success = false
- comment = __('Certificate for verification could not be found.')
- result = verify_certificate_chain(verify_sign_p7enc.certificates)
- if result.present?
- success = true
- comment = result
- if signed_type == 'wrapped'
- parse_new_mail(verify_sign_p7enc.data)
- end
- mail[:attachments].delete_if do |attachment|
- signed?(attachment.dig(:preferences, 'Content-Type'))
- end
- end
- article_preferences[:security][:sign] = {
- success: success,
- comment: comment,
- }
- end
- def verify_certificate_chain(certificates)
- return if certificates.blank?
- subjects = certificates.map(&:subject).map(&:to_s)
- return if subjects.blank?
- existing_certs = ::SMIMECertificate.where(subject: subjects).sort_by do |certificate|
- # ensure that we have the same order as the certificates in the mail
- subjects.index(certificate.subject)
- end
- return if existing_certs.blank?
- if subjects.size > existing_certs.size
- Rails.logger.debug { "S/MIME mail signed with chain '#{subjects.join(', ')}' but only found '#{existing_certs.map(&:subject).join(', ')}' in database." }
- end
- begin
- existing_certs_store = OpenSSL::X509::Store.new
- existing_certs.each do |existing_cert|
- existing_certs_store.add_cert(existing_cert.parsed)
- end
- success = verify_sign_p7enc.verify(certificates, existing_certs_store, nil, OPENSSL_PKCS7_VERIFY_FLAGS)
- return if !success
- existing_certs.map do |existing_cert|
- result = existing_cert.subject
- if existing_cert.expired?
- result += " (Certificate #{existing_cert.fingerprint} with start date #{existing_cert.not_before_at} and end date #{existing_cert.not_after_at} expired!)"
- end
- result
- end.join(', ')
- rescue => e
- Rails.logger.error "Error while verifying mail with S/MIME certificate subjects: #{subjects}"
- Rails.logger.error e
- nil
- end
- end
- private
- def verify_sign_p7enc
- @verify_sign_p7enc ||= OpenSSL::PKCS7.read_smime(mail[:raw])
- end
- def decrypt_p7enc
- @decrypt_p7enc ||= OpenSSL::PKCS7.read_smime(mail[:raw])
- end
- def log
- %i[sign encryption].each do |action|
- result = article_preferences[:security][action]
- next if result.blank?
- if result[:success]
- status = 'success'
- elsif result[:comment].blank?
- # means not performed
- next
- else
- status = 'failed'
- end
- HttpLog.create(
- direction: 'in',
- facility: 'S/MIME',
- url: "#{mail[:from]} -> #{mail[:to]}",
- status: status,
- ip: nil,
- request: {
- message_id: mail[:message_id],
- },
- response: article_preferences[:security],
- method: action,
- created_by_id: 1,
- updated_by_id: 1,
- )
- end
- end
- def parse_new_mail(new_mail)
- mail[:mail_instance].header['Content-Type'] = nil
- mail[:mail_instance].header['Content-Disposition'] = nil
- mail[:mail_instance].header['Content-Transfer-Encoding'] = nil
- mail[:mail_instance].header['Content-Description'] = nil
- new_raw_mail = "#{mail[:mail_instance].header}#{new_mail}"
- mail_new = Channel::EmailParser.new.parse(new_raw_mail)
- mail_new.each do |local_key, local_value|
- mail[local_key] = local_value
- end
- end
- end
|