sobota, 8 stycznia 2011

Cyfrowe podpisywanie plików PDF

Dzisiejszy post trochę odbiega od poprzednich, niemniej jednak uznałem, że temat jest warty uwagi. W tym tygodniu zajmowałem się podpisywaniem plików PDF przy pomocy klucza prywatnego i weryfikacją takiego podpisu w oparciu o klucz publiczny.

Zacznijmy od wygenerowania repozytorium z parą takich kluczy przy pomocy keytool'a

keytool -genkey -alias kuba  -keypass demokeypass  -storepass demostorepass  -keystore demo-keystore.jks -validity 360 -dname "CN=Jakub Pawlowski, OU=blog, O=javaspotlight.blogspot.com, L=Lodz, S=Lodzkie, C=PL"

Następnie należy wyeksportować klucz publiczny
keytool -export -alias kuba  -keypass demokeypass  -storepass demostorepass  -keystore demo-keystore.jks -file demo-cert.cer

Poniższe polecenia umożliwiają weryfikacje zawartości obu plików
keytool -list -alias kuba -storepass demostorepass -v -keystore ~/tmp/demo-keystore.jks

keytool -printcert -v -file ~/tmp/demo-cert.cer

Do podpisania PDF'a użyłem oczywiście biblioteki iText (wymagana jest także biblioteka Bouncy Castle).
Klasa odpowiedzialna za podpisanie PDF'a wygląda następująco
import com.itextpdf.text.DocumentException;
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.PdfSignatureAppearance;
import com.itextpdf.text.pdf.PdfStamper;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.UnrecoverableEntryException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.util.logging.Logger;

public class PdfSigner {

    private static final Logger log = Logger.getLogger(PdfSigner.class.getName());
    private static final String PDF_PATH = "/home/kuba/tmp/demo-doc.pdf";
    private static final String PDF_PATH_SIGNED = "/home/kuba/tmp/demo-doc-signed.pdf";
    private static final String KEYSTORE_PATH = "/home/kuba/tmp/demo-keystore.jks";
    private static final String KEYSTORE_PASSWORD = "demostorepass";
    private static final String KEY_ALIAS = "kuba";
    private static final String KEY_PASSWORD = "demokeypass";

    private Certificate[] certificates;
    PrivateKey privateKey;

    public PdfSigner() {

        try {
             KeyStore ks = KeyStore.getInstance("JKS");
             FileInputStream fis = new FileInputStream(KEYSTORE_PATH);
             ks.load(fis, KEYSTORE_PASSWORD.toCharArray());
             certificates = ks.getCertificateChain(KEY_ALIAS);
             privateKey = (PrivateKey)ks.getKey(KEY_ALIAS, KEY_PASSWORD.toCharArray());

        } catch (FileNotFoundException ex) {
            log.severe(ex.getMessage());
        } catch (KeyStoreException ex) {
            log.severe(ex.getMessage());
        } catch (IOException ex) {
            log.severe(ex.getMessage());
        } catch (NoSuchAlgorithmException ex) {
            log.severe(ex.getMessage());
        } catch (CertificateException ex) {
           log.severe(ex.getMessage());
        } catch (UnrecoverableEntryException ex) {
          log.severe(ex.getMessage());
        }

    }


    public void signPdf(){

        try {

             PdfReader pdfReader = new PdfReader(PDF_PATH);
             FileOutputStream fos = new FileOutputStream(PDF_PATH_SIGNED);
             PdfStamper stamper = PdfStamper.createSignature(pdfReader, fos, '\0');
             PdfSignatureAppearance appearance = stamper.getSignatureAppearance();
             appearance.setReason("Demo Blog");
             appearance.setCrypto(privateKey, certificates, null, PdfSignatureAppearance.WINCER_SIGNED);
             appearance.setCertificationLevel(PdfSignatureAppearance.CERTIFIED_NO_CHANGES_ALLOWED);
             stamper.close();
             log.info("Document signed");

        } catch (IOException ex) {
           log.severe(ex.getMessage());
        } catch (DocumentException ex) {
           log.severe(ex.getMessage());
        }

    }

}

Po zaimportowaniu klucza publicznego do Adobe Reader'a i otwarciu podpisanego dokumentu, powinny pokazać się następujące informacje o certyfikacie


Poprawność podpisu można także sprawdzić programowo
import com.itextpdf.text.pdf.AcroFields;
import com.itextpdf.text.pdf.PdfPKCS7;
import com.itextpdf.text.pdf.PdfReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.logging.Logger;

public class PdfValidator {

    private static final Logger log = Logger.getLogger(PdfValidator.class.getName());
    private static final String PDF_PATH_SIGNED = "/home/kuba/tmp/demo-doc-signed.pdf";
    private static final String CERTIFICATE_PATH = "/home/kuba/tmp/demo-cert.cer";

    private KeyStore keyStore;

    public PdfValidator() {

        try {
             keyStore = KeyStore.getInstance("JKS");
             keyStore.load(null, null);
             FileInputStream fis = new FileInputStream(CERTIFICATE_PATH);
             CertificateFactory cf = CertificateFactory.getInstance("X509");
             X509Certificate cert = (X509Certificate) cf.generateCertificate(fis);
             keyStore.setCertificateEntry("cacert", cert);

        } catch (FileNotFoundException ex) {
            log.severe(ex.getMessage());
        } catch (KeyStoreException ex) {
            log.severe(ex.getMessage());
        } catch (IOException ex) {
            log.severe(ex.getMessage());
        } catch (NoSuchAlgorithmException ex) {
            log.severe(ex.getMessage());
        } catch (CertificateException ex) {
           log.severe(ex.getMessage());
        }
    }
    

    public void validatePdf(){

        try {
            
            PdfReader reader = new PdfReader(PDF_PATH_SIGNED);
            AcroFields af = reader.getAcroFields();
            ArrayList names = af.getSignatureNames();
            
            for (String name : names) {
                
                PdfPKCS7 pk = af.verifySignature(name);
                Calendar calendar = pk.getSignDate();
                Certificate[] certificates = pk.getCertificates();
                log.info("Revision modified: " + !pk.verify());
                Object[] fails = PdfPKCS7.verifyCertificates(certificates, keyStore, null, calendar);
                if (fails == null) {
                    log.info("Certificates verified against the KeyStore");
                } else {
                    log.info("Certificate failed: " + fails[1]);
                }
            }

        } catch (SignatureException ex) {
            log.severe(ex.getMessage());
        } catch (IOException ex) {
            log.severe(ex.getMessage());
        }
    }
}

wtorek, 21 grudnia 2010

Contexts and Dependency Injection – część II

Drugi z serii wpisów na temat CDI poświęcony będzie zdarzeniom. Pozwalają one luźno powiązać ze sobą bean'y na zasadzie Akcja → Reakcja.
Załóżmy, że nasza aplikacja chce odnotować w dowolnym miejscu moment operacji na obiekcie użytkownika. Należy w tym celu wstrzyknąć obiekt javax.enterprise.event.Event i wywołać na nim metodę fire(). Jej argumentem, będzie obiekt, który ma podlegać obserwacji.
@Named
@RequestScoped
public class UserBean{

    private List users;

    @Inject
    Event createUserEvent;


    @PostConstruct
    private void init(){
        
        users = new ArrayList();
        users.add(new User("Nowak"));
        users.add(new User("Lis"));
    }

    public void addUser(){

        User user = new User("Kowalski");
        users.add(user);
        createUserEvent.fire(user);
    }
}

Teraz wystarczy już w dowolnym miejscu aplikacji zaimplementować metodę nasłuchującą, będącą odbiorcą powyższego zdarzenia
@Named
@RequestScoped
public class UserObserver {

    public void onCreateUser(@Observes User user){

        System.out.println(">> User "+ user.getName() + " has been created");
    }
}

A co w przypadku gdybyśmy chcieli obserwować kilka różnych zdarzeń na obiekcie User ? Tutaj znowu z pomocą przychodzą kwalifikatory
@Qualifier
@Retention(RUNTIME)
@Target({METHOD, FIELD, PARAMETER, TYPE})
public @interface CreateUser {
}
@Qualifier
@Retention(RUNTIME)
@Target({METHOD, FIELD, PARAMETER, TYPE})
public @interface DeleteUser {
}

Zmodyfikowana klasa obserwująca
import javaspotlight.qualifiers.CreateUser;
import javaspotlight.qualifiers.DeleteUser;
import javax.enterprise.event.Observes;
import javax.faces.bean.RequestScoped;
import javax.inject.Named;

@Named
@RequestScoped
public class UserObserver {

    public void onCreateUser(@Observes @CreateUser User user){

        System.out.println(">> User "+ user.getName() + " has been created");
    }

    public void onDeleteUser(@Observes @DeleteUser User user){

        System.out.println(">> User "+ user.getName() + " has been deleted");
    }
}

Sposób inicjacji zdarzeń – oczywiście nie muszą znajdować się one w tej samej klasie
import java.util.ArrayList;
import java.util.List;
import javaspotlight.qualifiers.CreateUser;
import javaspotlight.qualifiers.DeleteUser;
import javax.annotation.PostConstruct;
import javax.enterprise.event.Event;
import javax.faces.bean.RequestScoped;
import javax.inject.Inject;
import javax.inject.Named;

@Named
@RequestScoped
public class UserBean{

    private List users;

    @Inject
    @CreateUser
    Event createUserEvent;

