Salta el contingut
 

Certificats a Java

Autor: Joan Puigcerver Ibáñez

Correu electrònic: j.puigcerveribanez@edu.gva.es

Llicència: CC BY-NC-SA 4.0

(Reconeixement - NoComercial - CompartirIgual) 🅭

Certificats a Java

Un certificat és un document digital que s'utilitza per demostrar la identitat d'una entitat, com ara un lloc web o una persona. Conté informació sobre la identitat de l'entitat, així com la clau pública de l'entitat.

Els certificats els emet un tercer de confiança anomenat autoritat de certificació (CA), que verifica la identitat de l'entitat abans d'emetre el certificat.

Encara que normalment els certificats els emet una CA, qualsevol persona pot generar certificats i allotjar una CA.

Eina KeyTool

Documentació

L'eina proporciona una sèrie d'accions a realitzar.

Podeu consultar la llista completa a la documentació oficial: https://docs.oracle.com/en/java/javase/11/tools/keytool.html

L'eina KeyTool ve incorporada amb el JDK de Java. Per poder utilitzar-la, comproveu que la carpeta <JDK Installation Folder>/bin està afegida a la variable PATH del sistema.

Configuració del sistema on s'ha afegit la carpeta d'instal·lació de Java a la variable PATH

Figura 1. Configuració del sistema on s'ha afegit la carpeta d'instal·lació de Java a la variable PATH

Consell

Com configurar les variables d'entorn del sistema a Windows 11:

Generar un certificat

Per poder generar un certificat, podem utilitzar la següent comanda com example:

keytool -genkey -keyalg RSA \
    -alias example \
    -keystore files/ud4/example_keystore.jks \
    -validity 360 \
    -storepass 123456 -keysize 2048 \
    -dname "CN=Example, O=CIPFP Mislata, OU=PSP-DAM2S, L=Mislata, ST=València, C=ES"
Aquesta comanda s'utilitza per generar un parell de claus RSA de 2048 bits i guardar-les en un magatzem de claus Java KeyStore amb el nom keystore.jks i identificat amb el àlies example que serà vàlid durant 360 dies.

Les opcions utilitzades són:

  • keytool es la comanda de la utilitat Java KeyTool.
  • -genkey es l'opció per indicar que es genere un nou parell de claus.
  • -keyalg RSA especifica que les claus han d'utilitzar l'algorisme RSA.
  • -alias especifica l'àlies que identifica el certificat en el magatzem de claus KeyStore.
  • -keypass especifica la contrasenya del certificat.
  • -keystore especifica el nom del magatzem de claus on es guardarà el certificat.
  • -storepass especifica la contrasenya que protegeix el magatzem de claus.
  • -validity 360 especifica el nombre de dies que el certificat serà vàlid.
  • -keysize 2048 especifica la mida en bits de el parell de claus.
  • -dname permet especificar la informació del certificat.
  • CN o Common Name: Identificació del certificat.
  • O o Organization: Organització a qui pertany el certificat.
  • OU o Organizational Unit: Unitat dins de la organització.
  • L o Localitat
  • ST o State: Estat o província.
  • C o Country: País.

Llistar els certificats

Per llistar els certificats d'un magatzem de claus, podem utilitzar la següent comanda:

keytool -list -keystore files/ud4/example_keystore.jks -storepass 123456

Exportar certificats

Per exportar certificats, es pot utilitzar l'opció -export de l'eina KeyTool. Aquesta acció exportarà el certificat amb la clau pública.

En aquest exemple, exportem el certificat generat en l'apartat anterior:

keytool -export -alias example \
    -keystore files/ud4/example_keystore.jks \
    -file files/ud4/example_certificate.crt \
    -storepass 123456

Aquesta comanda exporta el certificat identificat per l'àlies example de el magatzem de claus ubicat a example_keystore.jks al fitxer example_certificate.crt.

Les opcions utilitzades són:

  • keytool es la comanda de la utilitat Java KeyTool.
  • -export es l'opció per indicar que s'exporte un certificat.
  • -alias especifica l'àlies que identifica el certificat en el magatzem de claus KeyStore que es desitja exportar.
  • -keystore especifica el nom del magatzem de claus des d'on s'exportarà el certificat.
  • -file especifica el fitxer on el certificat serà exportat.

