incoming.rb 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. # Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
  2. class SecureMailing::SMIME::Incoming < SecureMailing::Backend::Handler
  3. attr_accessor :mail, :content_type
  4. EXPRESSION_MIME = %r{application/(x-pkcs7|pkcs7)-mime}i.freeze
  5. EXPRESSION_SIGNATURE = %r{(application/(x-pkcs7|pkcs7)-signature|signed-data)}i.freeze
  6. OPENSSL_PKCS7_VERIFY_FLAGS = OpenSSL::PKCS7::NOVERIFY | OpenSSL::PKCS7::NOINTERN
  7. def initialize(mail)
  8. super()
  9. @mail = mail
  10. @content_type = mail[:mail_instance].content_type
  11. end
  12. def process
  13. return if !process?
  14. initialize_article_preferences
  15. decrypt
  16. verify_signature
  17. log
  18. end
  19. def initialize_article_preferences
  20. article_preferences[:security] = {
  21. type: 'S/MIME',
  22. sign: {
  23. success: false,
  24. comment: nil,
  25. },
  26. encryption: {
  27. success: false,
  28. comment: nil,
  29. }
  30. }
  31. end
  32. def article_preferences
  33. @article_preferences ||= begin
  34. key = :'x-zammad-article-preferences'
  35. mail[ key ] ||= {}
  36. mail[ key ]
  37. end
  38. end
  39. def process?
  40. signed? || smime?
  41. end
  42. def signed?(check_content_type = content_type)
  43. EXPRESSION_SIGNATURE.match?(check_content_type)
  44. end
  45. def signed_type
  46. @signed_type ||= begin
  47. # Special wrapped mime-type S/MIME signature check (e.g. for Microsoft Outlook).
  48. if content_type.include?('signed-data') && EXPRESSION_MIME.match?(content_type)
  49. 'wrapped'
  50. else
  51. 'inline'
  52. end
  53. end
  54. end
  55. def smime?(check_content_type = content_type)
  56. EXPRESSION_MIME.match?(check_content_type)
  57. end
  58. def decrypt
  59. return if !smime?
  60. success = false
  61. comment = __('Private key for decryption could not be found.')
  62. ::SMIMECertificate.where.not(private_key: [nil, '']).find_each do |cert|
  63. key = OpenSSL::PKey::RSA.new(cert.private_key, cert.private_key_secret)
  64. begin
  65. decrypted_data = decrypt_p7enc.decrypt(key, cert.parsed)
  66. rescue
  67. next
  68. end
  69. parse_new_mail(decrypted_data)
  70. success = true
  71. comment = cert.subject
  72. if cert.expired?
  73. comment += " (Certificate #{cert.fingerprint} with start date #{cert.not_before_at} and end date #{cert.not_after_at} expired!)"
  74. end
  75. # overwrite content_type for signature checking
  76. @content_type = mail[:mail_instance].content_type
  77. break
  78. end
  79. article_preferences[:security][:encryption] = {
  80. success: success,
  81. comment: comment,
  82. }
  83. end
  84. def verify_signature
  85. return if !signed?
  86. success = false
  87. comment = __('Certificate for verification could not be found.')
  88. result = verify_certificate_chain(verify_sign_p7enc.certificates)
  89. if result.present?
  90. success = true
  91. comment = result
  92. if signed_type == 'wrapped'
  93. parse_new_mail(verify_sign_p7enc.data)
  94. end
  95. mail[:attachments].delete_if do |attachment|
  96. signed?(attachment.dig(:preferences, 'Content-Type'))
  97. end
  98. end
  99. article_preferences[:security][:sign] = {
  100. success: success,
  101. comment: comment,
  102. }
  103. end
  104. def verify_certificate_chain(certificates)
  105. return if certificates.blank?
  106. subjects = certificates.map(&:subject).map(&:to_s)
  107. return if subjects.blank?
  108. existing_certs = ::SMIMECertificate.where(subject: subjects).sort_by do |certificate|
  109. # ensure that we have the same order as the certificates in the mail
  110. subjects.index(certificate.subject)
  111. end
  112. return if existing_certs.blank?
  113. if subjects.size > existing_certs.size
  114. Rails.logger.debug { "S/MIME mail signed with chain '#{subjects.join(', ')}' but only found '#{existing_certs.map(&:subject).join(', ')}' in database." }
  115. end
  116. begin
  117. existing_certs_store = OpenSSL::X509::Store.new
  118. existing_certs.each do |existing_cert|
  119. existing_certs_store.add_cert(existing_cert.parsed)
  120. end
  121. success = verify_sign_p7enc.verify(certificates, existing_certs_store, nil, OPENSSL_PKCS7_VERIFY_FLAGS)
  122. return if !success
  123. existing_certs.map do |existing_cert|
  124. result = existing_cert.subject
  125. if existing_cert.expired?
  126. result += " (Certificate #{existing_cert.fingerprint} with start date #{existing_cert.not_before_at} and end date #{existing_cert.not_after_at} expired!)"
  127. end
  128. result
  129. end.join(', ')
  130. rescue => e
  131. Rails.logger.error "Error while verifying mail with S/MIME certificate subjects: #{subjects}"
  132. Rails.logger.error e
  133. nil
  134. end
  135. end
  136. private
  137. def verify_sign_p7enc
  138. @verify_sign_p7enc ||= OpenSSL::PKCS7.read_smime(mail[:raw])
  139. end
  140. def decrypt_p7enc
  141. @decrypt_p7enc ||= OpenSSL::PKCS7.read_smime(mail[:raw])
  142. end
  143. def log
  144. %i[sign encryption].each do |action|
  145. result = article_preferences[:security][action]
  146. next if result.blank?
  147. if result[:success]
  148. status = 'success'
  149. elsif result[:comment].blank?
  150. # means not performed
  151. next
  152. else
  153. status = 'failed'
  154. end
  155. HttpLog.create(
  156. direction: 'in',
  157. facility: 'S/MIME',
  158. url: "#{mail[:from]} -> #{mail[:to]}",
  159. status: status,
  160. ip: nil,
  161. request: {
  162. message_id: mail[:message_id],
  163. },
  164. response: article_preferences[:security],
  165. method: action,
  166. created_by_id: 1,
  167. updated_by_id: 1,
  168. )
  169. end
  170. end
  171. def parse_new_mail(new_mail)
  172. mail[:mail_instance].header['Content-Type'] = nil
  173. mail[:mail_instance].header['Content-Disposition'] = nil
  174. mail[:mail_instance].header['Content-Transfer-Encoding'] = nil
  175. mail[:mail_instance].header['Content-Description'] = nil
  176. new_raw_mail = "#{mail[:mail_instance].header}#{new_mail}"
  177. mail_new = Channel::EmailParser.new.parse(new_raw_mail)
  178. mail_new.each do |local_key, local_value|
  179. mail[local_key] = local_value
  180. end
  181. end
  182. end