    @Inject 
    @DeleteUser
    Event deleteUserEvent;

    @PostConstruct
    private void init(){
        
        users = new ArrayList();
        users.add(new User("Nowak"));
        users.add(new User("Lis"));
    }

    public void addUser(){

        User user = new User("Kowalski");
        users.add(user);
        createUserEvent.fire(user);
    }

    public void removeUser(){

        for(User user : users){
     
            if("Nowak".equals(user.getName())){

                deleteUserEvent.fire(user);
                users.remove(user);
            }
        }
    }

sobota, 4 grudnia 2010

Contexts and Dependency Injection – część I

Jako wielki entuzjasta Jboss Seam'a i korzystania z adnotacji, nie mogłem odpuścić sobie nauki specyfikacji CDI, (JSR-229), Jest to zbiór usług stanowiący integralną część standardu Java EE 6 i ułatwiający  powiązanie ze sobą poszczególnych komponentów (warstw) składających się  na powyższą platformę.

Contexts – przywiązanie komponentu Java EE do ściśle zdefiniowanego (ale rozszerzalnego) kontekstu (cyklu życia).
Dependency Injection – możliwość interakcji pomiędzy komponentami  aplikacji takimi jak serwlet, sesyjny EJB czy  JSF'owy managed bean poprzez wstrzyknięcie (Injection) czyli umieszczenie ich w dowolnym momencie w cyklu życia aplikacji.

Najlepiej naukę zacząć od praktycznego przykłady. Ja wykorzystam w tym celu środowisko NetBeans 6.9.1 z dostarczonym przez nie serwerem GlassFish 3.0.1. Zakładamy nowy nowy projekt: File › New Project › Web Application. Przy wyborze serwera należy pamiętać aby zaznaczyć opcję „Contexts and Dependency Injection”.
Powyższy krok, spowoduje nasza aplikacja będzie korzystać z implementacji CDI – WELD. W katalogu WEB-INF zostanie utworzony plik beans.xml, Jego istnienie sygnalizuje kontenerowi CDI, że aplikacja zawiera wstrzykiwane bean'y i musi być skanowana w poszukiwaniu klas zawierających adnotacje.

W kolejnym kroku z dostępnych technologii wybieram JSF

Celem aplikacji jest wstrzyknięcie do managed bean'a interfejsu odpowiedzialnego za logowanie.
public interface MyLogger {

    public void log(Class c, String message);
}

Implementacja wygląda następująco:
import javaspotlight.beans.MyLogger;
import javaspotlight.qualifiers.InfoLog;

public class InfoLogger implements MyLogger{

    @Override
    public void log(Class c, String message) {

      Logger.getLogger(c.getName()).info(message);
    }

Do wstrzyknięcia komponentu używamy adnotacji: @javax.inject.Inject
import java.util.logging.*;
import javaspotlight.beans.MyLogger;
import javaspotlight.qualifiers.InfoLog;

import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.inject.Named;

@Named
@RequestScoped
public class ExampleManagedBean {

    private String outputMessage;

    @Inject
    MyLogger myLogger;

    public String exampleAction(){
        
        outputMessage = "Hello";

        myLogger.log(getClass(), outputMessage);
        return null;
    }
    


    public String getOutputMessage() {
        return outputMessage;
    }

    public void setOutputMessage(String outputMessage) {
        this.outputMessage = outputMessage;
    }

W sumie trywialne. Co jednak się stanie jeśli nasz interfejs będzie posiadał więcej implementacji ? Kontener nie będzie wiedział, której z nich należy użyć. Problem ten rozwiązują kwalifikatory (Qualifiers). Pozwalają nam one na przypisanie dodatkowych informacji do naszego bean'a.

Ja utworzyłem dwie implementacjie loggera:
@InfoLog
public class InfoLogger implements MyLogger{

    @Override
    public void log(Class c, String message) {

      Logger.getLogger(c.getName()).info(message);
    }
}

@WarnLog
public class WarnLogger implements MyLogger{

    @Override
    public void log(Class c, String message) {

      Logger.getLogger(c.getName()).warning(message);
    }
}

Teraz aby móc wskazywać przy pomocy adnotacji na konkretny typ, należy utworzyć kwalifikatory
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.inject.Qualifier;

@Qualifier
@Retention(RUNTIME)
@Target({METHOD, FIELD, PARAMETER, TYPE})
public @interface InfoLog {
}

@Qualifier
@Retention(RUNTIME)
@Target({METHOD, FIELD, PARAMETER, TYPE})
public @interface WarnLog {
}

dzięki nim, wstrzykując nasz interfejs łatwo możemy wskazać jakiego typu bean zostać użyty.
@Inject
@WarnLog
MyLogger myLogger;

albo
@Inject
@InfoLog
MyLogger myLogger;

Struktura mojego projektu wygląda następująco: