Consumir un WebService SOAP con certificado digital desde PHP

Rina, una lectora del newsletter de Leeway Academy, me escribe lo siguiente:

Necesito consumir un webservice con php utilizando certificado para autenticarse, recibí .pfx y lo convertí en .pem, obtuve cert.pem y key.pem.

A primera vista parece algo sencillo, ¿cierto? Se trata de conectarse a un servidor del que ya sabemos su URL, tenemos los certificados… ¿qué podría salir mal?

Claro que, como siempre que se trata de usar SOAP, las cosas no son tan fáciles como aparentan.

Para empezar, hay que decidir qué herramientas usaremos para realizar la conexión (cURL, file_get_contents, Guzzle…).

Después está el tema de cómo especificar el certificado… y por último, qué hacer con la respuesta del servidor una vez la hayamos obtenido.

Porque, no olvidemos que, siendo un WebService SOAP, la respuesta es un XML. Pero no un XML cualquiera, un XML diseñado según las especificaciones de SOAP.

Y, por si faltaba algo, lo más probable es que no se trate de una sola petición, no. Para poder hacer algo más que sólo conectarnos al servidor, seguramente tengamos que hacer unas cuantas.

Juntemos todo eso y tenemos un buen dolor de cabeza por delante.

Bueno… no necesariamente. Las cosas pueden ser bastante más sencillas si tenemos claro qué hacer.

Empecemos aclarando algunos conceptos.

Qué es un certificado digital

Un certificado digital es un mecanismo que permite a una computadora asegurarse de que su contraparte en una comunicación digital es efectivamente quien dice ser.

Más en concreto: cuando se realiza una comunicación web, típicamente existen dos actores, el cliente y el servidor.

El cliente envía un mensaje codificado en HTTP y el servidor le responde de la misma forma.

El problema es que, en ocasiones, puede haber algún intermediario no deseado (Lo que se conoce como ataque de man-in-the-middle):

Cuando se debe enviar información sensible, es muy importante para el emisor poder determinar que quien recibe esa información es, efectivamente, quien dice ser.

Y ¿cómo puede asegurarse la identidad de la contraparte? Precisamente, con un certificado digital.

Los certificados digitales son emitidos por entidades en las que el emisor confía (Autoridades de Certificación), algo similar al DNI que otorgan los entes gubernamentales a las personas.

De modo que, mediante criptografía, es posible determinar si un certificado presentado por una parte está avalado por alguna autoridad competente.

Todo esto pasa de forma transparente cada vez que navegás hacia una página que comienza con https en lugar de http.

Ese es el caso más común: como cliente no querés enviar tus datos de tarjeta de crédito a un servidor a menos que tengas la certeza de que estás comunicándote con quien creés que estás haciéndolo.

Pues bien, lo mismo puede ocurrir a la inversa. Es decir, un servidor puede negarse a establecer comunicación con un cliente del cual no conoce su identidad.

¿Cómo puede el cliente dar fé de su identidad? Del mismo modo, ofreciéndole al servidor un certificado avalado por alguna autoridad en la que él confíe.

Ese es el caso que estamos tratando en este artículo.

Cómo especificar el certificado digital en una llamada SOAP

Ahora sí, habiendo despejado los temas más teóricos, pasemos a la práctica.

Asumiendo que contamos con el archivo de certificado (.pem) y el de clave privada (.key), podemos realizar la conexión directa, usando cURL, con el servidor de esta forma:

$ch = curl_init(); 
curl_setopt($ch, CURLOPT_URL, $url); 
curl_setopt($ch, CURLOPT_VERBOSE, 1); 
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1); 
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 1); 
curl_setopt($ch, CURLOPT_FAILONERROR, 1); 
curl_setopt($ch, CURLOPT_SSLCERT, __DIR__ . '/client.crt'); 
curl_setopt($ch, CURLOPT_SSLCERTTYPE, 'PEM'); 
curl_setopt($ch, CURLOPT_SSLKEY, __DIR__ . '/client.key'); 
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: text/xml'));
curl_setopt($ch, CURLOPT_POSTFIELDS, $requestXml);
$ret = curl_exec($ch);

Algo bastante laborioso por cierto.

Una alternativa ligeramente más sencilla es usar file_get_contents en combinación con stream_context_create para manejar la parte de los certificados:

$ret = file_get_contents($url, false, stream_context_create(
[
        'ssl' => [
            'local_cert' => __DIR__ . '/client.crt',
            'local_pk' => __DIR__ . '/client.key',
        ]
    ]
);

El problema es que nos encontraremos, a la vuelta, con algo como:

<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope
    xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <soapenv:Body>
        <deudasDeudorResponse soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
            <deudasDeudorReturn href="#id0"/>
        </deudasDeudorResponse>
        <multiRef id="id0" soapenc:root="0" soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xsi:type="ns1:DeudasDeudorClient"
            xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"
            xmlns:ns1="urn:consultadeudasxinstWs.bcu.gub.uy">
            <deudas soapenc:arrayType="ns1:DeudaInstitucionClient[5]" xsi:type="soapenc:Array">
                <deudas href="#id1"/>
                <deudas href="#id2"/>
                <deudas href="#id3"/>
                <deudas href="#id4"/>
                <deudas href="#id5"/></deudas>
            <deudor href="#id6"/>
            <periodo href="#id7"/>
            <tipoDeCambio href="#id8"/>
        </multiRef>        
    </soapenv:Body>
</soapenv:Envelope>

Es decir, tendremos que interpretar este XML para poder procesarlo efectivamente.

Si bien es perfectamente posible hacerlo una herramienta como SimpleXML o, directamente parseándolo mediante expresiones regulares, la verdad es que cualquiera de estos métodos es sumamente ineficiente y propenso a errores.

De hecho, si el archivo contiene algo como <wsdl:import location="http://localhost:8080/soapservice/services/quoteService?wsdl=RandomQuote.wsdl" namespace="http://examples.javacodegeeks.com/"> vamos a tener que hacer otra llamada, otra vez especificando los parámetros de seguridad y así sucesivamente.

Un modo mucho mejor es utilizar la clase SoapClient, la cual permite especificar, entre otras, cómo conectarse al servidor remoto:

$client = new SoapClient($url,
        [
            'stream_context' => stream_context_create(
                [
                    'ssl' => [
                        'local_cert' => __DIR__ . '/client.crt',
                        'local_pk' => __DIR__ . '/client.key',
                    ]
                ]
            ),
        ]);

A partir de aquí ya es posible ejecutar cualquier servicio disponible en el WebService o, si lo necesitás, ver qué operaciones te ofrece.

mchojrin

Por mchojrin

Ayudo a desarrolladores PHP a acceder mercados y clientes más sofisticados y exigentes

3 comentarios

  1. Hola que tal ,tengo una duda me han entragado un archivo crt, intermediate, y root. para realizar una conexion webservice, estoy usando Curl para realizar la conexion, pero no se como se deben tratar estos archivos. No encuentro la clave privada por ningun lado, ayuda por favor.

  2. Muchas gracias por compartir tus conocimientos, esta publicación me ayudo en un momento critico de mi vida.
    ¡Gracias!

¿Te quedó alguna duda? Publica aca tu pregunta

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.