Diferencia entre revisiones de «Técnicas de programación segura con Java»

De MediaWiki
Ir a la navegación Ir a la búsqueda
 
(No se muestran 3 ediciones intermedias del mismo usuario)
Línea 14: Línea 14:
  
 
= Criptografía con JCA =
 
= Criptografía con JCA =
−
JCA (''Java Chriptography Architecture'') é unha arquitectura baseada en proveedores que proporciona un conxunto de API para todo o relacionado con criptografía de clave privada e pública:
+
'''JCA''' (''Java Chriptography Architecture'') é unha '''arquitectura baseada en proveedores que proporciona un conxunto de API para todo o relacionado con criptografía de clave privada e pública''':
 
* Firma dixital
 
* Firma dixital
 
*  Hash ou digest de mensaxes
 
*  Hash ou digest de mensaxes
Línea 22: Línea 22:
 
*  Xeneración de números aleatorios
 
*  Xeneración de números aleatorios
  
−
JCA é unha arquitectura estándar de Java, parte do JDK, que proporciona un marco ou ''framework'' no que se integran servizos criptográficos implementados por diferentes proveedores de servizos criptográfifios (ou CSP, ''cryptographic service provider'').  
+
JCA é unha '''arquitectura estándar de Java''', parte do JDK, que proporciona un marco ou ''framework'' no que se integran '''servizos criptográficos implementados por diferentes proveedores de servizos criptográficos''' (ou CSP, ''cryptographic service provider'').  
  
 
Inclúe paquetes coma:
 
Inclúe paquetes coma:
Línea 32: Línea 32:
 
</div>
 
</div>
  
−
Os principios de deseño desta arquitectura son:
+
Os '''principios de deseño''' desta arquitectura son:
−
* Independencia de implementación. Os servizos de seguridade son proporcionados por proveedores enlazados coa plataforma Java mediante interfaces estándares. Unha aplicación pode utilizar servizos proporcionados por múltiples proveedores independentes.
+
* '''Independencia de implementación'''. Os servizos de seguridade son proporcionados por proveedores enlazados coa plataforma Java mediante interfaces estándares. Unha aplicación pode utilizar servizos proporcionados por múltiples proveedores independentes.
−
* Interoperabilidade. Os servicios proporcionados por distintos proveedores son compatibles entre eles. Existen clases que proporcionan un tipo determinado de servizo, e a diferentes algoritmos para este tipo de servizo. Estas se denominan ''engine classes'' e son as seguintes:
+
* '''Interoperabilidade'''. Os servizos proporcionados por distintos proveedores son compatibles entre eles. Existen clases que proporcionan un tipo determinado de servizo, e a diferentes algoritmos para este tipo de servizo. Estas se denominan ''engine classes'' e son as seguintes:
  
 
{| class="wikitable"
 
{| class="wikitable"
Línea 66: Línea 66:
 
* certificados dixitais
 
* certificados dixitais
  
−
== Resúmenes de mensajes (funcións de hash ou digest) ==
+
== Resúmenes de mensaxes (funcións de hash ou digest) ==
−
JCA conten implementacións para os principales algoritmos de hash ou digest. Estos están disponibles mediante a ''engine classes''  MessageDigest.
+
JCA conten implementacións para os principales algoritmos de '''hash''' ou '''digest'''. Estos están disponibles mediante a ''engine classes''  MessageDigest.
  
 
Existen distintos algoritmos de hash: MD5, SHA-1, SHA2 y SHA-3.  
 
Existen distintos algoritmos de hash: MD5, SHA-1, SHA2 y SHA-3.  
Línea 74: Línea 74:
 
Para xenerar un hash con JCA procedese así:
 
Para xenerar un hash con JCA procedese así:
  
−
1. Créase un obxecto de clase MessageDigest co método estático getInstance() da mesma clase, especificando o nome do algoritmo. Opcionalmente pódese especificar o nome do proveedor.
+
1. Créase un obxecto de clase '''MessageDigest''' co método estático getInstance() da mesma clase, especificando o nome do algoritmo. Opcionalmente pódese especificar o nome do proveedor.
  
 
2. Engádense datos co método update(). Pódese engadir un byte ou un array de tipo byte[]. Esto se pode facer repetidamente para ir engadindo novos datos.
 
2. Engádense datos co método update(). Pódese engadir un byte ou un array de tipo byte[]. Esto se pode facer repetidamente para ir engadindo novos datos.
Línea 92: Línea 92:
 
   public static void main(String[] args) {
 
   public static void main(String[] args) {
 
     if (args.length < 1) {
 
     if (args.length < 1) {
−
       System.out.println(
+
       System.out.println("Introducir cadea de caracteres para calcular hash");
−
        “Introducir cadea de caracteres para calcular hash”);
 
 
       return;
 
       return;
 
     }
 
     }
Línea 99: Línea 98:
 
     String cadena = args[0];
 
     String cadena = args[0];
 
     for(int i=1; i<args.length; i++) {
 
     for(int i=1; i<args.length; i++) {
−
       cadena += “ “+args[i];
+
       cadena += " "+args[i];
 
     }
 
     }
 
     MessageDigest md;
 
     MessageDigest md;
 
     try {
 
     try {
 
       byte[] bytes = cadena.getBytes();
 
       byte[] bytes = cadena.getBytes();
−
       md = MessageDigest.getInstance(“SHA-256”);
+
       md = MessageDigest.getInstance("SHA-256");
 
       md.update(bytes);
 
       md.update(bytes);
 
       byte[] hash = md.digest();
 
       byte[] hash = md.digest();
−
       System.out.printf(“Cadea: [%s]\nHash: [%s]\n.”,
+
       System.out.printf("Cadea: [%s]\nHash: [%s]\n.",
 
         cadena, valorHexadecimal(hash));
 
         cadena, valorHexadecimal(hash));
 
     } catch (NoSuchAlgorithmException e) {
 
     } catch (NoSuchAlgorithmException e) {
−
       System.out.println(“Non disponible algoritmo de hash”);
+
       System.out.println("Non disponible algoritmo de hash");
 
     }
 
     }
 
   }
 
   }
 
   static String valorHexadecimal(byte[] bytes) {
 
   static String valorHexadecimal(byte[] bytes) {
−
     String result = “”;
+
     String result = "";
 
     for (byte b : bytes) {
 
     for (byte b : bytes) {
−
       result += String.format(String.format(“%x”, b));
+
       result += String.format(String.format("%x", b));
 
     }
 
     }
 
     return result;
 
     return result;
 
   }
 
   }
 
}
 
}
−
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 
Pódese comparar o resultado co obtido con comandos en linux.
 
Pódese comparar o resultado co obtido con comandos en linux.
  
−
Por exemplo o xeito de obter en Linux o hash SHA-2 de 256 bits para a cadena “Probando cadea” é  
+
Por exemplo o xeito de obter en Linux o hash SHA-2 de 256 bits para a cadena "Probando cadea" é:
−
  $ echo -n “Probando cadea”
+
  $ echo -n "Probando cadea" | sha256sum
−
  sha256sum da73486a45ca51ab22c9046b908a8150d21988d6b346f6c67fcd2fb4bc62e8ea
+
  da73486a45ca51ab22c9046b908a8150d21988d6b346f6c67fcd2fb4bc62e8ea
  
 
  '''Nota.'''
 
  '''Nota.'''

Revisión actual del 17:52 16 dic 2021

Introdución

Java inclúe un conxunto de API que permite garantizar a seguridade de comunicacións, mediante o uso de criptografía e o control de acceso.

A nivel pedagóxico, estes son os resultados de aprendizaxe e criterios de avaliación:

  • RA3. Protexe as aplicacións e os datos, para o que define e aplica criterios de seguridade no acceso, no almacenamento e na transmisión da información.
    CA3.1. Identificáronse e aplicáronse principios e prácticas de programación segura.
    CA3.2. Analizáronse as principais técnicas e prácticas criptográficas.
    CA3.3. Definíronse e implantáronse políticas de seguridade para limitar e controlar o acceso dos usuarios ás aplicacións desenvolvidas.
    CA3.4. Utilizáronse esquemas de seguridade baseados en papeis.
    CA3.5. Empregáronse algoritmos criptográficos para protexer o acceso á información almacenada.
    CA3.6. Identificáronse métodos para asegurar a información transmitida.
    CA3.7. Desenvolvéronse aplicacións que utilicen sockets seguros para a transmisión de información.
    CA3.8. Depuráronse e documentáronse as aplicacións desenvolvidas.

Criptografía con JCA

