Unverified Commit 960ad903 authored by IKEDA Soji's avatar IKEDA Soji Committed by GitHub
Browse files

Merge pull request #419 from jrlevine/arc-sign by jrlevine

Support for ARC signatures
parents 2a271cc4 cd9f47c5
......@@ -257,6 +257,10 @@ feature 'Mail::DKIM::Verifier', 'Required in order to use DKIM features (both fo
requires 'Mail::DKIM::Verifier', '>= 0.37';
};
feature 'Mail::DKIM::ARC::Signer', 'Required in order to use ARC features to add ARC seals.' => sub {
requires 'Mail::DKIM::ARC::Signer', '>= 0.53';
};
feature 'Net::DNS', 'This is required if you set a value for "dmarc_protection_mode" which requires DNS verification.' => sub {
requires 'Net::DNS', '>= 0.65';
};
......
......@@ -140,6 +140,12 @@ dkim_parameters owner,privileged_owner hidden
dkim_signature_apply_on listmaster write
dkim_signature_apply_on owner,privileged_owner hidden
arc_feature listmaster write
arc_feature owner,privileged_owner hidden
arc_parameters listmaster write
arc_parameters owner,privileged_owner hidden
create_list_request.tt2 owner,privileged_owner hidden
list_created.tt2 owner,privileged_owner hidden
......
......@@ -61,7 +61,7 @@
[%~ ELSIF group == 'data_source' ~%]
<h3>[%|loc%]Data sources setup[%END%]</h3>
[%~ ELSIF group == 'dkim' ~%]
<h3>[%|loc%]DKIM[%END%]</h3>
<h3>[%|loc%]DKIM/DMARC/ARC[%END%]</h3>
[%~ ELSIF group == 'other' ~%]
<h3>[%|loc%]Miscellaneous[%END%]
<a class="openInNewWindow"
......
......@@ -176,7 +176,8 @@
[% IF group == 'data_source' %][% SET class = 'active' %][% ELSE %][% SET class = '' %][% END %]
<li class="[% class %]"><a href="[% 'edit_list_request' | url_rel([list,'data_source']) %]" >[%|loc%]Data sources setup[%END%]</a></li>
[% IF group == 'dkim' %][% SET class = 'active' %][% ELSE %][% SET class = '' %][% END %]
<li class="[% class %]"><a href="[% 'edit_list_request' | url_rel([list,'dkim']) %]" >[%|loc%]DKIM[%END%]</a></li>
<li class="[% class %]"><a href="[% 'edit_list_request' | url_rel([list,'dkim']) %]"
>[%|loc%]DKIM/DMARC/ARC[%END%]</a></li>
[% IF action == 'editfile' && selected_file == 'description_templates' %][% SET class = 'active' %][% ELSE %][% SET class = '' %][% END %]
<li class="[% class %]"><a href="[% 'editfile' | url_rel([list,'description_templates']) %]">[%|loc%]List description/homepage[%END%]</a></li>
[% IF action == 'editfile' && selected_file == 'message_templates' %][% SET class = 'active' %][% ELSE %][% SET class = '' %][% END %]
......
......@@ -1793,7 +1793,7 @@ our @params = (
'default' => '3600',
},
{ 'gettext_id' => 'DKIM',
{ 'gettext_id' => 'DKIM and ARC',
'gettext_comment' =>
'DKIM signature verification and re-signing. It requires an external module: Mail-DKIM.',
},
......@@ -1856,6 +1856,46 @@ our @params = (
'optional' => '1',
'file' => 'sympa.conf',
},
{ 'name' => 'arc_feature',
'gettext_id' => 'Enable ARC',
'gettext_comment' =>
'If set to "on", Sympa may add ARC seals to outgoing messages.',
'default' => 'off',
'vhost' => '1',
'file' => 'sympa.conf',
},
{ 'name' => 'arc_srvid',
'gettext_id' => 'SRV ID for Authentication-Results used in ARC seal',
'gettext_comment' =>
'Typically the domain of the mail server',
'vhost' => '1',
'optional' => '1',
'file' => 'sympa.conf',
},
{ 'name' => 'arc_signer_domain',
'vhost' => '1',
'gettext_id' => 'The "d=" tag as defined in ARC',
'gettext_comment' =>
'The ARC "d=" tag is the domain of the signing entity. The DKIM d= domain name is used as its default value',
'optional' => '1',
'file' => 'sympa.conf',
},
{ 'name' => 'arc_selector',
'gettext_id' => 'Selector for DNS lookup of ARC public key',
'gettext_comment' =>
'The selector is used in order to build the DNS query for public key. It is up to you to choose the value you want but verify that you can query the public DKIM key for "<selector>._domainkey.your_domain". Default is the same selector as for DKIM signatures',
'vhost' => '1',
'optional' => '1',
'file' => 'sympa.conf',
},
{ 'name' => 'arc_private_key_path',
'vhost' => '1',
'gettext_id' => 'File path for ARC private key',
'gettext_comment' =>
'The file must contain a PEM encoded private key. Defaults to same file as DKIM private key',
'optional' => '1',
'file' => 'sympa.conf',
},
# Not yet implemented.
#{
# name => 'dkim_header_list',
......
......@@ -2153,8 +2153,58 @@ our %pinfo = (
'default' => {'conf' => 'dkim_signature_apply_on'}
},
'arc_feature' => {
order => 70.04,
'group' => 'dkim',
'gettext_id' => "Add ARC seals to messages sent to the list",
'gettext_comment' =>
"Enable/Disable ARC. This feature requires Mail::DKIM::ARC to be installed, and maybe some custom scenario to be updated",
'format' => ['on', 'off'],
'occurrence' => '1',
'default' => {'conf' => 'arc_feature'}
},
'arc_parameters' => {
order => 70.05,
'group' => 'dkim',
'gettext_id' => "ARC configuration",
'gettext_comment' =>
'A set of parameters in order to define outgoing ARC seal',
'format' => {
'arc_private_key_path' => {
'order' => 1,
'gettext_id' => "File path for list ARC private key",
'gettext_comment' =>
"The file must contain a RSA pem encoded private key. Default is DKIM private key.",
'format' => '\S+',
'occurrence' => '0-1',
'default' => {'conf' => 'arc_private_key_path'}
},
'arc_selector' => {
'order' => 2,
'gettext_id' => "Selector for DNS lookup of ARC public key",
'gettext_comment' =>
"The selector is used in order to build the DNS query for public key. It is up to you to choose the value you want but verify that you can query the public DKIM key for <selector>._domainkey.your_domain. Default is selector for DKIM signature",
'format' => '\S+',
'occurrence' => '0-1',
'default' => {'conf' => 'arc_selector'}
},
'arc_signer_domain' => {
'order' => 3,
'gettext_id' =>
'ARC "d=" tag, you should probably use the default value',
'gettext_comment' =>
'The ARC "d=" tag, is the domain of the sealing entity. The list domain MUST be included in the "d=" domain',
'format' => '\S+',
'occurrence' => '0-1',
'default' => {'conf' => 'arc_signer_domain'}
},
},
'occurrence' => '0-1'
},
'dmarc_protection' => {
order => 70.04,
order => 70.07,
'format' => {
'mode' => {
'format' => [
......
......@@ -442,6 +442,7 @@ BEGIN {
$has_mail_dkim_textwrap = !$EVAL_ERROR;
# Mail::DKIM::Signer prior to 0.38 doesn't import this.
eval 'use Mail::DKIM::PrivateKey';
eval 'use Mail::DKIM::ARC::Signer';
}
# Old name: tools::dkim_sign() which took string and returned string.
......@@ -529,7 +530,100 @@ sub dkim_sign {
return $self;
}
BEGIN { eval 'use Mail::DKIM::Verifier'; }
sub arc_seal {
$log->syslog('debug2', '(%s)', @_);
my $self = shift;
my %options = @_;
my $arc_d = $options{'arc_d'};
my $arc_selector = $options{'arc_selector'};
my $arc_privatekey = $options{'arc_privatekey'};
my $arc_srvid = $options{'arc_srvid'};
my $arc_cv = $options{'arc_cv'};
unless ($arc_selector) {
$log->syslog('err',
"ARC selector is undefined, could not seal message");
return undef;
}
unless ($arc_privatekey) {
$log->syslog('err',
"ARC key file is undefined, could not seal message");
return undef;
}
unless ($arc_d) {
$log->syslog('err',
"ARC d= tag is undefined, could not seal message");
return undef;
}
unless ($arc_cv =~ m{^(none|pass|fail)$}) {
$log->syslog('err',
"ARC chain value %s is invalid, could not seal message", $arc_cv);
return undef;
}
unless ($Mail::DKIM::ARC::Signer::VERSION) {
$log->syslog('err',
"Failed to load Mail::DKIM::ARC::Signer Perl module, no seal added"
);
return undef;
}
# DKIM::PrivateKey does never allow armour texts nor newlines. Strip them.
my $privatekey_string = join '',
grep { !/^---/ and $_ } split /\r\n|\r|\n/, $arc_privatekey;
my $privatekey = Mail::DKIM::PrivateKey->load(Data => $privatekey_string);
unless ($privatekey) {
$log->syslog('err', 'Can\'t create Mail::DKIM::PrivateKey');
return undef;
}
# create a signer object
my $arc = Mail::DKIM::ARC::Signer->new(
Algorithm => "rsa-sha256",
Chain => $arc_cv,
SrvId => $arc_srvid,
Domain => $arc_d,
Selector => $arc_selector,
Key => $privatekey,
);
unless ($arc) {
$log->syslog('err', 'Can\'t create Mail::DKIM::ARC::Signer');
return undef;
}
# $new_body will store the body as fed to Mail::DKIM to reuse it
# when returning the message as string. Line terminators must be
# normalized with CRLF.
my $msg_as_string = $self->as_string;
$msg_as_string =~ s/\r?\n/\r\n/g;
$msg_as_string =~ s/\r?\z/\r\n/ unless $msg_as_string =~ /\n\z/;
$arc->PRINT($msg_as_string);
unless ($arc->CLOSE) {
$log->syslog('err', 'Cannot ARC seal message');
return undef;
}
# don't need this since DKIM just did it
# my ($dummy, $new_body) = split /\r\n\r\n/, $msg_as_string, 2;
#$new_body =~ s/\r\n/\n/g;
# Seal is done. Add new headers for the seal
my @seal = $arc->as_strings();
foreach my $ahdr (@seal) {
my ($ah, $av) = split /:\s*/,$ahdr,2;
$self->add_header($ah, $av, 0);
}
#$self->{_body} = $new_body;
delete $self->{_entity_cache}; # Clear entity cache.
return $self;
}
BEGIN { eval 'use Mail::DKIM::Verifier';
eval 'use Mail::DKIM::ARC::Verifier';
}
sub check_dkim_signature {
my $self = shift;
......@@ -540,6 +634,7 @@ sub check_dkim_signature {
(ref $self->{context} eq 'Sympa::List')
? $self->{context}->{'domain'}
: $self->{context};
return
unless Sympa::Tools::Data::smart_eq(
Conf::get_robot_conf($robot_id || '*', 'dkim_feature'), 'on');
......@@ -570,6 +665,59 @@ sub check_dkim_signature {
delete $self->{'dkim_pass'};
}
sub check_arc_chain {
my $self = shift;
return unless $Mail::DKIM::ARC::Verifier::VERSION;
my $robot_id =
(ref $self->{context} eq 'Sympa::List')
? $self->{context}->{'domain'}
: $self->{context};
my $srvid;
unless($srvid = Conf::get_robot_conf($robot_id || '*', 'arc_srvid')) {
$log->syslog('debug2', 'ARC library installed, but no arc_srvid set');
return;
}
# if there is no authentication-results, not much point in checking ARC
# since we can't add a new seal
my @ars = grep { m{^\s*\Q$srvid\E;} } $self->get_header('Authentication-Results');
unless(@ars) {
$log->syslog('debug2', 'ARC enabled but no Authentication-Results: %s;', $srvid);
return;
}
# already checked?
foreach my $ar (@ars) {
if($ar =~ m{\barc=(pass|fail|none)\b}i) {
$log->syslog('debug2', "ARC already $1");
$self->{shelved}->{arc_cv} = $1;
return;
}
}
my $arc;
unless ($arc = Mail::DKIM::ARC::Verifier->new(Strict => 1)) {
$log->syslog('err', 'Could not create Mail::DKIM::ARC::Verifier');
return;
}
# Line terminators must be normalized with CRLF.
my $msg_as_string = $self->as_string;
$msg_as_string =~ s/\r?\n/\r\n/g;
$msg_as_string =~ s/\r?\z/\r\n/ unless $msg_as_string =~ /\n\z/;
$arc->PRINT($msg_as_string);
unless ($arc->CLOSE) {
$log->syslog('err', 'Cannot verify chain of (ARC) message');
return;
}
$log->syslog('debug2', 'result %s', $arc->result);
$self->{shelved}->{arc_cv} = $arc->result;
}
# Old name: tools::remove_invalid_dkim_signature() which takes a message as
# string and outputs idem without signature if invalid.
sub remove_invalid_dkim_signature {
......@@ -3402,6 +3550,18 @@ I<Instance method>.
Verifies DKIM signatures included in the message,
and if any of them are invalid, removes them.
=item check_arc_chain ( )
I<Instance method>.
Checks ARC chain of the message
and sets {shelved}{arc_cv} item of the message object.
=item arc_seal ( )
I<Instance method>.
Adds a new ARC seal if there's an arc_cv from check_arc_chain and
the cv is none or valid.
=item as_entity ( )
I<Instance method>.
......
......@@ -198,6 +198,8 @@ sub _twist {
$message->check_spam_status;
# Check DKIM signatures.
$message->check_dkim_signature;
# Check ARC seals
$message->check_arc_chain;
# Check S/MIME signature.
$message->check_smime_signature;
# Decrypt message. On success, check nested S/MIME signature.
......
......@@ -220,14 +220,17 @@ sub _twist {
# -5 S/MIME encryption
# -6 remove existing signature if altered
# -7 DKIM signing
# -8 ARC seal
if ($message->{shelved}{dmarc_protect}) {
$message->dmarc_protect;
}
my $dkim;
my ($dkim, $arc);
if ($message->{shelved}{dkim_sign}) {
$dkim = Sympa::Tools::DKIM::get_dkim_parameters($message->{context});
$arc = Sympa::Tools::DKIM::get_arc_parameters($message->{context})
if $message->{shelved}->{arc_cv};
}
if ( $message->{shelved}{merge}
......@@ -333,6 +336,16 @@ sub _twist {
'dkim_selector' => $dkim->{'selector'},
'dkim_privatekey' => $dkim->{'private_key'},
);
$new_message->arc_seal(
'arc_d' => $arc->{'d'},
'arc_selector' => $arc->{'selector'},
'arc_srvid' => $arc->{'srvid'},
'arc_privatekey' => $arc->{'private_key'},
'arc_cv' => $message->{shelved}->{arc_cv}
) if $arc;
delete $new_message->{shelved}{dkim_sign};
}
......@@ -382,6 +395,14 @@ sub _twist {
'dkim_selector' => $dkim->{'selector'},
'dkim_privatekey' => $dkim->{'private_key'},
);
$new_message->arc_seal(
'arc_d' => $arc->{'d'},
'arc_selector' => $arc->{'selector'},
'arc_srvid' => $arc->{'srvid'},
'arc_privatekey' => $arc->{'private_key'},
'arc_cv' => $message->{shelved}->{arc_cv}
) if $arc;
delete $new_message->{shelved}{dkim_sign};
}
......
......@@ -72,7 +72,6 @@ sub get_dkim_parameters {
Conf::get_robot_conf($robot_id, 'dkim_selector');
$keyfile = Conf::get_robot_conf($robot_id, 'dkim_private_key_path');
}
return undef
unless defined $data->{'d'}
and defined $data->{'selector'}
......@@ -90,6 +89,67 @@ sub get_dkim_parameters {
return $data;
}
sub get_arc_parameters {
$log->syslog('debug2', '(%s)', @_);
my $that = shift;
my ($robot_id, $list);
if (ref $that eq 'Sympa::List') {
$robot_id = $that->{'domain'};
$list = $that;
} elsif ($that and $that ne '*') {
$robot_id = $that;
} else {
$robot_id = '*';
}
my ($data, $keyfile);
if ($list) {
# check if enabled for the list
$log->syslog('debug2', 'list arc feature %s', $list->{'admin'}{'arc_feature'});
return undef unless $list->{'admin'}{'arc_feature'} eq 'on';
# fetch arc parameter in list context
$data->{'d'} = $list->{'admin'}{'arc_parameters'}{'arc_signer_domain'}
|| $list->{'admin'}{'dkim_parameters'}{'signer_domain'};
$data->{'selector'} = $list->{'admin'}{'arc_parameters'}{'arc_selector'}
|| $list->{'admin'}{'dkim_parameters'}{'selector'};
$keyfile = $list->{'admin'}{'arc_parameters'}{'arc_private_key_path'}
|| $list->{'admin'}{'dkim_parameters'}{'private_key_path'};
} else {
# in robot context
$log->syslog('debug2', 'robot arc feature %s',Conf::get_robot_conf($robot_id, 'arc_feature'));
return undef unless Conf::get_robot_conf($robot_id, 'arc_feature') eq 'on';
$data->{'d'} = Conf::get_robot_conf($robot_id, 'arc_signer_domain')
|| Conf::get_robot_conf($robot_id, 'dkim_signer_domain');
$data->{'selector'} =
Conf::get_robot_conf($robot_id, 'arc_selector') ||
Conf::get_robot_conf($robot_id, 'dkim_selector');
$keyfile = Conf::get_robot_conf($robot_id, ' arc_private_key_path')
|| Conf::get_robot_conf($robot_id, 'dkim_private_key_path');
}
$data->{'srvid'} = Conf::get_robot_conf($robot_id, 'arc_srvid')
|| $data->{'d'};
return undef
unless defined $data->{'d'}
and defined $data->{'selector'}
and defined $keyfile;
my $fh;
unless (open $fh, '<', $keyfile) {
$log->syslog('err', 'Could not read arc private key %s: %m',
$keyfile);
return undef;
}
$data->{'private_key'} = do { local $RS; <$fh> };
close $fh;
return $data;
}
# Old name: tools::dkim_verifier().
#DEPRECATED: Use Sympa::Message::check_dkim_signature().
#sub verifier($msg_as_string);
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment