EMMA Coverage Report (generated Thu Nov 19 17:07:02 CET 2009)
[all classes][org.apache.james.jdkim]

COVERAGE SUMMARY FOR SOURCE FILE [DKIMVerifier.java]

nameclass, %method, %block, %line, %
DKIMVerifier.java100% (1/1)92%  (12/13)67%  (419/629)68%  (91.6/134)

COVERAGE BREAKDOWN BY CLASS AND METHOD

nameclass, %method, %block, %line, %
     
class DKIMVerifier100% (1/1)92%  (12/13)67%  (419/629)68%  (91.6/134)
prepareException (Map): FailException 0%   (0/1)0%   (0/25)0%   (0/3)
verify (InputStream): List 100% (1/1)43%  (15/35)55%  (2.8/5)
publicKeySelector (List): PublicKeyRecord 100% (1/1)56%  (25/45)58%  (7/12)
verify (Headers, InputStream): List 100% (1/1)58%  (167/287)66%  (42/64)
publicRecordLookup (SignatureRecord): PublicKeyRecord 100% (1/1)70%  (47/67)62%  (14.9/24)
signatureVerify (Headers, SignatureRecord, byte [], PublicKeyRecord, List): void 100% (1/1)86%  (32/37)83%  (5/6)
DKIMVerifier (): void 100% (1/1)100% (12/12)100% (3/3)
DKIMVerifier (PublicKeyRecordRetriever): void 100% (1/1)100% (6/6)100% (3/3)
apply (PublicKeyRecord, SignatureRecord): void 100% (1/1)100% (97/97)100% (10/10)
getPublicKeyRecordRetriever (): PublicKeyRecordRetriever 100% (1/1)100% (3/3)100% (1/1)
newBodyHasher (SignatureRecord): BodyHasher 100% (1/1)100% (5/5)100% (1/1)
newPublicKeyRecord (String): PublicKeyRecord 100% (1/1)100% (5/5)100% (1/1)
newSignatureRecord (String): SignatureRecord 100% (1/1)100% (5/5)100% (1/1)