JCA (Java Chriptography Architecture) é unha arquitectura baseada en proveedores que proporciona un conxunto de API para todo o relacionado con criptografía de clave privada e pública:

  • Firma dixital
  • Hash ou digest de mensaxes
  • Certificados dixitais
  • Encriptación simétrica e asimétrica
  • Xeneración e gestión de claves
  • Xeneración de números aleatorios

JCA é unha arquitectura estándar de Java, parte do JDK, que proporciona un marco ou framework no que se integran servizos criptográficos implementados por diferentes proveedores de servizos criptográficos (ou CSP, cryptographic service provider).

Inclúe paquetes coma:

  • java.security
  • javax.crypto
  • javax.crypto.spec
  • javax.crypto.interfaces

Os principios de deseño desta arquitectura son:

  • Independencia de implementación. Os servizos de seguridade son proporcionados por proveedores enlazados coa plataforma Java mediante interfaces estándares. Unha aplicación pode utilizar servizos proporcionados por múltiples proveedores independentes.
  • Interoperabilidade. Os servizos proporcionados por distintos proveedores son compatibles entre eles. Existen clases que proporcionan un tipo determinado de servizo, e a diferentes algoritmos para este tipo de servizo. Estas se denominan engine classes e son as seguintes:
Clase Descripción
MessageDigest Para funcións hash ou digest
KeyGenerator Para xeneración e xestión de claves para criptografía simétrica
KeyPairGenerator Para xeneración e xestión de pares de claves para criptografía asimétrica ou de clave pública
Cipher Para algoritmos de cifrado de datos
Signature Para algoritmos de sinatura dixital
TAREFA. Obtén os proveedores disponibles no JDK que estés utilizando co método getProviders() da clase Security. Compara cos que aparecen no ficheiro de configuración do JDK, no ficheiro de configuración do JDK conf/security/java.security. Para cada un amosa o seu nome e outra información relevante.
Obtén despois todos os servizos proporcionados polo proveedor. Amosa para cada un o seu tipo e o nome do algoritmo. Presta atención os distintos tipos de servizos. Identifica os algoritmos que recoñezas, e localiza algún algoritmo proporcionado por máis dun proveedor.

Explican as capacidades de JCA nos siguientes apartados:

  • funcións de hash ou digest
  • criptografía de clave simétrica ou privada (comenzando pola xeneración de claves)
  • criptografía de clave asimétrica ou de clave pública (comezando pola xeneración de pares de claves)
  • certificados dixitais

Resúmenes de mensaxes (funcións de hash ou digest)

JCA conten implementacións para os principales algoritmos de hash ou digest. Estos están disponibles mediante a engine classes MessageDigest.

Existen distintos algoritmos de hash: MD5, SHA-1, SHA2 y SHA-3.

NOTA. MD5 e SHA-1 non se consideran seguros na actualidade para criptografía.

Para xenerar un hash con JCA procedese así:

1. Créase un obxecto de clase MessageDigest co método estático getInstance() da mesma clase, especificando o nome do algoritmo. Opcionalmente pódese especificar o nome do proveedor.

2. Engádense datos co método update(). Pódese engadir un byte ou un array de tipo byte[]. Esto se pode facer repetidamente para ir engadindo novos datos.

3. Obtense o valor de hash co método digest().

4. Se quixeramos calcular un novo hash invocaríase ó método reset().

Exemplo

O seguinte programa de exemplo realiza o hash SHA-2 de 256 bits dunha cadea de caracteres introducida como parámetros de líña de comando.

 1 package generacionhash;
 2 import java.security.MessageDigest;
 3 import java.security.NoSuchAlgorithmException;
 4 public class GeneracionHash {
 5   public static void main(String[] args) {
 6     if (args.length < 1) {
 7       System.out.println("Introducir cadea de caracteres para calcular hash");
 8       return;
 9     }
10     
11     String cadena = args[0];
12     for(int i=1; i<args.length; i++) {
13       cadena += " "+args[i];
14     }
15     MessageDigest md;
16     try {
17       byte[] bytes = cadena.getBytes();
18       md = MessageDigest.getInstance("SHA-256");
19       md.update(bytes);
20       byte[] hash = md.digest();
21       System.out.printf("Cadea: [%s]\nHash: [%s]\n.",
22         cadena, valorHexadecimal(hash));
23     } catch (NoSuchAlgorithmException e) {
24       System.out.println("Non disponible algoritmo de hash");
25     }
26   }
27   static String valorHexadecimal(byte[] bytes) {
28     String result = "";
29     for (byte b : bytes) {
30       result += String.format(String.format("%x", b));
31     }
32     return result;
33   }
34 }

Pódese comparar o resultado co obtido con comandos en linux.

Por exemplo o xeito de obter en Linux o hash SHA-2 de 256 bits para a cadena "Probando cadea" é:

$ echo -n "Probando cadea" | sha256sum 
da73486a45ca51ab22c9046b908a8150d21988d6b346f6c67fcd2fb4bc62e8ea
Nota.
A opción -n é importante para eliminar o retorno de carro
Tarefa.
Probar este programa para a mesma cadea de caracteres e cotexa a súa saída coa de un dos comandos de Linux: md5sum, sha1sum, sha224sum, sha256sum, etc.

Xeneración e xestión de claves

Desde unha perspectiva práctica das aplicacións en Java que deben utilizar as claves, caben dúas consideracións:

1. O valor en sí das claves.

Se se trata de criptografía de clave privada, hai unha soa clave e se pode elixir libremente dentro do posible conxunto de valores posibles, delimitado fundamentalmente pola lonxitude da clave, que é distinta para cada algoritmo de cifrado de clave simétrica.

Se se trata de clave pública, hai un par de claves relacionadas e a súa xeneración é máis complexa, debido á relación matemática que existe entre elas. As claves para algoritmos de cifrado adoitan xenerarse mediante métodos específicos de determinadas clases que implementan algoritmos de xeneración de claves.

2. O seu almacenamento no sistema.

As claves almacenaranse en ficheiros. Unha colección de claves pódese almacenar en repositorios especiales denominados keyrings (chaveros).

Criptografía de clave privada

Criptografía de clave pública

Xeneración e uso de certificados dixitais

Un certificado dixital conten (entre outros) unha clave pública para un algoritmo de criptografía de clave pública.

Un certificado digital (para ser considerado fiable) debe ser emitido por una AC (e por tanto estar asinado dixitalmente por esa AC). Isto implica un proceso administrativo. Nembargantes, para o desenvolvemento dunha aplicación e antes da súa posta en producción normalmente bastará con dispor de certificados xenerados polo mesmo equipo de desenvolvemento e autoasinados. É dicir, asinados polo mesmo emisor, coa clave privada correspondente á clave pública que conten.

Podemos xenerar certificados dixitais autoasinados con ferramentas coma:

  • openssl
  • keygen (incluida na JDK de Java)

Xeneración dun certificado digital autoasinado

É posible xenerar un certificado dixital autoasindado co comando:

openssl req

Este xenera unha solicitude (request) de firma de certificado, ou ben un certificado autoasinado. Un exemplo de xeneración dun certificado X.509 cunha clave pública para RSA, xunto cun ficheiro coa correspondente clave privada:

openssl req -x509 -utf8 -nodes -days 365 -newkey rsa:1024 -keyout claveprivada.pem -out certificadoX509.crt

A explicación dos parámetros utilizados;

Clase Descripción
-x509 Xenerar certificado conforme ó estándar X.509 autasinado, en lugar dunha solicitude
-utf8 Utilizar a codificación UTF-8 para todos os textos proporcionados para os diversos datos
-nodes Non encriptar a clave privada xenerada
-days Duración do periodo de validez do certificado en días desde o momento da súa creación
-newkey Crear nova clave privada. rsa:2048 indica que é unha clave de 2048 bits para o algoritmo RSA.
Pódense crear pares de claves para outros algoritmos (coma DSA ou de curvas elípticas)
-keyout Indica o nome de ficheuro para aa clave privada xenerada.
-out Indica o nome de ficheiro para o certificado xenerado.

O programa pregunta unha serie de datos necesarios para identificar ó titular do certificado. A clave privada xenérase nun ficheiro con formato PEM.

Pódese pasar ó formato PKCS#8 co siguiente comando:

openssl pkcs8 -topk8 -inform PEM -in claveprivada.pem -outform DER -out claveprivada.pkcs8 -nocrypt

Podense ver os datos do ficheiro de certificado co seguiente comando:

openssl x509 -nameopt utf8 -in certificadoX509.crt -text -noout

Amosaría a seguiente información para o certificado antes xenerado:

  • os datos de identificación son os indicados ó programa para xenerar o certificado
  • é autoasinado porque como emisor (issuer) aparece o mismo que para titular (subject)

Por exemplo (acortado, podes ver exemplo completo eiquí):

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            3e:f8:b7:03:2b:25:4a:ae:df:3f:4f:1e:2c:03:96:97:5f:3c:11:08
        Signature Algorithm: sha256WithRSAEncryption
         Issuer: C=ES, ST=A Coruña, L=Ferrol, O=Ucha, OU=DAM PSP, CN=www.psp_dam.org, emailAddress=pepe.lopez@psp_dam.org
        Validity
            Not Before: Jul 27 03:02:24 2020 GMT
        [...]

Podense ver os datos da clave privada con:

openssl pkcs8 -inform DER -nocrypt -in claveprivada.pkcs8

Obtención e uso da clave publica dun certificado

O uso dun certificado dixital en troques dun ficheiro de clave pública supón unha mellora importante para a seguridade nas comunicacións, porque proporciona unha identificación fiable do interlocutor respaldada por unha AC.

O seguinte programa obten a clave pública contida no certificado:

  • se é autoasinmado, verifica a súa firma dixital co método verify
  • se non é autoasinado, non pode verificar a súa firma digital. Sería preciso construír unha cadea de confianza ata un certificado raíz (superando o propósito pedagóxico deste exemplo)

En todo caso verifícase se o certifícado é válido (a data actual está dentro do periodo de validez do certificado) co método checkValidity.

A principal complicación deste programa é a gran cantidade de excepcións de todo tipo que poden lanzar os métodos verify e checkValidity.

A verificación do certificado encapsúlase nun método certificadoCorrecto (simplificando o programa principal).

 1 package consultacertificado;
 2 import java.io.FileInputStream;
 3 import java.io.FileNotFoundException;
 4 import java.io.IOException;
 5 import java.security.PublicKey;
 6 import java.security.InvalidKeyException;
 7 import java.security.NoSuchAlgorithmException;
 8 import java.security.NoSuchProviderException;
 9 import java.security.SignatureException;
10 import java.security.cert.CertificateFactory;
11 import java.security.cert.Certificate;
12 import java.security.cert.X509Certificate;
13 import java.security.cert.CertificateException;
14 import java.security.cert.CertificateExpiredException;
15 import java.security.cert.CertificateNotYetValidException;
16 public class ConsultaCertificado {
17   private static final String FICH_CERT = “certificadoX509.crt”;
18   private static boolean certificadoCorrecto(Certificate certif) {
19     boolean certifOK = true;
20     try {
21       PublicKey clavePublica = certif.getPublicKey();
22       certif.verify(clavePublica);
23       System.out.println(“Certificado autoasinado, e firma correcta.”);
24       if (certif.getType().equals(“X.509”)) {
25         X509Certificate certX509 = (X509Certificate) certif;
26         certX509.checkValidity();
27       }
28     } catch (NoSuchProviderException | SignatureException e) {
29       certifOK = false;
30       System.out.println(“ERROR: Excepción de certificado.”);
31       e.printStackTrace();
32     } catch (NoSuchAlgorithmException e) {
33       certifOK = false;
34       System.out.println(“ERROR: Excepción con certificado”);
35       e.printStackTrace();
36     } catch (CertificateExpiredException e) {
37       certifOK = false;
38       System.out.println(“ERROR: Certificado caducado.”);
39       e.printStackTrace();
40     } catch (CertificateNotYetValidException e) {
41       certifOK = false;
42       System.out.println(“ERROR: Certificado aínda non válido.”);
43       e.printStackTrace();
44     } catch (InvalidKeyException e) {
45       certifOK = false;
46       System.out.println(“AVISO: Certificado autoasinado e firma non correcta, ou no autoasinado (e sen verificar firma)”);
47       certifOK = false;
48     } catch (CertificateException e) {
49       certifOK = false;
50       System.out.println(“ERRO: Excepción de certificado.”);
51     }    
52     return certifOK;
53   }
54   public static void main(String[] args) {
55     try (FileInputStream fisCertificado = new FileInputStream(FICH_CERT)) {
56       CertificateFactory f = CertificateFactory.getInstance(“X.509”);
57       Certificate certif = f.generateCertificate(fisCertificado);
58       if (!certificadoCorrecto(certif)) {
59         System.out.printf(“Certificado %s non correcto.\n”, FICH_CERT);
60         System.exit(1);
61       }
62       PublicKey clavePublica = certif.getPublicKey();
63       System.out.printf(“Clave pública leída de certificado, formato: %s.\n”,
64         clavePublica.getFormat());
65     } catch (FileNotFoundException e) {
66       System.out.printf(“ERRO: non existe ficheiro de certificado %s\n.”,
67         FICH_CERT);
68     } catch (IOException e) {
69       System.out.printf(“ERRO: de E/S lendo clave de ficheiro %s\n.”,
70         FICH_CERT);
71     } catch (CertificateException e) {
72       System.out.println(“ERRO: Excepción de certificado.”);
73     }
74   }
75 }

Unha vez obtida a clave pública dun certificado podese utilizar para todas as operacións deventolas nos programas de exemplo previos; e ademáis moi sinxelo deutilizar para as operacións de cifrado e descifrado.

O método init da clase Cipher acepta tanto un obxecto de clase PrivateKey como un de clase Certificate. Neste último caso utiliza a clave pública do certificado. Pero antes de utilizar un certificado habería que verificar a súa firma dixital e que a data actual estivera dentro do seu periodo de validez.

Cifrado:

  • cifrado.init(Cipher.ENCRYPT_MODE, clavePublica);
  • cifrado.init(Cipher.ENCRYPT_MODE, certificado);

Descifrado:

  • cifrado.init(Cipher.DECRYPT_MODE, clavePrivada);
  • cifrado.init(Cipher.DECRYPT_MODE, certificado);

Xeneración e uso de firma dixital

Amosáronse exemplos de programas que calculan e verifican firmas dixitais. Pero a clase Signature de JCA facilita máis o proceso.

Tanto a firma dixital coma o cifrado e descifrado podense realizar con calquer conxunto de algoritmos de clave pública. Pero, para a firma dixital adoita utilizarse DSA (Digital Signature Algorithm).

DSA:

  • utilízase para firma pero non para cifrado e descifrado.
  • e moito menos rápido que RSA para firma dixital pero máis rápido para a verificación da firma

Para comprobar o funcionamento xeneramos previamente un certificado e a correspondente clave privada para o algoritmo DSA. As claves xeneraranse cunha lonxitude de 1024 bits. O proceso é algo distinto que para RSA.

Primerio debe xenerarse un ficheiro de parámetros dsaparams.pem, e a xeneración do certificado basease na información contida neste ficheiro.

openssl dsaparam -out dsaparams.pem 1024 
openssl req -x509 -utf8 -nodes -days 365 -newkey dsa:dsaparams.pem -keyout claveprivada.pem -out certificadoX509.crt
openssl pkcs8 -topk8 -inform PEM -in claveprivada.pem -outform DER -out claveprivada.pkcs8 -nocrypt

O certificado e a clave privada xenerados podense verificar cos mesmos comandos que para RSA.

openssl x509 -nameopt utf8 -in certificadoX509.crt -text -noout openssl pkcs8 -inform DER -nocrypt -in claveprivada.pkcs8t

Exemplo: xenera firma dixital co algortimo DSA

O seguinte programa de exemplo xenera a firma dixital dun ficheiro co algoritmo DSA.

 1 package generafirmadigital;
 2 import java.io.FileInputStream;
 3 import java.io.FileOutputStream;
 4 import java.io.BufferedInputStream;
 5 import java.io.BufferedOutputStream;
 6 import java.io.FileNotFoundException;
 7 import java.io.IOException;
 8 import java.security.KeyFactory;
 9 import java.security.PrivateKey;
10 import java.security.Signature;
11 import java.security.spec.PKCS8EncodedKeySpec;
12 import java.security.InvalidKeyException;
13 import java.security.NoSuchAlgorithmException;
14 import java.security.SignatureException;
15 import java.security.spec.InvalidKeySpecException;
16 import java.util.Base64;
17 public class GeneraFirmaDigital {
18   private static final String ALGORITMO_FIRMA = “DSA”;
19   private static final String FICH_CLAVE_PRIV = “claveprivada.pkcs8”;
20   private static final int TAM_BUFFER = 16384;
21   public static void main(String[] args) {
22     if (args.length < 1) {
23       System.out.println(
24         “ERROR: indicar nome de ficheiro para calcular firma dixital.”);
25       return;
26     }
27     String nomFich = args[0];
28     KeyFactory keyFactory;
29     byte clavePrivCodif[];
30     try (FileInputStream fisClavePriv = 
31         new FileInputStream(FICH_CLAVE_PRIV)) {
32       clavePrivCodif = fisClavePriv.readAllBytes();
33     } catch (FileNotFoundException e) {
34       System.out.printf(
35         “ERRO: non existe ficheiro de clave privada %s\n.”, FICH_CLAVE_PRIV);
36       return;
37     } catch (IOException e) {
38       System.out.printf(
39         “ERRO: de E/S lendo clave de ficheiro %s\n.”, FICH_CLAVE_PRIV);
40       return;
41     }
42     Signature s;
43     try (FileInputStream fis = new FileInputStream(nomFich);
44       FileOutputStream fos = new FileOutputStream(nomFich + “.firma.”
45         + ALGORITMO_FIRMA);
46       BufferedInputStream is = new BufferedInputStream(fis);
47       BufferedOutputStream os = new BufferedOutputStream(fos)
48       ) {
49       keyFactory = KeyFactory.getInstance(ALGORITMO_FIRMA);
50       PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(
51         clavePrivCodif);
52       PrivateKey clavePrivada = keyFactory.generatePrivate(privateKeySpec);
53       s = Signature.getInstance(ALGORITMO_FIRMA);
54       s.initSign(clavePrivada);
55       byte[] buff = new byte[TAM_BUFFER];
56       while (is.read(buff) != -1) {
57         s.update(buff);
58       }
59       byte[] firma = s.sign();
60       os.write(firma);
61       System.out.printf(“Firma digital en base 64 como texto:\n%s\n”,
62         Base64.getEncoder().encodeToString(firma).replaceAll(
63           “(.{76})”, “$1\n”
64       ));
65     } catch (NoSuchAlgorithmException e) {
66        System.out.printf(“Non existe algoritmo de firma %s.\n”, ALGORITMO_FIRMA);
67     } catch (SignatureException e) {
68       e.printStackTrace();
69     } catch (InvalidKeySpecException e) {
70       System.out.println(“Especificación de clave non válida.”);
71       e.printStackTrace();
72     } catch (InvalidKeyException e) {
73       System.out.println(“Clave non válida.”);
74       e.printStackTrace();
75     } catch (FileNotFoundException e) {
76       System.out.printf(“ERROR: no existe ficheiro %s.\n”, nomFich);
77     }
78     catch (IOException e) {
79       System.out.println(“ERRO: de E/S xenerando firma de ficheiro”);
80     }
81   }
82 }

Exemplo: verifica firma dixital

O seguinte programa de exemplo verifica a firma digital.

Para verificar o certificado utilízase o mesmo método certificadoCorrecto dun programa de exemplo anterior.

Para verificar a firma utilízase, coma no programa anterior, un obxecto de tipo Signature. Inicializase o obxecto co certificado dixital, en lugar de coa clave privada, como se facía para asinar.

 1 package verivcafirmadigital;
 2 import java.io.FileInputStream;
 3 import java.io.BufferedInputStream;
 4 import java.io.FileNotFoundException;
 5 import java.io.IOException;
 6 import java.security.Signature;
 7 import java.security.PublicKey;
 8 import java.security.InvalidKeyException;
 9 import java.security.NoSuchAlgorithmException;
10 import java.security.NoSuchProviderException;
11 import java.security.SignatureException;
12 import java.security.cert.CertificateFactory;
13 import java.security.cert.Certificate;
14 import java.security.cert.X509Certificate;
15 import java.security.cert.CertificateException;
16 import java.security.cert.CertificateExpiredException;
17 import java.security.cert.CertificateNotYetValidException;
18 public class VerificaFirmaDigital {
19   private static final String ALGORITMO_FIRMA = “DSA”;
20   private static final String FICH_CERT = “certificadoX509.crt”;
21   private static final int TAM_BUFFER = 16384;
22   private static boolean certificadoCorrecto(Certificate certif) {
23     (...)
24   }
25   public static void main(String[] args) {
26     if (args.length < 1) {
27       System.out.println(
28         “ERRO: indicar nome de ficheiro para verificar firma dixital.”);
29       return;
30     }
31     String nomFich = args[0];
32     String nomFichFirma = nomFich + “.firma.” + ALGORITMO_FIRMA;
33     Certificate certif = null;
34     try (FileInputStream fisCertificado = new FileInputStream(FICH_CERT)) {
35       CertificateFactory f = CertificateFactory.getInstance(“X.509”);
36       certif = f.generateCertificate(fisCertificado);
37       if (!certificadoCorrecto(certif)) {
38         System.out.printf(“Certificado %s non correcto.\n”, FICH_CERT);
39         System.exit(1);
40       }
41       PublicKey clavePublica = certif.getPublicKey();
42       System.out.printf(“Clave pública leída de certificado, formato: %s.\n”, clavePublica.getFormat());
43     } catch (FileNotFoundException e) {
44       System.out.printf(“ERRO: non existe ficheiro de certificado %s\n.”, FICH_CERT);
45       System.exit(1);
46     } catch (IOException e) {
47       System.out.printf(“ERRO: de E/S lendo ficheiro de certificado %s\n.”, FICH_CERT);
48       System.exit(1);
49     } catch (CertificateException e) {
50       System.out.println(“ERRO: Excepción de certificado.”);
51       System.exit(1);
52     }
53     Signature s;
54     try (FileInputStream fisFirma = new FileInputStream(nomFichFirma);
55             FileInputStream fis = new FileInputStream(nomFich);
56             BufferedInputStream is = new BufferedInputStream(fis)) {
57       byte[] firma = fisFirma.readAllBytes();
58       
59       s = Signature.getInstance(ALGORITMO_FIRMA);
60       s.initVerify(certif);
61       byte[] buff = new byte[TAM_BUFFER];
62       while (is.read(buff) != -1) {
63         s.update(buff);
64       }
65       
66       System.out.printf(“Verificación de firma en ficheiro %s para ficheiro %s.\n”, nomFichFirma, nomFich);      
67       System.out.println(s.verify(firma) ? “Firma correcta” : “ERRO: firma non correcta”);
68     } catch (NoSuchAlgorithmException e) {
69       System.out.printf(“Non existe algoritmo de firma %s.\n”, ALGORITMO_FIRMA);
70     } catch (SignatureException e) {
71       e.printStackTrace();
72     } catch (InvalidKeyException e) {
73       System.out.println(“Clave no válida.”);
74       e.printStackTrace();
75     } catch (FileNotFoundException e) {
76       System.out.printf(“ERRO: non existe ficheiro: %s.\n”, e.getMessage());
77     } catch (IOException e) {
78       System.out.println(“ERRO: de E/S”);
79       e.printStackTrace();
80     }
81   }
82 }

Comunicacións seguras con JSSE

JSSE é unha implementación de TLS/SSL en Java. As clases de JSSE están no paquete javax.net.ssl.

SSLServerSocket é filla de ServerSocket, e engade unha capa de encriptación baseada en TLS/SSL, de xeito que os datos que se transmiten en datagramas de TCP van cifrados. Para ela existe unha clase de tipo factory, SSLServerSocketFactory.

JSSE implementa diversas versións de TLS/SSL, e diversas opcións destes protocolos. Para sacar partido de todas elas, basta con que cada parte teña a súa clave privada e a clave pública da outra parte. Pero se non se precisa autenticación de cliente, bastaría con xenerar o par de claves para el servidor.

Pódese xenerar o par de claves pública e privada para cada parte con ferramentas de openssl; pero tambén con keygen, unha ferramenta propia de Java.

En cada parte (cliente e servidor) utilizaranse dous almacéns de claves (keystore). Os keystores xestionanse tamén con keygen.

  • nun keystore para o propio certificado almacena o propio certificado coa clave privada, xenerado coa opción -genkey, para o algoritmo RSA (–keyalg RSA), protexido por contrasinal especificada coa opción -storepass. Despois, se exporta a un ficheiro con -exportkey e -file.
  • dado que os certificados anteriores son autoasinados, para que se admitan pola outra parte, debe establecerse o seu emisor coma unha CA ou autoridade de certificación (o seu certificado, que conten a súa clave pública, debe importarse con -importcert a un keystore para certificados de autoridades de certificación ou emisores de confianza). Esto faise coa opción –trustcacerts (abreviatura de trusted CA certificates ou certificados de AC de confianza).

O seguiente script fai todo isto para ambas partes. Pero, para o uso máis habitual, no que non se realiza validación de cliente, solo son necesarias as operacións en grosa. É dicir, a xeneración nun keystore para o servidor dun certificado dixital coa súa clave pública máis a clave privada correspondente, e a importación do certificado coa clave pública nun keystore para o cliente.

mkdir certificados 
mkdir certificados/servidor 
mkdir certificados/cliente 

# Xenerar e exportar certificados de servidor e cliente
keytool -genkey -alias certServ -keyalg RSA -keystore keystoreServ -storepass pwdkeystoreserv
keytool -exportcert -alias certServ -keystore keystoreServ -storepass pwdkeystoreserv -file certificados/servidor/servidor.cer
keytool -genkey -alias certCli -keyalg RSA -keystore keystoreCli -storepass pwdkeystorecli 
keytool -exportcert -alias certCli -keystore keystoreCli -storepass pwdkeystorecli -file certificados/cliente/cliente.cer 

# Importar coma certificados de confianza certificado de servidor para cliente e de cliente para servidor 
keytool -importcert -trustcacerts -alias certConfCli -file certificados/servidor/servidor.cer -keystore keystoreCliCertConf -storepass pwdkeystoreconfcli 
keytool -importcert -trustcacerts -alias certConfServ -file certificados/cliente/cliente.cer -keystore keystoreServCertConf -storepass pwdkeystoreconfserv

Exemplo de eco con TLS/SSL

Amósanse as diferenzas (mínimas) entre os programas cliente e servidor de eco:

  • o desenvolto anteriomente para TCP
  • o programa equivalente para TLS/SSL, que fai uso das clases de JSSE.

No programa servidor para TLS/SSL con JSSE créase un socket de servidor da clase SSLServerSocket a partir dunha instancia de SSLServerSocketFactory. Antes hai que asignar valor a determinadas propiedades do sistema javax.net.ssl, en concreto:

  • os nomes dos keystores ou almacenes de claves
  • os contrasinais para acceder a eles.

So se usa un keystore que conten a clave privada do servidor. A SSLServerSocketFactory obtense con SSLServerSocketFactory.getDefault(), pero podense utilizar diversos constructores con diversos parámetros para utilizar as diferentes opcións de TLS/SSL.

