Skip to content

Authenticated Received Chain - ARC-sealing#31

Closed
agrinchenko wants to merge 117 commits intoapache:masterfrom
agrinchenko:master
Closed

Authenticated Received Chain - ARC-sealing#31
agrinchenko wants to merge 117 commits intoapache:masterfrom
agrinchenko:master

Conversation

@agrinchenko
Copy link
Copy Markdown

Draft for ARC-sealing for review/comments/feedback. Work in progress.

mbaechler and others added 30 commits July 5, 2019 09:25
Fix FWS is not trimmed correctly
Following alphabetical order with groupId > artifactId
The JDKIM mailet has been moved into James server project
After removing the mailet (been moved to James), those libs are not necessary anymore.
- Add ValiMail ARC validation coverage for no-chain base messages,
single-hop pass fixtures, missing and malformed ARC public keys, and
AMS/AS selector-domain independence.
- Add AMS bh= validation coverage for whitespace-tolerant body hashes,
simple and relaxed body canonicalization, missing/empty/non-base64 body
hash tags, modified body hashes, and modified message bodies.
- Verify AMS body hashes before accepting ARC-Message-Signature by decoding
bh=, applying the AMS body canonicalization mode, and comparing the
computed SHA-256 digest. Reject malformed or unsupported body hash inputs
cleanly while preserving existing multipart fixture behavior when the
Mime4j DOM parser cannot reconstruct a raw multipart body.
- Updated ARCVerifier to parse AMS canonicalization as header/body modes.
- Added support for AMS simple header canonicalization in addition to relaxed.
- Rejects empty/invalid c= values before accepting AMS.
 - d= is required, non-empty, and must look like a valid domain.
 - s= is required, non-empty, and must look like a valid selector.
 - t= remains optional, but if present must be numeric.
- Add ValiMail fixture-backed tests for the remaining AMS h= signed-header
cases, covering empty h=, whitespace and folded separators, case-insensitive
header names, duplicate header selection, missing headers, omitted h=, header
order failures, and ARC header inclusion behavior.
- Reject ARC-Seal in the AMS h= signed-header list so messages that include
arc-seal in AMS signing data fail validation as expected by the upstream test
suite, while preserving the allowed arc-message-signature case.
- Add explicit first-hop signing coverage for messages without an incoming ARC
chain, asserting that generated ARC sets start at i=1 and seal with cv=none.
@agrinchenko
Copy link
Copy Markdown
Author

I added the remaining ARC test coverage based on RFC 8617 and the public ValiMail ARC test suite and am moving this PR out of draft, ready for review.

The added tests cover the ARC behavior identified in the sources, including AMS h= handling, ARC-Seal inclusion behavior, and first-hop ARC set generation for messages without an existing ARC chain. We are at 185 tests now.

@agrinchenko agrinchenko marked this pull request as ready for review April 18, 2026 22:48
@agrinchenko
Copy link
Copy Markdown
Author

@chibenwa - PR is ready for the final review when you have a chance.

@agrinchenko agrinchenko changed the title [Draft] Authenticated Received Chain - ARC-sealing Authenticated Received Chain - ARC-sealing Apr 21, 2026
Copy link
Copy Markdown
Contributor

@chibenwa chibenwa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for perseverance and putting this together!

I spent most of my friday night piecing this together... THere remains a few sensitive points that I believe would need to be adressed before that work is integrated.

Happy to contribute the fixes if needed.

Cheers!

Benoit

