Line 0
Link Here
|
|
|
1 |
/* |
2 |
* Licensed to the Apache Software Foundation (ASF) under one or more |
3 |
* contributor license agreements. See the NOTICE file distributed with |
4 |
* this work for additional information regarding copyright ownership. |
5 |
* The ASF licenses this file to You under the Apache License, Version 2.0 |
6 |
* (the "License"); you may not use this file except in compliance with |
7 |
* the License. You may obtain a copy of the License at |
8 |
* |
9 |
* http://www.apache.org/licenses/LICENSE-2.0 |
10 |
* |
11 |
* Unless required by applicable law or agreed to in writing, software |
12 |
* distributed under the License is distributed on an "AS IS" BASIS, |
13 |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
14 |
* See the License for the specific language governing permissions and |
15 |
* limitations under the License. |
16 |
* |
17 |
*/ |
18 |
|
19 |
package org.apache.jmeter.assertions; |
20 |
|
21 |
import java.io.ByteArrayInputStream; |
22 |
import java.io.FileInputStream; |
23 |
import java.io.FileNotFoundException; |
24 |
import java.io.IOException; |
25 |
import java.io.Serializable; |
26 |
import java.math.BigInteger; |
27 |
import java.security.GeneralSecurityException; |
28 |
import java.security.Security; |
29 |
import java.security.cert.CertStore; |
30 |
import java.security.cert.CertificateException; |
31 |
import java.security.cert.CertificateFactory; |
32 |
import java.security.cert.X509Certificate; |
33 |
import java.util.ArrayList; |
34 |
import java.util.Iterator; |
35 |
import java.util.List; |
36 |
import java.util.Properties; |
37 |
|
38 |
import javax.mail.MessagingException; |
39 |
import javax.mail.Session; |
40 |
import javax.mail.internet.MimeMessage; |
41 |
import javax.mail.internet.MimeMultipart; |
42 |
import javax.security.auth.x500.X500Principal; |
43 |
|
44 |
import org.apache.jmeter.samplers.SampleResult; |
45 |
import org.apache.jmeter.testelement.AbstractTestElement; |
46 |
import org.apache.jmeter.testelement.property.BooleanProperty; |
47 |
import org.apache.jmeter.testelement.property.StringProperty; |
48 |
import org.apache.jorphan.logging.LoggingManager; |
49 |
import org.apache.log.Logger; |
50 |
|
51 |
import org.bouncycastle.asn1.x509.GeneralName; |
52 |
import org.bouncycastle.cms.CMSException; |
53 |
import org.bouncycastle.cms.SignerInformation; |
54 |
import org.bouncycastle.cms.SignerInformationStore; |
55 |
import org.bouncycastle.jce.PrincipalUtil; |
56 |
import org.bouncycastle.jce.X509Principal; |
57 |
import org.bouncycastle.jce.provider.BouncyCastleProvider; |
58 |
import org.bouncycastle.mail.smime.SMIMEException; |
59 |
import org.bouncycastle.mail.smime.SMIMESignedParser; |
60 |
import org.bouncycastle.x509.extension.X509ExtensionUtil; |
61 |
|
62 |
public class SMIMEAssertion extends AbstractTestElement implements |
63 |
Serializable, Assertion { |
64 |
|
65 |
private static final Logger log = LoggingManager.getLoggerForClass(); |
66 |
|
67 |
private static final String VERIFY_SIGNATURE_KEY = "SMIMEAssertion.verifySignature"; |
68 |
|
69 |
private static final String NOT_SIGNED_KEY = "SMIMEAssertion.notSigned"; |
70 |
|
71 |
private static final String SIGNER_NO_CHECK_KEY = "SMIMEAssertion.signerNoCheck"; |
72 |
|
73 |
private static final String SIGNER_CHECK_BY_FILE_KEY = "SMIMEAssertion.signerCheckByFile"; |
74 |
|
75 |
private static final String SIGNER_CERT_FILE_KEY = "SMIMEAssertion.signerCertFile"; |
76 |
|
77 |
private static final String SINGER_CHECK_CONSTRAINTS_KEY = "SMIMEAssertion.signerCheckConstraints"; |
78 |
|
79 |
private static final String SIGNER_SERIAL_KEY = "SMIMEAssertion.signerSerial"; |
80 |
|
81 |
private static final String SIGNER_EMAIL_KEY = "SMIMEAssertion.signerEmail"; |
82 |
|
83 |
private static final String SIGNER_DN_KEY = "SMIMEAssertion.signerDn"; |
84 |
|
85 |
private static final String ISSUER_DN_KEY = "SMIMEAssertion.issuerDn"; |
86 |
|
87 |
public AssertionResult getResult(SampleResult response) { |
88 |
checkForBouncycastle(); |
89 |
|
90 |
AssertionResult res = new AssertionResult(getName()); |
91 |
|
92 |
try { |
93 |
MimeMessage msg = getMessageFromResponse(response, 0); |
94 |
SMIMESignedParser s = null; |
95 |
if (msg.isMimeType("multipart/signed")) { |
96 |
MimeMultipart multipart = (MimeMultipart) msg.getContent(); |
97 |
s = new SMIMESignedParser(multipart); |
98 |
} else if (msg.isMimeType("application/pkcs7-mime") |
99 |
|| msg.isMimeType("application/x-pkcs7-mime")) { |
100 |
s = new SMIMESignedParser(msg); |
101 |
} |
102 |
|
103 |
if (null != s) { |
104 |
|
105 |
if (isNotSigned()) { |
106 |
res.setFailure(true); |
107 |
res.setFailureMessage("Mime message is signed"); |
108 |
} else if (isVerifySignature() || !isSignerNoCheck()) { |
109 |
res = verifySignature(s); |
110 |
} |
111 |
|
112 |
} else { |
113 |
if (!isNotSigned()) { |
114 |
res.setFailure(true); |
115 |
res.setFailureMessage("Mime message is not signed"); |
116 |
} |
117 |
} |
118 |
|
119 |
} catch (MessagingException e) { |
120 |
String msg = "Cannot parse mime msg: " + e.getMessage(); |
121 |
log.warn(msg, e); |
122 |
res.setFailure(true); |
123 |
res.setFailureMessage(msg); |
124 |
} catch (CMSException e) { |
125 |
res.setFailure(true); |
126 |
res.setFailureMessage("Error reading the signature: " |
127 |
+ e.getMessage()); |
128 |
} catch (SMIMEException e) { |
129 |
res.setFailure(true); |
130 |
res.setFailureMessage("Cannot extract signed body part from signature: " |
131 |
+ e.getMessage()); |
132 |
} catch (IOException e) { // should never happen |
133 |
log.error("Cannot read mime message content: " + e.getMessage(), e); |
134 |
res.setError(true); |
135 |
res.setFailureMessage(e.getMessage()); |
136 |
} |
137 |
|
138 |
return res; |
139 |
} |
140 |
|
141 |
private AssertionResult verifySignature(SMIMESignedParser s) |
142 |
throws CMSException { |
143 |
AssertionResult res = new AssertionResult(getName()); |
144 |
|
145 |
try { |
146 |
CertStore certs = s.getCertificatesAndCRLs("Collection", "BC"); |
147 |
SignerInformationStore signers = s.getSignerInfos(); |
148 |
Iterator signerIt = signers.getSigners().iterator(); |
149 |
|
150 |
if (signerIt.hasNext()) { |
151 |
|
152 |
SignerInformation signer = (SignerInformation) signerIt.next(); |
153 |
Iterator certIt = certs.getCertificates(signer.getSID()) |
154 |
.iterator(); |
155 |
|
156 |
if (certIt.hasNext()) { |
157 |
// the signer certificate |
158 |
X509Certificate cert = (X509Certificate) certIt.next(); |
159 |
|
160 |
if (isVerifySignature()) { |
161 |
|
162 |
if (!signer.verify(cert.getPublicKey(), "BC")) { |
163 |
res.setFailure(true); |
164 |
res.setFailureMessage("Signature is invalid"); |
165 |
} |
166 |
} |
167 |
|
168 |
if (isSignerCheckConstraints()) { |
169 |
StringBuffer failureMessage = new StringBuffer(); |
170 |
|
171 |
String serial = getSignerSerial(); |
172 |
if (serial.trim().length() > 0) { |
173 |
BigInteger serialNbr = readSerialNumber(serial); |
174 |
if (!serialNbr.equals(cert.getSerialNumber())) { |
175 |
res.setFailure(true); |
176 |
failureMessage |
177 |
.append("Serial number ") |
178 |
.append(serialNbr) |
179 |
.append( |
180 |
" does not match serial from signer certificate: ") |
181 |
.append(cert.getSerialNumber()) |
182 |
.append("\n"); |
183 |
} |
184 |
} |
185 |
|
186 |
String email = getSignerEmail(); |
187 |
if (email.trim().length() > 0) { |
188 |
List emailfromCert = getEmailFromCert(cert); |
189 |
if (!emailfromCert.contains(email)) { |
190 |
res.setFailure(true); |
191 |
failureMessage |
192 |
.append("Email address \"") |
193 |
.append(email) |
194 |
.append( |
195 |
"\" not present in signer certificate\n"); |
196 |
} |
197 |
|
198 |
} |
199 |
|
200 |
String subject = getSignerDn(); |
201 |
if (subject.length() > 0) { |
202 |
X500Principal principal = new X500Principal(subject); |
203 |
if (!principal.equals(cert |
204 |
.getSubjectX500Principal())) { |
205 |
res.setFailure(true); |
206 |
failureMessage |
207 |
.append( |
208 |
"Distinguished name of signer certificate does not match \"") |
209 |
.append(subject).append("\"\n"); |
210 |
} |
211 |
} |
212 |
|
213 |
String issuer = getIssuerDn(); |
214 |
if (issuer.length() > 0) { |
215 |
X500Principal principal = new X500Principal(issuer); |
216 |
if (!principal |
217 |
.equals(cert.getIssuerX500Principal())) { |
218 |
res.setFailure(true); |
219 |
failureMessage |
220 |
.append( |
221 |
"Issuer distinguished name of signer certificate does not match \"") |
222 |
.append(subject).append("\"\n"); |
223 |
} |
224 |
} |
225 |
|
226 |
if (failureMessage.length() > 0) { |
227 |
res.setFailureMessage(failureMessage.toString()); |
228 |
} |
229 |
} |
230 |
|
231 |
if (isSignerCheckByFile()) { |
232 |
CertificateFactory cf = CertificateFactory |
233 |
.getInstance("X.509"); |
234 |
X509Certificate certFromFile = (X509Certificate) cf |
235 |
.generateCertificate(new FileInputStream( |
236 |
getSignerCertFile())); |
237 |
|
238 |
if (!certFromFile.equals(cert)) { |
239 |
res.setFailure(true); |
240 |
res.setFailureMessage("Signer certificate does not match certificate " |
241 |
+ getSignerCertFile()); |
242 |
} |
243 |
} |
244 |
|
245 |
} else { |
246 |
res.setFailure(true); |
247 |
res.setFailureMessage("No signer certificate found in signature"); |
248 |
} |
249 |
|
250 |
} |
251 |
|
252 |
// TODO support multiple signers |
253 |
if (signerIt.hasNext()) { |
254 |
log.warn("SMIME message contains multiple signers! Checking multiple signers is not supported."); |
255 |
} |
256 |
|
257 |
} catch (GeneralSecurityException e) { |
258 |
log.error(e.getMessage(), e); |
259 |
res.setError(true); |
260 |
res.setFailureMessage(e.getMessage()); |
261 |
} catch (FileNotFoundException e) { |
262 |
res.setFailure(true); |
263 |
res.setFailureMessage("certificate file not found: " |
264 |
+ e.getMessage()); |
265 |
} |
266 |
|
267 |
return res; |
268 |
} |
269 |
|
270 |
public boolean isVerifySignature() { |
271 |
return getPropertyAsBoolean(VERIFY_SIGNATURE_KEY); |
272 |
} |
273 |
|
274 |
public void setVerifySignature(boolean verifySignature) { |
275 |
setProperty(new BooleanProperty(VERIFY_SIGNATURE_KEY, verifySignature)); |
276 |
} |
277 |
|
278 |
public String getIssuerDn() { |
279 |
return getPropertyAsString(ISSUER_DN_KEY); |
280 |
} |
281 |
|
282 |
public void setIssuerDn(String issuertDn) { |
283 |
setProperty(new StringProperty(ISSUER_DN_KEY, issuertDn)); |
284 |
} |
285 |
|
286 |
/** |
287 |
* extracts a MIME message from the SampleResult |
288 |
*/ |
289 |
private MimeMessage getMessageFromResponse(SampleResult response, |
290 |
int messageNumber) throws MessagingException { |
291 |
byte[] data = response.getResponseData(); |
292 |
Session session = Session.getDefaultInstance(new Properties()); |
293 |
MimeMessage msg = new MimeMessage(session, new ByteArrayInputStream( |
294 |
data)); |
295 |
|
296 |
log.debug("msg.getSize() = " + msg.getSize()); |
297 |
return msg; |
298 |
} |
299 |
|
300 |
/** |
301 |
* Convert the value of <code>serialString</code> into a BigInteger. |
302 |
* Strings starting with 0x or 0X are parsed as hex numbers, otherwise as |
303 |
* decimal number. |
304 |
* |
305 |
* @param serialString |
306 |
* the String representation of the serial Number |
307 |
* @return |
308 |
*/ |
309 |
private BigInteger readSerialNumber(String serialString) { |
310 |
if (serialString.startsWith("0x") || serialString.startsWith("0X")) { |
311 |
return new BigInteger(serialString.substring(2), 16); |
312 |
} else { |
313 |
return new BigInteger(serialString); |
314 |
} |
315 |
} |
316 |
|
317 |
/** |
318 |
* Extract email addresses from a certificate |
319 |
* |
320 |
* @param cert |
321 |
* @return a List of all email addresses found |
322 |
* @throws CertificateException |
323 |
*/ |
324 |
private List getEmailFromCert(X509Certificate cert) |
325 |
throws CertificateException { |
326 |
List res = new ArrayList(); |
327 |
|
328 |
X509Principal subject = PrincipalUtil.getSubjectX509Principal(cert); |
329 |
Iterator addressIt = subject.getValues(X509Principal.EmailAddress) |
330 |
.iterator(); |
331 |
while (addressIt.hasNext()) { |
332 |
String address = (String) addressIt.next(); |
333 |
res.add(address); |
334 |
} |
335 |
|
336 |
Iterator subjectAltNamesIt = X509ExtensionUtil |
337 |
.getSubjectAlternativeNames(cert).iterator(); |
338 |
while (subjectAltNamesIt.hasNext()) { |
339 |
List altName = (List) subjectAltNamesIt.next(); |
340 |
int type = ((Integer) altName.get(0)).intValue(); |
341 |
if (type == GeneralName.rfc822Name) { |
342 |
String address = (String) altName.get(1); |
343 |
res.add(address); |
344 |
} |
345 |
} |
346 |
|
347 |
return res; |
348 |
} |
349 |
|
350 |
/** |
351 |
* Check if the Bouncycastle jce provider is installed and dynamically load |
352 |
* it, if needed; |
353 |
* |
354 |
*/ |
355 |
private static void checkForBouncycastle() { |
356 |
if (null == Security.getProvider("BC")) { |
357 |
Security.addProvider(new BouncyCastleProvider()); |
358 |
} |
359 |
} |
360 |
|
361 |
public boolean isSignerCheckByFile() { |
362 |
return getPropertyAsBoolean(SIGNER_CHECK_BY_FILE_KEY); |
363 |
} |
364 |
|
365 |
public void setSignerCheckByFile(boolean signerCheckByFile) { |
366 |
setProperty(new BooleanProperty(SIGNER_CHECK_BY_FILE_KEY, |
367 |
signerCheckByFile)); |
368 |
} |
369 |
|
370 |
public boolean isSignerCheckConstraints() { |
371 |
return getPropertyAsBoolean(SINGER_CHECK_CONSTRAINTS_KEY); |
372 |
} |
373 |
|
374 |
public void setSignerCheckConstraints(boolean signerCheckConstraints) { |
375 |
setProperty(new BooleanProperty(SINGER_CHECK_CONSTRAINTS_KEY, |
376 |
signerCheckConstraints)); |
377 |
} |
378 |
|
379 |
public boolean isSignerNoCheck() { |
380 |
return getPropertyAsBoolean(SIGNER_NO_CHECK_KEY); |
381 |
} |
382 |
|
383 |
public void setSignerNoCheck(boolean signerNoCheck) { |
384 |
setProperty(new BooleanProperty(SIGNER_NO_CHECK_KEY, signerNoCheck)); |
385 |
} |
386 |
|
387 |
public String getSignerCertFile() { |
388 |
return getPropertyAsString(SIGNER_CERT_FILE_KEY); |
389 |
} |
390 |
|
391 |
public void setSignerCertFile(String signerCertFile) { |
392 |
setProperty(new StringProperty(SIGNER_CERT_FILE_KEY, signerCertFile)); |
393 |
} |
394 |
|
395 |
public String getSignerDn() { |
396 |
return getPropertyAsString(SIGNER_DN_KEY); |
397 |
} |
398 |
|
399 |
public void setSignerDn(String signerDn) { |
400 |
setProperty(new StringProperty(SIGNER_DN_KEY, signerDn)); |
401 |
} |
402 |
|
403 |
public String getSignerSerial() { |
404 |
return getPropertyAsString(SIGNER_SERIAL_KEY); |
405 |
} |
406 |
|
407 |
public void setSignerSerial(String signerSerial) { |
408 |
setProperty(new StringProperty(SIGNER_SERIAL_KEY, signerSerial)); |
409 |
} |
410 |
|
411 |
public String getSignerEmail() { |
412 |
return getPropertyAsString(SIGNER_EMAIL_KEY); |
413 |
} |
414 |
|
415 |
public void setSignerEmail(String signerEmail) { |
416 |
setProperty(new StringProperty(SIGNER_EMAIL_KEY, signerEmail)); |
417 |
} |
418 |
|
419 |
public boolean isNotSigned() { |
420 |
return getPropertyAsBoolean(NOT_SIGNED_KEY); |
421 |
} |
422 |
|
423 |
public void setNotSigned(boolean notSigned) { |
424 |
setProperty(new BooleanProperty(NOT_SIGNED_KEY, notSigned)); |
425 |
} |
426 |
|
427 |
} |