# Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ require 'rails_helper' RSpec.describe SecureMailing::PGP, :aggregate_failures do before do Setting.set('pgp_integration', true) end let(:raw_body) { 'Testing some Content' } let(:attachments) { [] } let(:system_email_address) { 'pgp1@example.com' } let(:customer_email_address) { 'pgp2@example.com' } let(:cc_customer_email_address) { 'pgp3@example.com' } let(:expired_email_address) { 'expiredpgp1@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, attachments: attachments ) 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::PGP::Incoming::SIGNATURE_CONTENT_TYPE) expect(build_mail.body).to eq(raw_body) end end context 'with signing' do let(:security_preferences) do { type: 'PGP', sign: { success: true, }, encryption: { success: false, }, } end context 'when private key present' do before do create(:pgp_key, :with_private, fixture: system_email_address) end it 'builds mail' do expect(build_mail.body).to match(SecureMailing::PGP::Incoming::SIGNATURE_CONTENT_TYPE) end it_behaves_like 'HttpLog writer', 'success' context 'with expired key' do let(:system_email_address) { expired_email_address } it 'raises exception' do expect { build_mail }.to raise_error ActiveRecord::RecordNotFound end it_behaves_like 'HttpLog writer', 'failed' end end context 'when no private key is present' do it 'raises exception' do expect { build_mail }.to raise_error ActiveRecord::RecordNotFound end it_behaves_like 'HttpLog writer', 'failed' end end context 'with encryption' do let(:security_preferences) do { type: 'PGP', sign: { success: false, }, encryption: { success: true, }, } end context 'when all needed keys are present' do before do create(:pgp_key, :with_private, fixture: system_email_address) create(:pgp_key, fixture: recipient_email_address) create(:pgp_key, fixture: cc_recipient_email_address) end it 'builds mail' do mail = build_mail expect(mail['Content-Type'].value).to include('multipart/encrypted') expect(mail.body).not_to include(raw_body) end it_behaves_like 'HttpLog writer', 'success' end context 'when needed keys are not present' do it 'raises exception' do expect { build_mail }.to raise_error ActiveRecord::RecordNotFound end end context 'when one key is expired' do before do create(:pgp_key, :with_private, fixture: system_email_address) create(:pgp_key, fixture: recipient_email_address) create(:pgp_key, fixture: cc_recipient_email_address) end let(:customer_email_address) { expired_email_address } it 'raises exception' do expect { build_mail }.to raise_error ActiveRecord::RecordNotFound end end context 'when a key with multiple UIDs is present' do let(:customer_email_address) { 'multipgp2@example.com' } before do create(:pgp_key, :with_private, fixture: system_email_address) create(:pgp_key, fixture: recipient_email_address) create(:pgp_key, fixture: cc_customer_email_address) end it 'builds mail' do mail = build_mail expect(mail['Content-Type'].value).to include('multipart/encrypted') expect(mail.body).not_to include(raw_body) end it_behaves_like 'HttpLog writer', 'success' end end context 'with encryption and signing' do let(:security_preferences) do { type: 'PGP', sign: { success: true, }, encryption: { success: true, }, } end before do create(:pgp_key, :with_private, fixture: system_email_address) create(:pgp_key, :with_private, fixture: recipient_email_address) create(:pgp_key, fixture: cc_customer_email_address) end it 'builds mail' do mail = build_mail expect(mail['Content-Type'].value).to include('multipart/encrypted') expect(mail.body).not_to include(raw_body) end context 'with inline image' do let(:article) do create(:ticket_article, ticket: create(:ticket), body: '
some message article helper test1

') end let(:attachments) do create_list( :store, 1, object: 'Ticket::Article', o_id: article.id, data: 'fake', filename: 'inline_image.jpg', preferences: { 'Content-Type' => 'image/jpeg', 'Mime-Type' => 'image/jpeg', 'Content-ID' => '<15.274327094.140939>', 'Content-Disposition' => 'inline', } ) end let(:raw_body) do <<~MSG_HTML.chomp
some message article helper test1

