Browse Source

Fixes #4503 - Improve S/MIME integration by adding meta information.

Co-authored-by: Florian Liebe <fl@zammad.com>
Co-authored-by: Tobias Schäfer <ts@zammad.com>
Florian Liebe 1 year ago
parent
commit
60e18bd980

+ 32 - 7
app/controllers/integration/smime_controller.rb

@@ -8,7 +8,7 @@ class Integration::SMIMEController < ApplicationController
 
     send_data(
       cert.raw,
-      filename:    "#{cert.doc_hash}.crt",
+      filename:    "#{cert.subject_hash}.crt",
       type:        'text/plain',
       disposition: 'attachment'
     )
@@ -19,17 +19,16 @@ class Integration::SMIMEController < ApplicationController
 
     send_data(
       cert.private_key,
-      filename:    "#{cert.doc_hash}.key",
+      filename:    "#{cert.subject_hash}.key",
       type:        'text/plain',
       disposition: 'attachment'
     )
   end
 
   def certificate_list
-    all = SMIMECertificate.all.map do |cert|
-      cert.attributes.merge({ 'subject_alternative_name' => cert.email_addresses })
-    end
-    render json: all
+    list = SMIMECertificate.all.map { |cert| cert_obj_to_json(cert) }
+
+    render json: list
   end
 
   def certificate_delete
@@ -45,11 +44,14 @@ class Integration::SMIMEController < ApplicationController
       string = params[:file].read.force_encoding('utf-8')
     end
 
+    cert = SecureMailing::SMIME::Certificate.parse(string)
+    cert.valid_smime_certificate!
+
     items = SMIMECertificate.create_certificates(string)
 
     render json: {
       result:   'ok',
-      response: items,
+      response: items.map { |c| cert_obj_to_json(c) },
     }
   rescue => e
     unprocessable_entity(e)
@@ -74,6 +76,9 @@ class Integration::SMIMEController < ApplicationController
 
     raise __("Parameter 'data' or 'file' required.") if string.blank?
 
+    private_key = SecureMailing::SMIME::PrivateKey.read(string, params[:secret])
+    private_key.valid_smime_private_key!
+
     SMIMECertificate.create_certificates(string)
     SMIMECertificate.create_private_keys(string, params[:secret])
 
@@ -105,4 +110,24 @@ class Integration::SMIMEController < ApplicationController
       commentPlaceholders: method_result.message_placeholders,
     }
   end
+
+  def cert_obj_to_json(cert)
+    info = cert.parsed
+
+    {
+      id:                       cert.id,
+      subject:                  info.subject.to_s,
+      doc_hash:                 cert.subject_hash,
+      fingerprint:              cert.fingerprint,
+      modulus:                  cert.uid,
+      not_before_at:            info.not_before,
+      not_after_at:             info.not_after,
+      raw:                      cert.pem,
+      private_key:              cert.private_key,
+      private_key_secret:       cert.private_key_secret,
+      created_at:               cert.created_at,
+      updated_at:               cert.updated_at,
+      subject_alternative_name: cert.email_addresses.join(', ')
+    }
+  end
 end

+ 86 - 115
app/models/smime_certificate.rb

@@ -1,153 +1,124 @@
 # Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/
 
 class SMIMECertificate < ApplicationModel
-  default_scope { order(not_after_at: :desc, not_before_at: :desc, id: :desc) }
+  default_scope { order(created_at: :desc, id: :desc) }
 
   validates :fingerprint, uniqueness: { case_sensitive: true }
 
-  def self.parts(raw)
-    raw.scan(%r{-----BEGIN[^-]+-----.+?-----END[^-]+-----}m)
-  end
+  # public class methods
 
-  def self.create_private_keys(raw, secret)
-    parts(raw).select { |part| part.include?('PRIVATE KEY') }.each do |part|
-      private_key = OpenSSL::PKey.read(part, secret)
-      modulus     = private_key.public_key.n.to_s(16)
-      certificate = find_by(modulus: modulus)
+  def self.find_for_multiple_email_addresses!(addresses, filter: nil, blame: false)
+    certificates      = []
+    missing_addresses = []
 
-      raise Exceptions::UnprocessableEntity, __('The certificate for this private key could not be found.') if !certificate
+    addresses.each do |address|
+      certs = find_by_email_address(address, filter: filter)
+
+      if certs.blank? && blame
+        missing_addresses << address
+        next
+      end
 
-      certificate.update!(private_key: part, private_key_secret: secret)
+      certificates.push(*certs)
     end
