svn commit: r1932331 - in spamassassin/trunk: . lib/Mail/SpamAssassin/Header lib/Mail/SpamAssassin/Plugin rules t t/data/arc t/data/dkim/arc

[email protected]
Newsgroups gmane.mail.spam.spamassassin.cvs
Message-ID <177365920345.3218395.18008155090872127781@svn02-us-east.apache.org>
Author: fkento
Date: Mon Mar 16 11:06:43 2026
New Revision: 1932331

Log:
Separate ARC into dedicated plugin from DKIM

  ARC (Authenticated Received Chain, RFC 8617) verification was previously
  embedded in the DKIM plugin. This moves it to a standalone
  Mail::SpamAssassin::Plugin::ARC with its own configuration and rules.

  New plugin features:
  - arc_trusted_sealers config to specify trusted ARC sealer domains
  - Parses ARC-Authentication-Results headers from trusted sealers
  - check_arc_trusted eval rule for messages with trusted ARC results
  - Runs in parsed_metadata phase so results are available to other plugins

  New Mail::SpamAssassin::Header::ArcAuthenticationResults class extends
  AuthenticationResults to handle the i= instance index prefix in AAR
  headers.

  DMARC plugin updated to use parsed AAR results: when DMARC fails and a
  trusted AAR contains dmarc=pass, the result is overridden directly
  instead of reconstructing validation from SPF+DKIM components.

Added:
   spamassassin/trunk/lib/Mail/SpamAssassin/Header/ArcAuthenticationResults.pm
   spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/ARC.pm
   spamassassin/trunk/rules/25_arc.cf
   spamassassin/trunk/t/arc_authentication_results.t
   spamassassin/trunk/t/data/arc/
   spamassassin/trunk/t/data/arc/ko01.eml
   spamassassin/trunk/t/data/arc/ok01.eml
Deleted:
   spamassassin/trunk/t/data/dkim/arc/
Modified:
   spamassassin/trunk/MANIFEST
   spamassassin/trunk/UPGRADE
   spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/DKIM.pm
   spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/DMARC.pm
   spamassassin/trunk/rules/25_dkim.cf
   spamassassin/trunk/rules/50_scores.cf
   spamassassin/trunk/rules/v403.pre
   spamassassin/trunk/t/arc.t
   spamassassin/trunk/t/config.dist
   spamassassin/trunk/t/debug.t
   spamassassin/trunk/t/plugin_file.t

Modified: spamassassin/trunk/MANIFEST
==============================================================================
--- spamassassin/trunk/MANIFEST	Mon Mar 16 08:30:15 2026	(r1932330)
+++ spamassassin/trunk/MANIFEST	Mon Mar 16 11:06:43 2026	(r1932331)
@@ -51,6 +51,7 @@ lib/Mail/SpamAssassin/Dns.pm
 lib/Mail/SpamAssassin/DnsResolver.pm
 lib/Mail/SpamAssassin/GeoDB.pm
 lib/Mail/SpamAssassin/Header.pm
+lib/Mail/SpamAssassin/Header/ArcAuthenticationResults.pm
 lib/Mail/SpamAssassin/Header/AuthenticationResults.pm
 lib/Mail/SpamAssassin/Header/ParameterHeader.pm
 lib/Mail/SpamAssassin/HTML.pm
@@ -85,6 +86,7 @@ lib/Mail/SpamAssassin/Plugin/Bayes.pm
 lib/Mail/SpamAssassin/Plugin/BodyEval.pm
 lib/Mail/SpamAssassin/Plugin/BodyRuleBaseExtractor.pm
 lib/Mail/SpamAssassin/Plugin/Check.pm
+lib/Mail/SpamAssassin/Plugin/ARC.pm
 lib/Mail/SpamAssassin/Plugin/DCC.pm
 lib/Mail/SpamAssassin/Plugin/DecodeShortURLs.pm
 lib/Mail/SpamAssassin/Plugin/DKIM.pm
@@ -294,11 +296,12 @@ t/cpp_comments_in_spamc.t
 t/cross_user_config_leak.t
 t/dmarc.t
 t/arc.t
+t/arc_authentication_results.t
 t/data/01_test_rules.cf
 t/data/01_test_rules.pre
 t/data/Dumpheaders.pm
-t/data/dkim/arc/ok01.eml
-t/data/dkim/arc/ko01.eml
+t/data/arc/ko01.eml
+t/data/arc/ok01.eml
 t/data/dkim/test-adsp-11.msg
 t/data/dkim/test-adsp-12.msg
 t/data/dkim/test-adsp-13.msg

Modified: spamassassin/trunk/UPGRADE
==============================================================================
--- spamassassin/trunk/UPGRADE	Mon Mar 16 08:30:15 2026	(r1932330)
+++ spamassassin/trunk/UPGRADE	Mon Mar 16 11:06:43 2026	(r1932331)
@@ -1,6 +1,18 @@
 Note for Users Upgrading to SpamAssassin 4.0.3
 ----------------------------------------------
 
+- ARC (Authenticated Received Chain) verification has been separated from
+  the DKIM plugin into its own plugin: Mail::SpamAssassin::Plugin::ARC.
+  It is loaded by default from v403.pre.
+
+  To use ARC authentication results as a DMARC fallback, you must
+  configure at least one trusted ARC sealer domain:
+
+    arc_trusted_sealers google.com microsoft.com yahoo.com
+
+  Without this setting, ARC signatures are verified but
+  ARC-Authentication-Results headers are not parsed.
+
 - Mail::SpamAssassin::Plugin::SPF no longer parses Authentication-Results
   headers directly. It now uses results from
   Mail::SpamAssassin::Plugin::AuthRes if available, falling back to

Added: spamassassin/trunk/lib/Mail/SpamAssassin/Header/ArcAuthenticationResults.pm
==============================================================================
--- /dev/null	00:00:00 1970	(empty, because file is newly added)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Header/ArcAuthenticationResults.pm	Mon Mar 16 11:06:43 2026	(r1932331)
@@ -0,0 +1,79 @@
+# <@LICENSE>
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to you under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# </@LICENSE>
+
+package Mail::SpamAssassin::Header::ArcAuthenticationResults;
+use strict;
+use warnings FATAL => 'all';
+
+use Mail::SpamAssassin::Header::AuthenticationResults;
+
+use parent qw(Mail::SpamAssassin::Header::AuthenticationResults);
+
+=head1 NAME
+
+Mail::SpamAssassin::Header::ArcAuthenticationResults - parser for ARC-Authentication-Results headers
+
+=head1 SYNOPSIS
+
+    my $aar = Mail::SpamAssassin::Header::ArcAuthenticationResults->new($hdr_value);
+    print $aar->arc_index();     # 1
+    print $aar->authserv_id();   # 'mx.example.com'
+    my @names = $aar->methods(); # ('spf', 'dkim', ...)
+
+=head1 DESCRIPTION
+
+This class inherits from AuthenticationResults to parse
+ARC-Authentication-Results header fields per RFC 8617.  The only
+difference from a standard Authentication-Results header is the
+leading C<i=N;> tag that identifies the ARC instance index.
+
+=head1 METHODS
+
+=over 4
+
+=item new($value)
+
+Creates a new instance, parsing the given raw header value.  The C<i=N;>
+prefix is extracted and stored before delegating to the parent parser.
+
+=cut
+
+sub new {
+    my ($class, $value) = @_;
+    # Extract i=N; prefix before parent parsing
+    my $arc_index;
+    if ($value =~ s/^\s*i=(\d+)\s*;\s*//) {
+        $arc_index = $1;
+    }
+    my $self = $class->SUPER::new($value);
+    $self->{arc_index} = $arc_index;
+    return $self;
+}
+
+=item arc_index()
+
+Returns the ARC instance index (the C<i=> tag value).
+
+=cut
+
+sub arc_index { $_[0]->{arc_index} }
+
+=back
+
+=cut
+
+1;

