/* * Copyright (C) 1996-2024 The Squid Software Foundation and contributors * * Squid software is distributed under GPLv2+ license and includes * contributions from numerous individuals and organizations. * Please see the COPYING and CONTRIBUTORS files for details. */ #include "squid.h" #include "base/HardFun.h" #include "base/TextException.h" #include "sbuf/Stream.h" #include "security/cert_generators/file/certificate_db.h" #include #include #include #include #if HAVE_SYS_STAT_H #include #endif #if HAVE_FCNTL_H #include #endif #if HAVE_SYS_FILE_H #include #endif Ssl::Lock::Lock(std::string const &aFilename) : filename(aFilename), #if _SQUID_WINDOWS_ hFile(INVALID_HANDLE_VALUE) #else fd(-1) #endif { } bool Ssl::Lock::locked() const { #if _SQUID_WINDOWS_ return hFile != INVALID_HANDLE_VALUE; #else return fd != -1; #endif } void Ssl::Lock::lock() { #if _SQUID_WINDOWS_ hFile = CreateFile(TEXT(filename.c_str()), GENERIC_READ, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); if (hFile == INVALID_HANDLE_VALUE) #else fd = open(filename.c_str(), O_RDWR); if (fd == -1) #endif throw TextException(ToSBuf("Failed to open file ", filename), Here()); #if _SQUID_WINDOWS_ if (!LockFile(hFile, 0, 0, 1, 0)) #elif _SQUID_SOLARIS_ if (lockf(fd, F_LOCK, 0) != 0) #else if (flock(fd, LOCK_EX) != 0) #endif throw TextException(ToSBuf("Failed to get a lock of ", filename), Here()); } void Ssl::Lock::unlock() { #if _SQUID_WINDOWS_ if (hFile != INVALID_HANDLE_VALUE) { UnlockFile(hFile, 0, 0, 1, 0); CloseHandle(hFile); hFile = INVALID_HANDLE_VALUE; } #else if (fd != -1) { #if _SQUID_SOLARIS_ lockf(fd, F_ULOCK, 0); #else flock(fd, LOCK_UN); #endif close(fd); fd = -1; } #endif else throw TextException(ToSBuf("Lock is already unlocked for ", filename), Here()); } Ssl::Lock::~Lock() { if (locked()) unlock(); } Ssl::Locker::Locker(Lock &aLock, const SourceLocation &aCaller): weLocked(false), lock(aLock), caller(aCaller) { if (!lock.locked()) { lock.lock(); weLocked = true; } } Ssl::Locker::~Locker() { if (weLocked) lock.unlock(); } Ssl::CertificateDb::Row::Row() : width(cnlNumber) { row = (char **)OPENSSL_malloc(sizeof(char *) * (width + 1)); for (size_t i = 0; i < width + 1; ++i) row[i] = nullptr; } Ssl::CertificateDb::Row::Row(char **aRow, size_t aWidth): width(aWidth) { row = aRow; } Ssl::CertificateDb::Row::~Row() { if (!row) return; void *max; if ((max = (void *)row[width]) != nullptr) { // It is an openSSL allocated row. The TXT_DB_read function stores the // index and row items one one memory segment. The row[width] points // to the end of buffer. We have to check for items in the array which // are not stored in this segment. These items should released. for (size_t i = 0; i < width + 1; ++i) { if (((row[i] < (char *)row) || (row[i] > max)) && (row[i] != nullptr)) OPENSSL_free(row[i]); } } else { for (size_t i = 0; i < width + 1; ++i) { if (row[i]) OPENSSL_free(row[i]); } } OPENSSL_free(row); } void Ssl::CertificateDb::Row::reset() { row = nullptr; } void Ssl::CertificateDb::Row::setValue(size_t cell, char const * value) { assert(cell < width); if (row[cell]) { free(row[cell]); } if (value) { row[cell] = static_cast(OPENSSL_malloc(sizeof(char) * (strlen(value) + 1))); memcpy(row[cell], value, sizeof(char) * (strlen(value) + 1)); } else row[cell] = nullptr; } char ** Ssl::CertificateDb::Row::getRow() { return row; } void Ssl::CertificateDb::sq_TXT_DB_delete(TXT_DB *db, const char **row) { if (!db) return; #if SQUID_SSLTXTDB_PSTRINGDATA for (int i = 0; i < sk_OPENSSL_PSTRING_num(db->data); ++i) { #if SQUID_STACKOF_PSTRINGDATA_HACK const char ** current_row = ((const char **)sk_value(CHECKED_STACK_OF(OPENSSL_PSTRING, db->data), i)); #else const char ** current_row = ((const char **)sk_OPENSSL_PSTRING_value(db->data, i)); #endif #else for (int i = 0; i < sk_num(db->data); ++i) { const char ** current_row = ((const char **)sk_value(db->data, i)); #endif if (current_row == row) { sq_TXT_DB_delete_row(db, i); return; } } } #define countof(arr) (sizeof(arr)/sizeof(*arr)) void Ssl::CertificateDb::sq_TXT_DB_delete_row(TXT_DB *db, int idx) { char **rrow; #if SQUID_SSLTXTDB_PSTRINGDATA rrow = (char **)sk_OPENSSL_PSTRING_delete(db->data, idx); #else rrow = (char **)sk_delete(db->data, idx); #endif if (!rrow) return; Row row(rrow, cnlNumber); // row wrapper used to free the rrow const Columns db_indexes[]= {cnlSerial, cnlKey}; for (unsigned int i = 0; i < countof(db_indexes); ++i) { void *data = nullptr; #if SQUID_SSLTXTDB_PSTRINGDATA if (LHASH_OF(OPENSSL_STRING) *fieldIndex = db->index[db_indexes[i]]) data = lh_OPENSSL_STRING_delete(fieldIndex, rrow); #else if (LHASH *fieldIndex = db->index[db_indexes[i]]) data = OPENSSL_LH_delete(fieldIndex, rrow); #endif if (data) assert(data == rrow); } } unsigned long Ssl::CertificateDb::index_serial_hash(const char **a) { const char *n = a[Ssl::CertificateDb::cnlSerial]; while (*n == '0') ++n; return OPENSSL_LH_strhash(n); } int Ssl::CertificateDb::index_serial_cmp(const char **a, const char **b) { const char *aa, *bb; for (aa = a[Ssl::CertificateDb::cnlSerial]; *aa == '0'; ++aa); for (bb = b[Ssl::CertificateDb::cnlSerial]; *bb == '0'; ++bb); return strcmp(aa, bb); } unsigned long Ssl::CertificateDb::index_name_hash(const char **a) { return(OPENSSL_LH_strhash(a[Ssl::CertificateDb::cnlKey])); } int Ssl::CertificateDb::index_name_cmp(const char **a, const char **b) { return(strcmp(a[Ssl::CertificateDb::cnlKey], b[CertificateDb::cnlKey])); } const std::string Ssl::CertificateDb::db_file("index.txt"); const std::string Ssl::CertificateDb::cert_dir("certs"); const std::string Ssl::CertificateDb::size_file("size"); Ssl::CertificateDb::CertificateDb(std::string const & aDb_path, size_t aMax_db_size, size_t aFs_block_size) : db_path(aDb_path), db_full(aDb_path + "/" + db_file), cert_full(aDb_path + "/" + cert_dir), size_full(aDb_path + "/" + size_file), max_db_size(aMax_db_size), fs_block_size((aFs_block_size ? aFs_block_size : 2048)), dbLock(db_full) {} bool Ssl::CertificateDb::find(std::string const &key, const Security::CertPointer &expectedOrig, Security::CertPointer &cert, Security::PrivateKeyPointer &pkey) { const Locker locker(dbLock, Here()); load(); return pure_find(key, expectedOrig, cert, pkey); } bool Ssl::CertificateDb::purgeCert(std::string const & key) { const Locker locker(dbLock, Here()); load(); if (!db) return false; if (!deleteByKey(key)) return false; save(); return true; } bool Ssl::CertificateDb::addCertAndPrivateKey(std::string const &useKey, const Security::CertPointer &cert, const Security::PrivateKeyPointer &pkey, const Security::CertPointer &orig) { const Locker locker(dbLock, Here()); load(); if (!db || !cert || !pkey) return false; if(useKey.empty()) return false; Row row; ASN1_INTEGER * ai = X509_get_serialNumber(cert.get()); std::string serial_string; Ssl::BIGNUM_Pointer serial(ASN1_INTEGER_to_BN(ai, nullptr)); { const UniqueCString hex_bn(BN_bn2hex(serial.get())); serial_string = std::string(hex_bn.get()); } row.setValue(cnlSerial, serial_string.c_str()); char ** rrow = TXT_DB_get_by_index(db.get(), cnlSerial, row.getRow()); // We are creating certificates with unique serial numbers. If the serial // number is found in the database, the same certificate is already stored. if (rrow != nullptr) { // TODO: check if the stored row is valid. return true; } // Remove any entry with given key deleteByKey(useKey); // check db size while trying to minimize calls to size() size_t dbSize = size(); if ((dbSize == 0 && hasRows()) || (dbSize > 0 && !hasRows()) || (dbSize > 10 * max_db_size)) { // Invalid database size, rebuild dbSize = rebuildSize(); } while (dbSize > max_db_size && deleteInvalidCertificate()) { dbSize = size(); // get the current database size // and try to find another invalid certificate if needed } // there are no more invalid ones, but there must be valid certificates while (dbSize > max_db_size) { if (!deleteOldestCertificate()) { rebuildSize(); // No certificates in database.Update the size file. save(); // Some entries may have been removed. Update the index file. return false; // errors prevented us from freeing enough space } dbSize = size(); // get the current database size } const auto tm = X509_getm_notAfter(cert.get()); row.setValue(cnlExp_date, std::string(reinterpret_cast(tm->data), tm->length).c_str()); const auto subject = OneLineSummary(*X509_get_subject_name(cert.get())); row.setValue(cnlName, subject.get()); row.setValue(cnlKey, useKey.c_str()); if (!TXT_DB_insert(db.get(), row.getRow())) { // failed to add index (???) but we may have already modified // the database so save before exit save(); return false; } rrow = row.getRow(); row.reset(); std::string filename(cert_full + "/" + serial_string + ".pem"); if (!WriteEntry(filename.c_str(), cert, pkey, orig)) { //remove row from txt_db and save sq_TXT_DB_delete(db.get(), (const char **)rrow); save(); return false; } addSize(filename); save(); return true; } void Ssl::CertificateDb::Create(std::string const & db_path) { if (db_path == "") throw TextException("Path to db is empty", Here()); std::string db_full(db_path + "/" + db_file); std::string cert_full(db_path + "/" + cert_dir); std::string size_full(db_path + "/" + size_file); if (mkdir(db_path.c_str(), 0777)) throw TextException(ToSBuf("Cannot create ", db_path), Here()); if (mkdir(cert_full.c_str(), 0777)) throw TextException(ToSBuf("Cannot create ", cert_full), Here()); std::ofstream size(size_full.c_str()); if (size) size << 0; else throw TextException(ToSBuf("Cannot open ", size_full, " to open"), Here()); std::ofstream db(db_full.c_str()); if (!db) throw TextException(ToSBuf("Cannot open ", db_full, " to open"), Here()); } void Ssl::CertificateDb::Check(std::string const & db_path, size_t max_db_size, size_t fs_block_size) { CertificateDb db(db_path, max_db_size, fs_block_size); db.load(); // Call readSize to force rebuild size file in the case it is corrupted (void)db.readSize(); } size_t Ssl::CertificateDb::rebuildSize() { size_t dbSize = 0; #if SQUID_SSLTXTDB_PSTRINGDATA for (int i = 0; i < sk_OPENSSL_PSTRING_num(db.get()->data); ++i) { #if SQUID_STACKOF_PSTRINGDATA_HACK const char ** current_row = ((const char **)sk_value(CHECKED_STACK_OF(OPENSSL_PSTRING, db.get()->data), i)); #else const char ** current_row = ((const char **)sk_OPENSSL_PSTRING_value(db.get()->data, i)); #endif #else for (int i = 0; i < sk_num(db.get()->data); ++i) { const char ** current_row = ((const char **)sk_value(db.get()->data, i)); #endif const std::string filename(cert_full + "/" + current_row[cnlSerial] + ".pem"); const size_t fSize = getFileSize(filename); dbSize += fSize; } writeSize(dbSize); return dbSize; } bool Ssl::CertificateDb::pure_find(std::string const &key, const Security::CertPointer &expectedOrig, Security::CertPointer &cert, Security::PrivateKeyPointer &pkey) { if (!db) return false; Row row; row.setValue(cnlKey, key.c_str()); char **rrow = TXT_DB_get_by_index(db.get(), cnlKey, row.getRow()); if (rrow == nullptr) return false; if (!sslDateIsInTheFuture(rrow[cnlExp_date])) return false; Security::CertPointer storedOrig; // read cert and pkey from file. std::string filename(cert_full + "/" + rrow[cnlSerial] + ".pem"); if (!ReadEntry(filename.c_str(), cert, pkey, storedOrig)) return false; if (!storedOrig && !expectedOrig) return true; else return Ssl::CertificatesCmp(expectedOrig, storedOrig); } size_t Ssl::CertificateDb::size() { return readSize(); } void Ssl::CertificateDb::addSize(std::string const & filename) { // readSize will rebuild 'size' file if missing or it is corrupted size_t dbSize = readSize(); dbSize += getFileSize(filename); writeSize(dbSize); } void Ssl::CertificateDb::subSize(std::string const & filename) { // readSize will rebuild 'size' file if missing or it is corrupted size_t dbSize = readSize(); const size_t fileSize = getFileSize(filename); dbSize = dbSize > fileSize ? dbSize - fileSize : 0; writeSize(dbSize); } size_t Ssl::CertificateDb::readSize() { std::ifstream ifstr(size_full.c_str()); size_t db_size = 0; if (!ifstr || !(ifstr >> db_size)) return rebuildSize(); return db_size; } void Ssl::CertificateDb::writeSize(size_t db_size) { std::ofstream ofstr(size_full.c_str()); if (!ofstr) throw TextException(ToSBuf("cannot write \"", size_full, "\" file"), Here()); ofstr << db_size; } size_t Ssl::CertificateDb::getFileSize(std::string const & filename) { std::ifstream file(filename.c_str(), std::ios::binary); if (!file) return 0; file.seekg(0, std::ios_base::end); const std::streampos file_size = file.tellg(); if (file_size < 0) return 0; return ((static_cast(file_size) + fs_block_size - 1) / fs_block_size) * fs_block_size; } void Ssl::CertificateDb::load() { // Load db from file. Ssl::BIO_Pointer in(BIO_new(BIO_s_file())); if (!in || BIO_read_filename(in.get(), db_full.c_str()) <= 0) throw TextException(ToSBuf("Uninitialized SSL certificate database directory: ", db_path, ". To initialize, run \"security_file_certgen -c -s ", db_path, "\"."), Here()); bool corrupt = false; Ssl::TXT_DB_Pointer temp_db(TXT_DB_read(in.get(), cnlNumber)); if (!temp_db) corrupt = true; // Create indexes in db. if (!corrupt && !TXT_DB_create_index(temp_db.get(), cnlSerial, nullptr, LHASH_HASH_FN(index_serial_hash), LHASH_COMP_FN(index_serial_cmp))) corrupt = true; if (!corrupt && !TXT_DB_create_index(temp_db.get(), cnlKey, nullptr, LHASH_HASH_FN(index_name_hash), LHASH_COMP_FN(index_name_cmp))) corrupt = true; if (corrupt) throw TextException(ToSBuf("The SSL certificate database ", db_path, " is corrupted. Please rebuild"), Here()); db.reset(temp_db.release()); } void Ssl::CertificateDb::save() { if (!db) throw TextException("The certificates database is not loaded", Here()); // To save the db to file, create a new BIO with BIO file methods. Ssl::BIO_Pointer out(BIO_new(BIO_s_file())); if (!out || !BIO_write_filename(out.get(), const_cast(db_full.c_str()))) throw TextException(ToSBuf("Failed to initialize ", db_full, " file for writing"), Here()); if (TXT_DB_write(out.get(), db.get()) < 0) throw TextException(ToSBuf("Failed to write ", db_full, " file"), Here()); } // Normally defined in defines.h file void Ssl::CertificateDb::deleteRow(const char **row, int rowIndex) { const std::string filename(cert_full + "/" + row[cnlSerial] + ".pem"); sq_TXT_DB_delete_row(db.get(), rowIndex); subSize(filename); int ret = remove(filename.c_str()); if (ret < 0 && errno != ENOENT) throw TextException(ToSBuf("Failed to remove certificate file ", filename, " from db"), Here()); } bool Ssl::CertificateDb::deleteInvalidCertificate() { if (!db) return false; bool removed_one = false; #if SQUID_SSLTXTDB_PSTRINGDATA for (int i = 0; i < sk_OPENSSL_PSTRING_num(db.get()->data); ++i) { #if SQUID_STACKOF_PSTRINGDATA_HACK const char ** current_row = ((const char **)sk_value(CHECKED_STACK_OF(OPENSSL_PSTRING, db.get()->data), i)); #else const char ** current_row = ((const char **)sk_OPENSSL_PSTRING_value(db.get()->data, i)); #endif #else for (int i = 0; i < sk_num(db.get()->data); ++i) { const char ** current_row = ((const char **)sk_value(db.get()->data, i)); #endif if (!sslDateIsInTheFuture(current_row[cnlExp_date])) { deleteRow(current_row, i); removed_one = true; break; } } if (!removed_one) return false; return true; } bool Ssl::CertificateDb::deleteOldestCertificate() { if (!hasRows()) return false; #if SQUID_SSLTXTDB_PSTRINGDATA #if SQUID_STACKOF_PSTRINGDATA_HACK const char **row = ((const char **)sk_value(CHECKED_STACK_OF(OPENSSL_PSTRING, db.get()->data), 0)); #else const char **row = (const char **)sk_OPENSSL_PSTRING_value(db.get()->data, 0); #endif #else const char **row = (const char **)sk_value(db.get()->data, 0); #endif deleteRow(row, 0); return true; } bool Ssl::CertificateDb::deleteByKey(std::string const & key) { if (!db) return false; #if SQUID_SSLTXTDB_PSTRINGDATA for (int i = 0; i < sk_OPENSSL_PSTRING_num(db.get()->data); ++i) { #if SQUID_STACKOF_PSTRINGDATA_HACK const char ** current_row = ((const char **)sk_value(CHECKED_STACK_OF(OPENSSL_PSTRING, db.get()->data), i)); #else const char ** current_row = ((const char **)sk_OPENSSL_PSTRING_value(db.get()->data, i)); #endif #else for (int i = 0; i < sk_num(db.get()->data); ++i) { const char ** current_row = ((const char **)sk_value(db.get()->data, i)); #endif if (key == current_row[cnlKey]) { deleteRow(current_row, i); return true; } } return false; } bool Ssl::CertificateDb::hasRows() const { if (!db) return false; #if SQUID_SSLTXTDB_PSTRINGDATA if (sk_OPENSSL_PSTRING_num(db.get()->data) == 0) #else if (sk_num(db.get()->data) == 0) #endif return false; return true; } bool Ssl::CertificateDb::WriteEntry(const std::string &filename, const Security::CertPointer &cert, const Security::PrivateKeyPointer &pkey, const Security::CertPointer &orig) { Ssl::BIO_Pointer bio; if (!Ssl::OpenCertsFileForWriting(bio, filename.c_str())) return false; if (!Ssl::WriteX509Certificate(bio, cert)) return false; if (!Ssl::WritePrivateKey(bio, pkey)) return false; if (orig && !Ssl::WriteX509Certificate(bio, orig)) return false; return true; } bool Ssl::CertificateDb::ReadEntry(std::string filename, Security::CertPointer &cert, Security::PrivateKeyPointer &pkey, Security::CertPointer &orig) { Ssl::BIO_Pointer bio; if (!Ssl::OpenCertsFileForReading(bio, filename.c_str())) return false; cert = Ssl::ReadCertificate(bio); if (!Ssl::ReadPrivateKey(bio, pkey, nullptr)) return false; orig = Ssl::ReadOptionalCertificate(bio); return true; }