smime_certificate.rb 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. # Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/
  2. class SMIMECertificate < ApplicationModel
  3. default_scope { order(not_after_at: :desc, not_before_at: :desc, id: :desc) }
  4. validates :fingerprint, uniqueness: { case_sensitive: true }
  5. def self.parts(raw)
  6. raw.scan(%r{-----BEGIN[^-]+-----.+?-----END[^-]+-----}m)
  7. end
  8. def self.create_private_keys(raw, secret)
  9. parts(raw).select { |part| part.include?('PRIVATE KEY') }.each do |part|
  10. private_key = OpenSSL::PKey.read(part, secret)
  11. modulus = private_key.public_key.n.to_s(16)
  12. certificate = find_by(modulus: modulus)
  13. raise Exceptions::UnprocessableEntity, __('The certificate for this private key could not be found.') if !certificate
  14. certificate.update!(private_key: part, private_key_secret: secret)
  15. end
  16. end
  17. def self.create_certificates(raw)
  18. parts(raw).select { |part| part.include?('CERTIFICATE') }.each_with_object([]) do |part, result|
  19. result << create!(public_key: part)
  20. end
  21. end
  22. def self.parse(raw)
  23. OpenSSL::X509::Certificate.new(raw.gsub(%r{(?:TRUSTED\s)?(CERTIFICATE---)}, '\1'))
  24. end
  25. # Search for the certificate of the given sender email address
  26. #
  27. # @example
  28. # certificate = SMIMECertificates.for_sender_email_address('some1@example.com')
  29. # # => #<SMIMECertificate:0x00007fdd4e27eec0...
  30. #
  31. # @return [SMIMECertificate, nil] The found certificate record or nil
  32. def self.for_sender_email_address(address)
  33. downcased_address = address.downcase
  34. where.not(private_key: nil).all.as_batches do |certificate|
  35. return certificate if certificate.email_addresses.include?(downcased_address)
  36. end
  37. end
  38. # Search for certificates of the given recipients email addresses
  39. #
  40. # @example
  41. # certificates = SMIMECertificates.for_recipipent_email_addresses!(['some1@example.com', 'some2@example.com'])
  42. # # => [#<SMIMECertificate:0x00007fdd4e27eec0...
  43. #
  44. # @raise [ActiveRecord::RecordNotFound] if there are recipients for which no certificate could be found
  45. #
  46. # @return [Array<SMIMECertificate>] The found certificate records
  47. def self.for_recipipent_email_addresses!(addresses)
  48. certificates = []
  49. remaining_addresses = addresses.map(&:downcase)
  50. all.as_batches do |certificate|
  51. # intersection of both lists
  52. cerfiticate_for = certificate.email_addresses & remaining_addresses
  53. next if cerfiticate_for.blank?
  54. certificates.push(certificate)
  55. # subtract found recipient(s)
  56. remaining_addresses -= cerfiticate_for
  57. # end loop if no addresses are remaining
  58. break if remaining_addresses.blank?
  59. end
  60. return certificates if remaining_addresses.blank?
  61. raise ActiveRecord::RecordNotFound, "Can't find S/MIME encryption certificates for: #{remaining_addresses.join(', ')}"
  62. end
  63. def public_key=(string)
  64. cert = self.class.parse(string)
  65. self.subject = cert.subject
  66. self.doc_hash = cert.subject.hash.to_s(16)
  67. self.fingerprint = OpenSSL::Digest.new('SHA1', cert.to_der).to_s
  68. self.modulus = cert.public_key.n.to_s(16)
  69. self.not_before_at = cert.not_before
  70. self.not_after_at = cert.not_after
  71. self.raw = cert.to_s
  72. end
  73. def parsed
  74. @parsed ||= self.class.parse(raw)
  75. end
  76. def email_addresses
  77. @email_addresses ||= begin
  78. subject_alt_name = parsed.extensions.detect { |extension| extension.oid == 'subjectAltName' }
  79. if subject_alt_name.blank?
  80. Rails.logger.warn <<~TEXT.squish
  81. SMIMECertificate with ID #{id} has no subjectAltName
  82. extension and therefore no email addresses assigned.
  83. This makes it useless in terms of S/MIME. Please check.
  84. TEXT
  85. []
  86. else
  87. email_addresses_from_subject_alt_name(subject_alt_name)
  88. end
  89. end
  90. end
  91. def expired?
  92. !Time.zone.now.between?(not_before_at, not_after_at)
  93. end
  94. private
  95. def email_addresses_from_subject_alt_name(subject_alt_name)
  96. # ["IP Address:192.168.7.23", "IP Address:192.168.7.42", "email:jd@example.com", "email:John.Doe@example.com", "dirName:dir_sect"]
  97. entries = subject_alt_name.value.split(%r{,\s?})
  98. entries.each_with_object([]) do |entry, result|
  99. # ["email:jd@example.com", "email:John.Doe@example.com"]
  100. identifier, email_address = entry.split(':').map(&:downcase)
  101. # See: https://stackoverflow.com/a/20671427
  102. # ["email:jd@example.com", "emailAddress:jd@example.com", "rfc822:jd@example.com", "rfc822Name:jd@example.com"]
  103. next if identifier.exclude?('email') && identifier.exclude?('rfc822')
  104. if !EmailAddressValidation.new(email_address).valid?
  105. Rails.logger.warn <<~TEXT.squish
  106. SMIMECertificate with ID #{id} has the malformed email address "#{email_address}"
  107. stored as "#{identifier}" in the subjectAltName extension.
  108. This makes it useless in terms of S/MIME. Please check.
  109. TEXT
  110. next
  111. end
  112. result.push(email_address)
  113. end
  114. end
  115. end