Firma digitale di file PDF con iText

Spread the love

Questo esercizio è tratto da

Capitolo 1 – Utilizzo delle funzioni di crittografia

La prima classe da scrivere calcola firme di file.

In inglese la firma elettronica si chiama anche digest, come Reader’s Digest, la rivista americana di estrema destra che significa “Il digesto del Lettore” o, meglio, “Il riassunto del Lettore”. DeWitt Wallace, il fondatore, voleva fare una rivista che pubblicasse riassunti di articoli di altre riviste.

Digest quindi come riassunto, sintesi: è una funzione di hash crittografico che mappa messaggi lunghi in messaggi corti a lunghezza fissa facendo in modo da evitare collisioni (la probabilità che due file diversi generino lo stesso digest è molto piccola) e in modo tale da rendere impossibile (sarebbe più onesto dire “molto difficile”) risalire al contenuto di un file dalla sua firma.

I package che vedremo supportano diversi tipi di digest.

Calcolo della firma di una stringa

Realizziamo questa cosa con la classe java C1_01_DigestDefault:

package com.itextpdf.samples.signatures.chapter01;

import java.io.File;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.util.Arrays;

public class C1_01_DigestDefault {
    public static final String DEST = "./target/test/resources/signatures/chapter01/";

    public static final String EXPECTED_OUTPUT = "Digest using MD5: 16\n" +
            "Digest: 5f4dcc3b5aa765d61d8327deb882cf99\n" +
            "Is the password 'password'? true\n" +
            "Is the password 'secret'? false\n" +
            "Digest using SHA-1: 20\n" +
            "Digest: 5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8\n" +
            "Is the password 'password'? true\n" +
            "Is the password 'secret'? false\n" +
            "Digest using SHA-224: 28\n" +
            "Digest: d63dc919e201d7bc4c825630d2cf25fdc93d4b2f0d46706d29038d01\n" +
            "Is the password 'password'? true\n" +
            "Is the password 'secret'? false\n" +
            "Digest using SHA-256: 32\n" +
            "Digest: 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8\n" +
            "Is the password 'password'? true\n" +
            "Is the password 'secret'? false\n" +
            "Digest using SHA-384: 48\n" +
            "Digest: a8b64babd0aca91a59bdbb7761b421d4f2bb38280d3a75ba0f21f2bebc4558" +
            "3d446c598660c94ce680c47d19c30783a7\n" +
            "Is the password 'password'? true\n" +
            "Is the password 'secret'? false\n" +
            "Digest using SHA-512: 64\n" +
            "Digest: b109f3bbbc244eb82441917ed06d618b9008dd09b3befd1b5e07394c706a8bb980b" +
            "1d7785e5976ec049b46df5f1326af5a2ea6d103fd07c95385ffab0cacbc86\n" +
            "Is the password 'password'? true\n" +
            "Is the password 'secret'? false\n" +
            "RIPEMD128 MessageDigest not available\n" +
            "RIPEMD160 MessageDigest not available\n" +
            "RIPEMD256 MessageDigest not available\n";

    protected byte[] digest;
    protected MessageDigest messageDigest;

    protected C1_01_DigestDefault(String password, String algorithm, String provider) throws GeneralSecurityException,
            UnsupportedEncodingException {
        if (provider == null) {
            messageDigest = MessageDigest.getInstance(algorithm);
        } else {
            messageDigest = MessageDigest.getInstance(algorithm, provider);
        }
        digest = messageDigest.digest(password.getBytes("UTF-8"));
    }

    public static C1_01_DigestDefault getInstance(String password, String algorithm) throws GeneralSecurityException,
            UnsupportedEncodingException {
        return new C1_01_DigestDefault(password, algorithm, null);
    }

    public static void main(String[] args) {
        File file = new File(DEST);
        file.mkdirs();

        testAll();
    }

    public static void testAll() {
        showTest("MD5");
        showTest("SHA-1");
        showTest("SHA-224");
        showTest("SHA-256");
        showTest("SHA-384");
        showTest("SHA-512");
        showTest("RIPEMD128");
        showTest("RIPEMD160");
        showTest("RIPEMD256");
    }

    public static void showTest(String algorithm) {
        try {
            C1_01_DigestDefault app = getInstance("password", algorithm);
            System.out.println("Digest using " + algorithm + ": " + app.getDigestSize());
            System.out.println("Digest: " + app.getDigestAsHexString());
            System.out.println("Is the password 'password'? " + app.checkPassword("password"));
            System.out.println("Is the password 'secret'? " + app.checkPassword("secret"));
        } catch (Exception exc) {
            System.out.println(exc.getMessage());
        }
    }

