org.apache.james.jdkim.DKIMVerifier Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of apache-jdkim-library Show documentation
Show all versions of apache-jdkim-library Show documentation
A Java implementation for the DKIM specification.
/****************************************************************
* 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.jdkim;
import org.apache.james.jdkim.api.BodyHasher;
import org.apache.james.jdkim.api.Headers;
import org.apache.james.jdkim.api.PublicKeyRecord;
import org.apache.james.jdkim.api.PublicKeyRecordRetriever;
import org.apache.james.jdkim.api.SignatureRecord;
import org.apache.james.jdkim.exceptions.FailException;
import org.apache.james.jdkim.exceptions.PermFailException;
import org.apache.james.jdkim.exceptions.TempFailException;
import org.apache.james.jdkim.impl.BodyHasherImpl;
import org.apache.james.jdkim.impl.CompoundBodyHasher;
import org.apache.james.jdkim.impl.DNSPublicKeyRecordRetriever;
import org.apache.james.jdkim.impl.HttpPublicKeyRecordRetriever;
import org.apache.james.jdkim.impl.Message;
import org.apache.james.jdkim.impl.MultiplexingPublicKeyRecordRetriever;
import org.apache.james.jdkim.impl.StoredPublicKeyRecordRetriever;
import org.apache.james.jdkim.tagvalue.PublicKeyRecordImpl;
import org.apache.james.jdkim.tagvalue.SignatureRecordImpl;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
public class DKIMVerifier extends DKIMCommon {
/* Servers in close proximity might have clocks slightly out of alignment
*/
protected int allowableFutureSeconds = 0;
private PublicKeyRecordRetriever publicKeyRecordRetriever;
public DKIMVerifier() {
MultiplexingPublicKeyRecordRetriever mpkrr =
new MultiplexingPublicKeyRecordRetriever();
mpkrr.addRetriever("dns", new DNSPublicKeyRecordRetriever());
mpkrr.addRetriever("http", new HttpPublicKeyRecordRetriever());
publicKeyRecordRetriever = mpkrr;
}
public DKIMVerifier(final PublicKeyRecordRetriever publicKeyRecordRetriever) {
this.publicKeyRecordRetriever = publicKeyRecordRetriever;
}
/** Clear any keys stored for private-exchange
*
*/
public static void clearStoredKeys() {
StoredPublicKeyRecordRetriever.clearKeys();
}
/** Save a stored key for private-exchange
*
* @param domain as per spec
* @param selector as per spec
* @param key as per spec
*/
public static void addStoredKey(final String domain,
final String selector, final String key) {
StoredPublicKeyRecordRetriever.addKey(domain, selector, key);
}
protected PublicKeyRecord newPublicKeyRecord(final String record) {
return new PublicKeyRecordImpl(record);
}
public SignatureRecord newSignatureRecord(final String record) {
return new SignatureRecordImpl(record);
}
protected BodyHasherImpl newBodyHasher(final SignatureRecord signRecord)
throws PermFailException {
return new BodyHasherImpl(signRecord);
}
protected PublicKeyRecordRetriever getPublicKeyRecordRetriever()
throws PermFailException {
return publicKeyRecordRetriever;
}
public PublicKeyRecord publicKeySelector(final List records)
throws PermFailException {
String lastError = null;
if ((records == null) || records.isEmpty()) {
lastError = "no key for signature";
} else {
for (Iterator i = records.iterator(); i.hasNext();) {
String record = i.next();
try {
PublicKeyRecord pk = newPublicKeyRecord(record);
pk.validate();
// we expect a single valid record, otherwise the result
// is unpredictable.
// in case of multiple valid records we use the first one.
return pk;
} catch (IllegalStateException e) {
// do this at last.
lastError = "invalid key for signature: " + e.getMessage();
}
}
}
// return PERMFAIL ($error).
throw new PermFailException(lastError);
}
/**
* asserts applicability of a signature record the a public key record.
* throws an
*
* @param pkr public key record
* @param sign signature record
* @throws PermFailException when the keys are not applicable
*/
public static void apply(final PublicKeyRecord pkr, final SignatureRecord sign) throws PermFailException {
try {
if (!pkr.getGranularityPattern().matcher(sign.getIdentityLocalPart())
.matches()) {
throw new PermFailException("inapplicable key identity local="
+ sign.getIdentityLocalPart() + " Pattern: "
+ pkr.getGranularityPattern().pattern(), sign.getIdentity().toString());
}
if (!pkr.isHashMethodSupported(sign.getHashMethod())) {
throw new PermFailException("inappropriate hash for a="
+ sign.getHashKeyType() + "/" + sign.getHashMethod(), sign.getIdentity().toString());
}
if (!pkr.isKeyTypeSupported(sign.getHashKeyType())) {
throw new PermFailException("inappropriate key type for a="
+ sign.getHashKeyType() + "/" + sign.getHashMethod(), sign.getIdentity().toString());
}
if (pkr.isDenySubdomains()) {
if (!sign.getIdentity().toString().toLowerCase().endsWith(
("@" + sign.getDToken()).toLowerCase())) {
throw new PermFailException(
"AUID in subdomain of SDID is not allowed by the public key record.", sign.getIdentity().toString());
}
}
} catch (IllegalStateException e) {
throw new PermFailException("Invalid public key: "+e.getMessage(), sign.getIdentity().toString());
}
}
/**
* Iterates through signature's declared lookup method
*
* @param sign
* the signature record
* @return an "applicable" PublicKeyRecord
* @throws TempFailException on error
* @throws PermFailException on error
*/
public PublicKeyRecord publicRecordLookup(final SignatureRecord sign)
throws TempFailException, PermFailException {
// System.out.println(sign);
PublicKeyRecord key = null;
TempFailException lastTempFailure = null;
PermFailException lastPermFailure = null;
for (Iterator rlm = sign.getRecordLookupMethods().iterator(); (key == null)
&& rlm.hasNext();) {
CharSequence method = rlm.next();
try {
PublicKeyRecordRetriever pkrr = getPublicKeyRecordRetriever();
List records = pkrr.getRecords(method, sign.getSelector()
.toString(), sign.getDToken().toString());
PublicKeyRecord tempKey = publicKeySelector(records);
// checks wether the key is applicable to the signature
// TODO check with the IETF group to understand if this is the
// right thing to do.
// TODO loggin
apply(tempKey, sign);
key = tempKey;
} catch (TempFailException tf) {
lastTempFailure = tf;
} catch (PermFailException pf) {
lastPermFailure = pf;
}
}
if (key == null) {
if (lastTempFailure != null) {
if (sign != null) {
lastTempFailure.setRelatedRecordIdentity(sign.getIdentity().toString());
}
throw lastTempFailure;
} else if (lastPermFailure != null) {
if (sign != null) {
lastPermFailure.setRelatedRecordIdentity(sign.getIdentity().toString());
}
throw lastPermFailure;
} // this is unexpected because the publicKeySelector always returns
// null or exception
else {
throw new PermFailException(
"no key for signature [unexpected condition]", sign.getIdentity().toString());
}
}
return key;
}
/**
* Verifies all of the DKIM-Signature records declared in the supplied input
* stream
*
* @param is
* inputStream
* @return a list of verified signature records.
* @throws IOException if error
* @throws FailException
* if no signature can be verified
*/
public List verify(final InputStream is) throws IOException,
FailException {
Message message;
try {
try {
message = new Message(is);
} catch (RuntimeException e) {
throw e;
} catch (IOException e) {
throw e;
} catch (Exception e1) {
// This can only be a MimeException but we don't declare to allow usage of
// DKIMSigner without Mime4J dependency.
throw new PermFailException("Mime parsing exception: "
+ e1.getMessage(), e1);
}
try {
return verify(message, message.getBodyInputStream());
} finally {
message.dispose();
}
} finally {
is.close();
}
}
public BodyHasher newBodyHasher(final Headers messageHeaders) throws FailException {
List fields = messageHeaders.getFields("DKIM-Signature");
if ((fields == null) || fields.isEmpty()) {
return null;
}
// For each DKIM-signature we prepare an hashjob.
// We calculate all hashes concurrently so to read
// the inputstream only once.
Map bodyHashJobs = new HashMap();
Hashtable signatureExceptions = new Hashtable();
for (Iterator i = fields.iterator(); i.hasNext();) {
String signatureField = i.next();
try {
int pos = signatureField.indexOf(':');
if (pos > 0) {
String v = signatureField.substring(pos + 1, signatureField
.length());
SignatureRecord signatureRecord;
try {
signatureRecord = newSignatureRecord(v);
// validate
signatureRecord.validate();
} catch (IllegalStateException e) {
throw new PermFailException("Invalid signature record: "+e.getMessage(), e);
}
// Specification say we MAY refuse to verify the signature.
if (signatureRecord.getSignatureTimestamp() != null) {
long signedTime = signatureRecord.getSignatureTimestamp().longValue();
long elapsed = ((System.currentTimeMillis()/1000) - signedTime);
if (elapsed < (-3600*24*365*3)) {
throw new PermFailException("Signature date is more than "
+ (-elapsed/(3600*24*365)) + " years in the future.");
} else if (elapsed < (-3600*24*30*3)) {
throw new PermFailException("Signature date is more than "
+ (-elapsed/(3600*24*30)) + " months in the future.");
} else if (elapsed < (-3600*24*3)) {
throw new PermFailException("Signature date is more than "
+ (-elapsed/(3600*24)) + " days in the future.");
} else if (elapsed < (-3600*3)) {
throw new PermFailException("Signature date is more than "
+ (-elapsed/3600) + " hours in the future.");
} else if (elapsed < (-60*3)) {
throw new PermFailException("Signature date is more than "
+ (-elapsed/60) + " minutes in the future.");
} else if (elapsed < -allowableFutureSeconds) {
throw new PermFailException("Signature date is "
+ elapsed + " seconds in the future.");
}
}
// TODO here we could check more parameters for
// validation before running a network operation like the
// dns lookup.
// e.g: the canonicalization method could be checked now.
PublicKeyRecord publicKeyRecord = publicRecordLookup(signatureRecord);
List signedHeadersList = signatureRecord.getHeaders();
byte[] decoded = signatureRecord.getSignature();
signatureVerify(messageHeaders, signatureRecord, decoded,
publicKeyRecord, signedHeadersList);
// we track all canonicalizations+limit+bodyHash we
// see so to be able to check all of them in a single
// stream run.
BodyHasherImpl bhj = newBodyHasher(signatureRecord);
bodyHashJobs.put(signatureField, bhj);
} else {
throw new PermFailException(
"unexpected bad signature field");
}
} catch (TempFailException e) {
signatureExceptions.put(signatureField, e);
} catch (PermFailException e) {
signatureExceptions.put(signatureField, e);
} catch (RuntimeException e) {
signatureExceptions.put(signatureField, new PermFailException(
"Unexpected exception processing signature", e));
}
}
if (bodyHashJobs.isEmpty()) {
if (signatureExceptions.size() > 0) {
throw prepareException(signatureExceptions);
} else {
throw new PermFailException("Unexpected condition with "+fields);
}
}
return new CompoundBodyHasher(bodyHashJobs, signatureExceptions);
}
/**
* Verifies all of the DKIM-Signature records declared in the Headers
* object.
*
* @param messageHeaders
* parsed headers
* @param bodyInputStream
* input stream for the body.
* @return a list of verified signature records
* @throws IOException on io error
* @throws FailException
* if no signature can be verified
*/
public List verify(final Headers messageHeaders,
final InputStream bodyInputStream) throws IOException, FailException {
BodyHasher bh = newBodyHasher(messageHeaders);
if (bh == null) {
return null;
}
CompoundBodyHasher cbh = validateBodyHasher(bh);
// simultaneous computation of all the hashes.
DKIMCommon.streamCopy(bodyInputStream, cbh.getOutputStream());
return verify(cbh);
}
/**
* Completes the simultaneous verification of multiple
* signatures given the previously prepared compound body hasher where
* the user already written the body to the outputstream and closed it.
*
* @param bh the BodyHasher previously obtained by this class.
* @return a list of valid (verified) signatures or null on null input.
* @throws FailException if no valid signature is found
*/
public List verify(final BodyHasher bh) throws FailException {
if (bh == null) {
return null;
}
CompoundBodyHasher cbh = validateBodyHasher(bh);
return verify(cbh);
}
/**
* Used by public "verify" methods to make sure the input
* bodyHasher is a CompoundBodyHasher as expected.
*
* @param bh the BodyHasher previously obtained by this class.
* @return a casted CompoundBodyHasher
* @throws PermFailException if it wasn't a CompoundBodyHasher
*/
private CompoundBodyHasher validateBodyHasher(final BodyHasher bh)
throws PermFailException {
if (!(bh instanceof CompoundBodyHasher)) {
throw new PermFailException("Unexpected BodyHasher type: this is not generated by DKIMVerifier!");
}
CompoundBodyHasher cbh = (CompoundBodyHasher) bh;
return cbh;
}
/**
* Internal method to complete the simultaneous verification of multiple
* signatures given the previously prepared compound body hasher where
* the user already written the body to the outputstream and closed it.
*
* @param compoundBodyHasher the BodyHasher previously obtained by this class.
* @return a list of valid (verified) signatures
* @throws FailException if no valid signature is found
*/
private List verify(final CompoundBodyHasher compoundBodyHasher)
throws FailException {
List verifiedSignatures = new LinkedList();
for (Iterator i = compoundBodyHasher.getBodyHashJobs().values().iterator(); i.hasNext();) {
BodyHasherImpl bhj = i.next();
byte[] computedHash = bhj.getDigest();
byte[] expectedBodyHash = bhj.getSignatureRecord().getBodyHash();
if (!Arrays.equals(expectedBodyHash, computedHash)) {
compoundBodyHasher.getSignatureExceptions()
.put(
"DKIM-Signature:"+bhj.getSignatureRecord().toString(),
new PermFailException(
"Computed bodyhash is different from the expected one"));
} else {
verifiedSignatures.add(bhj.getSignatureRecord());
}
}
if (verifiedSignatures.isEmpty()) {
throw prepareException(compoundBodyHasher.getSignatureExceptions());
} else {
// There is no access to the signatureExceptions when
// there is at least one valid signature (JDKIM-14)
/*
for (Iterator i = signatureExceptions.keySet().iterator(); i
.hasNext();) {
String f = (String) i.next();
System.out.println("DKIM-Error:"
+ ((FailException) signatureExceptions.get(f))
.getMessage() + " FIELD: " + f);
}
*/
/*
for (Iterator i = verifiedSignatures.iterator(); i.hasNext();) {
SignatureRecord sr = (SignatureRecord) i.next();
System.out.println("DKIM-Pass:" + sr);
}
*/
return verifiedSignatures;
}
}
/**
* Given a map of exceptions prepares a human readable exception.
* This simply return the exception if it is only one, otherwise returns
* a cumulative exception
*
* @param signatureExceptions input exceptions
* @return the resulting "compact" exception
*/
private FailException prepareException(final Map signatureExceptions) {
if (signatureExceptions.size() == 1) {
return signatureExceptions.values().iterator()
.next();
} else {
// TODO loops signatureExceptions to give a more complete
// response, using nested exception or a compound exception.
// System.out.println(signatureExceptions);
return new PermFailException("found " + signatureExceptions.size()
+ " invalid signatures");
}
}
/**
* Performs signature verification (excluding the body hash).
*
* @param h the headers
* @param sign the signature record
* @param decoded the expected signature hash
* @param key the DKIM public key record
* @param headers the list of signed headers
* @throws PermFailException
*/
private void signatureVerify(final Headers h, final SignatureRecord sign,
final byte[] decoded, final PublicKeyRecord key, final List headers)
throws PermFailException {
try {
Signature signature = Signature.getInstance(sign.getHashMethod()
.toString().toUpperCase()
+ "with" + sign.getHashKeyType().toString().toUpperCase());
PublicKey publicKey;
try {
publicKey = key.getPublicKey();
} catch (IllegalStateException e) {
throw new PermFailException("Invalid Public Key: "+e.getMessage(), e);
}
signature.initVerify(publicKey);
signatureCheck(h, sign, headers, signature);
if (!signature.verify(decoded)) {
throw new PermFailException("Header signature does not verify");
}
} catch (InvalidKeyException e) {
throw new PermFailException(e.getMessage(), e);
} catch (NoSuchAlgorithmException e) {
throw new PermFailException(e.getMessage(), e);
} catch (SignatureException e) {
throw new PermFailException(e.getMessage(), e);
}
}
}