[pmg-devel] [PATCH pmg-api v2 04/11] add PMG::DKIMSign module

Stoiko Ivanov s.ivanov at proxmox.com
Tue Oct 15 21:46:43 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 c864ae8..b03a27b 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -73,6 +73,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