From: Alex Rousskov Date: Thu, 22 Oct 2015 18:34:42 +0000 (-0600) Subject: Fetch missing certificates. X-Git-Tag: SQUID_4_0_13~5^2~26 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=55369ae649646901d3038c63217386174d01eb7b;p=thirdparty%2Fsquid.git Fetch missing certificates. Many web servers do not have complete certificate chains. Many browsers use certificate extensions of the server certificate and download the missing intermediate certificates automatically from the Internet. This patch adds a similar feature to Squid: - Parse Server Hello messages and extract certificates chain. - Check whether the issuers of each certificate exist in the chain. - If not, retrieve the issuer certificate URI from Authority Info extension of the certificate (if it is provided) and download the certificate. - Store downloaded certificates in Squid object cache, just like any other HTTP object. Implementation highlights: - A new Downloader class allows Squid subsystems to download objects via HTTP. These downloads are not backed by a proxy user. - Add support for an internal database of intermediate pre-loaded certificates to be used to complete incomplete chains. - Ssl::HandshakeParser parses TLS records and TLS Handshake messages. - Ssl::PeerConnector now uses the Downloader objects to download missing certificates. --- diff --git a/src/Downloader.cc b/src/Downloader.cc new file mode 100644 index 0000000000..2571683788 --- /dev/null +++ b/src/Downloader.cc @@ -0,0 +1,221 @@ +#include "squid.h" +#include "client_side.h" +#include "client_side_request.h" +#include "client_side_reply.h" +#include "Downloader.h" +#include "http/one/RequestParser.h" + +CBDATA_CLASS_INIT(Downloader); + +Downloader::Downloader(SBuf &url, const MasterXaction::Pointer &xact, AsyncCall::Pointer &aCallback): + AsyncJob("Downloader"), + ConnStateData(xact), + url_(url), + callback(aCallback), + status(Http::scNone) +{ + maxObjectSize = 512*1024; +} + +Downloader::~Downloader() +{ + debugs(33 , 2, "Downloader Finished"); +} + +void +Downloader::callException(const std::exception &e) +{ + debugs(33 , 2, "Downloader caught:" << e.what()); + AsyncJob::callException(e); +} + +bool +Downloader::doneAll() const +{ + return (!callback || callback->canceled()) && AsyncJob::doneAll(); +} + +void +Downloader::start() +{ + BodyProducer::start(); + HttpControlMsgSink::start(); + if (ClientSocketContext *context = parseOneRequest()) { + context->registerWithConn(); + processParsedRequest(context); + + /**/ + if (context->flags.deferred) { + if (context != context->http->getConn()->getCurrentContext().getRaw()) + context->deferRecipientForLater(context->deferredparams.node, context->deferredparams.rep, context->deferredparams.queuedBuffer); + else + context->http->getConn()->handleReply(context->deferredparams.rep, context->deferredparams.queuedBuffer); + } + /**/ + + } + +} + +void +Downloader::noteMoreBodySpaceAvailable(BodyPipe::Pointer) +{ + // This method required only if we need to support uploading data to server + // Currently only GET requests are supported + assert(0); +} + +void +Downloader::noteBodyConsumerAborted(BodyPipe::Pointer) +{ + // This method required only if we need to support uploading data to server + // Currently only GET requests are supported + assert(0); +} + +ClientSocketContext * +Downloader::parseOneRequest() +{ + const HttpRequestMethod method = Http::METHOD_GET; + + char *uri = strdup(url_.c_str()); + HttpRequest *const request = HttpRequest::CreateFromUrlAndMethod(uri, method); + if (!request) { + debugs(33, 5, "Invalid FTP URL: " << uri); + safe_free(uri); + return NULL; //earlyError(...) + } + request->http_ver = Http::ProtocolVersion(); + request->header.putStr(Http::HdrType::HOST, request->url.host()); + request->header.putTime(Http::HdrType::DATE, squid_curtime); + + ClientHttpRequest *const http = new ClientHttpRequest(this); + http->request = request; + HTTPMSGLOCK(http->request); + http->req_sz = 0; + http->uri = uri; + + ClientSocketContext *const context = new ClientSocketContext(NULL, http); + StoreIOBuffer tempBuffer; + tempBuffer.data = context->reqbuf; + tempBuffer.length = HTTP_REQBUF_SZ; + + ClientStreamData newServer = new clientReplyContext(http); + ClientStreamData newClient = context; + clientStreamInit(&http->client_stream, clientGetMoreData, clientReplyDetach, + clientReplyStatus, newServer, clientSocketRecipient, + clientSocketDetach, newClient, tempBuffer); + + context->flags.parsed_ok = 1; + return context; +} + +void +Downloader::processParsedRequest(ClientSocketContext *context) +{ + Must(context != NULL); + Must(getConcurrentRequestCount() == 1); + + ClientHttpRequest *const http = context->http; + assert(http != NULL); + + debugs(33, 4, "forwarding request to server side"); + assert(http->storeEntry() == NULL); + clientProcessRequest(this, Http1::RequestParserPointer(), context); +} + +time_t +Downloader::idleTimeout() const +{ + // No need to be implemented for connection-less ConnStateData object. + assert(0); + return 0; +} + +void +Downloader::writeControlMsgAndCall(ClientSocketContext *context, HttpReply *rep, AsyncCall::Pointer &call) +{ +} + +void +Downloader::handleReply(HttpReply *reply, StoreIOBuffer receivedData) +{ + bool existingContent = reply ? reply->content_length : 0; + bool exceedSize = (getCurrentContext()->startOfOutput() && existingContent > -1 && (size_t)existingContent > maxObjectSize) || + ((object.length() + receivedData.length) > maxObjectSize); + + if (exceedSize) { + status = Http::scInternalServerError; + callBack(); + return; + } + + debugs(33, 4, "Received " << receivedData.length << + " object data, offset: " << receivedData.offset << + " error flag:" << receivedData.flags.error); + + if (receivedData.length > 0) { + object.append(receivedData.data, receivedData.length); + getCurrentContext()->http->out.size += receivedData.length; + getCurrentContext()->noteSentBodyBytes(receivedData.length); + } + + switch (getCurrentContext()->socketState()) { + case STREAM_NONE: + debugs(33, 3, "Get more data"); + getCurrentContext()->pullData(); + break; + case STREAM_COMPLETE: + debugs(33, 3, "Object data transfer successfully complete"); + status = Http::scOkay; + callBack(); + break; + case STREAM_UNPLANNED_COMPLETE: + debugs(33, 3, "Object data transfer failed: STREAM_UNPLANNED_COMPLETE"); + status = Http::scInternalServerError; + callBack(); + break; + case STREAM_FAILED: + debugs(33, 3, "Object data transfer failed: STREAM_FAILED"); + status = Http::scInternalServerError; + callBack(); + break; + default: + fatal("unreachable code"); + } +} + +void +Downloader::downloadFinished() +{ + debugs(33, 3, "fake call, to just delete the Downloader"); + + // Not really needed. Squid will delete this object because "doneAll" is true. + //deleteThis("completed"); +} + +void +Downloader::callBack() +{ + CbDialer *dialer = dynamic_cast(callback->getDialer()); + Must(dialer); + dialer->status = status; + if (status == Http::scOkay) + dialer->object = object; + ScheduleCallHere(callback); + callback = NULL; + // Calling deleteThis method here to finish Downloader + // may result to squid crash. + // This method called by handleReply method which maybe called + // by ClientHttpRequest::doCallouts. The doCallouts after this object deleted + // may operate on non valid objects. + // Schedule a fake call here just to force squid to delete this object + CallJobHere(33, 7, CbcPointer(this), Downloader, downloadFinished); +} + +bool +Downloader::isOpen() const +{ + return cbdataReferenceValid(this) && // XXX: checking "this" in a method + callback != NULL; +} diff --git a/src/Downloader.h b/src/Downloader.h new file mode 100644 index 0000000000..a698b60411 --- /dev/null +++ b/src/Downloader.h @@ -0,0 +1,57 @@ +#ifndef SQUID_DOWNLOADER_H +#define SQUID_DOWNLOADER_H + +#include "client_side.h" +#include "cbdata.h" + +class Downloader: public ConnStateData +{ + CBDATA_CLASS(Downloader); + // XXX CBDATA_CLASS expands to nonvirtual toCbdata, AsyncJob::toCbdata + // is pure virtual. breaks build on clang if override is used + +public: + class CbDialer { + public: + CbDialer(): status(Http::scNone) {} + virtual ~CbDialer() {} + SBuf object; + Http::StatusCode status; + }; + + explicit Downloader(SBuf &url, const MasterXaction::Pointer &xact, AsyncCall::Pointer &aCallback); + virtual ~Downloader(); + void downloadFinished(); + + /* ConnStateData API */ + virtual bool isOpen() const; + + /* AsyncJob API */ + virtual void callException(const std::exception &e); + virtual bool doneAll() const; + + /*Bodypipe API*/ + virtual void noteMoreBodySpaceAvailable(BodyPipe::Pointer); + virtual void noteBodyConsumerAborted(BodyPipe::Pointer); + +protected: + /* ConnStateData API */ + virtual ClientSocketContext *parseOneRequest(); + virtual void processParsedRequest(ClientSocketContext *context); + virtual time_t idleTimeout() const; + virtual void writeControlMsgAndCall(ClientSocketContext *context, HttpReply *rep, AsyncCall::Pointer &call); + virtual void handleReply(HttpReply *header, StoreIOBuffer receivedData); + + /* AsyncJob API */ + virtual void start(); + +private: + void callBack(); + SBuf url_; + AsyncCall::Pointer callback; + Http::StatusCode status; + SBuf object; //object data + size_t maxObjectSize; +}; + +#endif diff --git a/src/Makefile.am b/src/Makefile.am index 66a3b3e08a..99ea95cd2f 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -285,6 +285,8 @@ squid_SOURCES = \ dlink.h \ dlink.cc \ $(DNSSOURCE) \ + Downloader.h \ + Downloader.cc \ enums.h \ err_type.h \ err_detail_type.h \ diff --git a/src/SquidConfig.h b/src/SquidConfig.h index 9d5e7e14e2..181049e3d2 100644 --- a/src/SquidConfig.h +++ b/src/SquidConfig.h @@ -19,6 +19,9 @@ #include "Notes.h" #include "security/forward.h" #include "SquidTime.h" +#if USE_OPENSSL +#include "ssl/support.h" +#endif #include "YesNoNone.h" #if USE_OPENSSL @@ -494,6 +497,7 @@ public: struct { Security::ContextPointer sslContext; #if USE_OPENSSL + char *untrustedCertsPath; acl_access *cert_error; sslproxy_cert_sign *cert_sign; sslproxy_cert_adapt *cert_adapt; diff --git a/src/cache_cf.cc b/src/cache_cf.cc index 7b4c04fd31..a7b9a55d47 100644 --- a/src/cache_cf.cc +++ b/src/cache_cf.cc @@ -865,6 +865,11 @@ configDoConfigure(void) Config2.effectiveGroupID = grp->gr_gid; } +#if USE_OPENSSL + if (Config.ssl_client.untrustedCertsPath) + Ssl::loadSquidUntrusted(Config.ssl_client.untrustedCertsPath); +#endif + if (Security::ProxyOutgoingConfig.encryptTransport) { debugs(3, DBG_IMPORTANT, "Initializing https:// proxy context"); Config.ssl_client.sslContext = Security::ProxyOutgoingConfig.createClientContext(false); @@ -875,6 +880,9 @@ configDoConfigure(void) debugs(3, DBG_IMPORTANT, "ERROR: proxying https:// currently still requires --with-openssl"); #endif } +#if USE_OPENSSL + Ssl::useSquidUntrusted(Config.ssl_client.sslContext); +#endif } for (CachePeer *p = Config.peers; p != NULL; p = p->next) { @@ -3825,6 +3833,7 @@ configFreeMemory(void) free_all(); #if USE_OPENSSL SSL_CTX_free(Config.ssl_client.sslContext); + Ssl::unloadSquidUntrusted(); #endif } diff --git a/src/cf.data.pre b/src/cf.data.pre index 091410cf6a..30a0b17d80 100644 --- a/src/cf.data.pre +++ b/src/cf.data.pre @@ -2559,6 +2559,20 @@ DOC_START Sets the cache size to use for ssl session DOC_END +NAME: sslproxy_untrusted_certs +IFDEF: USE_OPENSSL +DEFAULT: none +LOC: Config.ssl_client.untrustedCertsPath +TYPE: string +DOC_START + Squid uses the intermediate certificates pre-loaded from the specified + file to validate origin server certificate chains. Squid receives many + incomplete chains (i.e., chains with intermediate certificates missing). + The file is expected to contain zero or more PEM-encoded intermediate + certificates. These certificates are not treated as trusted root + certificates. +DOC_END + NAME: sslproxy_cert_sign_hash IFDEF: USE_OPENSSL DEFAULT: none diff --git a/src/client_side.cc b/src/client_side.cc index 7e9143de73..e21c2abb02 100644 --- a/src/client_side.cc +++ b/src/client_side.cc @@ -810,7 +810,8 @@ ConnStateData::swanSong() debugs(33, 2, HERE << clientConnection); flags.readMore = false; DeregisterRunner(this); - clientdbEstablished(clientConnection->remote, -1); /* decrement */ + if (clientConnection != NULL) + clientdbEstablished(clientConnection->remote, -1); /* decrement */ assert(areAllContextsForThisConnection()); freeAllContexts(); @@ -1422,9 +1423,14 @@ clientSocketRecipient(clientStreamNode * node, ClientHttpRequest * http, HttpReply * rep, StoreIOBuffer receivedData) { // dont tryt to deliver if client already ABORTED - if (!http->getConn() || !cbdataReferenceValid(http->getConn()) || !Comm::IsConnOpen(http->getConn()->clientConnection)) + if (!http->getConn() || !cbdataReferenceValid(http->getConn())) return; + // If it is not connectionless and connection is closed return + if (!http->getConn()->connectionless() && !Comm::IsConnOpen(http->getConn()->clientConnection)) + return; + + /* Test preconditions */ assert(node != NULL); PROF_start(clientSocketRecipient); @@ -2547,10 +2553,12 @@ clientProcessRequest(ConnStateData *conn, const Http1::RequestParserPointer &hp, request->flags.accelerated = http->flags.accel; request->flags.sslBumped=conn->switchedToHttps(); - request->flags.ignoreCc = conn->port->ignore_cc; - // TODO: decouple http->flags.accel from request->flags.sslBumped - request->flags.noDirect = (request->flags.accelerated && !request->flags.sslBumped) ? - !conn->port->allow_direct : 0; + if (!conn->connectionless()) { + request->flags.ignoreCc = conn->port->ignore_cc; + // TODO: decouple http->flags.accel from request->flags.sslBumped + request->flags.noDirect = (request->flags.accelerated && !request->flags.sslBumped) ? + !conn->port->allow_direct : 0; + } #if USE_AUTH if (request->flags.sslBumped) { if (conn->getAuth() != NULL) @@ -2593,14 +2601,16 @@ clientProcessRequest(ConnStateData *conn, const Http1::RequestParserPointer &hp, request->flags.internal = http->flags.internal; setLogUri (http, urlCanonicalClean(request.getRaw())); - request->client_addr = conn->clientConnection->remote; // XXX: remove reuest->client_addr member. + if (!conn->connectionless()) { + request->client_addr = conn->clientConnection->remote; // XXX: remove reuest->client_addr member. #if FOLLOW_X_FORWARDED_FOR // indirect client gets stored here because it is an HTTP header result (from X-Forwarded-For:) // not a details about teh TCP connection itself - request->indirect_client_addr = conn->clientConnection->remote; + request->indirect_client_addr = conn->clientConnection->remote; #endif /* FOLLOW_X_FORWARDED_FOR */ - request->my_addr = conn->clientConnection->local; - request->myportname = conn->port->name; + request->my_addr = conn->clientConnection->local; + request->myportname = conn->port->name; + } if (!isFtp) { // XXX: for non-HTTP messages instantiate a different HttpMsg child type @@ -3409,8 +3419,10 @@ ConnStateData::ConnStateData(const MasterXaction::Pointer &xact) : // store the details required for creating more MasterXaction objects as new requests come in clientConnection = xact->tcpClient; port = xact->squidPort; - transferProtocol = port->transport; // default to the *_port protocol= setting. may change later. - log_addr = xact->tcpClient->remote; + if (port != NULL) + transferProtocol = port->transport; // default to the *_port protocol= setting. may change later. + if (xact->tcpClient != NULL) + log_addr = xact->tcpClient->remote; log_addr.applyMask(Config.Addrs.client_netmask); // register to receive notice of Squid signal events diff --git a/src/client_side.h b/src/client_side.h index bd58c3359e..78e4fae6ef 100644 --- a/src/client_side.h +++ b/src/client_side.h @@ -185,7 +185,7 @@ public: ClientSocketContext::Pointer getCurrentContext() const; void addContextToQueue(ClientSocketContext * context); int getConcurrentRequestCount() const; - bool isOpen() const; + virtual bool isOpen() const; /// Update flags and timeout after the first byte received void receivedFirstByte(); @@ -267,6 +267,9 @@ public: /// Squid listening port details where this connection arrived. AnyP::PortCfgPointer port; + /// If the port is not set then it is a connection-less object + /// created by an internal squid subsystem + bool connectionless() const { return port == NULL; } bool transparent() const; bool reading() const; void stopReading(); ///< cancels comm_read if it is scheduled diff --git a/src/client_side_reply.cc b/src/client_side_reply.cc index 9029510c60..139e3bda9f 100644 --- a/src/client_side_reply.cc +++ b/src/client_side_reply.cc @@ -1354,7 +1354,7 @@ clientReplyContext::buildReplyHeader() if (EBIT_TEST(http->storeEntry()->flags, ENTRY_SPECIAL)) { hdr->delById(Http::HdrType::DATE); hdr->putTime(Http::HdrType::DATE, squid_curtime); - } else if (http->getConn() && http->getConn()->port->actAsOrigin) { + } else if (http->getConn() && !http->getConn()->connectionless() && http->getConn()->port->actAsOrigin) { // Swap the Date: header to current time if we are simulating an origin HttpHeaderEntry *h = hdr->findEntry(Http::HdrType::DATE); if (h) @@ -1526,7 +1526,10 @@ clientReplyContext::buildReplyHeader() request->flags.proxyKeepalive = false; } else if (http->getConn()) { ConnStateData * conn = http->getConn(); - if (!Comm::IsConnOpen(conn->port->listenConn)) { + if (conn->connectionless()) { + debugs(88, 3, "connection-less object, close after finished"); + request->flags.proxyKeepalive = false; + } else if (!Comm::IsConnOpen(conn->port->listenConn)) { // The listening port closed because of a reconfigure debugs(88, 3, "listening port closed"); request->flags.proxyKeepalive = false; diff --git a/src/client_side_request.cc b/src/client_side_request.cc index 3576242e3d..ef99a4e264 100644 --- a/src/client_side_request.cc +++ b/src/client_side_request.cc @@ -996,6 +996,10 @@ clientCheckPinning(ClientHttpRequest * http) if (!http_conn) return; + // Internal requests such as those from Doenloader does not have local port + if (http_conn->port == NULL) + return; + request->flags.connectionAuthDisabled = http_conn->port->connection_auth_disabled; if (!request->flags.connectionAuthDisabled) { if (Comm::IsConnOpen(http_conn->pinning.serverConnection)) { diff --git a/src/ssl/PeerConnector.cc b/src/ssl/PeerConnector.cc index c265473f43..1496598b16 100644 --- a/src/ssl/PeerConnector.cc +++ b/src/ssl/PeerConnector.cc @@ -14,6 +14,7 @@ #include "CachePeer.h" #include "client_side.h" #include "comm/Loops.h" +#include "Downloader.h" #include "errorpage.h" #include "fde.h" #include "globals.h" @@ -278,7 +279,6 @@ Ssl::PeekingPeerConnector::checkForPeekAndSpliceMatched(const Ssl::BumpMode acti } else if (finalAction != Ssl::bumpSplice) { //Allow write, proceed with the connection srvBio->holdWrite(false); - srvBio->recordInput(false); debugs(83,5, "Retry the fwdNegotiateSSL on FD " << serverConn->fd); Ssl::PeerConnector::noteWantWrite(); } else { @@ -455,8 +455,29 @@ Ssl::PeerConnector::handleNegotiateError(const int ret) void Ssl::PeerConnector::noteWantRead() { - setReadTimeout(); const int fd = serverConnection()->fd; + SSL *ssl = fd_table[fd].ssl; + BIO *b = SSL_get_rbio(ssl); + Ssl::ServerBio *srvBio = static_cast(b->ptr); + if (srvBio->holdRead()) { + if (srvBio->gotHello()) { + if (checkForMissingCertificates()) + return; // Wait to download certificates before proceed. + + srvBio->holdRead(false); + // Schedule a negotiateSSl to allow openSSL parse received data + Ssl::PeerConnector::NegotiateSsl(fd, this); + return; + } else if (srvBio->gotHelloFailed()) { + srvBio->holdRead(false); + debugs(83, DBG_IMPORTANT, "Error parsing SSL Server Hello Message on FD " << fd); + // Schedule a negotiateSSl to allow openSSL parse received data + Ssl::PeerConnector::NegotiateSsl(fd, this); + return; + } + } + + setReadTimeout(); Comm::SetSelect(fd, COMM_SELECT_READ, &NegotiateSsl, this, 0); } @@ -589,6 +610,94 @@ Ssl::PeerConnector::status() const return buf.content(); } +class PeerConnectorCertDownloaderDialer: public CallDialer, public Downloader::CbDialer +{ +public: + typedef void (Ssl::PeerConnector::*Method)(SBuf &object, int status); + + PeerConnectorCertDownloaderDialer(Method method, Ssl::PeerConnector *pc): + method_(method), + peerConnector_(pc) {} + + /* CallDialer API */ + virtual bool canDial(AsyncCall &call) { return peerConnector_.valid(); } + void dial(AsyncCall &call) { ((&(*peerConnector_))->*method_)(object, status); } + virtual void print(std::ostream &os) const { + os << '(' << peerConnector_.get() << ", Http Status:" << status << ')'; + } + + Method method_; + CbcPointer peerConnector_; +}; + +void +Ssl::PeerConnector::startCertDownloading(SBuf &url) +{ + AsyncCall::Pointer certCallback = asyncCall(81, 4, + "Ssl::PeerConnector::certDownloadingDone", + PeerConnectorCertDownloaderDialer(&Ssl::PeerConnector::certDownloadingDone, this)); + + MasterXaction *xaction = new MasterXaction; + Downloader *dl = new Downloader(url, xaction, certCallback); + AsyncJob::Start(dl); +} + +void +Ssl::PeerConnector::certDownloadingDone(SBuf &obj, int downloadStatus) +{ + debugs(81, 5, "OK! certificate downloaded, status: " << downloadStatus << " data size: " << obj.length()); + + // Get ServerBio from SSL object + const int fd = serverConnection()->fd; + SSL *ssl = fd_table[fd].ssl; + BIO *b = SSL_get_rbio(ssl); + Ssl::ServerBio *srvBio = static_cast(b->ptr); + + // Parse Certificate. Assume that it is in DER format. Probably we should handle PEM or other formats too + const unsigned char *raw = (const unsigned char*)obj.rawContent(); + if (X509 *cert = d2i_X509(NULL, &raw, obj.length())) { + char buffer[1024]; + debugs(81, 5, "Retrieved certificate: " << X509_NAME_oneline(X509_get_subject_name(cert), buffer, 1024)); + const Ssl::X509_STACK_Pointer &certsList = srvBio->serverCertificates(); + if (const char *issuerUri = Ssl::uriOfIssuerIfMissing(cert, certsList)) { + urlsOfMissingCerts.push(SBuf(issuerUri)); + } + Ssl::SSL_add_untrusted_cert(ssl, cert); + } + + // Check if has uri to donwload and add it to urlsOfMissingCerts + if (urlsOfMissingCerts.size()) { + startCertDownloading(urlsOfMissingCerts.front()); + urlsOfMissingCerts.pop(); + return; + } + + srvBio->holdRead(false); + Ssl::PeerConnector::NegotiateSsl(serverConnection()->fd, this); +} + +bool +Ssl::PeerConnector::checkForMissingCertificates () +{ + const int fd = serverConnection()->fd; + SSL *ssl = fd_table[fd].ssl; + BIO *b = SSL_get_rbio(ssl); + Ssl::ServerBio *srvBio = static_cast(b->ptr); + const Ssl::X509_STACK_Pointer &certs = srvBio->serverCertificates(); + + if (sk_X509_num(certs.get())) { + debugs(83, 5, "SSL server sent " << sk_X509_num(certs.get()) << " certificates"); + Ssl::missingChainCertificatesUrls(urlsOfMissingCerts, certs); + if (urlsOfMissingCerts.size()) { + startCertDownloading(urlsOfMissingCerts.front()); + urlsOfMissingCerts.pop(); + return true; + } + } + + return false; +} + SSL_CTX * Ssl::BlindPeerConnector::getSslContext() { @@ -705,7 +814,6 @@ Ssl::PeekingPeerConnector::initializeSsl() Ssl::ServerBio *srvBio = static_cast(b->ptr); // Inherite client features, like SSL version, SNI and other srvBio->setClientFeatures(features); - srvBio->recordInput(true); srvBio->mode(csd->sslBumpMode); } } diff --git a/src/ssl/PeerConnector.h b/src/ssl/PeerConnector.h index e70fc175b1..8bec0943d0 100644 --- a/src/ssl/PeerConnector.h +++ b/src/ssl/PeerConnector.h @@ -15,6 +15,7 @@ #include "security/EncryptorAnswer.h" #include "ssl/support.h" #include +#include class HttpRequest; class ErrorState; @@ -119,6 +120,12 @@ protected: /// Squid COMM_SELECT_READ handler. void noteWantRead(); + bool checkForMissingCertificates(); + + void startCertDownloading(SBuf &url); + + void certDownloadingDone(SBuf &object, int status); + /// Called when the openSSL SSL_connect function needs to write data to /// the remote SSL server. Sets the Squid COMM_SELECT_WRITE handler. virtual void noteWantWrite(); @@ -175,6 +182,8 @@ private: time_t negotiationTimeout; ///< the SSL connection timeout to use time_t startTime; ///< when the peer connector negotiation started bool useCertValidator_; ///< whether the certificate validator should bypassed + + std::queue urlsOfMissingCerts; }; /// A simple PeerConnector for SSL/TLS cache_peers. No SslBump capabilities. diff --git a/src/ssl/bio.cc b/src/ssl/bio.cc index d09905f79b..a4e8301dac 100644 --- a/src/ssl/bio.cc +++ b/src/ssl/bio.cc @@ -128,18 +128,18 @@ Ssl::Bio::read(char *buf, int size, BIO *table) } int -Ssl::Bio::readAndBuffer(char *buf, int size, BIO *table, const char *description) +Ssl::Bio::readAndBuffer(BIO *table, const char *description) { prepReadBuf(); - - size = min((int)rbuf.potentialSpaceSize(), size); + char buf[SQUID_TCP_SO_RCVBUF ]; + size_t size = min(rbuf.potentialSpaceSize(), (mb_size_t)SQUID_TCP_SO_RCVBUF); if (size <= 0) { debugs(83, DBG_IMPORTANT, "Not enough space to hold " << rbuf.contentSize() << "+ byte " << description); return -1; } - const int bytes = Ssl::Bio::read(buf, size, table); + const int bytes = Ssl::Bio::read(buf, sizeof(buf), table); debugs(83, 5, "read " << bytes << " out of " << size << " bytes"); // move to Ssl::Bio::read() if (bytes > 0) { @@ -219,7 +219,7 @@ int Ssl::ClientBio::read(char *buf, int size, BIO *table) { if (helloState < atHelloReceived) { - int bytes = readAndBuffer(buf, size, table, "TLS client Hello"); + int bytes = readAndBuffer(table, "TLS client Hello"); if (bytes <= 0) return bytes; } @@ -282,11 +282,51 @@ Ssl::ServerBio::setClientFeatures(const Ssl::Bio::sslFeatures &features) clientFeatures = features; }; +int +Ssl::ServerBio::readAndBufferServerHelloMsg(BIO *table, const char *description) +{ + + int ret = readAndBuffer(table, description); + if (ret <= 0) + return ret; + + if (!parser_.parseServerHello((const unsigned char *)rbuf.content(), rbuf.contentSize())) { + if (!parser_.parseError) + BIO_set_retry_read(table); + return -1; + } + + return 1; +} + int Ssl::ServerBio::read(char *buf, int size, BIO *table) { - return record_ ? - readAndBuffer(buf, size, table, "TLS server Hello") : Ssl::Bio::read(buf, size, table); + if (parser_.state < Ssl::HandshakeParser::atHelloDoneReceived) { + int ret = readAndBufferServerHelloMsg(table, "TLS server Hello"); + if (ret <= 0) + return ret; + } + + if (holdRead_) { + debugs(83, 7, "Hold flag is set on ServerBio, retry latter. (Hold " << size << "bytes)"); + BIO_set_retry_read(table); + return -1; + } + + if (parser_.parseDone && !parser_.parseError) { + int unsent = rbuf.contentSize() - rbufConsumePos; + if (unsent > 0) { + int bytes = (size <= unsent ? size : unsent); + memcpy(buf, rbuf.content()+rbufConsumePos, bytes); + rbufConsumePos += bytes; + debugs(83, 7, "Pass " << bytes << " bytes to openSSL as read"); + return bytes; + } else + return Ssl::Bio::read(buf, size, table); + } + + return -1; } // This function makes the required checks to examine if the client hello @@ -505,19 +545,18 @@ Ssl::ServerBio::flush(BIO *table) bool Ssl::ServerBio::resumingSession() { - if (!serverFeatures.initialized_) - serverFeatures.get(rbuf, false); - - if (!clientFeatures.sessionId.isEmpty() && !serverFeatures.sessionId.isEmpty()) - return clientFeatures.sessionId == serverFeatures.sessionId; + return parser_.ressumingSession; +} - // is this a session resuming attempt using TLS tickets? - if (clientFeatures.hasTlsTicket && - serverFeatures.tlsTicketsExtension && - serverFeatures.hasCcsOrNst) - return true; +const Ssl::X509_STACK_Pointer & +Ssl::ServerBio::serverCertificates() +{ + if (!serverCertificates_.get()) { + serverCertificates_.reset(sk_X509_new_null()); + parser_.parseServerCertificates(serverCertificates_, (const unsigned char *)rbuf.content(), rbuf.contentSize()); + } - return false; + return serverCertificates_; } /// initializes BIO table after allocation @@ -639,7 +678,7 @@ squid_ssl_info(const SSL *ssl, int where, int ret) } } -Ssl::Bio::sslFeatures::sslFeatures(): sslVersion(-1), compressMethod(-1), helloMsgSize(0), unknownCiphers(false), doHeartBeats(true), tlsTicketsExtension(false), hasTlsTicket(false), tlsStatusRequest(false), hasCcsOrNst(false), initialized_(false) +Ssl::Bio::sslFeatures::sslFeatures(): sslVersion(-1), compressMethod(-1), helloMsgSize(0), unknownCiphers(false), doHeartBeats(true), tlsTicketsExtension(false), hasTlsTicket(false), tlsStatusRequest(false), initialized_(false) { memset(client_random, 0, SSL3_RANDOM_SIZE); } @@ -794,39 +833,6 @@ Ssl::Bio::sslFeatures::parseMsgHead(const MemBuf &buf) return helloMsgSize; } -bool -Ssl::Bio::sslFeatures::checkForCcsOrNst(const unsigned char *msg, size_t size) -{ - while (size > 5) { - const int msgType = msg[0]; - const int msgSslVersion = (msg[1] << 8) | msg[2]; - debugs(83, 7, "SSL Message Version :" << std::hex << std::setw(8) << std::setfill('0') << msgSslVersion); - // Check for Change Cipher Spec message - // RFC5246 section 6.2.1 - if (msgType == 0x14) {// Change Cipher Spec message found - debugs(83, 7, "SSL Change Cipher Spec message found"); - return true; - } - // Check for New Session Ticket message - // RFC5077 section 3.3 - if (msgType == 0x04) {// New Session Ticket message found - debugs(83, 7, "TLS New Session Ticket message found"); - return true; - } - // The hello message size exist in 4th and 5th bytes - size_t msgLength = (msg[3] << 8) + msg[4]; - debugs(83, 7, "SSL Message Size: " << msgLength); - msgLength += 5; - - if (msgLength <= size) { - msg += msgLength; - size -= msgLength; - } else - size = 0; - } - return false; -} - bool Ssl::Bio::sslFeatures::get(const MemBuf &buf, bool record) { @@ -858,10 +864,7 @@ Ssl::Bio::sslFeatures::get(const MemBuf &buf, bool record) // The type 2 is a ServerHello, the type 1 is a ClientHello // RFC5246 section 7.4 if (msg[5] == 0x2) { // ServerHello message - if (parseV3ServerHello(msg, (size_t)msgSize)) { - hasCcsOrNst = checkForCcsOrNst(msg + msgSize, buf.contentSize() - msgSize); - return true; - } + return parseV3ServerHello(msg, (size_t)msgSize); } else if (msg[5] == 0x1) // ClientHello message, return parseV3Hello(msg, (size_t)msgSize); } @@ -1197,5 +1200,229 @@ Ssl::Bio::sslFeatures::print(std::ostream &os) const " opaquePrf:" << opaquePrf; } +bool +Ssl::HandshakeParser::parseNextContentRecord(const unsigned char *msg, size_t size) +{ + if (unParsedContent) + return true; + + if (parsingPos >= size) + return false; + + msg += parsingPos; + size -= parsingPos; + + if (size < 5) + return false; + + const unsigned int contentType = msg[0]; + // The hello message size exist in 4th and 5th bytes + size_t contentLength = (msg[3] << 8) + msg[4]; + if (contentLength > size - 5) + return false; //missing message data? + + unParsedContent = contentLength; + currentContentType = (ContentType)contentType; + parsingPos += 5; + + currentMsg = 0; + currentMsgSize = 0; + return true; +} + +bool +Ssl::HandshakeParser::skipContentDataRecord(const unsigned char *msg, size_t size) +{ + if (size < parsingPos) { + parseDone = true; + parseError = true; + return false; + } + parsingPos += unParsedContent; + unParsedContent = 0; + currentContentType = ctNone; + + currentMsg = 0; + currentMsgSize = 0; + + return true; +} + +Ssl::HandshakeParser::HandshakeType +Ssl::HandshakeParser::parseNextHandshakeMessage(const unsigned char *msg, size_t size) +{ + if (!unParsedContent) { + return hskNone; // No data to parse + } + + if (currentContentType != ctHandshake) { + parseError = true; + parseDone = true; + return hskNone; + } + + msg += parsingPos; + size -= parsingPos; + + const HandshakeType type = (HandshakeType)msg[0]; + size_t msgLength = (msg[1] << 16) | (msg[2] << 8) | msg[3]; + + if (msgLength > size + 4 || + msgLength > unParsedContent + 4) { + // The parseNextContentData call assure that we have all handshake data + // before parse this handshake message. So this is looks like a + // parse error + parseError = true; + parseDone = true; + return hskNone; + } + + currentMsg = parsingPos + 4; + currentMsgSize = msgLength; + + unParsedContent -= msgLength + 4; + parsingPos += msgLength + 4; + + return type; +} + +bool +Ssl::HandshakeParser::parseServerHello(const unsigned char *data, size_t dataSize) +{ + while(!parseDone && !parseError) { + switch(state) { + case atHelloNone: + case atHelloStarted: + if (!parseNextContentRecord(data, dataSize)) + return false; + { + HandshakeType type = parseNextHandshakeMessage(data, dataSize); + if (type == hskNone) + return false; //probably the message does not received yet + if (type != hskServerHello) { // parse error; Expecting Server hello + parseError = true; + parseDone = true; + return false; + } + } + state = atHelloReceived; + break; + + case atHelloReceived: + if (!parseNextContentRecord(data, dataSize)) + return false; + + if (currentContentType == ctChangeCipherSpec) { + state = atCcsReceived; + skipContentDataRecord(data, dataSize); // Skipe to next Record + } else { + HandshakeType type = parseNextHandshakeMessage(data, dataSize); + if (type == hskNone) + return false; // probably an error; + else if (type == hskCertificate) { + state = atCertificatesReceived; + certificatesMsgPos = currentMsg; + certificatesMsgSize = currentMsgSize; + } else if (type == shkNewSessionTicket) + state = atNstReceived; + } + break; + + case atNstReceived: + if (!parseNextContentRecord(data, dataSize)) + return false; + + if (currentContentType == ctChangeCipherSpec) + state = atCcsReceived; + skipContentDataRecord(data, dataSize); // Skipe to next Record + break; + + case atCertificatesReceived: { + if (!parseNextContentRecord(data, dataSize)) + return false; + + HandshakeType type = parseNextHandshakeMessage(data, dataSize); + if (type == hskNone) + return false; + if (type == hskServerHelloDone) + state = atHelloDoneReceived; + } + break; + + case atCcsReceived: { + if (!parseNextContentRecord(data, dataSize)) + return false; + + ressumingSession = true; + + // To parse a shkFinished handshake message: + // HandshakeType type = parseNextHandshakeMessage(data, dataSize); + // if (type == hskNone) + // return false; + // if (type == shkFinished) + // helloState = Ssl::Bio::atFinishReceived; + // + // However this message may arrived encrypted. + // We are accepting any handshake message for now. + if (currentContentType == ctHandshake && unParsedContent != 0) + state = atFinishReceived; + } + break; + case atHelloDoneReceived: + case atFinishReceived: + return (parseDone = true); + break; + } + } + + return parseDone; +} + +static X509 * +extractCertificate(const unsigned char *currentpos, size_t size, const unsigned char **next, size_t *nextSize) +{ + size_t certLen = currentpos[0] << 16 | currentpos[1] << 8 | currentpos[2]; + if (certLen + 3 > size) + return NULL; + *next = currentpos + 3 + certLen; + *nextSize = size - certLen - 3; + + const unsigned char *raw = (const unsigned char*)(currentpos + 3); + return d2i_X509(NULL, &(raw), certLen); +} + +bool +Ssl::HandshakeParser::parseServerCertificates(Ssl::X509_STACK_Pointer &serverCertificates, const unsigned char *msg, size_t size) +{ + if (!serverCertificates.get()) + return false; // No struct to store it? should we assert? + + if (!certificatesMsgPos) + return false; // There is no certificates message + + // We are checking for this while parsing handshake messages + assert(certificatesMsgPos <= size - 3); + + const unsigned char *certMsg = msg + certificatesMsgPos; + // First 3 bytes are the size of certificates + size_t certsLength = ((certMsg[0] << 16) | (certMsg[1] << 8) | certMsg[2]); + if (certsLength > size - certificatesMsgPos) { + debugs(83, 2, "Error parsing certificates Handshake Message. Parsed certificates length: " << certsLength << + " server hello length: " << size); + return false; + } + certMsg += 3; // Point to raw certificates data, in the form [certLength cert]* + + const unsigned char *next = certMsg; + size_t nextLen = certsLength; + while(nextLen) { + X509 *cert = NULL; + if (!(cert = extractCertificate(next, nextLen, &next, &nextLen))) + break; + sk_X509_push(serverCertificates.get(), cert); + } + return true; +} + #endif /* USE_SSL */ diff --git a/src/ssl/bio.h b/src/ssl/bio.h index 0ae6e4403e..eac0a8bcdd 100644 --- a/src/ssl/bio.h +++ b/src/ssl/bio.h @@ -21,6 +21,52 @@ namespace Ssl { +class HandshakeParser { +public: + /// The parsing states + typedef enum {atHelloNone = 0, atHelloStarted, atHelloReceived, atCertificatesReceived, atHelloDoneReceived, atNstReceived, atCcsReceived, atFinishReceived} ParserState; + + /// TLS record protocol, content types, RFC5246 section 6.2.1 + typedef enum {ctNone = 0, ctChangeCipherSpec = 20, ctAlert = 21, ctHandshake = 22, ctApplicationData} ContentType; + /// TLS Handshake protocol, handshake types, RFC5246 section 7.4 + typedef enum {hskNone = 0, hskServerHello = 2, shkNewSessionTicket = 4, hskCertificate = 11, hskServerHelloDone = 14, hskFinished = 20} HandshakeType; + + HandshakeParser(): state(atHelloNone), currentContentType(ctNone), unParsedContent(0), parsingPos(0), currentMsg(0), currentMsgSize(0), certificatesMsgPos(0), certificatesMsgSize(0), ressumingSession(false), parseDone(false), parseError(false) {} + + /// Parses the SSL Server Hello records stored in data. + /// Return false if the hello messages are not complete (HelloDone + /// or Finished handshake messages are not received) + /// On parse error, return false and sets the parseError member to true. + bool parseServerHello(const unsigned char *data, size_t dataSize); + + /// Parse server certificates message and store the certificate to serverCertificates list + bool parseServerCertificates(Ssl::X509_STACK_Pointer &serverCertificates, const unsigned char *msg, size_t size); + + ParserState state; ///< current parsing state. + + ContentType currentContentType; ///< The current SSL record content type + size_t unParsedContent; ///< The size of current SSL record, which is not parsed yet + size_t parsingPos; ///< The parsing position from the beginning of parsed data + size_t currentMsg; ///< The current handshake message possition from the beginning of parsed data + size_t currentMsgSize; ///< The current handshake message size. + + size_t certificatesMsgPos; ///< The possition of certificates message from the beggining of parsed data + size_t certificatesMsgSize; ///< The size of certificates message + bool ressumingSession; ///< True if this is a resumming session + + bool parseDone; ///< The parser finishes its job + bool parseError; ///< Set to tru by parse on parse error. + +private: + /// Do nothing if there are unparsed data from existing SSL record + /// else parses the next SSL record. + /// Return false if the next SSL record is not complete. + bool parseNextContentRecord(const unsigned char *msg, size_t size); + /// Consumes the current SSL record and set the parsingPos to the next + bool skipContentDataRecord(const unsigned char *msg, size_t size); + /// Parses the next handshake message in current SSL record + HandshakeType parseNextHandshakeMessage(const unsigned char *msg, size_t size); +}; /// BIO source and sink node, handling socket I/O and monitoring SSL state class Bio @@ -57,9 +103,6 @@ public: /// \retval 0 if the contents of the buffer are not enough /// \retval <0 if the contents of buf are not SSLv3 or TLS hello message int parseMsgHead(const MemBuf &); - /// Parses msg buffer and return true if one of the Change Cipher Spec - /// or New Session Ticket messages found - bool checkForCcsOrNst(const unsigned char *msg, size_t size); public: int sslVersion; ///< The requested/used SSL version int compressMethod; ///< The requested/used compressed method @@ -75,9 +118,6 @@ public: bool hasTlsTicket; ///< whether a TLS ticket is included bool tlsStatusRequest; ///< whether the TLS status request extension is set SBuf tlsAppLayerProtoNeg; ///< The value of the TLS application layer protocol extension if it is enabled - /// whether Change Cipher Spec message included in ServerHello - /// handshake message - bool hasCcsOrNst; /// The client random number unsigned char client_random[SSL3_RANDOM_SIZE]; SBuf sessionId; @@ -114,7 +154,7 @@ public: void prepReadBuf(); /// Reads data from socket and record them to a buffer - int readAndBuffer(char *buf, int size, BIO *table, const char *description); + int readAndBuffer(BIO *table, const char *description); const MemBuf &rBufData() {return rbuf;} protected: @@ -180,7 +220,7 @@ private: class ServerBio: public Bio { public: - explicit ServerBio(const int anFd): Bio(anFd), helloMsgSize(0), helloBuild(false), allowSplice(false), allowBump(false), holdWrite_(false), record_(false), bumpMode_(bumpNone) {} + explicit ServerBio(const int anFd): Bio(anFd), helloMsgSize(0), helloBuild(false), allowSplice(false), allowBump(false), holdWrite_(false), holdRead_(true), bumpMode_(bumpNone), rbufConsumePos(0) {} /// The ServerBio version of the Ssl::Bio::stateChanged method virtual void stateChanged(const SSL *ssl, int where, int ret); /// The ServerBio version of the Ssl::Bio::write method @@ -198,12 +238,19 @@ public: void setClientFeatures(const sslFeatures &features); bool resumingSession(); + + /// Reads Server hello message+certificates+ServerHelloDone message sent + /// by server and buffer it to rbuf member + int readAndBufferServerHelloMsg(BIO *table, const char *description); + /// The write hold state bool holdWrite() const {return holdWrite_;} /// Enables or disables the write hold state void holdWrite(bool h) {holdWrite_ = h;} - /// Enables or disables the input data recording, for internal analysis. - void recordInput(bool r) {record_ = r;} + /// The read hold state + bool holdRead() const {return holdRead_;} + /// Enables or disables the read hold state + void holdRead(bool h) {holdRead_ = h;} /// Whether we can splice or not the SSL stream bool canSplice() {return allowSplice;} /// Whether we can bump or not the SSL stream @@ -211,17 +258,29 @@ public: /// The bumping mode void mode(Ssl::BumpMode m) {bumpMode_ = m;} Ssl::BumpMode bumpMode() {return bumpMode_;} ///< return the bumping mode + + /// Return true if the Server hello message received + bool gotHello() const { return (parser_.parseDone && !parser_.parseError); } + + /// Return true if the Server Hello parsing failed + bool gotHelloFailed() const { return (parser_.parseDone && parser_.parseError); } + + const Ssl::X509_STACK_Pointer &serverCertificates(); private: sslFeatures clientFeatures; ///< SSL client features extracted from ClientHello message or SSL object - sslFeatures serverFeatures; ///< SSL server features extracted from ServerHello message SBuf helloMsg; ///< Used to buffer output data. mb_size_t helloMsgSize; bool helloBuild; ///< True if the client hello message sent to the server bool allowSplice; ///< True if the SSL stream can be spliced bool allowBump; ///< True if the SSL stream can be bumped bool holdWrite_; ///< The write hold state of the bio. - bool record_; ///< If true the input data recorded to rbuf for internal use + bool holdRead_; ///< The read hold state of the bio. Ssl::BumpMode bumpMode_; + + ///< The size of data stored in rbuf which passed to the openSSL + size_t rbufConsumePos; + HandshakeParser parser_; ///< The SSL messages parser. + Ssl::X509_STACK_Pointer serverCertificates_; ///< The certificates chain sent by the SSL server }; inline diff --git a/src/ssl/support.cc b/src/ssl/support.cc index 2ee7e21c80..6bc7dd426f 100644 --- a/src/ssl/support.cc +++ b/src/ssl/support.cc @@ -33,10 +33,15 @@ #include +// TODO: Move ssl_ex_index_* global variables from global.cc here. +int ssl_ex_index_ssl_untrusted_chain = -1; + static void setSessionCallbacks(SSL_CTX *ctx); Ipc::MemMap *SslSessionCache = NULL; const char *SslSessionCacheName = "ssl_session_cache"; +static Ssl::CertsIndexedList SquidUntrustedCerts; + const EVP_MD *Ssl::DefaultSignHash = NULL; const char *Ssl::BumpModeStr[] = { @@ -469,6 +474,27 @@ ssl_initialize(void) ssl_ex_index_ssl_errors = SSL_get_ex_new_index(0, (void *) "ssl_errors", NULL, NULL, &ssl_free_SslErrors); ssl_ex_index_ssl_cert_chain = SSL_get_ex_new_index(0, (void *) "ssl_cert_chain", NULL, NULL, &ssl_free_CertChain); ssl_ex_index_ssl_validation_counter = SSL_get_ex_new_index(0, (void *) "ssl_validation_counter", NULL, NULL, &ssl_free_int); + ssl_ex_index_ssl_untrusted_chain = SSL_get_ex_new_index(0, (void *) "ssl_untrusted_chain", NULL, NULL, &ssl_free_CertChain); +} + +bool +Ssl::loadCerts(const char *certsFile, Ssl::CertsIndexedList &list) +{ + BIO *in = BIO_new_file(certsFile, "r"); + if (!in) { + debugs(83, DBG_IMPORTANT, "Failed to open '" << certsFile << "' to load certificates"); + return false; + } + + X509 *aCert; + while((aCert = PEM_read_bio_X509(in, NULL, NULL, NULL))) { + static char buffer[2048]; + X509_NAME_oneline(X509_get_subject_name(aCert), buffer, sizeof(buffer)); + list.insert(std::pair(SBuf(buffer), aCert)); + } + debugs(83, 4, "Loaded " << list.size() << " certificates from file: '" << certsFile << "'"); + BIO_free(in); + return true; } #if defined(SSL3_FLAGS_NO_RENEGOTIATE_CIPHERS) @@ -1118,6 +1144,200 @@ void Ssl::addChainToSslContext(SSL_CTX *sslContext, STACK_OF(X509) *chain) } } +static const char * +hasAuthorityInfoAccessCaIssuers(X509 *cert) +{ + AUTHORITY_INFO_ACCESS *info; + if (!cert) + return NULL; + info = (AUTHORITY_INFO_ACCESS *)X509_get_ext_d2i(cert, NID_info_access, NULL, NULL); + if (!info) + return NULL; + + static char uri[MAX_URL]; + uri[0] = '\0'; + + for (int i = 0; i < sk_ACCESS_DESCRIPTION_num(info); i++) { + ACCESS_DESCRIPTION *ad = sk_ACCESS_DESCRIPTION_value(info, i); + if (OBJ_obj2nid(ad->method) == NID_ad_ca_issuers) { + if (ad->location->type == GEN_URI) { + xstrncpy(uri, (char *)ASN1_STRING_data(ad->location->d.uniformResourceIdentifier), sizeof(uri)); + } + break; + } + } + AUTHORITY_INFO_ACCESS_free(info); + return uri[0] != '\0' ? uri : NULL; +} + +/// quickly find a certificate with a given issuer in Ssl::CertsIndexedList. +static X509 * +findCertByIssuerFast(Ssl::CertsIndexedList &list, X509 *cert) +{ + static char buffer[2048]; + + if (X509_NAME *issuerName = X509_get_issuer_name(cert)) + X509_NAME_oneline(issuerName, buffer, sizeof(buffer)); + else + return NULL; + + const auto ret = list.equal_range(SBuf(buffer)); + for (Ssl::CertsIndexedList::iterator it = ret.first; it != ret.second; ++it) { + X509 *issuer = it->second; + if (X509_check_issued(cert, issuer)) { + return issuer; + } + } + return NULL; +} + +/// slowly find a certificate with a given issuer using linear search +static X509 * +findCertByIssuerSlowly(STACK_OF(X509) *sk, X509 *cert) +{ + if (!sk) + return NULL; + + const int skItemsNum = sk_X509_num(sk); + for (int i = 0; i < skItemsNum; ++i) { + X509 *issuer = sk_X509_value(sk, i); + if (X509_check_issued(cert, issuer) == X509_V_OK) + return issuer; + } + return NULL; +} + +const char * +Ssl::uriOfIssuerIfMissing(X509 *cert, Ssl::X509_STACK_Pointer const &serverCertificates) +{ + if (!cert || !serverCertificates.get()) + return NULL; + + if (!findCertByIssuerSlowly(serverCertificates.get(), cert)) { + //if issuer is missing ... + if (!findCertByIssuerFast(SquidUntrustedCerts, cert)) { + // and issuer not found in local untrusted certificates database + if (const char *issuerUri = hasAuthorityInfoAccessCaIssuers(cert)) { + // There is a URI where we can download a certificate. + // Check to see if this is required + return issuerUri; + } + } + } + return NULL; +} + +void +Ssl::missingChainCertificatesUrls(std::queue &URIs, Ssl::X509_STACK_Pointer const &serverCertificates) +{ + if (!serverCertificates.get()) + return; + + for (int i = 0; i < sk_X509_num(serverCertificates.get()); ++i) { + X509 *cert = sk_X509_value(serverCertificates.get(), i); + if (const char *issuerUri = uriOfIssuerIfMissing(cert, serverCertificates)) + URIs.push(SBuf(issuerUri)); + } +} + +void +Ssl::SSL_add_untrusted_cert(SSL *ssl, X509 *cert) +{ + STACK_OF(X509) *untrustedStack = static_cast (SSL_get_ex_data(ssl, ssl_ex_index_ssl_untrusted_chain)); + if (!untrustedStack) { + untrustedStack = sk_X509_new_null(); + if (!SSL_set_ex_data(ssl, ssl_ex_index_ssl_untrusted_chain, untrustedStack)) { + sk_X509_pop_free(untrustedStack, X509_free); //?? print an error? + return; + } + } + sk_X509_push(untrustedStack, cert); +} + +/// add missing issuer certificates to untrustedCerts +static void +completeIssuers(X509_STORE_CTX *ctx, STACK_OF(X509) *untrustedCerts) +{ + debugs(83, 2, "completing " << sk_X509_num(untrustedCerts) << " OpenSSL untrusted certs using " << SquidUntrustedCerts.size() << " configured untrusted certificates"); + + int depth = ctx->param->depth; + X509 *current = ctx->cert; + int i = 0; + for (i = 0; current && (i < depth); ++i) { + if (X509_check_issued(current, current)) { + // either ctx->cert is itself self-signed or untrustedCerts + // aready contain the self-signed current certificate + break; + } + + // untrustedCerts is short, not worth indexing + X509 *issuer = findCertByIssuerSlowly(untrustedCerts, current); + if (!issuer) { + if ((issuer = findCertByIssuerFast(SquidUntrustedCerts, current))) + sk_X509_push(untrustedCerts, issuer); + } + current = issuer; + } + + if (i >= depth) + debugs(83, 2, "exceeded the maximum certificate chain length: " << depth); +} + +/// OpenSSL certificate validation callback. +static int +untrustedToStoreCtx_cb(X509_STORE_CTX *ctx,void *data) +{ + debugs(83, 4, "Try to use pre-downloaded intermediate certificates\n"); + + SSL *ssl = (SSL *)X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx()); + STACK_OF(X509) *sslUntrustedStack = static_cast (SSL_get_ex_data(ssl, ssl_ex_index_ssl_untrusted_chain)); + + // OpenSSL already maintains ctx->untrusted but we cannot modify + // internal OpenSSL list directly. We have to give OpenSSL our own + // list, but it must include certificates on the OpenSSL ctx->untrusted + STACK_OF(X509) *oldUntrusted = ctx->untrusted; + STACK_OF(X509) *sk = sk_X509_dup(oldUntrusted); // oldUntrusted is always not NULL + + for (int i = 0; i < sk_X509_num(sslUntrustedStack); ++i) { + X509 *cert = sk_X509_value(sslUntrustedStack, i); + sk_X509_push(sk, cert); + } + + // If the local untrusted certificates internal database is used + // run completeIssuers to add missing certificates if possible. + if (SquidUntrustedCerts.size() > 0) + completeIssuers(ctx, sk); + + X509_STORE_CTX_set_chain(ctx, sk); // No locking/unlocking, just sets ctx->untrusted + int ret = X509_verify_cert(ctx); + X509_STORE_CTX_set_chain(ctx, oldUntrusted); // Set back the old untrusted list + sk_X509_free(sk); // Release sk list + return ret; +} + +void +Ssl::useSquidUntrusted(SSL_CTX *sslContext) +{ + SSL_CTX_set_cert_verify_callback(sslContext, untrustedToStoreCtx_cb, NULL); +} + +bool +Ssl::loadSquidUntrusted(const char *path) +{ + return Ssl::loadCerts(path, SquidUntrustedCerts); +} + +void +Ssl::unloadSquidUntrusted() +{ + if (SquidUntrustedCerts.size()) { + for (Ssl::CertsIndexedList::iterator it = SquidUntrustedCerts.begin(); it != SquidUntrustedCerts.end(); ++it) { + X509_free(it->second); + } + SquidUntrustedCerts.clear(); + } +} + /** \ingroup ServerProtocolSSLInternal * Read certificate from file. diff --git a/src/ssl/support.h b/src/ssl/support.h index 949fea79a2..6777249202 100644 --- a/src/ssl/support.h +++ b/src/ssl/support.h @@ -12,6 +12,7 @@ #define SQUID_SSL_SUPPORT_H #include "base/CbDataList.h" +#include "SBuf.h" #include "security/forward.h" #include "ssl/gadgets.h" @@ -24,6 +25,9 @@ #if HAVE_OPENSSL_ENGINE_H #include #endif +#include +#include + /** \defgroup ServerProtocolSSLAPI Server-Side SSL API @@ -155,6 +159,52 @@ inline const char *bumpMode(int bm) return (0 <= bm && bm < Ssl::bumpEnd) ? Ssl::BumpModeStr[bm] : NULL; } +/// certificates indexed by issuer name +typedef std::multimap CertsIndexedList; + +/** + \ingroup ServerProtocolSSLAPI + * Load PEM-encoded certificates from the given file. + */ +bool loadCerts(const char *certsFile, Ssl::CertsIndexedList &list); + +/** + \ingroup ServerProtocolSSLAPI + * Load PEM-encoded certificates to the squid untrusteds certificates + * internal DB from the given file. + */ +bool loadSquidUntrusted(const char *path); + +/** + \ingroup ServerProtocolSSLAPI + * Removes all certificates from squid untrusteds certificates + * internal DB and frees all memory + */ +void unloadSquidUntrusted(); + +/** + \ingroup ServerProtocolSSLAPI + * Add the certificate cert to ssl object untrusted certificates. + * Squid uses an attached to SSL object list of untrusted certificates, + * with certificates which can be used to complete incomplete chains sent + * by the SSL server. + */ +void SSL_add_untrusted_cert(SSL *ssl, X509 *cert); + +/** + \ingroup ServerProtocolSSLAPI + * Searches in serverCertificates list for the cert issuer and if not found + * and Authority Info Access of cert provides a URI return it. + */ +const char *uriOfIssuerIfMissing(X509 *cert, Ssl::X509_STACK_Pointer const &serverCertificates); + +/** + \ingroup ServerProtocolSSLAPI + * Fill URIs queue with the uris of missing certificates from serverCertificate chain + * if this information provided by Authority Info Access. + */ +void missingChainCertificatesUrls(std::queue &URIs, Ssl::X509_STACK_Pointer const &serverCertificates); + /** \ingroup ServerProtocolSSLAPI * Generate a certificate to be used as untrusted signing certificate, based on a trusted CA @@ -209,6 +259,13 @@ bool configureSSLUsingPkeyAndCertFromMemory(SSL *ssl, const char *data, AnyP::Po */ void addChainToSslContext(SSL_CTX *sslContext, STACK_OF(X509) *certList); +/** + \ingroup ServerProtocolSSLAPI + * Configures sslContext to use squid untrusted certificates internal list + * to complete certificate chains when verifies SSL servers certificates. + */ +void useSquidUntrusted(SSL_CTX *sslContext); + /** \ingroup ServerProtocolSSLAPI * Read certificate, private key and any certificates which must be chained from files.