[pmg-devel] [PATCH pmg-api 05/12] add PMG::DKIMSign module
Stoiko Ivanov
s.ivanov at proxmox.com
Mon Oct 7 21:28:49 CEST 2019
the module serves 2 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 infers the domain which should be used for signing based on the
sender's e-mail address (and the DKIM-settings in PMG-configuration)
* it provides helper methods for generating a new private key, outputting a
DKIM TXT record and for finding the domain which should be used for signing
a mail
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 to oversign is inspired by rspamd's choice [0].
for rationale see [1,2,3]
[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>
---
src/Makefile | 1 +
src/PMG/DKIMSign.pm | 150 ++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 151 insertions(+)
create mode 100644 src/PMG/DKIMSign.pm
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..fa61101
--- /dev/null
+++ b/src/PMG/DKIMSign.pm
@@ -0,0 +1,150 @@
+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);
+
+ # oversign certain headers
+ $self->extended_headers({
+ From => '+',
+ To => '+',
+ CC => '+',
+ 'Reply-To' => '+',
+ Subject => '+',
+ });
+
+ $self->{sign_all} = $sign_all;
+
+ return $self;
+}
+
+# MIME::Entity can output to all objects responding to 'read' (and does so in
+# chunks) Mail::DKIM::Signer has a 'READ' method and expects each line
+# terminated with "\r\n"
+
+
+sub print {
+ my ($self, $line) = @_;
+ $line =~ s/\012/\015\012/g;
+ $self->PRINT($line);
+}
+
+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 $input_domain;
+ if ( $sender_email =~ m/@([a-zA-Z0-9\-]+(?:\.[a-zA-Z0-9\-]+)*)$/ ) {
+ $input_domain = $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$/ ) {
+ $self->domain($domain);
+ return;
+ }
+ }
+
+ syslog('info', "not DKIM signing mail from $sender_email");
+
+ return;
+}
+
+# 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 && $keysize!= 2048 && $keysize != 4096);;
+ my $privkey_file = "/etc/pmg/dkim/$selector.private";
+
+ eval {
+ 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();
+ };
+ die "unable to generate dkim key for $selector ($keysize bits): $@\n" if $@;
+}
+1;
--
2.20.1
More information about the pmg-devel
mailing list