    public int getDigestSize() {
        return digest.length;
    }

    public String getDigestAsHexString() {
        return new BigInteger(1, digest).toString(16);
    }

    /* This method checks if the digest of the password is equal
     * to the digest of the text line which is passed as argument
     */
    public boolean checkPassword(String password) {
        return Arrays.equals(digest, messageDigest.digest(password.getBytes()));
    }
}

L’output del meodo main() di questa classe è il seguente

 Digest using MD5: 16
 Digest: 5f4dcc3b5aa765d61d8327deb882cf99
 Is the password 'password'? true
 Is the password 'secret'? false
 Digest using SHA-1: 20
 Digest: 5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8
 Is the password 'password'? true
 Is the password 'secret'? false
 Digest using SHA-224: 28
 Digest: d63dc919e201d7bc4c825630d2cf25fdc93d4b2f0d46706d29038d01
 Is the password 'password'? true
 Is the password 'secret'? false
 Digest using SHA-256: 32
 Digest: 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8
 Is the password 'password'? true
 Is the password 'secret'? false
 Digest using SHA-384: 48
 Digest: a8b64babd0aca91a59bdbb7761b421d4f2bb38280d3a75ba0f21f2bebc45583d446c598660c94ce680c47d19c30783a7
 Is the password 'password'? true
 Is the password 'secret'? false
 Digest using SHA-512: 64
 Digest: b109f3bbbc244eb82441917ed06d618b9008dd09b3befd1b5e07394c706a8bb980b1d7785e5976ec049b46df5f1326af5a2ea6d103fd07c95385ffab0cacbc86
 Is the password 'password'? true
 Is the password 'secret'? false
 RIPEMD128 MessageDigest not available
 RIPEMD160 MessageDigest not available
 RIPEMD256 MessageDigest not available
 Process finished with exit code 0

Stesso calcolo ma utilizzando la libreria di crittografia Bouncy Castle

Per poter utilizzare questa libreria occorre cercare nel JAR finder questo package: bcprov-jdk15on-169.jar

Io, utilizzando come jvm quella disponbile nel JDK1.8 (/usr/lib/jvm/jdk1.8.0_111/), sistemo gli script delle librerie sotto

/usr/lib/jvm/jdk1.8.0_111/jre/lib/ext

una volta sistemato lì, l’IDE sarà in grado di trovarlo, una volta che si è specificata la directory nella configurazione (Ctrl+Alt+Maius+S – Tab “Dependencies”, aggiunta nuova dependency (plsante +) – oppure Alt+Ins).

