From 7c76c54633a44e6a4095714a4f917473587789ad Mon Sep 17 00:00:00 2001 From: Florent Morselli Date: Fri, 21 Feb 2025 17:40:20 +0100 Subject: [PATCH] Refactor S/MIME encrypter to use certificate repository Replaces direct certificate path usage with a repository interface for managing S/MIME certificates. This improves flexibility by allowing custom certificate retrieval logic through `SmimeCertificateRepositoryInterface`. Adjusted related tests, configuration, and event listener implementation accordingly. --- .../DependencyInjection/Configuration.php | 4 +- .../FrameworkExtension.php | 6 +- .../Resources/config/mailer.php | 10 +-- .../Resources/config/schema/symfony-1.0.xsd | 2 +- .../DependencyInjection/ConfigurationTest.php | 2 +- .../SmimeCertificateRepositoryInterface.php | 25 +++++++ .../SmimeEncryptedMessageListener.php | 23 ++++++- .../SmimeEncryptedMessageListenerTest.php | 67 +++++++++++++++++-- 8 files changed, 115 insertions(+), 24 deletions(-) create mode 100644 src/Symfony/Component/Mailer/EventListener/SmimeCertificateRepositoryInterface.php diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index ca20f15a49619..7507ae392aea1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -2314,8 +2314,8 @@ private function addMailerSection(ArrayNodeDefinition $rootNode, callable $enabl ->canBeEnabled() ->info('S/MIME encrypter configuration') ->children() - ->scalarNode('certificate') - ->info('Path to certificate (in PEM format without the `file://` prefix)') + ->scalarNode('repository') + ->info('Path to the S/MIME certificate repository. Shall implement the `Symfony\Component\Mailer\EventListener\SmimeCertificateRepositoryInterface`.') ->defaultValue('') ->cannotBeEmpty() ->end() diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 4c36bf4dc8f24..331005d634d15 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -2893,11 +2893,9 @@ private function registerMailerConfiguration(array $config, ContainerBuilder $co if (!class_exists(SmimeEncryptedMessageListener::class)) { throw new LogicException('S/MIME encrypted messages support cannot be enabled as this version of the Mailer component does not support it.'); } - $smimeDecrypter = $container->getDefinition('mailer.smime_encrypter'); - $smimeDecrypter->setArgument(0, $config['smime_encrypter']['certificate']); - $smimeDecrypter->setArgument(1, $config['smime_encrypter']['cipher']); + $container->setAlias('mailer.smime_encrypter.repository', $config['smime_encrypter']['repository']); + $container->setParameter('mailer.smime_encrypter.cipher', $config['smime_encrypter']['cipher']); } else { - $container->removeDefinition('mailer.smime_encrypter'); $container->removeDefinition('mailer.smime_encrypter.listener'); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.php index 25b3fefdbfb00..71a43b9c81c3c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.php @@ -26,7 +26,6 @@ use Symfony\Component\Mailer\Transport\TransportInterface; use Symfony\Component\Mailer\Transport\Transports; use Symfony\Component\Mime\Crypto\DkimSigner; -use Symfony\Component\Mime\Crypto\SMimeEncrypter; use Symfony\Component\Mime\Crypto\SMimeSigner; return static function (ContainerConfigurator $container) { @@ -102,12 +101,6 @@ abstract_arg('signOptions'), ]) - ->set('mailer.smime_encrypter', SMimeEncrypter::class) - ->args([ - abstract_arg('certificate'), - abstract_arg('cipher'), - ]) - ->set('mailer.dkim_signer.listener', DkimSignedMessageListener::class) ->args([ service(DkimSigner::class), @@ -122,7 +115,8 @@ ->set('mailer.smime_encrypter.listener', SmimeEncryptedMessageListener::class) ->args([ - service('mailer.smime_encrypter'), + service('mailer.smime_encrypter.repository'), + param('mailer.smime_encrypter.cipher'), ]) ->tag('kernel.event_subscriber') diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd index b47de331a4775..d961ca97cfd41 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd @@ -836,7 +836,7 @@ - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index 607049274c7da..c4b273dc83823 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -941,7 +941,7 @@ class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphor ], 'smime_encrypter' => [ 'enabled' => false, - 'certificate' => '', + 'repository' => '', 'cipher' => null, ], ], diff --git a/src/Symfony/Component/Mailer/EventListener/SmimeCertificateRepositoryInterface.php b/src/Symfony/Component/Mailer/EventListener/SmimeCertificateRepositoryInterface.php new file mode 100644 index 0000000000000..b5d9081a283d6 --- /dev/null +++ b/src/Symfony/Component/Mailer/EventListener/SmimeCertificateRepositoryInterface.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\EventListener; + +/** + * Encrypts messages using S/MIME. + * + * @author Florent Morselli + */ +interface SmimeCertificateRepositoryInterface +{ + /** + * @return ?string The path to the certificate. null if not found + */ + public function findCertificatePathFor(string $email): ?string; +} diff --git a/src/Symfony/Component/Mailer/EventListener/SmimeEncryptedMessageListener.php b/src/Symfony/Component/Mailer/EventListener/SmimeEncryptedMessageListener.php index 3b01c1d1c999b..24bc574e83fbe 100644 --- a/src/Symfony/Component/Mailer/EventListener/SmimeEncryptedMessageListener.php +++ b/src/Symfony/Component/Mailer/EventListener/SmimeEncryptedMessageListener.php @@ -21,10 +21,11 @@ * * @author Elías Fernández */ -class SmimeEncryptedMessageListener implements EventSubscriberInterface +final class SmimeEncryptedMessageListener implements EventSubscriberInterface { public function __construct( - private SMimeEncrypter $encrypter, + private readonly SmimeCertificateRepositoryInterface $smimeRepository, + private readonly ?int $cipher = null, ) { } @@ -34,8 +35,24 @@ public function onMessage(MessageEvent $event): void if (!$message instanceof Message) { return; } + if (!$message->getHeaders()->has('X-SMime-Encrypt')) { + return; + } + $message->getHeaders()->remove('X-SMime-Encrypt'); + $certificatePaths = []; + foreach ($event->getEnvelope()->getRecipients() as $recipient) { + $certificatePath = $this->smimeRepository->findCertificatePathFor($recipient->getAddress()); + if (null === $certificatePath) { + return; + } + $certificatePaths[] = $certificatePath; + } + if (0 === \count($certificatePaths)) { + return; + } + $encrypter = new SMimeEncrypter($certificatePaths, $this->cipher); - $event->setMessage($this->encrypter->encrypt($message)); + $event->setMessage($encrypter->encrypt($message)); } public static function getSubscribedEvents(): array diff --git a/src/Symfony/Component/Mailer/Tests/EventListener/SmimeEncryptedMessageListenerTest.php b/src/Symfony/Component/Mailer/Tests/EventListener/SmimeEncryptedMessageListenerTest.php index 365b55a47dfec..a4c4af73625dd 100644 --- a/src/Symfony/Component/Mailer/Tests/EventListener/SmimeEncryptedMessageListenerTest.php +++ b/src/Symfony/Component/Mailer/Tests/EventListener/SmimeEncryptedMessageListenerTest.php @@ -14,11 +14,12 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Mailer\Envelope; use Symfony\Component\Mailer\Event\MessageEvent; +use Symfony\Component\Mailer\EventListener\SmimeCertificateRepositoryInterface; use Symfony\Component\Mailer\EventListener\SmimeEncryptedMessageListener; use Symfony\Component\Mime\Address; -use Symfony\Component\Mime\Crypto\SMimeEncrypter; use Symfony\Component\Mime\Header\Headers; use Symfony\Component\Mime\Header\MailboxListHeader; +use Symfony\Component\Mime\Header\UnstructuredHeader; use Symfony\Component\Mime\Message; use Symfony\Component\Mime\Part\SMimePart; use Symfony\Component\Mime\Part\TextPart; @@ -28,13 +29,15 @@ class SmimeEncryptedMessageListenerTest extends TestCase /** * @requires extension openssl */ - public function testSmimeMessageSigningProcess() + public function testSmimeMessageEncryptionProcess() { - $encrypter = new SMimeEncrypter(\dirname(__DIR__).'/Fixtures/sign.crt'); - $listener = new SmimeEncryptedMessageListener($encrypter); + $repository = $this->createMock(SmimeCertificateRepositoryInterface::class); + $repository->method('findCertificatePathFor')->willReturn(\dirname(__DIR__).'/Fixtures/sign.crt'); + $listener = new SmimeEncryptedMessageListener($repository); $message = new Message( new Headers( - new MailboxListHeader('From', [new Address('sender@example.com')]) + new MailboxListHeader('From', [new Address('sender@example.com')]), + new UnstructuredHeader('X-SMime-Encrypt', 'true'), ), new TextPart('hello') ); @@ -45,5 +48,59 @@ public function testSmimeMessageSigningProcess() $this->assertNotSame($message, $event->getMessage()); $this->assertInstanceOf(TextPart::class, $message->getBody()); $this->assertInstanceOf(SMimePart::class, $event->getMessage()->getBody()); + $this->assertFalse($event->getMessage()->getHeaders()->has('X-SMime-Encrypt')); + } + + /** + * @requires extension openssl + */ + public function testMessageNotEncryptedWhenOneRecipientCertificateIsMissing() + { + $repository = $this->createMock(SmimeCertificateRepositoryInterface::class); + $repository->method('findCertificatePathFor')->willReturnOnConsecutiveCalls(\dirname(__DIR__).'/Fixtures/sign.crt', null); + $listener = new SmimeEncryptedMessageListener($repository); + $message = new Message( + new Headers( + new MailboxListHeader('From', [new Address('sender@example.com')]), + new UnstructuredHeader('X-SMime-Encrypt', 'true'), + ), + new TextPart('hello') + ); + $envelope = new Envelope(new Address('sender@example.com'), [ + new Address('r1@example.com'), + new Address('r2@example.com'), + ]); + $event = new MessageEvent($message, $envelope, 'default'); + + $listener->onMessage($event); + $this->assertSame($message, $event->getMessage()); + $this->assertInstanceOf(TextPart::class, $message->getBody()); + $this->assertInstanceOf(TextPart::class, $event->getMessage()->getBody()); + } + + /** + * @requires extension openssl + */ + public function testMessageNotExplicitlyAskedForNonEncryption() + { + $repository = $this->createMock(SmimeCertificateRepositoryInterface::class); + $repository->method('findCertificatePathFor')->willReturn(\dirname(__DIR__).'/Fixtures/sign.crt'); + $listener = new SmimeEncryptedMessageListener($repository); + $message = new Message( + new Headers( + new MailboxListHeader('From', [new Address('sender@example.com')]), + ), + new TextPart('hello') + ); + $envelope = new Envelope(new Address('sender@example.com'), [ + new Address('r1@example.com'), + new Address('r2@example.com'), + ]); + $event = new MessageEvent($message, $envelope, 'default'); + + $listener->onMessage($event); + $this->assertSame($message, $event->getMessage()); + $this->assertInstanceOf(TextPart::class, $message->getBody()); + $this->assertInstanceOf(TextPart::class, $event->getMessage()->getBody()); } }