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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. /*
  2. * Copyright (c) 2006-2010 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 java.io.File;
  30. import java.io.FileInputStream;
  31. import java.io.IOException;
  32. import java.security.InvalidAlgorithmParameterException;
  33. import java.security.KeyStore;
  34. import java.security.KeyStoreException;
  35. import java.security.NoSuchAlgorithmException;
  36. import java.security.UnrecoverableKeyException;
  37. import java.security.cert.CertificateException;
  38. import java.security.cert.CertificateParsingException;
  39. import java.security.cert.PKIXParameters;
  40. import java.security.cert.TrustAnchor;
  41. import java.security.cert.X509Certificate;
  42. import java.util.ArrayList;
  43. import java.util.Arrays;
  44. import java.util.HashMap;
  45. import java.util.HashSet;
  46. import java.util.List;
  47. import java.util.Map;
  48. import java.util.Set;
  49. import java.util.concurrent.Semaphore;
  50. import javax.naming.InvalidNameException;
  51. import javax.naming.ldap.LdapName;
  52. import javax.naming.ldap.Rdn;
  53. import javax.net.ssl.KeyManager;
  54. import javax.net.ssl.KeyManagerFactory;
  55. import javax.net.ssl.X509TrustManager;
  56. import net.miginfocom.Base64;
  57. /**
  58. * Manages storage and validation of certificates used when connecting to
  59. * SSL servers.
  60. *
  61. * @since 0.6.3m1
  62. * @author chris
  63. */
  64. public class CertificateManager implements X509TrustManager {
  65. public static enum TrustResult {
  66. TRUSTED_CA(true),
  67. TRUSTED_MANUALLY(true),
  68. UNTRUSTED_EXCEPTION(false),
  69. UNTRUSTED_GENERAL(false);
  70. private final boolean trusted;
  71. private TrustResult(boolean trusted) {
  72. this.trusted = trusted;
  73. }
  74. public boolean isTrusted() {
  75. return trusted;
  76. }
  77. }
  78. /** The password for the global java cacert file. */
  79. private final String cacertpass;
  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 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.cacertpass = config.getOption("ssl", "cacertpass");
  103. this.checkDate = config.getOptionBool("ssl", "checkdate");
  104. this.checkIssuer = config.getOptionBool("ssl", "checkissuer");
  105. this.checkHost = config.getOptionBool("ssl", "checkhost");
  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, cacertpass.toCharArray());
  119. final PKIXParameters params = new PKIXParameters(keystore);
  120. for (TrustAnchor anchor : params.getTrustAnchors()) {
  121. globalTrustedCAs.add(anchor.getTrustedCert());
  122. }
  123. } catch (CertificateException ex) {
  124. Logger.userError(ErrorLevel.MEDIUM, "Unable to load trusted certificates", ex);
  125. } catch (IOException ex) {
  126. Logger.userError(ErrorLevel.MEDIUM, "Unable to load trusted certificates", ex);
  127. } catch (InvalidAlgorithmParameterException ex) {
  128. Logger.userError(ErrorLevel.MEDIUM, "Unable to load trusted certificates", ex);
  129. } catch (KeyStoreException ex) {
  130. Logger.userError(ErrorLevel.MEDIUM, "Unable to load trusted certificates", ex);
  131. } catch (NoSuchAlgorithmException ex) {
  132. Logger.userError(ErrorLevel.MEDIUM, "Unable to load trusted certificates", ex);
  133. } finally {
  134. if (is != null) {
  135. try {
  136. is.close();
  137. } catch (IOException ex) {
  138. // ...
  139. }
  140. }
  141. }
  142. }
  143. /**
  144. * Retrieves a KeyManager[] for the client certicate specified in the
  145. * configuration, if there is one.
  146. *
  147. * @return A KeyManager to use for the SSL connection
  148. */
  149. public KeyManager[] getKeyManager() {
  150. if (config.hasOptionString("ssl", "clientcert.file")) {
  151. FileInputStream fis = null;
  152. try {
  153. final char[] pass;
  154. if (config.hasOptionString("ssl", "clientcert.pass")) {
  155. pass = config.getOption("ssl", "clientcert.pass").toCharArray();
  156. } else {
  157. pass = null;
  158. }
  159. fis = new FileInputStream(config.getOption("ssl", "clientcert.file"));
  160. final KeyStore ks = KeyStore.getInstance("pkcs12");
  161. ks.load(fis, pass);
  162. final KeyManagerFactory kmf = KeyManagerFactory.getInstance(
  163. KeyManagerFactory.getDefaultAlgorithm());
  164. kmf.init(ks, pass);
  165. return kmf.getKeyManagers();
  166. } catch (KeyStoreException ex) {
  167. Logger.appError(ErrorLevel.MEDIUM, "Unable to get key manager", ex);
  168. } catch (IOException ex) {
  169. Logger.appError(ErrorLevel.MEDIUM, "Unable to get key manager", ex);
  170. } catch (CertificateException ex) {
  171. Logger.appError(ErrorLevel.MEDIUM, "Unable to get key manager", ex);
  172. } catch (NoSuchAlgorithmException ex) {
  173. Logger.appError(ErrorLevel.MEDIUM, "Unable to get key manager", ex);
  174. } catch (UnrecoverableKeyException ex) {
  175. Logger.appError(ErrorLevel.MEDIUM, "Unable to get key manager", ex);
  176. } finally {
  177. if (fis != null) {
  178. try {
  179. fis.close();
  180. } catch (IOException ex) {
  181. // ...
  182. }
  183. }
  184. }
  185. }
  186. return null;
  187. }
  188. /** {@inheritDoc} */
  189. @Override
  190. public void checkClientTrusted(final X509Certificate[] chain, final String authType)
  191. throws CertificateException {
  192. throw new CertificateException("Not supported.");
  193. }
  194. /**
  195. * Determines if the specified certificate is trusted by the user.
  196. *
  197. * @param certificate The certificate to be checked
  198. * @return True if the certificate matches one in the trusted certificate
  199. * store, or if the certificate's details are marked as trusted in the
  200. * DMDirc configuration file.
  201. */
  202. public TrustResult isTrusted(final X509Certificate certificate) {
  203. try {
  204. final String sig = Base64.encodeToString(certificate.getSignature(), false);
  205. if (config.hasOptionString("ssl", "trusted") && config.getOptionList("ssl",
  206. "trusted").contains(sig)) {
  207. return TrustResult.TRUSTED_MANUALLY;
  208. } else {
  209. for (X509Certificate trustedCert : globalTrustedCAs) {
  210. if (Arrays.equals(certificate.getSignature(), trustedCert.getSignature())
  211. && certificate.getIssuerDN().getName()
  212. .equals(trustedCert.getIssuerDN().getName())) {
  213. certificate.verify(trustedCert.getPublicKey());
  214. return TrustResult.TRUSTED_CA;
  215. }
  216. }
  217. }
  218. } catch (Exception ex) {
  219. return TrustResult.UNTRUSTED_EXCEPTION;
  220. }
  221. return TrustResult.UNTRUSTED_GENERAL;
  222. }
  223. public boolean isValidHost(final X509Certificate certificate) {
  224. final Map<String, String> fields = getDNFieldsFromCert(certificate);
  225. if (fields.containsKey("CN") && fields.get("CN").equals(serverName)) {
  226. return true;
  227. }
  228. try {
  229. if (certificate.getSubjectAlternativeNames() != null) {
  230. for (List<?> entry : certificate.getSubjectAlternativeNames()) {
  231. final int type = ((Integer) entry.get(0)).intValue();
  232. // DNS or IP
  233. if ((type == 2 || type == 7) && entry.get(1).equals(serverName)) {
  234. return true;
  235. }
  236. }
  237. }
  238. } catch (CertificateParsingException ex) {
  239. return false;
  240. }
  241. return false;
  242. }
  243. /** {@inheritDoc} */
  244. @Override
  245. public void checkServerTrusted(final X509Certificate[] chain, final String authType)
  246. throws CertificateException {
  247. final List<CertificateException> problems = new ArrayList<CertificateException>();
  248. boolean verified = false;
  249. boolean manual = false;
  250. if (checkHost) {
  251. // Check that the cert is issued to the correct host
  252. verified = isValidHost(chain[0]);
  253. if (!verified) {
  254. problems.add(new CertificateDoesntMatchHostException(
  255. "Certificate was not issued to " + serverName));
  256. }
  257. verified = false;
  258. }
  259. for (X509Certificate cert : chain) {
  260. TrustResult trustResult = isTrusted(cert);
  261. if (checkDate) {
  262. // Check that the certificate is in-date
  263. try {
  264. cert.checkValidity();
  265. } catch (CertificateException ex) {
  266. problems.add(ex);
  267. }
  268. }
  269. if (checkIssuer) {
  270. // Check that we trust an issuer
  271. verified |= trustResult.isTrusted();
  272. }
  273. if (trustResult == TrustResult.TRUSTED_MANUALLY) {
  274. manual = true;
  275. }
  276. }
  277. if (!verified && checkIssuer) {
  278. problems.add(new CertificateNotTrustedException("Issuer is not trusted"));
  279. }
  280. if (!problems.isEmpty() && !manual) {
  281. final SSLCertificateDialogModel model
  282. = new SSLCertificateDialogModel(chain, problems, this);
  283. Main.getUI().showSSLCertificateDialog(model);
  284. try {
  285. actionSem.acquire();
  286. } catch (InterruptedException ie) {
  287. throw new CertificateException("Thread aborted, ");
  288. }
  289. switch (action) {
  290. case DISCONNECT:
  291. throw new CertificateException("Not trusted");
  292. case IGNORE_PERMANENTY:
  293. final List<String> list = new ArrayList<String>(config
  294. .getOptionList("ssl", "trusted"));
  295. list.add(Base64.encodeToString(chain[0].getSignature(), false));
  296. IdentityManager.getConfigIdentity().setOption("ssl",
  297. "trusted", list);
  298. break;
  299. case IGNORE_TEMPORARILY:
  300. // Do nothing, continue connecting
  301. break;
  302. }
  303. }
  304. }
  305. /**
  306. * Sets the action to perform for the request that's in progress.
  307. *
  308. * @param action The action that's been selected
  309. */
  310. public void setAction(final CertificateAction action) {
  311. this.action = action;
  312. actionSem.release();
  313. }
  314. /**
  315. * Retrieves the name of the server to which the user is trying to connect.
  316. *
  317. * @return The name of the server that the user is trying to connect to
  318. */
  319. public String getServerName() {
  320. return serverName;
  321. }
  322. /**
  323. * Reads the fields from the subject's designated name in the specified
  324. * certificate.
  325. *
  326. * @param cert The certificate to read
  327. * @return A map of the fields in the certificate's subject's designated
  328. * name
  329. */
  330. public static Map<String, String> getDNFieldsFromCert(final X509Certificate cert) {
  331. final Map<String, String> res = new HashMap<String, String>();
  332. try {
  333. final LdapName name = new LdapName(cert.getSubjectX500Principal().getName());
  334. for (Rdn rdn : name.getRdns()) {
  335. res.put(rdn.getType(), rdn.getValue().toString());
  336. }
  337. } catch (InvalidNameException ex) {
  338. // Don't care
  339. }
  340. return res;
  341. }
  342. /** {@inheritDoc} */
  343. @Override
  344. public X509Certificate[] getAcceptedIssuers() {
  345. return globalTrustedCAs.toArray(new X509Certificate[globalTrustedCAs.size()]);
  346. }
  347. /**
  348. * An exception to indicate that the host on a certificate doesn't match
  349. * the host we're trying to connect to.
  350. */
  351. public static class CertificateDoesntMatchHostException extends CertificateException {
  352. /**
  353. * A version number for this class. It should be changed whenever the
  354. * class structure is changed (or anything else that would prevent
  355. * serialized objects being unserialized with the new class).
  356. */
  357. private static final long serialVersionUID = 1;
  358. /**
  359. * Creates a new CertificateDoesntMatchHostException
  360. *
  361. * @param msg A description of the problem
  362. */
  363. public CertificateDoesntMatchHostException(String msg) {
  364. super(msg);
  365. }
  366. }
  367. /**
  368. * An exception to indicate that we do not trust the issuer of the
  369. * certificate (or the CA).
  370. */
  371. public static class CertificateNotTrustedException extends CertificateException {
  372. /**
  373. * A version number for this class. It should be changed whenever the
  374. * class structure is changed (or anything else that would prevent
  375. * serialized objects being unserialized with the new class).
  376. */
  377. private static final long serialVersionUID = 1;
  378. /**
  379. * Creates a new CertificateNotTrustedException
  380. *
  381. * @param msg A description of the problem
  382. */
  383. public CertificateNotTrustedException(String msg) {
  384. super(msg);
  385. }
  386. }
  387. }