diff --git a/doc/api/deprecations.md b/doc/api/deprecations.md index 9217fb2616ade6..7fb04e82fee85c 100644 --- a/doc/api/deprecations.md +++ b/doc/api/deprecations.md @@ -3901,6 +3901,20 @@ of built-in modules. This was incomplete and matched the already deprecated `repl._builtinLibs` ([DEP0142][]) instead it's better to rely upon `require('node:module').builtinModules`. +### DEP0192: `require('node:_tls_common')` and `require('node:_tls_wrap')` + + + +Type: Runtime + +The `node:_tls_common` and `node:_tls_wrap` modules are deprecated as they should be considered +an internal nodejs implementation rather than a public facing API, use `node:tls` instead. + [DEP0142]: #dep0142-repl_builtinlibs [NIST SP 800-38D]: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf [RFC 6066]: https://tools.ietf.org/html/rfc6066#section-3 diff --git a/lib/_tls_common.js b/lib/_tls_common.js index 66331d2d9999e9..607875d43a11a8 100644 --- a/lib/_tls_common.js +++ b/lib/_tls_common.js @@ -1,156 +1,10 @@ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - 'use strict'; -const { - JSONParse, -} = primordials; - -const tls = require('tls'); - -const { - codes: { - ERR_TLS_INVALID_PROTOCOL_VERSION, - ERR_TLS_PROTOCOL_VERSION_CONFLICT, - }, -} = require('internal/errors'); - -const { - crypto: { - SSL_OP_CIPHER_SERVER_PREFERENCE, - TLS1_VERSION, - TLS1_1_VERSION, - TLS1_2_VERSION, - TLS1_3_VERSION, - }, -} = internalBinding('constants'); - -const { - kEmptyObject, -} = require('internal/util'); - -const { - validateInteger, -} = require('internal/validators'); - -const { - configSecureContext, -} = require('internal/tls/secure-context'); - -function toV(which, v, def) { - v ??= def; - if (v === 'TLSv1') return TLS1_VERSION; - if (v === 'TLSv1.1') return TLS1_1_VERSION; - if (v === 'TLSv1.2') return TLS1_2_VERSION; - if (v === 'TLSv1.3') return TLS1_3_VERSION; - throw new ERR_TLS_INVALID_PROTOCOL_VERSION(v, which); -} - -const { - SecureContext: NativeSecureContext, -} = internalBinding('crypto'); - -function SecureContext(secureProtocol, secureOptions, minVersion, maxVersion) { - if (!(this instanceof SecureContext)) { - return new SecureContext(secureProtocol, secureOptions, minVersion, - maxVersion); - } - - if (secureProtocol) { - if (minVersion != null) - throw new ERR_TLS_PROTOCOL_VERSION_CONFLICT(minVersion, secureProtocol); - if (maxVersion != null) - throw new ERR_TLS_PROTOCOL_VERSION_CONFLICT(maxVersion, secureProtocol); - } - - this.context = new NativeSecureContext(); - this.context.init(secureProtocol, - toV('minimum', minVersion, tls.DEFAULT_MIN_VERSION), - toV('maximum', maxVersion, tls.DEFAULT_MAX_VERSION)); - - if (secureOptions) { - validateInteger(secureOptions, 'secureOptions'); - this.context.setOptions(secureOptions); - } -} - -function createSecureContext(options) { - options ||= kEmptyObject; - const { - honorCipherOrder, - minVersion, - maxVersion, - secureProtocol, - } = options; - - let { secureOptions } = options; - - if (honorCipherOrder) - secureOptions |= SSL_OP_CIPHER_SERVER_PREFERENCE; - - const c = new SecureContext(secureProtocol, secureOptions, - minVersion, maxVersion); - - configSecureContext(c.context, options); - - return c; -} - -// Translate some fields from the handle's C-friendly format into more idiomatic -// javascript object representations before passing them back to the user. Can -// be used on any cert object, but changing the name would be semver-major. -function translatePeerCertificate(c) { - if (!c) - return null; - - if (c.issuerCertificate != null && c.issuerCertificate !== c) { - c.issuerCertificate = translatePeerCertificate(c.issuerCertificate); - } - if (c.infoAccess != null) { - const info = c.infoAccess; - c.infoAccess = { __proto__: null }; - - // XXX: More key validation? - info.replace(/([^\n:]*):([^\n]*)(?:\n|$)/g, - (all, key, val) => { - if (val.charCodeAt(0) === 0x22) { - // The translatePeerCertificate function is only - // used on internally created legacy certificate - // objects, and any value that contains a quote - // will always be a valid JSON string literal, - // so this should never throw. - val = JSONParse(val); - } - if (key in c.infoAccess) - c.infoAccess[key].push(val); - else - c.infoAccess[key] = [val]; - }); - } - return c; -} - +const { SecureContext, createSecureContext, translatePeerCertificate } = require('internal/tls/common'); module.exports = { SecureContext, createSecureContext, translatePeerCertificate, }; +process.emitWarning('The _tls_common module is deprecated.', + 'DeprecationWarning', 'DEP0192'); diff --git a/lib/_tls_wrap.js b/lib/_tls_wrap.js index 133544417b92f8..24bd9717281698 100644 --- a/lib/_tls_wrap.js +++ b/lib/_tls_wrap.js @@ -1,1782 +1,11 @@ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - 'use strict'; -const { - ObjectAssign, - ObjectDefineProperty, - ObjectSetPrototypeOf, - ReflectApply, - RegExp, - Symbol, - SymbolFor, -} = primordials; - -const { - assertCrypto, - kEmptyObject, -} = require('internal/util'); - -assertCrypto(); - -const { setImmediate } = require('timers'); -const assert = require('internal/assert'); -const crypto = require('crypto'); -const EE = require('events'); -const net = require('net'); -const tls = require('tls'); -const common = require('_tls_common'); -const { kReinitializeHandle } = require('internal/net'); -const JSStreamSocket = require('internal/js_stream_socket'); -const { Buffer } = require('buffer'); -let debug = require('internal/util/debuglog').debuglog('tls', (fn) => { - debug = fn; -}); -const { TCP, constants: TCPConstants } = internalBinding('tcp_wrap'); -const tls_wrap = internalBinding('tls_wrap'); -const { Pipe, constants: PipeConstants } = internalBinding('pipe_wrap'); -const { owner_symbol } = require('internal/async_hooks').symbols; -const { isArrayBufferView } = require('internal/util/types'); -const { SecureContext: NativeSecureContext } = internalBinding('crypto'); -const { - ConnResetException, - codes: { - ERR_INVALID_ARG_TYPE, - ERR_INVALID_ARG_VALUE, - ERR_MULTIPLE_CALLBACK, - ERR_SOCKET_CLOSED, - ERR_TLS_ALPN_CALLBACK_INVALID_RESULT, - ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS, - ERR_TLS_DH_PARAM_SIZE, - ERR_TLS_HANDSHAKE_TIMEOUT, - ERR_TLS_INVALID_CONTEXT, - ERR_TLS_INVALID_STATE, - ERR_TLS_RENEGOTIATION_DISABLED, - ERR_TLS_REQUIRED_SERVER_NAME, - ERR_TLS_SESSION_ATTACK, - ERR_TLS_SNI_FROM_SERVER, - }, -} = require('internal/errors'); -const { onpskexchange: kOnPskExchange } = internalBinding('symbols'); -const { - getOptionValue, - getAllowUnauthorized, -} = require('internal/options'); -const { - validateBoolean, - validateBuffer, - validateFunction, - validateInt32, - validateNumber, - validateObject, - validateString, - validateUint32, -} = require('internal/validators'); -const { - InternalX509Certificate, -} = require('internal/crypto/x509'); -const traceTls = getOptionValue('--trace-tls'); -const tlsKeylog = getOptionValue('--tls-keylog'); -const { appendFile } = require('fs'); -const kConnectOptions = Symbol('connect-options'); -const kDisableRenegotiation = Symbol('disable-renegotiation'); -const kErrorEmitted = Symbol('error-emitted'); -const kHandshakeTimeout = Symbol('handshake-timeout'); -const kRes = Symbol('res'); -const kSNICallback = Symbol('snicallback'); -const kALPNCallback = Symbol('alpncallback'); -const kEnableTrace = Symbol('enableTrace'); -const kPskCallback = Symbol('pskcallback'); -const kPskIdentityHint = Symbol('pskidentityhint'); -const kPendingSession = Symbol('pendingSession'); -const kIsVerified = Symbol('verified'); - -const noop = () => {}; - -let ipServernameWarned = false; -let tlsTracingWarned = false; - -// Server side times how long a handshake is taking to protect against slow -// handshakes being used for DoS. -function onhandshakestart(now) { - debug('server onhandshakestart'); - - const { lastHandshakeTime } = this; - assert(now >= lastHandshakeTime, - `now (${now}) < lastHandshakeTime (${lastHandshakeTime})`); - - this.lastHandshakeTime = now; - - // If this is the first handshake we can skip the rest of the checks. - if (lastHandshakeTime === 0) - return; - - if ((now - lastHandshakeTime) >= tls.CLIENT_RENEG_WINDOW * 1000) - this.handshakes = 1; - else - this.handshakes++; - - const owner = this[owner_symbol]; - - assert(owner._tlsOptions.isServer); - - if (this.handshakes > tls.CLIENT_RENEG_LIMIT) { - owner._emitTLSError(new ERR_TLS_SESSION_ATTACK()); - return; - } - - if (owner[kDisableRenegotiation]) - owner._emitTLSError(new ERR_TLS_RENEGOTIATION_DISABLED()); -} - -function onhandshakedone() { - debug('server onhandshakedone'); - - const owner = this[owner_symbol]; - assert(owner._tlsOptions.isServer); - - // `newSession` callback wasn't called yet - if (owner._newSessionPending) { - owner._securePending = true; - return; - } - - owner._finishInit(); -} - - -function loadSession(hello) { - debug('server onclienthello', - 'sessionid.len', hello.sessionId.length, - 'ticket?', hello.tlsTicket, - ); - const owner = this[owner_symbol]; - - let once = false; - function onSession(err, session) { - debug('server resumeSession callback(err %j, sess? %s)', err, !!session); - if (once) - return owner.destroy(new ERR_MULTIPLE_CALLBACK()); - once = true; - - if (err) - return owner.destroy(err); - - if (owner._handle === null) - return owner.destroy(new ERR_SOCKET_CLOSED()); - - owner._handle.loadSession(session); - // Session is loaded. End the parser to allow handshaking to continue. - owner._handle.endParser(); - } - - if (hello.sessionId.length <= 0 || - hello.tlsTicket || - (owner.server && - !owner.server.emit('resumeSession', hello.sessionId, onSession))) { - // Sessions without identifiers can't be resumed. - // Sessions with tickets can be resumed directly from the ticket, no server - // session storage is necessary. - // Without a call to a resumeSession listener, a session will never be - // loaded, so end the parser to allow handshaking to continue. - owner._handle.endParser(); - } -} - - -function loadSNI(info) { - const owner = this[owner_symbol]; - const servername = info.servername; - if (!servername || !owner._SNICallback) - return requestOCSP(owner, info); - - let once = false; - owner._SNICallback(servername, (err, context) => { - if (once) - return owner.destroy(new ERR_MULTIPLE_CALLBACK()); - once = true; - - if (err) - return owner.destroy(err); - - if (owner._handle === null) - return owner.destroy(new ERR_SOCKET_CLOSED()); - - // TODO(indutny): eventually disallow raw `SecureContext` - if (context) - owner._handle.sni_context = context.context || context; - - requestOCSP(owner, info); - }); -} - - -function callALPNCallback(protocolsBuffer) { - const handle = this; - const socket = handle[owner_symbol]; - - const servername = handle.getServername(); - - // Collect all the protocols from the given buffer: - const protocols = []; - let offset = 0; - while (offset < protocolsBuffer.length) { - const protocolLen = protocolsBuffer[offset]; - offset += 1; - - const protocol = protocolsBuffer.slice(offset, offset + protocolLen); - offset += protocolLen; - - protocols.push(protocol.toString('ascii')); - } - - const selectedProtocol = socket[kALPNCallback]({ - servername, - protocols, - }); - - // Undefined -> all proposed protocols rejected - if (selectedProtocol === undefined) return undefined; - - const protocolIndex = protocols.indexOf(selectedProtocol); - if (protocolIndex === -1) { - throw new ERR_TLS_ALPN_CALLBACK_INVALID_RESULT(selectedProtocol, protocols); - } - let protocolOffset = 0; - for (let i = 0; i < protocolIndex; i++) { - protocolOffset += 1 + protocols[i].length; - } - - return protocolOffset; -} - -function requestOCSP(socket, info) { - if (!info.OCSPRequest || !socket.server) - return requestOCSPDone(socket); - - let ctx = socket._handle.sni_context; - - if (!ctx) { - ctx = socket.server._sharedCreds; - - // TLS socket is using a `net.Server` instead of a tls.TLSServer. - // Some TLS properties like `server._sharedCreds` will not be present - if (!ctx) - return requestOCSPDone(socket); - } - - // TODO(indutny): eventually disallow raw `SecureContext` - if (ctx.context) - ctx = ctx.context; - - if (socket.server.listenerCount('OCSPRequest') === 0) { - return requestOCSPDone(socket); - } - - let once = false; - const onOCSP = (err, response) => { - debug('server OCSPRequest done', 'handle?', !!socket._handle, 'once?', once, - 'response?', !!response, 'err?', err); - if (once) - return socket.destroy(new ERR_MULTIPLE_CALLBACK()); - once = true; - - if (err) - return socket.destroy(err); - - if (socket._handle === null) - return socket.destroy(new ERR_SOCKET_CLOSED()); - - if (response) - socket._handle.setOCSPResponse(response); - requestOCSPDone(socket); - }; - - debug('server oncertcb emit OCSPRequest'); - socket.server.emit('OCSPRequest', - ctx.getCertificate(), - ctx.getIssuer(), - onOCSP); -} - -function requestOCSPDone(socket) { - debug('server certcb done'); - try { - socket._handle.certCbDone(); - } catch (e) { - debug('server certcb done errored', e); - socket.destroy(e); - } -} - -function onnewsessionclient(sessionId, session) { - debug('client emit session'); - const owner = this[owner_symbol]; - if (owner[kIsVerified]) { - owner.emit('session', session); - } else { - owner[kPendingSession] = session; - } -} - -function onnewsession(sessionId, session) { - debug('onnewsession'); - const owner = this[owner_symbol]; - - // TODO(@sam-github) no server to emit the event on, but handshake won't - // continue unless newSessionDone() is called, should it be, or is that - // situation unreachable, or only occurring during shutdown? - if (!owner.server) - return; - - let once = false; - const done = () => { - debug('onnewsession done'); - if (once) - return; - once = true; - - if (owner._handle === null) - return owner.destroy(new ERR_SOCKET_CLOSED()); - - this.newSessionDone(); - - owner._newSessionPending = false; - if (owner._securePending) - owner._finishInit(); - owner._securePending = false; - }; - - owner._newSessionPending = true; - if (!owner.server.emit('newSession', sessionId, session, done)) - done(); -} - -function onPskServerCallback(identity, maxPskLen) { - const owner = this[owner_symbol]; - const ret = owner[kPskCallback](owner, identity); - if (ret == null) - return undefined; - - let psk; - if (isArrayBufferView(ret)) { - psk = ret; - } else { - if (typeof ret !== 'object') { - throw new ERR_INVALID_ARG_TYPE( - 'ret', - ['Object', 'Buffer', 'TypedArray', 'DataView'], - ret, - ); - } - psk = ret.psk; - validateBuffer(psk, 'psk'); - } - - if (psk.length > maxPskLen) { - throw new ERR_INVALID_ARG_VALUE( - 'psk', - psk, - `Pre-shared key exceeds ${maxPskLen} bytes`, - ); - } - - return psk; -} - -function onPskClientCallback(hint, maxPskLen, maxIdentityLen) { - const owner = this[owner_symbol]; - const ret = owner[kPskCallback](hint); - if (ret == null) - return undefined; - - validateObject(ret, 'ret'); - - validateBuffer(ret.psk, 'psk'); - if (ret.psk.length > maxPskLen) { - throw new ERR_INVALID_ARG_VALUE( - 'psk', - ret.psk, - `Pre-shared key exceeds ${maxPskLen} bytes`, - ); - } - - validateString(ret.identity, 'identity'); - if (Buffer.byteLength(ret.identity) > maxIdentityLen) { - throw new ERR_INVALID_ARG_VALUE( - 'identity', - ret.identity, - `PSK identity exceeds ${maxIdentityLen} bytes`, - ); - } - - return { psk: ret.psk, identity: ret.identity }; -} - -function onkeylog(line) { - debug('onkeylog'); - this[owner_symbol].emit('keylog', line); -} - -function onocspresponse(resp) { - debug('client onocspresponse'); - this[owner_symbol].emit('OCSPResponse', resp); -} - -function onerror(err) { - const owner = this[owner_symbol]; - debug('%s onerror %s had? %j', - (typeof owner._tlsOptions === 'object' && owner._tlsOptions !== null) ? - owner._tlsOptions.isServer ? 'server' : 'client' : - 'unknown', - err, owner._hadError); - - if (owner._hadError) - return; - - owner._hadError = true; - - // Destroy socket if error happened before handshake's finish - if (!owner._secureEstablished) { - // When handshake fails control is not yet released, - // so self._tlsError will return null instead of actual error - - // Set closing the socket after emitting an event since the socket needs to - // be accessible when the `tlsClientError` event is emitted. - owner._closeAfterHandlingError = true; - owner.destroy(err); - } else if (owner._tlsOptions?.isServer && - owner._rejectUnauthorized && - /peer did not return a certificate/.test(err.message)) { - // Ignore server's authorization errors - owner.destroy(); - } else { - // Emit error - owner._emitTLSError(err); - } -} - -// Used by both client and server TLSSockets to start data flowing from _handle, -// read(0) causes a StreamBase::ReadStart, via Socket._read. -function initRead(tlsSocket, socket) { - debug('%s initRead', - tlsSocket._tlsOptions.isServer ? 'server' : 'client', - 'handle?', !!tlsSocket._handle, - 'buffered?', !!socket && socket.readableLength, - ); - // If we were destroyed already don't bother reading - if (!tlsSocket._handle) - return; - - // Socket already has some buffered data - emulate receiving it - if (socket?.readableLength) { - let buf; - while ((buf = socket.read()) !== null) - tlsSocket._handle.receive(buf); - } - - tlsSocket.read(0); -} - -/** - * Provides a wrap of socket stream to do encrypted communication. - */ - -function TLSSocket(socket, opts) { - const tlsOptions = { ...opts }; - let enableTrace = tlsOptions.enableTrace; - - if (enableTrace == null) { - enableTrace = traceTls; - - if (enableTrace && !tlsTracingWarned) { - tlsTracingWarned = true; - process.emitWarning('Enabling --trace-tls can expose sensitive data in ' + - 'the resulting log.'); - } - } else { - validateBoolean(enableTrace, 'options.enableTrace'); - } - - if (tlsOptions.ALPNProtocols) - tls.convertALPNProtocols(tlsOptions.ALPNProtocols, tlsOptions); - - this._tlsOptions = tlsOptions; - this._secureEstablished = false; - this._securePending = false; - this._newSessionPending = false; - this._controlReleased = false; - this.secureConnecting = true; - this._SNICallback = null; - this[kALPNCallback] = null; - this.servername = null; - this.alpnProtocol = null; - this.authorized = false; - this.authorizationError = null; - this[kRes] = null; - this[kIsVerified] = false; - this[kPendingSession] = null; - - let wrap; - let handle; - let wrapHasActiveWriteFromPrevOwner; - - if (socket) { - if (socket instanceof net.Socket && socket._handle) { - // 1. connected socket - wrap = socket; - } else { - // 2. socket has no handle so it is js not c++ - // 3. unconnected sockets are wrapped - // TLS expects to interact from C++ with a net.Socket that has a C++ stream - // handle, but a JS stream doesn't have one. Wrap it up to make it look like - // a socket. - wrap = new JSStreamSocket(socket); - } - - handle = wrap._handle; - wrapHasActiveWriteFromPrevOwner = wrap.writableLength > 0; - } else { - // 4. no socket, one will be created with net.Socket().connect - wrap = null; - wrapHasActiveWriteFromPrevOwner = false; - } - - // Just a documented property to make secure sockets - // distinguishable from regular ones. - this.encrypted = true; - - ReflectApply(net.Socket, this, [{ - handle: this._wrapHandle(wrap, handle, wrapHasActiveWriteFromPrevOwner), - allowHalfOpen: socket ? socket.allowHalfOpen : tlsOptions.allowHalfOpen, - pauseOnCreate: tlsOptions.pauseOnConnect, - manualStart: true, - highWaterMark: tlsOptions.highWaterMark, - onread: !socket ? tlsOptions.onread : null, - signal: tlsOptions.signal, - }]); - - // Proxy for API compatibility - this.ssl = this._handle; // C++ TLSWrap object - - this.on('error', this._tlsError); - - this._init(socket, wrap); - - if (enableTrace && this._handle) - this._handle.enableTrace(); - - if (wrapHasActiveWriteFromPrevOwner) { - // `wrap` is a streams.Writable in JS. This empty write will be queued - // and hence finish after all existing writes, which is the timing - // we want to start to send any tls data to `wrap`. - wrap.write('', (err) => { - if (err) { - debug('error got before writing any tls data to the underlying stream'); - this.destroy(err); - return; - } - - this._handle.writesIssuedByPrevListenerDone(); - }); - } - - // Read on next tick so the caller has a chance to setup listeners - process.nextTick(initRead, this, socket); -} -ObjectSetPrototypeOf(TLSSocket.prototype, net.Socket.prototype); -ObjectSetPrototypeOf(TLSSocket, net.Socket); -exports.TLSSocket = TLSSocket; - -const proxiedMethods = [ - 'ref', 'unref', 'open', 'bind', 'listen', 'connect', 'bind6', - 'connect6', 'getsockname', 'getpeername', 'setNoDelay', 'setKeepAlive', - 'setSimultaneousAccepts', 'setBlocking', - - // PipeWrap - 'setPendingInstances', -]; - -// Proxy HandleWrap, PipeWrap and TCPWrap methods -function makeMethodProxy(name) { - return function methodProxy(...args) { - if (this._parent[name]) - return ReflectApply(this._parent[name], this._parent, args); - }; -} -for (const proxiedMethod of proxiedMethods) { - tls_wrap.TLSWrap.prototype[proxiedMethod] = - makeMethodProxy(proxiedMethod); -} - -tls_wrap.TLSWrap.prototype.close = function close(cb) { - let ssl; - if (this[owner_symbol]) { - ssl = this[owner_symbol].ssl; - this[owner_symbol].ssl = null; - } - - // Invoke `destroySSL` on close to clean up possibly pending write requests - // that may self-reference TLSWrap, leading to leak - const done = () => { - if (ssl) { - ssl.destroySSL(); - if (ssl._secureContext.singleUse) { - ssl._secureContext.context.close(); - ssl._secureContext.context = null; - } - } - if (cb) - cb(); - }; - - if (this._parentWrap) { - if (this._parentWrap._handle === null) { - // The socket handle was already closed. - done(); - return; - } - - if (this._parentWrap._handle === this._parent) { - this._parentWrap.once('close', done); - this._parentWrap.destroy(); - return; - } - } - - return this._parent.close(done); -}; - -TLSSocket.prototype.disableRenegotiation = function disableRenegotiation() { - this[kDisableRenegotiation] = true; -}; - -/** - * - * @param {null|net.Socket} wrap - * @param {null|object} handle - * @param {boolean} wrapHasActiveWriteFromPrevOwner - * @returns {object} - */ -TLSSocket.prototype._wrapHandle = function(wrap, handle, wrapHasActiveWriteFromPrevOwner) { - const options = this._tlsOptions; - if (!handle) { - handle = options.pipe ? - new Pipe(PipeConstants.SOCKET) : - new TCP(TCPConstants.SOCKET); - handle[owner_symbol] = this; - } - - // Wrap socket's handle - const context = options.secureContext || - options.credentials || - tls.createSecureContext(options); - assert(handle.isStreamBase, 'handle must be a StreamBase'); - if (!(context.context instanceof NativeSecureContext)) { - throw new ERR_TLS_INVALID_CONTEXT('context'); - } - - const res = tls_wrap.wrap(handle, context.context, - !!options.isServer, - wrapHasActiveWriteFromPrevOwner); - res._parent = handle; // C++ "wrap" object: TCPWrap, JSStream, ... - res._parentWrap = wrap; // JS object: net.Socket, JSStreamSocket, ... - res._secureContext = context; - res.reading = handle.reading; - this[kRes] = res; - defineHandleReading(this, handle); - - // Guard against adding multiple listeners, as this method may be called - // repeatedly on the same socket by reinitializeHandle - if (this.listenerCount('close', onSocketCloseDestroySSL) === 0) { - this.on('close', onSocketCloseDestroySSL); - } - - if (wrap) { - wrap.on('close', () => this.destroy()); - } - - return res; -}; - -TLSSocket.prototype[kReinitializeHandle] = function reinitializeHandle(handle) { - const originalServername = this.ssl ? this._handle.getServername() : null; - const originalSession = this.ssl ? this._handle.getSession() : null; - - this.handle = this._wrapHandle(null, handle, false); - this.ssl = this._handle; - - net.Socket.prototype[kReinitializeHandle].call(this, this.handle); - this._init(); - - if (this._tlsOptions.enableTrace) { - this._handle.enableTrace(); - } - - if (originalSession) { - this.setSession(originalSession); - } - - if (originalServername) { - this.setServername(originalServername); - } -}; - -// This eliminates a cyclic reference to TLSWrap -// Ref: https://github.com/nodejs/node/commit/f7620fb96d339f704932f9bb9a0dceb9952df2d4 -function defineHandleReading(socket, handle) { - ObjectDefineProperty(handle, 'reading', { - __proto__: null, - get: () => { - return socket[kRes].reading; - }, - set: (value) => { - socket[kRes].reading = value; - }, - }); -} - -function onSocketCloseDestroySSL() { - // Make sure we are not doing it on OpenSSL's stack - setImmediate(destroySSL, this); - this[kRes] = null; -} - -function destroySSL(self) { - self._destroySSL(); -} - -TLSSocket.prototype._destroySSL = function _destroySSL() { - if (!this.ssl) return; - this.ssl.destroySSL(); - if (this.ssl._secureContext.singleUse) { - this.ssl._secureContext.context.close(); - this.ssl._secureContext.context = null; - } - this.ssl = null; - this[kPendingSession] = null; - this[kIsVerified] = false; -}; - -function keylogNewListener(event) { - if (event !== 'keylog') - return; - - // Guard against enableKeylogCallback after destroy - if (!this._handle) return; - this._handle.enableKeylogCallback(); - - // Remove this listener since it's no longer needed. - this.removeListener('newListener', keylogNewListener); -} - -function newListener(event) { - if (event !== 'session') - return; - - // Guard against enableSessionCallbacks after destroy - if (!this._handle) return; - this._handle.enableSessionCallbacks(); - - // Remove this listener since it's no longer needed. - this.removeListener('newListener', newListener); -} - -// Constructor guts, arbitrarily factored out. -let warnOnTlsKeylog = true; -let warnOnTlsKeylogError = true; -TLSSocket.prototype._init = function(socket, wrap) { - const options = this._tlsOptions; - const ssl = this._handle; - this.server = options.server; - - debug('%s _init', - options.isServer ? 'server' : 'client', - 'handle?', !!ssl, - ); - - // Clients (!isServer) always request a cert, servers request a client cert - // only on explicit configuration. - const requestCert = !!options.requestCert || !options.isServer; - const rejectUnauthorized = !!options.rejectUnauthorized; - - this._requestCert = requestCert; - this._rejectUnauthorized = rejectUnauthorized; - if (requestCert || rejectUnauthorized) - ssl.setVerifyMode(requestCert, rejectUnauthorized); - - // Only call .onkeylog if there is a keylog listener. - ssl.onkeylog = onkeylog; - - if (this.listenerCount('newListener', keylogNewListener) === 0) { - this.on('newListener', keylogNewListener); - } - - if (options.isServer) { - ssl.onhandshakestart = onhandshakestart; - ssl.onhandshakedone = onhandshakedone; - ssl.onclienthello = loadSession; - ssl.oncertcb = loadSNI; - ssl.onnewsession = onnewsession; - ssl.lastHandshakeTime = 0; - ssl.handshakes = 0; - - if (options.ALPNCallback) { - validateFunction(options.ALPNCallback, 'options.ALPNCallback'); - this[kALPNCallback] = options.ALPNCallback; - ssl.ALPNCallback = callALPNCallback; - ssl.enableALPNCb(); - } - - if (this.server) { - if (this.server.listenerCount('resumeSession') > 0 || - this.server.listenerCount('newSession') > 0) { - // Also starts the client hello parser as a side effect. - ssl.enableSessionCallbacks(); - } - if (this.server.listenerCount('OCSPRequest') > 0) - ssl.enableCertCb(); - } - } else { - ssl.onhandshakestart = noop; - ssl.onhandshakedone = () => { - debug('client onhandshakedone'); - this._finishInit(); - }; - ssl.onocspresponse = onocspresponse; - - if (options.session) - ssl.setSession(options.session); - - ssl.onnewsession = onnewsessionclient; - - // Only call .onnewsession if there is a session listener. - if (this.listenerCount('newListener', newListener) === 0) { - this.on('newListener', newListener); - } - } - - if (tlsKeylog) { - if (warnOnTlsKeylog) { - warnOnTlsKeylog = false; - process.emitWarning('Using --tls-keylog makes TLS connections insecure ' + - 'by writing secret key material to file ' + tlsKeylog); - } - this.on('keylog', (line) => { - appendFile(tlsKeylog, line, { mode: 0o600 }, (err) => { - if (err && warnOnTlsKeylogError) { - warnOnTlsKeylogError = false; - process.emitWarning('Failed to write TLS keylog (this warning ' + - 'will not be repeated): ' + err); - } - }); - }); - } - - ssl.onerror = onerror; - - // If custom SNICallback was given, or if - // there're SNI contexts to perform match against - - // set `.onsniselect` callback. - if (options.isServer && - options.SNICallback && - (options.SNICallback !== SNICallback || - (options.server && options.server._contexts.length))) { - validateFunction(options.SNICallback, 'options.SNICallback'); - this._SNICallback = options.SNICallback; - ssl.enableCertCb(); - } - - if (options.ALPNProtocols) - ssl.setALPNProtocols(options.ALPNProtocols); - - if (options.pskCallback && ssl.enablePskCallback) { - validateFunction(options.pskCallback, 'pskCallback'); - - ssl[kOnPskExchange] = options.isServer ? - onPskServerCallback : onPskClientCallback; - - this[kPskCallback] = options.pskCallback; - ssl.enablePskCallback(); - - if (options.pskIdentityHint) { - validateString(options.pskIdentityHint, 'options.pskIdentityHint'); - ssl.setPskIdentityHint(options.pskIdentityHint); - } - } - - // We can only come here via [kWrapConnectedHandle]() call that happens - // if the connection is established with `autoSelectFamily` set to `true`. - const connectOptions = this[kConnectOptions]; - if (!options.isServer && connectOptions) { - if (connectOptions.servername) { - this.setServername(connectOptions.servername); - } - } - - if (options.handshakeTimeout > 0) - this.setTimeout(options.handshakeTimeout, this._handleTimeout); - - if (socket instanceof net.Socket) { - this._parent = socket; - - // To prevent assertion in afterConnect() and properly kick off readStart - this.connecting = socket.connecting || !socket._handle; - socket.once('connect', () => { - this.connecting = false; - this.emit('connect'); - }); - } - - // Assume `tls.connect()` - if (wrap) { - wrap.on('error', (err) => this._emitTLSError(err)); - } else { - assert(!socket); - this.connecting = true; - } -}; - -TLSSocket.prototype.renegotiate = function(options, callback) { - validateObject(options, 'options'); - if (callback !== undefined) { - validateFunction(callback, 'callback'); - } - - debug('%s renegotiate()', - this._tlsOptions.isServer ? 'server' : 'client', - 'destroyed?', this.destroyed, - ); - - if (this.destroyed) - return; - - let requestCert = !!this._requestCert; - let rejectUnauthorized = !!this._rejectUnauthorized; - - if (options.requestCert !== undefined) - requestCert = !!options.requestCert; - if (options.rejectUnauthorized !== undefined) - rejectUnauthorized = !!options.rejectUnauthorized; - - if (requestCert !== this._requestCert || - rejectUnauthorized !== this._rejectUnauthorized) { - this._handle.setVerifyMode(requestCert, rejectUnauthorized); - this._requestCert = requestCert; - this._rejectUnauthorized = rejectUnauthorized; - } - // Ensure that we'll cycle through internal openssl's state - this.write(''); - - try { - this._handle.renegotiate(); - } catch (err) { - if (callback) { - process.nextTick(callback, err); - } - return false; - } - - // Ensure that we'll cycle through internal openssl's state - this.write(''); - - if (callback) { - this.once('secure', () => callback(null)); - } - - return true; -}; - -TLSSocket.prototype.exportKeyingMaterial = function(length, label, context) { - validateUint32(length, 'length', true); - validateString(label, 'label'); - if (context !== undefined) - validateBuffer(context, 'context'); - - if (!this._secureEstablished) - throw new ERR_TLS_INVALID_STATE(); - - return this._handle.exportKeyingMaterial(length, label, context); -}; - -TLSSocket.prototype.setMaxSendFragment = function setMaxSendFragment(size) { - validateInt32(size, 'size'); - return this._handle.setMaxSendFragment(size) === 1; -}; - -TLSSocket.prototype._handleTimeout = function() { - this._emitTLSError(new ERR_TLS_HANDSHAKE_TIMEOUT()); -}; - -TLSSocket.prototype._emitTLSError = function(err) { - const e = this._tlsError(err); - if (e) - this.emit('error', e); -}; - -TLSSocket.prototype._tlsError = function(err) { - this.emit('_tlsError', err); - if (this._controlReleased) - return err; - return null; -}; - -TLSSocket.prototype._releaseControl = function() { - if (this._controlReleased) - return false; - this._controlReleased = true; - this.removeListener('error', this._tlsError); - return true; -}; - -TLSSocket.prototype._finishInit = function() { - // Guard against getting onhandshakedone() after .destroy(). - // * 1.2: If destroy() during onocspresponse(), then write of next handshake - // record fails, the handshake done info callbacks does not occur, and the - // socket closes. - // * 1.3: The OCSP response comes in the same record that finishes handshake, - // so even after .destroy(), the handshake done info callback occurs - // immediately after onocspresponse(). Ignore it. - if (!this._handle) - return; - - this.alpnProtocol = this._handle.getALPNNegotiatedProtocol(); - // The servername could be set by TLSWrap::SelectSNIContextCallback(). - if (this.servername === null) { - this.servername = this._handle.getServername(); - } - - debug('%s _finishInit', - this._tlsOptions.isServer ? 'server' : 'client', - 'handle?', !!this._handle, - 'alpn', this.alpnProtocol, - 'servername', this.servername); - - this._secureEstablished = true; - if (this._tlsOptions.handshakeTimeout > 0) - this.setTimeout(0, this._handleTimeout); - this.emit('secure'); -}; - -TLSSocket.prototype._start = function() { - debug('%s _start', - this._tlsOptions.isServer ? 'server' : 'client', - 'handle?', !!this._handle, - 'connecting?', this.connecting, - 'requestOCSP?', !!this._tlsOptions.requestOCSP, - ); - if (this.connecting) { - this.once('connect', this._start); - return; - } - - // Socket was destroyed before the connection was established - if (!this._handle) - return; - - if (this._tlsOptions.requestOCSP) - this._handle.requestOCSP(); - this._handle.start(); -}; - -TLSSocket.prototype.setServername = function(name) { - validateString(name, 'name'); - - if (this._tlsOptions.isServer) { - throw new ERR_TLS_SNI_FROM_SERVER(); - } - - this._handle.setServername(name); -}; - -TLSSocket.prototype.setSession = function(session) { - if (typeof session === 'string') - session = Buffer.from(session, 'latin1'); - this._handle.setSession(session); -}; - -TLSSocket.prototype.getPeerCertificate = function(detailed) { - if (this._handle) { - return common.translatePeerCertificate( - this._handle.getPeerCertificate(detailed)) || {}; - } - - return null; -}; - -TLSSocket.prototype.getCertificate = function() { - if (this._handle) { - // It's not a peer cert, but the formatting is identical. - return common.translatePeerCertificate( - this._handle.getCertificate()) || {}; - } - - return null; -}; - -TLSSocket.prototype.getPeerX509Certificate = function(detailed) { - const cert = this._handle?.getPeerX509Certificate(); - return cert ? new InternalX509Certificate(cert) : undefined; -}; - -TLSSocket.prototype.getX509Certificate = function() { - const cert = this._handle?.getX509Certificate(); - return cert ? new InternalX509Certificate(cert) : undefined; -}; - -TLSSocket.prototype.setKeyCert = function(context) { - if (this._handle) { - let secureContext; - if (context instanceof common.SecureContext) - secureContext = context; - else - secureContext = tls.createSecureContext(context); - this._handle.setKeyCert(secureContext.context); - } -}; - -// Proxy TLSSocket handle methods -function makeSocketMethodProxy(name) { - return function socketMethodProxy(...args) { - if (this._handle) - return ReflectApply(this._handle[name], this._handle, args); - return null; - }; -} - -[ - 'getCipher', - 'getSharedSigalgs', - 'getEphemeralKeyInfo', - 'getFinished', - 'getPeerFinished', - 'getProtocol', - 'getSession', - 'getTLSTicket', - 'isSessionReused', - 'enableTrace', -].forEach((method) => { - TLSSocket.prototype[method] = makeSocketMethodProxy(method); -}); - -// TODO: support anonymous (nocert) - - -function onServerSocketSecure() { - if (this._requestCert) { - const verifyError = this._handle.verifyError(); - if (verifyError) { - this.authorizationError = verifyError.code; - - if (this._rejectUnauthorized) - this.destroy(); - } else { - this.authorized = true; - } - } - - if (!this.destroyed && this._releaseControl()) { - debug('server emit secureConnection'); - this.secureConnecting = false; - this._tlsOptions.server.emit('secureConnection', this); - } -} - -function onSocketTLSError(err) { - if (!this._controlReleased && !this[kErrorEmitted]) { - this[kErrorEmitted] = true; - debug('server emit tlsClientError:', err); - this._tlsOptions.server.emit('tlsClientError', err, this); - } -} - -function onSocketKeylog(line) { - this._tlsOptions.server.emit('keylog', line, this); -} - -function onSocketClose(err) { - // Closed because of error - no need to emit it twice - if (err) - return; - - // Emit ECONNRESET - if (!this._controlReleased && !this[kErrorEmitted]) { - this[kErrorEmitted] = true; - const connReset = new ConnResetException('socket hang up'); - this._tlsOptions.server.emit('tlsClientError', connReset, this); - } -} - -function tlsConnectionListener(rawSocket) { - debug('net.Server.on(connection): new TLSSocket'); - const socket = new TLSSocket(rawSocket, { - secureContext: this._sharedCreds, - isServer: true, - server: this, - requestCert: this.requestCert, - rejectUnauthorized: this.rejectUnauthorized, - handshakeTimeout: this[kHandshakeTimeout], - ALPNProtocols: this.ALPNProtocols, - ALPNCallback: this.ALPNCallback, - SNICallback: this[kSNICallback] || SNICallback, - enableTrace: this[kEnableTrace], - pauseOnConnect: this.pauseOnConnect, - pskCallback: this[kPskCallback], - pskIdentityHint: this[kPskIdentityHint], - }); - - socket.on('secure', onServerSocketSecure); - - if (this.listenerCount('keylog') > 0) - socket.on('keylog', onSocketKeylog); - - socket[kErrorEmitted] = false; - socket.on('close', onSocketClose); - socket.on('_tlsError', onSocketTLSError); -} - -// AUTHENTICATION MODES -// -// There are several levels of authentication that TLS/SSL supports. -// Read more about this in "man SSL_set_verify". -// -// 1. The server sends a certificate to the client but does not request a -// cert from the client. This is common for most HTTPS servers. The browser -// can verify the identity of the server, but the server does not know who -// the client is. Authenticating the client is usually done over HTTP using -// login boxes and cookies and stuff. -// -// 2. The server sends a cert to the client and requests that the client -// also send it a cert. The client knows who the server is and the server is -// requesting the client also identify themselves. There are several -// outcomes: -// -// A) verifyError returns null meaning the client's certificate is signed -// by one of the server's CAs. The server now knows the client's identity -// and the client is authorized. -// -// B) For some reason the client's certificate is not acceptable - -// verifyError returns a string indicating the problem. The server can -// either (i) reject the client or (ii) allow the client to connect as an -// unauthorized connection. -// -// The mode is controlled by two boolean variables. -// -// requestCert -// If true the server requests a certificate from client connections. For -// the common HTTPS case, users will want this to be false, which is what -// it defaults to. -// -// rejectUnauthorized -// If true clients whose certificates are invalid for any reason will not -// be allowed to make connections. If false, they will simply be marked as -// unauthorized but secure communication will continue. By default this is -// true. -// -// -// -// Options: -// - requestCert. Send verify request. Default to false. -// - rejectUnauthorized. Boolean, default to true. -// - key. string. -// - cert: string. -// - clientCertEngine: string. -// - ca: string or array of strings. -// - sessionTimeout: integer. -// -// emit 'secureConnection' -// function (tlsSocket) { } -// -// "UNABLE_TO_GET_ISSUER_CERT", "UNABLE_TO_GET_CRL", -// "UNABLE_TO_DECRYPT_CERT_SIGNATURE", "UNABLE_TO_DECRYPT_CRL_SIGNATURE", -// "UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY", "CERT_SIGNATURE_FAILURE", -// "CRL_SIGNATURE_FAILURE", "CERT_NOT_YET_VALID" "CERT_HAS_EXPIRED", -// "CRL_NOT_YET_VALID", "CRL_HAS_EXPIRED" "ERROR_IN_CERT_NOT_BEFORE_FIELD", -// "ERROR_IN_CERT_NOT_AFTER_FIELD", "ERROR_IN_CRL_LAST_UPDATE_FIELD", -// "ERROR_IN_CRL_NEXT_UPDATE_FIELD", "OUT_OF_MEM", -// "DEPTH_ZERO_SELF_SIGNED_CERT", "SELF_SIGNED_CERT_IN_CHAIN", -// "UNABLE_TO_GET_ISSUER_CERT_LOCALLY", "UNABLE_TO_VERIFY_LEAF_SIGNATURE", -// "CERT_CHAIN_TOO_LONG", "CERT_REVOKED" "INVALID_CA", -// "PATH_LENGTH_EXCEEDED", "INVALID_PURPOSE" "CERT_UNTRUSTED", -// "CERT_REJECTED" -// -function Server(options, listener) { - if (!(this instanceof Server)) - return new Server(options, listener); - - if (typeof options === 'function') { - listener = options; - options = kEmptyObject; - } else if (options == null || typeof options === 'object') { - options ??= kEmptyObject; - } else { - throw new ERR_INVALID_ARG_TYPE('options', 'Object', options); - } - - this._contexts = []; - this.requestCert = options.requestCert === true; - this.rejectUnauthorized = options.rejectUnauthorized !== false; - - this.ALPNCallback = options.ALPNCallback; - if (this.ALPNCallback && options.ALPNProtocols) { - throw new ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS(); - } - - if (options.sessionTimeout) - this.sessionTimeout = options.sessionTimeout; - - if (options.ticketKeys) - this.ticketKeys = options.ticketKeys; - - if (options.ALPNProtocols) - tls.convertALPNProtocols(options.ALPNProtocols, this); - - this.setSecureContext(options); - - this[kHandshakeTimeout] = options.handshakeTimeout || (120 * 1000); - this[kSNICallback] = options.SNICallback; - this[kPskCallback] = options.pskCallback; - this[kPskIdentityHint] = options.pskIdentityHint; - - validateNumber(this[kHandshakeTimeout], 'options.handshakeTimeout'); - - if (this[kSNICallback]) { - validateFunction(this[kSNICallback], 'options.SNICallback'); - } - - if (this[kPskCallback]) { - validateFunction(this[kPskCallback], 'options.pskCallback'); - } - - if (this[kPskIdentityHint]) { - validateString(this[kPskIdentityHint], 'options.pskIdentityHint'); - } - - // constructor call - ReflectApply(net.Server, this, [options, tlsConnectionListener]); - - if (listener) { - this.on('secureConnection', listener); - } - - this[kEnableTrace] = options.enableTrace; -} - -ObjectSetPrototypeOf(Server.prototype, net.Server.prototype); -ObjectSetPrototypeOf(Server, net.Server); -exports.Server = Server; -exports.createServer = function createServer(options, listener) { - return new Server(options, listener); -}; - - -Server.prototype.setSecureContext = function(options) { - validateObject(options, 'options'); - - if (options.pfx) - this.pfx = options.pfx; - else - this.pfx = undefined; - - if (options.key) - this.key = options.key; - else - this.key = undefined; - - if (options.passphrase) - this.passphrase = options.passphrase; - else - this.passphrase = undefined; - - if (options.cert) - this.cert = options.cert; - else - this.cert = undefined; - - if (options.clientCertEngine) - this.clientCertEngine = options.clientCertEngine; - else - this.clientCertEngine = undefined; - - if (options.ca) - this.ca = options.ca; - else - this.ca = undefined; - - if (options.minVersion) - this.minVersion = options.minVersion; - else - this.minVersion = undefined; - - if (options.maxVersion) - this.maxVersion = options.maxVersion; - else - this.maxVersion = undefined; - - if (options.secureProtocol) - this.secureProtocol = options.secureProtocol; - else - this.secureProtocol = undefined; - - if (options.crl) - this.crl = options.crl; - else - this.crl = undefined; - - this.sigalgs = options.sigalgs; - - if (options.ciphers) - this.ciphers = options.ciphers; - else - this.ciphers = undefined; - - this.ecdhCurve = options.ecdhCurve; - - if (options.dhparam) - this.dhparam = options.dhparam; - else - this.dhparam = undefined; - - if (options.honorCipherOrder !== undefined) - this.honorCipherOrder = !!options.honorCipherOrder; - else - this.honorCipherOrder = true; - - const secureOptions = options.secureOptions || 0; - - if (secureOptions) - this.secureOptions = secureOptions; - else - this.secureOptions = undefined; - - if (options.sessionIdContext) { - this.sessionIdContext = options.sessionIdContext; - } else { - this.sessionIdContext = crypto.createHash('sha1') - .update(process.argv.join(' ')) - .digest('hex') - .slice(0, 32); - } - - if (options.sessionTimeout) - this.sessionTimeout = options.sessionTimeout; - - if (options.ticketKeys) - this.ticketKeys = options.ticketKeys; - - this.privateKeyIdentifier = options.privateKeyIdentifier; - this.privateKeyEngine = options.privateKeyEngine; - - this._sharedCreds = tls.createSecureContext({ - pfx: this.pfx, - key: this.key, - passphrase: this.passphrase, - cert: this.cert, - clientCertEngine: this.clientCertEngine, - ca: this.ca, - ciphers: this.ciphers, - sigalgs: this.sigalgs, - ecdhCurve: this.ecdhCurve, - dhparam: this.dhparam, - minVersion: this.minVersion, - maxVersion: this.maxVersion, - secureProtocol: this.secureProtocol, - secureOptions: this.secureOptions, - honorCipherOrder: this.honorCipherOrder, - crl: this.crl, - sessionIdContext: this.sessionIdContext, - ticketKeys: this.ticketKeys, - sessionTimeout: this.sessionTimeout, - privateKeyIdentifier: this.privateKeyIdentifier, - privateKeyEngine: this.privateKeyEngine, - }); -}; - - -Server.prototype._getServerData = function() { - return { - ticketKeys: this.getTicketKeys().toString('hex'), - }; -}; - - -Server.prototype._setServerData = function(data) { - this.setTicketKeys(Buffer.from(data.ticketKeys, 'hex')); -}; - - -Server.prototype.getTicketKeys = function getTicketKeys() { - return this._sharedCreds.context.getTicketKeys(); -}; - - -Server.prototype.setTicketKeys = function setTicketKeys(keys) { - validateBuffer(keys); - assert(keys.byteLength === 48, - 'Session ticket keys must be a 48-byte buffer'); - this._sharedCreds.context.setTicketKeys(keys); -}; - -// SNI Contexts High-Level API -Server.prototype.addContext = function(servername, context) { - if (!servername) { - throw new ERR_TLS_REQUIRED_SERVER_NAME(); - } - - const re = new RegExp(`^${ - servername - .replace(/([.^$+?\-\\[\]{}])/g, '\\$1') - .replaceAll('*', '[^.]*') - }$`); - - const secureContext = - context instanceof common.SecureContext ? context : tls.createSecureContext(context); - this._contexts.push([re, secureContext.context]); -}; - -Server.prototype[EE.captureRejectionSymbol] = function( - err, event, sock) { - - switch (event) { - case 'secureConnection': - sock.destroy(err); - break; - default: - ReflectApply(net.Server.prototype[SymbolFor('nodejs.rejection')], this, - [err, event, sock]); - } -}; - -function SNICallback(servername, callback) { - const contexts = this.server._contexts; - - for (let i = contexts.length - 1; i >= 0; --i) { - const elem = contexts[i]; - if (elem[0].test(servername)) { - callback(null, elem[1]); - return; - } - } - - callback(null, undefined); -} - - -// Target API: -// -// let s = tls.connect({port: 8000, host: "google.com"}, function() { -// if (!s.authorized) { -// s.destroy(); -// return; -// } -// -// // s.socket; -// -// s.end("hello world\n"); -// }); -// -// -function normalizeConnectArgs(listArgs) { - const args = net._normalizeArgs(listArgs); - const options = args[0]; - const cb = args[1]; - - // If args[0] was options, then normalize dealt with it. - // If args[0] is port, or args[0], args[1] is host, port, we need to - // find the options and merge them in, normalize's options has only - // the host/port/path args that it knows about, not the tls options. - // This means that options.host overrides a host arg. - if (listArgs[1] !== null && typeof listArgs[1] === 'object') { - ObjectAssign(options, listArgs[1]); - } else { - ObjectAssign(options, listArgs[2]); - } - - return cb ? [options, cb] : [options]; -} - -function onConnectSecure() { - const options = this[kConnectOptions]; - - // Check the size of DHE parameter above minimum requirement - // specified in options. - const ekeyinfo = this.getEphemeralKeyInfo(); - if (ekeyinfo.type === 'DH' && ekeyinfo.size < options.minDHSize) { - const err = new ERR_TLS_DH_PARAM_SIZE(ekeyinfo.size); - debug('client emit:', err); - this.emit('error', err); - this.destroy(); - return; - } - - let verifyError = this._handle.verifyError(); - - // Verify that server's identity matches it's certificate's names - // Unless server has resumed our existing session - if (!verifyError && !this.isSessionReused()) { - const hostname = options.servername || - options.host || - (options.socket?._host) || - 'localhost'; - const cert = this.getPeerCertificate(true); - verifyError = options.checkServerIdentity(hostname, cert); - } - - if (verifyError) { - this.authorized = false; - this.authorizationError = verifyError.code || verifyError.message; - - // rejectUnauthorized property can be explicitly defined as `undefined` - // causing the assignment to default value (`true`) fail. Before assigning - // it to the tlssock connection options, explicitly check if it is false - // and update rejectUnauthorized property. The property gets used by - // TLSSocket connection handler to allow or reject connection if - // unauthorized. - // This check is potentially redundant, however it is better to keep it - // in case the option object gets modified somewhere. - if (options.rejectUnauthorized !== false) { - this.destroy(verifyError); - return; - } - debug('client emit secureConnect. rejectUnauthorized: %s, ' + - 'authorizationError: %s', options.rejectUnauthorized, - this.authorizationError); - } else { - this.authorized = true; - debug('client emit secureConnect. authorized:', this.authorized); - } - this.secureConnecting = false; - this.emit('secureConnect'); - - this[kIsVerified] = true; - const session = this[kPendingSession]; - this[kPendingSession] = null; - if (session) - this.emit('session', session); - - this.removeListener('end', onConnectEnd); -} - -function onConnectEnd() { - // NOTE: This logic is shared with _http_client.js - if (!this._hadError) { - const options = this[kConnectOptions]; - this._hadError = true; - const error = new ConnResetException('Client network socket disconnected ' + - 'before secure TLS connection was ' + - 'established'); - error.path = options.path; - error.host = options.host; - error.port = options.port; - error.localAddress = options.localAddress; - this.destroy(error); - } -} - -// Arguments: [port,] [host,] [options,] [cb] -exports.connect = function connect(...args) { - args = normalizeConnectArgs(args); - let options = args[0]; - const cb = args[1]; - const allowUnauthorized = getAllowUnauthorized(); - - options = { - rejectUnauthorized: !allowUnauthorized, - ciphers: tls.DEFAULT_CIPHERS, - checkServerIdentity: tls.checkServerIdentity, - minDHSize: 1024, - ...options, - }; - - if (!options.keepAlive) - options.singleUse = true; - - validateFunction(options.checkServerIdentity, 'options.checkServerIdentity'); - validateNumber(options.minDHSize, 'options.minDHSize', 1); - - const context = options.secureContext || tls.createSecureContext(options); - - const tlssock = new TLSSocket(options.socket, { - allowHalfOpen: options.allowHalfOpen, - pipe: !!options.path, - secureContext: context, - isServer: false, - requestCert: true, - rejectUnauthorized: options.rejectUnauthorized !== false, - session: options.session, - ALPNProtocols: options.ALPNProtocols, - requestOCSP: options.requestOCSP, - enableTrace: options.enableTrace, - pskCallback: options.pskCallback, - highWaterMark: options.highWaterMark, - onread: options.onread, - signal: options.signal, - }); - - // rejectUnauthorized property can be explicitly defined as `undefined` - // causing the assignment to default value (`true`) fail. Before assigning - // it to the tlssock connection options, explicitly check if it is false - // and update rejectUnauthorized property. The property gets used by TLSSocket - // connection handler to allow or reject connection if unauthorized - options.rejectUnauthorized = options.rejectUnauthorized !== false; - - tlssock[kConnectOptions] = options; - - if (cb) - tlssock.once('secureConnect', cb); - - if (!options.socket) { - // If user provided the socket, it's their responsibility to manage its - // connectivity. If we created one internally, we connect it. - if (options.timeout) { - tlssock.setTimeout(options.timeout); - } - - tlssock.connect(options, tlssock._start); - } - - tlssock._releaseControl(); - - if (options.session) - tlssock.setSession(options.session); - - if (options.servername) { - if (!ipServernameWarned && net.isIP(options.servername)) { - process.emitWarning( - 'Setting the TLS ServerName to an IP address is not permitted by ' + - 'RFC 6066. This will be ignored in a future version.', - 'DeprecationWarning', - 'DEP0123', - ); - ipServernameWarned = true; - } - tlssock.setServername(options.servername); - } - - if (options.socket) - tlssock._start(); - - tlssock.on('secure', onConnectSecure); - tlssock.prependListener('end', onConnectEnd); - - return tlssock; +const { TLSSocket, Server, createServer, connect } = require('internal/tls/wrap'); +module.exports = { + TLSSocket, + Server, + createServer, + connect, }; +process.emitWarning('The _tls_wrap module is deprecated.', + 'DeprecationWarning', 'DEP0192'); diff --git a/lib/internal/crypto/x509.js b/lib/internal/crypto/x509.js index 5bc88d7b41c9ab..b9f90c3d6c3ee4 100644 --- a/lib/internal/crypto/x509.js +++ b/lib/internal/crypto/x509.js @@ -371,7 +371,7 @@ class X509Certificate { // TODO(tniessen): do not depend on translatePeerCertificate here, return // the correct legacy representation from the binding lazyTranslatePeerCertificate ??= - require('_tls_common').translatePeerCertificate; + require('internal/tls/common').translatePeerCertificate; return lazyTranslatePeerCertificate(this[kHandle].toLegacy()); } } diff --git a/lib/internal/tls/common.js b/lib/internal/tls/common.js new file mode 100644 index 00000000000000..66331d2d9999e9 --- /dev/null +++ b/lib/internal/tls/common.js @@ -0,0 +1,156 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +const { + JSONParse, +} = primordials; + +const tls = require('tls'); + +const { + codes: { + ERR_TLS_INVALID_PROTOCOL_VERSION, + ERR_TLS_PROTOCOL_VERSION_CONFLICT, + }, +} = require('internal/errors'); + +const { + crypto: { + SSL_OP_CIPHER_SERVER_PREFERENCE, + TLS1_VERSION, + TLS1_1_VERSION, + TLS1_2_VERSION, + TLS1_3_VERSION, + }, +} = internalBinding('constants'); + +const { + kEmptyObject, +} = require('internal/util'); + +const { + validateInteger, +} = require('internal/validators'); + +const { + configSecureContext, +} = require('internal/tls/secure-context'); + +function toV(which, v, def) { + v ??= def; + if (v === 'TLSv1') return TLS1_VERSION; + if (v === 'TLSv1.1') return TLS1_1_VERSION; + if (v === 'TLSv1.2') return TLS1_2_VERSION; + if (v === 'TLSv1.3') return TLS1_3_VERSION; + throw new ERR_TLS_INVALID_PROTOCOL_VERSION(v, which); +} + +const { + SecureContext: NativeSecureContext, +} = internalBinding('crypto'); + +function SecureContext(secureProtocol, secureOptions, minVersion, maxVersion) { + if (!(this instanceof SecureContext)) { + return new SecureContext(secureProtocol, secureOptions, minVersion, + maxVersion); + } + + if (secureProtocol) { + if (minVersion != null) + throw new ERR_TLS_PROTOCOL_VERSION_CONFLICT(minVersion, secureProtocol); + if (maxVersion != null) + throw new ERR_TLS_PROTOCOL_VERSION_CONFLICT(maxVersion, secureProtocol); + } + + this.context = new NativeSecureContext(); + this.context.init(secureProtocol, + toV('minimum', minVersion, tls.DEFAULT_MIN_VERSION), + toV('maximum', maxVersion, tls.DEFAULT_MAX_VERSION)); + + if (secureOptions) { + validateInteger(secureOptions, 'secureOptions'); + this.context.setOptions(secureOptions); + } +} + +function createSecureContext(options) { + options ||= kEmptyObject; + const { + honorCipherOrder, + minVersion, + maxVersion, + secureProtocol, + } = options; + + let { secureOptions } = options; + + if (honorCipherOrder) + secureOptions |= SSL_OP_CIPHER_SERVER_PREFERENCE; + + const c = new SecureContext(secureProtocol, secureOptions, + minVersion, maxVersion); + + configSecureContext(c.context, options); + + return c; +} + +// Translate some fields from the handle's C-friendly format into more idiomatic +// javascript object representations before passing them back to the user. Can +// be used on any cert object, but changing the name would be semver-major. +function translatePeerCertificate(c) { + if (!c) + return null; + + if (c.issuerCertificate != null && c.issuerCertificate !== c) { + c.issuerCertificate = translatePeerCertificate(c.issuerCertificate); + } + if (c.infoAccess != null) { + const info = c.infoAccess; + c.infoAccess = { __proto__: null }; + + // XXX: More key validation? + info.replace(/([^\n:]*):([^\n]*)(?:\n|$)/g, + (all, key, val) => { + if (val.charCodeAt(0) === 0x22) { + // The translatePeerCertificate function is only + // used on internally created legacy certificate + // objects, and any value that contains a quote + // will always be a valid JSON string literal, + // so this should never throw. + val = JSONParse(val); + } + if (key in c.infoAccess) + c.infoAccess[key].push(val); + else + c.infoAccess[key] = [val]; + }); + } + return c; +} + +module.exports = { + SecureContext, + createSecureContext, + translatePeerCertificate, +}; diff --git a/lib/internal/tls/wrap.js b/lib/internal/tls/wrap.js new file mode 100644 index 00000000000000..4f1351f3e3e907 --- /dev/null +++ b/lib/internal/tls/wrap.js @@ -0,0 +1,1782 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +const { + ObjectAssign, + ObjectDefineProperty, + ObjectSetPrototypeOf, + ReflectApply, + RegExp, + Symbol, + SymbolFor, +} = primordials; + +const { + assertCrypto, + kEmptyObject, +} = require('internal/util'); + +assertCrypto(); + +const { setImmediate } = require('timers'); +const assert = require('internal/assert'); +const crypto = require('crypto'); +const EE = require('events'); +const net = require('net'); +const tls = require('tls'); +const common = require('internal/tls/common'); +const { kReinitializeHandle } = require('internal/net'); +const JSStreamSocket = require('internal/js_stream_socket'); +const { Buffer } = require('buffer'); +let debug = require('internal/util/debuglog').debuglog('tls', (fn) => { + debug = fn; +}); +const { TCP, constants: TCPConstants } = internalBinding('tcp_wrap'); +const tls_wrap = internalBinding('tls_wrap'); +const { Pipe, constants: PipeConstants } = internalBinding('pipe_wrap'); +const { owner_symbol } = require('internal/async_hooks').symbols; +const { isArrayBufferView } = require('internal/util/types'); +const { SecureContext: NativeSecureContext } = internalBinding('crypto'); +const { + ConnResetException, + codes: { + ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, + ERR_MULTIPLE_CALLBACK, + ERR_SOCKET_CLOSED, + ERR_TLS_ALPN_CALLBACK_INVALID_RESULT, + ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS, + ERR_TLS_DH_PARAM_SIZE, + ERR_TLS_HANDSHAKE_TIMEOUT, + ERR_TLS_INVALID_CONTEXT, + ERR_TLS_INVALID_STATE, + ERR_TLS_RENEGOTIATION_DISABLED, + ERR_TLS_REQUIRED_SERVER_NAME, + ERR_TLS_SESSION_ATTACK, + ERR_TLS_SNI_FROM_SERVER, + }, +} = require('internal/errors'); +const { onpskexchange: kOnPskExchange } = internalBinding('symbols'); +const { + getOptionValue, + getAllowUnauthorized, +} = require('internal/options'); +const { + validateBoolean, + validateBuffer, + validateFunction, + validateInt32, + validateNumber, + validateObject, + validateString, + validateUint32, +} = require('internal/validators'); +const { + InternalX509Certificate, +} = require('internal/crypto/x509'); +const traceTls = getOptionValue('--trace-tls'); +const tlsKeylog = getOptionValue('--tls-keylog'); +const { appendFile } = require('fs'); +const kConnectOptions = Symbol('connect-options'); +const kDisableRenegotiation = Symbol('disable-renegotiation'); +const kErrorEmitted = Symbol('error-emitted'); +const kHandshakeTimeout = Symbol('handshake-timeout'); +const kRes = Symbol('res'); +const kSNICallback = Symbol('snicallback'); +const kALPNCallback = Symbol('alpncallback'); +const kEnableTrace = Symbol('enableTrace'); +const kPskCallback = Symbol('pskcallback'); +const kPskIdentityHint = Symbol('pskidentityhint'); +const kPendingSession = Symbol('pendingSession'); +const kIsVerified = Symbol('verified'); + +const noop = () => {}; + +let ipServernameWarned = false; +let tlsTracingWarned = false; + +// Server side times how long a handshake is taking to protect against slow +// handshakes being used for DoS. +function onhandshakestart(now) { + debug('server onhandshakestart'); + + const { lastHandshakeTime } = this; + assert(now >= lastHandshakeTime, + `now (${now}) < lastHandshakeTime (${lastHandshakeTime})`); + + this.lastHandshakeTime = now; + + // If this is the first handshake we can skip the rest of the checks. + if (lastHandshakeTime === 0) + return; + + if ((now - lastHandshakeTime) >= tls.CLIENT_RENEG_WINDOW * 1000) + this.handshakes = 1; + else + this.handshakes++; + + const owner = this[owner_symbol]; + + assert(owner._tlsOptions.isServer); + + if (this.handshakes > tls.CLIENT_RENEG_LIMIT) { + owner._emitTLSError(new ERR_TLS_SESSION_ATTACK()); + return; + } + + if (owner[kDisableRenegotiation]) + owner._emitTLSError(new ERR_TLS_RENEGOTIATION_DISABLED()); +} + +function onhandshakedone() { + debug('server onhandshakedone'); + + const owner = this[owner_symbol]; + assert(owner._tlsOptions.isServer); + + // `newSession` callback wasn't called yet + if (owner._newSessionPending) { + owner._securePending = true; + return; + } + + owner._finishInit(); +} + + +function loadSession(hello) { + debug('server onclienthello', + 'sessionid.len', hello.sessionId.length, + 'ticket?', hello.tlsTicket, + ); + const owner = this[owner_symbol]; + + let once = false; + function onSession(err, session) { + debug('server resumeSession callback(err %j, sess? %s)', err, !!session); + if (once) + return owner.destroy(new ERR_MULTIPLE_CALLBACK()); + once = true; + + if (err) + return owner.destroy(err); + + if (owner._handle === null) + return owner.destroy(new ERR_SOCKET_CLOSED()); + + owner._handle.loadSession(session); + // Session is loaded. End the parser to allow handshaking to continue. + owner._handle.endParser(); + } + + if (hello.sessionId.length <= 0 || + hello.tlsTicket || + (owner.server && + !owner.server.emit('resumeSession', hello.sessionId, onSession))) { + // Sessions without identifiers can't be resumed. + // Sessions with tickets can be resumed directly from the ticket, no server + // session storage is necessary. + // Without a call to a resumeSession listener, a session will never be + // loaded, so end the parser to allow handshaking to continue. + owner._handle.endParser(); + } +} + + +function loadSNI(info) { + const owner = this[owner_symbol]; + const servername = info.servername; + if (!servername || !owner._SNICallback) + return requestOCSP(owner, info); + + let once = false; + owner._SNICallback(servername, (err, context) => { + if (once) + return owner.destroy(new ERR_MULTIPLE_CALLBACK()); + once = true; + + if (err) + return owner.destroy(err); + + if (owner._handle === null) + return owner.destroy(new ERR_SOCKET_CLOSED()); + + // TODO(indutny): eventually disallow raw `SecureContext` + if (context) + owner._handle.sni_context = context.context || context; + + requestOCSP(owner, info); + }); +} + + +function callALPNCallback(protocolsBuffer) { + const handle = this; + const socket = handle[owner_symbol]; + + const servername = handle.getServername(); + + // Collect all the protocols from the given buffer: + const protocols = []; + let offset = 0; + while (offset < protocolsBuffer.length) { + const protocolLen = protocolsBuffer[offset]; + offset += 1; + + const protocol = protocolsBuffer.slice(offset, offset + protocolLen); + offset += protocolLen; + + protocols.push(protocol.toString('ascii')); + } + + const selectedProtocol = socket[kALPNCallback]({ + servername, + protocols, + }); + + // Undefined -> all proposed protocols rejected + if (selectedProtocol === undefined) return undefined; + + const protocolIndex = protocols.indexOf(selectedProtocol); + if (protocolIndex === -1) { + throw new ERR_TLS_ALPN_CALLBACK_INVALID_RESULT(selectedProtocol, protocols); + } + let protocolOffset = 0; + for (let i = 0; i < protocolIndex; i++) { + protocolOffset += 1 + protocols[i].length; + } + + return protocolOffset; +} + +function requestOCSP(socket, info) { + if (!info.OCSPRequest || !socket.server) + return requestOCSPDone(socket); + + let ctx = socket._handle.sni_context; + + if (!ctx) { + ctx = socket.server._sharedCreds; + + // TLS socket is using a `net.Server` instead of a tls.TLSServer. + // Some TLS properties like `server._sharedCreds` will not be present + if (!ctx) + return requestOCSPDone(socket); + } + + // TODO(indutny): eventually disallow raw `SecureContext` + if (ctx.context) + ctx = ctx.context; + + if (socket.server.listenerCount('OCSPRequest') === 0) { + return requestOCSPDone(socket); + } + + let once = false; + const onOCSP = (err, response) => { + debug('server OCSPRequest done', 'handle?', !!socket._handle, 'once?', once, + 'response?', !!response, 'err?', err); + if (once) + return socket.destroy(new ERR_MULTIPLE_CALLBACK()); + once = true; + + if (err) + return socket.destroy(err); + + if (socket._handle === null) + return socket.destroy(new ERR_SOCKET_CLOSED()); + + if (response) + socket._handle.setOCSPResponse(response); + requestOCSPDone(socket); + }; + + debug('server oncertcb emit OCSPRequest'); + socket.server.emit('OCSPRequest', + ctx.getCertificate(), + ctx.getIssuer(), + onOCSP); +} + +function requestOCSPDone(socket) { + debug('server certcb done'); + try { + socket._handle.certCbDone(); + } catch (e) { + debug('server certcb done errored', e); + socket.destroy(e); + } +} + +function onnewsessionclient(sessionId, session) { + debug('client emit session'); + const owner = this[owner_symbol]; + if (owner[kIsVerified]) { + owner.emit('session', session); + } else { + owner[kPendingSession] = session; + } +} + +function onnewsession(sessionId, session) { + debug('onnewsession'); + const owner = this[owner_symbol]; + + // TODO(@sam-github) no server to emit the event on, but handshake won't + // continue unless newSessionDone() is called, should it be, or is that + // situation unreachable, or only occurring during shutdown? + if (!owner.server) + return; + + let once = false; + const done = () => { + debug('onnewsession done'); + if (once) + return; + once = true; + + if (owner._handle === null) + return owner.destroy(new ERR_SOCKET_CLOSED()); + + this.newSessionDone(); + + owner._newSessionPending = false; + if (owner._securePending) + owner._finishInit(); + owner._securePending = false; + }; + + owner._newSessionPending = true; + if (!owner.server.emit('newSession', sessionId, session, done)) + done(); +} + +function onPskServerCallback(identity, maxPskLen) { + const owner = this[owner_symbol]; + const ret = owner[kPskCallback](owner, identity); + if (ret == null) + return undefined; + + let psk; + if (isArrayBufferView(ret)) { + psk = ret; + } else { + if (typeof ret !== 'object') { + throw new ERR_INVALID_ARG_TYPE( + 'ret', + ['Object', 'Buffer', 'TypedArray', 'DataView'], + ret, + ); + } + psk = ret.psk; + validateBuffer(psk, 'psk'); + } + + if (psk.length > maxPskLen) { + throw new ERR_INVALID_ARG_VALUE( + 'psk', + psk, + `Pre-shared key exceeds ${maxPskLen} bytes`, + ); + } + + return psk; +} + +function onPskClientCallback(hint, maxPskLen, maxIdentityLen) { + const owner = this[owner_symbol]; + const ret = owner[kPskCallback](hint); + if (ret == null) + return undefined; + + validateObject(ret, 'ret'); + + validateBuffer(ret.psk, 'psk'); + if (ret.psk.length > maxPskLen) { + throw new ERR_INVALID_ARG_VALUE( + 'psk', + ret.psk, + `Pre-shared key exceeds ${maxPskLen} bytes`, + ); + } + + validateString(ret.identity, 'identity'); + if (Buffer.byteLength(ret.identity) > maxIdentityLen) { + throw new ERR_INVALID_ARG_VALUE( + 'identity', + ret.identity, + `PSK identity exceeds ${maxIdentityLen} bytes`, + ); + } + + return { psk: ret.psk, identity: ret.identity }; +} + +function onkeylog(line) { + debug('onkeylog'); + this[owner_symbol].emit('keylog', line); +} + +function onocspresponse(resp) { + debug('client onocspresponse'); + this[owner_symbol].emit('OCSPResponse', resp); +} + +function onerror(err) { + const owner = this[owner_symbol]; + debug('%s onerror %s had? %j', + (typeof owner._tlsOptions === 'object' && owner._tlsOptions !== null) ? + owner._tlsOptions.isServer ? 'server' : 'client' : + 'unknown', + err, owner._hadError); + + if (owner._hadError) + return; + + owner._hadError = true; + + // Destroy socket if error happened before handshake's finish + if (!owner._secureEstablished) { + // When handshake fails control is not yet released, + // so self._tlsError will return null instead of actual error + + // Set closing the socket after emitting an event since the socket needs to + // be accessible when the `tlsClientError` event is emitted. + owner._closeAfterHandlingError = true; + owner.destroy(err); + } else if (owner._tlsOptions?.isServer && + owner._rejectUnauthorized && + /peer did not return a certificate/.test(err.message)) { + // Ignore server's authorization errors + owner.destroy(); + } else { + // Emit error + owner._emitTLSError(err); + } +} + +// Used by both client and server TLSSockets to start data flowing from _handle, +// read(0) causes a StreamBase::ReadStart, via Socket._read. +function initRead(tlsSocket, socket) { + debug('%s initRead', + tlsSocket._tlsOptions.isServer ? 'server' : 'client', + 'handle?', !!tlsSocket._handle, + 'buffered?', !!socket && socket.readableLength, + ); + // If we were destroyed already don't bother reading + if (!tlsSocket._handle) + return; + + // Socket already has some buffered data - emulate receiving it + if (socket?.readableLength) { + let buf; + while ((buf = socket.read()) !== null) + tlsSocket._handle.receive(buf); + } + + tlsSocket.read(0); +} + +/** + * Provides a wrap of socket stream to do encrypted communication. + */ + +function TLSSocket(socket, opts) { + const tlsOptions = { ...opts }; + let enableTrace = tlsOptions.enableTrace; + + if (enableTrace == null) { + enableTrace = traceTls; + + if (enableTrace && !tlsTracingWarned) { + tlsTracingWarned = true; + process.emitWarning('Enabling --trace-tls can expose sensitive data in ' + + 'the resulting log.'); + } + } else { + validateBoolean(enableTrace, 'options.enableTrace'); + } + + if (tlsOptions.ALPNProtocols) + tls.convertALPNProtocols(tlsOptions.ALPNProtocols, tlsOptions); + + this._tlsOptions = tlsOptions; + this._secureEstablished = false; + this._securePending = false; + this._newSessionPending = false; + this._controlReleased = false; + this.secureConnecting = true; + this._SNICallback = null; + this[kALPNCallback] = null; + this.servername = null; + this.alpnProtocol = null; + this.authorized = false; + this.authorizationError = null; + this[kRes] = null; + this[kIsVerified] = false; + this[kPendingSession] = null; + + let wrap; + let handle; + let wrapHasActiveWriteFromPrevOwner; + + if (socket) { + if (socket instanceof net.Socket && socket._handle) { + // 1. connected socket + wrap = socket; + } else { + // 2. socket has no handle so it is js not c++ + // 3. unconnected sockets are wrapped + // TLS expects to interact from C++ with a net.Socket that has a C++ stream + // handle, but a JS stream doesn't have one. Wrap it up to make it look like + // a socket. + wrap = new JSStreamSocket(socket); + } + + handle = wrap._handle; + wrapHasActiveWriteFromPrevOwner = wrap.writableLength > 0; + } else { + // 4. no socket, one will be created with net.Socket().connect + wrap = null; + wrapHasActiveWriteFromPrevOwner = false; + } + + // Just a documented property to make secure sockets + // distinguishable from regular ones. + this.encrypted = true; + + ReflectApply(net.Socket, this, [{ + handle: this._wrapHandle(wrap, handle, wrapHasActiveWriteFromPrevOwner), + allowHalfOpen: socket ? socket.allowHalfOpen : tlsOptions.allowHalfOpen, + pauseOnCreate: tlsOptions.pauseOnConnect, + manualStart: true, + highWaterMark: tlsOptions.highWaterMark, + onread: !socket ? tlsOptions.onread : null, + signal: tlsOptions.signal, + }]); + + // Proxy for API compatibility + this.ssl = this._handle; // C++ TLSWrap object + + this.on('error', this._tlsError); + + this._init(socket, wrap); + + if (enableTrace && this._handle) + this._handle.enableTrace(); + + if (wrapHasActiveWriteFromPrevOwner) { + // `wrap` is a streams.Writable in JS. This empty write will be queued + // and hence finish after all existing writes, which is the timing + // we want to start to send any tls data to `wrap`. + wrap.write('', (err) => { + if (err) { + debug('error got before writing any tls data to the underlying stream'); + this.destroy(err); + return; + } + + this._handle.writesIssuedByPrevListenerDone(); + }); + } + + // Read on next tick so the caller has a chance to setup listeners + process.nextTick(initRead, this, socket); +} +ObjectSetPrototypeOf(TLSSocket.prototype, net.Socket.prototype); +ObjectSetPrototypeOf(TLSSocket, net.Socket); +exports.TLSSocket = TLSSocket; + +const proxiedMethods = [ + 'ref', 'unref', 'open', 'bind', 'listen', 'connect', 'bind6', + 'connect6', 'getsockname', 'getpeername', 'setNoDelay', 'setKeepAlive', + 'setSimultaneousAccepts', 'setBlocking', + + // PipeWrap + 'setPendingInstances', +]; + +// Proxy HandleWrap, PipeWrap and TCPWrap methods +function makeMethodProxy(name) { + return function methodProxy(...args) { + if (this._parent[name]) + return ReflectApply(this._parent[name], this._parent, args); + }; +} +for (const proxiedMethod of proxiedMethods) { + tls_wrap.TLSWrap.prototype[proxiedMethod] = + makeMethodProxy(proxiedMethod); +} + +tls_wrap.TLSWrap.prototype.close = function close(cb) { + let ssl; + if (this[owner_symbol]) { + ssl = this[owner_symbol].ssl; + this[owner_symbol].ssl = null; + } + + // Invoke `destroySSL` on close to clean up possibly pending write requests + // that may self-reference TLSWrap, leading to leak + const done = () => { + if (ssl) { + ssl.destroySSL(); + if (ssl._secureContext.singleUse) { + ssl._secureContext.context.close(); + ssl._secureContext.context = null; + } + } + if (cb) + cb(); + }; + + if (this._parentWrap) { + if (this._parentWrap._handle === null) { + // The socket handle was already closed. + done(); + return; + } + + if (this._parentWrap._handle === this._parent) { + this._parentWrap.once('close', done); + this._parentWrap.destroy(); + return; + } + } + + return this._parent.close(done); +}; + +TLSSocket.prototype.disableRenegotiation = function disableRenegotiation() { + this[kDisableRenegotiation] = true; +}; + +/** + * + * @param {null|net.Socket} wrap + * @param {null|object} handle + * @param {boolean} wrapHasActiveWriteFromPrevOwner + * @returns {object} + */ +TLSSocket.prototype._wrapHandle = function(wrap, handle, wrapHasActiveWriteFromPrevOwner) { + const options = this._tlsOptions; + if (!handle) { + handle = options.pipe ? + new Pipe(PipeConstants.SOCKET) : + new TCP(TCPConstants.SOCKET); + handle[owner_symbol] = this; + } + + // Wrap socket's handle + const context = options.secureContext || + options.credentials || + tls.createSecureContext(options); + assert(handle.isStreamBase, 'handle must be a StreamBase'); + if (!(context.context instanceof NativeSecureContext)) { + throw new ERR_TLS_INVALID_CONTEXT('context'); + } + + const res = tls_wrap.wrap(handle, context.context, + !!options.isServer, + wrapHasActiveWriteFromPrevOwner); + res._parent = handle; // C++ "wrap" object: TCPWrap, JSStream, ... + res._parentWrap = wrap; // JS object: net.Socket, JSStreamSocket, ... + res._secureContext = context; + res.reading = handle.reading; + this[kRes] = res; + defineHandleReading(this, handle); + + // Guard against adding multiple listeners, as this method may be called + // repeatedly on the same socket by reinitializeHandle + if (this.listenerCount('close', onSocketCloseDestroySSL) === 0) { + this.on('close', onSocketCloseDestroySSL); + } + + if (wrap) { + wrap.on('close', () => this.destroy()); + } + + return res; +}; + +TLSSocket.prototype[kReinitializeHandle] = function reinitializeHandle(handle) { + const originalServername = this.ssl ? this._handle.getServername() : null; + const originalSession = this.ssl ? this._handle.getSession() : null; + + this.handle = this._wrapHandle(null, handle, false); + this.ssl = this._handle; + + net.Socket.prototype[kReinitializeHandle].call(this, this.handle); + this._init(); + + if (this._tlsOptions.enableTrace) { + this._handle.enableTrace(); + } + + if (originalSession) { + this.setSession(originalSession); + } + + if (originalServername) { + this.setServername(originalServername); + } +}; + +// This eliminates a cyclic reference to TLSWrap +// Ref: https://github.com/nodejs/node/commit/f7620fb96d339f704932f9bb9a0dceb9952df2d4 +function defineHandleReading(socket, handle) { + ObjectDefineProperty(handle, 'reading', { + __proto__: null, + get: () => { + return socket[kRes].reading; + }, + set: (value) => { + socket[kRes].reading = value; + }, + }); +} + +function onSocketCloseDestroySSL() { + // Make sure we are not doing it on OpenSSL's stack + setImmediate(destroySSL, this); + this[kRes] = null; +} + +function destroySSL(self) { + self._destroySSL(); +} + +TLSSocket.prototype._destroySSL = function _destroySSL() { + if (!this.ssl) return; + this.ssl.destroySSL(); + if (this.ssl._secureContext.singleUse) { + this.ssl._secureContext.context.close(); + this.ssl._secureContext.context = null; + } + this.ssl = null; + this[kPendingSession] = null; + this[kIsVerified] = false; +}; + +function keylogNewListener(event) { + if (event !== 'keylog') + return; + + // Guard against enableKeylogCallback after destroy + if (!this._handle) return; + this._handle.enableKeylogCallback(); + + // Remove this listener since it's no longer needed. + this.removeListener('newListener', keylogNewListener); +} + +function newListener(event) { + if (event !== 'session') + return; + + // Guard against enableSessionCallbacks after destroy + if (!this._handle) return; + this._handle.enableSessionCallbacks(); + + // Remove this listener since it's no longer needed. + this.removeListener('newListener', newListener); +} + +// Constructor guts, arbitrarily factored out. +let warnOnTlsKeylog = true; +let warnOnTlsKeylogError = true; +TLSSocket.prototype._init = function(socket, wrap) { + const options = this._tlsOptions; + const ssl = this._handle; + this.server = options.server; + + debug('%s _init', + options.isServer ? 'server' : 'client', + 'handle?', !!ssl, + ); + + // Clients (!isServer) always request a cert, servers request a client cert + // only on explicit configuration. + const requestCert = !!options.requestCert || !options.isServer; + const rejectUnauthorized = !!options.rejectUnauthorized; + + this._requestCert = requestCert; + this._rejectUnauthorized = rejectUnauthorized; + if (requestCert || rejectUnauthorized) + ssl.setVerifyMode(requestCert, rejectUnauthorized); + + // Only call .onkeylog if there is a keylog listener. + ssl.onkeylog = onkeylog; + + if (this.listenerCount('newListener', keylogNewListener) === 0) { + this.on('newListener', keylogNewListener); + } + + if (options.isServer) { + ssl.onhandshakestart = onhandshakestart; + ssl.onhandshakedone = onhandshakedone; + ssl.onclienthello = loadSession; + ssl.oncertcb = loadSNI; + ssl.onnewsession = onnewsession; + ssl.lastHandshakeTime = 0; + ssl.handshakes = 0; + + if (options.ALPNCallback) { + validateFunction(options.ALPNCallback, 'options.ALPNCallback'); + this[kALPNCallback] = options.ALPNCallback; + ssl.ALPNCallback = callALPNCallback; + ssl.enableALPNCb(); + } + + if (this.server) { + if (this.server.listenerCount('resumeSession') > 0 || + this.server.listenerCount('newSession') > 0) { + // Also starts the client hello parser as a side effect. + ssl.enableSessionCallbacks(); + } + if (this.server.listenerCount('OCSPRequest') > 0) + ssl.enableCertCb(); + } + } else { + ssl.onhandshakestart = noop; + ssl.onhandshakedone = () => { + debug('client onhandshakedone'); + this._finishInit(); + }; + ssl.onocspresponse = onocspresponse; + + if (options.session) + ssl.setSession(options.session); + + ssl.onnewsession = onnewsessionclient; + + // Only call .onnewsession if there is a session listener. + if (this.listenerCount('newListener', newListener) === 0) { + this.on('newListener', newListener); + } + } + + if (tlsKeylog) { + if (warnOnTlsKeylog) { + warnOnTlsKeylog = false; + process.emitWarning('Using --tls-keylog makes TLS connections insecure ' + + 'by writing secret key material to file ' + tlsKeylog); + } + this.on('keylog', (line) => { + appendFile(tlsKeylog, line, { mode: 0o600 }, (err) => { + if (err && warnOnTlsKeylogError) { + warnOnTlsKeylogError = false; + process.emitWarning('Failed to write TLS keylog (this warning ' + + 'will not be repeated): ' + err); + } + }); + }); + } + + ssl.onerror = onerror; + + // If custom SNICallback was given, or if + // there're SNI contexts to perform match against - + // set `.onsniselect` callback. + if (options.isServer && + options.SNICallback && + (options.SNICallback !== SNICallback || + (options.server && options.server._contexts.length))) { + validateFunction(options.SNICallback, 'options.SNICallback'); + this._SNICallback = options.SNICallback; + ssl.enableCertCb(); + } + + if (options.ALPNProtocols) + ssl.setALPNProtocols(options.ALPNProtocols); + + if (options.pskCallback && ssl.enablePskCallback) { + validateFunction(options.pskCallback, 'pskCallback'); + + ssl[kOnPskExchange] = options.isServer ? + onPskServerCallback : onPskClientCallback; + + this[kPskCallback] = options.pskCallback; + ssl.enablePskCallback(); + + if (options.pskIdentityHint) { + validateString(options.pskIdentityHint, 'options.pskIdentityHint'); + ssl.setPskIdentityHint(options.pskIdentityHint); + } + } + + // We can only come here via [kWrapConnectedHandle]() call that happens + // if the connection is established with `autoSelectFamily` set to `true`. + const connectOptions = this[kConnectOptions]; + if (!options.isServer && connectOptions) { + if (connectOptions.servername) { + this.setServername(connectOptions.servername); + } + } + + if (options.handshakeTimeout > 0) + this.setTimeout(options.handshakeTimeout, this._handleTimeout); + + if (socket instanceof net.Socket) { + this._parent = socket; + + // To prevent assertion in afterConnect() and properly kick off readStart + this.connecting = socket.connecting || !socket._handle; + socket.once('connect', () => { + this.connecting = false; + this.emit('connect'); + }); + } + + // Assume `tls.connect()` + if (wrap) { + wrap.on('error', (err) => this._emitTLSError(err)); + } else { + assert(!socket); + this.connecting = true; + } +}; + +TLSSocket.prototype.renegotiate = function(options, callback) { + validateObject(options, 'options'); + if (callback !== undefined) { + validateFunction(callback, 'callback'); + } + + debug('%s renegotiate()', + this._tlsOptions.isServer ? 'server' : 'client', + 'destroyed?', this.destroyed, + ); + + if (this.destroyed) + return; + + let requestCert = !!this._requestCert; + let rejectUnauthorized = !!this._rejectUnauthorized; + + if (options.requestCert !== undefined) + requestCert = !!options.requestCert; + if (options.rejectUnauthorized !== undefined) + rejectUnauthorized = !!options.rejectUnauthorized; + + if (requestCert !== this._requestCert || + rejectUnauthorized !== this._rejectUnauthorized) { + this._handle.setVerifyMode(requestCert, rejectUnauthorized); + this._requestCert = requestCert; + this._rejectUnauthorized = rejectUnauthorized; + } + // Ensure that we'll cycle through internal openssl's state + this.write(''); + + try { + this._handle.renegotiate(); + } catch (err) { + if (callback) { + process.nextTick(callback, err); + } + return false; + } + + // Ensure that we'll cycle through internal openssl's state + this.write(''); + + if (callback) { + this.once('secure', () => callback(null)); + } + + return true; +}; + +TLSSocket.prototype.exportKeyingMaterial = function(length, label, context) { + validateUint32(length, 'length', true); + validateString(label, 'label'); + if (context !== undefined) + validateBuffer(context, 'context'); + + if (!this._secureEstablished) + throw new ERR_TLS_INVALID_STATE(); + + return this._handle.exportKeyingMaterial(length, label, context); +}; + +TLSSocket.prototype.setMaxSendFragment = function setMaxSendFragment(size) { + validateInt32(size, 'size'); + return this._handle.setMaxSendFragment(size) === 1; +}; + +TLSSocket.prototype._handleTimeout = function() { + this._emitTLSError(new ERR_TLS_HANDSHAKE_TIMEOUT()); +}; + +TLSSocket.prototype._emitTLSError = function(err) { + const e = this._tlsError(err); + if (e) + this.emit('error', e); +}; + +TLSSocket.prototype._tlsError = function(err) { + this.emit('_tlsError', err); + if (this._controlReleased) + return err; + return null; +}; + +TLSSocket.prototype._releaseControl = function() { + if (this._controlReleased) + return false; + this._controlReleased = true; + this.removeListener('error', this._tlsError); + return true; +}; + +TLSSocket.prototype._finishInit = function() { + // Guard against getting onhandshakedone() after .destroy(). + // * 1.2: If destroy() during onocspresponse(), then write of next handshake + // record fails, the handshake done info callbacks does not occur, and the + // socket closes. + // * 1.3: The OCSP response comes in the same record that finishes handshake, + // so even after .destroy(), the handshake done info callback occurs + // immediately after onocspresponse(). Ignore it. + if (!this._handle) + return; + + this.alpnProtocol = this._handle.getALPNNegotiatedProtocol(); + // The servername could be set by TLSWrap::SelectSNIContextCallback(). + if (this.servername === null) { + this.servername = this._handle.getServername(); + } + + debug('%s _finishInit', + this._tlsOptions.isServer ? 'server' : 'client', + 'handle?', !!this._handle, + 'alpn', this.alpnProtocol, + 'servername', this.servername); + + this._secureEstablished = true; + if (this._tlsOptions.handshakeTimeout > 0) + this.setTimeout(0, this._handleTimeout); + this.emit('secure'); +}; + +TLSSocket.prototype._start = function() { + debug('%s _start', + this._tlsOptions.isServer ? 'server' : 'client', + 'handle?', !!this._handle, + 'connecting?', this.connecting, + 'requestOCSP?', !!this._tlsOptions.requestOCSP, + ); + if (this.connecting) { + this.once('connect', this._start); + return; + } + + // Socket was destroyed before the connection was established + if (!this._handle) + return; + + if (this._tlsOptions.requestOCSP) + this._handle.requestOCSP(); + this._handle.start(); +}; + +TLSSocket.prototype.setServername = function(name) { + validateString(name, 'name'); + + if (this._tlsOptions.isServer) { + throw new ERR_TLS_SNI_FROM_SERVER(); + } + + this._handle.setServername(name); +}; + +TLSSocket.prototype.setSession = function(session) { + if (typeof session === 'string') + session = Buffer.from(session, 'latin1'); + this._handle.setSession(session); +}; + +TLSSocket.prototype.getPeerCertificate = function(detailed) { + if (this._handle) { + return common.translatePeerCertificate( + this._handle.getPeerCertificate(detailed)) || {}; + } + + return null; +}; + +TLSSocket.prototype.getCertificate = function() { + if (this._handle) { + // It's not a peer cert, but the formatting is identical. + return common.translatePeerCertificate( + this._handle.getCertificate()) || {}; + } + + return null; +}; + +TLSSocket.prototype.getPeerX509Certificate = function(detailed) { + const cert = this._handle?.getPeerX509Certificate(); + return cert ? new InternalX509Certificate(cert) : undefined; +}; + +TLSSocket.prototype.getX509Certificate = function() { + const cert = this._handle?.getX509Certificate(); + return cert ? new InternalX509Certificate(cert) : undefined; +}; + +TLSSocket.prototype.setKeyCert = function(context) { + if (this._handle) { + let secureContext; + if (context instanceof common.SecureContext) + secureContext = context; + else + secureContext = tls.createSecureContext(context); + this._handle.setKeyCert(secureContext.context); + } +}; + +// Proxy TLSSocket handle methods +function makeSocketMethodProxy(name) { + return function socketMethodProxy(...args) { + if (this._handle) + return ReflectApply(this._handle[name], this._handle, args); + return null; + }; +} + +[ + 'getCipher', + 'getSharedSigalgs', + 'getEphemeralKeyInfo', + 'getFinished', + 'getPeerFinished', + 'getProtocol', + 'getSession', + 'getTLSTicket', + 'isSessionReused', + 'enableTrace', +].forEach((method) => { + TLSSocket.prototype[method] = makeSocketMethodProxy(method); +}); + +// TODO: support anonymous (nocert) + + +function onServerSocketSecure() { + if (this._requestCert) { + const verifyError = this._handle.verifyError(); + if (verifyError) { + this.authorizationError = verifyError.code; + + if (this._rejectUnauthorized) + this.destroy(); + } else { + this.authorized = true; + } + } + + if (!this.destroyed && this._releaseControl()) { + debug('server emit secureConnection'); + this.secureConnecting = false; + this._tlsOptions.server.emit('secureConnection', this); + } +} + +function onSocketTLSError(err) { + if (!this._controlReleased && !this[kErrorEmitted]) { + this[kErrorEmitted] = true; + debug('server emit tlsClientError:', err); + this._tlsOptions.server.emit('tlsClientError', err, this); + } +} + +function onSocketKeylog(line) { + this._tlsOptions.server.emit('keylog', line, this); +} + +function onSocketClose(err) { + // Closed because of error - no need to emit it twice + if (err) + return; + + // Emit ECONNRESET + if (!this._controlReleased && !this[kErrorEmitted]) { + this[kErrorEmitted] = true; + const connReset = new ConnResetException('socket hang up'); + this._tlsOptions.server.emit('tlsClientError', connReset, this); + } +} + +function tlsConnectionListener(rawSocket) { + debug('net.Server.on(connection): new TLSSocket'); + const socket = new TLSSocket(rawSocket, { + secureContext: this._sharedCreds, + isServer: true, + server: this, + requestCert: this.requestCert, + rejectUnauthorized: this.rejectUnauthorized, + handshakeTimeout: this[kHandshakeTimeout], + ALPNProtocols: this.ALPNProtocols, + ALPNCallback: this.ALPNCallback, + SNICallback: this[kSNICallback] || SNICallback, + enableTrace: this[kEnableTrace], + pauseOnConnect: this.pauseOnConnect, + pskCallback: this[kPskCallback], + pskIdentityHint: this[kPskIdentityHint], + }); + + socket.on('secure', onServerSocketSecure); + + if (this.listenerCount('keylog') > 0) + socket.on('keylog', onSocketKeylog); + + socket[kErrorEmitted] = false; + socket.on('close', onSocketClose); + socket.on('_tlsError', onSocketTLSError); +} + +// AUTHENTICATION MODES +// +// There are several levels of authentication that TLS/SSL supports. +// Read more about this in "man SSL_set_verify". +// +// 1. The server sends a certificate to the client but does not request a +// cert from the client. This is common for most HTTPS servers. The browser +// can verify the identity of the server, but the server does not know who +// the client is. Authenticating the client is usually done over HTTP using +// login boxes and cookies and stuff. +// +// 2. The server sends a cert to the client and requests that the client +// also send it a cert. The client knows who the server is and the server is +// requesting the client also identify themselves. There are several +// outcomes: +// +// A) verifyError returns null meaning the client's certificate is signed +// by one of the server's CAs. The server now knows the client's identity +// and the client is authorized. +// +// B) For some reason the client's certificate is not acceptable - +// verifyError returns a string indicating the problem. The server can +// either (i) reject the client or (ii) allow the client to connect as an +// unauthorized connection. +// +// The mode is controlled by two boolean variables. +// +// requestCert +// If true the server requests a certificate from client connections. For +// the common HTTPS case, users will want this to be false, which is what +// it defaults to. +// +// rejectUnauthorized +// If true clients whose certificates are invalid for any reason will not +// be allowed to make connections. If false, they will simply be marked as +// unauthorized but secure communication will continue. By default this is +// true. +// +// +// +// Options: +// - requestCert. Send verify request. Default to false. +// - rejectUnauthorized. Boolean, default to true. +// - key. string. +// - cert: string. +// - clientCertEngine: string. +// - ca: string or array of strings. +// - sessionTimeout: integer. +// +// emit 'secureConnection' +// function (tlsSocket) { } +// +// "UNABLE_TO_GET_ISSUER_CERT", "UNABLE_TO_GET_CRL", +// "UNABLE_TO_DECRYPT_CERT_SIGNATURE", "UNABLE_TO_DECRYPT_CRL_SIGNATURE", +// "UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY", "CERT_SIGNATURE_FAILURE", +// "CRL_SIGNATURE_FAILURE", "CERT_NOT_YET_VALID" "CERT_HAS_EXPIRED", +// "CRL_NOT_YET_VALID", "CRL_HAS_EXPIRED" "ERROR_IN_CERT_NOT_BEFORE_FIELD", +// "ERROR_IN_CERT_NOT_AFTER_FIELD", "ERROR_IN_CRL_LAST_UPDATE_FIELD", +// "ERROR_IN_CRL_NEXT_UPDATE_FIELD", "OUT_OF_MEM", +// "DEPTH_ZERO_SELF_SIGNED_CERT", "SELF_SIGNED_CERT_IN_CHAIN", +// "UNABLE_TO_GET_ISSUER_CERT_LOCALLY", "UNABLE_TO_VERIFY_LEAF_SIGNATURE", +// "CERT_CHAIN_TOO_LONG", "CERT_REVOKED" "INVALID_CA", +// "PATH_LENGTH_EXCEEDED", "INVALID_PURPOSE" "CERT_UNTRUSTED", +// "CERT_REJECTED" +// +function Server(options, listener) { + if (!(this instanceof Server)) + return new Server(options, listener); + + if (typeof options === 'function') { + listener = options; + options = kEmptyObject; + } else if (options == null || typeof options === 'object') { + options ??= kEmptyObject; + } else { + throw new ERR_INVALID_ARG_TYPE('options', 'Object', options); + } + + this._contexts = []; + this.requestCert = options.requestCert === true; + this.rejectUnauthorized = options.rejectUnauthorized !== false; + + this.ALPNCallback = options.ALPNCallback; + if (this.ALPNCallback && options.ALPNProtocols) { + throw new ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS(); + } + + if (options.sessionTimeout) + this.sessionTimeout = options.sessionTimeout; + + if (options.ticketKeys) + this.ticketKeys = options.ticketKeys; + + if (options.ALPNProtocols) + tls.convertALPNProtocols(options.ALPNProtocols, this); + + this.setSecureContext(options); + + this[kHandshakeTimeout] = options.handshakeTimeout || (120 * 1000); + this[kSNICallback] = options.SNICallback; + this[kPskCallback] = options.pskCallback; + this[kPskIdentityHint] = options.pskIdentityHint; + + validateNumber(this[kHandshakeTimeout], 'options.handshakeTimeout'); + + if (this[kSNICallback]) { + validateFunction(this[kSNICallback], 'options.SNICallback'); + } + + if (this[kPskCallback]) { + validateFunction(this[kPskCallback], 'options.pskCallback'); + } + + if (this[kPskIdentityHint]) { + validateString(this[kPskIdentityHint], 'options.pskIdentityHint'); + } + + // constructor call + ReflectApply(net.Server, this, [options, tlsConnectionListener]); + + if (listener) { + this.on('secureConnection', listener); + } + + this[kEnableTrace] = options.enableTrace; +} + +ObjectSetPrototypeOf(Server.prototype, net.Server.prototype); +ObjectSetPrototypeOf(Server, net.Server); +exports.Server = Server; +exports.createServer = function createServer(options, listener) { + return new Server(options, listener); +}; + + +Server.prototype.setSecureContext = function(options) { + validateObject(options, 'options'); + + if (options.pfx) + this.pfx = options.pfx; + else + this.pfx = undefined; + + if (options.key) + this.key = options.key; + else + this.key = undefined; + + if (options.passphrase) + this.passphrase = options.passphrase; + else + this.passphrase = undefined; + + if (options.cert) + this.cert = options.cert; + else + this.cert = undefined; + + if (options.clientCertEngine) + this.clientCertEngine = options.clientCertEngine; + else + this.clientCertEngine = undefined; + + if (options.ca) + this.ca = options.ca; + else + this.ca = undefined; + + if (options.minVersion) + this.minVersion = options.minVersion; + else + this.minVersion = undefined; + + if (options.maxVersion) + this.maxVersion = options.maxVersion; + else + this.maxVersion = undefined; + + if (options.secureProtocol) + this.secureProtocol = options.secureProtocol; + else + this.secureProtocol = undefined; + + if (options.crl) + this.crl = options.crl; + else + this.crl = undefined; + + this.sigalgs = options.sigalgs; + + if (options.ciphers) + this.ciphers = options.ciphers; + else + this.ciphers = undefined; + + this.ecdhCurve = options.ecdhCurve; + + if (options.dhparam) + this.dhparam = options.dhparam; + else + this.dhparam = undefined; + + if (options.honorCipherOrder !== undefined) + this.honorCipherOrder = !!options.honorCipherOrder; + else + this.honorCipherOrder = true; + + const secureOptions = options.secureOptions || 0; + + if (secureOptions) + this.secureOptions = secureOptions; + else + this.secureOptions = undefined; + + if (options.sessionIdContext) { + this.sessionIdContext = options.sessionIdContext; + } else { + this.sessionIdContext = crypto.createHash('sha1') + .update(process.argv.join(' ')) + .digest('hex') + .slice(0, 32); + } + + if (options.sessionTimeout) + this.sessionTimeout = options.sessionTimeout; + + if (options.ticketKeys) + this.ticketKeys = options.ticketKeys; + + this.privateKeyIdentifier = options.privateKeyIdentifier; + this.privateKeyEngine = options.privateKeyEngine; + + this._sharedCreds = tls.createSecureContext({ + pfx: this.pfx, + key: this.key, + passphrase: this.passphrase, + cert: this.cert, + clientCertEngine: this.clientCertEngine, + ca: this.ca, + ciphers: this.ciphers, + sigalgs: this.sigalgs, + ecdhCurve: this.ecdhCurve, + dhparam: this.dhparam, + minVersion: this.minVersion, + maxVersion: this.maxVersion, + secureProtocol: this.secureProtocol, + secureOptions: this.secureOptions, + honorCipherOrder: this.honorCipherOrder, + crl: this.crl, + sessionIdContext: this.sessionIdContext, + ticketKeys: this.ticketKeys, + sessionTimeout: this.sessionTimeout, + privateKeyIdentifier: this.privateKeyIdentifier, + privateKeyEngine: this.privateKeyEngine, + }); +}; + + +Server.prototype._getServerData = function() { + return { + ticketKeys: this.getTicketKeys().toString('hex'), + }; +}; + + +Server.prototype._setServerData = function(data) { + this.setTicketKeys(Buffer.from(data.ticketKeys, 'hex')); +}; + + +Server.prototype.getTicketKeys = function getTicketKeys() { + return this._sharedCreds.context.getTicketKeys(); +}; + + +Server.prototype.setTicketKeys = function setTicketKeys(keys) { + validateBuffer(keys); + assert(keys.byteLength === 48, + 'Session ticket keys must be a 48-byte buffer'); + this._sharedCreds.context.setTicketKeys(keys); +}; + +// SNI Contexts High-Level API +Server.prototype.addContext = function(servername, context) { + if (!servername) { + throw new ERR_TLS_REQUIRED_SERVER_NAME(); + } + + const re = new RegExp(`^${ + servername + .replace(/([.^$+?\-\\[\]{}])/g, '\\$1') + .replaceAll('*', '[^.]*') + }$`); + + const secureContext = + context instanceof common.SecureContext ? context : tls.createSecureContext(context); + this._contexts.push([re, secureContext.context]); +}; + +Server.prototype[EE.captureRejectionSymbol] = function( + err, event, sock) { + + switch (event) { + case 'secureConnection': + sock.destroy(err); + break; + default: + ReflectApply(net.Server.prototype[SymbolFor('nodejs.rejection')], this, + [err, event, sock]); + } +}; + +function SNICallback(servername, callback) { + const contexts = this.server._contexts; + + for (let i = contexts.length - 1; i >= 0; --i) { + const elem = contexts[i]; + if (elem[0].test(servername)) { + callback(null, elem[1]); + return; + } + } + + callback(null, undefined); +} + + +// Target API: +// +// let s = tls.connect({port: 8000, host: "google.com"}, function() { +// if (!s.authorized) { +// s.destroy(); +// return; +// } +// +// // s.socket; +// +// s.end("hello world\n"); +// }); +// +// +function normalizeConnectArgs(listArgs) { + const args = net._normalizeArgs(listArgs); + const options = args[0]; + const cb = args[1]; + + // If args[0] was options, then normalize dealt with it. + // If args[0] is port, or args[0], args[1] is host, port, we need to + // find the options and merge them in, normalize's options has only + // the host/port/path args that it knows about, not the tls options. + // This means that options.host overrides a host arg. + if (listArgs[1] !== null && typeof listArgs[1] === 'object') { + ObjectAssign(options, listArgs[1]); + } else { + ObjectAssign(options, listArgs[2]); + } + + return cb ? [options, cb] : [options]; +} + +function onConnectSecure() { + const options = this[kConnectOptions]; + + // Check the size of DHE parameter above minimum requirement + // specified in options. + const ekeyinfo = this.getEphemeralKeyInfo(); + if (ekeyinfo.type === 'DH' && ekeyinfo.size < options.minDHSize) { + const err = new ERR_TLS_DH_PARAM_SIZE(ekeyinfo.size); + debug('client emit:', err); + this.emit('error', err); + this.destroy(); + return; + } + + let verifyError = this._handle.verifyError(); + + // Verify that server's identity matches it's certificate's names + // Unless server has resumed our existing session + if (!verifyError && !this.isSessionReused()) { + const hostname = options.servername || + options.host || + (options.socket?._host) || + 'localhost'; + const cert = this.getPeerCertificate(true); + verifyError = options.checkServerIdentity(hostname, cert); + } + + if (verifyError) { + this.authorized = false; + this.authorizationError = verifyError.code || verifyError.message; + + // rejectUnauthorized property can be explicitly defined as `undefined` + // causing the assignment to default value (`true`) fail. Before assigning + // it to the tlssock connection options, explicitly check if it is false + // and update rejectUnauthorized property. The property gets used by + // TLSSocket connection handler to allow or reject connection if + // unauthorized. + // This check is potentially redundant, however it is better to keep it + // in case the option object gets modified somewhere. + if (options.rejectUnauthorized !== false) { + this.destroy(verifyError); + return; + } + debug('client emit secureConnect. rejectUnauthorized: %s, ' + + 'authorizationError: %s', options.rejectUnauthorized, + this.authorizationError); + } else { + this.authorized = true; + debug('client emit secureConnect. authorized:', this.authorized); + } + this.secureConnecting = false; + this.emit('secureConnect'); + + this[kIsVerified] = true; + const session = this[kPendingSession]; + this[kPendingSession] = null; + if (session) + this.emit('session', session); + + this.removeListener('end', onConnectEnd); +} + +function onConnectEnd() { + // NOTE: This logic is shared with _http_client.js + if (!this._hadError) { + const options = this[kConnectOptions]; + this._hadError = true; + const error = new ConnResetException('Client network socket disconnected ' + + 'before secure TLS connection was ' + + 'established'); + error.path = options.path; + error.host = options.host; + error.port = options.port; + error.localAddress = options.localAddress; + this.destroy(error); + } +} + +// Arguments: [port,] [host,] [options,] [cb] +exports.connect = function connect(...args) { + args = normalizeConnectArgs(args); + let options = args[0]; + const cb = args[1]; + const allowUnauthorized = getAllowUnauthorized(); + + options = { + rejectUnauthorized: !allowUnauthorized, + ciphers: tls.DEFAULT_CIPHERS, + checkServerIdentity: tls.checkServerIdentity, + minDHSize: 1024, + ...options, + }; + + if (!options.keepAlive) + options.singleUse = true; + + validateFunction(options.checkServerIdentity, 'options.checkServerIdentity'); + validateNumber(options.minDHSize, 'options.minDHSize', 1); + + const context = options.secureContext || tls.createSecureContext(options); + + const tlssock = new TLSSocket(options.socket, { + allowHalfOpen: options.allowHalfOpen, + pipe: !!options.path, + secureContext: context, + isServer: false, + requestCert: true, + rejectUnauthorized: options.rejectUnauthorized !== false, + session: options.session, + ALPNProtocols: options.ALPNProtocols, + requestOCSP: options.requestOCSP, + enableTrace: options.enableTrace, + pskCallback: options.pskCallback, + highWaterMark: options.highWaterMark, + onread: options.onread, + signal: options.signal, + }); + + // rejectUnauthorized property can be explicitly defined as `undefined` + // causing the assignment to default value (`true`) fail. Before assigning + // it to the tlssock connection options, explicitly check if it is false + // and update rejectUnauthorized property. The property gets used by TLSSocket + // connection handler to allow or reject connection if unauthorized + options.rejectUnauthorized = options.rejectUnauthorized !== false; + + tlssock[kConnectOptions] = options; + + if (cb) + tlssock.once('secureConnect', cb); + + if (!options.socket) { + // If user provided the socket, it's their responsibility to manage its + // connectivity. If we created one internally, we connect it. + if (options.timeout) { + tlssock.setTimeout(options.timeout); + } + + tlssock.connect(options, tlssock._start); + } + + tlssock._releaseControl(); + + if (options.session) + tlssock.setSession(options.session); + + if (options.servername) { + if (!ipServernameWarned && net.isIP(options.servername)) { + process.emitWarning( + 'Setting the TLS ServerName to an IP address is not permitted by ' + + 'RFC 6066. This will be ignored in a future version.', + 'DeprecationWarning', + 'DEP0123', + ); + ipServernameWarned = true; + } + tlssock.setServername(options.servername); + } + + if (options.socket) + tlssock._start(); + + tlssock.on('secure', onConnectSecure); + tlssock.prependListener('end', onConnectEnd); + + return tlssock; +}; diff --git a/lib/tls.js b/lib/tls.js index 91ea8ad65a395c..88793f19ef7698 100644 --- a/lib/tls.js +++ b/lib/tls.js @@ -55,8 +55,8 @@ const { } = internalBinding('crypto'); const { Buffer } = require('buffer'); const { canonicalizeIP } = internalBinding('cares_wrap'); -const _tls_common = require('_tls_common'); -const _tls_wrap = require('_tls_wrap'); +const tlsCommon = require('internal/tls/common'); +const tlsWrap = require('internal/tls/wrap'); const { validateString } = require('internal/validators'); // Allow {CLIENT_RENEG_LIMIT} client-initiated session renegotiations @@ -386,9 +386,9 @@ exports.checkServerIdentity = function checkServerIdentity(hostname, cert) { } }; -exports.createSecureContext = _tls_common.createSecureContext; -exports.SecureContext = _tls_common.SecureContext; -exports.TLSSocket = _tls_wrap.TLSSocket; -exports.Server = _tls_wrap.Server; -exports.createServer = _tls_wrap.createServer; -exports.connect = _tls_wrap.connect; +exports.createSecureContext = tlsCommon.createSecureContext; +exports.SecureContext = tlsCommon.SecureContext; +exports.TLSSocket = tlsWrap.TLSSocket; +exports.Server = tlsWrap.Server; +exports.createServer = tlsWrap.createServer; +exports.connect = tlsWrap.connect; diff --git a/src/node_builtins.cc b/src/node_builtins.cc index 48b2debfbe9eac..253ecaad12dace 100644 --- a/src/node_builtins.cc +++ b/src/node_builtins.cc @@ -127,9 +127,10 @@ BuiltinLoader::BuiltinCategories BuiltinLoader::GetBuiltinCategories() const { #if !HAVE_OPENSSL "crypto", "crypto/promises", "https", "http2", "tls", "_tls_common", - "_tls_wrap", "internal/tls/parse-cert-string", - "internal/tls/secure-context", "internal/http2/core", - "internal/http2/compat", "internal/streams/lazy_transform", + "_tls_wrap", "internal/tls/parse-cert-string", "internal/tls/common", + "internal/tls/wrap", "internal/tls/secure-context", + "internal/http2/core", "internal/http2/compat", + "internal/streams/lazy_transform", #endif // !HAVE_OPENSSL #if !NODE_OPENSSL_HAS_QUIC "internal/quic/quic", "internal/quic/symbols", "internal/quic/stats", diff --git a/test/parallel/test-tls-translate-peer-certificate.js b/test/parallel/test-tls-translate-peer-certificate.js index de11567b138b15..0c5f63a6ae43a9 100644 --- a/test/parallel/test-tls-translate-peer-certificate.js +++ b/test/parallel/test-tls-translate-peer-certificate.js @@ -1,3 +1,4 @@ +// Flags: --expose-internals --no-warnings /* eslint-disable no-proto */ 'use strict'; const common = require('../common'); @@ -6,7 +7,7 @@ if (!common.hasCrypto) common.skip('missing crypto'); const { strictEqual, deepStrictEqual } = require('assert'); -const { translatePeerCertificate } = require('_tls_common'); +const { translatePeerCertificate } = require('internal/tls/common'); const certString = '__proto__=42\nA=1\nB=2\nC=3'; diff --git a/test/parallel/test-warn-tls-common-deprecation.js b/test/parallel/test-warn-tls-common-deprecation.js new file mode 100644 index 00000000000000..9d5d51c5f3d8d4 --- /dev/null +++ b/test/parallel/test-warn-tls-common-deprecation.js @@ -0,0 +1,11 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) common.skip('missing crypto'); + +// _tls_common is deprecated. + +common.expectWarning('DeprecationWarning', + 'The _tls_common module is deprecated.', 'DEP0192'); + +require('_tls_common'); diff --git a/test/parallel/test-warn-tls-wrap-deprecation.js b/test/parallel/test-warn-tls-wrap-deprecation.js new file mode 100644 index 00000000000000..318b3fc738317a --- /dev/null +++ b/test/parallel/test-warn-tls-wrap-deprecation.js @@ -0,0 +1,11 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) common.skip('missing crypto'); + +// _tls_wrap is deprecated. + +common.expectWarning('DeprecationWarning', + 'The _tls_wrap module is deprecated.', 'DEP0192'); + +require('_tls_wrap');