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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. /*
  2. * Copyright (c) 2006-2011 Chris Smith, Shane Mc Cormack, Gregory Holmes
  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;
  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.ui.core.dialogs.sslcertificate.CertificateAction;
  28. import com.dmdirc.ui.core.dialogs.sslcertificate.SSLCertificateDialogModel;
  29. import com.dmdirc.util.StreamUtil;
  30. import java.io.File;
  31. import java.io.FileInputStream;
  32. import java.io.FileNotFoundException;
  33. import java.io.IOException;
  34. import java.security.InvalidAlgorithmParameterException;
  35. import java.security.KeyStore;
  36. import java.security.KeyStoreException;
  37. import java.security.NoSuchAlgorithmException;
  38. import java.security.UnrecoverableKeyException;
  39. import java.security.cert.CertificateException;
  40. import java.security.cert.CertificateParsingException;
  41. import java.security.cert.PKIXParameters;
  42. import java.security.cert.TrustAnchor;
  43. import java.security.cert.X509Certificate;
  44. import java.util.ArrayList;
  45. import java.util.Arrays;
  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. * @author chris
  65. */
  66. public class CertificateManager implements X509TrustManager {
  67. public static enum TrustResult {
  68. TRUSTED_CA(true),
  69. TRUSTED_MANUALLY(true),
  70. UNTRUSTED_EXCEPTION(false),
  71. UNTRUSTED_GENERAL(false);
  72. private final boolean trusted;
  73. private TrustResult(final boolean trusted) {
  74. this.trusted = trusted;
  75. }
  76. public boolean isTrusted() {
  77. return trusted;
  78. }
  79. }
  80. /** The server name the user is trying to connect to. */
  81. private final String serverName;
  82. /** The configuration manager to use for settings. */
  83. private final ConfigManager config;
  84. /** The set of CAs from the global cacert file. */
  85. private final Set<X509Certificate> globalTrustedCAs = new HashSet<X509Certificate>();
  86. /** Whether or not to check specified parts of the certificate. */
  87. private boolean checkDate, checkIssuer, checkHost;
  88. /** Used to synchronise the manager with the certificate dialog. */
  89. private final Semaphore actionSem = new Semaphore(0);
  90. /** The action to perform. */
  91. private CertificateAction action;
  92. /**
  93. * Creates a new certificate manager for a client connecting to the
  94. * specified server.
  95. *
  96. * @param serverName The name the user used to connect to the server
  97. * @param config The configuration manager to use
  98. */
  99. public CertificateManager(final String serverName, final ConfigManager config) {
  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. loadTrustedCAs();
  106. }
  107. /**
  108. * Loads the trusted CA certificates from the Java cacerts store.
  109. */
  110. protected void loadTrustedCAs() {
  111. FileInputStream is = null;
  112. try {
  113. final String filename = System.getProperty("java.home")
  114. + "/lib/security/cacerts".replace('/', File.separatorChar);
  115. is = new FileInputStream(filename);
  116. final KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
  117. keystore.load(is, null);
  118. final PKIXParameters params = new PKIXParameters(keystore);
  119. for (TrustAnchor anchor : params.getTrustAnchors()) {
  120. globalTrustedCAs.add(anchor.getTrustedCert());
  121. }
  122. } catch (CertificateException ex) {
  123. Logger.userError(ErrorLevel.MEDIUM, "Unable to load trusted certificates", ex);
  124. } catch (IOException ex) {
  125. Logger.userError(ErrorLevel.MEDIUM, "Unable to load trusted certificates", ex);
  126. } catch (InvalidAlgorithmParameterException ex) {
  127. Logger.userError(ErrorLevel.MEDIUM, "Unable to load trusted certificates", ex);
  128. } catch (KeyStoreException ex) {
  129. Logger.userError(ErrorLevel.MEDIUM, "Unable to load trusted certificates", ex);
  130. } catch (NoSuchAlgorithmException ex) {
  131. Logger.userError(ErrorLevel.MEDIUM, "Unable to load trusted certificates", ex);
  132. } finally {
  133. StreamUtil.close(is);
  134. }
  135. }
  136. /**
  137. * Retrieves a KeyManager[] for the client certificate specified in the
  138. * configuration, if there is one.
  139. *
  140. * @return A KeyManager to use for the SSL connection
  141. */
  142. public KeyManager[] getKeyManager() {
  143. if (config.hasOptionString("ssl", "clientcert.file")) {
  144. FileInputStream fis = null;
  145. try {
  146. final char[] pass;
  147. if (config.hasOptionString("ssl", "clientcert.pass")) {
  148. pass = config.getOption("ssl", "clientcert.pass").toCharArray();
  149. } else {
  150. pass = null;
  151. }
  152. fis = new FileInputStream(config.getOption("ssl", "clientcert.file"));
  153. final KeyStore ks = KeyStore.getInstance("pkcs12");
  154. ks.load(fis, pass);
  155. final KeyManagerFactory kmf = KeyManagerFactory.getInstance(
  156. KeyManagerFactory.getDefaultAlgorithm());
  157. kmf.init(ks, pass);
  158. return kmf.getKeyManagers();
  159. } catch (FileNotFoundException ex) {
  160. Logger.userError(ErrorLevel.MEDIUM, "Certificate file not found", ex);
  161. } catch (KeyStoreException ex) {
  162. Logger.appError(ErrorLevel.MEDIUM, "Unable to get key manager", ex);
  163. } catch (IOException ex) {
  164. Logger.userError(ErrorLevel.MEDIUM, "Unable to get key manager", ex);
  165. } catch (CertificateException ex) {
  166. Logger.appError(ErrorLevel.MEDIUM, "Unable to get key manager", ex);
  167. } catch (NoSuchAlgorithmException ex) {
  168. Logger.appError(ErrorLevel.MEDIUM, "Unable to get key manager", ex);
  169. } catch (UnrecoverableKeyException ex) {
  170. Logger.appError(ErrorLevel.MEDIUM, "Unable to get key manager", ex);
  171. } finally {
  172. StreamUtil.close(fis);
  173. }
  174. }
  175. return null;
  176. }
  177. /** {@inheritDoc} */
  178. @Override
  179. public void checkClientTrusted(final X509Certificate[] chain, final String authType)
  180. throws CertificateException {
  181. throw new CertificateException("Not supported.");
  182. }
  183. /**
  184. * Determines if the specified certificate is trusted by the user.
  185. *
  186. * @param certificate The certificate to be checked
  187. * @return True if the certificate matches one in the trusted certificate
  188. * store, or if the certificate's details are marked as trusted in the
  189. * DMDirc configuration file.
  190. */
  191. public TrustResult isTrusted(final X509Certificate certificate) {
  192. try {
  193. final String sig = Base64.encodeToString(certificate.getSignature(), false);
  194. if (config.hasOptionString("ssl", "trusted") && config.getOptionList("ssl",
  195. "trusted").contains(sig)) {
  196. return TrustResult.TRUSTED_MANUALLY;
  197. } else {
  198. for (X509Certificate trustedCert : globalTrustedCAs) {
  199. if (Arrays.equals(certificate.getSignature(), trustedCert.getSignature())
  200. && certificate.getIssuerDN().getName()
  201. .equals(trustedCert.getIssuerDN().getName())) {
  202. certificate.verify(trustedCert.getPublicKey());
  203. return TrustResult.TRUSTED_CA;
  204. }
  205. }
  206. }
  207. } catch (Exception ex) {
  208. return TrustResult.UNTRUSTED_EXCEPTION;
  209. }
  210. return TrustResult.UNTRUSTED_GENERAL;
  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. final List<CertificateException> problems = new ArrayList<CertificateException>();
  261. boolean verified = false;
  262. boolean manual = false;
  263. if (checkHost) {
  264. // Check that the cert is issued to the correct host
  265. verified = isValidHost(chain[0]);
  266. if (!verified) {
  267. problems.add(new CertificateDoesntMatchHostException(
  268. "Certificate was not issued to " + serverName));
  269. }
  270. verified = false;
  271. }
  272. for (X509Certificate cert : chain) {
  273. TrustResult trustResult = isTrusted(cert);
  274. if (checkDate) {
  275. // Check that the certificate is in-date
  276. try {
  277. cert.checkValidity();
  278. } catch (CertificateException ex) {
  279. problems.add(ex);
  280. }
  281. }
  282. if (checkIssuer) {
  283. // Check that we trust an issuer
  284. verified |= trustResult.isTrusted();
  285. }
  286. if (trustResult == TrustResult.TRUSTED_MANUALLY) {
  287. manual = true;
  288. }
  289. }
  290. if (!verified && checkIssuer) {
  291. problems.add(new CertificateNotTrustedException("Issuer is not trusted"));
  292. }
  293. if (!problems.isEmpty() && !manual) {
  294. final SSLCertificateDialogModel model
  295. = new SSLCertificateDialogModel(chain, problems, this);
  296. Main.getUI().showSSLCertificateDialog(model);
  297. try {
  298. actionSem.acquire();
  299. } catch (InterruptedException ie) {
  300. throw new CertificateException("Thread aborted, ");
  301. }
  302. switch (action) {
  303. case DISCONNECT:
  304. throw new CertificateException("Not trusted");
  305. case IGNORE_PERMANENTY:
  306. final List<String> list = new ArrayList<String>(config
  307. .getOptionList("ssl", "trusted"));
  308. list.add(Base64.encodeToString(chain[0].getSignature(), false));
  309. IdentityManager.getConfigIdentity().setOption("ssl",
  310. "trusted", list);
  311. break;
  312. case IGNORE_TEMPORARILY:
  313. // Do nothing, continue connecting
  314. break;
  315. }
  316. }
  317. }
  318. /**
  319. * Sets the action to perform for the request that's in progress.
  320. *
  321. * @param action The action that's been selected
  322. */
  323. public void setAction(final CertificateAction action) {
  324. this.action = action;
  325. actionSem.release();
  326. }
  327. /**
  328. * Retrieves the name of the server to which the user is trying to connect.
  329. *
  330. * @return The name of the server that the user is trying to connect to
  331. */
  332. public String getServerName() {
  333. return serverName;
  334. }
  335. /**
  336. * Reads the fields from the subject's designated name in the specified
  337. * certificate.
  338. *
  339. * @param cert The certificate to read
  340. * @return A map of the fields in the certificate's subject's designated
  341. * name
  342. */
  343. public static Map<String, String> getDNFieldsFromCert(final X509Certificate cert) {
  344. final Map<String, String> res = new HashMap<String, String>();
  345. try {
  346. final LdapName name = new LdapName(cert.getSubjectX500Principal().getName());
  347. for (Rdn rdn : name.getRdns()) {
  348. res.put(rdn.getType(), rdn.getValue().toString());
  349. }
  350. } catch (InvalidNameException ex) {
  351. // Don't care
  352. }
  353. return res;
  354. }
  355. /** {@inheritDoc} */
  356. @Override
  357. public X509Certificate[] getAcceptedIssuers() {
  358. return globalTrustedCAs.toArray(new X509Certificate[globalTrustedCAs.size()]);
  359. }
  360. /**
  361. * An exception to indicate that the host on a certificate doesn't match
  362. * the host we're trying to connect to.
  363. */
  364. public static class CertificateDoesntMatchHostException extends CertificateException {
  365. /**
  366. * A version number for this class. It should be changed whenever the
  367. * class structure is changed (or anything else that would prevent
  368. * serialized objects being unserialized with the new class).
  369. */
  370. private static final long serialVersionUID = 1;
  371. /**
  372. * Creates a new CertificateDoesntMatchHostException
  373. *
  374. * @param msg A description of the problem
  375. */
  376. public CertificateDoesntMatchHostException(final String msg) {
  377. super(msg);
  378. }
  379. }
  380. /**
  381. * An exception to indicate that we do not trust the issuer of the
  382. * certificate (or the CA).
  383. */
  384. public static class CertificateNotTrustedException extends CertificateException {
  385. /**
  386. * A version number for this class. It should be changed whenever the
  387. * class structure is changed (or anything else that would prevent
  388. * serialized objects being unserialized with the new class).
  389. */
  390. private static final long serialVersionUID = 1;
  391. /**
  392. * Creates a new CertificateNotTrustedException
  393. *
  394. * @param msg A description of the problem
  395. */
  396. public CertificateNotTrustedException(final String msg) {
  397. super(msg);
  398. }
  399. }
  400. }