“Это решение ужасное, но а какое у вас решение есть?”
Про NCALayer
2015 г.
- Какие будут ваши доказательства?
Идёт модернизация настольной системы, требуется возможность работы в Интернет, система переписывается на PHP. В системе есть аутентификация пользователей и подпись контента по ЭЦП. Технический специалист чувствует боль пользователей настольных систем, которые мучаются с эксплуатацией NCALayer и печаль отрезанных мобильных пользователей, количество которых уже приближается к 80 процентам. Скачав пару библиотек на JavaScript и убедившись, что NCALayer на клиенте можно заменить JavaScript скриптами, технический специалист идёт показывать это решение административному специалисту и обнаруживает проблему. Доказать, что 400 КБ кода, взятые из сети Интернет, решают задачу аутентификации и подписания посредством ЭЦП, без авторитетных государственных структур технический специалист административному специалисту не может.
- Если не получается вертикально и горизонтально – делаем диагонально.
Чисто настольный вариант через NCALayer и чисто интернет вариант через JavaScript не устраивают. Остаётся компромиссный вариант – читать ключи на клиенте и проверять их на сервере. Попробуем аргументировать такое решение.
1) ЭЦП ключи можно угнать.
Ключи можно угнать и в настольном приложении NCALayer. Делаем защищённое SSL соединение с зелёным https и большинство пользователей уже будут доверять такому сайту. Можно зарегистрировать предприятие сайта в картотеке DUNS или D-U-N-S
en.wikipedia.org/wiki/Data_Universal_Numbering_System и отправлять туда особых скептиков.
2) SSL ключи дорого стоят и устанавливать их сложно.
Выход есть. Let’s Encrypt
letsencrypt.org/ — центр сертификации предоставляет бесплатные криптографические сертификаты X.509 для TLS шифрования (HTTPS). Процесс выдачи сертификатов полностью автоматизирован.
3) На PHP нет библиотек для работы с ключами GOST.
Читаем контейнер с ключами, переводим его в base64 и по защищённому https:// каналу отправляем на сервер на проверку. На сервере реализуем PHP/Java Bridge.
php-java-bridge.sourceforge.net/pjb/contact.php
nikita-petrov.com/articles/o-tom-kak-podruzhit-java-i-php
На лету в java проверяем сертификат на:
- правильность атрибутов сертификата, например период действия, политики
- соответствие цепочке сертификатов
- отсутствие сертификата в списке отозванных ключей
На лету подписываем данные необходимыми ключами.
Никаких следов ЭЦП на сервере не останется даже при сбоях в середине процесса.
В результате:
- Пользователи могут работать на всех устройствах, на которых можно прочитать файл.
- Административный специалист видит зелёный https://
- Технический специалист по сертификации систем видит библиотеку НУЦ.
Если можно что-то улучшить, критические замечания приветствуются.
Сырой код на Java для PHP/Java Bridge прикладывается.
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package xmlsign;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.StringWriter;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.Security;
import java.security.cert.TrustAnchor;
import java.security.cert.X509Certificate;
import java.security.cert.X509CRL;
import java.security.cert.X509Extension;
import java.security.cert.X509CertSelector;
import java.security.cert.PKIXCertPathBuilderResult;
import java.security.cert.PKIXBuilderParameters;
import java.security.cert.CertPathBuilder;
import java.security.cert.CollectionCertStoreParameters;
import java.security.cert.CertStore;
import java.util.Enumeration;
import java.util.Base64;
import java.util.ArrayList;
import java.util.Set;
import java.util.HashSet;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import kz.gov.pki.kalkan.asn1.pkcs.PKCSObjectIdentifiers;
import kz.gov.pki.kalkan.jce.provider.KalkanProvider;
import kz.gov.pki.kalkan.xmldsig.KncaXS;
import org.apache.xml.security.encryption.XMLCipherParameters;
import org.apache.xml.security.keys.KeyInfo;
import org.apache.xml.security.signature.XMLSignature;
import org.apache.xml.security.transforms.Transforms;
import org.apache.xml.security.utils.Constants;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.security.cert.CertificateFactory;
import java.io.FileInputStream;
/**
*
*
*/
public class XMLSign {
/**
* @param args the command line arguments
*/
public static void main(String[] args) {
String xml64 = "...";
/*String xmlString = "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
+ "<root>"
+ "<person id=\"someid\">"
+ "<name>Стеве Жобс</name>"
+ "<iin>123456789012</iin>"
+ "</person>"
+ "</root>";
//System.out.println("Hello World!");
XMLSign object = new XMLSign();
String signedXml = object.signBase64XML(xml64, "123456", xmlString);
object.verifyXml(signedXml);*/
XMLSign object = new XMLSign();
//Boolean checkedCRLErrors = object.checkCRL(xml64, "123456");
}
public Boolean checkCRL(String cert64, String password, String pathCa, String pathNca, String pathCrl, String pathRcrl) {
try {
// Получаем сертификат
Provider provider = new KalkanProvider();
Security.addProvider(provider);
KncaXS.loadXMLSecurity();
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
KeyStore store = KeyStore.getInstance("PKCS12", provider.getName());
// Здесь было чтение ключа из файла, сейчас читаем из base64
byte[] data = Base64.getDecoder().decode(cert64);
InputStream inputStream = new ByteArrayInputStream(data);
store.load(inputStream, password.toCharArray());
Enumeration<String> als = store.aliases();
String alias = null;
while (als.hasMoreElements()) {
alias = als.nextElement();
}
X509Certificate cert = (X509Certificate) store.getCertificate(alias);
// Дальше проверяем его на отозванность
CertificateFactory cf = CertificateFactory.getInstance("X.509", provider);
X509Certificate ca = (X509Certificate) cf.generateCertificate(new FileInputStream (pathCa));
ca.checkValidity();
X509Certificate nca = (X509Certificate) cf.generateCertificate(new FileInputStream (pathNca));
X509CRL crl = (X509CRL) cf.generateCRL(new FileInputStream (pathCrl));
X509CRL rcrl = (X509CRL) cf.generateCRL(new FileInputStream (pathRcrl));
ArrayList<X509Extension> list = new ArrayList<X509Extension>();
list.add(nca);
list.add(cert);
list.add(crl);
list.add(rcrl);
CollectionCertStoreParameters certStoreParameters = new CollectionCertStoreParameters(list);
CertStore certStore = CertStore.getInstance("Collection", certStoreParameters, provider);
CertPathBuilder builder = CertPathBuilder.getInstance("PKIX", provider);
// проверка до корневого CA, можно сократить до промежуточного NCA
TrustAnchor anchor = new TrustAnchor(ca, null);
Set<TrustAnchor> anchors = new HashSet<TrustAnchor>();
anchors.add(anchor);
X509CertSelector selector = new X509CertSelector();
// задаем параметры для селектора конечного сертификата или можно указать полное соответствие сертификата
//selector.setSerialNumber(cert.getSerialNumber());
//selector.setIssuer(cert.getIssuerX500Principal());
selector.setCertificate(cert);
PKIXBuilderParameters builderParameters = new PKIXBuilderParameters(anchors, selector);
// если не добавляли список CRL, то надо отключить проверку
builderParameters.setRevocationEnabled(false);
builderParameters.addCertStore(certStore);
builderParameters.setSigProvider(provider.getName());
PKIXCertPathBuilderResult builderResult = (PKIXCertPathBuilderResult) builder.build(builderParameters);
System.out.println(builderResult);
return true;
} catch (Exception e) {
return false;
}
}
//Подписываем XML
public String signBase64XML(String cert64, String password, String xmlString) {
String result = null;
try {
Provider provider = new KalkanProvider();
Security.addProvider(provider);
KncaXS.loadXMLSecurity();
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
DocumentBuilder documentBuilder = dbf.newDocumentBuilder();
final Document doc = documentBuilder.parse(new ByteArrayInputStream(xmlString.getBytes("UTF-8")));
final String signMethod;
final String digestMethod;
KeyStore store = KeyStore.getInstance("PKCS12", provider.getName());
// Здесь было чтение ключа из файла, сейчас читаем из base64
byte[] data = Base64.getDecoder().decode(cert64);
InputStream inputStream = new ByteArrayInputStream(data);
store.load(inputStream, password.toCharArray());
Enumeration<String> als = store.aliases();
String alias = null;
while (als.hasMoreElements()) {
alias = als.nextElement();
}
final PrivateKey privateKey = (PrivateKey) store.getKey(alias, password.toCharArray());
final X509Certificate x509Certificate = (X509Certificate) store.getCertificate(alias);
String sigAlgOid = x509Certificate.getSigAlgOID();
if (sigAlgOid.equals(PKCSObjectIdentifiers.sha1WithRSAEncryption.getId())) {
signMethod = Constants.MoreAlgorithmsSpecNS + "rsa-sha1";
digestMethod = Constants.MoreAlgorithmsSpecNS + "sha1";
} else if (sigAlgOid.equals(PKCSObjectIdentifiers.sha256WithRSAEncryption.getId())) {
signMethod = Constants.MoreAlgorithmsSpecNS + "rsa-sha256";
digestMethod = XMLCipherParameters.SHA256;
} else {
signMethod = Constants.MoreAlgorithmsSpecNS + "gost34310-gost34311";
digestMethod = Constants.MoreAlgorithmsSpecNS + "gost34311";
}
XMLSignature sig = new XMLSignature(doc, "", signMethod);
if (doc.getFirstChild() != null) {
doc.getFirstChild().appendChild(sig.getElement());
Transforms transforms = new Transforms(doc);
transforms.addTransform(Transforms.TRANSFORM_ENVELOPED_SIGNATURE);
transforms.addTransform(XMLCipherParameters.N14C_XML_CMMNTS);
sig.addDocument("", transforms, digestMethod);
sig.addKeyInfo(x509Certificate);
sig.sign(privateKey);
StringWriter os = new StringWriter();
TransformerFactory tf = TransformerFactory.newInstance();
Transformer trans = tf.newTransformer();
trans.transform(new DOMSource(doc), new StreamResult(os));
os.close();
result = os.toString();
}
System.err.println(result);
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
//Проверяем валидность XML
public boolean verifyXml(String xmlString) {
boolean result = false;
try {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
DocumentBuilder documentBuilder = dbf.newDocumentBuilder();
Document doc = documentBuilder.parse(new ByteArrayInputStream(xmlString.getBytes("UTF-8")));
Element sigElement = null;
Element rootEl = (Element) doc.getFirstChild();
NodeList list = rootEl.getElementsByTagName("ds:Signature");
int length = list.getLength();
for (int i = 0; i < length; i++) {
Node sigNode = list.item(length - 1);
sigElement = (Element) sigNode;
if (sigElement == null) {
System.err.println("Bad signature: Element 'ds:Reference' is not found in XML document");
}
XMLSignature signature = new XMLSignature(sigElement, "");
KeyInfo ki = signature.getKeyInfo();
X509Certificate cert = ki.getX509Certificate();
if (cert != null) {
result = signature.checkSignatureValue(cert);
rootEl.removeChild(sigElement);
}
}
} catch (Exception e) {
e.printStackTrace();
}
System.err.println("VERIFICATION RESULT IS: " + result);
return result;
}
}