[pmg-devel] [PATCH pmg-api v3 04/11] add PMG::DKIMSign module
Stoiko Ivanov
s.ivanov at proxmox.com
Fri Oct 18 16:53:51 CEST 2019
the module serves 3 purposes:
* it extends Mail::DKIM::Signer:
* it provides a glue layer between MIME::Entity's output method (which
expects print and uses \n as line terminator) and Mail::DKIM::Signer's
PRINT method (which expects \r\n)
* it integrates with PMG's config
* the domain which should be used for signing is selected based on the
sender's e-mail address and the DKIM-settings in PMG-configuration
* it provides a method which takes a MIME::Entity and returns it with
signature
* certain headers get oversigned (in order to prevent adding a previously
non-existing header (e.g. Reply-To) and retaining a valid signature).
the list of headers which are oversigned is inspired by rspamd's choice [0].
for rationale see [1,2]
* it provides methods for handling selectors and keys.
[0] https://rspamd.com/doc/modules/dkim_signing.html#sign-headers
[1] https://noxxi.de/research/breaking-dkim-on-purpose-and-by-chance.html
[2] https://github.com/rspamd/rspamd/issues/2136
Signed-off-by: Stoiko Ivanov <s.ivanov at proxmox.com>
---
debian/dirs | 1 +
src/Makefile | 1 +
src/PMG/DKIMSign.pm | 177 ++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 179 insertions(+)
create mode 100644 src/PMG/DKIMSign.pm
diff --git a/debian/dirs b/debian/dirs
index 558ce27..f7ac2e7 100644
--- a/debian/dirs
+++ b/debian/dirs
@@ -1,3 +1,4 @@
/etc/pmg
+/etc/pmg/dkim
/var/lib/pmg
/var/lib/pmg/backup
diff --git a/src/Makefile b/src/Makefile
index 95e63c9..44f4a4b 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -74,6 +74,7 @@ LIBSOURCES = \
PMG/LDAPSet.pm \
PMG/LDAPCache.pm \
PMG/DBTools.pm \
+ PMG/DKIMSign.pm \
PMG/Quarantine.pm \
PMG/Report.pm \
PMG/RuleDB/Group.pm \
diff --git a/src/PMG/DKIMSign.pm b/src/PMG/DKIMSign.pm
new file mode 100644
index 0000000..1107959
--- /dev/null
+++ b/src/PMG/DKIMSign.pm
@@ -0,0 +1,177 @@
+package PMG::DKIMSign;
+
+use strict;
+use warnings;
+use Mail::DKIM::Signer;
+use Mail::DKIM::TextWrap;
+use Crypt::OpenSSL::RSA;
+
+use PVE::Tools;
+use PVE::INotify;
+use PVE::SafeSyslog;
+
+use PMG::Utils;
+use PMG::Config;
+use base qw(Mail::DKIM::Signer);
+
+sub new {
+ my ($class, $selector, $sign_all) = @_;
+
+ die "no selector provided\n" if ! $selector;
+
+ my %opts = (
+ Algorithm => 'rsa-sha256',
+ Method => 'relaxed/relaxed',
+ Selector => $selector,
+ KeyFile => "/etc/pmg/dkim/$selector.private",
+ );
+
+ my $self = $class->SUPER::new(%opts);
+
+ $self->{sign_all} = $sign_all;
+
+ return $self;
+}
+
+# MIME::Entity can output to all objects responding to 'print' (and does so in
+# chunks) Mail::DKIM::Signer has a 'PRINT' method and expects each line
+# terminated with "\r\n"
+
+
+sub print {
+ my ($self, $chunk) = @_;
+ $chunk =~ s/\012/\015\012/g;
+ $self->PRINT($chunk);
+}
+
+sub create_signature {
+ my ($self) = @_;
+
+ $self->CLOSE();
+ return $self->signature->as_string();
+}
+
+#determines which domain should be used for signing based on the e-mailaddress
+sub signing_domain {
+ my ($self, $sender_email) = @_;
+
+ my @parts = split('@', $sender_email);
+ die "no domain in sender e-mail\n" if scalar(@parts) < 2;
+ my $input_domain = $parts[-1];
+
+ if ($self->{sign_all}) {
+ $self->domain($input_domain) if $self->{sign_all};
+ return;
+ }
+
+ # check that input_domain is in/a subdomain of in the
+ # dkimdomains, falling back to the relay domains.
+ my $dkimdomains = PVE::INotify::read_file('dkimdomains');
+ $dkimdomains = PVE::INotify::read_file('domains') if !scalar(%$dkimdomains);
+
+ foreach my $domain (sort keys %$dkimdomains) {
+ if ( $input_domain =~ /\Q$domain\E$/i ) {
+ $self->domain($domain);
+ return;
+ }
+ }
+
+ syslog('info', "not DKIM signing mail from $sender_email");
+
+ return;
+}
+
+
+sub sign_entity {
+ my ($entity, $selector, $sender, $sign_all) = @_;
+
+ die "no selector provided\n" if ! $selector;
+
+ #oversign certain headers
+ my @oversign_headers = (
+ 'from',
+ 'to',
+ 'cc',
+ 'reply-to',
+ 'subject',
+ );
+
+ my @cond_headers = (
+ 'content-type',
+ );
+
+ push(@oversign_headers, grep { $entity->head->mime_attr($_) } @cond_headers);
+
+ my $extended_headers = { map { $_ => '+' } @oversign_headers };
+
+ my $signer = __PACKAGE__->new($selector, $sign_all);
+
+ $signer->extended_headers($extended_headers);
+ $signer->signing_domain($sender);
+
+ $entity->print($signer);
+ my $signature = $signer->create_signature();
+ $entity->head->add('DKIM-Signature', $signature, 0);
+
+ return $entity;
+
+}
+
+# key-handling and utility methods
+sub get_selector_info {
+ my ($selector) = @_;
+
+ die "no selector provided\n" if !defined($selector);
+ my ($pubkey, $size);
+ eval {
+ my $privkeytext = PVE::Tools::file_get_contents("/etc/pmg/dkim/$selector.private");
+ my $privkey = Crypt::OpenSSL::RSA->new_private_key($privkeytext);
+ $size = $privkey->size() * 8;
+
+ $pubkey = $privkey->get_public_key_x509_string();
+ };
+ die "$@\n" if $@;
+
+ $pubkey =~ s/-----(?:BEGIN|END) PUBLIC KEY-----//g;
+ $pubkey =~ s/\v//mg;
+
+ # split record into 250 byte chunks for DNS-server compatibility
+ # see opendkim-genkey
+ my $record = qq{$selector._domainkey\tIN\tTXT\t( "v=DKIM1; h=sha256; k=rsa; "\n\t "p=};
+ my $len = length($pubkey);
+ my $cur = 0;
+ while ($len > 0) {
+ if ($len < 250) {
+ $record .= substr($pubkey, $cur);
+ $len = 0;
+ } else {
+ $record .= substr($pubkey, $cur, 250) . qq{"\n\t "};
+ $cur += 250;
+ $len -= 250;
+ }
+ }
+ $record .= qq{" ) ; ----- DKIM key $selector};
+
+ return ($record, $size);
+}
+
+sub set_selector {
+ my ($selector, $keysize) = @_;
+
+ die "no selector provided\n" if !defined($selector);
+ die "no keysize provided\n" if !defined($keysize);
+ die "invalid keysize\n" if ($keysize < 1024);
+ my $privkey_file = "/etc/pmg/dkim/$selector.private";
+
+ my $code = sub{
+ my $cmd = ['openssl', 'genrsa', '-out', $privkey_file, $keysize];
+ PMG::Utils::run_silent_cmd($cmd);
+ my $cfg = PMG::Config->new();
+ $cfg->set('admin', 'dkim_selector', $selector);
+ $cfg->write();
+ PMG::Utils::reload_smtp_filter();
+ };
+
+ PMG::Config::lock_config($code, "unable to generate DKIM key ($selector - $keysize bits)");
+}
+1;
--
2.20.1
More information about the pmg-devel
mailing list