# Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ require 'rails_helper' RSpec.describe SecureMailing::SMIME do before do Setting.set('smime_integration', true) end let(:raw_body) { 'Some text' } let(:system_email_address) { 'smime1@example.com' } let(:customer_email_address) { 'smime2@example.com' } let(:cc_customer_email_address) { 'smime3@example.com' } let(:sender_certificate_subject) { "/C=DE/ST=Berlin/L=Berlin/O=Example Security/OU=IT Department/CN=example.com/emailAddress=#{sender_email_address}" } let(:recipient_certificate_subject) { "/C=DE/ST=Berlin/L=Berlin/O=Example Security/OU=IT Department/CN=example.com/emailAddress=#{recipient_email_address}" } let(:cc_recipient_certificate_subject) { "/C=DE/ST=Berlin/L=Berlin/O=Example Security/OU=IT Department/CN=example.com/emailAddress=#{cc_recipient_email_address}" } let(:expired_email_address) { 'expiredsmime1@example.com' } let(:ca_certificate_subject) { '/emailAddress=RootCA@example.com/C=DE/ST=Berlin/L=Berlin/O=Example Security/OU=IT Department/CN=example.com' } let(:intermediate_ca_certificate_subject) { '/C=DE/ST=Berlin/O=Example Security/OU=IT Department/CN=example.com/emailAddress=IntermediateCA@example.com' } let(:content_type) { 'text/plain' } def build_mail Channel::EmailBuild.build( from: sender_email_address, to: recipient_email_address, cc: cc_recipient_email_address, body: raw_body, content_type: content_type, security: security_preferences ) end describe '.outgoing' do shared_examples 'HttpLog writer' do |status| it "logs #{status}" do expect do build_mail rescue # allow failures end.to change(HttpLog, :count).by(1) expect(HttpLog.last.attributes).to include('direction' => 'out', 'status' => status) end end let(:sender_email_address) { system_email_address } let(:recipient_email_address) { customer_email_address } let(:cc_recipient_email_address) { cc_customer_email_address } context 'without security' do let(:security_preferences) do nil end it 'builds mail' do expect(build_mail.body).not_to match(SecureMailing::SMIME::Incoming::EXPRESSION_SIGNATURE) expect(build_mail.body).not_to match(SecureMailing::SMIME::Incoming::EXPRESSION_MIME) expect(build_mail.body).to eq(raw_body) end end context 'signing' do let(:security_preferences) do { type: 'S/MIME', sign: { success: true, }, encryption: { success: false, }, } end context 'private key present' do let!(:sender_certificate) do create(:smime_certificate, :with_private, fixture: system_email_address) end it 'builds mail' do expect(build_mail.body).to match(SecureMailing::SMIME::Incoming::EXPRESSION_SIGNATURE) end it_behaves_like 'HttpLog writer', 'success' context 'expired certificate' do let(:system_email_address) { expired_email_address } it 'raises exception' do expect { build_mail }.to raise_error RuntimeError end it_behaves_like 'HttpLog writer', 'failed' end context 'when message is 7bit or 8bit encoded' do let(:mail) do smime_mail = build_mail mail = Channel::EmailParser.new.parse(smime_mail.to_s) SecureMailing.incoming(mail) mail end context 'when Content-Type is text/plain' do let(:raw_body) { "\r\n\r\n@john.doe, now known as John Dóe has accepted your invitation to join the Administrator / htmltest project.\r\n\r\nhttp://169.254.169.254:3000/root/htmltest\r\n\r\n-- \r\nYou're receiving this email because of your account on 169.254.169.254.\r\n\r\n\r\n\r\n" } it 'verifies' do expect(mail['x-zammad-article-preferences']['security']['sign']['success']).to be true end end context 'when Content-Type is text/html' do let(:content_type) { 'text/html' } let(:raw_body) { "
" } it 'verifies' do expect(mail['x-zammad-article-preferences']['security']['sign']['success']).to be true end end end context 'when certificate chain is present' do let(:system_email_address) { 'chain@example.com' } let!(:chain) do [ sender_certificate, create(:smime_certificate, fixture: 'IntermediateCA'), create(:smime_certificate, fixture: 'RootCA'), ] end let(:p7enc) do mail = Channel::EmailParser.new.parse(build_mail.to_s) OpenSSL::PKCS7.read_smime(mail[:raw]) end it 'is included in the generated mail' do expect(p7enc.certificates.map(&:subject).sort).to eq(chain.map { |x| x.parsed.subject }.sort) end end end context 'no private key present' do before do create(:smime_certificate, fixture: system_email_address) end it 'raises exception' do expect { build_mail }.to raise_error RuntimeError end it_behaves_like 'HttpLog writer', 'failed' end end context 'encryption' do let(:security_preferences) do { type: 'S/MIME', sign: { success: false, }, encryption: { success: true, }, } end context 'public key present' do before do create(:smime_certificate, fixture: recipient_email_address) create(:smime_certificate, fixture: cc_recipient_email_address) end it 'builds mail' do mail = build_mail expect(mail['Content-Type'].value).to match(SecureMailing::SMIME::Incoming::EXPRESSION_MIME) expect(mail.body).not_to include(raw_body) end it_behaves_like 'HttpLog writer', 'success' context 'expired certificate' do let(:recipient_email_address) { expired_email_address } it 'raises exception' do expect { build_mail }.to raise_error RuntimeError end it_behaves_like 'HttpLog writer', 'failed' end end context 'no public key present' do it 'raises exception' do expect { build_mail }.to raise_error ActiveRecord::RecordNotFound end it_behaves_like 'HttpLog writer', 'failed' end end end describe '.incoming' do shared_examples 'HttpLog writer' do |status| it "logs #{status}" do expect do mail rescue # allow failures end.to change(HttpLog, :count).by(2) expect(HttpLog.last.attributes).to include('direction' => 'in', 'status' => status) end end let(:sender_email_address) { customer_email_address } let(:recipient_email_address) { system_email_address } let(:cc_recipient_email_address) { cc_customer_email_address } context 'signature verification' do let(:allow_expired) { false } let(:security_preferences) do { type: 'S/MIME', sign: { success: true, allow_expired: allow_expired, }, encryption: { success: false, }, } end context 'sender certificate present' do before do create(:smime_certificate, :with_private, fixture: sender_email_address) end let(:mail) do smime_mail = build_mail mail = Channel::EmailParser.new.parse(smime_mail.to_s) SecureMailing.incoming(mail) mail end it 'verifies' do expect(mail[:body]).to include(raw_body) expect(mail['x-zammad-article-preferences'][:security][:sign][:success]).to be true expect(mail['x-zammad-article-preferences'][:security][:sign][:comment]).to eq(sender_certificate_subject) expect(mail['x-zammad-article-preferences'][:security][:encryption][:success]).to be false expect(mail['x-zammad-article-preferences'][:security][:encryption][:comment]).to be_nil end it_behaves_like 'HttpLog writer', 'success' context 'expired' do # required to build mail with expired certificate let(:allow_expired) { true } let(:sender_email_address) { expired_email_address } it 'verifies with comment' do expect(mail[:body]).to include(raw_body) expect(mail['x-zammad-article-preferences'][:security][:sign][:success]).to be true expect(mail['x-zammad-article-preferences'][:security][:sign][:comment]).to include(expired_email_address).and include('expired') expect(mail['x-zammad-article-preferences'][:security][:encryption][:success]).to be false expect(mail['x-zammad-article-preferences'][:security][:encryption][:comment]).to be_nil end it_behaves_like 'HttpLog writer', 'success' end context 'with wrapped mime-type S/MIME signature (e.g. for Microsoft Outlook)' do before do # We need to disable the open ssl detached flag, to force the smime-type with 'signed-data'. stub_const('OpenSSL::PKCS7::DETACHED', nil) end it 'check that mail was verified' do expect(mail['x-zammad-article-preferences'][:security][:sign][:success]).to be true end it 'check that signe comment exists' do expect(mail['x-zammad-article-preferences'][:security][:sign][:comment]).to eq(sender_certificate_subject) end it 'check that body was verified' do expect(mail[:body]).to include(raw_body) end end context 'sender is signer' do let(:sender_email_address) { system_email_address } let(:mail) do smime_mail = Rails.root.join('spec/fixtures/files/smime/sender_is_signer.eml').read mail = Channel::EmailParser.new.parse(smime_mail.to_s) SecureMailing.incoming(mail) mail end it 'verifies' do expect(mail['x-zammad-article-preferences'][:security][:sign][:success]).to be true expect(mail['x-zammad-article-preferences'][:security][:sign][:comment]).to eq(sender_certificate_subject) expect(mail['x-zammad-article-preferences'][:security][:encryption][:success]).to be false expect(mail['x-zammad-article-preferences'][:security][:encryption][:comment]).to be_nil end end context 'sender is signer, with upcased sender address' do let(:sender_email_address) { system_email_address } let(:mail) do smime_mail = Rails.root.join('spec/fixtures/files/smime/sender_is_signer.eml').read.sub('smime1@example.com', 'SMIME1@example.com') mail = Channel::EmailParser.new.parse(smime_mail.to_s) SecureMailing.incoming(mail) mail end it 'verifies' do expect(mail['x-zammad-article-preferences'][:security][:sign][:success]).to be true expect(mail['x-zammad-article-preferences'][:security][:sign][:comment]).to eq(sender_certificate_subject) expect(mail['x-zammad-article-preferences'][:security][:encryption][:success]).to be false expect(mail['x-zammad-article-preferences'][:security][:encryption][:comment]).to be_nil end end context 'sender is not signer' do before do create(:smime_certificate, :with_private, fixture: system_email_address) end let(:mail) do smime_mail = Rails.root.join('spec/fixtures/files/smime/sender_not_signer.eml').read mail = Channel::EmailParser.new.parse(smime_mail.to_s) SecureMailing.incoming(mail) mail end it 'verifies with comment' do expect(mail['x-zammad-article-preferences'][:security][:sign][:success]).to be false expect(mail['x-zammad-article-preferences'][:security][:sign][:comment]).to eq('This message was not signed by its sender.') expect(mail['x-zammad-article-preferences'][:security][:encryption][:success]).to be false expect(mail['x-zammad-article-preferences'][:security][:encryption][:comment]).to be_nil end end context 'sender certificate with another sender name signed by trusted CA (#4457)' do before do create(:smime_certificate, fixture: 'SenderCA') end let(:mail) do smime_mail = Rails.root.join('spec/fixtures/files/smime/sender_is_signer_with_ca.eml').read mail = Channel::EmailParser.new.parse(smime_mail.to_s) SecureMailing.incoming(mail) mail end it 'verifies' do expect(mail[:from]).to include('Zammad Helpdesk ') expect(mail[:body]).to include(raw_body) expect(mail['x-zammad-article-preferences'][:security][:sign][:success]).to be true expect(mail['x-zammad-article-preferences'][:security][:sign][:comment]).to eq('/emailAddress=SenderCA@example.com/C=DE/ST=Berlin/L=Berlin/O=Example Security/OU=IT Department/CN=example.com') expect(mail['x-zammad-article-preferences'][:security][:encryption][:success]).to be false expect(mail['x-zammad-article-preferences'][:security][:encryption][:comment]).to be_nil end end end context 'no sender certificate' do let!(:sender_certificate) { create(:smime_certificate, :with_private, fixture: sender_email_address) } let(:mail) do smime_mail = build_mail mail = Channel::EmailParser.new.parse(smime_mail.to_s) sender_certificate.destroy! SecureMailing.incoming(mail) mail end it 'fails' do expect(mail[:body]).to include(raw_body) expect(mail['x-zammad-article-preferences'][:security][:sign][:success]).to be false expect(mail['x-zammad-article-preferences'][:security][:sign][:comment]).to eq('The certificate for verification could not be found.') expect(mail['x-zammad-article-preferences'][:security][:encryption][:success]).to be false expect(mail['x-zammad-article-preferences'][:security][:encryption][:comment]).to be_nil end context 'public key present in signature' do let(:not_related_fixture) { 'smime3@example.com' } let!(:not_related_certificate) { create(:smime_certificate, fixture: not_related_fixture) } context 'not related certificate present' do it 'fails' do expect(mail[:body]).to include(raw_body) expect(mail['x-zammad-article-preferences'][:security][:sign][:success]).to be false expect(mail['x-zammad-article-preferences'][:security][:sign][:comment]).to eq('The certificate for verification could not be found.') expect(mail['x-zammad-article-preferences'][:security][:encryption][:success]).to be false expect(mail['x-zammad-article-preferences'][:security][:encryption][:comment]).to be_nil end it_behaves_like 'HttpLog writer', 'failed' context 'CA' do let(:not_related_fixture) { 'ExpiredCA' } it 'fails' do expect(mail[:body]).to include(raw_body) expect(mail['x-zammad-article-preferences'][:security][:sign][:success]).to be false expect(mail['x-zammad-article-preferences'][:security][:sign][:comment]).to eq('The certificate for verification could not be found.') expect(mail['x-zammad-article-preferences'][:security][:encryption][:success]).to be false expect(mail['x-zammad-article-preferences'][:security][:encryption][:comment]).to be_nil end it_behaves_like 'HttpLog writer', 'failed' end end context 'usage not prevented' do before do # remove OpenSSL::PKCS7::NOINTERN stub_const('SecureMailing::SMIME::Incoming::OPENSSL_PKCS7_VERIFY_FLAGS', OpenSSL::PKCS7::NOVERIFY) end it 'does not perform verification' do expect(mail[:body]).to include(raw_body) expect(mail['x-zammad-article-preferences'][:security][:sign][:success]).to be false expect(mail['x-zammad-article-preferences'][:security][:sign][:comment]).to eq('The certificate for verification could not be found.') expect(mail['x-zammad-article-preferences'][:security][:encryption][:success]).to be false expect(mail['x-zammad-article-preferences'][:security][:encryption][:comment]).to be_nil end end end context 'intermediate CA present' do before do create(:smime_certificate, fixture: ca_fixture) end let(:ca_fixture) { 'IntermediateCA' } it 'verifies' do expect(mail[:body]).to include(raw_body) expect(mail['x-zammad-article-preferences'][:security][:sign][:success]).to be true expect(mail['x-zammad-article-preferences'][:security][:sign][:comment]).to eq(intermediate_ca_certificate_subject) expect(mail['x-zammad-article-preferences'][:security][:encryption][:success]).to be false expect(mail['x-zammad-article-preferences'][:security][:encryption][:comment]).to be_nil end it_behaves_like 'HttpLog writer', 'success' context 'expired' do let(:ca_fixture) { 'ExpiredIntermediateCA' } it 'fails' do expect(mail[:body]).to include(raw_body) expect(mail['x-zammad-article-preferences'][:security][:sign][:success]).to be false expect(mail['x-zammad-article-preferences'][:security][:sign][:comment]).to eq('The certificate for verification could not be found.') expect(mail['x-zammad-article-preferences'][:security][:encryption][:success]).to be false expect(mail['x-zammad-article-preferences'][:security][:encryption][:comment]).to be_nil end it_behaves_like 'HttpLog writer', 'failed' context 'allowed' do let(:allow_expired) { true } # ATTENTION: expired CA is a special case where `allow_expired` does not count it 'fails' do expect(mail[:body]).to include(raw_body) expect(mail['x-zammad-article-preferences'][:security][:sign][:success]).to be false expect(mail['x-zammad-article-preferences'][:security][:sign][:comment]).to eq('The certificate for verification could not be found.') expect(mail['x-zammad-article-preferences'][:security][:encryption][:success]).to be false expect(mail['x-zammad-article-preferences'][:security][:encryption][:comment]).to be_nil end it_behaves_like 'HttpLog writer', 'failed' end end end context 'certificate chain' do let(:sender_email_address) { 'chain@example.com' } let(:ca_subject_chain) { ca_chain.reverse.map { |cert| cert.parsed.subject }.join(', ') } context 'incomplete certificate chain present' do before do create(:smime_certificate, fixture: 'RootCA') end it 'fails' do expect(mail[:body]).to include(raw_body) expect(mail['x-zammad-article-preferences'][:security][:sign][:success]).to be false expect(mail['x-zammad-article-preferences'][:security][:sign][:comment]).to eq('The certificate for verification could not be found.') expect(mail['x-zammad-article-preferences'][:security][:encryption][:success]).to be false expect(mail['x-zammad-article-preferences'][:security][:encryption][:comment]).to be_nil end end context 'certificate chain only partly present' do let(:ca_certificate_subject) { subject_chain } let!(:ca_chain) do create_list(:smime_certificate, 1, fixture: 'IntermediateCA') end it 'verifies' do expect(mail[:body]).to include(raw_body) expect(mail['x-zammad-article-preferences'][:security][:sign][:success]).to be true expect(mail['x-zammad-article-preferences'][:security][:sign][:comment]).to eq(ca_subject_chain) expect(mail['x-zammad-article-preferences'][:security][:encryption][:success]).to be false expect(mail['x-zammad-article-preferences'][:security][:encryption][:comment]).to be_nil end end context 'complete certificate chain present' do let!(:ca_chain) do create_list(:smime_certificate, 1, fixture: 'ChainCA') end it 'verifies' do allow(Rails.logger).to receive(:warn) expect(mail[:body]).to include(raw_body) expect(mail['x-zammad-article-preferences'][:security][:sign][:success]).to be true expect(mail['x-zammad-article-preferences'][:security][:sign][:comment]).to eq(ca_subject_chain) expect(mail['x-zammad-article-preferences'][:security][:encryption][:success]).to be false expect(mail['x-zammad-article-preferences'][:security][:encryption][:comment]).to be_nil expect(Rails.logger).not_to have_received(:warn).with(%r{#{Regexp.escape(ca_certificate_subject)}}) end end end end end context 'decryption' do let(:allow_expired) { false } let(:security_preferences) do { type: 'S/MIME', sign: { success: false, }, encryption: { success: true, allow_expired: allow_expired, }, } end let!(:sender_certificate) { create(:smime_certificate, :with_private, fixture: sender_email_address) } let!(:recipient_certificate) { create(:smime_certificate, :with_private, fixture: recipient_email_address) } let!(:cc_recipient_certificate) { create(:smime_certificate, :with_private, fixture: cc_recipient_email_address) } context 'private key present' do let(:mail) do smime_mail = build_mail mail = Channel::EmailParser.new.parse(smime_mail.to_s) SecureMailing.incoming(mail) mail end it 'decrypts' do expect(mail[:body]).to include(raw_body) expect(mail['x-zammad-article-preferences'][:security][:sign][:success]).to be false expect(mail['x-zammad-article-preferences'][:security][:sign][:comment]).to be_nil expect(mail['x-zammad-article-preferences'][:security][:encryption][:success]).to be true expect(mail['x-zammad-article-preferences'][:security][:encryption][:comment]).to eq(recipient_certificate_subject) end it_behaves_like 'HttpLog writer', 'success' context 'expired allowed' do let(:allow_expired) { true } let(:system_email_address) { expired_email_address } it 'decrypts with comment' do expect(mail[:body]).to include(raw_body) expect(mail['x-zammad-article-preferences'][:security][:sign][:success]).to be false expect(mail['x-zammad-article-preferences'][:security][:sign][:comment]).to be_nil expect(mail['x-zammad-article-preferences'][:security][:encryption][:success]).to be true expect(mail['x-zammad-article-preferences'][:security][:encryption][:comment]).to include(expired_email_address).and include('expired') end it_behaves_like 'HttpLog writer', 'success' end end context 'no private key present' do let(:mail) do smime_mail = build_mail mail = Channel::EmailParser.new.parse(smime_mail.to_s) sender_certificate.destroy! recipient_certificate.destroy! cc_recipient_certificate.destroy! SecureMailing.incoming(mail) mail end it 'fails' do expect(mail[:body]).to include('no visible content') expect(mail['x-zammad-article-preferences'][:security][:sign][:success]).to be false expect(mail['x-zammad-article-preferences'][:security][:sign][:comment]).to be_nil expect(mail['x-zammad-article-preferences'][:security][:encryption][:success]).to be false expect(mail['x-zammad-article-preferences'][:security][:encryption][:comment]).to eq('The private key for decryption could not be found.') end it_behaves_like 'HttpLog writer', 'failed' end end context 'with signature verification and decryption' do let!(:sender_certificate) { create(:smime_certificate, :with_private, fixture: sender_email_address) } let!(:recipient_certificate) { create(:smime_certificate, :with_private, fixture: recipient_email_address) } let!(:cc_recipient_certificate) { create(:smime_certificate, :with_private, fixture: cc_recipient_email_address) } let(:security_preferences) do { type: 'S/MIME', sign: { success: true, }, encryption: { success: true, }, } end let(:mail) do smime_mail = build_mail mail = Channel::EmailParser.new.parse(smime_mail.to_s) SecureMailing.incoming(mail) mail end context 'with wrapped mime-type S/MIME signature (e.g. for Microsoft Outlook)' do before do # We need to disable the open ssl detached flag, to force the smime-type with 'signed-data'. stub_const('OpenSSL::PKCS7::DETACHED', nil) end it 'check that mail was decrypted' do expect(mail['x-zammad-article-preferences'][:security][:encryption][:success]).to be true end it 'check that encryption comment exists' do expect(mail['x-zammad-article-preferences'][:security][:encryption][:comment]).to eq(recipient_certificate_subject) end it 'check that mail was verified' do expect(mail['x-zammad-article-preferences'][:security][:sign][:success]).to be true end it 'check that signe comment exists' do expect(mail['x-zammad-article-preferences'][:security][:sign][:comment]).to eq(sender_certificate_subject) end it 'check that body was encrypted and verified' do expect(mail[:body]).to include(raw_body) end end end end describe '.retry' do let(:sender_email_address) { customer_email_address } let(:recipient_email_address) { system_email_address } let(:cc_recipient_email_address) { system_email_address } let(:security_preferences) do { type: 'S/MIME', sign: { success: true, }, encryption: { success: true, }, } end let(:mail) do sender_certificate = create(:smime_certificate, :with_private, fixture: sender_email_address) recipient_certificate = create(:smime_certificate, :with_private, fixture: system_email_address) smime_mail = Channel::EmailBuild.build( from: sender_email_address, to: recipient_email_address, body: raw_body, content_type: 'text/plain', security: security_preferences, attachments: [ { content_type: 'text/plain', content: 'blub', filename: 'test-file1.txt', }, ], ) mail = Channel::EmailParser.new.parse(smime_mail.to_s) sender_certificate.destroy recipient_certificate.destroy mail end let!(:article) do _ticket, article, _user, _mail = Channel::EmailParser.new.process({}, mail['raw']) article end context 'private key added' do before do create(:smime_certificate, :with_private, fixture: recipient_email_address) create(:smime_certificate, fixture: sender_email_address) end it 'succeeds' do SecureMailing.retry(article) expect(article.preferences[:security][:sign][:success]).to be true expect(article.preferences[:security][:sign][:comment]).to eq(sender_certificate_subject) expect(article.preferences[:security][:encryption][:success]).to be true expect(article.preferences[:security][:encryption][:comment]).to eq(recipient_certificate_subject) expect(article.body).to include(raw_body) expect(article.attachments.count).to eq(1) expect(article.attachments.first.filename).to eq('test-file1.txt') end context 'S/MIME activated' do before do Setting.set('smime_integration', false) end it 'succeeds' do Setting.set('smime_integration', true) SecureMailing.retry(article) expect(article.preferences[:security][:sign][:success]).to be true expect(article.preferences[:security][:sign][:comment]).to eq(sender_certificate_subject) expect(article.preferences[:security][:encryption][:success]).to be true expect(article.preferences[:security][:encryption][:comment]).to eq(recipient_certificate_subject) expect(article.body).to include(raw_body) expect(article.attachments.count).to eq(1) expect(article.attachments.first.filename).to eq('test-file1.txt') end end end end end