Server IP : 85.214.239.14 / Your IP : 3.138.114.113 Web Server : Apache/2.4.62 (Debian) System : Linux h2886529.stratoserver.net 4.9.0 #1 SMP Tue Jan 9 19:45:01 MSK 2024 x86_64 User : www-data ( 33) PHP Version : 7.4.18 Disable Function : pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare, MySQL : OFF | cURL : OFF | WGET : ON | Perl : ON | Python : ON | Sudo : ON | Pkexec : OFF Directory : /usr/share/perl5/Amavis/ |
Upload File : |
# SPDX-License-Identifier: GPL-2.0-or-later package Amavis::Tools; use strict; use re 'taint'; use warnings; use warnings FATAL => qw(utf8 void); no warnings 'uninitialized'; # use warnings 'extra'; no warnings 'experimental::re_strict'; use re 'strict'; BEGIN { require Exporter; use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); $VERSION = '2.412'; @ISA = qw(Exporter); @EXPORT_OK = qw(&show_or_test_dkim_public_keys &generate_dkim_private_key &convert_dkim_keys_file); } use subs @EXPORT_OK; use Errno qw(ENOENT EACCES); use IO::File qw(O_RDONLY O_WRONLY O_RDWR O_APPEND O_CREAT O_EXCL); use Crypt::OpenSSL::RSA (); use Amavis::Conf qw(:platform c cr ca @dkim_signing_keys_list @dkim_signing_keys_storage); use Amavis::rfc2821_2822_Tools qw(rfc2822_timestamp); use Amavis::Util qw(untaint ll do_log safe_encode_utf8_inplace idn_to_ascii idn_to_utf8); # Prints DNS TXT resource records for corresponding DKIM private keys (as # previously declared by calls to dkim_key) in a format directly suitable # for inclusion in DNS zone files. If an argument is provided the result is # restricted to listed domains only, otherwise RR for all domains are shown. # Note that a domain may have more than one RR: one RR for each selector. # # When a search argument is provided (even if '.'), the printed list is # sorted according to reversed domain labels (e.g. com.example.sub.host), # entries with the same domain are kept in original order. When there are # no search arguments, the original order is retained. # sub show_or_test_dkim_public_keys($$) { my($cmd,$args) = @_; # when list is empty all domains are implied my(@seek_domains) = map(idn_to_ascii($_), @$args); my(@sort_list) = map { my $d = lc($dkim_signing_keys_list[$_]->{domain}); my $d_re = $dkim_signing_keys_list[$_]->{domain_re}; [$_, $d, $d_re, join('.',reverse split(/\./,$d,-1))] } 0 .. $#dkim_signing_keys_list; if (@seek_domains) { # sort only when there are any search arguments present @sort_list = sort {$a->[3] cmp $b->[3] || $a->[0] <=> $b->[0]} @sort_list; } my $any = 0; for my $e (@sort_list) { my($j,$domain,$domain_re) = @$e; local($1); safe_encode_utf8_inplace($domain); # to octets (if not already) my $domain_ace = idn_to_ascii($domain); next if @seek_domains && !grep { defined $domain_re ? lc($_) =~ /$domain_re/ : /^\.(.*)\z/s ? $domain_ace eq lc($1) || $domain_ace =~ /(?:\.|\z)\Q$1\E\z/si : $domain_ace eq lc($_) } @seek_domains; $any++; my $key_opts = $dkim_signing_keys_list[$j]; if ($cmd eq 'testkeys' || $cmd eq 'testkey') { test_dkim_key(%$key_opts); } else { my $selector = $key_opts->{selector}; safe_encode_utf8_inplace($selector); # to octets (if not already) my $selector_ace = idn_to_ascii($selector); my $key_storage_ind = $key_opts->{key_storage_ind}; my($key,$dev,$inode,$fname) = @{ $dkim_signing_keys_storage[$key_storage_ind] }; my(@pub) = split(/\r?\n/, $key->get_public_key_x509_string); @pub = grep(!/^---.*?---\z/ && !/^[ \t]*\z/, @pub); my(@tags) = map($_.'='.$key_opts->{$_}, grep(defined $key_opts->{$_}, qw(v g h k s t n))); my $key_size = 8 * $key->size; printf("; key#%d %d bits, s=%s, d=%s%s\n", $key_opts->{key_ind} + 1, $key_size, $selector, $domain, defined $fname ? ', '.$fname : ''); printf("; CANNOT DECLARE A WILDCARDED LABEL IN DNS, ". "AVOID OR EDIT MANUALLY!\n") if defined $key_opts->{domain_re}; printf("%s._domainkey.%s.\t%s TXT (%s)\n\n", $selector_ace, $domain_ace, '3600', join('', map("\n" . ' "' . $_ . '"', join('; ',@tags,'p='), @pub)) ); } } if (!@dkim_signing_keys_list) { printf("No DKIM private keys declared in a config file.\n"); } elsif (!$any) { printf("No DKIM private keys match the selection list.\n"); } } sub test_dkim_key(@) { my(%key_options) = @_; my $now = Time::HiRes::time; my $key_storage_ind = $key_options{key_storage_ind}; my($key,$dev,$inode,$fname) = @{ $dkim_signing_keys_storage[$key_storage_ind] }; if (UNIVERSAL::isa($key,'Crypt::OpenSSL::RSA')) { $key = Mail::DKIM::PrivateKey->load(Cork => $key); # avail since 0.31 # my $pkcs1 = $key->get_private_key_string; # most compact # $pkcs1 =~ s/^---.*?---(?:\r?\n|\z)//gm; $pkcs1 =~ tr/\r\n//d; # $key = Mail::DKIM::PrivateKey->load(Data => $pkcs1); } my $domain = idn_to_utf8($key_options{domain}); my $domain_ace = idn_to_ascii($domain); my $selector_ace = idn_to_ascii($key_options{selector}); my $policyfn = sub { my $dkim = $_[0]; $dkim->add_signature( Mail::DKIM::Signature->new( Selector => $selector_ace, Domain => $domain_ace, Method => 'simple/simple', Algorithm => 'rsa-sha256', Timestamp => int($now), Expiration => int($now)+24*3600, Key => $key, )); undef; }; my $msg = sprintf( "From: test\@%s\nMessage-ID: <123\@%s>\nDate: %s\nSubject: test\n\ntest\n", $domain, $domain, rfc2822_timestamp($now)); $msg =~ s{\n}{\015\012}gs; my(@gen_signatures, @read_signatures); eval { my $dkim_signer = Mail::DKIM::Signer->new(Policy => $policyfn); $dkim_signer or die "Could not create a Mail::DKIM::Signer object"; $dkim_signer->PRINT($msg) or die "Can't write to dkim: $!"; $dkim_signer->CLOSE or die "Can't close dkim signer: $!"; @gen_signatures = $dkim_signer->signatures; } or do { my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat; print STDERR "dkim signing failed: $eval_stat\n"; }; $msg = $_->as_string . "\015\012" . $msg for @gen_signatures; eval { my $dkim_verifier = Mail::DKIM::Verifier->new; $dkim_verifier or die "Could not create a Mail::DKIM::Verifier object"; $dkim_verifier->PRINT($msg) or die "Can't write to dkim: $!"; $dkim_verifier->CLOSE or die "Can't close dkim_verifier: $!"; @read_signatures = $dkim_verifier->signatures; } or do { my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat; print STDERR "dkim verification failed: $eval_stat\n"; }; # printf("%s\n", $fname) if defined $fname; printf("TESTING#%d %s: %s => %s\n", $key_options{key_ind} + 1, $domain, $_->selector . '._domainkey.' . $_->domain, $_->result_detail) for @read_signatures; } sub generate_dkim_private_key(@) { my($fname,$nbits) = @_; my $fh; eval { $nbits = 1024 if !defined($nbits) || $nbits eq ''; $nbits =~ /^\d+\z/ or die "Number of bits in a key must be numeric\n"; $nbits >= 512 or die "Number of bits is below 512 (suggested 1024..2048)\n"; $nbits <= 4096 or die "Number of bits too large (suggested 1024..2048)\n"; defined $fname && $fname ne '' or die "File name for a key not provided\n"; $nbits >= 1024 or printf STDERR ("INFO: RFC 6376 states: Signers MUST use RSA keys ". "of at least 1024 bits for long-lived keys.\n"); $fh = IO::File->new; $fh->open(untaint($fname), O_CREAT|O_EXCL|O_RDWR, 0600) or die "Can't create file \"$fname\": $!\n"; my $rsa = Crypt::OpenSSL::RSA->generate_key($nbits); $fh->print($rsa->get_private_key_string) or die "Error writing key to a file \"$fname\": $!\n"; $fh->close or die "Can't close file \"$fname\": $!\n"; undef $fh; printf STDERR ("Private RSA key successfully written to file \"%s\" ". "(%d bits, PEM format) \n", $fname,$nbits); 1; } or do { my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat; $fh->close if defined $fh; # ignoring status die "genrsa: $eval_stat\n"; } } # Reads a dkim-filter -compatible key specifications. From the dkim-filter # man page: The keyfile should contain a set of lines of the form # sender-pattern:signing-domain:keypath where sender-pattern is a pattern # to match against message senders (with a special character "*" interpreted # as "zero or more characters"), signing-domain is the domain to announce as # the signing domain when generating signatures (or a '*', implying author's # domain), and keypath is a path to the PEM-formatted private key to be used # for signing messages which match the sender-pattern. The selector used in # the signature will be the filename portion of keypath. A line starting # with "/" is interpreted as a root directory for keys, meaning the keypath # values after that line in the file are taken relative to that path. If a # file referenced by keypath cannot be opened, the filter will try again by # appending ".pem" and then ".private". '#'-delimited comments and blank # lines are ignored. # sub convert_dkim_keys_file($) { my $keysfile = $_[0]; my $inp = IO::File->new; $inp->open($keysfile,'<') or die "dkim_key_file: Can't open file $keysfile for reading: $!"; my($basedir,@options,@opt_re,%domain_selectors); my $rn = 0; my $ln; for ($! = 0; defined($ln=$inp->getline); $! = 0) { chomp($ln); $rn++; local($1); my($selector,$key_fn); if ($ln =~ /^ \s* (?: \# | \z)/xs) { # skip empty and all-comment lines } elsif ($ln =~ m{^/}) { $basedir = $ln; $basedir .= '/' if $basedir !~ m{/\z}; } else { my($sender_pattern, $signing_domain, $keypath) = map { my $s = $_; $s =~ s/^\s+//; $s =~ s/\s+\z//; $s } split(/:/, $ln, 3); defined $sender_pattern && $sender_pattern ne '' or die "Error in $keysfile, empty sender pattern, line $rn: $ln\n"; defined $keypath && $keypath ne '' || $signing_domain eq '' or die "Error in $keysfile, empty file name field, line $rn: $ln\n"; $keypath = $basedir . $keypath if defined $basedir && $keypath !~ m{^/}; for my $ext ('', '.pem', '.private') { my $errn = stat($keypath.$ext) ? 0 : 0+$!; if ($errn != ENOENT) { $key_fn = $keypath.$ext; last } } defined $key_fn or die "File $keypath does not exist, $keysfile line $rn: $ln\n"; $selector = lc($1) if $keypath =~ m{ (?: ^ | / ) ( [^/]+? ) (?: \.pem | \.private )? \z }xs; # must convert sender pattern to unquoted form to match actual addresses my $sender_domain; if ($sender_pattern eq '*' || $sender_pattern eq '*@*') { $sender_pattern = $sender_domain = '*'; } else { my $sender_localpart; ($sender_localpart, $sender_domain) = Amavis::rfc2821_2822_Tools::split_address( Amavis::rfc2821_2822_Tools::unquote_rfc2821_local($sender_pattern)); $sender_domain =~ s/^\@//; $sender_pattern = $sender_localpart.'@'.idn_to_ascii($sender_domain); } if ($signing_domain eq '*') { $signing_domain = $sender_domain } $signing_domain = idn_to_ascii($signing_domain); if ($signing_domain ne '' && !$domain_selectors{$signing_domain}{$selector}) { # dkim_key($signing_domain,$selector,$key_fn); # declare a signing key printf("dkim_key(%-18s %-12s '%s');\n", "'".$signing_domain."',", "'".$selector."',", $key_fn); $domain_selectors{$signing_domain}{$selector} = 1; } if ($signing_domain eq $sender_domain) { $signing_domain = '*' } push(@options, [$sender_pattern, $signing_domain, $selector]); } } defined $ln || $! == 0 or die "Error reading from $keysfile: $!"; $inp->close or die "Error closing $keysfile: $!"; # # prepare by_sender signature options lookup table when non-default # signing is required (e.g. third-party signatures) # my $in_options = 0; for my $opt (@options) { my($sender_pattern, $signing_domain, $selector) = @$opt; if ($signing_domain eq '*') { # implies author domain signature, no need for special options } else { $sender_pattern =~ s/\*{2,}/*/gs; # collapse successive wildcards $sender_pattern =~ # '*' is a wildcard, quote the rest s{ ([@\#/.^\$|*+?(){}\[\]\\]) }{ $1 eq '*' ? '.*' : '\\'.$1 }xgse; $sender_pattern = '^' . $sender_pattern . '\\z'; # implicit anchors # remove trailing first, leading next, preferring /^.*\z/ -> /^/, not /\z/ $sender_pattern =~ s/\.\*\\z\z//s; # remove trailing anchor if redundant $sender_pattern =~ s/^\^\.\*//s; # remove leading anchor if redundant $sender_pattern = '(?:)' if $sender_pattern eq ''; # just in case $signing_domain = undef if $signing_domain eq ''; $selector = undef if $selector eq ''; # case insensitive matching for compatibility with dkim-milter push(@opt_re, [ qr/$sender_pattern/is => ( !defined($signing_domain) || keys(%{$domain_selectors{$signing_domain}})==1 ? { d => $signing_domain } : { d => $signing_domain, s => $selector } ) ]); if (!$in_options) { printf("\n%s\n", '@dkim_signature_options_bysender_maps = (new_RE('); $in_options = 1; } printf(" [ %-30s => { d=>%s%s} ],\n", 'qr/' . $sender_pattern . '/is', !defined($signing_domain) ? 'undef' : "'".$signing_domain."'", !defined($signing_domain) || keys %{$domain_selectors{$signing_domain}} == 1 ? '' : !defined($selector) ? ', s=>undef' : ", s=>'".$selector."'"); } } printf("%s\n", '));') if $in_options; # use Devel::Peek qw(Dump); # use Data::Dump (); Data::Dump::dump(@opt_re); # unshift(@dkim_signature_options_bysender_maps, # Amavis::Lookup::RE->new(@opt_re)) if @opt_re; } 1;