| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent ca88587 commit 15da4a1
2 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1320,6 +1320,13 @@ function onServerSocketSecure() { | |||
| 1320 | 1320 | if (verifyError) { | |
| 1321 | 1321 | this.authorizationError = verifyError.code; | |
| 1322 | 1322 | ||
| 1323 | + if (this._rejectUnauthorized) | ||
| 1324 | + this.destroy(); | ||
| 1325 | + } else if (!this._handle.getPeerX509Certificate()) { | ||
| 1326 | + // Ncrypto reports X509_V_OK for TLS 1.3 resumption without a peer | ||
| 1327 | + // certificate, as it uses PSKs. Require one to authorize the socket. | ||
| 1328 | + this.authorizationError = 'UNABLE_TO_GET_ISSUER_CERT'; | ||
| 1329 | + | ||
| 1323 | 1330 | if (this._rejectUnauthorized) | |
| 1324 | 1331 | this.destroy(); | |
| 1325 | 1332 | } else { | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,199 @@ | |||
| 1 | + 'use strict'; | ||
| 2 | + const common = require('../common'); | ||
| 3 | + if (!common.hasCrypto) | ||
| 4 | + common.skip('missing crypto'); | ||
| 5 | + | ||
| 6 | + // Server-side client-certificate authorization must survive TLS session | ||
| 7 | + // resumption. On a resumed handshake the client does not re-send its | ||
| 8 | + // certificate, so the server has to report the same authorization state it | ||
| 9 | + // derived from the original full handshake: | ||
| 10 | + // | ||
| 11 | + // - a trusted certificate stays authorized, | ||
| 12 | + // - an untrusted certificate stays unauthorized with its verification error, | ||
| 13 | + // - a missing certificate stays unauthorized (UNABLE_TO_GET_ISSUER_CERT). | ||
| 14 | + // | ||
| 15 | + // The missing-certificate case is special on TLS 1.3: ncrypto reports X509_V_OK | ||
| 16 | + // for the resumed PSK handshake even though no certificate was presented, so | ||
| 17 | + // the absence has to be detected explicitly (see onServerSocketSecure() in | ||
| 18 | + // lib/internal/tls/wrap.js). The final case checks that such a certificate-less | ||
| 19 | + // resumed session is rejected outright when rejectUnauthorized is set. | ||
| 20 | + | ||
| 21 | + const assert = require('assert'); | ||
| 22 | + const crypto = require('crypto'); | ||
| 23 | + const tls = require('tls'); | ||
| 24 | + const fixtures = require('../common/fixtures'); | ||
| 25 | + const { once } = require('events'); | ||
| 26 | + | ||
| 27 | + const ca = fixtures.readKey('ca1-cert.pem'); | ||
| 28 | + const serverCert = { | ||
| 29 | + key: fixtures.readKey('agent2-key.pem'), | ||
| 30 | + cert: fixtures.readKey('agent2-cert.pem'), | ||
| 31 | + }; | ||
| 32 | + | ||
| 33 | + // Client certificate variants, keyed by the peer state they produce. | ||
| 34 | + const CLIENTS = { | ||
| 35 | + trusted: { // Signed by ca1 | ||
| 36 | + creds: { | ||
| 37 | + key: fixtures.readKey('agent1-key.pem'), | ||
| 38 | + cert: fixtures.readKey('agent1-cert.pem'), | ||
| 39 | + }, | ||
| 40 | + authorized: true, | ||
| 41 | + authorizationError: null, | ||
| 42 | + peerCN: 'agent1', | ||
| 43 | + }, | ||
| 44 | + untrusted: { // Signed by ca2, not trusted | ||
| 45 | + creds: { | ||
| 46 | + key: fixtures.readKey('agent3-key.pem'), | ||
| 47 | + cert: fixtures.readKey('agent3-cert.pem'), | ||
| 48 | + }, | ||
| 49 | + authorized: false, | ||
| 50 | + authorizationError: 'UNABLE_TO_VERIFY_LEAF_SIGNATURE', | ||
| 51 | + peerCN: 'agent3', | ||
| 52 | + }, | ||
| 53 | + missing: { // No client certificate | ||
| 54 | + creds: {}, | ||
| 55 | + authorized: false, | ||
| 56 | + authorizationError: 'UNABLE_TO_GET_ISSUER_CERT', | ||
| 57 | + peerCN: undefined, | ||
| 58 | + }, | ||
| 59 | + }; | ||
| 60 | + | ||
| 61 | + async function handshake(options, captureSession) { | ||
| 62 | + const socket = tls.connect(options); | ||
| 63 | + const sessionPromise = captureSession ? | ||
| 64 | + once(socket, 'session').then(([session]) => session) : null; | ||
| 65 | + | ||
| 66 | + socket.resume(); | ||
| 67 | + await once(socket, 'secureConnect'); | ||
| 68 | + | ||
| 69 | + const closePromise = once(socket, 'close'); | ||
| 70 | + const session = sessionPromise ? await sessionPromise : undefined; | ||
| 71 | + socket.end(); | ||
| 72 | + await closePromise; | ||
| 73 | + return session; | ||
| 74 | + } | ||
| 75 | + | ||
| 76 | + // Test a single resumption configuration and expected result: | ||
| 77 | + async function testResumption(version, name) { | ||
| 78 | + const { creds, authorized, authorizationError, peerCN } = CLIENTS[name]; | ||
| 79 | + | ||
| 80 | + let connections = 0; | ||
| 81 | + const server = tls.createServer({ | ||
| 82 | + ...serverCert, | ||
| 83 | + ca, | ||
| 84 | + requestCert: true, | ||
| 85 | + rejectUnauthorized: false, | ||
| 86 | + minVersion: version, | ||
| 87 | + maxVersion: version, | ||
| 88 | + }, common.mustCall((socket) => { | ||
| 89 | + // 2nd conn must resume: | ||
| 90 | + const resumed = connections++ === 1; | ||
| 91 | + const where = `${version} ${name} ${resumed ? 'resumed' : 'new'}`; | ||
| 92 | + assert.strictEqual(socket.isSessionReused(), resumed, where); | ||
| 93 | + | ||
| 94 | + // Both conns must report same expected auth state: | ||
| 95 | + assert.strictEqual(socket.authorized, authorized, where); | ||
| 96 | + assert.strictEqual(socket.authorizationError, authorizationError, where); | ||
| 97 | + const peer = socket.getPeerCertificate(); | ||
| 98 | + if (peerCN === undefined) | ||
| 99 | + assert.deepStrictEqual(peer, {}, where); | ||
| 100 | + else | ||
| 101 | + assert.strictEqual(peer.subject.CN, peerCN, where); | ||
| 102 | + | ||
| 103 | + // N.b. BoringSSL only sends a ticket after a write: | ||
| 104 | + socket.end('.'); | ||
| 105 | + }, 2)); | ||
| 106 | + | ||
| 107 | + server.listen(0); | ||
| 108 | + await once(server, 'listening'); | ||
| 109 | + | ||
| 110 | + const options = { | ||
| 111 | + port: server.address().port, | ||
| 112 | + host: '127.0.0.1', | ||
| 113 | + checkServerIdentity: () => undefined, | ||
| 114 | + rejectUnauthorized: false, | ||
| 115 | + minVersion: version, | ||
| 116 | + maxVersion: version, | ||
| 117 | + ...creds, | ||
| 118 | + }; | ||
| 119 | + | ||
| 120 | + try { | ||
| 121 | + const session = await handshake(options, true); | ||
| 122 | + assert(session); | ||
| 123 | + await handshake({ ...options, session }); | ||
| 124 | + } finally { | ||
| 125 | + server.close(); | ||
| 126 | + await once(server, 'close'); | ||
| 127 | + } | ||
| 128 | + } | ||
| 129 | + | ||
| 130 | + // Test the special case of resumption from rejectUnauthorized:false to | ||
| 131 | + // rejectUnauthorized:true, which must be rejected even though the original | ||
| 132 | + // session worked initially. | ||
| 133 | + async function testRejectResumedWithoutCert() { | ||
| 134 | + const options = { | ||
| 135 | + ...serverCert, | ||
| 136 | + ca, | ||
| 137 | + requestCert: true, | ||
| 138 | + minVersion: 'TLSv1.3', | ||
| 139 | + maxVersion: 'TLSv1.3', | ||
| 140 | + ticketKeys: crypto.randomBytes(48), | ||
| 141 | + }; | ||
| 142 | + const lenient = tls.createServer({ ...options, rejectUnauthorized: false }); | ||
| 143 | + lenient.on('secureConnection', common.mustCall((socket) => { | ||
| 144 | + assert.strictEqual(socket.authorized, false); | ||
| 145 | + assert.strictEqual(socket.authorizationError, 'UNABLE_TO_GET_ISSUER_CERT'); | ||
| 146 | + socket.end('.'); | ||
| 147 | + })); | ||
| 148 | + | ||
| 149 | + const strict = tls.createServer({ ...options, rejectUnauthorized: true }); | ||
| 150 | + strict.on('secureConnection', common.mustNotCall()); | ||
| 151 | + | ||
| 152 | + const clientOptions = (port) => ({ | ||
| 153 | + port, | ||
| 154 | + host: '127.0.0.1', | ||
| 155 | + rejectUnauthorized: false, | ||
| 156 | + checkServerIdentity: () => undefined, | ||
| 157 | + minVersion: 'TLSv1.3', | ||
| 158 | + maxVersion: 'TLSv1.3', | ||
| 159 | + }); | ||
| 160 | + | ||
| 161 | + lenient.listen(0); | ||
| 162 | + await once(lenient, 'listening'); | ||
| 163 | + const session = await handshake(clientOptions(lenient.address().port), true); | ||
| 164 | + assert(session); | ||
| 165 | + lenient.close(); | ||
| 166 | + await once(lenient, 'close'); | ||
| 167 | + | ||
| 168 | + strict.listen(0); | ||
| 169 | + await once(strict, 'listening'); | ||
| 170 | + | ||
| 171 | + const resumed = tls.connect({ ...clientOptions(strict.address().port), session }); | ||
| 172 | + resumed.on('error', () => {}); // May observe the server's reset. | ||
| 173 | + resumed.resume(); | ||
| 174 | + | ||
| 175 | + // The client completes the resumed handshake (it has the server's Finished) | ||
| 176 | + // before the server's reset can arrive, so this asserts the strict server | ||
| 177 | + // actually resumed rather than falling back to a rejected full handshake. | ||
| 178 | + await once(resumed, 'secureConnect'); | ||
| 179 | + assert.strictEqual(resumed.isSessionReused(), true); | ||
| 180 | + | ||
| 181 | + // Then the socket is destroyed during 'secure', which surfaces as a reset | ||
| 182 | + // rather than a handshake failure. | ||
| 183 | + const [err] = await once(strict, 'tlsClientError'); | ||
| 184 | + assert.strictEqual(err.code, 'ECONNRESET'); | ||
| 185 | + | ||
| 186 | + resumed.destroy(); | ||
| 187 | + strict.close(); | ||
| 188 | + await once(strict, 'close'); | ||
| 189 | + } | ||
| 190 | + | ||
| 191 | + (async function() { | ||
| 192 | + // Run the full matrix of configurations: | ||
| 193 | + for (const version of ['TLSv1.2', 'TLSv1.3']) | ||
| 194 | + for (const name of Object.keys(CLIENTS)) | ||
| 195 | + await testResumption(version, name); | ||
| 196 | + | ||
| 197 | + // Validate the rejectUnauth:false->true case | ||
| 198 | + await testRejectResumedWithoutCert(); | ||
| 199 | + })().then(common.mustCall()); | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments