123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724 |
- # Copyright (C) 2012-2022 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(:sender_certificate_subject) { "/emailAddress=#{sender_email_address}/C=DE/ST=Berlin/L=Berlin/O=Example Security/OU=IT Department/CN=example.com" }
- let(:recipient_certificate_subject) { "/emailAddress=#{recipient_email_address}/C=DE/ST=Berlin/L=Berlin/O=Example Security/OU=IT Department/CN=example.com" }
- 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(:content_type) { 'text/plain' }
- def build_mail
- Channel::EmailBuild.build(
- from: sender_email_address,
- to: 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 }
- 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) { "<div><ul><li><p>an \nexample „Text“ with ümläütß. </p></li></ul></div>" }
- 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: 'ChainCA'),
- 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).to eq(chain.map(&:parsed))
- 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)
- 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 }
- 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
- 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('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('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('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 "won't 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('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 'root CA present' do
- before do
- create(:smime_certificate, fixture: ca_fixture)
- end
- let(:ca_fixture) { 'RootCA' }
- 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_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) { '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('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('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(&:subject).join(', ') }
- context 'incomplete certificate chain present' do
- before do
- create(:smime_certificate, fixture: 'RootCA')
- create(:smime_certificate, fixture: 'IntermediateCA')
- 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('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(:smime_certificate, fixture: 'IntermediateCA'),
- create(:smime_certificate, fixture: 'ChainCA'),
- ]
- 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(:smime_certificate, fixture: 'RootCA'),
- create(:smime_certificate, fixture: 'IntermediateCA'),
- create(:smime_certificate, 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) }
- 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!
- 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('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(: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 endcrypted 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(: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
|