/* * Copyright (C) 2012 Intel Corporation * All rights reserved. */ package com.intel.mtwilson.setup.cmd; import com.intel.dcsg.cpg.crypto.CryptographyException; import com.intel.dcsg.cpg.crypto.RsaCredentialX509; import com.intel.dcsg.cpg.crypto.RsaUtil; import com.intel.dcsg.cpg.crypto.SimpleKeystore; import com.intel.dcsg.cpg.x509.X509Builder; import com.intel.mtwilson.model.*; import com.intel.dcsg.cpg.io.ByteArrayResource; import com.intel.mtwilson.ms.common.MSException; import com.intel.dcsg.cpg.console.Command; import com.intel.mtwilson.setup.RemoteSetup; import com.intel.mtwilson.setup.SetupContext; import com.intel.mtwilson.setup.Timeout; import com.intel.mtwilson.setup.model.AdminUser; import com.intel.mtwilson.setup.model.Database; import com.intel.mtwilson.setup.model.DatabaseType; import com.intel.mtwilson.setup.model.DistinguishedName; import com.intel.mtwilson.setup.model.PrivacyCA; import com.intel.mtwilson.setup.model.SetupTarget; import com.intel.mtwilson.setup.model.WebContainerType; import com.intel.mtwilson.setup.ui.console.IntegerInput; import com.intel.mtwilson.setup.ui.console.InternetAddressInput; import com.intel.mtwilson.setup.ui.console.StringInput; import com.intel.mtwilson.setup.ui.console.URLInput; import com.intel.mtwilson.setup.ui.console.YesNoInput; import com.intel.dcsg.cpg.validation.Fault; import com.intel.dcsg.cpg.validation.InputModel; import com.intel.dcsg.cpg.validation.Model; import java.io.Console; import java.io.IOException; import java.net.MalformedURLException; import java.net.SocketException; import java.net.URL; import java.security.KeyManagementException; import java.security.KeyPair; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.prefs.BackingStoreException; import java.util.prefs.Preferences; import net.schmizz.sshj.common.SecurityUtils; import net.schmizz.sshj.userauth.UserAuthException; import org.apache.commons.configuration.Configuration; /** * * @author jbuhacoff */ public class ConfigureRemote implements Command { public static final Console console = System.console(); // safe because Main checks that it exists before calling this command private static final InternetAddressInput INTERNET_ADDRESS_INPUT = new InternetAddressInput(); private static final URLInput URL_INPUT = new URLInput(); private static final YesNoInput YES_NO_INPUT = new YesNoInput(); private static final IntegerInput INTEGER_INPUT = new IntegerInput(); private static final StringInput STRING_INPUT = new StringInput(); public static final String MTWILSON_CA_KEYSTORE_PACKAGE = "com/intel/mtwilson/ca/keystores"; public static final String MTWILSON_SSH_KNOWN_HOSTS_PACKAGE = "com/intel/mtwilson/ssh/known_hosts"; private SetupContext ctx = new SetupContext(); private Configuration options = null; @Override public void setOptions(Configuration options) { this.options = options; } @Override public void execute(String[] args) throws Exception { ctx.target = SetupTarget.REMOTE; try { generateRootCAKey(); // create or load root ca key and put it in the setup context RemoteSetup remote = new RemoteSetup(ctx); if (remote == null) { throw new IllegalStateException("ConfigureRemote command failed: remote object is null."); } remote.setRemoteHost(getRequiredInternetAddressWithPrompt("SSH to remote host")); remote.setUsername(getRequiredStringWithPrompt("SSH Username (eg. root)")); remote.setPassword(getRequiredPasswordWithPrompt("SSH Password")); remote.setRemoteHostTimeout(new Timeout(60, TimeUnit.SECONDS)); try { remote.open(); if (remote.getRemoteHostKey() != null && remote.getRemoteHostKey().server != null && remote.getRemoteHostKey().publicKey != null) { boolean trustRemoteHost = shouldTrustRemoteHost(remote.getRemoteHostKey().server, remote.getRemoteHostKey().publicKey); if( !trustRemoteHost ) { remote.close(); return; } remote.getRemoteSettings(); // inputDistinguishedNameForCertificates(); if( ctx.rootCa != null ) { remote.deployRootCACertToServer(); // using ssh, write the root CA cert to file on disk so server can trust it // saml remote.downloadSamlCertFromServer(); remote.signSamlCertWithCaCert(); remote.uploadSamlCertToServer(); // tls remote.downloadTlsKeystoreFromServer(); if( ctx.tlsCertificate == null ) { System.err.println("FAILED TO READ TLS CERT"); printFaults(remote); remote.close(); return; } // remote.downloadTlsCertFromServer(); remote.signTlsCertWithCaCert(); remote.uploadTlsCertToServer(); remote.uploadTlsKeystoreToServer(); // privacy ca remote.downloadPrivacyCaKeystoreFromServer(); remote.signPrivacyCaCertWithRootCaCert(); remote.uploadPrivacyCaKeystoreToServer(); } remote.close(); } } catch(UserAuthException e) { System.out.println("Not able to ssh to remote host with given username and password: "+e.toString()); } collectUserInput(); createCertificates(); displaySummary(); } catch(Exception e) { e.printStackTrace(System.err); } } private boolean shouldTrustRemoteHost(String server, PublicKey serverPublicKey) throws IOException { Preferences prefs = Preferences.userRoot().node(MTWILSON_SSH_KNOWN_HOSTS_PACKAGE); byte[] knownHostKey = prefs.getByteArray(server, null); if( knownHostKey == null ) { // we don't know this server, so ask user if it is trusted System.out.println("Remote host key ("+serverPublicKey.getAlgorithm()+"): "+SecurityUtils.getFingerprint(serverPublicKey)); // can't just do Md5Digest.valueOf(remote.getRemoteHostKey().publicKey.getEncoded()).toString()) because ssh key fingerprints have a specific encoding, for example "ssh-rsa" followed by exponent length, exponent, modulus length, modulus... which is not the same as the byte from publickey.getEncoded() boolean trustRemoteHost = getRequiredYesNoWithPrompt("Trust this remote host? (run 'mtwilson fingerprint' on remote host to see ssh key)"); if( trustRemoteHost ) { prefs.putByteArray(server, serverPublicKey.getEncoded()); return true; } return false; } else { // we know this server, so check if the public key matches if( Arrays.equals(knownHostKey, serverPublicKey.getEncoded()) ) { return true; } // key did not match, ask user if s/he wants to accept the new key - with a warning System.err.println("WARNING: The remote host key has changed."); System.out.println("Remote host key ("+serverPublicKey.getAlgorithm()+"): "+SecurityUtils.getFingerprint(serverPublicKey)); // can't just do Md5Digest.valueOf(remote.getRemoteHostKey().publicKey.getEncoded()).toString()) because ssh key fingerprints have a specific encoding, for example "ssh-rsa" followed by exponent length, exponent, modulus length, modulus... which is not the same as the byte from publickey.getEncoded() boolean trustRemoteHost = getRequiredYesNoWithPrompt("Trust this remote host? (run 'mtwilson fingerprint' on remote host to see ssh key)"); if( trustRemoteHost ) { prefs.putByteArray(server, serverPublicKey.getEncoded()); return true; } return false; } } public void displaySummary() { System.out.println("Mt Wilson URL: "+ctx.serverUrl.toExternalForm()); if( ctx.attestationServiceDatabase != null ) { System.out.println("Mt Wilson Database: "+(ctx.attestationServiceDatabase.type==null?"unknown vendor":ctx.attestationServiceDatabase.type.displayName())); System.out.println(" hostname: "+(ctx.attestationServiceDatabase.hostname==null?"unknown hostname":ctx.attestationServiceDatabase.hostname.toString())); System.out.println(" port: "+ctx.attestationServiceDatabase.port); System.out.println(" username: "+ctx.attestationServiceDatabase.username); System.out.println(" password: "+ctx.attestationServiceDatabase.password); } } public void collectUserInput() throws IOException { try { inputMtWilsonURL(); inputMtWilsonDatabase(); inputManagementServiceAdminCredentials(); inputEkSigningKeyCredentials(); } catch(SocketException e) { System.err.println("Got error: "+e.toString()); } } public void createCertificates() { try { if( ctx.target.equals(SetupTarget.LOCAL)) { // inputDistinguishedNameForCertificates(); generateSelfSignedTlsKey(); generateSelfSignedSamlSigningKey(); // generateSelfSignedPrivacyCAKey(); // privacy ca currently creates its own key during setup } else { // inputDistinguishedNameForCertificates(); // generateRootCAKey(); // // deployRootCACertToServer(); // using ssh, write the root CA cert to file on disk so server can trust it // createServerTlsCertificate(); // deployServerTlsCertificateToServer(); // using ssh, write it to glassfish keystore.jks // createServerSamlCertificate(); // deployServerSamlCertificateToServer(); // using ssh, write it to mt wilson conf dir (inside keystore if possible and separate .crt file) } } catch(NoSuchAlgorithmException e) { console.printf("Ignoring NoSuchAlgorith exception thrown during createCertificates"); } catch(CryptographyException e) { console.printf("Ignoring Cryptography exception thrown during createCertificates"); } catch(IOException e) { console.printf("Ignoring IO exception thrown during createCertificates"); } } private String getConfirmedPasswordWithPrompt(String prompt) throws IOException { if (console == null) { throw new IOException("no console."); } while(true) { System.out.println(prompt); char[] password = console.readPassword("Password: "); char[] passwordAgain = console.readPassword("Password (again):"); if( password.length == passwordAgain.length && String.valueOf(password).equals(String.valueOf(passwordAgain)) ) { return String.valueOf(password); } System.out.println("Passwords must match."); } } private String getRequiredPasswordWithPrompt(String prompt) throws IOException { if (console == null) { throw new IOException("no console."); } while(true) { System.out.println(prompt); char[] password = console.readPassword("Password: "); if( password.length > 0 ) { return String.valueOf(password); } } } private int getSelectionFromListWithPrompt(List<String> list, String prompt) throws IOException { if (console == null) { throw new IOException("no console."); } while(true) { System.out.println(prompt); for(int i=0; i<list.size(); i++) { System.out.println(String.format("[%2d] %s", i+1, list.get(i))); } String selection = console.readLine("Choose 1-%d: ", list.size()); try { Integer value = Integer.valueOf(selection); if( value >=1 && value <= list.size() ) { return value-1; } } catch(java.lang.NumberFormatException e) { System.err.println("Press Ctrl+C to exit"); } } } private <T> T getRequiredEnumWithPrompt(Class<T> clazz, String prompt) throws IOException { T[] list = clazz.getEnumConstants(); if( list == null ) { throw new IllegalArgumentException(clazz.getName()+" is not an enum type"); } ArrayList<String> strings = new ArrayList<String>(); for( T item : list ) { strings.add(item.toString()); } int selected = getSelectionFromListWithPrompt(strings, prompt); return list[selected]; } private String getRequiredStringWithPrompt(String prompt) throws IOException { return getRequiredInputWithPrompt(STRING_INPUT, prompt, "String:"); } // commenting out unused function (6/11 1.2) //private Integer getRequiredIntegerWithPrompt(String prompt) throws IOException { // return getRequiredInputWithPrompt(INTEGER_INPUT, prompt, "Integer:"); //} private Integer getRequiredIntegerInRangeWithPrompt(int min, int max, String prompt) throws IOException { return getRequiredInputWithPrompt(new IntegerInput(min,max), prompt, String.format("Integer [%d-%d]:", min, max)); } /** * * @param prompt * @return true for Yes, false for No */ private boolean getRequiredYesNoWithPrompt(String prompt) throws IOException { return getRequiredInputWithPrompt(YES_NO_INPUT, prompt, "[Y]es or [N]o:").booleanValue(); } private URL getRequiredURLWithPrompt(String prompt) throws IOException { return getRequiredInputWithPrompt(URL_INPUT, prompt, "URL:"); } private InternetAddress getRequiredInternetAddressWithPrompt(String prompt) throws IOException { return getRequiredInputWithPrompt(INTERNET_ADDRESS_INPUT, prompt, "Hostname or IP Address:"); } private InternetAddress getRequiredInternetAddressWithDefaultPrompt(String prompt, String defaultValue) throws IOException { return getRequiredInputWithDefaultPrompt(INTERNET_ADDRESS_INPUT, prompt, "Hostname or IP Address:", defaultValue); } private <T> T getRequiredInputWithPrompt(InputModel<T> model, String caption, String prompt) throws IOException { if (console == null) { throw new IOException("no console."); } while(true) { System.out.println(caption); String input = console.readLine(prompt+" "); model.setInput(input); if( model.isValid() ) { return model.value(); } else { printFaults(model); } } } private void printFaults(Model model) { System.err.println("--- Errors ---"); for(Fault f : model.getFaults()) { printFault(f, 0); // level 0 means no indentation } } /** * * @param f * @param level of indentation; use 0 for top-level faults, and increment once for each level of logical nesting */ private void printFault(Fault f, int level) { String indentation; StringBuilder indentationBuilder = new StringBuilder(); for(int i=0; i<level; i++) { indentationBuilder.append(" "); // each level is indented two spaces from the previous level } indentation = indentationBuilder.toString(); System.err.println(String.format("%s- %s", indentation, f.toString())); if( f.getCause() != null ) { System.err.println(String.format("%s Caused by: %s", indentation, f.getCause().toString())); } if( f.getMore() != null && f.getMore().length > 0 ) { System.err.println(String.format("%s Related errors:", indentation)); Fault[] more = f.getMore(); for(int i=0; i<more.length; i++) { printFault(more[i], level+1); } } } private <T> T getRequiredInputWithDefaultPrompt(InputModel<T> model, String caption, String prompt, String defaultValue) throws IOException { if (console == null) { throw new IOException("no console."); } while(true) { System.out.println(caption); String input = console.readLine(prompt+" ["+defaultValue+"] "); if( input == null ) { input = defaultValue; } model.setInput(input); if( model.isValid() ) { return model.value(); } else { for(Fault f : model.getFaults()) { System.err.println(f.toString()); } } } } private InternetAddress getRequiredInternetAddressWithMenuPrompt(String prompt) throws SocketException, IOException { SetMtWilsonURL cmd = new SetMtWilsonURL(); List<String> options = cmd.getLocalAddresses(); if( ctx.serverAddress != null && !options.contains(ctx.serverAddress.toString())) { options.add(ctx.serverAddress.toString()); } options.add("Other"); int selected = getSelectionFromListWithPrompt(options, prompt); InternetAddress address; if( selected == options.size() - 1 ) { // "Other" address = getRequiredInternetAddressWithPrompt("Other "+prompt); } else { address = new InternetAddress(options.get(selected)); } return address; } public void inputMtWilsonURL() throws SocketException, IOException { if( ctx.target.equals(SetupTarget.LOCAL) ) { InternetAddress address = getRequiredInternetAddressWithMenuPrompt("Local Mt Wilson Hostname or IP Address"); System.out.println("selected: "+address.toString()); ctx.serverAddress = address; } else { InternetAddress address = getRequiredInternetAddressWithPrompt("Remote Mt Wilson Hostname or IP Address"); System.out.println("selected: "+address.toString()); ctx.serverAddress = address; } WebContainerType webContainerType = getRequiredEnumWithPrompt(WebContainerType.class, "Web application container"); String defaultUrl = String.format("https://%s:%d", ctx.serverAddress, webContainerType.defaultHttpsPort()); boolean urlOk = getRequiredYesNoWithPrompt(String.format("Default Mt Wilson URL: %s\nIs this ok?", defaultUrl)); if( urlOk ) { try { ctx.serverUrl = new URL(defaultUrl); } catch(MalformedURLException e) { System.err.println("There is a problem with this URL: "+defaultUrl); ctx.serverUrl = getRequiredURLWithPrompt("Please enter the Mt Wilson URL"); } } else { ctx.serverUrl = getRequiredURLWithPrompt("Please enter the Mt Wilson URL"); } // ctx.serverPort = ctx.serverUrl.getPort(); } public void inputMtWilsonDatabase() throws SocketException, IOException { Database db = new Database(); db.type = getRequiredEnumWithPrompt(DatabaseType.class, "Database system"); db.driver = db.type.defaultJdbcDriver(); if( ctx.target.equals(SetupTarget.LOCAL) ) { db.hostname = getRequiredInternetAddressWithMenuPrompt("Database server Hostname or IP Address"); } else { db.hostname = getRequiredInternetAddressWithDefaultPrompt("Database server Hostname or IP Address", ctx.serverAddress.toString()); } boolean useNonDefaultPort = getRequiredYesNoWithPrompt(String.format("Default port is %d. Do you want to change it?", db.type.defaultPort())); if( useNonDefaultPort ) { db.port = getRequiredIntegerInRangeWithPrompt(0,65535,"Database port"); } else { db.port = db.type.defaultPort(); } db.username = getRequiredStringWithPrompt("Database username"); db.password = getRequiredPasswordWithPrompt("Database password"); ctx.attestationServiceDatabase = db; } public void inputEkSigningKeyCredentials() throws IOException { System.out.println("In order to authorize Linux hosts using Trust Agent, an EK Signing Key is downloaded from Mt Wilson."); System.out.println("You must set a username and password to authenticate administrators who are downloading the key during a Trust Agent install."); PrivacyCA pca = new PrivacyCA(); pca.ekSigningKeyDownloadUsername = getRequiredStringWithPrompt("EK Signing Key Download Username"); pca.ekSigningKeyDownloadPassword = getConfirmedPasswordWithPrompt("EK Signing Key Download Password"); ctx.privacyCA = pca; } public void inputManagementServiceAdminCredentials() throws IOException { System.out.println("You must set a username and password for the first Mt Wilson administrator account."); AdminUser admin = new AdminUser(); admin.username = getRequiredStringWithPrompt("Administrator Username"); admin.password = getConfirmedPasswordWithPrompt("Administrator Password"); ctx.admin = admin; } //private void inputDistinguishedNameForCertificates() { // System.out.println("The X509 Certificates are customized with your organization's details. All of these fields are optional. Press enter without entering anything to leave them blank."); // DistinguishedName dn = new DistinguishedName(); // dn.commonName = getRequiredStringWithPrompt("Common Name (eg. Product Name)"); // dn.organizationUnit = getRequiredStringWithPrompt("Organization Unit (eg. Product Name)"); // dn.organization = getRequiredStringWithPrompt("Organization (eg. Your Company)"); // dn.locality = getRequiredStringWithPrompt("Locality (eg. Your City)"); // dn.state = getRequiredStringWithPrompt("Locality (eg. Your State or Province)"); // dn.country = getRequiredStringWithPrompt("Country (eg. US)"); // // ctx.dn = dn; //} /** * Precondition: ctx.serverAddress must be defined * @throws NoSuchAlgorithmException * @throws CryptographyException * @throws IOException */ private void generateSelfSignedTlsKey() throws NoSuchAlgorithmException, CryptographyException, IOException { System.out.println("Going to generate a TLS/SSL key and certificate"); KeyPair keypair = RsaUtil.generateRsaKeyPair(RsaUtil.MINIMUM_RSA_KEY_SIZE); String alternativeName ; if( ctx.serverAddress.isHostname() ) { alternativeName = "dns:"+ctx.serverAddress.toString(); } else { // assume isIPv4() or isIPv6() alternativeName = "ip:"+ctx.serverAddress.toString(); } X509Certificate certificate = RsaUtil.generateX509Certificate(ctx.serverAddress.toString()+" TLS", alternativeName, keypair, 3650); // valid for 10 years ctx.tlsKeypair = keypair; ctx.tlsCertificate = certificate; } /** * @throws NoSuchAlgorithmException * @throws CryptographyException * @throws IOException T */ private void generateSelfSignedSamlSigningKey() throws NoSuchAlgorithmException, CryptographyException, IOException { System.out.println("Going to generate a SAML key and certificate"); KeyPair keypair = RsaUtil.generateRsaKeyPair(RsaUtil.MINIMUM_RSA_KEY_SIZE); String alternativeName; if( ctx.serverAddress.isHostname() ) { alternativeName = "dns:"+ctx.serverAddress.toString(); } else { // assume isIPv4() or isIPv6() alternativeName = "ip:"+ctx.serverAddress.toString(); } X509Certificate certificate = RsaUtil.generateX509Certificate(ctx.serverAddress.toString()+" SAML", alternativeName, keypair, 3650); // valid for 10 years ctx.samlKeypair = keypair; ctx.samlCertificate = certificate; } /** * Postcondition: a Mt Wilson Root CA Key and Certificate are saved on the * local host protected by a user-selected password, or else an exception is * thrown. * * Check if there is already a root CA key created - if so, attempt to unlock it with user password; if not, create a new one. * * If the user cannot unlock the root CA (forgotten password), give the user * the option to export the locked key to a file and create a new key to put * in the preferences. * * The Mt Wilson Root CA is stored in the user's preferences (not system preferences). * * Because this class is a UI class, an explicit package name is provided for * the preferences. * * Note that max length of a Preferences value is 8192 characters, and * typical size of a keystore with one key is about 2200 characters. * * All CA keys and certificates are stored under com/intel/mtwilson/ca/keystores. * Each one has its own keystore in order to make sure that we don't exceed the 8KB * size limit on preference values. The name of the CA is the preference name, * which would also be part of the filename if it were to be exported to a file. * Inside the CA keystore, all CA's have their keypair in an entry alias "ca" * * @throws NoSuchAlgorithmException * @throws CryptographyException * @throws IOException */ private void generateRootCAKey() throws NoSuchAlgorithmException, CryptographyException, IOException, KeyManagementException, KeyStoreException, CertificateException, BackingStoreException { final String ROOT = "root"; final String CA_ALIAS = "ca"; Preferences prefs = Preferences.userRoot().node(MTWILSON_CA_KEYSTORE_PACKAGE); byte[] keystoreBytes = prefs.getByteArray(ROOT, null); if( keystoreBytes == null ) { System.out.println("Going to generate a Mt Wilson Root CA key and certificate"); String organizationName = getRequiredStringWithPrompt("Your organization name or company"); String organizationUnit = getRequiredStringWithPrompt("Your organizational unit, division, product, or service"); String dn = String.format("CN=Mt Wilson Root CA, O=%s, OU=%s", organizationName, organizationUnit); String keystorePassword = getConfirmedPasswordWithPrompt("New Root CA Password"); KeyPair keypair = RsaUtil.generateRsaKeyPair(RsaUtil.MINIMUM_RSA_KEY_SIZE); X509Builder x509 = X509Builder.factory().selfSigned(dn, keypair).expires(3650, TimeUnit.DAYS).keyUsageCertificateAuthority(); X509Certificate certificate = x509.build(); if( certificate == null ) { System.err.println("Cannot generate certificate:"); for(Fault f : x509.getFaults()) { System.err.println(String.format("- %s%s", f.toString(), f.getCause()==null?"":" ["+f.getCause().toString()+"]")); } return; } // X509Certificate certificate2 = RsaUtil.generateX509Certificate("Mt Wilson Root CA", keypair, 3650); // valid for 10 years RsaCredentialX509 rootCa = new RsaCredentialX509(keypair.getPrivate(), certificate); ctx.rootCa = rootCa; ByteArrayResource mwKeystoreResource = new ByteArrayResource(); SimpleKeystore keystore = new SimpleKeystore(mwKeystoreResource, keystorePassword); keystore.addKeyPairX509(keypair.getPrivate(), certificate, CA_ALIAS, keystorePassword); keystore.save(); // into mwKeystoreResource prefs.putByteArray(ROOT,mwKeystoreResource.toByteArray()); /* System.out.println("the keystore now has aliases: "+StringUtils.join(keystore.aliases(), ", ")); System.out.println("the resource has "+mwKeystoreResource.toByteArray().length+" bytes"); FileOutputStream tmp = new FileOutputStream(new File(System.getProperty("user.home")+File.separator+"tmpkeystore2.jks")); IOUtils.copy(mwKeystoreResource.getInputStream(), tmp); tmp.close(); */ } else { System.out.println("You already have a Mt Wilson Root CA"); ByteArrayResource mwKeystoreResource = new ByteArrayResource(keystoreBytes); /* FileOutputStream tmp = new FileOutputStream(new File(System.getProperty("user.home")+File.separator+"tmpkeystore.jks")); IOUtils.copy(mwKeystoreResource.getInputStream(), tmp); tmp.close(); */ while(true) { try { String keystorePassword = getRequiredPasswordWithPrompt("Existing Root CA Password"); SimpleKeystore keystore = new SimpleKeystore(mwKeystoreResource, keystorePassword); RsaCredentialX509 rootCa = keystore.getRsaCredentialX509(CA_ALIAS, keystorePassword); ctx.rootCa = rootCa; break; } catch(Exception e) { System.err.println("Cannot load Mt Wilson Root CA: "+e.toString()); boolean tryAgain = getRequiredYesNoWithPrompt("Try again?"); if( !tryAgain ) { break; } } } } } }