четверг, 20 ноября 2014 г.

Реализация взаимодействия с веб-сервисом и формирование гостовой подписи с помощью OpenSSL

В данной статье описывается как прикрутить OpenSSL для организации подписи (платежные шлюзы билайна и мосэнергосбыта). Подпись и проверку подписи будем делать вручную, перехватывая сообщения перед отправкой/после получения. поскольку реализация веб-сервиса не оставляет другого выбора.
Будем использовать apache CXF 2.7.8 для работы с веб-сервисом. Для подписи будем использовать OpenSSL, предварительно сконвертировав ключи в pkcs12 формат, как описано в предыдущей статье.

и немного кода:

        logger.info("---------------WS-URL---------------");
        logger.info(config.getString("ws.service.url"));
        logger.info("-------------------------------------");

        QName SERVICE_NAME = new QName("http://tempuri.org", "PaymentGateService");
        QName PORT_NAME = new QName("http://tempuri.org", "PaymentGatePort");

        Service paymnetService = Service.create(SERVICE_NAME);
        paymnetService.addPort(PORT_NAME, SOAPBinding.SOAP11HTTP_BINDING, Config.getWsAddress());
        PaymentGate iface = paymnetService.getPort(PORT_NAME, PaymentGate.class);

        SignatureInterceptor transformOutInterceptor = new SignatureInterceptor();
        Client client = ClientProxy.getClient(iface);
        HTTPConduit http = (HTTPConduit) client.getConduit();
        final TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {
            @Override
            public java.security.cert.X509Certificate[] getAcceptedIssuers() {
                return null;
            }
            @Override
            public void checkClientTrusted(X509Certificate[] certs, String authType) {
            }
            @Override
            public void checkServerTrusted(X509Certificate[] certs, String authType) {
            }
        }
        };
        TLSClientParameters parameters = new TLSClientParameters();
        parameters.setTrustManagers(trustAllCerts);
        parameters.setDisableCNCheck(true);
        http.setTlsClientParameters(parameters);

        HTTPClientPolicy httpClientPolicy = new HTTPClientPolicy();

        httpClientPolicy.setConnectionTimeout(10000);
        httpClientPolicy.setAllowChunking(false);
        httpClientPolicy.setReceiveTimeout(40000);

        http.setClient(httpClientPolicy);

        client.getOutInterceptors().add(new LoggingOutInterceptor());
        client.getOutInterceptors().add(transformOutInterceptor);
        client.getInInterceptors().add(new SignatureVerificationInterseptor());
        System.setProperty("com.sun.xml.ws.transport.http.client.HttpTransportPipe.dump", String.valueOf(Config.isHttpDump()));

Для формирования подписи нам нужно получить сообщение целиком в том виде, в котором оно будет отправлено. Каноникализация производится неявным образом, путем удаления всего форматирования xml документа. используем следующий код, частично скопированный из исходников CXF. Из тонких моментов - инвертация полученной подписи. Видимо есть различия в том, как CryptoPro и OpenSSL возвращают результат подписи, кто-то в Big-endian, кто-то в Little-endian
public class SignatureInterceptor extends AbstractOutDatabindingInterceptor {
    private final static Logger logger = LoggerFactory.getLogger(SignatureInterceptor.class);

    public SignatureInterceptor() {
        super(Phase.PRE_LOGICAL);
        getBefore().add(HolderOutInterceptor.class.getName());
    }

    public void handleMessage(Message message) throws Fault {
        try {
            OutputStream out = new ByteArrayOutputStream();
            Exchange exchange = message.getExchange();
            Service service = exchange.getService();
            XMLOutputFactory instance = XMLOutputFactory.newInstance();
            XMLStreamWriter xmlWriter = instance.createXMLStreamWriter(out);

            DataWriter<XMLStreamWriter> dataWriter = getDataWriter(message, service, XMLStreamWriter.class);
            OperationInfo op = exchange.getBindingOperationInfo().getOperationInfo();
            List<MessagePartInfo> parts = op.getInput().getMessageParts();
            MessageContentsList objs = MessageContentsList.getContentsList(message);
            for (MessagePartInfo part : parts) {
                if (objs.hasValue(part)) {
                    Object o = objs.get(part);
                    if (!part.getConcreteName().getLocalPart().equals("digitalSignature")) {
                        dataWriter.write(o, part, xmlWriter);
                    }
                }
            }
            xmlWriter.flush();
            logger.debug("SIGNATURE BEGIN");
            logger.debug(out.toString());
            String signature = sign(out.toString());
            logger.debug(signature);
            logger.debug("signature length = " + signature.length());
            logger.debug("SIGNATURE END");
            xmlWriter.close();
            for (MessagePartInfo part : parts) {
                if (part.getConcreteName().getLocalPart().equals("digitalSignature")) {
                    DigitalSignature ds = new DigitalSignature();
                    ds.setSignature(signature);
                    objs.put(part, ds);
                }
            }
        } catch (Exception ex) {
            throw new Fault(ex);
        }
    }

    private static String sign(String content) throws SignatureException{
        File f = null;
        File fOut = null;
        try {
            f = File.createTempFile("input", "data");
            FileOutputStream foutStream = new FileOutputStream(f);
            IOUtils.write(content, foutStream);
            foutStream.flush();
            IOUtils.closeQuietly(foutStream);
            fOut = File.createTempFile("input", "sng");

            ProcessBuilder pb = new ProcessBuilder(Config.getInstance().getString("crypto.command"),
                    "dgst",
                    "-engine",
                    "gost",
                    "-binary",
                    "-md_gost94",
                    "-out",
                    fOut.getAbsolutePath(),
                    "-sign",
                    Config.getInstance().getString("crypto.private.key"),
                    f.getAbsolutePath());

            logger.debug(StringUtils.join(pb.command(), " "));

            Process p = pb.start();
            InputStream is = p.getErrorStream();
            p.waitFor();
            if (p.exitValue() != 0) {
                ByteArrayOutputStream out = new ByteArrayOutputStream();
                IOUtils.copy(is, out);
                logger.error(out.toString("UTF-8"));
                IOUtils.closeQuietly(is);
                IOUtils.closeQuietly(out);
                throw new SignatureException("OpenSSL return error code: " + p.exitValue());
            }
            IOUtils.closeQuietly(is);
            FileInputStream fis = new FileInputStream(fOut);
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            IOUtils.copy(fis, bos);
            IOUtils.closeQuietly(fis);
            byte[] res = bos.toByteArray();
            IOUtils.closeQuietly(bos);
            // Инвертируем полученную подпись - иначе она считается невалидной.
            byte[] inverted = new byte[res.length];
            for(int i = 0; i< res.length; i++){
                inverted[res.length-i-1] = res[i];
            }
            return new String(Base64.encodeBase64(inverted, false));
        } catch (IOException ex) {
            throw new SignatureException(ex.getMessage(), ex);
        } catch (InterruptedException e) {
            logger.error("Signature process was interrupted, returning null...");
            return null;
        } finally {
            FileUtils.deleteQuietly(f);
            FileUtils.deleteQuietly(fOut);
        }
    }


}
И проверка подписи:
public class SignatureVerificationInterseptor extends LoggingInInterceptor {
    private final static Logger logger = LoggerFactory.getLogger(SignatureVerificationInterseptor.class);

    public SignatureVerificationInterseptor() {
        super();
    }

    @Override
    protected String formatLoggingMessage(LoggingMessage loggingMessage) {
        String soapXmlPayload = loggingMessage.getPayload().toString();

        try {
            String content = soapXmlPayload.substring(soapXmlPayload.indexOf("<soapenv:Body>") + "<soapenv:Body>".length(), 
                                                   soapXmlPayload.indexOf("</soapenv:Body>"));
            String signature = soapXmlPayload.substring(soapXmlPayload.indexOf("<signature>") + "<signature>".length(),
                                                   soapXmlPayload.indexOf("</signature>"));
            logger.debug("signature verifying");
            logger.debug(content);
            logger.debug(signature);
            if (!verify(content, signature)) {
                logger.debug(super.formatLoggingMessage(loggingMessage));
                throw new SignatureVerificationException("Signature verification error");
            }
        }catch (SignatureVerificationException ex){
            throw ex;
        }catch (Exception ex){
            logger.debug(super.formatLoggingMessage(loggingMessage));
            throw new RuntimeException("Signature not found in soap envelope");
        }
        return super.formatLoggingMessage(loggingMessage);
    }

    private static boolean verify(String content, String signature){
        File signatureFile = null;
        File datafile = null;
        try {
            signatureFile = File.createTempFile("output", "sng");
            FileOutputStream signatureStream = new FileOutputStream(signatureFile);
            byte[] sig = Base64.decodeBase64(signature.getBytes());
            // Инвертируем полученную подпись - иначе она считается невалидной.
            byte[] inverted = new byte[sig.length];
            for(int i = 0; i< sig.length; i++){
                inverted[sig.length-i-1] = sig[i];
            }
            IOUtils.write(inverted, signatureStream);
            signatureStream.flush();
            IOUtils.closeQuietly(signatureStream);


            datafile = File.createTempFile("output", "data");
            FileOutputStream dataStream = new FileOutputStream(datafile);
            IOUtils.write(content, dataStream);
            dataStream.flush();
            IOUtils.closeQuietly(dataStream);

            ProcessBuilder pb = new ProcessBuilder(Config.getInstance().getString("crypto.command"),
                    "dgst",
                    "-engine",
                    "gost",
                    "-binary",
                    "-verify",
                    Config.getInstance().getString("crypto.public.cert"),
                    "-signature",
                    signatureFile.getAbsolutePath(),
                    datafile.getAbsolutePath());

            logger.debug(StringUtils.join(pb.command(), " "));

            Process p = pb.start();
            InputStream is = p.getErrorStream();
            p.waitFor();
            if (p.exitValue() != 0) {
                ByteArrayOutputStream out = new ByteArrayOutputStream();
                IOUtils.copy(is, out);
                logger.error(out.toString("UTF-8"));
                IOUtils.closeQuietly(is);
                IOUtils.closeQuietly(out);
                return false;
            }
            return true;
        }catch (Exception ex){
            logger.error(ex.getClass().toString() + " message:" + ex.getMessage(), ex);
            return false;
        } finally {
            FileUtils.deleteQuietly(signatureFile);
            FileUtils.deleteQuietly(datafile);
        }
    }