четверг, 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 комментария: