четверг, 25 мая 2017 г.

Использование BouncyCastle вместо TumarCSP

Казахская криптография очень похожа на российскую, те же алгоритмы, те же ГОСТы, все отличие - в OID и форматах приватных ключей.
К примеру, ГОСТ 34.310-2004 соответсвует ГОСТ Р 34.10-2001, ГОСТ 34.311-95 соответсвует ГОСТ Р 34.11-94.
Так что если есть задача реализовать вызов WS-Security веб-сервиса с подписью по алгоритму  http://www.w3.org/2001/04/xmldsig-more#gost34310-gost34311 и хэшом по алгоритму http://www.w3.org/2001/04/xmldsig-more#gost34311 ( AvangardPlat к примеру ), то можно обойтись стандартными средствами типа BouncyCastle ( проверял на версии 1.57, последней на данный момент ), по крайней мере для ряда алгоритмов.

Задача состоит по большому счету из 3 частей - разбор плючевой пары из некоего проприетарного формата TumarCSP, выбор другого набора S-блоков для функции хэширования, регистрация алгоритмов подписи и функции хэширования для xmlsec.

К сожалению, придется писать свой собственный маленький криптопровайдер, который будет большую часть своей работы делегировать BouncyCastle.

Начнем с OID, у компании, которая разрабатывает TumarCSP есть свой набор идентификаторов, которые конечно же никто в BouncyCastle или OpenSSL не добавит. Частично посмотреть можно здесь http://www.oid-info.com/get/1.3.6.1.4.1.6801.1

смотрим сертификат с помощью dumpasn1, публичный ключ 80 байт, и первые 16 какие-то подозрительные 06 02 00 00 3A AA 00 00 00 45 43 31 00 02 00 00 если смотреть в ascii., то 45 43 31 это EC1, а 0xAA3A - внутренний идентификатор Tumar для алгоритма 1.3.6.1.4.1.6801.1.5.8. То есть тут продублирована некая описательная часть ключа, которая нам уже известна, нам интересны следуюшие 64 байта, которые представляют собой координаты точки на эллиптической кривой.

285  99:             SEQUENCE {
287  14:               SEQUENCE {
289  10:                 OBJECT IDENTIFIER '1 3 6 1 4 1 6801 1 5 8'
301   0:                 NULL
       :                 }
303  81:               BIT STRING
       :                 06 02 00 00 3A AA 00 00 00 45 43 31 00 02 00 00
       :                 59 5A 9B 09 B7 6E AA 2C 3E F6 37 1C D0 15 42 18
       :                 39 EF 5B 15 D1 D8 11 E4 D7 E7 84 4C 00 2A 11 19
       :                 61 C5 C3 E5 F1 44 98 46 93 0D 62 96 44 F6 E7 E7
       :                 82 4A EF 5D B2 12 A9 8C F6 2D 31 56 27 8F 7A 6F
       :               }

ищем что такое 1.3.6.1.4.1.6801.1.5.8, находим, что это все равно что CryptoProObjectIdentifiers.gostR3410_2001_CryptoPro_A (1.2.643.2.2.35.1) прописываем идентификатор.

public class GammaOID {
    static private final ASN1ObjectIdentifier GOST_id = new ASN1ObjectIdentifier("1.3.6.1.4.1.6801.1");
    static private final ASN1ObjectIdentifier GOST_34310_2004 = GOST_id.branch("5");
    static final ASN1ObjectIdentifier GOST_34310_2004_parameter_A = GOST_34310_2004.branch("8");
}

Теперь можно создавать провайдера, прописав ему кастомную KeyFactory для алгоритма 1.3.6.1.4.1.6801.1.5.8

public class KzGostProvider extends Provider {

    public KzGostProvider() {
        super("GOST", 1.0, "Little kazakh provider");
        put("Alg.Alias.KeyFactory." + GammaOID.GOST_34310_2004_parameter_A, "KZGOST");
        put("KeyFactory.KZGOST", "com.gost.jce.KzGostKeyFactorySpi");
    }
}

в KzGostKeyFactorySpi нужно реализовать логику по разбору ключей. Мы будем разбирать только тип ключей GammaOID.GOST_34310_2004_parameter_A, это позволит немного упростить код. Возможно другие типы точно так же просто разбираются, не проверял.

public class KzGostKeyFactorySpi extends org.bouncycastle.jcajce.provider.asymmetric.ecgost.KeyFactorySpi {
    public PrivateKey generatePrivate(PrivateKeyInfo keyInfo) throws IOException {
        ASN1ObjectIdentifier algOid = keyInfo.getPrivateKeyAlgorithm().getAlgorithm();
        if (algOid.equals(GammaOID.GOST_34310_2004_parameter_A)) {
            return KzGostKeyUtils.populateFromPrivKeyInfo(keyInfo);
        } else {
            throw new IOException("algorithm identifier " + algOid + " in key not recognised");
        }
    }

