Técnicas de programación segura con Java
Sumario
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.
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áfifios (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 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:
| 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 mensajes (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( 8 “Introducir cadea de caracteres para calcular hash”); 9 return; 10 } 11 12 String cadena = args[0]; 13 for(int i=1; i<args.length; i++) { 14 cadena += “ “+args[i]; 15 } 16 MessageDigest md; 17 try { 18 byte[] bytes = cadena.getBytes(); 19 md = MessageDigest.getInstance(“SHA-256”); 20 md.update(bytes); 21 byte[] hash = md.digest(); 22 System.out.printf(“Cadea: [%s]\nHash: [%s]\n.”, 23 cadena, valorHexadecimal(hash)); 24 } catch (NoSuchAlgorithmException e) { 25 System.out.println(“Non disponible algoritmo de hash”); 26 } 27 } 28 static String valorHexadecimal(byte[] bytes) { 29 String result = “”; 30 for (byte b : bytes) { 31 result += String.format(String.format(“%x”, b)); 32 } 33 return result; 34 } 35 }
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úblia
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. Poedense 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)
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).
package consultacertificado; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.security.PublicKey; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.SignatureException; import java.security.cert.CertificateFactory; import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateExpiredException; import java.security.cert.CertificateNotYetValidException; public class ConsultaCertificado {
private static final String FICH_CERT = “certificadoX509.crt”;
private static boolean certificadoCorrecto(Certificate certif) {
boolean certifOK = true;
try {
PublicKey clavePublica = certif.getPublicKey();
certif.verify(clavePublica);
System.out.println(“Certificado autoasinado, e firma correcta.”);
if (certif.getType().equals(“X.509”)) {
X509Certificate certX509 = (X509Certificate) certif;
certX509.checkValidity();
}
} catch (NoSuchProviderException | SignatureException e) {
certifOK = false;
System.out.println(“ERROR: Excepción de certificado.”);
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
certifOK = false;
System.out.println(“ERROR: Excepción con certificado”);
e.printStackTrace();
} catch (CertificateExpiredException e) {
certifOK = false;
System.out.println(“ERROR: Certificado caducado.”);
e.printStackTrace();
} catch (CertificateNotYetValidException e) {
certifOK = false;
System.out.println(“ERROR: Certificado aínda non válido.”);
e.printStackTrace();
} catch (InvalidKeyException e) {
certifOK = false;
System.out.println(“AVISO: Certificado autoasinado e firma non correcta, ou no autoasinado (e sen verificar firma)”);
certifOK = false;
} catch (CertificateException e) {
certifOK = false;
System.out.println(“ERRO: Excepción de certificado.”);
}
return certifOK;
}
public static void main(String[] args) {
try (FileInputStream fisCertificado = new FileInputStream(FICH_CERT)) {
CertificateFactory f = CertificateFactory.getInstance(“X.509”);
Certificate certif = f.generateCertificate(fisCertificado);
if (!certificadoCorrecto(certif)) {
System.out.printf(“Certificado %s non correcto.\n”, FICH_CERT);
System.exit(1);
}
PublicKey clavePublica = certif.getPublicKey();
System.out.printf(“Clave pública leída de certificado, formato: %s.\n”,
clavePublica.getFormat());
} catch (FileNotFoundException e) {
System.out.printf(“ERRO: non existe ficheiro de certificado %s\n.”,
FICH_CERT);
} catch (IOException e) {
System.out.printf(“ERRO: de E/S lendo clave de ficheiro %s\n.”,
FICH_CERT);
} catch (CertificateException e) {
System.out.println(“ERRO: Excepción de certificado.”);
}
}
}
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.init(Cipher.ENCRYPT_MODE, clavePublica); cifrado.init(Cipher.ENCRYPT_MODE, certificado);
cifrado.init(Cipher.DECRYPT_MODE, clavePrivada); cifrado.init(Cipher.DECRYPT_MODE, certificado);
Xeneración e uso de firma dixital
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.
Control de acceso con JAAS
JAAS (Java Authentication and Authorization Service, servicio de autorización y autenticación de Java) es una interfaz que pennite a las aplicaciones Java acceder a servicios de control de autenticación y acceso. Pódese utllizar para dous propósitos:
- para a autenticación de usuarios: para detenninar de forma fiable e segura quén está executando o noso código Java
- 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
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
- Invesigar o uso de Apache Shiro, Spring Security e OACC como substitutos de JAAS.
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.
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é)
Referencias e créditos
- Encriptación con Java
- Socket SSL con Java
- JSSE y JAAS
- Java Security Libraries
- Java Cryptography Architecture (JCA) Reference Guide
- Java Secure Socket Extension (JSSE) Reference Guide
- Java Authentication and Authorization Service (JAAS) Reference Guide
- JAAS Exemplo
- Using a CallBackHandler in Java Authentication and Authorization Services (JAAS)
- Online Tools