+
+    raise ActiveRecord::RecordNotFound, "Can't find S/MIME encryption certificates for: #{missing_addresses.join(', ')}" if missing_addresses.present? && blame
+
+    certificates
   end
 
-  def self.create_certificates(raw)
-    parts(raw).select { |part| part.include?('CERTIFICATE') }.each_with_object([]) do |part, result|
-      result << create!(public_key: part)
+  def self.find_by_email_address(address, filter: nil)
+    cert_selector = SMIMECertificate.where(SqlHelper.new(object: SMIMECertificate).array_contains_one('email_addresses', address.downcase))
+
+    return cert_selector.all if filter.nil?
+
+    filter.each do |filter_key, filter_value|
+      cert_selector = send("filter_#{filter_key}", cert_selector, filter_value)
+      return [] if cert_selector.blank?
     end
-  end
 
-  def self.parse(raw)
-    OpenSSL::X509::Certificate.new(raw.gsub(%r{(?:TRUSTED\s)?(CERTIFICATE---)}, '\1'))
+    cert_selector
   end
 
-  # Search for the certificate of the given sender email address
-  #
-  # @example
-  #  certificate = SMIMECertificates.for_sender_email_address('some1@example.com')
-  #  # => #<SMIMECertificate:0x00007fdd4e27eec0...
-  #
-  # @return [SMIMECertificate, nil] The found certificate record or nil
-  def self.for_sender_email_address(address)
-    downcased_address = address.downcase
-    where.not(private_key: nil).all.as_batches do |certificate|
-      next if certificate.key_usage_prohibits?('Digital Signature') # rubocop:disable Zammad/DetectTranslatableString
-
-      return certificate if certificate.email_addresses.include?(downcased_address)
+  def self.create_certificates(pem)
+    parts(pem).select { |part| part.include?('CERTIFICATE') }.each_with_object([]) do |part, result|
+      result << create!(public_key: part)
     end
   end
 
-  # Search for certificates of the given recipients email addresses
-  #
-  # @example
-  #  certificates = SMIMECertificates.for_recipient_email_addresses!(['some1@example.com', 'some2@example.com'])
-  #  # => [#<SMIMECertificate:0x00007fdd4e27eec0...
-  #
-  # @raise [ActiveRecord::RecordNotFound] if there are recipients for which no certificate could be found
-  #
-  # @return [Array<SMIMECertificate>] The found certificate records
-  def self.for_recipient_email_addresses!(addresses)
-    certificates        = []
-    remaining_addresses = addresses.map(&:downcase)
-    all.as_batches do |certificate|
-
-      # intersection of both lists
-      certificate_for = certificate.email_addresses & remaining_addresses
-      next if certificate_for.blank?
-      next if certificate.key_usage_prohibits?('Key Encipherment') # rubocop:disable Zammad/DetectTranslatableString
-
-      certificates.push(certificate)
-
-      # subtract found recipient(s)
-      remaining_addresses -= certificate_for
-
-      # end loop if no addresses are remaining
-      break if remaining_addresses.blank?
-    end
+  def self.create_private_keys(pem, secret)
+    parts(pem).select { |part| part.include?('PRIVATE KEY') }.each do |part|
+      private_key = SecureMailing::SMIME::PrivateKey.new(part, secret)
+      private_key.valid_smime_private_key!
 
-    return certificates if remaining_addresses.blank?
+      certificate = find_by(uid: private_key.uid)
+      raise Exceptions::UnprocessableEntity, __('The certificate for this private key could not be found.') if !certificate
 
-    raise ActiveRecord::RecordNotFound, "Can't find S/MIME encryption certificates for: #{remaining_addresses.join(', ')}"
+      certificate.update!(private_key: private_key.pem, private_key_secret: secret)
+    end
   end
 
-  def key_usage_prohibits?(usage_type)
-    # Respect restriction of keyUsage extension, if present.
-    # See https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.3 and https://www.gradenegger.eu/?p=9563
-    parsed.extensions.find { |ext| ext.oid == 'keyUsage' }&.value&.exclude?(usage_type)
-  end
+  # private class methods
 
-  def public_key=(string)
-    cert = self.class.parse(string)
-
-    self.subject       = cert.subject
-    self.doc_hash      = cert.subject.hash.to_s(16)
-    self.fingerprint   = OpenSSL::Digest.new('SHA1', cert.to_der).to_s
-    self.modulus       = cert.public_key.n.to_s(16)
-    self.not_before_at = cert.not_before
-    self.not_after_at  = cert.not_after
-    self.raw           = cert.to_s
-  end
+  def self.filter_ignore_usable(cert_selector, filter_value)
+    return cert_selector if cert_selector.blank?
+    return cert_selector if filter_value
 
