Authenticated Received Chain - ARC-sealing#31
Authenticated Received Chain - ARC-sealing#31agrinchenko wants to merge 117 commits intoapache:masterfrom
Conversation
Fix FWS is not trimmed correctly
- Use goals `single` instead of `attached` by the recommendation at http://maven.apache.org/plugins-archives/maven-assembly-plugin-2.5.5/attached-mojo.html
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.
|
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 |
|
@chibenwa - PR is ready for the final review when you have a chance. |
chibenwa
left a comment
There was a problem hiding this comment.
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
| * <p> | ||
| * This class is not instantiable and all methods are static. | ||
| */ | ||
| public class ARCVerifier { |
There was a problem hiding this comment.
-
There is no x= / t= timestamp check in verifyAms — ArcSignatureRecordImpl.validate() checks expiry, but verifyAms never instantiates that path. An expired AMS passes silently.
-
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
@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.
|
@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"); |
There was a problem hiding this comment.
Maybe we could get 'i' in the error message in order to know which one?
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
If expiration is unspecified, we need to check that t is not in the future.
There was a problem hiding this comment.
Updating AMS timestamp validation to reject a t= value that is in the future and adding a new test case for that.
| if (bodyBytes.length == 0 && message.getBody() instanceof Multipart) { | ||
| return true; | ||
| } |
There was a problem hiding this comment.
verifyAmsBodyHash: empty-Multipart early return true bypasses the bh= check.
Remove it and let Arrays.equals handle it ?
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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=.
|
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.
|
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. |
|
@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 :) |
|
Seen the new PR |
|
Closing this PR in favor of the clean replacement #33 |
|
This PR is being replaced by #33 |
Draft for ARC-sealing for review/comments/feedback. Work in progress.