Цель этой статьи - разобраться, как использовать библиотеку 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;
}