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

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