2021-06-01 12:20:20 +00:00
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
2020-06-02 11:01:16 +00:00
class SecureMailing :: SMIME :: Incoming < SecureMailing :: Backend :: Handler
2021-09-10 12:50:18 +00:00
attr_accessor :mail , :content_type
2020-06-02 11:01:16 +00:00
EXPRESSION_MIME = %r{ application/(x-pkcs7|pkcs7)-mime }i . freeze
2021-09-10 12:50:18 +00:00
EXPRESSION_SIGNATURE = %r{ (application/(x-pkcs7|pkcs7)-signature|signed-data) }i . freeze
2020-06-02 11:01:16 +00:00
OPENSSL_PKCS7_VERIFY_FLAGS = OpenSSL :: PKCS7 :: NOVERIFY | OpenSSL :: PKCS7 :: NOINTERN
def initialize ( mail )
2020-09-30 09:07:01 +00:00
super ( )
2020-06-02 11:01:16 +00:00
@mail = mail
2021-09-10 12:50:18 +00:00
@content_type = mail [ :mail_instance ] . content_type
2020-06-02 11:01:16 +00:00
end
def process
return if ! process?
initialize_article_preferences
decrypt
verify_signature
log
end
def initialize_article_preferences
article_preferences [ :security ] = {
type : 'S/MIME' ,
sign : {
success : false ,
comment : nil ,
} ,
encryption : {
success : false ,
comment : nil ,
}
}
end
def article_preferences
@article_preferences || = begin
2020-08-21 08:18:31 +00:00
key = :'x-zammad-article-preferences'
2021-09-10 12:50:18 +00:00
mail [ key ] || = { }
mail [ key ]
2020-06-02 11:01:16 +00:00
end
end
def process?
signed? || smime?
end
2021-09-10 12:50:18 +00:00
def signed? ( check_content_type = content_type )
EXPRESSION_SIGNATURE . match? ( check_content_type )
2020-06-02 11:01:16 +00:00
end
2021-09-10 12:50:18 +00:00
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 smime? ( check_content_type = content_type )
EXPRESSION_MIME . match? ( check_content_type )
2020-06-02 11:01:16 +00:00
end
def decrypt
return if ! smime?
success = false
comment = 'Unable to find private key to decrypt'
:: SMIMECertificate . where . not ( private_key : [ nil , '' ] ) . find_each do | cert |
key = OpenSSL :: PKey :: RSA . new ( cert . private_key , cert . private_key_secret )
begin
2021-09-10 12:50:18 +00:00
decrypted_data = decrypt_p7enc . decrypt ( key , cert . parsed )
2020-06-02 11:01:16 +00:00
rescue
next
end
2021-09-10 12:50:18 +00:00
parse_new_mail ( decrypted_data )
2020-06-02 11:01:16 +00:00
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!) "
end
# overwrite content_type for signature checking
2021-09-10 12:50:18 +00:00
@content_type = mail [ :mail_instance ] . content_type
2020-06-02 11:01:16 +00:00
break
end
article_preferences [ :security ] [ :encryption ] = {
success : success ,
comment : comment ,
}
end
def verify_signature
return if ! signed?
success = false
comment = 'Unable to find certificate for verification'
2021-09-10 12:50:18 +00:00
result = verify_certificate_chain ( verify_sign_p7enc . certificates )
2021-04-27 06:53:36 +00:00
if result . present?
success = true
comment = result
2020-06-02 11:01:16 +00:00
2021-09-10 12:50:18 +00:00
if signed_type == 'wrapped'
parse_new_mail ( verify_sign_p7enc . data )
end
mail [ :attachments ] . delete_if do | attachment |
2020-06-02 11:01:16 +00:00
signed? ( attachment . dig ( :preferences , 'Content-Type' ) )
end
end
article_preferences [ :security ] [ :sign ] = {
success : success ,
comment : comment ,
}
end
2021-04-27 06:53:36 +00:00
def verify_certificate_chain ( certificates )
return if certificates . blank?
2021-09-10 12:50:18 +00:00
subjects = certificates . map ( & :subject ) . map ( & :to_s )
2021-04-27 06:53:36 +00:00
return if subjects . blank?
existing_certs = :: SMIMECertificate . where ( subject : subjects ) . sort_by do | certificate |
# ensure that we have the same order as the certificates in the mail
subjects . index ( certificate . subject )
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. " }
end
begin
existing_certs_store = OpenSSL :: X509 :: Store . new
existing_certs . each do | existing_cert |
existing_certs_store . add_cert ( existing_cert . parsed )
end
2021-09-10 12:50:18 +00:00
success = verify_sign_p7enc . verify ( certificates , existing_certs_store , nil , OPENSSL_PKCS7_VERIFY_FLAGS )
2021-04-27 06:53:36 +00:00
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!) "
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
2021-09-10 12:50:18 +00:00
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 ] )
2020-06-02 11:01:16 +00:00
end
def log
% i [ sign encryption ] . each do | action |
result = article_preferences [ :security ] [ action ]
next if result . blank?
if result [ :success ]
status = 'success'
elsif result [ :comment ] . blank?
# means not performed
next
else
status = 'failed'
end
HttpLog . create (
direction : 'in' ,
facility : 'S/MIME' ,
2021-09-10 12:50:18 +00:00
url : " #{ mail [ :from ] } -> #{ mail [ :to ] } " ,
2020-06-02 11:01:16 +00:00
status : status ,
ip : nil ,
request : {
2021-09-10 12:50:18 +00:00
message_id : mail [ :message_id ] ,
2020-06-02 11:01:16 +00:00
} ,
response : article_preferences [ :security ] ,
method : action ,
created_by_id : 1 ,
updated_by_id : 1 ,
)
end
end
2021-09-10 12:50:18 +00:00
def parse_new_mail ( new_mail )
mail [ :mail_instance ] . header [ 'Content-Type' ] = nil
mail [ :mail_instance ] . header [ 'Content-Disposition' ] = nil
mail [ :mail_instance ] . header [ 'Content-Transfer-Encoding' ] = nil
mail [ :mail_instance ] . header [ 'Content-Description' ] = nil
new_raw_mail = " #{ mail [ :mail_instance ] . header } #{ new_mail } "
mail_new = Channel :: EmailParser . new . parse ( new_raw_mail )
mail_new . each do | local_key , local_value |
mail [ local_key ] = local_value
end
end
2020-06-02 11:01:16 +00:00
end