La libreria Bouncy Castle (“A lightweight cryptography API for Java and C#”) fornisce delle API generali per la crittografia.

package com.itextpdf.samples.signatures.chapter01;

import java.io.File;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.Provider;
import java.util.Arrays;

import org.bouncycastle.jce.provider.BouncyCastleProvider;

public class C1_02_DigestBC {
    public static final String DEST = "./target/test/resources/signatures/chapter01/";

    public static final BouncyCastleProvider PROVIDER = new BouncyCastleProvider();

    public static final String EXPECTED_OUTPUT = "Digest using MD5: 16\n" +
            "Digest: 5f4dcc3b5aa765d61d8327deb882cf99\n" +
            "Is the password 'password'? true\n" +
            "Is the password 'secret'? false\n" +
            "Digest using SHA-1: 20\n" +
            "Digest: 5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8\n" +
            "Is the password 'password'? true\n" +
            "Is the password 'secret'? false\n" +
            "Digest using SHA-224: 28\n" +
            "Digest: d63dc919e201d7bc4c825630d2cf25fdc93d4b2f0d46706d29038d01\n" +
            "Is the password 'password'? true\n" +
            "Is the password 'secret'? false\n" +
            "Digest using SHA-256: 32\n" +
            "Digest: 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8\n" +
            "Is the password 'password'? true\n" +
            "Is the password 'secret'? false\n" +
            "Digest using SHA-384: 48\n" +
            "Digest: a8b64babd0aca91a59bdbb7761b421d4f2bb38280d3a75ba0f21f2bebc45583d446c" +
            "598660c94ce680c47d19c30783a7\n" +
            "Is the password 'password'? true\n" +
            "Is the password 'secret'? false\n" +
            "Digest using SHA-512: 64\n" +
            "Digest: b109f3bbbc244eb82441917ed06d618b9008dd09b3befd1b5e07394c706a8bb980b1d7" +
            "785e5976ec049b46df5f1326af5a2ea6d103fd07c95385ffab0cacbc86\n" +
            "Is the password 'password'? true\n" +
            "Is the password 'secret'? false\n" +
            "Digest using RIPEMD128: 16\n" +
            "Digest: c9c6d316d6dc4d952a789fd4b8858ed7\n" +
            "Is the password 'password'? true\n" +
            "Is the password 'secret'? false\n" +
            "Digest using RIPEMD160: 20\n" +
            "Digest: 2c08e8f5884750a7b99f6f2f342fc638db25ff31\n" +
            "Is the password 'password'? true\n" +
            "Is the password 'secret'? false\n" +
            "Digest using RIPEMD256: 32\n" +
            "Digest: f94cf96c79103c3ccad10d308c02a1db73b986e2c48962e96ecd305e0b80ef1b\n" +
            "Is the password 'password'? true\n" +
            "Is the password 'secret'? false\n";

    protected byte[] digest;
    protected MessageDigest messageDigest;

    protected C1_02_DigestBC(String password, String algorithm, Provider provider) throws GeneralSecurityException,
            UnsupportedEncodingException {
        if (provider == null) {
            messageDigest = MessageDigest.getInstance(algorithm);
        } else {

            // BouncyCastle provider can be initialized in another way
            // by using Security.addProvider(Provider provider) method
            messageDigest = MessageDigest.getInstance(algorithm, provider);
        }
        digest = messageDigest.digest(password.getBytes("UTF-8"));
    }

    public static C1_02_DigestBC getInstance(String password, String algorithm) throws GeneralSecurityException,
            UnsupportedEncodingException {
        return new C1_02_DigestBC(password, algorithm, PROVIDER);
    }

    public static void main(String[] args) throws Exception {
        File file = new File(DEST);
        file.mkdirs();

        testAll();
    }

    public static void testAll() throws Exception {
        showTest("MD5");
        showTest("SHA-1");
        showTest("SHA-224");
        showTest("SHA-256");
        showTest("SHA-384");
        showTest("SHA-512");
        showTest("RIPEMD128");
        showTest("RIPEMD160");
        showTest("RIPEMD256");

    }

    public static void showTest(String algorithm) throws Exception {
        C1_02_DigestBC app = getInstance("password", algorithm);
        System.out.println("Digest using " + algorithm + ": " + app.getDigestSize());
        System.out.println("Digest: " + app.getDigestAsHexString());
        System.out.println("Is the password 'password'? " + app.checkPassword("password"));
        System.out.println("Is the password 'secret'? " + app.checkPassword("secret"));
    }

    public int getDigestSize() {
        return digest.length;
    }

    public String getDigestAsHexString() {
        return new BigInteger(1, digest).toString(16);
    }

    /* This method checks if the digest of the password is equal
     * to the digest of the text line which is passed as argument
     */
    public boolean checkPassword(String password) {
        return Arrays.equals(digest, messageDigest.digest(password.getBytes()));
    }
}

A differenza del default, qui al costruttore passiamo anche l’oggetto BouncyCastleProvider istanza della classe omonima, e lo passiamo esplicitamente al metodo che crea l’oggetto Digest (MessageDigest).

Anche stavolta l’output è quello atteso

Digest using MD5: 16
 Digest: 5f4dcc3b5aa765d61d8327deb882cf99
 Is the password 'password'? true
 Is the password 'secret'? false
 Digest using SHA-1: 20
 Digest: 5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8
 Is the password 'password'? true
 Is the password 'secret'? false
 Digest using SHA-224: 28
 Digest: d63dc919e201d7bc4c825630d2cf25fdc93d4b2f0d46706d29038d01
 Is the password 'password'? true
 Is the password 'secret'? false
 Digest using SHA-256: 32
 Digest: 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8
 Is the password 'password'? true
 Is the password 'secret'? false
 Digest using SHA-384: 48
 Digest: a8b64babd0aca91a59bdbb7761b421d4f2bb38280d3a75ba0f21f2bebc45583d446c598660c94ce680c47d19c30783a7
 Is the password 'password'? true
 Is the password 'secret'? false
 Digest using SHA-512: 64
 Digest: b109f3bbbc244eb82441917ed06d618b9008dd09b3befd1b5e07394c706a8bb980b1d7785e5976ec049b46df5f1326af5a2ea6d103fd07c95385ffab0cacbc86
 Is the password 'password'? true
 Is the password 'secret'? false
 Digest using RIPEMD128: 16
 Digest: c9c6d316d6dc4d952a789fd4b8858ed7
 Is the password 'password'? true
 Is the password 'secret'? false
 Digest using RIPEMD160: 20
 Digest: 2c08e8f5884750a7b99f6f2f342fc638db25ff31
 Is the password 'password'? true
 Is the password 'secret'? false
 Digest using RIPEMD256: 32
 Digest: f94cf96c79103c3ccad10d308c02a1db73b986e2c48962e96ecd305e0b80ef1b
 Is the password 'password'? true
 Is the password 'secret'? false
 Process finished with exit code 0

Classe di encryption/decription

package com.itextpdf.samples.signatures.chapter01;

import javax.crypto.Cipher;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.cert.X509Certificate;

public class C1_03_EncryptDecrypt {
    public static final String DEST = "./target/test/resources/signatures/chapter01/";

    protected static final String KEYSTORE = "./src/test/resources/encryption/ks";

    protected static final String PASSWORD = "password";
    protected KeyStore ks;

    public static void main(String[] args) throws Exception {
        File file = new File(DEST);
        file.mkdirs();

        encryptDecrypt();
    }

    public static void encryptDecrypt() throws GeneralSecurityException, IOException {
        C1_03_EncryptDecrypt app = new C1_03_EncryptDecrypt();
        app.initKeyStore(KEYSTORE, PASSWORD);
        Key publicKey = app.getPublicKey("demo");
        Key privateKey = app.getPrivateKey("demo", "password");

        // Encrypt the message with the public key and then decrypt it with the private key
        System.out.println("Let's encrypt 'secret message' with a public key");
        byte[] encrypted = app.encrypt(publicKey, "secret message");
        System.out.println("Encrypted message: " + app.getDigestAsHexString(encrypted));
        System.out.println("Let's decrypt it with the corresponding private key");
        String decrypted = app.decrypt(privateKey, encrypted);
        System.out.println(decrypted);

        // Encrypt the message with the private key and then decrypt it with the public key
        System.out.println("You can also encrypt the message with a private key");
        encrypted = app.encrypt(privateKey, "secret message");
        System.out.println("Encrypted message: " + app.getDigestAsHexString(encrypted));
        System.out.println("Now you need the public key to decrypt it");
        decrypted = app.decrypt(publicKey, encrypted);
        System.out.println(decrypted);
    }

    private void initKeyStore(String keystore, String ks_pass) throws GeneralSecurityException, IOException {
        ks = KeyStore.getInstance(KeyStore.getDefaultType());
        ks.load(new FileInputStream(keystore), ks_pass.toCharArray());
    }

    private String getDigestAsHexString(byte[] digest) {
        return new BigInteger(1, digest).toString(16);
    }

    private X509Certificate getCertificate(String alias) throws KeyStoreException {
        return (X509Certificate) ks.getCertificate(alias);
    }

    private Key getPublicKey(String alias) throws GeneralSecurityException, IOException {
        return getCertificate(alias).getPublicKey();
    }

    private Key getPrivateKey(String alias, String pk_pass) throws GeneralSecurityException, IOException {
        return ks.getKey(alias, pk_pass.toCharArray());
    }

    // This method encrypts the message (using RSA algorithm) with the key, got as the 1st argument
    public byte[] encrypt(Key key, String message) throws GeneralSecurityException {
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.ENCRYPT_MODE, key);
        byte[] cipherData = cipher.doFinal(message.getBytes());
        return cipherData;
    }

    // This method decrypts the message (using RSA algorithm) with the key, got as the 1st argument
    public String decrypt(Key key, byte[] message) throws GeneralSecurityException {
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.DECRYPT_MODE, key);
        byte[] cipherData = cipher.doFinal(message);
        return new String(cipherData);
    }
}

