1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
97
98
99 return pk;
100 } catch (IllegalStateException e) {
101
102 lastError = "invalid key for signature: " + e.getMessage();
103 }
104 }
105 }
106
107 throw new PermFailException(lastError);
108 }
109
110
111
112
113
114
115
116
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
147
148
149
150
151
152
153
154 public PublicKeyRecord publicRecordLookup(SignatureRecord sign)
155 throws TempFailException, PermFailException {
156
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
169
170
171
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
186
187 else
188 throw new PermFailException(
189 "no key for signature [unexpected condition]");
190 }
191 return key;
192 }
193
194
195
196
197
198
199
200
201
202
203
204
205 public List
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
221
222
223
224
225
226
227
228
229
230
231
232 public List
233 InputStream bodyInputStream) throws IOException, FailException {
234
235 List fields = messageHeaders.getFields("DKIM-Signature");
236
237 if (fields.size() == 0) {
238 throw new PermFailException("DKIM-Signature field not found");
239 }
240
241
242
243
244 Map
245 List
246 Map
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
258 signatureRecord.validate();
259 } catch (IllegalStateException e) {
260 throw new PermFailException(e.getMessage());
261 }
262
263
264
265
266
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
276
277
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
314 DKIMCommon.streamCopy(bodyInputStream, o);
315
316 List
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
338
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
361
362
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 }