| /** @file |
| |
| SSL Context management |
| |
| @section license License |
| |
| Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you under the Apache License, Version 2.0 (the |
| "License"); you may not use this file except in compliance |
| with the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| */ |
| |
| #include "P_SSLCertLookup.h" |
| |
| #include "tscore/ink_config.h" |
| #include "tscore/I_Layout.h" |
| #include "tscore/MatcherUtils.h" |
| #include "tscore/Regex.h" |
| #include "tscore/Trie.h" |
| #include "tscore/BufferWriter.h" |
| #include "tscore/bwf_std_format.h" |
| #include "tscore/TestBox.h" |
| |
| #include "I_EventSystem.h" |
| |
| #include "P_SSLUtils.h" |
| #include "P_SSLConfig.h" |
| #include "SSLSessionTicket.h" |
| |
| #include <unordered_map> |
| #include <utility> |
| #include <vector> |
| #include <algorithm> |
| |
| struct SSLAddressLookupKey { |
| explicit SSLAddressLookupKey(const IpEndpoint &ip) |
| { |
| // For IP addresses, the cache key is the hex address with the port concatenated. This makes the |
| // lookup insensitive to address formatting and also allow the longest match semantic to produce |
| // different matches if there is a certificate on the port. |
| |
| ts::FixedBufferWriter w{key, sizeof(key)}; |
| w.print("{}", ts::bwf::Hex_Dump(ip)); // dump as raw hex bytes, don't format as IP address. |
| if (in_port_t port = ip.host_order_port(); port) { |
| sep = static_cast<unsigned char>(w.size()); |
| w.print(".{:x}", port); |
| } |
| w.write('\0'); // force C-string termination. |
| } |
| |
| const char * |
| get() const |
| { |
| return key; |
| } |
| void |
| split() |
| { |
| key[sep] = '\0'; |
| } |
| void |
| unsplit() |
| { |
| key[sep] = '.'; |
| } |
| |
| private: |
| char key[(TS_IP6_SIZE * 2) /* hex addr */ + 1 /* dot */ + 4 /* port */ + 1 /* nullptr */]; |
| unsigned char sep = 0; // offset of address/port separator |
| }; |
| |
| struct SSLContextStorage { |
| public: |
| SSLContextStorage(); |
| ~SSLContextStorage(); |
| |
| /// Add a cert context to storage |
| /// @return The @a host_store index or -1 on error. |
| int insert(const char *name, SSLCertContext const &cc); |
| |
| /// Add a cert context to storage. |
| /// @a idx must be a value returned by a previous call to insert. |
| /// This creates an alias, a different @a name referring to the same |
| /// cert context. |
| /// @return @a idx |
| int insert(const char *name, int idx); |
| SSLCertContext *lookup(const char *name); |
| void printWildDomains() const; |
| unsigned |
| count() const |
| { |
| return this->ctx_store.size(); |
| } |
| SSLCertContext * |
| get(unsigned i) |
| { |
| return &this->ctx_store[i]; |
| } |
| |
| private: |
| /** A struct that can be stored a @c Trie. |
| It contains the index of the real certificate and the |
| linkage required by @c Trie. |
| */ |
| struct ContextRef { |
| ContextRef() {} |
| explicit ContextRef(int n) : idx(n) {} |
| void |
| Print() const |
| { |
| Debug("ssl", "Item=%p SSL_CTX=#%d", this, idx); |
| } |
| int idx = -1; ///< Index in the context store. |
| LINK(ContextRef, link); ///< Require by @c Trie |
| }; |
| |
| /// We can only match one layer with the wildcards |
| /// This table stores the wildcarded subdomain |
| std::unordered_map<std::string, int> wilddomains; |
| /// Contexts stored by IP address or FQDN |
| std::unordered_map<std::string, int> hostnames; |
| /// List for cleanup. |
| /// Exactly one pointer to each SSL context is stored here. |
| std::vector<SSLCertContext> ctx_store; |
| |
| /// Add a context to the clean up list. |
| /// @return The index of the added context. |
| int store(SSLCertContext const &cc); |
| }; |
| |
| namespace |
| { |
| /** Copy @a src to @a dst, transforming to lower case. |
| * |
| * @param src Input string. |
| * @param dst Output buffer. |
| */ |
| inline void |
| transform_lower(std::string_view src, ts::MemSpan<char> dst) |
| { |
| if (src.size() > dst.size() - 1) { // clip @a src, reserving space for the terminal nul. |
| src = std::string_view{src.data(), dst.size() - 1}; |
| } |
| auto final = std::transform(src.begin(), src.end(), dst.data(), [](char c) -> char { return std::tolower(c); }); |
| *final++ = '\0'; |
| } |
| } // namespace |
| |
| // Zero out and free the heap space allocated for ticket keys to avoid leaking secrets. |
| // The first several bytes stores the number of keys and the rest stores the ticket keys. |
| void |
| ticket_block_free(void *ptr) |
| { |
| if (ptr) { |
| ssl_ticket_key_block *key_block_ptr = static_cast<ssl_ticket_key_block *>(ptr); |
| unsigned num_ticket_keys = key_block_ptr->num_keys; |
| memset(ptr, 0, sizeof(ssl_ticket_key_block) + num_ticket_keys * sizeof(ssl_ticket_key_t)); |
| } |
| ats_free(ptr); |
| } |
| |
| ssl_ticket_key_block * |
| ticket_block_alloc(unsigned count) |
| { |
| ssl_ticket_key_block *ptr; |
| size_t nbytes = sizeof(ssl_ticket_key_block) + count * sizeof(ssl_ticket_key_t); |
| |
| ptr = static_cast<ssl_ticket_key_block *>(ats_malloc(nbytes)); |
| memset(ptr, 0, nbytes); |
| ptr->num_keys = count; |
| |
| return ptr; |
| } |
| ssl_ticket_key_block * |
| ticket_block_create(char *ticket_key_data, int ticket_key_len) |
| { |
| ssl_ticket_key_block *keyblock = nullptr; |
| unsigned num_ticket_keys = ticket_key_len / sizeof(ssl_ticket_key_t); |
| if (num_ticket_keys == 0) { |
| Error("SSL session ticket key is too short (>= 48 bytes are required)"); |
| goto fail; |
| } |
| Debug("ssl", "Create %d ticket key blocks", num_ticket_keys); |
| |
| keyblock = ticket_block_alloc(num_ticket_keys); |
| |
| // Slurp all the keys in the ticket key file. We will encrypt with the first key, and decrypt |
| // with any key (for rotation purposes). |
| for (unsigned i = 0; i < num_ticket_keys; ++i) { |
| const char *data = (const char *)ticket_key_data + (i * sizeof(ssl_ticket_key_t)); |
| |
| memcpy(keyblock->keys[i].key_name, data, sizeof(keyblock->keys[i].key_name)); |
| memcpy(keyblock->keys[i].hmac_secret, data + sizeof(keyblock->keys[i].key_name), sizeof(keyblock->keys[i].hmac_secret)); |
| memcpy(keyblock->keys[i].aes_key, data + sizeof(keyblock->keys[i].key_name) + sizeof(keyblock->keys[i].hmac_secret), |
| sizeof(keyblock->keys[i].aes_key)); |
| } |
| |
| return keyblock; |
| |
| fail: |
| ticket_block_free(keyblock); |
| return nullptr; |
| } |
| |
| ssl_ticket_key_block * |
| ssl_create_ticket_keyblock(const char *ticket_key_path) |
| { |
| #if TS_HAVE_OPENSSL_SESSION_TICKETS |
| ats_scoped_str ticket_key_data; |
| int ticket_key_len; |
| ssl_ticket_key_block *keyblock = nullptr; |
| |
| if (ticket_key_path != nullptr) { |
| ticket_key_data = readIntoBuffer(ticket_key_path, __func__, &ticket_key_len); |
| if (!ticket_key_data) { |
| Error("failed to read SSL session ticket key from %s", (const char *)ticket_key_path); |
| goto fail; |
| } |
| keyblock = ticket_block_create(ticket_key_data, ticket_key_len); |
| } else { |
| // Generate a random ticket key |
| ssl_ticket_key_t key; |
| RAND_bytes(reinterpret_cast<unsigned char *>(&key), sizeof(key)); |
| keyblock = ticket_block_create(reinterpret_cast<char *>(&key), sizeof(key)); |
| } |
| |
| return keyblock; |
| |
| fail: |
| ticket_block_free(keyblock); |
| return nullptr; |
| |
| #else /* !TS_HAVE_OPENSSL_SESSION_TICKETS */ |
| (void)ticket_key_path; |
| return nullptr; |
| #endif /* TS_HAVE_OPENSSL_SESSION_TICKETS */ |
| } |
| |
| SSLCertContext::SSLCertContext(SSLCertContext const &other) |
| { |
| opt = other.opt; |
| userconfig = other.userconfig; |
| keyblock = other.keyblock; |
| std::lock_guard<std::mutex> lock(other.ctx_mutex); |
| ctx = other.ctx; |
| } |
| |
| SSLCertContext & |
| SSLCertContext::operator=(SSLCertContext const &other) |
| { |
| if (&other != this) { |
| this->opt = other.opt; |
| this->userconfig = other.userconfig; |
| this->keyblock = other.keyblock; |
| std::lock_guard<std::mutex> lock(other.ctx_mutex); |
| this->ctx = other.ctx; |
| } |
| return *this; |
| } |
| |
| shared_SSL_CTX |
| SSLCertContext::getCtx() |
| { |
| std::lock_guard<std::mutex> lock(ctx_mutex); |
| return ctx; |
| } |
| |
| void |
| SSLCertContext::setCtx(shared_SSL_CTX sc) |
| { |
| std::lock_guard<std::mutex> lock(ctx_mutex); |
| ctx = std::move(sc); |
| } |
| |
| SSLCertLookup::SSLCertLookup() : ssl_storage(new SSLContextStorage()), ssl_default(nullptr), is_valid(true) {} |
| |
| SSLCertLookup::~SSLCertLookup() |
| { |
| delete this->ssl_storage; |
| } |
| |
| SSLCertContext * |
| SSLCertLookup::find(const char *address) const |
| { |
| return this->ssl_storage->lookup(address); |
| } |
| |
| SSLCertContext * |
| SSLCertLookup::find(const IpEndpoint &address) const |
| { |
| SSLCertContext *cc; |
| SSLAddressLookupKey key(address); |
| |
| // First try the full address. |
| if ((cc = this->ssl_storage->lookup(key.get()))) { |
| return cc; |
| } |
| |
| // If that failed, try the address without the port. |
| if (address.port()) { |
| key.split(); |
| return this->ssl_storage->lookup(key.get()); |
| } |
| |
| return nullptr; |
| } |
| |
| int |
| SSLCertLookup::insert(const char *name, SSLCertContext const &cc) |
| { |
| return this->ssl_storage->insert(name, cc); |
| } |
| |
| int |
| SSLCertLookup::insert(const IpEndpoint &address, SSLCertContext const &cc) |
| { |
| SSLAddressLookupKey key(address); |
| return this->ssl_storage->insert(key.get(), cc); |
| } |
| |
| unsigned |
| SSLCertLookup::count() const |
| { |
| return ssl_storage->count(); |
| } |
| |
| SSLCertContext * |
| SSLCertLookup::get(unsigned i) const |
| { |
| return ssl_storage->get(i); |
| } |
| |
| SSLContextStorage::SSLContextStorage() {} |
| |
| SSLContextStorage::~SSLContextStorage() {} |
| |
| int |
| SSLContextStorage::store(SSLCertContext const &cc) |
| { |
| this->ctx_store.push_back(cc); |
| return this->ctx_store.size() - 1; |
| } |
| |
| int |
| SSLContextStorage::insert(const char *name, SSLCertContext const &cc) |
| { |
| int idx = this->store(cc); |
| idx = this->insert(name, idx); |
| if (idx < 0) { |
| this->ctx_store.pop_back(); |
| } |
| return idx; |
| } |
| |
| int |
| SSLContextStorage::insert(const char *name, int idx) |
| { |
| ats_wildcard_matcher wildcard; |
| char lower_case_name[TS_MAX_HOST_NAME_LEN + 1]; |
| transform_lower(name, lower_case_name); |
| |
| shared_SSL_CTX ctx = this->ctx_store[idx].getCtx(); |
| if (wildcard.match(lower_case_name)) { |
| // Strip the wildcard and store the subdomain |
| const char *subdomain = index(lower_case_name, '*'); |
| if (subdomain && subdomain[1] == '.') { |
| subdomain += 2; // Move beyond the '.' |
| } else { |
| subdomain = nullptr; |
| } |
| if (subdomain) { |
| if (auto it = this->wilddomains.find(subdomain); it != this->wilddomains.end()) { |
| Debug("ssl", "previously indexed '%s' with SSL_CTX #%d, cannot index it with SSL_CTX #%d now", lower_case_name, it->second, |
| idx); |
| idx = -1; |
| } else { |
| this->wilddomains.emplace(subdomain, idx); |
| Debug("ssl", "indexed '%s' with SSL_CTX %p [%d]", lower_case_name, ctx.get(), idx); |
| } |
| } |
| } else { |
| if (auto it = this->hostnames.find(lower_case_name); it != this->hostnames.end() && idx != it->second) { |
| Debug("ssl", "previously indexed '%s' with SSL_CTX %d, cannot index it with SSL_CTX #%d now", lower_case_name, it->second, |
| idx); |
| idx = -1; |
| } else { |
| this->hostnames.emplace(lower_case_name, idx); |
| Debug("ssl", "indexed '%s' with SSL_CTX %p [%d]", lower_case_name, ctx.get(), idx); |
| } |
| } |
| return idx; |
| } |
| |
| void |
| SSLContextStorage::printWildDomains() const |
| { |
| for (auto &&it : this->wilddomains) { |
| Debug("ssl", "Stored wilddomain %s", it.first.c_str()); |
| } |
| } |
| |
| SSLCertContext * |
| SSLContextStorage::lookup(const char *name) |
| { |
| // First look for an exact name match |
| if (auto it = this->hostnames.find(name); it != this->hostnames.end()) { |
| return &(this->ctx_store[it->second]); |
| } |
| // Try lower casing it |
| char lower_case_name[TS_MAX_HOST_NAME_LEN + 1]; |
| transform_lower(name, lower_case_name); |
| if (auto it_lower = this->hostnames.find(lower_case_name); it_lower != this->hostnames.end()) { |
| return &(this->ctx_store[it_lower->second]); |
| } |
| |
| // Then strip off the top domain name and look for a wildcard domain match |
| const char *subdomain = index(lower_case_name, '.'); |
| if (subdomain) { |
| ++subdomain; // Move beyond the '.' |
| if (auto it = this->wilddomains.find(subdomain); it != this->wilddomains.end()) { |
| return &(this->ctx_store[it->second]); |
| } |
| } |
| return nullptr; |
| } |
| |
| #if TS_HAS_TESTS |
| |
| static char * |
| reverse_dns_name(const char *hostname, char (&reversed)[TS_MAX_HOST_NAME_LEN + 1]) |
| { |
| char *ptr = reversed + sizeof(reversed); |
| const char *part = hostname; |
| |
| *(--ptr) = '\0'; // NUL-terminate |
| |
| while (*part) { |
| ssize_t len = strcspn(part, "."); |
| ssize_t remain = ptr - reversed; |
| |
| if (remain < (len + 1)) { |
| return nullptr; |
| } |
| |
| ptr -= len; |
| memcpy(ptr, part, len); |
| |
| // Skip to the next domain component. This will take us to either a '.' or a NUL. |
| // If it's a '.' we need to skip over it. |
| part += len; |
| if (*part == '.') { |
| ++part; |
| *(--ptr) = '.'; |
| } |
| } |
| transform_lower(ptr, {ptr, strlen(ptr) + 1}); |
| |
| return ptr; |
| } |
| |
| REGRESSION_TEST(SSLWildcardMatch)(RegressionTest *t, int /* atype ATS_UNUSED */, int *pstatus) |
| { |
| TestBox box(t, pstatus); |
| ats_wildcard_matcher wildcard; |
| |
| box = REGRESSION_TEST_PASSED; |
| |
| box.check(wildcard.match("foo.com") == false, "foo.com is not a wildcard"); |
| box.check(wildcard.match("*.foo.com") == true, "*.foo.com is a wildcard"); |
| box.check(wildcard.match("bar*.foo.com") == false, "bar*.foo.com not a wildcard"); |
| box.check(wildcard.match("*") == false, "* is not a wildcard"); |
| box.check(wildcard.match("") == false, "'' is not a wildcard"); |
| } |
| |
| REGRESSION_TEST(SSLReverseHostname)(RegressionTest *t, int /* atype ATS_UNUSED */, int *pstatus) |
| { |
| TestBox box(t, pstatus); |
| |
| char reversed[TS_MAX_HOST_NAME_LEN + 1]; |
| |
| #define _R(name) reverse_dns_name(name, reversed) |
| |
| box = REGRESSION_TEST_PASSED; |
| |
| box.check(strcmp(_R("foo.com"), "com.foo") == 0, "reversed foo.com"); |
| box.check(strcmp(_R("bar.foo.com"), "com.foo.bar") == 0, "reversed bar.foo.com"); |
| box.check(strcmp(_R("foo"), "foo") == 0, "reversed foo"); |
| box.check(strcmp(_R("foo.Com"), "Com.foo") != 0, "mixed case reversed foo.com mismatch"); |
| box.check(strcmp(_R("foo.Com"), "com.foo") == 0, "mixed case reversed foo.com match"); |
| |
| #undef _R |
| } |
| |
| #endif // TS_HAS_TESTS |