`req.socket.authorized` 的结果使用 nodejs https 和表达 [相互认证] 不正确

Result of `req.socket.authorized` is not correct using nodejs https and express [mutual authentication]

我正在尝试设置具有相互身份验证的 https 服务器。
我为服务器创建了密钥和证书(自动签名)。

现在我使用firefox连接服务器没有提供任何客户端证书
这应该导致 req.socket.authorized 变为 false(如 here 所述),但由于某些原因在刷新后(并且没有更改任何内容)消息从右侧更改
Unauthorized: Client certificate required (UNABLE_TO_GET_ISSUER_CERT)

Client certificate was authenticated but certificate information could not be retrieved.

对我来说这是出乎意料的,因为这意味着 req.socket.authorized == true 即使没有客户端证书。谁能解释一下为什么会这样?


这是我的代码:

const express = require('express')
const app = express()
const fs = require('fs')
const https = require('https')

// ...

const opts = { key: fs.readFileSync('./cryptoMaterial/private_key.pem'),
               cert: fs.readFileSync('./cryptoMaterial/certificate.pem'),
               requestCert: true,
               rejectUnauthorized: false,
               ca: [ fs.readFileSync('./cryptoMaterial/certificate.pem') ]
             }

const clientAuthMiddleware = () => (req, res, next) => {
    if (!req.secure && req.header('x-forwarded-proto') != 'https') {
        return res.redirect('https://' + req.header('host') + req.url);
    }

    // Ensure that the certificate was validated at the protocol level
    if (!req.socket.authorized) { // <-- THIS SHOULD BE ALWAYS FALSE
        res.status(401).send(
            'Unauthorized: Client certificate required ' + 
                '(' + req.socket.authorizationError + ')'
        );
        return
    }

    // Obtain certificate details
    var cert = req.socket.getPeerCertificate();
    if (!cert || !Object.keys(cert).length) {
        // Handle the bizarre and probably not-real case that a certificate was
        // validated but we can't actually inspect it
        res.status(500).send(
            'Client certificate was authenticated but certificate ' +
                'information could not be retrieved.'
        );
        return
    }
    return next();
};
app.use(clientAuthMiddleware());

// ...

https.createServer(opts, app).listen(PORT)

我刚才遇到了同样的问题,并在 github 上创建了一个问题。这似乎是故意的行为。参见 https://github.com/nodejs/node/issues/35317

引用 bnoordhuis 的回答“我猜这可能是由于某些 TLS 连接重用逻辑。” 在问题中:

Close, it's not the connection but the TLS session that's reused. :-)

Reuse cuts the handshake short (and cuts out the client certificate exchange) because it reuses the previously established session parameters. That's per spec and normally what you want. Chromium probably creates a new session when you reload.

[...]

socket.authorized is false when a verification error happened during the handshake (e.g. invalid or untrusted certificate) but true otherwise.

A new connection started from a resumed session doesn't do that verification and hence assumes socket.authorized = true. The nature of TLS sessions is such that I'm not sure this can be fixed even if we wanted to.


作为解决方法,您应该禁用 TLS 重新协商并为每个连接强制一个新的 TLS 会话,据我所知,这只能在 TLSv1.2 上完成。

这是我如何使用 Typescript 实现它的示例:

import fs from 'fs';
import path from 'path';
import https from 'https';
import tls from 'tls';
import express from 'express';

if (tls.DEFAULT_MAX_VERSION !== "TLSv1.2") {
    throw Error('Specify --tls-max-v1.2 as a node option (see https://github.com/nodejs/node/issues/35317)');
}
    
const httpsOptions = {
    key: fs.readFileSync(path.join('certs', 'key')),
    cert: fs.readFileSync(path.join('certs', 'cert')),
    ca: fs.readFileSync(path.join('certs', 'ca')),
    // crl: fs.readFileSync(path.join('certs', 'crl')), /* Enable this if you have a CRL */
    requestCert: true,
    rejectUnauthorized: false
};

expressServer = https.createServer(httpsOptions, expressApp);

/* Authentication middleware */
expressApp.use((req,res,next) => {
    let tlsSocket = (req.socket as tls.TLSSocket);
    if (tlsSocket.isSessionReused()) {
        /* Force renegotiation (see https://github.com/nodejs/node/issues/35317) */
        tlsSocket.renegotiate({rejectUnauthorized: false, requestCert: true}, (err) => {
            if (!(tlsSocket as tls.TLSSocket).authorized) {
                console.log('Unauthorized');
                return res.status(401).send('Unauthorized');
            }
        }
    }
    else {
        if (!(tlsSocket as tls.TLSSocket).authorized) {
            console.log('Unauthorized');
            return res.status(401).send('Unauthorized');
        }
    }
    next();
});