miércoles, 20 de julio de 2011

Utilizando SSLSockets en un ambiente de prueba

He estado desarrollando una aplicación en Java que utiliza Sockets para la comunicación entre el cliente y el servidor. Inicialmente lo hice con Sockets normales, pero a la hora de pasarlo a SSLSockets me encontré con varios problemitas que complicaron la implementación, después de invesitgar un poco y hacer cambios a prueba y error logré hacerlo funcionar.

Siguiendo esta lista de pasos, podrás hacer funcionar tu aplicación con SSLSockets en un ambiente de pruebas donde quizás no tengas instalado un certificado SSL válido (Este post asume que conoces, al menos de manera general, qué es SSL y cómo funciona):


Servidor

Lo primero que se necesita hacer es crear un certificado SSL para el servidor, el cual será utilizado para establecer la comunicación con el cliente. Para la creación de este certificado se puede utilizar la herramienta keytool que viene en el JDK y en el JRE de Sun Microsystems.

Para crear este certificado corremos la siguiente instrucción:
keytool -genkey -keystore miKeyStore -alias unAlias -keypass clave -storepass claveDeKeyStore
-keystore genera un archivo que es el almacén de claves (KeyStore), en él se guardará el certificado autofirmado y el par de claves pública/privada. Si no especificamos este comando se creará un archivo en el directorio raíz del usuario con el nombre ".keystore".

-alias es el nombre con el que haremos referencia al par de claves creado.

-keypass es el password para acceder a la clave privada

-storepass es la clave para acceder al KeyStore


Para saber que escribir en cada una de los inputs que el programa esperará, o para conocer que otras características puedes utilizar con keytool puedes consultar alguno de estos links: http://www.adictosaltrabajo.com/tutoriales/tutoriales.php?pagina=securitySSLKeytool o http://download.oracle.com/javase/1.3/docs/tooldocs/win32/keytool.html. Una vez creado el archivo deberá ser copiado en el directorio de trabajo donde correrá nuestro Servidor.

Ahora estamos listos para programar nuestro servidor en Java, el cual debiera ser algo similar a esto:

public class Servidor {
    public static void main(String args[]) {
      System.setProperty("javax.net.ssl.keyStore", "miKeyStore");
      System.setProperty("javax.net.ssl.keyStorePassword", "claveDeKeyStore");
      try {
        SSLServerSocketFactory ssf = (SSLServerSocketFactory)SSLServerSocketFactory.getDefault();
        SSLServerSocket sss = (SSLServerSocket)ssf.createServerSocket(9999);

        SSLSocket s = (SSLSocket)sss.accept();
        LineNumberReader in = new LineNumberReader(new InputStreamReader(s.getInputStream()));
        PrintWriter out = new PrintWriter(s.getOutputStream());

        String linea = in.readLine();
        System.out.println(linea);
        out.println("respuesta");
        out.flush();
      } catch (Exception e) {e.printStackTrace();}
    }
}


No es estrictamente necesario incluir los setProperty en el código, pueden agregarse en la línea de comando al momento de correr el programa, de la siguiente manera:

java -Djavax.net.ssl.keyStore=miKeystore -Djavax.net.ssl.keyStorePassword=claveDeKeyStore Server

Aquí puede ser que tengas tu primer problema, y que al momento de correr el servidor obtengas el siguiente error:

java.net.SocketException: java.security.NoSuchAlgorithmException: Error constructing implementation (algorithm: Default, provider: SunJSSE, class: com.sun.net.ssl.internal.ssl.DefaultSSLContextImpl)

Este error se debe a que no pudo obtenerse la clave del certificado y puede ser por varias razones, la más común es que no estés enviando la clave correcta o que no se encuentre el archivo especificado; pero si se tiene seguridad que no es debido a estos problemas, el error puede deberse a que indicaste una clave distinta en el keypass y el storepass. Como estamos en un ambiente de prueba podemos establecer ambos passwords con el mismo valor y de esta manera el error desaparecerá. Para implementarlo en un ambiente de producción, deberás programas un KeyManager ya que JSSE no provee una manera adecuada de especificar estos passwords a través de System Properties.


