5 use Email
::Address
::XS
;
6 use Mail
::DKIM
::Signer
;
7 use Mail
::DKIM
::TextWrap
;
8 use Crypt
::OpenSSL
::RSA
;
16 use base
qw(Mail::DKIM::Signer);
19 my ($class, $selector, $sign_all) = @_;
21 die "no selector provided\n" if ! $selector;
24 Algorithm
=> 'rsa-sha256',
25 Method
=> 'relaxed/relaxed',
26 Selector
=> $selector,
27 KeyFile
=> "/etc/pmg/dkim/$selector.private",
30 my $self = $class->SUPER::new
(%opts);
32 $self->{sign_all
} = $sign_all;
37 # MIME::Entity can output to all objects responding to 'print' (and does so in
38 # chunks) Mail::DKIM::Signer has a 'PRINT' method and expects each line
39 # terminated with "\r\n"
43 my ($self, $chunk) = @_;
44 $chunk =~ s/\012/\015\012/g;
48 sub create_signature
{
52 return $self->signature->as_string();
55 #determines which domain should be used for signing based on the e-mailaddress
57 my ($self, $sender_email, $entity, $use_domain) = @_;
60 if ($use_domain eq 'header') {
61 $input_domain = parse_headers_for_signing
($entity);
63 my @parts = split('@', $sender_email);
64 die "no domain in sender e-mail\n" if scalar(@parts) < 2;
65 $input_domain = $parts[-1];
68 if ($self->{sign_all
}) {
69 $self->domain($input_domain) if $self->{sign_all
};
73 # check that input_domain is in/a subdomain of in the
74 # dkimdomains, falling back to the relay domains.
75 my $dkimdomains = PVE
::INotify
::read_file
('dkimdomains');
76 $dkimdomains = PVE
::INotify
::read_file
('domains') if !scalar(%$dkimdomains);
78 # Sort domains by length first, so if we have both a sub domain and its parent
79 # the correct one will be returned
80 foreach my $domain (sort { length($b) <=> length($a) || $a cmp $b} keys %$dkimdomains) {
81 if ( $input_domain =~ /\Q$domain\E$/i ) {
82 $self->domain($domain);
87 syslog
('info', "not DKIM signing mail from $sender_email");
93 sub parse_headers_for_signing
{
94 # Following RFC 7489 [1], we only sign emails with exactly one sender in the
97 # [1] https://datatracker.ietf.org/doc/html/rfc7489#section-6.6.1
103 my @from_headers = $entity->head->get('from');
104 foreach my $from_header (@from_headers) {
105 my @addresses = Email
::Address
::XS
::parse_email_addresses
($from_header);
106 $from_count += scalar(@addresses);
107 $domain = $addresses[0]->host() if scalar(@addresses) > 0;
110 die "there is more than one sender in the header\n" if $from_count > 1;
111 die "there is no sender in the header\n" if $from_count == 0;
117 my ($entity, $dkim, $sender) = @_;
119 my $sign_all = $dkim->{sign_all
};
120 my $use_domain = $dkim->{use_domain
};
121 my $selector = $dkim->{selector
};
123 die "no selector provided\n" if ! $selector;
125 #oversign certain headers
126 my @oversign_headers = (
138 push(@oversign_headers, grep { $entity->head->mime_attr($_) } @cond_headers);
140 my $extended_headers = { map { $_ => '+' } @oversign_headers };
142 my $signer = __PACKAGE__-
>new($selector, $sign_all);
144 $signer->extended_headers($extended_headers);
146 if ($signer->signing_domain($sender, $entity, $use_domain)) {
147 $entity->print($signer);
148 my $signature = $signer->create_signature();
149 $entity->head->add('DKIM-Signature', $signature, 0);
156 # key-handling and utility methods
157 sub get_selector_info
{
160 die "no selector provided\n" if !defined($selector);
163 my $privkeytext = PVE
::Tools
::file_get_contents
("/etc/pmg/dkim/$selector.private");
164 my $privkey = Crypt
::OpenSSL
::RSA-
>new_private_key($privkeytext);
165 $size = $privkey->size() * 8;
167 $pubkey = $privkey->get_public_key_x509_string();
171 $pubkey =~ s/-----(?:BEGIN|END) PUBLIC KEY-----//g;
174 # split record into 250 byte chunks for DNS-server compatibility
175 # see opendkim-genkey
176 my $record = qq{$selector._domainkey\tIN\tTXT\t( "v=DKIM1; h=sha256; k=rsa; "\n\t "p=};
177 my $len = length($pubkey);
181 $record .= substr($pubkey, $cur);
184 $record .= substr($pubkey, $cur, 250) . qq{"\n\t "};
189 $record .= qq{" ) ; ----- DKIM key $selector};
191 return ($record, $size);
195 my ($selector, $keysize, $force) = @_;
197 die "no selector provided\n" if !defined($selector);
198 die "no keysize provided\n" if !defined($keysize);
199 die "invalid keysize\n" if ($keysize < 1024);
200 my $privkey_file = "/etc/pmg/dkim/$selector.private";
203 my $genkey = $force || (! -e
$privkey_file);
205 my ($privkey, $cursize);
207 my $privkeytext = PVE
::Tools
::file_get_contents
($privkey_file);
208 $privkey = Crypt
::OpenSSL
::RSA-
>new_private_key($privkeytext);
209 $cursize = $privkey->size() * 8;
211 die "error checking $privkey_file: $@\n" if $@;
212 die "$privkey_file already exists, but has different size ($cursize bits)\n"
213 if $cursize != $keysize;
215 my $cmd = ['openssl', 'genrsa', '-out', $privkey_file, $keysize];
216 PMG
::Utils
::run_silent_cmd
($cmd);
218 my $cfg = PMG
::Config-
>new();
219 $cfg->set('admin', 'dkim_selector', $selector);
221 PMG
::Utils
::reload_smtp_filter
();
224 PMG
::Config
::lock_config
($code, "unable to set DKIM key ($selector - $keysize bits)");