// Verified client transport. Custom BIO never changes process-wide signal policy. struct SocketTLS { SSL_CTX* context=nullptr; SSL* ssl=nullptr; int fd=-1; std::function releaseResources; ~SocketTLS() { SSL_free(ssl); SSL_CTX_free(context); } static int create(BIO* bio) { BIO_set_init(bio,1); return 1; } static int destroy(BIO*) { return 1; } // Socket owns the descriptor. static int read(BIO* bio,char* bytes,int size) { BIO_clear_retry_flags(bio); int fd=*static_cast(BIO_get_data(bio)); auto n=::recv(fd,bytes,static_cast(size),0); if(n<0 && (errno==EINTR || errno==EAGAIN || errno==EWOULDBLOCK)) BIO_set_retry_read(bio); return static_cast(n); } static int write(BIO* bio,const char* bytes,int size) { BIO_clear_retry_flags(bio); int fd=*static_cast(BIO_get_data(bio)); auto n=::send(fd,bytes,static_cast(size),MSG_NOSIGNAL); if(n<0 && (errno==EINTR || errno==EAGAIN || errno==EWOULDBLOCK)) BIO_set_retry_write(bio); return static_cast(n); } static long control(BIO*,int command,long,void*) { return command==BIO_CTRL_FLUSH ? 1 : 0; } static BIO_METHOD* method() { struct Method { BIO_METHOD* value=BIO_meth_new(BIO_TYPE_SOURCE_SINK|BIO_get_new_index(),"Vanilla nonblocking socket"); Method() { if(value && !(BIO_meth_set_create(value,create) && BIO_meth_set_destroy(value,destroy) && BIO_meth_set_read(value,read) && BIO_meth_set_write(value,write) && BIO_meth_set_ctrl(value,control))) { BIO_meth_free(value); value=nullptr; } } ~Method() { BIO_meth_free(value); } }; static Method method; return method.value; } [[noreturn]] void error(const Source& s,const char* message,int systemError=errno) { bool memory=false; while(auto e=ERR_get_error()) if(ERR_GET_REASON(e)==ERR_R_MALLOC_FAILURE) memory=true; if(memory || systemError==ENOMEM || systemError==ENOBUFS || systemError==EMFILE || systemError==ENFILE) { releaseResources(); // Free descriptors before constructing diagnostics. failLimit(s,"socket TLS resource limit"); } fail(s,message); } // Retry within this call only: caller invalidates the handle on any exception. int perform(int operation,void* bytes,int size,SocketClock::time_point deadline,const Source& s) { for(;;) { socketDeadline(deadline,s); ERR_clear_error(); errno=0; int n=operation==0 ? SSL_connect(ssl) : operation==1 ? SSL_read(ssl,bytes,size) : SSL_write(ssl,bytes,size); int systemError=errno; if(n>0) return n; int reason=SSL_get_error(ssl,n); if(reason==SSL_ERROR_ZERO_RETURN && operation==1) return 0; if(reason==SSL_ERROR_WANT_READ || reason==SSL_ERROR_WANT_WRITE) { // First attempt always precedes poll, consuming SSL_pending data. socketWait(fd,reason==SSL_ERROR_WANT_READ ? POLLIN : POLLOUT,deadline,s); continue; } error(s,operation==0 ? "socket TLS handshake or certificate verification failed" : "socket TLS transport failed (possible truncated stream)",systemError); } } void connect(int descriptor,const std::string& host,const Options& options,SocketClock::time_point deadline,const Source& s) { fd=descriptor; ERR_clear_error(); context=SSL_CTX_new(TLS_client_method()); if(!context) error(s,"socket TLS context failed"); SSL_CTX_set_verify(context,SSL_VERIFY_PEER,nullptr); SSL_CTX_set_verify_depth(context,8); SSL_CTX_set_options(context,SSL_OP_NO_COMPRESSION|SSL_OP_NO_RENEGOTIATION); SSL_CTX_set_session_cache_mode(context,SSL_SESS_CACHE_OFF); SSL_CTX_set_max_cert_list(context,128*1024); if(!SSL_CTX_set_min_proto_version(context,TLS1_2_VERSION)) error(s,"socket TLS version configuration failed"); int trusted=options.tlsCAFile.empty() ? SSL_CTX_set_default_verify_paths(context) : SSL_CTX_load_verify_locations(context,options.tlsCAFile.c_str(),nullptr); if(trusted!=1) error(s,"socket TLS trust configuration failed"); ssl=SSL_new(context); if(!ssl) error(s,"socket TLS allocation failed"); SSL_set_mode(ssl,SSL_MODE_ENABLE_PARTIAL_WRITE); SSL_set_hostflags(ssl,X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS); if(socketNumeric(host,1).length) { if(X509_VERIFY_PARAM_set1_ip_asc(SSL_get0_param(ssl),host.c_str())!=1) error(s,"socket TLS IP configuration failed"); } else if(SSL_set1_host(ssl,host.c_str())!=1 || SSL_set_tlsext_host_name(ssl,host.c_str())!=1) error(s,"socket TLS name configuration failed"); auto* m=method(); if(!m) error(s,"socket TLS BIO allocation failed"); BIO* bio=BIO_new(m); if(!bio) error(s,"socket TLS BIO allocation failed"); BIO_set_data(bio,&fd); SSL_set_bio(ssl,bio,bio); perform(0,nullptr,0,deadline,s); socketDeadline(deadline,s); if(SSL_get_verify_result(ssl)!=X509_V_OK || !SSL_get0_peer_certificate(ssl)) error(s,"socket TLS certificate verification failed"); } // One library call only. Owned caller storage survives every WANT retry. int step(int operation,void* bytes,int size,short& want,const Source& s) { ERR_clear_error(); errno=0; int n=operation==3 ? SSL_accept(ssl) : operation==1 ? SSL_read(ssl,bytes,size) : SSL_write(ssl,bytes,size); int systemError=errno; if(n>0) { want=0; return n; } int reason=SSL_get_error(ssl,n); if(reason==SSL_ERROR_ZERO_RETURN && operation==1) { want=0; return 0; } if(reason==SSL_ERROR_WANT_READ || reason==SSL_ERROR_WANT_WRITE) { want=reason==SSL_ERROR_WANT_READ ? POLLIN : POLLOUT; return -1; } error(s,operation==3 ? "socket TLS handshake failed" : "socket TLS transport failed (possible truncated stream)",systemError); } static int noPassword(char*,int,int,void*) { return 0; } void serverContext(const Options& options,const Source& s) { ERR_clear_error(); errno=0; context=SSL_CTX_new(TLS_server_method()); if(!context) error(s,"socket TLS server context failed"); SSL_CTX_set_default_passwd_cb(context,noPassword); SSL_CTX_set_options(context,SSL_OP_NO_COMPRESSION|SSL_OP_NO_RENEGOTIATION); SSL_CTX_set_session_cache_mode(context,SSL_SESS_CACHE_OFF); SSL_CTX_set_max_cert_list(context,128*1024); if(!SSL_CTX_set_min_proto_version(context,TLS1_2_VERSION) || SSL_CTX_use_certificate_chain_file(context,options.tlsCertFile.c_str())!=1 || SSL_CTX_use_PrivateKey_file(context,options.tlsKeyFile.c_str(),SSL_FILETYPE_PEM)!=1 || SSL_CTX_check_private_key(context)!=1) error(s,"socket TLS server certificate/key configuration failed"); } void serverConnection(int descriptor,SSL_CTX* parent,const Source& s) { fd=descriptor; ERR_clear_error(); errno=0; ssl=SSL_new(parent); // SSL owns a context reference, independent of listener. if(!ssl) error(s,"socket TLS allocation failed"); SSL_set_mode(ssl,SSL_MODE_ENABLE_PARTIAL_WRITE); auto* m=method(); if(!m) error(s,"socket TLS BIO allocation failed"); BIO* bio=BIO_new(m); if(!bio) error(s,"socket TLS BIO allocation failed"); BIO_set_data(bio,&fd); SSL_set_bio(ssl,bio,bio); SSL_set_accept_state(ssl); } void close() noexcept { if(ssl) { ERR_clear_error(); (void)SSL_shutdown(ssl); ERR_clear_error(); } } };