From 2d8826005b61a19533f95160fb3a2bba07b5c024 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jairo=20Grater=C3=B3n?= <58091322+jgrateron@users.noreply.github.com> Date: Sun, 14 Apr 2024 08:43:28 -0400 Subject: [PATCH] Add export selected certificates (#490) * add export selected certificates * remove checkbox order, add icon, case 'select a key pair entry' * remove tabs * fix NPE examine crt from url * fix NPE verify from view certificate --- .../org/kse/crypto/x509/X509CertUtil.java | 73 ++++++++++ kse/src/main/java/org/kse/gui/KseFrame.java | 9 ++ .../gui/actions/ExamineClipboardAction.java | 20 +-- .../ExportSelectedCertificatesAction.java | 136 ++++++++++++++++++ .../org/kse/gui/dialogs/DViewCertificate.java | 3 +- .../importexport/DExportCertificates.java | 28 +++- .../actions/images/exportselectedcerts.png | Bin 0 -> 1627 bytes .../org/kse/gui/actions/resources.properties | 8 ++ .../dialogs/importexport/resources.properties | 1 + 9 files changed, 260 insertions(+), 18 deletions(-) create mode 100644 kse/src/main/java/org/kse/gui/actions/ExportSelectedCertificatesAction.java create mode 100644 kse/src/main/resources/org/kse/gui/actions/images/exportselectedcerts.png diff --git a/kse/src/main/java/org/kse/crypto/x509/X509CertUtil.java b/kse/src/main/java/org/kse/crypto/x509/X509CertUtil.java index e2821b0cd..aa48f07ff 100644 --- a/kse/src/main/java/org/kse/crypto/x509/X509CertUtil.java +++ b/kse/src/main/java/org/kse/crypto/x509/X509CertUtil.java @@ -328,6 +328,79 @@ public static X509Certificate[] orderX509CertChain(X509Certificate[] certs) { // Return longest path return longestPath.toArray(new X509Certificate[0]); } + /* + * Tries to sort the certificates according to their hierarchy, + * and adds at the end those that have no dependencies. + */ + public static X509Certificate[] orderX509CertsChain(X509Certificate[] certs) { + if (certs == null) { + return new X509Certificate[0]; + } + if (certs.length <= 1) { + return certs; + } + ArrayList> paths = new ArrayList<>(); + for (X509Certificate cert : certs) { + ArrayList path = new ArrayList<>(); + path.add(cert); + for (X509Certificate issuerCert : certs) { + if (certificatesEquals(issuerCert, cert)) { + continue; + } + if (isIssuedBy(cert, issuerCert)) { + path.add(issuerCert); + } + } + if (path.size() > 1) { + paths.add(path); + } + } + List listCertificates = new ArrayList<>(); + for (ArrayList path : paths) { + X509Certificate cert = path.get(0); + X509Certificate issuerCert = path.get(1); + int posIssuer = -1; + int posCert = -1; + for (int i = 0; i < listCertificates.size(); i++) { + X509Certificate cert2 = listCertificates.get(i); + if (certificatesEquals(issuerCert, cert2)) { + posIssuer = i; + } + if (certificatesEquals(cert, cert2)) { + posCert = i; + } + } + if (posIssuer == -1) { + if (posCert == -1) { + listCertificates.add(cert); + } + listCertificates.add(issuerCert); + } else { + listCertificates.add(posIssuer, cert); + } + } + if (listCertificates.size() != certs.length) { + for (X509Certificate cert1 : certs) { + boolean found = false; + for (X509Certificate cert2 : listCertificates) { + if (certificatesEquals(cert1, cert2)) { + found = true; + break; + } + } + if (!found) { + listCertificates.add(cert1); + } + } + } + return listCertificates.toArray(new X509Certificate[0]); + } + + private static boolean certificatesEquals(X509Certificate cert1, X509Certificate cert2) { + return cert1.getSubjectX500Principal().equals(cert2.getSubjectX500Principal()) + && cert1.getIssuerX500Principal().equals(cert2.getIssuerX500Principal()) + && cert1.getSerialNumber().equals(cert2.getSerialNumber()); + } private static X509Certificate findIssuedCert(X509Certificate issuerCert, X509Certificate[] certs) { // Find a certificate issued by the supplied certificate diff --git a/kse/src/main/java/org/kse/gui/KseFrame.java b/kse/src/main/java/org/kse/gui/KseFrame.java index 88b10c83f..da2ff209c 100644 --- a/kse/src/main/java/org/kse/gui/KseFrame.java +++ b/kse/src/main/java/org/kse/gui/KseFrame.java @@ -121,6 +121,7 @@ import org.kse.gui.actions.ExportKeyPairCertificateChainAction; import org.kse.gui.actions.ExportKeyPairPrivateKeyAction; import org.kse.gui.actions.ExportKeyPairPublicKeyAction; +import org.kse.gui.actions.ExportSelectedCertificatesAction; import org.kse.gui.actions.ExportTrustedCertificateAction; import org.kse.gui.actions.ExportTrustedCertificatePublicKeyAction; import org.kse.gui.actions.FindAction; @@ -400,6 +401,7 @@ public final class KseFrame implements StatusBar { private JMenuItem jmiMultEntrySelCopy; private JMenuItem jmiMultiEntrySelDelete; private JMenuItem jmiMultiEntryCompare; + private JMenuItem jmiMultiEntryExport; // // Main display controls @@ -530,6 +532,7 @@ public final class KseFrame implements StatusBar { private final SetKeyPasswordAction setKeyPasswordAction = new SetKeyPasswordAction(this); private final DeleteKeyAction deleteKeyAction = new DeleteKeyAction(this); private final RenameKeyAction renameKeyAction = new RenameKeyAction(this); + private final ExportSelectedCertificatesAction exportSelectedCertificatesAction = new ExportSelectedCertificatesAction(this); // // Action map keys - map input to action @@ -2197,11 +2200,17 @@ private void initKeyStoreEntryPopupMenus() { new StatusBarChangeHandler(jmiMultiEntryCompare, (String) compareCertificateAction.getValue(Action.LONG_DESCRIPTION), this); + jmiMultiEntryExport = new JMenuItem(exportSelectedCertificatesAction); + jmiMultiEntryExport.setToolTipText(null); + new StatusBarChangeHandler(jmiMultiEntryExport, + (String) exportSelectedCertificatesAction.getValue(Action.LONG_DESCRIPTION), this); + jpmMultiEntrySel = new JPopupMenu(); jpmMultiEntrySel.add(jmiMultiEntrySelCut); jpmMultiEntrySel.add(jmiMultEntrySelCopy); jpmMultiEntrySel.add(jmiMultiEntrySelDelete); jpmMultiEntrySel.add(jmiMultiEntryCompare); + jpmMultiEntrySel.add(jmiMultiEntryExport); } diff --git a/kse/src/main/java/org/kse/gui/actions/ExamineClipboardAction.java b/kse/src/main/java/org/kse/gui/actions/ExamineClipboardAction.java index 2f019505a..ea3143b59 100644 --- a/kse/src/main/java/org/kse/gui/actions/ExamineClipboardAction.java +++ b/kse/src/main/java/org/kse/gui/actions/ExamineClipboardAction.java @@ -240,19 +240,19 @@ private void downloadCrl(URL url) throws IOException, CryptoException { } private void downloadCert(URL url) throws IOException, CryptoException { - HttpURLConnection urlConn = (HttpURLConnection) url.openConnection(); - int status = urlConn.getResponseCode(); - if (isRedirect(status)) { - String newUrl = urlConn.getHeaderField("Location"); - url = new URL(newUrl); - urlConn = (HttpURLConnection) url.openConnection(); - } + HttpURLConnection urlConn = (HttpURLConnection) url.openConnection(); + int status = urlConn.getResponseCode(); + if (isRedirect(status)) { + String newUrl = urlConn.getHeaderField("Location"); + url = new URL(newUrl); + urlConn = (HttpURLConnection) url.openConnection(); + } try (InputStream is = urlConn.getInputStream()) { X509Certificate[] certs = X509CertUtil.loadCertificates(IOUtils.toByteArray(is)); if (certs != null && certs.length > 0) { - DViewCertificate dViewCertificate = new DViewCertificate(frame, MessageFormat.format( - resExt.getString("DViewExtensions.ViewCert.Title"), url.toString()), certs, null, - DViewCertificate.NONE); + DViewCertificate dViewCertificate = new DViewCertificate(frame, + MessageFormat.format(resExt.getString("DViewExtensions.ViewCert.Title"), url.toString()), certs, + this.kseFrame, DViewCertificate.NONE); dViewCertificate.setLocationRelativeTo(frame); dViewCertificate.setVisible(true); } diff --git a/kse/src/main/java/org/kse/gui/actions/ExportSelectedCertificatesAction.java b/kse/src/main/java/org/kse/gui/actions/ExportSelectedCertificatesAction.java new file mode 100644 index 000000000..e558ae977 --- /dev/null +++ b/kse/src/main/java/org/kse/gui/actions/ExportSelectedCertificatesAction.java @@ -0,0 +1,136 @@ +package org.kse.gui.actions; + +import java.awt.Toolkit; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.swing.ImageIcon; +import javax.swing.JOptionPane; + +import org.kse.crypto.keystore.KeyStoreUtil; +import org.kse.crypto.x509.X509CertUtil; +import org.kse.gui.KseFrame; +import org.kse.gui.dialogs.importexport.DExportCertificates; +import org.kse.gui.error.DError; +import org.kse.utilities.history.KeyStoreHistory; +import org.kse.utilities.history.KeyStoreState; +import org.kse.utilities.io.FileNameUtil; + +/** + * Action to export selected certificates. + */ +public class ExportSelectedCertificatesAction extends KeyStoreExplorerAction { + + private static final long serialVersionUID = 1L; + + public ExportSelectedCertificatesAction(KseFrame kseFrame) { + super(kseFrame); + putValue(LONG_DESCRIPTION, res.getString("ExportSelectedCertificatesAction.statusbar")); + putValue(NAME, res.getString("ExportSelectedCertificatesAction.text")); + putValue(SHORT_DESCRIPTION, res.getString("ExportSelectedCertificatesAction.tooltip")); + putValue(SMALL_ICON, new ImageIcon( + Toolkit.getDefaultToolkit().createImage(getClass().getResource("images/exportselectedcerts.png")))); + } + + @Override + protected void doAction() { + List listCertificate = getCertificates(); + File exportFile = null; + try { + if (listCertificate.size() > 0) { + KeyStoreHistory history = kseFrame.getActiveKeyStoreHistory(); + String fileName = FileNameUtil.removeExtension(history.getName()); + DExportCertificates dExportCertificates = new DExportCertificates(frame, fileName, false, true); + dExportCertificates.setLocationRelativeTo(frame); + dExportCertificates.setVisible(true); + + if (!dExportCertificates.exportSelected()) { + return; + } + X509Certificate[] certs = listCertificate.toArray(new X509Certificate[0]); + certs = X509CertUtil.orderX509CertsChain(certs); + exportFile = dExportCertificates.getExportFile(); + + boolean pemEncode = dExportCertificates.pemEncode(); + + byte[] encoded = null; + + if (dExportCertificates.exportFormatX509()) { + encoded = X509CertUtil.getCertsEncodedX509Pem(certs).getBytes(); + } else if (dExportCertificates.exportFormatPkcs7()) { + if (pemEncode) { + encoded = X509CertUtil.getCertsEncodedPkcs7Pem(certs).getBytes(); + } else { + encoded = X509CertUtil.getCertsEncodedPkcs7(certs); + } + } else if (dExportCertificates.exportFormatPkiPath()) { + encoded = X509CertUtil.getCertsEncodedPkiPath(certs); + } else if (dExportCertificates.exportFormatSpc()) { + encoded = X509CertUtil.getCertsEncodedPkcs7(certs); // SPC is just DER PKCS #7 + } + exportEncodedCertificate(encoded, exportFile); + JOptionPane.showMessageDialog(frame, + res.getString("ExportSelectedCertificatesAction.ExportCertificateSuccessful.message"), + res.getString("ExportSelectedCertificatesAction.ExportCertificate.Title"), + JOptionPane.INFORMATION_MESSAGE); + } else { + JOptionPane.showMessageDialog(frame, res.getString("ExportSelectedCertificatesAction.onemore.message"), + res.getString("ExportSelectedCertificatesAction.ExportCertificate.Title"), + JOptionPane.WARNING_MESSAGE); + } + } catch (FileNotFoundException ex) { + String message = MessageFormat.format(res.getString("ExportSelectedCertificatesAction.NoWriteFile.message"), + exportFile); + + JOptionPane.showMessageDialog(frame, message, + res.getString("ExportSelectedCertificatesAction.ExportCertificate.Title"), + JOptionPane.WARNING_MESSAGE); + } catch (Exception ex) { + DError.displayError(frame, ex); + } + } + + private List getCertificates() { + KeyStoreHistory history = kseFrame.getActiveKeyStoreHistory(); + KeyStoreState currentState = history.getCurrentState(); + + String[] aliases = kseFrame.getSelectedEntryAliases(); + try { + List listCertificates = new ArrayList<>(); + KeyStore keyStore = currentState.getKeyStore(); + for (String alias : aliases) { + if (KeyStoreUtil.isTrustedCertificateEntry(alias, keyStore)) { + Certificate certificate = keyStore.getCertificate(alias); + listCertificates.add((X509Certificate) certificate); + } else if (KeyStoreUtil.isKeyPairEntry(alias, keyStore)) { + Certificate[] chain = keyStore.getCertificateChain(alias); + if (chain != null) { + for (Certificate certificate : chain) { + listCertificates.add((X509Certificate) certificate); + } + } + } + } + return listCertificates; + } catch (Exception ex) { + DError.displayError(frame, ex); + return Collections.emptyList(); + } + } + + private void exportEncodedCertificate(byte[] encoded, File exportFile) throws IOException { + try (FileOutputStream fos = new FileOutputStream(exportFile)) { + fos.write(encoded); + fos.flush(); + } + } +} diff --git a/kse/src/main/java/org/kse/gui/dialogs/DViewCertificate.java b/kse/src/main/java/org/kse/gui/dialogs/DViewCertificate.java index 4607af14c..c5e0eec27 100644 --- a/kse/src/main/java/org/kse/gui/dialogs/DViewCertificate.java +++ b/kse/src/main/java/org/kse/gui/dialogs/DViewCertificate.java @@ -271,6 +271,7 @@ private void initComponents(X509Certificate[] certs) throws CryptoException { jbVerify = new JButton(res.getString("DViewCertificate.jbVerify.text")); jbVerify.setToolTipText(res.getString("DViewCertificate.jbVerify.tooltip")); + jbVerify.setVisible(importExport != NONE); PlatformUtil.setMnemonic(jbVerify, res.getString("DViewCertificate.jbVerify.mnemonic").charAt(0)); Container pane = getContentPane(); @@ -302,7 +303,7 @@ private void initComponents(X509Certificate[] certs) throws CryptoException { pane.add(jbExport, "hidemode 1"); pane.add(jbExtensions, ""); pane.add(jbPem, ""); - pane.add(jbVerify, ""); + pane.add(jbVerify, "hidemode 1"); pane.add(jbAsn1, "wrap"); pane.add(new JSeparator(), "spanx, growx, wrap 15:push"); pane.add(jbOK, "spanx, tag ok"); diff --git a/kse/src/main/java/org/kse/gui/dialogs/importexport/DExportCertificates.java b/kse/src/main/java/org/kse/gui/dialogs/importexport/DExportCertificates.java index 6d8bdc0b6..c35eea322 100644 --- a/kse/src/main/java/org/kse/gui/dialogs/importexport/DExportCertificates.java +++ b/kse/src/main/java/org/kse/gui/dialogs/importexport/DExportCertificates.java @@ -91,6 +91,7 @@ public class DExportCertificates extends JEscDialog { private boolean formatPkiPath; private boolean formatSpc; private boolean pemEncode; + private boolean certificateSelected = false; /** * Creates a new DExportCertificate dialog. @@ -106,6 +107,14 @@ public DExportCertificates(JFrame parent, String certificateAlias, boolean chain initComponents(); } + public DExportCertificates(JFrame parent, String certificateAlias, boolean chain, boolean certificateSelected) { + super(parent, Dialog.ModalityType.DOCUMENT_MODAL); + this.certificateAlias = certificateAlias; + this.chain = chain; + this.certificateSelected = certificateSelected; + initComponents(); + } + private void initComponents() { jlExportLength = new JLabel(res.getString("DExportCertificates.jlExportLength.text")); @@ -155,7 +164,9 @@ private void initComponents() { jcbExportPem = new JCheckBox(); jcbExportPem.setSelected(true); jcbExportPem.setToolTipText(res.getString("DExportCertificates.jcbExportPem.tooltip")); - + if (jrbExportChain.isSelected() && jrbExportX509.isSelected()) { + jcbExportPem.setEnabled(false); + } jlExportFile = new JLabel(res.getString("DExportCertificates.jlExportFile.text")); jtfExportFile = new JTextField(30); @@ -282,12 +293,15 @@ public void windowClosing(WindowEvent evt) { } }); - if (chain) { - setTitle(MessageFormat.format(res.getString("DExportCertificates.CertificateChain.Title"), - certificateAlias)); - } else { - setTitle(MessageFormat.format(res.getString("DExportCertificates.Certificate.Title"), certificateAlias)); - } + if (certificateSelected) { + setTitle(MessageFormat.format(res.getString("DExportCertificates.CertificateSelected.Title"), + certificateAlias)); + } else if (chain) { + setTitle(MessageFormat.format(res.getString("DExportCertificates.CertificateChain.Title"), + certificateAlias)); + } else { + setTitle(MessageFormat.format(res.getString("DExportCertificates.Certificate.Title"), certificateAlias)); + } setResizable(false); diff --git a/kse/src/main/resources/org/kse/gui/actions/images/exportselectedcerts.png b/kse/src/main/resources/org/kse/gui/actions/images/exportselectedcerts.png new file mode 100644 index 0000000000000000000000000000000000000000..dea14d653609c2b99f10fd27079f79bdf7d05d3b GIT binary patch literal 1627 zcmaJ>drT8|9Pc1HVMu@>Fhw|zO_#8?cfGdIlcLvNYb(?Tppe-`kM{Z)^seh&X;C(V zV0b8qLCl<}z_KtOKvdXtDu`elK7h^P6v$x1=zKvO8)~-Tt`!)6EM9W=dwo8i@AG#h z>(;EA@4wKW$K%b{YVZ_p4D!75=5pWY@s|WQyu+$8*i_oYx(El!!;Ew$321GEnM@%G zW9}ziWCD-pYqh3juo=2!)Ii$=ga;#V+w2^h$4gk@wiAXdk_9qJvz3yAW9QmHz-p9( z=@K2Jvnxr9RkPJWrfyx6X4sl#kQu=hi9muIw0v_W>Y+U`Pmwgs_MY$DxP_g&<(+0Xb_9 zqX|vH)l;^(l^nFNtQ{2!T`rfvg$QVeSqRHyGLMEx#ODxvCYNFfH=kl+rWJ6KF*vMt z)=E==N0G>+ova+>Jbe{{&7NX4TUcgBoMNVu#idQ?ChS6300|vlj3>TnmQMG2hRrsE zX4n++Kj;5fpGnKLlfo2|p`8u`S4C6IEK{#HK@u$ONTccO>1J4Gp;?--&~`wXiU8?W%1FByPn|fgJbsc6YYu5tJ7^o= zO&MyPW!>rIOk+w|EmFlJh)9g#Y8b{aMZ8ji$RIpkqL3&MaEfT06|$G8l;Mb2CY6dY zM2X`ttbpP%g+ifJtKm2iERljj&JCfbKW6pI^cL5Ip6N{YbBme#LsDG29o+4B4*KNq zcs|8i97}WeXQw7<(umN>(ZOxzyxj#~j$GGQTB-}7;AY+4h`X0Q4laCBw^1->)46#& zD*AE0Pxxqmb8x3>SH6Bupytn&hssYqH-nnX3@Lko~nGIm6ZUEqzB|-I@OW zihH!QGCz3U%Dnl>)l2Rd4nC}EN#?g#G%wzy$9tiQ*s37i(%6%{%A=3(r6-g}$j)~M zy`6XB_t9%XC5GY4WnEpt-!^7FNb`TnF1$d+oi8iP7#b?twBgqW4;6Oz?Pv1Hob|{u z)Yg`>{X#`g>=zyR>cCq9VP#LcPiA!FVr*cbprID4RX3IXG?4H|{YQ@*1;S@pf^_q) z#*X&V5`EwS#T!ve`IY0j<~NUog|$|^JkP^b=GYH?mn8Nc)%(w_U9UIwZS?P1e#mli zAa+8tIl3@r@Yv{DpGUGAciXXJm&dLw9D9;Q7u~#H&{ZFLq_Z&f*n*uC1*_KD0-KcM z;}iF~yC;`+K-tx;%~2yOV`_U%x5RCJ^{c}gCQ7dy{0<8_{K(f=Ee_GP9z0M|dU{zz zKtOM}=xl(~H?*OFdW#P3iAqk^{~#+UyP;b$w4j)uSmJl(w~dJh`>v||1|t`ps|nww zAy0QT6%{d$`$E0vtIq77d+O)FOW`#mfye(kOx=+VY�?N`4Dit=51w&52EHN0(0 zzU%L<+uyv~TWqT9TK#?pzxvbatD99VXMSwCK2h=gWY`v7!P6H3yrcWlJ->UcY7O3~ H*s|ka4wr2q literal 0 HcmV?d00001 diff --git a/kse/src/main/resources/org/kse/gui/actions/resources.properties b/kse/src/main/resources/org/kse/gui/actions/resources.properties index f47564666..ba97f7c06 100644 --- a/kse/src/main/resources/org/kse/gui/actions/resources.properties +++ b/kse/src/main/resources/org/kse/gui/actions/resources.properties @@ -233,6 +233,14 @@ ExportTrustedCertificateAction.statusbar = Export the ExportTrustedCertificateAction.text = Export Certificate ExportTrustedCertificateAction.tooltip = Export Trusted Certificate entry +ExportSelectedCertificatesAction.statusbar = Export the Selected Certificates entries as X.509 PKCS #7, PKI Path or SPC +ExportSelectedCertificatesAction.text = Export Certificates +ExportSelectedCertificatesAction.tooltip = Export Selected Certificates +ExportSelectedCertificatesAction.ExportCertificate.Title = Export Selected Certificates +ExportSelectedCertificatesAction.ExportCertificateSuccessful.message = Export Selected Certificates Successful. +ExportSelectedCertificatesAction.onemore.message = You must select one or more certificates +ExportSelectedCertificatesAction.NoWriteFile.message = Could not write to file ''{0}''. + ExportTrustedCertificatePublicKeyAction.ExportPublicKeyOpenSsl.Title = Export Public Key as OpenSSL ExportTrustedCertificatePublicKeyAction.ExportPublicKeyOpenSslSuccessful.message = Export Public Key as OpenSSL Successful. ExportTrustedCertificatePublicKeyAction.NoAccessEntry.message = Could not access KeyStore entry ''{0}''. diff --git a/kse/src/main/resources/org/kse/gui/dialogs/importexport/resources.properties b/kse/src/main/resources/org/kse/gui/dialogs/importexport/resources.properties index 42ac190f1..5d0f80cf0 100644 --- a/kse/src/main/resources/org/kse/gui/dialogs/importexport/resources.properties +++ b/kse/src/main/resources/org/kse/gui/dialogs/importexport/resources.properties @@ -1,6 +1,7 @@ DExportCertificates.Certificate.Simple.Title = Export Certificate DExportCertificates.Certificate.Title = Export Certificate ''{0}'' +DExportCertificates.CertificateSelected.Title = Export Selected Certificates ''{0}'' DExportCertificates.CertificateChain.Simple.Title = Export Certificate Chain DExportCertificates.CertificateChain.Title = Export Certificate Chain ''{0}'' DExportCertificates.ChooseExportFile.Title = Choose Certificate Export File