-  def parsed
-    @parsed ||= self.class.parse(raw)
+    cert_selector.select { |cert| cert.parsed.usable? }
   end
 
-  def email_addresses
-    @email_addresses ||= begin
-      subject_alt_name = parsed.extensions.detect { |extension| extension.oid == 'subjectAltName' }
-      if subject_alt_name.blank?
-        Rails.logger.warn <<~TEXT.squish
-          SMIMECertificate with ID #{id} has no subjectAltName
-          extension and therefore no email addresses assigned.
-          This makes it useless in terms of S/MIME. Please check.
-        TEXT
-
-        []
-      else
-        email_addresses_from_subject_alt_name(subject_alt_name)
-      end
-    end
-  end
+  def self.filter_key(cert_selector, filter_value)
+    raise ArgumentError, 'filter_value must be either "public" or "private"' if %w[public private].exclude?(filter_value.to_s)
+    return cert_selector if cert_selector.blank?
+    return cert_selector if filter_value.eql?('public')
 
-  def expired?
-    !Time.zone.now.between?(not_before_at, not_after_at)
+    cert_selector.where.not(private_key: nil)
   end
 
-  private
+  def self.filter_usage(cert_selector, filter_value)
+    raise ArgumentError, 'filter_value must be either "signature" or "encryption"' if %w[signature encryption].exclude?(filter_value.to_s)
+    return cert_selector if cert_selector.blank?
 
-  def email_addresses_from_subject_alt_name(subject_alt_name)
-    # ["IP Address:192.168.7.23", "IP Address:192.168.7.42", "email:jd@example.com", "email:John.Doe@example.com", "dirName:dir_sect"]
-    entries = subject_alt_name.value.split(%r{,\s?})
+    cert_selector.select { |cert| cert.parsed.send("#{filter_value}?") }
+  end
 
-    entries.each_with_object([]) do |entry, result|
-      # ["email:jd@example.com", "email:John.Doe@example.com"]
-      identifier, email_address = entry.split(':').map(&:downcase)
+  def self.parts(pem)
+    pem.scan(%r{-----BEGIN[^-]+-----.+?-----END[^-]+-----}m)
+  end
 
-      # See: https://stackoverflow.com/a/20671427
-      # ["email:jd@example.com", "emailAddress:jd@example.com", "rfc822:jd@example.com", "rfc822Name:jd@example.com"]
-      next if identifier.exclude?('email') && identifier.exclude?('rfc822')
+  private_class_method %i[
+    filter_ignore_usable
+    filter_key
+    filter_usage
+    parts
+  ]
 
-      if !EmailAddressValidation.new(email_address).valid?
-        Rails.logger.warn <<~TEXT.squish
-          SMIMECertificate with ID #{id} has the malformed email address "#{email_address}"
-          stored as "#{identifier}" in the subjectAltName extension.
-          This makes it useless in terms of S/MIME. Please check.
-        TEXT
+  # public instance methods
 
-        next
-      end
+  def parsed
+    @parsed ||= SecureMailing::SMIME::Certificate.new(pem)
+  end
 
-      result.push(email_address)
-    end
+  def public_key=(string)
+    cert = SecureMailing::SMIME::Certificate.new(string)
+
+    self.email_addresses = cert.email_addresses
+    self.pem             = cert.to_pem
+
+    # The fingerprint is a hash of the certificate in DER format.
+    self.fingerprint = cert.fingerprint
+
+    # The following both attributes are hashes of the certificate issuer and
+    # subject strings.
+    # They are used for certificate chain checks.
+    #
+    # Because of legacy certificates the usage of the x509 extension
+    # "subjectKeyIdentifier" and "authorityKeyIdentifier" is not possible.
+    self.issuer_hash  = cert.issuer_hash
+    self.subject_hash = cert.subject_hash
+
+    # This is a unique information of the public key.
+    # It is used to find the corresponding private key.
+    self.uid = cert.uid
   end
 end

+ 13 - 9
db/migrate/20120101000001_create_base.rb

@@ -748,20 +748,24 @@ class CreateBase < ActiveRecord::Migration[4.2]
     add_index :active_job_locks, :active_job_id, unique: true
 
     create_table :smime_certificates do |t|
