'Axios/HTTPS certificate expired error only on Windows
I am currently working on a VSCode extension that internally makes some API calls to a HTTPS protected endpoint. On Linux and Mac OS the extension works as expected but on Windows machines axios, the internal HTTP client used to make the API calls, is rejecting those requests due to the certificates being expired. When I access the API endpoint though via Firefox, Chrome and even Edge the certificates seems find.
I have upgraded Node to 16.14.0 and also to 17.6.0 but the problem still remains. As the API is only accessible through our VPN, with my VPN activated of course, I used testssl.sh to verify that the whole trust-chain is still valid:
Testing protocols via sockets except NPN+ALPN
SSLv2 not offered (OK)
SSLv3 not offered (OK)
TLS 1 not offered
TLS 1.1 not offered
TLS 1.2 offered (OK)
TLS 1.3 offered (OK): final
NPN/SPDY not offered
ALPN/HTTP2 h2, http/1.1 (offered)
Testing cipher categories
NULL ciphers (no encryption) not offered (OK)
Anonymous NULL Ciphers (no authentication) not offered (OK)
Export ciphers (w/o ADH+NULL) not offered (OK)
LOW: 64 Bit + DES, RC[2,4] (w/o export) not offered (OK)
Triple DES Ciphers / IDEA not offered
Obsolete CBC ciphers (AES, ARIA etc.) offered
Strong encryption (AEAD ciphers) offered (OK)
Testing robust (perfect) forward secrecy, (P)FS -- omitting Null Authentication/Encryption, 3DES, RC4
PFS is offered (OK) TLS_AES_256_GCM_SHA384 TLS_CHACHA20_POLY1305_SHA256 ECDHE-RSA-AES256-GCM-SHA384 ECDHE-RSA-AES256-SHA384 ECDHE-RSA-AES256-SHA
DHE-RSA-AES256-GCM-SHA384 DHE-RSA-AES256-SHA256 DHE-RSA-AES256-SHA TLS_AES_128_GCM_SHA256 ECDHE-RSA-AES128-GCM-SHA256 ECDHE-RSA-AES128-SHA256
ECDHE-RSA-AES128-SHA DHE-RSA-AES128-GCM-SHA256 DHE-RSA-AES128-SHA256 DHE-RSA-AES128-SHA
Elliptic curves offered: prime256v1 secp384r1 secp521r1 X25519 X448
DH group offered: HAProxy (2048 bits)
Testing server preferences
Has server cipher order? yes (OK) -- TLS 1.3 and below
Negotiated protocol TLSv1.3
Negotiated cipher TLS_AES_256_GCM_SHA384, 253 bit ECDH (X25519)
Cipher order
TLSv1.2: ECDHE-RSA-AES128-GCM-SHA256 ECDHE-RSA-AES256-GCM-SHA384 DHE-RSA-AES128-GCM-SHA256 DHE-RSA-AES256-GCM-SHA384 ECDHE-RSA-AES128-SHA256 ECDHE-RSA-AES128-SHA
ECDHE-RSA-AES256-SHA384 ECDHE-RSA-AES256-SHA DHE-RSA-AES128-SHA256 DHE-RSA-AES128-SHA DHE-RSA-AES256-SHA256 DHE-RSA-AES256-SHA
TLSv1.3: TLS_AES_256_GCM_SHA384 TLS_CHACHA20_POLY1305_SHA256 TLS_AES_128_GCM_SHA256
...
Certificate Validity (UTC) 65 >= 30 days (2022-02-01 08:18 --> 2022-05-02 08:18)
...
The other certificates in the trustchain are also valid and not expired at all.
Axios is used within an own HttpClient class that looks like this:
export class HttpClient {
private readonly instance: AxiosInstance;
constructor() {
this.instance = axios.create();
}
async asyncGet<T>(url: string): Promise<Response<T>> {
return this.instance.get<T>(url)
.then(axiosResponse => {
...
})
.catch(error => {
console.error(error);
throw error;
});
}
}
I also tried the https
const options: https:RequestOptions = {
hostname: '...',
port: 443,
path: '/...',
method: 'GET'
};
const request = https.request(url, options, (res: IncomingMessage) => {
console.log(`statusCode: ${res.statusCode ? res.statusCode : 'undefined'}`);
res.on('data', d => {
process.stdout.write(d as string);
}
});
request.on('error', error => {
console.error(error);
};
request.end();
analog to the samples given in the NodeJS documentation and tls
const socket = tls.connect(443, hostname, undefined, () => {
console.log('Connected to ' + hostname);
console.log('TLS.connect', socket.authorized ? 'authorized' : 'unauthorized');
console.log('Cipher: ' + JSON.stringify(socket.getCipher()));
console.log('Protocol: ' + JSON.stringify(socket.getProtocol()));
const peerCert: tls.DetailedPeerCertificate = socket.getPeerCertificate(true);
console.log(`Peer-Cert ${JSON.stringify(peerCert.subject)} - valid from: ${peerCert ? peerCert.valid_from : 'invalid'} valid till: ${peerCert ? peerCert.valid_to : 'invalid'}`);
const issuerCert = peerCert.issuerCertificate;
console.log(`issuer-Cert ${JSON.stringify(issuerCert.subject)} - valid from: ${issuerCert ? issuerCert.valid_from : 'invalid'} valid till: ${issuerCert ? issuerCert.valid_to : 'invalid'}`);
const rootCert = issuerCert.issuerCertificate;
console.log(`root-Cert ${JSON.stringify(rootCert.subject)} - valid from: ${rootCert ? rootCert.valid_from : 'invalid'} valid till: ${rootCert ? rootCert.valid_to : 'invalid'}`);
});
as proposed in this answer.
Both axios and https return an error like this:
{
"message": "certificate has expired",
"name": "Error",
"stack": "Error: certificate has expired\n\tat TLSSocket.onConnectSecure (_tls_wrap.js:1497:34)\n\tat TLSSocket.emit (events.js:315:20)\n\tat TLSSocket._finishInit (_tls_wrap.js:932:8)\n\tat TLSWrap.onhandshakedone (_tls_wrap.js:706:12)\n\tat TLSWrap.callbackTrampoline (internal/async_hooks.js:131:14)",
"config": {
"transitional": {
"silentJSONParsing": true,
"forcedJSONParsing": true,
"clarifyTimeoutError": false
},
"transformRequest": [
"null"
],
"transformResponse": [
"null"
],
"timeout": 0,
"xsrfCookieName": "XSRF-TOKEN",
"xsrfHeaderName": "X-XSRF-TOKEN",
"maxContentLength": -1,
"maxBodyLength": -1,
"headers": {
"Accept": "application/json, text/plain, */*",
"user-Agent": "axios/0.26.0"
},
"method": "get",
"url": "https://..."
},
"code": "CERT_HAS_EXPIRED",
"status": null
}
with a more human-readable stacktrace:
Error: certificate has expired
at TLSSocket.onConnectSecure (_tls_wrap.js:1497:34)
at TLSSocket.emit (events.js:315:20)
at TLSSocket._finishInit (_tls_wrap.js:932:8)
at TLSWrap.onhandshakedone (_tls_wrap.js:706:12)
at TLSWrap.callbackTrampoline (internal/async_hooks.js:131:14)
while for tls I get the following output:
Connected to ...
TLS.connect authorized
Cipher: {"name":"TLS_CHACHA20_POLY1305_SHA256","standardName":"TLS_CHACHA20_POLY1305_SHA256","version":"TLSv1/SSLv3"}
Protocol: "TLSv1.3"
Peer-Cert {"CN": "*...."} - valid from: Feb 1 08:18:23 2022 GMT valid till: May 2 08:18:22 2022 GMT
issuer-Cert {"C":"US","O":"Let's Encrypt","CN":"R3"} - valid from Sep 4 00:00:00 2020 GMT valid till: Sep 15 16:00:00 2025 GMT
root-Cert {"C":"US","O":"Internet Security Research Group","CN":"ISRG Root X1"} - valid from: Jan 20 19:14:03 2021 GMT valid till: Sep 30 18:14:03 2024 GMT
So tls seems to be able to connect to the API server and perform the SSL/TLS handshake, but https and axios somehow fail.
I also stumbled upon this question here, which seems to be related, but as I am already on the latest NodeJS release (as well as any dependency used in the extension is on the most recent version) and this error only occurs on Windows (mostly 10, unsure if and how many users actually use Windows 11) machines I think the question deserves its own spot here on SO.
In order to rule out a lack of common supported ciphers between the Windows and Node.js based tls implementation and the nginx managed server side, I also checked the available ciphers in Node via node -p crypto.constants.defaultCoreCipherList which returns a list like this:
TLS_AES_256_GCM_SHA384
TLS_CHACHA20_POLY1305_SHA256
TLS_AES_128_GCM_SHA256
ECDHE-RSA-AES128-GCM-SHA256
ECDHE-ECDSA-AES128-GCM-SHA256
ECDHE-RSA-AES256-GCM-SHA384
ECDHE-ECDSA-AES256-GCM-SHA384
DHE-RSA-AES128-GCM-SHA256
ECDHE-RSA-AES128-SHA256
DHE-RSA-AES128-SHA256
ECDHE-RSA-AES256-SHA384
DHE-RSA-AES256-SHA384
ECDHE-RSA-AES256-SHA256
DHE-RSA-AES256-SHA256
HIGH
!aNULL
!eNULL
!EXPORT
!DES
!RC4
!MD5
!PSK
!SRP
!CAMELLIA
which indicates that enough ciphers would overlap between client and server.
Why do I still get a certificate expired error on Windows machines with axios/https
when Linux and MacOS work just fine with these settings and tls is able to connect to the remote API sucessfully even on Windows machines?
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|
