четверг, 21 июня 2012 г.

Генерация pdf из java

Появилась недавно задача - добавить в pdf шаблон несколько строк, причем pdf был довольно сложный по составу. Немного поискав варианты решения, понял, есть два пути - использовать генератор с шаблонами, либо загружать готовый pdf документ и непосредственно выводить в него текст. Поняв, что первый вариант меня не устраивает по той причине, что шаблон уже в pdf формате, и рисовать его заново мне не хочется, я остановился на варианте с непосредственным выводом текста в pdf.

Первой мне попалась библиотека pdfbox. К сожалению, не удалось заставить ее корректно отображать русские буквы. Возможно, мне просто не повезло со шрифтом, который я пытался использовать, или не понравилась кодировка, не знаю. Возможно когда-нибудь я предоставлю ей второй шанс.

Следующая на очереди была iText. С помощью неё удалось решить поставленную задачу. Замеченные особенности:

  • Практически все тулзы для генерации pdf файлов внедряют в итоговый файл только subset'ы используемых шрифтов. Вывод - не нужно использовать внедренные в pdf шрифты, если не хотите получить слова с отсутствующими буквами.
  • Стандартные шрифты, например BaseFont.HELVETICA, содержат в себе только латинские символы и не могут быть использованы для вывода текста в кириллице.
  • Используйте Unicode шрифты, загружайте их из файла и внедряйте в итоговый pdf документ. Сделать это просто, вызвав BaseFont.createFont("Liberation-Sans.ttf", BaseFont.IDENTITY_H, true)
И пример использования:

    File tmp = File.createTempFile("pdf", null);
    FileOutputStream outStream = new FileOutputStream(tmp);
    Document pdDoc = new Document(PageSize.A4);
    PdfWriter writer = PdfWriter.getInstance(pdDoc, outStream);
    pdDoc.open();
    try{
        PdfContentByte cb = writer.getDirectContent();

        InputStream is = 
                    getClass().getClassLoader().getResourceAsStream("/templates/template.pdf");
        if (is == null) {
            throw new DocumentException("cannot load template");
        }

        String fontPath = "/templates/" + 
                          Config.getInstance().getString("templates.font");
        URL u = getClass().getClassLoader().getResource(fontPath);
        if(u == null){
            throw new  DocumentException("cannot load font");
        }
        // load font from resource
        BaseFont bf = BaseFont.createFont(u.getFile(), BaseFont.IDENTITY_H, true);
        PdfReader reader = new PdfReader(is);
        PdfImportedPage page = writer.getImportedPage(reader, 1);

        // Copy first page of existing PDF into output PDF
        pdDoc.newPage();
        cb.addTemplate(page, 0, 0);

        cb.saveState();

        // write  text
        cb.beginText();
        cb.setFontAndSize(bf, 10);
        cb.setTextMatrix(197, 676);
        cb.showText("some text");

        cb.setFontAndSize(bf, 7);

        cb.setTextMatrix(70, 645);
        cb.showText("smaller text");

        cb.endText();
        cb.restoreState();


        page = writer.getImportedPage(reader, 2);
        pdDoc.newPage();
        //add second page as is
        cb.addTemplate(page, 0, 0);


        page = writer.getImportedPage(reader, 3);
        pdDoc.newPage();
        //add third page as is
        cb.addTemplate(page, 0, 0);
    }finally{
        pdDoc.close();
    }

среда, 20 июня 2012 г.

Внедрение Groovy для вычисления простых выражений

Небольшой пример внедрения groovy для вычисления простых выражений. Не требует создания сложных тяжеловесных конструкций с Binding и создания отдельных файлов скриптов.

import groovy.util.Eval;

HashMap params = new HashMap();
params.put("field1", "10");
params.put("field2", "11");

Boolean res = (Boolean) Eval.me("param", params, "param.field1 == param.field2");
Документация на класс Eval

пятница, 15 июня 2012 г.

Использование OpenSource крипто библиотек для работы с ГОСТовыми сертификатами CryptoPro

В данной статье делается попытка ответить на вопрос: возможно ли использовать OpenSource библиотеки для работы с ГОСТовыми сертификатами?
Если коротко, то да.
Для проверки использовалась последняя на данный момент версия OpenSSL (1.0.1с), собранная из исходников с поддержкой gost engine
Для начала нужно установить КриптоПро CSP 3.6 R3, поскольку в нем реализована поддержка экспорта сертификатов в формате pfx. Эту часть работ нужно делать в операционной системе Windows. Не буду описывать подробно этот процесс, скажу лишь, что нужно сохранить приватный ключ в формате CryptoPro на флэшку и импортировать его в хранилище "Реестр". Выгрузив сертификат в файл, например store.pfx (включая приватный ключ), можно вывести приватный ключ в stdout командой
/usr/local/ssl/bin/openssl pkcs12 -info -engine gost -nodes -in ~/store.pfx 

  • -info вывести информацию о содержимом файла в формате pkcs12 (.pfx)
  • -engine использовать указанный криптографический модуль
  • -nodes не нужно шифровать приватный ключ
  • -in входной файл
