пятница, 30 марта 2012 г.

SimpleDateFormat is not thread-safe

Забавно, но класс SimpleDateFormat оказывается не tread-safe, так что хранение инстанса в статической переменной и использование ее в многопоточном приложение иногда приводит к тому, что дата парсится неправильно.

Из вариантов решения: либо синхронизировать вызовы методов parse и format, либо использовать копирующий конструктор при каждом обращении.

четверг, 15 марта 2012 г.

Создание ЭЦП с помощью Signal COM

Цель этой статьи - разобраться, как использовать библиотеку Signal COM для создания ЭЦП. Под ЭЦП понимается подпись в формате PKCS#7, которая содержит помимо собственно подписи(ей) также информацию о сертификатах и не обязательные дополнительные атрибуты (время подписи и т.п)

  • Получение и установка дистрибутива

Вначале нужно получить дистрибутив библиотеки Signal COM, купив ее у производителя или скачав триальную версию с сайта. Нужны дистрибутивы Java CMS и JCP. Приобретать SDK настоятельно не рекомендую, поскольку содержание полностью соответствует таковому из триальной версии.

Установив дистрибутив, лучше в отдельную копию JDK, можно посмотреть, что же поменялось. Добавился новые jar файлы с реализацией криптопровайдера sccsp.jar и CMS sccms.jar, запись о регистрации криптопровайдера ru.signalcom.crypto.provider.SignalCOMProvider в файле java.security, подменились файлы local_policy.jar и US_export_policy.jar. По идее можно сразу приступать к работе.

  • Формирование хранилища

SignalCOM использует для хранения приватного ключа формат pkcs#8. Приватный ключ связан каким-то образом с генератором случайных чисел (при инициализации генератора случайных чисел требуется пароль от приватного ключа). Для работы в java удобнее всего сформировать хранилище в формате JKS или PKCS#12, для этого нужен приватный ключ и цепочка сертификатов.

Ниже приведен пример сборки хранилища для формата PKCS#12; формирование хранилища в формате JKS ничего принципиально от него ничем не отличается. Позднее, используя keytool, всегда можно сконвертировать хранилище в нужный формат, главное не забыть указать опцию -srcprovidername SC, поскольку не все провайдеры могут работать с гостовыми сертификатами. Также нужно учесть, что пароль на хранилище и приватный ключ будет одинаковым, даже если первоначально приватный ключ не имел пароля.

String root = "/home/gr/contact/TestKey";
        String certRoot = "/home/gr/contact/TestKey/CA";
        String clientCertRoot = "/home/gr/contact/TestKey/OpenKey";
        String passwd = "changeit";

        // Инициализация хранилища
        KeyStore store = KeyStore.getInstance("PKCS#12", "SC");
        store.load(null, null);

        // Чтение секретного ключа PKCS#8
        KeyFactory keyFac = KeyFactory.getInstance("PKCS#8", "SC");
        byte[] encoded = fread(root + FILE_SEPARATOR + "Keys" + FILE_SEPARATOR + "00000001.key");
        KeySpec privkeySpec = new PKCS8EncodedKeySpec(encoded);
        PrivateKey priv = keyFac.generatePrivate(privkeySpec);

        // Чтение сертификата УЦ
        CertificateFactory cf = CertificateFactory.getInstance("X.509", "SC");
        FileInputStream in = new FileInputStream(certRoot + FILE_SEPARATOR + "certca_1902.pem"); // CA 7
        X509Certificate cacert = (X509Certificate) cf.generateCertificate(in);
        in.close();

        // Чтение собственного сертификата
        in = new FileInputStream(clientCertRoot + FILE_SEPARATOR + "cert_9342.pem"); // CA 7
        X509Certificate cert = (X509Certificate) cf.generateCertificate(in);
        in.close();

        // Формирование цепочки сертификатов
        Certificate[] chain = new Certificate[2];
        chain[0] = cert;
        chain[1] = cacert;

        // Помещение в хранилище секретного ключа с цепочкой сертификатов
        store.setKeyEntry("Test key", priv, passwd.toCharArray(), chain);
        // Помещение в хранилище сертификата УЦ
        store.setCertificateEntry("Test CA", cacert);

        // Запись хранилища
        FileOutputStream out = new FileOutputStream(new File(root + FILE_SEPARATOR + "store.pfx"));
        store.store(out, passwd.toCharArray());
        out.close();

Пример конвертирования хранилища из формата PKCS#12 в JKS с использованием keytool

$keytool -importkeystore -srcstoretype pkcs12 -deststoretype JKS -srckeystore store.pfx -destkeystore test.jks -srcprovidername SC
  • Формирование подписи

После создания хранилища можно приступать к формированию подписи, это можно сделать с помощью следующего кода:

public static byte[] signPkcs7SignalCom(String pkcs12Store, String pass, String alias, 
                                        String keyPassword, byte[] data, SecureRandom random) throws SignatureException {
        try {
            FileInputStream in = null;
            ByteArrayInputStream inp = null;
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            OutputStream sigOut = null;
            try{
                in = new FileInputStream(KEYSTORE_PATH + pkcs12Store);
                KeyStore keyStore = getKeystore(pkcs12Store, keyPassword, "PKCS12", "SC", random);
                keyStore.load(in, pass.toCharArray());
                List certs = new ArrayList();
                X509Certificate cert = (X509Certificate) keyStore.getCertificate(alias);
                //указываем, каким сертификатом будем подписывать
                certs.add(cert);
                PrivateKey priv = (PrivateKey) keyStore.getKey(alias, pass.toCharArray());

                SignedDataGenerator generator = new SignedDataGenerator(out);
                // экземпляр объекта SecureRandom должен быть предварительно проинициализирован!
                generator.addSigner(new Signer(priv, cert, random));
                generator.addCertificatesAndCRLs(CertStore.getInstance("Collection", new CollectionCertStoreParameters(certs)));
                //для создания отсоединенной подписи, т.е такой, которая не включает в себя данные, ставим true
                generator.setDetached(true);
                sigOut = generator.open();
                inp = new ByteArrayInputStream(data);
                byte[] buf = new byte[1024];
                int len;
                while ((len = inp.read(buf)) > 0) {
                    sigOut.write(buf, 0, len);
                }
                generator.close();
                byte[] signedData = out.toByteArray();
                // для тестовых целей можно провалидировать подпись, но в общем случае это не нужно
                //boolean res = verifyPkcs7Sign(signedData, data);
                return signedData;

            }finally{
                if(in != null)
                    in.close();
                if(inp != null)
                    inp.close();
                if(out!= null)
                    out.close();
                if(sigOut != null)
                    sigOut.close();
            }

        } catch (IOException e) {
            throw new SignatureException("Signing error", e);
        } catch (GeneralSecurityException e) {
            throw new SignatureException("Signing error", e);
        }catch (ru.signalcom.crypto.cms.CMSException e) {
            throw new SignatureException("Signing error", e);
        }
    }

Необычным тут является использование объекта SecureRandom, все остальное в принципе похоже на подписи с помощью остальных криптопровайдеров. SecureRandom нужно предварительно проинициализовать, используя следующий код:

        SecureRandom random = null;
        try {
            random = SecureRandom.getInstance("GOST28147PRNG", "SC");
            random.setSeed(new String(privateKeyDirectory + ";NonInteractive;Password=" + pass).getBytes());
        } catch (Exception e) {
            throw new SignatureException("Cannot init class secure random", e);
        }

privateKeyDirectory - путь до директории с файлами kek.opq masks.db3 mk.db3 rand.opq. причем для файла rand.opq должны быть выдано право на запись.

  • Валидация подписи

Для валидации подписи необходим сертификат, которым была осуществлена подпись и, если подпись была не присоединенной, исходные данные, которые были подписаны. В простейшем случае проверка подписи осуществляется так (этот код только проверяет, что подпись была осуществлена данным сертификатом, цепочка сертификатов не валидируется):

public static boolean verifyPkcs7SignatureSignalCom(String certKeystore, String keystorePass, 
                                                    String alias, byte[] signed, byte[] data) throws Exception {
        //List certStores = new ArrayList();

        FileInputStream inp = new FileInputStream(KEYSTORE_PATH + certKeystore);
        KeyStore store = KeyStore.getInstance("PKCS12", "SC");
        store.load(inp, keystorePass.toCharArray());
        inp.close();
        // получаем сертификат, которым производилась подпись
        X509Certificate cert = (X509Certificate) store.getCertificate(alias);
        if(cert ==null){
            throw new SignatureException("Certificate not found");
        }
        InputStream in = new ByteArrayInputStream(signed);
        ContentInfoParser cinfoParser = ContentInfoParser.getInstance(in);
        if (!(cinfoParser instanceof SignedDataParser)) {
            throw new RuntimeException("SignedData expected here");
        }
        SignedDataParser parser = (SignedDataParser) cinfoParser;
        InputStream content = parser.getContent();
        if (content == null) {
            // отсоединённая подпись
            if (data == null) {
                throw new RuntimeException("signature detached, please provide data to compare with");
            }
            parser.setContent(new ByteArrayInputStream(data));
        }
        parser.process();
        // почему-то не работает эта часть кода, хотя в документации сертификат получаелся именно таким способом
        //certStores.add(parser.getCertificatesAndCRLs());


        try {
            Collection signerInfos = parser.getSignerInfos();
            Iterator it = signerInfos.iterator();
            while (it.hasNext()) {
                SignerInfo signerInfo = it.next();
                // собственно валидация
                boolean res = signerInfo.verify(cert);
                return res;
            }
        } finally {
            parser.close();
            in.close();
        }

        return false;
    }

пятница, 2 марта 2012 г.

Настройка axis клиента на работу с несколькими хостами по протоколу https c клиентскими сертификатами

Итак, пока есть время, быстренько опишу этот опыт.

Axis, использует системные настройки для осуществления соединения по https, т.е. теоретически можно указать системные свойства

System.setProperty("javax.net.ssl.keyStore", "securirty/cacerts");
System.setProperty("javax.net.ssl.keyStorePassword", "");
System.setProperty("javax.net.ssl.trustStore", "security/cacerts");

и клиент будет их использовать, что вполне допустимо, если нужно соединение только с одним хостом. Но что делать, если нужно коннектиться к нескольким хостам, к каждому с разными сертификатами, причем эти сертификаты находятся в разных хранилищах и имеют разные пароли? Вариантов решения проблемы два: первый - использовать jax-ws для генерации клиента, и дальше идет черная магия с конфигурацией, либо использовать свою SSLConnectionFactory, но завязываться на имя хоста. Какой их этих путей лучше, не знаю, я выбрал решение с созданием SSLConnectionFactory, как более простое. идея и часть кода взята отсюда

Итак, создаем стандартных axis клиентов по wsdl файлам, для каждого хоста получатся свои классы клиента. Далее, перед вызовом методов веб сервиса необходимо установить свойство

AxisProperties.setProperty("axis.socketSecureFactory", "org.yourcompany.packagename.CustomSSLSocketFactory");

Содержимое файла CustomSSLSocketFactory.java примерно следующее:

package org.yourcompany.packagename;

import org.apache.axis.components.net.BooleanHolder;
import org.apache.axis.components.net.JSSESocketFactory;
import org.apache.axis.components.net.SecureSocketFactory;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import java.io.FileInputStream;
import java.net.Socket;
import java.security.KeyStore;
import java.util.Hashtable;

/**
 * Custom SSL socket factory to use our integrated keystore.
 * 

* Based loosely on org.apache.axis.components.net.SunJSSESocketFactory */ public class CustomSSLSocketFactory extends JSSESocketFactory implements SecureSocketFactory { private static final String FILE_SEPARATOR = System.getProperty("file.separator"); private static final String KEYSTORE_PATH = System.getProperty("java.home") + FILE_SEPARATOR + "lib" + FILE_SEPARATOR + "security" + FILE_SEPARATOR; public CustomSSLSocketFactory(Hashtable attributes) { super(attributes); } @Override public Socket create(String host, int port, StringBuffer otherHeaders, BooleanHolder useFullURL) throws Exception { sslFactory = createSSlSocketFactory(host); return super.create(host, port, otherHeaders, useFullURL); } private SSLSocketFactory createSSlSocketFactory(String host) throws Exception { String keystorePass = null; String keystorePath = null; if (host.equals("google.com")){ keystorePass = "passforgoogle"; keystorePath = "googlestore.jks"; } else if (host.equals("yandex.com")) { keystorePass = "passforyandex"; keystorePath = "yandexstore.jks"; } else if (host.equals("yahoo.com")) { keystorePass = "passforyahoo"; keystorePath = "yahoostore.jks"; } if (keystorePass == null || keystorePath == null) { System.out.println("unknown host "+ host +", cannot create socket factory"); return null; } char[] keystorepass = keystorePass.toCharArray(); FileInputStream is = new FileInputStream(KEYSTORE_PATH + keystorePath); // create required keystores and their corresponding manager objects KeyStore keyStore = KeyStore.getInstance("JKS"); keyStore.load(is, keystorepass); is.close(); KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); kmf.init(keyStore, keystorepass); SSLContext sslContext = SSLContext.getInstance("SSL"); sslContext.init(kmf.getKeyManagers(), null, null); return sslContext.getSocketFactory(); } }

Основным недостатком данного метода является то, что нужно каким-то образом задать список хостов и паролей, это может создать трудности при внедрении в системы, требующие безостановочной работы. Как вариант можно запрашивать эти данные из базы или конфигурационного файла.

Следующей на очереди будет описание работы с библиотекой шифрования Signal COM.

Генерация запроса на сертификат и загрузка сертификата в JKS

Волею судеб на новой работе приходится возиться с большим количеством криптографии, так что пришлось в ней разобраться. При этом я ни разу не претендую на титул эксперта, но некоторые вещи опишу здесь, чтобы было.
Опуская цели и принципы криптографии, остановлюсь на некоторых, полезных с точки зрения пользователя, деталях.
  • Генерация запроса на сертификат

Сразу оговорюсь, что речь идет о выпуске полноценного сертификата, а не самоподписанного, по самоподписанным много информации есть в сети. Генерация запроса на выпуск сертификата невозможна без приватного ключа, обычно запрос на сертификат и приватный ключ генерируются совместно, одной командой.

 
$openssl req -out request.csr -new -newkey rsa:2048 -utf8 -keyout private.key

Доментация на команду openssl req
подробнее относительно опций:
  • -utf8 означает, что аттрибуты будут храниться как utf8 строки, что необходимо, если используются русские буквы, иначе в запросе получите кракозябры.
  • -new генерирует новый запрос на сертификат, с запросом всех полей.
  • -newkey rsa:2048 генерируется новый RSA ключ длиной 2048 бит.
  • запрос записывается в файл request.csr, приватный ключ сохраняется в файл private.key

Полученный запрос (request.csr) отравляется в центр сертификации (Certificate Authority, CA), который через некоторое время присылает сертификат. Обычно сертификат присылается на email, указанным при генерации запроса на сертификат. Ни в коем случае не нужно отправлять приватный ключ в центр сертификации, если вы это сделаете, ключ будет считаться скомпрометированным и придется повторять процесс заново.
  • Импорт сертификата в JKS хранилище.

Для успешного импорта нужно следующее: приватный ключ, личный (клиентский) сертификат, и сертификат центра сертификации. Обычно приватный ключ имеет расширение pem, key или der и слово private в названии файла. Если ключ в формате PEM, то проверить, что это приватный ключ, можно в любом текстовом редакторе. Приватный ключ должен начинаться со строк -----BEGIN ENCRYPTED PRIVATE KEY----- или -----BEGIN PRIVATE KEY-----
Клиентский сертификат обычно имеет расширение pem, der или crt. Сертификат центра сертификации имеет расширение pem, der или crt и обычно содержит в имени файла слово CA.
Просмотреть содержимое сертификата можно командой
$keytool -printcert -v -file cert.pem
Существуют определенные сложности с импортом только приватного ключа в jks хранилище хранилище посредством keytool, поэтому импорт производится в два этапа, на первом создается хранилище в формате PKCS#12, в которое помещаются приватный ключ и цепочка сертификатов, и потом это хранилище конвертируется в формат jks. При задании пароля на хранилище рекомендуется указать тот же пароль, что и и на приватный ключ.

$openssl pkcs12 -export -out store.pfx -inkey private.key -in clientcert.pem -certfile CAcert.pem
$keytool -importkeystore -destkeystore store.jks -srckeystore store.pfx -srcstoretype PKCS12

Доментация на команду openssl pkcs12
После конвертации можно поменять пользовательское имя (alias) у клиентского сертификата, по умолчанию он будет иметь имя "1".
Посмотреть, что же получилось, можно приведенной ниже командой.
 
$keytool -v -list -keystore store.jks
Альтернативным способом проверки будет загрузка полученного на предыдущем шаге файла store.pfx в IE и открытие в нем сайта, с которым необходимо осуществлять защищенное соединение.
Обычно требуется также добавить сертификат центра сертификации в cacerts, если сертификат сервера, на который производится соединение, выдан тем же CA (а обычно это так), сделать это можно командой

 
$keytool -import -trustcacerts -alias write_alias_here -file CAcert.pem -keystore cacerts

Полученное хранилище можно использовать для организации защищенного соединения с удаленным хостом.