XRootD
Loading...
Searching...
No Matches
XrdTlsTempCA.cc
Go to the documentation of this file.
1/******************************************************************************/
2/* */
3/* X r d T l s T e m p C A . c c */
4/* */
5/* (c) 2021 by the Board of Trustees of the Leland Stanford, Jr., University */
6/* Produced by Brian Bockelman */
7/* */
8/* This file is part of the XRootD software suite. */
9/* */
10/* XRootD is free software: you can redistribute it and/or modify it under */
11/* the terms of the GNU Lesser General Public License as published by the */
12/* Free Software Foundation, either version 3 of the License, or (at your */
13/* option) any later version. */
14/* */
15/* XRootD is distributed in the hope that it will be useful, but WITHOUT */
16/* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or */
17/* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public */
18/* License for more details. */
19/* */
20/* You should have received a copy of the GNU Lesser General Public License */
21/* along with XRootD in a file called COPYING.LESSER (LGPL license) and file */
22/* COPYING (GPL license). If not, see <http://www.gnu.org/licenses/>. */
23/* */
24/* The copyright holder's institutional names and contributor's names may not */
25/* be used to endorse or promote products derived from this software without */
26/* specific prior written permission of the institution or contributor. */
27/******************************************************************************/
28
29
30#include <cstdlib>
31#include <fcntl.h>
32#include <dirent.h>
33#include <poll.h>
34
35#include <unordered_set>
36#include <memory>
37
38#include "XrdSys/XrdSysError.hh"
39#include "XrdSys/XrdSysFD.hh"
44
45#include "XrdTlsTempCA.hh"
46
47#include <sstream>
48#include <vector>
49#include <atomic>
50
51namespace {
52
53typedef std::unique_ptr<FILE, int(*)(FILE*)> file_smart_ptr;
54
55
56static uint64_t monotonic_time_s() {
57 struct timespec tp;
58 clock_gettime(CLOCK_MONOTONIC, &tp);
59 return tp.tv_sec + (tp.tv_nsec >= 500000000);
60}
61
66class Set {
67public:
68 Set(int output_fd, XrdSysError & err) : m_log(err),m_output_fp(file_smart_ptr(fdopen(XrdSysFD_Dup(output_fd), "w"), &fclose)){
69 if(!m_output_fp.get()) {
70 m_output_fp.reset();
71 }
72 }
73 virtual ~Set() = default;
74protected:
75 // Reference to the logging that can be used by the inheriting classes.
76 XrdSysError &m_log;
77 // Pointer to the CA or CRL output file
78 file_smart_ptr m_output_fp;
79};
80
81class CASet : public Set {
82public:
83 CASet(int output_fd, XrdSysError &err):Set(output_fd,err){}
84
96 bool processFile(file_smart_ptr &fd, const std::string &fname);
97
98private:
99
100 // Grid CA directories tend to keep everything in triplicate;
101 // we keep a unique hash of all known CAs so we write out each
102 // one only once.
103 std::unordered_set<std::string> m_known_cas;
104};
105
106
107bool
108CASet::processFile(file_smart_ptr &fp, const std::string &fname)
109{
110 XrdCryptoX509Chain chain;
111 // Not checking return value here; function returns `0` on error and
112 // if no certificate is found.
113 XrdCryptosslX509ParseFile(fp.get(), &chain, fname.c_str());
114
115 auto ca = chain.Begin();
116 if (!m_output_fp.get()) {
117 m_log.Emsg("CAset", "No output file has been opened", fname.c_str());
118 chain.Cleanup();
119 return false;
120 }
121 while (ca) {
122 auto hash_ptr = ca->SubjectHash();
123 if (!hash_ptr) {
124 continue;
125 }
126 auto iter = m_known_cas.find(hash_ptr);
127 if (iter != m_known_cas.end()) {
128 //m_log.Emsg("CAset", "Skipping known CA with hash", fname.c_str(), hash_ptr);
129 ca = chain.Next();
130 continue;
131 }
132 //m_log.Emsg("CAset", "New CA with hash", fname.c_str(), hash_ptr);
133 m_known_cas.insert(hash_ptr);
134
135 if (XrdCryptosslX509ToFile(ca, m_output_fp.get(), fname.c_str())) {
136 m_log.Emsg("CAset", "Failed to write out CA", fname.c_str());
137 chain.Cleanup();
138 return false;
139 }
140 ca = chain.Next();
141 }
142 fflush(m_output_fp.get());
143 chain.Cleanup();
144
145 return true;
146}
147
148
149class CRLSet : public Set {
150public:
151 CRLSet(int output_fd, XrdSysError &err):Set(output_fd,err){}
163 bool processFile(file_smart_ptr &fd, const std::string &fname);
169 bool atLeastOneValidCRLFound() const;
176 bool processCRLWithCriticalExt();
177
178private:
179
180 // Grid CA directories tend to keep everything in triplicate;
181 // we keep a unique hash of all known CRLs so we write out each
182 // one only once.
183 std::unordered_set<std::string> m_known_crls;
184 std::atomic<bool> m_atLeastOneValidCRLFound;
185 //Store the CRLs containing critical extensions to defer their insertion
186 //at the end of the bundled CRL file. Issue https://github.com/xrootd/xrootd/issues/2065
187 std::vector<std::unique_ptr<XrdCryptosslX509Crl>> m_crls_critical_extension;
188};
189
190
191bool
192CRLSet::processFile(file_smart_ptr &fp, const std::string &fname)
193{
194 if (!m_output_fp.get()) {
195 m_log.Emsg("CRLSet", "No output file has been opened", fname.c_str());
196 return false;
197 }
198 // Assume we can safely ignore a failure to parse; we load every file in
199 // the directory and that will naturally include a number of non-CRL files.
200 for (std::unique_ptr<XrdCryptosslX509Crl> xrd_crl(new XrdCryptosslX509Crl(fp.get(), fname.c_str()));
201 xrd_crl->IsValid();
202 xrd_crl = std::unique_ptr<XrdCryptosslX509Crl>(new XrdCryptosslX509Crl(fp.get(), fname.c_str())))
203 {
204 auto hash_ptr = xrd_crl->IssuerHash(1);
205 if (!hash_ptr) {
206 continue;
207 }
208 m_atLeastOneValidCRLFound = true;
209 auto iter = m_known_crls.find(hash_ptr);
210 if (iter != m_known_crls.end()) {
211 //m_log.Emsg("CRLset", "Skipping known CRL with hash", fname.c_str(), hash_ptr);
212 continue;
213 }
214 //m_log.Emsg("CRLset", "New CRL with hash", fname.c_str(), hash_ptr);
215 m_known_crls.insert(hash_ptr);
216
217 if(xrd_crl->hasCriticalExtension()) {
218 // Issue https://github.com/xrootd/xrootd/issues/2065
219 // This CRL will be put at the end of the bundled file
220 m_crls_critical_extension.emplace_back(std::move(xrd_crl));
221 } else {
222 // No critical extension found on that CRL, just insert it on the CRL bundled file
223 if (!xrd_crl->ToFile(m_output_fp.get())) {
224 m_log.Emsg("CRLset", "Failed to write out CRL", fname.c_str());
225 fflush(m_output_fp.get());
226 return false;
227 }
228 }
229 }
230 fflush(m_output_fp.get());
231
232 return true;
233}
234
235bool CRLSet::atLeastOneValidCRLFound() const {
236 return m_atLeastOneValidCRLFound;
237}
238
239bool CRLSet::processCRLWithCriticalExt() {
240 if(!m_crls_critical_extension.empty()) {
241 if (!m_output_fp.get()) {
242 m_log.Emsg("CRLSet", "No output file has been opened to add CRLs with critical extension");
243 return false;
244 }
245 for (const auto &crl: m_crls_critical_extension) {
246 if (!crl->ToFile(m_output_fp.get())) {
247 m_log.Emsg("CRLset", "Failed to write out CRL with critical extension", crl->ParentFile());
248 fflush(m_output_fp.get());
249 return false;
250 }
251 }
252 fflush(m_output_fp.get());
253 }
254 return true;
255}
256
257}
258
259
260std::unique_ptr<XrdTlsTempCA::TempCAGuard>
261XrdTlsTempCA::TempCAGuard::create(XrdSysError &err, const std::string &ca_tmp_dir) {
262
263 if (-1 == mkdir(ca_tmp_dir.c_str(), S_IRWXU) && errno != EEXIST) {
264 err.Emsg("TempCA", "Unable to create CA temp directory", ca_tmp_dir.c_str(), strerror(errno));
265 }
266
267 std::stringstream ss;
268 ss << ca_tmp_dir << "/ca_file.XXXXXX.pem";
269 std::vector<char> ca_fname;
270 ca_fname.resize(ss.str().size() + 1);
271 memcpy(ca_fname.data(), ss.str().c_str(), ss.str().size());
272
273 int ca_fd = mkstemps(ca_fname.data(), 4);
274 if (ca_fd < 0) {
275 err.Emsg("TempCA", "Failed to create temp file:", strerror(errno));
276 return std::unique_ptr<TempCAGuard>();
277 }
278
279 std::stringstream ss2;
280 ss2 << ca_tmp_dir << "/crl_file.XXXXXX.pem";
281 std::vector<char> crl_fname;
282 crl_fname.resize(ss2.str().size() + 1);
283 memcpy(crl_fname.data(), ss2.str().c_str(), ss2.str().size());
284
285 int crl_fd = mkstemps(crl_fname.data(), 4);
286 if (crl_fd < 0) {
287 err.Emsg("TempCA", "Failed to create temp file:", strerror(errno));
288 return std::unique_ptr<TempCAGuard>();
289 }
290 return std::unique_ptr<TempCAGuard>(new TempCAGuard(ca_fd, crl_fd, ca_tmp_dir, ca_fname.data(), crl_fname.data()));
291}
292
293
295 if (m_ca_fd >= 0) {
296 unlink(m_ca_fname.c_str());
297 close(m_ca_fd);
298 }
299 if (m_crl_fd >= 0) {
300 unlink(m_crl_fname.c_str());
301 close(m_crl_fd);
302 }
303}
304
305
306bool
308 if (m_ca_fd < 0 || m_ca_tmp_dir.empty()) {return false;}
309 close(m_ca_fd);
310 m_ca_fd = -1;
311 std::string ca_fname = m_ca_tmp_dir + "/ca_file.pem";
312 if (-1 == rename(m_ca_fname.c_str(), ca_fname.c_str())) {
313 return false;
314 }
315 m_ca_fname = ca_fname;
316
317 if (m_crl_fd < 0 || m_ca_tmp_dir.empty()) {return false;}
318 close(m_crl_fd);
319 m_crl_fd = -1;
320 std::string crl_fname = m_ca_tmp_dir + "/crl_file.pem";
321 if (-1 == rename(m_crl_fname.c_str(), crl_fname.c_str())) {
322 return false;
323 }
324 m_crl_fname = crl_fname;
325
326 return true;
327}
328
329
330XrdTlsTempCA::TempCAGuard::TempCAGuard(int ca_fd, int crl_fd, const std::string &ca_tmp_dir, const std::string &ca_fname, const std::string &crl_fname)
331 : m_ca_fd(ca_fd), m_crl_fd(crl_fd), m_ca_tmp_dir(ca_tmp_dir), m_ca_fname(ca_fname), m_crl_fname(crl_fname)
332 {}
333
334
335XrdTlsTempCA::XrdTlsTempCA(XrdSysError *err, std::string ca_dir, bool build_store)
336 : m_log(*err),
337 m_ca_dir(ca_dir),
338 m_build_store(build_store)
339{
340 // Setup communication pipes; we write one byte to the child to tell it to shutdown;
341 // it'll write one byte back to acknowledge before our destructor exits.
342 int pipes[2];
343 if (-1 == XrdSysFD_Pipe(pipes)) {
344 m_log.Emsg("XrdTlsTempCA", "Failed to create communication pipes", strerror(errno));
345 return;
346 }
347 m_maintenance_pipe_r = pipes[0];
348 m_maintenance_pipe_w = pipes[1];
349 if (-1 == XrdSysFD_Pipe(pipes)) {
350 m_log.Emsg("XrdTlsTempCA", "Failed to create communication pipes", strerror(errno));
351 return;
352 }
353 m_maintenance_thread_pipe_r = pipes[0];
354 m_maintenance_thread_pipe_w = pipes[1];
355 if (!Maintenance()) {return;}
356
357 pthread_t tid;
358 auto rc = XrdSysThread::Run(&tid, XrdTlsTempCA::MaintenanceThread,
359 static_cast<void*>(this), 0, "CA/CRL refresh");
360 if (rc) {
361 m_log.Emsg("XrdTlsTempCA", "Failed to launch CA monitoring thread");
362 m_ca_file.reset();
363 m_crl_file.reset();
364 }
365}
366
367
369{
370 char indicator[1];
371 if (m_maintenance_pipe_w >= 0) {
372 indicator[0] = '1';
373 int rval;
374 do {rval = write(m_maintenance_pipe_w, indicator, 1);} while (rval != -1 || errno == EINTR);
375 if (m_maintenance_thread_pipe_r >= 0) {
376 do {rval = read(m_maintenance_thread_pipe_r, indicator, 1);} while (rval != -1 || errno == EINTR);
377 close(m_maintenance_thread_pipe_r);
378 close(m_maintenance_thread_pipe_w);
379 }
380 close(m_maintenance_pipe_r);
381 close(m_maintenance_pipe_w);
382 }
383}
384
385
386std::shared_ptr<X509_STORE>
387XrdTlsTempCA::BuildCAStore(const std::string &ca_fname, const std::string &crl_fname,
388 bool use_crls)
389{
390 std::shared_ptr<X509_STORE> store(X509_STORE_new(), &X509_STORE_free);
391 if (!store) {
392 m_log.Emsg("TempCA", "Failed to allocate a certificate store");
393 return nullptr;
394 }
395
396 if (1 != X509_STORE_load_locations(store.get(), ca_fname.c_str(), nullptr)) {
397 m_log.Emsg("TempCA", "Failed to load the CA bundle into the certificate store",
398 ca_fname.c_str());
399 return nullptr;
400 }
401
402 // The verification flags below mirror what libcurl applies when it is handed
403 // these same files through CURLOPT_CAINFO / CURLOPT_CRLFILE; see
404 // ossl_populate_x509_store() in its lib/vtls/openssl.c. Consumers install this
405 // store in place of the one libcurl built, so any flag left out here is lost.
406 unsigned long x509flags;
407
408 if (use_crls) {
409 X509_LOOKUP *lookup = X509_STORE_add_lookup(store.get(), X509_LOOKUP_file());
410 if (!lookup) {
411 m_log.Emsg("TempCA", "Failed to add a file lookup to the certificate store");
412 return nullptr;
413 }
414 if (X509_load_crl_file(lookup, crl_fname.c_str(), X509_FILETYPE_PEM) <= 0) {
415 m_log.Emsg("TempCA", "Failed to load the CRL bundle into the certificate store",
416 crl_fname.c_str());
417 return nullptr;
418 }
419 x509flags = X509_V_FLAG_CRL_CHECK | X509_V_FLAG_CRL_CHECK_ALL;
420 } else {
421 // Treat non-self-signed certificates in the store as trust anchors, so that
422 // a server can be verified from an intermediate alone. This is not an
423 // OpenSSL default, but libcurl enables it unless asked not to, so leaving it
424 // out would reject chains that are accepted today. It is deliberately not
425 // combined with CRL checking, which OpenSSL does not support:
426 // https://github.com/openssl/openssl/issues/5081
427 x509flags = X509_V_FLAG_PARTIAL_CHAIN;
428 }
429
430 X509_STORE_set_flags(store.get(), x509flags);
431
432 // Purely an optimization; OpenSSL sorts the store itself when it needs to.
433 // Adding objects to a store leaves its lookup stack unsorted, and OpenSSL
434 // sorts lazily on first use -- under a write lock, while every other thread
435 // verifying against this store waits. Sorting here means the concurrent
436 // verifications that follow a reload only ever need the read lock.
437 sk_X509_OBJECT_sort(X509_STORE_get0_objects(store.get()));
438
439 return store;
440}
441
442
443bool
444XrdTlsTempCA::Maintenance()
445{
446 m_log.Emsg("TempCA", "Reloading the list of CAs and CRLs in directory");
447
448 auto adminpath = getenv("XRDADMINPATH");
449 if (!adminpath) {
450 m_log.Emsg("TempCA", "Admin path is not set!");
451 return false;
452 }
453 std::string ca_tmp_dir = std::string(adminpath) + "/.xrdtls";
454
455 std::unique_ptr<TempCAGuard> new_file(TempCAGuard::create(m_log, ca_tmp_dir));
456 if (!new_file) {
457 m_log.Emsg("TempCA", "Failed to create a new temp CA / CRL file");
458 return false;
459 }
460
461 int fddir = XrdSysFD_Open(m_ca_dir.c_str(), O_DIRECTORY);
462 if (fddir < 0) {
463 m_log.Emsg("TempCA", "Failed to open the CA directory", m_ca_dir.c_str());
464 return false;
465 }
466
467 DIR *dirp = fdopendir(fddir);
468 if (!dirp) {
469 m_log.Emsg("Maintenance", "Failed to allocate a directory pointer");
470 return false;
471 }
472
473 struct dirent *result;
474 bool atLeastOneCRLFound = false;
475 errno = 0;
476 {
477 CASet ca_builder(new_file->getCAFD(), m_log);
478 CRLSet crl_builder(new_file->getCRLFD(), m_log);
479 while ((result = readdir(dirp))) {
480 //m_log.Emsg("Will parse file for CA certificates", result->d_name);
481 if (result->d_name[0] == '.') {continue;}
482 if (result->d_type != DT_REG)
483 {if (result->d_type != DT_UNKNOWN && result->d_type != DT_LNK)
484 continue;
485 struct stat Stat;
486 if (fstatat(fddir, result->d_name, &Stat, 0))
487 {m_log.Emsg("Maintenance", "Failed to stat certificate file",
488 result->d_name, strerror(errno));
489 continue;
490 }
491 if (!S_ISREG(Stat.st_mode)) continue;
492 }
493 int fd = XrdSysFD_Openat(fddir, result->d_name, O_RDONLY);
494 if (fd < 0) {
495 m_log.Emsg("Maintenance", "Failed to open certificate file", result->d_name, strerror(errno));
496 closedir(dirp);
497 return false;
498 }
499 file_smart_ptr fp(fdopen(fd, "r"), &fclose);
500
501 if (!ca_builder.processFile(fp, result->d_name)) {
502 m_log.Emsg("Maintenance", "Failed to process file for CAs", result->d_name);
503 }
504 rewind(fp.get());
505 if (!crl_builder.processFile(fp, result->d_name)) {
506 m_log.Emsg("Maintenance", "Failed to process file for CRLs", result->d_name);
507 }
508 errno = 0;
509 }
510 if (errno) {
511 m_log.Emsg("Maintenance", "Failure during readdir", strerror(errno));
512 closedir(dirp);
513 return false;
514 }
515 closedir(dirp);
516
517 if (!crl_builder.processCRLWithCriticalExt()) {
518 m_log.Emsg("Maintenance", "Failed to insert CRLs with critical extension for CRLs", result->d_name);
519 }
520 atLeastOneCRLFound = crl_builder.atLeastOneValidCRLFound();
521 }
522
523 if (!new_file->commit()) {
524 m_log.Emsg("Maintenance", "Failed to finalize new CA / CRL files");
525 return false;
526 }
527 //m_log.Emsg("Maintenance", "Successfully created CA and CRL files", new_file->getCAFilename().c_str(),
528 // new_file->getCRLFilename().c_str());
529 const std::string ca_fname = new_file->getCAFilename();
530 const std::string crl_fname = new_file->getCRLFilename();
531
532 std::shared_ptr<X509_STORE> new_store;
533 if (m_build_store) {
534 // An empty CRL bundle makes every verification fail, so CRL checking is only
535 // enabled once we know at least one CRL was written out.
536 // See https://github.com/xrootd/xrootd/issues/1543
537 struct stat crl_stat;
538 const bool use_crls = atLeastOneCRLFound
539 && !stat(crl_fname.c_str(), &crl_stat)
540 && crl_stat.st_size > 0;
541 if (!use_crls) {
542 std::stringstream ss;
543 ss << "No valid CRL file has been found in the file " << crl_fname
544 << ". Disabling CRL checking.";
545 m_log.Emsg("Maintenance", ss.str().c_str());
546 }
547
548 // Parse the bundles once here rather than once per consumer. This is the
549 // expensive part of a reload, so it is deliberately done before taking the
550 // lock that publishes the result.
551 new_store = BuildCAStore(ca_fname, crl_fname, use_crls);
552 if (!new_store) {
553 // Deliberately publish nothing and let the maintenance thread retry on
554 // the short interval, leaving consumers on the previous store. The
555 // tempting alternative -- carrying on and letting each consumer load the
556 // bundles for itself -- silently reinstates the per-transfer parsing that
557 // this store exists to avoid, turning a CA refresh problem into memory
558 // exhaustion. See https://github.com/xrootd/xrootd/issues/2873
559 m_log.Emsg("Maintenance", "Failed to build the certificate store; "
560 "retaining the previously loaded CAs and CRLs");
561 return false;
562 }
563 }
564
565 XrdSysMutexHelper lock(m_mutex);
566 m_ca_file.reset(new std::string(ca_fname));
567 m_crl_file.reset(new std::string(crl_fname));
568 m_atLeastOneCRLFound = atLeastOneCRLFound;
569 m_ca_store = std::move(new_store);
570
571 return true;
572}
573
574
575void *XrdTlsTempCA::MaintenanceThread(void *myself_raw)
576{
577 auto myself = static_cast<XrdTlsTempCA *>(myself_raw);
578
579 auto now = monotonic_time_s();
580 auto next_update = now + m_update_interval;
581 while (true) {
582 now = monotonic_time_s();
583 auto remaining = next_update - now;
584 struct pollfd fds;
585 fds.fd = myself->m_maintenance_pipe_r;
586 fds.events = POLLIN;
587 auto rval = poll(&fds, 1, remaining*1000);
588 if (rval == -1) {
589 if (rval == EINTR) continue;
590 else break;
591 } else if (rval == 0) { // timeout! Let's run maintenance.
592 if (myself->Maintenance()) {
593 next_update = monotonic_time_s() + m_update_interval;
594 } else {
595 next_update = monotonic_time_s() + m_update_interval_failure;
596 }
597 } else { // FD ready; let's shutdown
598 if (fds.revents & POLLIN) {
599 char indicator[1];
600 do {rval = read(myself->m_maintenance_pipe_r, indicator, 1);} while (rval != -1 || errno == EINTR);
601 }
602 }
603 }
604 if (errno) {
605 myself->m_log.Emsg("Maintenance", "Failed to poll for events from parent object");
606 }
607 char indicator = '1';
608 int rval;
609 do {rval = write(myself->m_maintenance_thread_pipe_w, &indicator, 1);} while (rval != -1 || errno == EINTR);
610
611 return nullptr;
612}
struct stat Stat
Definition XrdCks.cc:49
int XrdCryptosslX509ToFile(XrdCryptoX509 *x509, FILE *file, const char *fname)
int XrdCryptosslX509ParseFile(const char *fname, XrdCryptoX509Chain *chain, const char *fkey)
int fclose(FILE *stream)
int fflush(FILE *stream)
#define close(a)
Definition XrdPosix.hh:48
#define fstatat(a, b, c, d)
Definition XrdPosix.hh:64
#define write(a, b, c)
Definition XrdPosix.hh:121
#define mkdir(a, b)
Definition XrdPosix.hh:76
#define closedir(a)
Definition XrdPosix.hh:50
#define unlink(a)
Definition XrdPosix.hh:119
#define stat(a, b)
Definition XrdPosix.hh:105
#define rename(a, b)
Definition XrdPosix.hh:96
#define readdir(a)
Definition XrdPosix.hh:90
#define read(a, b, c)
Definition XrdPosix.hh:86
XrdCryptoX509 * Next()
XrdCryptoX509 * Begin()
void Cleanup(bool keepCA=0)
XrdCryptosslX509Crl(const char *crlf, int opt=0)
int Emsg(const char *esfx, int ecode, const char *text1, const char *text2=0)
static int Run(pthread_t *, void *(*proc)(void *), void *arg, int opts=0, const char *desc=0)
static std::unique_ptr< TempCAGuard > create(XrdSysError &, const std::string &ca_tmp_dir)
TempCAGuard(const TempCAGuard &)=delete
XrdTlsTempCA(XrdSysError *log, std::string ca_dir, bool build_store=true)