Если вы получаете ошибку вида "unknown PBE algorithm"
Enter Import Password:
MAC Iteration 2000
MAC verified OK
PKCS7 Data
Shrouded Keybag: undefined, Iteration 2000
Bag Attributes
    Microsoft Local Key set: <No Values>
    localKeyID: 01 00 00 00
    friendlyName: REGISTRY\\1ba6dcf8-b953-4774-8a9e-de98de071f24
    Microsoft CSP Name: Crypto-Pro GOST R 34.10-2001 Cryptographic Service Provider
Error outputting keys and certificates
140669637789344:error:06074079:digital envelope routines:EVP_PBE_CipherInit:unknown pbe algorithm:evp_pbe.c:167:TYPE=1.2.840.113549.1.12.1.80
140669637789344:error:23077073:PKCS12 routines:PKCS12_pbe_crypt:pkcs12 algor cipherinit error:p12_decr.c:83:
140669637789344:error:2306A075:PKCS12 routines:PKCS12_item_decrypt_d2i:pkcs12 pbe crypt error:p12_decr.c:130:
То это означает, что вам потребуется другое средство для получения pfx файла, утилитка P12FromGostCSP.exe
Если вы получаете ошибку вида "unknown digest algorithm"

Enter Import Password:
MAC Iteration 2048
Mac verify error: invalid password?
140572666472096:error:2306B076:PKCS12 routines:PKCS12_gen_mac:unknown digest algorithm:p12_mutl.c:88:
140572666472096:error:2307E06D:PKCS12 routines:PKCS12_verify_mac:mac generation error:p12_mutl.c:122:

то скорее всего в вашем openssl нет поддержки ГОСТовых алгоритмов либо вы используете не ту копию openssl.

Если все прошло без ошибок, то в консоли вы получите приватный ключ и сертификат. Скопировав выведенный в консоль приватный ключ (вместе со словами -----BEGIN PRIVATE KEY----- и -----END PRIVATE KEY-----) в отдельный файл private.key мы сможем с его помощью создавать ЭЦП.

Команда для генерации примерно следующая:

/usr/local/ssl/bin/openssl cms -sign -inkey ~/private.key -in ~/file.txt -CAfile ~/CA.cer 
-signer ~/client.cer -engine gost -out ~/test.sign -outform DER -noattr -binary

  • -sign генерация подписи
  • -inkey путь к приватному ключу
  • -in подписываемый файл
  • -CAfile файл УЦ
  • -signer файл с сертификатом, которым осуществляется подпись
  • -engine использовать указанный криптографический модуль
  • -out файл, в который будет записана ЭЦП
  • -outform формат подписи
  • -noattr не добавлять дополнительных аттрибутов в подпись
  • -binary считать подписываемый файл массивом байт, а не текстом

К сожалению, Bouncy Castle (версии 1.46), хоть в нем и декларируется поддержка ГОСТовых алгоритмов, не может разобрать формат приватного ключа, так что использовать его для генерации ЭЦП на данный момент не представляется возможным.

UPD:
Bouncy Castle 1.55 уже умеет работать с такими приватными ключами

суббота, 9 июня 2012 г.

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

В данной статье описывается процесс создания ЭЦП c помощью библиотеки CryptoPro.
Для создания подписи необходим приватный ключ и сертификат. Рассмотрим гипотетическую ситуацию, когда приватный ключ прислан по почте, а не сгенерирован самостоятельно - порочная, но широко распространенная практика.
Предположим, что файл клиентского сертификата называется client.cer, сертификата УЦ - CA.cer, а приватный ключ находится в директории 999996.000

1. Установка CryptoPro CSP и CryptoPro JCP

Добываем каким-либо образом дистрибутивы, самый простой способ - скачать их с сайта CryptoPro, предварительно там зарегистрировавшись. Триальная версия полнофункциональна и работает 30 дней.
При установке CSP пакет cprocsp-rdr-gui скорее всего не установится, поскольку использует древние версии Motif, но он не нужен для работы, так что установка данного пакета не является обязательной.
Также нужно убедиться, что в системе есть libcurl.so (эта библиотека используется для получения CRL)
После установки необходимо проверить, что в файле /etc/opt/cprocsp/config.ini прописан правильный путь к libcurl.so, по умолчанию он ведет на /usr/local/lib/64/libcurl.so что является ошибкой.
После этого устанавливаем CryptoPro JCP (jcp_plus_jtls_1.0.53.jar)
После успешной установки нужно запустить ControlPane.sh из-под рута и поменять путь к хранилищу ключей на /var/opt/cprocsp/keys/{$user.name}

2. Установка приватного ключа и сертификатов

CryptoPro имеет свой собственный формат приватного ключа и свои собственные контейнеры для хранения ключей и сертификатов. Чтобы установить ключ и сертификаты в контейнеры, нужно проделать следующие действия:

  • Скопировать в корень дискеты или флэшки сертификат и приватный ключ. Под приватным ключом понимается директория 999996.000 и ее содержимое, файлы header.key, masks2.key, masks.key, name.key, primary2.key, primary.key
    $ cp -R /path/to/key/999996.000 /media/flashdrive/
    $ cp /path/to/cert/client.cer /media/flashdrive/
    
  • Выполнить команду по копированию ключа с флэшки на диск. Ключ попадет в пользовательское хранилище 'My'. Выполнять команду нужно под пользователем, который будет использовать данный контейнер для подписи. 999996 - название (alias) контейнера. gate@example.com - то, что прописано в поле E сертификата ( можно посмотреть командой keytool --printcert -file /path/to/cert/client.cer )
    $ csptest -keycopy -src '\\.\FLASH\gate@example.com' -dest '\\.\HDIMAGE\999996'
    Проверить, что все скопировалось, можно командой
    $ ls -al /var/opt/cprocsp/keys/<username>
  • Альтернативный путь, если нет дискеты или csptest выдает ошибку
    Error number 0x8009000f (2148073487). Object already exists.
    Руками скопировать приватный ключ в хранилище командой
    $ cp -R /path/to/key/999996.000 /var/opt/cprocsp/keys/<username>/
  • Ассоциировать сертификат с контейнером. Сертификат попадет в пользовательское хранилище 'My'
    $ certmgr -inst -file /path/to/file/client.cer -cont '\\.\HDIMAGE\999996'
  • Установить сертификат УЦ из-под пользователя root командой
    # certmgr -inst -store root -file /path/to/file/CA.cer
Контейнер должен быть готов к использованию, проверить это можно командами:
$ certmgr --list
Certmgr 0.9 prerelease (c) "CryptoPro",  2007-2010.
program for managing certificate(CRL) and stores

=============================================================================
1-------
Issuer         : DC=ru, DC=issuer, CN=EXAMPLE
Subject        : C=RU, S=RUSSIA, L=MOSCOW, O=ORGANIZATION, OU=IT, CN=GATE_DEMO, E=gate@example.com
Serial         : 0x2225000000007DF78065
PrivateKey Link: Yes. Container: HDIMAGE\\999996.000\D7BB
=============================================================================

[ErrorCode: 0x00000000]
Обратите внимание на строку "PrivateKey Link: Yes. Container: HDIMAGE\\999996.000\D7BB". Она показывает наличие связи сертификата и приватного ключа, если выводится "PrivateKey Link: No" это означает, что связь не установлена и использовать такой контейнер для подписи не удастся.
$ certmgr --list -store root
Certmgr 0.9 prerelease (c) "CryptoPro",  2007-2010.
program for managing certificate(CRL) and stores

=============================================================================
1-------
Issuer         : DC=ru, DC=issuer, CN=EXAMPLE
Subject        : DC=ru, DC=issuer, CN=EXAMPLE
Serial         : 0xE44263EF7B42044F9E20FFF14C6F1327
PrivateKey Link: No
=============================================================================

[ErrorCode: 0x00000000]

3. Генерация ЭЦП

Под ЭЦП обычно понимается отсоединенная (detached) подпись в формате pkcs#7. Т.е помимо самой подписи, в сообщение внедряется вся цепочка сертификатов. CryptoPro не предоставляет отдельного пакета CMS для легкой генерации криптографических сообщений, но в принципе в пакете JCP есть все необходимое, чтобы сформировать корректное сообщение самостоятельно. Код по большей части взят из примеров, которые идут в JCP.
    private static byte[] signWithCryptoProJcp(byte[] data) throws Exception{

        String alias = "999996";
        String caFile = "/path/to/CA.cer";
        String certFile = "/path/to/client.cer";

        //load keys for sign
        final PrivateKey[] keys = new PrivateKey[1];
        keys[0] = CMStools.loadKey(alias, null);

        //load certificates chain
        final Certificate[] certs = new Certificate[2];
// функция CMStools.loadCertificate() почему-то не работает, хотя сертификат есть в хранилище
// пришлось читать сертификат из файла
        certs[0] = CMStools.readCertificate(certFile);
        certs[1] = CMStools.readCertificate(caFile);
        return createCMS(data, keys, certs, true);
    }


    private static byte[] createCMS(byte[] data, PrivateKey[] keys,
                                   Certificate[] certs,
                                   boolean detached)
            throws Exception {
        //create CMS
//        Array.writeFile("/home/grigory/test.msg", data);
        final ContentInfo all = new ContentInfo();
        all.contentType = new Asn1ObjectIdentifier(new OID(CMStools.STR_CMS_OID_SIGNED).value);
        final SignedData cms = new SignedData();
        all.content = cms;
        cms.version = new CMSVersion(1);
        // digest
        cms.digestAlgorithms = new DigestAlgorithmIdentifiers(1);
        final DigestAlgorithmIdentifier a = new DigestAlgorithmIdentifier(
                new OID(CMStools.DIGEST_OID).value);
        a.parameters = new Asn1Null();
        cms.digestAlgorithms.elements[0] = a;
        if (detached)
            cms.encapContentInfo = new EncapsulatedContentInfo(
                    new Asn1ObjectIdentifier(
                            new OID(CMStools.STR_CMS_OID_DATA).value),
                    null);
        else
            cms.encapContentInfo =
                    new EncapsulatedContentInfo(new Asn1ObjectIdentifier(
                            new OID(CMStools.STR_CMS_OID_DATA).value),
                            new Asn1OctetString(data));
        // certificates
        final int ncerts = certs.length;
        cms.certificates = new CertificateSet(ncerts);
        cms.certificates.elements = new CertificateChoices[ncerts];
        for (int i = 0; i < cms.certificates.elements.length; i++) {
            final ru.CryptoPro.JCP.ASN.PKIX1Explicit88.Certificate certificate =
                    new ru.CryptoPro.JCP.ASN.PKIX1Explicit88.Certificate();
            final Asn1BerDecodeBuffer decodeBuffer =
                    new Asn1BerDecodeBuffer(certs[i].getEncoded());
            certificate.decode(decodeBuffer);
            cms.certificates.elements[i] = new CertificateChoices();
            cms.certificates.elements[i].set_certificate(certificate);
        }
        // Signature.getInstance
        final Signature signature =
                Signature.getInstance(JCP.GOST_EL_SIGN_NAME);
        byte[] sign;
        // signer infos
        final int nsign = keys.length;
        cms.signerInfos = new SignerInfos(nsign);
        for (int i = 0; i < cms.signerInfos.elements.length; i++) {
            signature.initSign(keys[i]);
            signature.update(data);
            sign = signature.sign();
            cms.signerInfos.elements[i] = 
                     new ru.CryptoPro.JCP.ASN.CryptographicMessageSyntax.SignerInfo();
            cms.signerInfos.elements[i].version = new CMSVersion(1);
            cms.signerInfos.elements[i].sid = new SignerIdentifier();

            final byte[] encodedName =
                    ((X509Certificate) certs[i]).getIssuerX500Principal().getEncoded();
            final Asn1BerDecodeBuffer nameBuf = new Asn1BerDecodeBuffer(encodedName);
            final Name name = new Name();
            name.decode(nameBuf);

            final CertificateSerialNumber num = new CertificateSerialNumber(
                    ((X509Certificate) certs[i]).getSerialNumber());
            cms.signerInfos.elements[i].sid.set_issuerAndSerialNumber(
                    new IssuerAndSerialNumber(name, num));
            cms.signerInfos.elements[i].digestAlgorithm =
                    new DigestAlgorithmIdentifier(new OID(CMStools.DIGEST_OID).value);
            cms.signerInfos.elements[i].digestAlgorithm.parameters = new Asn1Null();
            cms.signerInfos.elements[i].signatureAlgorithm =
                    new SignatureAlgorithmIdentifier(new OID(CMStools.SIGN_OID).value);
            cms.signerInfos.elements[i].signatureAlgorithm.parameters = new Asn1Null();
            cms.signerInfos.elements[i].signature = new SignatureValue(sign);
        }
        // encode
        final Asn1BerEncodeBuffer asnBuf = new Asn1BerEncodeBuffer();
        all.encode(asnBuf, true);
//        Array.writeFile("/home/grigory/test.signature", asnBuf.getMsgCopy());
        return asnBuf.getMsgCopy();
    }
 
У CryptoPro есть замечательный сервис проверки ЭЦП, где можно проверить, что сгенерированная подпись верна.