diff --git a/AUTHORS b/AUTHORS index 8d40f0d8b..ed10652bc 100644 --- a/AUTHORS +++ b/AUTHORS @@ -39,6 +39,7 @@ Ville Skyttä Fridrich Strba Andrew Su Joshua Sumali +David Tavoularis Joel Tesdall Michal Vala Jiri Vanek diff --git a/core/src/main/java/net/adoptopenjdk/icedteaweb/client/parts/dialogs/security/ClientCertSelectionPane.java b/core/src/main/java/net/adoptopenjdk/icedteaweb/client/parts/dialogs/security/ClientCertSelectionPane.java new file mode 100644 index 000000000..875b42e58 --- /dev/null +++ b/core/src/main/java/net/adoptopenjdk/icedteaweb/client/parts/dialogs/security/ClientCertSelectionPane.java @@ -0,0 +1,274 @@ +/* ClientCertSelectionPane.java + Copyright (C) 2021 Karakun AG. + +This file is part of IcedTea. + +IcedTea 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, version 2. + +IcedTea 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 +IcedTea; see the file COPYING. If not, write to the +Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +02110-1301 USA. + +Linking this library statically or dynamically with other modules is making a +combined work based on this library. Thus, the terms and conditions of the GNU +General Public License cover the whole combination. + +As a special exception, the copyright holders of this library give you +permission to link this library with independent modules to produce an +executable, regardless of the license terms of these independent modules, and +to copy and distribute the resulting executable under terms of your choice, +provided that you also meet, for each linked independent module, the terms and +conditions of the license of that module. An independent module is a module +which is not derived from or based on this library. If you modify this library, +you may extend this exception to your version of the library, but you are not +obligated to do so. If you do not wish to do so, delete this exception +statement from your version. +*/ + +package net.adoptopenjdk.icedteaweb.client.parts.dialogs.security; + +import net.adoptopenjdk.icedteaweb.ui.swing.dialogresults.DialogResult; +import net.sourceforge.jnlp.security.SecurityUtil; + +import javax.swing.Box; +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.ListSelectionModel; +import java.awt.BorderLayout; +import java.awt.Cursor; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import static net.adoptopenjdk.icedteaweb.Assert.requireNonNull; +import static net.adoptopenjdk.icedteaweb.i18n.Translator.R; + +/** + * Modal non-minimizable dialog to request client cert selection + */ +public class ClientCertSelectionPane extends SecurityDialogPanel { + + public ClientCertSelectionPane(SecurityDialog parent, Object[] extras) { + super(requireNonNull(parent, "parent")); + setLayout(new GridBagLayout()); + final JLabel jlInfo = new JLabel("" + R("CVCertificateViewer") + ""); + + final List certificateOptions = getAliases(extras); + final List displayedNames = new ArrayList<>(); + for (final CertificateOption entry : certificateOptions) { + final String subject = SecurityUtil.getCN(entry.certificate.getSubjectX500Principal().getName()); + final String issuer = SecurityUtil.getCN(entry.certificate.getIssuerX500Principal().getName()); + final int pos = entry.alias.lastIndexOf(" (from "); + final String source = (pos != -1) ? entry.alias.substring(pos) : ""; + displayedNames.add(subject + ":" + issuer + source); + } + + final JList jList = new JList<>(displayedNames.toArray(new String[0])); + jList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + jList.setSelectedIndex(0); + final JScrollPane scrollPane = new JScrollPane(jList); + + final JButton jbOK = new JButton(R("ButOk")); + final JButton jbCancel = new JButton(R("ButCancel")); + jbOK.setPreferredSize(jbCancel.getPreferredSize()); + final JButton jbDetails = new JButton(R("ButShowDetails")); + jbDetails.setBorderPainted(false); + jbDetails.setContentAreaFilled(false); + jbDetails.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + jbDetails.setMargin(new Insets(0, 0, 0, 0)); + + final GridBagConstraints jlInfoConstraints = new GridBagConstraints(); + jlInfoConstraints.anchor = GridBagConstraints.NORTHWEST; + jlInfoConstraints.gridx = 0; + jlInfoConstraints.gridy = 0; + jlInfoConstraints.weightx = 1.0; + jlInfoConstraints.insets = new Insets(10, 5, 3, 3); + add(jlInfo, jlInfoConstraints); + + final GridBagConstraints scrollPaneConstraints = new GridBagConstraints(); + scrollPaneConstraints.fill = GridBagConstraints.BOTH; + scrollPaneConstraints.gridx = 0; + scrollPaneConstraints.gridy = 1; + scrollPaneConstraints.weightx = 1.0; + scrollPaneConstraints.weighty = 1.0; + scrollPaneConstraints.insets = new Insets(10, 5, 3, 3); + add(scrollPane, scrollPaneConstraints); + + final GridBagConstraints jbDetailsConstraints = new GridBagConstraints(); + jbDetailsConstraints.anchor = GridBagConstraints.SOUTHEAST; + jbDetailsConstraints.gridx = 0; + jbDetailsConstraints.gridy = 2; + jbDetailsConstraints.weightx = 1.0; + jbDetailsConstraints.insets = new Insets(5, 5, 3, 3); + add(jbDetails, jbDetailsConstraints); + + final GridBagConstraints okCancelPaneConstraints = new GridBagConstraints(); + okCancelPaneConstraints.anchor = GridBagConstraints.SOUTHEAST; + okCancelPaneConstraints.gridx = 0; + okCancelPaneConstraints.gridy = 3; + okCancelPaneConstraints.weightx = 1.0; + okCancelPaneConstraints.insets = new Insets(3, 3, 10, 10); + + final JPanel okCancelPane = new JPanel(new FlowLayout(FlowLayout.TRAILING, 0, 0)); + okCancelPane.add(jbOK); + okCancelPane.add(Box.createHorizontalStrut(10)); + okCancelPane.add(jbCancel); + add(okCancelPane, okCancelPaneConstraints); + + parent.getViwableDialog().setMinimumSize(new Dimension(500, 300)); + parent.getViwableDialog().setLocationRelativeTo(null); + parent.getViwableDialog().pack(); + + initialFocusComponent = scrollPane; + + // click on OK + jbOK.addActionListener(new ActionListener() { + @Override + public void actionPerformed(final ActionEvent e) { + ClientCertSelectionPane.this.certSelected(parent, jList); + } + }); + // double-click on selection + jList.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent me) { + if (me.getClickCount() == 2) { + certSelected(parent, jList); + } + } + }); + // enter on selection + jList.addKeyListener(new KeyAdapter() { + @Override + public void keyReleased(KeyEvent ke) { + if (ke.getKeyCode() == KeyEvent.VK_ENTER) { + certSelected(parent, jList); + } + } + }); + // open certificate details + jbDetails.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + SecurityDialog.showSingleCertInfoDialog(certificateOptions.get(jList.getSelectedIndex()).certificate, + ClientCertSelectionPane.this); + } + }); + // click on Cancel + jbCancel.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + parent.setValue(null); + parent.getViwableDialog().dispose(); + } + }); + } + + @SuppressWarnings("unchecked") + private List getAliases(final Object[] extras) { + final Object firstExtra = extras[0]; + if (firstExtra instanceof List) { + return (List) firstExtra; + } + return new ArrayList<>(); + } + + @Override + public DialogResult getDefaultNegativeAnswer() { + return null; + } + + @Override + public DialogResult getDefaultPositiveAnswer() { + return null; + } + + @Override + public DialogResult readFromStdIn(String what) { + return null; + } + + @Override + public String helpToStdIn() { + return ""; + } + + private void certSelected(SecurityDialog parent, JList jlist) { + parent.setValue(new DialogResult() { + @Override + public int getButtonIndex() { + return jlist.getSelectedIndex(); + } + + @Override + public boolean toBoolean() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public String writeValue() { + throw new UnsupportedOperationException("Not supported yet."); + } + }); + parent.getViwableDialog().dispose(); + } + + public static void main(String[] args) throws Exception { + final KeyStore ks = KeyStore.getInstance("Windows-MY"); + ks.load(null, null); + final Enumeration aliases = ks.aliases(); + final List certificateOptions = new ArrayList<>(); + while (aliases.hasMoreElements()) { + final String alias = aliases.nextElement(); + final Certificate c = ks.getCertificate(alias); + if (c instanceof X509Certificate) { + certificateOptions.add(new CertificateOption(alias + " (from browser keystore)", (X509Certificate) c)); + } + } + final JFrame f = new JFrame(); + f.setMinimumSize(new Dimension(500, 300)); + f.setSize(700, 300); + f.add(new ClientCertSelectionPane(null, new Object[]{certificateOptions}), BorderLayout.CENTER); + f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + f.pack(); + f.setVisible(true); + } + + public static class CertificateOption { + public final String alias; + public final X509Certificate certificate; + + public CertificateOption(final String alias, final X509Certificate certificate) { + this.alias = alias; + this.certificate = certificate; + } + } +} diff --git a/core/src/main/java/net/adoptopenjdk/icedteaweb/client/parts/dialogs/security/SecurityDialog.java b/core/src/main/java/net/adoptopenjdk/icedteaweb/client/parts/dialogs/security/SecurityDialog.java index 1955314b7..ca3f9e762 100644 --- a/core/src/main/java/net/adoptopenjdk/icedteaweb/client/parts/dialogs/security/SecurityDialog.java +++ b/core/src/main/java/net/adoptopenjdk/icedteaweb/client/parts/dialogs/security/SecurityDialog.java @@ -48,7 +48,6 @@ import java.awt.BorderLayout; import java.awt.Component; import java.awt.Dialog.ModalityType; -import java.awt.Window; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.net.URL; @@ -64,7 +63,7 @@ */ public class SecurityDialog { - private final static Logger LOG = LoggerFactory.getLogger(SecurityDialog.class); + private static final Logger LOG = LoggerFactory.getLogger(SecurityDialog.class); /** The type of dialog we want to show */ private final SecurityDialogs.DialogType dialogType; @@ -87,26 +86,27 @@ public class SecurityDialog { */ private final Object[] extras; - /** Whether or not this object has been fully initialized */ - private boolean initialized = false; + private final ViewableDialog viewableDialog; private DialogResult value; - - private ViwableDialog viwableDialog; /** Should show signed JNLP file warning */ private boolean requiresSignedJNLPWarning; SecurityDialog(SecurityDialogs.DialogType dialogType, AccessType accessType, JNLPFile file, CertVerifier JarCertVerifier, X509Certificate cert, Object[] extras) { - this.viwableDialog = new ViwableDialog(); + this(dialogType, accessType, file, JarCertVerifier, cert, extras, false); + } + + SecurityDialog(SecurityDialogs.DialogType dialogType, AccessType accessType, + JNLPFile file, CertVerifier JarCertVerifier, X509Certificate cert, Object[] extras, boolean showInTaskBar) { + this.viewableDialog = new ViewableDialog(showInTaskBar); this.dialogType = dialogType; this.accessType = accessType; this.file = file; this.certVerifier = JarCertVerifier; this.cert = cert; this.extras = extras; - initialized = true; if(file != null) requiresSignedJNLPWarning= file.requiresSignedJNLPWarning(); @@ -114,14 +114,6 @@ public class SecurityDialog { initDialog(); } - /** - * Construct a SecurityDialog to display some sort of access warning - */ - private SecurityDialog(SecurityDialogs.DialogType dialogType, AccessType accessType, - JNLPFile file) { - this(dialogType, accessType, file, null, null, null); - } - /** * Create a SecurityDialog to display a certificate-related warning */ @@ -130,23 +122,6 @@ private SecurityDialog(SecurityDialogs.DialogType dialogType, AccessType accessT this(dialogType, accessType, file, certVerifier, null, null); } - /** - * Create a SecurityDialog to display a certificate-related warning - */ - private SecurityDialog(SecurityDialogs.DialogType dialogType, AccessType accessType, - CertVerifier certVerifier) { - this(dialogType, accessType, null, certVerifier, null, null); - } - - /** - * Create a SecurityDialog to display some sort of access warning - * with more information - */ - private SecurityDialog(SecurityDialogs.DialogType dialogType, AccessType accessType, - JNLPFile file, Object[] extras) { - this(dialogType, accessType, file, null, null, extras); - } - /** * Create a SecurityWarningDialog to display information about a single * certificate @@ -155,14 +130,6 @@ private SecurityDialog(SecurityDialogs.DialogType dialogType, X509Certificate c) this(dialogType, null, null, null, c, null); } - /** - * Returns if this dialog has been fully initialized yet. - * @return true if this dialog has been initialized, and false otherwise. - */ - public boolean isInitialized() { - return initialized; - } - /** * Shows more information regarding jar code signing * @@ -204,7 +171,7 @@ public static void showCertInfoDialog(CertVerifier certVerifier, * @param parent the parent pane. */ public static void showSingleCertInfoDialog(X509Certificate c, - Window parent) { + Component parent) { SecurityDialog dialog = new SecurityDialog(SecurityDialogs.DialogType.SINGLE_CERT_INFO, c); dialog.getViwableDialog().setLocationRelativeTo(parent); dialog.getViwableDialog().setModalityType(ModalityType.APPLICATION_MODAL); @@ -215,20 +182,15 @@ public static void showSingleCertInfoDialog(X509Certificate c, private void initDialog() { String dialogTitle = createTitle(); - // Note: ViwableDialog methods are deferred until show(): + // Note: ViewableDialog methods are deferred until show(): getViwableDialog().setTitle(dialogTitle); getViwableDialog().setModalityType(ModalityType.MODELESS); getViwableDialog().setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); - // Initialize panel now as its constructor may call getViwableDialog() deferred methods - // to modify dialog state: - SwingUtils.invokeAndWait(new Runnable() { - @Override - public void run() { - installPanel(); - } - }); + // Initialize panel now as its constructor may call getViewableDialog() deferred methods + // to modify dialog state: + SwingUtils.invokeAndWait(this::installPanel); getViwableDialog().pack(); getViwableDialog().centerDialog(); @@ -281,7 +243,7 @@ else if (dtype == SecurityDialogs.DialogType.APPLET_WARNING) dialogTitle = "Applet Warning"; else if (dtype == SecurityDialogs.DialogType.PARTIALLY_SIGNED_WARNING) dialogTitle = "Security Warning"; - else if (dtype == SecurityDialogs.DialogType.AUTHENTICATION) + else if (dtype == SecurityDialogs.DialogType.AUTHENTICATION || dtype == SecurityDialogs.DialogType.CLIENT_CERT_SELECTION) dialogTitle = "Authentication Required"; return dialogTitle; } @@ -308,46 +270,47 @@ public X509Certificate getCert() { private SecurityDialogPanel getPanel() { return getPanel(this); } - + /* * find appropriate JPanel to given Dialog, based on {@link DialogType}. */ static SecurityDialogPanel getPanel(SecurityDialog sd) { return getPanel(sd.dialogType, sd); } - + static SecurityDialogPanel getPanel(SecurityDialogs.DialogType type, SecurityDialog sd) { - SecurityDialogPanel lpanel = null; - if (type == SecurityDialogs.DialogType.CERT_WARNING) { - lpanel = new CertWarningPane(sd, sd.certVerifier, (SecurityDelegate) sd.extras[0]); - } else if (type == SecurityDialogs.DialogType.MORE_INFO) { - lpanel = new MoreInfoPane(sd, sd.certVerifier); - } else if (type == SecurityDialogs.DialogType.CERT_INFO) { - lpanel = new CertsInfoPane(sd, sd.certVerifier); - } else if (type == SecurityDialogs.DialogType.SINGLE_CERT_INFO) { - lpanel = new SingleCertInfoPane(sd, sd.certVerifier); - } else if (type == SecurityDialogs.DialogType.ACCESS_WARNING) { - lpanel = new AccessWarningPane(sd, sd.extras, sd.certVerifier); - } else if (type == SecurityDialogs.DialogType.APPLET_WARNING) { - lpanel = new AppletWarningPane(sd, sd.certVerifier); - } else if (type == SecurityDialogs.DialogType.PARTIALLY_SIGNED_WARNING) { - lpanel = AppTrustWarningDialog.partiallySigned(sd, sd.file, (SecurityDelegate) sd.extras[0]); - } else if (type == SecurityDialogs.DialogType.UNSIGNED_WARNING) { - lpanel = AppTrustWarningDialog.unsigned(sd, sd.file); // Only necessary for applets on 'high security' or above - } else if (type == SecurityDialogs.DialogType.AUTHENTICATION) { - lpanel = new PasswordAuthenticationPane(sd, sd.extras); - } else if (type == SecurityDialogs.DialogType.UNSIGNED_EAS_NO_PERMISSIONS_WARNING) { - lpanel = new MissingPermissionsAttributePanel(sd, sd.file.getTitle(), sd.file.getNotNullProbableCodeBase().toExternalForm()); - } else if (type == SecurityDialogs.DialogType.MISSING_ALACA) { - lpanel = new MissingALACAttributePanel(sd, sd.file.getTitle(), (String) sd.extras[0], (String) sd.extras[1]); - } else if (type == SecurityDialogs.DialogType.MATCHING_ALACA) { - lpanel = AppTrustWarningDialog.matchingAlaca(sd, sd.file, (String) sd.extras[0], (String) sd.extras[1]); - } else if (type == SecurityDialogs.DialogType.SECURITY_511) { - lpanel = new InetSecurity511Panel(sd, (URL) sd.extras[0]); - } else { - throw new RuntimeException("Unknown value of " + sd.dialogType + ". Panel will be null. That's not allowed."); + switch (type) { + case CERT_WARNING: + return new CertWarningPane(sd, sd.certVerifier, (SecurityDelegate) sd.extras[0]); + case MORE_INFO: + return new MoreInfoPane(sd, sd.certVerifier); + case CERT_INFO: + return new CertsInfoPane(sd, sd.certVerifier); + case SINGLE_CERT_INFO: + return new SingleCertInfoPane(sd, sd.certVerifier); + case ACCESS_WARNING: + return new AccessWarningPane(sd, sd.extras, sd.certVerifier); + case APPLET_WARNING: + return new AppletWarningPane(sd, sd.certVerifier); + case PARTIALLY_SIGNED_WARNING: + return AppTrustWarningDialog.partiallySigned(sd, sd.file, (SecurityDelegate) sd.extras[0]); + case UNSIGNED_WARNING: + return AppTrustWarningDialog.unsigned(sd, sd.file); + case AUTHENTICATION: + return new PasswordAuthenticationPane(sd, sd.extras); + case CLIENT_CERT_SELECTION: + return new ClientCertSelectionPane(sd, sd.extras); + case UNSIGNED_EAS_NO_PERMISSIONS_WARNING: + return new MissingPermissionsAttributePanel(sd, sd.file.getTitle(), sd.file.getNotNullProbableCodeBase().toExternalForm()); + case MISSING_ALACA: + return new MissingALACAttributePanel(sd, sd.file.getTitle(), (String) sd.extras[0], (String) sd.extras[1]); + case MATCHING_ALACA: + return AppTrustWarningDialog.matchingAlaca(sd, sd.file, (String) sd.extras[0], (String) sd.extras[1]); + case SECURITY_511: + return new InetSecurity511Panel(sd, (URL) sd.extras[0]); + default: + throw new RuntimeException("Unknown value of " + sd.dialogType + ". Panel will be null. That's not allowed."); } - return lpanel; } /* @@ -376,7 +339,7 @@ public DialogResult getValue() { return value; } - + public boolean requiresSignedJNLPWarning() { return requiresSignedJNLPWarning; @@ -402,10 +365,10 @@ String helpToStdIn(){ return panel.helpToStdIn(); } - public ViwableDialog getViwableDialog() { - return viwableDialog; + public ViewableDialog getViwableDialog() { + return viewableDialog; } - + public SecurityDialogPanel getSecurityDialogPanel(){ return panel; } diff --git a/core/src/main/java/net/adoptopenjdk/icedteaweb/client/parts/dialogs/security/SecurityDialogMessage.java b/core/src/main/java/net/adoptopenjdk/icedteaweb/client/parts/dialogs/security/SecurityDialogMessage.java index 4b2bfa939..8f2a38a1c 100644 --- a/core/src/main/java/net/adoptopenjdk/icedteaweb/client/parts/dialogs/security/SecurityDialogMessage.java +++ b/core/src/main/java/net/adoptopenjdk/icedteaweb/client/parts/dialogs/security/SecurityDialogMessage.java @@ -81,5 +81,6 @@ public SecurityDialogMessage(JNLPFile file) { public Semaphore lock; //if dialog slip out of awt thread, fake modal dialog is created. This is keeping it. public JDialog toDispose; + public boolean showInTaskBar; } diff --git a/core/src/main/java/net/adoptopenjdk/icedteaweb/client/parts/dialogs/security/SecurityDialogMessageHandler.java b/core/src/main/java/net/adoptopenjdk/icedteaweb/client/parts/dialogs/security/SecurityDialogMessageHandler.java index 6be4f0c76..db8f93265 100644 --- a/core/src/main/java/net/adoptopenjdk/icedteaweb/client/parts/dialogs/security/SecurityDialogMessageHandler.java +++ b/core/src/main/java/net/adoptopenjdk/icedteaweb/client/parts/dialogs/security/SecurityDialogMessageHandler.java @@ -110,7 +110,7 @@ public void run() { protected void handleMessage(final SecurityDialogMessage message) { final SecurityDialog dialog = new SecurityDialog(message.dialogType, - message.accessType, message.file, message.certVerifier, message.certificate, message.extras); + message.accessType, message.file, message.certVerifier, message.certificate, message.extras, message.showInTaskBar); if (processAutomatedAnswers(message, dialog)){ return; diff --git a/core/src/main/java/net/adoptopenjdk/icedteaweb/client/parts/dialogs/security/SecurityDialogs.java b/core/src/main/java/net/adoptopenjdk/icedteaweb/client/parts/dialogs/security/SecurityDialogs.java index 59ce9fdcc..c56965b45 100644 --- a/core/src/main/java/net/adoptopenjdk/icedteaweb/client/parts/dialogs/security/SecurityDialogs.java +++ b/core/src/main/java/net/adoptopenjdk/icedteaweb/client/parts/dialogs/security/SecurityDialogs.java @@ -44,8 +44,8 @@ import net.adoptopenjdk.icedteaweb.ui.swing.dialogresults.YesNoSandbox; import net.adoptopenjdk.icedteaweb.ui.swing.dialogresults.YesNoSandboxLimited; import net.sourceforge.jnlp.JNLPFile; -import net.sourceforge.jnlp.runtime.classloader.SecurityDelegate; import net.sourceforge.jnlp.runtime.JNLPRuntime; +import net.sourceforge.jnlp.runtime.classloader.SecurityDelegate; import net.sourceforge.jnlp.security.AccessType; import net.sourceforge.jnlp.security.CertVerifier; import net.sourceforge.jnlp.util.UrlUtils; @@ -58,6 +58,8 @@ import java.net.URL; import java.security.AccessController; import java.security.PrivilegedAction; +import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.concurrent.Semaphore; @@ -76,12 +78,12 @@ */ public class SecurityDialogs { - private final static Logger LOG = LoggerFactory.getLogger(SecurityDialogs.class); + private static final Logger LOG = LoggerFactory.getLogger(SecurityDialogs.class); /** * Types of dialogs we can create */ - public static enum DialogType { + public enum DialogType { CERT_WARNING, MORE_INFO, @@ -92,6 +94,7 @@ public static enum DialogType { UNSIGNED_WARNING, /* requires confirmation with 'high-security' setting */ APPLET_WARNING, AUTHENTICATION, + CLIENT_CERT_SELECTION, UNSIGNED_EAS_NO_PERMISSIONS_WARNING, /* when Extended applet security is at High Security and no permission attribute is find, */ MISSING_ALACA, /*alaca - Application-Library-Allowable-Codebase Attribute*/ MATCHING_ALACA, @@ -107,9 +110,10 @@ public static enum DialogType { * @param extras array of objects used as extra.toString or similarly later * @return true if permission was granted by the user, false otherwise. */ - public static AccessWarningPaneComplexReturn showAccessWarningDialog(final AccessType accessType, - final JNLPFile file, final Object[] extras) { - + public static AccessWarningPaneComplexReturn showAccessWarningDialog( + final AccessType accessType, + final JNLPFile file, final Object[] extras + ) { final SecurityDialogMessage message = new SecurityDialogMessage(file); message.dialogType = DialogType.ACCESS_WARNING; @@ -133,7 +137,7 @@ public static YesNoSandboxLimited showUnsignedWarningDialog(JNLPFile file) { message.dialogType = DialogType.UNSIGNED_WARNING; message.accessType = AccessType.UNSIGNED; - DialogResult r = getUserResponse(message); + final DialogResult r = getUserResponse(message); return (YesNoSandboxLimited) r; } @@ -153,16 +157,18 @@ public static YesNoSandboxLimited showUnsignedWarningDialog(JNLPFile file) { * wants the applet to run with only sandbox permissions, or CANCEL if the * user did not accept running the applet */ - public static YesNoSandbox showCertWarningDialog(AccessType accessType, - JNLPFile file, CertVerifier certVerifier, SecurityDelegate securityDelegate) { - + public static YesNoSandbox showCertWarningDialog( + AccessType accessType, + JNLPFile file, + CertVerifier certVerifier, SecurityDelegate securityDelegate + ) { final SecurityDialogMessage message = new SecurityDialogMessage(file); message.dialogType = DialogType.CERT_WARNING; message.accessType = accessType; message.certVerifier = certVerifier; message.extras = new Object[]{securityDelegate}; - DialogResult selectedValue = getUserResponse(message); + final DialogResult selectedValue = getUserResponse(message); return (YesNoSandbox) selectedValue; } @@ -176,16 +182,18 @@ public static YesNoSandbox showCertWarningDialog(AccessType accessType, * @param securityDelegate the delegate for security atts. * @return true if permission was granted by the user, false otherwise. */ - public static YesNoSandbox showPartiallySignedWarningDialog(JNLPFile file, CertVerifier certVerifier, - SecurityDelegate securityDelegate) { - + public static YesNoSandbox showPartiallySignedWarningDialog( + JNLPFile file, + CertVerifier certVerifier, + SecurityDelegate securityDelegate + ) { final SecurityDialogMessage message = new SecurityDialogMessage(file); message.dialogType = DialogType.PARTIALLY_SIGNED_WARNING; message.accessType = AccessType.PARTIALLY_SIGNED; message.certVerifier = certVerifier; message.extras = new Object[]{securityDelegate}; - DialogResult r = getUserResponse(message); + final DialogResult r = getUserResponse(message); return (YesNoSandbox) r; } @@ -203,12 +211,10 @@ public static YesNoSandbox showPartiallySignedWarningDialog(JNLPFile file, CertV * permissions. */ public static NamePassword showAuthenticationPrompt(String host, int port, String prompt, String type) { - LOG.debug("Showing dialog for basic auth for {}:{} with name {} of type {}", host, port, prompt, type); - SecurityManager sm = System.getSecurityManager(); + final SecurityManager sm = System.getSecurityManager(); if (sm != null) { - NetPermission requestPermission - = new NetPermission("requestPasswordAuthentication"); + final NetPermission requestPermission = new NetPermission("requestPasswordAuthentication"); sm.checkPermission(requestPermission); } @@ -217,22 +223,36 @@ public static NamePassword showAuthenticationPrompt(String host, int port, Strin message.dialogType = DialogType.AUTHENTICATION; message.extras = new Object[]{host, port, prompt, type}; - DialogResult response = getUserResponse(message); + final DialogResult response = getUserResponse(message); + LOG.debug("Decided action for matching alaca at was {}", response); return (NamePassword) response; } + public static DialogResult showClientCertSelectionPrompt(List certificateOptions) { + + final SecurityDialogMessage message = new SecurityDialogMessage(null); + message.dialogType = DialogType.CLIENT_CERT_SELECTION; + message.extras = new Object[] {certificateOptions}; + message.showInTaskBar = true; + + final DialogResult response = getUserResponse(message); + LOG.debug("Decided action for selecting client certificate was {}", response); + return response; + } + public static boolean showMissingALACAttributePanel(JNLPFile file, URL codeBase, Set remoteUrls) { - SecurityDialogMessage message = new SecurityDialogMessage(file); + final SecurityDialogMessage message = new SecurityDialogMessage(file); message.dialogType = DialogType.MISSING_ALACA; - String urlToShow = file.getNotNullProbableCodeBase().toExternalForm(); + final String urlToShow; if (codeBase != null) { urlToShow = codeBase.toString(); } else { + urlToShow = file.getNotNullProbableCodeBase().toExternalForm(); LOG.warn("Warning, null codebase wants to show in ALACA!"); } message.extras = new Object[]{urlToShow, UrlUtils.setOfUrlsToHtmlList(remoteUrls)}; - DialogResult selectedValue = getUserResponse(message); + final DialogResult selectedValue = getUserResponse(message); LOG.debug("Decided action for matching alaca at {} was {}", file.getCodeBase(), selectedValue); @@ -244,14 +264,16 @@ public static boolean showMissingALACAttributePanel(JNLPFile file, URL codeBase, public static boolean showMatchingALACAttributePanel(JNLPFile file, URL documentBase, Set remoteUrls) { - SecurityDialogMessage message = new SecurityDialogMessage(file); + final SecurityDialogMessage message = new SecurityDialogMessage(file); message.dialogType = DialogType.MATCHING_ALACA; - String docBaseString = "null-documentbase"; + final String docBaseString; if (documentBase != null) { docBaseString = documentBase.toString(); + } else { + docBaseString = "null-documentbase"; } message.extras = new Object[]{docBaseString, UrlUtils.setOfUrlsToHtmlList(remoteUrls)}; - DialogResult selectedValue = getUserResponse(message); + final DialogResult selectedValue = getUserResponse(message); LOG.debug("Decided action for matching alaca at {} was {}", file.getCodeBase(), selectedValue); @@ -260,14 +282,13 @@ public static boolean showMatchingALACAttributePanel(JNLPFile file, URL document } return false; - } public static boolean showMissingPermissionsAttributeDialogue(JNLPFile file) { - SecurityDialogMessage message = new SecurityDialogMessage(file); + final SecurityDialogMessage message = new SecurityDialogMessage(file); message.dialogType = DialogType.UNSIGNED_EAS_NO_PERMISSIONS_WARNING; - DialogResult selectedValue = getUserResponse(message); + final DialogResult selectedValue = getUserResponse(message); LOG.debug("Decided action for missing permissions at {} was {}", file.getCodeBase(), selectedValue); if (selectedValue != null) { @@ -322,12 +343,9 @@ private static DialogResult getUserResponse(final SecurityDialogMessage message) public void windowOpened(WindowEvent e) { message.toDispose = fakeDialog; message.lock = null; - AccessController.doPrivileged(new PrivilegedAction() { - @Override - public Void run() { - JNLPRuntime.getSecurityDialogHandler().postMessage(message); - return null; - } + AccessController.doPrivileged((PrivilegedAction) () -> { + JNLPRuntime.getSecurityDialogHandler().postMessage(message); + return null; }); } }); @@ -361,14 +379,11 @@ public Void run() { // false = terminate ITW // true = continue public static boolean show511Dialogue(Resource r) { - SecurityDialogMessage message = new SecurityDialogMessage(null); + final SecurityDialogMessage message = new SecurityDialogMessage(null); message.dialogType = DialogType.SECURITY_511; message.extras = new Object[]{r.getLocation()}; - DialogResult selectedValue = getUserResponse(message); - if (selectedValue != null && selectedValue.equals(YesCancel.cancel())) { - return false; //kill command - } - return true; + final DialogResult selectedValue = getUserResponse(message); + return !Objects.equals(selectedValue, YesCancel.cancel()); } } diff --git a/core/src/main/java/net/adoptopenjdk/icedteaweb/client/parts/dialogs/security/ViwableDialog.java b/core/src/main/java/net/adoptopenjdk/icedteaweb/client/parts/dialogs/security/ViewableDialog.java similarity index 67% rename from core/src/main/java/net/adoptopenjdk/icedteaweb/client/parts/dialogs/security/ViwableDialog.java rename to core/src/main/java/net/adoptopenjdk/icedteaweb/client/parts/dialogs/security/ViewableDialog.java index 8ff3bf791..d35d13bd5 100644 --- a/core/src/main/java/net/adoptopenjdk/icedteaweb/client/parts/dialogs/security/ViwableDialog.java +++ b/core/src/main/java/net/adoptopenjdk/icedteaweb/client/parts/dialogs/security/ViewableDialog.java @@ -48,70 +48,54 @@ import java.util.concurrent.CopyOnWriteArrayList; /** - * This class encapsulate viwable part of SecurityDialog, so it do not need to + * This class encapsulate viewable part of SecurityDialog, so it do not need to * extend it. - * + *

* It is accepting commons setters for jdialog, but actually applying them right before it is created. * Obviously it do not have getters, but jdialog itself should not be keeper of any information. SecurityPanel is. */ -public class ViwableDialog { +public class ViewableDialog { + + private final List operations = new ArrayList<>(); + private final boolean showInTaskBar; private JDialog jd = null; - List operations = new ArrayList(); - public ViwableDialog() { + public ViewableDialog(boolean showInTaskBar) { + this.showInTaskBar = showInTaskBar; } private JDialog createJDialog() { - jd = new JDialog(); + jd = showInTaskBar ? new JDialog((Dialog) null) : new JDialog(); jd.setName("ViwableDialog"); SwingUtils.info(jd); jd.setIconImages(ImageResources.INSTANCE.getApplicationImages()); - - for (Runnable operation : operations) { + + for (final Runnable operation : operations) { operation.run(); } - // prune operations. May throw NPE if operations used after createJDialog() - operations = null; + operations.clear(); return jd; } public void setMinimumSize(final Dimension minimumSize) { - operations.add(new Runnable() { - @Override - public void run() { - jd.setMinimumSize(minimumSize); - } - }); + operations.add(() -> jd.setMinimumSize(minimumSize)); } public void pack() { - operations.add(new Runnable() { - @Override - public void run() { - jd.pack(); - } - }); + operations.add(() -> jd.pack()); } public void setLocationRelativeTo(final Component c) { - operations.add(new Runnable() { - @Override - public void run() { - jd.setLocationRelativeTo(c); - } - }); + operations.add(() -> jd.setLocationRelativeTo(c)); } public void show() { - SwingUtils.invokeAndWait(new Runnable() { - @Override - public void run() { - if (jd == null) { - jd = createJDialog(); - } - jd.setVisible(true); + SwingUtils.invokeAndWait(() -> { + if (jd == null) { + jd = createJDialog(); } + jd.setVisible(true); }); } @@ -120,7 +104,7 @@ public void run() { * choice (Ok, Cancel, etc) or closed the window */ public void dispose() { - // avoid reentrance: + // avoid re-entrance: if (jd != null) { notifySelectionMade(); @@ -154,39 +138,19 @@ public void addActionListener(ActionListener listener) { } public void add(final SecurityDialogPanel panel, final String constraints) { - operations.add(new Runnable() { - @Override - public void run() { - jd.add(panel, constraints); - } - }); + operations.add(() -> jd.add(panel, constraints)); } public void setModalityType(final Dialog.ModalityType modalityType) { - operations.add(new Runnable() { - @Override - public void run() { - jd.setModalityType(modalityType); - } - }); + operations.add(() -> jd.setModalityType(modalityType)); } public void setTitle(final String title) { - operations.add(new Runnable() { - @Override - public void run() { - jd.setTitle(title); - } - }); + operations.add(() -> jd.setTitle(title)); } public void setDefaultCloseOperation(final int op) { - operations.add(new Runnable() { - @Override - public void run() { - jd.setDefaultCloseOperation(op); - } - }); + operations.add(() -> jd.setDefaultCloseOperation(op)); } private static void centerDialog(JDialog dialog) { @@ -194,12 +158,7 @@ private static void centerDialog(JDialog dialog) { } public void centerDialog() { - operations.add(new Runnable() { - @Override - public void run() { - centerDialog(jd); - } - }); + operations.add(() -> centerDialog(jd)); } public void setResizable(final boolean b) { @@ -210,21 +169,10 @@ public void setResizable(final boolean b) { } public void addWindowListener(final WindowAdapter adapter) { - operations.add(new Runnable() { - @Override - public void run() { - jd.addWindowListener(adapter); - } - }); + operations.add(() -> jd.addWindowListener(adapter)); } public void addWindowFocusListener(final WindowAdapter adapter) { - operations.add(new Runnable() { - @Override - public void run() { - jd.addWindowFocusListener(adapter); - } - }); + operations.add(() -> jd.addWindowFocusListener(adapter)); } - } diff --git a/core/src/main/java/net/adoptopenjdk/icedteaweb/resources/ResourceTracker.java b/core/src/main/java/net/adoptopenjdk/icedteaweb/resources/ResourceTracker.java index b879331cf..d2b06e776 100644 --- a/core/src/main/java/net/adoptopenjdk/icedteaweb/resources/ResourceTracker.java +++ b/core/src/main/java/net/adoptopenjdk/icedteaweb/resources/ResourceTracker.java @@ -21,6 +21,7 @@ import net.adoptopenjdk.icedteaweb.logging.Logger; import net.adoptopenjdk.icedteaweb.logging.LoggerFactory; import net.adoptopenjdk.icedteaweb.resources.CachedDaemonThreadPoolProvider.DaemonThreadFactory; +import net.adoptopenjdk.icedteaweb.resources.Resource.Status; import net.sourceforge.jnlp.DownloadOptions; import net.sourceforge.jnlp.cache.CacheUtil; import net.sourceforge.jnlp.config.ConfigurationConstants; @@ -383,7 +384,7 @@ private void waitForCompletion(Resource... resources) { final int threadCount = Math.min(configuredThreadCount, resources.length); final ExecutorService downloadExecutor = Executors.newFixedThreadPool(threadCount, new DaemonThreadFactory()); try { - final List> futures = Arrays.asList(resources).stream() + final List> futures = Arrays.stream(resources) .map(r -> triggerDownloadFor(r, downloadExecutor)) .collect(Collectors.toList()); @@ -396,6 +397,10 @@ private void waitForCompletion(Resource... resources) { LOG.debug("Download done. Shutting down executor"); downloadExecutor.shutdownNow(); } + + if (resources.length == 1 && resources[0].isSet(Status.ERROR)) { + throw new RuntimeException("Error while downloading initial resource " + resources[0]); + } } private Future triggerDownloadFor(Resource resource, final Executor downloadExecutor) { diff --git a/core/src/main/java/net/sourceforge/jnlp/cache/CacheUtil.java b/core/src/main/java/net/sourceforge/jnlp/cache/CacheUtil.java index a5e87f669..4698734d8 100644 --- a/core/src/main/java/net/sourceforge/jnlp/cache/CacheUtil.java +++ b/core/src/main/java/net/sourceforge/jnlp/cache/CacheUtil.java @@ -234,20 +234,29 @@ public static String hex(String origName, String candidate) throws NoSuchAlgorit * @param title name of the download */ public static void waitForResources(final JNLPClassLoader jnlpClassLoader, final ResourceTracker tracker, final URL[] resources, final String title) { + // download first initial jar : so in case of client certificate, only 1 https client handshake is done + boolean downloadInitialJarFirst = resources.length > 1 && resources[0].getProtocol().equals("https"); try { final DownloadIndicator indicator = Optional.ofNullable(JNLPRuntime.getDefaultDownloadIndicator()) - .orElseGet(() -> new DummyDownloadIndicator()); + .orElseGet(DummyDownloadIndicator::new); final DownloadServiceListener listener = getDownloadServiceListener(jnlpClassLoader, title, resources, indicator); try { for (URL url : resources) { tracker.addDownloadListener(url, resources, listener); } + if (downloadInitialJarFirst) { + tracker.waitForResources(resources[0]); + } + // download all remaining ones tracker.waitForResources(resources); } finally { indicator.disposeListener(listener); } } catch (Exception ex) { LOG.error("Downloading of resources ended with error", ex); + if (downloadInitialJarFirst) { + throw new RuntimeException(ex); + } } } diff --git a/core/src/main/java/net/sourceforge/jnlp/runtime/JNLPRuntime.java b/core/src/main/java/net/sourceforge/jnlp/runtime/JNLPRuntime.java index 7bffd2c48..435e95cba 100644 --- a/core/src/main/java/net/sourceforge/jnlp/runtime/JNLPRuntime.java +++ b/core/src/main/java/net/sourceforge/jnlp/runtime/JNLPRuntime.java @@ -35,8 +35,6 @@ import net.sourceforge.jnlp.config.DeploymentConfiguration; import net.sourceforge.jnlp.config.PathsAndFiles; import net.sourceforge.jnlp.security.JNLPAuthenticator; -import net.sourceforge.jnlp.security.KeyStores; -import net.sourceforge.jnlp.security.SecurityUtil; import net.sourceforge.jnlp.services.XServiceManagerStub; import net.sourceforge.jnlp.util.RestrictedFileUtils; import net.sourceforge.jnlp.util.logging.LogConfig; @@ -46,7 +44,7 @@ import javax.jnlp.ServiceManager; import javax.naming.ConfigurationException; import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.KeyManager; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; @@ -70,7 +68,6 @@ import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.security.AllPermission; -import java.security.KeyStore; import java.security.Policy; import java.security.Security; import java.text.DateFormat; @@ -272,13 +269,9 @@ public static void initialize() throws IllegalStateException { try { SSLSocketFactory sslSocketFactory; SSLContext context = SSLContext.getInstance("SSL"); - KeyStore ks = KeyStores.getKeyStore(KeyStores.Level.USER, KeyStores.Type.CLIENT_CERTS).getKs(); - KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); - SecurityUtil.initKeyManagerFactory(kmf, ks); TrustManager[] trust = new TrustManager[] { getSSLSocketTrustManager() }; - context.init(kmf.getKeyManagers(), trust, null); + context.init(new KeyManager[] { new MergedKeyManager() }, trust, null); sslSocketFactory = context.getSocketFactory(); - HttpsURLConnection.setDefaultSSLSocketFactory(sslSocketFactory); } catch (Exception e) { LOG.error("Unable to set SSLSocketfactory (may _prevent_ access to sites that should be trusted)! Continuing anyway...", e); diff --git a/core/src/main/java/net/sourceforge/jnlp/runtime/MergedKeyManager.java b/core/src/main/java/net/sourceforge/jnlp/runtime/MergedKeyManager.java new file mode 100644 index 000000000..a040d9446 --- /dev/null +++ b/core/src/main/java/net/sourceforge/jnlp/runtime/MergedKeyManager.java @@ -0,0 +1,319 @@ +package net.sourceforge.jnlp.runtime; + +import net.adoptopenjdk.icedteaweb.client.parts.dialogs.security.ClientCertSelectionPane.CertificateOption; +import net.adoptopenjdk.icedteaweb.client.parts.dialogs.security.SecurityDialogs; +import net.adoptopenjdk.icedteaweb.logging.Logger; +import net.adoptopenjdk.icedteaweb.logging.LoggerFactory; +import net.adoptopenjdk.icedteaweb.os.OsUtil; +import net.adoptopenjdk.icedteaweb.ui.swing.dialogresults.DialogResult; +import net.sourceforge.jnlp.security.SecurityUtil; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.X509ExtendedKeyManager; +import javax.net.ssl.X509KeyManager; +import java.lang.reflect.Method; +import java.net.Socket; +import java.security.KeyStore; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.cert.CertificateException; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Stream; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.stream.Collectors.toList; +import static net.sourceforge.jnlp.security.KeyStores.Level.SYSTEM; +import static net.sourceforge.jnlp.security.KeyStores.Level.USER; +import static net.sourceforge.jnlp.security.KeyStores.Type.CLIENT_CERTS; +import static net.sourceforge.jnlp.security.KeyStores.getKeyStore; + +public class MergedKeyManager extends X509ExtendedKeyManager { + private static final Logger LOG = LoggerFactory.getLogger(MergedKeyManager.class); + private static final Map hostToClientAliasCache = new ConcurrentHashMap<>(); + private final X509KeyManager browserKeyManager; + private final X509KeyManager userKeyManager; + private final X509KeyManager systemKeyManager; + private static final String USER_SUFFIX = " (from user keystore)"; + private static final String SYSTEM_SUFFIX = " (from system keystore)"; + private static final String BROWSER_SUFFIX = " (from browser keystore)"; + private static boolean userCancelled = false; + + MergedKeyManager() { + userKeyManager = getKeyManager(getKeyStore(USER, CLIENT_CERTS).getKs()); + systemKeyManager = getKeyManager(getKeyStore(SYSTEM, CLIENT_CERTS).getKs()); + browserKeyManager = getX509KeyManager(); + } + + private X509KeyManager getX509KeyManager() { + if (OsUtil.isWindows()) { + try { + final KeyStore ks = KeyStore.getInstance("Windows-MY"); + ks.load(null, null); + return getKeyManager(ks); + } catch (Exception e) { + LOG.error("Unable to get browser keystore information", e); + } + } + return null; + } + + private X509KeyManager getKeyManager(KeyStore ks) { + try { + final KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); + SecurityUtil.initKeyManagerFactory(kmf, ks); + final KeyManager[] keyManagers = kmf.getKeyManagers(); + if (keyManagers != null) { + for (KeyManager keyManager : keyManagers) { + if (keyManager instanceof X509KeyManager) { + return (X509KeyManager) keyManager; + } + } + } + } catch (Exception e) { + LOG.warn("Unable to get KeyStore " + ks, e); + } + return null; + } + + @Override + public String chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket) { + if (userCancelled) { + LOG.warn("Client certificate selection previously cancelled by user, returning null alias"); + return null; + } + + final String host = getHostFromSocket(socket); + LOG.info("Retrieved host from socket : " + host); + if (host != null) { + final String alias = hostToClientAliasCache.get(host.toLowerCase()); + LOG.info("Found " + alias + " alias in cache for " + host.toLowerCase() + " and keyTypes " + Arrays.toString(keyTypes)); + if (alias != null) { + return alias; + } + } + + final List validClientAliases = new ArrayList<>(); + final List expiredClientAliases = new ArrayList<>(); + final List otherAliases = new ArrayList<>(); + for (final String keyType : keyTypes) { + final String[] aliasesWithSuffix = getClientAliases(keyType, issuers); + //if (aliasesWithSuffix != null) + for (final String aliasWithSuffix : aliasesWithSuffix) { + X509Certificate[] certs = getCertificateChain(removeAliasSuffix(aliasWithSuffix)); + if (certs == null || certs.length == 0) { + continue; + } + final X509Certificate cert = certs[0]; + try { + final List usage = cert.getExtendedKeyUsage(); + // Extensions : 1.3.6.1.5.5.7.3.2=ClientCert 2.5.29.37.0=ANY + if (usage != null && (usage.contains("1.3.6.1.5.5.7.3.2") || usage.contains("2.5.29.37.0"))) { + try { + cert.checkValidity(); + LOG.info("Found valid client alias with clientCert or ANY extension: " + aliasWithSuffix); + validClientAliases.add(new CertificateOption(aliasWithSuffix, cert)); + } catch (CertificateException e) { + LOG.warn("Found expired client alias with clientCert or ANY extension: " + aliasWithSuffix); + expiredClientAliases.add(new CertificateOption(aliasWithSuffix, cert)); + } + } else { + LOG.warn("Found non-client alias: " + aliasWithSuffix); + otherAliases.add(new CertificateOption(aliasWithSuffix, cert)); + } + } catch (CertificateParsingException e) { + LOG.warn("Exception while getting ExtendedKeyUsage for alias " + aliasWithSuffix); + otherAliases.add(new CertificateOption(aliasWithSuffix, cert)); + } + } + } + + final String alias; + if (!validClientAliases.isEmpty()) { + alias = getPreferredAlias(validClientAliases, "valid client"); + } else { + expiredClientAliases.addAll(otherAliases); + if (!expiredClientAliases.isEmpty()) { + alias = getPreferredAlias(expiredClientAliases, "remaining"); + } else { + LOG.warn("Could not find any client alias for keyTypes " + Arrays.toString(keyTypes)); + alias = null; + } + } + + if (socket instanceof SSLSocket && host != null && alias != null) { + LOG.info("Added " + alias + " alias in cache for " + host.toLowerCase()); + hostToClientAliasCache.put(host.toLowerCase(), alias); + } + return alias; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private String getHostFromSocket(Socket socket) { + try { + final Class c = Class.forName("sun.security.ssl.SSLSocketImpl"); + if (c.isInstance(socket)) { + final Object o = c.cast(socket); + Method m; + try { + m = c.getDeclaredMethod("getHost"); + m.setAccessible(true); + } catch (NoSuchMethodException e) { + m = c.getDeclaredMethod("getPeerHost"); + } + return (String) m.invoke(o); + } + } catch (Exception e) { + LOG.warn("Cannot get remote host from Socket", e); + } + return null; + } + + private String getPreferredAlias(List certificateOptions, String aliasType) { + final String alias; + if (certificateOptions.size() > 1) { + if (JNLPRuntime.isHeadless()) { + alias = certificateOptions.get(0).alias; + LOG.info("Returning the first " + aliasType + " alias in headless mode : " + alias); + } else { + final DialogResult res = SecurityDialogs.showClientCertSelectionPrompt(certificateOptions); + if (res == null) { + alias = null; + userCancelled = true; + LOG.warn("Client certificate selection cancelled by user"); + } else { + alias = certificateOptions.get(res.getButtonIndex()).alias; + LOG.info("Returning the selected " + aliasType + " alias : " + alias); + } + } + } else if (certificateOptions.size() == 1) { + alias = certificateOptions.get(0).alias; + LOG.info("Returning the only " + aliasType + " alias : " + alias); + } else { + alias = null; + } + return removeAliasSuffix(alias); + } + + private String removeAliasSuffix(String aliasWithSuffix) { + if (aliasWithSuffix == null) { + return null; + } + if (aliasWithSuffix.endsWith(USER_SUFFIX)) { + return aliasWithSuffix.substring(0, aliasWithSuffix.length() - USER_SUFFIX.length()); + } + if (aliasWithSuffix.endsWith(SYSTEM_SUFFIX)) { + return aliasWithSuffix.substring(0, aliasWithSuffix.length() - SYSTEM_SUFFIX.length()); + } + if (aliasWithSuffix.endsWith(BROWSER_SUFFIX)) { + return aliasWithSuffix.substring(0, aliasWithSuffix.length() - BROWSER_SUFFIX.length()); + } + return aliasWithSuffix; + } + + @Override + public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { + String alias = null; + if (userKeyManager != null) { + alias = userKeyManager.chooseServerAlias(keyType, issuers, socket); + } + if (alias == null && systemKeyManager != null) { + alias = systemKeyManager.chooseServerAlias(keyType, issuers, socket); + } + if (alias == null && browserKeyManager != null) { + alias = browserKeyManager.chooseServerAlias(keyType, issuers, socket); + } + return alias; + } + + @Override + public X509Certificate[] getCertificateChain(String alias) { + X509Certificate[] x509certificates = null; + if (userKeyManager != null) { + x509certificates = userKeyManager.getCertificateChain(alias); + } + if (x509certificates == null && systemKeyManager != null) { + x509certificates = systemKeyManager.getCertificateChain(alias); + } + if (x509certificates == null && browserKeyManager != null) { + x509certificates = browserKeyManager.getCertificateChain(alias); + } + return x509certificates; + } + + @Override + public String[] getClientAliases(String keyType, Principal[] issuers) { + final List aliases = new ArrayList<>(); + if (userKeyManager != null) { + aliases.addAll(appendSuffixToAll(userKeyManager.getClientAliases(keyType, issuers), USER_SUFFIX)); + } + if (systemKeyManager != null) { + aliases.addAll(appendSuffixToAll(systemKeyManager.getClientAliases(keyType, issuers), SYSTEM_SUFFIX)); + } + if (browserKeyManager != null) { + aliases.addAll(appendSuffixToAll(browserKeyManager.getClientAliases(keyType, issuers), BROWSER_SUFFIX)); + } + return aliases.toArray(new String[0]); + } + + @Override + public PrivateKey getPrivateKey(String alias) { + PrivateKey privateKey = null; + if (userKeyManager != null) { + privateKey = userKeyManager.getPrivateKey(alias); + } + if (privateKey == null && systemKeyManager != null) { + privateKey = systemKeyManager.getPrivateKey(alias); + } + if (privateKey == null && browserKeyManager != null) { + privateKey = browserKeyManager.getPrivateKey(alias); + } + return privateKey; + } + + @Override + public String[] getServerAliases(String keyType, Principal[] issuers) { + final List aliases = new ArrayList<>(); + if (userKeyManager != null) { + aliases.addAll(appendSuffixToAll(userKeyManager.getServerAliases(keyType, issuers), null)); + } + if (systemKeyManager != null) { + aliases.addAll(appendSuffixToAll(systemKeyManager.getServerAliases(keyType, issuers), null)); + } + if (browserKeyManager != null) { + aliases.addAll(appendSuffixToAll(browserKeyManager.getServerAliases(keyType, issuers), null)); + } + return aliases.toArray(new String[0]); + } + + @Override + public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, SSLEngine engine) { + return chooseClientAlias(keyType, issuers, null); + } + + @Override + public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine) { + return chooseServerAlias(keyType, issuers, null); + } + + private List appendSuffixToAll(String[] array, String suffix) { + if (array == null) { + return emptyList(); + } + if (suffix == null) { + return asList(array); + } + return Stream.of(array) + .map(s -> s + suffix) + .collect(toList()); + } +} diff --git a/core/src/main/java/net/sourceforge/jnlp/runtime/classloader/JNLPClassLoader.java b/core/src/main/java/net/sourceforge/jnlp/runtime/classloader/JNLPClassLoader.java index 676166716..5e5b7b6e6 100644 --- a/core/src/main/java/net/sourceforge/jnlp/runtime/classloader/JNLPClassLoader.java +++ b/core/src/main/java/net/sourceforge/jnlp/runtime/classloader/JNLPClassLoader.java @@ -316,7 +316,8 @@ private JNLPClassLoader(JNLPFile file, UpdatePolicy policy, String mainName, boo strict = Boolean.parseBoolean(JNLPRuntime.getConfiguration().getProperty(ConfigurationConstants.KEY_STRICT_JNLP_CLASSLOADER)); this.file = file; - this.tracker = new ResourceTracker(true, file.getDownloadOptions(), JNLPRuntime.getDefaultUpdatePolicy()); + // no prefetch otherwise deployment.cache.parallelDownloadCount is not enforced + this.tracker = new ResourceTracker(false, file.getDownloadOptions(), JNLPRuntime.getDefaultUpdatePolicy()); this.updatePolicy = policy; this.resources = file.getResources(); @@ -684,7 +685,6 @@ private void initializeResources() throws LaunchException { if (jar.isEager() || jar.isMain()) { initialJars.add(jar); // regardless of part } - // FIXME: this will trigger an eager download as the tracker is created with prefetch == true tracker.addResource(jar.getLocation(), jar.getVersion(), jar.isCacheable() ? JNLPRuntime.getDefaultUpdatePolicy() : UpdatePolicy.FORCE); }