-      t.string :subject,            limit: 500,  null: false
-      t.string :doc_hash,           limit: 250,  null: false
       t.string :fingerprint,        limit: 250,  null: false
-      t.string :modulus,            limit: 1024, null: false
-      t.datetime :not_before_at,                 null: true, limit: 3
-      t.datetime :not_after_at,                  null: true, limit: 3
-      t.binary :raw,                limit: 10.megabytes,  null: false
+      t.string :uid,                limit: 1024, null: false
+
+      if Rails.application.config.db_column_array
+        t.string :email_addresses, null: true, array: true
+      else
+        t.json :email_addresses, null: true
+      end
+
+      t.binary :pem,                limit: 10.megabytes,  null: false
       t.binary :private_key,        limit: 10.megabytes,  null: true
-      t.string :private_key_secret, limit: 500,  null: true
+      t.string :private_key_secret, limit: 500,           null: true
+      t.string :issuer_hash,        limit: 128,           null: true
+      t.string :subject_hash,       limit: 128,           null: true
       t.timestamps limit: 3, null: false
     end
     add_index :smime_certificates, [:fingerprint], unique: true
-    add_index :smime_certificates, [:modulus]
-    add_index :smime_certificates, [:subject]
+    add_index :smime_certificates, [:uid]
 
     create_table :data_privacy_tasks do |t|
       t.column :state,                :string, limit: 150, default: 'in process', null: true

+ 47 - 0
db/migrate/20230726082734_smime_meta_information_table.rb

@@ -0,0 +1,47 @@
+# Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/
+
+class SMIMEMetaInformationTable < ActiveRecord::Migration[6.1]
+  def change
+    # return if it's a new setup
+    return if !Setting.exists?(name: 'system_init_done')
+
+    migrate_table
+  end
+
+  private
+
+  def migrate_table
+    change_table :smime_certificates do |t|
+      remove_columns(t)
+      rename_columns(t)
+      add_columns(t)
+    end
+
+    SMIMECertificate.reset_column_information
+  end
+
+  def remove_columns(t)
+    t.remove_index :modulus
+    t.remove_index :subject
+
+    t.remove :subject, :doc_hash, :not_before_at, :not_after_at
+  end
+
+  def rename_columns(t)
+    t.rename :modulus, :uid
+    t.rename :raw, :pem
+  end
+
+  def add_columns(t)
+    if Rails.application.config.db_column_array
+      t.column :email_addresses, :string, null: true, array: true
+    else
+      t.column :email_addresses, :json, null: true
+    end
+
+    t.string :issuer_hash,  limit: 128, null: true
+    t.string :subject_hash, limit: 128, null: true
+
+    t.index [:uid]
+  end
+end

+ 32 - 0
db/migrate/20230728073916_smime_meta_information_data.rb

@@ -0,0 +1,32 @@
+# Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/
+
+class SMIMEMetaInformationData < ActiveRecord::Migration[6.1]
+  def change # rubocop:disable Metrics/AbcSize
+    # return if it's a new setup
+    return if !Setting.exists?(name: 'system_init_done')
+
+    SMIMECertificate.in_batches.each_record do |record|
+      begin
+        cert = SecureMailing::SMIME::Certificate.new(record.pem)
+        data = {
+          email_addresses: cert.email_addresses,
+          issuer_hash:     cert.issuer.hash.to_s(16),
+          subject_hash:    cert.subject.hash.to_s(16)
+        }
+      rescue
+        Rails.logger.warn <<~TEXT.squish
+          SMIME: The migration of the certificate with fingerprint #{record.fingerprint} failed.
+          The certificate might not be usable anymore.
+        TEXT
+
+        data = {
+          email_addresses: [],
+          issuer_hash:     '',
+          subject_hash:    ''
+        }
+      end
+
+      record.update!(data)
+    end
+  end
+end

+ 18 - 2
i18n/zammad.pot

@@ -11022,7 +11022,7 @@ msgid "The browser is outdated. It does not support WebSocket - the technology w
 msgstr ""
 
 #: lib/secure_mailing/smime/security_options.rb
-msgid "The certificate for %s was found, but has expired."
+msgid "The certificate for %s was found, but it is not valid yet or has expired."
 msgstr ""
 
 #: lib/secure_mailing/smime/security_options.rb
@@ -11041,6 +11041,14 @@ msgstr ""
 msgid "The certificate for verification could not be found."
 msgstr ""
 
