/* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * * Copyright @ 2015 Atlassian Pty Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.java.sip.communicator.plugin.desktoputil; import java.awt.*; import java.security.*; import java.security.cert.*; import java.security.cert.Certificate; import java.security.interfaces.*; import java.util.*; import javax.naming.*; import javax.naming.ldap.*; import javax.security.auth.x500.*; import javax.swing.*; import javax.swing.border.*; import javax.swing.event.*; import javax.swing.text.*; import javax.swing.tree.*; import org.jitsi.service.resources.*; /** * Panel that shows the content of an X509Certificate. */ public class X509CertificatePanel extends TransparentPanel { private static final long serialVersionUID = -8368302061995971947L; private final JEditorPane infoTextPane = new JEditorPane(); private final ResourceManagementService R = DesktopUtilActivator.getResources(); /** * Constructs a X509 certificate panel from a single certificate. * If a chain is available instead use the second constructor. * This constructor is kept for backwards compatibility and for convenience * when there is only one certificate of interest. * * @param certificate <tt>X509Certificate</tt> object */ public X509CertificatePanel(Certificate certificate) { this(new Certificate[] { certificate }); } /** * Constructs a X509 certificate panel. * * @param certificates <tt>X509Certificate</tt> objects */ public X509CertificatePanel(Certificate[] certificates) { setLayout(new BorderLayout(5, 5)); // Certificate chain list TransparentPanel topPanel = new TransparentPanel(new BorderLayout()); topPanel.add(new JLabel("<html><body><b>" + R.getI18NString("service.gui.CERT_INFO_CHAIN") + "</b></body></html>"), BorderLayout.NORTH); DefaultMutableTreeNode top = new DefaultMutableTreeNode(); DefaultMutableTreeNode previous = top; for (int i = certificates.length - 1; i >= 0; i--) { Certificate cert = certificates[i]; DefaultMutableTreeNode next = new DefaultMutableTreeNode(cert); previous.add(next); previous = next; } JTree tree = new JTree(top); tree.setBorder(new BevelBorder(BevelBorder.LOWERED)); tree.setRootVisible(false); tree.setExpandsSelectedPaths(true); tree.getSelectionModel().setSelectionMode( TreeSelectionModel.SINGLE_TREE_SELECTION); tree.setCellRenderer(new DefaultTreeCellRenderer() { @Override public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) { JLabel component = (JLabel) super.getTreeCellRendererComponent( tree, value, sel, expanded, leaf, row, hasFocus); if (value instanceof DefaultMutableTreeNode) { Object o = ((DefaultMutableTreeNode) value).getUserObject(); if (o instanceof X509Certificate) { component.setText( getSimplifiedName((X509Certificate) o)); } else { // We don't know how to represent this certificate type, // let's use the first 20 characters String text = o.toString(); if (text.length() > 20) { text = text.substring(0, 20); } component.setText(text); } } return component; } }); tree.getSelectionModel().addTreeSelectionListener( new TreeSelectionListener() { @Override public void valueChanged(TreeSelectionEvent e) { valueChangedPerformed(e); } }); tree.setSelectionPath(new TreePath((( (DefaultTreeModel)tree.getModel()).getPathToRoot(previous)))); topPanel.add(tree, BorderLayout.CENTER); add(topPanel, BorderLayout.NORTH); // Certificate details pane Caret caret = infoTextPane.getCaret(); if (caret instanceof DefaultCaret) { ((DefaultCaret) caret).setUpdatePolicy(DefaultCaret.NEVER_UPDATE); } /* * Make JEditorPane respect our default font because we will be using it * to just display text. */ infoTextPane.putClientProperty( JEditorPane.HONOR_DISPLAY_PROPERTIES, true); infoTextPane.setOpaque(false); infoTextPane.setEditable(false); infoTextPane.setContentType("text/html"); infoTextPane.setText(toString(certificates[0])); final JScrollPane certScroll = new JScrollPane(infoTextPane); certScroll.setPreferredSize(new Dimension(300, 500)); add(certScroll, BorderLayout.CENTER); } /** * Creates a String representation of the given object. * @param certificate to print * @return the String representation */ private String toString(Object certificate) { final StringBuilder sb = new StringBuilder(); sb.append("<html><body>\n"); if (certificate instanceof X509Certificate) { renderX509(sb, (X509Certificate) certificate); } else { sb.append("<pre>\n"); sb.append(certificate.toString()); sb.append("</pre>\n"); } sb.append("</body></html>"); return sb.toString(); } /** * Appends an HTML representation of the given X509Certificate. * @param sb StringBuilder to append to * @param certificate to print */ private void renderX509(StringBuilder sb, X509Certificate certificate) { X500Principal issuer = certificate.getIssuerX500Principal(); X500Principal subject = certificate.getSubjectX500Principal(); sb.append("<table cellspacing='1' cellpadding='1'>\n"); // subject addTitle(sb, R.getI18NString("service.gui.CERT_INFO_ISSUED_TO")); try { for(Rdn name : new LdapName(subject.getName()).getRdns()) { String nameType = name.getType(); String lblKey = "service.gui.CERT_INFO_" + nameType; String lbl = R.getI18NString(lblKey); if ((lbl == null) || ("!" + lblKey + "!").equals(lbl)) lbl = nameType; final String value; Object nameValue = name.getValue(); if (nameValue instanceof byte[]) { byte[] nameValueAsByteArray = (byte[]) nameValue; value = getHex(nameValueAsByteArray) + " (" + new String(nameValueAsByteArray) + ")"; } else value = nameValue.toString(); addField(sb, lbl, value); } } catch (InvalidNameException ine) { addField(sb, R.getI18NString("service.gui.CERT_INFO_CN"), subject.getName()); } // issuer addTitle(sb, R.getI18NString("service.gui.CERT_INFO_ISSUED_BY")); try { for(Rdn name : new LdapName(issuer.getName()).getRdns()) { String nameType = name.getType(); String lblKey = "service.gui.CERT_INFO_" + nameType; String lbl = R.getI18NString(lblKey); if ((lbl == null) || ("!" + lblKey + "!").equals(lbl)) lbl = nameType; final String value; Object nameValue = name.getValue(); if (nameValue instanceof byte[]) { byte[] nameValueAsByteArray = (byte[]) nameValue; value = getHex(nameValueAsByteArray) + " (" + new String(nameValueAsByteArray) + ")"; } else value = nameValue.toString(); addField(sb, lbl, value); } } catch (InvalidNameException ine) { addField(sb, R.getI18NString("service.gui.CERT_INFO_CN"), issuer.getName()); } // validity addTitle(sb, R.getI18NString("service.gui.CERT_INFO_VALIDITY")); addField(sb, R.getI18NString("service.gui.CERT_INFO_ISSUED_ON"), certificate.getNotBefore().toString()); addField(sb, R.getI18NString("service.gui.CERT_INFO_EXPIRES_ON"), certificate.getNotAfter().toString()); addTitle(sb, R.getI18NString("service.gui.CERT_INFO_FINGERPRINTS")); try { String sha256String = getThumbprint(certificate, "SHA-256"); String sha1String = getThumbprint(certificate, "SHA1"); addField(sb, "SHA256:", sha256String, 48); addField(sb, "SHA1:", sha1String, 72); } catch (CertificateException e) { // do nothing as we cannot show this value } addTitle(sb, R.getI18NString("service.gui.CERT_INFO_CERT_DETAILS")); addField(sb, R.getI18NString("service.gui.CERT_INFO_SER_NUM"), certificate.getSerialNumber().toString()); addField(sb, R.getI18NString("service.gui.CERT_INFO_VER"), String.valueOf(certificate.getVersion())); addField(sb, R.getI18NString("service.gui.CERT_INFO_SIGN_ALG"), String.valueOf(certificate.getSigAlgName())); addTitle(sb, R.getI18NString("service.gui.CERT_INFO_PUB_KEY_INFO")); addField(sb, R.getI18NString("service.gui.CERT_INFO_ALG"), certificate.getPublicKey().getAlgorithm()); if(certificate.getPublicKey().getAlgorithm().equals("RSA")) { RSAPublicKey key = (RSAPublicKey)certificate.getPublicKey(); addField(sb, R.getI18NString("service.gui.CERT_INFO_PUB_KEY"), R.getI18NString("service.gui.CERT_INFO_KEY_BITS_PRINT", new String[]{ String.valueOf( (key.getModulus().toByteArray().length-1)*8) }), getHex(key.getModulus().toByteArray()), 48); addField(sb, R.getI18NString("service.gui.CERT_INFO_EXP"), key.getPublicExponent().toString()); addField(sb, R.getI18NString("service.gui.CERT_INFO_KEY_SIZE"), R.getI18NString( "service.gui.CERT_INFO_KEY_BITS_PRINT", new String[]{ String.valueOf(key.getModulus().bitLength())})); } else if(certificate.getPublicKey().getAlgorithm().equals("DSA")) { DSAPublicKey key = (DSAPublicKey)certificate.getPublicKey(); addField(sb, "Y:", key.getY().toString(16)); } addField(sb, R.getI18NString("service.gui.CERT_INFO_SIGN"), R.getI18NString( "service.gui.CERT_INFO_KEY_BITS_PRINT", new String[]{ String.valueOf(certificate.getSignature().length*8), }), getHex(certificate.getSignature()), 48); sb.append("</table>\n"); } /** * Add a title. * * @param sb StringBuilder to append to * @param title to print */ private void addTitle(StringBuilder sb, String title) { sb.append("<tr><td colspan='2'") .append(" style='margin-top: 5pt; white-space: nowrap'><p><b>") .append(title).append("</b></p></td></tr>\n"); } /** * Add a field. * @param sb StringBuilder to append to * @param field name of the certificate field * @param value to print */ private void addField(StringBuilder sb, String field, String value) { addField(sb, field, value, null, 0); } /** * Add a field. * @param sb StringBuilder to append to * @param field name of the certificate field * @param value to print * @param wrap force-wrap after number of characters */ private void addField(StringBuilder sb, String field, String value, int wrap) { addField(sb, field, value, null, wrap); } /** * Add a field. * @param sb StringBuilder to append to * @param field name of the certificate field * @param value to print (not wrapped) * @param otherValue second line of value to print (wrapped) * @param wrap force-wrap after number of characters */ private void addField(StringBuilder sb, String field, String value, String otherValue, int wrap) { sb.append("<tr><td style='margin-left: 5pt; margin-right: 25pt;") .append("white-space: nowrap' valign='top'>") .append(field).append("</td><td><span"); if (otherValue != null) { sb.append('>').append(value).append("</span><br/><span"); value = otherValue; } if (wrap > 0) { sb.append(" style='font-family:monospace'>"); for (int i = 0; i < value.length(); i++) { if (i % wrap == 0 && i > 0) { sb.append("<br/>"); } sb.append(value.charAt(i)); } } else { sb.append(">"); sb.append(value); } sb.append("</span></td></tr>"); } /** * Converts the byte array to hex string. * @param raw the data. * @return the hex string. */ private String getHex( byte [] raw ) { if (raw == null) return null; StringBuilder hex = new StringBuilder(2 * raw.length); Formatter f = new Formatter(hex); try { for (byte b : raw) f.format("%02X:", b); } finally { f.close(); } return hex.substring(0, hex.length() - 1); } /** * Calculates the hash of the certificate known as the "thumbprint" * and returns it as a string representation. * * @param cert The certificate to hash. * @param algorithm The hash algorithm to use. * @return The SHA-1 hash of the certificate. * @throws CertificateException */ private static String getThumbprint(X509Certificate cert, String algorithm) throws CertificateException { MessageDigest digest; try { digest = MessageDigest.getInstance(algorithm); } catch (NoSuchAlgorithmException e) { throw new CertificateException(e); } byte[] encodedCert = cert.getEncoded(); StringBuilder sb = new StringBuilder(encodedCert.length * 2); Formatter f = new Formatter(sb); try { for (byte b : digest.digest(encodedCert)) f.format("%02X:", b); } finally { f.close(); } return sb.substring(0, sb.length() - 1); } /** * Construct a "simplified name" based on the subject DN from the * certificate. The purpose is to have something shorter to display in the * list. The name used is one of the following DN parts, if * available, otherwise the complete DN: * 'CN', 'OU' or else 'O'. * @param cert to read subject DN from * @return the simplified name */ private static String getSimplifiedName(X509Certificate cert) { final HashMap<String, String> parts = new HashMap<String, String>(); try { for (Rdn name : new LdapName( cert.getSubjectX500Principal().getName()).getRdns()) { if (name.getType() != null && name.getValue() != null) { parts.put(name.getType(), name.getValue().toString()); } } } catch (InvalidNameException ignored) // NOPMD { } String result = parts.get("CN"); if (result == null) { result = parts.get("OU"); } if (result == null) { result = parts.get("O"); } if (result == null) { result = cert.getSubjectX500Principal().getName(); } return result; } /** * Called when the selection changed in the tree. * Loads the selected certificate. * @param e the event */ private void valueChangedPerformed(TreeSelectionEvent e) { Object o = e.getNewLeadSelectionPath().getLastPathComponent(); if (o instanceof DefaultMutableTreeNode) { DefaultMutableTreeNode node = (DefaultMutableTreeNode) o; infoTextPane.setText(toString(node.getUserObject())); } } }