incoming.rb 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. # Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/
  2. class SecureMailing::SMIME::Incoming < SecureMailing::Backend::HandlerIncoming
  3. EXPRESSION_MIME = %r{application/(x-pkcs7|pkcs7)-mime}i
  4. EXPRESSION_SIGNATURE = %r{(application/(x-pkcs7|pkcs7)-signature|signed-data)}i
  5. OPENSSL_PKCS7_VERIFY_FLAGS = OpenSSL::PKCS7::NOVERIFY | OpenSSL::PKCS7::NOINTERN
  6. def type
  7. 'S/MIME'
  8. end
  9. def signed?(check_content_type = content_type)
  10. EXPRESSION_SIGNATURE.match?(check_content_type)
  11. end
  12. def signed_type
  13. @signed_type ||= begin
  14. # Special wrapped mime-type S/MIME signature check (e.g. for Microsoft Outlook).
  15. if content_type.include?('signed-data') && EXPRESSION_MIME.match?(content_type)
  16. 'wrapped'
  17. else
  18. 'inline'
  19. end
  20. end
  21. end
  22. def encrypted?(check_content_type = content_type)
  23. EXPRESSION_MIME.match?(check_content_type)
  24. end
  25. def decrypt
  26. return if !encrypted?
  27. success = false
  28. comment = __('The private key for decryption could not be found.')
  29. decryption_certificates.each do |cert|
  30. key = OpenSSL::PKey::RSA.new(cert.private_key, cert.private_key_secret)
  31. begin
  32. decrypted_data = decrypt_p7enc.decrypt(key, cert.parsed)
  33. rescue
  34. next
  35. end
  36. parse_decrypted_mail(decrypted_data)
  37. success = true
  38. comment = cert.parsed.subject.to_s
  39. if !cert.parsed.usable?
  40. comment += " (Certificate #{cert.fingerprint} with start date #{cert.parsed.not_before} and end date #{cert.parsed.not_after} expired!)"
  41. end
  42. break
  43. end
  44. set_article_preferences(
  45. operation: :encryption,
  46. comment: comment,
  47. success: success,
  48. )
  49. end
  50. def verify_signature
  51. return if !signed?
  52. success = false
  53. comment = __('The certificate for verification could not be found.')
  54. result = verify_certificate_chain(verify_sign_p7enc.certificates)
  55. if result.present?
  56. success = true
  57. comment = result
  58. if signed_type == 'wrapped'
  59. parse_decrypted_mail(verify_sign_p7enc.data)
  60. end
  61. mail[:attachments].delete_if do |attachment|
  62. signed?(attachment.dig(:preferences, 'Content-Type'))
  63. end
  64. if !sender_is_signer?
  65. success = false
  66. comment = __('This message was not signed by its sender.')
  67. end
  68. end
  69. set_article_preferences(
  70. operation: :sign,
  71. comment: comment,
  72. success: success,
  73. )
  74. end
  75. def verify_certificate_chain(certificates)
  76. return if certificates.blank?
  77. subjects = certificates.map(&:subject)
  78. subject_hashes = subjects.map { |subject| subject.hash.to_s(16) }
  79. return if subject_hashes.blank?
  80. existing_certs = ::SMIMECertificate.where(subject_hash: subject_hashes).sort_by do |certificate|
  81. # ensure that we have the same order as the certificates in the mail
  82. subject_hashes.index(certificate.parsed.subject.hash.to_s(16))
  83. end
  84. return if existing_certs.blank?
  85. if subject_hashes.size > existing_certs.size
  86. existing_certs_subjects = existing_certs.map { |cert| cert.parsed.subject.to_s }.join(', ')
  87. Rails.logger.debug { "S/MIME mail signed with chain '#{subjects.join(', ')}' but only found '#{existing_certs_subjects}' in database." }
  88. end
  89. begin
  90. existing_certs_store = OpenSSL::X509::Store.new
  91. existing_certs.each do |existing_cert|
  92. existing_certs_store.add_cert(existing_cert.parsed)
  93. end
  94. success = verify_sign_p7enc.verify(certificates, existing_certs_store, nil, OPENSSL_PKCS7_VERIFY_FLAGS)
  95. return if !success
  96. existing_certs.map do |existing_cert|
  97. result = existing_cert.parsed.subject.to_s
  98. if !existing_cert.parsed.usable?
  99. result += " (Certificate #{existing_cert.fingerprint} with start date #{existing_cert.parsed.not_before} and end date #{existing_cert.parsed.not_after} expired!)"
  100. end
  101. result
  102. end.join(', ')
  103. rescue => e
  104. Rails.logger.error "Error while verifying mail with S/MIME certificate subjects: #{subjects}"
  105. Rails.logger.error e
  106. nil
  107. end
  108. end
  109. private
  110. def verify_sign_p7enc
  111. @verify_sign_p7enc ||= OpenSSL::PKCS7.read_smime(mail[:raw])
  112. end
  113. def decrypt_p7enc
  114. @decrypt_p7enc ||= OpenSSL::PKCS7.read_smime(mail[:raw])
  115. end
  116. def sender_is_signer?
  117. signers = email_addresses_from_subject_alt_name
  118. result = signers.include?(mail[:mail_instance].from.first.downcase)
  119. Rails.logger.warn { "S/MIME mail #{mail[:message_id]} signed by #{signers.join(', ')} but sender is #{mail[:mail_instance].from.first}" } if !result
  120. result
  121. end
  122. def email_addresses_from_subject_alt_name
  123. result = []
  124. @verify_sign_p7enc.certificates.each do |cert|
  125. subject_alt_name = cert.extensions.detect { |extension| extension.oid == 'subjectAltName' }
  126. next if subject_alt_name.nil?
  127. entries = subject_alt_name.value.split(%r{,\s?})
  128. entries.each do |entry|
  129. identifier, email_address = entry.split(':').map(&:downcase)
  130. next if identifier.exclude?('email') && identifier.exclude?('rfc822')
  131. next if !EmailAddressValidation.new(email_address).valid?
  132. result.push(email_address)
  133. end
  134. end
  135. result
  136. end
  137. def decryption_certificates
  138. certs = []
  139. mail[:mail_instance].to.each { |to| certs += ::SMIMECertificate.find_by_email_address(to, filter: { key: 'private', usage: :encryption }) }
  140. if mail[:mail_instance].cc.present?
  141. mail[:mail_instance].cc.each { |cc| certs += ::SMIMECertificate.find_by_email_address(cc, filter: { key: 'private', usage: :encryption }) }
  142. end
  143. certs
  144. end
  145. end