Comment thread arc/src/main/java/org/apache/james/arc/ARCChainValidator.java
Comment thread arc/src/main/java/org/apache/james/arc/ARCVerifier.java Outdated
Comment thread README.adoc Outdated
Comment thread arc/src/main/java/org/apache/james/arc/ARCVerifier.java Outdated
Comment thread arc/src/main/java/org/apache/james/arc/DNSPublicKeyRecordRetrieverArc.java Outdated
Comment thread arc/src/main/java/org/apache/james/arc/ARCVerifier.java Outdated
Comment thread arc/src/main/java/org/apache/james/arc/ARCVerifier.java Outdated
Comment thread dmarc/src/main/java/org/apache/james/dmarc/DMARCVerifier.java
Comment thread arc/src/main/java/org/apache/james/arc/ARCVerifier.java Outdated
* <p>
* This class is not instantiable and all methods are static.
*/
public class ARCVerifier {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. There is no x= / t= timestamp check in verifyAms — ArcSignatureRecordImpl.validate() checks expiry, but verifyAms never instantiates that path. An expired AMS passes silently.

  2. ARC-Seal h= content isnot validated — RFC 8617 §4.1.1 specifies that the Seal's h= must cover exactly {aar, ams, as} for all prior hops in order. The code doesn't validate this constraint at all.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch on AMS timestamps. I am adding 'x=' validation in 'verifyAms' and expired AMS signatures now fail, and 'x=' must be greater than 't='
when both are present.

Regarding the second point, I am checking the RFC 8617 and my reading is that 4.1.1 describes ARC-Authentication-Results only; ARC-Seal is defined in
4.1.3. My understanding is that ARC-Seal does not carry an 'h=' tag. The ARC headers covered by AS are defined by the sealing/validation algorithm, not by an AS 'h=' list. I will tighten validation to reject ARC-Seal headers that include 'h=' and keep the existing AS signing/verification path responsible for covering the ARC set headers in order.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed the first point in the commit agrinchenko@2f8955f

The second point is addressed in a way to reject ARC-Seal headers that include 'h='. Again, this is based on my earlier comment above regarding the second point.

Comment thread dmarc/src/main/resources/public_suffix_list.dat Outdated
@agrinchenko
Copy link
Copy Markdown
Author

@chibenwa, thanks for the review and feedback! Going through the points you raised.

- Validate every ARC-Message-Signature in multi-hop chains and use the ARC-Seal
  key from the instance being verified.
- Parse ARC i= tags numerically, enforce the valid 1..50 range, and avoid i=1
  matching i=10.
- Harden AMS verification for unsupported algorithms, x=/t= expiry, no-body
  bh= checks, and reject ARC-Seal h= tags.
- Fix multipart body reconstruction to emit CRLF after the closing boundary.
- Return DMARC permerror for malformed From headers instead of throwing.
- Replace the bundled public suffix list with Guava InternetDomainName and add
  Guava as a managed DMARC dependency.
- Remove the SPF thread context classloader workaround.
- Add ARC and DMARC regression tests for the validation, canonicalization, and
  malformed input cases above.
- Refresh README ARC/DMARC documentation while preserving upstream DKIM content.
@agrinchenko
Copy link
Copy Markdown
Author

@chibenwa I pushed the updates addressing the latest review feedback and the PR is ready for another pass. Thanks again for the detailed review!

for (int i = 1; i <= numArcInstances; i++) {
Set<Field> arcSet = arcVerifier.extractArcSet(messageHeaders, i);
if (arcSet == null || !checkArcAms(arcSet, message, arcVerifier)) {
return new ArcValidationOutcome(ArcValidationResult.FAIL, "Previous ARC hops validation failed");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could get 'i' in the error message in order to know which one?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea! Updating the ARC previous-hop validation failure message to include the failing ARC instance, like this -- "Previous ARC hop validation failed at i=...".

if (timestamp != null && expirationEpoch <= Long.parseLong(timestamp)) {
throw new ArcException("AMS x= expiration must be greater than t= timestamp");
}
long now = System.currentTimeMillis() / 1000;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be wonderful to rely on a Clock that can be overriden by the library user, defaulting to Clock.systemUTC if none: this would ease testability.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting idea and it does make sense! Updating "ARCVerifier" to accept an injectable "Clock" override, with the existing constructor defaulting to "Clock.systemUTC()". The AMS expiration check now uses that clock. I am also updating AMS regression test to use a fixed clock so it is deterministic.

private void validateAmsTimestamp(Map<String, String> tags) {
String timestamp = tags.get("t");
String expiration = tags.get("x");
if (expiration == null) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If expiration is unspecified, we need to check that t is not in the future.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updating AMS timestamp validation to reject a t= value that is in the future and adding a new test case for that.

Comment on lines 230 to 232
if (bodyBytes.length == 0 && message.getBody() instanceof Multipart) {
return true;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

verifyAmsBodyHash: empty-Multipart early return true bypasses the bh= check.

Remove it and let Arrays.equals handle it ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I am removing the empty-multipart early return so bh= is always compared normally. Also, I am adding a test for an empty multipart with an invalid body hash.

}
import com.google.common.net.InternetDomainName;

public class PublicSuffixList {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please have it as aan interface and allow people to use there own

Depending on Guava (and a specific one) is not cool for a lib, we need to have a built in alternative for people: exclude guava dependency, write their own with their own version instead...

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, changing "PublicSuffixList" into an interface and making DMARCVerifier accept it. The default constructor will use a built-in implementation, and GuavaPublicSuffixList will remain available as an optional implementation. Also marking Guava optional in the DMARC module and adding a test covering custom PSL injection.

return true;
}

private boolean checkArcSealTags(List<Field> arcSet) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

checkArcSealTags only rejects h=

I think we miss validation of required tags (i=, a=, cv=, d=, s=, b=).

Maybe we also need to check cv value to be one of none pass fail.

Maybe we also need to valide d and s on Arc-Seal too (it's done for AMS as far as I understand)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will tighten checkArcSealTags so ARC-Seal structure validation will require non-empty i, a, cv, d, s, and b tags and will validate cv against none/pass/fail. I will also validate d and s using the same domain/selector rules as AMS, still rejecting h=.

@chibenwa
Copy link
Copy Markdown
Contributor

Also we might want to solve the merge conflict...

- Include the failing ARC instance in previous-hop AMS validation errors.
- Add injectable Clock support to ARCVerifier and use it for AMS timestamp checks.
- Reject AMS t= timestamps in the future, even when x= is absent.
- Remove empty multipart bh= bypass and always compare computed body hashes.
- Fix parsed multipart reconstruction by reading the boundary from parent Content-Type.
- Tighten ARC-Seal tag validation for required i/a/cv/d/s/b tags, cv values, d/s syntax, and h= rejection.
- Convert DMARC PublicSuffixList to an injectable interface.
- Add built-in default PSL resolver and optional Guava-backed implementation.
- Mark Guava optional and add custom PSL injection coverage.
- Add regression tests for diagnostics, timestamp validation, multipart bh= handling, and custom PSL wiring.
@agrinchenko
Copy link
Copy Markdown
Author

I checked after pushing the latest updates, and GitHub currently says "No conflicts with base branch" and "Changes can be cleanly merged." I do not see an active merge conflict at the moment, but I’ll keep an eye on it if the base branch moves again.

@Arsnael
Copy link
Copy Markdown
Contributor

Arsnael commented Apr 28, 2026

@agrinchenko maybe you should rebase this work on latest master if not done already? I see conflicts as well, and more than a 100 commits that do not belong with this work :)

@chibenwa
Copy link
Copy Markdown
Contributor

Seen the new PR
I'll try to review it in the coming days
I propose to close this one for the time being.
Thanks for your involvment @agrinchenko !

@agrinchenko
Copy link
Copy Markdown
Author

@chibenwa, @Arsnael I created a new PR-33 that is based off the current apache-master to address the merge issues.

@agrinchenko
Copy link
Copy Markdown
Author

Closing this PR in favor of the clean replacement #33

@agrinchenko
Copy link
Copy Markdown
Author

This PR is being replaced by #33

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants