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.mailets; |
21 | |
22 | import java.io.ByteArrayInputStream; |
23 | import java.io.IOException; |
24 | import java.security.GeneralSecurityException; |
25 | import java.security.NoSuchAlgorithmException; |
26 | import java.security.PrivateKey; |
27 | import java.security.spec.InvalidKeySpecException; |
28 | import java.util.Enumeration; |
29 | import java.util.HashMap; |
30 | import java.util.Iterator; |
31 | import java.util.LinkedList; |
32 | import java.util.List; |
33 | import java.util.Map; |
34 | |
35 | import javax.mail.Header; |
36 | import javax.mail.MessagingException; |
37 | import javax.mail.internet.MimeMessage; |
38 | |
39 | import org.apache.commons.ssl.PKCS8Key; |
40 | import org.apache.james.jdkim.DKIMSigner; |
41 | import org.apache.james.jdkim.api.BodyHasher; |
42 | import org.apache.james.jdkim.api.Headers; |
43 | import org.apache.james.jdkim.api.SignatureRecord; |
44 | import org.apache.james.jdkim.exceptions.PermFailException; |
45 | import org.apache.mailet.Mail; |
46 | import org.apache.mailet.base.GenericMailet; |
47 | |
48 | /** |
49 | * This mailet sign a message using the DKIM protocol |
50 | * If the privateKey is encoded using a password then you can pass |
51 | * the password as privateKeyPassword parameter. |
52 | * |
53 | * Sample configuration: |
54 | * |
55 | * <pre><code> |
56 | * <mailet match="All" class="DKIMSign"> |
57 | * <signatureTemplate>v=1; s=selector; d=example.com; h=from:to:received:received; a=rsa-sha256; bh=; b=;</signatureTemplate> |
58 | * <privateKey> |
59 | * -----BEGIN RSA PRIVATE KEY----- |
60 | * MIICXAIBAAKBgQDYDaYKXzwVYwqWbLhmuJ66aTAN8wmDR+rfHE8HfnkSOax0oIoT |
61 | * M5zquZrTLo30870YMfYzxwfB6j/Nz3QdwrUD/t0YMYJiUKyWJnCKfZXHJBJ+yfRH |
62 | * r7oW+UW3cVo9CG2bBfIxsInwYe175g9UjyntJpWueqdEIo1c2bhv9Mp66QIDAQAB |
63 | * AoGBAI8XcwnZi0Sq5N89wF+gFNhnREFo3rsJDaCY8iqHdA5DDlnr3abb/yhipw0I |
64 | * /1HlgC6fIG2oexXOXFWl+USgqRt1kTt9jXhVFExg8mNko2UelAwFtsl8CRjVcYQO |
65 | * cedeH/WM/mXjg2wUqqZenBmlKlD6vNb70jFJeVaDJ/7n7j8BAkEA9NkH2D4Zgj/I |
66 | * OAVYccZYH74+VgO0e7VkUjQk9wtJ2j6cGqJ6Pfj0roVIMUWzoBb8YfErR8l6JnVQ |
67 | * bfy83gJeiQJBAOHk3ow7JjAn8XuOyZx24KcTaYWKUkAQfRWYDFFOYQF4KV9xLSEt |
68 | * ycY0kjsdxGKDudWcsATllFzXDCQF6DTNIWECQEA52ePwTjKrVnLTfCLEG4OgHKvl |
69 | * Zud4amthwDyJWoMEH2ChNB2je1N4JLrABOE+hk+OuoKnKAKEjWd8f3Jg/rkCQHj8 |
70 | * mQmogHqYWikgP/FSZl518jV48Tao3iXbqvU9Mo2T6yzYNCCqIoDLFWseNVnCTZ0Q |
71 | * b+IfiEf1UeZVV5o4J+ECQDatNnS3V9qYUKjj/krNRD/U0+7eh8S2ylLqD3RlSn9K |
72 | * tYGRMgAtUXtiOEizBH6bd/orzI9V9sw8yBz+ZqIH25Q= |
73 | * -----END RSA PRIVATE KEY----- |
74 | * </privateKey> |
75 | * </mailet> |
76 | * </code></pre> |
77 | * |
78 | * @version CVS $Revision: 713949 $ $Date: 2008-11-14 08:40:21 +0100 (ven, 14 |
79 | * nov 2008) $ |
80 | * @since 2.2.0 |
81 | */ |
82 | public class DKIMSign extends GenericMailet { |
83 | |
84 | /** |
85 | * An adapter to let DKIMSigner read headers from MimeMessage |
86 | */ |
87 | private final class MimeMessageHeaders implements Headers { |
88 | |
89 | private Map/* String, String */headers; |
90 | private List/* String */fields; |
91 | |
92 | public MimeMessageHeaders(MimeMessage message) |
93 | throws MessagingException { |
94 | headers = new HashMap(); |
95 | fields = new LinkedList(); |
96 | for (Enumeration e = message.getAllHeaderLines(); e |
97 | .hasMoreElements();) { |
98 | String head = (String) e.nextElement(); |
99 | int p = head.indexOf(':'); |
100 | if (p <= 0) |
101 | throw new MessagingException("Bad header line: " + head); |
102 | String headerName = head.substring(0, p).trim(); |
103 | String headerNameLC = headerName.toLowerCase(); |
104 | fields.add(headerName); |
105 | List/* String */strings = (List) headers.get(headerNameLC); |
106 | if (strings == null) { |
107 | strings = new LinkedList(); |
108 | headers.put(headerNameLC, strings); |
109 | } |
110 | strings.add(head); |
111 | } |
112 | } |
113 | |
114 | public List getFields() { |
115 | return fields; |
116 | } |
117 | |
118 | public List getFields(String name) { |
119 | return (List) headers.get(name); |
120 | } |
121 | } |
122 | |
123 | private String signatureTemplate; |
124 | private PrivateKey privateKey; |
125 | |
126 | public void init() throws MessagingException { |
127 | signatureTemplate = getInitParameter("signatureTemplate"); |
128 | String privateKeyString = getInitParameter("privateKey"); |
129 | String privateKeyPassword = getInitParameter("privateKeyPassword", null); |
130 | try { |
131 | PKCS8Key pkcs8 = new PKCS8Key(new ByteArrayInputStream( |
132 | privateKeyString.getBytes()), |
133 | privateKeyPassword != null ? privateKeyPassword |
134 | .toCharArray() : null); |
135 | privateKey = pkcs8.getPrivateKey(); |
136 | // privateKey = DKIMSigner.getPrivateKey(privateKeyString); |
137 | } catch (NoSuchAlgorithmException e) { |
138 | throw new MessagingException("Unknown private key algorythm: " |
139 | + e.getMessage(), e); |
140 | } catch (InvalidKeySpecException e) { |
141 | throw new MessagingException( |
142 | "PrivateKey should be in base64 encoded PKCS8 (der) format: " |
143 | + e.getMessage(), e); |
144 | } catch (GeneralSecurityException e) { |
145 | throw new MessagingException("General security exception: " |
146 | + e.getMessage(), e); |
147 | } |
148 | } |
149 | |
150 | public void service(Mail mail) throws MessagingException { |
151 | DKIMSigner signer = new DKIMSigner(signatureTemplate, privateKey); |
152 | SignatureRecord signRecord = signer |
153 | .newSignatureRecordTemplate(signatureTemplate); |
154 | try { |
155 | BodyHasher bhj = signer.newBodyHasher(signRecord); |
156 | MimeMessage message = mail.getMessage(); |
157 | Headers headers = new MimeMessageHeaders(message); |
158 | try { |
159 | message.writeTo(new HeaderSkippingOutputStream(bhj |
160 | .getOutputStream())); |
161 | bhj.getOutputStream().close(); |
162 | } catch (IOException e) { |
163 | throw new MessagingException("Exception calculating bodyhash: " |
164 | + e.getMessage(), e); |
165 | } |
166 | String signatureHeader = signer.sign(headers, bhj); |
167 | // Unfortunately JavaMail does not give us a method to add headers |
168 | // on top. |
169 | // message.addHeaderLine(signatureHeader); |
170 | prependHeader(message, signatureHeader); |
171 | } catch (PermFailException e) { |
172 | throw new MessagingException("PermFail while signing: " |
173 | + e.getMessage(), e); |
174 | } |
175 | |
176 | } |
177 | |
178 | private void prependHeader(MimeMessage message, String signatureHeader) |
179 | throws MessagingException { |
180 | List prevHeader = new LinkedList(); |
181 | // read all the headers |
182 | for (Enumeration e = message.getAllHeaderLines(); e.hasMoreElements();) { |
183 | String headerLine = (String) e.nextElement(); |
184 | prevHeader.add(headerLine); |
185 | } |
186 | // remove all the headers |
187 | for (Enumeration e = message.getAllHeaders(); e.hasMoreElements();) { |
188 | Header header = (Header) e.nextElement(); |
189 | message.removeHeader(header.getName()); |
190 | } |
191 | // add our header |
192 | message.addHeaderLine(signatureHeader); |
193 | // add the remaining headers using "addHeaderLine" that won't alter the |
194 | // insertion order. |
195 | for (Iterator i = prevHeader.iterator(); i.hasNext();) { |
196 | String header = (String) i.next(); |
197 | message.addHeaderLine(header); |
198 | } |
199 | } |
200 | |
201 | } |