View Javadoc

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  
20  package org.apache.james.jdkim;
21  
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.io.OutputStream;
25  import java.security.InvalidKeyException;
26  import java.security.NoSuchAlgorithmException;
27  import java.security.Signature;
28  import java.security.SignatureException;
29  import java.util.Arrays;
30  import java.util.HashMap;
31  import java.util.Hashtable;
32  import java.util.Iterator;
33  import java.util.LinkedList;
34  import java.util.List;
35  import java.util.Map;
36  
37  import org.apache.james.jdkim.api.BodyHasher;
38  import org.apache.james.jdkim.api.Headers;
39  import org.apache.james.jdkim.api.PublicKeyRecord;
40  import org.apache.james.jdkim.api.PublicKeyRecordRetriever;
41  import org.apache.james.jdkim.api.SignatureRecord;
42  import org.apache.james.jdkim.canon.CompoundOutputStream;
43  import org.apache.james.jdkim.exceptions.FailException;
44  import org.apache.james.jdkim.exceptions.PermFailException;
45  import org.apache.james.jdkim.exceptions.TempFailException;
46  import org.apache.james.jdkim.impl.BodyHasherImpl;
47  import org.apache.james.jdkim.impl.DNSPublicKeyRecordRetriever;
48  import org.apache.james.jdkim.impl.Message;
49  import org.apache.james.jdkim.impl.MultiplexingPublicKeyRecordRetriever;
50  import org.apache.james.jdkim.tagvalue.PublicKeyRecordImpl;
51  import org.apache.james.jdkim.tagvalue.SignatureRecordImpl;
52  import org.apache.james.mime4j.MimeException;
53  
54  public 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 }