'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