You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

CertificateManager.java 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. /*
  2. * Copyright (c) 2006-2011 DMDirc Developers
  3. *
  4. * Permission is hereby granted, free of charge, to any person obtaining a copy
  5. * of this software and associated documentation files (the "Software"), to deal
  6. * in the Software without restriction, including without limitation the rights
  7. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  8. * copies of the Software, and to permit persons to whom the Software is
  9. * furnished to do so, subject to the following conditions:
  10. *
  11. * The above copyright notice and this permission notice shall be included in
  12. * all copies or substantial portions of the Software.
  13. *
  14. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  19. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  20. * SOFTWARE.
  21. */
  22. package com.dmdirc.tls;
  23. import com.dmdirc.config.ConfigManager;
  24. import com.dmdirc.config.IdentityManager;
  25. import com.dmdirc.logger.ErrorLevel;
  26. import com.dmdirc.logger.Logger;
  27. import com.dmdirc.util.collections.ListenerList;
  28. import com.dmdirc.util.io.StreamUtils;
  29. import java.io.File;
  30. import java.io.FileInputStream;
  31. import java.io.FileNotFoundException;
  32. import java.io.IOException;
  33. import java.security.InvalidAlgorithmParameterException;
  34. import java.security.KeyStore;
  35. import java.security.KeyStoreException;
  36. import java.security.NoSuchAlgorithmException;
  37. import java.security.UnrecoverableKeyException;
  38. import java.security.cert.CertificateException;
  39. import java.security.cert.CertificateParsingException;
  40. import java.security.cert.PKIXParameters;
  41. import java.security.cert.TrustAnchor;
  42. import java.security.cert.X509Certificate;
  43. import java.util.ArrayList;
  44. import java.util.Arrays;
  45. import java.util.Collection;
  46. import java.util.HashMap;
  47. import java.util.HashSet;
  48. import java.util.List;
  49. import java.util.Map;
  50. import java.util.Set;
  51. import java.util.concurrent.Semaphore;
  52. import javax.naming.InvalidNameException;
  53. import javax.naming.ldap.LdapName;
  54. import javax.naming.ldap.Rdn;
  55. import javax.net.ssl.KeyManager;
  56. import javax.net.ssl.KeyManagerFactory;
  57. import javax.net.ssl.X509TrustManager;
  58. import net.miginfocom.Base64;
  59. /**
  60. * Manages storage and validation of certificates used when connecting to
  61. * SSL servers.
  62. *
  63. * @since 0.6.3m1
  64. */
  65. public class CertificateManager implements X509TrustManager {
  66. /** List of listeners. */
  67. private final ListenerList listeners = new ListenerList();
  68. /** The server name the user is trying to connect to. */
  69. private final String serverName;
  70. /** The configuration manager to use for settings. */
  71. private final ConfigManager config;
  72. /** The set of CAs from the global cacert file. */
  73. private final Set<X509Certificate> globalTrustedCAs = new HashSet<X509Certificate>();
  74. /** Whether or not to check specified parts of the certificate. */
  75. private boolean checkDate, checkIssuer, checkHost;
  76. /** Used to synchronise the manager with the certificate dialog. */
  77. private final Semaphore actionSem = new Semaphore(0);
  78. /** The action to perform. */
  79. private CertificateAction action;
  80. /** A list of problems encountered most recently. */
  81. private final List<CertificateException> problems = new ArrayList<CertificateException>();
  82. /** The chain of certificates currently being validated. */
  83. private X509Certificate[] chain;
  84. /**
  85. * Creates a new certificate manager for a client connecting to the
  86. * specified server.
  87. *
  88. * @param serverName The name the user used to connect to the server
  89. * @param config The configuration manager to use
  90. */
  91. public CertificateManager(final String serverName, final ConfigManager config) {
  92. this.serverName = serverName;
  93. this.config = config;
  94. this.checkDate = config.getOptionBool("ssl", "checkdate");
  95. this.checkIssuer = config.getOptionBool("ssl", "checkissuer");
  96. this.checkHost = config.getOptionBool("ssl", "checkhost");
  97. loadTrustedCAs();
  98. }
  99. /**
  100. * Loads the trusted CA certificates from the Java cacerts store.
  101. */
  102. protected void loadTrustedCAs() {
  103. FileInputStream is = null;
  104. try {
  105. final String filename = System.getProperty("java.home")
  106. + "/lib/security/cacerts".replace('/', File.separatorChar);
  107. is = new FileInputStream(filename);
  108. final KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
  109. keystore.load(is, null);
  110. final PKIXParameters params = new PKIXParameters(keystore);
  111. for (TrustAnchor anchor : params.getTrustAnchors()) {
  112. globalTrustedCAs.add(anchor.getTrustedCert());
  113. }
  114. } catch (CertificateException ex) {
  115. Logger.userError(ErrorLevel.MEDIUM, "Unable to load trusted certificates", ex);
  116. } catch (IOException ex) {
  117. Logger.userError(ErrorLevel.MEDIUM, "Unable to load trusted certificates", ex);
  118. } catch (InvalidAlgorithmParameterException ex) {
  119. Logger.userError(ErrorLevel.MEDIUM, "Unable to load trusted certificates", ex);
  120. } catch (KeyStoreException ex) {
  121. Logger.userError(ErrorLevel.MEDIUM, "Unable to load trusted certificates", ex);
  122. } catch (NoSuchAlgorithmException ex) {
  123. Logger.userError(ErrorLevel.MEDIUM, "Unable to load trusted certificates", ex);
  124. } finally {
  125. StreamUtils.close(is);
  126. }
  127. }
  128. /**
  129. * Retrieves a KeyManager[] for the client certificate specified in the
  130. * configuration, if there is one.
  131. *
  132. * @return A KeyManager to use for the SSL connection
  133. */
  134. public KeyManager[] getKeyManager() {
  135. if (config.hasOptionString("ssl", "clientcert.file")) {
  136. FileInputStream fis = null;
  137. try {
  138. final char[] pass;
  139. if (config.hasOptionString("ssl", "clientcert.pass")) {
  140. pass = config.getOption("ssl", "clientcert.pass").toCharArray();
  141. } else {
  142. pass = null;
  143. }
  144. fis = new FileInputStream(config.getOption("ssl", "clientcert.file"));
  145. final KeyStore ks = KeyStore.getInstance("pkcs12");
  146. ks.load(fis, pass);
  147. final KeyManagerFactory kmf = KeyManagerFactory.getInstance(
  148. KeyManagerFactory.getDefaultAlgorithm());
  149. kmf.init(ks, pass);
  150. return kmf.getKeyManagers();
  151. } catch (FileNotFoundException ex) {
  152. Logger.userError(ErrorLevel.MEDIUM, "Certificate file not found", ex);
  153. } catch (KeyStoreException ex) {
  154. Logger.appError(ErrorLevel.MEDIUM, "Unable to get key manager", ex);
  155. } catch (IOException ex) {
  156. Logger.userError(ErrorLevel.MEDIUM, "Unable to get key manager", ex);
  157. } catch (CertificateException ex) {
  158. Logger.appError(ErrorLevel.MEDIUM, "Unable to get key manager", ex);
  159. } catch (NoSuchAlgorithmException ex) {
  160. Logger.appError(ErrorLevel.MEDIUM, "Unable to get key manager", ex);
  161. } catch (UnrecoverableKeyException ex) {
  162. Logger.appError(ErrorLevel.MEDIUM, "Unable to get key manager", ex);
  163. } finally {
  164. StreamUtils.close(fis);
  165. }
  166. }
  167. return null;
  168. }
  169. /** {@inheritDoc} */
  170. @Override
  171. public void checkClientTrusted(final X509Certificate[] chain, final String authType)
  172. throws CertificateException {
  173. throw new CertificateException("Not supported.");
  174. }
  175. /**
  176. * Determines if the specified certificate is trusted by the user.
  177. *
  178. * @param certificate The certificate to be checked
  179. * @return True if the certificate matches one in the trusted certificate
  180. * store, or if the certificate's details are marked as trusted in the
  181. * DMDirc configuration file.
  182. */
  183. public TrustResult isTrusted(final X509Certificate certificate) {
  184. try {
  185. final String sig = Base64.encodeToString(certificate.getSignature(), false);
  186. if (config.hasOptionString("ssl", "trusted") && config.getOptionList("ssl",
  187. "trusted").contains(sig)) {
  188. return TrustResult.TRUSTED_MANUALLY;
  189. } else {
  190. for (X509Certificate trustedCert : globalTrustedCAs) {
  191. if (Arrays.equals(certificate.getSignature(), trustedCert.getSignature())
  192. && certificate.getIssuerDN().getName()
  193. .equals(trustedCert.getIssuerDN().getName())) {
  194. certificate.verify(trustedCert.getPublicKey());
  195. return TrustResult.TRUSTED_CA;
  196. }
  197. }
  198. }
  199. } catch (Exception ex) {
  200. return TrustResult.UNTRUSTED_EXCEPTION;
  201. }
  202. return TrustResult.UNTRUSTED_GENERAL;
  203. }
  204. /**
  205. * Determines whether the given certificate has a valid CN or alternate
  206. * name for this server's hostname.
  207. *
  208. * @param certificate The certificate to be validated
  209. * @return True if the certificate is valid for this server's host, false
  210. * otherwise
  211. */
  212. public boolean isValidHost(final X509Certificate certificate) {
  213. final Map<String, String> fields = getDNFieldsFromCert(certificate);
  214. if (fields.containsKey("CN") && isMatchingServerName(fields.get("CN"))) {
  215. return true;
  216. }
  217. try {
  218. if (certificate.getSubjectAlternativeNames() != null) {
  219. for (List<?> entry : certificate.getSubjectAlternativeNames()) {
  220. final int type = ((Integer) entry.get(0)).intValue();
  221. // DNS or IP
  222. if ((type == 2 || type == 7) && isMatchingServerName((String) entry.get(1))) {
  223. return true;
  224. }
  225. }
  226. }
  227. } catch (CertificateParsingException ex) {
  228. return false;
  229. }
  230. return false;
  231. }
  232. /**
  233. * Checks whether the specified target matches the server name this
  234. * certificate manager was initialised with.
  235. *
  236. * Target names may contain wildcards per RFC2818.
  237. *
  238. * @since 0.6.5
  239. * @param target The target to compare to our server name
  240. * @return True if the target matches, false otherwise
  241. */
  242. protected boolean isMatchingServerName(final String target) {
  243. final String[] targetParts = target.split("\\.");
  244. final String[] serverParts = serverName.split("\\.");
  245. if (targetParts.length != serverParts.length) {
  246. // Fail fast if they don't match
  247. return false;
  248. }
  249. for (int i = 0; i < serverParts.length; i++) {
  250. if (!serverParts[i].matches("\\Q" + targetParts[i].replace("*", "\\E.*\\Q") + "\\E")) {
  251. return false;
  252. }
  253. }
  254. return true;
  255. }
  256. /** {@inheritDoc} */
  257. @Override
  258. public void checkServerTrusted(final X509Certificate[] chain, final String authType)
  259. throws CertificateException {
  260. this.chain = chain;
  261. problems.clear();
  262. boolean verified = false;
  263. boolean manual = false;
  264. if (checkHost) {
  265. // Check that the cert is issued to the correct host
  266. verified = isValidHost(chain[0]);
  267. if (!verified) {
  268. problems.add(new CertificateDoesntMatchHostException(
  269. "Certificate was not issued to " + serverName));
  270. }
  271. verified = false;
  272. }
  273. for (X509Certificate cert : chain) {
  274. final TrustResult trustResult = isTrusted(cert);
  275. if (checkDate) {
  276. // Check that the certificate is in-date
  277. try {
  278. cert.checkValidity();
  279. } catch (CertificateException ex) {
  280. problems.add(ex);
  281. }
  282. }
  283. if (checkIssuer) {
  284. // Check that we trust an issuer
  285. verified |= trustResult.isTrusted();
  286. }
  287. if (trustResult == TrustResult.TRUSTED_MANUALLY) {
  288. manual = true;
  289. }
  290. }
  291. if (!verified && checkIssuer) {
  292. problems.add(new CertificateNotTrustedException("Issuer is not trusted"));
  293. }
  294. if (!problems.isEmpty() && !manual) {
  295. for (CertificateProblemListener listener : listeners.get(CertificateProblemListener.class)) {
  296. listener.certificateProblemEncountered(chain, problems, this);
  297. }
  298. try {
  299. actionSem.acquire();
  300. } catch (InterruptedException ie) {
  301. throw new CertificateException("Thread aborted", ie);
  302. } finally {
  303. problems.clear();
  304. for (CertificateProblemListener listener : listeners.get(CertificateProblemListener.class)) {
  305. listener.certificateProblemResolved(this);
  306. }
  307. }
  308. switch (action) {
  309. case DISCONNECT:
  310. throw new CertificateException("Not trusted");
  311. case IGNORE_PERMANENTY:
  312. final List<String> list = new ArrayList<String>(config
  313. .getOptionList("ssl", "trusted"));
  314. list.add(Base64.encodeToString(chain[0].getSignature(), false));
  315. IdentityManager.getConfigIdentity().setOption("ssl",
  316. "trusted", list);
  317. break;
  318. case IGNORE_TEMPORARILY:
  319. // Do nothing, continue connecting
  320. break;
  321. }
  322. }
  323. if (manual) {
  324. problems.clear();
  325. }
  326. }
  327. /**
  328. * Gets the chain of certificates currently being validated, if any.
  329. *
  330. * @return The chain of certificates being validated
  331. */
  332. public X509Certificate[] getChain() {
  333. return chain;
  334. }
  335. /**
  336. * Gets the set of problems that were encountered with the last certificate.
  337. *
  338. * @return The set of problems encountered, or any empty collection if there
  339. * is no current validation attempt ongoing.
  340. */
  341. public Collection<CertificateException> getProblems() {
  342. return problems;
  343. }
  344. /**
  345. * Sets the action to perform for the request that's in progress.
  346. *
  347. * @param action The action that's been selected
  348. */
  349. public void setAction(final CertificateAction action) {
  350. this.action = action;
  351. actionSem.release();
  352. }
  353. /**
  354. * Retrieves the name of the server to which the user is trying to connect.
  355. *
  356. * @return The name of the server that the user is trying to connect to
  357. */
  358. public String getServerName() {
  359. return serverName;
  360. }
  361. /**
  362. * Reads the fields from the subject's designated name in the specified
  363. * certificate.
  364. *
  365. * @param cert The certificate to read
  366. * @return A map of the fields in the certificate's subject's designated
  367. * name
  368. */
  369. public static Map<String, String> getDNFieldsFromCert(final X509Certificate cert) {
  370. final Map<String, String> res = new HashMap<String, String>();
  371. try {
  372. final LdapName name = new LdapName(cert.getSubjectX500Principal().getName());
  373. for (Rdn rdn : name.getRdns()) {
  374. res.put(rdn.getType(), rdn.getValue().toString());
  375. }
  376. } catch (InvalidNameException ex) {
  377. // Don't care
  378. }
  379. return res;
  380. }
  381. /** {@inheritDoc} */
  382. @Override
  383. public X509Certificate[] getAcceptedIssuers() {
  384. return globalTrustedCAs.toArray(new X509Certificate[globalTrustedCAs.size()]);
  385. }
  386. /**
  387. * Adds a new certificate problem listener to this manager.
  388. *
  389. * @param listener The listener to be added
  390. */
  391. public void addCertificateProblemListener(final CertificateProblemListener listener) {
  392. listeners.add(CertificateProblemListener.class, listener);
  393. }
  394. /**
  395. * Removes the specified listener from this manager.
  396. *
  397. * @param listener The listener to be removed
  398. */
  399. public void removeCertificateProblemListener(final CertificateProblemListener listener) {
  400. listeners.remove(CertificateProblemListener.class, listener);
  401. }
  402. }