1 /*
   2  * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 import java.io.BufferedInputStream;
  25 import java.io.BufferedOutputStream;
  26 import java.io.File;
  27 import java.io.IOException;
  28 import java.security.NoSuchAlgorithmException;
  29 import java.security.PrivilegedActionException;
  30 import java.security.PrivilegedExceptionAction;
  31 import java.util.ArrayList;
  32 import java.util.Arrays;
  33 import java.util.List;
  34 import java.util.Map;
  35 import javax.net.ssl.SNIHostName;
  36 import javax.net.ssl.SNIMatcher;
  37 import javax.net.ssl.SNIServerName;
  38 import javax.net.ssl.SSLContext;
  39 import javax.net.ssl.SSLParameters;
  40 import javax.net.ssl.SSLServerSocket;
  41 import javax.net.ssl.SSLServerSocketFactory;
  42 import javax.net.ssl.SSLSocket;
  43 import javax.net.ssl.SSLSocketFactory;
  44 import javax.security.auth.Subject;
  45 import javax.security.auth.login.LoginContext;
  46 import javax.security.auth.login.LoginException;
  47 
  48 /*
  49  * Helper class for unbound krb5 tests.
  50  */
  51 class UnboundSSLUtils {
  52 
  53     static enum KtabMode { APPEND, EXISTING };
  54 
  55     static final String KTAB_FILENAME = "krb5.keytab.data";
  56     static final String HOST = "localhost";
  57     static final String REALM = "TEST.REALM";
  58     static final String KRBTGT_PRINCIPAL = "krbtgt/" + REALM;
  59     static final String TEST_SRC = System.getProperty("test.src", ".");
  60     static final String TLS_KRB5_FILTER = "TLS_KRB5";
  61     static final String USER = "USER";
  62     static final String USER_PASSWORD = "password";
  63     static final String FS = System.getProperty("file.separator");
  64     static final String SNI_PATTERN = ".*";
  65     static final String USER_PRINCIPAL = USER + "@" + REALM;
  66     static final String KRB5_CONF_FILENAME = "krb5.conf";
  67     static final int DELAY = 1000;
  68 
  69    static String[] filterStringArray(String[] src, String filter) {
  70         return Arrays.stream(src).filter((item) -> item.startsWith(filter))
  71                 .toArray(size -> new String[size]);
  72     }
  73 
  74     /*
  75      * The method does JAAS login,
  76      * and runs an SSL server in the JAAS context.
  77      */
  78     static void startServerWithJaas(final SSLEchoServer server,
  79             String config) throws LoginException, PrivilegedActionException {
  80         LoginContext context = new LoginContext(config);
  81         context.login();
  82         System.out.println("Server: successful authentication");
  83         Subject.doAs(context.getSubject(),
  84                 (PrivilegedExceptionAction<Object>) () -> {
  85             SSLEchoServer.startServer(server);
  86             return null;
  87         });
  88     }
  89 
  90     /*
  91      * Start a KDC server:
  92      *   - create a KDC instance
  93      *   - create Kerberos principals
  94      *   - save Kerberos configuration
  95      *   - save keys to keytab file
  96      *   - no pre-auth required
  97      */
  98     static void startKDC(String realm, Map<String, String> principals,
  99             String ktab, KtabMode mode) {
 100         try {
 101             KDC kdc = KDC.create(realm, HOST, 0, true);
 102             kdc.setOption(KDC.Option.PREAUTH_REQUIRED, Boolean.FALSE);
 103             if (principals != null) {
 104                 for (Map.Entry<String, String> entry : principals.entrySet()) {
 105                     String name = entry.getKey();
 106                     String password = entry.getValue();
 107                     if (password == null || password.isEmpty()) {
 108                         System.out.println("KDC: add a principal '" + name +
 109                                 "' with a random password");
 110                         kdc.addPrincipalRandKey(name);
 111                     } else {
 112                         System.out.println("KDC: add a principal '" + name +
 113                                 "' with '" + password + "' password");
 114                         kdc.addPrincipal(name, password.toCharArray());
 115                     }
 116                 }
 117             }
 118 
 119             // wait for the KDC is ready
 120             while (!kdc.isReady()) {
 121                 Thread.sleep(DELAY);
 122             }
 123 
 124             KDC.saveConfig(KRB5_CONF_FILENAME, kdc);
 125 
 126             if (ktab != null) {
 127                 File ktabFile = new File(ktab);
 128                 if (mode == KtabMode.APPEND) {
 129                     if (ktabFile.exists()) {
 130                         System.out.println("KDC: append keys to an exising " +
 131                                 "keytab file " + ktab);
 132                         kdc.appendKtab(ktab);
 133                     } else {
 134                         System.out.println("KDC: create a new keytab file " +
 135                                 ktab);
 136                         kdc.writeKtab(ktab);
 137                     }
 138                 } else if (mode == KtabMode.EXISTING) {
 139                     System.out.println("KDC: use an existing keytab file "
 140                             + ktab);
 141                 } else {
 142                     throw new RuntimeException("KDC: unsupported keytab mode: "
 143                             + mode);
 144                 }
 145             }
 146 
 147             System.out.println("KDC: started on " + HOST + ":" + kdc.getPort()
 148                     + " with '" + realm + "' realm");
 149         } catch (Exception e) {
 150             throw new RuntimeException("KDC: unexpected exception", e);
 151         }
 152     }
 153 
 154 }
 155 
 156 class SSLClient {
 157 
 158     private final static byte[][] arrays = {
 159         new byte[] {-1, 0, 2},
 160         new byte[] {}
 161     };
 162 
 163     private final SSLSocket socket;
 164 
 165     private SSLClient(SSLSocket socket) {
 166         this.socket = socket;
 167     }
 168 
 169     void connect() throws IOException {
 170         System.out.println("Client: connect to server");
 171         try (BufferedInputStream bis = new BufferedInputStream(
 172                         socket.getInputStream());
 173                 BufferedOutputStream bos = new BufferedOutputStream(
 174                         socket.getOutputStream())) {
 175 
 176             for (byte[] bytes : arrays) {
 177                 System.out.println("Client: send byte array: "
 178                         + Arrays.toString(bytes));
 179 
 180                 bos.write(bytes);
 181                 bos.flush();
 182 
 183                 byte[] recieved = new byte[bytes.length];
 184                 int read = bis.read(recieved, 0, bytes.length);
 185                 if (read < 0) {
 186                     throw new IOException("Client: couldn't read a response");
 187                 }
 188 
 189                 System.out.println("Client: recieved byte array: "
 190                         + Arrays.toString(recieved));
 191 
 192                 if (!Arrays.equals(bytes, recieved)) {
 193                     throw new IOException("Client: sent byte array "
 194                                 + "is not equal with recieved byte array");
 195                 }
 196             }
 197             socket.getSession().invalidate();
 198         } finally {
 199             if (!socket.isClosed()) {
 200                 socket.close();
 201             }
 202         }
 203     }
 204 
 205     static SSLClient init(String host, int port, String cipherSuiteFilter,
 206             String sniHostName) throws NoSuchAlgorithmException, IOException {
 207         SSLContext sslContext = SSLContext.getDefault();
 208         SSLSocketFactory ssf = (SSLSocketFactory) sslContext.getSocketFactory();
 209         SSLSocket socket = (SSLSocket) ssf.createSocket(host, port);
 210         SSLParameters params = new SSLParameters();
 211 
 212         if (cipherSuiteFilter != null) {
 213             String[] cipherSuites = UnboundSSLUtils.filterStringArray(
 214                     ssf.getSupportedCipherSuites(), cipherSuiteFilter);
 215             System.out.println("Client: enabled cipher suites: "
 216                     + Arrays.toString(cipherSuites));
 217             params.setCipherSuites(cipherSuites);
 218         }
 219 
 220         if (sniHostName != null) {
 221             System.out.println("Client: set SNI hostname: " + sniHostName);
 222             SNIHostName serverName = new SNIHostName(sniHostName);
 223             List<SNIServerName> serverNames = new ArrayList<>();
 224             serverNames.add(serverName);
 225             params.setServerNames(serverNames);
 226         }
 227 
 228         socket.setSSLParameters(params);
 229 
 230         return new SSLClient(socket);
 231     }
 232 
 233 }
 234 
 235 class SSLEchoServer implements Runnable, AutoCloseable {
 236 
 237     private final SSLServerSocket ssocket;
 238     private volatile boolean stopped = false;
 239     private volatile boolean ready = false;
 240 
 241     /*
 242      * Starts the server in a separate thread.
 243      */
 244     static void startServer(SSLEchoServer server) {
 245         Thread serverThread = new Thread(server, "SSL echo server thread");
 246         serverThread.setDaemon(true);
 247         serverThread.start();
 248     }
 249 
 250     private SSLEchoServer(SSLServerSocket ssocket) {
 251         this.ssocket = ssocket;
 252     }
 253 
 254     /*
 255      * Main server loop.
 256      */
 257     @Override
 258     public void run() {
 259         System.out.println("Server: started");
 260         while (!stopped) {
 261             ready = true;
 262             try (SSLSocket socket = (SSLSocket) ssocket.accept()) {
 263                 System.out.println("Server: client connection accepted");
 264                 try (
 265                     BufferedInputStream bis = new BufferedInputStream(
 266                             socket.getInputStream());
 267                     BufferedOutputStream bos = new BufferedOutputStream(
 268                             socket.getOutputStream())
 269                 ) {
 270                     byte[] buffer = new byte[1024];
 271                     int read;
 272                     while ((read = bis.read(buffer)) > 0) {
 273                         bos.write(buffer, 0, read);
 274                         System.out.println("Server: recieved " + read
 275                                 + " bytes: "
 276                                 + Arrays.toString(Arrays.copyOf(buffer, read)));
 277                         bos.flush();
 278                     }
 279                 }
 280             } catch (IOException e) {
 281                 if (stopped) {
 282                     // stopped == true means that stop() method was called,
 283                     // so just ignore the exception, and finish the loop
 284                     break;
 285                 }
 286                 System.out.println("Server: couldn't accept client connection: "
 287                         + e);
 288             }
 289         }
 290         System.out.println("Server: finished");
 291     }
 292 
 293     boolean isReady() {
 294         return ready;
 295     }
 296 
 297     void stop() {
 298         stopped = true;
 299         ready = false;
 300 
 301         // close the server socket to interupt accept() method
 302         try {
 303             if (!ssocket.isClosed()) {
 304                 ssocket.close();
 305             }
 306         } catch (IOException e) {
 307             throw new RuntimeException("Unexpected exception: " + e);
 308         }
 309     }
 310 
 311     @Override
 312     public void close() {
 313         stop();
 314     }
 315 
 316     int getPort() {
 317         return ssocket.getLocalPort();
 318     }
 319 
 320     /*
 321      * Creates server instance.
 322      *
 323      * @param cipherSuiteFilter Filter for enabled cipher suites
 324      * @param sniMatcherPattern Pattern for SNI server hame
 325      */
 326     static SSLEchoServer init(String cipherSuiteFilter,
 327             String sniPattern) throws NoSuchAlgorithmException, IOException {
 328         SSLContext context = SSLContext.getDefault();
 329         SSLServerSocketFactory ssf =
 330                 (SSLServerSocketFactory) context.getServerSocketFactory();
 331         SSLServerSocket ssocket =
 332                 (SSLServerSocket) ssf.createServerSocket(0);
 333 
 334         // specify enabled cipher suites
 335         if (cipherSuiteFilter != null) {
 336             String[] ciphersuites = UnboundSSLUtils.filterStringArray(
 337                     ssf.getSupportedCipherSuites(), cipherSuiteFilter);
 338             System.out.println("Server: enabled cipher suites: "
 339                     + Arrays.toString(ciphersuites));
 340             ssocket.setEnabledCipherSuites(ciphersuites);
 341         }
 342 
 343         // specify SNI matcher pattern
 344         if (sniPattern != null) {
 345             System.out.println("Server: set SNI matcher: " + sniPattern);
 346             SNIMatcher matcher = SNIHostName.createSNIMatcher(sniPattern);
 347             List<SNIMatcher> matchers = new ArrayList<>();
 348             matchers.add(matcher);
 349             SSLParameters params = ssocket.getSSLParameters();
 350             params.setSNIMatchers(matchers);
 351             ssocket.setSSLParameters(params);
 352         }
 353 
 354         return new SSLEchoServer(ssocket);
 355     }
 356 
 357 }
 358