all lists on lists.proxmox.com
 help / color / mirror / Atom feed
* [pmg-devel] [PATCH pmg-api v5] fix-2971: DKIM: Add setting to use From header when signing
@ 2024-02-16 12:41 Maximiliano Sandoval
  2024-02-23 11:31 ` Stoiko Ivanov
  0 siblings, 1 reply; 2+ messages in thread
From: Maximiliano Sandoval @ 2024-02-16 12:41 UTC (permalink / raw)
  To: pmg-devel

Following RFC 5322 [2], we add an option to use the address from the
`From:` header instead of the Envelope From address.

From RFC 7489 [1]:

   To illustrate, in relaxed mode, if a validated DKIM signature
   successfully verifies with a "d=" domain of "example.com", and the
   RFC5322.From address is "alerts@news.example.com", the DKIM "d="
   domain and the RFC5322.From domain are considered to be "in
   alignment".  In strict mode, this test would fail, since the "d="
   domain does not exactly match the FQDN of the address.

Tested with the following command:

    swaks --from foo@envelope.domain --to EMAIL -s PMG_ADDR:26 --data "Date: %DATE%\nTo: %TO_ADDRESS%\nFrom: bar@header.domain\nSubject: test %DATE%\nMessage-Id: <%MESSAGEID%>\nX-Mailer: swaks v%SWAKS_VERSION% jetmore.org/john/code/swaks/\n%NEW_HEADERS%\n%BODY%\n"

Where EMAIL and PMG_ADDR are given adequate values, and envelope.domain
and header.domain are in the 'Sign Domains' list.

[1] https://datatracker.ietf.org/doc/html/rfc7489#section-3.1.1
[2] https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.2

Signed-off-by: Maximiliano Sandoval <m.sandoval@proxmox.com>
---
Do note that for testing one has to restart pmg-smtp-filter after changing the setting value.

Differences from v4:
 - Rename setting to dkim_use_domain and make it an enum
 - Improved commit message
 - Do not use 'Sender' field as a fallback

Differences from v3:
 - Take into account setting
 - rename parsing function to parse_headers_for_signing

 src/PMG/Config.pm        |  8 +++++++
 src/PMG/DKIMSign.pm      | 48 ++++++++++++++++++++++++++++++++++------
 src/PMG/RuleDB/Accept.pm |  3 +--
 src/PMG/RuleDB/BCC.pm    |  3 +--
 src/bin/pmg-smtp-filter  |  1 +
 5 files changed, 52 insertions(+), 11 deletions(-)

diff --git a/src/PMG/Config.pm b/src/PMG/Config.pm
index 7339e0d..e9e740d 100644
--- a/src/PMG/Config.pm
+++ b/src/PMG/Config.pm
@@ -134,6 +134,12 @@ EODESC
 	    description => "Default DKIM selector",
 	    type => 'string', format => 'dns-name', #see RFC6376 3.1
 	},
+	dkim_use_domain => {
+	    description => "Whether to sign using the address from the header or the envelope.",
+	    type => 'string',
+	    enum => [qw(header envelope)],
+	    default => 'envelope',
+	},
     };
 }
 
@@ -152,6 +158,7 @@ sub options {
 	dkim_sign => { optional => 1 },
 	dkim_sign_all_mail => { optional => 1 },
 	dkim_selector => { optional => 1 },
+	dkim_use_domain => { optional => 1 },
     };
 }
 
@@ -1788,6 +1795,7 @@ my $pmg_service_params = {
 	dkim_selector => 1,
 	dkim_sign => 1,
 	dkim_sign_all_mail => 1,
+	dkim_use_domain => 1,
     },
 };
 
diff --git a/src/PMG/DKIMSign.pm b/src/PMG/DKIMSign.pm
index 08197f8..5df9aa8 100644
--- a/src/PMG/DKIMSign.pm
+++ b/src/PMG/DKIMSign.pm
@@ -2,6 +2,7 @@ package PMG::DKIMSign;
 
 use strict;
 use warnings;
+use Email::Address::XS;
 use Mail::DKIM::Signer;
 use Mail::DKIM::TextWrap;
 use Crypt::OpenSSL::RSA;
@@ -53,11 +54,16 @@ sub create_signature {
 
 #determines which domain should be used for signing based on the e-mailaddress
 sub signing_domain {
-    my ($self, $sender_email) = @_;
-
-    my @parts = split('@', $sender_email);
-    die "no domain in sender e-mail\n" if scalar(@parts) < 2;
-    my $input_domain = $parts[-1];
+    my ($self, $sender_email, $entity, $use_domain) = @_;
+
+    my $input_domain;
+    if ($use_domain eq 'header') {
+	$input_domain = parse_headers_for_signing($entity);
+    } else {
+	my @parts = split('@', $sender_email);
+	die "no domain in sender e-mail\n" if scalar(@parts) < 2;
+	$input_domain = $parts[-1];
+    }
 
     if ($self->{sign_all}) {
 	    $self->domain($input_domain) if $self->{sign_all};
@@ -84,8 +90,36 @@ sub signing_domain {
 }
 
 
+sub parse_headers_for_signing {
+    # Following RFC 7489 [1], we only sign emails with exactly one sender in the
+    # From header.
+    #
+    # [1] https://datatracker.ietf.org/doc/html/rfc7489#section-6.6.1
+    my ($entity) = @_;
+
+    my @from_list;
+
+    my @from_headers = $entity->head->get('from');
+    foreach my $from_header (@from_headers) {
+	my @senders = Email::Address::XS->parse($from_header);
+	foreach my $sender (@senders) {
+	    push(@from_list, @senders);
+	}
+    }
+    my $from_count = scalar(@from_list);
+
+    if ($from_count == 1) {
+	return $from_list[0]->host();
+    }
+}
+
+
 sub sign_entity {
-    my ($entity, $selector, $sender, $sign_all) = @_;
+    my ($entity, $dkim, $sender) = @_;
+
+    my $sign_all = $dkim->{sign_all};
+    my $use_domain = $dkim->{use_domain};
+    my $selector = $dkim->{selector};
 
     die "no selector provided\n" if ! $selector;
 
@@ -110,7 +144,7 @@ sub sign_entity {
 
     $signer->extended_headers($extended_headers);
 
-    if ($signer->signing_domain($sender)) {
+    if ($signer->signing_domain($sender, $entity, $use_domain)) {
 	$entity->print($signer);
 	my $signature = $signer->create_signature();
 	$entity->head->add('DKIM-Signature', $signature, 0);
diff --git a/src/PMG/RuleDB/Accept.pm b/src/PMG/RuleDB/Accept.pm
index 4ebd6da..d14c2fb 100644
--- a/src/PMG/RuleDB/Accept.pm
+++ b/src/PMG/RuleDB/Accept.pm
@@ -102,8 +102,7 @@ sub execute {
 
 	if ($dkim->{sign}) {
 	    eval {
-		$entity = PMG::DKIMSign::sign_entity($entity,
-		    $dkim->{selector}, $msginfo->{sender}, $dkim->{sign_all});
+		$entity = PMG::DKIMSign::sign_entity($entity, $dkim, $msginfo->{sender});
 	    };
 	    syslog('warning',
 		"Could not create DKIM-Signature - disabling Signing: $@") if $@;
diff --git a/src/PMG/RuleDB/BCC.pm b/src/PMG/RuleDB/BCC.pm
index 0f016f8..65b6fb5 100644
--- a/src/PMG/RuleDB/BCC.pm
+++ b/src/PMG/RuleDB/BCC.pm
@@ -142,8 +142,7 @@ sub execute {
 	my $dkim = $msginfo->{dkim} // {};
 	if ($dkim->{sign}) {
 	    eval {
-		$entity = PMG::DKIMSign::sign_entity($entity,
-		    $dkim->{selector}, $msginfo->{sender}, $dkim->{sign_all});
+		$entity = PMG::DKIMSign::sign_entity($entity, $dkim, $msginfo->{sender});
 	    };
 	    syslog('warning',
 		"Could not create DKIM-Signature - disabling Signing: $@") if $@;
diff --git a/src/bin/pmg-smtp-filter b/src/bin/pmg-smtp-filter
index 7da3de8..aedae0c 100755
--- a/src/bin/pmg-smtp-filter
+++ b/src/bin/pmg-smtp-filter
@@ -627,6 +627,7 @@ sub handle_smtp {
 	my $dkim_sign = $msginfo->{trusted} && $pmg_cfg->get('admin', 'dkim_sign');
 	if ($dkim_sign) {
 	    $msginfo->{dkim}->{sign} = $dkim_sign;
+	    $msginfo->{dkim}->{use_domain} = $pmg_cfg->get('admin', 'dkim_use_domain');
 	    $msginfo->{dkim}->{sign_all} = $pmg_cfg->get('admin', 'dkim_sign_all_mail');
 	    $msginfo->{dkim}->{selector} = $pmg_cfg->get('admin', 'dkim_selector');
 	}
-- 
2.39.2





^ permalink raw reply	[flat|nested] 2+ messages in thread

* Re: [pmg-devel] [PATCH pmg-api v5] fix-2971: DKIM: Add setting to use From header when signing
  2024-02-16 12:41 [pmg-devel] [PATCH pmg-api v5] fix-2971: DKIM: Add setting to use From header when signing Maximiliano Sandoval
@ 2024-02-23 11:31 ` Stoiko Ivanov
  0 siblings, 0 replies; 2+ messages in thread
