all lists on lists.proxmox.com
 help / color / mirror / Atom feed
* [pmg-devel] [PATCH pmg-api v4] fix-2971: DKIM: Add setting to use From header when signing
@ 2024-01-16 10:43 Maximiliano Sandoval
  2024-02-15 19:21 ` Stoiko Ivanov
  0 siblings, 1 reply; 3+ messages in thread
From: Maximiliano Sandoval @ 2024-01-16 10:43 UTC (permalink / raw)
  To: pmg-devel

We add an option to use the address from the `From:` header instead of
the Envelope From address. Following RFC 5322 [2], we use the `Sender:`
header when there are multiple addresses in the `From:` header.

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@bar1 --to EMAIL -s PMG_ADDR:26 --data "Date: %DATE%\nTo: %TO_ADDRESS%\nFrom: foo@bar2\nSubject: test %DATE%\nMessage-Id: <%MESSAGEID%>\nX-Mailer: swaks v%SWAKS_VERSION% jetmore.org/john/code/swaks/\n%NEW_HEADERS%\n%BODY%\n"

[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 v3:
 - Take into account setting
 - rename parsing function to parse_headers_for_signing

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

diff --git a/src/PMG/Config.pm b/src/PMG/Config.pm
index 7339e0d..840c0ad 100644
--- a/src/PMG/Config.pm
+++ b/src/PMG/Config.pm
@@ -134,6 +134,11 @@ EODESC
 	    description => "Default DKIM selector",
 	    type => 'string', format => 'dns-name', #see RFC6376 3.1
 	},
+	dkim_prefer_rfc5322_domain => {
+	    description => "Whether to use the address from the From header when DKIM signing outbound mails.",
+	    type => 'boolean',
+	    default => 0,
+	},
     };
 }
 
@@ -152,6 +157,7 @@ sub options {
 	dkim_sign => { optional => 1 },
 	dkim_sign_all_mail => { optional => 1 },
 	dkim_selector => { optional => 1 },
+	dkim_prefer_rfc5322_domain => { optional => 1 },
     };
 }
 
@@ -1788,6 +1794,7 @@ my $pmg_service_params = {
 	dkim_selector => 1,
 	dkim_sign => 1,
 	dkim_sign_all_mail => 1,
+	dkim_prefer_rfc5322_domain => 1,
     },
 };
 
diff --git a/src/PMG/DKIMSign.pm b/src/PMG/DKIMSign.pm
index 08197f8..2c73101 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, $prefer_rfc5322_domain) = @_;
+
+    my $input_domain;
+    if ($prefer_rfc5322_domain) {
+	$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,60 @@ sub signing_domain {
 }
 
 
+sub parse_headers_for_signing {
+    # If there is exactly one single address in the 'From' header we use that
+    # address for signing, otherwise we use the 'Sender' header.
+    my ($entity) = @_;
+
+    my @sender_list;
+    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) {
+	my @send_headers = $entity->head->get('sender');
+	foreach my $send_header (@send_headers) {
+	    my @senders = Email::Address::XS->parse($send_header);
+	    foreach my $sender (@senders) {
+		push(@sender_list, $sender);
+	    }
+	}
+	my $send_count = scalar(@sender_list);
+
+	if ($send_count > 1) {
+	    syslog(
+		'warning',
+		"Email has more than one address in its 'Sender' it won't be signed."
+		." See RFC 5322 Section 3.6.2"
+	    );
+	} elsif ($send_count == 1) {
+	    return $sender_list[0]->host();
+	} else {
+	    syslog(
+		'warning',
+		"Email has multiple addresses in its 'From' header and no 'Sender'"
+		." header, it won't be signed. See RFC 5322 Section 3.6.2"
+	    );
+	}
+    } elsif ($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 $prefer_rfc5322_domain = $dkim->{prefer_rfc5322_domain};
+    my $selector = $dkim->{selector};
 
     die "no selector provided\n" if ! $selector;
 
@@ -110,7 +168,7 @@ sub sign_entity {
 
     $signer->extended_headers($extended_headers);
 
-    if ($signer->signing_domain($sender)) {
+    if ($signer->signing_domain($sender, $entity, $prefer_rfc5322_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..f7502f2 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}->{prefer_rfc5322_domain} = $pmg_cfg->get('admin', 'dkim_prefer_rfc5322_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] 3+ messages in thread

* Re: [pmg-devel] [PATCH pmg-api v4] fix-2971: DKIM: Add setting to use From header when signing
  2024-01-16 10:43 [pmg-devel] [PATCH pmg-api v4] fix-2971: DKIM: Add setting to use From header when signing Maximiliano Sandoval
@ 2024-02-15 19:21 ` Stoiko Ivanov
  2024-02-16 12:36   ` Maximiliano Sandoval
  0 siblings, 1 reply; 3+ messages in thread
From: Stoiko Ivanov @ 2024-02-15 19:21 UTC (permalink / raw)
  To: Maximiliano Sandoval; +Cc: pmg-devel

Thanks for the update!

I got around to give it a spin in my testbed - and it looks ok from a
quick spin.

The docs-patch would be great for the next version!

a few comments inline:

On Tue, 16 Jan 2024 11:43:01 +0100
Maximiliano Sandoval <m.sandoval@proxmox.com> wrote:

> We add an option to use the address from the `From:` header instead of
> the Envelope From address. Following RFC 5322 [2], we use the `Sender:`
> header when there are multiple addresses in the `From:` header.
> 
> 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@bar1 --to EMAIL -s PMG_ADDR:26 --data "Date: %DATE%\nTo: %TO_ADDRESS%\nFrom: foo@bar2\nSubject: test %DATE%\nMessage-Id: <%MESSAGEID%>\nX-Mailer: swaks v%SWAKS_VERSION% jetmore.org/john/code/swaks/\n%NEW_HEADERS%\n%BODY%\n"
> 
> [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 v3:
>  - Take into account setting
>  - rename parsing function to parse_headers_for_signing
> 
>  src/PMG/Config.pm        |  7 ++++
>  src/PMG/DKIMSign.pm      | 72 ++++++++++++++++++++++++++++++++++++----
>  src/PMG/RuleDB/Accept.pm |  3 +-
>  src/PMG/RuleDB/BCC.pm    |  3 +-
>  src/bin/pmg-smtp-filter  |  1 +
>  5 files changed, 75 insertions(+), 11 deletions(-)
> 
> diff --git a/src/PMG/Config.pm b/src/PMG/Config.pm
> index 7339e0d..840c0ad 100644
> --- a/src/PMG/Config.pm
> +++ b/src/PMG/Config.pm
> @@ -134,6 +134,11 @@ EODESC
>  	    description => "Default DKIM selector",
>  	    type => 'string', format => 'dns-name', #see RFC6376 3.1
>  	},
> +	dkim_prefer_rfc5322_domain => {
I checked what others do for those hard-to-name things - maybe we could
orient ourselves on other mail-filters:
* https://rspamd.com/doc/modules/dkim_signing.html (dkim_use_domain a enum
  with "envelope" and "header") - one upside is that that will remain a
  term even if RFC5322 is obsoleted at some future point...
* opendkim seems to only use header information (but lets you select the
  headers, which are search, which I'd like to avoid for usage-simplicity)
  - http://opendkim.org/opendkim.conf.5.html


> +	    description => "Whether to use the address from the From header when DKIM signing outbound mails.",
> +	    type => 'boolean',
> +	    default => 0,
> +	},
>      };
>  }
>  
> @@ -152,6 +157,7 @@ sub options {
>  	dkim_sign => { optional => 1 },
>  	dkim_sign_all_mail => { optional => 1 },
>  	dkim_selector => { optional => 1 },
> +	dkim_prefer_rfc5322_domain => { optional => 1 },
>      };
>  }
>  
> @@ -1788,6 +1794,7 @@ my $pmg_service_params = {
>  	dkim_selector => 1,
>  	dkim_sign => 1,
>  	dkim_sign_all_mail => 1,
> +	dkim_prefer_rfc5322_domain => 1,
>      },
>  };
>  
> diff --git a/src/PMG/DKIMSign.pm b/src/PMG/DKIMSign.pm
> index 08197f8..2c73101 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, $prefer_rfc5322_domain) = @_;
> +
> +    my $input_domain;
> +    if ($prefer_rfc5322_domain) {
> +	$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,60 @@ sub signing_domain {
>  }
>  
>  
> +sub parse_headers_for_signing {
> +    # If there is exactly one single address in the 'From' header we use that
> +    # address for signing, otherwise we use the 'Sender' header.
as hinted in my reply to your v1:
https://lists.proxmox.com/pipermail/pmg-devel/2024-January/002625.html
if I read the DMARC-RFC right this fallback is not wanted there
(and DKIM itself does not mention that the dkim-domain (d= tag in the
signature header, would need to be particularly related to either header
or envelope-addresses)


> +    my ($entity) = @_;
> +
> +    my @sender_list;
> +    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) {
> +	my @send_headers = $entity->head->get('sender');
> +	foreach my $send_header (@send_headers) {
> +	    my @senders = Email::Address::XS->parse($send_header);
> +	    foreach my $sender (@senders) {
> +		push(@sender_list, $sender);
> +	    }
> +	}
> +	my $send_count = scalar(@sender_list);
> +
> +	if ($send_count > 1) {
> +	    syslog(
> +		'warning',
> +		"Email has more than one address in its 'Sender' it won't be signed."
> +		." See RFC 5322 Section 3.6.2"
> +	    );
> +	} elsif ($send_count == 1) {
> +	    return $sender_list[0]->host();
> +	} else {
> +	    syslog(
> +		'warning',
> +		"Email has multiple addresses in its 'From' header and no 'Sender'"
> +		." header, it won't be signed. See RFC 5322 Section 3.6.2"
> +	    );
> +	}
> +    } elsif ($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 $prefer_rfc5322_domain = $dkim->{prefer_rfc5322_domain};
> +    my $selector = $dkim->{selector};
>  
>      die "no selector provided\n" if ! $selector;
>  
> @@ -110,7 +168,7 @@ sub sign_entity {
>  
>      $signer->extended_headers($extended_headers);
>  
> -    if ($signer->signing_domain($sender)) {
> +    if ($signer->signing_domain($sender, $entity, $prefer_rfc5322_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..f7502f2 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}->{prefer_rfc5322_domain} = $pmg_cfg->get('admin', 'dkim_prefer_rfc5322_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] 3+ messages in thread

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


Stoiko Ivanov <s.ivanov@proxmox.com> writes:

>> +	dkim_prefer_rfc5322_domain => {
> I checked what others do for those hard-to-name things - maybe we could
> orient ourselves on other mail-filters:
> * https://rspamd.com/doc/modules/dkim_signing.html (dkim_use_domain a enum
>   with "envelope" and "header") - one upside is that that will remain a
>   term even if RFC5322 is obsoleted at some future point...
> * opendkim seems to only use header information (but lets you select the
>   headers, which are search, which I'd like to avoid for usage-simplicity)
>   - http://opendkim.org/opendkim.conf.5.html

👍 for using dkim_use_domain as a setting name with possible values
being 'envelope', 'header'. Will send v5 in a bit.

--
Maximiliano




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

end of thread, other threads:[~2024-02-16 12:39 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2024-01-16 10:43 [pmg-devel] [PATCH pmg-api v4] fix-2971: DKIM: Add setting to use From header when signing Maximiliano Sandoval
2024-02-15 19:21 ` Stoiko Ivanov
2024-02-16 12:36   ` Maximiliano Sandoval

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