1/****************************************************************
2 * Licensed to the Apache Software Foundation (ASF) under one   *
3 * or more contributor license agreements.  See the NOTICE file *
4 * distributed with this work for additional information        *
5 * regarding copyright ownership.  The ASF licenses this file   *
6 * to you under the Apache License, Version 2.0 (the            *
7 * "License"); you may not use this file except in compliance   *
8 * with the License.  You may obtain a copy of the License at   *
9 *                                                              *
10 *   http://www.apache.org/licenses/LICENSE-2.0                 *
11 *                                                              *
12 * Unless required by applicable law or agreed to in writing,   *
13 * software distributed under the License is distributed on an  *
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
15 * KIND, either express or implied.  See the License for the    *
16 * specific language governing permissions and limitations      *
17 * under the License.                                           *
18 ****************************************************************/
19 
20package org.apache.james.jdkim;
21 
22import java.io.IOException;
23import java.io.InputStream;
24import java.io.OutputStream;
25import java.security.InvalidKeyException;
26import java.security.NoSuchAlgorithmException;
27import java.security.Signature;
28import java.security.SignatureException;
29import java.util.Arrays;
30import java.util.HashMap;
31import java.util.Hashtable;
32import java.util.Iterator;
33import java.util.LinkedList;
34import java.util.List;
35import java.util.Map;
36 
37import org.apache.james.jdkim.api.BodyHasher;
38import org.apache.james.jdkim.api.Headers;
39import org.apache.james.jdkim.api.PublicKeyRecord;
40import org.apache.james.jdkim.api.PublicKeyRecordRetriever;
41import org.apache.james.jdkim.api.SignatureRecord;
42import org.apache.james.jdkim.canon.CompoundOutputStream;
43import org.apache.james.jdkim.exceptions.FailException;
44import org.apache.james.jdkim.exceptions.PermFailException;
45import org.apache.james.jdkim.exceptions.TempFailException;
46import org.apache.james.jdkim.impl.BodyHasherImpl;
47import org.apache.james.jdkim.impl.DNSPublicKeyRecordRetriever;
48import org.apache.james.jdkim.impl.Message;
49import org.apache.james.jdkim.impl.MultiplexingPublicKeyRecordRetriever;
50import org.apache.james.jdkim.tagvalue.PublicKeyRecordImpl;
51import org.apache.james.jdkim.tagvalue.SignatureRecordImpl;
52import org.apache.james.mime4j.MimeException;
53 
54public class DKIMVerifier extends DKIMCommon {
55 
56    private PublicKeyRecordRetriever publicKeyRecordRetriever;
57 
58    public DKIMVerifier() {
59        this.publicKeyRecordRetriever = new MultiplexingPublicKeyRecordRetriever(
60                "dns", new DNSPublicKeyRecordRetriever());
61    }
62 
63    public DKIMVerifier(PublicKeyRecordRetriever publicKeyRecordRetriever) {
64        this.publicKeyRecordRetriever = publicKeyRecordRetriever;
65    }
66 
67    protected PublicKeyRecord newPublicKeyRecord(String record) {
68        return new PublicKeyRecordImpl(record);
69    }
70 
71    public SignatureRecord newSignatureRecord(String record) {
72        return new SignatureRecordImpl(record);
73    }
74 
75    public BodyHasher newBodyHasher(SignatureRecord signRecord)
76            throws PermFailException {
77        return new BodyHasherImpl(signRecord);
78    }
79 
80    protected PublicKeyRecordRetriever getPublicKeyRecordRetriever()
81            throws PermFailException {
82        return publicKeyRecordRetriever;
83    }
84 
85    public PublicKeyRecord publicKeySelector(List records)
86            throws PermFailException {
87        String lastError = null;
88        if (records == null || records.size() == 0) {
89            lastError = "no key for signature";
90        } else {
91            for (Iterator i = records.iterator(); i.hasNext();) {
92                String record = (String) i.next();
93                try {
94                    PublicKeyRecord pk = newPublicKeyRecord(record);
95                    pk.validate();
96                    // we expect a single valid record, otherwise the result
97                    // is unpredictable.
98                    // in case of multiple valid records we use the first one.
99                    return pk;
100                } catch (IllegalStateException e) {
101                    // do this at last.
102                    lastError = "invalid key for signature: " + e.getMessage();
103                }
104            }
105        }
106        // return PERMFAIL ($error).
107        throw new PermFailException(lastError);
108    }
109 
110    /**
111     * asserts applicability of a signature record the a public key record.
112     * throws an 
113     * 
114     * @param pkr public key record
115     * @param sign signature record
116     * @throws PermFailException when the keys are not applicable
117     */
118    public static void apply(PublicKeyRecord pkr, SignatureRecord sign) throws PermFailException {
119        if (!pkr.getGranularityPattern().matcher(sign.getIdentityLocalPart())
120                .matches()) {
121            throw new PermFailException("inapplicable key identity local="
122                    + sign.getIdentityLocalPart() + " Pattern: "
123                    + pkr.getGranularityPattern().pattern());
124        }
125 
126        if (!pkr.isHashMethodSupported(sign.getHashMethod())) {
127            throw new PermFailException("inappropriate hash for a="
128                    + sign.getHashKeyType() + "/" + sign.getHashMethod());
129        }
130        if (!pkr.isKeyTypeSupported(sign.getHashKeyType())) {
131            throw new PermFailException("inappropriate key type for a="
132                    + sign.getHashKeyType() + "/" + sign.getHashMethod());
133        }
134 
135        if (pkr.isDenySubdomains()) {
136            if (!sign.getIdentity().toString().toLowerCase().endsWith(
137                    ("@" + sign.getDToken()).toLowerCase())) {
138                throw new PermFailException(
139                        "AUID in subdomain of SDID is not allowed by the public key record.");
140            }
141        }
142 
143    }
144 
145    /**
146     * Iterates through signature's declared lookup method
147     * 
148     * @param sign
149     *                the signature record
150     * @return an "applicable" PublicKeyRecord
151     * @throws TempFailException
152     * @throws PermFailException
153     */
154    public PublicKeyRecord publicRecordLookup(SignatureRecord sign)
155            throws TempFailException, PermFailException {
156        // System.out.println(sign);
157        PublicKeyRecord key = null;
158        TempFailException lastTempFailure = null;
159        PermFailException lastPermFailure = null;
160        for (Iterator rlm = sign.getRecordLookupMethods().iterator(); key == null
161                && rlm.hasNext();) {
162            String method = (String) rlm.next();
163            try {
164                PublicKeyRecordRetriever pkrr = getPublicKeyRecordRetriever();
165                List records = pkrr.getRecords(method, sign.getSelector()
166                        .toString(), sign.getDToken().toString());
167                PublicKeyRecord tempKey = publicKeySelector(records);
168                // checks wether the key is applicable to the signature
169                // TODO check with the IETF group to understand if this is the
170                // right thing to do.
171                // TODO loggin
172                apply(tempKey, sign);
173                key = tempKey;
174            } catch (TempFailException tf) {
175                lastTempFailure = tf;
176            } catch (PermFailException pf) {
177                lastPermFailure = pf;
178            }
179        }
180        if (key == null) {
181            if (lastTempFailure != null)
182                throw lastTempFailure;
183            else if (lastPermFailure != null)
184                throw lastPermFailure;
185            // this is unexpected because the publicKeySelector always returns
186            // null or exception
187            else
188                throw new PermFailException(
189                        "no key for signature [unexpected condition]");
190        }
191        return key;
192    }
193 
194    /**
195     * Verifies all of the DKIM-Signature records declared in the supplied input
196     * stream
197     * 
198     * @param is
199     *                inputStream
200     * @return a list of verified signature records.
201     * @throws IOException
202     * @throws FailException
203     *                 if no signature can be verified
204     */
205    public List/* SignatureRecord */verify(InputStream is) throws IOException,
206            FailException {
207        Message message;
208        try {
209            message = new Message(is);
210            return verify(message, message.getBodyInputStream());
211        } catch (MimeException e1) {
212            throw new PermFailException("Mime parsing exception: "
213                    + e1.getMessage(), e1);
214        } finally {
215            is.close();
216        }
217    }
218 
219    /**
220     * Verifies all of the DKIM-Signature records declared in the Headers
221     * object.
222     * 
223     * @param messageHeaders
224     *                parsed headers
225     * @param bodyInputStream
226     *                input stream for the body.
227     * @return a list of verified signature records
228     * @throws IOException
229     * @throws FailException
230     *                 if no signature can be verified
231     */
232    public List/* SignatureRecord */verify(Headers messageHeaders,
233            InputStream bodyInputStream) throws IOException, FailException {
234        // System.out.println(message.getFields("DKIM-Signature"));
235        List fields = messageHeaders.getFields("DKIM-Signature");
236        // if (fields.size() > 1) throw new RuntimeException("here we are!");
237        if (fields.size() == 0) {
238            throw new PermFailException("DKIM-Signature field not found");
239        }
240 
241        // For each DKIM-signature we prepare an hashjob.
242        // We calculate all hashes concurrently so to read
243        // the inputstream only once.
244        Map/* String, BodyHashJob */bodyHashJobs = new HashMap();
245        List/* OutputStream */outputStreams = new LinkedList();
246        Map/* String, Exception */signatureExceptions = new Hashtable();
247        for (Iterator i = fields.iterator(); i.hasNext();) {
248            String signatureField = (String) i.next();
249            try {
250                int pos = signatureField.indexOf(':');
251                if (pos > 0) {
252                    String v = signatureField.substring(pos + 1, signatureField
253                            .length());
254                    SignatureRecord signatureRecord;
255                    try {
256                        signatureRecord = newSignatureRecord(v);
257                        // validate
258                        signatureRecord.validate();
259                    } catch (IllegalStateException e) {
260                        throw new PermFailException(e.getMessage());
261                    }
262 
263                    // TODO here we could check more parameters for
264                    // validation before running a network operation like the
265                    // dns lookup.
266                    // e.g: the canonicalization method could be checked now.
267                    PublicKeyRecord publicKeyRecord = publicRecordLookup(signatureRecord);
268 
269                    List signedHeadersList = signatureRecord.getHeaders();
270 
271                    byte[] decoded = signatureRecord.getSignature();
272                    signatureVerify(messageHeaders, signatureRecord, decoded,
273                            publicKeyRecord, signedHeadersList);
274 
275                    // we track all canonicalizations+limit+bodyHash we
276                    // see so to be able to check all of them in a single
277                    // stream run.
278                    BodyHasher bhj = newBodyHasher(signatureRecord);
279 
280                    bodyHashJobs.put(signatureField, bhj);
281                    outputStreams.add(bhj.getOutputStream());
282 
283                } else {
284                    throw new PermFailException(
285                            "unexpected bad signature field");
286                }
287            } catch (TempFailException e) {
288                signatureExceptions.put(signatureField, e);
289            } catch (PermFailException e) {
290                signatureExceptions.put(signatureField, e);
291            } catch (InvalidKeyException e) {
292                signatureExceptions.put(signatureField, new PermFailException(e
293                        .getMessage(), e));
294            } catch (NoSuchAlgorithmException e) {
295                signatureExceptions.put(signatureField, new PermFailException(e
296                        .getMessage(), e));
297            } catch (SignatureException e) {
298                signatureExceptions.put(signatureField, new PermFailException(e
299                        .getMessage(), e));
300            }
301        }
302 
303        OutputStream o;
304        if (bodyHashJobs.size() == 0) {
305            throw prepareException(signatureExceptions);
306        } else if (bodyHashJobs.size() == 1) {
307            o = ((BodyHasher) bodyHashJobs.values().iterator().next())
308                    .getOutputStream();
309        } else {
310            o = new CompoundOutputStream(outputStreams);
311        }
312 
313        // simultaneous computation of all the hashes.
314        DKIMCommon.streamCopy(bodyInputStream, o);
315 
316        List/* SignatureRecord */verifiedSignatures = new LinkedList();
317        for (Iterator i = bodyHashJobs.values().iterator(); i.hasNext();) {
318            BodyHasher bhj = (BodyHasher) i.next();
319 
320            byte[] computedHash = bhj.getDigest();
321            byte[] expectedBodyHash = bhj.getSignatureRecord().getBodyHash();
322 
323            if (!Arrays.equals(expectedBodyHash, computedHash)) {
324                signatureExceptions
325                        .put(
326                                "DKIM-Signature:"+bhj.getSignatureRecord().toString(),
327                                new PermFailException(
328                                        "Computed bodyhash is different from the expected one"));
329            } else {
330                verifiedSignatures.add(bhj.getSignatureRecord());
331            }
332        }
333 
334        if (verifiedSignatures.size() == 0) {
335            throw prepareException(signatureExceptions);
336        } else {
337            // TODO list good and bad signatures.
338            // remove system out.
339            for (Iterator i = signatureExceptions.keySet().iterator(); i
340                    .hasNext();) {
341                String f = (String) i.next();
342                System.out.println("DKIM-Error: "
343                        + ((FailException) signatureExceptions.get(f))
344                                .getMessage() + " FIELD: " + f);
345            }
346            for (Iterator i = verifiedSignatures.iterator(); i.hasNext();) {
347                SignatureRecord sr = (SignatureRecord) i.next();
348                System.out.println("DKIM-Pass: " + sr);
349            }
350            return verifiedSignatures;
351        }
352 
353    }
354 
355    private FailException prepareException(Map signatureExceptions) {
356        if (signatureExceptions.size() == 1) {
357            return (FailException) signatureExceptions.values().iterator()
358                    .next();
359        } else {
360            // TODO loops signatureExceptions to give a more complete
361            // response, using nested exception or a compound exception.
362            // System.out.println(signatureExceptions);
363            return new PermFailException("found " + signatureExceptions.size()
364                    + " invalid signatures");
365        }
366    }
367 
368    private void signatureVerify(Headers h, SignatureRecord sign,
369            byte[] decoded, PublicKeyRecord key, List headers)
370            throws NoSuchAlgorithmException, InvalidKeyException,
371            SignatureException, PermFailException {
372 
373        Signature signature = Signature.getInstance(sign.getHashMethod()
374                .toString().toUpperCase()
375                + "with" + sign.getHashKeyType().toString().toUpperCase());
376        signature.initVerify(key.getPublicKey());
377 
378        signatureCheck(h, sign, headers, signature);
379 
380        if (!signature.verify(decoded))
381            throw new PermFailException("Header signature does not verify");
382    }
383 
384}

[all classes][org.apache.james.jdkim]
EMMA 2.0.5312 (C) Vladimir Roubtsov