Importar certificats

Per importar certificats, es pot utilitzar l'opció -import de l'eina KeyTool.

En aquest exemple, importarem un nou certificat al magatzem de claus generat en l'apartat anterior:

keytool -import -alias example \
    -keystore files/ud4/example_truststore.jks \
    -file files/ud4/example_certificate.crt \
    -storepass 123456

Aquesta comanda importa el certificat del fitxer example_certificate.crt i el emmagatzemarà en el magatzem de claus ubicat a example_truststore.jks i l'identificarà per l'àlies example.

Les opcions utilitzades són:

  • keytool es la comanda de la utilitat Java KeyTool.
  • -import es l'opció per indicar que s'importe un certificat.
  • -alias especifica l'àlies que identifica el certificat importat en el magatzem de claus KeyStore
  • -keystore especifica el nom del magatzem de claus on s'importarà el certificat.
  • -file especifica el fitxer on el certificat importat.

Certificats JKS en Java

Per treballar amb els certificats generats amb l'eina KeyTool en Java, cal treballar amb els magatzems de claus KeyStore.

Carregar una KeyStore

Per carregar un magatzem de claus KeyStore des d'un fitxer, podem utilitzar el següent mètode.

public KeyStore loadKeyStore(String ksFile, String ksPwd) throws Exception {
    KeyStore ks = KeyStore.getInstance("JKS");
    File f = new File (ksFile);
    if (f.isFile()) {
        FileInputStream in = new FileInputStream (f);
        ks.load(in, ksPwd.toCharArray()); 
    }
    return ks;    
}

Una vegada ja s'ha carregat el magatzem, interactuar amb ell per poder accedir a les seves entrades, indexades amb un àlies.

Documentació

Documentació API Java KeyStore

Entre els mètodes més significatius, podem trobar:

  • int size(): consulta el nombre d’entrades al magatzem.
  • Key getKey(String alias, char[] password): obté la clau (simètrica o privada, el que corresponga per l’entrada) associada a un àlies, si existeix. Si la seva entrada està protegida amb contrasenya, cal proporcionar-la al segon paràmetre. Una clau simètrica és de tipus SecretKey, mentre que una de privada és de tipus PrivateKey.
  • Certificate getCertificate(String alias): permet extreure un certificat guardat a un àlies concret.

Encriptació utilitzant un certificat

Els certificats inclouen una clau pública PublicKey i una clau privada PrivateKey, que poden ser utilitzades per encriptar i desencriptar missatges mitjançant l'algorisme RSA.

Signatura utilitzant un certificat

La clau pública i privada d'un certificat també serveixen per signar un missatge i verificar que la signatura és correcta.

El procés és:

  • L'emissor signa un missatge amb la seua clau privada.
  • La signatura és generada a partir de la clau privada i el contingut del missatge.
  • L'emissor envia el missatge i la signatura al receptor.
  • El receptor verifica la signatura a partir de la clau pública de l'emissor i el missatge rebut.

Si la signatura és vàlida, significa que el missatge rebut és el mateix que el missatge enviat per l'emissor. Si la signatura és invàlida, significa que el missatge rebut no és el mateix missatge enviat per l'emissor i ha pogut ser modificat per tercers.

Els següents mètodes permeten signar i comprovar la signatura d'un String fàcilment. No obstant això, qualsevol byte[] pot ser signat i verificat.

public static String signText(PrivateKey privateKey, String text) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
    Signature signature = Signature.getInstance("SHA256withRSA");
    signature.initSign(privateKey);
    signature.update(text.getBytes());
    byte[] signatureBytes = signature.sign();
    return Base64.getEncoder().encodeToString(signatureBytes);
}

public static boolean verifySignature(PublicKey publicKey, String text, String signed) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
    Signature signature = Signature.getInstance("SHA256withRSA");
    signature.initVerify(publicKey);
    signature.update(text.getBytes());

    byte[] signatureBytes = Base64.getDecoder().decode(signed);

    return signature.verify(signatureBytes);
}

Fitxer amb la configuració

En aquest exemple s'ha utilitzat un fitxer de configuració .properties per especificar les dades sensibles o que poden canviar ens els diferents entorns (desenvolupament, test, producció).

