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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  1. /*
  2. * Copyright (c) 2006-2014 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.interfaces.config.AggregateConfigProvider;
  24. import com.dmdirc.interfaces.config.ConfigProvider;
  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.GeneralSecurityException;
  34. import java.security.InvalidAlgorithmParameterException;
  35. import java.security.KeyStore;
  36. import java.security.KeyStoreException;
  37. import java.security.NoSuchAlgorithmException;
  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 SSL servers.
  61. *
  62. * @since 0.6.3m1
  63. */
  64. public class CertificateManager implements X509TrustManager {
  65. /** List of listeners. */
  66. private final ListenerList listeners = new ListenerList();
  67. /** The server name the user is trying to connect to. */
  68. private final String serverName;
  69. /** The configuration manager to use for settings. */
  70. private final AggregateConfigProvider config;
  71. /** The set of CAs from the global cacert file. */
  72. private final Set<X509Certificate> globalTrustedCAs = new HashSet<>();
  73. /** Whether or not to the issue and expiry dates of the certificate. */
  74. private final boolean checkDate;
  75. /** Whether or not to the issuer of the certificate. */
  76. private final boolean checkIssuer;
  77. /** Whether or not to the hostname of the certificate. */
  78. private final boolean checkHost;
  79. /** Used to synchronise the manager with the certificate dialog. */
  80. private final Semaphore actionSem = new Semaphore(0);
  81. /** The action to perform. */
  82. private CertificateAction action;
  83. /** A list of problems encountered most recently. */
  84. private final List<CertificateException> problems = new ArrayList<>();
  85. /** The chain of certificates currently being validated. */
  86. private X509Certificate[] chain;
  87. /** The user settings to write to. */
  88. private final ConfigProvider userSettings;
  89. /**
  90. * Creates a new certificate manager for a client connecting to the specified server.
  91. *
  92. * @param serverName The name the user used to connect to the server
  93. * @param config The configuration manager to use
  94. * @param userSettings The user settings to write to.
  95. */
  96. public CertificateManager(
  97. final String serverName,
  98. final AggregateConfigProvider config,
  99. final ConfigProvider userSettings) {
  100. this.serverName = serverName;
  101. this.config = config;
  102. this.checkDate = config.getOptionBool("ssl", "checkdate");
  103. this.checkIssuer = config.getOptionBool("ssl", "checkissuer");
  104. this.checkHost = config.getOptionBool("ssl", "checkhost");
  105. this.userSettings = userSettings;
  106. loadTrustedCAs();
  107. }
  108. /**
  109. * Loads the trusted CA certificates from the Java cacerts store.
  110. */
  111. protected void loadTrustedCAs() {
  112. FileInputStream is = null;
  113. try {
  114. final String filename = System.getProperty("java.home")
  115. + "/lib/security/cacerts".replace('/', File.separatorChar);
  116. is = new FileInputStream(filename);
  117. final KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
  118. keystore.load(is, null);
  119. final PKIXParameters params = new PKIXParameters(keystore);
  120. for (TrustAnchor anchor : params.getTrustAnchors()) {
  121. globalTrustedCAs.add(anchor.getTrustedCert());
  122. }
  123. } catch (CertificateException | IOException | InvalidAlgorithmParameterException |
  124. KeyStoreException | NoSuchAlgorithmException ex) {
  125. Logger.userError(ErrorLevel.MEDIUM, "Unable to load trusted certificates", ex);
  126. } finally {
  127. StreamUtils.close(is);
  128. }
  129. }
  130. /**
  131. * Retrieves a KeyManager[] for the client certificate specified in the configuration, if there
  132. * is one.
  133. *
  134. * @return A KeyManager to use for the SSL connection
  135. */
  136. public KeyManager[] getKeyManager() {
  137. if (config.hasOptionString("ssl", "clientcert.file")) {
  138. FileInputStream fis = null;
  139. try {
  140. final char[] pass;
  141. if (config.hasOptionString("ssl", "clientcert.pass")) {
  142. pass = config.getOption("ssl", "clientcert.pass").toCharArray();
  143. } else {
  144. pass = null;
  145. }
  146. fis = new FileInputStream(config.getOption("ssl", "clientcert.file"));
  147. final KeyStore ks = KeyStore.getInstance("pkcs12");
  148. ks.load(fis, pass);
  149. final KeyManagerFactory kmf = KeyManagerFactory.getInstance(
  150. KeyManagerFactory.getDefaultAlgorithm());
  151. kmf.init(ks, pass);
  152. return kmf.getKeyManagers();
  153. } catch (FileNotFoundException ex) {
  154. Logger.userError(ErrorLevel.MEDIUM, "Certificate file not found", ex);
  155. } catch (GeneralSecurityException | IOException ex) {
  156. Logger.appError(ErrorLevel.MEDIUM, "Unable to get key manager", ex);
  157. } finally {
  158. StreamUtils.close(fis);
  159. }
  160. }
  161. return null;
  162. }
  163. /** {@inheritDoc} */
  164. @Override
  165. public void checkClientTrusted(final X509Certificate[] chain, final String authType)
  166. throws CertificateException {
  167. throw new CertificateException("Not supported.");
  168. }
  169. /**
  170. * Determines if the specified certificate is trusted by the user.
  171. *
  172. * @param certificate The certificate to be checked
  173. *
  174. * @return True if the certificate matches one in the trusted certificate store, or if the
  175. * certificate's details are marked as trusted in the DMDirc configuration file.
  176. */
  177. public TrustResult isTrusted(final X509Certificate certificate) {
  178. try {
  179. final String sig = Base64.encodeToString(certificate.getSignature(), false);
  180. if (config.hasOptionString("ssl", "trusted") && config.getOptionList("ssl",
  181. "trusted").contains(sig)) {
  182. return TrustResult.TRUSTED_MANUALLY;
  183. } else {
  184. for (X509Certificate trustedCert : globalTrustedCAs) {
  185. if (Arrays.equals(certificate.getSignature(), trustedCert.getSignature())
  186. && certificate.getIssuerDN().getName()
  187. .equals(trustedCert.getIssuerDN().getName())) {
  188. certificate.verify(trustedCert.getPublicKey());
  189. return TrustResult.TRUSTED_CA;
  190. }
  191. }
  192. }
  193. } catch (GeneralSecurityException ex) {
  194. return TrustResult.UNTRUSTED_EXCEPTION;
  195. }
  196. return TrustResult.UNTRUSTED_GENERAL;
  197. }
  198. /**
  199. * Determines whether the given certificate has a valid CN or alternate name for this server's
  200. * hostname.
  201. *
  202. * @param certificate The certificate to be validated
  203. *
  204. * @return True if the certificate is valid for this server's host, false otherwise
  205. */
  206. public boolean isValidHost(final X509Certificate certificate) {
  207. final Map<String, String> fields = getDNFieldsFromCert(certificate);
  208. if (fields.containsKey("CN") && isMatchingServerName(fields.get("CN"))) {
  209. return true;
  210. }
  211. try {
  212. if (certificate.getSubjectAlternativeNames() != null) {
  213. for (List<?> entry : certificate.getSubjectAlternativeNames()) {
  214. final int type = ((Integer) entry.get(0)).intValue();
  215. // DNS or IP
  216. if ((type == 2 || type == 7) && isMatchingServerName((String) entry.get(1))) {
  217. return true;
  218. }
  219. }
  220. }
  221. } catch (CertificateParsingException ex) {
  222. return false;
  223. }
  224. return false;
  225. }
  226. /**
  227. * Checks whether the specified target matches the server name this certificate manager was
  228. * initialised with.
  229. *
  230. * Target names may contain wildcards per RFC2818.
  231. *
  232. * @since 0.6.5
  233. * @param target The target to compare to our server name
  234. *
  235. * @return True if the target matches, false otherwise
  236. */
  237. protected boolean isMatchingServerName(final String target) {
  238. final String[] targetParts = target.split("\\.");
  239. final String[] serverParts = serverName.split("\\.");
  240. if (targetParts.length != serverParts.length) {
  241. // Fail fast if they don't match
  242. return false;
  243. }
  244. for (int i = 0; i < serverParts.length; i++) {
  245. if (!serverParts[i].matches("\\Q" + targetParts[i].replace("*", "\\E.*\\Q") + "\\E")) {
  246. return false;
  247. }
  248. }
  249. return true;
  250. }
  251. /** {@inheritDoc} */
  252. @Override
  253. public void checkServerTrusted(final X509Certificate[] chain, final String authType)
  254. throws CertificateException {
  255. this.chain = chain;
  256. problems.clear();
  257. boolean verified = false;
  258. boolean manual = false;
  259. if (checkHost) {
  260. // Check that the cert is issued to the correct host
  261. verified = isValidHost(chain[0]);
  262. if (!verified) {
  263. problems.add(new CertificateDoesntMatchHostException(
  264. "Certificate was not issued to " + serverName));
  265. }
  266. verified = false;
  267. }
  268. for (X509Certificate cert : chain) {
  269. final TrustResult trustResult = isTrusted(cert);
  270. if (checkDate) {
  271. // Check that the certificate is in-date
  272. try {
  273. cert.checkValidity();
  274. } catch (CertificateException ex) {
  275. problems.add(ex);
  276. }
  277. }
  278. if (checkIssuer) {
  279. // Check that we trust an issuer
  280. verified |= trustResult.isTrusted();
  281. }
  282. if (trustResult == TrustResult.TRUSTED_MANUALLY) {
  283. manual = true;
  284. }
  285. }
  286. if (!verified && checkIssuer) {
  287. problems.add(new CertificateNotTrustedException("Issuer is not trusted"));
  288. }
  289. if (!problems.isEmpty() && !manual) {
  290. for (CertificateProblemListener listener : listeners.get(
  291. CertificateProblemListener.class)) {
  292. listener.certificateProblemEncountered(chain, problems, this);
  293. }
  294. try {
  295. actionSem.acquire();
  296. } catch (InterruptedException ie) {
  297. throw new CertificateException("Thread aborted", ie);
  298. } finally {
  299. problems.clear();
  300. for (CertificateProblemListener listener : listeners.get(
  301. CertificateProblemListener.class)) {
  302. listener.certificateProblemResolved(this);
  303. }
  304. }
  305. switch (action) {
  306. case DISCONNECT:
  307. throw new CertificateException("Not trusted");
  308. case IGNORE_PERMANENTLY:
  309. final List<String> list = new ArrayList<>(config
  310. .getOptionList("ssl", "trusted"));
  311. list.add(Base64.encodeToString(chain[0].getSignature(), false));
  312. userSettings.setOption("ssl", "trusted", list);
  313. break;
  314. case IGNORE_TEMPORARILY:
  315. // Do nothing, continue connecting
  316. break;
  317. }
  318. }
  319. if (manual) {
  320. problems.clear();
  321. }
  322. }
  323. /**
  324. * Gets the chain of certificates currently being validated, if any.
  325. *
  326. * @return The chain of certificates being validated
  327. */
  328. public X509Certificate[] getChain() {
  329. return chain;
  330. }
  331. /**
  332. * Gets the set of problems that were encountered with the last certificate.
  333. *
  334. * @return The set of problems encountered, or any empty collection if there is no current
  335. * validation attempt ongoing.
  336. */
  337. public Collection<CertificateException> getProblems() {
  338. return problems;
  339. }
  340. /**
  341. * Sets the action to perform for the request that's in progress.
  342. *
  343. * @param action The action that's been selected
  344. */
  345. public void setAction(final CertificateAction action) {
  346. this.action = action;
  347. actionSem.release();
  348. }
  349. /**
  350. * Retrieves the name of the server to which the user is trying to connect.
  351. *
  352. * @return The name of the server that the user is trying to connect to
  353. */
  354. public String getServerName() {
  355. return serverName;
  356. }
  357. /**
  358. * Reads the fields from the subject's designated name in the specified certificate.
  359. *
  360. * @param cert The certificate to read
  361. *
  362. * @return A map of the fields in the certificate's subject's designated name
  363. */
  364. public static Map<String, String> getDNFieldsFromCert(final X509Certificate cert) {
  365. final Map<String, String> res = new HashMap<>();
  366. try {
  367. final LdapName name = new LdapName(cert.getSubjectX500Principal().getName());
  368. for (Rdn rdn : name.getRdns()) {
  369. res.put(rdn.getType(), rdn.getValue().toString());
  370. }
  371. } catch (InvalidNameException ex) {
  372. // Don't care
  373. }
  374. return res;
  375. }
  376. /** {@inheritDoc} */
  377. @Override
  378. public X509Certificate[] getAcceptedIssuers() {
  379. return globalTrustedCAs.toArray(new X509Certificate[globalTrustedCAs.size()]);
  380. }
  381. /**
  382. * Adds a new certificate problem listener to this manager.
  383. *
  384. * @param listener The listener to be added
  385. */
  386. public void addCertificateProblemListener(final CertificateProblemListener listener) {
  387. listeners.add(CertificateProblemListener.class, listener);
  388. }
  389. /**
  390. * Removes the specified listener from this manager.
  391. *
  392. * @param listener The listener to be removed
  393. */
  394. public void removeCertificateProblemListener(final CertificateProblemListener listener) {
  395. listeners.remove(CertificateProblemListener.class, listener);
  396. }
  397. }