123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201 |
- # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
- class SecureMailing::SMIME::Incoming < SecureMailing::Backend::HandlerIncoming
- EXPRESSION_MIME = %r{application/(x-pkcs7|pkcs7)-mime}i
- EXPRESSION_SIGNATURE = %r{(application/(x-pkcs7|pkcs7)-signature|signed-data)}i
- OPENSSL_PKCS7_VERIFY_FLAGS = OpenSSL::PKCS7::NOVERIFY | OpenSSL::PKCS7::NOINTERN
- def type
- 'S/MIME'
- 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 encrypted?(check_content_type = content_type)
- EXPRESSION_MIME.match?(check_content_type)
- end
- def decrypt
- return if !encrypted?
- success = false
- comment = __('The private key for decryption could not be found.')
- decryption_certificates.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_decrypted_mail(decrypted_data)
- success = true
- comment = cert.parsed.subject.to_s
- if !cert.parsed.usable?
- comment += " (Certificate #{cert.fingerprint} with start date #{cert.parsed.not_before} and end date #{cert.parsed.not_after} expired!)"
- end
- break
- end
- set_article_preferences(
- operation: :encryption,
- comment: comment,
- success: success,
- )
- end
- def verify_signature
- return if !signed?
- success = false
- comment = __('The 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_decrypted_mail(verify_sign_p7enc.data)
- end
- mail[:attachments].delete_if do |attachment|
- signed?(attachment.dig(:preferences, 'Content-Type'))
- end
- if !sender_is_signer?
- success = false
- comment = __('This message was not signed by its sender.')
- end
- end
- set_article_preferences(
- operation: :sign,
- comment: comment,
- success: success,
- )
- end
- def verify_certificate_chain(certificates)
- return if certificates.blank?
- subjects = certificates.map(&:subject)
- subject_hashes = subjects.map { |subject| subject.hash.to_s(16) }
- return if subject_hashes.blank?
- # Try to find CA/Public key for the sender certificate
- # 1. In the SMIME store with the mail chain certifiates (reordered)
- # 2. In the SMIME store with the issuer of the sender certificate
- # 3. In the SSL store with the issuer of the sender certificate
- certificates_by_mail_chain = ::SMIMECertificate.where(subject_hash: subject_hashes).sort_by do |certificate|
- subject_hashes.index(certificate.parsed.subject.hash.to_s(16))
- end.presence
- certificate_by_issuer_smime_store = ::SMIMECertificate.where(subject_hash: certificates.first.issuer.hash.to_s(16)).presence
- certificate_by_issuer_ssl_store = ::SSLCertificate.where(subject: certificates.first.issuer.to_s, ca: true).filter_map do |cert|
- ::SMIMECertificate.new(public_key: cert.certificate)
- rescue
- next
- end.presence
- existing_certs = certificates_by_mail_chain || certificate_by_issuer_smime_store || certificate_by_issuer_ssl_store
- return if existing_certs.blank?
- if subject_hashes.size > existing_certs.size
- existing_certs_subjects = existing_certs.map { |cert| cert.parsed.subject.to_s }.join(', ')
- Rails.logger.debug { "S/MIME mail signed with chain '#{subjects.join(', ')}' but only found '#{existing_certs_subjects}' 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.parsed.subject.to_s
- if !existing_cert.parsed.usable?
- result += " (Certificate #{existing_cert.fingerprint} with start date #{existing_cert.parsed.not_before} and end date #{existing_cert.parsed.not_after} 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 sender_is_signer?
- signers = email_addresses_from_subject_alt_name
- result = signers.include?(mail[:mail_instance].from.first.downcase)
- Rails.logger.warn { "S/MIME mail #{mail[:message_id]} signed by #{signers.join(', ')} but sender is #{mail[:mail_instance].from.first}" } if !result
- result
- end
- def email_addresses_from_subject_alt_name
- result = []
- @verify_sign_p7enc.certificates.each do |cert|
- subject_alt_name = cert.extensions.detect { |extension| extension.oid == 'subjectAltName' }
- next if subject_alt_name.nil?
- entries = subject_alt_name.value.split(%r{,\s?})
- entries.each do |entry|
- identifier, email_address = entry.split(':').map(&:downcase)
- next if identifier.exclude?('email') && identifier.exclude?('rfc822')
- next if !EmailAddressValidation.new(email_address).valid?
- result.push(email_address)
- end
- end
- result
- end
- def decryption_certificates
- certs = []
- mail[:mail_instance].to.each { |to| certs += ::SMIMECertificate.find_by_email_address(to, filter: { key: 'private', usage: :encryption }) }
- if mail[:mail_instance].cc.present?
- mail[:mail_instance].cc.each { |cc| certs += ::SMIMECertificate.find_by_email_address(cc, filter: { key: 'private', usage: :encryption }) }
- end
- certs
- end
- end
|