Цель этой статьи - разобраться, как использовать библиотеку 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()); Listcerts = 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 { //ListcertStores = 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; }
пришлось кстати утерянное знание)
ОтветитьУдалитьЯ вроде оставлял ссылку на этот бложег Сергею.
Удалить