Added: spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/ARC.pm
==============================================================================
--- /dev/null	00:00:00 1970	(empty, because file is newly added)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/ARC.pm	Mon Mar 16 11:06:43 2026	(r1932331)
@@ -0,0 +1,470 @@
+# <@LICENSE>
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to you under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# </@LICENSE>
+
+=head1 NAME
+
+Mail::SpamAssassin::Plugin::ARC - perform ARC verification tests
+
+=head1 SYNOPSIS
+
+ loadplugin Mail::SpamAssassin::Plugin::ARC [/path/to/ARC.pm]
+
+ full   ARC_SIGNED            eval:check_arc_signed()
+ full   ARC_VALID             eval:check_arc_valid()
+ full   ARC_TRUSTED           eval:check_arc_trusted()
+
+=head1 DESCRIPTION
+
+This SpamAssassin plugin implements ARC (Authenticated Received Chain)
+verification as described by RFC 8617.
+
+It requires the C<Mail::DKIM> CPAN module version 0.50 or later to operate,
+specifically the C<Mail::DKIM::ARC::Verifier> module.
+
+=head1 SEE ALSO
+
+C<Mail::DKIM> Mail::SpamAssassin::Plugin(3)
+
+  https://www.rfc-editor.org/rfc/rfc8617
+
+=cut
+
+package Mail::SpamAssassin::Plugin::ARC;
+
+use Mail::SpamAssassin::Plugin;
+use Mail::SpamAssassin::Logger;
+use Mail::SpamAssassin::Timeout;
+use Mail::SpamAssassin::Header::ArcAuthenticationResults;
+use version;
+
+use strict;
+use warnings;
+use re 'taint';
+
+our @ISA = qw(Mail::SpamAssassin::Plugin);
+
+sub new {
+  my $class = shift;
+  my $mailsaobject = shift;
+
+  $class = ref($class) || $class;
+  my $self = $class->SUPER::new($mailsaobject);
+  bless ($self, $class);
+
+  $self->register_eval_rule("check_arc_signed", $Mail::SpamAssassin::Conf::TYPE_FULL_EVALS);
+  $self->register_eval_rule("check_arc_valid", $Mail::SpamAssassin::Conf::TYPE_FULL_EVALS);
+  $self->register_eval_rule("check_arc_trusted", $Mail::SpamAssassin::Conf::TYPE_FULL_EVALS);
+
+  # run before DKIM, SPF, DMARC so arc_auth_results is available
+  $self->register_method_priority("parsed_metadata", -20);
+
+  $self->set_config($mailsaobject->{conf});
+
+  return $self;
+}
+
+###########################################################################
+
+sub set_config {
+  my($self, $conf) = @_;
+  my @cmds;
+
+=head1 USER SETTINGS
+
+=over 4
+
+=item arc_timeout n             (default: 5)
+
+Timeout in seconds for ARC signature verification. If Mail::DKIM cannot
+complete verification within this time, the ARC check will be aborted.
+
+=item arc_trusted_sealers domain1 domain2 ...
+
+Specify domains that are trusted as ARC sealers.  When the ARC chain is
+cryptographically valid, the plugin will parse ARC-Authentication-Results
+headers from trusted sealers and make them available to other plugins
+(such as DMARC) via C<$pms-E<gt>{arc_auth_results}>.  ARC instances sealed
+by untrusted domains are ignored.
+
+If no trusted sealers are configured, ARC-Authentication-Results headers
+will not be parsed.
+
+Can be specified multiple times, additional entries are appended.
+
+  arc_trusted_sealers google.com microsoft.com
+  arc_trusted_sealers yahoo.com
+
+=back
+
+=cut
+
+  push(@cmds, {
+    setting => 'arc_timeout',
+    default => 5,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC,
+  });
+
+  push(@cmds, {
+    setting => 'arc_trusted_sealers',
+    default => {},
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_HASH_KEY_VALUE,
+    code => sub {
+      my ($self, $key, $value, $line) = @_;
+      if (!defined $value || $value =~ /^$/) {
+        return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
+      }
+      foreach my $domain (split(/\s+/, lc $value)) {
+        $self->{arc_trusted_sealers}->{$domain} = 1;
+      }
+    }
+  });
+
+  $conf->{parser}->register_commands(\@cmds);
+}
+
+###########################################################################
+
+sub parsed_metadata {
+  my ($self, $opts) = @_;
+  my $pms = $opts->{permsgstatus};
+
+  my(@arc_signatures, @arc_valid_signatures);
+
+  $pms->{arc_signatures_ready} = 0;
+  $pms->{arc_signatures_dependable} = 0;
+  $pms->{arc_signatures} = \@arc_signatures;
+  $pms->{arc_valid_signatures} = \@arc_valid_signatures;
+  $pms->{arc_signed} = 0;
+  $pms->{arc_valid} = 0;
+
+  my $suppl_attrib = $pms->{msg}->{suppl_attrib};
+  if (defined $suppl_attrib && exists $suppl_attrib->{arc_signatures}) {
+    my $provided_arc_signatures = $suppl_attrib->{arc_signatures};
+    if (ref $provided_arc_signatures) {
+      @arc_signatures = @$provided_arc_signatures;
+      $pms->{arc_signatures_ready} = 1;
+      $pms->{arc_signatures_dependable} = 1;
+      dbg("arc: ARC signatures provided by the caller, %d signatures",
+          scalar(@arc_signatures));
+    }
+  }
+
+  if ($pms->{arc_signatures_ready}) {
+    $self->_check_arc_valid_signature($pms, \@arc_signatures);
+  } elsif (!$pms->is_dns_available()) {
+    dbg("arc: signature verification disabled, DNS resolving not available");
+  } elsif (!$self->_arc_load_modules()) {
+    # Mail::DKIM::ARC module not available
+  } else {
+    my $timemethod = $self->{main}->time_method("check_arc_signature");
+    my $arc_verifier = Mail::DKIM::ARC::Verifier->new;
+    $self->_check_signature($pms, $arc_verifier, \@arc_signatures);
+  }
+}
+
+sub check_arc_signed {
+  my ($self, $pms, $full_ref, @acceptable_domains) = @_;
+  my $result = 0;
+  if (!$pms->{arc_signed}) {
+    # don't bother
+  } elsif (!@acceptable_domains) {
+    $result = 1;  # no additional constraints, any signing domain will do
+  }
+  return $result;
+}
+
+sub check_arc_valid {
+  my ($self, $pms, $full_ref, @acceptable_domains) = @_;
+  my $result = 0;
+  if (!$pms->{arc_valid}) {
+    # don't bother
+  } elsif (!@acceptable_domains) {
+    $result = 1;  # no additional constraints, any signing domain will do,
+                  # also any signing key size will do
+  }
+  return $result;
+}
+
+sub check_arc_trusted {
+  my ($self, $pms) = @_;
+  return $pms->{arc_auth_results} ? 1 : 0;
+}
+
+# ---------------------------------------------------------------------------
+
+sub _arc_load_modules {
+  my ($self) = @_;
+
+  if (!$self->{tried_loading}) {
+    $self->{service_available} = 0;
+    my $timemethod = $self->{main}->time_method("arc_load_modules");
+    my $eval_stat;
+    eval {
+      { require Mail::DKIM::ARC::Verifier }
+      1;
+    } or do {
+      $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
+    };
+    $self->{tried_loading} = 1;
+
+    if (defined $eval_stat) {
+      dbg("arc: cannot load Mail::DKIM::ARC::Verifier module, ARC checks disabled: %s",
+          $eval_stat);
+    } else {
+      my $version = Mail::DKIM::ARC::Verifier->VERSION;
+      dbg("arc: using Mail::DKIM::ARC::Verifier version $version");
+      if (version->parse($version) >= version->parse(0.40)) {
+        # Let Mail::DKIM use our interface to Net::DNS::Resolver.
+        my $res = $self->{main}->{resolver};
+        dbg("arc: providing our own resolver: %s", ref $res);
+        Mail::DKIM::DNS::resolver($res);
+      }
+      $self->{service_available} = 1;
+    }
+  }
+  return $self->{service_available};
+}
+
+sub _check_signature {
+  my($self, $pms, $verifier, $signatures) = @_;
+
+  my $conf = $pms->{conf};
+  if (!$verifier) {
+    dbg("arc: cannot create Mail::DKIM::ARC::Verifier object");
+    return;
+  }
+  $pms->{arc_verifier} = $verifier;
+
+  eval {
+    my $str = $pms->{msg}->get_pristine();
+    if ($pms->{msg}->{line_ending} eq "\015\012") {
+      $verifier->PRINT($str);
+    } else {
+      $str =~ s/\012/\015\012/gs;
+      $verifier->PRINT($str);
+      undef $str;
+    }
+    1;
+  } or do {
+    my $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
+    dbg("arc: verification failed, intercepted error: $eval_stat");
+    return 0;
+  };
+
+  my $timeout = $conf->{arc_timeout};
+  my $timer = Mail::SpamAssassin::Timeout->new(
+                { secs => $timeout, deadline => $pms->{master_deadline} });
+
+  my $err = $timer->run_and_catch(sub {
+    dbg("arc: performing public ARC key lookup and signature verification");
+    $verifier->CLOSE();
+
+    @$signatures = $verifier->UNIVERSAL::can("signatures") ?
+                               $verifier->signatures : $verifier->signature;
+    if (would_log("dbg","arc")) {
+      foreach my $signature (@$signatures) {
+        dbg("arc: ARC signature i=%s d=%s",
+          map(!defined $_ ? '(undef)' : $_,
+            $signature->identity, $signature->domain
+          )
+        );
+      }
+    }
+  });
+  if ($timer->timed_out()) {
+    dbg("arc: public key lookup or verification timed out after %s s",
+        $timeout);
+  } elsif ($err) {
+    chomp $err;
+    dbg("arc: ARC public key lookup or verification failed: $err");
+  }
+
+  $pms->{arc_signatures_ready} = 1;
+  if (!@$signatures || !$pms->{tests_already_hit}->{'__TRUNCATED'}) {
+    $pms->{arc_signatures_dependable} = 1;
+  }
+  $self->_check_arc_valid_signature($pms, \@$signatures);
+}
+
+sub _check_arc_valid_signature {
+  my($self, $pms, $signatures) = @_;
+
+  my(@valid_signatures);
+  my $conf = $pms->{conf};
+  my @arc_sig;
+
+  if ($pms->{arc_signatures_ready}) {
+    foreach my $signature (@$signatures) {
+      next if !defined $signature;
+      next if !defined $signature->selector || $signature->selector eq "";
+
+      my($info, $valid, $expired);
+      $valid = $signature->result eq 'pass';
+      $info = $valid ? 'VALID' : 'FAILED';
+      if ($valid && $signature->UNIVERSAL::can("check_expiration")) {
+        $expired = !$signature->check_expiration;
+        $info .= ' EXPIRED' if $expired;
+      }
+
+      my %arc;
+      $arc{prefix} = $signature->prefix;
+      $arc{valid} = $valid;
+      push(@arc_sig, \%arc);
+
+      push(@valid_signatures, $signature) if $valid && !$expired;
+
+      if (would_log("dbg","arc")) {
+        my ($d) = (defined $signature->identity) ? $signature->identity =~ /\@(\S+)/ : ($signature->domain);
+        $d = defined $d ? lc $d : undef;
+        dbg("arc: %s ARC, i=%s, d=%s, s=%s, a=%s, c=%s, %s",
+          $info,
+          map(!defined $_ ? '(undef)' : $_,
+            $signature->identity, $d, $signature->selector,
+            $signature->algorithm, scalar($signature->canonicalization),
+            $signature->result),
+        );
+      }
+    }
+
+    if (@valid_signatures) {
+      $pms->{arc_signed} = 1;
+      my $arc_seal_valid = 0;
+      my $arc_message_valid = 0;
+      my $arc_message_found = 0;
+      # All ARC-Seals signatures and the most recent ARC-Message-Signature must be valid
+      foreach my $arc ( @arc_sig ) {
+        if ($arc->{prefix} eq 'ARC-Message-Signature:') {
+          next if $arc_message_found;
+          if ($arc->{valid}) {
+            $arc_message_valid = 1;
+            dbg("arc: valid ARC-Message-Signature signature");
+          } else {
+            dbg("arc: invalid ARC-Message-Signature signature");
+          }
+          $arc_message_found = 1;
+        }
+        if (($arc->{prefix} eq 'ARC-Seal:') and $arc->{valid}) {
+          $arc_seal_valid = 1;
+          dbg("arc: valid ARC-Seal signature");
+        } elsif (($arc->{prefix} eq 'ARC-Seal:') and not $arc->{valid}) {
+          $arc_seal_valid = 0;
+          dbg("arc: invalid ARC-Seal signature");
+          last;
+        }
+      }
+      if ($arc_message_valid and $arc_seal_valid) {
+        $pms->{arc_valid} = 1;
+        $self->_parse_trusted_aar($pms);
+      }
+      my $sig = $valid_signatures[0];
+      my $sig_res = $sig->result_detail;
+      dbg("arc: ARC signature verification result: %s", uc($sig_res));
+
+    } elsif (@$signatures) {
+      $pms->{arc_signed} = 1;
+      my $sig = @$signatures[0];
+      my $sig_res = $sig->result_detail;
+      dbg("arc: ARC signature verification result: %s", uc($sig_res));
+
+    } else {
+      dbg("arc: ARC signature verification result: none");
+    }
+  }
+}
+
+sub _parse_trusted_aar {
+  my ($self, $pms) = @_;
+
+  my $trusted = $pms->{conf}->{arc_trusted_sealers};
+  return if !$trusted || !%$trusted;
+
+  my $verifier = $pms->{arc_verifier};
+  return if !ref($verifier) || !defined $verifier->{seals};
+
+  # Build set of ARC instance indices with trusted seal domains
+  my %trusted_indices;
+  foreach my $seal (@{$verifier->{seals}}) {
+    my $d = $seal->{tags_by_name}{d}{value};
+    my $i = $seal->{tags_by_name}{i}{value};
+    next if !defined $d || !defined $i;
+    if ($trusted->{lc $d}) {
+      $trusted_indices{$i} = 1;
+      dbg("arc: seal i=%s d=%s is trusted", $i, $d);
+    } else {
+      dbg("arc: seal i=%s d=%s is not trusted", $i, $d);
+    }
+  }
+  return if !%trusted_indices;
+
+  # Parse ARC-Authentication-Results headers from trusted sealers
+  my @aar = $pms->{msg}->get_pristine_header('ARC-Authentication-Results');
+  return if !@aar;
+
+  my @arc_auth_results;
+  foreach my $hdr (@aar) {
+    chomp $hdr;
+
+    my $ar = eval {
+      Mail::SpamAssassin::Header::ArcAuthenticationResults->new($hdr);
+    };
+    if ($@ || !$ar) {
+      dbg("arc: failed to parse AAR header: %s", $@ || 'unknown error');
+      next;
+    }
+
+    my $arc_index = $ar->arc_index();
+    if (!defined $arc_index) {
+      dbg("arc: AAR header missing i= tag, skipping");
+      next;
+    }
+
+    if (!$trusted_indices{$arc_index}) {
+      dbg("arc: AAR i=%s not from trusted sealer, skipping", $arc_index);
+      next;
+    }
+
+    my $authserv = $ar->authserv_id();
+    my %results;
+    foreach my $method ($ar->methods()) {
+      foreach my $m ($ar->method($method)) {
+        $results{$method} = {
+          result     => $m->{result},
+          reason     => $m->{reason},
+          properties => $m->{properties},
+        };
+      }
+    }
+
+    push @arc_auth_results, {
+      arc_index => $arc_index,
+      authserv  => $authserv,
+      results   => \%results,
+    };
+
+    dbg("arc: parsed trusted AAR i=%s authserv=%s methods=%s",
+        $arc_index, $authserv, join(' ', sort keys %results));
+  }
+
+  if (@arc_auth_results) {
+    @arc_auth_results = sort { $a->{arc_index} <=> $b->{arc_index} }
+                        @arc_auth_results;
+    $pms->{arc_auth_results} = \@arc_auth_results;
+  }
+}
+
+1;
+

Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/DKIM.pm
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/DKIM.pm	Mon Mar 16 08:30:15 2026	(r1932330)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/DKIM.pm	Mon Mar 16 11:06:43 2026	(r1932331)
@@ -30,12 +30,6 @@ Taking into account signatures from any
  full   DKIM_VALID_AU         eval:check_dkim_valid_author_sig()
  full   DKIM_VALID_EF         eval:check_dkim_valid_envelopefrom()
 