+#: lib/secure_mailing/smime/certificate.rb
+msgid "The certificate is not valid for S/MIME usage. Please check the certificate format."
+msgstr ""
+
+#: lib/secure_mailing/smime/certificate.rb
+msgid "The certificate is not valid for S/MIME usage. Please check the key usage, subject alternative name and public key cryptographic algorithm."
+msgstr ""
+
 #: app/assets/javascripts/app/views/integration/exchange_certificate_issue.jst.eco
 msgid "The certificate of the domain |%s| could not be verified. This may allow hackers to steal your credentials. If you are sure that you are using a self-signed certificate, you can press \"Proceed\". Otherwise, please \"Cancel\"."
 msgstr ""
@@ -11277,6 +11285,14 @@ msgstr ""
 msgid "The private key for decryption could not be found."
 msgstr ""
 
+#: lib/secure_mailing/smime/private_key.rb
+msgid "The private key is not valid for S/MIME usage. Please check the key cryptographic algorithm."
+msgstr ""
+
+#: lib/secure_mailing/smime/private_key.rb
+msgid "The private key is not valid for S/MIME usage. Please check the key format and the secret."
+msgstr ""
+
 #: app/frontend/apps/mobile/pages/account/views/AccountOverview.vue
 msgid "The product version could not be fetched."
 msgstr ""
@@ -11744,7 +11760,7 @@ msgid "There were PGP keys found for %s, but at least one of them has expired."
 msgstr ""
 
 #: lib/secure_mailing/smime/security_options.rb
-msgid "There were certificates found for %s, but at least one of them has expired."
+msgid "There were certificates found for %s, but at least one of them is not valid yet or has expired."
 msgstr ""
 
 #: app/assets/javascripts/app/controllers/_manage/security.coffee

+ 114 - 0
lib/secure_mailing/smime/certificate.rb

@@ -0,0 +1,114 @@
+# Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/
+
+class SecureMailing::SMIME::Certificate < OpenSSL::X509::Certificate
+  include SecureMailing::SMIME::Certificate::Attributes
+
+  attr_reader :email_addresses, :fingerprint, :issuer_hash, :uid, :subject_hash
+
+  def self.parse(pem)
+    begin
+      new(pem)
+    rescue OpenSSL::X509::CertificateError
+      raise Exceptions::UnprocessableEntity, __('The certificate is not valid for S/MIME usage. Please check the certificate format.')
+    end
+  end
+
+  def initialize(pem)
+    super(pem.gsub(%r{(?:TRUSTED\s)?(CERTIFICATE---)}, '\1'))
+
+    @email_addresses = fetch_email_addresses
+    @fingerprint     = OpenSSL::Digest.new('SHA1', to_der).to_s
+    @subject_hash    = subject.hash.to_s(16)
+    @issuer_hash     = issuer.hash.to_s(16)
+
+    @uid = determine_uid
+  end
+
+  def ca?
+    return false if !extensions_as_hash.key?('basicConstraints')
+
+    basic_constraints = extensions_as_hash['basicConstraints']
+    return false if basic_constraints.exclude?('CA:TRUE')
+
+    true
+  end
+
+  def rsa?
+    public_key.class.name.end_with?('RSA')
+  end
+
+  def ec?
+    public_key.class.name.end_with?('EC')
+  end
+
+  def effective?
+    Time.zone.now >= not_before
+  end
+
+  def expired?
+    Time.zone.now > not_after
+  end
+
+  def applicable?
+    return false if ca?
+
+    extended_key_usage = extensions_as_hash['extendedKeyUsage']
+
+    # This is necessary because some legacy certificates may not have an extended key usage.
+    return true if extended_key_usage.nil?
+
+    extended_key_usage.include?('E-mail Protection')
+  end
+
+  def signature?
+    return false if ca?
+
+    key_usage = extensions_as_hash['keyUsage']
+
+    # This is necessary because some legacy certificates may not have a key usage.
+    return true if key_usage.nil?
+
+    return false if !applicable?
+
+    key_usage.include?('Digital Signature')
+  end
+
+  def encryption?
+    return false if ca?
+
+    key_usage = extensions_as_hash['keyUsage']
+
+    # This is necessary because some legacy certificates may not have a key usage.
+    return true if key_usage.nil?
+
+    return false if !applicable?
+
+    key_usage.include?('Key Encipherment')
+  end
+
+  def usable?
+    effective? && !expired?
+  end
+
+  def valid_smime_certificate? # rubocop:disable Metrics/CyclomaticComplexity
+    return true if ca?
+
+    return false if !applicable?
+    return false if !signature? && !encryption?
+    return false if @email_addresses.blank?
+    return false if !rsa? && !ec?
+
+    true
+  end
+
+  def valid_smime_certificate!
+    return if valid_smime_certificate?
+
+    message = __('The certificate is not valid for S/MIME usage. Please check the key usage, subject alternative name and public key cryptographic algorithm.')
+
+    Rails.logger.error { "SMIME::Certificate: #{message}" }
+    Rails.logger.error { "SMIME::Certificate:\n #{to_text}" }
+
+    raise Exceptions::UnprocessableEntity, message
+  end
+end

