@@ -193,6 +193,7 @@ Local<FunctionTemplate> DTLSSession::GetConstructorTemplate(Environment* env) {
193193 SetProtoMethod (isolate, tmpl, " getSession" , GetSession);
194194 SetProtoMethod (isolate, tmpl, " wasReused" , WasReused);
195195 SetProtoMethod (isolate, tmpl, " getVerifyError" , GetVerifyError);
196+ SetProtoMethod (isolate, tmpl, " start" , DoStart);
196197
197198 env->set_dtls_session_constructor_template (tmpl);
198199 }
@@ -229,6 +230,7 @@ void DTLSSession::RegisterExternalReferences(
229230 registry->Register (GetVerifyError);
230231 registry->Register (GetSession);
231232 registry->Register (WasReused);
233+ registry->Register (DoStart);
232234}
233235
234236BaseObjectPtr<DTLSSession> DTLSSession::Create (
@@ -280,13 +282,17 @@ BaseObjectPtr<DTLSSession> DTLSSession::Create(
280282 SSL_set_connect_state (ssl.get ());
281283
282284 // Offer a previous session for resumption. Like SNI this has to happen
283- // before Cycle () emits the ClientHello, since the session id and ticket
285+ // before Start () emits the ClientHello, since the session id and ticket
284286 // ride in it.
285287 //
286288 // A session that OpenSSL rejects is not an error: it falls back to a full
287289 // handshake, which is what an expired or unknown ticket should do. Only a
288290 // blob that will not parse is worth reporting, and that is the caller
289291 // handing over something that is not a session at all.
292+ //
293+ // This also copies the recorded verification result onto the connection,
294+ // where a resumed handshake leaves it: PeerVerificationPassed() is what
295+ // stops it standing in for verification that never ran.
290296 if (resume.len > 0 ) {
291297 const unsigned char * p = resume.data ;
292298 ncrypto::SSLSessionPointer sess (d2i_SSL_SESSION (nullptr , &p, resume.len ));
@@ -300,10 +306,10 @@ BaseObjectPtr<DTLSSession> DTLSSession::Create(
300306 }
301307
302308 // Configure SNI and peer identity verification BEFORE the handshake
303- // starts. The caller (DTLSEndpoint::Connect) runs Cycle() immediately
304- // after Create() returns, which emits the ClientHello, so anything that
305- // must appear in that flight (SNI) has to be set here rather than via a
306- // post-construction setter.
309+ // starts. Start() emits the ClientHello and is called as soon as the
310+ // JavaScript wrapper is built, so anything that must appear in that
311+ // flight (SNI) has to be set here rather than via a post-construction
312+ // setter.
307313 if (servername != nullptr && servername[0 ] != ' \0 ' ) {
308314 if (!SSL_set_tlsext_host_name (ssl.get (), servername)) {
309315 THROW_ERR_CRYPTO_OPERATION_FAILED (env,
@@ -412,6 +418,16 @@ void DTLSSession::Receive(const uint8_t* data, size_t len) {
412418 Cycle ();
413419}
414420
421+ void DTLSSession::Start () {
422+ // A datagram can arrive for a server session between the new-session emit
423+ // and this call, and Receive() runs the pump. Starting again after that is
424+ // harmless but pointless, and a start() reaching the binding twice should
425+ // not re-enter the handshake.
426+ if (started_ || destroyed_) return ;
427+ started_ = true ;
428+ Cycle ();
429+ }
430+
415431void DTLSSession::Cycle () {
416432 if (destroyed_) return ;
417433
@@ -484,6 +500,19 @@ void DTLSSession::CycleInner() {
484500
485501 // Check if handshake just completed.
486502 if (SSL_is_init_finished (ssl_.get ()) && !handshake_complete_) {
503+ // A resumed handshake carries no Certificate message, so OpenSSL runs
504+ // no verification and the verify mode has nothing to abort on. What it
505+ // does instead is restore the result recorded when the session was
506+ // first established -- SSL_set_session() copies verify_result straight
507+ // onto the connection, and both it and the peer certificate round-trip
508+ // through the DER blob. A session authenticated under
509+ // rejectUnauthorized: false therefore arrives here carrying its
510+ // original failure alongside a handshake that succeeded, and nothing
511+ // downstream re-checks it: a caller that asked for a verified peer
512+ // would be handed one that never verified. node:tls checks
513+ // verifyError() after a resumption for this reason.
514+ if (!PeerVerificationPassed ()) return ;
515+
487516 handshake_complete_ = true ;
488517 state_->handshaking = 0 ;
489518 state_->open = 1 ;
@@ -625,6 +654,74 @@ bool DTLSSession::HandshakeDeadlineExpired() const {
625654 return uv_hrtime () / 1000000 >= handshake_deadline_;
626655}
627656
657+ long DTLSSession::PeerVerifyResult () const { // NOLINT(runtime/int)
658+ // SSL_get_verify_result() reports X509_V_OK when the peer sent no
659+ // certificate at all, because there was nothing to find fault with. Route
660+ // through ncrypto, which reports std::nullopt for that case (allowing for
661+ // PSK, where the identity was authenticated by the key instead) so it can
662+ // be distinguished from a certificate that actually verified.
663+ return ssl_.verifyPeerCertificate ().value_or (
664+ X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT );
665+ }
666+
667+ bool DTLSSession::PeerVerificationPassed () {
668+ // Consulted on every completed handshake rather than only a resumed one. It
669+ // costs nothing where OpenSSL already enforced -- there the handshake
670+ // failed and never reached here -- and it also covers a resumption the
671+ // server refused, where the result restored by SSL_set_session() stays on
672+ // the connection unless the full handshake that replaced it verified
673+ // something of its own.
674+ //
675+ // Whether a failure is fatal is decided by the verify mode OpenSSL itself
676+ // was given, read back off the SSL rather than tracked beside it, so the
677+ // two cannot disagree -- including after an SNI callback swapped the
678+ // SSL_CTX out from under this connection.
679+ //
680+ // A client aborts on a bad chain whenever the mode is not SSL_VERIFY_NONE.
681+ // That is the test OpenSSL itself applies when processing the server's
682+ // certificate -- it validates "if any flag is set", not only for
683+ // SSL_VERIFY_PEER -- and it is the whole of what rejectUnauthorized selects
684+ // between.
685+ //
686+ // A server sets SSL_VERIFY_PEER for requestCert and adds
687+ // SSL_VERIFY_FAIL_IF_NO_PEER_CERT only when rejectUnauthorized is set too:
688+ // requestCert on its own installs a permissive verify callback precisely so
689+ // the application can judge the certificate itself, so it is the added flag
690+ // and not SSL_VERIFY_PEER that marks a failure fatal there.
691+ int mode = SSL_get_verify_mode (ssl_.get ());
692+ bool fatal = is_server_ ? (mode & SSL_VERIFY_FAIL_IF_NO_PEER_CERT ) != 0
693+ : mode != SSL_VERIFY_NONE ;
694+ if (!fatal) return true ;
695+
696+ // Exactly the result session.authorized reports, so what is refused here
697+ // and what reads as unauthorized there cannot drift apart. It is also the
698+ // predicate node:tls applies -- TLSWrap::VerifyError() is the same
699+ // verifyPeerCertificate().value_or() -- so a peer authenticated by a PSK
700+ // rather than a certificate passes on the strength of the cipher's auth
701+ // method, as it does there.
702+ //
703+ // One difference from node:tls, which additionally requires a peer
704+ // certificate because ncrypto reports X509_V_OK for a TLS 1.3 resumption
705+ // without one: a DTLS 1.2 resumption restores the peer certificate along
706+ // with the result, so there is a real result to read here. Enabling DTLS
707+ // 1.3, once the bundled OpenSSL offers it, means revisiting this.
708+ long verify_error = PeerVerifyResult (); // NOLINT(runtime/int)
709+ if (verify_error == X509_V_OK ) return true ;
710+
711+ // enc_out_ is deliberately not flushed, unlike the alert path in
712+ // CycleInner(): there is no alert queued to get out, and what is queued is
713+ // this side's own Finished. The peer is left to time out, as it is for
714+ // every other handshake failure that OpenSSL did not itself alert on.
715+ std::string message = " Peer certificate verification failed: " ;
716+ message += ncrypto::X509Pointer::ErrorCode (verify_error);
717+ Local<Value> str;
718+ if (ToV8Value (env ()->context (), message).ToLocal (&str)) {
719+ Local<Value> argv[] = {str};
720+ EmitCallback (DTLS_CB_SESSION_ERROR , 1 , argv);
721+ }
722+ return false ;
723+ }
724+
628725void DTLSSession::EmitHandshakeTimeout () {
629726 HandleScope hs (env ()->isolate ());
630727 Context::Scope cs (env ()->context ());
@@ -861,6 +958,12 @@ MaybeLocal<Value> DTLSSession::EmitCallback(int cb_index,
861958
862959// --- JS binding methods ---
863960
961+ void DTLSSession::DoStart (const FunctionCallbackInfo<Value>& args) {
962+ DTLSSession* session;
963+ ASSIGN_OR_RETURN_UNWRAP (&session, args.This ());
964+ session->Start ();
965+ }
966+
864967void DTLSSession::DoSend (const FunctionCallbackInfo<Value>& args) {
865968 DTLSSession* session;
866969 ASSIGN_OR_RETURN_UNWRAP (&session, args.This ());
@@ -1114,14 +1217,7 @@ void DTLSSession::GetVerifyError(const FunctionCallbackInfo<Value>& args) {
11141217 return ;
11151218 }
11161219
1117- // SSL_get_verify_result() reports X509_V_OK when the peer sent no
1118- // certificate at all, because there was nothing to find fault with. Route
1119- // through ncrypto, which reports std::nullopt for that case (allowing for
1120- // PSK and resumption, where the absence is legitimate) so it can be
1121- // distinguished from a certificate that actually verified.
1122- long verify_error = // NOLINT(runtime/int)
1123- session->ssl_ .verifyPeerCertificate ().value_or (
1124- X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT );
1220+ long verify_error = session->PeerVerifyResult (); // NOLINT(runtime/int)
11251221
11261222 // undefined means authorized; anything else is the short error code, e.g.
11271223 // "UNABLE_TO_GET_ISSUER_CERT" or "CERT_HAS_EXPIRED".
0 commit comments