-Taking into account ARC signatures (Authenticated Received Chain, RFC 8617)
-from any signing domains:
-
- full   ARC_SIGNED            eval:check_arc_signed()
- full   ARC_VALID             eval:check_arc_valid()
-
 Taking into account signatures from specified signing domains only:
 (quotes may be omitted on domain names consisting only of letters, digits,
 dots, and minus characters)
@@ -154,9 +148,7 @@ sub new {
 
   # signatures
   $self->register_eval_rule("check_dkim_signed", $Mail::SpamAssassin::Conf::TYPE_FULL_EVALS);
-  $self->register_eval_rule("check_arc_signed", $Mail::SpamAssassin::Conf::TYPE_FULL_EVALS);
   $self->register_eval_rule("check_dkim_valid", $Mail::SpamAssassin::Conf::TYPE_FULL_EVALS);
-  $self->register_eval_rule("check_arc_valid", $Mail::SpamAssassin::Conf::TYPE_FULL_EVALS);
   $self->register_eval_rule("check_dkim_valid_author_sig", $Mail::SpamAssassin::Conf::TYPE_FULL_EVALS);
   $self->register_eval_rule("check_dkim_testing", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
   $self->register_eval_rule("check_dkim_valid_envelopefrom", $Mail::SpamAssassin::Conf::TYPE_FULL_EVALS);
@@ -407,7 +399,6 @@ prepend its own signature on a copy of s
 which makes it no more trustworthy than without such signature. This is also
 a reason for a rule DKIM_VALID to have a near-zero score, i.e. a rule hit
 is only informational.
-This option is evaluated on ARC signatures checks as well.
 
 =cut
 
@@ -552,18 +543,6 @@ sub check_dkim_signed {
   return $result;
 }
 
-sub check_arc_signed {
-  my ($self, $pms, $full_ref, @acceptable_domains) = @_;
-  $self->_check_arc_signature($pms)  if !$pms->{arc_checked_signature};
-  my $result = 0;
-  if (!$pms->{arc_signed}) {
-    # don't bother
-  } elsif (!@acceptable_domains) {
-    $result = 1;  # no additional constraints, any signing domain will do
-  }
-  return $result;
-}
-
 sub check_dkim_valid {
   my ($self, $pms, $full_ref, @acceptable_domains) = @_;
   $self->_check_dkim_signature($pms)  if !$pms->{dkim_checked_signature};
@@ -579,19 +558,6 @@ sub check_dkim_valid {
   return $result;
 }
 
-sub check_arc_valid {
-  my ($self, $pms, $full_ref, @acceptable_domains) = @_;
-  $self->_check_arc_signature($pms)  if !$pms->{arc_checked_signature};
-  my $result = 0;
-  if (!$pms->{arc_valid}) {
-    # don't bother
-  } elsif (!@acceptable_domains) {
-    $result = 1;  # no additional constraints, any signing domain will do,
-                  # also any signing key size will do
-  }
-  return $result;
-}
-
 sub check_dkim_valid_author_sig {
   my ($self, $pms, $full_ref, @acceptable_domains) = @_;
   $self->_check_dkim_signature($pms)  if !$pms->{dkim_checked_signature};
@@ -734,9 +700,6 @@ sub _dkim_load_modules {
       my $version = Mail::DKIM::Verifier->VERSION;
       if (version->parse($version) >= version->parse(0.31)) {
         dbg("dkim: using Mail::DKIM version $version");
-      } elsif (version->parse($version) < version->parse(0.50)) {
-        dbg("dkim: Mail::DKIM $version is older than 0.50 ".
-             "ARC support will not be available, suggested upgrade to 0.50 or later!");
       } else {
         info("dkim: Mail::DKIM $version is older than the required ".
              "minimal version 0.31, suggested upgrade to 0.37 or later!");
@@ -755,18 +718,6 @@ sub _dkim_load_modules {
         eval { require Mail::DKIM::DkimPolicy }  # ignoring status
       }
     }
-    eval {
-      # Have to do this so that RPM doesn't find these as required perl modules.
-      { require Mail::DKIM::ARC::Verifier }
-      $self->{arc_available} = 1;
-    } or do {
-      $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
-      if (defined $eval_stat) {
-        dbg("dkim: cannot load Mail::DKIM::ARC module, DKIM::ARC checks disabled: %s",
-          $eval_stat);
-      }
-      $self->{arc_available} = 0;
-    };
   }
   return $self->{service_available};
 }
@@ -867,7 +818,7 @@ sub _check_dkim_signature {
 
   if ($pms->{dkim_signatures_ready}) {
     # signatures already available and verified
-    _check_valid_signature($self, $pms, 'DKIM', \@signatures);
+    _check_valid_signature($self, $pms, \@signatures);
   } elsif (!$pms->is_dns_available()) {
     dbg("dkim: signature verification disabled, DNS resolving not available");
   } elsif (!$self->_dkim_load_modules()) {
@@ -876,76 +827,22 @@ sub _check_dkim_signature {
     # signature objects not provided by the caller, must verify for ourselves
     my $timemethod = $self->{main}->time_method("check_dkim_signature");
     my $verifier = Mail::DKIM::Verifier->new;
-    _check_signature($self, $pms, $verifier, 'DKIM', \@signatures);
-  }
-}
-
-sub _check_arc_signature {
-  my ($self, $pms) = @_;
-
-  my(@arc_signatures, @arc_valid_signatures);
-
-  $pms->{arc_checked_signature} = 1;  # has this sub already been invoked?
-  $pms->{arc_signatures_ready} = 0;  # have we obtained & verified signatures?
-  $pms->{arc_signatures_dependable} = 0;
-  # arc_signatures_dependable =
-  #   (signatures supplied by a caller) or
-  #   ( (signatures obtained by this plugin) and
-  #     (no signatures, or message was not truncated) )
-  $pms->{arc_signatures} = \@arc_signatures;
-  $pms->{arc_valid_signatures} = \@arc_valid_signatures;
-  $pms->{arc_signed} = 0;
-  $pms->{arc_valid} = 0;
-
-  my $suppl_attrib = $pms->{msg}->{suppl_attrib};
-  if (defined $suppl_attrib && exists $suppl_attrib->{arc_signatures}) {
-    # caller of SpamAssassin already supplied ARC signature objects
-    my $provided_arc_signatures = $suppl_attrib->{arc_signatures};
-    if (ref $provided_arc_signatures) {
-      @arc_signatures = @$provided_arc_signatures;
-      $pms->{arc_signatures_ready} = 1;
-      $pms->{arc_signatures_dependable} = 1;
-      dbg("dkim: ARC signatures provided by the caller, %d signatures",
-          scalar(@arc_signatures));
-    }
-  }
-
-  if ($pms->{arc_signatures_ready}) {
-    # signatures already available and verified
-    _check_valid_signature($self, $pms, 'ARC', \@arc_signatures);
-  } elsif (!$pms->is_dns_available()) {
-    dbg("dkim: signature verification disabled, DNS resolving not available");
-  } elsif (!$self->_dkim_load_modules() || !$self->{arc_available}) {
-    # Mail::DKIM::ARC module not available
-  } else {
-    # signature objects not provided by the caller, must verify for ourselves
-    my $timemethod = $self->{main}->time_method("check_arc_signature");
-    my $arc_verifier = Mail::DKIM::ARC::Verifier->new;
-    _check_signature($self, $pms, $arc_verifier, 'ARC', \@arc_signatures);
+    _check_signature($self, $pms, $verifier, \@signatures);
   }
 }
 
 sub _check_signature {
-  my($self, $pms, $verifier, $type, $signatures) = @_;
+  my($self, $pms, $verifier, $signatures) = @_;
 
   $self->_get_authors($pms)  if !$pms->{"dkim_author_addresses"};
 
   my(@valid_signatures);
   my $conf = $pms->{conf};
   if (!$verifier) {
-    if ($type eq 'DKIM') {
-      dbg("dkim: cannot create Mail::DKIM::Verifier object");
-    } elsif ($type eq 'ARC') {
-      dbg("dkim: cannot create Mail::DKIM::ARC::Verifier object");
-    }
+    dbg("dkim: cannot create Mail::DKIM::Verifier object");
     return;
-  } else {
-    if ($type eq 'DKIM') {
-      $pms->{dkim_verifier} = $verifier;
-    } elsif ($type eq 'ARC') {
-      $pms->{arc_verifier} = $verifier;
-    }
   }
+  $pms->{dkim_verifier} = $verifier;
   # feed content of a message into verifier, using \r\n endings,
   # required by Mail::DKIM API (see bug 5300)
   # note: bug 5179 comment 28: perl does silly things on non-Unix platforms
@@ -973,7 +870,7 @@ sub _check_signature {
                 { secs => $timeout, deadline => $pms->{master_deadline} });
 
   my $err = $timer->run_and_catch(sub {
-    dbg("dkim: performing public $type key lookup and signature verification");
+    dbg("dkim: performing public DKIM key lookup and signature verification");
     $verifier->CLOSE();  # the action happens here
 
     # currently SpamAssassin's parsing is better than Mail::Address parsing,
@@ -986,7 +883,7 @@ sub _check_signature {
                                $verifier->signatures : $verifier->signature;
     if (would_log("dbg","dkim")) {
       foreach my $signature (@$signatures) {
-        dbg("dkim: $type signature i=%s d=%s",
+        dbg("dkim: DKIM signature i=%s d=%s",
           map(!defined $_ ? '(undef)' : $_,
             $signature->identity, $signature->domain
           )
@@ -1002,36 +899,24 @@ sub _check_signature {
 
   } elsif ($err) {
     chomp $err;
-    dbg("dkim: $type public key lookup or verification failed: $err");
+    dbg("dkim: DKIM public key lookup or verification failed: $err");
   }
-  if ($type eq 'DKIM') {
-    $pms->{dkim_signatures_ready} = 1;
-    if (!@$signatures || !$pms->{tests_already_hit}->{'__TRUNCATED'}) {
-      $pms->{dkim_signatures_dependable} = 1;
-    }
-    _check_valid_signature($self, $pms, 'DKIM', \@$signatures);
-  } elsif ($type eq 'ARC') {
-    $pms->{arc_signatures_ready} = 1;
-    if (!@$signatures || !$pms->{tests_already_hit}->{'__TRUNCATED'}) {
-      $pms->{arc_signatures_dependable} = 1;
-    }
-    _check_valid_signature($self, $pms, 'ARC', \@$signatures);
+  $pms->{dkim_signatures_ready} = 1;
+  if (!@$signatures || !$pms->{tests_already_hit}->{'__TRUNCATED'}) {
+    $pms->{dkim_signatures_dependable} = 1;
   }
+  _check_valid_signature($self, $pms, \@$signatures);
 }
 
 sub _check_valid_signature {
-  my($self, $pms, $type, $signatures) = @_;
+  my($self, $pms, $signatures) = @_;
 
-  my $sig_type = lc $type;
   $self->_get_authors($pms)  if !$pms->{"dkim_author_addresses"};
 
   my(@valid_signatures);
   my $conf = $pms->{conf};
-  my @arc_sig;
-  # DKIM signatures check
-  if ($pms->{"${sig_type}_signatures_ready"}) {
-    my $sig_result_supported;
-    # dkim_minimum_key_bits is evaluated for ARC signatures as well
+
+  if ($pms->{dkim_signatures_ready}) {
     my $minimum_key_bits = $conf->{dkim_minimum_key_bits};
     foreach my $signature (@$signatures) {
       # old versions of Mail::DKIM would give undef for an invalid signature
@@ -1055,12 +940,6 @@ sub _check_valid_signature {
           $info .= " WEAK($key_size)"  if $key_size < $minimum_key_bits;
         }
       }
-      if($type eq 'ARC') {
-        my %arc;
-        $arc{prefix} = $signature->prefix;
-        $arc{valid} = $valid;
-        push(@arc_sig, \%arc);
-      }
       push(@valid_signatures, $signature)  if $valid && !$expired;
 
       # check if we have a potential Author Domain Signature, valid or not
@@ -1070,115 +949,64 @@ sub _check_valid_signature {
       } else {
         $d = lc $d;
         if ($pms->{dkim_author_domains}->{$d}) {  # SDID matches author domain
-          $pms->{"${sig_type}_has_any_author_sig"}->{$d} = 1;
+          $pms->{dkim_has_any_author_sig}->{$d} = 1;
           if ($valid && !$expired &&
               $key_size && $key_size >= $minimum_key_bits) {
-            $pms->{"${sig_type}_has_valid_author_sig"}->{$d} = 1;
+            $pms->{dkim_has_valid_author_sig}->{$d} = 1;
           } elsif ( $signature->result_detail
                    =~ /\b(?:timed out|SERVFAIL)\b/i) {
-            $pms->{"${sig_type}_author_sig_tempfailed"}->{$d} = 1;
+            $pms->{dkim_author_sig_tempfailed}->{$d} = 1;
           }
         }
       }
-      if ($type eq 'DKIM') {
-        if (would_log("dbg","dkim")) {
-          dbg("dkim: %s %s, i=%s, d=%s, s=%s, a=%s, c=%s, %s, %s, %s",
-            $info,
-            $signature->isa('Mail::DKIM::DkSignature') ? 'DK' : 'DKIM',
-            map(!defined $_ ? '(undef)' : $_,
-              $signature->identity, $d, $signature->selector,
-              $signature->algorithm, scalar($signature->canonicalization),
-              $key_size ? "key_bits=$key_size" : "unknown key size",
-              $signature->result ),
-            defined $d && $pms->{dkim_author_domains}->{$d}
-              ? 'matches author domain'
-              : 'does not match author domain',
-          );
-        }
-      } elsif ($type eq 'ARC') {
-        if (would_log("dbg","dkim")) {
-          dbg("dkim: %s %s, i=%s, d=%s, s=%s, a=%s, c=%s, %s, %s, %s",
-            $info,
-            $type,
-            map(!defined $_ ? '(undef)' : $_,
-              $signature->identity, $d, $signature->selector,
-              $signature->algorithm, scalar($signature->canonicalization),
-              $key_size ? "key_bits=$key_size" : "unknown key size",
-              $signature->result ),
-            defined $d && $pms->{dkim_author_domains}->{$d}
-              ? 'matches author domain'
-              : 'does not match author domain',
-          );
-        }
+      if (would_log("dbg","dkim")) {
+        dbg("dkim: %s %s, i=%s, d=%s, s=%s, a=%s, c=%s, %s, %s, %s",
+          $info,
+          $signature->isa('Mail::DKIM::DkSignature') ? 'DK' : 'DKIM',
+          map(!defined $_ ? '(undef)' : $_,
+            $signature->identity, $d, $signature->selector,
+            $signature->algorithm, scalar($signature->canonicalization),
+            $key_size ? "key_bits=$key_size" : "unknown key size",
+            $signature->result ),
+          defined $d && $pms->{dkim_author_domains}->{$d}
+            ? 'matches author domain'
+            : 'does not match author domain',
+        );
       }
     }
 
     if (@valid_signatures) {
-      if ($type eq 'DKIM') {
-        $pms->{dkim_signed} = 1;
-        $pms->{dkim_valid} = 1;
-
-        # supply values for both tags
-        my(%seen1, %seen2, %seen3, @identity_list, @domain_list, @selector_list);
-        @identity_list = grep(defined $_ && $_ ne '' && !$seen1{$_}++,
-                            map($_->identity, @valid_signatures));
-        @domain_list =   grep(defined $_ && $_ ne '' && !$seen2{$_}++,
-                            map($_->domain, @valid_signatures));
-        @selector_list = grep(defined $_ && $_ ne '' && !$seen3{$_}++,
-                            map($_->selector, @valid_signatures));
-        $pms->set_tag('DKIMIDENTITY',
-                    @identity_list == 1 ? $identity_list[0] : \@identity_list);
-        $pms->set_tag('DKIMDOMAIN',
-                    @domain_list == 1   ? $domain_list[0]   : \@domain_list);
-        $pms->set_tag('DKIMSELECTOR',
-                    @selector_list == 1 ? $selector_list[0] : \@selector_list);
-      } elsif ($type eq 'ARC') {
-        $pms->{arc_signed} = 1;
-        my $arc_seal_valid = 0;
-        my $arc_message_valid = 0;
-        my $arc_message_found = 0;
-        # All ARC-Seals signatures and the most recent ARC-Message-Signature must be valid
-        foreach my $arc ( @arc_sig ) {
-          if($arc->{prefix} eq 'ARC-Message-Signature:') {
-            next if $arc_message_found;
-            if($arc->{valid}) {
-              $arc_message_valid = 1;
-              dbg("dkim: valid ARC-Message-Signature signature");
-            } else {
-              dbg("dkim: invalid ARC-Message-Signature signature");
-            }
-            $arc_message_found = 1;
-          }
-          if(($arc->{prefix} eq 'ARC-Seal:') and $arc->{valid}) {
-            $arc_seal_valid = 1;
-            dbg("dkim: valid ARC-Seal signature");
-          } elsif(($arc->{prefix} eq 'ARC-Seal:') and not $arc->{valid}) {
-            $arc_seal_valid = 0;
-            dbg("dkim: invalid ARC-Seal signature");
-            last;
-          }
-        }
-        if($arc_message_valid and $arc_seal_valid) {
-          $pms->{arc_valid} = 1;
-        }
-      }
+      $pms->{dkim_signed} = 1;
+      $pms->{dkim_valid} = 1;
+
+      # supply values for both tags
+      my(%seen1, %seen2, %seen3, @identity_list, @domain_list, @selector_list);
+      @identity_list = grep(defined $_ && $_ ne '' && !$seen1{$_}++,
+                          map($_->identity, @valid_signatures));
+      @domain_list =   grep(defined $_ && $_ ne '' && !$seen2{$_}++,
+                          map($_->domain, @valid_signatures));
+      @selector_list = grep(defined $_ && $_ ne '' && !$seen3{$_}++,
+                          map($_->selector, @valid_signatures));
+      $pms->set_tag('DKIMIDENTITY',
+                  @identity_list == 1 ? $identity_list[0] : \@identity_list);
+      $pms->set_tag('DKIMDOMAIN',
+                  @domain_list == 1   ? $domain_list[0]   : \@domain_list);
+      $pms->set_tag('DKIMSELECTOR',
+                  @selector_list == 1 ? $selector_list[0] : \@selector_list);
+
       # let the result stand out more clearly in the log, use uppercase
       my $sig = $valid_signatures[0];
       my $sig_res = $sig->result_detail;
-      dbg("dkim: $type signature verification result: %s", uc($sig_res));
+      dbg("dkim: DKIM signature verification result: %s", uc($sig_res));
 
     } elsif (@$signatures) {
-      if ($type eq 'DKIM') {
-        $pms->{dkim_signed} = 1;
-      } elsif ($type eq 'ARC') {
-        $pms->{arc_signed} = 1;
-      }
+      $pms->{dkim_signed} = 1;
       my $sig = @$signatures[0];
       my $sig_res = $sig->result_detail;
-      dbg("dkim: $type signature verification result: %s", uc($sig_res));
+      dbg("dkim: DKIM signature verification result: %s", uc($sig_res));
 
     } else {
-      dbg("dkim: $type signature verification result: none");
+      dbg("dkim: DKIM signature verification result: none");
     }
   }
 }
@@ -1542,7 +1370,4 @@ sub _wlcheck_list {
   return ($any_match_at_all, \%any_match_by_wl);
 }
 
-# Version features
-sub has_arc { 1 }
-
 1;

Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/DMARC.pm
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/DMARC.pm	Mon Mar 16 08:30:15 2026	(r1932330)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/DMARC.pm	Mon Mar 16 11:06:43 2026	(r1932331)
@@ -333,88 +333,21 @@ sub _check_dmarc {
     return;
   }
 
-  my $dmarc_arc_verified = 0;
-  my $mfrom_dom = $mfrom_domain;
-  if (($result->result ne 'pass') and (ref($pms->{arc_verifier}) and ($pms->{arc_verifier}->result eq 'pass'))) {
-    undef $result;
-    $dmarc_arc_verified = 1;
-    # if DMARC fails retry by reading data from AAR headers
-    # use Mail::SpamAssassin::Plugin::AuthRes if available to read ARC signature details
-    my @spf_parsed = sort { ( $a->{arc_index} // 0 ) <=> ( $b->{arc_index} // 0 ) } @{$pms->{authres_parsed}{spf} // []};
-    my $old_arc_index = 0;
-    foreach my $spf_parse ( @spf_parsed ) {
-      last if not defined $spf_parse->{arc_index};
-      last if $old_arc_index > $spf_parse->{arc_index};
-      dbg("Evaluate DMARC using AAR spf information for index $spf_parse->{arc_index}");
-      if(exists $spf_parse->{properties}{smtp}{mailfrom}) {
-        $mfrom_dom = $spf_parse->{properties}{smtp}{mailfrom};
-        if($mfrom_dom =~ /\@(.*)/) {
-          $mfrom_dom = $1;
-        } else {
-	  $mfrom_dom = $mfrom_domain
-	}
-        $dmarc->spf([
-          {
-            scope  => 'mfrom',
-            domain => $mfrom_dom,
-            result => $spf_parse->{result},
-          }
-        ]);
-      }
-      if(exists $spf_parse->{properties}{smtp}{helo}) {
-        $dmarc->spf([
-          {
-            scope  => 'helo',
-            domain => $spf_parse->{properties}{smtp}{helo},
-            result => $spf_parse->{result},
-          }
-        ]);
-      }
-      $old_arc_index = $spf_parse->{arc_index};
-    }
-
-    my @tmp_arc_seals;
-    my @arc_seals;
-    if(defined $pms->{arc_verifier}{seals}) {
-      @tmp_arc_seals = @{$pms->{arc_verifier}{seals}};
-      @arc_seals = sort { ( $a->{tags_by_name}{i}{value} // 0 ) <=> ( $b->{tags_by_name}{i}{value} // 0 ) } @tmp_arc_seals;
-      foreach my $seals ( @arc_seals ) {
-        if(exists($seals->{tags_by_name}{d}) and exists($pms->{arc_author_domains}->{$mfrom_domain})) {
-          dbg("Evaluate DMARC using AAR dkim information for index $seals->{tags_by_name}{i}{value} on domain $mfrom_domain and selector $seals->{tags_by_name}{s}{value}. Result is $seals->{verify_result}");
-          my $arc_result = $seals->{verify_result};
-          if($seals->{verify_result} eq 'invalid') {
-            $arc_result = 'permerror';
-          }
-          $dmarc->dkim(domain => $mfrom_domain, selector => $seals->{tags_by_name}{s}{value}, result => $arc_result);
-          last;
-        }
-      }
-    }
-
-    eval { $result = $dmarc->validate(); };
-    if ($@) {
-      dbg("error while validating domain $mfrom_dom: $@");
-      return;
-    }
-  }
-
-  if(defined $result and ($result->result ne 'none') and (defined $result->{published}) and ($result->published->can('stringify'))) {
+  if (defined $result and ($result->result ne 'none') and (defined $result->{published}) and ($result->published->can('stringify'))) {
     dbg("Evaluated DMARC record \"" . $result->published->stringify . "\" for domain $from_domain");
   }
 
-  # Report that DMARC failed but it has been overridden because of AAR headers
-  if(ref($pms->{arc_verifier}) and ($pms->{arc_verifier}->result) and ($dmarc_arc_verified)) {
-    $result->reason->[0]{type} = 'local_policy';
-    $result->reason->[0]{comment} = "arc=" . $pms->{arc_verifier}->result;
-    my $cnt = 1;
-    foreach my $seals ( @{$pms->{arc_verifier}{seals}} ) {
-      if(exists($seals->{tags_by_name}{d}) and exists($seals->{tags_by_name}{s})) {
-        $result->reason->[0]{comment} .= " as[$cnt].d=$seals->{tags_by_name}{d}{value} as[$cnt].s=$seals->{tags_by_name}{s}{value}";
-        $cnt++;
-      }
-    }
-    if($cnt > 1) {
-      $result->reason->[0]{comment} .= " remote-ip[1]=$lasthop->{ip}";
+  # If DMARC fails, check for a dmarc=pass in trusted ARC-Authentication-Results
+  if ($result->result ne 'pass' && $pms->{arc_auth_results}) {
+    foreach my $aar (@{$pms->{arc_auth_results}}) {
+      my $dmarc_aar = $aar->{results}{dmarc};
+      next if !$dmarc_aar || $dmarc_aar->{result} ne 'pass';
+      dbg("DMARC overridden by trusted ARC AAR i=%s: dmarc=%s",
+          $aar->{arc_index}, $dmarc_aar->{result});
+      $result->{result} = 'pass';
+      $result->reason->[0]{type} = 'local_policy';
+      $result->reason->[0]{comment} = "arc=pass (trusted ARC sealer)";
+      last;
     }
   }
 

Added: spamassassin/trunk/rules/25_arc.cf
==============================================================================
--- /dev/null	00:00:00 1970	(empty, because file is newly added)
+++ spamassassin/trunk/rules/25_arc.cf	Mon Mar 16 11:06:43 2026	(r1932331)
@@ -0,0 +1,47 @@
+# SpamAssassin - ARC rules
+#
+# Please don't modify this file as your changes will be overwritten with
+# the next update. Use @@LOCAL_RULES_DIR@@/local.cf instead.
+# See 'perldoc Mail::SpamAssassin::Conf' for details.
+#
+# <@LICENSE>
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to you under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# </@LICENSE>
+#
+###########################################################################
+
+# Requires the Mail::SpamAssassin::Plugin::ARC plugin be loaded.
+
+ifplugin Mail::SpamAssassin::Plugin::ARC
+
+full     ARC_SIGNED		eval:check_arc_signed()
+describe ARC_SIGNED		Message has a ARC signature
+tflags   ARC_SIGNED		net
+reuse    ARC_SIGNED
+
+full     ARC_VALID		eval:check_arc_valid()
+describe ARC_VALID		Message has a valid ARC signature
+tflags   ARC_VALID		net nice
+reuse    ARC_VALID
+
+meta     ARC_INVALID		ARC_SIGNED && !ARC_VALID
+describe ARC_INVALID		ARC signature exists, but is not valid
+
+full     ARC_TRUSTED		eval:check_arc_trusted()
+describe ARC_TRUSTED		Message has valid ARC chain from trusted sealer
+tflags   ARC_TRUSTED		net nice
+
+endif   # Mail::SpamAssassin::Plugin::ARC

Modified: spamassassin/trunk/rules/25_dkim.cf
==============================================================================
--- spamassassin/trunk/rules/25_dkim.cf	Mon Mar 16 08:30:15 2026	(r1932330)
+++ spamassassin/trunk/rules/25_dkim.cf	Mon Mar 16 11:06:43 2026	(r1932331)
@@ -109,20 +109,6 @@ describe NML_ADSP_CUSTOM_MED    ADSP cus
 meta     NML_ADSP_CUSTOM_HIGH   DKIM_ADSP_CUSTOM_HIGH && !__VIA_ML && !__VIA_RESIGNER
 describe NML_ADSP_CUSTOM_HIGH   ADSP custom_high hit, and not from a mailing list
 
-if can(Mail::SpamAssassin::Plugin::DKIM::has_arc)
-  full     ARC_SIGNED		eval:check_arc_signed()
-  describe ARC_SIGNED		Message has a ARC signature
-  tflags   ARC_SIGNED		net
-  reuse    ARC_SIGNED
-
-  full     ARC_VALID		eval:check_arc_valid()
-  describe ARC_VALID		Message has a valid ARC signature
-  tflags   ARC_VALID		net nice
-  reuse    ARC_VALID
-
-  meta     ARC_INVALID		ARC_SIGNED && !ARC_VALID
-  describe ARC_INVALID		ARC signature exists, but is not valid
-endif
 
 #
 # old, declared for compatibility with pre-3.3, should have scores 0

Modified: spamassassin/trunk/rules/50_scores.cf
==============================================================================
--- spamassassin/trunk/rules/50_scores.cf	Mon Mar 16 08:30:15 2026	(r1932330)
+++ spamassassin/trunk/rules/50_scores.cf	Mon Mar 16 11:06:43 2026	(r1932331)
@@ -734,7 +734,7 @@ if (version >= 3.004002)
 score DKIM_VALID_EF -0.1
 endif
 
-if can(Mail::SpamAssassin::Plugin::DKIM::has_arc)
+ifplugin Mail::SpamAssassin::Plugin::ARC
   score ARC_SIGNED 0.001
   score ARC_VALID -0.1
   score ARC_INVALID 0.1

Modified: spamassassin/trunk/rules/v403.pre
==============================================================================
--- spamassassin/trunk/rules/v403.pre	Mon Mar 16 08:30:15 2026	(r1932330)
+++ spamassassin/trunk/rules/v403.pre	Mon Mar 16 11:06:43 2026	(r1932331)
@@ -16,6 +16,12 @@
 # added to new files, named according to the release they're added in.
 ###########################################################################
 
+# ARC - perform ARC (Authenticated Received Chain) verification
+#
+# Mail::DKIM version 0.50 or later required for use.
+#
+loadplugin Mail::SpamAssassin::Plugin::ARC
+
 # NeuralNetwork - Experimental plugin that checks emails using Neural Network algorithm
 #
 # loadplugin Mail::SpamAssassin::Plugin::NeuralNetwork

Modified: spamassassin/trunk/t/arc.t
==============================================================================
--- spamassassin/trunk/t/arc.t	Mon Mar 16 08:30:15 2026	(r1932330)
+++ spamassassin/trunk/t/arc.t	Mon Mar 16 11:06:43 2026	(r1932331)
@@ -6,29 +6,37 @@ use SATest; sa_t_init("arc");
 use Test::More;
 plan skip_all => "Net tests disabled" unless conf_bool('run_net_tests');
 plan skip_all => "Needs Mail::DKIM::ARC::Verifier >= 0.50" unless HAS_DKIM_VERIFIER ;
-plan tests => 2;
+plan tests => 3;
 
 tstlocalrules (q{
-  loadplugin Mail::SpamAssassin::Plugin::DKIM
+  loadplugin Mail::SpamAssassin::Plugin::ARC
 
   full     ARC_SIGNED eval:check_arc_signed()
   score    ARC_SIGNED 0.1
 
   full     ARC_VALID eval:check_arc_valid()
   score    ARC_VALID 0.1
+
+  full     ARC_TRUSTED eval:check_arc_trusted()
+  score    ARC_TRUSTED 0.1
+
+  arc_trusted_sealers sa-test.spamassassin.org
 });
 
 
+# ok01.eml: valid ARC chain from trusted sealer
 %patterns = (
   q{ 0.1 ARC_SIGNED }, 'ARC_SIGNED',
 );
-sarun ("-t < data/dkim/arc/ok01.eml", \&patterns_run_cb);
+sarun ("-t < data/arc/ok01.eml", \&patterns_run_cb);
 ok_all_patterns();
 clear_pattern_counters();
 
+# ko01.eml: no ARC headers
 %patterns = ();
 %anti_patterns = (
   q{ 0.1 ARC_SIGNED }, 'ARC_SIGNED',
+  q{ 0.1 ARC_TRUSTED }, 'ARC_TRUSTED',
 );
-sarun ("-t < data/dkim/arc/ko01.eml", \&patterns_run_cb);
+sarun ("-t < data/arc/ko01.eml", \&patterns_run_cb);
 ok_all_patterns();

Added: spamassassin/trunk/t/arc_authentication_results.t
==============================================================================
--- /dev/null	00:00:00 1970	(empty, because file is newly added)
+++ spamassassin/trunk/t/arc_authentication_results.t	Mon Mar 16 11:06:43 2026	(r1932331)
@@ -0,0 +1,35 @@
+#!/usr/bin/perl -T
+
+use lib '.'; use lib 't'; use lib 'lib';
+
+use strict;
+use warnings;
+use Test::More tests => 10;
+
+use_ok('Mail::SpamAssassin::Header::ArcAuthenticationResults');
+
+# Basic AAR header with i=1
+my $aar = Mail::SpamAssassin::Header::ArcAuthenticationResults->new(
+  'i=1; mx.example.com; spf=pass [email protected]; dkim=pass header.d=example.com'
+);
+ok($aar, 'parsed basic AAR header');
+is($aar->arc_index(), 1, 'arc_index is 1');
+is($aar->authserv_id(), 'mx.example.com', 'authserv_id');
+ok(scalar $aar->methods() >= 2, 'has spf and dkim methods');
+
+my ($spf) = $aar->method('spf');
+is($spf->{result}, 'pass', 'spf result is pass');
+is($spf->{properties}{smtp}{mailfrom}, '[email protected]', 'spf mailfrom property');
+
+# Higher instance index
+my $aar2 = Mail::SpamAssassin::Header::ArcAuthenticationResults->new(
+  'i=3; relay.example.org; dmarc=fail header.from=example.net'
+);
+is($aar2->arc_index(), 3, 'arc_index is 3');
+is($aar2->authserv_id(), 'relay.example.org', 'authserv_id for i=3');
+
+# Missing i= tag
+my $aar3 = Mail::SpamAssassin::Header::ArcAuthenticationResults->new(
+  'mx.example.com; spf=pass [email protected]'
+);
+is($aar3->arc_index(), undef, 'arc_index is undef when i= missing');

Modified: spamassassin/trunk/t/config.dist
==============================================================================
--- spamassassin/trunk/t/config.dist	Mon Mar 16 08:30:15 2026	(r1932330)
+++ spamassassin/trunk/t/config.dist	Mon Mar 16 11:06:43 2026	(r1932331)
@@ -1,4 +1,4 @@
-# Settings for SpamAssassin test suite.   These are the default settings.
+config.dist# Settings for SpamAssassin test suite.   These are the default settings.
 # Please copy this file to 'config' and edit to your taste.
 
 # ---------------------------------------------------------------------------

Added: spamassassin/trunk/t/data/arc/ko01.eml
==============================================================================
--- /dev/null	00:00:00 1970	(empty, because file is newly added)
+++ spamassassin/trunk/t/data/arc/ko01.eml	Mon Mar 16 11:06:43 2026	(r1932331)
@@ -0,0 +1,16 @@
+Authentication-Results: sa-test.spamassassin.org; [email protected]; dkim=pass (
+        message from spamassassin.org verified; );
+DKIM-Signature: v=1; a=rsa-sha256; d=sa-test.spamassassin.org; h=from:to
+	:subject:message-id:date:mime-version:content-type; s=t0768; bh=
+	15pFrAvOGi+eHKJgB6psh6iIBCbvYSuhPj+wQn6C7Ss=; b=ZFopU9lJ/WFWddnO
+	1nrYuptGphxfk2c4Tl0w/5HP0LhDMXX2KQRKHDh8p/AXxCERk6esOtX+BjME/ZOF
+	PnFrSh7naSjaT22YrT91gLD548OK73YUxR3Zh5nVOmSfn0TM
+From: SpamAssassin Test <[email protected]>
+To: undisclosed-recipients:;
+Subject: test message 2
+Message-ID: <[email protected]>
+Date: Mon, 08 Jun 2009 12:00:00 +0000
+MIME-Version: 1.0
+Content-Type: text/plain; charset=us-ascii
+
+testing

Added: spamassassin/trunk/t/data/arc/ok01.eml
==============================================================================
--- /dev/null	00:00:00 1970	(empty, because file is newly added)
+++ spamassassin/trunk/t/data/arc/ok01.eml	Mon Mar 16 11:06:43 2026	(r1932331)
@@ -0,0 +1,20 @@
+ARC-Seal: i=1; a=rsa-sha256; cv=none; d=sa-test.spamassassin.org; s=t0768; t=12345; b=GCLxX6NFV3/REpxEmzeKIRip5xJVP55GQTgOYndidGhYC+iXTNTm3xJf5zKQSaEikmtHgzL92QpgdpNcXGg+XvUI3UmQEOuyMCzJRw4hX0W3MFPSZ2xQr3hBKOnRpd96fAzGbDWJ9FjCwyloL+Uaylu+UNbfg1vcMv6/8NbMsF2gRSzJjhs8xQPMSZgqE0lWPkU1rmWmKbkx91txRNNrpKNQc0SlEIB1VdAsNWnnqLSp1B+EoGKsRJ1n55hpXRB6ytf+W+Edoi8Pkeb9IjNaoG8Zunwwpx59EP5iBcmGwdkYsS1eOu+92IbxihKUOMyRG9av1eJs0bSvPKS5OEs8dw==
+ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=sa-test.spamassassin.org; h=from:to:subject:message-id:date:mime-version:content-type; s=t0768; t=12345; bh=15pFrAvOGi+eHKJgB6psh6iIBCbvYSuhPj+wQn6C7Ss=; b=hqOPs3BhWVPBH2RfcQm5HGhfsqbaof3LXv1QAH4rmONbxuZXw4Rf4lDUrhWQB3UhkIsunu3TjslsODCkwPdDtSSEfbpRa7VHuq4O6tI4Ufinm5FGXflfY/o0sjx8S1gX9VFI/z3K1A2KFM/r1YnsgmoEjC7pLwgPWWyji3k0nUdaaYVzKSGktvkjMkfjgLPN/zlw0oN9ZUlfzEy6pFQdXjOuoYDQDHcq7AVm34grcj/8Mh1oNv0fUm3kAHSobebZxZb9jwp93WZPeAH/AnpaDg11U7k2IdSvbvis4qUt3SiLUYoGzCOkNywKPd8uqOilAGherx3aVpAuSwC8gdKiOw==
+ARC-Authentication-Results: i=1; sa-test.spamassassin.org; [email protected]; dkim=pass (
+        message from spamassassin.org verified; )
+Authentication-Results: sa-test.spamassassin.org; [email protected]; dkim=pass (
+        message from spamassassin.org verified; );
+DKIM-Signature: v=1; a=rsa-sha256; d=sa-test.spamassassin.org; h=from:to
+	:subject:message-id:date:mime-version:content-type; s=t0768; bh=
+	15pFrAvOGi+eHKJgB6psh6iIBCbvYSuhPj+wQn6C7Ss=; b=ZFopU9lJ/WFWddnO
+	1nrYuptGphxfk2c4Tl0w/5HP0LhDMXX2KQRKHDh8p/AXxCERk6esOtX+BjME/ZOF
+	PnFrSh7naSjaT22YrT91gLD548OK73YUxR3Zh5nVOmSfn0TM
+From: SpamAssassin Test <[email protected]>
+To: undisclosed-recipients:;
+Subject: test message 2
+Message-ID: <[email protected]>
+Date: Mon, 08 Jun 2009 12:00:00 +0000
+MIME-Version: 1.0
+Content-Type: text/plain; charset=us-ascii
+
+testing

Modified: spamassassin/trunk/t/debug.t
==============================================================================
--- spamassassin/trunk/t/debug.t	Mon Mar 16 08:30:15 2026	(r1932330)
+++ spamassassin/trunk/t/debug.t	Mon Mar 16 11:06:43 2026	(r1932331)
@@ -11,7 +11,7 @@ plan tests => 3;
 
 # list of known debug facilities
 my %facility = map( ($_, 1),
-  qw( accessdb archive-iterator async auto-welcomelist bayes check config daemon
+  qw( accessdb arc archive-iterator async auto-welcomelist bayes check config daemon
       dcc dkim askdns dns dnseval eval generic https_http_mismatch facility FreeMail
       ident ignore info ldap learn locker log logger markup HashBL
       message metadata mimeheader netset plugin prefork progress pyzor razor2

Modified: spamassassin/trunk/t/plugin_file.t
==============================================================================
--- spamassassin/trunk/t/plugin_file.t	Mon Mar 16 08:30:15 2026	(r1932330)
+++ spamassassin/trunk/t/plugin_file.t	Mon Mar 16 11:06:43 2026	(r1932331)
@@ -12,7 +12,7 @@ use Test::More tests => 9;
   'registered myTestPlugin',	'registered',
   'myTestPlugin eval test called', 'test_called',
   'myTestPlugin finishing',	'plugin_finished',
-  'test: plugins loaded: Mail::SpamAssassin::Plugin::ASN=HASH', 'plugins_loaded',
+  'test: plugins loaded: Mail::SpamAssassin::Plugin::ARC=HASH', 'plugins_loaded',
   'myTestPlugin=HASH',		'plugins_loaded2',
 );
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.