+ 29 - 0
lib/secure_mailing/smime/certificate/attributes.rb

@@ -0,0 +1,29 @@
+# Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/
+
+module SecureMailing::SMIME::Certificate::Attributes
+  def extensions_as_hash
+    extensions.each_with_object({}) do |ext, hash|
+      hash[ext.oid] = ext.value.split(',').map(&:strip)
+    end
+  end
+
+  def fetch_email_addresses
+    subject_alt_name = extensions_as_hash['subjectAltName']
+    return [] if subject_alt_name.blank?
+
+    subject_alt_name.each_with_object([]) do |entry, result|
+      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
+
+  def determine_uid
+    return public_key.n.to_s(16) if rsa?
+
+    OpenSSL::Digest.new('SHA1', public_key.to_der).to_s
+  end
+end

+ 28 - 13
lib/secure_mailing/smime/incoming.rb

@@ -34,7 +34,8 @@ class SecureMailing::SMIME::Incoming < SecureMailing::Backend::HandlerIncoming
 
     success = false
     comment = __('The private key for decryption could not be found.')
-    ::SMIMECertificate.where.not(private_key: [nil, '']).find_each do |cert|
+
+    decryption_certificates.each do |cert|
       key = OpenSSL::PKey::RSA.new(cert.private_key, cert.private_key_secret)
 
       begin
@@ -46,9 +47,9 @@ class SecureMailing::SMIME::Incoming < SecureMailing::Backend::HandlerIncoming
       parse_decrypted_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!)"
+      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
@@ -96,17 +97,19 @@ class SecureMailing::SMIME::Incoming < SecureMailing::Backend::HandlerIncoming
   def verify_certificate_chain(certificates)
     return if certificates.blank?
 
-    subjects = certificates.map(&:subject).map(&:to_s)
-    return if subjects.blank?
+    subjects       = certificates.map(&:subject)
+    subject_hashes = subjects.map { |subject| subject.hash.to_s(16) }
+    return if subject_hashes.blank?
 
-    existing_certs = ::SMIMECertificate.where(subject: subjects).sort_by do |certificate|
+    existing_certs = ::SMIMECertificate.where(subject_hash: subject_hashes).sort_by do |certificate|
       # ensure that we have the same order as the certificates in the mail
-      subjects.index(certificate.subject)
+      subject_hashes.index(certificate.parsed.subject.hash.to_s(16))
     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." }
+    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
@@ -120,9 +123,9 @@ class SecureMailing::SMIME::Incoming < SecureMailing::Backend::HandlerIncoming
       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!)"
+        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(', ')
@@ -172,4 +175,16 @@ class SecureMailing::SMIME::Incoming < SecureMailing::Backend::HandlerIncoming
 
     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

+ 3 - 3
lib/secure_mailing/smime/notification_options.rb

@@ -7,14 +7,14 @@ class SecureMailing::SMIME::NotificationOptions < SecureMailing::Backend::Handle
 
   def check_sign
     return if from_certificate.nil?
-    return if from_certificate.expired?
+    return if !from_certificate.parsed.usable?
 
     security_options[:sign] = { success: true }
   end
 
   def check_encrypt
     begin
-      SMIMECertificate.for_recipient_email_addresses!(recipients)
+      SMIMECertificate.find_for_multiple_email_addresses!(recipients, filter: { key: 'public', ignore_usable: true, usage: :encryption }, blame: true)
       security_options[:encryption] = { success: true }
     rescue ActiveRecord::RecordNotFound
       # no-op
@@ -26,7 +26,7 @@ class SecureMailing::SMIME::NotificationOptions < SecureMailing::Backend::Handle
   def from_certificate
     @from_certificate ||= begin
       list = Mail::AddressList.new(from.email)
-      SMIMECertificate.for_sender_email_address(list.addresses.first.to_s)
+      SMIMECertificate.find_by_email_address(list.addresses.first.to_s, filter: { key: 'private', usage: :signature, ignore_usable: false }).first
     end
   end
 end

Some files were not shown because too many files changed in this diff