Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions arc/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<artifactId>apache-jdkim-project</artifactId>
<groupId>org.apache.james.jdkim</groupId>
<version>0.6-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

<artifactId>apache-arc-library</artifactId>

<name>Apache James :: ARC</name>
<description>A Java implementation for the ARC specification.</description>
<url>http://james.apache.org/jdkim/main/</url>
<inceptionYear>2008</inceptionYear>

<dependencies>
<dependency>
<groupId>org.apache.james.jdkim</groupId>
<artifactId>apache-dmarc-library</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.james.jdkim</groupId>
<artifactId>apache-dmarc-library</artifactId>
<version>${project.version}</version>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.james.jdkim</groupId>
<artifactId>apache-jdkim-library</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.james.jdkim</groupId>
<artifactId>apache-jdkim-library</artifactId>
<version>${project.version}</version>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.james.jspf</groupId>
<artifactId>apache-jspf-resolver</artifactId>
<version>${jspf-resolver.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>org.apache.james</groupId>
<artifactId>apache-mime4j-core</artifactId>
</dependency>
<dependency>
<groupId>org.apache.james</groupId>
<artifactId>apache-mime4j-dom</artifactId>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
164 changes: 164 additions & 0 deletions arc/src/main/java/org/apache/james/arc/ARCChainValidator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/****************************************************************
* 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. *
****************************************************************/
package org.apache.james.arc;

import org.apache.james.arc.exceptions.ArcException;
import org.apache.james.mime4j.dom.Message;
import org.apache.james.mime4j.dom.Header;
import org.apache.james.mime4j.stream.Field;

import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* Validates the ARC (Authenticated Received Chain) chain in an email message.
* <p>
* This class provides methods to validate the ARC chain by checking the structure,
* verifying ARC-Message-Signature and ARC-Seal headers, and ensuring the integrity
* of previous ARC hops. It uses DNS records and cryptographic verification to
* ensure the authenticity of the ARC chain.
* </p>
*/
public class ARCChainValidator {
public static final String ARC_MESSAGE_SIGNATURE = "ARC-Message-Signature";
public static final String ARC_SEAL = "ARC-Seal";
private static final String SHA256_RSA = "SHA256withRSA";
private final Pattern INST_RGX_PATTERN = Pattern.compile("i=([0-9]+)");
private final PublicKeyRetrieverArc _keyRecordRetriever;

public ARCChainValidator(PublicKeyRetrieverArc keyRecordRetriever) {
this._keyRecordRetriever = keyRecordRetriever;
}

public ArcValidationOutcome validateArcChain(Message message) {

Header messageHeaders = message.getHeader();
int curInstance = getCurrentInstance(messageHeaders); // Incremented by 1

if (curInstance == 1) { //we are the first ARC Hop and there is no previous ARC hops in the chain to validate
return new ArcValidationOutcome(ArcValidationResult.NONE, "No previous ARC hops to validate");
}
else if (curInstance > 51) { // Not allowed to be > 50
return new ArcValidationOutcome(ArcValidationResult.FAIL, "ARC instance number exceeds maximum allowed value of 50");
}
else { // there are previous ARC hops that need to be validated
return validatePreviousArcHops(message, messageHeaders, curInstance);
}
}

private ArcValidationOutcome validatePreviousArcHops(Message message, Header messageHeaders, int myInstance) {
ARCVerifier arcVerifier = new ARCVerifier(_keyRecordRetriever);
Map<Integer, List<Field>> arcHeadersByI = arcVerifier.getArcHeadersByI(messageHeaders.getFields());
int numArcInstances = myInstance - 1;
boolean isArcSetStructureOK = arcVerifier.validateArcSetStructure(arcHeadersByI);
if (!isArcSetStructureOK) {
return new ArcValidationOutcome(ArcValidationResult.FAIL, "ARC set structure is invalid");
}

Set<Field> prevArcSet;
prevArcSet = arcVerifier.extractArcSet(messageHeaders, numArcInstances);
if (prevArcSet != null) {
boolean amsOk = checkArcAms(prevArcSet, message, arcVerifier);
boolean asOk = checkArcSeal(messageHeaders.getFields(), numArcInstances, arcVerifier);
if (amsOk && asOk) {
return new ArcValidationOutcome(ArcValidationResult.PASS, "All previous ARC hops validated successfully");
}
}
return new ArcValidationOutcome(ArcValidationResult.FAIL, "Previous ARC hops validation failed");
}

private boolean checkArcAms(Set<Field> prevArcSet, Message message, ARCVerifier arcVerifier){
boolean retVal = false;

Field amsHeader = prevArcSet.stream()
.filter(f -> f.getName().equalsIgnoreCase(ARC_MESSAGE_SIGNATURE))
.findFirst().orElse(null);
if (amsHeader == null) return retVal;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if (amsHeader == null) return retVal;
if (amsHeader == null) {
return false;
}

Here and in other places


String txtDnsRecord = arcVerifier.getTxtDnsRecordByField(amsHeader);
if (txtDnsRecord == null) return retVal;

retVal = arcVerifier.verifyAms(amsHeader, message, txtDnsRecord);

return retVal;
Comment on lines +107 to +109
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
retVal = arcVerifier.verifyAms(amsHeader, message, txtDnsRecord);
return retVal;
return arcVerifier.verifyAms(amsHeader, message, txtDnsRecord);

And we get rid of retVal?

}

private boolean checkArcSeal(List<Field> headers, int instToVerify, ARCVerifier arcVerifier) {
boolean retVal = false;
Map<Integer, List<Field>> arcHeadersByI = arcVerifier.getArcHeadersByI(headers);
ArcSealVerifyData verifyData = arcVerifier.buildArcSealSigningData(arcHeadersByI, instToVerify);
Field arcSealHeader = headers.stream()
.filter(f -> f.getName().equalsIgnoreCase(ARC_SEAL))
.findFirst().orElse(null);
if (arcSealHeader == null) return retVal;

String txtDnsRecord = arcVerifier.getTxtDnsRecordByField(arcSealHeader);
if (txtDnsRecord == null) return retVal;

PublicKey publicKey = arcVerifier.parsePublicKeyFromDns(txtDnsRecord);
if (publicKey == null) {
throw new ArcException(String.format("Unable to parse public key from dns record %s", txtDnsRecord));
}

String b64 = verifyData.getB64Signature();
String data = verifyData.getSignedData();

try {
Signature sig = Signature.getInstance(SHA256_RSA);
sig.initVerify(publicKey);
sig.update(data.getBytes(StandardCharsets.UTF_8));
byte[] signatureBytes = Base64.getDecoder().decode(b64);
retVal = sig.verify(signatureBytes);
} catch (NoSuchAlgorithmException e) {
throw new ArcException("Unsupported signing algorithm", e);
}
catch (InvalidKeyException e) {
throw new ArcException(String.format("Invalid public key used for %s record", txtDnsRecord), e);
} catch (SignatureException e) {
throw new ArcException(String.format("Invalid signature for %s record", txtDnsRecord), e);
}
return retVal;
}

public int getCurrentInstance(Header messageHeaders) {
int retVal = 1;
for (Field field : messageHeaders.getFields()) {
if (field.getName().startsWith("ARC-")) {
Matcher m = INST_RGX_PATTERN.matcher(field.getBody());
if (m.find()) {
int iVal = Integer.parseInt(m.group(1));
if (iVal >= retVal) {
retVal = iVal + 1;
}
}
}
}
return retVal;
}
}
134 changes: 134 additions & 0 deletions arc/src/main/java/org/apache/james/arc/ARCCommon.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/****************************************************************
* 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. *
****************************************************************/
package org.apache.james.arc;

import org.apache.james.jdkim.api.Headers;
import org.apache.james.jdkim.api.SignatureRecord;
import org.apache.james.jdkim.exceptions.PermFailException;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.Signature;
import java.security.SignatureException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Utility class for ARC (Authenticated Received Chain) operations.
* <p>
* Provides methods for:
* <ul>
* <li>Canonicalizing and updating cryptographic signatures for ARC headers</li>
* <li>Signing ARC-Message-Signature and ARC-Seal headers</li>
* <li>Copying streams</li>
* <li>Decoding Base64-encoded PKCS#8 private keys</li>
* </ul>
* <p>
* This class is not intended to be instantiated.
*/
public class ARCCommon {
private ARCCommon(){}

private static void updateSignature(Signature signature, boolean relaxed,
CharSequence header, String fv) throws SignatureException {
if (relaxed) {
signature.update(header.toString().toLowerCase().getBytes());
signature.update(":".getBytes());
String headerValue = fv.substring(fv.indexOf(':') + 1);
headerValue = headerValue.replaceAll("\r\n[\t ]", " ");
headerValue = headerValue.replaceAll("[\t ]+", " ");
headerValue = headerValue.trim();
signature.update(headerValue.getBytes());
} else {
signature.update(fv.getBytes());
}
}

static void amsSign(Headers h, SignatureRecord sign,
List<CharSequence> headers, Signature signature)
throws SignatureException, PermFailException {

boolean relaxedHeaders = isRelaxedHeaders(sign, true);

Map<String, Integer> processedHeader = new HashMap<>();

for (CharSequence header : headers) {
List<String> hl = h.getFields(header.toString());
if (hl != null && !hl.isEmpty()) {
Integer done = processedHeader.get(header.toString());
if (done == null)
done = 0;
int doneHeaders = done + 1;
if (doneHeaders <= hl.size()) {
String fv = hl.get(hl.size() - doneHeaders);
updateSignature(signature, relaxedHeaders, header, fv);
signature.update("\r\n".getBytes());
processedHeader.put(header.toString(), doneHeaders);
}
}
}

String amsHeader = "ARC-Message-Signature:" + sign.toUnsignedString();
updateSignature(signature, relaxedHeaders, "arc-message-signature", amsHeader);
}

static void arcSeal(SignatureRecord sign,
Map<String, String> headersToSeal, Signature signature)
throws SignatureException, PermFailException {

boolean relaxedHeaders = isRelaxedHeaders(sign, false);

for (Map.Entry<String, String> headerEntry : headersToSeal.entrySet()) {
String headerName = headerEntry.getKey();
String headerValue = headerName+": " +headerEntry.getValue();
updateSignature(signature, relaxedHeaders, headerName, headerValue);
signature.update("\r\n".getBytes());
}

String signatureStub = "ARC-Seal:" + sign.toUnsignedString();
updateSignature(signature, relaxedHeaders, "arc-seal",
signatureStub);
}

private static boolean isRelaxedHeaders(SignatureRecord sign, boolean isAms) throws PermFailException {
boolean relaxedHeaders = !isAms || SignatureRecord.RELAXED.equals(sign
.getHeaderCanonicalisationMethod()); // RFC 8617 : ARC-seal: only "relaxed" header field canonicalization allowed
if (!relaxedHeaders
&& !SignatureRecord.SIMPLE.equals(sign
.getHeaderCanonicalisationMethod())) {

throw new PermFailException(
"Unsupported canonicalization algorithm: "
+ sign.getHeaderCanonicalisationMethod());
}
return relaxedHeaders;
}

public static void streamCopy(InputStream bodyIs, OutputStream out)
throws IOException {
byte[] buffer = new byte[2048];
int read;
while ((read = bodyIs.read(buffer)) > 0) {
out.write(buffer, 0, read);
}
bodyIs.close();
out.close();
}
Comment on lines +124 to +133
Copy link
Contributor

Choose a reason for hiding this comment

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

Can't we rely on InputStream::transfer ?

}
Loading