// // $Id: InteractiveTrustManager.java 441 2008-04-16 07:58:02Z etienne_sf $ // // jupload - A file upload applet. // // Copyright 2007 The JUpload Team // // Created: 30.05.2007 // Creator: felfert // Last modified: $Date: 2008-04-16 00:58:02 -0700 (Wed, 16 Apr 2008) $ // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. package wjhk.jupload2.upload; import java.awt.BorderLayout; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.security.cert.CertificateExpiredException; import java.security.cert.CertificateNotYetValidException; import java.security.cert.X509Certificate; import java.util.Iterator; import java.util.StringTokenizer; import java.util.Vector; import javax.crypto.BadPaddingException; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.PasswordCallback; import javax.security.auth.callback.UnsupportedCallbackException; import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JEditorPane; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPasswordField; import wjhk.jupload2.policies.UploadPolicy; /** * An implementation of {@link javax.net.ssl.X509TrustManager} which can operate * in different modes. If mode is {@link #NONE}, then any server certificate is * accepted and no certificate-based client authentication is performed. If mode * is SERVER, then server certificates are verified and if verification is * unsuccessful, a dialog is presented to the user, which allows accepting a * certificate temporarily or permanently. If mode is CLIENT, then * certificate-based client authentication is performed. Finally, there is a * mode STRICT, which combines both SERVER and CLIENT modes. * * @author felfert */ public class InteractiveTrustManager implements X509TrustManager, CallbackHandler { /** * Mode for accepting any certificate. */ public final static int NONE = 0; /** * Mode for verifying server certificate chains. */ public final static int SERVER = 1; /** * Mode for using client certificates. */ public final static int CLIENT = 2; /** * Mode for performing both client authentication and server cert * verification. */ public final static int STRICT = SERVER + CLIENT; private UploadPolicy uploadPolicy; private int mode = STRICT; private String hostname; private final static String TS = ".truststore"; private final static String TSKEY = "javax.net.ssl.trustStore"; private final static String USERTS = System.getProperty("user.home") + File.separator + TS; /** * Absolute path of the truststore to use. */ private String tsname = null; private String tspasswd = null; private TrustManagerFactory tmf = null; private static KeyManagerFactory kmf = null; /** * The truststore for validation of server certificates */ private static KeyStore ts = null; /** * The keystore for client certificates. */ private KeyStore ks = null; private String getPassword(String storename) { JPasswordField pwf = new JPasswordField(16); JLabel l = new JLabel(String.format(this.uploadPolicy .getString("itm_prompt_pass"), storename)); l.setLabelFor(pwf); JPanel p = new JPanel(new BorderLayout(10, 0)); p.setBorder(BorderFactory.createEmptyBorder(5, 10, 5, 10)); p.add(l, BorderLayout.LINE_START); p.add(pwf, BorderLayout.LINE_END); int res = JOptionPane.showConfirmDialog(null, p, String.format( this.uploadPolicy.getString("itm_title_pass"), storename), JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE); if (res == JOptionPane.OK_OPTION) return new String(pwf.getPassword()); return null; } /** * @see javax.security.auth.callback.CallbackHandler#handle(javax.security.auth.callback.Callback[]) */ public void handle(Callback[] callbacks) throws UnsupportedCallbackException { for (int i = 0; i < callbacks.length; i++) { if (callbacks[i] instanceof PasswordCallback) { // prompt the user for sensitive information PasswordCallback pc = (PasswordCallback) callbacks[i]; String pw = getPassword(pc.getPrompt()); pc.setPassword((pw == null) ? null : pw.toCharArray()); pw = null; } else { throw new UnsupportedCallbackException(callbacks[i], "Unrecognized Callback"); } } } /** * Create a new instance. * * @param p The UploadPolicy to use for this instance. * @param hostname * @param passwd An optional password for the truststore. * @throws NoSuchAlgorithmException * @throws KeyStoreException * @throws CertificateException * @throws IllegalArgumentException * @throws UnrecoverableKeyException */ public InteractiveTrustManager(UploadPolicy p, String hostname, String passwd) throws NoSuchAlgorithmException, KeyStoreException, CertificateException, IllegalArgumentException, UnrecoverableKeyException { this.mode = p.getSslVerifyCert(); this.uploadPolicy = p; if ((this.mode & SERVER) != 0) { if (null == passwd) // The default password as distributed by Sun. passwd = "changeit"; this.tsname = System.getProperty(TSKEY); if (null == this.tsname) { // The default system-wide truststore this.tsname = System.getProperty("java.home") + File.separator + "lib" + File.separator + "security" + File.separator + "cacerts"; // If the a user-specific truststore exists, it has precedence. if (new File(USERTS).exists()) this.tsname = USERTS; } if (null == hostname || hostname.length() == 0) throw new IllegalArgumentException( "hostname may not be null or empty."); this.hostname = hostname; // Initialize the keystore only once, so that we can // reuse it during the session if (null == ts) { ts = KeyStore.getInstance(KeyStore.getDefaultType()); while (true) { try { FileInputStream is = new FileInputStream(this.tsname); ts.load(is, passwd.toCharArray()); is.close(); // need it later for eventual storing. this.tspasswd = passwd; break; } catch (IOException e) { if (e .getMessage() .equals( "Keystore was tampered with, or password was incorrect")) { passwd = getPassword(this.uploadPolicy .getString("itm_tstore")); if (null != passwd) continue; } throw new KeyStoreException("Could not load truststore"); } } } this.tmf = TrustManagerFactory.getInstance(TrustManagerFactory .getDefaultAlgorithm()); this.tmf.init(ts); } if ((this.mode & CLIENT) != 0) { String ksname = System.getProperty("javax.net.ssl.keyStore"); if (null == ksname) ksname = System.getProperty("user.home") + File.separator + ".keystore"; String cpass = "changeit"; File f = new File(ksname); if (!(f.exists() && f.isFile())) throw new KeyStoreException("Keystore " + ksname + " does not exist."); if (null == kmf) { String kstype = ksname.toLowerCase().endsWith(".p12") ? "PKCS12" : KeyStore.getDefaultType(); this.ks = KeyStore.getInstance(kstype); while (true) { try { FileInputStream is = new FileInputStream(ksname); this.ks.load(is, cpass.toCharArray()); is.close(); break; } catch (IOException e) { if ((e.getCause() instanceof BadPaddingException) || (e.getMessage() .equals("Keystore was tampered with, or password was incorrect"))) { cpass = getPassword("Keystore"); if (null != cpass) continue; } throw new KeyStoreException("Could not load keystore: " + e.getMessage()); } } kmf = KeyManagerFactory.getInstance(KeyManagerFactory .getDefaultAlgorithm()); kmf.init(this.ks, cpass.toCharArray()); } } } /** * Retrieve key managers. * * @return The current array of key managers. */ public KeyManager[] getKeyManagers() { return ((this.mode & CLIENT) == 0) ? null : kmf.getKeyManagers(); } /** * Retrieve trust managers. * * @return The current array of trust managers */ public X509TrustManager[] getTrustManagers() { return new X509TrustManager[] { this }; } /** * As this class is used on the client side only, The implementation of this * method does nothing. * * @see javax.net.ssl.X509TrustManager#checkClientTrusted(java.security.cert.X509Certificate[], * java.lang.String) */ public void checkClientTrusted(@SuppressWarnings("unused") X509Certificate[] arg0, @SuppressWarnings("unused") String arg1) { // Nothing to do. } /** * Format a DN. This method formats a DN (Distinguished Name) string as * returned from {@link javax.security.auth.x500.X500Principal#getName()} to * HTML table columns. * * @param dn The DN to format. * @param cn An optional CN (Common Name) to match against the CN in the DN. * If this parameter is non null and the CN, encoded in the DN * does not match the CN specified, it is considered an error and * the CN is printed accordingly (red). * @param reason A vector of error-strings. If the CN-comparison fails, an * explanation is added to this vector. * @return A string, containing the HTML code rendering the given DN in a * table. */ private String formatDN(String dn, String cn, Vector<String> reason) { StringBuffer ret = new StringBuffer(); StringTokenizer t = new StringTokenizer(dn, ","); while (t.hasMoreTokens()) { String tok = t.nextToken(); while (tok.endsWith("\\")) tok += t.nextToken(); String kv[] = tok.split("=", 2); if (kv.length == 2) { if (kv[0].equals("C")) ret.append("<tr><td>").append( this.uploadPolicy.getString("itm_cert_C")).append( "</td><td>").append(kv[1]).append("</td></tr>\n"); if (kv[0].equals("CN")) { boolean ok = true; if (null != cn) ok = cn.equals(kv[1]); ret.append("<tr><td>").append( this.uploadPolicy.getString("itm_cert_CN")).append( "</td><td"); ret.append(ok ? ">" : " class=\"err\">").append(kv[1]) .append("</td></tr>\n"); if (!ok) reason.add(String.format(this.uploadPolicy .getString("itm_reason_cnmatch"), cn)); } if (kv[0].equals("L")) ret.append("<tr><td>").append( this.uploadPolicy.getString("itm_cert_L")).append( "</td><td>").append(kv[1]).append("</td></tr>\n"); if (kv[0].equals("ST")) ret.append("<tr><td>").append( this.uploadPolicy.getString("itm_cert_ST")).append( "</td><td>").append(kv[1]).append("</td></tr>\n"); if (kv[0].equals("O")) ret.append("<tr><td>").append( this.uploadPolicy.getString("itm_cert_O")).append( "</td><td>").append(kv[1]).append("</td></tr>\n"); if (kv[0].equals("OU")) ret.append("<tr><td>").append( this.uploadPolicy.getString("itm_cert_OU")).append( "</td><td>").append(kv[1]).append("</td></tr>\n"); } } return ret.toString(); } private void CertDialog(X509Certificate c) throws CertificateException { int i; boolean expired = false; boolean notyet = false; Vector<String> reason = new Vector<String>(); reason.add(this.uploadPolicy.getString("itm_reason_itrust")); try { c.checkValidity(); } catch (CertificateExpiredException e1) { expired = true; reason.add(this.uploadPolicy.getString("itm_reason_expired")); } catch (CertificateNotYetValidException e2) { notyet = true; reason.add(this.uploadPolicy.getString("itm_reason_notyet")); } StringBuffer msg = new StringBuffer(); msg.append("<html><head>"); msg.append("<style type=\"text/css\">\n"); msg.append("td, th, p, body { "); msg.append("font-family: Arial, Helvetica, sans-serif; "); msg.append("font-size: 12pt; "); // PLAF hassle. The PLAF renders controls with different text colors, // but // does not set SystemColor.controlText. So we create a dummy button and // retrieve its text color. Integer ii = new Integer( new JButton(".").getForeground().getRGB() & 0x00ffffff); msg.append("color: ").append(String.format("#%06x", ii)).append(" }\n"); msg.append("th { text-align: left; }\n"); msg.append("td { margin-left: 20; }\n"); msg.append(".err { color: red; }\n"); msg.append("</style>\n"); msg.append("</head><body>"); msg.append("<h3>").append( this.uploadPolicy.getString("itm_fail_verify")).append("</h3>"); msg.append("<h4>").append( this.uploadPolicy.getString("itm_cert_details")) .append("</h4>"); msg.append("<table>"); msg.append("<tr><th colspan=2>").append( this.uploadPolicy.getString("itm_cert_subject")).append( "</th></tr>"); msg.append(formatDN(c.getSubjectX500Principal().getName(), this.hostname, reason)); msg.append("<tr><td>").append( this.uploadPolicy.getString("itm_cert_nbefore")) .append("</td>"); msg.append(notyet ? "<td class=\"err\">" : "<td>").append( c.getNotBefore()).append("</td></tr>\n"); msg.append("<tr><td>").append( this.uploadPolicy.getString("itm_cert_nafter")).append("</td>"); msg.append(expired ? "<td class=\"err\">" : "<td>").append( c.getNotAfter()).append("</td></tr>\n"); msg.append("<tr><td>").append( this.uploadPolicy.getString("itm_cert_serial")).append( "</td><td>"); msg.append(c.getSerialNumber()); msg.append("</td></tr>\n"); msg.append("<tr><td>").append( String.format(this.uploadPolicy.getString("itm_cert_fprint"), "SHA1")).append("</td><td>"); MessageDigest d; StringBuffer fp = new StringBuffer(); try { d = MessageDigest.getInstance("SHA1"); } catch (NoSuchAlgorithmException e) { throw new CertificateException( "Unable to calculate certificate SHA1 fingerprint: " + e.getMessage()); } byte[] sha1sum = d.digest(c.getEncoded()); for (i = 0; i < sha1sum.length; i++) { if (i > 0) fp.append(":"); fp.append(Integer.toHexString((sha1sum[i] >> 4) & 0x0f)); fp.append(Integer.toHexString(sha1sum[i] & 0x0f)); } msg.append(fp).append("</td></tr>\n"); fp.setLength(0); msg.append("<tr><td>").append( String.format(this.uploadPolicy.getString("itm_cert_fprint"), "MD5")).append("</td><td>"); try { d = MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException e) { throw new CertificateException( "Unable to calculate certificate MD5 fingerprint: " + e.getMessage()); } byte[] md5sum = d.digest(c.getEncoded()); for (i = 0; i < md5sum.length; i++) { if (i > 0) fp.append(":"); fp.append(Integer.toHexString((md5sum[i] >> 4) & 0x0f)); fp.append(Integer.toHexString(md5sum[i] & 0x0f)); } msg.append(fp).append("</td></tr>\n"); msg.append("</table><table>"); msg.append("<tr><th colspan=2>").append( this.uploadPolicy.getString("itm_cert_issuer")).append( "</th></tr>"); msg .append(formatDN(c.getIssuerX500Principal().getName(), null, reason)); msg.append("</table>"); msg.append("<p><b>").append(this.uploadPolicy.getString("itm_reasons")) .append("</b><br><ul>"); Iterator<String> it = reason.iterator(); while (it.hasNext()) { msg.append("<li>" + it.next() + "</li>\n"); } msg.append("</ul></p>"); msg.append("<p><b>").append( this.uploadPolicy.getString("itm_accept_prompt")).append( "</b></p>"); msg.append("</body></html>"); JPanel p = new JPanel(); p.setLayout(new BorderLayout()); JEditorPane ep = new JEditorPane("text/html", msg.toString()); ep.setEditable(false); ep.setBackground(p.getBackground()); p.add(ep, BorderLayout.CENTER); String no = this.uploadPolicy.getString("itm_accept_no"); int ans = JOptionPane.showOptionDialog(null, p, "SSL Certificate Alert", JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE, null, new String[] { this.uploadPolicy.getString("itm_accept_always"), this.uploadPolicy.getString("itm_accept_now"), no }, no); switch (ans) { case JOptionPane.CANCEL_OPTION: case JOptionPane.CLOSED_OPTION: throw new CertificateException("Server certificate rejected."); case JOptionPane.NO_OPTION: case JOptionPane.YES_OPTION: // Add certificate to truststore try { ts.setCertificateEntry(fp.toString(), c); } catch (KeyStoreException e) { throw new CertificateException( "Unable to add certificate: " + e.getMessage()); } if (ans == JOptionPane.YES_OPTION) { // Save truststore for permanent acceptance. // If not explicitely specified, we save to a // user-truststore. if (null == System.getProperty(TSKEY)) this.tsname = USERTS; while (true) { try { File f = new File(this.tsname); boolean old = false; if (f.exists()) { if (!f.renameTo(new File(this.tsname + ".old"))) throw new IOException( "Could not rename truststore"); old = true; } else { // New truststore, get a new password. this.tspasswd = this .getPassword(this.uploadPolicy .getString("itm_new_tstore")); if (null == this.tspasswd) this.tspasswd = "changeit"; } FileOutputStream os = new FileOutputStream( this.tsname); ts.store(os, this.tspasswd.toCharArray()); os.close(); if (old && (!f.delete())) throw new IOException( "Could not delete old truststore"); // Must re-initialize TrustManagerFactory this.tmf.init(ts); System.out.println("Saved cert to " + this.tsname); break; } catch (Exception e) { if (this.tsname.equals(USERTS)) throw new CertificateException(e); this.tsname = USERTS; } } } } } /** * @see javax.net.ssl.X509TrustManager#checkServerTrusted(java.security.cert.X509Certificate[], * java.lang.String) */ public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { if ((this.mode & SERVER) != 0) { if (null == chain || chain.length == 0) throw new IllegalArgumentException( "Certificate chain is null or empty"); int i; TrustManager[] mgrs = this.tmf.getTrustManagers(); for (i = 0; i < mgrs.length; i++) { if (mgrs[i] instanceof X509TrustManager) { X509TrustManager m = (X509TrustManager) (mgrs[i]); try { m.checkServerTrusted(chain, authType); return; } catch (Exception e) { // try next } } } // If we get here, the certificate could not be verified. // Ask the user what to do. CertDialog(chain[0]); } // In dummy mode: Nothing to do. } /** * @see javax.net.ssl.X509TrustManager#getAcceptedIssuers() */ public X509Certificate[] getAcceptedIssuers() { System.out.println("getAcceptedIssuers"); return new X509Certificate[0]; } }