]> git.proxmox.com Git - pmg-api.git/blame - src/PMG/DKIMSign.pm
pmg7to8: sync over changes from stable-7 branch
[pmg-api.git] / src / PMG / DKIMSign.pm
CommitLineData
ad6e35cf
SI
1package PMG::DKIMSign;
2
3use strict;
4use warnings;
5use Mail::DKIM::Signer;
6use Mail::DKIM::TextWrap;
7use Crypt::OpenSSL::RSA;
8
9use PVE::Tools;
10use PVE::INotify;
11use PVE::SafeSyslog;
12
13use PMG::Utils;
14use PMG::Config;
15use base qw(Mail::DKIM::Signer);
16
17sub new {
18 my ($class, $selector, $sign_all) = @_;
19
20 die "no selector provided\n" if ! $selector;
21
22 my %opts = (
23 Algorithm => 'rsa-sha256',
24 Method => 'relaxed/relaxed',
25 Selector => $selector,
26 KeyFile => "/etc/pmg/dkim/$selector.private",
27 );
28
29 my $self = $class->SUPER::new(%opts);
30
31 $self->{sign_all} = $sign_all;
32
33 return $self;
34}
35
36# MIME::Entity can output to all objects responding to 'print' (and does so in
37# chunks) Mail::DKIM::Signer has a 'PRINT' method and expects each line
38# terminated with "\r\n"
39
40
41sub print {
42 my ($self, $chunk) = @_;
43 $chunk =~ s/\012/\015\012/g;
44 $self->PRINT($chunk);
45}
46
47sub create_signature {
48 my ($self) = @_;
49
50 $self->CLOSE();
51 return $self->signature->as_string();
52}
53
54#determines which domain should be used for signing based on the e-mailaddress
55sub signing_domain {
56 my ($self, $sender_email) = @_;
57
58 my @parts = split('@', $sender_email);
59 die "no domain in sender e-mail\n" if scalar(@parts) < 2;
60 my $input_domain = $parts[-1];
61
62 if ($self->{sign_all}) {
63 $self->domain($input_domain) if $self->{sign_all};
b8e7dac7 64 return 1;
ad6e35cf
SI
65 }
66
67 # check that input_domain is in/a subdomain of in the
68 # dkimdomains, falling back to the relay domains.
69 my $dkimdomains = PVE::INotify::read_file('dkimdomains');
70 $dkimdomains = PVE::INotify::read_file('domains') if !scalar(%$dkimdomains);
71
3caa5927
DB
72 # Sort domains by length first, so if we have both a sub domain and its parent
73 # the correct one will be returned
74 foreach my $domain (sort { length($b) <=> length($a) || $a cmp $b} keys %$dkimdomains) {
ad6e35cf
SI
75 if ( $input_domain =~ /\Q$domain\E$/i ) {
76 $self->domain($domain);
b8e7dac7 77 return 1;
ad6e35cf
SI
78 }
79 }
80
81 syslog('info', "not DKIM signing mail from $sender_email");
82
b8e7dac7 83 return 0;
ad6e35cf
SI
84}
85
86
87sub sign_entity {
88 my ($entity, $selector, $sender, $sign_all) = @_;
89
90 die "no selector provided\n" if ! $selector;
91
92 #oversign certain headers
93 my @oversign_headers = (
94 'from',
95 'to',
96 'cc',
97 'reply-to',
98 'subject',
99 );
100
101 my @cond_headers = (
102 'content-type',
103 );
104
105 push(@oversign_headers, grep { $entity->head->mime_attr($_) } @cond_headers);
106
107 my $extended_headers = { map { $_ => '+' } @oversign_headers };
108
109 my $signer = __PACKAGE__->new($selector, $sign_all);
110
111 $signer->extended_headers($extended_headers);
ad6e35cf 112
b8e7dac7
SI
113 if ($signer->signing_domain($sender)) {
114 $entity->print($signer);
115 my $signature = $signer->create_signature();
116 $entity->head->add('DKIM-Signature', $signature, 0);
117 }
ad6e35cf
SI
118
119 return $entity;
120
121}
122
123# key-handling and utility methods
124sub get_selector_info {
125 my ($selector) = @_;
126
127 die "no selector provided\n" if !defined($selector);
128 my ($pubkey, $size);
129 eval {
130 my $privkeytext = PVE::Tools::file_get_contents("/etc/pmg/dkim/$selector.private");
131 my $privkey = Crypt::OpenSSL::RSA->new_private_key($privkeytext);
132 $size = $privkey->size() * 8;
133
134 $pubkey = $privkey->get_public_key_x509_string();
135 };
136 die "$@\n" if $@;
137
138 $pubkey =~ s/-----(?:BEGIN|END) PUBLIC KEY-----//g;
139 $pubkey =~ s/\v//mg;
140
141 # split record into 250 byte chunks for DNS-server compatibility
142 # see opendkim-genkey
143 my $record = qq{$selector._domainkey\tIN\tTXT\t( "v=DKIM1; h=sha256; k=rsa; "\n\t "p=};
144 my $len = length($pubkey);
145 my $cur = 0;
146 while ($len > 0) {
147 if ($len < 250) {
148 $record .= substr($pubkey, $cur);
149 $len = 0;
150 } else {
151 $record .= substr($pubkey, $cur, 250) . qq{"\n\t "};
152 $cur += 250;
153 $len -= 250;
154 }
155 }
156 $record .= qq{" ) ; ----- DKIM key $selector};
157
158 return ($record, $size);
159}
160
161sub set_selector {
d95c075a 162 my ($selector, $keysize, $force) = @_;
ad6e35cf
SI
163
164 die "no selector provided\n" if !defined($selector);
165 die "no keysize provided\n" if !defined($keysize);
166 die "invalid keysize\n" if ($keysize < 1024);
167 my $privkey_file = "/etc/pmg/dkim/$selector.private";
168
d95c075a
SI
169 my $code = sub {
170 my $genkey = $force || (! -e $privkey_file);
171 if (!$genkey) {
172 my ($privkey, $cursize);
173 eval {
174 my $privkeytext = PVE::Tools::file_get_contents($privkey_file);
175 $privkey = Crypt::OpenSSL::RSA->new_private_key($privkeytext);
176 $cursize = $privkey->size() * 8;
177 };
178 die "error checking $privkey_file: $@\n" if $@;
179 die "$privkey_file already exists, but has different size ($cursize bits)\n"
180 if $cursize != $keysize;
181 } else {
182 my $cmd = ['openssl', 'genrsa', '-out', $privkey_file, $keysize];
183 PMG::Utils::run_silent_cmd($cmd);
184 }
ad6e35cf
SI
185 my $cfg = PMG::Config->new();
186 $cfg->set('admin', 'dkim_selector', $selector);
187 $cfg->write();
188 PMG::Utils::reload_smtp_filter();
189 };
190
d95c075a 191 PMG::Config::lock_config($code, "unable to set DKIM key ($selector - $keysize bits)");
ad6e35cf
SI
192}
1931;