/* * Zed Attack Proxy (ZAP) and its related class files. * * ZAP is an HTTP/HTTPS proxy for assessing web application security. * * Copyright 2011 mawoki@ymail.com * * 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 org.zaproxy.zap.extension.dynssl; import java.awt.BorderLayout; import java.awt.Desktop; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.StringWriter; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.security.KeyStore; import java.security.cert.Certificate; import javax.swing.GroupLayout; import javax.swing.GroupLayout.Alignment; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JFileChooser; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.LayoutStyle.ComponentPlacement; import javax.swing.border.EmptyBorder; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.filechooser.FileFilter; import org.apache.commons.io.FileUtils; import org.apache.log4j.Logger; import org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator; import org.bouncycastle.util.io.pem.PemWriter; import org.parosproxy.paros.Constant; import org.parosproxy.paros.model.OptionsParam; import org.parosproxy.paros.security.SslCertificateService; import org.parosproxy.paros.view.AbstractParamPanel; import org.zaproxy.zap.utils.FontUtils; import org.zaproxy.zap.utils.ZapTextArea; import org.zaproxy.zap.utils.ZapXmlConfiguration; public class DynamicSSLPanel extends AbstractParamPanel { private static final long serialVersionUID = 1L; private static final int MIN_CERT_LENGTH = 10; private static final String OWASP_ZAP_ROOT_CA_NAME = "owasp_zap_root_ca"; private static final String OWASP_ZAP_ROOT_CA_FILE_EXT = ".cer"; private static final String OWASP_ZAP_ROOT_CA_FILENAME = OWASP_ZAP_ROOT_CA_NAME + OWASP_ZAP_ROOT_CA_FILE_EXT; private static final String CONFIGURATION_FILENAME = Constant.FILE_CONFIG_NAME; private ZapTextArea txt_PubCert; private JButton bt_view; private JButton bt_save; private KeyStore rootca; private ExtensionDynSSL extension; private static final Logger logger = Logger.getLogger(DynamicSSLPanel.class); /** * Create the panel. */ public DynamicSSLPanel(ExtensionDynSSL extension) { super(); this.extension = extension; setName(Constant.messages.getString("dynssl.options.name")); setLayout(new BorderLayout(0, 0)); final JPanel panel = new JPanel(); panel.setBorder(new EmptyBorder(2, 2, 2, 2)); add(panel); final JLabel lbl_Cert = new JLabel(Constant.messages.getString("dynssl.label.rootca")); txt_PubCert = new ZapTextArea(); txt_PubCert.setFont(FontUtils.getFont("Monospaced")); txt_PubCert.setEditable(false); txt_PubCert.getDocument().addDocumentListener(new DocumentListener() { @Override public void removeUpdate(DocumentEvent e) { checkAndEnableButtons(); } @Override public void insertUpdate(DocumentEvent e) { checkAndEnableButtons(); } @Override public void changedUpdate(DocumentEvent e) { checkAndEnableButtons(); } private void checkAndEnableButtons() { checkAndEnableViewButton(); checkAndEnableSaveButton(); } }); final JScrollPane pubCertScrollPane = new JScrollPane(txt_PubCert); final JButton bt_generate = new JButton(Constant.messages.getString("dynssl.button.generate")); bt_generate.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { doGenerate(); } }); bt_generate.setIcon(new ImageIcon(DynamicSSLPanel.class.getResource("/resource/icon/16/041.png"))); bt_save = new JButton(Constant.messages.getString("menu.file.save")); checkAndEnableSaveButton(); bt_save.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { doSave(); } }); bt_save.setIcon(new ImageIcon(DynamicSSLPanel.class.getResource("/resource/icon/16/096.png"))); bt_view = new JButton(Constant.messages.getString("menu.view")); checkAndEnableViewButton(); bt_view.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { doView(); } }); bt_view.setIcon(new ImageIcon(DynamicSSLPanel.class.getResource("/resource/icon/16/049.png"))); final JButton bt_import = new JButton(Constant.messages.getString("dynssl.button.import")); bt_import.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { doImport(); } }); bt_import.setIcon(new ImageIcon(DynamicSSLPanel.class.getResource("/resource/icon/16/047.png"))); final GroupLayout gl_panel = new GroupLayout(panel); gl_panel.setHorizontalGroup( gl_panel.createParallelGroup(Alignment.LEADING) .addGroup(gl_panel.createSequentialGroup() .addContainerGap() .addGroup(gl_panel.createParallelGroup(Alignment.LEADING, false) .addGroup(gl_panel.createSequentialGroup() .addGroup(gl_panel.createParallelGroup(Alignment.LEADING, false) .addComponent(lbl_Cert, GroupLayout.PREFERRED_SIZE, 115, GroupLayout.PREFERRED_SIZE) .addGroup(gl_panel.createSequentialGroup() .addPreferredGap(ComponentPlacement.RELATED) .addComponent(bt_generate))) .addGap(6)) .addGroup(gl_panel.createSequentialGroup() .addComponent(bt_import) .addPreferredGap(ComponentPlacement.RELATED))) .addGroup(gl_panel.createParallelGroup(Alignment.LEADING) .addGroup(gl_panel.createSequentialGroup() .addComponent(bt_view) .addPreferredGap(ComponentPlacement.RELATED) .addComponent(bt_save)) .addComponent(pubCertScrollPane, GroupLayout.DEFAULT_SIZE, 369, Short.MAX_VALUE)) .addContainerGap()) ); gl_panel.setVerticalGroup( gl_panel.createParallelGroup(Alignment.LEADING) .addGroup(gl_panel.createSequentialGroup() .addGap(10) .addGroup(gl_panel.createParallelGroup(Alignment.BASELINE) .addGroup(gl_panel.createSequentialGroup() .addComponent(lbl_Cert) .addGap(10) .addComponent(bt_generate, GroupLayout.PREFERRED_SIZE, 25, GroupLayout.PREFERRED_SIZE) .addPreferredGap(ComponentPlacement.RELATED) .addComponent(bt_import, GroupLayout.PREFERRED_SIZE, 25, GroupLayout.PREFERRED_SIZE)) .addComponent(pubCertScrollPane, GroupLayout.PREFERRED_SIZE, 400, GroupLayout.PREFERRED_SIZE)) .addPreferredGap(ComponentPlacement.RELATED) .addGroup(gl_panel.createParallelGroup(Alignment.BASELINE) .addComponent(bt_save, GroupLayout.PREFERRED_SIZE, 25, GroupLayout.PREFERRED_SIZE) .addComponent(bt_view, GroupLayout.PREFERRED_SIZE, 25, GroupLayout.PREFERRED_SIZE)) .addGap(0, 29, Short.MAX_VALUE)) ); panel.setLayout(gl_panel); } @Override public void initParam(Object obj) { final OptionsParam options = (OptionsParam) obj; final DynSSLParam param = options.getParamSet(DynSSLParam.class); setRootca(param.getRootca()); } @Override public void saveParam(Object obj) throws Exception { final OptionsParam options = (OptionsParam) obj; final DynSSLParam param = options.getParamSet(DynSSLParam.class); param.setRootca(rootca); extension.setRootCa(rootca); } @Override public String getHelpIndex() { return "ui.dialogs.options.dynsslcert"; } private void setRootca(KeyStore rootca) { this.rootca = rootca; final StringWriter sw = new StringWriter(); if (rootca != null) { try { final Certificate cert = rootca.getCertificate(SslCertificateService.ZAPROXY_JKS_ALIAS); try (final PemWriter pw = new PemWriter(sw)) { pw.writeObject(new JcaMiscPEMGenerator(cert)); pw.flush(); } } catch (final Exception e) { logger.error("Error while extracting public part from generated Root CA certificate.", e); } } if (logger.isDebugEnabled()) { logger.debug("Certificate defined.\n" + sw.toString()); } txt_PubCert.setText(sw.toString()); } /** * Viewing is only allowed, if * (a) when java.Desktop#open() works * (b) there's a certificate (text) in the text area */ private void checkAndEnableViewButton() { boolean enabled = true; enabled &= Desktop.isDesktopSupported(); enabled &= txt_PubCert.getDocument().getLength() > MIN_CERT_LENGTH; bt_view.setEnabled(enabled); } /** * Saving is only allowed, if * (a) there's a certificate (text) in the text area */ private void checkAndEnableSaveButton() { boolean enabled = true; enabled &= txt_PubCert.getDocument().getLength() > MIN_CERT_LENGTH; bt_save.setEnabled(enabled); } /** * Import Root CA certificate from other ZAP configuration files. */ private void doImport() { if (checkExistingCertificate()) { // prevent overwriting return; } final JFileChooser fc = new JFileChooser(System.getProperty("user.home")); fc.setFileSelectionMode(JFileChooser.FILES_ONLY); fc.setMultiSelectionEnabled(false); fc.setSelectedFile(new File(CONFIGURATION_FILENAME)); fc.setFileFilter(new FileFilter() { @Override public String getDescription() { // config.xml or *.pem files return Constant.messages.getString("dynssl.filter.file"); } @Override public boolean accept(File f) { return f.getName().toLowerCase().endsWith(CONFIGURATION_FILENAME) || f.getName().toLowerCase().endsWith("pem") || f.isDirectory(); } }); final int result = fc.showOpenDialog(this); final File f = fc.getSelectedFile(); if (result == JFileChooser.APPROVE_OPTION && f.exists()) { if (logger.isInfoEnabled()) { logger.info("Loading Root CA certificate from " + f); } KeyStore ks = null; if (f.getName().toLowerCase().endsWith("pem")) { ks = convertPemFileToKeyStore(f.toPath()); } else { try { final ZapXmlConfiguration conf = new ZapXmlConfiguration(f); final String rootcastr = conf.getString(DynSSLParam.PARAM_ROOT_CA); ks = SslCertificateUtils.string2Keystore(rootcastr); } catch (final Exception e) { logger.error("Error importing Root CA cert from config file:", e); JOptionPane.showMessageDialog(this, Constant.messages.getString("dynssl.message1.filecouldntloaded"), Constant.messages.getString("dynssl.message1.title"), JOptionPane.ERROR_MESSAGE); } } if (ks != null) { setRootca(ks); } } } /** * Converts the given {@code .pem} file into a {@link KeyStore}. * * @param pemFile the {@code .pem} file that contains the certificate and the private key. * @return the {@code KeyStore} with the certificate, or {@code null} if the conversion failed. */ private KeyStore convertPemFileToKeyStore(Path pemFile) { String pem; try { pem = FileUtils.readFileToString(pemFile.toFile(), StandardCharsets.US_ASCII); } catch (IOException e) { logger.warn("Failed to read .pem file:", e); JOptionPane.showMessageDialog( this, Constant.messages.getString("dynssl.importpem.failedreadfile", e.getLocalizedMessage()), Constant.messages.getString("dynssl.importpem.failed.title"), JOptionPane.ERROR_MESSAGE); return null; } byte[] cert; try { cert = SslCertificateUtils.extractCertificate(pem); if (cert.length == 0) { JOptionPane.showMessageDialog( this, Constant.messages.getString( "dynssl.importpem.nocertsection", SslCertificateUtils.BEGIN_CERTIFICATE_TOKEN, SslCertificateUtils.END_CERTIFICATE_TOKEN), Constant.messages.getString("dynssl.importpem.failed.title"), JOptionPane.ERROR_MESSAGE); return null; } } catch (IllegalArgumentException e) { logger.warn("Failed to base64 decode the certificate from .pem file:", e); JOptionPane.showMessageDialog( this, Constant.messages.getString("dynssl.importpem.certnobase64"), Constant.messages.getString("dynssl.importpem.failed.title"), JOptionPane.ERROR_MESSAGE); return null; } byte[] key; try { key = SslCertificateUtils.extractPrivateKey(pem); if (key.length == 0) { JOptionPane.showMessageDialog( this, Constant.messages.getString( "dynssl.importpem.noprivkeysection", SslCertificateUtils.BEGIN_PRIVATE_KEY_TOKEN, SslCertificateUtils.END_PRIVATE_KEY_TOKEN), Constant.messages.getString("dynssl.importpem.failed.title"), JOptionPane.ERROR_MESSAGE); return null; } } catch (IllegalArgumentException e) { logger.warn("Failed to base64 decode the private key from .pem file:", e); JOptionPane.showMessageDialog( this, Constant.messages.getString("dynssl.importpem.privkeynobase64"), Constant.messages.getString("dynssl.importpem.failed.title"), JOptionPane.ERROR_MESSAGE); return null; } try { return SslCertificateUtils.pem2KeyStore(cert, key); } catch (Exception e) { logger.error("Error creating KeyStore for Root CA cert from .pem file:", e); JOptionPane.showMessageDialog( this, Constant.messages.getString("dynssl.importpem.failedkeystore", e.getLocalizedMessage()), Constant.messages.getString("dynssl.importpem.failed.title"), JOptionPane.ERROR_MESSAGE); return null; } } /** * Saving Root CA certificate to disk. */ private void doSave() { if (txt_PubCert.getDocument().getLength() < MIN_CERT_LENGTH) { logger.error("Illegal state! There seems to be no certificate available."); bt_save.setEnabled(false); } final JFileChooser fc = new JFileChooser(System.getProperty("user.home")); fc.setFileSelectionMode(JFileChooser.FILES_ONLY); fc.setMultiSelectionEnabled(false); fc.setSelectedFile(new File(OWASP_ZAP_ROOT_CA_FILENAME)); if (fc.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) { final File f = fc.getSelectedFile(); if (logger.isInfoEnabled()) { logger.info("Saving Root CA certificate to " + f); } try { writePubCertificateToFile(f); } catch (final Exception e) { logger.error("Error while writing certificate data to file " + f, e); } } } private void writePubCertificateToFile(File file) throws IOException { try (final OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(file), "ASCII")) { osw.write(txt_PubCert.getText()); } } /** * Generates a new Root CA certificate ... */ private void doGenerate() { if (checkExistingCertificate()) { // prevent overwriting return; } try { final KeyStore newrootca = SslCertificateUtils.createRootCA(); setRootca(newrootca); } catch (final Exception e) { logger.error("Error while generating Root CA certificate", e); } } /** * Check if certificate already exists. It will ask the user to overwrite. * @return True, if certificate exists OR it should be overwritten. */ private boolean checkExistingCertificate() { boolean alreadyexists = txt_PubCert.getDocument().getLength() > MIN_CERT_LENGTH; if (alreadyexists) { final int result = JOptionPane.showConfirmDialog(this, Constant.messages.getString("dynssl.message2.caalreadyexists") + "\n" + Constant.messages.getString("dynssl.message2.willreplace") + "\n\n" + Constant.messages.getString("dynssl.message2.wanttooverwrite"), Constant.messages.getString("dynssl.message2.title"), JOptionPane.YES_NO_OPTION); alreadyexists = !(result == JOptionPane.YES_OPTION); } return alreadyexists; } /** * writes the certificate to a temporary file and tells the OS to open it * using java.awrt.Desktop#open() */ private void doView() { if (txt_PubCert.getDocument().getLength() < MIN_CERT_LENGTH) { logger.error("Illegal state! There seems to be no certificate available."); bt_view.setEnabled(false); } boolean written = false; File tmpfile = null; try { tmpfile = File.createTempFile(OWASP_ZAP_ROOT_CA_NAME, OWASP_ZAP_ROOT_CA_FILE_EXT); writePubCertificateToFile(tmpfile); written = true; } catch (final Exception e) { logger.error("Error while writing certificate data into temporary file.", e); } if (tmpfile != null && written) { if (Desktop.isDesktopSupported()) { try { Desktop.getDesktop().open(tmpfile); } catch (final IOException e) { logger.error("Error while telling the Operating System to open "+tmpfile, e); } } } } }