+ Cambios en servidor baseado en TCP para utilizar TLS/SSL
Servidor para TCP Servidor para TLS/SSL (con JSSE)
package socketstcp_servidoreco; package jsse_servidoreco;
import javax.net.ssl.SSLServerSocketFactory; public class SocketsTCP_ServidorEco { public class JSSE_ServidorEco {
System.setProperty(“javax.net.ssl.keyStore”,”keystoreServ”);

System.setProperty(“javax.net.ssl.keyStorePassword”,”(pwd) “); //System.setProperty(“javax.net.ssl.trustStore”,”keystoreServCertConf”); //System.setProperty(“javax.net.ssl.trustStorePassword”,”(pwd)”);

try (ServerSocket socketServidor = new ServerSocket(numPuerto)) { SSLServerSocketFactory sfactSSL = (SSLServerSocketFactory) SSLServerSocketFactory.getDefault(); try (ServerSocket socketServidor = sfactSSL.createServerSocket(numPuerto)) {

Entre os programas clientes as diferenzas son análogas. Con JSSE crease un socket de comunicación da SSLSocket a partir dunha instancia de SSLSocketFactory. Antes hai que asignar valor a determinadas propiedades del sistema javax.net.ssl:

  • os nombres dos keystores ou almacenes de claves
  • os contrasinais para acceder a eles.

Solo se usa, en reciprocidade co programa de servidor, un keystore que conten a clave pública do servidor. A SSLSocketFactory obtense con SSLSocketFactory.getDefault(), pero se poden utilizar diversos constructores con diferentes parámetros para utilizar aa opcións de TLS/SSL.

+ Cambios en cliente baseado en TCP para utilizar TLS/SSL
Cliente para TCP Cliente para TLS/SSL (con JSSE)
package socketstcp_clienteeco; package jsse_clienteeco;
import javax.net.ssl.SSLSocketFactory;
public class SocketsTCP_ClienteEco { public class JSSE_ClienteEco {
// System.setProperty(“javax.net.ssl.keyStore”,”keystoreCli”);
// System.setProperty(“javax.net.ssl.keyStorePassword”,”(pwd)”);
System.setProperty(“javax.net.ssl.trustStore”, “keystoreCliCertConf”);
System.setProperty (“javax.net.ssl.trustStorePassword”, “(pwd)”);
try (Socket socketComunicacion = new Socket(nomHost, numPuerto)) { SSLSocketFactory sfactSSL = (SSLSocketFactory) SSLSocketFactory.getDefault(); try (Socket socketComunicacion = sfactSSL.createSocket(nomHost, numPuerto)) {

Se se fixera autenticación do cliente debería utilizarse no cliente o keystore para a súa clave privada; e no servidor, un keystore para incluir o seu certificado coma un certificado de confianza, ou no seu defecto certificados raíces ou intermedios que permitiran validalo.

Políticas de seguridade e de acceso

As políticas de acceso permiten determinar as accións que pode realizar a aplicación no equipo do usuario. Por exemplo podemos indicar:

  • que a aplicación poida ler os ficheiros dunha determinada carpeta
  • que poida enviar datos a través de Internet a un determinado equipo
  • etc.

Así aínda que un usuario malicioso utilice a aplicación de forma incorrecta queda limitado o impacto de seu ataque (por exemplo se restrinximos a que só pode leer os ficheros da carpeta c:/temp)

Control de acceso con JAAS

JAAS (Java Authentication and Authorization Service, servizo de autorización e autenticación de Java) é unha interfaz que permite as aplicacións Java acceder a servizos de control, de autenticación e acceso. Pódese utllizar para dous propósitos:

  1. para a autenticación de usuarios: para detenninar de forma fiable e segura quén está executando o noso código Java
  2. para a autorización de usuarios: para asegurarse de que quen o executa ten os permisos necesarios para realizar as accións

Están involucradas as seguientes clases e interfaces:

  • LoginContext, contexto de inicio de sesión: inicia e xestiona o proceso de autenticación mediante a creación dun Subject.
    • A autenticación faise chamando ó método login()
  • LoginModule, módulo de conexión: é a interfaz que debe implementarse para definir os mecanismos de autenticación na aplicación. Encárgase de validar os datos nun proceso de autenticación. Debense implementar os seguintes métodos:
    • initialize()
    • login()
    • commit()
    • abort()
    • logout()
  • Subject: clase que representa a un ente autenticable dentro da aplicación (entidade,usuario, sistema)
  • Principal: clase que representa os atributos que posee cada Subject recuperado una vez que se efectua o ingreso á aplicación. Un Subject puede contener varios principales.
  • CallBackHandler: interfaz que se debe implementar cando se precisa recibir do usuario a información para a autenticación; encárgase da interacción co usuario para obter os datos de autenticación. Débese desarrollar o método handle() para implementala.

As clases e interfaces principais de JAAS están disponibles nos seguintes paquetes:

  • javax.security.auth.*: contén as clases de base e interfaces para os mecanismos de autenticación e autorización
    • javax.security.auth.callback.*: contén as clases e interfaces para definir as credenciais de autenticación da aplicación
    • javax.security.auth.login.*: contén as clases para entrar e sair dun dominio de aplicación
    • javax.security.auth.spi.*: contén interfaces para un proveedor de JAAS para implementar módulos JAAS

Autenticación

Autorización

Asinado e verificación dos JAR

Existen ferramentas proporcionadas no Kit de Desenvolvemento Java ™ para asinar e verificar os ficheiros JAR.

Por qué asinar un ficheiro? Pola mesma razón que podemos asinar un documento en papel con pluma e tinta: para que os lectores saiban quén escribíu o documento, e que conta coa aprobación de quen o escribíu.

Do mesmo xeit cando se asina dixitalmente un ficheiro, calquera persona que recoñece a súa sinatura dixital sabe a procedencia do ficheiro: a ese proceso de recoñecemento das sinaturas electrónicas chámaselle verificación.

La posibilidad de firmar y verificar archivos es una parte importante de la arquitectura de seguridad de la plataforma Java. La seguridad es controlada por la política de seguridad que esté en vigor en tiempo de ejecución. Puede configurar la directiva para conceder privilegios de seguridad para los applets y aplicaciones. Por ejemplo, puede conceder permiso a un applet para realizar operaciones normalmente prohibidas, tales como leer y escribir archivos locales o ejecutar programas locales ejecutables. Si ha descargado un código firmado por una entidad de confianza, puede utilizar este hecho como un criterio para decidir cuál de los permisos de seguridad asignará al código.

Una vez que usted (o su navegador) han comprobado que un applet es de una fuente confiable, usted puede relajar las restricciones de seguridad para que el applet realice operaciones que normalmente estarían prohibidas. Un applet de confianza puede tener libertades como se especifica en el archivo de política en vigor.

La plataforma Java permite a firma e verificación mediante o uso de claves públicas e privadas, que veñen en pares e desempeñan papeles complementarios.

  • A clave privada é a "pluma" electrónica coa que se pode asinar un ficheiro. É coñecida só polo asinante para que ninguen poida "falsificar" a súa sinatura.
  • Un ficheiro asinado coa súa clave privada só pode ser comprobado pola correspondente clave pública.
  • As claves pública e privada non son suficientes para verificar a sinatura; aínda que se verifique que un ficheiro asinado conten un par de claves correspondentes entre si, precísase tamén confirmar que a clave pública provee da persona da que a sinatura pretende provenir.

Requirese un elemento más para hacer a sinatura e verificación: o certificado que o asinante inclúe nun ficheiro JAR firmado:

  • Un certificado é unha declaración asinada dixitalmente de unha autoridade de certificación recoñecida que indica que é o dono dunha clave pública determinada.

Unha autoridad de certificación son as entidades (xeneralmente empresas especializadas en seguridade dixital) que son de confianza en toda la industria para asinar e emitir certificados.

Nos ficheiros JAR asiados o certificado indica que é odno da clave pública contida no ficheiro JAR.

O asinar un fichero JAR a súa clave pública colócase dentro do ficheiro, xunto con un certificado asociado para que sexa de fácil acceso para o seu uso por quen desexe verificar a súa sinatura.

En resumo para a sinatura dixital:

  1. O asinante asina o ficheiro JAR utilizando unha clave privada
  2. A clave pública correspondente colócase no ficheiro JAR xunto co seu certificado polo que está disponible para o seu uso por quen queira verificar a sinatura.

Resúmenes e o ficheiro de firmas

Ö firmar un ficheiro JAR cada un dos ficheiros internos ten unha entrada de resumo no ficheiro de manifesto. Este sería un exemplo:

Name: test/classes/ClassOne.class
SHA1-Digest: TD1GZt8G11dXY2p4olSZPc5Rj64=

Os valores resumen son representacións hash dos contenidos dos ficheiros tal e como estaban no momento da sinatura. O resumo dun ficheiro cambiará só se o propio ficheiro cambia.

Cuando se asina un ficheiro JAR xenérase automáticamente un fichero de sinatura e colóca no directorio META-INF do ficheiro JAR, o mesmo directorio que conten o ficheiro de manifesto. Os ficheiros de firmas teñen nomes de ficheiro cunha extensión. SF. Este sería un exemplo:

Signature-Version: 1.0
SHA1-Digest-Manifest: h1yS+K9T7DyHtZrtI+LxvgqaMYM=
Created-By: 1.6.0 (Sun Microsystems Inc.)
Name: test/classes/ClassOne.class
SHA1-Digest: fcav7ShIG6i86xPepmitOVo4vWY=
Name: test/classes/ClassTwo.class
SHA1-Digest: xrQem9snnPhLySDiZyclMlsFdtM=
Name: test/images/ImageOne.gif
SHA1-Digest: kdHbE7kL9ZHLgK7akHttYV4XIa0=
Name: test/images/ImageTwo.gif
SHA1-Digest: mF0D5zpk68R4oaxEqoS9Q7nhm60=

O ficheiro de sinatura conten entradas de resumo para os ficheiros do archivo comprimido que se parecen as entradas de valor de síntesis no manifesto. Pero:

  • os valores de resumo no manifesto calcúlanse a partir dos propios ficheiros
  • os valores de resumo no ficheiro de sinatura calculase a partir das entradas correspondentes o manifiesto

Os ficheiros de firma tamén conteñen un valor de resumo para o manifiesto completo (ver a cabecera SHA1-Digest-Manifest no exemplo anterior).

Cando un ficheiro JAR asinado está sendo verificado, os resúmenes de cada un dos ficheiros volvense a calcular e comparar cos resúmenes registrados no manifiesto para asegurar que o contido do ficheiro JAR non cambiou desde que se asinou. Coma una comprobación adicional, os valores de resumo para o ficheiro de manifiesto en si volvese a calcular e compárase cos valores rexistrados no ficheiro de sinatura.

Na documentación oficial podes atopar máis información do manifesto.

O ficheiro de bloque de sinatura

Ademáis dos ficheiros de sinaturas colócase automáticamente un ficheiro de bloque de sinatura no directorio META-INF cando se asina un ficheiro JAR. A diferenza do ficheiro de manifiesto ou do ficheiro de sinatura, os ficheiros de bloques de sinaturas non son lexibles.

O ficheiro de bloque de firma contén dous elementos esenciais para a verificación:

  • a sinatura dixital para o ficheiro JAR que se xenerou coa clave privada do asinante
  • O certificado que contén a clave pública do asinante, para ser utilizado por calquera que desexe verificar o ficheiro JAR asinado.

Os nomes dos ficheiros de bloques de firmas adoitan ter unha extensión .DSA o que indica que foron creados polo algoritmo predeterminado de sinatura dixital; outras extensións son posibles se as claves asociadas con algún algoritmo estándar disntinto utilízanse para a sinatura.

Asinado de JAR

Para asinar e vertificar a firma de jar pódese utilizar o comando jarsigner

Para asinar un ficheiro JAR: Primeiro debemos ter unha clave privada. As claves privadas e as súas correspondentes claves públicas almacénanse en bases de datos protexidas chamada keystore ou almacén de chaves. Un keystore conten as claves de moitos firmantes potenciales. Cada clave do almacén de claves pode ser identificada por un alias que adoita ser o nome do asinante e quen posee a clave. A clave que pertence a Xose López, podería ter por exemplo o alias de Xose..

A forma básica do comando para a sinatura dun ficheiro JAR é:

jarsigner jar-file alias
jar-file é a ruta do ficheiro JAR a firmar 
alias é o alias que identifica a clave privada que se utiliza para asinar o ficheiro JAR, e o seu certificado asociado
jarsigner pedirá o contrasinal para o almacén de claves e alias

Nesta execución básica do comando:

  • asúmese que o almacén de claves a utilizar está nun ficheiro chamado keystore no seu directorio personal
  • crearase a sinatura e os ficheiros de bloques de sinaturas con nomes x.SF e x.DSA respectivamente, onde x son as primeiras oito letras do alias en maiúsculas
  • sobrescribe o ficheiro JAR orixinal co JAR asinado

O comando permite estas opcións (en particular recomendable o uso de timestamp) que deben preceder á ruta jar-file.

Option Description
-keystore url Specifies a keystore to be used if you don't want to use the .keystore default database.
-sigfile file Specifies the base name for the .SF and .DSA files if you don't want the base name to be taken from your alias. file must be composed only of upper case letters (A-Z), numerals (0-9), hyphen (-), and underscore (_).
-signedjar file Specifies the name of the signed JAR file to be generated if you don't want the original unsigned file to be overwritten with the signed file.
-tsa url Generates a time stamp for the signature using the Time Stamping Authority (TSA) identified by the URL.
-tsacert alias Generates a time stamp for the signature using the TSA's public key certificate identified by alias.
-altsigner class Indicates that an alternative signing mechanism be used to time stamp the signature. The fully-qualified class name identifies the class used.
-altsignerpath classpathlist Provides the path to the class identified by the altsigner option and any JAR files that the class depends on.

Exemplo práctico

Aprenderemos a crear un fichero jar e asinalo dixitalmente para establecer máis adelante correctamente as súas políticas de acceso.

Paso 1. Obtén un codigo fonte (facílitase este de exemplo)

 1 // Download from http://docs.oracle.com/javase/tutorial/security/tour2/examples/GetProps.java
 2 import java.lang.*;
 3 import java.security.*;
 4  
 5 class GetProps {
 6     public static void main(String[] args) {
 7         /* Test reading properties w & w/out security manager */
 8         String s;
 9         try {
10  
11             System.out.println("About to get os.name property value");
12  
13             s = System.getProperty("os.name", "not specified");
14             System.out.println("  The name of your operating system is: " + s);
15  
16             System.out.println("About to get java.version property value");
17  
18             s = System.getProperty("java.version", "not specified");
19             System.out.println("  The version of the JVM you are running is: " + s);
20  
21             System.out.println("About to get user.home property value");
22  
23             s = System.getProperty("user.home", "not specified");
24             System.out.println("  Your user home directory is: " + s);
25  
26             System.out.println("About to get java.home property value");
27  
28             s = System.getProperty("java.home", "not specified");
29             System.out.println("  Your JRE installation directory is: " + s);
30  
31         } catch (Exception e) {
32             System.err.println("Caught exception " + e.toString());
33         }
34     }
35 }

Paso 2. Compila o código fonte:

javac GetProps.java

Paso 3. Crea o ficheiro jar:

jar cvf GetProps.jar GetProps.class

Paso 4. Xenera as claves. Creamos un par de claves (unha clave privada e unha clave pública) para asinar o ficheiro:

  • A clave privada utilizarémola para asinar o ficheiro
  • A clave pública utilizarana os demáis usuarios para comprobar que o ficheiro é correcto. E

Para xenerar as claves:

keytool –genkey –alias omeual –keypass omeukeypass –keystore omeukeystore –storepass omeustorepass

-alias omeual. Indica o alias que se vai a utilizar para referirnos ó keystore (que é onde se almacenaran as chaves xeneradas).
-keypass omeukeypass. Especifica o contrasinal da chave privada.
-keystore omeukeystore. Indica o nome do keystore que se esta creando ou utilizando.
-storepass omeustorepass. Indica o contrasinal do keystore.

Introduciremos os datos do certificado: nome e epelido, organización, localidade, etc.

Paso 5. Asinamos o ficheiro jar utilizando o certificado creado

jarsigner –keystore DAM –signedjar sGetProps.jar GetProps.jar firmar

-keystore omeukeystore. O keystore creado anteriormente.
-signedjar sGetPrrops.jar. Nome do ficheiro jar asinado.
GetProps.jar Ficheiro jar a asinar.
omeualias. Alias que se creou anteriormente.

Paso 6. Exportamos a chave pública do certificado executando:

keytool –export –keystore omeukeystore –alias omeual –file Xose.cert

Xa temos todos os ficheiros necesarios para enviarllos ós clientes que queiran utilizar a nosa aplicación:

  • o fichero jar asinado (sGetProps.jar)
  • o certificado (Xose.cert)

Ahora podería intentarse executar directamente la aplicación, pero... ó utilizar o SecurityManger teremos problemas de seguridade.

java –cp sGetProps.jar GetProps
java –Djava.security.manager –cp sGetProps.jar GetProps

Para que a aplicación funcione correctamente:

Paso 1. Importamos o certificado:

keytool –import alias omeual –file Xose.cert –keystore omeukeystore

Paso 2. Configurar as políticas de acceso para que o usuario omeual poida ejecutar correctamente a aplicación

Paso 2a. Executamos o comando policytool oara iniciar a ferramenta que permite administrar as políticas de acceso. Paso 2b. Abrimos o ficheiro:

C:\Archivos de programa\Java\jre1.8.0_301\lib\security\java.policy 
a ubicación pode cambiar dependendo da versión de java
Policy Tool con ficheiro java.policy de políticas de acceso aberto

Paso 2c. Indicamos o keystore que utilizaremos. No menú Almacen de claves seleccionamos Editar e no campo URL de almacén de claves indicamos a ubicación das claves:

file:\c:\ejemplo\omeukeystore

Paso 2d. Engadimos unha entrada para permitir o acceso do noso código. Pulsamos o botón Agregar Entrada de Política Indamos a orixe do ficheiro pola súa ubicación (codebase) ou pola súa firma (SignedBy). No noso caso, no campo SignedBy escribimos omeual Pulsamos en Agregar Permiso Engadimos dous permisos cos seguintes datos:

PropertyPermission = java.util.PropertyPermission
Nombre de Destino = user.home
Acciones = read
PropertyPermission = java.util.PropertyPermission
Nombre de Destino = java.home
Acciones = read

Gardamos todos os cambios.

Paso 3. Comrpobamos o funcionamento

java –Djava.security.manager –cp sGetProps.jar GetProps
Xa funciona correctamente

Seguridade interna de aplicación

Cando deseñamas a aplicación debemos pensar en que se programe dun xeito robusto para que se comporte tal e como esperamos dela.

Algunhas das técnicas que podemos implementar son:

  • a xestión de excepcións
  • as validacións de entradas de datos
  • a utilización dos ficheros de rexistro

Xestión de excepcións

O manexo de excepcións é unha técnica de programación que permite ó programador controlar os erros ocasionados durante a execución dun programa informático. Cando ocurre certo tipo de erro o sistema reacciona executando un fragmento de código que resolve a situación (por exemplo retornando unha mensaxe de erro ou devolviendo un valor por defecto).

Un ejemplo práctico: desexamos crear unha aplicación que lea un ficheiro e o envíe pola rede. Cales serían algunhas das posibles excepcións?

  • que o dispositivo onde se almacena o ficheiro no esté disponible
  • que non teña permisos de lectura sobre o ficheiro
  • que o fichero non exista
  • que a red non esté disponible
  • que o dispositivo de destino non esté disponible
  • que no dispositivo de destino non dispoñamos de permisos de escritura
  • que o dispositivo de destino non haiba espacio suficiente para almacenar o ficheiro
  • etc

Como programadores, debemos xestionar as posibles excepcións que poidan ocurrir para evitar calquier tipo de fallos na execución do programa.

Tipos de excepcions

Existen en Java varios tipos fundamentales de excepcións:

Error
Excepcións que indican problemas moi graves que adoitan ser non recuperables e non deben ser casi nunca capturadas.
Exception
Excepcións non definitivas pero que se detectan fora do tempo de execución.
RuntimeException
Excepcións que se dan durante a execución do programa.

Bloques try / catch / finally

En xeral para incluir o manexo de excepcións nun programa hai que:

  1. Dentro dun bloque try insertar o fluxo normal das instruccións.
  2. Estudiar os erros que poden producirse durante a execución das instruccións e comprobe que cada un deles provoca unha excepción.
  3. Capturar e xestionar as excepcións en bloques catch.

Cando o programador executa un código que pode provocar unha excepción (por exemplo unha lectura ou escritura dun fichero), debe incluír este fragmento de código dentro dun bloque try:

1  try { 
2    // Código posiblemente problemático 
3  }

Mais o importante é cómo controlar qué facer coa posible excepción que se xenera: usaranse as claúsulas catch que permiten especificar a acción a realizar cando se produce unha determinada excepción.

1  try {
2  // Código posiblemente problemático
3  } catch( tipo_de_excepcion e) {
4   // Código para solucionar a excepción e
5   } catch( tipo_de_excepcion_mas_general e) {
6   // Código para solucionar a excepción e
7  }

Pódense anidar sentencias catch para ir xestionando diferentes erros de forma tamén diferente. En último lugar irán as excepcións máis xenerales (as que se atopan máis arriba no árbol de herdanza de excepcións), porque o intérprete Java executa o bloque de código catch co parámetro que é do tipo dunha excepción lanzada.

Para realizar unha acción común a todas as excepcións utilízase a sentenza finally: este código execútase tanto se se trata dunha excepción (catch) coma se non.

1  try {
2      // Código posiblemente problemático
3  } catch( Exception e ) {
4  // Código para solucionar a excepción e
5  } finally {
6    // Execútase tras try o catch
7  }


Podes repasar estes conceptos no titorial oficial de Java para excepcións

Exemplo

Programa que xenera un vector de cen elementos con valores aleatorios e posteriormente os almacena no ficheiro de Salida.txt.

Partimos deste exemplo pedagóxico (que non funciona por non controlar as excepcións):


// Este código non funciona: é para uso pedagóxico

 1 import java.io.*;
 2 import java.util.Vector;
 3 import java.util.Random;
 4 
 5 class EscribirNumeros {
 6     private Vector<Integer> numeros;
 7     private static final int SIZE = 100;
 8 
 9     public EscribirNumeros (){
10         // Xeneramos o vector con números aleatorios
11         numeros = new Vector<Integer>(SIZE);
12         Random randomGenerator = new Random();
13 
14         for (int i = 0; i < SIZE; i++){
15             Integer num=randomGenerator.nextInt(100);
16             numeros.addElement(new Integer(num));
17         }
18 
19         // Gardamos o vector nun ficheiro
20         PrintWriter out = null; 
21         System.out.println("Entrando a la zona Try");
22         out = new PrintWriter(new FileWriter("Salida.txt"));
23          
24         for (int i = 0; i < SIZE; i++)
25             out.println("Valor de: " + i + " = " + numeros.elementAt(i));
26         out.close();
27     }
28 
29      public static void main( String[] arg ) {
30           new EscribirNumeros ();
31      }
32 }
33 
34 }

Non se pode producir ningún tipo de excepción onde se xeneran os valores aleatorios.

Si é posible:

  • que non se poida abrir o ficheiro para gardar os valores e se produza unha excepción de entrada/saída (IOException)
  • que ó executar a sentenza numeros.elementAt(i) o número de elemento solicitado (i) esté fóra do rango do vector (de 0 a 99) e se produza unha excepción (IndexOutOfBoundsException)

O código que trata as dúas excepcións:

 1 import java.io.*;
 2 import java.util.Vector;
 3 import java.util.Random;
 4 
 5 class EscribirNumeros {
 6     private Vector<Integer> numeros;
 7     private static final int SIZE = 100;
 8 
 9     public EscribirNumeros (){
10         // Xeneramos o vector con números aleatorios
11         numeros = new Vector<Integer>(SIZE);
12         Random randomGenerator = new Random();
13 
14         for (int i = 0; i < SIZE; i++){
15             Integer num=randomGenerator.nextInt(100);
16             numeros.addElement(new Integer(num));
17         }
18 
19         // Gardamos o vector nun ficheiro
20         PrintWriter out = null;
21 
22         try {
23             System.out.println("Entrando na zona Try");
24             out = new PrintWriter(new FileWriter("Salida.txt"));
25 
26             for (int i = 0; i < SIZE; i++)
27                 out.println("Valor de: " + i + " = " + numeros.elementAt(i));
28             } catch (ArrayIndexOutOfBoundsException e) {
29                 System.err.println("Caught ArrayIndexOutOfBoundsException: " +e.getMessage());
30             } catch (IOException e) {
31                 System.err.println("Caught IOException: " + e.getMessage());
32             } finally {
33                 if (out != null) {
34                     System.out.println("Pechando o ficheiro");
35                     out.close();
36                 } else {
37                     System.out.println("NON se abríu o ficheiro");
38                 }
39             }
40     }
41 
42      public static void main( String[] arg ) {
43           new EscribirNumeros ();
44      }

Validacións de entradas de datos

Acontecen importantes erros de seguridade (entre outras as SQL Injection e os buffer overflow) e de inconsistencia de datos dentro dunha aplicación a través dos datos que introducen os usuarios.

Erros baseados en buffer overflow

Prodúcense cando se desborda o tamaño dunha determinada variable, vector, matriz, etc. e se consigue acceder a zonas de memoria reservadas. Por exemplo, se reservamos memoria para o nombre de usuario que ocupa 30 caracteres e o usuario consigue que a aplicación almacene máis estase producindo un buffer overflow: os primeros 30 valores se almacenan no lugar correcto reservado en memoria pero os restantes valores almacenanse en zonas de memoria destinadas a outros fines.

Erros baseados en SQL Injection

Acontece cando cos datos introducidos (e sen validar) lánzase unha consulta a base de datos que pode arroxar información da mesma ou facilitar unha escalada de privilexios. A solución pasa por usar preparedStatement.

A validación dos datos (formato e tamaño) permite:

  • Manter a consistencia dos datos (se a un usuario lle indicamos que debe introducir o seu DNI éste debe ter o mesmo formato sempre)
  • Evitar desbordamientos de memoria buffer overflow, comprobando o formato e a lonxitude do campo evitando desbordamientos de memoria e accesos mailentcionados a bases de datos.

Librería regex

Java conta dunha librería (java.util.regex) para utilizar a clase Pattern que nos permite definir expresións regulares.

As expresións regulares permiten definir exactamente o formato da entrada de datos. Así se fai: 1. Importamos a librería:

1  import java.util.regex.*;

2.Definimos Pattern y Matcher:

1  Pattern pat=null;
2  Matcher mat=null;

3. Compilamos o patrón a utilizar:

1  pat=Pattern.compile(patron);

onde o patrón a comprobar é onde temos que indicar o formato que vai ter a entrada.

Algúns elementos para a validación entradas
Elemento Comentario
x O carácter x.
[abc] Os caracteres a, b ou c.
[a-z] Unha letra en minúscula.
[A-Z] Unha letra en maiúscula.
[a-zA-Z] Unha letra en minúscula ou maiúscula
[0-9] Un número comprendido entre el 0 y el 9.
[a-zA-Z0-9] Unha letra en minúscula, maiúscula ou un número.
Operadores para a validación entradas
Operador Comentario
[a-z]{3} Hai que introducir 3 letras en minúsculas.
[a-z]{3,6} Hay que introducir de 3 a 6 letras en minúsculas.
[a-z]{3,} Hay que introducir máis de 3 letras en minúsculas.
adeus Operación OR lóxica: permite indicar que se introduzca o texto ola ou adeus.
XY Operación AND lóxica: permite indicar que se deben introducir unha expresión X seguida de Y.
a) mar Os delimitadores ( ) permiten facer expresións máis complexas. No exemplo, debe introducirse o texto "no mar" ou "na mar".

Algúns exemplos de expresións:

1  //Teléfono (formato 000-000000)
2  pat=Pattern.compile("[0-9]{3}-[0-9]{6}");
3  //DNI
4  pat=Pattern.compile("[0-9]{8}-[a-zA-Z]");
5  //Provincias galegas
6  pat=Pattern.compile("A Coruña","Lugo","Ourense","Pontevedra");

4. Pasámoslle o evaluador de expresións o texto a comprobar.

1  mat=pat.matcher(texto_a_comprobar);

5. Comprobamos se hai algunha coincidencia:

1  if(mat.find()){
2    // Coincide co patrón 
3   } else{
4    // NON coincide co patrón 
5   }

Podes atopar documentación e exemplos en moitas webs ([1][2][3]

Utilización dos ficheiros de rexistro

Os ficheiros de rexistro ou logs permiten almacenar información sobre as distintas accións que realiza a aplicación. Estes ficheros permiten realizar un seguemento detallado de que acontece no sistema.

En Java, o paquete incluído por defecto para levar esta tarefa é java.util.logging. Na maioría dos proxectos, cando se require máis funcionalidade, a aposta xeneralizada e polo uso de Apache Log4j 2. Unha solución interesante para abstraernos do uso dunha ou outra implementación é Apache Commons Loggin. Podes ver argumentos a favor e en contra das diferentes opcións.

Podes ver exemplos:

  • de java.util.logging [4]
  • de Apache Log4j ([5][6])
  • de Apache Log4j 2 ([7])

Resumo

  • JCA (Java Chriptography Architecure) é unha arquitectura de Java para criptografía.
  • JSSE (Java Secure Socket Extension) é unha implementación en Java de TLS/SSL.
  • JAAS (Java Authentication and Authorization Service) proporciona
    • autenticación de usuarios que executan código de Java
    • autorización de usuarios para realizar determinadas accións

Ampliación

Actividades propostas

Xenera o resumo dun ficheiro

O ficheiro débese elixir do disco duro coa clase JFileChooser. O resumo se debe amosar en hexadecimal. Utiliza os algoritmos SHA e MD5 e comproba os resúmenes xenerados

Creación de sockets seguros

Crea unha conexión sobre un socket servidor seguro e que atenderá ata catro conexións de clientes que se identificarán cun certificado válido. O servidor agarda as conexións; cada cliente que se conecta recibe unha mensaxe e a continuación envíalle un saúdo.

Pista

Autenticación e autorización

Elabora unha aplicación que nos pida o nome de usuario e contrasinal desde unha ventana de diálogo. A autenticación farase mediante Jaas, onde os usuarios Xosé, Lois e Alba terán acceso. A continuación, haberá dous botóns:

  • amosar o número de ficheiros que hai no directorio onde se está executando o programa (poden facelo Xosé, Lois e Alba)
  • crear un ficheiro que conteña o nome dos ficheiros que hai no directorio onde se está executando o programa (só pode facelo Xosé)

Anexo I - Ferramientas de seguridade en Java

policytool
Ferrameenta gráfica que permite administrar as normas de seguridade dunha instalación local de JDK. Máis información en [8]
keytool
Permite administrar a base de datos de claves e certificados do sistema. Máis información en [9]
jarsigner
Permite asinar e verificar a sinatura dos ficheiros jar. Máis información en [10]

Referencias e créditos

JAAS

JAR

Otros