Cliente

El código para el cliente podría ser de la siguiente forma:

public class Cliente {

    public static void main(String args[]) {
      try {
        SSLSocketFactory sslsocketfactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
        SSLSocket sslsocket = (SSLSocket) sslsocketfactory.createSocket("localhost", 9999);

        LineNumberReader in;
        PrintWriter out;
        in = new LineNumberReader(new InputStreamReader(s.getInputStream()));
        out = new PrintWriter(s.getOutputStream());

        out.println("hola");
        out.flush();
        System.out.println(in.readLine());

      } catch (Exception exception) { exception.printStackTrace(); }
    }
}

Pero muy probablemente obtendremos el siguiente error:

javax.net.ssl.SSLPeerUnverifiedException: peer not authenticated

Este error se debe a que el certificado que estamos utilizando no es un certificado SSL válido. El esquema utilizado por SSL es conocido como X.509 y Java utiliza un TrustManager para este esquema, llamado x509TrustManager. Para poder evitar este error tenemos que crear un TrustManager diferente que acepte todos los certificados y no nos genere un error y utilizar este para la creación del Socket. Nuestro código quedaría algo así:

public class Cliente {

    public static void main(String args[]) {
      try {
        X509TrustManager tm = new X509TrustManager() {  
          public void checkClientTrusted(X509Certificate[] xcs, String string) throws CertificateException { }
          public void checkServerTrusted(X509Certificate[] xcs, String string) throws CertificateException { }
          public X509Certificate[] getAcceptedIssuers() { return null; }
        };            

      SSLContext ctx = SSLContext.getInstance("TLS");
      ctx.init(null, new TrustManager[]{tm}, null);

      SSLSocketFactory sslsocketfactory = ctx.getSocketFactory();
      SSLSocket s = (SSLSocket) sslsocketfactory.createSocket("192.168.0.32", 9999);

      LineNumberReader in;
      PrintWriter out;
      in = new LineNumberReader(new InputStreamReader(s.getInputStream()));
      out = new PrintWriter(s.getOutputStream());

      out.println("hola");
      out.flush();
      System.out.println(in.readLine());

      } catch (Exception exception) {
      exception.printStackTrace();
      }
    }
}

Una vez hecho esto, nuestros dos procesos se comunicarán utilizando SSL sin problemas :).

lunes, 18 de julio de 2011

No se puede iniciar sesión localmente en un Windows SBS 2003

Al intentar entrar a un servidor Windows SBS 2003 de manera local con el usuario administrador me aparecía el mensaje "Las directivas locales de este sistema no le permiten iniciar una sesión interactiva", pero si podía ingresar al sistema desde una conexión remota. Después de revisar la ayuda en el sitio de Microsoft (http://support.microsoft.com/kb/841188/es) encontré que si el administrador pertenece al grupo Operadores Remotos (Remote Operators) no podrá ingresar de manera local, ya que por default en Windows SBS 2003 este grupo tiene denegado el acceso local al sistema.

Noté que este era mi caso y procedí a eliminar el usuario administrador del grupo Operadores Remotos. Excelente, ya podía ingresar al servidor de manera local, pero ya no podía hacerlo de manera remota.

¿Cómo hacer que el administrador pueda ingresar de manera remota y local al servidor? Esto no debería ser un problema, pero no sé por qué razón el administrador estaba en Operadores Remotos ni porque no me le daba acceso normal al quitarlo de ahi, pero la solución es sencilla:
  • Abrir el editor de objetos de directivas de grupo escribiendo en la línea de comandos gpedit.msc
  • Irse a Configuración del equipo -> Configuración de Windows -> Configuración de seguridad -> Directivas locales -> Asignación de derechos de usuario.
  • Encontrarás ahi la directiva "Permitir el inicio de sesión a través de Servicios de Terminal Server", darle doble click
  • Presionar el botón Agregar un usuario o grupo
  • Escribir el usuario o grupo que deseas agregar, asegúrate de incluir el nombre de dominio, por ejemplo: MiDominio\administrador

Con esto el administrador ya podrá ingresar de manera local y remota.