Utilitzar fitxers de configuració es considera una bona pràctica, ja que permet configurar diferents dades per diferents entorns i evita que es puguen extraure dades sensibles del codi font o dels executables (utilitzant mètodes com la descompilació).

En el nostre entorn, hem creat el fitxer application.properties, on s'ha guardat la informació:

ud4.examples.keystore.passwd=123456

Després, hem definit la classe Config que permet llegir la configuració d'un fitxer.

Config.java
package ud4.examples;

import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;

public class Config {
    public static Properties getConfig(String path) throws IOException {
        Properties properties = new Properties();
        properties.load(new FileInputStream(path));
        return properties;
    }
}

Llibreria d'utilitats en Java

Aquesta classe recopila els mètodes anteriors per poder ser utilitzats.

A més, inclou un exemple d'utilització.

CertificateUtils.java
package ud4.examples;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.*;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.Properties;

public class CertificateUtils {
    public static KeyStore loadKeyStore(String ksFile, String ksPwd) throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException {
        KeyStore ks = KeyStore.getInstance("JKS");
        File f = new File (ksFile);
        if (f.isFile()) {
            FileInputStream in = new FileInputStream (f);
            ks.load(in, ksPwd.toCharArray());
        }
        return ks;
    }

    public static String getCertificateInfo(Certificate certificate){
        X509Certificate cert = (X509Certificate) certificate;
        return cert.getSubjectX500Principal().getName();
    }

    public static String signText(PrivateKey privateKey, String text) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
        Signature signature = Signature.getInstance("SHA256withRSA");
        signature.initSign(privateKey);
        signature.update(text.getBytes());
        byte[] signatureBytes = signature.sign();
        return Base64.getEncoder().encodeToString(signatureBytes);
    }

    public static boolean verifySignature(PublicKey publicKey, String text, String signed) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
        Signature signature = Signature.getInstance("SHA256withRSA");
        signature.initVerify(publicKey);
        signature.update(text.getBytes());

        byte[] signatureBytes = Base64.getDecoder().decode(signed);

        return signature.verify(signatureBytes);
    }

    public static void main(String[] args) {
        try {
            /*
             * Guardar contrasenyes en el codi NO ÉS UNA BONA PRÀCTICA,
             * cal utilitzar variables d'entorn o un fitxer de configuració.
             */
            Properties config = Config.getConfig("application.properties");
            String keyStorePassword = config.getProperty("ud4.examples.keystore.passwd");
            KeyStore keyStore = loadKeyStore("files/ud4/example_keystore.jks", keyStorePassword);

            List<String> aliases = Collections.list(keyStore.aliases());
            System.out.println("Certificats en el magatzem de claus.");
            System.out.printf("Total: %d\n", keyStore.size());
            for(String alias : aliases)
                System.out.println("- " + alias);

            /*
             El certificat ha segut creat prèviament amb la comanda:
             ```
            keytool -genkey -keyalg RSA \
                -alias example \
                -keystore files/ud4/example_keystore.jks \
                -validiry 360 \
                -storepass 123456 -keysize 2048 \
                -dname "CN=Example, O=CIPFP Mislata, OU=PSP-DAM2S, L=Mislata, ST=València, C=ES"
             ```

             Per poder llegir informació sobre el subjecte del certificat,
             necessitem utilitzar l'objecte X509Certificate.
             */
            for (String alias : aliases) {
                Certificate certificate = keyStore.getCertificate(alias);
                System.out.println(getCertificateInfo(certificate));

                // Clau pública del certificat
                PublicKey examplePublic = certificate.getPublicKey();

                // Clau privada
                PrivateKey examplePrivate = (PrivateKey) keyStore.getKey(alias, keyStorePassword.toCharArray());

                String message = "This is a secret information.";
                System.out.printf("Message: %s\n", message);

                String encrypted = RSA.encrypt(examplePublic, message);
                System.out.printf("Encrypted: %s\n", encrypted);

                String decrypted = RSA.decrypt(examplePrivate, encrypted);
                System.out.printf("Decrypted: %s\n", decrypted);

                String signautre = signText(examplePrivate, message);
                System.out.printf("Signed: %s\n", signautre);

                System.out.println("Signature verification: " + verifySignature(examplePublic, message, signautre));
            }
        } catch (Exception e) {
            System.err.println(e.getMessage());
        }
    }
}

Comentaris