    public PublicKey generatePublic(SubjectPublicKeyInfo keyInfo) throws IOException {
        ASN1ObjectIdentifier algOid = keyInfo.getAlgorithm().getAlgorithm();
        if (algOid.equals(GammaOID.GOST_34310_2004_parameter_A)) {
            return KzGostKeyUtils.populateFromPubKeyInfo(keyInfo);
        } else {
            throw new IOException("algorithm identifier " + algOid + " in key not recognised");
        }
    }
}

Вот где вся логика по чтению ключей, она большей частью взята из BouncyCastle, различие только в представлении публичного ключа - пришлось выкинуть первые 16 байт заголовка. Важно! x и y в публичном ключе нужно разворачивать, а в приватном - d не нужно. И BouncyCastle проверяет публичный ключ, но не делает этого для приватного. В приватник можно скормить все что угодно, оно якобы будет работать, но по факту подпись не будет проходить проверку.

public class KzGostKeyUtils {

    private static ASN1ObjectIdentifier OID = CryptoProObjectIdentifiers.gostR3410_2001_CryptoPro_A;

    public static BCECGOST3410PrivateKey populateFromPrivKeyInfo(PrivateKeyInfo kryInfo) throws IOException {

        ECNamedCurveParameterSpec spec = ECGOST3410NamedCurveTable.getParameterSpec(OID.toString());
        ECCurve var4 = spec.getCurve();
        EllipticCurve curve = EC5Util.convertCurve(var4, spec.getSeed());
        ECParameterSpec ecSpec = new ECNamedCurveSpec(ECGOST3410NamedCurves.getName(OID), curve, new ECPoint(spec.getG().getAffineXCoord().toBigInteger(), spec.getG().getAffineYCoord().toBigInteger()), spec.getN(), spec.getH());
        ASN1Encodable privateKey = kryInfo.parsePrivateKey();

        ASN1Sequence sequence = (ASN1Sequence) privateKey;
        // 0 - integer !
        // 1 - private key
        DEROctetString key = (DEROctetString) sequence.getObjectAt(1);
        byte[] keyOctets = key.getOctets();
        BigInteger d = new BigInteger(1, keyOctets);
        ECPrivateKeySpec keySpec = new ECPrivateKeySpec(d, ecSpec);
        return new BCECGOST3410PrivateKey(keySpec);

    }

    public static BCECGOST3410PublicKey populateFromPubKeyInfo(SubjectPublicKeyInfo info) throws IOException {
        DERBitString bits = info.getPublicKeyData();

        String algorithm = "ECGOST3410";
        byte[] keyEnc = new byte[64];

        // skip first 16 byte - some type of header, not related.
        System.arraycopy(bits.getBytes(), 16, keyEnc, 0, 64);
        byte[] x = new byte[32];
        byte[] y = new byte[32];

        for (int i = 0; i != x.length; i++) {
            x[i] = keyEnc[32 - 1 - i];
        }

        for (int i = 0; i != y.length; i++) {
            y[i] = keyEnc[64 - 1 - i];
        }

        ECNamedCurveParameterSpec spec = ECGOST3410NamedCurveTable.getParameterSpec(OID.toString());
        ECCurve curve = spec.getCurve();
        EllipticCurve ellipticCurve = EC5Util.convertCurve(curve, spec.getSeed());
        org.bouncycastle.math.ec.ECPoint q = curve.createPoint(new BigInteger(1, x), new BigInteger(1, y));
        ECPublicKeyParameters publicKeyParameters = new ECPublicKeyParameters(q, ECUtil.getDomainParameters(null, spec));
        ECPoint p = new ECPoint(spec.getG().getAffineXCoord().toBigInteger(), spec.getG().getAffineYCoord().toBigInteger());
        ECNamedCurveSpec ecSpec = new ECNamedCurveSpec(
                spec.getName(),
                ellipticCurve,
                p, spec.getN(), spec.getH());

        return new BCECGOST3410PublicKey(algorithm, publicKeyParameters, ecSpec);
    }

Можно тестировать загрузку ключей из pkcs12

    @Before
    public void setUp() throws Exception {
        Security.addProvider(new BouncyCastleProvider());
        Security.addProvider(new KzGostProvider());
        XmlSecAlgorithmRegistrator.register(); //это будет описано далее
    }