MSG_HTML end let(:content_type) { 'text/html' } it 'builds mail' do mail = build_mail expect(mail['Content-Type'].value).to include('multipart/encrypted') end 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 shared_examples 'decrypting message content' do it 'decrypts message content' 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 '' end end shared_examples 'decrypting and verifying signature' do it 'decrypts and verifies signature' 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('Good signature') expect(mail['x-zammad-article-preferences'][:security][:encryption][:success]).to be true expect(mail['x-zammad-article-preferences'][:security][:encryption][:comment]).to eq '' 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 'when signature verification' do context 'when sender public key present' do before do create(:pgp_key, :with_private, fixture: sender_email_address) end let(:security_preferences) do { type: 'PGP', sign: { success: true, }, encryption: { success: false, }, } end let(:mail) do pgp_mail = build_mail mail = Channel::EmailParser.new.parse(pgp_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 'Good signature' 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 'with html mail' do let(:raw_content) { '
> Welcome!
>
> Thank you for installing Zammad. äöüß
>
' } let(:raw_body) do <<~MSG_HTML.chomp #{raw_content} MSG_HTML end let(:content_type) { 'text/html' } it 'verifies' do expect(mail[:body]).to eq(raw_content) expect(mail['x-zammad-article-preferences'][:security][:sign][:success]).to be true expect(mail['x-zammad-article-preferences'][:security][:sign][:comment]).to eq 'Good signature' 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 'when key is expired' do let(:mail) do # Import a mail which was created with a now expired key. pgp_mail = Rails.root.join('spec/fixtures/files/pgp/mail/mail-expired.box').read mail = Channel::EmailParser.new.parse(pgp_mail) SecureMailing.incoming(mail) mail end let(:sender_email_address) { expired_email_address } it 'not verified' 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 'Good signature' 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 end context 'with decryption' do let(:security_preferences) do { type: 'PGP', sign: { success: false, }, encryption: { success: true, }, } end let!(:sender_key) { create(:pgp_key, :with_private, fixture: sender_email_address) } let!(:recipient_key) { create(:pgp_key, :with_private, fixture: recipient_email_address) } let!(:cc_recipient_key) { create(:pgp_key, :with_private, fixture: cc_recipient_email_address) } context 'when private key present' do let(:mail) do pgp_mail = build_mail parsed_mail = Channel::EmailParser.new.parse(pgp_mail.to_s) SecureMailing.incoming(parsed_mail) parsed_mail end it_behaves_like 'decrypting message content' it_behaves_like 'HttpLog writer', 'success' context 'with existing second key for same uid' do let(:mail) do # Import a mail which was created with a now expired key. pgp_mail = Rails.root.join('spec/fixtures/files/pgp/mail/mail-other-key.box').read mail = Channel::EmailParser.new.parse(pgp_mail) SecureMailing.incoming(mail) mail end before do create(:pgp_key, :with_private, fixture: "#{recipient_email_address}-other") end it_behaves_like 'decrypting message content' end context 'with OCB key' do let(:recipient_email_address) { 'ocbpgp1@example.com' } let(:mail) do # Import a mail which was created with an OCB key. pgp_mail = Rails.root.join('spec/fixtures/files/pgp/mail/mail-ocb.box').read mail = Channel::EmailParser.new.parse(pgp_mail) SecureMailing.incoming(mail) mail end context 'with GPG version >= 2.2.27', if: SecureMailing::PGP::Tool.version >= '2.2.27' do it_behaves_like 'decrypting message content' end context 'with GPG version < 2.2.27', if: ENV['CI'] && SecureMailing::PGP::Tool.version < '2.2.27' do it 'provides an error message as an article comment' do 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 'There was an unknown PGP error. This PGP email was encrypted with a potentially unknown encryption algorithm.' end end end end context 'with no private key present' do let(:mail) do pgp_mail = build_mail mail = Channel::EmailParser.new.parse(pgp_mail.to_s) sender_key.destroy! recipient_key.destroy! cc_recipient_key.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 PGP key could not be found.') end it_behaves_like 'HttpLog writer', 'failed' end end context 'with signature verification and decryption' do let(:security_preferences) do { type: 'PGP', sign: { success: true, }, encryption: { success: true, }, } end context 'when the mail is signed and encrypted separately' do before do create(:pgp_key, :with_private, fixture: sender_email_address) create(:pgp_key, :with_private, fixture: recipient_email_address) create(:pgp_key, fixture: cc_recipient_email_address) end let(:mail) do pgp_mail = build_mail mail = Channel::EmailParser.new.parse(pgp_mail.to_s) SecureMailing.incoming(mail) mail end it_behaves_like 'decrypting and verifying signature' end context 'when the mail is signed and encrypted (detached signature)' do let(:sender_key) { create(:pgp_key, :with_private, fixture: sender_email_address) } let(:recipient_key) { create(:pgp_key, :with_private, fixture: recipient_email_address) } let(:cc_recipient_key) { create(:pgp_key, :with_private, fixture: cc_recipient_email_address) } let(:mail) do # Import a mail that was signed + encrypted with a detached signature. pgp_mail = Rails.root.join('spec/fixtures/files/pgp/mail/mail-detached.box').read mail = Channel::EmailParser.new.parse(pgp_mail.to_s) SecureMailing.incoming(mail) mail end context 'when all keys are present' do before do sender_key recipient_key cc_recipient_key end it_behaves_like 'decrypting and verifying signature' end context 'when only cc recipient key is present for decryption' do before do sender_key cc_recipient_key end it_behaves_like 'decrypting and verifying signature' end context 'when only decryption key is present' do before do recipient_key cc_recipient_key end it 'decrypts, but verifies signature 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 public PGP key could not be found.') expect(mail['x-zammad-article-preferences'][:security][:encryption][:success]).to be true expect(mail['x-zammad-article-preferences'][:security][:encryption][:comment]).to eq '' end end context 'when all keys are present, but addresses are in upcase' do let(:mail) do # Import a mail and change the case of the sender address. pgp_mail = Rails.root.join('spec/fixtures/files/pgp/mail/mail-detached.box').read pgp_mail = pgp_mail.sub!('pgp1@example.com', 'PGP1@EXAMPLE.COM') pgp_mail = pgp_mail.sub!('pgp2@example.com', 'PGP2@EXAMPLE.COM') pgp_mail = pgp_mail.sub!('pgp3@example.com', 'PGP3@EXAMPLE.COM') mail = Channel::EmailParser.new.parse(pgp_mail.to_s) SecureMailing.incoming(mail) mail end before do sender_key recipient_key cc_recipient_key end it_behaves_like 'decrypting and verifying signature' end end context 'when the mail is signed and encrypted (attached signature)' do let(:sender_key) { create(:pgp_key, :with_private, fixture: sender_email_address) } let(:recipient_key) { create(:pgp_key, :with_private, fixture: recipient_email_address) } let(:cc_recipient_key) { create(:pgp_key, :with_private, fixture: cc_recipient_email_address) } let(:mail) do # Import a mail that was signed + encrypted with an attached signature. pgp_mail = Rails.root.join('spec/fixtures/files/pgp/mail/mail-attached.box').read mail = Channel::EmailParser.new.parse(pgp_mail.to_s) SecureMailing.incoming(mail) mail end context 'when all keys are present' do before do sender_key recipient_key cc_recipient_key end it_behaves_like 'decrypting and verifying signature' end context 'when only cc recipient key is present for decryption' do before do sender_key cc_recipient_key end it_behaves_like 'decrypting and verifying signature' end context 'when only decryption key is present' do before do recipient_key cc_recipient_key end it 'decrypts, but verifies signature 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 public PGP key could not be found.') expect(mail['x-zammad-article-preferences'][:security][:encryption][:success]).to be true expect(mail['x-zammad-article-preferences'][:security][:encryption][:comment]).to eq '' end end end context 'when the mail is signed and encrypted in the same go (combined)' do let(:sender_key) { create(:pgp_key, :with_private, fixture: sender_email_address) } let(:recipient_key) { create(:pgp_key, :with_private, fixture: recipient_email_address) } let(:cc_recipient_key) { create(:pgp_key, :with_private, fixture: cc_recipient_email_address) } let(:mail) do # Import a mail that was signed + encrypted with the same command. pgp_mail = Rails.root.join('spec/fixtures/files/pgp/mail/mail-combined.box').read mail = Channel::EmailParser.new.parse(pgp_mail.to_s) SecureMailing.incoming(mail) mail end context 'when all keys are present' do before do sender_key recipient_key cc_recipient_key end it_behaves_like 'decrypting and verifying signature' end context 'when only cc recipient key is present for decryption' do before do sender_key cc_recipient_key end it_behaves_like 'decrypting and verifying signature' end context 'when only decryption key is present' do before do recipient_key cc_recipient_key end it 'decrypts, but verifies signature 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 public PGP key could not be found.') expect(mail['x-zammad-article-preferences'][:security][:encryption][:success]).to be true expect(mail['x-zammad-article-preferences'][:security][:encryption][:comment]).to eq '' end end end context 'when domain alias support is used' do before do Setting.set('pgp_recipient_alias_configuration', true) create(:pgp_key, :with_private, fixture: 'pgp1@example.com', domain_alias: 'domain1.com') create(:pgp_key, :with_private, fixture: 'pgp2@example.com', domain_alias: 'domain2.com') create(:pgp_key, fixture: 'pgp3@example.com', domain_alias: 'domain3.com') end let(:system_email_address) { 'pgp1@domain1.com' } let(:customer_email_address) { 'pgp2@domain2.com' } let(:cc_customer_email_address) { 'pgp3@domain3.com' } let(:mail) do pgp_mail = build_mail mail = Channel::EmailParser.new.parse(pgp_mail.to_s) SecureMailing.incoming(mail) mail end it_behaves_like 'decrypting and verifying signature' end end end describe '.required_version?' do context 'with GnuPG being present on the system' do it 'succeeds' do expect(described_class.required_version?).to be true end end context 'without GnuPG being present on the system' do it 'fails' do allow(SecureMailing::PGP::Tool).to receive(:version).and_raise(Errno::ENOENT) expect(described_class.required_version?).to be false end end context 'with and outdated version of GnuPG' do it 'fails' do allow(SecureMailing::PGP::Tool).to receive(:version).and_return('1.8.0') expect(described_class.required_version?).to be false 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) { nil } let(:security_preferences) do { type: 'PGP', sign: { success: false, }, encryption: { success: true, }, } end let(:mail) do sender_pgp_key = create(:pgp_key, :with_private, fixture: sender_email_address) recipient_pgp_key = create(:pgp_key, :with_private, fixture: system_email_address) pgp_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(pgp_mail.to_s) sender_pgp_key.destroy recipient_pgp_key.destroy mail end let!(:article) do _ticket, article, _user, _mail = Channel::EmailParser.new.process({}, mail['raw']) article end context 'when private key added' do before do create(:pgp_key, :with_private, fixture: recipient_email_address) end it 'succeeds' do SecureMailing.retry(article) expect(article.preferences[:security][:sign][:success]).to be false expect(article.preferences[:security][:sign][:comment]).to be_nil expect(article.preferences[:security][:encryption][:success]).to be true expect(article.preferences[:security][:encryption][:comment]).to eq '' 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 'when PGP activated' do before do Setting.set('pgp_integration', false) end it 'succeeds' do Setting.set('pgp_integration', true) SecureMailing.retry(article) expect(article.preferences[:security][:sign][:success]).to be false expect(article.preferences[:security][:sign][:comment]).to be_nil expect(article.preferences[:security][:encryption][:success]).to be true expect(article.preferences[:security][:encryption][:comment]).to eq '' 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