From: Stoiko Ivanov @ 2024-02-23 11:31 UTC (permalink / raw)
  To: Maximiliano Sandoval; +Cc: pmg-devel

Thanks for the update - the enum indeed feels nicer!

as said off-list - please also add this to the GUI

a few comments inline:

On Fri, 16 Feb 2024 13:41:35 +0100
Maximiliano Sandoval <m.sandoval@proxmox.com> wrote:

> Following RFC 5322 [2], we add an option to use the address from the
> `From:` header instead of the Envelope From address.
> 
> From RFC 7489 [1]:
> 
>    To illustrate, in relaxed mode, if a validated DKIM signature
>    successfully verifies with a "d=" domain of "example.com", and the
>    RFC5322.From address is "alerts@news.example.com", the DKIM "d="
>    domain and the RFC5322.From domain are considered to be "in
>    alignment".  In strict mode, this test would fail, since the "d="
>    domain does not exactly match the FQDN of the address.
> 
> Tested with the following command:
> 
>     swaks --from foo@envelope.domain --to EMAIL -s PMG_ADDR:26 --data "Date: %DATE%\nTo: %TO_ADDRESS%\nFrom: bar@header.domain\nSubject: test %DATE%\nMessage-Id: <%MESSAGEID%>\nX-Mailer: swaks v%SWAKS_VERSION% jetmore.org/john/code/swaks/\n%NEW_HEADERS%\n%BODY%\n"
> 
> Where EMAIL and PMG_ADDR are given adequate values, and envelope.domain
> and header.domain are in the 'Sign Domains' list.
> 
> [1] https://datatracker.ietf.org/doc/html/rfc7489#section-3.1.1
> [2] https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.2
> 
> Signed-off-by: Maximiliano Sandoval <m.sandoval@proxmox.com>
> ---
> Do note that for testing one has to restart pmg-smtp-filter after changing the setting value.
> 
> Differences from v4:
>  - Rename setting to dkim_use_domain and make it an enum
>  - Improved commit message
>  - Do not use 'Sender' field as a fallback
> 
> Differences from v3:
>  - Take into account setting
>  - rename parsing function to parse_headers_for_signing
> 
>  src/PMG/Config.pm        |  8 +++++++
>  src/PMG/DKIMSign.pm      | 48 ++++++++++++++++++++++++++++++++++------
>  src/PMG/RuleDB/Accept.pm |  3 +--
>  src/PMG/RuleDB/BCC.pm    |  3 +--
>  src/bin/pmg-smtp-filter  |  1 +
>  5 files changed, 52 insertions(+), 11 deletions(-)
> 
> diff --git a/src/PMG/Config.pm b/src/PMG/Config.pm
> index 7339e0d..e9e740d 100644
> --- a/src/PMG/Config.pm
> +++ b/src/PMG/Config.pm
> @@ -134,6 +134,12 @@ EODESC
>  	    description => "Default DKIM selector",
>  	    type => 'string', format => 'dns-name', #see RFC6376 3.1
>  	},
> +	dkim_use_domain => {
please use 'kebab-case' for new configs instead of 'snake_case'

> +	    description => "Whether to sign using the address from the header or the envelope.",
> +	    type => 'string',
> +	    enum => [qw(header envelope)],
> +	    default => 'envelope',
> +	},
>      };
>  }
>  
> @@ -152,6 +158,7 @@ sub options {
>  	dkim_sign => { optional => 1 },
>  	dkim_sign_all_mail => { optional => 1 },
>  	dkim_selector => { optional => 1 },
> +	dkim_use_domain => { optional => 1 },
>      };
>  }
>  
> @@ -1788,6 +1795,7 @@ my $pmg_service_params = {
>  	dkim_selector => 1,
>  	dkim_sign => 1,
>  	dkim_sign_all_mail => 1,
> +	dkim_use_domain => 1,
>      },
>  };
>  
> diff --git a/src/PMG/DKIMSign.pm b/src/PMG/DKIMSign.pm
> index 08197f8..5df9aa8 100644
> --- a/src/PMG/DKIMSign.pm
> +++ b/src/PMG/DKIMSign.pm
> @@ -2,6 +2,7 @@ package PMG::DKIMSign;
>  
>  use strict;
>  use warnings;
> +use Email::Address::XS;
>  use Mail::DKIM::Signer;
>  use Mail::DKIM::TextWrap;
>  use Crypt::OpenSSL::RSA;
> @@ -53,11 +54,16 @@ sub create_signature {
>  
>  #determines which domain should be used for signing based on the e-mailaddress
>  sub signing_domain {
> -    my ($self, $sender_email) = @_;
> -
> -    my @parts = split('@', $sender_email);
> -    die "no domain in sender e-mail\n" if scalar(@parts) < 2;
> -    my $input_domain = $parts[-1];
> +    my ($self, $sender_email, $entity, $use_domain) = @_;
> +
> +    my $input_domain;
> +    if ($use_domain eq 'header') {
> +	$input_domain = parse_headers_for_signing($entity);
> +    } else {
> +	my @parts = split('@', $sender_email);
> +	die "no domain in sender e-mail\n" if scalar(@parts) < 2;
> +	$input_domain = $parts[-1];
> +    }
>  
>      if ($self->{sign_all}) {
>  	    $self->domain($input_domain) if $self->{sign_all};
> @@ -84,8 +90,36 @@ sub signing_domain {
>  }
>  
>  
> +sub parse_headers_for_signing {
> +    # Following RFC 7489 [1], we only sign emails with exactly one sender in the
> +    # From header.
> +    #
> +    # [1] https://datatracker.ietf.org/doc/html/rfc7489#section-6.6.1
> +    my ($entity) = @_;
> +
> +    my @from_list;
> +
> +    my @from_headers = $entity->head->get('from');
> +    foreach my $from_header (@from_headers) {
> +	my @senders = Email::Address::XS->parse($from_header);
> +	foreach my $sender (@senders) {
> +	    push(@from_list, @senders);
why do you push the list of senders X times to the from_list (X being the
number in that particular from-header line)?
I think the for-loop is simply not needed
In general - if you want to see how some data-structure looks like during
testing - I can recommend adding Data::Dumper statements - with our
services in PMG you can usually log them with syslog e.g.:
```
use Data::Dumper;
$Data::Dumper::Sortkeys = 1;
syslog('err', "DEBUG: ". Dumper($data_for_debug));
```
it's very crude (and the syslog is not the right place for this) but gives
a good overview


> +	}


> +    }
> +    my $from_count = scalar(@from_list);
if you only use the list for counting - why not use an integer and
increment it or return early above if you have more than one element?

> +
> +    if ($from_count == 1) {
> +	return $from_list[0]->host();
I'd consider a `die` here in case the count !=1 - that way it should be
logged by the actions (but please verify the call-sites) - we do log this
for the current code in sign_entity as well
(was confused why I did not get a warning for a mail without any
from-header)

> +    }
> +}
> +
> +
>  sub sign_entity {
> -    my ($entity, $selector, $sender, $sign_all) = @_;
> +    my ($entity, $dkim, $sender) = @_;
> +
> +    my $sign_all = $dkim->{sign_all};
> +    my $use_domain = $dkim->{use_domain};
> +    my $selector = $dkim->{selector};
>  
>      die "no selector provided\n" if ! $selector;
>  
> @@ -110,7 +144,7 @@ sub sign_entity {
>  
>      $signer->extended_headers($extended_headers);
>  
> -    if ($signer->signing_domain($sender)) {
> +    if ($signer->signing_domain($sender, $entity, $use_domain)) {
>  	$entity->print($signer);
>  	my $signature = $signer->create_signature();
>  	$entity->head->add('DKIM-Signature', $signature, 0);
> diff --git a/src/PMG/RuleDB/Accept.pm b/src/PMG/RuleDB/Accept.pm
> index 4ebd6da..d14c2fb 100644
> --- a/src/PMG/RuleDB/Accept.pm
> +++ b/src/PMG/RuleDB/Accept.pm
> @@ -102,8 +102,7 @@ sub execute {
>  
>  	if ($dkim->{sign}) {
>  	    eval {
> -		$entity = PMG::DKIMSign::sign_entity($entity,
> -		    $dkim->{selector}, $msginfo->{sender}, $dkim->{sign_all});
> +		$entity = PMG::DKIMSign::sign_entity($entity, $dkim, $msginfo->{sender});
>  	    };
>  	    syslog('warning',
>  		"Could not create DKIM-Signature - disabling Signing: $@") if $@;
> diff --git a/src/PMG/RuleDB/BCC.pm b/src/PMG/RuleDB/BCC.pm
> index 0f016f8..65b6fb5 100644
> --- a/src/PMG/RuleDB/BCC.pm
> +++ b/src/PMG/RuleDB/BCC.pm
> @@ -142,8 +142,7 @@ sub execute {
>  	my $dkim = $msginfo->{dkim} // {};
>  	if ($dkim->{sign}) {
>  	    eval {
> -		$entity = PMG::DKIMSign::sign_entity($entity,
> -		    $dkim->{selector}, $msginfo->{sender}, $dkim->{sign_all});
> +		$entity = PMG::DKIMSign::sign_entity($entity, $dkim, $msginfo->{sender});
>  	    };
>  	    syslog('warning',
>  		"Could not create DKIM-Signature - disabling Signing: $@") if $@;
> diff --git a/src/bin/pmg-smtp-filter b/src/bin/pmg-smtp-filter
> index 7da3de8..aedae0c 100755
> --- a/src/bin/pmg-smtp-filter
> +++ b/src/bin/pmg-smtp-filter
> @@ -627,6 +627,7 @@ sub handle_smtp {
>  	my $dkim_sign = $msginfo->{trusted} && $pmg_cfg->get('admin', 'dkim_sign');
>  	if ($dkim_sign) {
>  	    $msginfo->{dkim}->{sign} = $dkim_sign;
> +	    $msginfo->{dkim}->{use_domain} = $pmg_cfg->get('admin', 'dkim_use_domain');
>  	    $msginfo->{dkim}->{sign_all} = $pmg_cfg->get('admin', 'dkim_sign_all_mail');
>  	    $msginfo->{dkim}->{selector} = $pmg_cfg->get('admin', 'dkim_selector');
>  	}





^ permalink raw reply	[flat|nested] 2+ messages in thread

end of thread, other threads:[~2024-02-23 11:31 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2024-02-16 12:41 [pmg-devel] [PATCH pmg-api v5] fix-2971: DKIM: Add setting to use From header when signing Maximiliano Sandoval
2024-02-23 11:31 ` Stoiko Ivanov

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal