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