Certificados con cadena incompleta

Recientemente, al desarrollar una llamada a un servicio REST me encontré con el error Unable to verify the first certificate, tanto en Salesforce como en una app Nodejs en Heroku:

El error en Salesforce:

System.HttpRequest retrying request in response to handshake failure: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

El error en Heroku/NodeJs:

heroku/router at=info method=GET path="/dev/UNSERVICIOCUALQUIERA" host=myhost.herokuapp.com dyno=web.1 connect=0ms service=353ms status=500 bytes=404 protocol=https
app/web.1 Error: unable to verify the first certificate
app/web.1     at TLSSocket.onConnectSecure (node:_tls_wrap:1530:34)
app/web.1     at TLSSocket.emit (node:events:526:28)
app/web.1     at TLSSocket._finishInit (node:_tls_wrap:944:8)
app/web.1     at TLSWrap.ssl.onhandshakedone (node:_tls_wrap:725:12)

Lo extraño en un principio es que esto no pasa si accedo al servidor mediante el navegador. Al hacer servidor, me aparece en el navegador como que el certificado está OK y la página es segura.

Buscando por internet, encontré que a alguien le pasa lo mismo node.js - Error: unable to verify the first certificate in nodejs - Stack Overflow, y la solución apunta a usar el paquete ssl-root-cas - npm (npmjs.com), para importar los certificados en la app nodejs. Sin embargo, esta solución no serviria para el acceso desde Salesforce. En la primera línea de la documentación del paquete se dice algo muy interesante:

IMPORTANT: Try this first 2015-Jul-13: I just discovered that the most common reason you would have the kind of problems this module solves is actually due to failing to properly bundle the Intermediate CAs with the server certificate.

Buscando en la documentación de Salesforce, encontré este artículo, que dice:

Salesforce trusts only root certificate authority (CA) certificates, with few historical exceptions. Salesforce's certificate trust policy is to require server and client certificate chains to include all intermediate certificates that exist between the server or client certificate and the chain's root certificate. Salesforce will not honor requests to add intermediate certificates to its trust list. Salesforce trusts many generally trusted root certificates, but not all.

Esto es, que para que la conexión funcione es necesario que el certificado presente toda la cadena de certificación, excepto la root CA (que es la única en la que confía Salesforce).

Para comprobar que el problema coincide con lo que estaba diciendo la documentación, usé un validador de certificados online, por ejemplo Digicert checker, y SSL Shopper SSL Checker. Usé Digicert ya que era la CA raíz del certificado que estaba validando, y el resultado fue el siguiente:

DigicertCheck.png

¡Voilà! El check de digicert indica que no se está enviando el certificado intermedio.

Resumiendo:

En este caso, había un certificado intermedio que no se había incluido en el archivo .cer que luego se cargó en el servidor.

  • Los navegadores por defecto validaban el certificado intermedio por su cuenta.
  • Heroku y Salesforce, en cambio, no validan certificados intermedios si no están incluidos, por lo que fallan.
  • En Android ocurre de forma similar.

La solución

Del lado del servidor Digicert había enviado dos certificados: un .cer con el certificado específico del site (el que se había instalado), y otro .cer con el certificado intermedio. La solución consistió en combinar los dos certificados usando un editor de texto siguiendo este ejemplo, y después importar este nuevo .cer combinado en el servidor.

Un punto importante es que los certificados se concatenen en el orden correcto, ya que en este caso el orden SÍ que importa.