    @Test
    public void testReadTumarKey() throws Exception{

        Set<Object> keySet = Security.getProvider("GOSTKZ").keySet();
        for(Object o : keySet){
            if(o.toString().contains("1.3.6.1.4.1.6801.1.5.8")) {
                System.out.println(o + " -> " + Security.getProvider("GOSTKZ").get(o));
            }
        }

        String keyPath = "key/GOST/pkcs12_sign.p12";
        KeyStore ks = KeyStore.getInstance("PKCS12");
        ks.load(new FileInputStream(keyPath), password.toCharArray());
        Assert.assertNotNull(ks);
        Enumeration<String> aliases = ks.aliases();
        String a;
        PrivateKey privateKey = null;
        X509Certificate cert = null;
        while (aliases.hasMoreElements()){
            a = aliases.nextElement();
            System.out.println(a);
            if(ks.isKeyEntry(a)) {
                privateKey = (PrivateKey) ks.getKey(a, password.toCharArray());
                Assert.assertNotNull(privateKey);
                cert = (X509Certificate) ks.getCertificate(a);
                Assert.assertNotNull(cert);
            }
        }
   }

Но это только треть работы. Приступаем к формированию подписи с помошью org.apache.santuario.xmlsec (2.0.8) последней на данный момент версии.
Нам нужно зарестрировать алгоритмы для xmlsec

public class XmlSecAlgorithmRegistrator {

    public static final String signatureUrl = "http://www.w3.org/2001/04/xmldsig-more#gost34310-gost34311";
    public static final String digestUrl = "http://www.w3.org/2001/04/xmldsig-more#gost34311";

    public static void register() throws ClassNotFoundException, AlgorithmAlreadyRegisteredException, XMLSignatureException, ParserConfigurationException {
        Init.init();
        DocumentBuilderFactory documentbuilderfactory = DocumentBuilderFactory.newInstance();
        documentbuilderfactory.setNamespaceAware(true);

        SignatureAlgorithm.register(signatureUrl, "com.gost.xmlsec.XmlSecSignatureGost34310$Gost34310Gost34311XmlSec");
        SignatureAlgorithm.register(digestUrl, "com.gost.xmlsec.XmlSecSignatureGost34310$Gost34311XmlSec");


        String throwable = "http://www.xmlsecurity.org/NS/#configuration";
        Document document = documentbuilderfactory.newDocumentBuilder().newDocument();
        {
            Element element = document.createElementNS(throwable, "Algorithm");
            element.setAttribute("URI", signatureUrl);
            element.setAttribute("Description", "GOST 34310 Digital Signature Algorithm with GOST 34311 Digest");
            element.setAttribute("AlgorithmClass", "Signature");
            element.setAttribute("RequirementLevel", "OPTIONAL");
            element.setAttribute("JCEName", "ECGOST34310");
            JCEMapper.Algorithm algorithm = new JCEMapper.Algorithm(element);
            JCEMapper.register(signatureUrl, algorithm);
        }
        {
            Element element = document.createElementNS(throwable, "Algorithm");
            element.setAttribute("URI", digestUrl);
            element.setAttribute("Description", "GOST 34311 Digest");
            element.setAttribute("AlgorithmClass", "MessageDigest");
            element.setAttribute("RequirementLevel", "OPTIONAL");
            element.setAttribute("JCEName", "ECGOST34311");
            JCEMapper.Algorithm algorithm = new JCEMapper.Algorithm(element);
            JCEMapper.register(digestUrl, algorithm);
        }

    }

}

тут примечательны строки 
        SignatureAlgorithm.register(signatureUrl, "com.gost.xmlsec.XmlSecSignatureGost34310$Gost34310Gost34311XmlSec");
        SignatureAlgorithm.register(digestUrl, "com.gost.xmlsec.XmlSecSignatureGost34310$Gost34311XmlSec");

в которых происходит регистрация классов оберток для реализаций алгоритмов, и 
            element.setAttribute("JCEName", "ECGOST34310"); 
            element.setAttribute("JCEName", "ECGOST34311");
в которых происходит указание на то. какой класс из JCE провайдера использовать.

public abstract class XmlSecSignatureGost34310 extends org.apache.xml.security.algorithms.SignatureAlgorithmSpi {
    private Signature signatureAlgorithm = null;

    public abstract String engineGetURI();

    public XmlSecSignatureGost34310() throws XMLSignatureException {
        String s = JCEMapper.translateURItoJCEID(this.engineGetURI());
        try {
            this.signatureAlgorithm = Signature.getInstance(s);
        } catch (NoSuchAlgorithmException var4) {
            throw new XMLSignatureException("algorithms.NoSuchAlgorithm", new Object[]{s, var4.getLocalizedMessage()});
        }
    }

    protected void engineSetParameter(AlgorithmParameterSpec spec) throws XMLSignatureException {
        try {
            this.signatureAlgorithm.setParameter(spec);
        } catch (InvalidAlgorithmParameterException e) {
            throw new XMLSignatureException(e);
        }
    }
    .... куча методов - оберток, делегирующих методы signatureAlgorithm

    protected String engineGetJCEAlgorithmString() {
        return this.signatureAlgorithm.getAlgorithm();
    }

    protected String engineGetJCEProviderName() {
        return this.signatureAlgorithm.getProvider().getName();
    }

    protected void engineSetHMACOutputLength(int i) throws XMLSignatureException {
        throw new XMLSignatureException("algorithms.HMACOutputLengthOnlyForHMAC");
    }

    protected void engineInitSign(Key key, AlgorithmParameterSpec algorithmparameterspec) throws XMLSignatureException {
        throw new XMLSignatureException("algorithms.CannotUseAlgorithmParameterSpecOnSignatureGost34310");
    }

    public static class Gost34310Gost34311XmlSec extends XmlSecSignatureGost34310 {

        public String engineGetURI() {
            return "http://www.w3.org/2001/04/xmldsig-more#gost34310-gost34311";
        }

        public Gost34310Gost34311XmlSec() throws XMLSignatureException {
        }
    }
    public static class Gost34311XmlSec extends XmlSecSignatureGost34310 {

        public String engineGetURI() {
            return "http://www.w3.org/2001/04/xmldsig-more#gost34311";
        }

        public Gost34311XmlSec() throws XMLSignatureException {
        }
    }
}

и в провайдер добавляются строки 

        put("Signature.ECGOST34310", "com.gost.jce.ECGOST34310SignatureSpi"); // signature ECGOST3410 with digest GOST3411 with D-Test SBlock
        put("MessageDigest.ECGOST34311", "com.gost.jce.ECGOST34311Digest"); // hash GOST3411 with D-Test SBlock

которые отсылают к конкретной реализации подписи и хэша. Если бы Tumar использовал для хэширования те же самые S-блоки, что и BouncyCastle (D-A) то создавать отдельных файлов не потребовалось бы, а так приходится заменять digest GOST3411Digest другим экземпляром, созданным с правильными S-блоками, иначе другое значение хэша и подпись не пройдет проверку.

Тут через рефлекшн подменяется поле digest.
public class ECGOST34310SignatureSpi extends org.bouncycastle.jcajce.provider.asymmetric.ecgost.SignatureSpi {

    public ECGOST34310SignatureSpi() throws Exception {
            final Field digest = SignatureSpi.class.getDeclaredField("digest");
            digest.setAccessible(true);
            digest.set(this, new GOST3411Digest(GOST28147Engine.getSBox("D-test")));
        }catch (NoSuchFieldException ex){
            logger.error("tested with BouncyCastle 1.57, check private field  `digest` in org.bouncycastle.jcajce.provider.asymmetric.ecgost.SignatureSpi", ex);
           throw ex;
        }catch (SecurityException | IllegalAccessException ex){
            logger.error(ex);
            throw ex;
        }
    }
}

А тут просто тривиальная реализация с правильным digest.
public class ECGOST34311Digest extends MessageDigest {
    private GOST3411Digest digest = new GOST3411Digest(GOST28147Engine.getSBox("D-test"));

    public ECGOST34311Digest() {
        super("GOST3411");
    }

    protected ECGOST34311Digest(String algorithm) {
        super(algorithm);
    }

    @Override
    protected void engineUpdate(byte input) {
        digest.update(input);
    }

    @Override
    protected void engineUpdate(byte[] input, int offset, int len) {
        digest.update(input, offset, len);
    }

    @Override
    protected byte[] engineDigest() {
        byte[] hash = new byte[digest.getDigestSize()];
        digest.doFinal(hash, 0);
        return hash;
    }

    @Override
    protected void engineReset() {
        digest.reset();
    }
}

В принципе, теперь почти все готово для тестирования, нужно только написать классы-обертки для формирования и проверки подписи.

public class XmlSecRequestHelper {

    public static String signRequest(String requestXml, X509Certificate cert, PrivateKey privateKey) throws Exception {
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        dbf.setNamespaceAware(true);
        DocumentBuilder documentBuilder = dbf.newDocumentBuilder();
        Document doc = documentBuilder.parse(new ByteArrayInputStream(requestXml.getBytes(UTF_8)));

        Element tbsEl = (Element) doc.getElementsByTagName("body").item(0);
        tbsEl.setIdAttribute("id", true);

        XMLSignature xmlSignature = new XMLSignature(doc, "", XmlSecAlgorithmRegistrator.signatureUrl);

        Transforms transforms = new Transforms(doc);
        transforms.addTransform(TRANSFORM_ENVELOPED_SIGNATURE);
        transforms.addTransform(TRANSFORM_C14N_WITH_COMMENTS);

        doc.getElementsByTagName("security").item(0).appendChild(xmlSignature.getElement());

        xmlSignature.addDocument("#signedContent", transforms, XmlSecAlgorithmRegistrator.digestUrl);
        xmlSignature.addKeyInfo(cert);
        xmlSignature.sign(privateKey);

        StringWriter os = new StringWriter();
        TransformerFactory tf = TransformerFactory.newInstance();
        Transformer trans = tf.newTransformer();
        trans.transform(new DOMSource(doc), new StreamResult(os));
        os.close();
        return os.toString();
    }


    public static boolean checkSignature(String requestXml) throws XMLSecurityException, IOException, SAXException, ParserConfigurationException {

        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        dbf.setNamespaceAware(true);
        DocumentBuilder documentBuilder = dbf.newDocumentBuilder();
        Document doc = documentBuilder.parse(new ByteArrayInputStream(requestXml.getBytes(UTF_8)));
        Element sigElement = (Element) doc.getElementsByTagName("ds:Signature").item(0);
        ((Element) doc.getElementsByTagName("body").item(0)).setIdAttribute("id", true);
        XMLSignature signature = new XMLSignature(sigElement, "");
        KeyInfo ki = signature.getKeyInfo();
        return signature.checkSignatureValue(ki.getX509Certificate());
    }
}

и остаток теста

          String request ="<request><header><security/></header><body id=\"signedContent\"><serviceId>753</serviceId><accountId>7688</accountId><amount>123</amount><commission>10</commission></body></request>";

        String signedRequest = XmlSecRequestHelper.signRequest(request, cert, privateKey );
        System.out.println(signedRequest);

        String terminalId = "000";
        String userId = "111";
        String password = "xxx";

        String url = "https://xxx.xxx.xxx.xxx:8080/";
        HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection();
        connection.setHostnameVerifier(new HostnameVerifier() {
            @Override
            public boolean verify(String s, SSLSession sslSession) {
                return true;
            }
        });
        connection.setSSLSocketFactory(getSSLContext().getSocketFactory()); // getSSLContext возвращает кастомный SSL контекст, поскольку сервер использует самоподписанный сертификат
        connection.setDoOutput(true);
        connection.setRequestMethod("POST");
        connection.setRequestProperty("Content-Type", "application/xml");
        connection.setRequestProperty("ProtocolVersion", "1");
        connection.setRequestProperty("OperationType", "1");
        connection.setRequestProperty("Authorization", "Basic " + java.util.Base64.getEncoder().encodeToString((terminalId + "|" + userId + ":" + password).getBytes(UTF_8)));

        try( DataOutputStream wr = new DataOutputStream( connection.getOutputStream())) {
            wr.write( signedRequest.getBytes(UTF_8) );
            wr.flush();
        }
        Reader in = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
        Map<String, List<String>> headers = connection.getHeaderFields();

        for(Map.Entry<String, List<String>> entry : headers.entrySet()){
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
        System.out.println("--------------------------------------------");
        StringBuffer sb = new StringBuffer();

        for (int c; (c = in.read()) >= 0;) {
            System.out.print((char) c);
            sb.append((char)c);
        }
        String prolog = "<?xml version=\"1.0\" encoding=\"utf-8\"?>";
        int ind = sb.indexOf(prolog);
        sb.insert(ind + prolog.length(), "\n"); // если это топо, но работает, то это не так уж и тупо?
        System.out.println("signature valid : " + XmlSecRequestHelper.checkSignature(sb.toString()));

Как результат - запрос отправляется, подпись валидна, TumarCSP не используется.


3 комментария:

  1. Мужик нужна помощь!
    Передо мной как раз стоит задача не использования Tumar. Натолкнулся на эту статью, но признаюсь я тупой, не могу собрать пример. Только начал работу в этой сфере не разбираюсь ни в криптографии, ни в Боунси Кастл, короче плохи пока мои дела. Можно ли скинуть пример полностью со всеми импортами и требуемыми библиотеками.

    ОтветитьУдалить
  2. Не, ну не компилируется и все. Не пойму что я упускаю.

    ОтветитьУдалить
  3. Е мое разобрался. Огромная благодарность и респект автору статьи!

    ОтветитьУдалить