Il cui output è

Let's encrypt 'secret message' with a public key
 Encrypted message: 18d8b419247c13816563897d0f3cef7ff917be86e7a20528133b7a95377c1690f2326e4cbc9c001e6cb877c5b3645c5f395b4cd48ade4a87651052d09810e4b423aa973c69853c4e0efde238d9b4c038ecd1a53e81b19ce44248ee67056d7d74edb381b29bfc5a7cbad67a147e06e02d7ff65f4fa52718ee7f3265a48c4c146bc0ae98762d57e6b82a77bee31ea1749a47a32c2936df7914d361853c17c46b8064300865c3f763e6a87a1d42594bf254e880b6181946890199e7ffbe26f3f59ce74627594eca7c3a590a4eaa31efeb7d95b2366dd2bafe921c5497bde7e827fa87bec87a26f887434b751555189e3e244f44ea6d63b725709fe8c68ed98c98d8
 Let's decrypt it with the corresponding private key
 secret message
 You can also encrypt the message with a private key
 Encrypted message: 1ecff10fc820eea4647060890ead7669f0f845cb0c42ea8ede8a962d9848dc30bd3ba34113f858264cca1ea195dd30beeff15dae1100c68a75790c0fbba8739791f01a236174e7e1593336def60793713a95c8e4ea53cf1527d73fea52468a11974d8786de7945e3f5e7a0e3f27e6488997811fc6781c4909d27fb0224340a2e40e4e49e5023a5cc9f8531e1ee2d5b763049dac0197b0b252564d1e556a2b5de2c2026c95f94f9de5ea63d95c231c9611eb92736df617474af78c86963d3381738de84d10bf594b07d10edee2aab752c43501cc7356d4cbd0826de2c461828dba4f47bae564defc80b849c27b0dfb1cbbdffabdeb94d71ccff26810986ae2c22
 Now you need the public key to decrypt it
 secret message
 Process finished with exit code 0

Abbiamo quindi cifrato e dicifrato un semlice messaggio “secret message”.

Alla fine, firmiamo il pdf

Posizioniamo il documento da firmare (HelloWorld.pdf) nella directory

./src/test/resources/pdfs/

e lo firmiamo in 4 modi diversi utiizzando gli algoritmi di firma SHA256 (standrd CMS e CADES), SHA512 (CMS) e RIPEMD160 (CADES).

package com.itextpdf.samples.signatures.chapter02;

import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.StampingProperties;
import com.itextpdf.signatures.*;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.Certificate;

public class C2_01_SignHelloWorld {
    public static final String DEST = "./target/signatures/chapter02/";

    public static final String KEYSTORE = "./src/test/resources/encryption/ks";
    public static final String SRC = "./src/test/resources/pdfs/HelloWorld.pdf";

    public static final char[] PASSWORD = "password".toCharArray();

    public static final String[] RESULT_FILES = new String[] {
            "hello_signed1.pdf",
            "hello_signed2.pdf",
            "hello_signed3.pdf",
            "hello_signed4.pdf"
    };

    public void sign(String src, String dest, Certificate[] chain, PrivateKey pk, String digestAlgorithm,
                     String provider, PdfSigner.CryptoStandard signatureType, String reason, String location)
            throws GeneralSecurityException, IOException {
        PdfReader reader = new PdfReader(src);
        PdfSigner signer = new PdfSigner(reader, new FileOutputStream(dest), new StampingProperties());

        // Create the signature appearance
        Rectangle rect = new Rectangle(36, 648, 200, 100);
        PdfSignatureAppearance appearance = signer.getSignatureAppearance();
        appearance
                .setReason(reason)
                .setLocation(location)

                // Specify if the appearance before field is signed will be used
                // as a background for the signed field. The "false" value is the default value.
                .setReuseAppearance(false)
                .setPageRect(rect)
                .setPageNumber(1);
        signer.setFieldName("sig");

        IExternalSignature pks = new PrivateKeySignature(pk, digestAlgorithm, provider);
        IExternalDigest digest = new BouncyCastleDigest();

        // Sign the document using the detached mode, CMS or CAdES equivalent.
        signer.signDetached(digest, pks, chain, null, null, null, 0, signatureType);
    }

    public static void main(String[] args) throws GeneralSecurityException, IOException {
        File file = new File(DEST);
        file.mkdirs();

        BouncyCastleProvider provider = new BouncyCastleProvider();
        Security.addProvider(provider);
        KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
        ks.load(new FileInputStream(KEYSTORE), PASSWORD);
        String alias = ks.aliases().nextElement();
        PrivateKey pk = (PrivateKey) ks.getKey(alias, PASSWORD);
        Certificate[] chain = ks.getCertificateChain(alias);

        C2_01_SignHelloWorld app = new C2_01_SignHelloWorld();
        app.sign(SRC, DEST + RESULT_FILES[0], chain, pk, DigestAlgorithms.SHA256, provider.getName(),
                PdfSigner.CryptoStandard.CMS, "Prova 1", "Venezia");
        app.sign(SRC, DEST + RESULT_FILES[1], chain, pk, DigestAlgorithms.SHA512, provider.getName(),
                PdfSigner.CryptoStandard.CMS, "Prova 2", "Venezia");
        app.sign(SRC, DEST + RESULT_FILES[2], chain, pk, DigestAlgorithms.SHA256, provider.getName(),
                PdfSigner.CryptoStandard.CADES, "Prova 3", "Venezia");
        app.sign(SRC, DEST + RESULT_FILES[3], chain, pk, DigestAlgorithms.RIPEMD160, provider.getName(),
                PdfSigner.CryptoStandard.CADES, "Prova 4", "Venezia");
    }
}

I file che otteniamo in output sono questi:

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.

Questo sito usa Akismet per ridurre lo spam. Scopri come i tuoi dati vengono elaborati.