unable to verify the first certificate
The certificate chain is incomplete.
It means that the webserver you are connecting to is misconfigured and did not include the intermediate certificate in the certificate chain it sent to you.
Certificate chain
It most likely looks as follows:
- Server certificate - stores a certificate signed by intermediate.
- Intermediate certificate - stores a certificate signed by root.
- Root certificate - stores a self-signed certificate.
Intermediate certificate should be installed on the server, along with the server certificate.
Root certificates are embedded into the software applications, browsers and operating systems.
The application serving the certificate has to send the complete chain, this means the server certificate itself and all the intermediates. The root certificate is supposed to be known by the client.
Recreate the problem
Go to https://incomplete-chain.badssl.com using your browser.
It doesn't show any error (padlock in the address bar is green).
It's because browsers tend to complete the chain if it’s not sent from the server.
Now, connect to https://incomplete-chain.badssl.com using Node:
// index.js
const axios = require('axios');
axios.get('https://incomplete-chain.badssl.com')
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
Logs: "Error: unable to verify the first certificate".
Solution
You need to complete the certificate chain yourself.
To do that:
1: You need to get the missing intermediate certificate in .pem
format, then
2a: extend Node’s built-in certificate store using NODE_EXTRA_CA_CERTS
,
2b: or pass your own certificate bundle (intermediates and root) using ca
option.
1. How do I get intermediate certificate?
Using openssl
(comes with Git for Windows).
Save the remote server's certificate details:
openssl s_client -connect incomplete-chain.badssl.com:443 -servername incomplete-chain.badssl.com | tee logcertfile
We're looking for the issuer (the intermediate certificate is the issuer / signer of the server certificate):
openssl x509 -in logcertfile -noout -text | grep -i "issuer"
It should give you URI of the signing certificate. Download it:
curl --output intermediate.crt http://cacerts.digicert.com/DigiCertSHA2SecureServerCA.crt
Finally, convert it to .pem
:
openssl x509 -inform DER -in intermediate.crt -out intermediate.pem -text
2a. NODE_EXTRA_CA_CERTS
I'm using cross-env to set environment variables in package.json
file:
"start": "cross-env NODE_EXTRA_CA_CERTS=\"C:\\Users\\USERNAME\\Desktop\\ssl-connect\\intermediate.pem\" node index.js"
2b. ca
option
This option is going to overwrite the Node's built-in root CAs.
That's why we need to create our own root CA. Use ssl-root-cas.
Then, create a custom https
agent configured with our certificate bundle (root and intermediate). Pass this agent to axios
when making request.
// index.js
const axios = require('axios');
const path = require('path');
const https = require('https');
const rootCas = require('ssl-root-cas').create();
rootCas.addFile(path.resolve(__dirname, 'intermediate.pem'));
const httpsAgent = new https.Agent({ca: rootCas});
axios.get('https://incomplete-chain.badssl.com', { httpsAgent })
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
Instead of creating a custom https
agent and passing it to axios
, you can place the certifcates on the https
global agent:
// Applies to ALL requests (whether using https directly or the request module)
https.globalAgent.options.ca = rootCas;
Resources:
- https://levelup.gitconnected.com/how-to-resolve-certificate-errors-in-nodejs-app-involving-ssl-calls-781ce48daded
- https://www.npmjs.com/package/ssl-root-cas
- https://github.com/nodejs/node/issues/16336
- https://www.namecheap.com/support/knowledgebase/article.aspx/9605/69/how-to-check-ca-chain-installation
- https://superuser.com/questions/97201/how-to-save-a-remote-server-ssl-certificate-locally-as-a-file/
- How to convert .crt to .pem